Use real image embed components for draft preview

Replace custom image preview with AutoSizedImage for single images
and ImageLayoutGrid for multiple images. This gives drafts the same
polished image display as regular posts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-01-13 17:21:38 +02:00
parent 56bd02e7b3
commit 9a681a22ef
+56 -40
View File
@@ -1,12 +1,20 @@
import {useCallback, useEffect, useState} from 'react' import {useCallback, useEffect, useMemo, useState} from 'react'
import {Image, Pressable, View} from 'react-native' import {Pressable, View} from 'react-native'
import {Image} from 'expo-image'
import {type AppBskyEmbedImages} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {type DraftPostDisplay, type DraftSummary} from '#/state/drafts' import {
type DraftPostDisplay,
type DraftSummary,
type LocalMediaRef,
} from '#/state/drafts'
import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile' import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {AutoSizedImage} from '#/view/com/util/images/AutoSizedImage'
import {ImageLayoutGrid} from '#/view/com/util/images/ImageLayoutGrid'
import {TimeElapsed} from '#/view/com/util/TimeElapsed' import {TimeElapsed} from '#/view/com/util/TimeElapsed'
import {UserAvatar} from '#/view/com/util/UserAvatar' import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
@@ -134,13 +142,13 @@ function DraftPostRow({
</View> </View>
{/* Content column */} {/* Content column */}
<View style={[a.flex_1, a.gap_xs]}> <View style={[a.flex_1, a.gap_2xs]}>
{/* Header row: name, handle, timestamp, menu */} {/* 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.gap_xs]}>
<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={[a.text_md, a.font_bold, t.atoms.text]} style={[a.text_md, a.font_semi_bold, t.atoms.text]}
numberOfLines={1}> numberOfLines={1}>
{displayName} {displayName}
</Text> </Text>
@@ -197,10 +205,15 @@ function DraftPostRow({
) )
} }
type LoadedImage = {
url: string
meta: LocalMediaRef
}
function DraftMediaPreview({post}: {post: DraftPostDisplay}) { function DraftMediaPreview({post}: {post: DraftPostDisplay}) {
const t = useTheme() const t = useTheme()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const [imageUrls, setImageUrls] = useState<string[]>([]) const [loadedImages, setLoadedImages] = useState<LoadedImage[]>([])
const [gifUrl, setGifUrl] = useState<string | null>(null) const [gifUrl, setGifUrl] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
@@ -209,20 +222,20 @@ function DraftMediaPreview({post}: {post: DraftPostDisplay}) {
// Load images // Load images
if (post.images && post.images.length > 0) { if (post.images && post.images.length > 0) {
const urls: string[] = [] const loaded: LoadedImage[] = []
for (const image of post.images) { for (const image of post.images) {
try { try {
const url = await storage.loadMediaFromLocal( const url = await storage.loadMediaFromLocal(
currentAccount.did, currentAccount.did,
image.localId, image.localId,
) )
urls.push(url) loaded.push({url, meta: image})
} catch (e) { } catch (e) {
// Image might not exist anymore // Image might not exist anymore
console.warn('Failed to load draft image', e) console.warn('Failed to load draft image', e)
} }
} }
setImageUrls(urls) setLoadedImages(loaded)
} }
// GIFs have a URL directly // GIFs have a URL directly
@@ -234,52 +247,50 @@ function DraftMediaPreview({post}: {post: DraftPostDisplay}) {
loadMedia() loadMedia()
}, [currentAccount?.did, post.images, post.gif]) }, [currentAccount?.did, post.images, post.gif])
// Convert loaded images to ViewImage format for the embed components
const viewImages = useMemo<AppBskyEmbedImages.ViewImage[]>(() => {
return loadedImages.map(({url, meta}) => ({
thumb: url,
fullsize: url,
alt: meta.altText || '',
aspectRatio:
meta.width && meta.height
? {width: meta.width, height: meta.height}
: undefined,
}))
}, [loadedImages])
// Nothing to show // Nothing to show
if (imageUrls.length === 0 && !gifUrl && !post.video) { if (viewImages.length === 0 && !gifUrl && !post.video) {
return null return null
} }
return ( return (
<View style={[a.gap_xs, a.pt_xs]}> <View style={[a.pt_xs]}>
{/* Images grid */} {/* Images - use real embed components */}
{imageUrls.length > 0 && ( {viewImages.length === 1 && (
<View style={[a.flex_row, a.gap_xs, a.flex_wrap]}> <AutoSizedImage image={viewImages[0]} hideBadge />
{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>
)} )}
{viewImages.length > 1 && <ImageLayoutGrid images={viewImages} />}
{/* GIF preview */} {/* GIF preview */}
{gifUrl && ( {gifUrl && (
<View <View
style={[ style={[
a.rounded_sm, a.rounded_md,
a.overflow_hidden, a.overflow_hidden,
t.atoms.bg_contrast_25, t.atoms.bg_contrast_25,
{aspectRatio: 16 / 9}, {
aspectRatio:
post.gif?.width && post.gif?.height
? post.gif.width / post.gif.height
: 16 / 9,
},
]}> ]}>
<Image <Image
source={{uri: gifUrl}} source={{uri: gifUrl}}
style={[a.flex_1]} style={[a.flex_1]}
resizeMode="cover" contentFit="cover"
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
/> />
</View> </View>
@@ -289,12 +300,17 @@ function DraftMediaPreview({post}: {post: DraftPostDisplay}) {
{post.video && ( {post.video && (
<View <View
style={[ style={[
a.rounded_sm, a.rounded_md,
a.p_md, a.p_md,
a.align_center, a.align_center,
a.justify_center, a.justify_center,
t.atoms.bg_contrast_50, t.atoms.bg_contrast_50,
{aspectRatio: 16 / 9}, {
aspectRatio:
post.video.width && post.video.height
? post.video.width / post.video.height
: 16 / 9,
},
]}> ]}>
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}> <Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
<Trans>Video attached</Trans> <Trans>Video attached</Trans>