| 1 | import { useEffect, useEffectEvent } from "react"; |
| 2 | import { Alert, Modal, Pressable, View } from "react-native"; |
| 3 | import ImageViewing from "react-native-image-viewing"; |
| 4 | |
| 5 | import { openAttachmentInViewer } from "../lib/attachmentDownload"; |
| 6 | import type { ResolvedFilePreviewSource } from "./FilePreviewModal.types"; |
| 7 | import { MediaImagePreview } from "./MediaImagePreview"; |
| 8 | import { AppText as Text } from "./AppText"; |
| 9 | |
| 10 | function 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 | |
| 68 | export 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 | } |