pl-fe: remove most of immutable from compose reducer

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-11-13 19:47:07 +01:00
parent 36b27a3410
commit a21f75c82c
9 changed files with 398 additions and 292 deletions

View file

@ -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<string | number>;
path: ['spoiler_text'] | ['poll', 'options', number];
}
const selectComposeSuggestion = (composeId: string, position: number, token: string | null, suggestion: AutoSuggestion, path: Array<string | number>) =>
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<Tag>, 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,
};

View file

@ -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(),
};

View file

@ -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<typeof ReducerCompose>) =>
const checkComposeContent = (compose?: Compose) =>
!!compose && [
compose.editorState && compose.editorState.length > 0,
compose.spoiler_text.length > 0,
@ -59,7 +59,7 @@ const ModalRoot: React.FC<IModalRoot> = ({ 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') {

View file

@ -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) {

View file

@ -15,7 +15,7 @@ const QuotedStatusContainer: React.FC<IQuotedStatusContainer> = ({ 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));

View file

@ -113,7 +113,7 @@ const ComposeEditor = React.forwardRef<LexicalEditor, IComposeEditor>(({
theme,
editorState: dispatch((_, getState) => {
const state = getState();
const compose = state.compose.get(composeId);
const compose = state.compose[composeId];
if (!compose) return;

View file

@ -29,7 +29,7 @@ const StatePlugin: React.FC<IStatePlugin> = ({ 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<IStatePlugin> = ({ 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;

View file

@ -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 = <ID extends string>(composeId: ID extends 'default' ? never : ID): ReturnType<typeof ReducerCompose> =>
useAppSelector((state) => state.compose.get(composeId, state.compose.get('default')!));
const useCompose = <ID extends string>(composeId: ID extends 'default' ? never : ID): Compose =>
useAppSelector((state) => state.compose[composeId] || state.compose.default);
export { useCompose };

View file

@ -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<string>;
options_map: ImmutableList<ImmutableMap<Language, string>>;
expires_in: number;
multiple: boolean;
hide_totals: boolean;
}
const newPoll = (params: Partial<ComposePoll> = {}): ComposePoll => ({
options: ImmutableList(['', '']),
options_map: ImmutableList<ImmutableMap<Language, string>>([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<Language, 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: ImmutableList<MediaAttachment>;
poll: ComposePoll | null;
privacy: string;
progress: number;
quote: string | null;
resetFileKey: number | null;
schedule: Date | null;
sensitive: boolean;
spoiler_text: string;
spoilerTextMap: ImmutableMap<Language, string>;
suggestions: ImmutableList<string>;
suggestion_token: string | null;
tagHistory: ImmutableList<string>;
text: string;
textMap: ImmutableMap<Language, string>;
to: ImmutableOrderedSet<string>;
parent_reblogged_by: string | null;
dismissed_quotes: ImmutableOrderedSet<string>;
language: Language | null;
modified_language: Language | null;
suggested_language: string | null;
federated: boolean;
approvalRequired: boolean;
}
const newCompose = (params: Partial<Compose> = {}): 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<Language, string | null>(),
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<MediaAttachment>(),
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<Language, string>(),
suggestions: ImmutableList<string>(),
suggestion_token: null as string | null,
suggestion_token: null,
tagHistory: ImmutableList<string>(),
text: '',
textMap: ImmutableMap<Language, string>(),
to: ImmutableOrderedSet<string>(),
parent_reblogged_by: null as string | null,
parent_reblogged_by: null,
dismissed_quotes: ImmutableOrderedSet<string>(),
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<string, Compose>;
type Compose = ReturnType<typeof ReducerCompose>;
type Poll = ReturnType<typeof PollRecord>;
type State = {
default: Compose;
[key: string]: Compose;
};
const statusToTextMentions = (status: Pick<Status, 'account' | 'mentions'>, account: Pick<Account, 'acct'>) => {
const author = status.account.acct;
@ -160,53 +213,47 @@ const statusToMentionsAccountIdsArray = (status: Pick<Status, 'mentions' | 'acco
const appendMedia = (compose: Compose, media: MediaAttachment, defaultSensitive?: boolean) => {
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<string | number>) =>
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<Status, 'content' | 'menti
const importAccount = (compose: Compose, account: CredentialAccount) => {
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<string>();
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<string>([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<string>([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<string>));
return updateCompose(state, action.composeId, compose => {
compose.tagHistory = ImmutableList(fromJS(action.tags)) as ImmutableList<string>;
});
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<string>();
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<string>).push(action.title))
.updateIn(['poll', 'options_map'], options_map => (options_map as ImmutableList<ImmutableMap<Language, string>>).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<string>).delete(action.index))
.updateIn(['poll', 'options_map'], options_map => (options_map as ImmutableList<ImmutableMap<Language, string>>).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,