Skip to content
Source
Sign inSign up

ThreadUndoNotice.tsx

96 lines3.6 KB

1import { useAndroidFloatingTabs } from "../settings/appearance/AppearancePreferencesProvider";
2import { useState } from "react";
3import { useKeyboardState } from "react-native-keyboard-controller";
4import { Platform, View } from "react-native";
5import { useSafeAreaInsets } from "react-native-safe-area-context";
6import { Toast } from "../../components/ui/Toast";
7import {
8 useDeviceType,
9 useDuoBottomBarInset,
10 useNativeLayoutMetrics,
11 useNativeColumnLayoutMetrics,
12} from "../../native/native-layout-metrics";
13import { workspaceSearchBottomClearance } from "../layout/workspace/workspace-tabs-state";
14import { useWorkspaceTabs } from "../layout/workspace/WorkspaceTabsContext";
15import {
16 dismissThreadUndoNotice,
17 readThreadUndoNotice,
18 useThreadUndoNotice,
19} from "./thread-undo-notice";
20​
21export function ThreadUndoNotice(props: { readonly visible: boolean }) {
22 const floatingTabsEnabled = useAndroidFloatingTabs();
23 const notice = useThreadUndoNotice();
24 const insets = useSafeAreaInsets();
25 const columnMetrics = useNativeColumnLayoutMetrics();
26 const rootMetrics = useNativeLayoutMetrics();
27 const workspace = useWorkspaceTabs();
28 const deviceType = useDeviceType();
29 const keyboardHeight = useKeyboardState((keyboard) => keyboard.height);
30 const horizontalInsets =
31 Platform.OS === "ios"
32 ? deviceType === "phone"
33 ? { left: 29, right: 23 }
34 : { left: 20, right: 20 }
35 : { left: 12, right: 12 };
36 // The Duo's native tab pill extends above the 48-point toolbar reservation.
37 const workspacePadding =
38 useDuoBottomBarInset() === undefined
39 ? deviceType === "phone" && keyboardHeight === 0
40 ? 32
41 : 16
42 : 8;
43 // Replacing a notice must also cancel the native gesture tracking the old one.
44 const [instance, setInstance] = useState({ notice, key: 0 });
45 if (instance.notice !== notice) setInstance({ notice, key: instance.key + 1 });
46 if (!props.visible || notice === null) return null;
47 const foldedResting =
48 rootMetrics?.hasHinge &&
49 rootMetrics.horizontalSizeClass === "compact" &&
50 workspace?.state.tabs.threads.searchMode === "resting";
51 const bottom =
52 Platform.OS === "ios"
53 ? foldedResting
54 ? Math.max(rootMetrics.safeArea.bottom, keyboardHeight) + 8
55 : rootMetrics?.hasHinge && rootMetrics.verticalBarEdge !== "none"
56 ? // The column's safe area already includes UIKit's search toolbar in this posture.
57 workspaceSearchBottomClearance({
58 bottomInset: rootMetrics.safeArea.bottom,
59 bottomBarInset: rootMetrics.bottomBarInset,
60 keyboardHeight,
61 }) -
62 16 +
63 8
64 : Math.max(
65 insets.bottom,
66 columnMetrics?.safeArea.bottom ?? 0,
67 (workspace?.bottomClearance ?? 0) - workspacePadding,
68 ) + 8
69 : floatingTabsEnabled && keyboardHeight === 0
70 ? Math.max(insets.bottom + 12, workspace?.bottomClearance ?? 0)
71 : insets.bottom + 12;
72 return (
73 <View
74 key={instance.key}
75 collapsable={false}
76 className="absolute z-[100]"
77 style={{ bottom, ...horizontalInsets }}
78 pointerEvents="box-none"
79 >
80 <Toast
81 message={`${notice.action} thread`}
82 action={{
83 label: "Undo",
84 onPress: () => {
85 void notice.undo();
86 },
87 }}
88 dismissMode="swipe"
89 onDismiss={() => {
90 // A newer action can replace the notice while the swipe is animating out.
91 if (readThreadUndoNotice() === notice) dismissThreadUndoNotice();
92 }}
93 />
94 </View>
95 );