parent
7363d9c7f8
commit
387ebfc56c
@ -0,0 +1,38 @@
|
|||||||
|
import api from 'soapbox/api';
|
||||||
|
|
||||||
|
import { importFetchedAccounts } from './importer';
|
||||||
|
|
||||||
|
export const HISTORY_FETCH_REQUEST = 'HISTORY_FETCH_REQUEST';
|
||||||
|
export const HISTORY_FETCH_SUCCESS = 'HISTORY_FETCH_SUCCESS';
|
||||||
|
export const HISTORY_FETCH_FAIL = 'HISTORY_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const fetchHistory = statusId => (dispatch, getState) => {
|
||||||
|
const loading = getState().getIn(['history', statusId, 'loading']);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(fetchHistoryRequest(statusId));
|
||||||
|
|
||||||
|
api(getState).get(`/api/v1/statuses/${statusId}/history`).then(({ data }) => {
|
||||||
|
dispatch(importFetchedAccounts(data.map(x => x.account)));
|
||||||
|
dispatch(fetchHistorySuccess(statusId, data));
|
||||||
|
}).catch(error => dispatch(fetchHistoryFail(error)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchHistoryRequest = statusId => ({
|
||||||
|
type: HISTORY_FETCH_REQUEST,
|
||||||
|
statusId,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchHistorySuccess = (statusId, history) => ({
|
||||||
|
type: HISTORY_FETCH_SUCCESS,
|
||||||
|
statusId,
|
||||||
|
history,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchHistoryFail = error => ({
|
||||||
|
type: HISTORY_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
@ -0,0 +1,67 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { FormattedDate, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { fetchHistory } from 'soapbox/actions/history';
|
||||||
|
import { Modal, Spinner, Text } from 'soapbox/components/ui';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
interface ICompareHistoryModal {
|
||||||
|
onClose: (string: string) => void,
|
||||||
|
statusId: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const CompareHistoryModal: React.FC<ICompareHistoryModal> = ({ onClose, statusId }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const loading = useAppSelector(state => state.history.getIn([statusId, 'loading']));
|
||||||
|
const versions = useAppSelector<any>(state => state.history.getIn([statusId, 'items']));
|
||||||
|
|
||||||
|
const onClickClose = () => {
|
||||||
|
onClose('COMPARE_HISTORY');
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchHistory(statusId));
|
||||||
|
}, [statusId]);
|
||||||
|
|
||||||
|
let body;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
body = <Spinner />;
|
||||||
|
} else {
|
||||||
|
body = (
|
||||||
|
<div className='divide-y divide-solid divide-gray-200 dark:divide-slate-700'>
|
||||||
|
{versions?.map((version: any) => {
|
||||||
|
const content = { __html: version.contentHtml };
|
||||||
|
const spoilerContent = { __html: version.spoilerHtml };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col py-2 first:pt-0 last:pb-0'>
|
||||||
|
{version.spoiler_text?.length > 0 && (
|
||||||
|
<>
|
||||||
|
<span dangerouslySetInnerHTML={spoilerContent} />
|
||||||
|
<hr />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className='status__content' dangerouslySetInnerHTML={content} />
|
||||||
|
<Text align='right' tag='span' theme='muted' size='sm'>
|
||||||
|
<FormattedDate value={new Date(version.created_at)} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={<FormattedMessage id='compare_history_modal.header' defaultMessage='Edit history' />}
|
||||||
|
onClose={onClickClose}
|
||||||
|
>
|
||||||
|
{body}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CompareHistoryModal;
|
@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Status edit normalizer
|
||||||
|
*/
|
||||||
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
|
import {
|
||||||
|
Map as ImmutableMap,
|
||||||
|
List as ImmutableList,
|
||||||
|
Record as ImmutableRecord,
|
||||||
|
fromJS,
|
||||||
|
} from 'immutable';
|
||||||
|
|
||||||
|
import emojify from 'soapbox/features/emoji/emoji';
|
||||||
|
import { normalizeAttachment } from 'soapbox/normalizers/attachment';
|
||||||
|
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
||||||
|
import { normalizePoll } from 'soapbox/normalizers/poll';
|
||||||
|
import { stripCompatibilityFeatures } from 'soapbox/utils/html';
|
||||||
|
import { makeEmojiMap } from 'soapbox/utils/normalizers';
|
||||||
|
|
||||||
|
import type { ReducerAccount } from 'soapbox/reducers/accounts';
|
||||||
|
import type { Account, Attachment, Emoji, EmbeddedEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
export const StatusEditRecord = ImmutableRecord({
|
||||||
|
account: null as EmbeddedEntity<Account | ReducerAccount>,
|
||||||
|
content: '',
|
||||||
|
created_at: new Date(),
|
||||||
|
emojis: ImmutableList<Emoji>(),
|
||||||
|
favourited: false,
|
||||||
|
media_attachments: ImmutableList<Attachment>(),
|
||||||
|
sensitive: false,
|
||||||
|
spoiler_text: '',
|
||||||
|
|
||||||
|
// Internal fields
|
||||||
|
contentHtml: '',
|
||||||
|
spoilerHtml: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalizeAttachments = (statusEdit: ImmutableMap<string, any>) => {
|
||||||
|
return statusEdit.update('media_attachments', ImmutableList(), attachments => {
|
||||||
|
return attachments.map(normalizeAttachment);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normalize emojis
|
||||||
|
const normalizeEmojis = (entity: ImmutableMap<string, any>) => {
|
||||||
|
return entity.update('emojis', ImmutableList(), emojis => {
|
||||||
|
return emojis.map(normalizeEmoji);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normalize the poll in the status, if applicable
|
||||||
|
const normalizeStatusPoll = (statusEdit: ImmutableMap<string, any>) => {
|
||||||
|
if (statusEdit.hasIn(['poll', 'options'])) {
|
||||||
|
return statusEdit.update('poll', ImmutableMap(), normalizePoll);
|
||||||
|
} else {
|
||||||
|
return statusEdit.set('poll', null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeContent = (statusEdit: ImmutableMap<string, any>) => {
|
||||||
|
const emojiMap = makeEmojiMap(statusEdit.get('emojis'));
|
||||||
|
const contentHtml = stripCompatibilityFeatures(emojify(statusEdit.get('content'), emojiMap));
|
||||||
|
const spoilerHtml = emojify(escapeTextContentForBrowser(statusEdit.get('spoiler_text')), emojiMap);
|
||||||
|
|
||||||
|
return statusEdit
|
||||||
|
.set('contentHtml', contentHtml)
|
||||||
|
.set('spoilerHtml', spoilerHtml);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeStatusEdit = (statusEdit: Record<string, any>) => {
|
||||||
|
return StatusEditRecord(
|
||||||
|
ImmutableMap(fromJS(statusEdit)).withMutations(statusEdit => {
|
||||||
|
normalizeAttachments(statusEdit);
|
||||||
|
normalizeEmojis(statusEdit);
|
||||||
|
normalizeStatusPoll(statusEdit);
|
||||||
|
normalizeContent(statusEdit);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,35 @@
|
|||||||
|
import { List as ImmutableList, Map as ImmutableMap, Record as ImmutableRecord } from 'immutable';
|
||||||
|
import { AnyAction } from 'redux';
|
||||||
|
|
||||||
|
import { HISTORY_FETCH_REQUEST, HISTORY_FETCH_SUCCESS, HISTORY_FETCH_FAIL } from 'soapbox/actions/history';
|
||||||
|
import { normalizeStatusEdit } from 'soapbox/normalizers';
|
||||||
|
|
||||||
|
type StatusEditRecord = ReturnType<typeof normalizeStatusEdit>;
|
||||||
|
|
||||||
|
const HistoryRecord = ImmutableRecord({
|
||||||
|
loading: false,
|
||||||
|
items: ImmutableList<StatusEditRecord>(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type State = ImmutableMap<string, ReturnType<typeof HistoryRecord>>;
|
||||||
|
|
||||||
|
const initialState: State = ImmutableMap();
|
||||||
|
|
||||||
|
export default function history(state: State = initialState, action: AnyAction) {
|
||||||
|
switch(action.type) {
|
||||||
|
case HISTORY_FETCH_REQUEST:
|
||||||
|
return state.update(action.statusId, HistoryRecord(), history => history!.withMutations(map => {
|
||||||
|
map.set('loading', true);
|
||||||
|
map.set('items', ImmutableList());
|
||||||
|
}));
|
||||||
|
case HISTORY_FETCH_SUCCESS:
|
||||||
|
return state.update(action.statusId, HistoryRecord(), history => history!.withMutations(map => {
|
||||||
|
map.set('loading', false);
|
||||||
|
map.set('items', ImmutableList(action.history.map((x: any, i: number) => ({ ...x, account: x.account.id, original: i === 0 })).reverse().map(normalizeStatusEdit)));
|
||||||
|
}));
|
||||||
|
case HISTORY_FETCH_FAIL:
|
||||||
|
return state.update(action.statusId, HistoryRecord(), history => history!.set('loading', false));
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue