1import { useId, useMemo, useState } from "react";
2import { Image, Pressable, View } from "react-native";
3
4import { FilePreviewLoading } from "./FilePreviewFeedback";
5import { EmptyState } from "../../components/EmptyState";
6import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal";
7import { PresentationSource } from "../../components/NativePresentation";
8import { useMediaActions } from "../../state/mediaActions";
9import { type MediaActionsSource } from "../../lib/mediaActionsSource";
10import { MediaActionsMenu } from "../../components/MediaActionsMenu";
11
12function ResolvedWorkspaceFileImagePreview(props: {
13 readonly accessibilityLabel: string;
14 readonly uri: string;
15 readonly actionsSource?: MediaActionsSource;
16}) {
17 const [loadError, setLoadError] = useState<string | null>(null);
18 const [preview, setPreview] = useState<FilePreviewSource | null>(null);
19 const sourceIdentifier = useId();
20 const mediaActions = useMediaActions(props.actionsSource);
21 const imageSource = useMemo(
22 () => ({ uri: props.uri, cache: "force-cache" as const }),
23 [props.uri],
24 );
25
26 return (
27 <View className="relative flex-1 bg-subtle">
28 <MediaActionsMenu media={mediaActions} style={{ flex: 1 }}>
29 <Pressable
30 accessibilityRole="button"
31 accessibilityLabel={`Open full-screen preview of ${props.accessibilityLabel}`}
32 accessibilityHint={
33 mediaActions.actions.length > 0 ? "Touch and hold for media actions" : undefined
34 }
35 disabled={loadError !== null}
36 className="flex-1 p-4 active:bg-subtle-strong"
37 onPress={() =>
38 setPreview({
39 kind: "image",
40 uri: props.uri,
41 name: props.accessibilityLabel,
42 sourceIdentifier,
43 actionsSource: props.actionsSource,
44 })
45 }
46 >
47 <PresentationSource identifier={sourceIdentifier} style={{ flex: 1 }}>
48 <Image
49 accessible={false}
50 source={imageSource}
51 className="h-full w-full"
52 resizeMode="contain"
53 onLoadStart={() => setLoadError(null)}
54 onError={(event) => {
55 setLoadError(event.nativeEvent.error || "The image could not be rendered.");
56 }}
57 />
58 </PresentationSource>
59 </Pressable>
60 </MediaActionsMenu>
61 {loadError !== null ? (
62 <View
63 pointerEvents="none"
64 className="absolute inset-0 items-center justify-center bg-card px-6"
65 >
66 <EmptyState title="Image unavailable" detail={loadError} />
67 </View>
68 ) : null}
69 <FilePreviewModal source={preview} onRequestClose={() => setPreview(null)} />
70 </View>
71 );
72}
73
74export function WorkspaceFileImagePreview(props: {
75 readonly accessibilityLabel: string;
76 readonly uri: string | null;
77 readonly actionsSource?: MediaActionsSource;
78}) {
79 if (props.uri === null) {
80 return <FilePreviewLoading message="Preparing image preview..." background="card" />;
81 }
82
83 return (
84 <ResolvedWorkspaceFileImagePreview
85 accessibilityLabel={props.accessibilityLabel}
86 uri={props.uri}
87 actionsSource={props.actionsSource}
88 />
89 );
90}