Fix draft item styling based on feedback
- Add border and shadow to draft cards - Remove trash button, move delete to overflow menu prompt - Remove size differences for thread posts (same avatar/text size) - Add spacing between header and first draft item - Change prompt wording to "Discard draft" Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import {useEffect, useState} from 'react'
|
import {useCallback, useEffect, useState} from 'react'
|
||||||
import {Image, Pressable, View} from 'react-native'
|
import {Image, Pressable, View} from 'react-native'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
@@ -12,7 +12,7 @@ import {UserAvatar} from '#/view/com/util/UserAvatar'
|
|||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonIcon} from '#/components/Button'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
import {DotGrid_Stroke2_Corner0_Rounded as DotsIcon} from '#/components/icons/DotGrid'
|
import {DotGrid_Stroke2_Corner0_Rounded as DotsIcon} from '#/components/icons/DotGrid'
|
||||||
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
// Platform-specific storage import
|
// Platform-specific storage import
|
||||||
@@ -24,53 +24,68 @@ export function DraftItem({
|
|||||||
draft,
|
draft,
|
||||||
onSelect,
|
onSelect,
|
||||||
onDelete,
|
onDelete,
|
||||||
isDeleting,
|
|
||||||
}: {
|
}: {
|
||||||
draft: DraftSummary
|
draft: DraftSummary
|
||||||
onSelect: (draft: DraftSummary) => void
|
onSelect: (draft: DraftSummary) => void
|
||||||
onDelete: (draftId: string) => void
|
onDelete: (draftId: string) => void
|
||||||
isDeleting: boolean
|
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const discardPromptControl = Prompt.usePromptControl()
|
||||||
|
|
||||||
|
const handleDelete = useCallback(() => {
|
||||||
|
onDelete(draft.id)
|
||||||
|
}, [onDelete, draft.id])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<>
|
||||||
accessibilityRole="button"
|
<Pressable
|
||||||
accessibilityLabel={_(msg`Open draft`)}
|
accessibilityRole="button"
|
||||||
accessibilityHint={_(msg`Opens this draft in the composer`)}
|
accessibilityLabel={_(msg`Open draft`)}
|
||||||
onPress={() => onSelect(draft)}
|
accessibilityHint={_(msg`Opens this draft in the composer`)}
|
||||||
style={({pressed, hovered}) => [
|
onPress={() => onSelect(draft)}
|
||||||
a.rounded_md,
|
style={({pressed, hovered}) => [
|
||||||
a.overflow_hidden,
|
a.rounded_md,
|
||||||
t.atoms.bg,
|
a.overflow_hidden,
|
||||||
(pressed || hovered) && t.atoms.bg_contrast_25,
|
a.border,
|
||||||
]}>
|
t.atoms.bg,
|
||||||
<View style={[a.p_md, a.gap_sm]}>
|
t.atoms.border_contrast_low,
|
||||||
{/* Reply indicator */}
|
t.atoms.shadow_sm,
|
||||||
{draft.isReply && draft.replyToHandle && (
|
(pressed || hovered) && t.atoms.bg_contrast_25,
|
||||||
<Text
|
]}>
|
||||||
style={[a.text_xs, t.atoms.text_contrast_medium, a.pb_2xs]}
|
<View style={[a.p_md, a.gap_sm]}>
|
||||||
numberOfLines={1}>
|
{/* Reply indicator */}
|
||||||
<Trans>Replying to @{draft.replyToHandle}</Trans>
|
{draft.isReply && draft.replyToHandle && (
|
||||||
</Text>
|
<Text
|
||||||
)}
|
style={[a.text_xs, t.atoms.text_contrast_medium, a.pb_2xs]}
|
||||||
|
numberOfLines={1}>
|
||||||
|
<Trans>Replying to @{draft.replyToHandle}</Trans>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Posts */}
|
{/* Posts */}
|
||||||
{draft.posts.map((post, index) => (
|
{draft.posts.map((post, index) => (
|
||||||
<DraftPostRow
|
<DraftPostRow
|
||||||
key={post.id}
|
key={post.id}
|
||||||
post={post}
|
post={post}
|
||||||
isFirst={index === 0}
|
isFirst={index === 0}
|
||||||
isLast={index === draft.posts.length - 1}
|
isLast={index === draft.posts.length - 1}
|
||||||
timestamp={draft.updatedAt}
|
timestamp={draft.updatedAt}
|
||||||
draftId={draft.id}
|
discardPromptControl={discardPromptControl}
|
||||||
onDelete={onDelete}
|
/>
|
||||||
isDeleting={isDeleting}
|
))}
|
||||||
/>
|
</View>
|
||||||
))}
|
</Pressable>
|
||||||
</View>
|
|
||||||
</Pressable>
|
<Prompt.Basic
|
||||||
|
control={discardPromptControl}
|
||||||
|
title={_(msg`Discard draft?`)}
|
||||||
|
description={_(msg`This draft will be permanently deleted.`)}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
confirmButtonCta={_(msg`Discard`)}
|
||||||
|
confirmButtonColor="negative"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,17 +94,13 @@ function DraftPostRow({
|
|||||||
isFirst,
|
isFirst,
|
||||||
isLast,
|
isLast,
|
||||||
timestamp,
|
timestamp,
|
||||||
draftId,
|
discardPromptControl,
|
||||||
onDelete,
|
|
||||||
isDeleting,
|
|
||||||
}: {
|
}: {
|
||||||
post: DraftPostDisplay
|
post: DraftPostDisplay
|
||||||
isFirst: boolean
|
isFirst: boolean
|
||||||
isLast: boolean
|
isLast: boolean
|
||||||
timestamp: string
|
timestamp: string
|
||||||
draftId: string
|
discardPromptControl: Prompt.PromptControlProps
|
||||||
onDelete: (draftId: string) => void
|
|
||||||
isDeleting: boolean
|
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -101,15 +112,11 @@ function DraftPostRow({
|
|||||||
const handle = profile?.handle || currentAccount?.handle || ''
|
const handle = profile?.handle || currentAccount?.handle || ''
|
||||||
const avatarUrl = profile?.avatar
|
const avatarUrl = profile?.avatar
|
||||||
|
|
||||||
// Nested posts (not first) have smaller styling
|
|
||||||
const avatarSize = isFirst ? 42 : 32
|
|
||||||
const textStyle = isFirst ? a.text_md : a.text_sm
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_row, a.gap_sm, !isFirst && [a.ml_xl, a.pt_xs]]}>
|
<View style={[a.flex_row, a.gap_sm]}>
|
||||||
{/* Avatar column with thread line */}
|
{/* Avatar column with thread line */}
|
||||||
<View style={[a.align_center]}>
|
<View style={[a.align_center]}>
|
||||||
<UserAvatar type="user" size={avatarSize} avatar={avatarUrl} />
|
<UserAvatar type="user" size={42} avatar={avatarUrl} />
|
||||||
{/* Thread line connecting posts */}
|
{/* Thread line connecting posts */}
|
||||||
{!isLast && (
|
{!isLast && (
|
||||||
<View
|
<View
|
||||||
@@ -133,23 +140,23 @@ function DraftPostRow({
|
|||||||
<View style={[a.flex_row, a.align_center, a.flex_1, a.gap_xs]}>
|
<View style={[a.flex_row, a.align_center, a.flex_1, a.gap_xs]}>
|
||||||
{displayName && (
|
{displayName && (
|
||||||
<Text
|
<Text
|
||||||
style={[textStyle, a.font_bold, t.atoms.text]}
|
style={[a.text_md, a.font_bold, t.atoms.text]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}>
|
||||||
{displayName}
|
{displayName}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<Text
|
<Text
|
||||||
style={[textStyle, t.atoms.text_contrast_medium]}
|
style={[a.text_md, t.atoms.text_contrast_medium]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}>
|
||||||
@{handle}
|
@{handle}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[textStyle, t.atoms.text_contrast_medium]}>
|
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
|
||||||
·
|
·
|
||||||
</Text>
|
</Text>
|
||||||
<TimeElapsed timestamp={timestamp}>
|
<TimeElapsed timestamp={timestamp}>
|
||||||
{({timeElapsed}) => (
|
{({timeElapsed}) => (
|
||||||
<Text
|
<Text
|
||||||
style={[textStyle, t.atoms.text_contrast_medium]}
|
style={[a.text_md, t.atoms.text_contrast_medium]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}>
|
||||||
{timeElapsed}
|
{timeElapsed}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -167,7 +174,7 @@ function DraftPostRow({
|
|||||||
size="tiny"
|
size="tiny"
|
||||||
onPress={e => {
|
onPress={e => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
// TODO: Show menu with options
|
discardPromptControl.open()
|
||||||
}}>
|
}}>
|
||||||
<ButtonIcon icon={DotsIcon} />
|
<ButtonIcon icon={DotsIcon} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -176,34 +183,15 @@ function DraftPostRow({
|
|||||||
|
|
||||||
{/* Post text - full, not truncated */}
|
{/* Post text - full, not truncated */}
|
||||||
{post.text ? (
|
{post.text ? (
|
||||||
<Text style={[textStyle, t.atoms.text]}>{post.text}</Text>
|
<Text style={[a.text_md, t.atoms.text]}>{post.text}</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text style={[textStyle, t.atoms.text_contrast_medium, a.italic]}>
|
<Text style={[a.text_md, t.atoms.text_contrast_medium, a.italic]}>
|
||||||
<Trans>(No text)</Trans>
|
<Trans>(No text)</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Media preview */}
|
{/* Media preview */}
|
||||||
<DraftMediaPreview post={post} />
|
<DraftMediaPreview post={post} />
|
||||||
|
|
||||||
{/* Delete button (only on last post) */}
|
|
||||||
{isLast && (
|
|
||||||
<View style={[a.flex_row, a.justify_end, a.pt_xs]}>
|
|
||||||
<Button
|
|
||||||
label={_(msg`Delete draft`)}
|
|
||||||
variant="ghost"
|
|
||||||
color="negative"
|
|
||||||
shape="round"
|
|
||||||
size="small"
|
|
||||||
disabled={isDeleting}
|
|
||||||
onPress={e => {
|
|
||||||
e.stopPropagation()
|
|
||||||
onDelete(draftId)
|
|
||||||
}}>
|
|
||||||
<ButtonIcon icon={TrashIcon} />
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function DraftsListDialog({
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {data: drafts, isLoading} = useDrafts()
|
const {data: drafts, isLoading} = useDrafts()
|
||||||
const loadDraft = useLoadDraft()
|
const loadDraft = useLoadDraft()
|
||||||
const {mutate: deleteDraft, isPending: isDeleting} = useDeleteDraft()
|
const {mutate: deleteDraft} = useDeleteDraft()
|
||||||
|
|
||||||
const handleSelectDraft = useCallback(
|
const handleSelectDraft = useCallback(
|
||||||
async (summary: DraftSummary) => {
|
async (summary: DraftSummary) => {
|
||||||
@@ -68,11 +68,15 @@ export function DraftsListDialog({
|
|||||||
|
|
||||||
const listHeader = useMemo(() => {
|
const listHeader = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
<Dialog.Header renderLeft={backButton}>
|
<View>
|
||||||
<Dialog.HeaderText>
|
<Dialog.Header renderLeft={backButton}>
|
||||||
<Trans>Drafts</Trans>
|
<Dialog.HeaderText>
|
||||||
</Dialog.HeaderText>
|
<Trans>Drafts</Trans>
|
||||||
</Dialog.Header>
|
</Dialog.HeaderText>
|
||||||
|
</Dialog.Header>
|
||||||
|
{/* Spacer between header and first item */}
|
||||||
|
<View style={[{height: 12}]} />
|
||||||
|
</View>
|
||||||
)
|
)
|
||||||
}, [backButton])
|
}, [backButton])
|
||||||
|
|
||||||
@@ -84,12 +88,11 @@ export function DraftsListDialog({
|
|||||||
draft={item}
|
draft={item}
|
||||||
onSelect={handleSelectDraft}
|
onSelect={handleSelectDraft}
|
||||||
onDelete={handleDeleteDraft}
|
onDelete={handleDeleteDraft}
|
||||||
isDeleting={isDeleting}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[handleSelectDraft, handleDeleteDraft, isDeleting],
|
[handleSelectDraft, handleDeleteDraft],
|
||||||
)
|
)
|
||||||
|
|
||||||
const itemSeparator = useCallback(() => <View style={[{height: 12}]} />, [])
|
const itemSeparator = useCallback(() => <View style={[{height: 12}]} />, [])
|
||||||
@@ -102,7 +105,13 @@ export function DraftsListDialog({
|
|||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return <EmptyState icon={PageXIcon} message={_(msg`No drafts yet`)} />
|
return (
|
||||||
|
<EmptyState
|
||||||
|
icon={PageXIcon}
|
||||||
|
message={_(msg`No drafts yet`)}
|
||||||
|
style={[a.justify_center, {minHeight: 500}]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}, [isLoading, _])
|
}, [isLoading, _])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -115,8 +124,8 @@ export function DraftsListDialog({
|
|||||||
ListEmptyComponent={emptyComponent}
|
ListEmptyComponent={emptyComponent}
|
||||||
ItemSeparatorComponent={itemSeparator}
|
ItemSeparatorComponent={itemSeparator}
|
||||||
stickyHeaderIndices={[0]}
|
stickyHeaderIndices={[0]}
|
||||||
style={t.atoms.bg_contrast_25}
|
style={[t.atoms.bg_contrast_50, a.px_0]}
|
||||||
contentContainerStyle={[a.pb_lg]}
|
webInnerContentContainerStyle={[a.py_0]}
|
||||||
/>
|
/>
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -95,12 +95,13 @@ export function EmptyState({
|
|||||||
a.leading_snug,
|
a.leading_snug,
|
||||||
a.text_center,
|
a.text_center,
|
||||||
a.self_center,
|
a.self_center,
|
||||||
|
!button && a.mb_5xl,
|
||||||
textStyle,
|
textStyle,
|
||||||
]}>
|
]}>
|
||||||
{message}
|
{message}
|
||||||
</Text>
|
</Text>
|
||||||
{button && (
|
{button && (
|
||||||
<View style={[a.flex_shrink, a.mt_xl, a.self_center]}>
|
<View style={[a.flex_shrink, a.mt_xl, a.self_center, a.mb_5xl]}>
|
||||||
<Button {...button}>
|
<Button {...button}>
|
||||||
<ButtonText>{button.text}</ButtonText>
|
<ButtonText>{button.text}</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user