diff --git a/app/soapbox/actions/compose.ts b/app/soapbox/actions/compose.ts index c5cd29428..a8bec65ee 100644 --- a/app/soapbox/actions/compose.ts +++ b/app/soapbox/actions/compose.ts @@ -31,61 +31,60 @@ const { CancelToken, isCancel } = axios; let cancelFetchComposeSuggestionsAccounts: Canceler; -const COMPOSE_CHANGE = 'COMPOSE_CHANGE'; -const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST'; -const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS'; -const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL'; -const COMPOSE_REPLY = 'COMPOSE_REPLY'; -const COMPOSE_EVENT_REPLY = 'COMPOSE_EVENT_REPLY'; -const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL'; -const COMPOSE_QUOTE = 'COMPOSE_QUOTE'; -const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL'; -const COMPOSE_DIRECT = 'COMPOSE_DIRECT'; -const COMPOSE_MENTION = 'COMPOSE_MENTION'; -const COMPOSE_RESET = 'COMPOSE_RESET'; -const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST'; -const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS'; -const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL'; -const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS'; -const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO'; -const COMPOSE_GROUP_POST = 'COMPOSE_GROUP_POST'; -const COMPOSE_SET_GROUP_TIMELINE_VISIBLE = 'COMPOSE_SET_GROUP_TIMELINE_VISIBLE'; +const COMPOSE_CHANGE = 'COMPOSE_CHANGE' as const; +const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST' as const; +const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS' as const; +const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL' as const; +const COMPOSE_REPLY = 'COMPOSE_REPLY' as const; +const COMPOSE_EVENT_REPLY = 'COMPOSE_EVENT_REPLY' as const; +const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL' as const; +const COMPOSE_QUOTE = 'COMPOSE_QUOTE' as const; +const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL' as const; +const COMPOSE_DIRECT = 'COMPOSE_DIRECT' as const; +const COMPOSE_MENTION = 'COMPOSE_MENTION' as const; +const COMPOSE_RESET = 'COMPOSE_RESET' as const; +const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST' as const; +const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS' as const; +const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL' as const; +const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS' as const; +const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO' as const; +const COMPOSE_GROUP_POST = 'COMPOSE_GROUP_POST' as const; +const COMPOSE_SET_GROUP_TIMELINE_VISIBLE = 'COMPOSE_SET_GROUP_TIMELINE_VISIBLE' as const; -const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR'; -const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY'; -const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT'; -const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE'; +const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR' as const; +const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY' as const; +const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT' as const; +const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE' as const; -const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE'; +const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE' as const; -const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE'; -const COMPOSE_TYPE_CHANGE = 'COMPOSE_TYPE_CHANGE'; -const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE'; -const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE'; -const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE'; -const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE'; +const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE' as const; +const COMPOSE_TYPE_CHANGE = 'COMPOSE_TYPE_CHANGE' as const; +const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE' as const; +const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE' as const; +const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE' as const; -const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT'; +const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT' as const; -const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST'; -const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS'; -const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL'; +const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST' as const; +const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS' as const; +const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL' as const; -const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD'; -const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE'; -const COMPOSE_POLL_OPTION_ADD = 'COMPOSE_POLL_OPTION_ADD'; -const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE'; -const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE'; -const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE'; +const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD' as const; +const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE' as const; +const COMPOSE_POLL_OPTION_ADD = 'COMPOSE_POLL_OPTION_ADD' as const; +const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE' as const; +const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE' as const; +const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE' as const; -const COMPOSE_SCHEDULE_ADD = 'COMPOSE_SCHEDULE_ADD'; -const COMPOSE_SCHEDULE_SET = 'COMPOSE_SCHEDULE_SET'; -const COMPOSE_SCHEDULE_REMOVE = 'COMPOSE_SCHEDULE_REMOVE'; +const COMPOSE_SCHEDULE_ADD = 'COMPOSE_SCHEDULE_ADD' as const; +const COMPOSE_SCHEDULE_SET = 'COMPOSE_SCHEDULE_SET' as const; +const COMPOSE_SCHEDULE_REMOVE = 'COMPOSE_SCHEDULE_REMOVE' as const; -const COMPOSE_ADD_TO_MENTIONS = 'COMPOSE_ADD_TO_MENTIONS'; -const COMPOSE_REMOVE_FROM_MENTIONS = 'COMPOSE_REMOVE_FROM_MENTIONS'; +const COMPOSE_ADD_TO_MENTIONS = 'COMPOSE_ADD_TO_MENTIONS' as const; +const COMPOSE_REMOVE_FROM_MENTIONS = 'COMPOSE_REMOVE_FROM_MENTIONS' as const; -const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS'; +const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS' as const; const messages = defineMessages({ exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' }, @@ -101,12 +100,24 @@ const messages = defineMessages({ replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, }); +interface ComposeSetStatusAction { + type: typeof COMPOSE_SET_STATUS + id: string + status: Status + rawText: string + explicitAddressing: boolean + spoilerText?: string + contentType?: string | false + v: ReturnType + withRedraft?: boolean +} + const setComposeToStatus = (status: Status, rawText: string, spoilerText?: string, contentType?: string | false, withRedraft?: boolean) => (dispatch: AppDispatch, getState: () => RootState) => { const { instance } = getState(); const { explicitAddressing } = getFeatures(instance); - dispatch({ + const action: ComposeSetStatusAction = { type: COMPOSE_SET_STATUS, id: 'compose-modal', status, @@ -116,7 +127,9 @@ const setComposeToStatus = (status: Status, rawText: string, spoilerText?: strin contentType, v: parseVersion(instance.version), withRedraft, - }); + }; + + dispatch(action); }; const changeCompose = (composeId: string, text: string) => ({ @@ -125,20 +138,29 @@ const changeCompose = (composeId: string, text: string) => ({ text: text, }); +interface ComposeReplyAction { + type: typeof COMPOSE_REPLY + id: string + status: Status + account: Account + explicitAddressing: boolean +} + const replyCompose = (status: Status) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const instance = state.instance; const { explicitAddressing } = getFeatures(instance); - dispatch({ + const action: ComposeReplyAction = { type: COMPOSE_REPLY, id: 'compose-modal', status: status, - account: state.accounts.get(state.me), + account: state.accounts.get(state.me)!, explicitAddressing, - }); + }; + dispatch(action); dispatch(openModal('COMPOSE')); }; @@ -147,20 +169,29 @@ const cancelReplyCompose = () => ({ id: 'compose-modal', }); +interface ComposeQuoteAction { + type: typeof COMPOSE_QUOTE + id: string + status: Status + account: Account | undefined + explicitAddressing: boolean +} + const quoteCompose = (status: Status) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const instance = state.instance; const { explicitAddressing } = getFeatures(instance); - dispatch({ + const action: ComposeQuoteAction = { type: COMPOSE_QUOTE, id: 'compose-modal', status: status, account: state.accounts.get(state.me), explicitAddressing, - }); + }; + dispatch(action); dispatch(openModal('COMPOSE')); }; @@ -182,38 +213,54 @@ const resetCompose = (composeId = 'compose-modal') => ({ id: composeId, }); +interface ComposeMentionAction { + type: typeof COMPOSE_MENTION + id: string + account: Account +} + const mentionCompose = (account: Account) => (dispatch: AppDispatch) => { - dispatch({ + const action: ComposeMentionAction = { type: COMPOSE_MENTION, id: 'compose-modal', account: account, - }); + }; + dispatch(action); dispatch(openModal('COMPOSE')); }; +interface ComposeDirectAction { + type: typeof COMPOSE_DIRECT + id: string + account: Account +} + const directCompose = (account: Account) => (dispatch: AppDispatch) => { - dispatch({ + const action: ComposeDirectAction = { type: COMPOSE_DIRECT, id: 'compose-modal', - account: account, - }); + account, + }; + dispatch(action); dispatch(openModal('COMPOSE')); }; const directComposeById = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { const account = getState().accounts.get(accountId); + if (!account) return; - dispatch({ + const action: ComposeDirectAction = { type: COMPOSE_DIRECT, id: 'compose-modal', - account: account, - }); + account, + }; + dispatch(action); dispatch(openModal('COMPOSE')); }; @@ -487,14 +534,11 @@ const undoUploadCompose = (composeId: string, media_id: string) => ({ media_id: media_id, }); -const groupCompose = (composeId: string, groupId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ - type: COMPOSE_GROUP_POST, - id: composeId, - group_id: groupId, - }); - }; +const groupCompose = (composeId: string, groupId: string) => ({ + type: COMPOSE_GROUP_POST, + id: composeId, + group_id: groupId, +}); const setGroupTimelineVisible = (composeId: string, groupTimelineVisible: boolean) => ({ type: COMPOSE_SET_GROUP_TIMELINE_VISIBLE, @@ -564,6 +608,14 @@ const fetchComposeSuggestions = (composeId: string, token: string) => } }; +interface ComposeSuggestionsReadyAction { + type: typeof COMPOSE_SUGGESTIONS_READY + id: string + token: string + emojis?: Emoji[] + accounts?: APIEntity[] +} + const readyComposeSuggestionsEmojis = (composeId: string, token: string, emojis: Emoji[]) => ({ type: COMPOSE_SUGGESTIONS_READY, id: composeId, @@ -578,6 +630,15 @@ const readyComposeSuggestionsAccounts = (composeId: string, token: string, accou accounts, }); +interface ComposeSuggestionSelectAction { + type: typeof COMPOSE_SUGGESTION_SELECT + id: string + position: number + token: string | null + completion: string + path: Array +} + const selectComposeSuggestion = (composeId: string, position: number, token: string | null, suggestion: AutoSuggestion, path: Array) => (dispatch: AppDispatch, getState: () => RootState) => { let completion, startPosition; @@ -595,14 +656,16 @@ const selectComposeSuggestion = (composeId: string, position: number, token: str startPosition = position; } - dispatch({ + const action: ComposeSuggestionSelectAction = { type: COMPOSE_SUGGESTION_SELECT, id: composeId, position: startPosition, token, completion, path, - }); + }; + + dispatch(action); }; const updateSuggestionTags = (composeId: string, token: string, currentTrends: ImmutableList) => ({ @@ -712,7 +775,7 @@ const removePollOption = (composeId: string, index: number) => ({ index, }); -const changePollSettings = (composeId: string, expiresIn?: string | number, isMultiple?: boolean) => ({ +const changePollSettings = (composeId: string, expiresIn?: number, isMultiple?: boolean) => ({ type: COMPOSE_POLL_SETTINGS_CHANGE, id: composeId, expiresIn, @@ -726,30 +789,54 @@ const openComposeWithText = (composeId: string, text = '') => dispatch(changeCompose(composeId, text)); }; +interface ComposeAddToMentionsAction { + type: typeof COMPOSE_ADD_TO_MENTIONS + id: string + account: string +} + const addToMentions = (composeId: string, accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const acct = state.accounts.get(accountId)!.acct; - return dispatch({ + const action: ComposeAddToMentionsAction = { type: COMPOSE_ADD_TO_MENTIONS, id: composeId, account: acct, - }); + }; + + return dispatch(action); }; +interface ComposeRemoveFromMentionsAction { + type: typeof COMPOSE_REMOVE_FROM_MENTIONS + id: string + account: string +} + const removeFromMentions = (composeId: string, accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const acct = state.accounts.get(accountId)!.acct; - return dispatch({ + const action: ComposeRemoveFromMentionsAction = { type: COMPOSE_REMOVE_FROM_MENTIONS, id: composeId, account: acct, - }); + }; + + return dispatch(action); }; +interface ComposeEventReplyAction { + type: typeof COMPOSE_EVENT_REPLY + id: string + status: Status + account: Account + explicitAddressing: boolean +} + const eventDiscussionCompose = (composeId: string, status: Status) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); @@ -765,6 +852,52 @@ const eventDiscussionCompose = (composeId: string, status: Status) => }); }; +type ComposeAction = + ComposeSetStatusAction + | ReturnType + | ComposeReplyAction + | ReturnType + | ComposeQuoteAction + | ReturnType + | ReturnType + | ComposeMentionAction + | ComposeDirectAction + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ComposeSuggestionsReadyAction + | ComposeSuggestionSelectAction + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ComposeAddToMentionsAction + | ComposeRemoveFromMentionsAction + | ComposeEventReplyAction + export { COMPOSE_CHANGE, COMPOSE_SUBMIT_REQUEST, @@ -794,7 +927,6 @@ export { COMPOSE_SPOILER_TEXT_CHANGE, COMPOSE_VISIBILITY_CHANGE, COMPOSE_LISTABILITY_CHANGE, - COMPOSE_COMPOSING_CHANGE, COMPOSE_EMOJI_INSERT, COMPOSE_UPLOAD_CHANGE_REQUEST, COMPOSE_UPLOAD_CHANGE_SUCCESS, @@ -865,4 +997,5 @@ export { addToMentions, removeFromMentions, eventDiscussionCompose, + type ComposeAction, }; diff --git a/app/soapbox/actions/me.ts b/app/soapbox/actions/me.ts index a8b275200..75599aeeb 100644 --- a/app/soapbox/actions/me.ts +++ b/app/soapbox/actions/me.ts @@ -10,14 +10,14 @@ import type { AxiosError, RawAxiosRequestHeaders } from 'axios'; import type { AppDispatch, RootState } from 'soapbox/store'; import type { APIEntity } from 'soapbox/types/entities'; -const ME_FETCH_REQUEST = 'ME_FETCH_REQUEST'; -const ME_FETCH_SUCCESS = 'ME_FETCH_SUCCESS'; -const ME_FETCH_FAIL = 'ME_FETCH_FAIL'; -const ME_FETCH_SKIP = 'ME_FETCH_SKIP'; +const ME_FETCH_REQUEST = 'ME_FETCH_REQUEST' as const; +const ME_FETCH_SUCCESS = 'ME_FETCH_SUCCESS' as const; +const ME_FETCH_FAIL = 'ME_FETCH_FAIL' as const; +const ME_FETCH_SKIP = 'ME_FETCH_SKIP' as const; -const ME_PATCH_REQUEST = 'ME_PATCH_REQUEST'; -const ME_PATCH_SUCCESS = 'ME_PATCH_SUCCESS'; -const ME_PATCH_FAIL = 'ME_PATCH_FAIL'; +const ME_PATCH_REQUEST = 'ME_PATCH_REQUEST' as const; +const ME_PATCH_SUCCESS = 'ME_PATCH_SUCCESS' as const; +const ME_PATCH_FAIL = 'ME_PATCH_FAIL' as const; const noOp = () => new Promise(f => f(undefined)); @@ -85,13 +85,10 @@ const fetchMeRequest = () => ({ type: ME_FETCH_REQUEST, }); -const fetchMeSuccess = (me: APIEntity) => - (dispatch: AppDispatch) => { - dispatch({ - type: ME_FETCH_SUCCESS, - me, - }); - }; +const fetchMeSuccess = (me: APIEntity) => ({ + type: ME_FETCH_SUCCESS, + me, +}); const fetchMeFail = (error: APIEntity) => ({ type: ME_FETCH_FAIL, @@ -103,13 +100,20 @@ const patchMeRequest = () => ({ type: ME_PATCH_REQUEST, }); +interface MePatchSuccessAction { + type: typeof ME_PATCH_SUCCESS + me: APIEntity +} + const patchMeSuccess = (me: APIEntity) => (dispatch: AppDispatch) => { - dispatch(importFetchedAccount(me)); - dispatch({ + const action: MePatchSuccessAction = { type: ME_PATCH_SUCCESS, me, - }); + }; + + dispatch(importFetchedAccount(me)); + dispatch(action); }; const patchMeFail = (error: AxiosError) => ({ @@ -118,6 +122,14 @@ const patchMeFail = (error: AxiosError) => ({ skipAlert: true, }); +type MeAction = + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | MePatchSuccessAction + | ReturnType; + export { ME_FETCH_REQUEST, ME_FETCH_SUCCESS, @@ -134,4 +146,5 @@ export { patchMeRequest, patchMeSuccess, patchMeFail, + type MeAction, }; diff --git a/app/soapbox/actions/settings.ts b/app/soapbox/actions/settings.ts index fdc6f394c..f72ec5e96 100644 --- a/app/soapbox/actions/settings.ts +++ b/app/soapbox/actions/settings.ts @@ -10,9 +10,9 @@ import { isLoggedIn } from 'soapbox/utils/auth'; import type { AppDispatch, RootState } from 'soapbox/store'; -const SETTING_CHANGE = 'SETTING_CHANGE'; -const SETTING_SAVE = 'SETTING_SAVE'; -const SETTINGS_UPDATE = 'SETTINGS_UPDATE'; +const SETTING_CHANGE = 'SETTING_CHANGE' as const; +const SETTING_SAVE = 'SETTING_SAVE' as const; +const SETTINGS_UPDATE = 'SETTINGS_UPDATE' as const; const FE_NAME = 'soapbox_fe'; @@ -181,25 +181,33 @@ const getSettings = createSelector([ .mergeDeep(settings); }); +interface SettingChangeAction { + type: typeof SETTING_CHANGE + path: string[] + value: any +} + const changeSettingImmediate = (path: string[], value: any, opts?: SettingOpts) => (dispatch: AppDispatch) => { - dispatch({ + const action: SettingChangeAction = { type: SETTING_CHANGE, path, value, - }); + }; + dispatch(action); dispatch(saveSettingsImmediate(opts)); }; const changeSetting = (path: string[], value: any, opts?: SettingOpts) => (dispatch: AppDispatch) => { - dispatch({ + const action: SettingChangeAction = { type: SETTING_CHANGE, path, value, - }); + }; + dispatch(action); return dispatch(saveSettings(opts)); }; @@ -236,6 +244,10 @@ const getLocale = (state: RootState, fallback = 'en') => { return Object.keys(messages).includes(localeWithVariant) ? localeWithVariant : Object.keys(messages).includes(locale) ? locale : fallback; }; +type SettingsAction = + | SettingChangeAction + | { type: typeof SETTING_SAVE } + export { SETTING_CHANGE, SETTING_SAVE, @@ -248,4 +260,5 @@ export { saveSettingsImmediate, saveSettings, getLocale, + type SettingsAction, }; diff --git a/app/soapbox/actions/timelines.ts b/app/soapbox/actions/timelines.ts index d6dd70db3..2e1e1710a 100644 --- a/app/soapbox/actions/timelines.ts +++ b/app/soapbox/actions/timelines.ts @@ -13,23 +13,23 @@ import type { AxiosError } from 'axios'; import type { AppDispatch, RootState } from 'soapbox/store'; import type { APIEntity, Status } from 'soapbox/types/entities'; -const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; -const TIMELINE_DELETE = 'TIMELINE_DELETE'; -const TIMELINE_CLEAR = 'TIMELINE_CLEAR'; -const TIMELINE_UPDATE_QUEUE = 'TIMELINE_UPDATE_QUEUE'; -const TIMELINE_DEQUEUE = 'TIMELINE_DEQUEUE'; -const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; +const TIMELINE_UPDATE = 'TIMELINE_UPDATE' as const; +const TIMELINE_DELETE = 'TIMELINE_DELETE' as const; +const TIMELINE_CLEAR = 'TIMELINE_CLEAR' as const; +const TIMELINE_UPDATE_QUEUE = 'TIMELINE_UPDATE_QUEUE' as const; +const TIMELINE_DEQUEUE = 'TIMELINE_DEQUEUE' as const; +const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP' as const; -const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; -const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS'; -const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; +const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST' as const; +const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS' as const; +const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL' as const; -const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; -const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; +const TIMELINE_CONNECT = 'TIMELINE_CONNECT' as const; +const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT' as const; -const TIMELINE_REPLACE = 'TIMELINE_REPLACE'; -const TIMELINE_INSERT = 'TIMELINE_INSERT'; -const TIMELINE_CLEAR_FEED_ACCOUNT_ID = 'TIMELINE_CLEAR_FEED_ACCOUNT_ID'; +const TIMELINE_REPLACE = 'TIMELINE_REPLACE' as const; +const TIMELINE_INSERT = 'TIMELINE_INSERT' as const; +const TIMELINE_CLEAR_FEED_ACCOUNT_ID = 'TIMELINE_CLEAR_FEED_ACCOUNT_ID' as const; const MAX_QUEUED_ITEMS = 40; @@ -111,19 +111,29 @@ const dequeueTimeline = (timelineId: string, expandFunc?: (lastStatusId: string) } }; +interface TimelineDeleteAction { + type: typeof TIMELINE_DELETE + id: string + accountId: string + references: ImmutableMap + reblogOf: unknown +} + const deleteFromTimelines = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { - const accountId = getState().statuses.get(id)?.account; - const references = getState().statuses.filter(status => status.reblog === id).map(status => [status.id, status.account]); + const accountId = getState().statuses.get(id)?.account?.id!; + const references = getState().statuses.filter(status => status.reblog === id).map(status => [status.id, status.account.id] as const); const reblogOf = getState().statuses.getIn([id, 'reblog'], null); - dispatch({ + const action: TimelineDeleteAction = { type: TIMELINE_DELETE, id, accountId, references, reblogOf, - }); + }; + + dispatch(action); }; const clearTimeline = (timeline: string) => @@ -327,6 +337,9 @@ const clearFeedAccountId = () => (dispatch: AppDispatch, _getState: () => RootSt dispatch({ type: TIMELINE_CLEAR_FEED_ACCOUNT_ID }); }; +// TODO: other actions +type TimelineAction = TimelineDeleteAction; + export { TIMELINE_UPDATE, TIMELINE_DELETE, @@ -373,4 +386,5 @@ export { scrollTopTimeline, insertSuggestionsIntoTimeline, clearFeedAccountId, + type TimelineAction, }; diff --git a/app/soapbox/features/compose/components/polls/poll-form.tsx b/app/soapbox/features/compose/components/polls/poll-form.tsx index 66445111b..9dc5b6f6d 100644 --- a/app/soapbox/features/compose/components/polls/poll-form.tsx +++ b/app/soapbox/features/compose/components/polls/poll-form.tsx @@ -126,10 +126,10 @@ const PollForm: React.FC = ({ composeId }) => { const onRemoveOption = (index: number) => dispatch(removePollOption(composeId, index)); const onChangeOption = (index: number, title: string) => dispatch(changePollOption(composeId, index, title)); const handleAddOption = () => dispatch(addPollOption(composeId, '')); - const onChangeSettings = (expiresIn: string | number | undefined, isMultiple?: boolean) => + const onChangeSettings = (expiresIn: number, isMultiple?: boolean) => dispatch(changePollSettings(composeId, expiresIn, isMultiple)); const handleSelectDuration = (value: number) => onChangeSettings(value, isMultiple); - const handleToggleMultiple = () => onChangeSettings(expiresIn, !isMultiple); + const handleToggleMultiple = () => onChangeSettings(Number(expiresIn), !isMultiple); const onRemovePoll = () => dispatch(removePoll(composeId)); if (!options) { diff --git a/app/soapbox/reducers/__tests__/compose.test.ts b/app/soapbox/reducers/__tests__/compose.test.ts index 5d2d63361..307946ce5 100644 --- a/app/soapbox/reducers/__tests__/compose.test.ts +++ b/app/soapbox/reducers/__tests__/compose.test.ts @@ -48,7 +48,7 @@ describe('compose reducer', () => { withRedraft: true, }; - const result = reducer(undefined, action); + const result = reducer(undefined, action as any); expect(result.get('compose-modal')!.media_attachments.isEmpty()).toBe(true); }); @@ -59,7 +59,7 @@ describe('compose reducer', () => { status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))), }; - const result = reducer(undefined, action); + const result = reducer(undefined, action as any); expect(result.get('compose-modal')!.media_attachments.getIn([0, 'id'])).toEqual('508107650'); }); @@ -71,7 +71,7 @@ describe('compose reducer', () => { status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))), }; - const result = reducer(undefined, action); + const result = reducer(undefined, action as any); expect(result.get('compose-modal')!.id).toEqual('AHU2RrX0wdcwzCYjFQ'); }); @@ -83,7 +83,7 @@ describe('compose reducer', () => { status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))), }; - const result = reducer(undefined, action); + const result = reducer(undefined, action as any); expect(result.get('compose-modal')!.id).toEqual(null); }); }); @@ -95,7 +95,7 @@ describe('compose reducer', () => { status: ImmutableRecord({})(), account: ImmutableRecord({})(), }; - expect(reducer(undefined, action).toJS()['compose-modal']).toMatchObject({ privacy: 'public' }); + expect(reducer(undefined, action as any).toJS()['compose-modal']).toMatchObject({ privacy: 'public' }); }); it('uses \'direct\' scope when replying to a DM', () => { @@ -106,7 +106,7 @@ describe('compose reducer', () => { status: ImmutableRecord({ visibility: 'direct' })(), account: ImmutableRecord({})(), }; - expect(reducer(state as any, action).toJS()['compose-modal']).toMatchObject({ privacy: 'direct' }); + expect(reducer(state as any, action as any).toJS()['compose-modal']).toMatchObject({ privacy: 'direct' }); }); it('uses \'private\' scope when replying to a private post', () => { @@ -117,7 +117,7 @@ describe('compose reducer', () => { status: ImmutableRecord({ visibility: 'private' })(), account: ImmutableRecord({})(), }; - expect(reducer(state as any, action).toJS()['compose-modal']).toMatchObject({ privacy: 'private' }); + expect(reducer(state as any, action as any).toJS()['compose-modal']).toMatchObject({ privacy: 'private' }); }); it('uses \'unlisted\' scope when replying to an unlisted post', () => { @@ -128,7 +128,7 @@ describe('compose reducer', () => { status: ImmutableRecord({ visibility: 'unlisted' })(), account: ImmutableRecord({})(), }; - expect(reducer(state, action).toJS()['compose-modal']).toMatchObject({ privacy: 'unlisted' }); + expect(reducer(state, action as any).toJS()['compose-modal']).toMatchObject({ privacy: 'unlisted' }); }); it('uses \'private\' scope when set as preference and replying to a public post', () => { @@ -139,7 +139,7 @@ describe('compose reducer', () => { status: ImmutableRecord({ visibility: 'public' })(), account: ImmutableRecord({})(), }; - expect(reducer(state, action).toJS()['compose-modal']).toMatchObject({ privacy: 'private' }); + expect(reducer(state, action as any).toJS()['compose-modal']).toMatchObject({ privacy: 'private' }); }); it('uses \'unlisted\' scope when set as preference and replying to a public post', () => { @@ -150,7 +150,7 @@ describe('compose reducer', () => { status: ImmutableRecord({ visibility: 'public' })(), account: ImmutableRecord({})(), }; - expect(reducer(state, action).toJS()['compose-modal']).toMatchObject({ privacy: 'unlisted' }); + expect(reducer(state, action as any).toJS()['compose-modal']).toMatchObject({ privacy: 'unlisted' }); }); it('sets preferred scope on user login', () => { @@ -238,18 +238,6 @@ describe('compose reducer', () => { }); }); - it('should handle COMPOSE_COMPOSING_CHANGE', () => { - const state = initialState.set('home', ReducerCompose({ is_composing: true })); - const action = { - type: actions.COMPOSE_COMPOSING_CHANGE, - id: 'home', - value: false, - }; - expect(reducer(state, action).toJS().home).toMatchObject({ - is_composing: false, - }); - }); - it('should handle COMPOSE_SUBMIT_REQUEST', () => { const state = initialState.set('home', ReducerCompose({ is_submitting: false })); const action = { @@ -267,7 +255,7 @@ describe('compose reducer', () => { type: actions.COMPOSE_UPLOAD_CHANGE_REQUEST, id: 'home', }; - expect(reducer(state, action).toJS().home).toMatchObject({ + expect(reducer(state, action as any).toJS().home).toMatchObject({ is_changing_upload: true, }); }); @@ -278,7 +266,7 @@ describe('compose reducer', () => { type: actions.COMPOSE_SUBMIT_SUCCESS, id: 'home', }; - expect(reducer(state, action).toJS().home).toMatchObject({ + expect(reducer(state, action as any).toJS().home).toMatchObject({ privacy: 'public', }); }); @@ -289,7 +277,7 @@ describe('compose reducer', () => { type: actions.COMPOSE_SUBMIT_FAIL, id: 'home', }; - expect(reducer(state, action).toJS().home).toMatchObject({ + expect(reducer(state, action as any).toJS().home).toMatchObject({ is_submitting: false, }); }); @@ -300,7 +288,7 @@ describe('compose reducer', () => { type: actions.COMPOSE_UPLOAD_CHANGE_FAIL, composeId: 'home', }; - expect(reducer(state, action).toJS().home).toMatchObject({ + expect(reducer(state, action as any).toJS().home).toMatchObject({ is_changing_upload: false, }); }); @@ -311,7 +299,7 @@ describe('compose reducer', () => { type: actions.COMPOSE_UPLOAD_REQUEST, id: 'home', }; - expect(reducer(state, action).toJS().home).toMatchObject({ + expect(reducer(state, action as any).toJS().home).toMatchObject({ is_uploading: true, }); }); @@ -338,7 +326,7 @@ describe('compose reducer', () => { media: media, skipLoading: true, }; - expect(reducer(state, action).toJS().home).toMatchObject({ + expect(reducer(state, action as any).toJS().home).toMatchObject({ is_uploading: false, }); }); @@ -349,7 +337,7 @@ describe('compose reducer', () => { type: actions.COMPOSE_UPLOAD_FAIL, id: 'home', }; - expect(reducer(state, action).toJS().home).toMatchObject({ + expect(reducer(state, action as any).toJS().home).toMatchObject({ is_uploading: false, }); }); @@ -414,7 +402,7 @@ describe('compose reducer', () => { type: TIMELINE_DELETE, id: '9wk6pmImMrZjgrK7iC', }; - expect(reducer(state, action).toJS()['compose-modal']).toMatchObject({ + expect(reducer(state, action as any).toJS()['compose-modal']).toMatchObject({ in_reply_to: null, }); }); diff --git a/app/soapbox/reducers/compose.ts b/app/soapbox/reducers/compose.ts index e2be226fe..b5fefa4d1 100644 --- a/app/soapbox/reducers/compose.ts +++ b/app/soapbox/reducers/compose.ts @@ -2,6 +2,7 @@ import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrde import { v4 as uuid } from 'uuid'; import { isNativeEmoji } from 'soapbox/features/emoji'; +import { Account } from 'soapbox/schemas'; import { tagHistory } from 'soapbox/settings'; import { PLEROMA } from 'soapbox/utils/features'; import { hasIntegerMediaIds } from 'soapbox/utils/status'; @@ -32,7 +33,6 @@ import { COMPOSE_TYPE_CHANGE, COMPOSE_SPOILER_TEXT_CHANGE, COMPOSE_VISIBILITY_CHANGE, - COMPOSE_COMPOSING_CHANGE, COMPOSE_EMOJI_INSERT, COMPOSE_UPLOAD_CHANGE_REQUEST, COMPOSE_UPLOAD_CHANGE_SUCCESS, @@ -52,19 +52,19 @@ import { COMPOSE_SET_STATUS, COMPOSE_EVENT_REPLY, COMPOSE_SET_GROUP_TIMELINE_VISIBLE, + ComposeAction, } from '../actions/compose'; -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 { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS, MeAction } from '../actions/me'; +import { SETTING_CHANGE, FE_NAME, SettingsAction } from '../actions/settings'; +import { TIMELINE_DELETE, TimelineAction } from '../actions/timelines'; import { normalizeAttachment } from '../normalizers/attachment'; import { unescapeHTML } from '../utils/html'; -import type { AnyAction } from 'redux'; import type { Emoji } from 'soapbox/features/emoji'; import type { - Account as AccountEntity, APIEntity, Attachment as AttachmentEntity, + Status, Status as StatusEntity, Tag, } from 'soapbox/types/entities'; @@ -111,9 +111,9 @@ type State = ImmutableMap; type Compose = ReturnType; type Poll = ReturnType; -const statusToTextMentions = (status: ImmutableMap, account: AccountEntity) => { +const statusToTextMentions = (status: Status, account: Account) => { const author = status.getIn(['account', 'acct']); - const mentions = status.get('mentions')?.map((m: ImmutableMap) => m.get('acct')) || []; + const mentions = status.get('mentions')?.map((m) => m.acct) || []; return ImmutableOrderedSet([author]) .concat(mentions) @@ -122,22 +122,21 @@ const statusToTextMentions = (status: ImmutableMap, account: Accoun .join(''); }; -export const statusToMentionsArray = (status: ImmutableMap, account: AccountEntity) => { +export const statusToMentionsArray = (status: Status, account: Account) => { const author = status.getIn(['account', 'acct']) as string; - const mentions = status.get('mentions')?.map((m: ImmutableMap) => m.get('acct')) || []; + const mentions = status.get('mentions')?.map((m) => m.acct) || []; return ImmutableOrderedSet([author]) .concat(mentions) .delete(account.acct) as ImmutableOrderedSet; }; -export const statusToMentionsAccountIdsArray = (status: StatusEntity, account: AccountEntity) => { - const author = (status.account as AccountEntity).id; +export const statusToMentionsAccountIdsArray = (status: StatusEntity, account: Account) => { const mentions = status.mentions.map((m) => m.id); - return ImmutableOrderedSet([author]) + return ImmutableOrderedSet([account.id]) .concat(mentions) - .delete(account.id) as ImmutableOrderedSet; + .delete(account.id); }; const appendMedia = (compose: Compose, media: APIEntity, defaultSensitive?: boolean) => { @@ -168,9 +167,9 @@ const removeMedia = (compose: Compose, mediaId: string) => { }); }; -const insertSuggestion = (compose: Compose, position: number, token: string, completion: string, path: Array) => { +const insertSuggestion = (compose: Compose, position: number, token: string | null, completion: string, path: Array) => { return compose.withMutations(map => { - map.updateIn(path, oldText => `${(oldText as string).slice(0, position)}${completion} ${(oldText as string).slice(position + token.length)}`); + map.updateIn(path, oldText => `${(oldText as string).slice(0, position)}${completion} ${(oldText as string).slice(position + (token?.length ?? 0))}`); map.set('suggestion_token', null); map.set('suggestions', ImmutableList()); if (path.length === 1 && path[0] === 'text') { @@ -216,10 +215,10 @@ const privacyPreference = (a: string, b: string) => { const domParser = new DOMParser(); -const expandMentions = (status: ImmutableMap) => { +const expandMentions = (status: Status) => { const fragment = domParser.parseFromString(status.get('content'), 'text/html').documentElement; - status.get('mentions').forEach((mention: ImmutableMap) => { + status.get('mentions').forEach((mention) => { const node = fragment.querySelector(`a[href="${mention.get('url')}"]`); if (node) node.textContent = `@${mention.get('acct')}`; }); @@ -227,13 +226,13 @@ const expandMentions = (status: ImmutableMap) => { return fragment.innerHTML; }; -const getExplicitMentions = (me: string, status: ImmutableMap) => { - const fragment = domParser.parseFromString(status.get('content'), 'text/html').documentElement; +const getExplicitMentions = (me: string, status: Status) => { + const fragment = domParser.parseFromString(status.content, 'text/html').documentElement; const mentions = status .get('mentions') - .filter((mention: ImmutableMap) => !(fragment.querySelector(`a[href="${mention.get('url')}"]`) || mention.get('id') === me)) - .map((m: ImmutableMap) => m.get('acct')); + .filter((mention) => !(fragment.querySelector(`a[href="${mention.url}"]`) || mention.id === me)) + .map((m) => m.acct); return ImmutableOrderedSet(mentions); }; @@ -274,7 +273,7 @@ export const initialState: State = ImmutableMap({ default: ReducerCompose({ idempotencyKey: uuid(), resetFileKey: getResetFileKey() }), }); -export default function compose(state = initialState, action: AnyAction) { +export default function compose(state = initialState, action: ComposeAction | MeAction | SettingsAction | TimelineAction) { switch (action.type) { case COMPOSE_TYPE_CHANGE: return updateCompose(state, action.id, compose => compose.withMutations(map => { @@ -300,13 +299,11 @@ export default function compose(state = initialState, action: AnyAction) { return updateCompose(state, action.id, compose => compose .set('text', action.text) .set('idempotencyKey', uuid())); - case COMPOSE_COMPOSING_CHANGE: - return updateCompose(state, action.id, compose => compose.set('is_composing', action.value)); case COMPOSE_REPLY: return updateCompose(state, action.id, compose => compose.withMutations(map => { const defaultCompose = state.get('default')!; - map.set('group_id', action.status.getIn(['group', 'id']) || action.status.get('group')); + map.set('group_id', action.status.getIn(['group', 'id']) as string); map.set('in_reply_to', action.status.get('id')); map.set('to', action.explicitAddressing ? statusToMentionsArray(action.status, action.account) : ImmutableOrderedSet()); map.set('text', !action.explicitAddressing ? statusToTextMentions(action.status, action.account) : ''); @@ -324,11 +321,11 @@ export default function compose(state = initialState, action: AnyAction) { })); case COMPOSE_QUOTE: return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => { - const author = action.status.getIn(['account', 'acct']); + const author = action.status.getIn(['account', 'acct']) as string; const defaultCompose = state.get('default')!; map.set('quote', action.status.get('id')); - map.set('to', ImmutableOrderedSet([author])); + map.set('to', ImmutableOrderedSet([author])); map.set('text', ''); map.set('privacy', privacyPreference(action.status.visibility, defaultCompose.privacy)); map.set('focusDate', new Date()); @@ -342,7 +339,7 @@ export default function compose(state = initialState, action: AnyAction) { if (action.status.group?.group_visibility === 'everyone') { map.set('privacy', privacyPreference('public', defaultCompose.privacy)); } else if (action.status.group?.group_visibility === 'members_only') { - map.set('group_id', action.status.getIn(['group', 'id']) || action.status.get('group')); + map.set('group_id', action.status.getIn(['group', 'id']) as string); map.set('privacy', 'group'); } } @@ -379,14 +376,14 @@ export default function compose(state = initialState, action: AnyAction) { return updateCompose(state, action.id, compose => compose.set('progress', Math.round((action.loaded / action.total) * 100))); case COMPOSE_MENTION: return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => { - map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' ')); + map.update('text', text => [text.trim(), `@${action.account.acct} `].filter((str) => str.length !== 0).join(' ')); map.set('focusDate', new Date()); map.set('caretPosition', null); map.set('idempotencyKey', uuid()); })); case COMPOSE_DIRECT: return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => { - map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' ')); + map.update('text', text => [text.trim(), `@${action.account.acct} `].filter((str) => str.length !== 0).join(' ')); map.set('privacy', 'direct'); map.set('focusDate', new Date()); map.set('caretPosition', null); @@ -435,7 +432,7 @@ export default function compose(state = initialState, action: AnyAction) { case COMPOSE_SET_STATUS: return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => { if (!action.withRedraft) { - map.set('id', action.status.get('id')); + map.set('id', action.status.id); } map.set('text', action.rawText || unescapeHTML(expandMentions(action.status))); map.set('to', action.explicitAddressing ? getExplicitMentions(action.status.account.id, action.status) : ImmutableOrderedSet()); @@ -445,10 +442,10 @@ export default function compose(state = initialState, action: AnyAction) { map.set('caretPosition', null); map.set('idempotencyKey', uuid()); map.set('content_type', action.contentType || 'text/plain'); - map.set('quote', action.status.get('quote')); - map.set('group_id', action.status.get('group')); + map.set('quote', action.status.getIn(['quote', 'id']) as string); + map.set('group_id', action.status.getIn(['group', 'id']) as string); - if (action.v?.software === PLEROMA && action.withRedraft && hasIntegerMediaIds(action.status)) { + if (action.v?.software === PLEROMA && action.withRedraft && hasIntegerMediaIds(action.status.toJS() as any)) { map.set('media_attachments', ImmutableList()); } else { map.set('media_attachments', action.status.media_attachments); @@ -462,9 +459,9 @@ export default function compose(state = initialState, action: AnyAction) { map.set('spoiler_text', ''); } - if (action.status.get('poll')) { + if (action.status.poll && typeof action.status.poll === 'object') { map.set('poll', PollRecord({ - options: action.status.poll.options.map((x: APIEntity) => x.get('title')), + options: ImmutableList(action.status.poll.options.map(({ title }) => title)), multiple: action.status.poll.multiple, expires_in: 24 * 3600, })); @@ -487,7 +484,17 @@ export default function compose(state = initialState, action: AnyAction) { case COMPOSE_POLL_OPTION_REMOVE: return updateCompose(state, action.id, compose => compose.updateIn(['poll', 'options'], options => (options as ImmutableList).delete(action.index))); case COMPOSE_POLL_SETTINGS_CHANGE: - return updateCompose(state, action.id, compose => compose.update('poll', poll => poll!.set('expires_in', action.expiresIn).set('multiple', action.isMultiple))); + return updateCompose(state, action.id, compose => compose.update('poll', poll => { + if (!poll) return null; + return poll.withMutations((poll) => { + if (action.expiresIn) { + poll.set('expires_in', action.expiresIn); + } + if (typeof action.isMultiple === 'boolean') { + poll.set('multiple', action.isMultiple); + } + }); + })); case COMPOSE_ADD_TO_MENTIONS: return updateCompose(state, action.id, compose => compose.update('to', mentions => mentions!.add(action.account))); case COMPOSE_REMOVE_FROM_MENTIONS: