fix: prevent duplicate artifact creation

This commit is contained in:
talksik
2026-03-21 16:38:37 -07:00
parent e488cc79d0
commit 8168b7d8f5
+35 -14
View File
@@ -19,6 +19,9 @@ interface ComposeOverlayProps {
onActiveChange?: (active: boolean) => void;
}
const HOLD_THRESHOLD_MS = 250;
/**
* Self-contained compose overlay. Each consumer renders its own instance
* with props that determine the mode (new stream vs. reply).
@@ -41,9 +44,15 @@ export function ComposeOverlay({
const createParticle = useCreateParticle();
const createStream = useCreateStreamParticle();
// Refs to avoid stale closures in keyboard handler
// Refs for synchronous reads in keyboard handlers
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
useEffect(() => {
@@ -51,21 +60,22 @@ export function ComposeOverlay({
}, [step, onActiveChange]);
const cancel = useCallback(() => {
setStep("idle");
submittingRef.current = false;
setStepSync("idle");
setError(null);
setTextContent("");
setMediaStream(null);
setReviewBlob(null);
setReviewDurationMs(0);
setReviewMimeType(null);
}, []);
}, [setStepSync]);
const { startRecording, stopRecording, cancelRecording } = useRecorder({
mode: recordingMode,
onStreamReady: (stream) => setMediaStream(stream),
onStreamCleanup: () => setMediaStream(null),
onFinish: (blob, durationMs, mimeType) => {
setStep("reviewing");
setStepSync("reviewing");
setReviewBlob(blob);
setReviewDurationMs(durationMs);
setReviewMimeType(mimeType);
@@ -144,7 +154,8 @@ export function ComposeOverlay({
// Reply mode: create particle directly under targetPath
const onSubmitReply = useEffectEvent(async () => {
if (!targetPath || !userEmail) return;
if (!targetPath || !userEmail || submittingRef.current) return;
submittingRef.current = true;
await createChildParticle(targetPath);
cancel();
});
@@ -152,7 +163,8 @@ export function ComposeOverlay({
// New stream mode: create stream + first child
const handleStreamSubmit = useCallback(
async (streamName: string, visibleTo: string[]) => {
if (!userEmail) return;
if (!userEmail || submittingRef.current) return;
submittingRef.current = true;
const streamId = await createStream.mutateAsync({
networkId,
@@ -199,17 +211,22 @@ export function ComposeOverlay({
case "idle": {
if (e.key === "`" && !e.repeat) {
e.preventDefault();
setStep("recording");
recordStartRef.current = Date.now();
setStepSync("recording");
startRecording();
} else if (e.key === "t" || e.key === "T") {
e.preventDefault();
setStep("typing");
setStepSync("typing");
}
break;
}
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();
cancelRecording();
cancel();
@@ -227,7 +244,7 @@ export function ComposeOverlay({
if (targetPath) {
onSubmitReply();
} else {
setStep("configuring");
setStepSync("configuring");
}
}
break;
@@ -238,7 +255,11 @@ export function ComposeOverlay({
const handleKeyUp = (e: KeyboardEvent) => {
if (stepRef.current === "recording" && e.key === "`") {
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("keyup", handleKeyUp);
};
}, [targetPath, startRecording, stopRecording, cancelRecording, cancel]);
}, [targetPath, startRecording, stopRecording, cancelRecording, cancel, setStepSync]);
// --- Render ---
@@ -256,7 +277,7 @@ export function ComposeOverlay({
const handleTextAdvance = targetPath
? onSubmitReply
: () => setStep("configuring");
: () => setStepSync("configuring");
return (
<>