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({