diff --git a/packages/pl-fe/src/actions/compose.ts b/packages/pl-fe/src/actions/compose.ts index 48faabe45..c2b03bd29 100644 --- a/packages/pl-fe/src/actions/compose.ts +++ b/packages/pl-fe/src/actions/compose.ts @@ -297,7 +297,7 @@ const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, c const state = getState(); const accountUrl = getAccount(state, state.me as string)!.url; - const draftId = getState().compose.get(composeId)!.draft_id; + const draftId = getState().compose[composeId]!.draft_id; dispatch(submitComposeSuccess(composeId, data, accountUrl, draftId)); if (data.scheduled_at === null) { @@ -315,7 +315,7 @@ const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, c }; const needsDescriptions = (state: RootState, composeId: string) => { - const media = state.compose.get(composeId)!.media_attachments; + const media = state.compose[composeId]!.media_attachments; const missingDescriptionModal = useSettingsStore.getState().settings.missingDescriptionModal; const hasMissing = media.filter(item => !item.description).size > 0; @@ -324,7 +324,7 @@ const needsDescriptions = (state: RootState, composeId: string) => { }; const validateSchedule = (state: RootState, composeId: string) => { - const schedule = state.compose.get(composeId)?.schedule; + const schedule = state.compose[composeId]?.schedule; if (!schedule) return true; const fiveMinutesFromNow = new Date(new Date().getTime() + 300000); @@ -345,7 +345,7 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) => if (!isLoggedIn(getState)) return; const state = getState(); - const compose = state.compose.get(composeId)!; + const compose = state.compose[composeId]!; const status = compose.text; const media = compose.media_attachments; @@ -467,7 +467,7 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) => if (!isLoggedIn(getState)) return; const attachmentLimit = getState().instance.configuration.statuses.max_media_attachments; - const media = getState().compose.get(composeId)?.media_attachments; + const media = getState().compose[composeId]?.media_attachments; const progress = new Array(files.length).fill(0); let total = Array.from(files).reduce((a, v) => a + v.size, 0); @@ -673,10 +673,10 @@ interface ComposeSuggestionSelectAction { position: number; token: string | null; completion: string; - path: Array; + path: ['spoiler_text'] | ['poll', 'options', number]; } -const selectComposeSuggestion = (composeId: string, position: number, token: string | null, suggestion: AutoSuggestion, path: Array) => +const selectComposeSuggestion = (composeId: string, position: number, token: string | null, suggestion: AutoSuggestion, path: ComposeSuggestionSelectAction['path']) => (dispatch: AppDispatch, getState: () => RootState) => { let completion = '', startPosition = position; @@ -719,7 +719,7 @@ const updateTagHistory = (composeId: string, tags: string[]) => ({ const insertIntoTagHistory = (composeId: string, recognizedTags: Array, text: string) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - const oldHistory = state.compose.get(composeId)!.tagHistory; + const oldHistory = state.compose[composeId]!.tagHistory; const me = state.me; const names = recognizedTags .filter(tag => text.match(new RegExp(`#${tag.name}`, 'i'))) @@ -1100,5 +1100,6 @@ export { addSuggestedLanguage, changeComposeFederated, type ComposeReplyAction, + type ComposeSuggestionSelectAction, type ComposeAction, }; diff --git a/packages/pl-fe/src/actions/draft-statuses.ts b/packages/pl-fe/src/actions/draft-statuses.ts index 953419f30..83ce94e36 100644 --- a/packages/pl-fe/src/actions/draft-statuses.ts +++ b/packages/pl-fe/src/actions/draft-statuses.ts @@ -44,10 +44,10 @@ const saveDraftStatus = (composeId: string) => const state = getState(); const accountUrl = getAccount(state, state.me as string)!.url; - const compose = state.compose.get(composeId)!; + const compose = state.compose[composeId]!; const draft = { - ...compose.toJS(), + ...compose, draft_id: compose.draft_id || crypto.randomUUID(), }; diff --git a/packages/pl-fe/src/components/modal-root.tsx b/packages/pl-fe/src/components/modal-root.tsx index 3462e1394..a9839c65c 100644 --- a/packages/pl-fe/src/components/modal-root.tsx +++ b/packages/pl-fe/src/components/modal-root.tsx @@ -10,7 +10,7 @@ import { usePrevious } from 'pl-fe/hooks/use-previous'; import { useModalsStore } from 'pl-fe/stores/modals'; import type { ModalType } from 'pl-fe/features/ui/components/modal-root'; -import type { ReducerCompose } from 'pl-fe/reducers/compose'; +import type { Compose } from 'pl-fe/reducers/compose'; const messages = defineMessages({ confirm: { id: 'confirmations.cancel.confirm', defaultMessage: 'Discard' }, @@ -18,7 +18,7 @@ const messages = defineMessages({ saveDraft: { id: 'confirmations.cancel_editing.save_draft', defaultMessage: 'Save draft' }, }); -const checkComposeContent = (compose?: ReturnType) => +const checkComposeContent = (compose?: Compose) => !!compose && [ compose.editorState && compose.editorState.length > 0, compose.spoiler_text.length > 0, @@ -59,7 +59,7 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type }) const handleOnClose = () => { dispatch((_, getState) => { - const compose = getState().compose.get('compose-modal'); + const compose = getState().compose['compose-modal']; const hasComposeContent = checkComposeContent(compose); if (hasComposeContent && type === 'COMPOSE') { diff --git a/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx b/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx index 80cf18832..9e04323fc 100644 --- a/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx +++ b/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx @@ -16,7 +16,7 @@ const ReplyGroupIndicator = (props: IReplyGroupIndicator) => { const getStatus = useCallback(makeGetStatus(), []); - const status = useAppSelector((state) => getStatus(state, { id: state.compose.get(composeId)?.in_reply_to! })); + const status = useAppSelector((state) => getStatus(state, { id: state.compose[composeId]?.in_reply_to! })); const group = status?.group; if (!group) { diff --git a/packages/pl-fe/src/features/compose/containers/quoted-status-container.tsx b/packages/pl-fe/src/features/compose/containers/quoted-status-container.tsx index 79c6dfe4a..bfe2b4bee 100644 --- a/packages/pl-fe/src/features/compose/containers/quoted-status-container.tsx +++ b/packages/pl-fe/src/features/compose/containers/quoted-status-container.tsx @@ -15,7 +15,7 @@ const QuotedStatusContainer: React.FC = ({ composeId }) const dispatch = useAppDispatch(); const getStatus = useCallback(makeGetStatus(), []); - const status = useAppSelector(state => getStatus(state, { id: state.compose.get(composeId)?.quote! })); + const status = useAppSelector(state => getStatus(state, { id: state.compose[composeId]?.quote! })); const onCancel = () => { dispatch(cancelQuoteCompose(composeId)); diff --git a/packages/pl-fe/src/features/compose/editor/index.tsx b/packages/pl-fe/src/features/compose/editor/index.tsx index 800872f9f..0fd6a4f71 100644 --- a/packages/pl-fe/src/features/compose/editor/index.tsx +++ b/packages/pl-fe/src/features/compose/editor/index.tsx @@ -113,7 +113,7 @@ const ComposeEditor = React.forwardRef(({ theme, editorState: dispatch((_, getState) => { const state = getState(); - const compose = state.compose.get(composeId); + const compose = state.compose[composeId]; if (!compose) return; diff --git a/packages/pl-fe/src/features/compose/editor/plugins/state-plugin.tsx b/packages/pl-fe/src/features/compose/editor/plugins/state-plugin.tsx index ba5a410b9..dbf60957e 100644 --- a/packages/pl-fe/src/features/compose/editor/plugins/state-plugin.tsx +++ b/packages/pl-fe/src/features/compose/editor/plugins/state-plugin.tsx @@ -29,7 +29,7 @@ const StatePlugin: React.FC = ({ composeId, isWysiwyg }) => { const getQuoteSuggestions = useCallback(debounce((text: string) => { dispatch(async (_, getState) => { const state = getState(); - const compose = state.compose.get(composeId); + const compose = state.compose[composeId]; if (!features.quotePosts || compose?.quote) return; @@ -60,7 +60,7 @@ const StatePlugin: React.FC = ({ composeId, isWysiwyg }) => { const detectLanguage = useCallback(debounce(async (text: string) => { dispatch(async (dispatch, getState) => { const state = getState(); - const compose = state.compose.get(composeId); + const compose = state.compose[composeId]; if (!features.postLanguages || features.languageDetection || compose?.language) return; diff --git a/packages/pl-fe/src/hooks/use-compose.ts b/packages/pl-fe/src/hooks/use-compose.ts index 6e3409b17..b20b738e5 100644 --- a/packages/pl-fe/src/hooks/use-compose.ts +++ b/packages/pl-fe/src/hooks/use-compose.ts @@ -1,9 +1,9 @@ import { useAppSelector } from './use-app-selector'; -import type { ReducerCompose } from 'pl-fe/reducers/compose'; +import type { Compose } from 'pl-fe/reducers/compose'; /** Get compose for given key with fallback to 'default' */ -const useCompose = (composeId: ID extends 'default' ? never : ID): ReturnType => - useAppSelector((state) => state.compose.get(composeId, state.compose.get('default')!)); +const useCompose = (composeId: ID extends 'default' ? never : ID): Compose => + useAppSelector((state) => state.compose[composeId] || state.compose.default); export { useCompose }; diff --git a/packages/pl-fe/src/reducers/compose.ts b/packages/pl-fe/src/reducers/compose.ts index 5d7c4597c..d79558826 100644 --- a/packages/pl-fe/src/reducers/compose.ts +++ b/packages/pl-fe/src/reducers/compose.ts @@ -1,4 +1,5 @@ -import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord, fromJS } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; +import { create } from 'mutative'; import { Instance, PLEROMA, type CredentialAccount, type MediaAttachment, type Tag } from 'pl-api'; import { INSTANCE_FETCH_SUCCESS, InstanceAction } from 'pl-fe/actions/instance'; @@ -59,7 +60,8 @@ import { COMPOSE_CHANGE_MEDIA_ORDER, COMPOSE_ADD_SUGGESTED_QUOTE, COMPOSE_FEDERATED_CHANGE, - ComposeAction, + type ComposeAction, + type ComposeSuggestionSelectAction, } from '../actions/compose'; import { EVENT_COMPOSE_CANCEL, EVENT_FORM_SET, type EventsAction } from '../actions/events'; import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS, MeAction } from '../actions/me'; @@ -75,57 +77,108 @@ import type { APIEntity } from 'pl-fe/types/entities'; const getResetFileKey = () => Math.floor((Math.random() * 0x10000)); -const PollRecord = ImmutableRecord({ +interface ComposePoll { + options: ImmutableList; + options_map: ImmutableList>; + expires_in: number; + multiple: boolean; + hide_totals: boolean; +} + +const newPoll = (params: Partial = {}): ComposePoll => ({ options: ImmutableList(['', '']), - options_map: ImmutableList>([ImmutableMap(), ImmutableMap()]), + options_map: ImmutableList([ImmutableMap(), ImmutableMap()]), expires_in: 24 * 3600, multiple: false, hide_totals: false, + ...params, }); -const ReducerCompose = ImmutableRecord({ - caretPosition: null as number | null, +interface Compose { + caretPosition: number | null; + content_type: string; + draft_id: string | null; + editorState: string | null; + editorStateMap: ImmutableMap; + focusDate: Date | null; + group_id: string | null; + idempotencyKey: string; + id: string | null; + in_reply_to: string | null; + is_changing_upload: boolean; + is_composing: boolean; + is_submitting: boolean; + is_uploading: boolean; + media_attachments: ImmutableList; + poll: ComposePoll | null; + privacy: string; + progress: number; + quote: string | null; + resetFileKey: number | null; + schedule: Date | null; + sensitive: boolean; + spoiler_text: string; + spoilerTextMap: ImmutableMap; + suggestions: ImmutableList; + suggestion_token: string | null; + tagHistory: ImmutableList; + text: string; + textMap: ImmutableMap; + to: ImmutableOrderedSet; + parent_reblogged_by: string | null; + dismissed_quotes: ImmutableOrderedSet; + language: Language | null; + modified_language: Language | null; + suggested_language: string | null; + federated: boolean; + approvalRequired: boolean; +} + +const newCompose = (params: Partial = {}): Compose => ({ + caretPosition: null, content_type: 'text/plain', - draft_id: null as string | null, - editorState: null as string | null, + draft_id: null, + editorState: null, editorStateMap: ImmutableMap(), - focusDate: null as Date | null, - group_id: null as string | null, + focusDate: null, + group_id: null, idempotencyKey: '', - id: null as string | null, - in_reply_to: null as string | null, + id: null, + in_reply_to: null, is_changing_upload: false, is_composing: false, is_submitting: false, is_uploading: false, media_attachments: ImmutableList(), - poll: null as Poll | null, + poll: null, privacy: 'public', progress: 0, - quote: null as string | null, - resetFileKey: null as number | null, - schedule: null as Date | null, + quote: null, + resetFileKey: null, + schedule: null, sensitive: false, spoiler_text: '', spoilerTextMap: ImmutableMap(), suggestions: ImmutableList(), - suggestion_token: null as string | null, + suggestion_token: null, tagHistory: ImmutableList(), text: '', textMap: ImmutableMap(), to: ImmutableOrderedSet(), - parent_reblogged_by: null as string | null, + parent_reblogged_by: null, dismissed_quotes: ImmutableOrderedSet(), - language: null as Language | null, - modified_language: null as Language | null, - suggested_language: null as string | null, + language: null, + modified_language: null, + suggested_language: null, federated: true, approvalRequired: false, + ...params, }); -type State = ImmutableMap; -type Compose = ReturnType; -type Poll = ReturnType; +type State = { + default: Compose; + [key: string]: Compose; +}; const statusToTextMentions = (status: Pick, account: Pick) => { const author = status.account.acct; @@ -160,53 +213,47 @@ const statusToMentionsAccountIdsArray = (status: Pick { const prevSize = compose.media_attachments.size; - return compose.withMutations(map => { - map.update('media_attachments', list => list.push(media)); - map.set('is_uploading', false); - map.set('resetFileKey', Math.floor((Math.random() * 0x10000))); - map.set('idempotencyKey', crypto.randomUUID()); + compose.media_attachments.push(media); + compose.is_uploading = false; + compose.resetFileKey = Math.floor((Math.random() * 0x10000)); + compose.idempotencyKey = crypto.randomUUID(); - if (prevSize === 0 && (defaultSensitive || compose.sensitive)) { - map.set('sensitive', true); - } - }); + if (prevSize === 0 && (defaultSensitive || compose.sensitive)) { + compose.sensitive = true; + } }; const removeMedia = (compose: Compose, mediaId: string) => { const prevSize = compose.media_attachments.size; - return compose.withMutations(map => { - map.update('media_attachments', list => list.filterNot(item => item.id === mediaId)); - map.set('idempotencyKey', crypto.randomUUID()); + compose.media_attachments = compose.media_attachments.filter(item => item.id !== mediaId); + compose.idempotencyKey = crypto.randomUUID(); - if (prevSize === 1) { - map.set('sensitive', false); - } - }); + if (prevSize === 1) { + compose.sensitive = false; + } }; -const insertSuggestion = (compose: Compose, position: number, token: string | null, completion: string, path: Array) => - compose.withMutations(map => { - 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') { - map.set('focusDate', new Date()); - map.set('caretPosition', position + completion.length + 1); - } - map.set('idempotencyKey', crypto.randomUUID()); - }); +const insertSuggestion = (compose: Compose, position: number, token: string | null, completion: string, path: ComposeSuggestionSelectAction['path']) => { + const updateText = (oldText?: string) => `${oldText?.slice(0, position)}${completion} ${oldText?.slice(position + (token?.length ?? 0))}`; + if (path[0] === 'spoiler_text') { + compose.spoiler_text = updateText(compose.spoiler_text); + } else if (compose.poll) { + compose.poll.options = compose.poll.options.update(path[2], updateText); + } + compose.suggestion_token = null; + compose.suggestions = ImmutableList(); + compose.idempotencyKey = crypto.randomUUID(); +}; const updateSuggestionTags = (compose: Compose, token: string, tags: Tag[]) => { const prefix = token.slice(1); - return compose.merge({ - suggestions: ImmutableList(tags - .filter((tag) => tag.name.toLowerCase().startsWith(prefix.toLowerCase())) - .slice(0, 4) - .map((tag) => '#' + tag.name)), - suggestion_token: token, - }); + compose.suggestions = ImmutableList(tags + .filter((tag) => tag.name.toLowerCase().startsWith(prefix.toLowerCase())) + .slice(0, 4) + .map((tag) => '#' + tag.name)); + compose.suggestion_token = token; }; const insertEmoji = (compose: Compose, position: number, emojiData: Emoji, needsSpace: boolean) => { @@ -214,12 +261,10 @@ const insertEmoji = (compose: Compose, position: number, emojiData: Emoji, needs const emojiText = isNativeEmoji(emojiData) ? emojiData.native : emojiData.colons; const emoji = needsSpace ? ' ' + emojiText : emojiText; - return compose.merge({ - text: `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`, - focusDate: new Date(), - caretPosition: position + emoji.length + 1, - idempotencyKey: crypto.randomUUID(), - }); + compose.text = `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`; + compose.focusDate = new Date(); + compose.caretPosition = position + emoji.length + 1; + compose.idempotencyKey = crypto.randomUUID(); }; const privacyPreference = (a: string, b: string) => { @@ -257,13 +302,11 @@ const getExplicitMentions = (me: string, status: Pick { const settings = account.settings_store?.[FE_NAME]; - if (!settings) return compose; + if (!settings) return; - return compose.withMutations(compose => { - if (settings.defaultPrivacy) compose.set('privacy', settings.defaultPrivacy); - if (settings.defaultContentType) compose.set('content_type', settings.defaultContentType); - compose.set('tagHistory', ImmutableList(tagHistory.get(account.id))); - }); + if (settings.defaultPrivacy) compose.privacy = settings.defaultPrivacy; + if (settings.defaultContentType) compose.content_type = settings.defaultContentType; + compose.tagHistory = ImmutableList(tagHistory.get(account.id)); }; // const updateSetting = (compose: Compose, path: string[], value: string) => { @@ -281,323 +324,385 @@ const importAccount = (compose: Compose, account: CredentialAccount) => { const updateDefaultContentType = (compose: Compose, instance: Instance) => { const postFormats = instance.pleroma.metadata.post_formats; - return compose.update('content_type', type => postFormats.includes(type) ? type : postFormats.includes('text/markdown') ? 'text/markdown' : postFormats[0]); + compose.content_type = postFormats.includes(compose.content_type) ? compose.content_type : postFormats.includes('text/markdown') ? 'text/markdown' : postFormats[0]; }; -const updateCompose = (state: State, key: string, updater: (compose: Compose) => Compose) => - state.update(key, state.get('default')!, updater); +const updateCompose = (state: State, key: string, updater: (compose: Compose) => void) => + create(state, draft => { + draft[key] = draft[key] || create(draft.default, () => {}); + updater(draft[key]); + }); + // state.update(key, state.get('default')!, updater); -const initialState: State = ImmutableMap({ - default: ReducerCompose({ idempotencyKey: crypto.randomUUID(), resetFileKey: getResetFileKey() }), -}); +const initialState: State = { + default: newCompose({ idempotencyKey: crypto.randomUUID(), resetFileKey: getResetFileKey() }), +}; -const compose = (state = initialState, action: ComposeAction | EventsAction | InstanceAction | MeAction | TimelineAction) => { +const compose = (state = initialState, action: ComposeAction | EventsAction | InstanceAction | MeAction | TimelineAction): State => { switch (action.type) { case COMPOSE_TYPE_CHANGE: - return updateCompose(state, action.composeId, compose => compose.withMutations(map => { - map.set('content_type', action.value); - map.set('idempotencyKey', crypto.randomUUID()); - })); + return updateCompose(state, action.composeId, compose => { + compose.content_type = action.value; + compose.idempotencyKey = crypto.randomUUID(); + }); case COMPOSE_SPOILERNESS_CHANGE: - return updateCompose(state, action.composeId, compose => compose.withMutations(map => { - map.set('sensitive', !compose.sensitive); - map.set('idempotencyKey', crypto.randomUUID()); - })); + return updateCompose(state, action.composeId, compose => { + compose.sensitive = !compose.sensitive; + compose.idempotencyKey = crypto.randomUUID(); + }); case COMPOSE_SPOILER_TEXT_CHANGE: return updateCompose(state, action.composeId, compose => { - return compose - .setIn(compose.modified_language === compose.language ? ['spoiler_text'] : ['spoilerTextMap', compose.modified_language], action.text) - .set('idempotencyKey', crypto.randomUUID()); + if (compose.modified_language === compose.language) { + compose.spoiler_text = action.text; + } else { + compose.spoilerTextMap = compose.spoilerTextMap.set(compose.modified_language!, action.text); + } }); case COMPOSE_VISIBILITY_CHANGE: - return updateCompose(state, action.composeId, compose => compose - .set('privacy', action.value) - .set('idempotencyKey', crypto.randomUUID())); + return updateCompose(state, action.composeId, compose => { + compose.privacy = action.value; + compose.idempotencyKey = crypto.randomUUID(); + }); case COMPOSE_LANGUAGE_CHANGE: - return updateCompose(state, action.composeId, compose => compose.withMutations(map => { - map.set('language', action.value); - map.set('modified_language', action.value); - map.set('idempotencyKey', crypto.randomUUID()); - })); + return updateCompose(state, action.composeId, compose => { + compose.language = action.value; + compose.modified_language = action.value; + compose.idempotencyKey = crypto.randomUUID(); + }); case COMPOSE_MODIFIED_LANGUAGE_CHANGE: - return updateCompose(state, action.composeId, compose => compose.withMutations(map => { - map.set('modified_language', action.value); - map.set('idempotencyKey', crypto.randomUUID()); - })); + return updateCompose(state, action.composeId, compose => { + compose.modified_language = action.value; + compose.idempotencyKey = crypto.randomUUID(); + }); case COMPOSE_CHANGE: - return updateCompose(state, action.composeId, compose => compose - .set('text', action.text) - .set('idempotencyKey', crypto.randomUUID())); + return updateCompose(state, action.composeId, compose => { + compose.text = action.text; + compose.idempotencyKey = crypto.randomUUID(); + }); case COMPOSE_REPLY: - return updateCompose(state, action.composeId, compose => compose.withMutations(map => { - const defaultCompose = state.get('default')!; + return updateCompose(state, action.composeId, compose => { + const defaultCompose = state.default!; const to = action.explicitAddressing ? statusToMentionsArray(action.status, action.account, action.rebloggedBy) : ImmutableOrderedSet(); - map.set('group_id', action.status.group_id); - map.set('in_reply_to', action.status.id); - map.set('to', to); - map.set('parent_reblogged_by', action.rebloggedBy?.id || null); - map.set('text', !action.explicitAddressing ? statusToTextMentions(action.status, action.account) : ''); - map.set('privacy', privacyPreference(action.status.visibility, defaultCompose.privacy)); - map.set('focusDate', new Date()); - map.set('caretPosition', null); - map.set('idempotencyKey', crypto.randomUUID()); - map.set('content_type', defaultCompose.content_type); - map.set('approvalRequired', action.approvalRequired || false); + compose.group_id = action.status.group_id; + compose.in_reply_to = action.status.id; + compose.to = to; + compose.parent_reblogged_by = action.rebloggedBy?.id || null; + compose.text = !action.explicitAddressing ? statusToTextMentions(action.status, action.account) : ''; + compose.privacy = privacyPreference(action.status.visibility, defaultCompose.privacy); + compose.focusDate = new Date(); + compose.caretPosition = null; + compose.idempotencyKey = crypto.randomUUID(); + compose.content_type = defaultCompose.content_type; + compose.approvalRequired = action.approvalRequired || false; if (action.preserveSpoilers && action.status.spoiler_text) { - map.set('sensitive', true); - map.set('spoiler_text', action.status.spoiler_text); + compose.sensitive = true; + compose.spoiler_text = action.status.spoiler_text; } - })); + }); case COMPOSE_EVENT_REPLY: - return updateCompose(state, action.composeId, compose => compose.withMutations(map => { - map.set('in_reply_to', action.status.id); - map.set('to', statusToMentionsArray(action.status, action.account)); - map.set('idempotencyKey', crypto.randomUUID()); - })); + return updateCompose(state, action.composeId, compose => { + compose.in_reply_to = action.status.id; + compose.to = statusToMentionsArray(action.status, action.account); + compose.idempotencyKey = crypto.randomUUID(); + }); case COMPOSE_QUOTE: - return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => { + return updateCompose(state, 'compose-modal', compose => { const author = action.status.account.acct; - const defaultCompose = state.get('default')!; + const defaultCompose = state.default; - map.set('quote', action.status.id); - map.set('to', ImmutableOrderedSet([author])); - map.set('parent_reblogged_by', null); - map.set('text', ''); - map.set('privacy', privacyPreference(action.status.visibility, defaultCompose.privacy)); - map.set('focusDate', new Date()); - map.set('caretPosition', null); - map.set('idempotencyKey', crypto.randomUUID()); - map.set('content_type', defaultCompose.content_type); - map.set('spoiler_text', ''); + compose.quote = action.status.id; + compose.to = ImmutableOrderedSet([author]); + compose.parent_reblogged_by = null; + compose.text = ''; + compose.privacy = privacyPreference(action.status.visibility, defaultCompose.privacy); + compose.focusDate = new Date(); + compose.caretPosition = null; + compose.idempotencyKey = crypto.randomUUID(); + compose.content_type = defaultCompose.content_type; + compose.spoiler_text = ''; if (action.status.visibility === 'group') { - map.set('group_id', action.status.group_id); - map.set('privacy', 'group'); + compose.group_id = action.status.group_id; + compose.privacy = 'group'; } - })); + }); case COMPOSE_SUBMIT_REQUEST: - return updateCompose(state, action.composeId, compose => compose.set('is_submitting', true)); + return updateCompose(state, action.composeId, compose => { + compose.is_submitting = true; + }); case COMPOSE_UPLOAD_CHANGE_REQUEST: - return updateCompose(state, action.composeId, compose => compose.set('is_changing_upload', true)); + return updateCompose(state, action.composeId, compose => { + compose.is_changing_upload = true; + }); case COMPOSE_REPLY_CANCEL: case COMPOSE_RESET: case COMPOSE_SUBMIT_SUCCESS: - return updateCompose(state, action.composeId, () => state.get('default')!.withMutations(map => { - map.set('idempotencyKey', crypto.randomUUID()); - map.set('in_reply_to', action.composeId.startsWith('reply:') ? action.composeId.slice(6) : null); - if (action.composeId.startsWith('group:')) { - map.set('privacy', 'group'); - map.set('group_id', action.composeId.slice(6)); - } - })); + return create(state, (draft) => { + draft[action.composeId] = newCompose({ + idempotencyKey: crypto.randomUUID(), + in_reply_to: action.composeId.startsWith('reply:') ? action.composeId.slice(6) : null, + ...(action.composeId.startsWith('group:') ? { + privacy: 'group', + group_id: action.composeId.slice(6), + } : undefined), + }); + }); case COMPOSE_SUBMIT_FAIL: - return updateCompose(state, action.composeId, compose => compose.set('is_submitting', false)); + return updateCompose(state, action.composeId, compose => { + compose.is_submitting = false; + }); case COMPOSE_UPLOAD_CHANGE_FAIL: - return updateCompose(state, action.composeId, compose => compose.set('is_changing_upload', false)); + return updateCompose(state, action.composeId, compose => { + compose.is_changing_upload = false; + }); case COMPOSE_UPLOAD_REQUEST: - return updateCompose(state, action.composeId, compose => compose.set('is_uploading', true)); + return updateCompose(state, action.composeId, compose => { + compose.is_uploading = true; + }); case COMPOSE_UPLOAD_SUCCESS: - return updateCompose(state, action.composeId, compose => appendMedia(compose, action.media, state.get('default')!.sensitive)); + return updateCompose(state, action.composeId, compose => appendMedia(compose, action.media, state.default.sensitive)); case COMPOSE_UPLOAD_FAIL: - return updateCompose(state, action.composeId, compose => compose.set('is_uploading', false)); + return updateCompose(state, action.composeId, compose => { + compose.is_uploading = false; + }); case COMPOSE_UPLOAD_UNDO: return updateCompose(state, action.composeId, compose => removeMedia(compose, action.mediaId)); case COMPOSE_UPLOAD_PROGRESS: - return updateCompose(state, action.composeId, compose => compose.set('progress', Math.round((action.loaded / action.total) * 100))); + return updateCompose(state, action.composeId, compose => { + compose.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.acct} `].filter((str) => str.length !== 0).join(' ')); - map.set('focusDate', new Date()); - map.set('caretPosition', null); - map.set('idempotencyKey', crypto.randomUUID()); - })); + return updateCompose(state, 'compose-modal', compose => { + compose.text = [compose.text.trim(), `@${action.account.acct} `].filter((str) => str.length !== 0).join(' '); + compose.focusDate = new Date(); + compose.caretPosition = null; + compose.idempotencyKey = crypto.randomUUID(); + }); case COMPOSE_DIRECT: - return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => { - 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); - map.set('idempotencyKey', crypto.randomUUID()); - })); + return updateCompose(state, 'compose-modal', compose => { + compose.text = [compose.text.trim(), `@${action.account.acct} `].filter((str) => str.length !== 0).join(' '); + compose.privacy = 'direct'; + compose.focusDate = new Date(); + compose.caretPosition = null; + compose.idempotencyKey = crypto.randomUUID(); + }); case COMPOSE_GROUP_POST: - return updateCompose(state, action.composeId, compose => compose.withMutations(map => { - map.set('privacy', 'group'); - map.set('group_id', action.groupId); - map.set('focusDate', new Date()); - map.set('caretPosition', null); - map.set('idempotencyKey', crypto.randomUUID()); - })); + return updateCompose(state, action.composeId, compose => { + compose.privacy = 'group'; + compose.group_id = action.groupId; + compose.focusDate = new Date(); + compose.caretPosition = null; + compose.idempotencyKey = crypto.randomUUID(); + }); case COMPOSE_SUGGESTIONS_CLEAR: - return updateCompose(state, action.composeId, compose => compose.update('suggestions', list => list?.clear()).set('suggestion_token', null)); + return updateCompose(state, action.composeId, compose => { + compose.suggestions = compose.suggestions.clear(); + compose.suggestion_token = null; + }); case COMPOSE_SUGGESTIONS_READY: - return updateCompose(state, action.composeId, compose => compose.set('suggestions', ImmutableList(action.accounts ? action.accounts.map((item: APIEntity) => item.id) : action.emojis)).set('suggestion_token', action.token)); + return updateCompose(state, action.composeId, compose => { + compose.suggestions = ImmutableList(action.accounts ? action.accounts.map((item: APIEntity) => item.id) : action.emojis); + compose.suggestion_token = action.token; + }); case COMPOSE_SUGGESTION_SELECT: return updateCompose(state, action.composeId, compose => insertSuggestion(compose, action.position, action.token, action.completion, action.path)); case COMPOSE_SUGGESTION_TAGS_UPDATE: return updateCompose(state, action.composeId, compose => updateSuggestionTags(compose, action.token, action.tags)); case COMPOSE_TAG_HISTORY_UPDATE: - return updateCompose(state, action.composeId, compose => compose.set('tagHistory', ImmutableList(fromJS(action.tags)) as ImmutableList)); + return updateCompose(state, action.composeId, compose => { + compose.tagHistory = ImmutableList(fromJS(action.tags)) as ImmutableList; + }); case TIMELINE_DELETE: return updateCompose(state, 'compose-modal', compose => { if (action.statusId === compose.in_reply_to) { - return compose.set('in_reply_to', null); + compose.in_reply_to = null; } if (action.statusId === compose.quote) { - return compose.set('quote', null); - } else { - return compose; + compose.quote = null; } }); case COMPOSE_EMOJI_INSERT: return updateCompose(state, action.composeId, compose => insertEmoji(compose, action.position, action.emoji, action.needsSpace)); case COMPOSE_UPLOAD_CHANGE_SUCCESS: - return updateCompose(state, action.composeId, compose => compose - .set('is_changing_upload', false) - .update('media_attachments', list => list.map(item => { + return updateCompose(state, action.composeId, compose => { + compose.is_changing_upload = false; + + compose.media_attachments = compose.media_attachments.map(item => { if (item.id === action.media.id) { return action.media; } return item; - }))); + }); + }); case COMPOSE_SET_STATUS: - return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => { + return updateCompose(state, 'compose-modal', compose => { const to = action.explicitAddressing ? getExplicitMentions(action.status.account.id, action.status) : ImmutableOrderedSet(); if (!action.withRedraft && !action.draftId) { - map.set('id', action.status.id); + compose.id = action.status.id; } - map.set('text', action.rawText || unescapeHTML(expandMentions(action.status))); - map.set('to', to); - map.set('parent_reblogged_by', null); - map.set('in_reply_to', action.status.in_reply_to_id); - map.set('privacy', action.status.visibility); - map.set('focusDate', new Date()); - map.set('caretPosition', null); - map.set('idempotencyKey', crypto.randomUUID()); - map.set('content_type', action.contentType || 'text/plain'); - map.set('quote', action.status.quote_id); - map.set('group_id', action.status.group_id); + compose.text = action.rawText || unescapeHTML(expandMentions(action.status)); + compose.to = to; + compose.parent_reblogged_by = null; + compose.in_reply_to = action.status.in_reply_to_id; + compose.privacy = action.status.visibility; + compose.focusDate = new Date(); + compose.caretPosition = null; + compose.idempotencyKey = crypto.randomUUID(); + compose.content_type = action.contentType || 'text/plain'; + compose.quote = action.status.quote_id; + compose.group_id = action.status.group_id; if (action.v?.software === PLEROMA && action.withRedraft && hasIntegerMediaIds(action.status)) { - map.set('media_attachments', ImmutableList()); + compose.media_attachments = ImmutableList(); } else { - map.set('media_attachments', ImmutableList(action.status.media_attachments)); + compose.media_attachments = ImmutableList(action.status.media_attachments); } if (action.status.spoiler_text.length > 0) { - map.set('spoiler_text', action.status.spoiler_text); + compose.spoiler_text = action.status.spoiler_text; } else { - map.set('spoiler_text', ''); + compose.spoiler_text = ''; } if (action.poll) { - map.set('poll', PollRecord({ + compose.poll = newPoll({ options: ImmutableList(action.poll.options.map(({ title }) => title)), multiple: action.poll.multiple, expires_in: 24 * 3600, - })); + }); } if (action.draftId) { - map.set('draft_id', action.draftId); + compose.draft_id = action.draftId; } if (action.editorState) { - map.set('editorState', action.editorState); + compose.editorState = action.editorState; } - })); + }); case COMPOSE_POLL_ADD: - return updateCompose(state, action.composeId, compose => compose.set('poll', PollRecord())); + return updateCompose(state, action.composeId, compose => { + compose.poll = newPoll(); + }); case COMPOSE_POLL_REMOVE: - return updateCompose(state, action.composeId, compose => compose.set('poll', null)); + return updateCompose(state, action.composeId, compose => { + compose.poll = null; + }); case COMPOSE_SCHEDULE_ADD: - return updateCompose(state, action.composeId, compose => compose.set('schedule', new Date(Date.now() + 10 * 60 * 1000))); + return updateCompose(state, action.composeId, compose => { + compose.schedule = new Date(Date.now() + 10 * 60 * 1000); + }); case COMPOSE_SCHEDULE_SET: - return updateCompose(state, action.composeId, compose => compose.set('schedule', action.date)); + return updateCompose(state, action.composeId, compose => { + compose.schedule = action.date; + }); case COMPOSE_SCHEDULE_REMOVE: - return updateCompose(state, action.composeId, compose => compose.set('schedule', null)); + return updateCompose(state, action.composeId, compose => { + compose.schedule = null; + }); case COMPOSE_POLL_OPTION_ADD: - return updateCompose(state, action.composeId, compose => - compose - .updateIn(['poll', 'options'], options => (options as ImmutableList).push(action.title)) - .updateIn(['poll', 'options_map'], options_map => (options_map as ImmutableList>).push(ImmutableMap(compose.textMap.map(_ => action.title)))), - ); + return updateCompose(state, action.composeId, compose => { + if (!compose.poll) return; + compose.poll.options = compose.poll.options.push(action.title); + compose.poll.options_map = compose.poll.options_map.push(ImmutableMap(compose.textMap.map(_ => action.title))); + }); case COMPOSE_POLL_OPTION_CHANGE: - return updateCompose(state, action.composeId, compose => - compose.setIn(!compose.modified_language || compose.modified_language === compose.language ? ['poll', 'options', action.index] : ['poll', 'options_map', action.index, compose.modified_language], action.title), - ); + return updateCompose(state, action.composeId, compose => { + if (!compose.poll) return; + if (!compose.modified_language || compose.modified_language === compose.language) { + compose.poll.options = compose.poll.options.set(action.index, action.title); + compose.poll.options_map = compose.poll.options_map.setIn([action.index, compose.modified_language], action.title); + } + }); case COMPOSE_POLL_OPTION_REMOVE: - return updateCompose(state, action.composeId, compose => - compose - .updateIn(['poll', 'options'], options => (options as ImmutableList).delete(action.index)) - .updateIn(['poll', 'options_map'], options_map => (options_map as ImmutableList>).delete(action.index)), - ); + return updateCompose(state, action.composeId, compose => { + if (!compose.poll) return; + compose.poll.options = compose.poll.options.delete(action.index); + compose.poll.options_map = compose.poll.options_map.delete(action.index); + }); case COMPOSE_POLL_SETTINGS_CHANGE: - return updateCompose(state, action.composeId, 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); - } - }); - })); + return updateCompose(state, action.composeId, compose => { + if (!compose.poll) return null; + if (action.expiresIn) { + compose.poll.expires_in = action.expiresIn; + } + if (typeof action.isMultiple === 'boolean') { + compose.poll.multiple = action.isMultiple; + } + }); case COMPOSE_ADD_TO_MENTIONS: - return updateCompose(state, action.composeId, compose => compose.update('to', mentions => mentions!.add(action.account))); + return updateCompose(state, action.composeId, compose => { + compose.to = compose.to.add(action.account); + }); case COMPOSE_REMOVE_FROM_MENTIONS: - return updateCompose(state, action.composeId, compose => compose.update('to', mentions => mentions!.delete(action.account))); + return updateCompose(state, action.composeId, compose => { + compose.to = compose.to.delete(action.account); + }); case ME_FETCH_SUCCESS: case ME_PATCH_SUCCESS: return updateCompose(state, 'default', compose => importAccount(compose, action.me)); // case SETTING_CHANGE: // return updateCompose(state, 'default', compose => updateSetting(compose, action.path, action.value)); case COMPOSE_EDITOR_STATE_SET: - return updateCompose(state, action.composeId, compose => compose - .setIn(!compose.modified_language || compose.modified_language === compose.language ? ['editorState'] : ['editorStateMap', compose.modified_language], action.editorState as string) - .setIn(!compose.modified_language || compose.modified_language === compose.language ? ['text'] : ['textMap', compose.modified_language], action.text as string)); + return updateCompose(state, action.composeId, compose => { + if (!compose.modified_language || compose.modified_language) { + compose.editorState = action.editorState as string; + compose.text = action.text as string; + } else { + compose.editorStateMap = compose.editorStateMap.set(compose.modified_language, action.editorState as string); + compose.textMap = compose.textMap.set(compose.modified_language, action.text as string); + } + }); case EVENT_COMPOSE_CANCEL: - return updateCompose(state, 'event-compose-modal', compose => compose.set('text', '')); + return updateCompose(state, 'event-compose-modal', compose => { + compose.text = ''; + }); case EVENT_FORM_SET: - return updateCompose(state, action.composeId, compose => compose.set('text', action.text)); + return updateCompose(state, action.composeId, compose => { + compose.text = action.text; + }); case COMPOSE_CHANGE_MEDIA_ORDER: - return updateCompose(state, action.composeId, compose => compose.update('media_attachments', list => { - const indexA = list.findIndex(x => x.id === action.a); - const moveItem = list.get(indexA)!; - const indexB = list.findIndex(x => x.id === action.b); + return updateCompose(state, action.composeId, compose => { + const indexA = compose.media_attachments.findIndex(x => x.id === action.a); + const moveItem = compose.media_attachments.get(indexA)!; + const indexB = compose.media_attachments.findIndex(x => x.id === action.b); - return list.splice(indexA, 1).splice(indexB, 0, moveItem); - })); + return compose.media_attachments.splice(indexA, 1).splice(indexB, 0, moveItem); + }); case COMPOSE_ADD_SUGGESTED_QUOTE: - return updateCompose(state, action.composeId, compose => compose.set('quote', action.quoteId)); + return updateCompose(state, action.composeId, compose => { + compose.quote = action.quoteId; + }); case COMPOSE_ADD_SUGGESTED_LANGUAGE: - return updateCompose(state, action.composeId, compose => compose.set('suggested_language', action.language)); + return updateCompose(state, action.composeId, compose => { + compose.suggested_language = action.language; + }); case COMPOSE_LANGUAGE_ADD: - return updateCompose(state, action.composeId, compose => - compose - .setIn(['editorStateMap', action.value], compose.editorState) - .setIn(['textMap', action.value], compose.text) - .setIn(['spoilerTextMap', action.value], compose.spoiler_text) - .update('poll', poll => { - if (!poll) return poll; - return poll.update('options_map', optionsMap => optionsMap.map((option, key) => option.set(action.value, poll.options.get(key)!))); - }), - ); + return updateCompose(state, action.composeId, compose => { + compose.editorStateMap = compose.editorStateMap.set(action.value, compose.editorState); + compose.textMap = compose.textMap.set(action.value, compose.text); + compose.spoilerTextMap = compose.spoilerTextMap.set(action.value, compose.spoiler_text); + if (compose.poll) compose.poll.options_map = compose.poll.options_map.map((option, key) => option.set(action.value, compose.poll!.options.get(key)!)); + }); case COMPOSE_LANGUAGE_DELETE: - return updateCompose(state, action.composeId, compose => compose - .removeIn(['editorStateMap', action.value]) - .removeIn(['textMap', action.value]) - .removeIn(['spoilerTextMap', action.value])); + return updateCompose(state, action.composeId, compose => { + compose.editorStateMap = compose.editorStateMap.delete(action.value); + compose.textMap = compose.textMap.delete(action.value); + compose.spoilerTextMap = compose.spoilerTextMap.delete(action.value); + }); case COMPOSE_QUOTE_CANCEL: - return updateCompose(state, action.composeId, compose => compose - .update('dismissed_quotes', quotes => compose.quote ? quotes.add(compose.quote) : quotes) - .set('quote', null)); + return updateCompose(state, action.composeId, (compose) => { + if (compose.quote) compose.dismissed_quotes = compose.dismissed_quotes.add(compose.quote); + compose.quote = null; + }); case COMPOSE_FEDERATED_CHANGE: - return updateCompose(state, action.composeId, compose => compose.update('federated', value => !value)); + return updateCompose(state, action.composeId, compose => { + compose.federated = !compose.federated; + }); case INSTANCE_FETCH_SUCCESS: return updateCompose(state, 'default', (compose) => updateDefaultContentType(compose, action.instance)); default: @@ -606,7 +711,7 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | In }; export { - ReducerCompose, + type Compose, statusToMentionsAccountIdsArray, initialState, compose as default,