This commit is contained in:
Arjun Patel
2026-06-01 14:15:22 -07:00
parent 2d9b4805e0
commit 580703fdf6
21 changed files with 302 additions and 220 deletions
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useEffectEvent, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { useAuthStore } from "@/stores/auth-store";
import { useCreateParticle, useCreateStreamParticle } from "@/hooks/use-create-particle";
@@ -80,15 +80,17 @@ export function ComposeOverlay({
const invalidateUsage = useInvalidateNetworkUsage();
const quotaExhausted = isUsageExhausted(usage);
// Refs for synchronous reads in keyboard handlers
// Latest props/state for synchronous reads in keyboard handlers.
const stepRef = useRef(step);
const recordStartRef = useRef(0);
const disabledRef = useRef(disabled);
disabledRef.current = disabled;
const quotaExhaustedRef = useRef(quotaExhausted);
quotaExhaustedRef.current = quotaExhausted;
const recordingSourceRef = useRef(recordingSource);
recordingSourceRef.current = recordingSource;
useEffect(() => {
disabledRef.current = disabled;
quotaExhaustedRef.current = quotaExhausted;
recordingSourceRef.current = recordingSource;
}, [disabled, quotaExhausted, recordingSource]);
const setStepSync = useCallback((next: ComposeStep) => {
stepRef.current = next;
@@ -361,8 +363,8 @@ export function ComposeOverlay({
return false;
}, [cancel]);
// Reply mode: create particle directly under targetPath
const onSubmitReply = useEffectEvent(async () => {
// Reply mode: create particle directly under targetPath.
const onSubmitReply = useCallback(async () => {
if (!targetPath || !userId || stepRef.current === "submitting") return;
setStepSync("submitting");
try {
@@ -371,7 +373,7 @@ export function ComposeOverlay({
} catch (err) {
if (!handleQuotaError(err)) throw err;
}
});
}, [targetPath, userId, setStepSync, createChildParticle, cancel, handleQuotaError]);
// New stream mode: create stream + first child
const handleStreamSubmit = useCallback(
@@ -397,7 +399,7 @@ export function ComposeOverlay({
if (!handleQuotaError(err)) throw err;
}
},
[networkId, userId, createStream, createChildParticle, cancel, handleQuotaError],
[networkId, userId, createStream, createChildParticle, cancel, handleQuotaError, setStepSync],
);
// --- Compose intent handlers ---
@@ -468,20 +470,25 @@ export function ComposeOverlay({
// executes the matching handler and clears the intent. Keyboard handlers
// call the same handlers directly without a store round-trip.
const intent = useComposeIntentStore((s) => s.intent);
const clearIntent = useComposeIntentStore((s) => s.clear);
// Consume fire-and-forget intents from the external store. Reacting in the
// store subscription (not an effect body) keeps these state-updating handlers
// off the render path and avoids an extra dispatch→render bounce.
useEffect(() => {
if (!intent) return;
switch (intent.kind) {
case "record": handleRecordIntent(); break;
case "text": handleTextIntent(); break;
case "stop": handleStopIntent(); break;
case "cancel": handleCancelIntent(); break;
case "send": handleSendIntent(); break;
}
clearIntent();
}, [intent, handleRecordIntent, handleTextIntent, handleStopIntent, handleCancelIntent, handleSendIntent, clearIntent]);
return useComposeIntentStore.subscribe((state, prev) => {
const intent = state.intent;
if (!intent || intent === prev.intent) return;
switch (intent.kind) {
case "record": handleRecordIntent(); break;
case "text": handleTextIntent(); break;
case "stop": handleStopIntent(); break;
case "cancel": handleCancelIntent(); break;
case "send": handleSendIntent(); break;
}
clearIntent();
});
}, [handleRecordIntent, handleTextIntent, handleStopIntent, handleCancelIntent, handleSendIntent, clearIntent]);
// --- Keyboard handling ---