Track draft ID in composer state machine

Adds draftId to ComposerState so that editing an existing draft and
saving it again updates the draft rather than creating a new one.

- Add draftId?: string to ComposerState type
- Set draftId when restoring from draft via restore_from_draft action
- Pass existingDraftId to save functions from composerState.draftId
- Add PageX icon for empty drafts state

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-01-13 16:25:42 +02:00
parent ead99f123b
commit 08e83b6259
6 changed files with 29 additions and 20 deletions
+3 -1
View File
@@ -330,7 +330,7 @@ export const ComposePost = ({
// Load media from local storage
const loadedMedia = await loadDraftMedia(currentDid, draft)
// Dispatch restore action
// Dispatch restore action (this also sets draftId in state)
composerDispatch({
type: 'restore_from_draft',
draft,
@@ -352,6 +352,7 @@ export const ComposePost = ({
await saveDraft({
composerState,
replyTo,
existingDraftId: composerState.draftId,
})
onClose()
} catch (e) {
@@ -365,6 +366,7 @@ export const ComposePost = ({
await saveDraft({
composerState,
replyTo,
existingDraftId: composerState.draftId,
})
}, [saveDraft, composerState, replyTo])
@@ -74,12 +74,12 @@ export function DraftsButton({
</Prompt.DescriptionText>
<Prompt.Actions>
<Prompt.Action
cta={_(msg`Save & View Drafts`)}
cta={_(msg`Save changes`)}
onPress={handleSaveAndOpen}
color="primary"
/>
<Prompt.Action
cta={_(msg`Discard & View Drafts`)}
cta={_(msg`Discard`)}
onPress={handleDiscardAndOpen}
color="negative"
/>
@@ -11,11 +11,11 @@ import {
useLoadDraft,
} from '#/state/drafts'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow'
import {PageX_Stroke2_Corner0_Rounded_Large} from '#/components/icons/PageX'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {EmptyState} from '../../util/EmptyState'
import {DraftItem} from './DraftItem'
export function DraftsListDialog({
@@ -57,9 +57,10 @@ export function DraftsListDialog({
onPress={() => control.close()}
size="small"
color="primary"
variant="ghost"
shape="round">
<ButtonIcon icon={ArrowLeftIcon} size="md" />
variant="ghost">
<ButtonText style={[a.text_md]}>
<Trans>Back</Trans>
</ButtonText>
</Button>
),
[control, _],
@@ -98,17 +99,15 @@ export function DraftsListDialog({
)
}
return (
<View style={[a.py_xl, a.align_center]}>
<Text style={[t.atoms.text_contrast_medium]}>
<Trans>No drafts saved</Trans>
</Text>
</View>
<EmptyState
icon={PageX_Stroke2_Corner0_Rounded_Large}
message={_(msg`No drafts yet`)}
/>
)
}, [isLoading, t.atoms.text_contrast_medium])
}, [isLoading, _])
return (
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
<Dialog.Handle />
<Dialog.Outer control={control}>
<Dialog.InnerFlatList
data={drafts ?? []}
renderItem={renderItem}
@@ -116,9 +115,7 @@ export function DraftsListDialog({
ListHeaderComponent={listHeader}
ListEmptyComponent={emptyComponent}
stickyHeaderIndices={[0]}
contentContainerStyle={[a.pb_lg]}
ItemSeparatorComponent={() => <View style={[{height: 8}]} />}
style={[a.px_lg]}
style={t.atoms.bg_contrast_25}
/>
</Dialog.Outer>
)
+3
View File
@@ -102,6 +102,8 @@ export type ComposerState = {
thread: ThreadDraft
activePostIndex: number
mutableNeedsFocusActive: boolean
/** ID of the draft being edited, if any. Used to update existing draft on save. */
draftId?: string
}
export type ComposerAction =
@@ -299,6 +301,7 @@ export function composerReducer(
return {
activePostIndex: 0,
mutableNeedsFocusActive: true,
draftId: draft.id,
thread: {
posts,
postgate: draft.postgate || state.thread.postgate,