import { useIsFocused } from "@react-navigation/native"; import { requireNativeModule } from "expo"; import { useEffect, useEffectEvent, useId, useState } from "react"; import { Alert, Keyboard } from "react-native"; import { loadLocalAttachmentPreview } from "../lib/localAttachmentPreview"; import { mediaVideoPreviewUri, type VideoPreviewSource } from "../lib/videoPreviewSource"; import { useAssetUrlState, useRefreshAssetUrl } from "../state/assets"; import { usePreparedConnection } from "../state/session"; export type { VideoPreviewSource } from "../lib/videoPreviewSource"; const NativeControls = requireNativeModule<{ presentVideo( uri: string, title: string, sourceIdentifier: string, identifier: string, ): Promise; dismissVideo(identifier: string): Promise; }>("T3NativeControls"); function NativeVideoPreview(props: { readonly source: VideoPreviewSource; readonly onRequestClose: () => void; }) { const { source } = props; const localAttachment = source.type === "local" ? source.attachment : null; const identifier = useId(); const onRequestClose = useEffectEvent(props.onRequestClose); const environmentId = source.type === "media" && "environmentId" in source ? source.environmentId : null; const resource = source.type === "media" && "resource" in source ? source.resource : null; const preparedConnection = usePreparedConnection(environmentId); const assetUrl = useAssetUrlState(environmentId, resource); const refreshAssetUrl = useEffectEvent(useRefreshAssetUrl(environmentId, resource)); const name = source.type === "media" ? source.name : source.attachment.name; // The first minted URL is kept so a background refresh does not restart playback. const resolvedUrl = source.type === "media" ? mediaVideoPreviewUri(source, assetUrl._tag === "Success" ? assetUrl.url : null) : null; const [playbackUrl, setPlaybackUrl] = useState(resolvedUrl); const loadError = resource !== null && playbackUrl === null ? preparedConnection._tag === "None" ? "Reconnect to this environment and open the video again." : assetUrl._tag === "Failure" ? "Could not load this video. Check the connection and try again." : null : null; useEffect(() => Keyboard.dismiss(), []); useEffect(() => { if (playbackUrl === null && resolvedUrl !== null) setPlaybackUrl(resolvedUrl); }, [playbackUrl, resolvedUrl]); useEffect(() => { if (!loadError) return; Alert.alert("Could not open video", loadError); onRequestClose(); }, [loadError]); useEffect(() => { if (localAttachment === null && playbackUrl === null) return; const controller = new AbortController(); let ready = false; void (async () => { const file = localAttachment !== null ? await loadLocalAttachmentPreview(localAttachment, controller.signal) : null; if (localAttachment !== null && !file) return; try { if (controller.signal.aborted) return; ready = true; await NativeControls.presentVideo( file?.uri ?? playbackUrl!, name, source.sourceIdentifier ?? "", identifier, ); if (!controller.signal.aborted) onRequestClose(); } finally { // Native completion follows dismissal, so local playback keeps its file lease. file?.dispose(); } })().catch((error: unknown) => { if (controller.signal.aborted) return; // AVKit gives no retry, so re-mint now; the cached URL may simply have expired. if (ready) void refreshAssetUrl(); Alert.alert( "Could not open video", ready ? "This video couldn't be loaded or played. Check the connection, or touch and hold the video to save or share the original." : error instanceof Error ? error.message : "Could not load this video.", ); onRequestClose(); }); return () => { controller.abort(); void NativeControls.dismissVideo(identifier).catch(() => undefined); }; }, [localAttachment, name, source.sourceIdentifier, playbackUrl, identifier]); return null; } export function VideoPreviewModal(props: { readonly source: VideoPreviewSource | null; readonly onRequestClose: () => void; }) { const isFocused = useIsFocused(); const hasSource = props.source !== null; const onRequestClose = useEffectEvent(props.onRequestClose); useEffect(() => { if (!isFocused && hasSource) onRequestClose(); }, [isFocused, hasSource]); if (!props.source || !isFocused) return null; return ; }