import { useIsFocused } from "@react-navigation/native"; import type { ChatFileAttachment, EnvironmentId } from "@t3tools/contracts"; import { videoMimeType } from "@t3tools/shared/video"; import { useEvent } from "expo"; import { useVideoPlayer, VideoView } from "expo-video"; import { useEffect, useRef, useState } from "react"; import { ActivityIndicator, AppState, Keyboard, Modal, Pressable, StyleSheet, View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { downloadAttachmentForPreview, type AttachmentPreviewFile, } from "../lib/attachmentDownload"; import type { DraftComposerFileAttachment } from "../lib/composerImages"; import { loadLocalAttachmentPreview } from "../lib/localAttachmentPreview"; import { useAssetUrlState } from "../state/assets"; import { usePreparedConnection } from "../state/session"; import { SymbolView } from "./AppSymbol"; import { AppText } from "./AppText"; export type VideoPreviewSource = ( | { readonly type: "local"; readonly attachment: DraftComposerFileAttachment } | { readonly type: "remote"; readonly environmentId: EnvironmentId; readonly attachment: ChatFileAttachment; } ) & { readonly sourceIdentifier?: string }; function VideoPlayback(props: { readonly file: AttachmentPreviewFile }) { const player = useVideoPlayer(props.file.uri, (player) => { player.staysActiveInBackground = false; if (AppState.currentState === "active") player.play(); }); const { status } = useEvent(player, "statusChange", { status: player.status }); const shareControllerRef = useRef(null); const [sharing, setSharing] = useState(false); const [shareError, setShareError] = useState(null); useEffect( () => () => { shareControllerRef.current?.abort(); shareControllerRef.current = null; }, [], ); const onShare = () => { if (shareControllerRef.current) return; player.pause(); const controller = new AbortController(); shareControllerRef.current = controller; setSharing(true); setShareError(null); void props.file .share(controller.signal) .catch((error: unknown) => { if (!controller.signal.aborted) { setShareError(error instanceof Error ? error.message : "Could not share this video."); } }) .finally(() => { if (shareControllerRef.current === controller) { shareControllerRef.current = null; setSharing(false); } }); }; return ( <> {status === "error" ? ( This video couldn't be played on this device. You can save or share the original file. ) : ( <> {status === "loading" ? ( ) : null} )} {sharing ? "Opening share sheet..." : "Save or share video"} {shareError ? ( {shareError} ) : null} ); } function OpenVideoPreviewModal(props: { readonly source: VideoPreviewSource; readonly onRequestClose: () => void; }) { const { source } = props; const { attachment } = source; const insets = useSafeAreaInsets(); const environmentId = source.type === "remote" ? source.environmentId : null; const preparedConnection = usePreparedConnection(environmentId); const fileUri = source.type === "local" ? source.attachment.fileUri : null; const mimeType = videoMimeType(attachment) ?? attachment.mimeType; const assetUrl = useAssetUrlState( environmentId, source.type === "remote" ? { _tag: "attachment", attachmentId: attachment.id, fileName: attachment.name, mimeType } : null, ); const [downloadUrl, setDownloadUrl] = useState(null); const [file, setFile] = useState(null); const [failure, setFailure] = useState(null); useEffect(() => Keyboard.dismiss(), []); useEffect(() => { if (environmentId !== null && downloadUrl === null && assetUrl._tag === "Success") { setDownloadUrl(assetUrl.url); } }, [environmentId, downloadUrl, assetUrl]); useEffect(() => { if (source.type === "remote" && downloadUrl === null) return; const controller = new AbortController(); let preview: AttachmentPreviewFile | null = null; setFile(null); setFailure(null); const loading = source.type === "local" ? loadLocalAttachmentPreview(source.attachment, controller.signal) : downloadAttachmentForPreview({ url: downloadUrl!, attachment: { name: attachment.name, mimeType }, signal: controller.signal, }); void loading.then( (loaded) => { if (controller.signal.aborted) { loaded?.dispose(); return; } preview = loaded; setFile(loaded); }, (error: unknown) => { if (!controller.signal.aborted) { setFailure(error instanceof Error ? error.message : "Could not load this video."); } }, ); return () => { controller.abort(); preview?.dispose(); }; }, [source.type, environmentId, attachment.id, attachment.name, mimeType, fileUri, downloadUrl]); const loadError = failure ?? (environmentId !== null && downloadUrl === null ? preparedConnection._tag === "None" ? "This environment is disconnected. Reconnect and open the video again." : assetUrl._tag === "Failure" ? "Could not load this video. Check the connection to this environment and try again." : null : null); return ( {attachment.name} {file ? ( ) : ( {loadError ? ( {loadError} ) : ( <> Loading 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; const key = source.type === "local" ? `local:${source.attachment.id}:${source.attachment.fileUri}` : `remote:${source.environmentId}:${source.attachment.id}`; return ; }