1import { useNavigation, useNavigationState } from "@react-navigation/native";
2import { useCallback, useEffect, useRef } from "react";
3import { BackHandler, Keyboard, View, type TextInputInstance } from "react-native";
4import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader";
5import { CompactBrandTitle } from "../../components/CompactBrandTitle";
6import { MaterialSearchField } from "../../components/MaterialSearchField";
7import { WorkspaceConnectionTitle } from "../home/WorkspaceConnectionTitle";
8import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands";
9import { useWorkspaceTabs } from "../workspace/WorkspaceTabsContext";
10
11/** PR search shares the thread list's native Android header placement. */
12export function MaterialPullRequestToolbar(props: { readonly onRequestVisibility?: () => void }) {
13 const navigation = useNavigation();
14 const workspace = useWorkspaceTabs()!;
15 const { dispatch, getScrollOffset, recordScrollOffset, isTabActive } = workspace;
16 const homeFocused = useNavigationState((state) => state.routes[state.index]?.name === "Home");
17 const listFocused = homeFocused && workspace.state.selectedTab === "pullRequests";
18 const { onRequestVisibility } = props;
19 const tab = workspace.state.tabs.pullRequests;
20 const input = useRef<TextInputInstance>(null);
21 const searching = tab.searchMode !== "resting";
22 const openSearch = useCallback(() => {
23 onRequestVisibility?.();
24 dispatch({
25 type: "scroll",
26 tab: "pullRequests",
27 offset: getScrollOffset("pullRequests"),
28 });
29 dispatch({ type: "search", tab: "pullRequests" });
30 input.current?.focus();
31 return true;
32 }, [dispatch, getScrollOffset, onRequestVisibility]);
33 useHardwareKeyboardCommand("focusSearch", openSearch);
34 const closeSearch = useCallback(() => {
35 Keyboard.dismiss();
36 recordScrollOffset("pullRequests", tab.searchEntryOffset);
37 dispatch({ type: "cancel", tab: "pullRequests" });
38 }, [dispatch, recordScrollOffset, tab.searchEntryOffset]);
39 useEffect(() => {
40 if (!searching || !listFocused) return;
41 const subscription = BackHandler.addEventListener("hardwareBackPress", () => {
42 // Frozen Home screens can keep effects alive after a detail route opens.
43 const state = navigation.getState();
44 if (!state || state.routes[state.index]?.name !== "Home" || !isTabActive("pullRequests"))
45 return false;
46 if (Keyboard.isVisible()) Keyboard.dismiss();
47 else closeSearch();
48 return true;
49 });
50 return () => subscription.remove();
51 }, [searching, closeSearch, listFocused, navigation, isTabActive]);
52 return (
53 <AndroidScreenHeader
54 title="Pull requests"
55 titleContent={
56 searching ? (
57 <MaterialSearchField
58 inputRef={input}
59 accessibilityLabel="Search pull requests"
60 clearAccessibilityLabel="Clear search"
61 placeholder="Search pull requests"
62 value={tab.query}
63 onChangeText={(query) => dispatch({ type: "query", tab: "pullRequests", query })}
64 />
65 ) : (
66 <View className="min-w-0 flex-1 pl-3">
67 <WorkspaceConnectionTitle
68 grow
69 brand={
70 <CompactBrandTitle
71 allowFontScaling={false}
72 accessibilityLabel="The Knife, Pull requests"
73 />
74 }
75 onPress={() =>
76 navigation.navigate("SettingsSheet", {
77 screen: "SettingsContent",
78 params: { screen: "SettingsEnvironments" },
79 })
80 }
81 />
82 </View>
83 )
84 }
85 leading={
86 searching ? (
87 <AndroidHeaderIconButton
88 accessibilityLabel="Close search"
89 icon="arrow.left"
90 onPress={closeSearch}
91 />
92 ) : undefined
93 }
94 actions={
95 searching
96 ? []
97 : [
98 {
99 accessibilityLabel: "Search pull requests",
100 icon: "magnifyingglass",
101 onPress: openSearch,
102 },
103 {
104 accessibilityLabel: "Open settings",
105 icon: "gearshape",
106 onPress: () =>
107 navigation.navigate("SettingsSheet", {
108 screen: "SettingsContent",
109 params: { screen: "Settings" },
110 }),
111 },
112 ]
113 }
114 />
115 );
116}