fix: prevent duplicate artifact creation
This commit is contained in:
@@ -19,6 +19,9 @@ interface ComposeOverlayProps {
|
|||||||
onActiveChange?: (active: boolean) => void;
|
onActiveChange?: (active: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const HOLD_THRESHOLD_MS = 250;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Self-contained compose overlay. Each consumer renders its own instance
|
* Self-contained compose overlay. Each consumer renders its own instance
|
||||||
* with props that determine the mode (new stream vs. reply).
|
* with props that determine the mode (new stream vs. reply).
|
||||||
@@ -41,9 +44,15 @@ export function ComposeOverlay({
|
|||||||
const createParticle = useCreateParticle();
|
const createParticle = useCreateParticle();
|
||||||
const createStream = useCreateStreamParticle();
|
const createStream = useCreateStreamParticle();
|
||||||
|
|
||||||
// Refs to avoid stale closures in keyboard handler
|
// Refs for synchronous reads in keyboard handlers
|
||||||
const stepRef = useRef(step);
|
const stepRef = useRef(step);
|
||||||
stepRef.current = step;
|
const submittingRef = useRef(false);
|
||||||
|
const recordStartRef = useRef(0);
|
||||||
|
|
||||||
|
const setStepSync = useCallback((next: ComposeStep) => {
|
||||||
|
stepRef.current = next;
|
||||||
|
setStep(next);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Notify parent when active state changes
|
// Notify parent when active state changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -51,21 +60,22 @@ export function ComposeOverlay({
|
|||||||
}, [step, onActiveChange]);
|
}, [step, onActiveChange]);
|
||||||
|
|
||||||
const cancel = useCallback(() => {
|
const cancel = useCallback(() => {
|
||||||
setStep("idle");
|
submittingRef.current = false;
|
||||||
|
setStepSync("idle");
|
||||||
setError(null);
|
setError(null);
|
||||||
setTextContent("");
|
setTextContent("");
|
||||||
setMediaStream(null);
|
setMediaStream(null);
|
||||||
setReviewBlob(null);
|
setReviewBlob(null);
|
||||||
setReviewDurationMs(0);
|
setReviewDurationMs(0);
|
||||||
setReviewMimeType(null);
|
setReviewMimeType(null);
|
||||||
}, []);
|
}, [setStepSync]);
|
||||||
|
|
||||||
const { startRecording, stopRecording, cancelRecording } = useRecorder({
|
const { startRecording, stopRecording, cancelRecording } = useRecorder({
|
||||||
mode: recordingMode,
|
mode: recordingMode,
|
||||||
onStreamReady: (stream) => setMediaStream(stream),
|
onStreamReady: (stream) => setMediaStream(stream),
|
||||||
onStreamCleanup: () => setMediaStream(null),
|
onStreamCleanup: () => setMediaStream(null),
|
||||||
onFinish: (blob, durationMs, mimeType) => {
|
onFinish: (blob, durationMs, mimeType) => {
|
||||||
setStep("reviewing");
|
setStepSync("reviewing");
|
||||||
setReviewBlob(blob);
|
setReviewBlob(blob);
|
||||||
setReviewDurationMs(durationMs);
|
setReviewDurationMs(durationMs);
|
||||||
setReviewMimeType(mimeType);
|
setReviewMimeType(mimeType);
|
||||||
@@ -144,7 +154,8 @@ export function ComposeOverlay({
|
|||||||
|
|
||||||
// Reply mode: create particle directly under targetPath
|
// Reply mode: create particle directly under targetPath
|
||||||
const onSubmitReply = useEffectEvent(async () => {
|
const onSubmitReply = useEffectEvent(async () => {
|
||||||
if (!targetPath || !userEmail) return;
|
if (!targetPath || !userEmail || submittingRef.current) return;
|
||||||
|
submittingRef.current = true;
|
||||||
await createChildParticle(targetPath);
|
await createChildParticle(targetPath);
|
||||||
cancel();
|
cancel();
|
||||||
});
|
});
|
||||||
@@ -152,7 +163,8 @@ export function ComposeOverlay({
|
|||||||
// New stream mode: create stream + first child
|
// New stream mode: create stream + first child
|
||||||
const handleStreamSubmit = useCallback(
|
const handleStreamSubmit = useCallback(
|
||||||
async (streamName: string, visibleTo: string[]) => {
|
async (streamName: string, visibleTo: string[]) => {
|
||||||
if (!userEmail) return;
|
if (!userEmail || submittingRef.current) return;
|
||||||
|
submittingRef.current = true;
|
||||||
|
|
||||||
const streamId = await createStream.mutateAsync({
|
const streamId = await createStream.mutateAsync({
|
||||||
networkId,
|
networkId,
|
||||||
@@ -199,17 +211,22 @@ export function ComposeOverlay({
|
|||||||
case "idle": {
|
case "idle": {
|
||||||
if (e.key === "`" && !e.repeat) {
|
if (e.key === "`" && !e.repeat) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setStep("recording");
|
recordStartRef.current = Date.now();
|
||||||
|
setStepSync("recording");
|
||||||
startRecording();
|
startRecording();
|
||||||
} else if (e.key === "t" || e.key === "T") {
|
} else if (e.key === "t" || e.key === "T") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setStep("typing");
|
setStepSync("typing");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "recording": {
|
case "recording": {
|
||||||
if (e.key === "q" || e.key === "Q" || e.key === "Escape") {
|
if (e.key === "`" && !e.repeat) {
|
||||||
|
// Second tap stops recording (toggle mode)
|
||||||
|
e.preventDefault();
|
||||||
|
stopRecording();
|
||||||
|
} else if (e.key === "q" || e.key === "Q" || e.key === "Escape") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
cancelRecording();
|
cancelRecording();
|
||||||
cancel();
|
cancel();
|
||||||
@@ -227,7 +244,7 @@ export function ComposeOverlay({
|
|||||||
if (targetPath) {
|
if (targetPath) {
|
||||||
onSubmitReply();
|
onSubmitReply();
|
||||||
} else {
|
} else {
|
||||||
setStep("configuring");
|
setStepSync("configuring");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -238,7 +255,11 @@ export function ComposeOverlay({
|
|||||||
const handleKeyUp = (e: KeyboardEvent) => {
|
const handleKeyUp = (e: KeyboardEvent) => {
|
||||||
if (stepRef.current === "recording" && e.key === "`") {
|
if (stepRef.current === "recording" && e.key === "`") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
stopRecording();
|
// Only stop on release if held long enough (hold-to-record mode).
|
||||||
|
// Quick taps are handled by the second keydown (toggle mode).
|
||||||
|
if (Date.now() - recordStartRef.current >= HOLD_THRESHOLD_MS) {
|
||||||
|
stopRecording();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -248,7 +269,7 @@ export function ComposeOverlay({
|
|||||||
window.removeEventListener("keydown", handleKeyDown);
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
window.removeEventListener("keyup", handleKeyUp);
|
window.removeEventListener("keyup", handleKeyUp);
|
||||||
};
|
};
|
||||||
}, [targetPath, startRecording, stopRecording, cancelRecording, cancel]);
|
}, [targetPath, startRecording, stopRecording, cancelRecording, cancel, setStepSync]);
|
||||||
|
|
||||||
// --- Render ---
|
// --- Render ---
|
||||||
|
|
||||||
@@ -256,7 +277,7 @@ export function ComposeOverlay({
|
|||||||
|
|
||||||
const handleTextAdvance = targetPath
|
const handleTextAdvance = targetPath
|
||||||
? onSubmitReply
|
? onSubmitReply
|
||||||
: () => setStep("configuring");
|
: () => setStepSync("configuring");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in New Issue
Block a user