import { useIsFocused } from "@react-navigation/native"; import { videoMimeType } from "@t3tools/shared/video"; import { useEffect, useRef, useState } from "react"; import { ActivityIndicator, Keyboard, Modal, Pressable, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { loadLocalAttachmentPreview } from "../lib/localAttachmentPreview"; import { useMediaActions, type MediaActionsSource } from "../lib/mediaActions"; import { mediaVideoPreviewUri, mediaVideoThumbnailKey, type LocalVideoPreviewSource, type MediaVideoPreviewSource, type VideoPreviewSource, } from "../lib/videoPreviewSource"; import { useAssetUrlState, useRefreshAssetUrl } from "../state/assets"; import { usePreparedConnection } from "../state/session"; import { AppText } from "./AppText"; import { SymbolView } from "./AppSymbol"; import { MediaActionsMenu } from "./MediaActionsMenu"; import { MediaSourceCaption } from "./MediaSourceCaption"; import { MediaVideoPlayer } from "./MediaVideoPlayer"; export type { VideoPreviewSource } from "../lib/videoPreviewSource"; interface PlaybackState { readonly uri: string | null; readonly resolvePlaybackUri?: () => Promise; readonly unavailable: boolean; readonly error: string | null; readonly actionsSource: MediaActionsSource | undefined; } function useMediaPlayback(source: MediaVideoPreviewSource): PlaybackState { const environmentId = "environmentId" in source ? source.environmentId : null; const resource = "resource" in source ? source.resource : null; const connection = usePreparedConnection(environmentId); const asset = useAssetUrlState(environmentId, resource); const refreshAssetUrl = useRefreshAssetUrl(environmentId, resource); const uri = mediaVideoPreviewUri(source, asset._tag === "Success" ? asset.url : null); return { uri, ...(resource !== null ? { resolvePlaybackUri: async () => mediaVideoPreviewUri(source, await refreshAssetUrl()) } : {}), unavailable: uri === null && environmentId !== null && (connection._tag === "None" || asset._tag === "Failure"), error: null, actionsSource: source.actionsSource, }; } function useLocalPlayback(source: LocalVideoPreviewSource): PlaybackState { const { attachment } = source; const [uri, setUri] = useState(null); const [error, setError] = useState(null); // Only a different file needs a new lease; a metadata update on the same // draft must not dispose the file Android is still playing. const attachmentRef = useRef(attachment); attachmentRef.current = attachment; const { id: attachmentId, fileUri } = attachment; useEffect(() => { setUri(null); setError(null); const controller = new AbortController(); const loading = loadLocalAttachmentPreview(attachmentRef.current, controller.signal); void loading.then( (file) => { if (file === null) return; if (controller.signal.aborted) file.dispose(); else setUri(file.uri); }, (cause: unknown) => { if (!controller.signal.aborted) { setError(cause instanceof Error ? cause.message : "Could not load this video."); } }, ); return () => { controller.abort(); void loading.then( (file) => file?.dispose(), () => undefined, ); }; }, [attachmentId, fileUri]); const mimeType = videoMimeType(attachment) ?? attachment.mimeType; return { uri, unavailable: error !== null, error, actionsSource: uri === null ? undefined : { name: attachment.name, mimeType, uri }, }; } function MediaPreviewModal(props: { readonly source: MediaVideoPreviewSource; readonly onRequestClose: () => void; }) { return ( ); } function LocalPreviewModal(props: { readonly source: LocalVideoPreviewSource; readonly onRequestClose: () => void; }) { return ( ); } function OpenVideoPreviewModal(props: { readonly name: string; readonly thumbnailKey: string; readonly playback: PlaybackState; readonly onRequestClose: () => void; }) { const { playback } = props; const insets = useSafeAreaInsets(); const mediaActions = useMediaActions(playback.actionsSource, props.onRequestClose); useEffect(() => Keyboard.dismiss(), []); return ( {props.name} {playback.uri === null && !playback.unavailable ? ( Loading video... ) : ( )} {playback.error ? ( {playback.error} ) : null} {mediaActions.sharing ? "Opening share sheet..." : "Save or share video"} ); } export function VideoPreviewModal(props: { readonly source: VideoPreviewSource | null; readonly onRequestClose: () => void; }) { const isFocused = useIsFocused(); const hasSource = props.source !== null; useEffect(() => { if (!isFocused && hasSource) props.onRequestClose(); }, [isFocused, hasSource, props.onRequestClose]); const { source } = props; if (source === null || !isFocused) return null; return source.type === "local" ? ( ) : ( ); }