From 9ffcf7b3ef49648403229c6b43777b430f47b19f Mon Sep 17 00:00:00 2001 From: benya Date: Sun, 8 Mar 2026 20:06:04 +0300 Subject: [PATCH] perf(realtime): debounce typing start/stop events --- web/src/components/MessageComposer.tsx | 60 +++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/web/src/components/MessageComposer.tsx b/web/src/components/MessageComposer.tsx index cdb4ab7..57d8397 100644 --- a/web/src/components/MessageComposer.tsx +++ b/web/src/components/MessageComposer.tsx @@ -109,6 +109,8 @@ export function MessageComposer() { const recordingStateRef = useRef("idle"); const lastEditingMessageIdRef = useRef(null); const prevActiveChatIdRef = useRef(null); + const typingActiveRef = useRef(false); + const typingStopTimerRef = useRef(null); const [isUploading, setIsUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); @@ -247,6 +249,9 @@ export function MessageComposer() { if (pointerUpHandlerRef.current) { window.removeEventListener("pointerup", pointerUpHandlerRef.current); } + if (typingStopTimerRef.current !== null) { + window.clearTimeout(typingStopTimerRef.current); + } }; }, [previewUrl]); @@ -261,6 +266,11 @@ export function MessageComposer() { if (prev && prev !== activeChatId) { const ws = getWs(); ws?.send(JSON.stringify({ event: "typing_stop", payload: { chat_id: prev } })); + typingActiveRef.current = false; + if (typingStopTimerRef.current !== null) { + window.clearTimeout(typingStopTimerRef.current); + typingStopTimerRef.current = null; + } } prevActiveChatIdRef.current = activeChatId; }, [activeChatId]); @@ -333,6 +343,46 @@ export function MessageComposer() { ws?.send(JSON.stringify({ event: eventName, payload: { chat_id: activeChatId } })); } + function emitTypingStopIfActive() { + if (!activeChatId) { + return; + } + if (typingStopTimerRef.current !== null) { + window.clearTimeout(typingStopTimerRef.current); + typingStopTimerRef.current = null; + } + if (typingActiveRef.current) { + sendRealtimeChatEvent("typing_stop"); + typingActiveRef.current = false; + } + } + + function syncTypingForText(nextText: string) { + if (!activeChatId) { + return; + } + const hasText = nextText.trim().length > 0; + if (!hasText) { + emitTypingStopIfActive(); + return; + } + if (!typingActiveRef.current) { + sendRealtimeChatEvent("typing_start"); + typingActiveRef.current = true; + } + if (typingStopTimerRef.current !== null) { + window.clearTimeout(typingStopTimerRef.current); + } + typingStopTimerRef.current = window.setTimeout(() => { + if (!typingActiveRef.current) { + return; + } + sendRealtimeChatEvent("typing_stop"); + typingActiveRef.current = false; + typingStopTimerRef.current = null; + }, 2500); + } + async function handleSend() { if (!activeChatId || !text.trim() || !me || !canSendInActiveChat) { return; @@ -373,7 +423,7 @@ export function MessageComposer() { setText(""); clearDraft(activeChatId); setReplyToMessage(activeChatId, null); - sendRealtimeChatEvent("typing_stop"); + emitTypingStopIfActive(); } catch { removeOptimisticMessage(activeChatId, clientMessageId); setUploadError("Message send failed. Please try again."); @@ -644,7 +694,7 @@ export function MessageComposer() { recordingStartedAtRef.current = Date.now(); setRecordSeconds(0); setRecordingState("recording"); - sendRealtimeChatEvent("typing_stop"); + emitTypingStopIfActive(); sendRealtimeChatEvent("recording_voice_start"); return true; } catch { @@ -1304,13 +1354,11 @@ export function MessageComposer() { setText(next); if (activeChatId) { setDraft(activeChatId, next); - sendRealtimeChatEvent(next.trim().length > 0 ? "typing_start" : "typing_stop"); + syncTypingForText(next); } }} onBlur={() => { - if (activeChatId) { - sendRealtimeChatEvent("typing_stop"); - } + emitTypingStopIfActive(); }} />