Make Compose reducer type-safe

This commit is contained in:
Alex Gleason 2023-06-28 17:53:17 -05:00
parent 118cbd5994
commit de0b05d691
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
7 changed files with 357 additions and 189 deletions

View File

@ -31,61 +31,60 @@ const { CancelToken, isCancel } = axios;
let cancelFetchComposeSuggestionsAccounts: Canceler; let cancelFetchComposeSuggestionsAccounts: Canceler;
const COMPOSE_CHANGE = 'COMPOSE_CHANGE'; const COMPOSE_CHANGE = 'COMPOSE_CHANGE' as const;
const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST'; const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST' as const;
const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS'; const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS' as const;
const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL'; const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL' as const;
const COMPOSE_REPLY = 'COMPOSE_REPLY'; const COMPOSE_REPLY = 'COMPOSE_REPLY' as const;
const COMPOSE_EVENT_REPLY = 'COMPOSE_EVENT_REPLY'; const COMPOSE_EVENT_REPLY = 'COMPOSE_EVENT_REPLY' as const;
const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL'; const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL' as const;
const COMPOSE_QUOTE = 'COMPOSE_QUOTE'; const COMPOSE_QUOTE = 'COMPOSE_QUOTE' as const;
const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL'; const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL' as const;
const COMPOSE_DIRECT = 'COMPOSE_DIRECT'; const COMPOSE_DIRECT = 'COMPOSE_DIRECT' as const;
const COMPOSE_MENTION = 'COMPOSE_MENTION'; const COMPOSE_MENTION = 'COMPOSE_MENTION' as const;
const COMPOSE_RESET = 'COMPOSE_RESET'; const COMPOSE_RESET = 'COMPOSE_RESET' as const;
const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST'; const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST' as const;
const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS'; const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS' as const;
const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL'; const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL' as const;
const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS'; const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS' as const;
const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO'; const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO' as const;
const COMPOSE_GROUP_POST = 'COMPOSE_GROUP_POST'; const COMPOSE_GROUP_POST = 'COMPOSE_GROUP_POST' as const;
const COMPOSE_SET_GROUP_TIMELINE_VISIBLE = 'COMPOSE_SET_GROUP_TIMELINE_VISIBLE'; const COMPOSE_SET_GROUP_TIMELINE_VISIBLE = 'COMPOSE_SET_GROUP_TIMELINE_VISIBLE' as const;
const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR'; const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR' as const;
const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY'; const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY' as const;
const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT'; const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT' as const;
const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE'; const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE' as const;
const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE'; const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE' as const;
const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE'; const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE' as const;
const COMPOSE_TYPE_CHANGE = 'COMPOSE_TYPE_CHANGE'; const COMPOSE_TYPE_CHANGE = 'COMPOSE_TYPE_CHANGE' as const;
const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE'; const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE' as const;
const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE'; const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE' as const;
const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE'; const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE' as const;
const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT'; const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT' as const;
const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST'; const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST' as const;
const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS'; const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS' as const;
const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL'; const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL' as const;
const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD'; const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD' as const;
const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE'; const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE' as const;
const COMPOSE_POLL_OPTION_ADD = 'COMPOSE_POLL_OPTION_ADD'; const COMPOSE_POLL_OPTION_ADD = 'COMPOSE_POLL_OPTION_ADD' as const;
const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE'; const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE' as const;
const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE'; const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE' as const;
const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE'; const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE' as const;
const COMPOSE_SCHEDULE_ADD = 'COMPOSE_SCHEDULE_ADD'; const COMPOSE_SCHEDULE_ADD = 'COMPOSE_SCHEDULE_ADD' as const;
const COMPOSE_SCHEDULE_SET = 'COMPOSE_SCHEDULE_SET'; const COMPOSE_SCHEDULE_SET = 'COMPOSE_SCHEDULE_SET' as const;
const COMPOSE_SCHEDULE_REMOVE = 'COMPOSE_SCHEDULE_REMOVE'; const COMPOSE_SCHEDULE_REMOVE = 'COMPOSE_SCHEDULE_REMOVE' as const;
const COMPOSE_ADD_TO_MENTIONS = 'COMPOSE_ADD_TO_MENTIONS'; const COMPOSE_ADD_TO_MENTIONS = 'COMPOSE_ADD_TO_MENTIONS' as const;
const COMPOSE_REMOVE_FROM_MENTIONS = 'COMPOSE_REMOVE_FROM_MENTIONS'; const COMPOSE_REMOVE_FROM_MENTIONS = 'COMPOSE_REMOVE_FROM_MENTIONS' as const;
const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS'; const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS' as const;
const messages = defineMessages({ const messages = defineMessages({
exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' }, exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' },
@ -101,12 +100,24 @@ const messages = defineMessages({
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
}); });
interface ComposeSetStatusAction {
type: typeof COMPOSE_SET_STATUS
id: string
status: Status
rawText: string
explicitAddressing: boolean
spoilerText?: string
contentType?: string | false
v: ReturnType<typeof parseVersion>
withRedraft?: boolean
}
const setComposeToStatus = (status: Status, rawText: string, spoilerText?: string, contentType?: string | false, withRedraft?: boolean) => const setComposeToStatus = (status: Status, rawText: string, spoilerText?: string, contentType?: string | false, withRedraft?: boolean) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const { instance } = getState(); const { instance } = getState();
const { explicitAddressing } = getFeatures(instance); const { explicitAddressing } = getFeatures(instance);
dispatch({ const action: ComposeSetStatusAction = {
type: COMPOSE_SET_STATUS, type: COMPOSE_SET_STATUS,
id: 'compose-modal', id: 'compose-modal',
status, status,
@ -116,7 +127,9 @@ const setComposeToStatus = (status: Status, rawText: string, spoilerText?: strin
contentType, contentType,
v: parseVersion(instance.version), v: parseVersion(instance.version),
withRedraft, withRedraft,
}); };
dispatch(action);
}; };
const changeCompose = (composeId: string, text: string) => ({ const changeCompose = (composeId: string, text: string) => ({
@ -125,20 +138,29 @@ const changeCompose = (composeId: string, text: string) => ({
text: text, text: text,
}); });
interface ComposeReplyAction {
type: typeof COMPOSE_REPLY
id: string
status: Status
account: Account
explicitAddressing: boolean
}
const replyCompose = (status: Status) => const replyCompose = (status: Status) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState(); const state = getState();
const instance = state.instance; const instance = state.instance;
const { explicitAddressing } = getFeatures(instance); const { explicitAddressing } = getFeatures(instance);
dispatch({ const action: ComposeReplyAction = {
type: COMPOSE_REPLY, type: COMPOSE_REPLY,
id: 'compose-modal', id: 'compose-modal',
status: status, status: status,
account: state.accounts.get(state.me), account: state.accounts.get(state.me)!,
explicitAddressing, explicitAddressing,
}); };
dispatch(action);
dispatch(openModal('COMPOSE')); dispatch(openModal('COMPOSE'));
}; };
@ -147,20 +169,29 @@ const cancelReplyCompose = () => ({
id: 'compose-modal', id: 'compose-modal',
}); });
interface ComposeQuoteAction {
type: typeof COMPOSE_QUOTE
id: string
status: Status
account: Account | undefined
explicitAddressing: boolean
}
const quoteCompose = (status: Status) => const quoteCompose = (status: Status) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState(); const state = getState();
const instance = state.instance; const instance = state.instance;
const { explicitAddressing } = getFeatures(instance); const { explicitAddressing } = getFeatures(instance);
dispatch({ const action: ComposeQuoteAction = {
type: COMPOSE_QUOTE, type: COMPOSE_QUOTE,
id: 'compose-modal', id: 'compose-modal',
status: status, status: status,
account: state.accounts.get(state.me), account: state.accounts.get(state.me),
explicitAddressing, explicitAddressing,
}); };
dispatch(action);
dispatch(openModal('COMPOSE')); dispatch(openModal('COMPOSE'));
}; };
@ -182,38 +213,54 @@ const resetCompose = (composeId = 'compose-modal') => ({
id: composeId, id: composeId,
}); });
interface ComposeMentionAction {
type: typeof COMPOSE_MENTION
id: string
account: Account
}
const mentionCompose = (account: Account) => const mentionCompose = (account: Account) =>
(dispatch: AppDispatch) => { (dispatch: AppDispatch) => {
dispatch({ const action: ComposeMentionAction = {
type: COMPOSE_MENTION, type: COMPOSE_MENTION,
id: 'compose-modal', id: 'compose-modal',
account: account, account: account,
}); };
dispatch(action);
dispatch(openModal('COMPOSE')); dispatch(openModal('COMPOSE'));
}; };
interface ComposeDirectAction {
type: typeof COMPOSE_DIRECT
id: string
account: Account
}
const directCompose = (account: Account) => const directCompose = (account: Account) =>
(dispatch: AppDispatch) => { (dispatch: AppDispatch) => {
dispatch({ const action: ComposeDirectAction = {
type: COMPOSE_DIRECT, type: COMPOSE_DIRECT,
id: 'compose-modal', id: 'compose-modal',
account: account, account,
}); };
dispatch(action);
dispatch(openModal('COMPOSE')); dispatch(openModal('COMPOSE'));
}; };
const directComposeById = (accountId: string) => const directComposeById = (accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const account = getState().accounts.get(accountId); const account = getState().accounts.get(accountId);
if (!account) return;
dispatch({ const action: ComposeDirectAction = {
type: COMPOSE_DIRECT, type: COMPOSE_DIRECT,
id: 'compose-modal', id: 'compose-modal',
account: account, account,
}); };
dispatch(action);
dispatch(openModal('COMPOSE')); dispatch(openModal('COMPOSE'));
}; };
@ -487,14 +534,11 @@ const undoUploadCompose = (composeId: string, media_id: string) => ({
media_id: media_id, media_id: media_id,
}); });
const groupCompose = (composeId: string, groupId: string) => const groupCompose = (composeId: string, groupId: string) => ({
(dispatch: AppDispatch, getState: () => RootState) => { type: COMPOSE_GROUP_POST,
dispatch({ id: composeId,
type: COMPOSE_GROUP_POST, group_id: groupId,
id: composeId, });
group_id: groupId,
});
};
const setGroupTimelineVisible = (composeId: string, groupTimelineVisible: boolean) => ({ const setGroupTimelineVisible = (composeId: string, groupTimelineVisible: boolean) => ({
type: COMPOSE_SET_GROUP_TIMELINE_VISIBLE, type: COMPOSE_SET_GROUP_TIMELINE_VISIBLE,
@ -564,6 +608,14 @@ const fetchComposeSuggestions = (composeId: string, token: string) =>
} }
}; };
interface ComposeSuggestionsReadyAction {
type: typeof COMPOSE_SUGGESTIONS_READY
id: string
token: string
emojis?: Emoji[]
accounts?: APIEntity[]
}
const readyComposeSuggestionsEmojis = (composeId: string, token: string, emojis: Emoji[]) => ({ const readyComposeSuggestionsEmojis = (composeId: string, token: string, emojis: Emoji[]) => ({
type: COMPOSE_SUGGESTIONS_READY, type: COMPOSE_SUGGESTIONS_READY,
id: composeId, id: composeId,
@ -578,6 +630,15 @@ const readyComposeSuggestionsAccounts = (composeId: string, token: string, accou
accounts, accounts,
}); });
interface ComposeSuggestionSelectAction {
type: typeof COMPOSE_SUGGESTION_SELECT
id: string
position: number
token: string | null
completion: string
path: Array<string | 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: Array<string | number>) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
let completion, startPosition; let completion, startPosition;
@ -595,14 +656,16 @@ const selectComposeSuggestion = (composeId: string, position: number, token: str
startPosition = position; startPosition = position;
} }
dispatch({ const action: ComposeSuggestionSelectAction = {
type: COMPOSE_SUGGESTION_SELECT, type: COMPOSE_SUGGESTION_SELECT,
id: composeId, id: composeId,
position: startPosition, position: startPosition,
token, token,
completion, completion,
path, path,
}); };
dispatch(action);
}; };
const updateSuggestionTags = (composeId: string, token: string, currentTrends: ImmutableList<Tag>) => ({ const updateSuggestionTags = (composeId: string, token: string, currentTrends: ImmutableList<Tag>) => ({
@ -712,7 +775,7 @@ const removePollOption = (composeId: string, index: number) => ({
index, index,
}); });
const changePollSettings = (composeId: string, expiresIn?: string | number, isMultiple?: boolean) => ({ const changePollSettings = (composeId: string, expiresIn?: number, isMultiple?: boolean) => ({
type: COMPOSE_POLL_SETTINGS_CHANGE, type: COMPOSE_POLL_SETTINGS_CHANGE,
id: composeId, id: composeId,
expiresIn, expiresIn,
@ -726,30 +789,54 @@ const openComposeWithText = (composeId: string, text = '') =>
dispatch(changeCompose(composeId, text)); dispatch(changeCompose(composeId, text));
}; };
interface ComposeAddToMentionsAction {
type: typeof COMPOSE_ADD_TO_MENTIONS
id: string
account: string
}
const addToMentions = (composeId: string, accountId: string) => const addToMentions = (composeId: string, accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState(); const state = getState();
const acct = state.accounts.get(accountId)!.acct; const acct = state.accounts.get(accountId)!.acct;
return dispatch({ const action: ComposeAddToMentionsAction = {
type: COMPOSE_ADD_TO_MENTIONS, type: COMPOSE_ADD_TO_MENTIONS,
id: composeId, id: composeId,
account: acct, account: acct,
}); };
return dispatch(action);
}; };
interface ComposeRemoveFromMentionsAction {
type: typeof COMPOSE_REMOVE_FROM_MENTIONS
id: string
account: string
}
const removeFromMentions = (composeId: string, accountId: string) => const removeFromMentions = (composeId: string, accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState(); const state = getState();
const acct = state.accounts.get(accountId)!.acct; const acct = state.accounts.get(accountId)!.acct;
return dispatch({ const action: ComposeRemoveFromMentionsAction = {
type: COMPOSE_REMOVE_FROM_MENTIONS, type: COMPOSE_REMOVE_FROM_MENTIONS,
id: composeId, id: composeId,
account: acct, account: acct,
}); };
return dispatch(action);
}; };
interface ComposeEventReplyAction {
type: typeof COMPOSE_EVENT_REPLY
id: string
status: Status
account: Account
explicitAddressing: boolean
}
const eventDiscussionCompose = (composeId: string, status: Status) => const eventDiscussionCompose = (composeId: string, status: Status) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState(); const state = getState();
@ -765,6 +852,52 @@ const eventDiscussionCompose = (composeId: string, status: Status) =>
}); });
}; };
type ComposeAction =
ComposeSetStatusAction
| ReturnType<typeof changeCompose>
| ComposeReplyAction
| ReturnType<typeof cancelReplyCompose>
| ComposeQuoteAction
| ReturnType<typeof cancelQuoteCompose>
| ReturnType<typeof resetCompose>
| ComposeMentionAction
| ComposeDirectAction
| ReturnType<typeof submitComposeRequest>
| ReturnType<typeof submitComposeSuccess>
| ReturnType<typeof submitComposeFail>
| ReturnType<typeof changeUploadComposeRequest>
| ReturnType<typeof changeUploadComposeSuccess>
| ReturnType<typeof changeUploadComposeFail>
| ReturnType<typeof uploadComposeRequest>
| ReturnType<typeof uploadComposeProgress>
| ReturnType<typeof uploadComposeSuccess>
| ReturnType<typeof uploadComposeFail>
| ReturnType<typeof undoUploadCompose>
| ReturnType<typeof groupCompose>
| ReturnType<typeof setGroupTimelineVisible>
| ReturnType<typeof clearComposeSuggestions>
| ComposeSuggestionsReadyAction
| ComposeSuggestionSelectAction
| ReturnType<typeof updateSuggestionTags>
| ReturnType<typeof updateTagHistory>
| ReturnType<typeof changeComposeSpoilerness>
| ReturnType<typeof changeComposeContentType>
| ReturnType<typeof changeComposeSpoilerText>
| ReturnType<typeof changeComposeVisibility>
| ReturnType<typeof insertEmojiCompose>
| ReturnType<typeof addPoll>
| ReturnType<typeof removePoll>
| ReturnType<typeof addSchedule>
| ReturnType<typeof setSchedule>
| ReturnType<typeof removeSchedule>
| ReturnType<typeof addPollOption>
| ReturnType<typeof changePollOption>
| ReturnType<typeof removePollOption>
| ReturnType<typeof changePollSettings>
| ComposeAddToMentionsAction
| ComposeRemoveFromMentionsAction
| ComposeEventReplyAction
export { export {
COMPOSE_CHANGE, COMPOSE_CHANGE,
COMPOSE_SUBMIT_REQUEST, COMPOSE_SUBMIT_REQUEST,
@ -794,7 +927,6 @@ export {
COMPOSE_SPOILER_TEXT_CHANGE, COMPOSE_SPOILER_TEXT_CHANGE,
COMPOSE_VISIBILITY_CHANGE, COMPOSE_VISIBILITY_CHANGE,
COMPOSE_LISTABILITY_CHANGE, COMPOSE_LISTABILITY_CHANGE,
COMPOSE_COMPOSING_CHANGE,
COMPOSE_EMOJI_INSERT, COMPOSE_EMOJI_INSERT,
COMPOSE_UPLOAD_CHANGE_REQUEST, COMPOSE_UPLOAD_CHANGE_REQUEST,
COMPOSE_UPLOAD_CHANGE_SUCCESS, COMPOSE_UPLOAD_CHANGE_SUCCESS,
@ -865,4 +997,5 @@ export {
addToMentions, addToMentions,
removeFromMentions, removeFromMentions,
eventDiscussionCompose, eventDiscussionCompose,
type ComposeAction,
}; };

View File

@ -10,14 +10,14 @@ import type { AxiosError, RawAxiosRequestHeaders } from 'axios';
import type { AppDispatch, RootState } from 'soapbox/store'; import type { AppDispatch, RootState } from 'soapbox/store';
import type { APIEntity } from 'soapbox/types/entities'; import type { APIEntity } from 'soapbox/types/entities';
const ME_FETCH_REQUEST = 'ME_FETCH_REQUEST'; const ME_FETCH_REQUEST = 'ME_FETCH_REQUEST' as const;
const ME_FETCH_SUCCESS = 'ME_FETCH_SUCCESS'; const ME_FETCH_SUCCESS = 'ME_FETCH_SUCCESS' as const;
const ME_FETCH_FAIL = 'ME_FETCH_FAIL'; const ME_FETCH_FAIL = 'ME_FETCH_FAIL' as const;
const ME_FETCH_SKIP = 'ME_FETCH_SKIP'; const ME_FETCH_SKIP = 'ME_FETCH_SKIP' as const;
const ME_PATCH_REQUEST = 'ME_PATCH_REQUEST'; const ME_PATCH_REQUEST = 'ME_PATCH_REQUEST' as const;
const ME_PATCH_SUCCESS = 'ME_PATCH_SUCCESS'; const ME_PATCH_SUCCESS = 'ME_PATCH_SUCCESS' as const;
const ME_PATCH_FAIL = 'ME_PATCH_FAIL'; const ME_PATCH_FAIL = 'ME_PATCH_FAIL' as const;
const noOp = () => new Promise(f => f(undefined)); const noOp = () => new Promise(f => f(undefined));
@ -85,13 +85,10 @@ const fetchMeRequest = () => ({
type: ME_FETCH_REQUEST, type: ME_FETCH_REQUEST,
}); });
const fetchMeSuccess = (me: APIEntity) => const fetchMeSuccess = (me: APIEntity) => ({
(dispatch: AppDispatch) => { type: ME_FETCH_SUCCESS,
dispatch({ me,
type: ME_FETCH_SUCCESS, });
me,
});
};
const fetchMeFail = (error: APIEntity) => ({ const fetchMeFail = (error: APIEntity) => ({
type: ME_FETCH_FAIL, type: ME_FETCH_FAIL,
@ -103,13 +100,20 @@ const patchMeRequest = () => ({
type: ME_PATCH_REQUEST, type: ME_PATCH_REQUEST,
}); });
interface MePatchSuccessAction {
type: typeof ME_PATCH_SUCCESS
me: APIEntity
}
const patchMeSuccess = (me: APIEntity) => const patchMeSuccess = (me: APIEntity) =>
(dispatch: AppDispatch) => { (dispatch: AppDispatch) => {
dispatch(importFetchedAccount(me)); const action: MePatchSuccessAction = {
dispatch({
type: ME_PATCH_SUCCESS, type: ME_PATCH_SUCCESS,
me, me,
}); };
dispatch(importFetchedAccount(me));
dispatch(action);
}; };
const patchMeFail = (error: AxiosError) => ({ const patchMeFail = (error: AxiosError) => ({
@ -118,6 +122,14 @@ const patchMeFail = (error: AxiosError) => ({
skipAlert: true, skipAlert: true,
}); });
type MeAction =
| ReturnType<typeof fetchMeRequest>
| ReturnType<typeof fetchMeSuccess>
| ReturnType<typeof fetchMeFail>
| ReturnType<typeof patchMeRequest>
| MePatchSuccessAction
| ReturnType<typeof patchMeFail>;
export { export {
ME_FETCH_REQUEST, ME_FETCH_REQUEST,
ME_FETCH_SUCCESS, ME_FETCH_SUCCESS,
@ -134,4 +146,5 @@ export {
patchMeRequest, patchMeRequest,
patchMeSuccess, patchMeSuccess,
patchMeFail, patchMeFail,
type MeAction,
}; };

View File

@ -10,9 +10,9 @@ import { isLoggedIn } from 'soapbox/utils/auth';
import type { AppDispatch, RootState } from 'soapbox/store'; import type { AppDispatch, RootState } from 'soapbox/store';
const SETTING_CHANGE = 'SETTING_CHANGE'; const SETTING_CHANGE = 'SETTING_CHANGE' as const;
const SETTING_SAVE = 'SETTING_SAVE'; const SETTING_SAVE = 'SETTING_SAVE' as const;
const SETTINGS_UPDATE = 'SETTINGS_UPDATE'; const SETTINGS_UPDATE = 'SETTINGS_UPDATE' as const;
const FE_NAME = 'soapbox_fe'; const FE_NAME = 'soapbox_fe';
@ -181,25 +181,33 @@ const getSettings = createSelector([
.mergeDeep(settings); .mergeDeep(settings);
}); });
interface SettingChangeAction {
type: typeof SETTING_CHANGE
path: string[]
value: any
}
const changeSettingImmediate = (path: string[], value: any, opts?: SettingOpts) => const changeSettingImmediate = (path: string[], value: any, opts?: SettingOpts) =>
(dispatch: AppDispatch) => { (dispatch: AppDispatch) => {
dispatch({ const action: SettingChangeAction = {
type: SETTING_CHANGE, type: SETTING_CHANGE,
path, path,
value, value,
}); };
dispatch(action);
dispatch(saveSettingsImmediate(opts)); dispatch(saveSettingsImmediate(opts));
}; };
const changeSetting = (path: string[], value: any, opts?: SettingOpts) => const changeSetting = (path: string[], value: any, opts?: SettingOpts) =>
(dispatch: AppDispatch) => { (dispatch: AppDispatch) => {
dispatch({ const action: SettingChangeAction = {
type: SETTING_CHANGE, type: SETTING_CHANGE,
path, path,
value, value,
}); };
dispatch(action);
return dispatch(saveSettings(opts)); return dispatch(saveSettings(opts));
}; };
@ -236,6 +244,10 @@ const getLocale = (state: RootState, fallback = 'en') => {
return Object.keys(messages).includes(localeWithVariant) ? localeWithVariant : Object.keys(messages).includes(locale) ? locale : fallback; return Object.keys(messages).includes(localeWithVariant) ? localeWithVariant : Object.keys(messages).includes(locale) ? locale : fallback;
}; };
type SettingsAction =
| SettingChangeAction
| { type: typeof SETTING_SAVE }
export { export {
SETTING_CHANGE, SETTING_CHANGE,
SETTING_SAVE, SETTING_SAVE,
@ -248,4 +260,5 @@ export {
saveSettingsImmediate, saveSettingsImmediate,
saveSettings, saveSettings,
getLocale, getLocale,
type SettingsAction,
}; };

View File

@ -13,23 +13,23 @@ import type { AxiosError } from 'axios';
import type { AppDispatch, RootState } from 'soapbox/store'; import type { AppDispatch, RootState } from 'soapbox/store';
import type { APIEntity, Status } from 'soapbox/types/entities'; import type { APIEntity, Status } from 'soapbox/types/entities';
const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; const TIMELINE_UPDATE = 'TIMELINE_UPDATE' as const;
const TIMELINE_DELETE = 'TIMELINE_DELETE'; const TIMELINE_DELETE = 'TIMELINE_DELETE' as const;
const TIMELINE_CLEAR = 'TIMELINE_CLEAR'; const TIMELINE_CLEAR = 'TIMELINE_CLEAR' as const;
const TIMELINE_UPDATE_QUEUE = 'TIMELINE_UPDATE_QUEUE'; const TIMELINE_UPDATE_QUEUE = 'TIMELINE_UPDATE_QUEUE' as const;
const TIMELINE_DEQUEUE = 'TIMELINE_DEQUEUE'; const TIMELINE_DEQUEUE = 'TIMELINE_DEQUEUE' as const;
const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP' as const;
const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST' as const;
const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS'; const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS' as const;
const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL' as const;
const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; const TIMELINE_CONNECT = 'TIMELINE_CONNECT' as const;
const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT' as const;
const TIMELINE_REPLACE = 'TIMELINE_REPLACE'; const TIMELINE_REPLACE = 'TIMELINE_REPLACE' as const;
const TIMELINE_INSERT = 'TIMELINE_INSERT'; const TIMELINE_INSERT = 'TIMELINE_INSERT' as const;
const TIMELINE_CLEAR_FEED_ACCOUNT_ID = 'TIMELINE_CLEAR_FEED_ACCOUNT_ID'; const TIMELINE_CLEAR_FEED_ACCOUNT_ID = 'TIMELINE_CLEAR_FEED_ACCOUNT_ID' as const;
const MAX_QUEUED_ITEMS = 40; const MAX_QUEUED_ITEMS = 40;
@ -111,19 +111,29 @@ const dequeueTimeline = (timelineId: string, expandFunc?: (lastStatusId: string)
} }
}; };
interface TimelineDeleteAction {
type: typeof TIMELINE_DELETE
id: string
accountId: string
references: ImmutableMap<string, readonly [statusId: string, accountId: string]>
reblogOf: unknown
}
const deleteFromTimelines = (id: string) => const deleteFromTimelines = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const accountId = getState().statuses.get(id)?.account; const accountId = getState().statuses.get(id)?.account?.id!;
const references = getState().statuses.filter(status => status.reblog === id).map(status => [status.id, status.account]); const references = getState().statuses.filter(status => status.reblog === id).map(status => [status.id, status.account.id] as const);
const reblogOf = getState().statuses.getIn([id, 'reblog'], null); const reblogOf = getState().statuses.getIn([id, 'reblog'], null);
dispatch({ const action: TimelineDeleteAction = {
type: TIMELINE_DELETE, type: TIMELINE_DELETE,
id, id,
accountId, accountId,
references, references,
reblogOf, reblogOf,
}); };
dispatch(action);
}; };
const clearTimeline = (timeline: string) => const clearTimeline = (timeline: string) =>
@ -327,6 +337,9 @@ const clearFeedAccountId = () => (dispatch: AppDispatch, _getState: () => RootSt
dispatch({ type: TIMELINE_CLEAR_FEED_ACCOUNT_ID }); dispatch({ type: TIMELINE_CLEAR_FEED_ACCOUNT_ID });
}; };
// TODO: other actions
type TimelineAction = TimelineDeleteAction;
export { export {
TIMELINE_UPDATE, TIMELINE_UPDATE,
TIMELINE_DELETE, TIMELINE_DELETE,
@ -373,4 +386,5 @@ export {
scrollTopTimeline, scrollTopTimeline,
insertSuggestionsIntoTimeline, insertSuggestionsIntoTimeline,
clearFeedAccountId, clearFeedAccountId,
type TimelineAction,
}; };

View File

@ -126,10 +126,10 @@ const PollForm: React.FC<IPollForm> = ({ composeId }) => {
const onRemoveOption = (index: number) => dispatch(removePollOption(composeId, index)); const onRemoveOption = (index: number) => dispatch(removePollOption(composeId, index));
const onChangeOption = (index: number, title: string) => dispatch(changePollOption(composeId, index, title)); const onChangeOption = (index: number, title: string) => dispatch(changePollOption(composeId, index, title));
const handleAddOption = () => dispatch(addPollOption(composeId, '')); const handleAddOption = () => dispatch(addPollOption(composeId, ''));
const onChangeSettings = (expiresIn: string | number | undefined, isMultiple?: boolean) => const onChangeSettings = (expiresIn: number, isMultiple?: boolean) =>
dispatch(changePollSettings(composeId, expiresIn, isMultiple)); dispatch(changePollSettings(composeId, expiresIn, isMultiple));
const handleSelectDuration = (value: number) => onChangeSettings(value, isMultiple); const handleSelectDuration = (value: number) => onChangeSettings(value, isMultiple);
const handleToggleMultiple = () => onChangeSettings(expiresIn, !isMultiple); const handleToggleMultiple = () => onChangeSettings(Number(expiresIn), !isMultiple);
const onRemovePoll = () => dispatch(removePoll(composeId)); const onRemovePoll = () => dispatch(removePoll(composeId));
if (!options) { if (!options) {

View File

@ -48,7 +48,7 @@ describe('compose reducer', () => {
withRedraft: true, withRedraft: true,
}; };
const result = reducer(undefined, action); const result = reducer(undefined, action as any);
expect(result.get('compose-modal')!.media_attachments.isEmpty()).toBe(true); expect(result.get('compose-modal')!.media_attachments.isEmpty()).toBe(true);
}); });
@ -59,7 +59,7 @@ describe('compose reducer', () => {
status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))), status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))),
}; };
const result = reducer(undefined, action); const result = reducer(undefined, action as any);
expect(result.get('compose-modal')!.media_attachments.getIn([0, 'id'])).toEqual('508107650'); expect(result.get('compose-modal')!.media_attachments.getIn([0, 'id'])).toEqual('508107650');
}); });
@ -71,7 +71,7 @@ describe('compose reducer', () => {
status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))), status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))),
}; };
const result = reducer(undefined, action); const result = reducer(undefined, action as any);
expect(result.get('compose-modal')!.id).toEqual('AHU2RrX0wdcwzCYjFQ'); expect(result.get('compose-modal')!.id).toEqual('AHU2RrX0wdcwzCYjFQ');
}); });
@ -83,7 +83,7 @@ describe('compose reducer', () => {
status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))), status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))),
}; };
const result = reducer(undefined, action); const result = reducer(undefined, action as any);
expect(result.get('compose-modal')!.id).toEqual(null); expect(result.get('compose-modal')!.id).toEqual(null);
}); });
}); });
@ -95,7 +95,7 @@ describe('compose reducer', () => {
status: ImmutableRecord({})(), status: ImmutableRecord({})(),
account: ImmutableRecord({})(), account: ImmutableRecord({})(),
}; };
expect(reducer(undefined, action).toJS()['compose-modal']).toMatchObject({ privacy: 'public' }); expect(reducer(undefined, action as any).toJS()['compose-modal']).toMatchObject({ privacy: 'public' });
}); });
it('uses \'direct\' scope when replying to a DM', () => { it('uses \'direct\' scope when replying to a DM', () => {
@ -106,7 +106,7 @@ describe('compose reducer', () => {
status: ImmutableRecord({ visibility: 'direct' })(), status: ImmutableRecord({ visibility: 'direct' })(),
account: ImmutableRecord({})(), account: ImmutableRecord({})(),
}; };
expect(reducer(state as any, action).toJS()['compose-modal']).toMatchObject({ privacy: 'direct' }); expect(reducer(state as any, action as any).toJS()['compose-modal']).toMatchObject({ privacy: 'direct' });
}); });
it('uses \'private\' scope when replying to a private post', () => { it('uses \'private\' scope when replying to a private post', () => {
@ -117,7 +117,7 @@ describe('compose reducer', () => {
status: ImmutableRecord({ visibility: 'private' })(), status: ImmutableRecord({ visibility: 'private' })(),
account: ImmutableRecord({})(), account: ImmutableRecord({})(),
}; };
expect(reducer(state as any, action).toJS()['compose-modal']).toMatchObject({ privacy: 'private' }); expect(reducer(state as any, action as any).toJS()['compose-modal']).toMatchObject({ privacy: 'private' });
}); });
it('uses \'unlisted\' scope when replying to an unlisted post', () => { it('uses \'unlisted\' scope when replying to an unlisted post', () => {
@ -128,7 +128,7 @@ describe('compose reducer', () => {
status: ImmutableRecord({ visibility: 'unlisted' })(), status: ImmutableRecord({ visibility: 'unlisted' })(),
account: ImmutableRecord({})(), account: ImmutableRecord({})(),
}; };
expect(reducer(state, action).toJS()['compose-modal']).toMatchObject({ privacy: 'unlisted' }); expect(reducer(state, action as any).toJS()['compose-modal']).toMatchObject({ privacy: 'unlisted' });
}); });
it('uses \'private\' scope when set as preference and replying to a public post', () => { it('uses \'private\' scope when set as preference and replying to a public post', () => {
@ -139,7 +139,7 @@ describe('compose reducer', () => {
status: ImmutableRecord({ visibility: 'public' })(), status: ImmutableRecord({ visibility: 'public' })(),
account: ImmutableRecord({})(), account: ImmutableRecord({})(),
}; };
expect(reducer(state, action).toJS()['compose-modal']).toMatchObject({ privacy: 'private' }); expect(reducer(state, action as any).toJS()['compose-modal']).toMatchObject({ privacy: 'private' });
}); });
it('uses \'unlisted\' scope when set as preference and replying to a public post', () => { it('uses \'unlisted\' scope when set as preference and replying to a public post', () => {
@ -150,7 +150,7 @@ describe('compose reducer', () => {
status: ImmutableRecord({ visibility: 'public' })(), status: ImmutableRecord({ visibility: 'public' })(),
account: ImmutableRecord({})(), account: ImmutableRecord({})(),
}; };
expect(reducer(state, action).toJS()['compose-modal']).toMatchObject({ privacy: 'unlisted' }); expect(reducer(state, action as any).toJS()['compose-modal']).toMatchObject({ privacy: 'unlisted' });
}); });
it('sets preferred scope on user login', () => { it('sets preferred scope on user login', () => {
@ -238,18 +238,6 @@ describe('compose reducer', () => {
}); });
}); });
it('should handle COMPOSE_COMPOSING_CHANGE', () => {
const state = initialState.set('home', ReducerCompose({ is_composing: true }));
const action = {
type: actions.COMPOSE_COMPOSING_CHANGE,
id: 'home',
value: false,
};
expect(reducer(state, action).toJS().home).toMatchObject({
is_composing: false,
});
});
it('should handle COMPOSE_SUBMIT_REQUEST', () => { it('should handle COMPOSE_SUBMIT_REQUEST', () => {
const state = initialState.set('home', ReducerCompose({ is_submitting: false })); const state = initialState.set('home', ReducerCompose({ is_submitting: false }));
const action = { const action = {
@ -267,7 +255,7 @@ describe('compose reducer', () => {
type: actions.COMPOSE_UPLOAD_CHANGE_REQUEST, type: actions.COMPOSE_UPLOAD_CHANGE_REQUEST,
id: 'home', id: 'home',
}; };
expect(reducer(state, action).toJS().home).toMatchObject({ expect(reducer(state, action as any).toJS().home).toMatchObject({
is_changing_upload: true, is_changing_upload: true,
}); });
}); });
@ -278,7 +266,7 @@ describe('compose reducer', () => {
type: actions.COMPOSE_SUBMIT_SUCCESS, type: actions.COMPOSE_SUBMIT_SUCCESS,
id: 'home', id: 'home',
}; };
expect(reducer(state, action).toJS().home).toMatchObject({ expect(reducer(state, action as any).toJS().home).toMatchObject({
privacy: 'public', privacy: 'public',
}); });
}); });
@ -289,7 +277,7 @@ describe('compose reducer', () => {
type: actions.COMPOSE_SUBMIT_FAIL, type: actions.COMPOSE_SUBMIT_FAIL,
id: 'home', id: 'home',
}; };
expect(reducer(state, action).toJS().home).toMatchObject({ expect(reducer(state, action as any).toJS().home).toMatchObject({
is_submitting: false, is_submitting: false,
}); });
}); });
@ -300,7 +288,7 @@ describe('compose reducer', () => {
type: actions.COMPOSE_UPLOAD_CHANGE_FAIL, type: actions.COMPOSE_UPLOAD_CHANGE_FAIL,
composeId: 'home', composeId: 'home',
}; };
expect(reducer(state, action).toJS().home).toMatchObject({ expect(reducer(state, action as any).toJS().home).toMatchObject({
is_changing_upload: false, is_changing_upload: false,
}); });
}); });
@ -311,7 +299,7 @@ describe('compose reducer', () => {
type: actions.COMPOSE_UPLOAD_REQUEST, type: actions.COMPOSE_UPLOAD_REQUEST,
id: 'home', id: 'home',
}; };
expect(reducer(state, action).toJS().home).toMatchObject({ expect(reducer(state, action as any).toJS().home).toMatchObject({
is_uploading: true, is_uploading: true,
}); });
}); });
@ -338,7 +326,7 @@ describe('compose reducer', () => {
media: media, media: media,
skipLoading: true, skipLoading: true,
}; };
expect(reducer(state, action).toJS().home).toMatchObject({ expect(reducer(state, action as any).toJS().home).toMatchObject({
is_uploading: false, is_uploading: false,
}); });
}); });
@ -349,7 +337,7 @@ describe('compose reducer', () => {
type: actions.COMPOSE_UPLOAD_FAIL, type: actions.COMPOSE_UPLOAD_FAIL,
id: 'home', id: 'home',
}; };
expect(reducer(state, action).toJS().home).toMatchObject({ expect(reducer(state, action as any).toJS().home).toMatchObject({
is_uploading: false, is_uploading: false,
}); });
}); });
@ -414,7 +402,7 @@ describe('compose reducer', () => {
type: TIMELINE_DELETE, type: TIMELINE_DELETE,
id: '9wk6pmImMrZjgrK7iC', id: '9wk6pmImMrZjgrK7iC',
}; };
expect(reducer(state, action).toJS()['compose-modal']).toMatchObject({ expect(reducer(state, action as any).toJS()['compose-modal']).toMatchObject({
in_reply_to: null, in_reply_to: null,
}); });
}); });

