1import { createContext, useContext, useState, useMemo } from "react";
2import { Pressable, View } from "react-native";
3import ImageViewing from "react-native-image-viewing";
4import { useSafeAreaInsets } from "react-native-safe-area-context";
5
6import { useMediaActions } from "../state/mediaActions";
7import { AppText } from "./AppText";
8import { SymbolView } from "./AppSymbol";
9import type { ResolvedFilePreviewSource } from "./FilePreviewModal.types";
10import { MediaActionsMenu } from "./MediaActionsMenu";
11import { MediaSourceCaption } from "./MediaSourceCaption";
12
13type MediaImagePreviewProps = {
14 readonly source: ResolvedFilePreviewSource;
15 readonly onRequestClose: () => void;
16};
17
18const ImagePreviewContext = createContext<MediaImagePreviewProps | null>(null);
19
20function ImagePreviewHeader() {
21 const props = useContext(ImagePreviewContext)!;
22 const insets = useSafeAreaInsets();
23 const mediaActions = useMediaActions(props.source.actionsSource, props.onRequestClose);
24 return (
25 <View className="bg-black/70" style={{ paddingTop: insets.top }}>
26 <View className="flex-row items-center gap-2 px-3">
27 <AppText className="flex-1 text-base text-white" numberOfLines={2}>
28 {props.source.name ?? "Image"}
29 </AppText>
30 <MediaActionsMenu media={mediaActions} inModal />
31 <Pressable
32 accessibilityRole="button"
33 accessibilityLabel="Close image"
34 onPress={props.onRequestClose}
35 className="min-h-11 min-w-11 items-center justify-center"
36 >
37 <SymbolView name="xmark" size={20} tintColor="#ffffff" type="monochrome" />
38 </Pressable>
39 </View>
40 <MediaSourceCaption source={mediaActions.title} />
41 </View>
42 );
43}
44
45/** Android keeps media actions in its in-app image viewer. iOS uses Quick Look. */
46export function MediaImagePreview(props: MediaImagePreviewProps) {
47 const gallery = props.source.gallery ?? [props.source];
48 const [index, setIndex] = useState(() =>
49 Math.max(
50 0,
51 gallery.findIndex((item) => item.uri === props.source.uri),
52 ),
53 );
54 const selected = gallery[index] ?? props.source;
55 const context = useMemo(() => ({ ...props, source: selected }), [props, selected]);
56 return (
57 <ImagePreviewContext value={context}>
58 <ImageViewing
59 images={gallery.map((item) => ({ uri: item.uri }))}
60 imageIndex={index}
61 onImageIndexChange={setIndex}
62 visible
63 presentationStyle="fullScreen"
64 onRequestClose={props.onRequestClose}
65 swipeToCloseEnabled={false}
66 doubleTapToZoomEnabled
67 HeaderComponent={ImagePreviewHeader}
68 />
69 </ImagePreviewContext>
70 );
71}