Redesign drafts list to show full thread preview
- Display all posts in a draft thread, not just the first - First post uses larger avatar (42px), subsequent posts nested with smaller avatar (32px) and thread connector line - Show author avatar, display name, handle, and relative timestamp - Add overflow menu button (placeholder) on first post - Display full text instead of truncated preview - Add media preview component for images, GIFs, and videos - Card layout with rounded corners and proper spacing - Add gap separators between draft cards in list Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -92,6 +92,21 @@ export type StoredDraft = {
|
|||||||
syncStatus: 'local' | 'synced' | 'dirty'
|
syncStatus: 'local' | 'synced' | 'dirty'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post content for display in draft list
|
||||||
|
*/
|
||||||
|
export type DraftPostDisplay = {
|
||||||
|
id: string
|
||||||
|
/** Full text content */
|
||||||
|
text: string
|
||||||
|
/** Image URLs for display (local IDs that need to be loaded) */
|
||||||
|
images?: LocalMediaRef[]
|
||||||
|
/** Video reference */
|
||||||
|
video?: LocalMediaRef
|
||||||
|
/** GIF metadata */
|
||||||
|
gif?: StoredGif
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draft summary for list display
|
* Draft summary for list display
|
||||||
*/
|
*/
|
||||||
@@ -111,4 +126,6 @@ export type DraftSummary = {
|
|||||||
replyToHandle?: string
|
replyToHandle?: string
|
||||||
/** ISO timestamp of last update */
|
/** ISO timestamp of last update */
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
|
/** All posts in the draft for full display */
|
||||||
|
posts: DraftPostDisplay[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import {
|
|||||||
import {nanoid} from 'nanoid/non-secure'
|
import {nanoid} from 'nanoid/non-secure'
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {type DraftSummary, type StoredDraft} from './schema'
|
import {
|
||||||
|
type DraftPostDisplay,
|
||||||
|
type DraftSummary,
|
||||||
|
type StoredDraft,
|
||||||
|
} from './schema'
|
||||||
|
|
||||||
const DRAFTS_DIR = 'bsky-drafts'
|
const DRAFTS_DIR = 'bsky-drafts'
|
||||||
|
|
||||||
@@ -271,6 +275,8 @@ function createDraftSummary(draft: StoredDraft): DraftSummary {
|
|||||||
let mediaCount = 0
|
let mediaCount = 0
|
||||||
let hasMedia = false
|
let hasMedia = false
|
||||||
|
|
||||||
|
const posts: DraftPostDisplay[] = []
|
||||||
|
|
||||||
for (const post of draft.posts) {
|
for (const post of draft.posts) {
|
||||||
if (post.images) {
|
if (post.images) {
|
||||||
mediaCount += post.images.length
|
mediaCount += post.images.length
|
||||||
@@ -284,6 +290,14 @@ function createDraftSummary(draft: StoredDraft): DraftSummary {
|
|||||||
mediaCount += 1
|
mediaCount += 1
|
||||||
hasMedia = true
|
hasMedia = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
posts.push({
|
||||||
|
id: post.id,
|
||||||
|
text: post.richtext.text,
|
||||||
|
images: post.images,
|
||||||
|
video: post.video,
|
||||||
|
gif: post.gif,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -295,6 +309,7 @@ function createDraftSummary(draft: StoredDraft): DraftSummary {
|
|||||||
isReply: Boolean(draft.replyToUri),
|
isReply: Boolean(draft.replyToUri),
|
||||||
replyToHandle: draft.replyToAuthor?.handle,
|
replyToHandle: draft.replyToAuthor?.handle,
|
||||||
updatedAt: draft.updatedAt,
|
updatedAt: draft.updatedAt,
|
||||||
|
posts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import {type DBSchema, type IDBPDatabase, openDB} from 'idb'
|
|||||||
import {nanoid} from 'nanoid/non-secure'
|
import {nanoid} from 'nanoid/non-secure'
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {type DraftSummary, type StoredDraft} from './schema'
|
import {
|
||||||
|
type DraftPostDisplay,
|
||||||
|
type DraftSummary,
|
||||||
|
type StoredDraft,
|
||||||
|
} from './schema'
|
||||||
|
|
||||||
const DB_NAME = 'bsky-drafts'
|
const DB_NAME = 'bsky-drafts'
|
||||||
const DB_VERSION = 1
|
const DB_VERSION = 1
|
||||||
@@ -287,6 +291,8 @@ function createDraftSummary(draft: StoredDraft): DraftSummary {
|
|||||||
let mediaCount = 0
|
let mediaCount = 0
|
||||||
let hasMedia = false
|
let hasMedia = false
|
||||||
|
|
||||||
|
const posts: DraftPostDisplay[] = []
|
||||||
|
|
||||||
for (const post of draft.posts) {
|
for (const post of draft.posts) {
|
||||||
if (post.images) {
|
if (post.images) {
|
||||||
mediaCount += post.images.length
|
mediaCount += post.images.length
|
||||||
@@ -300,6 +306,14 @@ function createDraftSummary(draft: StoredDraft): DraftSummary {
|
|||||||
mediaCount += 1
|
mediaCount += 1
|
||||||
hasMedia = true
|
hasMedia = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
posts.push({
|
||||||
|
id: post.id,
|
||||||
|
text: post.richtext.text,
|
||||||
|
images: post.images,
|
||||||
|
video: post.video,
|
||||||
|
gif: post.gif,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -311,6 +325,7 @@ function createDraftSummary(draft: StoredDraft): DraftSummary {
|
|||||||
isReply: Boolean(draft.replyToUri),
|
isReply: Boolean(draft.replyToUri),
|
||||||
replyToHandle: draft.replyToAuthor?.handle,
|
replyToHandle: draft.replyToAuthor?.handle,
|
||||||
updatedAt: draft.updatedAt,
|
updatedAt: draft.updatedAt,
|
||||||
|
posts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
import {Pressable, View} from 'react-native'
|
import {useEffect, useState} from 'react'
|
||||||
|
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'
|
||||||
|
|
||||||
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
import {isNative} from '#/platform/detection'
|
||||||
import {type DraftSummary} from '#/state/drafts'
|
import {type DraftPostDisplay, type DraftSummary} from '#/state/drafts'
|
||||||
|
import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile'
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
|
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
|
||||||
|
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 {Camera_Stroke2_Corner0_Rounded as MediaIcon} from '#/components/icons/Camera'
|
import {DotGrid_Stroke2_Corner0_Rounded as DotsIcon} from '#/components/icons/DotGrid'
|
||||||
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
|
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
// Platform-specific storage import
|
||||||
|
const storage = isNative
|
||||||
|
? require('#/state/drafts/storage')
|
||||||
|
: require('#/state/drafts/storage.web')
|
||||||
|
|
||||||
export function DraftItem({
|
export function DraftItem({
|
||||||
draft,
|
draft,
|
||||||
onSelect,
|
onSelect,
|
||||||
@@ -23,81 +33,286 @@ export function DraftItem({
|
|||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const getTimeAgo = useGetTimeAgo()
|
|
||||||
|
|
||||||
const previewText = draft.previewText || _(msg`(No text)`)
|
|
||||||
const timeAgo = getTimeAgo(new Date(draft.updatedAt), new Date())
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={_(msg`Open draft: ${previewText}`)}
|
accessibilityLabel={_(msg`Open draft`)}
|
||||||
accessibilityHint={_(msg`Opens this draft in the composer`)}
|
accessibilityHint={_(msg`Opens this draft in the composer`)}
|
||||||
onPress={() => onSelect(draft)}
|
onPress={() => onSelect(draft)}
|
||||||
style={({pressed, hovered}) => [
|
style={({pressed, hovered}) => [
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
a.gap_md,
|
|
||||||
a.p_md,
|
|
||||||
a.rounded_md,
|
a.rounded_md,
|
||||||
t.atoms.bg_contrast_25,
|
a.overflow_hidden,
|
||||||
(pressed || hovered) && t.atoms.bg_contrast_50,
|
t.atoms.bg,
|
||||||
|
(pressed || hovered) && t.atoms.bg_contrast_25,
|
||||||
]}>
|
]}>
|
||||||
<View style={[a.flex_1, a.gap_xs]}>
|
<View style={[a.p_md, a.gap_sm]}>
|
||||||
{/* Reply indicator */}
|
{/* Reply indicator */}
|
||||||
{draft.isReply && draft.replyToHandle && (
|
{draft.isReply && draft.replyToHandle && (
|
||||||
<Text
|
<Text
|
||||||
style={[a.text_xs, t.atoms.text_contrast_medium]}
|
style={[a.text_xs, t.atoms.text_contrast_medium, a.pb_2xs]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}>
|
||||||
<Trans>Replying to @{draft.replyToHandle}</Trans>
|
<Trans>Replying to @{draft.replyToHandle}</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Preview text */}
|
{/* Posts */}
|
||||||
<Text style={[a.text_md]} numberOfLines={2}>
|
{draft.posts.map((post, index) => (
|
||||||
{previewText}
|
<DraftPostRow
|
||||||
</Text>
|
key={post.id}
|
||||||
|
post={post}
|
||||||
{/* Metadata row */}
|
isFirst={index === 0}
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
isLast={index === draft.posts.length - 1}
|
||||||
{/* Time ago */}
|
timestamp={draft.updatedAt}
|
||||||
<Text style={[a.text_xs, t.atoms.text_contrast_medium]}>
|
draftId={draft.id}
|
||||||
{timeAgo}
|
onDelete={onDelete}
|
||||||
</Text>
|
isDeleting={isDeleting}
|
||||||
|
/>
|
||||||
{/* Media indicator */}
|
))}
|
||||||
{draft.hasMedia && (
|
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_2xs]}>
|
|
||||||
<MediaIcon size="xs" style={[t.atoms.text_contrast_medium]} />
|
|
||||||
<Text style={[a.text_xs, t.atoms.text_contrast_medium]}>
|
|
||||||
{draft.mediaCount}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Thread indicator */}
|
|
||||||
{draft.postCount > 1 && (
|
|
||||||
<Text style={[a.text_xs, t.atoms.text_contrast_medium]}>
|
|
||||||
<Trans>{draft.postCount} posts</Trans>
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Delete button */}
|
|
||||||
<Button
|
|
||||||
label={_(msg`Delete draft`)}
|
|
||||||
variant="ghost"
|
|
||||||
color="negative"
|
|
||||||
shape="round"
|
|
||||||
size="small"
|
|
||||||
disabled={isDeleting}
|
|
||||||
onPress={e => {
|
|
||||||
e.stopPropagation()
|
|
||||||
onDelete(draft.id)
|
|
||||||
}}>
|
|
||||||
<ButtonIcon icon={TrashIcon} />
|
|
||||||
</Button>
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DraftPostRow({
|
||||||
|
post,
|
||||||
|
isFirst,
|
||||||
|
isLast,
|
||||||
|
timestamp,
|
||||||
|
draftId,
|
||||||
|
onDelete,
|
||||||
|
isDeleting,
|
||||||
|
}: {
|
||||||
|
post: DraftPostDisplay
|
||||||
|
isFirst: boolean
|
||||||
|
isLast: boolean
|
||||||
|
timestamp: string
|
||||||
|
draftId: string
|
||||||
|
onDelete: (draftId: string) => void
|
||||||
|
isDeleting: boolean
|
||||||
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
const profile = useCurrentAccountProfile()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
|
const displayName =
|
||||||
|
profile?.displayName || profile?.handle || currentAccount?.handle || ''
|
||||||
|
const handle = profile?.handle || currentAccount?.handle || ''
|
||||||
|
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 (
|
||||||
|
<View style={[a.flex_row, a.gap_sm, !isFirst && [a.ml_xl, a.pt_xs]]}>
|
||||||
|
{/* Avatar column with thread line */}
|
||||||
|
<View style={[a.align_center]}>
|
||||||
|
<UserAvatar type="user" size={avatarSize} avatar={avatarUrl} />
|
||||||
|
{/* Thread line connecting posts */}
|
||||||
|
{!isLast && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.mt_xs,
|
||||||
|
{
|
||||||
|
width: 2,
|
||||||
|
backgroundColor: t.palette.contrast_100,
|
||||||
|
minHeight: 8,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Content column */}
|
||||||
|
<View style={[a.flex_1, a.gap_xs]}>
|
||||||
|
{/* Header row: name, handle, timestamp, menu */}
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.flex_1, a.gap_xs]}>
|
||||||
|
{displayName && (
|
||||||
|
<Text
|
||||||
|
style={[textStyle, a.font_bold, t.atoms.text]}
|
||||||
|
numberOfLines={1}>
|
||||||
|
{displayName}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text
|
||||||
|
style={[textStyle, t.atoms.text_contrast_medium]}
|
||||||
|
numberOfLines={1}>
|
||||||
|
@{handle}
|
||||||
|
</Text>
|
||||||
|
<Text style={[textStyle, t.atoms.text_contrast_medium]}>
|
||||||
|
·
|
||||||
|
</Text>
|
||||||
|
<TimeElapsed timestamp={timestamp}>
|
||||||
|
{({timeElapsed}) => (
|
||||||
|
<Text
|
||||||
|
style={[textStyle, t.atoms.text_contrast_medium]}
|
||||||
|
numberOfLines={1}>
|
||||||
|
{timeElapsed}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</TimeElapsed>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Overflow menu (only on first post) */}
|
||||||
|
{isFirst && (
|
||||||
|
<Button
|
||||||
|
label={_(msg`More options`)}
|
||||||
|
variant="ghost"
|
||||||
|
color="secondary"
|
||||||
|
shape="round"
|
||||||
|
size="tiny"
|
||||||
|
onPress={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
// TODO: Show menu with options
|
||||||
|
}}>
|
||||||
|
<ButtonIcon icon={DotsIcon} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Post text - full, not truncated */}
|
||||||
|
{post.text ? (
|
||||||
|
<Text style={[textStyle, t.atoms.text]}>{post.text}</Text>
|
||||||
|
) : (
|
||||||
|
<Text style={[textStyle, t.atoms.text_contrast_medium, a.italic]}>
|
||||||
|
<Trans>(No text)</Trans>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Media preview */}
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DraftMediaPreview({post}: {post: DraftPostDisplay}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
const [imageUrls, setImageUrls] = useState<string[]>([])
|
||||||
|
const [gifUrl, setGifUrl] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadMedia() {
|
||||||
|
if (!currentAccount?.did) return
|
||||||
|
|
||||||
|
// Load images
|
||||||
|
if (post.images && post.images.length > 0) {
|
||||||
|
const urls: string[] = []
|
||||||
|
for (const image of post.images) {
|
||||||
|
try {
|
||||||
|
const url = await storage.loadMediaFromLocal(
|
||||||
|
currentAccount.did,
|
||||||
|
image.localId,
|
||||||
|
)
|
||||||
|
urls.push(url)
|
||||||
|
} catch (e) {
|
||||||
|
// Image might not exist anymore
|
||||||
|
console.warn('Failed to load draft image', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setImageUrls(urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GIFs have a URL directly
|
||||||
|
if (post.gif) {
|
||||||
|
setGifUrl(post.gif.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMedia()
|
||||||
|
}, [currentAccount?.did, post.images, post.gif])
|
||||||
|
|
||||||
|
// Nothing to show
|
||||||
|
if (imageUrls.length === 0 && !gifUrl && !post.video) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.gap_xs, a.pt_xs]}>
|
||||||
|
{/* Images grid */}
|
||||||
|
{imageUrls.length > 0 && (
|
||||||
|
<View style={[a.flex_row, a.gap_xs, a.flex_wrap]}>
|
||||||
|
{imageUrls.map((url, index) => (
|
||||||
|
<View
|
||||||
|
key={index}
|
||||||
|
style={[
|
||||||
|
a.rounded_sm,
|
||||||
|
a.overflow_hidden,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
{
|
||||||
|
width: imageUrls.length === 1 ? '100%' : '48%',
|
||||||
|
aspectRatio: imageUrls.length === 1 ? 16 / 9 : 1,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Image
|
||||||
|
source={{uri: url}}
|
||||||
|
style={[a.flex_1]}
|
||||||
|
resizeMode="cover"
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* GIF preview */}
|
||||||
|
{gifUrl && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.rounded_sm,
|
||||||
|
a.overflow_hidden,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
{aspectRatio: 16 / 9},
|
||||||
|
]}>
|
||||||
|
<Image
|
||||||
|
source={{uri: gifUrl}}
|
||||||
|
style={[a.flex_1]}
|
||||||
|
resizeMode="cover"
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Video indicator */}
|
||||||
|
{post.video && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.rounded_sm,
|
||||||
|
a.p_md,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
t.atoms.bg_contrast_50,
|
||||||
|
{aspectRatio: 16 / 9},
|
||||||
|
]}>
|
||||||
|
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>Video attached</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {PageX_Stroke2_Corner0_Rounded_Large} from '#/components/icons/PageX'
|
import {PageX_Stroke2_Corner0_Rounded_Large as PageXIcon} from '#/components/icons/PageX'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {EmptyState} from '../../util/EmptyState'
|
import {EmptyState} from '../../util/EmptyState'
|
||||||
import {DraftItem} from './DraftItem'
|
import {DraftItem} from './DraftItem'
|
||||||
@@ -79,17 +79,21 @@ export function DraftsListDialog({
|
|||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
({item}: {item: DraftSummary}) => {
|
({item}: {item: DraftSummary}) => {
|
||||||
return (
|
return (
|
||||||
<DraftItem
|
<View style={[a.px_lg]}>
|
||||||
draft={item}
|
<DraftItem
|
||||||
onSelect={handleSelectDraft}
|
draft={item}
|
||||||
onDelete={handleDeleteDraft}
|
onSelect={handleSelectDraft}
|
||||||
isDeleting={isDeleting}
|
onDelete={handleDeleteDraft}
|
||||||
/>
|
isDeleting={isDeleting}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[handleSelectDraft, handleDeleteDraft, isDeleting],
|
[handleSelectDraft, handleDeleteDraft, isDeleting],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const itemSeparator = useCallback(() => <View style={[{height: 12}]} />, [])
|
||||||
|
|
||||||
const emptyComponent = useMemo(() => {
|
const emptyComponent = useMemo(() => {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -98,12 +102,7 @@ export function DraftsListDialog({
|
|||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return <EmptyState icon={PageXIcon} message={_(msg`No drafts yet`)} />
|
||||||
<EmptyState
|
|
||||||
icon={PageX_Stroke2_Corner0_Rounded_Large}
|
|
||||||
message={_(msg`No drafts yet`)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}, [isLoading, _])
|
}, [isLoading, _])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -114,8 +113,10 @@ export function DraftsListDialog({
|
|||||||
keyExtractor={item => item.id}
|
keyExtractor={item => item.id}
|
||||||
ListHeaderComponent={listHeader}
|
ListHeaderComponent={listHeader}
|
||||||
ListEmptyComponent={emptyComponent}
|
ListEmptyComponent={emptyComponent}
|
||||||
|
ItemSeparatorComponent={itemSeparator}
|
||||||
stickyHeaderIndices={[0]}
|
stickyHeaderIndices={[0]}
|
||||||
style={t.atoms.bg_contrast_25}
|
style={t.atoms.bg_contrast_25}
|
||||||
|
contentContainerStyle={[a.pb_lg]}
|
||||||
/>
|
/>
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user