From 92768047e9ac546d4202d4d4d73c76b87a017b74 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 23 Jul 2026 20:03:26 +0800 Subject: [PATCH] fix(chat): debounce post-send action menu --- .../__tests__/chat-input-bar.test.tsx | 30 +++++++++++++++++++ src/app/chat/components/chat-input-bar.tsx | 13 ++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/app/chat/components/__tests__/chat-input-bar.test.tsx b/src/app/chat/components/__tests__/chat-input-bar.test.tsx index 53fcea2d..2296073c 100644 --- a/src/app/chat/components/__tests__/chat-input-bar.test.tsx +++ b/src/app/chat/components/__tests__/chat-input-bar.test.tsx @@ -124,6 +124,36 @@ describe("ChatInputBar", () => { expect(getButton("Open chat actions")).not.toBeNull(); }); + it("suppresses the Facebook WebView click that follows a pointer send for 300ms", () => { + const now = vi.spyOn(Date, "now").mockReturnValue(1_000); + renderBar(); + setTextareaValue(getTextarea(), "Hello Maya"); + const sendButton = getButton("Send message"); + const pointerDown = new Event("pointerdown", { + bubbles: true, + cancelable: true, + }); + Object.defineProperty(pointerDown, "pointerType", { value: "touch" }); + + act(() => sendButton.dispatchEvent(pointerDown)); + + expect(mocks.dispatch).toHaveBeenCalledTimes(1); + expect(mocks.dispatch).toHaveBeenCalledWith({ + type: "ChatSendMessage", + content: "Hello Maya", + }); + expect(getTextarea().value).toBe(""); + expect(getButton("Open chat actions")).toBe(sendButton); + + now.mockReturnValue(1_299); + act(() => sendButton.click()); + expect(container.querySelector('[aria-label="Chat actions"]')).toBeNull(); + + now.mockReturnValue(1_300); + act(() => sendButton.click()); + expect(container.querySelector('[aria-label="Chat actions"]')).not.toBeNull(); + }); + it("closes the menu on outside interaction and when disabled", () => { renderBar(); act(() => getButton("Open chat actions").click()); diff --git a/src/app/chat/components/chat-input-bar.tsx b/src/app/chat/components/chat-input-bar.tsx index b20840de..f3857aa0 100644 --- a/src/app/chat/components/chat-input-bar.tsx +++ b/src/app/chat/components/chat-input-bar.tsx @@ -17,7 +17,7 @@ import { ChatSendButton } from "./chat-send-button"; import styles from "./chat-input-bar.module.css"; const log = new Logger("AppChatComponentsChatInputBar"); -const POINTER_SEND_CLICK_DEDUPE_MS = 500; +const POINTER_SEND_DEBOUNCE_MS = 300; const CHAT_ACTION_MENU_ID = "chat-composer-action-menu"; export interface ChatInputBarProps { @@ -82,7 +82,7 @@ export function ChatInputBar({ disabled = false }: ChatInputBarProps) { const handleSend = (source: "click" | "keyboard" | "pointerdown") => { if (source === "click") { const elapsed = Date.now() - lastPointerSendAtRef.current; - if (elapsed >= 0 && elapsed < POINTER_SEND_CLICK_DEDUPE_MS) { + if (elapsed >= 0 && elapsed < POINTER_SEND_DEBOUNCE_MS) { log.debug("[chat-input-bar] suppress duplicate click", { elapsed, contentLength: input.length, @@ -115,6 +115,15 @@ export function ChatInputBar({ disabled = false }: ChatInputBarProps) { const handleMenuToggle = () => { if (disabled || hasContent) return; + const elapsed = Date.now() - lastPointerSendAtRef.current; + if (elapsed >= 0 && elapsed < POINTER_SEND_DEBOUNCE_MS) { + log.debug("[chat-input-bar] suppress post-send menu toggle", { + elapsed, + disabled, + isFocused, + }); + return; + } if (!isActionMenuOpen) textareaRef.current?.blur(); setIsActionMenuOpen((open) => !open); };