Add clear action to discard composer content

When pressing the Drafts button with content in the composer, the user
can choose to discard. This now properly clears the composer by
dispatching a 'clear' action that resets to an empty state.

- Add 'clear' action type to ComposerAction
- Implement clear case in composerReducer (resets to single empty post)
- Add handleClearComposer callback in Composer.tsx
- Pass onDiscard prop through ComposerTopBar to DraftsButton
- Call onDiscard before opening drafts dialog on discard

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-01-13 16:30:03 +02:00
parent 08e83b6259
commit 10d80dab7b
3 changed files with 41 additions and 1 deletions
+10 -1
View File
@@ -388,6 +388,11 @@ export const ComposePost = ({
return true return true
}, [thread.posts]) }, [thread.posts])
// Clear the composer (discard current content)
const handleClearComposer = React.useCallback(() => {
composerDispatch({type: 'clear'})
}, [composerDispatch])
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const viewStyles = useMemo( const viewStyles = useMemo(
() => ({ () => ({
@@ -814,6 +819,7 @@ export const ComposePost = ({
onPublish={onPressPublish} onPublish={onPressPublish}
onSelectDraft={handleSelectDraft} onSelectDraft={handleSelectDraft}
onSaveDraft={saveCurrentDraft} onSaveDraft={saveCurrentDraft}
onDiscard={handleClearComposer}
isEmpty={isComposerEmpty}> isEmpty={isComposerEmpty}>
{missingAltError && <AltTextReminder error={missingAltError} />} {missingAltError && <AltTextReminder error={missingAltError} />}
<ErrorBanner <ErrorBanner
@@ -998,7 +1004,7 @@ let ComposerPost = React.memo(function ComposerPost({
a.mb_sm, a.mb_sm,
!isActive && isLastPost && a.mb_lg, !isActive && isLastPost && a.mb_lg,
!isActive && styles.inactivePost, !isActive && styles.inactivePost,
isTextOnly && IS_NATIVE && a.flex_grow, isTextOnly && isLastPost && IS_NATIVE && a.flex_grow,
]}> ]}>
<View style={[a.flex_row, IS_NATIVE && a.flex_1]}> <View style={[a.flex_row, IS_NATIVE && a.flex_1]}>
<UserAvatar <UserAvatar
@@ -1104,6 +1110,7 @@ function ComposerTopBar({
onPublish, onPublish,
onSelectDraft, onSelectDraft,
onSaveDraft, onSaveDraft,
onDiscard,
isEmpty, isEmpty,
topBarAnimatedStyle, topBarAnimatedStyle,
children, children,
@@ -1118,6 +1125,7 @@ function ComposerTopBar({
onPublish: () => void onPublish: () => void
onSelectDraft: (draft: StoredDraft) => void onSelectDraft: (draft: StoredDraft) => void
onSaveDraft: () => Promise<void> onSaveDraft: () => Promise<void>
onDiscard: () => void
isEmpty: boolean isEmpty: boolean
topBarAnimatedStyle: StyleProp<ViewStyle> topBarAnimatedStyle: StyleProp<ViewStyle>
children?: React.ReactNode children?: React.ReactNode
@@ -1148,6 +1156,7 @@ function ComposerTopBar({
<DraftsButton <DraftsButton
onSelectDraft={onSelectDraft} onSelectDraft={onSelectDraft}
onSaveDraft={onSaveDraft} onSaveDraft={onSaveDraft}
onDiscard={onDiscard}
isEmpty={isEmpty} isEmpty={isEmpty}
/> />
{isPublishing ? ( {isPublishing ? (
@@ -11,10 +11,12 @@ import {DraftsListDialog} from './DraftsListDialog'
export function DraftsButton({ export function DraftsButton({
onSelectDraft, onSelectDraft,
onSaveDraft, onSaveDraft,
onDiscard,
isEmpty, isEmpty,
}: { }: {
onSelectDraft: (draft: StoredDraft) => void onSelectDraft: (draft: StoredDraft) => void
onSaveDraft: () => Promise<void> onSaveDraft: () => Promise<void>
onDiscard: () => void
isEmpty: boolean isEmpty: boolean
}) { }) {
const {_} = useLingui() const {_} = useLingui()
@@ -38,6 +40,7 @@ export function DraftsButton({
} }
const handleDiscardAndOpen = () => { const handleDiscardAndOpen = () => {
onDiscard()
draftsDialogControl.open() draftsDialogControl.open()
} }
+28
View File
@@ -131,6 +131,9 @@ export type ComposerAction =
/** Map of localId -> loaded media path/URL */ /** Map of localId -> loaded media path/URL */
loadedMedia: Map<string, string> loadedMedia: Map<string, string>
} }
| {
type: 'clear'
}
export const MAX_IMAGES = 4 export const MAX_IMAGES = 4
@@ -309,6 +312,31 @@ export function composerReducer(
}, },
} }
} }
case 'clear': {
return {
activePostIndex: 0,
mutableNeedsFocusActive: true,
draftId: undefined,
thread: {
posts: [
{
id: nanoid(),
richtext: new RichText({text: ''}),
shortenedGraphemeLength: 0,
labels: [],
embed: {
quote: undefined,
media: undefined,
link: undefined,
},
},
],
// Keep the user's default interaction settings
postgate: state.thread.postgate,
threadgate: state.thread.threadgate,
},
}
}
} }
} }