1import { useFocusEffect } from "@react-navigation/native";
2import { useCallback, useEffect, useRef } from "react";
3import {
4 voiceInputBlocksSubmission,
5 type VoiceInputState,
6} from "@t3tools/client-runtime/voice-input";
7
8import type { ComposerEditorSelection } from "../../components/ComposerEditor";
9import { useGlobalVoiceInput } from "./VoiceInputProvider";
10import { createVoiceInputTarget } from "./voiceInputSession";
11
12const IDLE_STATE: VoiceInputState = { phase: "idle", error: null, errorAction: null };
13
14export function useVoiceInputController(input: {
15 readonly ownerKey: string | null;
16 /** Shown by the global dictation pill when this composer is off screen. */
17 readonly label: string;
18 readonly readDraftMessage: () => string | null;
19 readonly subscribeToDraftChanges: (onChange: () => void) => () => void;
20 readonly selection: ComposerEditorSelection;
21 readonly disabled?: boolean;
22 readonly onChangeDraftMessage: (value: string) => void;
23 readonly onChangeSelection: (selection: ComposerEditorSelection) => void;
24}) {
25 const global = useGlobalVoiceInput();
26 const { setOwnerFocused, session } = global;
27 const latestInput = useRef(input);
28 latestInput.current = input;
29 const mounted = useRef(true);
30 useEffect(() => {
31 mounted.current = true;
32 return () => {
33 mounted.current = false;
34 };
35 }, []);
36
37 useFocusEffect(
38 useCallback(() => {
39 const ownerKey = input.ownerKey;
40 if (!ownerKey) return;
41 setOwnerFocused(ownerKey, true);
42 return () => setOwnerFocused(ownerKey, false);
43 }, [input.ownerKey, setOwnerFocused]),
44 );
45
46 const start = useCallback(() => {
47 const captured = latestInput.current;
48 if (!captured.ownerKey || captured.disabled) return;
49 void session.start({
50 ...createVoiceInputTarget(
51 captured.ownerKey,
52 captured.readDraftMessage,
53 (text, selection) => {
54 captured.onChangeDraftMessage(text);
55 if (mounted.current && latestInput.current.ownerKey === captured.ownerKey) {
56 latestInput.current.onChangeSelection(selection);
57 }
58 },
59 captured.selection,
60 captured.subscribeToDraftChanges,
61 ),
62 label: captured.label,
63 });
64 }, [session]);
65 const state = global.ownerKey === input.ownerKey ? global.state : IDLE_STATE;
66 const isBusy = voiceInputBlocksSubmission(state);
67 return {
68 isAvailable: global.isAvailable && (!global.isBusy || global.ownerKey === input.ownerKey),
69 state,
70 audioLevels: global.audioLevels,
71 elapsedSeconds: global.elapsedSeconds,
72 isBusy,
73 freezesEditor: isBusy,
74 blocksSubmission: isBusy,
75 start,
76 stop: global.stop,
77 cancel: global.cancel,
78 };
79}