fix: prevent duplicate artifact creation
This commit is contained in:
@@ -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 (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user