From b38e5ec8e33d7b1707fd870eba33deb5232ce2fe Mon Sep 17 00:00:00 2001 From: marcin mikolajczak Date: Wed, 14 Sep 2022 22:05:40 +0200 Subject: [PATCH] tests i can't run locally for now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikolajczak Signed-off-by: marcin mikołajczak --- app/soapbox/actions/__tests__/compose.test.ts | 23 +- .../actions/__tests__/statuses.test.ts | 1 + app/soapbox/actions/compose.ts | 111 +- .../compose/components/compose-form.tsx | 8 +- .../emoji-picker/emoji-picker-dropdown.tsx | 1 - .../compose/components/polls/poll-form.tsx | 6 +- .../compose/components/schedule_button.tsx | 4 +- .../features/compose/components/upload.tsx | 4 +- .../containers/upload_button_container.ts | 2 +- .../features/reply_mentions/account.tsx | 8 +- .../reducers/__tests__/compose.test.ts | 1009 +++++++++-------- app/soapbox/reducers/compose.ts | 10 +- app/soapbox/reducers/index.ts | 1 - 13 files changed, 614 insertions(+), 574 deletions(-) diff --git a/app/soapbox/actions/__tests__/compose.test.ts b/app/soapbox/actions/__tests__/compose.test.ts index 88f8c8858..1579d63c9 100644 --- a/app/soapbox/actions/__tests__/compose.test.ts +++ b/app/soapbox/actions/__tests__/compose.test.ts @@ -2,6 +2,7 @@ import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutabl import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { InstanceRecord } from 'soapbox/normalizers'; +import { ReducerCompose } from 'soapbox/reducers/compose'; import { uploadCompose, submitCompose } from '../compose'; import { STATUS_CREATE_REQUEST } from '../statuses'; @@ -26,7 +27,8 @@ describe('uploadCompose()', () => { const state = rootState .set('me', '1234') - .set('instance', instance); + .set('instance', instance) + .setIn(['compose', 'home'], ReducerCompose()); store = mockStore(state); files = [{ @@ -43,7 +45,7 @@ describe('uploadCompose()', () => { } as unknown as IntlShape; const expectedActions = [ - { type: 'COMPOSE_UPLOAD_REQUEST', skipLoading: true }, + { type: 'COMPOSE_UPLOAD_REQUEST', id: 'home', skipLoading: true }, { type: 'ALERT_SHOW', message: 'Image exceeds the current file size limit (10 Bytes)', @@ -51,10 +53,10 @@ describe('uploadCompose()', () => { actionLink: undefined, severity: 'error', }, - { type: 'COMPOSE_UPLOAD_FAIL', error: true, skipLoading: true }, + { type: 'COMPOSE_UPLOAD_FAIL', id: 'home', error: true, skipLoading: true }, ]; - await store.dispatch(uploadCompose(files, mockIntl)); + await store.dispatch(uploadCompose('home', files, mockIntl)); const actions = store.getActions(); expect(actions).toEqual(expectedActions); @@ -78,7 +80,8 @@ describe('uploadCompose()', () => { const state = rootState .set('me', '1234') - .set('instance', instance); + .set('instance', instance) + .setIn(['compose', 'home'], ReducerCompose()); store = mockStore(state); files = [{ @@ -95,7 +98,7 @@ describe('uploadCompose()', () => { } as unknown as IntlShape; const expectedActions = [ - { type: 'COMPOSE_UPLOAD_REQUEST', skipLoading: true }, + { type: 'COMPOSE_UPLOAD_REQUEST', id: 'home', skipLoading: true }, { type: 'ALERT_SHOW', message: 'Video exceeds the current file size limit (10 Bytes)', @@ -103,10 +106,10 @@ describe('uploadCompose()', () => { actionLink: undefined, severity: 'error', }, - { type: 'COMPOSE_UPLOAD_FAIL', error: true, skipLoading: true }, + { type: 'COMPOSE_UPLOAD_FAIL', id: 'home', error: true, skipLoading: true }, ]; - await store.dispatch(uploadCompose(files, mockIntl)); + await store.dispatch(uploadCompose('home', files, mockIntl)); const actions = store.getActions(); expect(actions).toEqual(expectedActions); @@ -118,10 +121,10 @@ describe('submitCompose()', () => { it('inserts mentions from text', async() => { const state = rootState .set('me', '123') - .setIn(['compose', 'text'], '@alex hello @mkljczk@pl.fediverse.pl @gg@汉语/漢語.com alex@alexgleason.me'); + .setIn(['compose', 'home'], ReducerCompose({ text: '@alex hello @mkljczk@pl.fediverse.pl @gg@汉语/漢語.com alex@alexgleason.me' })); const store = mockStore(state); - await store.dispatch(submitCompose()); + await store.dispatch(submitCompose('home')); const actions = store.getActions(); const statusCreateRequest = actions.find(action => action.type === STATUS_CREATE_REQUEST); diff --git a/app/soapbox/actions/__tests__/statuses.test.ts b/app/soapbox/actions/__tests__/statuses.test.ts index 18cbc173b..68af7608f 100644 --- a/app/soapbox/actions/__tests__/statuses.test.ts +++ b/app/soapbox/actions/__tests__/statuses.test.ts @@ -121,6 +121,7 @@ describe('deleteStatus()', () => { version: '0.0.0', }, withRedraft: true, + id: 'compose-modal', }, { type: 'MODAL_OPEN', modalType: 'COMPOSE', modalProps: undefined }, ]; diff --git a/app/soapbox/actions/compose.ts b/app/soapbox/actions/compose.ts index b9873da82..ab4d47b97 100644 --- a/app/soapbox/actions/compose.ts +++ b/app/soapbox/actions/compose.ts @@ -105,6 +105,7 @@ const setComposeToStatus = (status: Status, rawText: string, spoilerText?: strin dispatch({ type: COMPOSE_SET_STATUS, + id: 'compose-modal', status, rawText, explicitAddressing, @@ -129,6 +130,7 @@ const replyCompose = (status: Status) => dispatch({ type: COMPOSE_REPLY, + id: 'compose-modal', status: status, account: state.accounts.get(state.me), explicitAddressing, @@ -139,6 +141,7 @@ const replyCompose = (status: Status) => const cancelReplyCompose = () => ({ type: COMPOSE_REPLY_CANCEL, + id: 'compose-modal', }); const quoteCompose = (status: Status) => @@ -149,6 +152,7 @@ const quoteCompose = (status: Status) => dispatch({ type: COMPOSE_QUOTE, + id: 'compose-modal', status: status, account: state.accounts.get(state.me), explicitAddressing, @@ -159,16 +163,19 @@ const quoteCompose = (status: Status) => const cancelQuoteCompose = () => ({ type: COMPOSE_QUOTE_CANCEL, + id: 'compose-modal', }); -const resetCompose = () => ({ +const resetCompose = (composeId = 'compose-modal') => ({ type: COMPOSE_RESET, + id: composeId, }); const mentionCompose = (account: Account) => (dispatch: AppDispatch) => { dispatch({ type: COMPOSE_MENTION, + id: 'compose-modal', account: account, }); @@ -179,6 +186,7 @@ const directCompose = (account: Account) => (dispatch: AppDispatch) => { dispatch({ type: COMPOSE_DIRECT, + id: 'compose-modal', account: account, }); @@ -191,6 +199,7 @@ const directComposeById = (accountId: string) => dispatch({ type: COMPOSE_DIRECT, + id: 'compose-modal', account: account, }); @@ -323,7 +332,7 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) => return; } - dispatch(uploadComposeRequest()); + dispatch(uploadComposeRequest(composeId)); Array.from(files).forEach(async(f, i) => { if (media.size + i > attachmentLimit - 1) return; @@ -336,18 +345,18 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) => const limit = formatBytes(maxImageSize); const message = intl.formatMessage(messages.exceededImageSizeLimit, { limit }); dispatch(snackbar.error(message)); - dispatch(uploadComposeFail(true)); + dispatch(uploadComposeFail(composeId, true)); return; } else if (isVideo && maxVideoSize && (f.size > maxVideoSize)) { const limit = formatBytes(maxVideoSize); const message = intl.formatMessage(messages.exceededVideoSizeLimit, { limit }); dispatch(snackbar.error(message)); - dispatch(uploadComposeFail(true)); + dispatch(uploadComposeFail(composeId, true)); return; } else if (isVideo && maxVideoDuration && (videoDurationInSeconds > maxVideoDuration)) { const message = intl.formatMessage(messages.exceededVideoDurationLimit, { limit: maxVideoDuration }); dispatch(snackbar.error(message)); - dispatch(uploadComposeFail(true)); + dispatch(uploadComposeFail(composeId, true)); return; } @@ -361,7 +370,7 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) => const onUploadProgress = ({ loaded }: any) => { progress[i] = loaded; - dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total)); + dispatch(uploadComposeProgress(composeId, progress.reduce((a, v) => a + v, 0), total)); }; return dispatch(uploadMedia(data, onUploadProgress)) @@ -369,98 +378,107 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) => // If server-side processing of the media attachment has not completed yet, // poll the server until it is, before showing the media attachment as uploaded if (status === 200) { - dispatch(uploadComposeSuccess(data, f)); + dispatch(uploadComposeSuccess(composeId, data, f)); } else if (status === 202) { const poll = () => { dispatch(fetchMedia(data.id)).then(({ status, data }) => { if (status === 200) { - dispatch(uploadComposeSuccess(data, f)); + dispatch(uploadComposeSuccess(composeId, data, f)); } else if (status === 206) { setTimeout(() => poll(), 1000); } - }).catch(error => dispatch(uploadComposeFail(error))); + }).catch(error => dispatch(uploadComposeFail(composeId, error))); }; poll(); } }); - }).catch(error => dispatch(uploadComposeFail(error))); + }).catch(error => dispatch(uploadComposeFail(composeId, error))); /* eslint-enable no-loop-func */ }); }; -const changeUploadCompose = (id: string, params: Record) => +const changeUploadCompose = (composeId: string, id: string, params: Record) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - dispatch(changeUploadComposeRequest()); + dispatch(changeUploadComposeRequest(composeId)); dispatch(updateMedia(id, params)).then(response => { - dispatch(changeUploadComposeSuccess(response.data)); + dispatch(changeUploadComposeSuccess(composeId, response.data)); }).catch(error => { - dispatch(changeUploadComposeFail(id, error)); + dispatch(changeUploadComposeFail(composeId, id, error)); }); }; -const changeUploadComposeRequest = () => ({ +const changeUploadComposeRequest = (composeId: string) => ({ type: COMPOSE_UPLOAD_CHANGE_REQUEST, + id: composeId, skipLoading: true, }); -const changeUploadComposeSuccess = (media: APIEntity) => ({ +const changeUploadComposeSuccess = (composeId: string, media: APIEntity) => ({ type: COMPOSE_UPLOAD_CHANGE_SUCCESS, + id: composeId, media: media, skipLoading: true, }); -const changeUploadComposeFail = (id: string, error: AxiosError) => ({ +const changeUploadComposeFail = (composeId: string, id: string, error: AxiosError) => ({ type: COMPOSE_UPLOAD_CHANGE_FAIL, + composeId, id, error: error, skipLoading: true, }); -const uploadComposeRequest = () => ({ +const uploadComposeRequest = (composeId: string) => ({ type: COMPOSE_UPLOAD_REQUEST, + id: composeId, skipLoading: true, }); -const uploadComposeProgress = (loaded: number, total: number) => ({ +const uploadComposeProgress = (composeId: string, loaded: number, total: number) => ({ type: COMPOSE_UPLOAD_PROGRESS, + id: composeId, loaded: loaded, total: total, }); -const uploadComposeSuccess = (media: APIEntity, file: File) => ({ +const uploadComposeSuccess = (composeId: string, media: APIEntity, file: File) => ({ type: COMPOSE_UPLOAD_SUCCESS, + id: composeId, media: media, file, skipLoading: true, }); -const uploadComposeFail = (error: AxiosError | true) => ({ +const uploadComposeFail = (composeId: string, error: AxiosError | true) => ({ type: COMPOSE_UPLOAD_FAIL, + id: composeId, error: error, skipLoading: true, }); -const undoUploadCompose = (media_id: string) => ({ +const undoUploadCompose = (composeId: string, media_id: string) => ({ type: COMPOSE_UPLOAD_UNDO, + id: composeId, media_id: media_id, }); -const clearComposeSuggestions = () => { +const clearComposeSuggestions = (composeId: string) => { if (cancelFetchComposeSuggestionsAccounts) { cancelFetchComposeSuggestionsAccounts(); } return { type: COMPOSE_SUGGESTIONS_CLEAR, + id: composeId, }; }; -const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => { +const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId, token) => { if (cancelFetchComposeSuggestionsAccounts) { - cancelFetchComposeSuggestionsAccounts(); + cancelFetchComposeSuggestionsAccounts(composeId); } api(getState).get('/api/v1/accounts/search', { cancelToken: new CancelToken(cancel => { @@ -473,7 +491,7 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => }, }).then(response => { dispatch(importFetchedAccounts(response.data)); - dispatch(readyComposeSuggestionsAccounts(token, response.data)); + dispatch(readyComposeSuggestionsAccounts(composeId, token, response.data)); }).catch(error => { if (!isCancel(error)) { dispatch(showAlertForError(error)); @@ -481,46 +499,48 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => }); }, 200, { leading: true, trailing: true }); -const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, getState: () => RootState, token: string) => { +const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => { const results = emojiSearch(token.replace(':', ''), { maxResults: 5 } as any); - dispatch(readyComposeSuggestionsEmojis(token, results)); + dispatch(readyComposeSuggestionsEmojis(composeId, token, results)); }; -const fetchComposeSuggestionsTags = (dispatch: AppDispatch, getState: () => RootState, token: string) => { +const fetchComposeSuggestionsTags = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => { const state = getState(); const currentTrends = state.trends.items; - dispatch(updateSuggestionTags(token, currentTrends)); + dispatch(updateSuggestionTags(composeId, token, currentTrends)); }; -const fetchComposeSuggestions = (token: string) => +const fetchComposeSuggestions = (composeId: string, token: string) => (dispatch: AppDispatch, getState: () => RootState) => { switch (token[0]) { case ':': - fetchComposeSuggestionsEmojis(dispatch, getState, token); + fetchComposeSuggestionsEmojis(dispatch, getState, composeId, token); break; case '#': - fetchComposeSuggestionsTags(dispatch, getState, token); + fetchComposeSuggestionsTags(dispatch, getState, composeId, token); break; default: - fetchComposeSuggestionsAccounts(dispatch, getState, token); + fetchComposeSuggestionsAccounts(dispatch, getState, composeId, token); break; } }; -const readyComposeSuggestionsEmojis = (token: string, emojis: Emoji[]) => ({ +const readyComposeSuggestionsEmojis = (composeId: string, token: string, emojis: Emoji[]) => ({ type: COMPOSE_SUGGESTIONS_READY, + id: composeId, token, emojis, }); -const readyComposeSuggestionsAccounts = (token: string, accounts: APIEntity[]) => ({ +const readyComposeSuggestionsAccounts = (composeId: string, token: string, accounts: APIEntity[]) => ({ type: COMPOSE_SUGGESTIONS_READY, + id: composeId, token, accounts, }); -const selectComposeSuggestion = (position: number, token: string | null, suggestion: AutoSuggestion, path: Array) => +const selectComposeSuggestion = (composeId: string, position: number, token: string | null, suggestion: AutoSuggestion, path: Array) => (dispatch: AppDispatch, getState: () => RootState) => { let completion, startPosition; @@ -539,6 +559,7 @@ const selectComposeSuggestion = (position: number, token: string | null, suggest dispatch({ type: COMPOSE_SUGGESTION_SELECT, + id: composeId, position: startPosition, token, completion, @@ -546,14 +567,16 @@ const selectComposeSuggestion = (position: number, token: string | null, suggest }); }; -const updateSuggestionTags = (token: string, currentTrends: ImmutableList) => ({ +const updateSuggestionTags = (composeId: string, token: string, currentTrends: ImmutableList) => ({ type: COMPOSE_SUGGESTION_TAGS_UPDATE, + id: composeId, token, currentTrends, }); -const updateTagHistory = (tags: string[]) => ({ +const updateTagHistory = (composeId: string, tags: string[]) => ({ type: COMPOSE_TAG_HISTORY_UPDATE, + id: composeId, tags, }); @@ -572,7 +595,7 @@ const insertIntoTagHistory = (composeId: string, recognizedTags: APIEntity[], te const newHistory = names.slice(0, 1000); tagHistory.set(me as string, newHistory); - dispatch(updateTagHistory(newHistory)); + dispatch(updateTagHistory(composeId, newHistory)); }; const changeComposeSensitivity = (composeId: string) => ({ @@ -665,29 +688,31 @@ const changePollSettings = (composeId: string, expiresIn?: string | number, isMu const openComposeWithText = (composeId: string, text = '') => (dispatch: AppDispatch) => { - dispatch(resetCompose()); + dispatch(resetCompose(composeId)); dispatch(openModal('COMPOSE')); dispatch(changeCompose(composeId, text)); }; -const addToMentions = (accountId: string) => +const addToMentions = (composeId: string, accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const acct = state.accounts.get(accountId)!.acct; return dispatch({ type: COMPOSE_ADD_TO_MENTIONS, + id: composeId, account: acct, }); }; -const removeFromMentions = (accountId: string) => +const removeFromMentions = (composeId: string, accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const acct = state.accounts.get(accountId)!.acct; return dispatch({ type: COMPOSE_REMOVE_FROM_MENTIONS, + id: composeId, account: acct, }); }; diff --git a/app/soapbox/features/compose/components/compose-form.tsx b/app/soapbox/features/compose/components/compose-form.tsx index 3e58e298a..d4b6a4406 100644 --- a/app/soapbox/features/compose/components/compose-form.tsx +++ b/app/soapbox/features/compose/components/compose-form.tsx @@ -149,19 +149,19 @@ const ComposeForm: React.FC = ({ id, shouldCondense, autoFocus, cl }; const onSuggestionsClearRequested = () => { - dispatch(clearComposeSuggestions()); + dispatch(clearComposeSuggestions(id)); }; const onSuggestionsFetchRequested = (token: string | number) => { - dispatch(fetchComposeSuggestions(token as string)); + dispatch(fetchComposeSuggestions(id, token as string)); }; const onSuggestionSelected = (tokenStart: number, token: string | null, value: string | undefined) => { - if (value) dispatch(selectComposeSuggestion(tokenStart, token, value, ['text'])); + if (value) dispatch(selectComposeSuggestion(id, tokenStart, token, value, ['text'])); }; const onSpoilerSuggestionSelected = (tokenStart: number, token: string | null, value: AutoSuggestion) => { - dispatch(selectComposeSuggestion(tokenStart, token, value, ['spoiler_text'])); + dispatch(selectComposeSuggestion(id, tokenStart, token, value, ['spoiler_text'])); }; const handleChangeSpoilerText: React.ChangeEventHandler = (e) => { diff --git a/app/soapbox/features/compose/components/emoji-picker/emoji-picker-dropdown.tsx b/app/soapbox/features/compose/components/emoji-picker/emoji-picker-dropdown.tsx index 282438c9f..697f38c8c 100644 --- a/app/soapbox/features/compose/components/emoji-picker/emoji-picker-dropdown.tsx +++ b/app/soapbox/features/compose/components/emoji-picker/emoji-picker-dropdown.tsx @@ -115,7 +115,6 @@ const EmojiPickerDropdown: React.FC = ({ onPickEmoji, butt }; const handlePickEmoji = (emoji: EmojiType) => { - console.log(emoji); // eslint-disable-next-line react-hooks/rules-of-hooks dispatch(useEmoji(emoji)); diff --git a/app/soapbox/features/compose/components/polls/poll-form.tsx b/app/soapbox/features/compose/components/polls/poll-form.tsx index b9dd626ed..4daf54048 100644 --- a/app/soapbox/features/compose/components/polls/poll-form.tsx +++ b/app/soapbox/features/compose/components/polls/poll-form.tsx @@ -61,13 +61,13 @@ const Option: React.FC = ({ } }; - const onSuggestionsClearRequested = () => dispatch(clearComposeSuggestions()); + const onSuggestionsClearRequested = () => dispatch(clearComposeSuggestions(composeId)); - const onSuggestionsFetchRequested = (token: string) => dispatch(fetchComposeSuggestions(token)); + const onSuggestionsFetchRequested = (token: string) => dispatch(fetchComposeSuggestions(composeId, token)); const onSuggestionSelected = (tokenStart: number, token: string | null, value: AutoSuggestion) => { if (token && typeof token === 'string') { - dispatch(selectComposeSuggestion(tokenStart, token, value, ['poll', 'options', index])); + dispatch(selectComposeSuggestion(composeId, tokenStart, token, value, ['poll', 'options', index])); } }; diff --git a/app/soapbox/features/compose/components/schedule_button.tsx b/app/soapbox/features/compose/components/schedule_button.tsx index 8eee5db11..16b3cd163 100644 --- a/app/soapbox/features/compose/components/schedule_button.tsx +++ b/app/soapbox/features/compose/components/schedule_button.tsx @@ -22,8 +22,8 @@ const ScheduleButton: React.FC = ({ composeId, disabled }) => { const compose = useCompose(composeId); - const active = compose.schedule; - const unavailable = compose.id; + const active = !!compose.schedule; + const unavailable = !!compose.id; const handleClick = () => { if (active) { diff --git a/app/soapbox/features/compose/components/upload.tsx b/app/soapbox/features/compose/components/upload.tsx index 7c7da5009..9b3befa68 100644 --- a/app/soapbox/features/compose/components/upload.tsx +++ b/app/soapbox/features/compose/components/upload.tsx @@ -91,7 +91,7 @@ const Upload: React.FC = ({ composeId, id }) => { const handleUndoClick: React.MouseEventHandler = e => { e.stopPropagation(); - dispatch(undoUploadCompose(media.id)); + dispatch(undoUploadCompose(composeId, media.id)); }; const handleInputChange: React.ChangeEventHandler = e => { @@ -119,7 +119,7 @@ const Upload: React.FC = ({ composeId, id }) => { setDirtyDescription(null); if (dirtyDescription !== null) { - dispatch(changeUploadCompose(media.id, { dirtyDescription })); + dispatch(changeUploadCompose(composeId, media.id, { dirtyDescription })); } }; diff --git a/app/soapbox/features/compose/containers/upload_button_container.ts b/app/soapbox/features/compose/containers/upload_button_container.ts index 01bf548af..c338f7449 100644 --- a/app/soapbox/features/compose/containers/upload_button_container.ts +++ b/app/soapbox/features/compose/containers/upload_button_container.ts @@ -9,7 +9,7 @@ import type { AppDispatch, RootState } from 'soapbox/store'; const mapStateToProps = (state: RootState, { composeId }: { composeId: string }) => ({ disabled: state.compose.get(composeId)?.is_uploading, - resetFileKey: state.compose.get(composeId)?.resetFileKey, + resetFileKey: state.compose.get(composeId)?.resetFileKey!, }); const mapDispatchToProps = (dispatch: AppDispatch, { composeId }: { composeId: string }) => ({ diff --git a/app/soapbox/features/reply_mentions/account.tsx b/app/soapbox/features/reply_mentions/account.tsx index ca3293abd..ffb960840 100644 --- a/app/soapbox/features/reply_mentions/account.tsx +++ b/app/soapbox/features/reply_mentions/account.tsx @@ -26,11 +26,13 @@ const Account: React.FC = ({ composeId, accountId, author }) => { const intl = useIntl(); const dispatch = useAppDispatch(); + const compose = useCompose(composeId); + const account = useAppSelector((state) => getAccount(state, accountId)); - const added = !!account && useCompose(composeId).to?.includes(account.acct); + const added = !!account && compose.to?.includes(account.acct); - const onRemove = () => dispatch(removeFromMentions(accountId)); - const onAdd = () => dispatch(addToMentions(accountId)); + const onRemove = () => dispatch(removeFromMentions(composeId, accountId)); + const onAdd = () => dispatch(addToMentions(composeId, accountId)); useEffect(() => { if (accountId && !account) { diff --git a/app/soapbox/reducers/__tests__/compose.test.ts b/app/soapbox/reducers/__tests__/compose.test.ts index 168006c25..69b4f178d 100644 --- a/app/soapbox/reducers/__tests__/compose.test.ts +++ b/app/soapbox/reducers/__tests__/compose.test.ts @@ -1,499 +1,510 @@ -// import { List as ImmutableList, Record as ImmutableRecord, fromJS } from 'immutable'; - -// import * as actions from 'soapbox/actions/compose'; -// import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me'; -// import { SETTING_CHANGE } from 'soapbox/actions/settings'; -// import { TIMELINE_DELETE } from 'soapbox/actions/timelines'; -// import { TagRecord } from 'soapbox/normalizers'; -// import { normalizeStatus } from 'soapbox/normalizers/status'; - -// import reducer, { ReducerRecord } from '../compose'; - -// describe('compose reducer', () => { -// it('returns the initial state by default', () => { -// const state = reducer(undefined, {} as any); -// expect(state.toJS()).toMatchObject({ -// mounted: 0, -// sensitive: false, -// spoiler: false, -// spoiler_text: '', -// privacy: 'public', -// text: '', -// focusDate: null, -// caretPosition: null, -// in_reply_to: null, -// is_composing: false, -// is_submitting: false, -// is_changing_upload: false, -// is_uploading: false, -// progress: 0, -// media_attachments: [], -// poll: null, -// suggestion_token: null, -// suggestions: [], -// default_privacy: 'public', -// default_sensitive: false, -// tagHistory: [], -// content_type: 'text/plain', -// }); -// expect(state.get('idempotencyKey').length === 36); -// }); - -// describe('COMPOSE_SET_STATUS', () => { -// it('strips Pleroma integer attachments', () => { -// const action = { -// type: actions.COMPOSE_SET_STATUS, -// status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))), -// v: { software: 'Pleroma' }, -// withRedraft: true, -// }; - -// const result = reducer(undefined, action); -// expect(result.get('media_attachments').isEmpty()).toBe(true); -// }); - -// it('leaves non-Pleroma integer attachments alone', () => { -// const action = { -// type: actions.COMPOSE_SET_STATUS, -// status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))), -// }; - -// const result = reducer(undefined, action); -// expect(result.getIn(['media_attachments', 0, 'id'])).toEqual('508107650'); -// }); - -// it('sets the id when editing a post', () => { -// const action = { -// withRedraft: false, -// type: actions.COMPOSE_SET_STATUS, -// status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))), -// }; - -// const result = reducer(undefined, action); -// expect(result.get('id')).toEqual('AHU2RrX0wdcwzCYjFQ'); -// }); - -// it('does not set the id when redrafting a post', () => { -// const action = { -// withRedraft: true, -// type: actions.COMPOSE_SET_STATUS, -// status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))), -// }; - -// const result = reducer(undefined, action); -// expect(result.get('id')).toEqual(null); -// }); -// }); - -// it('uses \'public\' scope as default', () => { -// const action = { -// type: actions.COMPOSE_REPLY, -// status: ImmutableRecord({})(), -// account: ImmutableRecord({})(), -// }; -// expect(reducer(undefined, action).toJS()).toMatchObject({ privacy: 'public' }); -// }); - -// it('uses \'direct\' scope when replying to a DM', () => { -// const state = ReducerRecord({ default_privacy: 'public' }); -// const action = { -// type: actions.COMPOSE_REPLY, -// status: ImmutableRecord({ visibility: 'direct' })(), -// account: ImmutableRecord({})(), -// }; -// expect(reducer(state as any, action).toJS()).toMatchObject({ privacy: 'direct' }); -// }); - -// it('uses \'private\' scope when replying to a private post', () => { -// const state = ReducerRecord({ default_privacy: 'public' }); -// const action = { -// type: actions.COMPOSE_REPLY, -// status: ImmutableRecord({ visibility: 'private' })(), -// account: ImmutableRecord({})(), -// }; -// expect(reducer(state as any, action).toJS()).toMatchObject({ privacy: 'private' }); -// }); - -// it('uses \'unlisted\' scope when replying to an unlisted post', () => { -// const state = ReducerRecord({ default_privacy: 'public' }); -// const action = { -// type: actions.COMPOSE_REPLY, -// status: ImmutableRecord({ visibility: 'unlisted' })(), -// account: ImmutableRecord({})(), -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ privacy: 'unlisted' }); -// }); - -// it('uses \'private\' scope when set as preference and replying to a public post', () => { -// const state = ReducerRecord({ default_privacy: 'private' }); -// const action = { -// type: actions.COMPOSE_REPLY, -// status: ImmutableRecord({ visibility: 'public' })(), -// account: ImmutableRecord({})(), -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ privacy: 'private' }); -// }); - -// it('uses \'unlisted\' scope when set as preference and replying to a public post', () => { -// const state = ReducerRecord({ default_privacy: 'unlisted' }); -// const action = { -// type: actions.COMPOSE_REPLY, -// status: ImmutableRecord({ visibility: 'public' })(), -// account: ImmutableRecord({})(), -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ privacy: 'unlisted' }); -// }); - -// it('sets preferred scope on user login', () => { -// const state = ReducerRecord({ default_privacy: 'public' }); -// const action = { -// type: ME_FETCH_SUCCESS, -// me: { pleroma: { settings_store: { soapbox_fe: { defaultPrivacy: 'unlisted' } } } }, -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// default_privacy: 'unlisted', -// privacy: 'unlisted', -// }); -// }); - -// it('sets preferred scope on settings change', () => { -// const state = ReducerRecord({ default_privacy: 'public' }); -// const action = { -// type: SETTING_CHANGE, -// path: ['defaultPrivacy'], -// value: 'unlisted', -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// default_privacy: 'unlisted', -// privacy: 'unlisted', -// }); -// }); - -// it('sets default scope on settings save (but retains current scope)', () => { -// const state = ReducerRecord({ default_privacy: 'public', privacy: 'public' }); -// const action = { -// type: ME_PATCH_SUCCESS, -// me: { pleroma: { settings_store: { soapbox_fe: { defaultPrivacy: 'unlisted' } } } }, -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// default_privacy: 'unlisted', -// privacy: 'public', -// }); -// }); - -// it('should handle COMPOSE_MOUNT', () => { -// const state = ReducerRecord({ mounted: 1 }); -// const action = { -// type: actions.COMPOSE_MOUNT, -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// mounted: 2, -// }); -// }); - -// it('should handle COMPOSE_UNMOUNT', () => { -// const state = ReducerRecord({ mounted: 1 }); -// const action = { -// type: actions.COMPOSE_UNMOUNT, -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// mounted: 0, -// }); -// }); - -// it('should handle COMPOSE_SENSITIVITY_CHANGE on Mark Sensitive click, don\'t toggle if spoiler active', () => { -// const state = ReducerRecord({ spoiler: true, sensitive: true, idempotencyKey: '' }); -// const action = { -// type: actions.COMPOSE_SENSITIVITY_CHANGE, -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// sensitive: true, -// }); -// }); - -// it('should handle COMPOSE_SENSITIVITY_CHANGE on Mark Sensitive click, toggle if spoiler inactive', () => { -// const state = ReducerRecord({ spoiler: false, sensitive: true }); -// const action = { -// type: actions.COMPOSE_SENSITIVITY_CHANGE, -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// sensitive: false, -// }); -// }); - -// it('should handle COMPOSE_SPOILERNESS_CHANGE on CW button click', () => { -// const state = ReducerRecord({ spoiler_text: 'spoiler text', spoiler: true, media_attachments: ImmutableList() }); -// const action = { -// type: actions.COMPOSE_SPOILERNESS_CHANGE, -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// spoiler: false, -// spoiler_text: '', -// }); -// }); - -// it('should handle COMPOSE_SPOILER_TEXT_CHANGE', () => { -// const state = ReducerRecord({ spoiler_text: 'prevtext' }); -// const action = { -// type: actions.COMPOSE_SPOILER_TEXT_CHANGE, -// text: 'nexttext', -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// spoiler_text: 'nexttext', -// }); -// }); - -// it('should handle COMPOSE_VISIBILITY_CHANGE', () => { -// const state = ReducerRecord({ privacy: 'public' }); -// const action = { -// type: actions.COMPOSE_VISIBILITY_CHANGE, -// value: 'direct', -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// privacy: 'direct', -// }); -// }); - -// describe('COMPOSE_CHANGE', () => { -// it('should handle text changing', () => { -// const state = ReducerRecord({ text: 'prevtext' }); -// const action = { -// type: actions.COMPOSE_CHANGE, -// text: 'nexttext', -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// text: 'nexttext', -// }); -// }); -// }); - -// it('should handle COMPOSE_COMPOSING_CHANGE', () => { -// const state = ReducerRecord({ is_composing: true }); -// const action = { -// type: actions.COMPOSE_COMPOSING_CHANGE, -// value: false, -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// is_composing: false, -// }); -// }); - -// it('should handle COMPOSE_SUBMIT_REQUEST', () => { -// const state = ReducerRecord({ is_submitting: false }); -// const action = { -// type: actions.COMPOSE_SUBMIT_REQUEST, -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// is_submitting: true, -// }); -// }); - -// it('should handle COMPOSE_UPLOAD_CHANGE_REQUEST', () => { -// const state = ReducerRecord({ is_changing_upload: false }); -// const action = { -// type: actions.COMPOSE_UPLOAD_CHANGE_REQUEST, -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// is_changing_upload: true, -// }); -// }); - -// it('should handle COMPOSE_SUBMIT_SUCCESS', () => { -// const state = ReducerRecord({ default_privacy: 'public', privacy: 'private' }); -// const action = { -// type: actions.COMPOSE_SUBMIT_SUCCESS, -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// privacy: 'public', -// }); -// }); - -// it('should handle COMPOSE_SUBMIT_FAIL', () => { -// const state = ReducerRecord({ is_submitting: true }); -// const action = { -// type: actions.COMPOSE_SUBMIT_FAIL, -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// is_submitting: false, -// }); -// }); - -// it('should handle COMPOSE_UPLOAD_CHANGE_FAIL', () => { -// const state = ReducerRecord({ is_changing_upload: true }); -// const action = { -// type: actions.COMPOSE_UPLOAD_CHANGE_FAIL, -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// is_changing_upload: false, -// }); -// }); - -// it('should handle COMPOSE_UPLOAD_REQUEST', () => { -// const state = ReducerRecord({ is_uploading: false }); -// const action = { -// type: actions.COMPOSE_UPLOAD_REQUEST, -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// is_uploading: true, -// }); -// }); - -// it('should handle COMPOSE_UPLOAD_SUCCESS', () => { -// const state = ReducerRecord({ media_attachments: ImmutableList() }); -// const media = [ -// { -// description: null, -// id: '1375732379', -// pleroma: { -// mime_type: 'image/jpeg', -// }, -// preview_url: 'https://media.gleasonator.com/media_attachments/files/000/853/856/original/7035d67937053e1d.jpg', -// remote_url: 'https://media.gleasonator.com/media_attachments/files/000/853/856/original/7035d67937053e1d.jpg', -// text_url: 'https://media.gleasonator.com/media_attachments/files/000/853/856/original/7035d67937053e1d.jpg', -// type: 'image', -// url: 'https://media.gleasonator.com/media_attachments/files/000/853/856/original/7035d67937053e1d.jpg', -// }, -// ]; -// const action = { -// type: actions.COMPOSE_UPLOAD_SUCCESS, -// media: media, -// skipLoading: true, -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// is_uploading: false, -// }); -// }); - -// it('should handle COMPOSE_UPLOAD_FAIL', () => { -// const state = ReducerRecord({ is_uploading: true }); -// const action = { -// type: actions.COMPOSE_UPLOAD_FAIL, -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// is_uploading: false, -// }); -// }); - -// it('should handle COMPOSE_UPLOAD_PROGRESS', () => { -// const state = ReducerRecord({ progress: 0 }); -// const action = { -// type: actions.COMPOSE_UPLOAD_PROGRESS, -// loaded: 10, -// total: 15, -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// progress: 67, -// }); -// }); - -// it('should handle COMPOSE_SUGGESTIONS_CLEAR', () => { -// const action = { -// type: actions.COMPOSE_SUGGESTIONS_CLEAR, -// suggestions: [], -// suggestion_token: 'aiekdns3', -// }; -// expect(reducer(undefined, action).toJS()).toMatchObject({ -// suggestion_token: null, -// }); -// }); - -// it('should handle COMPOSE_SUGGESTION_TAGS_UPDATE', () => { -// const state = ReducerRecord({ tagHistory: ImmutableList([ 'hashtag' ]) }); -// const action = { -// type: actions.COMPOSE_SUGGESTION_TAGS_UPDATE, -// token: 'aaadken3', -// currentTrends: ImmutableList([ -// TagRecord({ name: 'hashtag' }), -// ]), -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// suggestion_token: 'aaadken3', -// suggestions: [], -// tagHistory: [ 'hashtag' ], -// }); -// }); - -// it('should handle COMPOSE_TAG_HISTORY_UPDATE', () => { -// const action = { -// type: actions.COMPOSE_TAG_HISTORY_UPDATE, -// tags: [ 'hashtag', 'hashtag2'], -// }; -// expect(reducer(undefined, action).toJS()).toMatchObject({ -// tagHistory: [ 'hashtag', 'hashtag2' ], -// }); -// }); - -// it('should handle TIMELINE_DELETE - delete status from timeline', () => { -// const state = ReducerRecord({ in_reply_to: '9wk6pmImMrZjgrK7iC' }); -// const action = { -// type: TIMELINE_DELETE, -// id: '9wk6pmImMrZjgrK7iC', -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// in_reply_to: null, -// }); -// }); - -// it('should handle COMPOSE_POLL_ADD', () => { -// const state = ReducerRecord({ poll: null }); -// const initialPoll = Object({ -// options: [ -// '', -// '', -// ], -// expires_in: 86400, -// multiple: false, -// }); -// const action = { -// type: actions.COMPOSE_POLL_ADD, -// }; -// expect(reducer(state, action).toJS()).toMatchObject({ -// poll: initialPoll, -// }); -// }); - -// it('should handle COMPOSE_POLL_REMOVE', () => { -// const action = { -// type: actions.COMPOSE_POLL_REMOVE, -// }; -// expect(reducer(undefined, action).toJS()).toMatchObject({ -// poll: null, -// }); -// }); - -// it('should handle COMPOSE_POLL_OPTION_CHANGE', () => { -// const initialPoll = Object({ -// options: [ -// 'option 1', -// 'option 2', -// ], -// expires_in: 86400, -// multiple: false, -// }); -// const state = ReducerRecord({ poll: initialPoll }); -// const action = { -// type: actions.COMPOSE_POLL_OPTION_CHANGE, -// index: 0, -// title: 'change option', -// }; -// const updatedPoll = Object({ -// options: [ -// 'change option', -// 'option 2', -// ], -// expires_in: 86400, -// multiple: false, -// }); -// expect(reducer(state, action).toJS()).toMatchObject({ -// poll: updatedPoll, -// }); -// }); - -// it('sets the post content-type', () => { -// const action = { -// type: actions.COMPOSE_TYPE_CHANGE, -// value: 'text/plain', -// }; -// expect(reducer(undefined, action).toJS()).toMatchObject({ content_type: 'text/plain' }); -// }); -// }); +import { List as ImmutableList, Record as ImmutableRecord, fromJS } from 'immutable'; + +import * as actions from 'soapbox/actions/compose'; +import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me'; +import { SETTING_CHANGE } from 'soapbox/actions/settings'; +import { TIMELINE_DELETE } from 'soapbox/actions/timelines'; +import { TagRecord } from 'soapbox/normalizers'; +import { normalizeStatus } from 'soapbox/normalizers/status'; + +import reducer, { initialState, ReducerCompose } from '../compose'; + +describe('compose reducer', () => { + it('returns the initial state by default', () => { + const state = reducer(undefined, {} as any); + expect(state.toJS()).toMatchObject({ + default: { + sensitive: false, + spoiler: false, + spoiler_text: '', + privacy: 'public', + text: '', + focusDate: null, + caretPosition: null, + in_reply_to: null, + is_composing: false, + is_submitting: false, + is_changing_upload: false, + is_uploading: false, + progress: 0, + media_attachments: [], + poll: null, + suggestion_token: null, + suggestions: [], + default_privacy: 'public', + default_sensitive: false, + tagHistory: [], + content_type: 'text/plain', + }, + }); + expect(state.get('default')!.idempotencyKey.length === 36); + }); + + describe('COMPOSE_SET_STATUS', () => { + it('strips Pleroma integer attachments', () => { + const action = { + type: actions.COMPOSE_SET_STATUS, + id: 'compose-modal', + status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))), + v: { software: 'Pleroma' }, + withRedraft: true, + }; + + const result = reducer(undefined, action); + expect(result.get('compose-modal')!.media_attachments.isEmpty()).toBe(true); + }); + + it('leaves non-Pleroma integer attachments alone', () => { + const action = { + type: actions.COMPOSE_SET_STATUS, + id: 'compose-modal', + status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))), + }; + + const result = reducer(undefined, action); + expect(result.get('compose-modal')!.media_attachments.getIn([0, 'id'])).toEqual('508107650'); + }); + + it('sets the id when editing a post', () => { + const action = { + id: 'compose-modal', + withRedraft: false, + type: actions.COMPOSE_SET_STATUS, + status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))), + }; + + const result = reducer(undefined, action); + expect(result.get('compose-modal')!.id).toEqual('AHU2RrX0wdcwzCYjFQ'); + }); + + it('does not set the id when redrafting a post', () => { + const action = { + id: 'compose-modal', + withRedraft: true, + type: actions.COMPOSE_SET_STATUS, + status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))), + }; + + const result = reducer(undefined, action); + expect(result.get('compose-modal')!.id).toEqual(null); + }); + }); + + it('uses \'public\' scope as default', () => { + const action = { + type: actions.COMPOSE_REPLY, + status: ImmutableRecord({})(), + account: ImmutableRecord({})(), + }; + expect(reducer(undefined, action).toJS()['compose-modal']).toMatchObject({ privacy: 'public' }); + }); + + it('uses \'direct\' scope when replying to a DM', () => { + const state = initialState.set('default', ReducerCompose({ default_privacy: 'public' })); + const action = { + type: actions.COMPOSE_REPLY, + status: ImmutableRecord({ visibility: 'direct' })(), + account: ImmutableRecord({})(), + }; + expect(reducer(state as any, action).toJS()['compose-modal']).toMatchObject({ privacy: 'direct' }); + }); + + it('uses \'private\' scope when replying to a private post', () => { + const state = initialState.set('default', ReducerCompose({ default_privacy: 'public' })); + const action = { + type: actions.COMPOSE_REPLY, + status: ImmutableRecord({ visibility: 'private' })(), + account: ImmutableRecord({})(), + }; + expect(reducer(state as any, action).toJS()['compose-modal']).toMatchObject({ privacy: 'private' }); + }); + + it('uses \'unlisted\' scope when replying to an unlisted post', () => { + const state = initialState.set('default', ReducerCompose({ default_privacy: 'public' })); + const action = { + type: actions.COMPOSE_REPLY, + status: ImmutableRecord({ visibility: 'unlisted' })(), + account: ImmutableRecord({})(), + }; + expect(reducer(state, action).toJS()['compose-modal']).toMatchObject({ privacy: 'unlisted' }); + }); + + it('uses \'private\' scope when set as preference and replying to a public post', () => { + const state = initialState.set('default', ReducerCompose({ default_privacy: 'private' })); + const action = { + type: actions.COMPOSE_REPLY, + status: ImmutableRecord({ visibility: 'public' })(), + account: ImmutableRecord({})(), + }; + expect(reducer(state, action).toJS()['compose-modal']).toMatchObject({ privacy: 'private' }); + }); + + it('uses \'unlisted\' scope when set as preference and replying to a public post', () => { + const state = initialState.set('default', ReducerCompose({ default_privacy: 'unlisted' })); + const action = { + type: actions.COMPOSE_REPLY, + status: ImmutableRecord({ visibility: 'public' })(), + account: ImmutableRecord({})(), + }; + expect(reducer(state, action).toJS()['compose-modal']).toMatchObject({ privacy: 'unlisted' }); + }); + + it('sets preferred scope on user login', () => { + const state = initialState.set('default', ReducerCompose({ default_privacy: 'public' })); + const action = { + type: ME_FETCH_SUCCESS, + me: { pleroma: { settings_store: { soapbox_fe: { defaultPrivacy: 'unlisted' } } } }, + }; + expect(reducer(state, action).toJS().default).toMatchObject({ + default_privacy: 'unlisted', + privacy: 'unlisted', + }); + }); + + it('sets preferred scope on settings change', () => { + const state = initialState.set('default', ReducerCompose({ default_privacy: 'public' })); + const action = { + type: SETTING_CHANGE, + path: ['defaultPrivacy'], + value: 'unlisted', + }; + expect(reducer(state, action).toJS().default).toMatchObject({ + default_privacy: 'unlisted', + privacy: 'unlisted', + }); + }); + + it('sets default scope on settings save (but retains current scope)', () => { + const state = initialState.set('default', ReducerCompose({ default_privacy: 'public', privacy: 'public' })); + const action = { + type: ME_PATCH_SUCCESS, + me: { pleroma: { settings_store: { soapbox_fe: { defaultPrivacy: 'unlisted' } } } }, + }; + expect(reducer(state, action).toJS().default).toMatchObject({ + default_privacy: 'unlisted', + privacy: 'public', + }); + }); + + it('should handle COMPOSE_SENSITIVITY_CHANGE on Mark Sensitive click, don\'t toggle if spoiler active', () => { + const state = initialState.set('home', ReducerCompose({ spoiler: true, sensitive: true, idempotencyKey: '' })); + const action = { + type: actions.COMPOSE_SENSITIVITY_CHANGE, + id: 'home', + }; + expect(reducer(state, action).toJS().home).toMatchObject({ + sensitive: true, + }); + }); + + it('should handle COMPOSE_SENSITIVITY_CHANGE on Mark Sensitive click, toggle if spoiler inactive', () => { + const state = initialState.set('home', ReducerCompose({ spoiler: false, sensitive: true })); + const action = { + type: actions.COMPOSE_SENSITIVITY_CHANGE, + id: 'home', + }; + expect(reducer(state, action).toJS().home).toMatchObject({ + sensitive: false, + }); + }); + + it('should handle COMPOSE_SPOILERNESS_CHANGE on CW button click', () => { + const state = initialState.set('home', ReducerCompose({ spoiler_text: 'spoiler text', spoiler: true, media_attachments: ImmutableList() })); + const action = { + type: actions.COMPOSE_SPOILERNESS_CHANGE, + id: 'home', + }; + expect(reducer(state, action).toJS().home).toMatchObject({ + spoiler: false, + spoiler_text: '', + }); + }); + + it('should handle COMPOSE_SPOILER_TEXT_CHANGE', () => { + const state = initialState.set('home', ReducerCompose({ spoiler_text: 'prevtext' })); + const action = { + type: actions.COMPOSE_SPOILER_TEXT_CHANGE, + id: 'home', + text: 'nexttext', + }; + expect(reducer(state, action).toJS().home).toMatchObject({ + spoiler_text: 'nexttext', + }); + }); + + it('should handle COMPOSE_VISIBILITY_CHANGE', () => { + const state = initialState.set('home', ReducerCompose({ privacy: 'public' })); + const action = { + type: actions.COMPOSE_VISIBILITY_CHANGE, + id: 'home', + value: 'direct', + }; + expect(reducer(state, action).toJS().home).toMatchObject({ + privacy: 'direct', + }); + }); + + describe('COMPOSE_CHANGE', () => { + it('should handle text changing', () => { + const state = initialState.set('home', ReducerCompose({ text: 'prevtext' })); + const action = { + type: actions.COMPOSE_CHANGE, + id: 'home', + text: 'nexttext', + }; + expect(reducer(state, action).toJS().home).toMatchObject({ + text: 'nexttext', + }); + }); + }); + + it('should handle COMPOSE_COMPOSING_CHANGE', () => { + const state = initialState.set('home', ReducerCompose({ is_composing: true })); + const action = { + type: actions.COMPOSE_COMPOSING_CHANGE, + id: 'home', + value: false, + }; + expect(reducer(state, action).toJS().home).toMatchObject({ + is_composing: false, + }); + }); + + it('should handle COMPOSE_SUBMIT_REQUEST', () => { + const state = initialState.set('home', ReducerCompose({ is_submitting: false })); + const action = { + type: actions.COMPOSE_SUBMIT_REQUEST, + id: 'home', + }; + expect(reducer(state, action).toJS().home).toMatchObject({ + is_submitting: true, + }); + }); + + it('should handle COMPOSE_UPLOAD_CHANGE_REQUEST', () => { + const state = initialState.set('home', ReducerCompose({ is_changing_upload: false })); + const action = { + type: actions.COMPOSE_UPLOAD_CHANGE_REQUEST, + id: 'home', + }; + expect(reducer(state, action).toJS().home).toMatchObject({ + is_changing_upload: true, + }); + }); + + it('should handle COMPOSE_SUBMIT_SUCCESS', () => { + const state = initialState.set('home', ReducerCompose({ default_privacy: 'public', privacy: 'private' })); + const action = { + type: actions.COMPOSE_SUBMIT_SUCCESS, + id: 'home', + }; + expect(reducer(state, action).toJS().home).toMatchObject({ + privacy: 'public', + }); + }); + + it('should handle COMPOSE_SUBMIT_FAIL', () => { + const state = initialState.set('home', ReducerCompose({ is_submitting: true })); + const action = { + type: actions.COMPOSE_SUBMIT_FAIL, + id: 'home', + }; + expect(reducer(state, action).toJS().home).toMatchObject({ + is_submitting: false, + }); + }); + + it('should handle COMPOSE_UPLOAD_CHANGE_FAIL', () => { + const state = initialState.set('home', ReducerCompose({ is_changing_upload: true })); + const action = { + type: actions.COMPOSE_UPLOAD_CHANGE_FAIL, + composeId: 'home', + }; + expect(reducer(state, action).toJS().home).toMatchObject({ + is_changing_upload: false, + }); + }); + + it('should handle COMPOSE_UPLOAD_REQUEST', () => { + const state = initialState.set('home', ReducerCompose({ is_uploading: false })); + const action = { + type: actions.COMPOSE_UPLOAD_REQUEST, + id: 'home', + }; + expect(reducer(state, action).toJS().home).toMatchObject({ + is_uploading: true, + }); + }); + + it('should handle COMPOSE_UPLOAD_SUCCESS', () => { + const state = initialState.set('home', ReducerCompose({ media_attachments: ImmutableList() })); + const media = [ + { + description: null, + id: '1375732379', + pleroma: { + mime_type: 'image/jpeg', + }, + preview_url: 'https://media.gleasonator.com/media_attachments/files/000/853/856/original/7035d67937053e1d.jpg', + remote_url: 'https://media.gleasonator.com/media_attachments/files/000/853/856/original/7035d67937053e1d.jpg', + text_url: 'https://media.gleasonator.com/media_attachments/files/000/853/856/original/7035d67937053e1d.jpg', + type: 'image', + url: 'https://media.gleasonator.com/media_attachments/files/000/853/856/original/7035d67937053e1d.jpg', + }, + ]; + const action = { + type: actions.COMPOSE_UPLOAD_SUCCESS, + id: 'home', + media: media, + skipLoading: true, + }; + expect(reducer(state, action).toJS().home).toMatchObject({ + is_uploading: false, + }); + }); + + it('should handle COMPOSE_UPLOAD_FAIL', () => { + const state = initialState.set('home', ReducerCompose({ is_uploading: true })); + const action = { + type: actions.COMPOSE_UPLOAD_FAIL, + id: 'home', + }; + expect(reducer(state, action).toJS().home).toMatchObject({ + is_uploading: false, + }); + }); + + it('should handle COMPOSE_UPLOAD_PROGRESS', () => { + const state = initialState.set('home', ReducerCompose({ progress: 0 })); + const action = { + type: actions.COMPOSE_UPLOAD_PROGRESS, + id: 'home', + loaded: 10, + total: 15, + }; + expect(reducer(state, action).toJS().home).toMatchObject({ + progress: 67, + }); + }); + + it('should handle COMPOSE_SUGGESTIONS_CLEAR', () => { + const state = initialState.set('home', ReducerCompose()); + const action = { + type: actions.COMPOSE_SUGGESTIONS_CLEAR, + id: 'home', + suggestions: [], + suggestion_token: 'aiekdns3', + }; + expect(reducer(state, action).toJS().home).toMatchObject({ + suggestion_token: null, + }); + }); + + it('should handle COMPOSE_SUGGESTION_TAGS_UPDATE', () => { + const state = initialState.set('home', ReducerCompose({ tagHistory: ImmutableList([ 'hashtag' ]) })); + const action = { + type: actions.COMPOSE_SUGGESTION_TAGS_UPDATE, + id: 'home', + token: 'aaadken3', + currentTrends: ImmutableList([ + TagRecord({ name: 'hashtag' }), + ]), + }; + expect(reducer(state, action).toJS().home).toMatchObject({ + suggestion_token: 'aaadken3', + suggestions: [], + tagHistory: [ 'hashtag' ], + }); + }); + + it('should handle COMPOSE_TAG_HISTORY_UPDATE', () => { + const action = { + type: actions.COMPOSE_TAG_HISTORY_UPDATE, + id: 'home', + tags: [ 'hashtag', 'hashtag2'], + }; + expect(reducer(undefined, action).toJS().home).toMatchObject({ + tagHistory: [ 'hashtag', 'hashtag2' ], + }); + }); + + it('should handle TIMELINE_DELETE - delete status from timeline', () => { + const state = initialState.set('compose-modal', ReducerCompose({ in_reply_to: '9wk6pmImMrZjgrK7iC' })); + const action = { + type: TIMELINE_DELETE, + id: '9wk6pmImMrZjgrK7iC', + }; + expect(reducer(state, action).toJS()['compose-modal']).toMatchObject({ + in_reply_to: null, + }); + }); + + it('should handle COMPOSE_POLL_ADD', () => { + const state = initialState.set('home', ReducerCompose({ poll: null })); + const initialPoll = Object({ + options: [ + '', + '', + ], + expires_in: 86400, + multiple: false, + }); + const action = { + type: actions.COMPOSE_POLL_ADD, + id: 'home', + }; + expect(reducer(state, action).toJS().home).toMatchObject({ + poll: initialPoll, + }); + }); + + it('should handle COMPOSE_POLL_REMOVE', () => { + const state = initialState.set('home', ReducerCompose()); + const action = { + type: actions.COMPOSE_POLL_REMOVE, + id: 'home', + }; + expect(reducer(state, action).toJS().home).toMatchObject({ + poll: null, + }); + }); + + it('should handle COMPOSE_POLL_OPTION_CHANGE', () => { + const initialPoll = Object({ + options: [ + 'option 1', + 'option 2', + ], + expires_in: 86400, + multiple: false, + }); + const state = initialState.set('home', ReducerCompose({ poll: initialPoll })); + const action = { + type: actions.COMPOSE_POLL_OPTION_CHANGE, + id: 'home', + index: 0, + title: 'change option', + }; + const updatedPoll = Object({ + options: [ + 'change option', + 'option 2', + ], + expires_in: 86400, + multiple: false, + }); + expect(reducer(state, action).toJS().home).toMatchObject({ + poll: updatedPoll, + }); + }); + + it('sets the post content-type', () => { + const state = initialState.set('home', ReducerCompose()); + const action = { + type: actions.COMPOSE_TYPE_CHANGE, + id: 'home', + value: 'text/plain', + }; + expect(reducer(state, action).toJS().home).toMatchObject({ content_type: 'text/plain' }); + }); +}); diff --git a/app/soapbox/reducers/compose.ts b/app/soapbox/reducers/compose.ts index 08d350fed..a5d51f432 100644 --- a/app/soapbox/reducers/compose.ts +++ b/app/soapbox/reducers/compose.ts @@ -278,7 +278,7 @@ const updateSetting = (compose: Compose, path: string[], value: string) => { const updateCompose = (state: State, key: string, updater: (compose: Compose) => Compose) => state.update(key, state.get('default')!, updater); -const initialState: State = ImmutableMap({ +export const initialState: State = ImmutableMap({ default: ReducerCompose({ idempotencyKey: uuid(), resetFileKey: getResetFileKey() }), }); @@ -361,11 +361,11 @@ export default function compose(state = initialState, action: AnyAction) { case COMPOSE_QUOTE_CANCEL: case COMPOSE_RESET: case COMPOSE_SUBMIT_SUCCESS: - return state.get('default')!.set('idempotencyKey', uuid()); + return updateCompose(state, action.id, () => state.get('default')!.set('idempotencyKey', uuid())); case COMPOSE_SUBMIT_FAIL: return updateCompose(state, action.id, compose => compose.set('is_submitting', false)); case COMPOSE_UPLOAD_CHANGE_FAIL: - return updateCompose(state, action.id, compose => compose.set('is_changing_upload', false)); + return updateCompose(state, action.composeId, compose => compose.set('is_changing_upload', false)); case COMPOSE_UPLOAD_REQUEST: return updateCompose(state, action.id, compose => compose.set('is_uploading', true)); case COMPOSE_UPLOAD_SUCCESS: @@ -392,7 +392,7 @@ export default function compose(state = initialState, action: AnyAction) { map.set('idempotencyKey', uuid()); })); case COMPOSE_SUGGESTIONS_CLEAR: - return updateCompose(state, action.id, compose => compose.update('suggestions', list => list.clear()).set('suggestion_token', null)); + return updateCompose(state, action.id, compose => compose.update('suggestions', list => list?.clear()).set('suggestion_token', null)); case COMPOSE_SUGGESTIONS_READY: return updateCompose(state, action.id, compose => compose.set('suggestions', ImmutableList(action.accounts ? action.accounts.map((item: APIEntity) => item.id) : action.emojis)).set('suggestion_token', action.token)); case COMPOSE_SUGGESTION_SELECT: @@ -402,7 +402,7 @@ export default function compose(state = initialState, action: AnyAction) { case COMPOSE_TAG_HISTORY_UPDATE: return updateCompose(state, action.id, compose => compose.set('tagHistory', ImmutableList(fromJS(action.tags)) as ImmutableList)); case TIMELINE_DELETE: - return updateCompose(state, action.id, compose => { + return updateCompose(state, 'compose-modal', compose => { if (action.id === compose.in_reply_to) { return compose.set('in_reply_to', null); } if (action.id === compose.quote) { diff --git a/app/soapbox/reducers/index.ts b/app/soapbox/reducers/index.ts index e1c64f0ad..87750381b 100644 --- a/app/soapbox/reducers/index.ts +++ b/app/soapbox/reducers/index.ts @@ -157,7 +157,6 @@ const rootReducer: typeof appReducer = (state, action) => { case AUTH_LOGGED_OUT: return appReducer(logOut(state), action); default: - console.log(action.type); return appReducer(state, action); } };