From 4ad1a2cf3ecd2b079b278c21a0d9012882e8a1c6 Mon Sep 17 00:00:00 2001 From: Echo Date: Fri, 14 Aug 2026 11:25:36 +0000 Subject: [PATCH] Composer redesign: Fixes bug with new post button (#40166) --- .../compose/redesign/cancel_modal.tsx | 13 +++- .../features/compose/redesign/header.tsx | 4 +- .../mastodon/reducers/slices/composer.ts | 70 ++++++++++++++++--- 3 files changed, 72 insertions(+), 15 deletions(-) diff --git a/app/javascript/mastodon/features/compose/redesign/cancel_modal.tsx b/app/javascript/mastodon/features/compose/redesign/cancel_modal.tsx index 7c797772269..2f19babbdab 100644 --- a/app/javascript/mastodon/features/compose/redesign/cancel_modal.tsx +++ b/app/javascript/mastodon/features/compose/redesign/cancel_modal.tsx @@ -6,20 +6,27 @@ import { closeModal } from '@/mastodon/actions/modal'; import { Button } from '@/mastodon/components/button/redesign'; import { focusComposerTextarea, + openNewComposer, resetComposer, } from '@/mastodon/reducers/slices/composer'; import { useAppDispatch } from '@/mastodon/store'; import classes from './modals.module.scss'; -const ComposerCancelConfirmModal: React.FC = () => { +const ComposerCancelConfirmModal: React.FC<{ openNew?: boolean }> = ({ + openNew, +}) => { const dispatch = useAppDispatch(); const handleDelete = useCallback(() => { - dispatch(resetComposer()); + if (openNew) { + dispatch(openNewComposer({ force: true })); + } else { + dispatch(resetComposer()); + } dispatch( closeModal({ modalType: 'COMPOSER_DRAFT_DELETE', ignoreFocus: false }), ); - }, [dispatch]); + }, [dispatch, openNew]); const handleContinue = useCallback(() => { dispatch( closeModal({ modalType: 'COMPOSER_DRAFT_DELETE', ignoreFocus: false }), diff --git a/app/javascript/mastodon/features/compose/redesign/header.tsx b/app/javascript/mastodon/features/compose/redesign/header.tsx index 029169c29dd..85edc35e59c 100644 --- a/app/javascript/mastodon/features/compose/redesign/header.tsx +++ b/app/javascript/mastodon/features/compose/redesign/header.tsx @@ -6,7 +6,7 @@ import { ArrowsOutSimpleIcon, MinusIcon, XIcon } from '@phosphor-icons/react'; import { IconButton } from '@/mastodon/components/button/redesign'; import { - hideComposer, + closeComposer, minimizeComposerToggle, selectIsMinimized, } from '@/mastodon/reducers/slices/composer'; @@ -63,7 +63,7 @@ export const ComposeFormHeader: React.FC<{ const dispatch = useAppDispatch(); const onClose = useCallback(() => { - dispatch(hideComposer()); + dispatch(closeComposer()); }, [dispatch]); const onMinimize = useCallback(() => { dispatch(minimizeComposerToggle()); diff --git a/app/javascript/mastodon/reducers/slices/composer.ts b/app/javascript/mastodon/reducers/slices/composer.ts index 3009ed149d2..ce2ea8a0609 100644 --- a/app/javascript/mastodon/reducers/slices/composer.ts +++ b/app/javascript/mastodon/reducers/slices/composer.ts @@ -65,6 +65,27 @@ const composerSlice = createSlice({ export const composer = composerSlice.reducer; export const { minimizeComposerToggle } = composerSlice.actions; +export const selectComposerIsChanged = createAppSelector( + [ + (state) => state.compose.get('text') as string, + (state) => state.compose.get('spoiler_text') as string, + (state) => !!state.compose.get('poll'), + (state) => !!state.compose.get('quoted_status_id'), + (state) => + state.compose.get( + 'media_attachments', + ) as unknown as Immutable.List, + (state) => Number(state.compose.get('pending_media_attachments')), + ], + (text, spoilerText, hasPoll, hasQuote, attachments, pendingAttachmentsNum) => + text.trim().length > 0 || + spoilerText.trim().length > 0 || + hasPoll || + hasQuote || + attachments.size > 0 || + pendingAttachmentsNum > 0, +); + interface ComposeNewPost { type?: 'post'; } @@ -76,10 +97,26 @@ interface ComposeNewMessage { type: 'message'; toAccountId?: string; } -type ComposeNewPayload = ComposeNewPost | ComposeNewReply | ComposeNewMessage; +type ComposeNewPayload = ( + | ComposeNewPost + | ComposeNewReply + | ComposeNewMessage +) & { force?: boolean }; export const openNewComposer = createAppThunk( (payload: ComposeNewPayload, { dispatch, getState }) => { + if (!payload.force && selectComposerIsChanged(getState())) { + dispatch( + openModal({ + modalType: 'COMPOSER_DRAFT_DELETE', + modalProps: { + openNew: true, + }, + }), + ); + return; + } + dispatch(resetCompose()); if (payload.type === 'message') { const account = @@ -93,6 +130,11 @@ export const openNewComposer = createAppThunk( dispatch(replyComposeById(payload.toStatusId)); } dispatch(composerSlice.actions.showComposer()); + + // Delay for a frame to ensure the DOM has updated. + requestAnimationFrame(() => { + focusComposerTextarea(); + }); }, ); @@ -101,15 +143,23 @@ export const resetComposer = createAppThunk((_arg, { dispatch }) => { dispatch(resetCompose()); }); -export const hideComposer = createAppThunk((_arg, { getState, dispatch }) => { - const compose = getState().compose; - const isChanged = - !!compose.get('text') || - !!compose.get('spoiler_text') || - !!compose.get('poll') || - (compose.get('media_attachments') as unknown as Immutable.List) - .size > 0 || - Number(compose.get('pending_media_attachments')) > 0; +export const closeComposer = createAppThunk((_arg, { getState, dispatch }) => { + const isChanged = selectComposerIsChanged(getState()); + + if (!isChanged) { + dispatch(resetComposer()); + } else { + dispatch( + openModal({ + modalType: 'COMPOSER_DRAFT_DELETE', + modalProps: {}, + }), + ); + } +}); + +export const newComposer = createAppThunk((_arg, { getState, dispatch }) => { + const isChanged = selectComposerIsChanged(getState()); if (!isChanged) { dispatch(resetComposer());