1import { useRef, useState, type ComponentProps } from "react";
2import {
3 useWindowDimensions,
4 View,
5 type NativeScrollEvent,
6 type NativeSyntheticEvent,
7} from "react-native";
8import { useSafeAreaInsets } from "react-native-safe-area-context";
9import { ScrollComposeButton } from "../../components/ScrollComposeButton.android";
10import {
11 androidHomeFabPosition,
12 type AndroidHomeFabLayout as SharedAndroidHomeFabLayout,
13} from "./AndroidHomeFab.shared";
14import { useWorkspaceState } from "../../state/workspace";
15import { FabScrollContext } from "./FabScrollContext";
16import { updateFabScroll } from "./fab-scroll";
17import { useAndroidControlSizing } from "../../components/ui/useAndroidControlSizing";
18import { WorkspaceTabsContext, useWorkspaceTabs } from "../layout/workspace/WorkspaceTabsContext";
19import { FLOATING_TABS, floatingTabsMetrics } from "../layout/workspace/floating-workspace-tabs";
20import { useEnabledNavigationTabs } from "../layout/workspace/navigation-tabs";
21import {
22 useAppearancePreferences,
23 useAndroidFloatingTabs,
24} from "../settings/appearance/AppearancePreferencesProvider";
25
26export function AndroidHomeFabLayout(props: ComponentProps<typeof SharedAndroidHomeFabLayout>) {
27 const floatingTabsEnabled = useAndroidFloatingTabs();
28 const insets = useSafeAreaInsets();
29 const { appearance } = useAppearancePreferences();
30 const { fontScale } = useWindowDimensions();
31 const [layoutWidth, setLayoutWidth] = useState<number | null>(null);
32 const { state } = useWorkspaceState();
33 const [expanded, setExpanded] = useState(true);
34 const { scale, fabSize } = useAndroidControlSizing();
35 const enabledTabs = useEnabledNavigationTabs();
36 const barWidth =
37 layoutWidth === null
38 ? undefined
39 : layoutWidth - (Math.max(insets.left, FLOATING_TABS.edge) - FLOATING_TABS.edge);
40 const size = floatingTabsEnabled
41 ? floatingTabsMetrics(enabledTabs.length + 1, scale, fabSize, barWidth).height
42 : fabSize;
43 const workspace = useWorkspaceTabs();
44 const bottomClearance = insets.bottom + FLOATING_TABS.bottom + FLOATING_TABS.edge + size;
45 const context = floatingTabsEnabled && workspace ? { ...workspace, bottomClearance } : workspace;
46 const scrollState = useRef({ anchor: 0, expanded: true });
47 const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
48 const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
49 const next = updateFabScroll(
50 scrollState.current,
51 contentOffset.y,
52 contentSize.height - layoutMeasurement.height,
53 );
54 if (next.expanded !== scrollState.current.expanded) setExpanded(next.expanded);
55 scrollState.current = next;
56 };
57
58 // Remount only the FAB when its font or pane changes to clear the cached expanded width.
59 // Floating navigation shares this row; Material split views put New thread in the rail.
60 return (
61 <View className="flex-1" onLayout={(event) => setLayoutWidth(event.nativeEvent.layout.width)}>
62 <WorkspaceTabsContext value={context}>
63 <FabScrollContext value={onScroll}>{props.children}</FabScrollContext>
64 </WorkspaceTabsContext>
65 {!props.disabled &&
66 (!props.sidebar || floatingTabsEnabled) &&
67 state.hasConnections &&
68 layoutWidth !== null ? (
69 <ScrollComposeButton
70 key={`${appearance.baseFontSize}:${fontScale}:${layoutWidth}`}
71 size={size}
72 expanded={floatingTabsEnabled ? false : expanded}
73 onPress={props.onStartNewTask}
74 className="absolute"
75 style={
76 floatingTabsEnabled
77 ? {
78 right: FLOATING_TABS.edge,
79 bottom: insets.bottom + FLOATING_TABS.bottom,
80 }
81 : androidHomeFabPosition(insets.bottom, props.sidebar)
82 }
83 />
84 ) : null}
85 </View>
86 );
87}