| 1 | import { createContext, useContext, useState, useMemo } from "react"; |
| 2 | import { Pressable, View } from "react-native"; |
| 3 | import ImageViewing from "react-native-image-viewing"; |
| 4 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 5 | |
| 6 | import { useMediaActions } from "../state/mediaActions"; |
| 7 | import { AppText } from "./AppText"; |
| 8 | import { SymbolView } from "./AppSymbol"; |
| 9 | import type { ResolvedFilePreviewSource } from "./FilePreviewModal.types"; |
| 10 | import { MediaActionsMenu } from "./MediaActionsMenu"; |
| 11 | import { MediaSourceCaption } from "./MediaSourceCaption"; |
| 12 | |
| 13 | type MediaImagePreviewProps = { |
| 14 | readonly source: ResolvedFilePreviewSource; |
| 15 | readonly onRequestClose: () => void; |
| 16 | }; |
| 17 | |
| 18 | const ImagePreviewContext = createContext<MediaImagePreviewProps | null>(null); |
| 19 | |
| 20 | function 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. */ |
| 46 | export 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 | } |