fix(chat): debounce post-send action menu
Docker Image / Build and Push Docker Image (push) Successful in 1m56s

This commit is contained in:
Codex
2026-07-23 20:03:26 +08:00
parent c659c5fc3f
commit 606e6d60ff
2 changed files with 41 additions and 2 deletions
@@ -124,6 +124,36 @@ describe("ChatInputBar", () => {
expect(getButton("Open chat actions")).not.toBeNull(); 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", () => { it("closes the menu on outside interaction and when disabled", () => {
renderBar(); renderBar();
act(() => getButton("Open chat actions").click()); act(() => getButton("Open chat actions").click());
+11 -2
View File
@@ -17,7 +17,7 @@ import { ChatSendButton } from "./chat-send-button";
import styles from "./chat-input-bar.module.css"; import styles from "./chat-input-bar.module.css";
const log = new Logger("AppChatComponentsChatInputBar"); 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"; const CHAT_ACTION_MENU_ID = "chat-composer-action-menu";
export interface ChatInputBarProps { export interface ChatInputBarProps {
@@ -82,7 +82,7 @@ export function ChatInputBar({ disabled = false }: ChatInputBarProps) {
const handleSend = (source: "click" | "keyboard" | "pointerdown") => { const handleSend = (source: "click" | "keyboard" | "pointerdown") => {
if (source === "click") { if (source === "click") {
const elapsed = Date.now() - lastPointerSendAtRef.current; 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", { log.debug("[chat-input-bar] suppress duplicate click", {
elapsed, elapsed,
contentLength: input.length, contentLength: input.length,
@@ -115,6 +115,15 @@ export function ChatInputBar({ disabled = false }: ChatInputBarProps) {
const handleMenuToggle = () => { const handleMenuToggle = () => {
if (disabled || hasContent) return; 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(); if (!isActionMenuOpen) textareaRef.current?.blur();
setIsActionMenuOpen((open) => !open); setIsActionMenuOpen((open) => !open);
}; };