From 092528dc0552e7d3490f55e30c1e5fab19ecf4e8 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 13 Jan 2026 16:34:21 +0200 Subject: [PATCH] Track dirty state to skip discard prompt for unchanged drafts When a draft is loaded and the user hasn't made any changes, closing the composer should not show the discard prompt since nothing would be lost. - Add isDirty field to ComposerState - Set isDirty: true on all content-modifying actions - Set isDirty: false on restore_from_draft, clear, and initial state - Update onPressCancel to only show prompt if no draft or isDirty Co-Authored-By: Claude Opus 4.5 --- src/view/com/composer/Composer.tsx | 28 +++++++++++++++++-------- src/view/com/composer/state/composer.ts | 10 +++++++++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index c91a4eeab5..ac3033dd34 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -413,21 +413,31 @@ export const ComposePost = ({ const onPressCancel = useCallback(() => { if (textInput.current?.maybeClosePopup()) { return - } else if ( - thread.posts.some( - post => - post.shortenedGraphemeLength > 0 || - post.embed.media || - post.embed.link, - ) - ) { + } + + const hasContent = thread.posts.some( + post => + post.shortenedGraphemeLength > 0 || post.embed.media || post.embed.link, + ) + + // Show discard prompt if there's content AND either: + // - No draft is loaded (new composition) + // - Draft is loaded but has been modified + if (hasContent && (!composerState.draftId || composerState.isDirty)) { closeAllDialogs() Keyboard.dismiss() discardPromptControl.open() } else { onClose() } - }, [thread, closeAllDialogs, discardPromptControl, onClose]) + }, [ + thread, + composerState.draftId, + composerState.isDirty, + closeAllDialogs, + discardPromptControl, + onClose, + ]) useImperativeHandle(cancelRef, () => ({onPressCancel})) diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts index 17d76ddd84..8ac26d1feb 100644 --- a/src/view/com/composer/state/composer.ts +++ b/src/view/com/composer/state/composer.ts @@ -104,6 +104,8 @@ export type ComposerState = { mutableNeedsFocusActive: boolean /** ID of the draft being edited, if any. Used to update existing draft on save. */ draftId?: string + /** Whether the composer has been modified since loading a draft. */ + isDirty: boolean } export type ComposerAction = @@ -145,6 +147,7 @@ export function composerReducer( case 'update_postgate': { return { ...state, + isDirty: true, thread: { ...state.thread, postgate: action.postgate, @@ -154,6 +157,7 @@ export function composerReducer( case 'update_threadgate': { return { ...state, + isDirty: true, thread: { ...state.thread, threadgate: action.threadgate, @@ -174,6 +178,7 @@ export function composerReducer( } return { ...state, + isDirty: true, thread: { ...state.thread, posts: nextPosts, @@ -196,6 +201,7 @@ export function composerReducer( }) return { ...state, + isDirty: true, thread: { ...state.thread, posts: nextPosts, @@ -221,6 +227,7 @@ export function composerReducer( } return { ...state, + isDirty: true, activePostIndex: nextActivePostIndex, mutableNeedsFocusActive: true, thread: { @@ -305,6 +312,7 @@ export function composerReducer( activePostIndex: 0, mutableNeedsFocusActive: true, draftId: draft.id, + isDirty: false, thread: { posts, postgate: draft.postgate || state.thread.postgate, @@ -317,6 +325,7 @@ export function composerReducer( activePostIndex: 0, mutableNeedsFocusActive: true, draftId: undefined, + isDirty: false, thread: { posts: [ { @@ -699,6 +708,7 @@ export function createComposerState({ return { activePostIndex: 0, mutableNeedsFocusActive: false, + isDirty: false, thread: { posts: [ {