From 4eb064b6a4358a525b204b3497cabb1c9789b9c0 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 13 Jan 2026 13:51:38 +0200 Subject: [PATCH] Fix drafts button: always visible, adjacent to post button - Make drafts button always visible (not just when drafts exist) - Move button to be adjacent to the publish button - If composer is empty: opens drafts list directly - If composer has content: shows prompt to save/discard before viewing drafts - Add badge showing draft count when drafts exist Co-Authored-By: Claude Opus 4.5 --- .../drafts/__tests__/serialization.test.ts | 4 +- src/view/com/composer/Composer.tsx | 40 +++++- src/view/com/composer/drafts/DraftsButton.tsx | 115 +++++++++++++----- 3 files changed, 127 insertions(+), 32 deletions(-) diff --git a/src/state/drafts/__tests__/serialization.test.ts b/src/state/drafts/__tests__/serialization.test.ts index 206ca340de..c80c387c9c 100644 --- a/src/state/drafts/__tests__/serialization.test.ts +++ b/src/state/drafts/__tests__/serialization.test.ts @@ -416,7 +416,7 @@ describe('Draft serialization', () => { labels: [], }, ], - threadgate: ['nobody'], + threadgate: [{type: 'nobody'}], syncStatus: 'local', } @@ -428,7 +428,7 @@ describe('Draft serialization', () => { loadedMedia, }) - expect(newState.thread.threadgate).toEqual(['nobody']) + expect(newState.thread.threadgate).toEqual([{type: 'nobody'}]) }) it('restores reply information', () => { diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 6772cdbe56..0e0c773e75 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -360,6 +360,32 @@ export const ComposePost = ({ } }, [saveDraft, composerState, replyTo, onClose, _]) + // Save without closing - for use by DraftsButton + const saveCurrentDraft = React.useCallback(async () => { + await saveDraft({ + composerState, + replyTo, + }) + }, [saveDraft, composerState, replyTo]) + + // Check if composer is empty (no content to save) + const isComposerEmpty = React.useMemo(() => { + // Has multiple posts means it's not empty + if (thread.posts.length > 1) return false + + const firstPost = thread.posts[0] + // Has text + if (firstPost.richtext.text.trim().length > 0) return false + // Has media + if (firstPost.embed.media) return false + // Has quote + if (firstPost.embed.quote) return false + // Has link + if (firstPost.embed.link) return false + + return true + }, [thread.posts]) + const insets = useSafeAreaInsets() const viewStyles = useMemo( () => ({ @@ -784,7 +810,9 @@ export const ComposePost = ({ topBarAnimatedStyle={topBarAnimatedStyle} onCancel={onPressCancel} onPublish={onPressPublish} - onSelectDraft={handleSelectDraft}> + onSelectDraft={handleSelectDraft} + onSaveDraft={saveCurrentDraft} + isEmpty={isComposerEmpty}> {missingAltError && } void onPublish: () => void onSelectDraft: (draft: StoredDraft) => void + onSaveDraft: () => Promise + isEmpty: boolean topBarAnimatedStyle: StyleProp children?: React.ReactNode }) { @@ -1110,8 +1142,12 @@ function ComposerTopBar({ Cancel - + {isPublishing ? ( <> {publishingStage} diff --git a/src/view/com/composer/drafts/DraftsButton.tsx b/src/view/com/composer/drafts/DraftsButton.tsx index b7fab0a4ae..b27b225103 100644 --- a/src/view/com/composer/drafts/DraftsButton.tsx +++ b/src/view/com/composer/drafts/DraftsButton.tsx @@ -2,57 +2,116 @@ import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {type StoredDraft, useDrafts} from '#/state/drafts' +import {type StoredDraft, useDrafts, useSaveDraft} from '#/state/drafts' import {atoms as a, useTheme} from '#/alf' -import {Button, ButtonText} from '#/components/Button' +import {Button, ButtonIcon} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {PageText_Stroke2_Corner0_Rounded as DraftIcon} from '#/components/icons/PageText' +import * as Prompt from '#/components/Prompt' import {Text} from '#/components/Typography' import {DraftsListDialog} from './DraftsListDialog' export function DraftsButton({ onSelectDraft, + onSaveDraft, + isEmpty, }: { onSelectDraft: (draft: StoredDraft) => void + onSaveDraft: () => Promise + isEmpty: boolean }) { const {_} = useLingui() const t = useTheme() - const control = Dialog.useDialogControl() - const {data: drafts, isLoading} = useDrafts() + const draftsDialogControl = Dialog.useDialogControl() + const savePromptControl = Prompt.usePromptControl() + const {data: drafts} = useDrafts() + const {isPending: isSaving} = useSaveDraft() - const hasDrafts = drafts && drafts.length > 0 + const draftCount = drafts?.length ?? 0 - if (isLoading || !hasDrafts) { - return null + const handlePress = () => { + if (isEmpty) { + // Composer is empty, go directly to drafts list + draftsDialogControl.open() + } else { + // Composer has content, ask what to do + savePromptControl.open() + } + } + + const handleSaveAndOpen = async () => { + await onSaveDraft() + draftsDialogControl.open() + } + + const handleDiscardAndOpen = () => { + draftsDialogControl.open() } return ( <> - + + + + + + Save current draft? + + + + You have unsaved changes. Would you like to save them before viewing + your drafts? + + + + + + + + ) }