From 736ec6d1757f3fcf024397a2cb3340ca56ab4c04 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 23 Jun 2026 11:32:40 -0700 Subject: [PATCH] FN-6957: prevent stale mailbox deep links from reselecting messages Treat mailbox deep links as one-shot navigation so mobile users can switch messages without stale URL state restoring the prior selection. - consume the current mailbox deep link before explicit user navigation, compose, delete, reply, and approval actions - prevent repeated deep-link highlighting and clear stale selections when switching agent mailboxes or subtabs - add regression coverage for modal and full mailbox views, plus a published CLI patch changeset Files changed: .changeset/fn-6957-mailbox-stale-deeplink.md | 5 + packages/dashboard/app/components/MailboxModal.tsx | 56 ++++++--- packages/dashboard/app/components/MailboxView.tsx | 60 +++++++--- .../app/components/__tests__/MailboxModal.test.tsx | 76 ++++++++++++ .../app/components/__tests__/MailboxView.test.tsx | 130 +++++++++++++++++++++ 5 files changed, 293 insertions(+), 34 deletions(-) Fusion-Task-Id: FN-6957 Fusion-Task-Lineage: 2a85c8d1-2163-434a-987f-2e842b847651 --- .changeset/fn-6957-mailbox-stale-deeplink.md | 5 + .../dashboard/app/components/MailboxModal.tsx | 56 +++++--- .../dashboard/app/components/MailboxView.tsx | 60 +++++--- .../__tests__/MailboxModal.test.tsx | 76 ++++++++++ .../components/__tests__/MailboxView.test.tsx | 130 ++++++++++++++++++ 5 files changed, 293 insertions(+), 34 deletions(-) create mode 100644 .changeset/fn-6957-mailbox-stale-deeplink.md diff --git a/.changeset/fn-6957-mailbox-stale-deeplink.md b/.changeset/fn-6957-mailbox-stale-deeplink.md new file mode 100644 index 0000000000..3bdeba7c12 --- /dev/null +++ b/.changeset/fn-6957-mailbox-stale-deeplink.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Fix mobile mailbox message selection so stale deep links no longer override the user's selected message. diff --git a/packages/dashboard/app/components/MailboxModal.tsx b/packages/dashboard/app/components/MailboxModal.tsx index 672002fb96..6ee4668b5c 100644 --- a/packages/dashboard/app/components/MailboxModal.tsx +++ b/packages/dashboard/app/components/MailboxModal.tsx @@ -198,6 +198,20 @@ export function MailboxModal({ const [replyContextLoading, setReplyContextLoading] = useState>({}); const [replyContextErrors, setReplyContextErrors] = useState>({}); const [replyContextCache, setReplyContextCache] = useState>(new Map()); + const consumedDeepLinkedMessageIdRef = useRef(null); + const highlightedDeepLinkedMessageIdRef = useRef(null); + + /* + * FNXC:MailboxMobile 2026-06-23-10:55: + * Modal mailbox deep links are one-shot initializers. Once the user taps Back, changes tabs, composes, deletes, or opens another row, the URL target is stale state and must not win over the explicit mobile selection. + */ + const consumeCurrentDeepLink = useCallback(() => { + const deepLinkedMessageId = getDeepLinkedMessageId(); + if (deepLinkedMessageId) { + consumedDeepLinkedMessageIdRef.current = deepLinkedMessageId; + } + }, []); + const skipOpenSpinnerInboxRef = useRef(false); const skipOpenSpinnerOutboxRef = useRef(false); const agentNamesById = useMemo(() => { @@ -383,7 +397,10 @@ export function MailboxModal({ // ── Actions ─────────────────────────────────────────────────────────── - const handleOpenMessage = useCallback(async (message: Message) => { + const handleOpenMessage = useCallback(async (message: Message, source: "deep-link" | "user" = "user") => { + if (source === "user") { + consumeCurrentDeepLink(); + } setSelectedMessage(message); setReplyContextExpanded({}); setReplyContextLoading({}); @@ -424,7 +441,7 @@ export function MailboxModal({ } catch { setConversationMessages([message]); } - }, [activeTab, inboxCacheKey, projectId, unreadCountCacheKey]); + }, [activeTab, inboxCacheKey, projectId, unreadCountCacheKey, consumeCurrentDeepLink]); // Deep-link: open and highlight a specific message from URL params. useEffect(() => { @@ -433,7 +450,7 @@ export function MailboxModal({ } const deepLinkedMessageId = getDeepLinkedMessageId(); - if (!deepLinkedMessageId) { + if (!deepLinkedMessageId || consumedDeepLinkedMessageIdRef.current === deepLinkedMessageId) { return; } @@ -450,7 +467,8 @@ export function MailboxModal({ return; } - void handleOpenMessage(message); + consumedDeepLinkedMessageIdRef.current = deepLinkedMessageId; + void handleOpenMessage(message, "deep-link"); }, [isOpen, inbox, outbox, agentMailbox, allAgentsMailbox, conversationMessages, handleOpenMessage]); useEffect(() => { @@ -459,7 +477,7 @@ export function MailboxModal({ } const deepLinkedMessageId = getDeepLinkedMessageId(); - if (!deepLinkedMessageId) { + if (!deepLinkedMessageId || selectedMessage?.id !== deepLinkedMessageId || highlightedDeepLinkedMessageIdRef.current === deepLinkedMessageId) { return; } @@ -468,6 +486,7 @@ export function MailboxModal({ return; } + highlightedDeepLinkedMessageIdRef.current = deepLinkedMessageId; element.scrollIntoView({ behavior: "smooth", block: "center" }); element.classList.add("mailbox-message-highlight"); const timer = window.setTimeout(() => { @@ -480,12 +499,13 @@ export function MailboxModal({ }, [isOpen, selectedMessage, conversationMessages]); const handleCloseMessage = useCallback(() => { + consumeCurrentDeepLink(); setSelectedMessage(null); setConversationMessages([]); setReplyContextExpanded({}); setReplyContextLoading({}); setReplyContextErrors({}); - }, []); + }, [consumeCurrentDeepLink]); const handleMarkAllRead = useCallback(async () => { try { @@ -512,6 +532,7 @@ export function MailboxModal({ }, [addToast, inboxCacheKey, projectId, unreadCountCacheKey, t]); const handleDeleteMessage = useCallback(async (id: string) => { + consumeCurrentDeepLink(); try { await deleteMessage(id, projectId); setSelectedMessage(null); @@ -525,16 +546,17 @@ export function MailboxModal({ } catch { addToast?.(t("mailbox.deleteFailed", "Failed to delete message"), "error"); } - }, [projectId, activeTab, selectedAgentId, loadInbox, loadOutbox, loadAgentMailbox, loadAllAgentsMailbox, addToast, t]); + }, [projectId, activeTab, selectedAgentId, loadInbox, loadOutbox, loadAgentMailbox, loadAllAgentsMailbox, addToast, t, consumeCurrentDeepLink]); const handleReply = useCallback((message: Message) => { + consumeCurrentDeepLink(); setComposeRecipient({ id: message.fromId, type: message.fromType }); setComposeReplyContext({ messageId: message.id, preview: messagePreview(message.content, 120), }); setShowComposer(true); - }, []); + }, [consumeCurrentDeepLink]); const handleMessageSent = useCallback(() => { setShowComposer(false); @@ -548,6 +570,7 @@ export function MailboxModal({ }, [activeTab, loadOutbox, selectedAgentId, loadAgentMailbox, loadAllAgentsMailbox, addToast, t]); const handleOpenCompose = useCallback(() => { + consumeCurrentDeepLink(); // Pre-fill recipient from selected agent if available if (activeTab === "agents" && selectedAgentId && selectedAgentId !== ALL_AGENTS_MAILBOX_ID) { setComposeRecipient({ id: selectedAgentId, type: "agent" }); @@ -556,13 +579,14 @@ export function MailboxModal({ } setComposeReplyContext(null); setShowComposer(true); - }, [activeTab, selectedAgentId]); + }, [activeTab, selectedAgentId, consumeCurrentDeepLink]); const handleComposeCancel = useCallback(() => { + consumeCurrentDeepLink(); setShowComposer(false); setComposeRecipient(null); setComposeReplyContext(null); - }, []); + }, [consumeCurrentDeepLink]); const threadMessages = selectedMessage ? buildReplyThread(conversationMessages, selectedMessage) : []; @@ -751,7 +775,7 @@ export function MailboxModal({