Skip to content
Source
Sign inSign up

ThreadBrowserFloat.tsx

138 lines5.1 KB

1import { useIsFocused } from "@react-navigation/native";
2import type { EnvironmentId, PreviewSessionSnapshot, ThreadId } from "@t3tools/contracts";
3import { previewPdfAssetUrl } from "@t3tools/shared/preview";
4import * as Option from "effect/Option";
5import { useState } from "react";
6import { Pressable, View } from "react-native";
7import Animated, { FadeIn, FadeOut, ReduceMotion } from "react-native-reanimated";
8​
9import { SymbolView } from "../../components/ui/AppSymbol";
10import { useAppForeground } from "../../lib/appState";
11import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
12import { browserTabTitle } from "./browserTabs";
13import { PreviewStreamWebView } from "./PreviewStreamWebView";
14import { usePreparedConnection } from "../../state/session";
15​
16const PLAYER_LONG_SIDE = 184;
17const PLAYER_ENTERING = FadeIn.duration(180).reduceMotion(ReduceMotion.System);
18const PLAYER_EXITING = FadeOut.duration(120).reduceMotion(ReduceMotion.System);
19​
20export function ThreadBrowserFloat(props: {
21 readonly environmentId: EnvironmentId;
22 readonly threadId: ThreadId;
23 readonly tabs: ReadonlyArray<PreviewSessionSnapshot>;
24 readonly loaded: boolean;
25 readonly top: number;
26 readonly onOpen: (tabId: string) => void;
27}) {
28 const [tabId, setTabId] = useState<string | null>(null);
29 // The reveal requests already seen, so only a tab revealed after the first
30 // list pops the player.
31 const [seen, setSeen] = useState<{
32 readonly tabs: ReadonlyArray<PreviewSessionSnapshot>;
33 readonly reveals: ReadonlyMap<string, string | undefined>;
34 } | null>(null);
35 if (props.loaded && seen?.tabs !== props.tabs) {
36 setSeen({
37 tabs: props.tabs,
38 reveals: new Map(props.tabs.map((tab) => [tab.tabId, tab.revealRequest?.id])),
39 });
40 const previous = seen?.reveals;
41 const opened =
42 previous === undefined
43 ? undefined
44 : props.tabs.findLast(
45 (tab) =>
46 tab.reveal === true &&
47 (tab.revealRequest
48 ? previous.get(tab.tabId) !== tab.revealRequest.id
49 : !previous.has(tab.tabId)),
50 );
51 if (opened !== undefined) setTabId(opened.tabId);
52 }
53 const focused = useIsFocused();
54 const foreground = useAppForeground();
55 const tab = props.tabs.find((entry) => entry.tabId === tabId) ?? null;
56 const onOpen = props.onOpen;
57 const open = () => {
58 if (tabId === null) return;
59 setTabId(null);
60 onOpen(tabId);
61 };
62 const close = () => setTabId(null);
63 if (!tab || !focused) return null;
64 return (
65 <FloatingBrowserPlayer
66 environmentId={props.environmentId}
67 threadId={props.threadId}
68 tab={tab}
69 live={foreground}
70 top={props.top}
71 onOpen={open}
72 onClose={close}
73 />
74 );
76​
77function FloatingBrowserPlayer(props: {
78 readonly environmentId: EnvironmentId;
79 readonly threadId: ThreadId;
80 readonly tab: PreviewSessionSnapshot;
81 readonly live: boolean;
82 readonly top: number;
83 readonly onOpen: () => void;
84 readonly onClose: () => void;
85}) {
86 const { themeVariables } = useAppearancePreferences();
87 const prepared = usePreparedConnection(props.environmentId);
88 const httpBaseUrl = Option.isSome(prepared) ? prepared.value.httpBaseUrl : null;
89 const [viewport, setViewport] = useState<{ width: number; height: number } | null>(null);
90 const aspect = Math.min(Math.max(viewport ? viewport.width / viewport.height : 16 / 10, 0.5), 2);
91 const width = aspect >= 1 ? PLAYER_LONG_SIDE : Math.round(PLAYER_LONG_SIDE * aspect);
92 const height = Math.round(width / aspect);
93 const background = themeVariables["--color-sheet-solid"];
94 return (
95 <Animated.View
96 entering={PLAYER_ENTERING}
97 exiting={PLAYER_EXITING}
98 className="absolute right-3 z-30 overflow-hidden rounded-2xl border border-border shadow-md shadow-black/20"
99 style={{ top: props.top, width, height, backgroundColor: background }}
100 >
101 <Pressable
102 accessibilityRole="button"
103 accessibilityLabel={`Open browser, ${browserTabTitle(props.tab)}`}
104 accessibilityHint="Opens the browser tab full screen"
105 onPress={props.onOpen}
106 className="flex-1"
107 >
108 <View pointerEvents="none" className="flex-1">
109 <PreviewStreamWebView
110 environmentId={props.environmentId}
111 threadId={props.threadId}
112 tabId={props.tab.tabId}
113 pdfUri={
114 props.tab.navStatus._tag === "Success"
115 ? previewPdfAssetUrl(props.tab.navStatus.url, httpBaseUrl)
116 : null
117 }
118 interactive={false}
119 background={background}
120 compact
121 paused={!props.live}
122 onViewport={setViewport}
123 onGone={props.onClose}
124 />
125 </View>
126 </Pressable>
127 <Pressable
128 accessibilityRole="button"
129 accessibilityLabel="Close floating preview"
130 hitSlop={8}
131 onPress={props.onClose}
132 className="absolute right-1.5 top-1.5 size-6 items-center justify-center rounded-full bg-black/55"
133 >
134 <SymbolView name="xmark" size={11} tintColor="#ffffff" type="monochrome" />
135 </Pressable>
136 </Animated.View>
137 );