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 <noreply@anthropic.com>
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
@@ -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 && <AltTextReminder error={missingAltError} />}
|
||||
<ErrorBanner
|
||||
error={error}
|
||||
@@ -1073,6 +1101,8 @@ function ComposerTopBar({
|
||||
onCancel,
|
||||
onPublish,
|
||||
onSelectDraft,
|
||||
onSaveDraft,
|
||||
isEmpty,
|
||||
topBarAnimatedStyle,
|
||||
children,
|
||||
}: {
|
||||
@@ -1085,6 +1115,8 @@ function ComposerTopBar({
|
||||
onCancel: () => void
|
||||
onPublish: () => void
|
||||
onSelectDraft: (draft: StoredDraft) => void
|
||||
onSaveDraft: () => Promise<void>
|
||||
isEmpty: boolean
|
||||
topBarAnimatedStyle: StyleProp<ViewStyle>
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
@@ -1110,8 +1142,12 @@ function ComposerTopBar({
|
||||
<Trans>Cancel</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<DraftsButton onSelectDraft={onSelectDraft} />
|
||||
<View style={a.flex_1} />
|
||||
<DraftsButton
|
||||
onSelectDraft={onSelectDraft}
|
||||
onSaveDraft={onSaveDraft}
|
||||
isEmpty={isEmpty}
|
||||
/>
|
||||
{isPublishing ? (
|
||||
<>
|
||||
<Text style={pal.textLight}>{publishingStage}</Text>
|
||||
|
||||
@@ -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<void>
|
||||
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 (
|
||||
<>
|
||||
<Button
|
||||
label={_(msg`See drafts`)}
|
||||
label={_(msg`Drafts`)}
|
||||
variant="ghost"
|
||||
color="primary"
|
||||
shape="default"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
size="small"
|
||||
style={[a.rounded_full, a.py_xs, a.px_sm, a.ml_xs]}
|
||||
onPress={() => control.open()}>
|
||||
<DraftIcon size="sm" style={[t.atoms.text_contrast_medium]} />
|
||||
<ButtonText style={[a.text_sm]}>
|
||||
<Trans>Drafts</Trans>
|
||||
</ButtonText>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_full,
|
||||
a.px_xs,
|
||||
a.ml_2xs,
|
||||
{backgroundColor: t.palette.primary_500},
|
||||
]}>
|
||||
<Text style={[a.text_xs, a.font_bold, {color: t.palette.white}]}>
|
||||
{drafts.length}
|
||||
</Text>
|
||||
</View>
|
||||
style={[a.mx_xs]}
|
||||
disabled={isSaving}
|
||||
onPress={handlePress}>
|
||||
<ButtonIcon icon={DraftIcon} />
|
||||
{draftCount > 0 && (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.rounded_full,
|
||||
{
|
||||
top: -2,
|
||||
right: -2,
|
||||
minWidth: 16,
|
||||
height: 16,
|
||||
backgroundColor: t.palette.primary_500,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
]}>
|
||||
<Text style={[a.text_2xs, a.font_bold, {color: t.palette.white}]}>
|
||||
{draftCount}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</Button>
|
||||
<DraftsListDialog control={control} onSelectDraft={onSelectDraft} />
|
||||
|
||||
<DraftsListDialog
|
||||
control={draftsDialogControl}
|
||||
onSelectDraft={onSelectDraft}
|
||||
/>
|
||||
|
||||
<Prompt.Outer control={savePromptControl}>
|
||||
<Prompt.TitleText>
|
||||
<Trans>Save current draft?</Trans>
|
||||
</Prompt.TitleText>
|
||||
<Prompt.DescriptionText>
|
||||
<Trans>
|
||||
You have unsaved changes. Would you like to save them before viewing
|
||||
your drafts?
|
||||
</Trans>
|
||||
</Prompt.DescriptionText>
|
||||
<Prompt.Actions>
|
||||
<Prompt.Action
|
||||
cta={_(msg`Save & View Drafts`)}
|
||||
onPress={handleSaveAndOpen}
|
||||
color="primary"
|
||||
/>
|
||||
<Prompt.Action
|
||||
cta={_(msg`Discard & View Drafts`)}
|
||||
onPress={handleDiscardAndOpen}
|
||||
color="negative"
|
||||
/>
|
||||
<Prompt.Cancel />
|
||||
</Prompt.Actions>
|
||||
</Prompt.Outer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user