1import { Image } from "expo-image";
2import { useIsFocused } from "@react-navigation/native";
3import type { VideoThumbnail } from "expo-video";
4import { useEffect, useState } from "react";
5import { StyleSheet } from "react-native";
6
7import type { DraftComposerFileAttachment } from "../lib/composerImages";
8import { loadLocalAttachmentPreview } from "../lib/localAttachmentPreview";
9import { cachedVideoThumbnail, loadVideoThumbnail } from "../lib/videoThumbnails";
10
11export function VideoThumbnailImage(props: {
12 readonly cacheKey: string;
13 readonly source: string | DraftComposerFileAttachment | null;
14 readonly contentFit?: "cover" | "contain";
15}) {
16 const { cacheKey, source } = props;
17 const isFocused = useIsFocused();
18 const [loaded, setLoaded] = useState<{ key: string; thumbnail: VideoThumbnail } | null>(null);
19 const thumbnail = loaded?.key === cacheKey ? loaded.thumbnail : cachedVideoThumbnail(cacheKey);
20
21 useEffect(() => {
22 if (!source || !isFocused) return;
23 const controller = new AbortController();
24 void loadVideoThumbnail(
25 cacheKey,
26 async (signal) =>
27 typeof source === "string"
28 ? { uri: source, dispose: () => undefined }
29 : loadLocalAttachmentPreview(source, signal),
30 controller.signal,
31 ).then((thumbnail) => {
32 if (thumbnail && !controller.signal.aborted) setLoaded({ key: cacheKey, thumbnail });
33 });
34 return () => controller.abort();
35 }, [cacheKey, source, isFocused]);
36
37 return thumbnail ? (
38 <Image
39 source={thumbnail}
40 style={StyleSheet.absoluteFill}
41 contentFit={props.contentFit ?? "cover"}
42 recyclingKey={cacheKey}
43 accessible={false}
44 />
45 ) : null;
46}