Reducers: TypeScript

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2022-06-20 19:59:51 +02:00
parent af695e3812
commit 419ab93077
24 changed files with 219 additions and 61 deletions

View file

@ -1,5 +1,4 @@
import axios, { AxiosError, Canceler } from 'axios'; import axios, { AxiosError, Canceler } from 'axios';
import { List as ImmutableList, Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
import throttle from 'lodash/throttle'; import throttle from 'lodash/throttle';
import { defineMessages, IntlShape } from 'react-intl'; import { defineMessages, IntlShape } from 'react-intl';
@ -100,7 +99,7 @@ const messages = defineMessages({
const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1); const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1);
const ensureComposeIsVisible = (getState: () => RootState, routerHistory: History) => { const ensureComposeIsVisible = (getState: () => RootState, routerHistory: History) => {
if (!getState().compose.get('mounted') && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) { if (!getState().compose.mounted && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) {
routerHistory.push('/posts/new'); routerHistory.push('/posts/new');
} }
}; };
@ -212,16 +211,16 @@ const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, d
}; };
const needsDescriptions = (state: RootState) => { const needsDescriptions = (state: RootState) => {
const media = state.compose.get('media_attachments') as ImmutableList<ImmutableMap<string, any>>; const media = state.compose.media_attachments;
const missingDescriptionModal = getSettings(state).get('missingDescriptionModal'); const missingDescriptionModal = getSettings(state).get('missingDescriptionModal');
const hasMissing = media.filter(item => !item.get('description')).size > 0; const hasMissing = media.filter(item => !item.description).size > 0;
return missingDescriptionModal && hasMissing; return missingDescriptionModal && hasMissing;
}; };
const validateSchedule = (state: RootState) => { const validateSchedule = (state: RootState) => {
const schedule = state.compose.get('schedule'); const schedule = state.compose.schedule;
if (!schedule) return true; if (!schedule) return true;
const fiveMinutesFromNow = new Date(new Date().getTime() + 300000); const fiveMinutesFromNow = new Date(new Date().getTime() + 300000);
@ -234,10 +233,10 @@ const submitCompose = (routerHistory: History, force = false) =>
if (!isLoggedIn(getState)) return; if (!isLoggedIn(getState)) return;
const state = getState(); const state = getState();
const status = state.compose.get('text') || ''; const status = state.compose.text;
const media = state.compose.get('media_attachments') as ImmutableList<ImmutableMap<string, any>>; const media = state.compose.media_attachments;
const statusId = state.compose.get('id') || null; const statusId = state.compose.id;
let to = state.compose.get('to') || ImmutableOrderedSet(); let to = state.compose.to;
if (!validateSchedule(state)) { if (!validateSchedule(state)) {
dispatch(snackbar.error(messages.scheduleError)); dispatch(snackbar.error(messages.scheduleError));
@ -259,7 +258,7 @@ const submitCompose = (routerHistory: History, force = false) =>
} }
if (to && status) { if (to && status) {
const mentions: string[] = status.match(/(?:^|\s|\.)@([a-z0-9_]+(?:@[a-z0-9\.\-]+)?)/gi); // not a perfect regex const mentions: string[] | null = status.match(/(?:^|\s|\.)@([a-z0-9_]+(?:@[a-z0-9\.\-]+)?)/gi); // not a perfect regex
if (mentions) if (mentions)
to = to.union(mentions.map(mention => mention.trim().slice(1))); to = to.union(mentions.map(mention => mention.trim().slice(1)));
@ -268,19 +267,19 @@ const submitCompose = (routerHistory: History, force = false) =>
dispatch(submitComposeRequest()); dispatch(submitComposeRequest());
dispatch(closeModal()); dispatch(closeModal());
const idempotencyKey = state.compose.get('idempotencyKey'); const idempotencyKey = state.compose.idempotencyKey;
const params = { const params = {
status, status,
in_reply_to_id: state.compose.get('in_reply_to') || null, in_reply_to_id: state.compose.in_reply_to,
quote_id: state.compose.get('quote') || null, quote_id: state.compose.quote,
media_ids: media.map(item => item.get('id')), media_ids: media.map(item => item.id),
sensitive: state.compose.get('sensitive'), sensitive: state.compose.sensitive,
spoiler_text: state.compose.get('spoiler_text') || '', spoiler_text: state.compose.spoiler_text,
visibility: state.compose.get('privacy'), visibility: state.compose.privacy,
content_type: state.compose.get('content_type'), content_type: state.compose.content_type,
poll: state.compose.get('poll') || null, poll: state.compose.poll,
scheduled_at: state.compose.get('schedule') || null, scheduled_at: state.compose.schedule,
to, to,
}; };
@ -315,7 +314,7 @@ const uploadCompose = (files: FileList, intl: IntlShape) =>
const maxImageSize = getState().instance.configuration.getIn(['media_attachments', 'image_size_limit']) as number | undefined; const maxImageSize = getState().instance.configuration.getIn(['media_attachments', 'image_size_limit']) as number | undefined;
const maxVideoSize = getState().instance.configuration.getIn(['media_attachments', 'video_size_limit']) as number | undefined; const maxVideoSize = getState().instance.configuration.getIn(['media_attachments', 'video_size_limit']) as number | undefined;
const media = getState().compose.get('media_attachments'); const media = getState().compose.media_attachments;
const progress = new Array(files.length).fill(0); const progress = new Array(files.length).fill(0);
let total = Array.from(files).reduce((a, v) => a + v.size, 0); let total = Array.from(files).reduce((a, v) => a + v.size, 0);
@ -550,7 +549,7 @@ const updateTagHistory = (tags: string[]) => ({
const insertIntoTagHistory = (recognizedTags: APIEntity[], text: string) => const insertIntoTagHistory = (recognizedTags: APIEntity[], text: string) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState(); const state = getState();
const oldHistory = state.compose.get('tagHistory') as ImmutableList<string>; const oldHistory = state.compose.tagHistory;
const me = state.me; const me = state.me;
const names = recognizedTags const names = recognizedTags
.filter(tag => text.match(new RegExp(`#${tag.name}`, 'i'))) .filter(tag => text.match(new RegExp(`#${tag.name}`, 'i')))

View file

@ -25,7 +25,7 @@ const getMeId = (state: RootState) => state.me || getAuthUserId(state);
const getMeUrl = (state: RootState) => { const getMeUrl = (state: RootState) => {
const accountId = getMeId(state); const accountId = getMeId(state);
return state.accounts.get(accountId)!.url || getAuthUserUrl(state); return state.accounts.get(accountId)?.url || getAuthUserUrl(state);
}; };
const getMeToken = (state: RootState) => { const getMeToken = (state: RootState) => {

View file

@ -47,7 +47,7 @@ const statusExists = (getState: () => RootState, statusId: string) => {
return (getState().statuses.get(statusId) || null) !== null; return (getState().statuses.get(statusId) || null) !== null;
}; };
const createStatus = (params: Record<string, any>, idempotencyKey: string, statusId: string) => { const createStatus = (params: Record<string, any>, idempotencyKey: string, statusId: string | null) => {
return (dispatch: AppDispatch, getState: () => RootState) => { return (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: STATUS_CREATE_REQUEST, params, idempotencyKey }); dispatch({ type: STATUS_CREATE_REQUEST, params, idempotencyKey });

Binary file not shown.

View file

@ -22,7 +22,7 @@ const Backups = () => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const backups = useAppSelector<ImmutableList<ImmutableMap<string, any>>>((state) => state.backups.toList().sortBy((backup: ImmutableMap<string, any>) => backup.get('inserted_at'))); const backups = useAppSelector((state) => state.backups.toList().sortBy((backup) => backup.inserted_at));
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -63,12 +63,12 @@ const Backups = () => {
> >
{backups.map((backup) => ( {backups.map((backup) => (
<div <div
className={classNames('backup', { 'backup--pending': !backup.get('processed') })} className={classNames('backup', { 'backup--pending': !backup.processed })}
key={backup.get('id')} key={backup.id}
> >
{backup.get('processed') {backup.processed
? <a href={backup.get('url')} target='_blank'>{backup.get('inserted_at')}</a> ? <a href={backup.url} target='_blank'>{backup.inserted_at}</a>
: <div>{intl.formatMessage(messages.pending)}: {backup.get('inserted_at')}</div> : <div>{intl.formatMessage(messages.pending)}: {backup.inserted_at}</div>
} }
</div> </div>
))} ))}

View file

@ -49,7 +49,7 @@ const Option = (props: IOption) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const intl = useIntl(); const intl = useIntl();
const suggestions = useAppSelector((state) => state.compose.get('suggestions')); const suggestions = useAppSelector((state) => state.compose.suggestions);
const handleOptionTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => onChange(index, event.target.value); const handleOptionTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => onChange(index, event.target.value);
@ -107,9 +107,9 @@ const PollForm = () => {
const intl = useIntl(); const intl = useIntl();
const pollLimits = useAppSelector((state) => state.instance.getIn(['configuration', 'polls']) as any); const pollLimits = useAppSelector((state) => state.instance.getIn(['configuration', 'polls']) as any);
const options = useAppSelector((state) => state.compose.getIn(['poll', 'options'])); const options = useAppSelector((state) => state.compose.poll?.options);
const expiresIn = useAppSelector((state) => state.compose.getIn(['poll', 'expires_in'])); const expiresIn = useAppSelector((state) => state.compose.poll?.expires_in);
const isMultiple = useAppSelector((state) => state.compose.getIn(['poll', 'multiple'])); const isMultiple = useAppSelector((state) => state.compose.poll?.multiple);
const maxOptions = pollLimits.get('max_options'); const maxOptions = pollLimits.get('max_options');
const maxOptionChars = pollLimits.get('max_characters_per_option'); const maxOptionChars = pollLimits.get('max_characters_per_option');

View file

@ -13,9 +13,9 @@ import type { Status as StatusEntity } from 'soapbox/types/entities';
const ReplyMentions: React.FC = () => { const ReplyMentions: React.FC = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const instance = useAppSelector((state) => state.instance); const instance = useAppSelector((state) => state.instance);
const status = useAppSelector<StatusEntity | null>(state => makeGetStatus()(state, { id: state.compose.get('in_reply_to') })); const status = useAppSelector<StatusEntity | null>(state => makeGetStatus()(state, { id: state.compose.in_reply_to! }));
const to = useAppSelector((state) => state.compose.get('to')); const to = useAppSelector((state) => state.compose.to);
const account = useAppSelector((state) => state.accounts.get(state.me)); const account = useAppSelector((state) => state.accounts.get(state.me));
const { explicitAddressing } = getFeatures(instance); const { explicitAddressing } = getFeatures(instance);
@ -24,7 +24,7 @@ const ReplyMentions: React.FC = () => {
return null; return null;
} }
const parentTo = status && statusToMentionsAccountIdsArray(status, account); const parentTo = status && statusToMentionsAccountIdsArray(status, account!);
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => { const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault(); e.preventDefault();

View file

@ -31,7 +31,7 @@ const ScheduleForm: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const intl = useIntl(); const intl = useIntl();
const scheduledAt = useAppSelector((state) => state.compose.get('schedule')); const scheduledAt = useAppSelector((state) => state.compose.schedule);
const active = !!scheduledAt; const active = !!scheduledAt;
const onSchedule = (date: Date) => { const onSchedule = (date: Date) => {

View file

@ -15,8 +15,8 @@ const SensitiveButton: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const active = useAppSelector(state => state.compose.get('sensitive') === true); const active = useAppSelector(state => state.compose.sensitive === true);
const disabled = useAppSelector(state => state.compose.get('spoiler') === true); const disabled = useAppSelector(state => state.compose.spoiler === true);
const onClick = () => { const onClick = () => {
dispatch(changeComposeSensitivity()); dispatch(changeComposeSensitivity());

View file

@ -5,8 +5,8 @@ import { useAppSelector } from 'soapbox/hooks';
/** File upload progress bar for post composer. */ /** File upload progress bar for post composer. */
const ComposeUploadProgress = () => { const ComposeUploadProgress = () => {
const active = useAppSelector((state) => state.compose.get('is_uploading')); const active = useAppSelector((state) => state.compose.is_uploading);
const progress = useAppSelector((state) => state.compose.get('progress')); const progress = useAppSelector((state) => state.compose.progress);
if (!active) { if (!active) {
return null; return null;

View file

@ -10,7 +10,7 @@ import UploadContainer from '../containers/upload_container';
import type { Attachment as AttachmentEntity } from 'soapbox/types/entities'; import type { Attachment as AttachmentEntity } from 'soapbox/types/entities';
const UploadForm = () => { const UploadForm = () => {
const mediaIds = useAppSelector((state) => state.compose.get('media_attachments').map((item: AttachmentEntity) => item.get('id'))); const mediaIds = useAppSelector((state) => state.compose.media_attachments.map((item: AttachmentEntity) => item.id));
const classes = classNames('compose-form__uploads-wrapper', { const classes = classNames('compose-form__uploads-wrapper', {
'contains-media': mediaIds.size !== 0, 'contains-media': mediaIds.size !== 0,
}); });

View file

@ -10,7 +10,7 @@ const getStatus = makeGetStatus();
/** QuotedStatus shown in post composer. */ /** QuotedStatus shown in post composer. */
const QuotedStatusContainer: React.FC = () => { const QuotedStatusContainer: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const status = useAppSelector(state => getStatus(state, { id: state.compose.get('quote') })); const status = useAppSelector(state => getStatus(state, { id: state.compose.quote! }));
const onCancel = () => { const onCancel = () => {
dispatch(cancelQuoteCompose()); dispatch(cancelQuoteCompose());

View file

@ -26,7 +26,7 @@ const Account: React.FC<IAccount> = ({ accountId, author }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const account = useAppSelector((state) => getAccount(state, accountId)); const account = useAppSelector((state) => getAccount(state, accountId));
const added = useAppSelector((state) => !!account && state.compose.get('to').includes(account.acct)); const added = useAppSelector((state) => !!account && state.compose.to?.includes(account.acct));
const onRemove = () => dispatch(removeFromMentions(accountId)); const onRemove = () => dispatch(removeFromMentions(accountId));
const onAdd = () => dispatch(addToMentions(accountId)); const onAdd = () => dispatch(addToMentions(accountId));

View file

@ -165,7 +165,7 @@ const makeMapStateToProps = () => {
status, status,
ancestorsIds, ancestorsIds,
descendantsIds, descendantsIds,
askReplyConfirmation: state.compose.get('text', '').trim().length !== 0, askReplyConfirmation: state.compose.text.trim().length !== 0,
me: state.me, me: state.me,
displayMedia: getSettings(state).get('displayMedia'), displayMedia: getSettings(state).get('displayMedia'),
allowedEmoji: soapbox.allowedEmoji, allowedEmoji: soapbox.allowedEmoji,

View file

@ -0,0 +1,71 @@
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { cancelReplyCompose } from 'soapbox/actions/compose';
import { openModal, closeModal } from 'soapbox/actions/modals';
import { Modal } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import ComposeFormContainer from '../../compose/containers/compose_form_container';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
confirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
});
interface IComposeModal {
onClose: (type?: string) => void,
}
const ComposeModal: React.FC<IComposeModal> = ({ onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const statusId = useAppSelector((state) => state.compose.id);
const composeText = useAppSelector((state) => state.compose.text);
const privacy = useAppSelector((state) => state.compose.privacy);
const inReplyTo = useAppSelector((state) => state.compose.in_reply_to);
const quote = useAppSelector((state) => state.compose.quote);
const onClickClose = () => {
if (composeText) {
dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/icons/trash.svg'),
heading: <FormattedMessage id='confirmations.delete.heading' defaultMessage='Delete post' />,
message: <FormattedMessage id='confirmations.delete.message' defaultMessage='Are you sure you want to delete this post?' />,
confirm: intl.formatMessage(messages.confirm),
onConfirm: () => {
dispatch(closeModal('COMPOSE'));
dispatch(cancelReplyCompose());
},
}));
} else {
onClose('COMPOSE');
}
};
const renderTitle = () => {
if (statusId) {
return <FormattedMessage id='navigation_bar.compose_edit' defaultMessage='Edit post' />;
} else if (privacy === 'direct') {
return <FormattedMessage id='navigation_bar.compose_direct' defaultMessage='Direct message' />;
} else if (inReplyTo) {
return <FormattedMessage id='navigation_bar.compose_reply' defaultMessage='Reply to post' />;
} else if (quote) {
return <FormattedMessage id='navigation_bar.compose_quote' defaultMessage='Quote post' />;
} else {
return <FormattedMessage id='navigation_bar.compose' defaultMessage='Compose new post' />;
}
};
return (
<Modal
title={renderTitle()}
onClose={onClickClose}
>
<ComposeFormContainer />
</Modal>
);
};
export default ComposeModal;

View file

@ -15,10 +15,10 @@ interface IReplyMentionsModal {
} }
const ReplyMentionsModal: React.FC<IReplyMentionsModal> = ({ onClose }) => { const ReplyMentionsModal: React.FC<IReplyMentionsModal> = ({ onClose }) => {
const status = useAppSelector<StatusEntity | null>(state => makeGetStatus()(state, { id: state.compose.get('in_reply_to') })); const status = useAppSelector<StatusEntity | null>(state => makeGetStatus()(state, { id: state.compose.in_reply_to! }));
const account = useAppSelector((state) => state.accounts.get(state.me)); const account = useAppSelector((state) => state.accounts.get(state.me));
const mentions = statusToMentionsAccountIdsArray(status, account); const mentions = statusToMentionsAccountIdsArray(status!, account!);
const author = (status?.account as AccountEntity).id; const author = (status?.account as AccountEntity).id;
const onClickClose = () => { const onClickClose = () => {

View file

@ -4,21 +4,22 @@ import { normalizeStatus } from 'soapbox/normalizers/status';
import { calculateStatus } from 'soapbox/reducers/statuses'; import { calculateStatus } from 'soapbox/reducers/statuses';
import { makeGetAccount } from 'soapbox/selectors'; import { makeGetAccount } from 'soapbox/selectors';
import type { PendingStatus } from 'soapbox/reducers/pending_statuses';
import type { RootState } from 'soapbox/store'; import type { RootState } from 'soapbox/store';
const getAccount = makeGetAccount(); const getAccount = makeGetAccount();
const buildMentions = (pendingStatus: ImmutableMap<string, any>) => { const buildMentions = (pendingStatus: PendingStatus) => {
if (pendingStatus.get('in_reply_to_id')) { if (pendingStatus.in_reply_to_id) {
return ImmutableList(pendingStatus.get('to') || []).map(acct => ImmutableMap({ acct })); return ImmutableList(pendingStatus.to || []).map(acct => ImmutableMap({ acct }));
} else { } else {
return ImmutableList(); return ImmutableList();
} }
}; };
const buildPoll = (pendingStatus: ImmutableMap<string, any>) => { const buildPoll = (pendingStatus: PendingStatus) => {
if (pendingStatus.hasIn(['poll', 'options'])) { if (pendingStatus.hasIn(['poll', 'options'])) {
return pendingStatus.get('poll').update('options', (options: ImmutableMap<string, any>) => { return pendingStatus.poll!.update('options', (options: ImmutableMap<string, any>) => {
return options.map((title: string) => ImmutableMap({ title })); return options.map((title: string) => ImmutableMap({ title }));
}); });
} else { } else {
@ -26,23 +27,23 @@ const buildPoll = (pendingStatus: ImmutableMap<string, any>) => {
} }
}; };
export const buildStatus = (state: RootState, pendingStatus: ImmutableMap<string, any>, idempotencyKey: string) => { export const buildStatus = (state: RootState, pendingStatus: PendingStatus, idempotencyKey: string) => {
const me = state.me as string; const me = state.me as string;
const account = getAccount(state, me); const account = getAccount(state, me);
const inReplyToId = pendingStatus.get('in_reply_to_id'); const inReplyToId = pendingStatus.in_reply_to_id;
const status = ImmutableMap({ const status = ImmutableMap({
account, account,
content: pendingStatus.get('status', '').replace(new RegExp('\n', 'g'), '<br>'), /* eslint-disable-line no-control-regex */ content: pendingStatus.status.replace(new RegExp('\n', 'g'), '<br>'), /* eslint-disable-line no-control-regex */
id: `末pending-${idempotencyKey}`, id: `末pending-${idempotencyKey}`,
in_reply_to_account_id: state.statuses.getIn([inReplyToId, 'account'], null), in_reply_to_account_id: state.statuses.getIn([inReplyToId, 'account'], null),
in_reply_to_id: inReplyToId, in_reply_to_id: inReplyToId,
media_attachments: pendingStatus.get('media_ids', ImmutableList()).map((id: string) => ImmutableMap({ id })), media_attachments: (pendingStatus.media_ids || ImmutableList()).map((id: string) => ImmutableMap({ id })),
mentions: buildMentions(pendingStatus), mentions: buildMentions(pendingStatus),
poll: buildPoll(pendingStatus), poll: buildPoll(pendingStatus),
quote: pendingStatus.get('quote_id', null), quote: pendingStatus.quote_id,
sensitive: pendingStatus.get('sensitive', false), sensitive: pendingStatus.sensitive,
visibility: pendingStatus.get('visibility', 'public'), visibility: pendingStatus.visibility,
}); });
return calculateStatus(normalizeStatus(status)); return calculateStatus(normalizeStatus(status));

View file

@ -19,7 +19,7 @@ import { normalizePoll } from 'soapbox/normalizers/poll';
import type { ReducerAccount } from 'soapbox/reducers/accounts'; import type { ReducerAccount } from 'soapbox/reducers/accounts';
import type { Account, Attachment, Card, Emoji, Mention, Poll, EmbeddedEntity } from 'soapbox/types/entities'; import type { Account, Attachment, Card, Emoji, Mention, Poll, EmbeddedEntity } from 'soapbox/types/entities';
type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct'; export type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct';
// https://docs.joinmastodon.org/entities/status/ // https://docs.joinmastodon.org/entities/status/
export const StatusRecord = ImmutableRecord({ export const StatusRecord = ImmutableRecord({

Binary file not shown.

View file

@ -0,0 +1,43 @@
import { Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
import {
BACKUPS_FETCH_SUCCESS,
BACKUPS_CREATE_SUCCESS,
} from '../actions/backups';
import type { AnyAction } from 'redux';
import type { APIEntity } from 'soapbox/types/entities';
const BackupRecord = ImmutableRecord({
id: null as number | null,
content_type: '',
url: '',
file_size: null as number | null,
processed: false,
inserted_at: '',
});
type Backup = ReturnType<typeof BackupRecord>;
type State = ImmutableMap<string, Backup>;
const initialState: State = ImmutableMap();
const importBackup = (state: State, backup: APIEntity) => {
return state.set(backup.inserted_at, BackupRecord(backup));
};
const importBackups = (state: State, backups: APIEntity[]) => {
return state.withMutations(mutable => {
backups.forEach(backup => importBackup(mutable, backup));
});
};
export default function backups(state = initialState, action: AnyAction) {
switch (action.type) {
case BACKUPS_FETCH_SUCCESS:
case BACKUPS_CREATE_SUCCESS:
return importBackups(state, action.backups);
default:
return state;
}
}

View file

@ -0,0 +1,44 @@
import { List as ImmutableList, Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
import { AnyAction } from 'redux';
import {
STATUS_CREATE_REQUEST,
STATUS_CREATE_SUCCESS,
} from 'soapbox/actions/statuses';
import type { StatusVisibility } from 'soapbox/normalizers/status';
const PendingStatusRecord = ImmutableRecord({
content_type: '',
in_reply_to_id: null as string | null,
media_ids: null as ImmutableList<string> | null,
quote_id: null as string | null,
poll: null as ImmutableMap<string, any> | null,
sensitive: false,
spoiler_text: '',
status: '',
to: null as ImmutableList<string> | null,
visibility: 'public' as StatusVisibility,
});
export type PendingStatus = ReturnType<typeof PendingStatusRecord>;
type State = ImmutableMap<string, PendingStatus>;
const initialState: State = ImmutableMap();
const importStatus = (state: State, params: ImmutableMap<string, any>, idempotencyKey: string) => {
return state.set(idempotencyKey, PendingStatusRecord(params));
};
const deleteStatus = (state: State, idempotencyKey: string) => state.delete(idempotencyKey);
export default function pending_statuses(state = initialState, action: AnyAction) {
switch (action.type) {
case STATUS_CREATE_REQUEST:
return importStatus(state, ImmutableMap(fromJS(action.params)), action.idempotencyKey);
case STATUS_CREATE_SUCCESS:
return deleteStatus(state, action.idempotencyKey);
default:
return state;
}
}