mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): send finalized dictation in one click
Co-authored-by: Kenny Lopez <klopez4212@gmail.com> Signed-off-by: John Tennant <jtennant@squareup.com>
This commit is contained in:
co-authored by
Kenny Lopez
parent
3057890dd6
commit
0da28eff4e
@@ -1,7 +1,10 @@
|
||||
import type * as React from "react";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useFeatureEnabled } from "@/shared/features";
|
||||
import { getDictationSendDecision } from "../lib/voiceInput";
|
||||
import {
|
||||
getDictationSendDecision,
|
||||
shouldAutoSubmitDictation,
|
||||
} from "../lib/voiceInput";
|
||||
import { DictationButton } from "../ui/DictationButton";
|
||||
import { useComposerDictation } from "./useComposerDictation";
|
||||
|
||||
@@ -15,10 +18,14 @@ interface UseMessageComposerDictationOptions {
|
||||
setEditorContent: (text: string) => void;
|
||||
draftKey: string | null;
|
||||
composerRef: React.RefObject<HTMLElement | null>;
|
||||
submitMessageRef: React.MutableRefObject<() => void>;
|
||||
}
|
||||
|
||||
export function useMessageComposerDictation({
|
||||
disabled,
|
||||
draftKey,
|
||||
setEditorContent,
|
||||
submitMessageRef,
|
||||
...options
|
||||
}: UseMessageComposerDictationOptions) {
|
||||
const enabled = useFeatureEnabled("voiceDictation");
|
||||
@@ -26,10 +33,34 @@ export function useMessageComposerDictation({
|
||||
setEditorContentRef.current = setEditorContent;
|
||||
const dictation = useComposerDictation({
|
||||
...options,
|
||||
disabled,
|
||||
draftKey,
|
||||
enabled,
|
||||
setEditorContentRef,
|
||||
});
|
||||
const { isRecording, isStarting, isTranscribing, stopRecording } = dictation;
|
||||
const sendAfterTranscriptionRef = useRef(false);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: composer scope and availability are the reset triggers for a queued send
|
||||
useEffect(() => {
|
||||
sendAfterTranscriptionRef.current = false;
|
||||
}, [disabled, draftKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!shouldAutoSubmitDictation({
|
||||
requested: sendAfterTranscriptionRef.current,
|
||||
isRecording,
|
||||
isStarting,
|
||||
isTranscribing,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
sendAfterTranscriptionRef.current = false;
|
||||
submitMessageRef.current();
|
||||
}, [isRecording, isStarting, isTranscribing, submitMessageRef]);
|
||||
|
||||
const prepareToSubmit = useCallback(() => {
|
||||
const decision = getDictationSendDecision({
|
||||
isRecording,
|
||||
@@ -37,6 +68,7 @@ export function useMessageComposerDictation({
|
||||
isTranscribing,
|
||||
});
|
||||
if (decision === "stop-recording") {
|
||||
sendAfterTranscriptionRef.current = true;
|
||||
stopRecording();
|
||||
}
|
||||
return decision === "send";
|
||||
|
||||
@@ -4,6 +4,7 @@ import test from "node:test";
|
||||
import {
|
||||
getDictationSendDecision,
|
||||
replaceTrailingTranscribedText,
|
||||
shouldAutoSubmitDictation,
|
||||
} from "./voiceInput.ts";
|
||||
|
||||
test("dictation send stops capture before submitting", () => {
|
||||
@@ -44,6 +45,32 @@ test("dictation send waits for the final transcript", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("dictation auto-submit waits until capture and transcription settle", () => {
|
||||
const state = {
|
||||
requested: true,
|
||||
isRecording: false,
|
||||
isStarting: false,
|
||||
isTranscribing: false,
|
||||
};
|
||||
assert.equal(shouldAutoSubmitDictation(state), true);
|
||||
assert.equal(
|
||||
shouldAutoSubmitDictation({ ...state, requested: false }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldAutoSubmitDictation({ ...state, isRecording: true }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldAutoSubmitDictation({ ...state, isStarting: true }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldAutoSubmitDictation({ ...state, isTranscribing: true }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
// ── replaceTrailingTranscribedText ──────────────────────────────────────────
|
||||
|
||||
test("replaceTrailingTranscribedText_appendsWhenNoPrevious", () => {
|
||||
|
||||
@@ -24,6 +24,20 @@ export function getDictationSendDecision({
|
||||
return "send";
|
||||
}
|
||||
|
||||
export function shouldAutoSubmitDictation({
|
||||
requested,
|
||||
isRecording,
|
||||
isStarting,
|
||||
isTranscribing,
|
||||
}: {
|
||||
requested: boolean;
|
||||
isRecording: boolean;
|
||||
isStarting: boolean;
|
||||
isTranscribing: boolean;
|
||||
}): boolean {
|
||||
return requested && !isRecording && !isStarting && !isTranscribing;
|
||||
}
|
||||
|
||||
export function replaceTrailingTranscribedText(
|
||||
fullText: string,
|
||||
previousTranscribedText: string,
|
||||
|
||||
@@ -232,7 +232,6 @@ function MessageComposerImpl({
|
||||
(replyTarget
|
||||
? `Reply to ${replyTarget.author} in #${channelName}`
|
||||
: `Message #${channelName}`));
|
||||
|
||||
const richText = useRichTextEditor({
|
||||
placeholder: computedPlaceholder,
|
||||
editable: !disabled,
|
||||
@@ -277,6 +276,7 @@ function MessageComposerImpl({
|
||||
setEditorContent: richText.setContent,
|
||||
draftKey: effectiveDraftKey,
|
||||
composerRef: composerScrollRef,
|
||||
submitMessageRef,
|
||||
});
|
||||
prepareDictationSubmitRef.current = dictation.prepareToSubmit;
|
||||
const linkEditor = useLinkEditor(richText);
|
||||
|
||||
Reference in New Issue
Block a user