From 419ab93077e7e7ac1a05caef013e0ae028695c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 20 Jun 2022 19:59:51 +0200 Subject: [PATCH] Reducers: TypeScript MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/compose.ts | 43 ++-- app/soapbox/actions/me.ts | 2 +- app/soapbox/actions/statuses.ts | 2 +- app/soapbox/components/modal_root.js | 16 +- app/soapbox/features/backups/index.tsx | 12 +- .../compose/components/polls/poll-form.tsx | 8 +- .../compose/components/reply_mentions.tsx | 6 +- .../compose/components/schedule_form.tsx | 2 +- .../compose/components/sensitive-button.tsx | 4 +- .../compose/components/upload-progress.tsx | 4 +- .../compose/components/upload_form.tsx | 2 +- .../containers/quoted_status_container.tsx | 2 +- .../features/reply_mentions/account.tsx | 2 +- app/soapbox/features/status/index.tsx | 2 +- .../features/ui/components/compose_modal.js | 91 ------- .../features/ui/components/compose_modal.tsx | 71 ++++++ .../ui/components/reply_mentions_modal.tsx | 4 +- .../ui/util/pending_status_builder.ts | 25 +- app/soapbox/normalizers/status.ts | 2 +- app/soapbox/reducers/backups.js | 28 --- app/soapbox/reducers/backups.tsx | 43 ++++ .../reducers/{compose.js => compose.ts} | 229 ++++++++++-------- app/soapbox/reducers/pending_statuses.js | 25 -- app/soapbox/reducers/pending_statuses.ts | 44 ++++ 24 files changed, 349 insertions(+), 320 deletions(-) delete mode 100644 app/soapbox/features/ui/components/compose_modal.js create mode 100644 app/soapbox/features/ui/components/compose_modal.tsx delete mode 100644 app/soapbox/reducers/backups.js create mode 100644 app/soapbox/reducers/backups.tsx rename app/soapbox/reducers/{compose.js => compose.ts} (66%) delete mode 100644 app/soapbox/reducers/pending_statuses.js create mode 100644 app/soapbox/reducers/pending_statuses.ts diff --git a/app/soapbox/actions/compose.ts b/app/soapbox/actions/compose.ts index 5ebb229a9..a6b6dcd13 100644 --- a/app/soapbox/actions/compose.ts +++ b/app/soapbox/actions/compose.ts @@ -1,5 +1,4 @@ import axios, { AxiosError, Canceler } from 'axios'; -import { List as ImmutableList, Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; import throttle from 'lodash/throttle'; import { defineMessages, IntlShape } from 'react-intl'; @@ -100,7 +99,7 @@ const messages = defineMessages({ const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1); 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'); } }; @@ -212,16 +211,16 @@ const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, d }; const needsDescriptions = (state: RootState) => { - const media = state.compose.get('media_attachments') as ImmutableList>; + const media = state.compose.media_attachments; 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; }; const validateSchedule = (state: RootState) => { - const schedule = state.compose.get('schedule'); + const schedule = state.compose.schedule; if (!schedule) return true; const fiveMinutesFromNow = new Date(new Date().getTime() + 300000); @@ -234,10 +233,10 @@ const submitCompose = (routerHistory: History, force = false) => if (!isLoggedIn(getState)) return; const state = getState(); - const status = state.compose.get('text') || ''; - const media = state.compose.get('media_attachments') as ImmutableList>; - const statusId = state.compose.get('id') || null; - let to = state.compose.get('to') || ImmutableOrderedSet(); + const status = state.compose.text; + const media = state.compose.media_attachments; + const statusId = state.compose.id; + let to = state.compose.to; if (!validateSchedule(state)) { dispatch(snackbar.error(messages.scheduleError)); @@ -259,7 +258,7 @@ const submitCompose = (routerHistory: History, force = false) => } 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) to = to.union(mentions.map(mention => mention.trim().slice(1))); @@ -268,19 +267,19 @@ const submitCompose = (routerHistory: History, force = false) => dispatch(submitComposeRequest()); dispatch(closeModal()); - const idempotencyKey = state.compose.get('idempotencyKey'); + const idempotencyKey = state.compose.idempotencyKey; const params = { status, - in_reply_to_id: state.compose.get('in_reply_to') || null, - quote_id: state.compose.get('quote') || null, - media_ids: media.map(item => item.get('id')), - sensitive: state.compose.get('sensitive'), - spoiler_text: state.compose.get('spoiler_text') || '', - visibility: state.compose.get('privacy'), - content_type: state.compose.get('content_type'), - poll: state.compose.get('poll') || null, - scheduled_at: state.compose.get('schedule') || null, + in_reply_to_id: state.compose.in_reply_to, + quote_id: state.compose.quote, + media_ids: media.map(item => item.id), + sensitive: state.compose.sensitive, + spoiler_text: state.compose.spoiler_text, + visibility: state.compose.privacy, + content_type: state.compose.content_type, + poll: state.compose.poll, + scheduled_at: state.compose.schedule, 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 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); 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) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - const oldHistory = state.compose.get('tagHistory') as ImmutableList; + const oldHistory = state.compose.tagHistory; const me = state.me; const names = recognizedTags .filter(tag => text.match(new RegExp(`#${tag.name}`, 'i'))) diff --git a/app/soapbox/actions/me.ts b/app/soapbox/actions/me.ts index acd845f8b..4cb4b2350 100644 --- a/app/soapbox/actions/me.ts +++ b/app/soapbox/actions/me.ts @@ -25,7 +25,7 @@ const getMeId = (state: RootState) => state.me || getAuthUserId(state); const getMeUrl = (state: RootState) => { const accountId = getMeId(state); - return state.accounts.get(accountId)!.url || getAuthUserUrl(state); + return state.accounts.get(accountId)?.url || getAuthUserUrl(state); }; const getMeToken = (state: RootState) => { diff --git a/app/soapbox/actions/statuses.ts b/app/soapbox/actions/statuses.ts index cf9306b67..f0a6aad15 100644 --- a/app/soapbox/actions/statuses.ts +++ b/app/soapbox/actions/statuses.ts @@ -47,7 +47,7 @@ const statusExists = (getState: () => RootState, statusId: string) => { return (getState().statuses.get(statusId) || null) !== null; }; -const createStatus = (params: Record, idempotencyKey: string, statusId: string) => { +const createStatus = (params: Record, idempotencyKey: string, statusId: string | null) => { return (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: STATUS_CREATE_REQUEST, params, idempotencyKey }); diff --git a/app/soapbox/components/modal_root.js b/app/soapbox/components/modal_root.js index 5acedae29..842b66ba6 100644 --- a/app/soapbox/components/modal_root.js +++ b/app/soapbox/components/modal_root.js @@ -16,18 +16,18 @@ const messages = defineMessages({ const checkComposeContent = compose => { return [ - compose.get('text').length > 0, - compose.get('spoiler_text').length > 0, - compose.get('media_attachments').size > 0, - compose.get('in_reply_to') !== null, - compose.get('quote') !== null, - compose.get('poll') !== null, + compose.text.length > 0, + compose.spoiler_text.length > 0, + compose.media_attachments.size > 0, + compose.in_reply_to !== null, + compose.quote !== null, + compose.poll !== null, ].some(check => check === true); }; const mapStateToProps = state => ({ - hasComposeContent: checkComposeContent(state.get('compose')), - isEditing: state.compose.get('id') !== null, + hasComposeContent: checkComposeContent(state.compose), + isEditing: state.compose.id !== null, }); const mapDispatchToProps = (dispatch) => ({ diff --git a/app/soapbox/features/backups/index.tsx b/app/soapbox/features/backups/index.tsx index c78ceb298..0efc4e15e 100644 --- a/app/soapbox/features/backups/index.tsx +++ b/app/soapbox/features/backups/index.tsx @@ -22,7 +22,7 @@ const Backups = () => { const intl = useIntl(); const dispatch = useAppDispatch(); - const backups = useAppSelector>>((state) => state.backups.toList().sortBy((backup: ImmutableMap) => backup.get('inserted_at'))); + const backups = useAppSelector((state) => state.backups.toList().sortBy((backup) => backup.inserted_at)); const [isLoading, setIsLoading] = useState(true); @@ -63,12 +63,12 @@ const Backups = () => { > {backups.map((backup) => (
- {backup.get('processed') - ? {backup.get('inserted_at')} - :
{intl.formatMessage(messages.pending)}: {backup.get('inserted_at')}
+ {backup.processed + ? {backup.inserted_at} + :
{intl.formatMessage(messages.pending)}: {backup.inserted_at}
}
))} diff --git a/app/soapbox/features/compose/components/polls/poll-form.tsx b/app/soapbox/features/compose/components/polls/poll-form.tsx index cfcefc822..4f427dfcd 100644 --- a/app/soapbox/features/compose/components/polls/poll-form.tsx +++ b/app/soapbox/features/compose/components/polls/poll-form.tsx @@ -49,7 +49,7 @@ const Option = (props: IOption) => { const dispatch = useAppDispatch(); const intl = useIntl(); - const suggestions = useAppSelector((state) => state.compose.get('suggestions')); + const suggestions = useAppSelector((state) => state.compose.suggestions); const handleOptionTitleChange = (event: React.ChangeEvent) => onChange(index, event.target.value); @@ -107,9 +107,9 @@ const PollForm = () => { const intl = useIntl(); const pollLimits = useAppSelector((state) => state.instance.getIn(['configuration', 'polls']) as any); - const options = useAppSelector((state) => state.compose.getIn(['poll', 'options'])); - const expiresIn = useAppSelector((state) => state.compose.getIn(['poll', 'expires_in'])); - const isMultiple = useAppSelector((state) => state.compose.getIn(['poll', 'multiple'])); + const options = useAppSelector((state) => state.compose.poll?.options); + const expiresIn = useAppSelector((state) => state.compose.poll?.expires_in); + const isMultiple = useAppSelector((state) => state.compose.poll?.multiple); const maxOptions = pollLimits.get('max_options'); const maxOptionChars = pollLimits.get('max_characters_per_option'); diff --git a/app/soapbox/features/compose/components/reply_mentions.tsx b/app/soapbox/features/compose/components/reply_mentions.tsx index c25c2f3df..8a7be612f 100644 --- a/app/soapbox/features/compose/components/reply_mentions.tsx +++ b/app/soapbox/features/compose/components/reply_mentions.tsx @@ -13,9 +13,9 @@ import type { Status as StatusEntity } from 'soapbox/types/entities'; const ReplyMentions: React.FC = () => { const dispatch = useDispatch(); const instance = useAppSelector((state) => state.instance); - const status = useAppSelector(state => makeGetStatus()(state, { id: state.compose.get('in_reply_to') })); + const status = useAppSelector(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 { explicitAddressing } = getFeatures(instance); @@ -24,7 +24,7 @@ const ReplyMentions: React.FC = () => { return null; } - const parentTo = status && statusToMentionsAccountIdsArray(status, account); + const parentTo = status && statusToMentionsAccountIdsArray(status, account!); const handleClick = (e: React.MouseEvent) => { e.preventDefault(); diff --git a/app/soapbox/features/compose/components/schedule_form.tsx b/app/soapbox/features/compose/components/schedule_form.tsx index 6467ba51e..5352b15ee 100644 --- a/app/soapbox/features/compose/components/schedule_form.tsx +++ b/app/soapbox/features/compose/components/schedule_form.tsx @@ -31,7 +31,7 @@ const ScheduleForm: React.FC = () => { const dispatch = useAppDispatch(); const intl = useIntl(); - const scheduledAt = useAppSelector((state) => state.compose.get('schedule')); + const scheduledAt = useAppSelector((state) => state.compose.schedule); const active = !!scheduledAt; const onSchedule = (date: Date) => { diff --git a/app/soapbox/features/compose/components/sensitive-button.tsx b/app/soapbox/features/compose/components/sensitive-button.tsx index e1c7b48ba..fbf415e07 100644 --- a/app/soapbox/features/compose/components/sensitive-button.tsx +++ b/app/soapbox/features/compose/components/sensitive-button.tsx @@ -15,8 +15,8 @@ const SensitiveButton: React.FC = () => { const intl = useIntl(); const dispatch = useAppDispatch(); - const active = useAppSelector(state => state.compose.get('sensitive') === true); - const disabled = useAppSelector(state => state.compose.get('spoiler') === true); + const active = useAppSelector(state => state.compose.sensitive === true); + const disabled = useAppSelector(state => state.compose.spoiler === true); const onClick = () => { dispatch(changeComposeSensitivity()); diff --git a/app/soapbox/features/compose/components/upload-progress.tsx b/app/soapbox/features/compose/components/upload-progress.tsx index 083cbf675..6b6fd9303 100644 --- a/app/soapbox/features/compose/components/upload-progress.tsx +++ b/app/soapbox/features/compose/components/upload-progress.tsx @@ -5,8 +5,8 @@ import { useAppSelector } from 'soapbox/hooks'; /** File upload progress bar for post composer. */ const ComposeUploadProgress = () => { - const active = useAppSelector((state) => state.compose.get('is_uploading')); - const progress = useAppSelector((state) => state.compose.get('progress')); + const active = useAppSelector((state) => state.compose.is_uploading); + const progress = useAppSelector((state) => state.compose.progress); if (!active) { return null; diff --git a/app/soapbox/features/compose/components/upload_form.tsx b/app/soapbox/features/compose/components/upload_form.tsx index ae44d2561..b177e1695 100644 --- a/app/soapbox/features/compose/components/upload_form.tsx +++ b/app/soapbox/features/compose/components/upload_form.tsx @@ -10,7 +10,7 @@ import UploadContainer from '../containers/upload_container'; import type { Attachment as AttachmentEntity } from 'soapbox/types/entities'; 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', { 'contains-media': mediaIds.size !== 0, }); diff --git a/app/soapbox/features/compose/containers/quoted_status_container.tsx b/app/soapbox/features/compose/containers/quoted_status_container.tsx index 923ee7baa..0ab894578 100644 --- a/app/soapbox/features/compose/containers/quoted_status_container.tsx +++ b/app/soapbox/features/compose/containers/quoted_status_container.tsx @@ -10,7 +10,7 @@ const getStatus = makeGetStatus(); /** QuotedStatus shown in post composer. */ const QuotedStatusContainer: React.FC = () => { 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 = () => { dispatch(cancelQuoteCompose()); diff --git a/app/soapbox/features/reply_mentions/account.tsx b/app/soapbox/features/reply_mentions/account.tsx index b96d6cf5b..fbfcdec0b 100644 --- a/app/soapbox/features/reply_mentions/account.tsx +++ b/app/soapbox/features/reply_mentions/account.tsx @@ -26,7 +26,7 @@ const Account: React.FC = ({ accountId, author }) => { const dispatch = useAppDispatch(); 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 onAdd = () => dispatch(addToMentions(accountId)); diff --git a/app/soapbox/features/status/index.tsx b/app/soapbox/features/status/index.tsx index bc571aa6c..b27be7e5b 100644 --- a/app/soapbox/features/status/index.tsx +++ b/app/soapbox/features/status/index.tsx @@ -165,7 +165,7 @@ const makeMapStateToProps = () => { status, ancestorsIds, descendantsIds, - askReplyConfirmation: state.compose.get('text', '').trim().length !== 0, + askReplyConfirmation: state.compose.text.trim().length !== 0, me: state.me, displayMedia: getSettings(state).get('displayMedia'), allowedEmoji: soapbox.allowedEmoji, diff --git a/app/soapbox/features/ui/components/compose_modal.js b/app/soapbox/features/ui/components/compose_modal.js deleted file mode 100644 index 3e029070d..000000000 --- a/app/soapbox/features/ui/components/compose_modal.js +++ /dev/null @@ -1,91 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; - -import { cancelReplyCompose } from '../../../actions/compose'; -import { openModal, closeModal } from '../../../actions/modals'; -import { Modal } from '../../../components/ui'; -import ComposeFormContainer from '../../compose/containers/compose_form_container'; - -const messages = defineMessages({ - close: { id: 'lightbox.close', defaultMessage: 'Close' }, - confirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, -}); - -const mapStateToProps = state => { - const me = state.get('me'); - return { - statusId: state.getIn(['compose', 'id']), - account: state.getIn(['accounts', me]), - composeText: state.getIn(['compose', 'text']), - privacy: state.getIn(['compose', 'privacy']), - inReplyTo: state.getIn(['compose', 'in_reply_to']), - quote: state.getIn(['compose', 'quote']), - }; -}; - -class ComposeModal extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.record, - intl: PropTypes.object.isRequired, - onClose: PropTypes.func.isRequired, - composeText: PropTypes.string, - privacy: PropTypes.string, - inReplyTo: PropTypes.string, - quote: PropTypes.string, - dispatch: PropTypes.func.isRequired, - }; - - onClickClose = () => { - const { composeText, dispatch, onClose, intl } = this.props; - - if (composeText) { - dispatch(openModal('CONFIRM', { - icon: require('@tabler/icons/icons/trash.svg'), - heading: , - message: , - confirm: intl.formatMessage(messages.confirm), - onConfirm: () => { - dispatch(closeModal('COMPOSE')); - dispatch(cancelReplyCompose()); - }, - })); - } else { - onClose('COMPOSE'); - } - }; - - renderTitle = () => { - const { statusId, privacy, inReplyTo, quote } = this.props; - - if (statusId) { - return ; - } else if (privacy === 'direct') { - return ; - } else if (inReplyTo) { - return ; - } else if (quote) { - return ; - } else { - return ; - } - } - - render() { - return ( - - - - ); - } - -} - -export default injectIntl(connect(mapStateToProps)(ComposeModal)); diff --git a/app/soapbox/features/ui/components/compose_modal.tsx b/app/soapbox/features/ui/components/compose_modal.tsx new file mode 100644 index 000000000..470c04b69 --- /dev/null +++ b/app/soapbox/features/ui/components/compose_modal.tsx @@ -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 = ({ 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: , + message: , + confirm: intl.formatMessage(messages.confirm), + onConfirm: () => { + dispatch(closeModal('COMPOSE')); + dispatch(cancelReplyCompose()); + }, + })); + } else { + onClose('COMPOSE'); + } + }; + + const renderTitle = () => { + if (statusId) { + return ; + } else if (privacy === 'direct') { + return ; + } else if (inReplyTo) { + return ; + } else if (quote) { + return ; + } else { + return ; + } + }; + + return ( + + + + ); +}; + +export default ComposeModal; diff --git a/app/soapbox/features/ui/components/reply_mentions_modal.tsx b/app/soapbox/features/ui/components/reply_mentions_modal.tsx index b1a959afb..a21a92d3a 100644 --- a/app/soapbox/features/ui/components/reply_mentions_modal.tsx +++ b/app/soapbox/features/ui/components/reply_mentions_modal.tsx @@ -15,10 +15,10 @@ interface IReplyMentionsModal { } const ReplyMentionsModal: React.FC = ({ onClose }) => { - const status = useAppSelector(state => makeGetStatus()(state, { id: state.compose.get('in_reply_to') })); + const status = useAppSelector(state => makeGetStatus()(state, { id: state.compose.in_reply_to! })); 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 onClickClose = () => { diff --git a/app/soapbox/features/ui/util/pending_status_builder.ts b/app/soapbox/features/ui/util/pending_status_builder.ts index e74b0a897..1ec288bbe 100644 --- a/app/soapbox/features/ui/util/pending_status_builder.ts +++ b/app/soapbox/features/ui/util/pending_status_builder.ts @@ -4,21 +4,22 @@ import { normalizeStatus } from 'soapbox/normalizers/status'; import { calculateStatus } from 'soapbox/reducers/statuses'; import { makeGetAccount } from 'soapbox/selectors'; +import type { PendingStatus } from 'soapbox/reducers/pending_statuses'; import type { RootState } from 'soapbox/store'; const getAccount = makeGetAccount(); -const buildMentions = (pendingStatus: ImmutableMap) => { - if (pendingStatus.get('in_reply_to_id')) { - return ImmutableList(pendingStatus.get('to') || []).map(acct => ImmutableMap({ acct })); +const buildMentions = (pendingStatus: PendingStatus) => { + if (pendingStatus.in_reply_to_id) { + return ImmutableList(pendingStatus.to || []).map(acct => ImmutableMap({ acct })); } else { return ImmutableList(); } }; -const buildPoll = (pendingStatus: ImmutableMap) => { +const buildPoll = (pendingStatus: PendingStatus) => { if (pendingStatus.hasIn(['poll', 'options'])) { - return pendingStatus.get('poll').update('options', (options: ImmutableMap) => { + return pendingStatus.poll!.update('options', (options: ImmutableMap) => { return options.map((title: string) => ImmutableMap({ title })); }); } else { @@ -26,23 +27,23 @@ const buildPoll = (pendingStatus: ImmutableMap) => { } }; -export const buildStatus = (state: RootState, pendingStatus: ImmutableMap, idempotencyKey: string) => { +export const buildStatus = (state: RootState, pendingStatus: PendingStatus, idempotencyKey: string) => { const me = state.me as string; const account = getAccount(state, me); - const inReplyToId = pendingStatus.get('in_reply_to_id'); + const inReplyToId = pendingStatus.in_reply_to_id; const status = ImmutableMap({ account, - content: pendingStatus.get('status', '').replace(new RegExp('\n', 'g'), '
'), /* eslint-disable-line no-control-regex */ + content: pendingStatus.status.replace(new RegExp('\n', 'g'), '
'), /* eslint-disable-line no-control-regex */ id: `末pending-${idempotencyKey}`, in_reply_to_account_id: state.statuses.getIn([inReplyToId, 'account'], null), 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), poll: buildPoll(pendingStatus), - quote: pendingStatus.get('quote_id', null), - sensitive: pendingStatus.get('sensitive', false), - visibility: pendingStatus.get('visibility', 'public'), + quote: pendingStatus.quote_id, + sensitive: pendingStatus.sensitive, + visibility: pendingStatus.visibility, }); return calculateStatus(normalizeStatus(status)); diff --git a/app/soapbox/normalizers/status.ts b/app/soapbox/normalizers/status.ts index 9a2bb1337..5474fa395 100644 --- a/app/soapbox/normalizers/status.ts +++ b/app/soapbox/normalizers/status.ts @@ -19,7 +19,7 @@ import { normalizePoll } from 'soapbox/normalizers/poll'; import type { ReducerAccount } from 'soapbox/reducers/accounts'; 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/ export const StatusRecord = ImmutableRecord({ diff --git a/app/soapbox/reducers/backups.js b/app/soapbox/reducers/backups.js deleted file mode 100644 index 4ce644610..000000000 --- a/app/soapbox/reducers/backups.js +++ /dev/null @@ -1,28 +0,0 @@ -import { Map as ImmutableMap, fromJS } from 'immutable'; - -import { - BACKUPS_FETCH_SUCCESS, - BACKUPS_CREATE_SUCCESS, -} from '../actions/backups'; - -const initialState = ImmutableMap(); - -const importBackup = (state, backup) => { - return state.set(backup.get('inserted_at'), backup); -}; - -const importBackups = (state, backups) => { - return state.withMutations(mutable => { - backups.forEach(backup => importBackup(mutable, backup)); - }); -}; - -export default function backups(state = initialState, action) { - switch (action.type) { - case BACKUPS_FETCH_SUCCESS: - case BACKUPS_CREATE_SUCCESS: - return importBackups(state, fromJS(action.backups)); - default: - return state; - } -} diff --git a/app/soapbox/reducers/backups.tsx b/app/soapbox/reducers/backups.tsx new file mode 100644 index 000000000..6786827ce --- /dev/null +++ b/app/soapbox/reducers/backups.tsx @@ -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; +type State = ImmutableMap; + +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; + } +} diff --git a/app/soapbox/reducers/compose.js b/app/soapbox/reducers/compose.ts similarity index 66% rename from app/soapbox/reducers/compose.js rename to app/soapbox/reducers/compose.ts index 60f713657..ad10c903d 100644 --- a/app/soapbox/reducers/compose.js +++ b/app/soapbox/reducers/compose.ts @@ -1,4 +1,4 @@ -import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord, fromJS } from 'immutable'; import { v4 as uuid } from 'uuid'; import { tagHistory } from 'soapbox/settings'; @@ -55,86 +55,103 @@ import { import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from '../actions/me'; import { SETTING_CHANGE, FE_NAME } from '../actions/settings'; import { TIMELINE_DELETE } from '../actions/timelines'; +import { normalizeAttachment } from '../normalizers/attachment'; import { unescapeHTML } from '../utils/html'; -const initialState = ImmutableMap({ - id: null, - mounted: 0, - sensitive: false, - spoiler: false, - spoiler_text: '', - content_type: 'text/plain', - privacy: 'public', - text: '', - focusDate: null, - caretPosition: null, - in_reply_to: null, - quote: null, - is_composing: false, - is_submitting: false, - is_changing_upload: false, - is_uploading: false, - progress: 0, - media_attachments: ImmutableList(), - poll: null, - suggestion_token: null, - suggestions: ImmutableList(), - default_privacy: 'public', - default_sensitive: false, - default_content_type: 'text/plain', - resetFileKey: Math.floor((Math.random() * 0x10000)), - idempotencyKey: uuid(), - tagHistory: ImmutableList(), -}); +import type { AnyAction } from 'redux'; +import type { Emoji } from 'soapbox/components/autosuggest_emoji'; +import type { + Account as AccountEntity, + APIEntity, + Attachment as AttachmentEntity, + Status as StatusEntity, +} from 'soapbox/types/entities'; -const initialPoll = ImmutableMap({ +const getResetFileKey = () => Math.floor((Math.random() * 0x10000)); + +const PollRecord = ImmutableRecord({ options: ImmutableList(['', '']), expires_in: 24 * 3600, multiple: false, }); -const statusToTextMentions = (state, status, account) => { +const ReducerRecord = ImmutableRecord({ + caretPosition: null as number | null, + content_type: 'text/plain', + default_content_type: 'text/plain', + default_privacy: 'public', + default_sensitive: false, + focusDate: null as Date | null, + idempotencyKey: uuid(), + id: null as string | null, + in_reply_to: null as string | null, + is_changing_upload: false, + is_composing: false, + is_submitting: false, + is_uploading: false, + media_attachments: ImmutableList(), + mounted: 0, + poll: null as Poll | null, + privacy: 'public', + progress: 0, + quote: null as string | null, + resetFileKey: null as number | null, + schedule: null as Date | null, + sensitive: false, + spoiler: false, + spoiler_text: '', + suggestions: ImmutableList(), + suggestion_token: null as string | null, + tagHistory: ImmutableList(), + text: '', + to: null as ImmutableOrderedSet | null, +}); + +type State = ReturnType; +type Poll = ReturnType; + +const statusToTextMentions = (state: State, status: ImmutableMap, account: AccountEntity) => { const author = status.getIn(['account', 'acct']); - const mentions = status.get('mentions', []).map(m => m.get('acct')); + const mentions = status.get('mentions').map((m: ImmutableMap) => m.get('acct')); return ImmutableOrderedSet([author]) .concat(mentions) - .delete(account.get('acct')) + .delete(account.acct) .map(m => `@${m} `) .join(''); }; -export const statusToMentionsArray = (status, account) => { - const author = status.getIn(['account', 'acct']); - const mentions = status.get('mentions', []).map(m => m.get('acct')); +export const statusToMentionsArray = (status: ImmutableMap, account: AccountEntity) => { + const author = status.getIn(['account', 'acct']) as string; + const mentions = status.get('mentions').map((m: ImmutableMap) => m.get('acct')); return ImmutableOrderedSet([author]) .concat(mentions) - .delete(account.get('acct')); + .delete(account.get('acct')) as ImmutableOrderedSet; }; -export const statusToMentionsAccountIdsArray = (status, account) => { - const author = status.getIn(['account', 'id']); - const mentions = status.get('mentions', []).map(m => m.get('id')); +export const statusToMentionsAccountIdsArray = (status: StatusEntity, account: AccountEntity) => { + const author = (status.account as AccountEntity).id; + const mentions = status.mentions.map((m) => m.id); return ImmutableOrderedSet([author]) .concat(mentions) - .delete(account.get('id')); + .delete(account.id) as ImmutableOrderedSet; }; -function clearAll(state) { +function clearAll(state: State) { return state.withMutations(map => { map.set('id', null); map.set('text', ''); map.set('to', ImmutableOrderedSet()); map.set('spoiler', false); map.set('spoiler_text', ''); - map.set('content_type', state.get('default_content_type')); + map.set('content_type', state.default_content_type); map.set('is_submitting', false); map.set('is_changing_upload', false); map.set('in_reply_to', null); map.set('quote', null); - map.set('privacy', state.get('default_privacy')); + map.set('privacy', state.default_privacy); map.set('sensitive', false); map.set('media_attachments', ImmutableList()); map.set('poll', null); @@ -143,26 +160,26 @@ function clearAll(state) { }); } -function appendMedia(state, media) { - const prevSize = state.get('media_attachments').size; +function appendMedia(state: State, media: APIEntity) { + const prevSize = state.media_attachments.size; return state.withMutations(map => { - map.update('media_attachments', list => list.push(media)); + map.update('media_attachments', list => list.push(normalizeAttachment(media))); map.set('is_uploading', false); map.set('resetFileKey', Math.floor((Math.random() * 0x10000))); map.set('idempotencyKey', uuid()); - if (prevSize === 0 && (state.get('default_sensitive') || state.get('spoiler'))) { + if (prevSize === 0 && (state.default_sensitive || state.spoiler)) { map.set('sensitive', true); } }); } -function removeMedia(state, mediaId) { - const prevSize = state.get('media_attachments').size; +function removeMedia(state: State, mediaId: string) { + const prevSize = state.media_attachments.size; return state.withMutations(map => { - map.update('media_attachments', list => list.filterNot(item => item.get('id') === mediaId)); + map.update('media_attachments', list => list.filterNot(item => item.id === mediaId)); map.set('idempotencyKey', uuid()); if (prevSize === 1) { @@ -171,9 +188,9 @@ function removeMedia(state, mediaId) { }); } -const insertSuggestion = (state, position, token, completion, path) => { +const insertSuggestion = (state: State, position: number, token: string, completion: string, path: Array) => { return state.withMutations(map => { - map.updateIn(path, oldText => `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`); + map.updateIn(path, oldText => `${(oldText as string).slice(0, position)}${completion} ${(oldText as string).slice(position + token.length)}`); map.set('suggestion_token', null); map.set('suggestions', ImmutableList()); if (path.length === 1 && path[0] === 'text') { @@ -184,11 +201,11 @@ const insertSuggestion = (state, position, token, completion, path) => { }); }; -const updateSuggestionTags = (state, token) => { +const updateSuggestionTags = (state: State, token: string) => { const prefix = token.slice(1); return state.merge({ - suggestions: state.get('tagHistory') + suggestions: state.tagHistory .filter(tag => tag.toLowerCase().startsWith(prefix.toLowerCase())) .slice(0, 4) .map(tag => '#' + tag), @@ -196,8 +213,8 @@ const updateSuggestionTags = (state, token) => { }); }; -const insertEmoji = (state, position, emojiData, needsSpace) => { - const oldText = state.get('text'); +const insertEmoji = (state: State, position: number, emojiData: Emoji, needsSpace: boolean) => { + const oldText = state.text; const emoji = needsSpace ? ' ' + emojiData.native : emojiData.native; return state.merge({ @@ -208,17 +225,17 @@ const insertEmoji = (state, position, emojiData, needsSpace) => { }); }; -const privacyPreference = (a, b) => { +const privacyPreference = (a: string, b: string) => { const order = ['public', 'unlisted', 'private', 'direct']; return order[Math.max(order.indexOf(a), order.indexOf(b), 0)]; }; const domParser = new DOMParser(); -const expandMentions = status => { +const expandMentions = (status: ImmutableMap) => { const fragment = domParser.parseFromString(status.get('content'), 'text/html').documentElement; - status.get('mentions').forEach(mention => { + status.get('mentions').forEach((mention: ImmutableMap) => { const node = fragment.querySelector(`a[href="${mention.get('url')}"]`); if (node) node.textContent = `@${mention.get('acct')}`; }); @@ -226,24 +243,23 @@ const expandMentions = status => { return fragment.innerHTML; }; -const getExplicitMentions = (me, status) => { +const getExplicitMentions = (me: string, status: ImmutableMap) => { const fragment = domParser.parseFromString(status.get('content'), 'text/html').documentElement; const mentions = status .get('mentions') - .filter(mention => !(fragment.querySelector(`a[href="${mention.get('url')}"]`) || mention.get('id') === me)) - .map(m => m.get('acct')); + .filter((mention: ImmutableMap) => !(fragment.querySelector(`a[href="${mention.get('url')}"]`) || mention.get('id') === me)) + .map((m: ImmutableMap) => m.get('acct')); - return ImmutableOrderedSet(mentions); + return ImmutableOrderedSet(mentions); }; -const getAccountSettings = account => { - return account.getIn(['pleroma', 'settings_store', FE_NAME], ImmutableMap()); +const getAccountSettings = (account: ImmutableMap) => { + return account.getIn(['pleroma', 'settings_store', FE_NAME], ImmutableMap()) as ImmutableMap; }; -const importAccount = (state, account) => { - account = fromJS(account); - const settings = getAccountSettings(account); +const importAccount = (state: State, account: APIEntity) => { + const settings = getAccountSettings(ImmutableMap(fromJS(account))); const defaultPrivacy = settings.get('defaultPrivacy', 'public'); const defaultContentType = settings.get('defaultContentType', 'text/plain'); @@ -253,13 +269,12 @@ const importAccount = (state, account) => { privacy: defaultPrivacy, default_content_type: defaultContentType, content_type: defaultContentType, - tagHistory: ImmutableList(tagHistory.get(account.get('id'))), + tagHistory: ImmutableList(tagHistory.get(account.id)), }); }; -const updateAccount = (state, account) => { - account = fromJS(account); - const settings = getAccountSettings(account); +const updateAccount = (state: State, account: APIEntity) => { + const settings = getAccountSettings(ImmutableMap(fromJS(account))); const defaultPrivacy = settings.get('defaultPrivacy'); const defaultContentType = settings.get('defaultContentType'); @@ -270,7 +285,7 @@ const updateAccount = (state, account) => { }); }; -const updateSetting = (state, path, value) => { +const updateSetting = (state: State, path: string[], value: string) => { const pathString = path.join(','); switch (pathString) { case 'defaultPrivacy': @@ -282,18 +297,18 @@ const updateSetting = (state, path, value) => { } }; -export default function compose(state = initialState, action) { +export default function compose(state = ReducerRecord({ resetFileKey: getResetFileKey() }), action: AnyAction) { switch (action.type) { case COMPOSE_MOUNT: - return state.set('mounted', state.get('mounted') + 1); + return state.set('mounted', state.mounted + 1); case COMPOSE_UNMOUNT: return state - .set('mounted', Math.max(state.get('mounted') - 1, 0)) + .set('mounted', Math.max(state.mounted - 1, 0)) .set('is_composing', false); case COMPOSE_SENSITIVITY_CHANGE: return state.withMutations(map => { - if (!state.get('spoiler')) { - map.set('sensitive', !state.get('sensitive')); + if (!state.spoiler) { + map.set('sensitive', !state.sensitive); } map.set('idempotencyKey', uuid()); @@ -306,10 +321,10 @@ export default function compose(state = initialState, action) { case COMPOSE_SPOILERNESS_CHANGE: return state.withMutations(map => { map.set('spoiler_text', ''); - map.set('spoiler', !state.get('spoiler')); + map.set('spoiler', !state.spoiler); map.set('idempotencyKey', uuid()); - if (!state.get('sensitive') && state.get('media_attachments').size >= 1) { + if (!state.sensitive && state.media_attachments.size >= 1) { map.set('sensitive', true); } }); @@ -330,17 +345,17 @@ export default function compose(state = initialState, action) { case COMPOSE_REPLY: return state.withMutations(map => { map.set('in_reply_to', action.status.get('id')); - map.set('to', action.explicitAddressing ? statusToMentionsArray(action.status, action.account) : ImmutableOrderedSet()); + map.set('to', action.explicitAddressing ? statusToMentionsArray(action.status, action.account) : ImmutableOrderedSet()); map.set('text', !action.explicitAddressing ? statusToTextMentions(state, action.status, action.account) : ''); - map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy'))); + map.set('privacy', privacyPreference(action.status.visibility, state.default_privacy)); map.set('focusDate', new Date()); map.set('caretPosition', null); map.set('idempotencyKey', uuid()); - map.set('content_type', state.get('default_content_type')); + map.set('content_type', state.default_content_type); if (action.status.get('spoiler_text', '').length > 0) { map.set('spoiler', true); - map.set('spoiler_text', action.status.get('spoiler_text')); + map.set('spoiler_text', action.status.spoiler_text); } else { map.set('spoiler', false); map.set('spoiler_text', ''); @@ -349,13 +364,13 @@ export default function compose(state = initialState, action) { case COMPOSE_QUOTE: return state.withMutations(map => { map.set('quote', action.status.get('id')); - map.set('to', undefined); + map.set('to', null); map.set('text', ''); - map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy'))); + map.set('privacy', privacyPreference(action.status.visibility, state.default_privacy)); map.set('focusDate', new Date()); map.set('caretPosition', null); map.set('idempotencyKey', uuid()); - map.set('content_type', state.get('default_content_type')); + map.set('content_type', state.default_content_type); map.set('spoiler', false); map.set('spoiler_text', ''); }); @@ -398,19 +413,19 @@ export default function compose(state = initialState, action) { map.set('idempotencyKey', uuid()); }); case COMPOSE_SUGGESTIONS_CLEAR: - return state.update('suggestions', ImmutableList(), list => list.clear()).set('suggestion_token', null); + return state.update('suggestions', list => list.clear()).set('suggestion_token', null); case COMPOSE_SUGGESTIONS_READY: - return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map(item => item.id) : action.emojis)).set('suggestion_token', action.token); + return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map((item: APIEntity) => item.id) : action.emojis)).set('suggestion_token', action.token); case COMPOSE_SUGGESTION_SELECT: return insertSuggestion(state, action.position, action.token, action.completion, action.path); case COMPOSE_SUGGESTION_TAGS_UPDATE: return updateSuggestionTags(state, action.token); case COMPOSE_TAG_HISTORY_UPDATE: - return state.set('tagHistory', fromJS(action.tags)); + return state.set('tagHistory', ImmutableList(fromJS(action.tags)) as ImmutableList); case TIMELINE_DELETE: - if (action.id === state.get('in_reply_to')) { + if (action.id === state.in_reply_to) { return state.set('in_reply_to', null); - } if (action.id === state.get('quote')) { + } if (action.id === state.quote) { return state.set('quote', null); } else { return state; @@ -421,8 +436,8 @@ export default function compose(state = initialState, action) { return state .set('is_changing_upload', false) .update('media_attachments', list => list.map(item => { - if (item.get('id') === action.media.id) { - return fromJS(action.media); + if (item.id === action.media.id) { + return normalizeAttachment(action.media); } return item; @@ -433,7 +448,7 @@ export default function compose(state = initialState, action) { map.set('id', action.status.get('id')); } map.set('text', action.rawText || unescapeHTML(expandMentions(action.status))); - map.set('to', action.explicitAddressing ? getExplicitMentions(action.status.get('account', 'id'), action.status) : ImmutableOrderedSet()); + map.set('to', action.explicitAddressing ? getExplicitMentions(action.status.account.id, action.status) : ImmutableOrderedSet()); map.set('in_reply_to', action.status.get('in_reply_to_id')); map.set('privacy', action.status.get('visibility')); map.set('focusDate', new Date()); @@ -444,7 +459,7 @@ export default function compose(state = initialState, action) { if (action.v?.software === PLEROMA && hasIntegerMediaIds(action.status)) { map.set('media_attachments', ImmutableList()); } else { - map.set('media_attachments', action.status.get('media_attachments')); + map.set('media_attachments', action.status.media_attachments); } if (action.status.get('spoiler_text').length > 0) { @@ -456,15 +471,15 @@ export default function compose(state = initialState, action) { } if (action.status.get('poll')) { - map.set('poll', ImmutableMap({ - options: action.status.getIn(['poll', 'options']).map(x => x.get('title')), - multiple: action.status.getIn(['poll', 'multiple']), + map.set('poll', PollRecord({ + options: action.status.poll.options.map((x: APIEntity) => x.get('title')), + multiple: action.status.poll.multiple, expires_in: 24 * 3600, })); } }); case COMPOSE_POLL_ADD: - return state.set('poll', initialPoll); + return state.set('poll', PollRecord()); case COMPOSE_POLL_REMOVE: return state.set('poll', null); case COMPOSE_SCHEDULE_ADD: @@ -474,17 +489,17 @@ export default function compose(state = initialState, action) { case COMPOSE_SCHEDULE_REMOVE: return state.set('schedule', null); case COMPOSE_POLL_OPTION_ADD: - return state.updateIn(['poll', 'options'], options => options.push(action.title)); + return state.updateIn(['poll', 'options'], options => (options as ImmutableList).push(action.title)); case COMPOSE_POLL_OPTION_CHANGE: return state.setIn(['poll', 'options', action.index], action.title); case COMPOSE_POLL_OPTION_REMOVE: - return state.updateIn(['poll', 'options'], options => options.delete(action.index)); + return state.updateIn(['poll', 'options'], options => (options as ImmutableList).delete(action.index)); case COMPOSE_POLL_SETTINGS_CHANGE: - return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple)); + return state.update('poll', poll => poll!.set('expires_in', action.expiresIn).set('multiple', action.isMultiple)); case COMPOSE_ADD_TO_MENTIONS: - return state.update('to', mentions => mentions.add(action.account)); + return state.update('to', mentions => mentions!.add(action.account)); case COMPOSE_REMOVE_FROM_MENTIONS: - return state.update('to', mentions => mentions.delete(action.account)); + return state.update('to', mentions => mentions!.delete(action.account)); case ME_FETCH_SUCCESS: return importAccount(state, action.me); case ME_PATCH_SUCCESS: diff --git a/app/soapbox/reducers/pending_statuses.js b/app/soapbox/reducers/pending_statuses.js deleted file mode 100644 index 4e9c247a9..000000000 --- a/app/soapbox/reducers/pending_statuses.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Map as ImmutableMap, fromJS } from 'immutable'; - -import { - STATUS_CREATE_REQUEST, - STATUS_CREATE_SUCCESS, -} from 'soapbox/actions/statuses'; - -const importStatus = (state, params, idempotencyKey) => { - return state.set(idempotencyKey, params); -}; - -const deleteStatus = (state, idempotencyKey) => state.delete(idempotencyKey); - -const initialState = ImmutableMap(); - -export default function pending_statuses(state = initialState, action) { - switch (action.type) { - case STATUS_CREATE_REQUEST: - return importStatus(state, fromJS(action.params), action.idempotencyKey); - case STATUS_CREATE_SUCCESS: - return deleteStatus(state, action.idempotencyKey); - default: - return state; - } -} diff --git a/app/soapbox/reducers/pending_statuses.ts b/app/soapbox/reducers/pending_statuses.ts new file mode 100644 index 000000000..090b8694a --- /dev/null +++ b/app/soapbox/reducers/pending_statuses.ts @@ -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 | null, + quote_id: null as string | null, + poll: null as ImmutableMap | null, + sensitive: false, + spoiler_text: '', + status: '', + to: null as ImmutableList | null, + visibility: 'public' as StatusVisibility, +}); + +export type PendingStatus = ReturnType; +type State = ImmutableMap; + +const initialState: State = ImmutableMap(); + +const importStatus = (state: State, params: ImmutableMap, 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; + } +}