import {useCallback, useEffect, useMemo, useState} from 'react' import {Pressable, View} from 'react-native' import * as VideoThumbnails from 'expo-video-thumbnails' import {msg, plural} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import * as device from '#/lib/deviceName' import {logger} from '#/view/com/composer/drafts/state/logger' import {TimeElapsed} from '#/view/com/util/TimeElapsed' import {atoms as a, select, useTheme} from '#/alf' import {Button} from '#/components/Button' import {CirclePlus_Stroke2_Corner0_Rounded as CirclePlusIcon} from '#/components/icons/CirclePlus' import {type Props as SVGIconProps} from '#/components/icons/common' import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsIcon} from '#/components/icons/DotGrid' import {CloseQuote_Stroke2_Corner0_Rounded as CloseQuoteIcon} from '#/components/icons/Quote' import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' import * as MediaPreview from '#/components/MediaPreview' import * as Prompt from '#/components/Prompt' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' import {IS_WEB} from '#/env' import {type DraftPostDisplay, type DraftSummary} from './state/schema' import * as storage from './state/storage' export function DraftItem({ draft, onSelect, onDelete, }: { draft: DraftSummary onSelect: (draft: DraftSummary) => void onDelete: (draft: DraftSummary) => void }) { const {_} = useLingui() const t = useTheme() const discardPromptControl = Prompt.usePromptControl() const post = draft.posts[0] const mediaExistsOnOtherDevice = !draft.meta.isOriginatingDevice && draft.meta.hasMissingMedia const mediaIsMissing = draft.meta.isOriginatingDevice && draft.meta.hasMissingMedia const hasMetadata = draft.meta.replyCount > 0 || mediaExistsOnOtherDevice || draft.meta.hasQuotes const isUnknownDevice = useMemo(() => { const raw = draft.draft.deviceName switch (raw) { case device.FALLBACK_IOS: case device.FALLBACK_ANDROID: case device.FALLBACK_WEB: return true default: return false } }, [draft]) const handleDelete = useCallback(() => { onDelete(draft) }, [onDelete, draft]) return ( <> onSelect(draft)} style={({pressed, hovered}) => [ a.rounded_md, a.border, t.atoms.shadow_sm, pressed || hovered ? t.atoms.border_contrast_medium : t.atoms.border_contrast_low, { backgroundColor: select(t.name, { light: t.atoms.bg.backgroundColor, dark: t.atoms.bg_contrast_25.backgroundColor, dim: t.atoms.bg_contrast_25.backgroundColor, }), }, ]}> {!!post.text.trim().length && ( )} {!mediaExistsOnOtherDevice && } {hasMetadata && ( {mediaExistsOnOtherDevice && ( )} {mediaIsMissing && ( )} {draft.meta.hasQuotes && ( )} {draft.meta.replyCount > 0 && ( )} )} {/* Timestamp */} {({timeElapsed}) => ( {timeElapsed} )} {/* Menu button */} ) } function DraftMetadataTag({ display = 'info', icon: Icon, text, }: { display?: 'info' | 'warning' icon: React.ComponentType text: string }) { const t = useTheme() const color = { info: t.atoms.text_contrast_medium.color, warning: select(t.name, { light: '#C99A00', dark: t.palette.yellow, dim: t.palette.yellow, }), }[display] return ( {text} ) } type LoadedImage = { url: string alt: string } function DraftMediaPreview({post}: {post: DraftPostDisplay}) { const [loadedImages, setLoadedImages] = useState([]) const [videoThumbnail, setVideoThumbnail] = useState() useEffect(() => { async function loadMedia() { if (post.images && post.images.length > 0) { const loaded: LoadedImage[] = [] for (const image of post.images) { try { const url = await storage.loadMediaFromLocal(image.localPath) loaded.push({url, alt: image.altText || ''}) } catch (e) { // Image doesn't exist locally, skip it } } setLoadedImages(loaded) } if (post.video?.exists && post.video.localPath) { try { const url = await storage.loadMediaFromLocal(post.video.localPath) if (IS_WEB) { // can't generate thumbnails on web setVideoThumbnail("yep, there's a video") } else { logger.debug('generating thumbnail of ', {url}) const thumbnail = await VideoThumbnails.getThumbnailAsync(url, { time: 0, quality: 0.2, }) logger.debug('thumbnail generated', {thumbnail}) setVideoThumbnail(thumbnail.uri) } } catch (e) { // Video doesn't exist locally } } } void loadMedia() }, [post.images, post.video]) // Nothing to show if (loadedImages.length === 0 && !post.gif && !post.video) { return null } return ( {loadedImages.map((image, i) => ( ))} {post.gif && ( )} {post.video && videoThumbnail && ( )} ) }