View File

@ -2,6 +2,7 @@ import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrde
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { isNativeEmoji } from 'soapbox/features/emoji'; import { isNativeEmoji } from 'soapbox/features/emoji';
import { Account } from 'soapbox/schemas';
import { tagHistory } from 'soapbox/settings'; import { tagHistory } from 'soapbox/settings';
import { PLEROMA } from 'soapbox/utils/features'; import { PLEROMA } from 'soapbox/utils/features';
import { hasIntegerMediaIds } from 'soapbox/utils/status'; import { hasIntegerMediaIds } from 'soapbox/utils/status';
@ -32,7 +33,6 @@ import {
COMPOSE_TYPE_CHANGE, COMPOSE_TYPE_CHANGE,
COMPOSE_SPOILER_TEXT_CHANGE, COMPOSE_SPOILER_TEXT_CHANGE,
COMPOSE_VISIBILITY_CHANGE, COMPOSE_VISIBILITY_CHANGE,
COMPOSE_COMPOSING_CHANGE,
COMPOSE_EMOJI_INSERT, COMPOSE_EMOJI_INSERT,
COMPOSE_UPLOAD_CHANGE_REQUEST, COMPOSE_UPLOAD_CHANGE_REQUEST,
COMPOSE_UPLOAD_CHANGE_SUCCESS, COMPOSE_UPLOAD_CHANGE_SUCCESS,
@ -52,19 +52,19 @@ import {
COMPOSE_SET_STATUS, COMPOSE_SET_STATUS,
COMPOSE_EVENT_REPLY, COMPOSE_EVENT_REPLY,
COMPOSE_SET_GROUP_TIMELINE_VISIBLE, COMPOSE_SET_GROUP_TIMELINE_VISIBLE,
ComposeAction,
} from '../actions/compose'; } from '../actions/compose';
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from '../actions/me'; import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS, MeAction } from '../actions/me';
import { SETTING_CHANGE, FE_NAME } from '../actions/settings'; import { SETTING_CHANGE, FE_NAME, SettingsAction } from '../actions/settings';
import { TIMELINE_DELETE } from '../actions/timelines'; import { TIMELINE_DELETE, TimelineAction } from '../actions/timelines';
import { normalizeAttachment } from '../normalizers/attachment'; import { normalizeAttachment } from '../normalizers/attachment';
import { unescapeHTML } from '../utils/html'; import { unescapeHTML } from '../utils/html';
import type { AnyAction } from 'redux';
import type { Emoji } from 'soapbox/features/emoji'; import type { Emoji } from 'soapbox/features/emoji';
import type { import type {
Account as AccountEntity,
APIEntity, APIEntity,
Attachment as AttachmentEntity, Attachment as AttachmentEntity,
Status,
Status as StatusEntity, Status as StatusEntity,
Tag, Tag,
} from 'soapbox/types/entities'; } from 'soapbox/types/entities';
@ -111,9 +111,9 @@ type State = ImmutableMap<string, Compose>;
type Compose = ReturnType<typeof ReducerCompose>; type Compose = ReturnType<typeof ReducerCompose>;
type Poll = ReturnType<typeof PollRecord>; type Poll = ReturnType<typeof PollRecord>;
const statusToTextMentions = (status: ImmutableMap<string, any>, account: AccountEntity) => { const statusToTextMentions = (status: Status, account: Account) => {
const author = status.getIn(['account', 'acct']); const author = status.getIn(['account', 'acct']);
const mentions = status.get('mentions')?.map((m: ImmutableMap<string, any>) => m.get('acct')) || []; const mentions = status.get('mentions')?.map((m) => m.acct) || [];
return ImmutableOrderedSet([author]) return ImmutableOrderedSet([author])
.concat(mentions) .concat(mentions)
@ -122,22 +122,21 @@ const statusToTextMentions = (status: ImmutableMap<string, any>, account: Accoun
.join(''); .join('');
}; };
export const statusToMentionsArray = (status: ImmutableMap<string, any>, account: AccountEntity) => { export const statusToMentionsArray = (status: Status, account: Account) => {
const author = status.getIn(['account', 'acct']) as string; const author = status.getIn(['account', 'acct']) as string;
const mentions = status.get('mentions')?.map((m: ImmutableMap<string, any>) => m.get('acct')) || []; const mentions = status.get('mentions')?.map((m) => m.acct) || [];
return ImmutableOrderedSet<string>([author]) return ImmutableOrderedSet<string>([author])
.concat(mentions) .concat(mentions)
.delete(account.acct) as ImmutableOrderedSet<string>; .delete(account.acct) as ImmutableOrderedSet<string>;
}; };
export const statusToMentionsAccountIdsArray = (status: StatusEntity, account: AccountEntity) => { export const statusToMentionsAccountIdsArray = (status: StatusEntity, account: Account) => {
const author = (status.account as AccountEntity).id;
const mentions = status.mentions.map((m) => m.id); const mentions = status.mentions.map((m) => m.id);
return ImmutableOrderedSet([author]) return ImmutableOrderedSet<string>([account.id])
.concat(mentions) .concat(mentions)
.delete(account.id) as ImmutableOrderedSet<string>; .delete(account.id);
}; };
const appendMedia = (compose: Compose, media: APIEntity, defaultSensitive?: boolean) => { const appendMedia = (compose: Compose, media: APIEntity, defaultSensitive?: boolean) => {
@ -168,9 +167,9 @@ const removeMedia = (compose: Compose, mediaId: string) => {
}); });
}; };
const insertSuggestion = (compose: Compose, position: number, token: string, completion: string, path: Array<string | number>) => { const insertSuggestion = (compose: Compose, position: number, token: string | null, completion: string, path: Array<string | number>) => {
return compose.withMutations(map => { return compose.withMutations(map => {
map.updateIn(path, oldText => `${(oldText as string).slice(0, position)}${completion} ${(oldText as string).slice(position + token.length)}`); map.updateIn(path, oldText => `${(oldText as string).slice(0, position)}${completion} ${(oldText as string).slice(position + (token?.length ?? 0))}`);
map.set('suggestion_token', null); map.set('suggestion_token', null);
map.set('suggestions', ImmutableList()); map.set('suggestions', ImmutableList());
if (path.length === 1 && path[0] === 'text') { if (path.length === 1 && path[0] === 'text') {
@ -216,10 +215,10 @@ const privacyPreference = (a: string, b: string) => {
const domParser = new DOMParser(); const domParser = new DOMParser();
const expandMentions = (status: ImmutableMap<string, any>) => { const expandMentions = (status: Status) => {
const fragment = domParser.parseFromString(status.get('content'), 'text/html').documentElement; const fragment = domParser.parseFromString(status.get('content'), 'text/html').documentElement;
status.get('mentions').forEach((mention: ImmutableMap<string, any>) => { status.get('mentions').forEach((mention) => {
const node = fragment.querySelector(`a[href="${mention.get('url')}"]`); const node = fragment.querySelector(`a[href="${mention.get('url')}"]`);
if (node) node.textContent = `@${mention.get('acct')}`; if (node) node.textContent = `@${mention.get('acct')}`;
}); });
@ -227,13 +226,13 @@ const expandMentions = (status: ImmutableMap<string, any>) => {
return fragment.innerHTML; return fragment.innerHTML;
}; };
const getExplicitMentions = (me: string, status: ImmutableMap<string, any>) => { const getExplicitMentions = (me: string, status: Status) => {
const fragment = domParser.parseFromString(status.get('content'), 'text/html').documentElement; const fragment = domParser.parseFromString(status.content, 'text/html').documentElement;
const mentions = status const mentions = status
.get('mentions') .get('mentions')
.filter((mention: ImmutableMap<string, any>) => !(fragment.querySelector(`a[href="${mention.get('url')}"]`) || mention.get('id') === me)) .filter((mention) => !(fragment.querySelector(`a[href="${mention.url}"]`) || mention.id === me))
.map((m: ImmutableMap<string, any>) => m.get('acct')); .map((m) => m.acct);
return ImmutableOrderedSet<string>(mentions); return ImmutableOrderedSet<string>(mentions);
}; };
@ -274,7 +273,7 @@ export const initialState: State = ImmutableMap({
default: ReducerCompose({ idempotencyKey: uuid(), resetFileKey: getResetFileKey() }), default: ReducerCompose({ idempotencyKey: uuid(), resetFileKey: getResetFileKey() }),
}); });
export default function compose(state = initialState, action: AnyAction) { export default function compose(state = initialState, action: ComposeAction | MeAction | SettingsAction | TimelineAction) {
switch (action.type) { switch (action.type) {
case COMPOSE_TYPE_CHANGE: case COMPOSE_TYPE_CHANGE:
return updateCompose(state, action.id, compose => compose.withMutations(map => { return updateCompose(state, action.id, compose => compose.withMutations(map => {
@ -300,13 +299,11 @@ export default function compose(state = initialState, action: AnyAction) {
return updateCompose(state, action.id, compose => compose return updateCompose(state, action.id, compose => compose
.set('text', action.text) .set('text', action.text)
.set('idempotencyKey', uuid())); .set('idempotencyKey', uuid()));
case COMPOSE_COMPOSING_CHANGE:
return updateCompose(state, action.id, compose => compose.set('is_composing', action.value));
case COMPOSE_REPLY: case COMPOSE_REPLY:
return updateCompose(state, action.id, compose => compose.withMutations(map => { return updateCompose(state, action.id, compose => compose.withMutations(map => {
const defaultCompose = state.get('default')!; const defaultCompose = state.get('default')!;
map.set('group_id', action.status.getIn(['group', 'id']) || action.status.get('group')); map.set('group_id', action.status.getIn(['group', 'id']) as string);
map.set('in_reply_to', action.status.get('id')); map.set('in_reply_to', action.status.get('id'));
map.set('to', action.explicitAddressing ? statusToMentionsArray(action.status, action.account) : ImmutableOrderedSet<string>()); map.set('to', action.explicitAddressing ? statusToMentionsArray(action.status, action.account) : ImmutableOrderedSet<string>());
map.set('text', !action.explicitAddressing ? statusToTextMentions(action.status, action.account) : ''); map.set('text', !action.explicitAddressing ? statusToTextMentions(action.status, action.account) : '');
@ -324,11 +321,11 @@ export default function compose(state = initialState, action: AnyAction) {
})); }));
case COMPOSE_QUOTE: case COMPOSE_QUOTE:
return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => { return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => {
const author = action.status.getIn(['account', 'acct']); const author = action.status.getIn(['account', 'acct']) as string;
const defaultCompose = state.get('default')!; const defaultCompose = state.get('default')!;
map.set('quote', action.status.get('id')); map.set('quote', action.status.get('id'));
map.set('to', ImmutableOrderedSet([author])); map.set('to', ImmutableOrderedSet<string>([author]));
map.set('text', ''); map.set('text', '');
map.set('privacy', privacyPreference(action.status.visibility, defaultCompose.privacy)); map.set('privacy', privacyPreference(action.status.visibility, defaultCompose.privacy));
map.set('focusDate', new Date()); map.set('focusDate', new Date());
@ -342,7 +339,7 @@ export default function compose(state = initialState, action: AnyAction) {
if (action.status.group?.group_visibility === 'everyone') { if (action.status.group?.group_visibility === 'everyone') {
map.set('privacy', privacyPreference('public', defaultCompose.privacy)); map.set('privacy', privacyPreference('public', defaultCompose.privacy));
} else if (action.status.group?.group_visibility === 'members_only') { } else if (action.status.group?.group_visibility === 'members_only') {
map.set('group_id', action.status.getIn(['group', 'id']) || action.status.get('group')); map.set('group_id', action.status.getIn(['group', 'id']) as string);
map.set('privacy', 'group'); map.set('privacy', 'group');
} }
} }
@ -379,14 +376,14 @@ export default function compose(state = initialState, action: AnyAction) {
return updateCompose(state, action.id, compose => compose.set('progress', Math.round((action.loaded / action.total) * 100))); return updateCompose(state, action.id, compose => compose.set('progress', Math.round((action.loaded / action.total) * 100)));
case COMPOSE_MENTION: case COMPOSE_MENTION:
return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => { return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => {
map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' ')); map.update('text', text => [text.trim(), `@${action.account.acct} `].filter((str) => str.length !== 0).join(' '));
map.set('focusDate', new Date()); map.set('focusDate', new Date());
map.set('caretPosition', null); map.set('caretPosition', null);
map.set('idempotencyKey', uuid()); map.set('idempotencyKey', uuid());
})); }));
case COMPOSE_DIRECT: case COMPOSE_DIRECT:
return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => { return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => {
map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' ')); map.update('text', text => [text.trim(), `@${action.account.acct} `].filter((str) => str.length !== 0).join(' '));
map.set('privacy', 'direct'); map.set('privacy', 'direct');
map.set('focusDate', new Date()); map.set('focusDate', new Date());
map.set('caretPosition', null); map.set('caretPosition', null);
@ -435,7 +432,7 @@ export default function compose(state = initialState, action: AnyAction) {
case COMPOSE_SET_STATUS: case COMPOSE_SET_STATUS:
return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => { return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => {
if (!action.withRedraft) { if (!action.withRedraft) {
map.set('id', action.status.get('id')); map.set('id', action.status.id);
} }
map.set('text', action.rawText || unescapeHTML(expandMentions(action.status))); map.set('text', action.rawText || unescapeHTML(expandMentions(action.status)));
map.set('to', action.explicitAddressing ? getExplicitMentions(action.status.account.id, action.status) : ImmutableOrderedSet<string>()); map.set('to', action.explicitAddressing ? getExplicitMentions(action.status.account.id, action.status) : ImmutableOrderedSet<string>());
@ -445,10 +442,10 @@ export default function compose(state = initialState, action: AnyAction) {
map.set('caretPosition', null); map.set('caretPosition', null);
map.set('idempotencyKey', uuid()); map.set('idempotencyKey', uuid());
map.set('content_type', action.contentType || 'text/plain'); map.set('content_type', action.contentType || 'text/plain');
map.set('quote', action.status.get('quote')); map.set('quote', action.status.getIn(['quote', 'id']) as string);
map.set('group_id', action.status.get('group')); map.set('group_id', action.status.getIn(['group', 'id']) as string);
if (action.v?.software === PLEROMA && action.withRedraft && hasIntegerMediaIds(action.status)) { if (action.v?.software === PLEROMA && action.withRedraft && hasIntegerMediaIds(action.status.toJS() as any)) {
map.set('media_attachments', ImmutableList()); map.set('media_attachments', ImmutableList());
} else { } else {
map.set('media_attachments', action.status.media_attachments); map.set('media_attachments', action.status.media_attachments);
@ -462,9 +459,9 @@ export default function compose(state = initialState, action: AnyAction) {
map.set('spoiler_text', ''); map.set('spoiler_text', '');
} }
if (action.status.get('poll')) { if (action.status.poll && typeof action.status.poll === 'object') {
map.set('poll', PollRecord({ map.set('poll', PollRecord({
options: action.status.poll.options.map((x: APIEntity) => x.get('title')), options: ImmutableList(action.status.poll.options.map(({ title }) => title)),
multiple: action.status.poll.multiple, multiple: action.status.poll.multiple,
expires_in: 24 * 3600, expires_in: 24 * 3600,
})); }));
@ -487,7 +484,17 @@ export default function compose(state = initialState, action: AnyAction) {
case COMPOSE_POLL_OPTION_REMOVE: case COMPOSE_POLL_OPTION_REMOVE:
return updateCompose(state, action.id, compose => compose.updateIn(['poll', 'options'], options => (options as ImmutableList<string>).delete(action.index))); return updateCompose(state, action.id, compose => compose.updateIn(['poll', 'options'], options => (options as ImmutableList<string>).delete(action.index)));
case COMPOSE_POLL_SETTINGS_CHANGE: case COMPOSE_POLL_SETTINGS_CHANGE:
return updateCompose(state, action.id, compose => compose.update('poll', poll => poll!.set('expires_in', action.expiresIn).set('multiple', action.isMultiple))); return updateCompose(state, action.id, compose => compose.update('poll', poll => {
if (!poll) return null;
return poll.withMutations((poll) => {
if (action.expiresIn) {
poll.set('expires_in', action.expiresIn);
}
if (typeof action.isMultiple === 'boolean') {
poll.set('multiple', action.isMultiple);
}
});
}));
case COMPOSE_ADD_TO_MENTIONS: 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.update('to', mentions => mentions!.add(action.account)));
case COMPOSE_REMOVE_FROM_MENTIONS: case COMPOSE_REMOVE_FROM_MENTIONS: