import { StackActions, useNavigation } from "@react-navigation/native"; import { resolveThreadReferenceCopyTarget } from "@t3tools/shared/threadReference"; import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, type PropsWithChildren, } from "react"; import { tryCopyTextWithHaptic } from "../../lib/copyTextWithHaptic"; import { T3KeyboardCommands } from "../../native/T3KeyboardCommands"; import { useThreadShell } from "../../state/entities"; import type { GitActionProgress } from "../../state/use-vcs-action-state"; import { GitActionProgressOverlay } from "../threads/GitActionProgressOverlay"; import { dispatchHardwareKeyboardCommand, getHardwareKeyboardCommandRegistrationVersion, getRegisteredHardwareKeyboardCommands, parseActiveThreadPath, subscribeToHardwareKeyboardCommandRegistrations, type HardwareKeyboardCommand, } from "./hardwareKeyboardCommands"; const EMPTY_COPY_FEEDBACK: GitActionProgress = { phase: "idle", label: null, description: null, }; const COPY_FEEDBACK_DISMISS_MS = 3_000; export function HardwareKeyboardCommandProvider({ children, pathname, }: PropsWithChildren<{ readonly pathname: string }>) { const navigation = useNavigation(); const activeThreadRef = useMemo(() => parseActiveThreadPath(pathname), [pathname]); const activeThread = useThreadShell(activeThreadRef); const copyTarget = useMemo( () => activeThreadRef === null ? null : resolveThreadReferenceCopyTarget({ threadId: activeThread?.id ?? activeThreadRef.threadId, pullRequests: activeThread?.pullRequests, linkedPullRequestUrl: (activeThread?.linkedPullRequest ?? activeThread?.branchPullRequest)?.url ?? null, }), [activeThread, activeThreadRef], ); const [copyFeedback, setCopyFeedback] = useState(EMPTY_COPY_FEEDBACK); const copyRequestIdRef = useRef(0); const copyFeedbackTimerRef = useRef | null>(null); const dismissCopyFeedback = useCallback(() => { if (copyFeedbackTimerRef.current !== null) { clearTimeout(copyFeedbackTimerRef.current); copyFeedbackTimerRef.current = null; } setCopyFeedback(EMPTY_COPY_FEEDBACK); }, []); const showCopyFeedback = useCallback((feedback: GitActionProgress) => { if (copyFeedbackTimerRef.current !== null) { clearTimeout(copyFeedbackTimerRef.current); } setCopyFeedback(feedback); copyFeedbackTimerRef.current = setTimeout(() => { copyFeedbackTimerRef.current = null; setCopyFeedback(EMPTY_COPY_FEEDBACK); }, COPY_FEEDBACK_DISMISS_MS); }, []); useEffect( () => () => { if (copyFeedbackTimerRef.current !== null) { clearTimeout(copyFeedbackTimerRef.current); } }, [], ); const registrationVersion = useSyncExternalStore( subscribeToHardwareKeyboardCommandRegistrations, getHardwareKeyboardCommandRegistrationVersion, getHardwareKeyboardCommandRegistrationVersion, ); const enabledCommands = useMemo(() => { const commands = new Set(getRegisteredHardwareKeyboardCommands()); commands.add("newTask"); if (pathname !== "/" || navigation.canGoBack()) commands.add("back"); if (activeThreadRef !== null) { commands.add("files"); commands.add("terminal"); commands.add("review"); if (pathname.split("/")[4] !== "terminal") commands.add("copyThreadReference"); } return [...commands]; }, [pathname, registrationVersion, navigation]); const onCommand = useCallback( (command: HardwareKeyboardCommand) => { if (dispatchHardwareKeyboardCommand(command)) return; if (command === "copyThreadReference") { if (copyTarget === null) return; const requestId = ++copyRequestIdRef.current; void tryCopyTextWithHaptic(copyTarget.value, { target: copyTarget.clipboardTarget, }).then((didCopy) => { if (requestId !== copyRequestIdRef.current) return; showCopyFeedback( didCopy ? { phase: "success", label: copyTarget.successTitle, description: copyTarget.value, } : { phase: "error", label: copyTarget.failureTitle, description: "Try again.", }, ); }); return; } if (command === "newTask") { navigation.navigate("NewTaskSheet", { screen: "NewTask" }); return; } if (command === "back") { if (navigation.canGoBack()) { navigation.goBack(); } else { navigation.dispatch(StackActions.replace("Home")); } return; } const thread = parseActiveThreadPath(pathname); if (!thread) return; if (command === "files" && !/\/files(?:\/|$)/.test(pathname)) { navigation.navigate("ThreadFiles", thread); } if (command === "terminal" && !/\/terminal(?:\/|$)/.test(pathname)) { navigation.navigate("ThreadTerminal", thread); } if (command === "review" && !/\/review(?:\/|$)/.test(pathname)) { navigation.navigate("ThreadReview", thread); } }, [copyTarget, navigation, pathname, showCopyFeedback], ); return ( <> {children} ); }