diff --git a/src/reducers/compose.ts b/src/reducers/compose.ts index 1c517f86e..094c73cc4 100644 --- a/src/reducers/compose.ts +++ b/src/reducers/compose.ts @@ -1,4 +1,4 @@ -import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord, fromJS } from 'immutable'; +import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import { isNativeEmoji } from 'soapbox/features/emoji/index.ts'; import { Account } from 'soapbox/schemas/index.ts'; @@ -73,65 +73,136 @@ import type { const getResetFileKey = () => Math.floor((Math.random() * 0x10000)); -const PollRecord = ImmutableRecord({ - options: ImmutableList(['', '']), +const PollRecord = { + options: ['', ''], expires_in: 24 * 3600, multiple: false, -}); +}; -export const ReducerCompose = ImmutableRecord({ - caretPosition: null as number | null, +export interface Compose { + caretPosition: number | null; + content_type: string; + editorState: string | null; + 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: AttachmentEntity[]; + poll: Poll | null; + privacy: string; + progress: number; + quote: string | null; + resetFileKey: number | null; + schedule: Date | null; + sensitive: boolean; + spoiler: boolean; + spoiler_text: string; + suggestions: string[] | Emoji[]; + suggestion_token: string | null; + tagHistory: string[]; + text: string; + to: Set; + group_timeline_visible: boolean; // TruthSocial +} + +const initialCompose: Compose = { + caretPosition: null, content_type: 'text/plain', - editorState: null as string | null, - focusDate: null as Date | null, - group_id: null as string | null, - idempotencyKey: '', - id: null as string | null, - in_reply_to: null as string | null, + editorState: null, + focusDate: null, + group_id: null, + idempotencyKey: crypto.randomUUID(), + 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, + media_attachments: [], + poll: null, privacy: 'public', progress: 0, - quote: null as string | null, - resetFileKey: null as number | null, - schedule: null as Date | null, + quote: null, + resetFileKey: getResetFileKey(), + schedule: null, sensitive: false, spoiler: false, spoiler_text: '', - suggestions: ImmutableList(), - suggestion_token: null as string | null, - tagHistory: ImmutableList(), + suggestions: [], + suggestion_token: null, + tagHistory: [], text: '', - to: ImmutableOrderedSet(), + to: new Set(), group_timeline_visible: false, // TruthSocial -}); +}; -type State = ImmutableMap; -type Compose = ReturnType; -type Poll = ReturnType; +// export const ReducerCompose = ImmutableRecord({ +// caretPosition: null as number | null, +// content_type: 'text/plain', +// editorState: null as string | null, +// focusDate: null as Date | null, +// group_id: null as string | null, +// idempotencyKey: '', +// 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(), +// 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: ImmutableOrderedSet(), +// group_timeline_visible: false, // TruthSocial +// }); + +type State = Record; +// type Poll = ReturnType; +type Poll = { + options: string[]; + expires_in: number; + multiple: boolean; +}; +type Path = Array; const statusToTextMentions = (status: Status, account: Account) => { - const author = status.getIn(['account', 'acct']); - const mentions = status.get('mentions')?.map((m) => m.acct) || []; + const author = status.account.acct; + const mentions = status.mentions?.map((m) => m.acct) || []; - return ImmutableOrderedSet([author]) - .concat(mentions) - .delete(account.acct) - .map(m => `@${m} `) - .join(''); + const textMentions = new Set([author, ...mentions]); + + textMentions.delete(account.acct); + + const updatedTextMentions = Array.from(textMentions).map(m => `@${m}`).join(''); + + return updatedTextMentions; }; export const statusToMentionsArray = (status: Status, account: Account) => { - const author = status.getIn(['account', 'acct']) as string; - const mentions = status.get('mentions')?.map((m) => m.acct) || []; + const author = status.account.acct as string; + const mentions = status.mentions?.map((m) => m.acct) || []; - return ImmutableOrderedSet([author]) - .concat(mentions) - .delete(account.acct) as ImmutableOrderedSet; + const mentionsSet = new Set([author, ...mentions]); + + mentionsSet.delete(account.acct); + + return mentionsSet; }; export const statusToMentionsAccountIdsArray = (status: StatusEntity, account: Account) => { @@ -143,56 +214,79 @@ export const statusToMentionsAccountIdsArray = (status: StatusEntity, account: A }; const appendMedia = (compose: Compose, media: APIEntity, defaultSensitive?: boolean) => { - const prevSize = compose.media_attachments.size; + const prevSize = compose.media_attachments.length; - return compose.withMutations(map => { - 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', crypto.randomUUID()); - - if (prevSize === 0 && (defaultSensitive || compose.spoiler)) { - map.set('sensitive', true); - } - }); + return { + ...compose, + media_attachments: [...compose.media_attachments, normalizeAttachment(media)], + is_uploading: false, + resetFileKey: Math.floor((Math.random() * 0x10000)), + idempotencyKey: crypto.randomUUID(), + sensitive: (prevSize === 0 && (defaultSensitive || compose.spoiler)) ? true : compose.sensitive, + }; }; const removeMedia = (compose: Compose, mediaId: string) => { - const prevSize = compose.media_attachments.size; + const prevSize = compose.media_attachments.length; - return compose.withMutations(map => { - map.update('media_attachments', list => list.filterNot(item => item.id === mediaId)); - map.set('idempotencyKey', crypto.randomUUID()); - - if (prevSize === 1) { - map.set('sensitive', false); - } - }); + return { + ...compose, + media_attachments: compose.media_attachments.filter(item => item.id !== mediaId), + idempotencyKey: crypto.randomUUID(), + sensitive: prevSize === 1 ? false : compose.sensitive, + }; }; 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 ?? 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); + const prevCompose = { ...compose }; + + const updateInPath = (obj: any, path: Path, updateFn: (value: any) => any): any => { + if (path.length === 0) return updateFn(obj); + + const [head, ...tail] = path; + if (Array.isArray(obj[head])) { + return [ + ...obj[head].map((item: any, index: number) => + index === tail[0] ? updateInPath(item, tail, updateFn) : item, + ), + ]; + } else { + return { + ...obj, + [head]: updateInPath(obj[head], tail, updateFn), + }; } - map.set('idempotencyKey', crypto.randomUUID()); - }); + }; + + if (path.length === 1 && path[0] === 'text') { + const oldText = prevCompose.text || ''; + const updatedText = `${oldText.slice(0, position)}${completion} ${oldText.slice(position + (token?.length ?? 0))}`; + + prevCompose.text = updatedText; + prevCompose.focusDate = new Date(); + prevCompose.caretPosition = position + completion.length + 1; + } + + prevCompose.suggestion_token = null; + prevCompose.suggestions = []; + prevCompose.idempotencyKey = crypto.randomUUID(); + + prevCompose.text = updateInPath(prevCompose.text, path, oldText => `${oldText.slice(0, position)}${completion} ${(oldText as string).slice(position + (token?.length ?? 0))}`); + + return prevCompose; }; -const updateSuggestionTags = (compose: Compose, token: string, tags: ImmutableList) => { +const updateSuggestionTags = (compose: Compose, token: string, tags: Tag[]) => { const prefix = token.slice(1); - return compose.merge({ - suggestions: ImmutableList(tags + return { + ...compose, + suggestions: tags .filter((tag) => tag.get('name').toLowerCase().startsWith(prefix.toLowerCase())) .slice(0, 4) - .map((tag) => '#' + tag.name)), + .map((tag) => '#' + tag.name), suggestion_token: token, - }); + }; }; const insertEmoji = (compose: Compose, position: number, emojiData: Emoji, needsSpace: boolean) => { @@ -200,12 +294,13 @@ 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({ + return { + ...compose, text: `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`, focusDate: new Date(), caretPosition: position + emoji.length + 1, idempotencyKey: crypto.randomUUID(), - }); + }; }; const privacyPreference = (a: string, b: string) => { @@ -233,11 +328,11 @@ const getExplicitMentions = (me: string, status: Status) => { const fragment = domParser.parseFromString(status.content, 'text/html').documentElement; const mentions = status - .get('mentions') + .mentions .filter((mention) => !(fragment.querySelector(`a[href="${mention.url}"]`) || mention.id === me)) .map((m) => m.acct); - return ImmutableOrderedSet(mentions); + return new Set(mentions); }; const getAccountSettings = (account: ImmutableMap) => { @@ -250,177 +345,263 @@ const importAccount = (compose: Compose, account: APIEntity) => { const defaultPrivacy = settings.get('defaultPrivacy'); const defaultContentType = settings.get('defaultContentType'); - return compose.withMutations(compose => { - if (defaultPrivacy) compose.set('privacy', defaultPrivacy); - if (defaultContentType) compose.set('content_type', defaultContentType); - compose.set('tagHistory', ImmutableList(tagHistory.get(account.id))); - }); + return { + ...compose, + privacy: defaultPrivacy ?? compose.privacy, + content_type: defaultContentType ?? compose.content_type, + tagHistory: tagHistory.get(account.id) || compose.tagHistory, + }; + // compose.withMutations(compose => { + // if (defaultPrivacy) compose.set('privacy', defaultPrivacy); + // if (defaultContentType) compose.set('content_type', defaultContentType); + // compose.set('tagHistory', ImmutableList(tagHistory.get(account.id))); + // }); }; const updateSetting = (compose: Compose, path: string[], value: string) => { const pathString = path.join(','); switch (pathString) { case 'defaultPrivacy': - return compose.set('privacy', value); + return { ...compose, privacy: value }; case 'defaultContentType': - return compose.set('content_type', value); + return { ...compose, content_type: value }; default: return compose; } }; -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) => Compose) => { + const updatedCompose = updater(state[key]); + return { + ...compose, + [key]: updatedCompose, + }; +}; -export const initialState: State = ImmutableMap({ - default: ReducerCompose({ idempotencyKey: crypto.randomUUID(), resetFileKey: getResetFileKey() }), -}); +export const initialState: State = { + default: initialCompose, +}; export default function compose(state = initialState, action: ComposeAction | EventsAction | MeAction | SettingsAction | TimelineAction) { switch (action.type) { case COMPOSE_TYPE_CHANGE: - return updateCompose(state, action.id, compose => compose.withMutations(map => { - map.set('content_type', action.value); - map.set('idempotencyKey', crypto.randomUUID()); - })); + return updateCompose(state, action.id, compose => { + return { + ...compose, + content_type: action.value, + idempotencyKey: crypto.randomUUID(), + }; + }); case COMPOSE_SPOILERNESS_CHANGE: - return updateCompose(state, action.id, compose => compose.withMutations(map => { - map.set('spoiler_text', ''); - map.set('spoiler', !compose.spoiler); - map.set('sensitive', !compose.spoiler); - map.set('idempotencyKey', crypto.randomUUID()); - })); + return updateCompose(state, action.id, compose => { + return { + ...compose, + spoiler_text: '', + spoiler: !compose.spoiler, + sensitive: !compose.spoiler, + idempotencyKey: crypto.randomUUID(), + }; + }); case COMPOSE_SPOILER_TEXT_CHANGE: - return updateCompose(state, action.id, compose => compose - .set('spoiler_text', action.text) - .set('idempotencyKey', crypto.randomUUID())); + return updateCompose(state, action.id, compose => { + return { + ...compose, + spoiler_text: action.text, + idempotencyKey: crypto.randomUUID(), + }; + }); case COMPOSE_VISIBILITY_CHANGE: - return updateCompose(state, action.id, compose => compose - .set('privacy', action.value) - .set('idempotencyKey', crypto.randomUUID())); + return updateCompose(state, action.id, compose => { + return { + ...compose, + privacy: action.value, + idempotencyKey: crypto.randomUUID(), + }; + }); case COMPOSE_CHANGE: - return updateCompose(state, action.id, compose => compose - .set('text', action.text) - .set('idempotencyKey', crypto.randomUUID())); + return updateCompose(state, action.id, compose => { + return { + ...compose, + text: action.text, + idempotencyKey: crypto.randomUUID(), + }; + }); case COMPOSE_REPLY: - return updateCompose(state, action.id, compose => compose.withMutations(map => { - const defaultCompose = state.get('default')!; + return updateCompose(state, action.id, compose => { + const defaultCompose = state.default; + + const updatedCompose = { + ...compose, + group_id: action.status.group?.id as string, + in_reply_to: action.status.id, + to: action.explicitAddressing ? statusToMentionsArray(action.status, action.account) : new Set(), + text: !action.explicitAddressing ? statusToTextMentions(action.status, action.account) : '', + privacy: privacyPreference(action.status.visibility, defaultCompose.privacy), + focusDate: new Date(), + caretPosition: null, + idempotencyKey: crypto.randomUUID(), + content_type: defaultCompose.content_type, + }; - 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) : ''); - 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); if (action.preserveSpoilers && action.status.spoiler_text) { - map.set('spoiler', true); - map.set('sensitive', true); - map.set('spoiler_text', action.status.spoiler_text); + updatedCompose.spoiler = true; + updatedCompose.sensitive = true; + updatedCompose.spoiler_text = action.status.spoiler_text; } - })); - case COMPOSE_EVENT_REPLY: - return updateCompose(state, action.id, compose => compose.withMutations(map => { - map.set('in_reply_to', action.status.get('id')); - map.set('to', statusToMentionsArray(action.status, action.account)); - map.set('idempotencyKey', crypto.randomUUID()); - })); - case COMPOSE_QUOTE: - return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => { - 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('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', false); - map.set('spoiler_text', ''); + return updatedCompose; + }); + case COMPOSE_EVENT_REPLY: + return updateCompose(state, action.id, compose => { + return { + ...compose, + in_reply_to: action.status.id, + to: statusToMentionsArray(action.status, action.account), + idempotencyKey: crypto.randomUUID(), + }; + }); + case COMPOSE_QUOTE: + return updateCompose(state, 'compose-modal', compose => { + const author = action.status.account.acct as string; + const defaultCompose = state.default; + + let updatedCompose = { + ...compose, + quote: action.status.id, + to: new Set([author]), + text: '', + privacy: privacyPreference(action.status.visibility, defaultCompose.privacy), + focusDate: new Date(), + caretPosition: null, + idempotencyKey: crypto.randomUUID(), + content_type: defaultCompose.content_type, + spoiler: false, + spoiler_text: '', + }; if (action.status.visibility === 'group') { if (action.status.group?.group_visibility === 'everyone') { - map.set('privacy', privacyPreference('public', defaultCompose.privacy)); + updatedCompose = { + ...updatedCompose, + privacy: privacyPreference('public', defaultCompose.privacy), + }; } else if (action.status.group?.group_visibility === 'members_only') { - map.set('group_id', action.status.getIn(['group', 'id']) as string); - map.set('privacy', 'group'); + updatedCompose = { + ...updatedCompose, + group_id: action.status.group?.id as string, + privacy: 'group', + }; } } - })); + + return updatedCompose; + + }); case COMPOSE_SUBMIT_REQUEST: - return updateCompose(state, action.id, compose => compose.set('is_submitting', true)); + return updateCompose(state, action.id, compose => { + return { + ...compose, + is_submitting: true, + }; + }); case COMPOSE_UPLOAD_CHANGE_REQUEST: - return updateCompose(state, action.id, compose => compose.set('is_changing_upload', true)); + return updateCompose(state, action.id, compose => { + return { + ...compose, + is_changing_upload: true, + }; + }); case COMPOSE_REPLY_CANCEL: case COMPOSE_QUOTE_CANCEL: case COMPOSE_RESET: case COMPOSE_SUBMIT_SUCCESS: - return updateCompose(state, action.id, () => state.get('default')!.withMutations(map => { - map.set('idempotencyKey', crypto.randomUUID()); - map.set('in_reply_to', action.id.startsWith('reply:') ? action.id.slice(6) : null); + return updateCompose(state, action.id, () => { + let updatedState = { + ...state.default, + idempotencyKey: crypto.randomUUID(), + in_reply_to: action.id.startsWith('reply:') ? action.id.slice(6) : null, + }; + if (action.id.startsWith('group:')) { - map.set('privacy', 'group'); - map.set('group_id', action.id.slice(6)); + updatedState = { + ...updatedState, + privacy: 'group', + group_id: action.id.slice(6), + }; } - })); + + return updatedState; + }); case COMPOSE_SUBMIT_FAIL: - return updateCompose(state, action.id, compose => compose.set('is_submitting', false)); + return updateCompose(state, action.id, 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.id, compose => compose.set('is_uploading', true)); + return updateCompose(state, action.id, compose => ({ ...compose, is_uploading: true })); case COMPOSE_UPLOAD_SUCCESS: - return updateCompose(state, action.id, compose => appendMedia(compose, fromJS(action.media), state.get('default')!.sensitive)); + return updateCompose(state, action.id, compose => appendMedia(compose, action.media, state.default.sensitive)); case COMPOSE_UPLOAD_FAIL: - return updateCompose(state, action.id, compose => compose.set('is_uploading', false)); + return updateCompose(state, action.id, compose => ({ ...compose, is_uploading: false })); case COMPOSE_UPLOAD_UNDO: return updateCompose(state, action.id, compose => removeMedia(compose, action.media_id)); case COMPOSE_UPLOAD_PROGRESS: - return updateCompose(state, action.id, compose => compose.set('progress', Math.round((action.loaded / action.total) * 100))); + return updateCompose(state, action.id, 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(' '), + focusDate: new Date(), + caretPosition: null, + 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(' '), + privacy: 'direct', + focusDate: new Date(), + caretPosition: null, + idempotencyKey: crypto.randomUUID(), })); case COMPOSE_GROUP_POST: - return updateCompose(state, action.id, compose => compose.withMutations(map => { - map.set('privacy', 'group'); - map.set('group_id', action.group_id); - map.set('focusDate', new Date()); - map.set('caretPosition', null); - map.set('idempotencyKey', crypto.randomUUID()); - })); + return updateCompose(state, action.id, compose => { + return { + ...compose, + privacy: 'group', + group_id: action.group_id, + focusDate: new Date(), + caretPosition: null, + idempotencyKey: crypto.randomUUID(), + }; + }); case COMPOSE_SUGGESTIONS_CLEAR: - return updateCompose(state, action.id, compose => compose.update('suggestions', list => list?.clear()).set('suggestion_token', null)); + return updateCompose(state, action.id, compose => ({ ...compose, suggestions: [] as string[], + suggestion_token: null, + })); case COMPOSE_SUGGESTIONS_READY: - return updateCompose(state, action.id, 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.id, compose => ( + { + ...compose, + suggestions: action.accounts + ? action.accounts.map((item: APIEntity): string => item.id) + : (action.emojis ?? []), + suggestion_token: action.token, + } + )); case COMPOSE_SUGGESTION_SELECT: return updateCompose(state, action.id, compose => insertSuggestion(compose, action.position, action.token, action.completion, action.path)); case COMPOSE_SUGGESTION_TAGS_UPDATE: return updateCompose(state, action.id, compose => updateSuggestionTags(compose, action.token, action.tags)); case COMPOSE_TAG_HISTORY_UPDATE: - return updateCompose(state, action.id, compose => compose.set('tagHistory', ImmutableList(fromJS(action.tags)) as ImmutableList)); + return updateCompose(state, action.id, compose => ({ ...compose, tagHistory: action.tags })); case TIMELINE_DELETE: return updateCompose(state, 'compose-modal', compose => { if (action.id === compose.in_reply_to) { - return compose.set('in_reply_to', null); + return { ...compose, in_reply_to: null }; } if (action.id === compose.quote) { - return compose.set('quote', null); + return { ...compose, quote: null }; } else { return compose; } @@ -428,106 +609,185 @@ export default function compose(state = initialState, action: ComposeAction | Ev case COMPOSE_EMOJI_INSERT: return updateCompose(state, action.id, compose => insertEmoji(compose, action.position, action.emoji, action.needsSpace)); case COMPOSE_UPLOAD_CHANGE_SUCCESS: - return updateCompose(state, action.id, compose => compose - .set('is_changing_upload', false) - .update('media_attachments', list => list.map(item => { + return updateCompose(state, action.id, compose => ({ + ...compose, + is_changing_upload: false, + media_attachments: compose.media_attachments.map(item => { if (item.id === action.media.id) { return normalizeAttachment(action.media); } - return item; - }))); + }), + })); case COMPOSE_SET_STATUS: - return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => { - if (!action.withRedraft) { - map.set('id', action.status.id); - } - map.set('text', action.rawText || htmlToPlaintext(expandMentions(action.status))); - 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()); - map.set('caretPosition', null); - map.set('idempotencyKey', crypto.randomUUID()); - map.set('content_type', action.contentType || 'text/plain'); - map.set('quote', action.status.getIn(['quote', 'id']) as string); - map.set('group_id', action.status.getIn(['group', 'id']) as string); + return updateCompose(state, 'compose-modal', compose => { + let updatedCompose = { + ...compose, + text: action.rawText || htmlToPlaintext(expandMentions(action.status)), + to: action.explicitAddressing ? getExplicitMentions(action.status.account.id, action.status) : new Set(), + in_reply_to: action.status.in_reply_to_id, + privacy: action.status.visibility, + focusDate: new Date(), + caretPosition: null, + idempotencyKey: crypto.randomUUID(), + content_type: action.contentType || 'text/plain', + quote: action.status.getIn(['quote', 'id']) as string, + group_id: action.status.getIn(['group', 'id']) as string, + }; if (action.v?.software === PLEROMA && action.withRedraft && hasIntegerMediaIds(action.status.toJS() as any)) { - map.set('media_attachments', ImmutableList()); + updatedCompose = { + ...updatedCompose, + media_attachments: [], + + }; } else { - map.set('media_attachments', action.status.media_attachments); + updatedCompose = { + ...updatedCompose, + media_attachments: action.status.media_attachments.toArray(), + }; } if (action.status.get('spoiler_text').length > 0) { - map.set('spoiler', true); - map.set('spoiler_text', action.status.get('spoiler_text')); + updatedCompose = { + ...updatedCompose, + spoiler: true, + spoiler_text: action.status.spoiler_text, + }; } else { - map.set('spoiler', false); - map.set('spoiler_text', ''); + updatedCompose = { + ...updatedCompose, + spoiler: false, + spoiler_text: '', + }; } if (action.status.poll && typeof action.status.poll === 'object') { - map.set('poll', PollRecord({ - options: ImmutableList(action.status.poll.options.map(({ title }) => title)), - multiple: action.status.poll.multiple, - expires_in: 24 * 3600, - })); + updatedCompose = { + ...updatedCompose, + poll: { + ...PollRecord, + options: action.status.poll.options.map(({ title }) => title), + multiple: action.status.poll.multiple, + expires_in: 24 * 3600, + }, + }; } - })); + return updatedCompose; + }); case COMPOSE_POLL_ADD: - return updateCompose(state, action.id, compose => compose.set('poll', PollRecord())); + return updateCompose(state, action.id, compose => ({ ...compose, poll: { ...PollRecord } })); case COMPOSE_POLL_REMOVE: - return updateCompose(state, action.id, compose => compose.set('poll', null)); + return updateCompose(state, action.id, compose => ({ ...compose, poll: null })); case COMPOSE_SCHEDULE_ADD: - return updateCompose(state, action.id, compose => compose.set('schedule', new Date(Date.now() + 10 * 60 * 1000))); + return updateCompose(state, action.id, compose => ({ ...compose, schedule: new Date(Date.now() + 10 * 60 * 1000) })); case COMPOSE_SCHEDULE_SET: - return updateCompose(state, action.id, compose => compose.set('schedule', action.date)); + return updateCompose(state, action.id, compose => ({ ...compose, schedule: action.date })); case COMPOSE_SCHEDULE_REMOVE: - return updateCompose(state, action.id, compose => compose.set('schedule', null)); + return updateCompose(state, action.id, compose => ({ ...compose, schedule: null })); case COMPOSE_POLL_OPTION_ADD: - return updateCompose(state, action.id, compose => compose.updateIn(['poll', 'options'], options => (options as ImmutableList).push(action.title))); + return updateCompose(state, action.id, compose => { + const updatedPoll = { + ...compose.poll, + options: [ ...(compose.poll?.options || ['', '']), action.title ], + expires_in: compose.poll?.expires_in ?? 24 * 3600, + multiple: compose.poll?.multiple ?? false, + }; + return { + ...compose, + poll: updatedPoll, + }; + }); case COMPOSE_POLL_OPTION_CHANGE: - return updateCompose(state, action.id, compose => compose.setIn(['poll', 'options', action.index], action.title)); + return updateCompose(state, action.id, compose => { + const updatedPoll = { + ...compose.poll, + options: (compose.poll?.options || ['', '']).map((option, index) => + index === action.index ? action.title : option, + ), + expires_in: compose.poll?.expires_in ?? 24 * 3600, + multiple: compose.poll?.multiple ?? false, + }; + + return { + ...compose, + poll: updatedPoll, + }; + + }); case COMPOSE_POLL_OPTION_REMOVE: - return updateCompose(state, action.id, compose => compose.updateIn(['poll', 'options'], options => (options as ImmutableList).delete(action.index))); + return updateCompose(state, action.id, compose => { + + const updatedPollOptions = (compose.poll?.options || []).filter((option, index) => index !== action.index); + + return { + ...compose, + poll: { + ...compose.poll, + options: updatedPollOptions, + expires_in: compose.poll?.expires_in ?? 24 * 3600, + multiple: compose.poll?.multiple ?? false, + }, + }; + }); case COMPOSE_POLL_SETTINGS_CHANGE: - 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); - } - }); - })); + return updateCompose(state, action.id, compose => { + + const updatedPoll = { + ...compose.poll, + options: compose.poll?.options ?? ['', ''], + expires_in: action.expiresIn ?? 24 * 3600, + multiple: (typeof action.isMultiple === 'boolean') ? action.isMultiple : false, + }; + + return { + ...compose, + poll: compose.poll ? updatedPoll : null, + }; + }); case COMPOSE_ADD_TO_MENTIONS: - return updateCompose(state, action.id, compose => compose.update('to', mentions => mentions!.add(action.account))); + return updateCompose(state, action.id, compose => ({ + ...compose, to: compose.to.add(action.account), + })); case COMPOSE_REMOVE_FROM_MENTIONS: - return updateCompose(state, action.id, compose => compose.update('to', mentions => mentions!.delete(action.account))); + return updateCompose(state, action.id, compose => { + const updatedTo = new Set(compose.to); + updatedTo.delete(action.account); + + return { + ...compose, + to: updatedTo, + }; + }); case COMPOSE_SET_GROUP_TIMELINE_VISIBLE: - return updateCompose(state, action.id, compose => compose.set('group_timeline_visible', action.groupTimelineVisible)); + return updateCompose(state, action.id, compose => ({ ...compose, group_timeline_visible: action.groupTimelineVisible })); 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.id, compose => compose.set('editorState', action.editorState as string)); + return updateCompose(state, action.id, compose => ({ ...compose, editorState: action.editorState 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, 'event-compose-modal', compose => compose.set('text', action.text)); + return updateCompose(state, 'event-compose-modal', compose => ({ ...compose, text: action.text })); case COMPOSE_CHANGE_MEDIA_ORDER: - return updateCompose(state, action.id, compose => compose.update('media_attachments', list => { - const indexA = list.findIndex(x => x.get('id') === action.a); - const moveItem = list.get(indexA)!; - const indexB = list.findIndex(x => x.get('id') === action.b); + return updateCompose(state, action.id, compose => { + const updatedMediaAttachments = [...compose.media_attachments]; - return list.splice(indexA, 1).splice(indexB, 0, moveItem); - })); + const indexA = updatedMediaAttachments.findIndex(x => x.id === action.a); + const moveItem = updatedMediaAttachments[indexA]; + const indexB = updatedMediaAttachments.findIndex(x => x.id === action.b); + + updatedMediaAttachments.splice(indexA, 1); + updatedMediaAttachments.splice(indexB, 0, moveItem); + + return { + ...compose, + media_attachments: updatedMediaAttachments, + }; + }); default: return state; }