1import { useEffect, useEffectEvent } from "react";
2import { Alert, Modal, Pressable, View } from "react-native";
3import ImageViewing from "react-native-image-viewing";
4
5import { openAttachmentInViewer } from "../lib/attachmentDownload";
6import type { ResolvedFilePreviewSource } from "./FilePreviewModal.types";
7import { MediaImagePreview } from "./MediaImagePreview";
8import { AppText as Text } from "./AppText";
9
10function DocumentPreview(props: {
11 readonly source: ResolvedFilePreviewSource;
12 readonly onRequestClose: () => void;
13 readonly onOpenError?: (error: unknown) => void;
14}) {
15 const { uri, name } = props.source;
16 const onRequestClose = useEffectEvent(props.onRequestClose);
17 const onOpenError = useEffectEvent((error: unknown) => {
18 if (props.onOpenError) props.onOpenError(error);
19 else
20 Alert.alert(
21 "Could not open document",
22 "A compatible viewer must be installed. Check your connection and try again.",
23 );
24 });
25 useEffect(() => {
26 const controller = new AbortController();
27 const input = {
28 uri,
29 attachment: {
30 name: name ?? "Document",
31 mimeType:
32 props.source.mimeType ??
33 (props.source.kind === "pdf" ? "application/pdf" : "application/octet-stream"),
34 },
35 signal: controller.signal,
36 };
37 const opened = openAttachmentInViewer(input);
38 void opened
39 .catch((error: unknown) => {
40 if (!controller.signal.aborted) onOpenError(error);
41 })
42 .finally(() => {
43 if (!controller.signal.aborted) onRequestClose();
44 });
45 return () => controller.abort();
46 }, [uri, name, props.source.mimeType, props.source.kind]);
47 return (
48 <Modal transparent animationType="fade" onRequestClose={props.onRequestClose}>
49 <View className="flex-1 items-center justify-center bg-backdrop p-6">
50 <View className="w-full max-w-sm gap-4 rounded-2xl bg-sheet-solid p-6">
51 <Text className="font-t3-semibold text-foreground">Opening document…</Text>
52 <Text className="text-foreground-muted" numberOfLines={2}>
53 {name ?? "Document"}
54 </Text>
55 <Pressable
56 accessibilityRole="button"
57 onPress={props.onRequestClose}
58 className="self-end p-3"
59 >
60 <Text className="text-foreground">Cancel</Text>
61 </Pressable>
62 </View>
63 </View>
64 </Modal>
65 );
66}
67
68export function FilePreview(props: {
69 readonly source: ResolvedFilePreviewSource;
70 readonly onRequestClose: () => void;
71 readonly onOpenError?: (error: unknown) => void;
72}) {
73 if (props.source.kind !== "image") return <DocumentPreview {...props} />;
74 if (props.source.actionsSource) return <MediaImagePreview {...props} />;
75 return (
76 <ImageViewing
77 images={
78 props.source.gallery?.map((item) => ({ uri: item.uri })) ?? [{ uri: props.source.uri }]
79 }
80 imageIndex={Math.max(
81 0,
82 props.source.gallery?.findIndex((item) => item.uri === props.source.uri) ?? 0,
83 )}
84 visible
85 onRequestClose={props.onRequestClose}
86 swipeToCloseEnabled={false}
87 doubleTapToZoomEnabled
88 />
89 );
90}