163 lines · 5.5 KB · TypeScript
RawDownload
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<GitActionProgress>(EMPTY_COPY_FEEDBACK);  const copyRequestIdRef = useRef(0);  const copyFeedbackTimerRef = useRef<ReturnType<typeof setTimeout> | 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<HardwareKeyboardCommand>(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 (    <>      <T3KeyboardCommands enabledCommands={enabledCommands} onCommand={onCommand}>        {children}      </T3KeyboardCommands>      <GitActionProgressOverlay progress={copyFeedback} onDismiss={dismissCopyFeedback} />    </>  );}