fix(desktop): cancel superseded navigation work

Track deferred sidebar and thread operations so newer navigation intent,
route changes, toggles, and unmounts cancel stale callbacks. Strengthen the
E2E coverage to prove painted feedback and latest-intent history semantics.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
This commit is contained in:
Wes
2026-08-15 17:05:09 -06:00
co-authored by Carl
parent f4556496d4
commit 79601009fa
7 changed files with 382 additions and 60 deletions
+26 -45
View File
@@ -9,6 +9,7 @@ import { AppShellChannelSurface } from "@/app/AppShellChannelSurface";
import { AppHuddleShell } from "@/app/AppHuddleShell";
import { AppTopChrome } from "@/app/AppTopChrome";
import { useAppNavigation } from "@/app/navigation/useAppNavigation";
import { useDeferredSidebarNavigation } from "@/app/navigation/useDeferredSidebarNavigation";
import { useBackForwardControls } from "@/app/navigation/useBackForwardControls";
import { useCommunityNavigationTransitions } from "@/app/useCommunityNavigationTransitions";
import { useLiveHomeFeedActions } from "@/app/useLiveHomeFeedActions";
@@ -153,47 +154,15 @@ export function AppShell() {
() => deriveShellRoute(location.pathname),
[location.pathname],
);
const [pendingSidebarChannelId, setPendingSidebarChannelId] = React.useState<
string | null
>(null);
const pendingSidebarNavigationFrameRef = React.useRef<number | null>(null);
const pendingSidebarNavigationTimerRef = React.useRef<number | null>(null);
React.useEffect(() => {
return () => {
if (pendingSidebarNavigationFrameRef.current !== null) {
window.cancelAnimationFrame(pendingSidebarNavigationFrameRef.current);
}
if (pendingSidebarNavigationTimerRef.current !== null) {
window.clearTimeout(pendingSidebarNavigationTimerRef.current);
}
};
}, []);
React.useEffect(() => {
if (pendingSidebarChannelId === selectedChannelId) {
setPendingSidebarChannelId(null);
}
}, [pendingSidebarChannelId, selectedChannelId]);
const handleSidebarChannelSelect = React.useCallback(
(channelId: string) => {
setPendingSidebarChannelId(channelId);
if (pendingSidebarNavigationFrameRef.current !== null) {
window.cancelAnimationFrame(pendingSidebarNavigationFrameRef.current);
}
if (pendingSidebarNavigationTimerRef.current !== null) {
window.clearTimeout(pendingSidebarNavigationTimerRef.current);
}
pendingSidebarNavigationFrameRef.current = window.requestAnimationFrame(
() => {
pendingSidebarNavigationFrameRef.current = null;
pendingSidebarNavigationTimerRef.current = window.setTimeout(() => {
pendingSidebarNavigationTimerRef.current = null;
selectSidebarChannel(channelId);
}, 0);
},
);
},
[selectSidebarChannel],
);
const {
cancel: cancelPendingSidebarNavigation,
pendingChannelId: pendingSidebarChannelId,
selectDeferred: handleSidebarChannelSelect,
} = useDeferredSidebarNavigation({
pathname: location.pathname,
selectedChannelId,
selectChannel: selectSidebarChannel,
});
const sidebarSelectedChannelId = pendingSidebarChannelId ?? selectedChannelId;
const {
removeCommunity: handleRemoveCommunity,
@@ -889,7 +858,10 @@ export function AppShell() {
});
await goChannel(directMessage.id);
}}
onSelectAgents={() => void goAgents()}
onSelectAgents={() => {
cancelPendingSidebarNavigation();
void goAgents();
}}
onSelectChannel={handleSidebarChannelSelect}
onOpenSearchResult={handleOpenSearchResult}
searchChannels={channels}
@@ -897,9 +869,18 @@ export function AppShell() {
searchFocusRequest,
scopeSearchFocusRequest,
]}
onSelectHome={() => void goHome()}
onSelectProjects={() => void goProjects()}
onSelectPulse={() => void goPulse()}
onSelectHome={() => {
cancelPendingSidebarNavigation();
void goHome();
}}
onSelectProjects={() => {
cancelPendingSidebarNavigation();
void goProjects();
}}
onSelectPulse={() => {
cancelPendingSidebarNavigation();
void goPulse();
}}
onSelectSettings={handleOpenSettings}
onSelectWorkflows={() => void goWorkflows()}
onSetPresenceStatus={(status) =>
@@ -0,0 +1,3 @@
export function dispatchNavigationIntent(): void {
window.dispatchEvent(new Event("buzz:navigation-intent"));
}
@@ -6,6 +6,7 @@ import {
useRouter,
} from "@tanstack/react-router";
import { dispatchNavigationIntent } from "@/app/navigation/navigationIntent";
import { cacheSearchHitEvent } from "@/app/navigation/searchHitEventCache";
import { resolveSearchHitDestination } from "@/app/navigation/resolveSearchHitDestination";
import type { SearchHit } from "@/shared/api/types";
@@ -38,6 +39,7 @@ export function useAppNavigation() {
return false;
}
dispatchNavigationIntent();
await navigate({
...next,
replace: behavior.replace,
@@ -273,6 +275,7 @@ export function useAppNavigation() {
const closeSettings = React.useCallback(() => {
if (canGoBack) {
dispatchNavigationIntent();
router.history.back();
return;
}
@@ -282,6 +285,7 @@ export function useAppNavigation() {
const closeWorkflowDetail = React.useCallback(() => {
if (canGoBack) {
dispatchNavigationIntent();
router.history.back();
return;
}
@@ -292,6 +296,7 @@ export function useAppNavigation() {
const closeForumPost = React.useCallback(
(channelId: string) => {
if (canGoBack) {
dispatchNavigationIntent();
router.history.back();
return;
}
@@ -7,6 +7,7 @@ import {
import { isTauri } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { dispatchNavigationIntent } from "@/app/navigation/navigationIntent";
import { matchBackForwardChord } from "@/app/navigation/backForwardChords";
import { isMacPlatform } from "@/shared/lib/platform";
import { trimMapToSize } from "@/shared/lib/trimMapToSize";
@@ -59,6 +60,7 @@ export function useBackForwardControls() {
return;
}
dispatchNavigationIntent();
router.history.back();
}, [canGoBack, router.history]);
@@ -67,6 +69,7 @@ export function useBackForwardControls() {
return;
}
dispatchNavigationIntent();
router.history.forward();
}, [canGoForward, router.history]);
@@ -0,0 +1,88 @@
import * as React from "react";
import { dispatchNavigationIntent } from "@/app/navigation/navigationIntent";
type DeferredSidebarNavigationOptions = {
pathname: string;
selectedChannelId: string | null;
selectChannel: (channelId: string) => void;
};
export function useDeferredSidebarNavigation({
pathname,
selectedChannelId,
selectChannel,
}: DeferredSidebarNavigationOptions) {
const [pendingChannelId, setPendingChannelId] = React.useState<string | null>(
null,
);
const frameRef = React.useRef<number | null>(null);
const timerRef = React.useRef<number | null>(null);
const generationRef = React.useRef(0);
const pathnameRef = React.useRef(pathname);
pathnameRef.current = pathname;
const cancel = React.useCallback(() => {
generationRef.current += 1;
if (frameRef.current !== null) {
window.cancelAnimationFrame(frameRef.current);
frameRef.current = null;
}
if (timerRef.current !== null) {
window.clearTimeout(timerRef.current);
timerRef.current = null;
}
setPendingChannelId(null);
}, []);
React.useEffect(() => cancel, [cancel]);
React.useEffect(() => {
// A committed route change supersedes any deferred sidebar intent.
void pathname;
cancel();
}, [cancel, pathname]);
React.useEffect(() => {
if (pendingChannelId === selectedChannelId) setPendingChannelId(null);
}, [pendingChannelId, selectedChannelId]);
const selectDeferred = React.useCallback(
(channelId: string) => {
dispatchNavigationIntent();
cancel();
const generation = generationRef.current;
const sourcePathname = pathnameRef.current;
setPendingChannelId(channelId);
frameRef.current = window.requestAnimationFrame(() => {
frameRef.current = null;
if (
generationRef.current !== generation ||
pathnameRef.current !== sourcePathname
) {
return;
}
frameRef.current = window.requestAnimationFrame(() => {
frameRef.current = null;
if (
generationRef.current !== generation ||
pathnameRef.current !== sourcePathname
) {
return;
}
timerRef.current = window.setTimeout(() => {
timerRef.current = null;
if (
generationRef.current !== generation ||
pathnameRef.current !== sourcePathname
) {
return;
}
selectChannel(channelId);
}, 0);
});
});
},
[cancel, selectChannel],
);
return { cancel, pendingChannelId, selectDeferred };
}
@@ -109,17 +109,70 @@ export function useChannelPaneHandlers({
const markRevealedRepliesReadRef = React.useRef(markRevealedRepliesRead);
markRevealedRepliesReadRef.current = markRevealedRepliesRead;
const deferredPanelFrameRef = React.useRef<number | null>(null);
const deferredPanelTimerRef = React.useRef<number | null>(null);
const deferredPanelGenerationRef = React.useRef(0);
const intendedOpenThreadHeadIdRef = React.useRef(openThreadHeadId);
if (
deferredPanelFrameRef.current === null &&
deferredPanelTimerRef.current === null
) {
intendedOpenThreadHeadIdRef.current = openThreadHeadId;
}
const deferPanelState = React.useCallback((update: () => void) => {
window.setTimeout(() => {
React.startTransition(update);
}, 0);
deferredPanelGenerationRef.current += 1;
const generation = deferredPanelGenerationRef.current;
if (deferredPanelFrameRef.current !== null) {
window.cancelAnimationFrame(deferredPanelFrameRef.current);
}
if (deferredPanelTimerRef.current !== null) {
window.clearTimeout(deferredPanelTimerRef.current);
}
deferredPanelFrameRef.current = window.requestAnimationFrame(() => {
if (deferredPanelGenerationRef.current !== generation) return;
deferredPanelFrameRef.current = window.requestAnimationFrame(() => {
if (deferredPanelGenerationRef.current !== generation) return;
deferredPanelFrameRef.current = null;
deferredPanelTimerRef.current = window.setTimeout(() => {
if (deferredPanelGenerationRef.current !== generation) return;
deferredPanelTimerRef.current = null;
React.startTransition(update);
}, 0);
});
});
}, []);
React.useEffect(() => {
const cancelDeferredPanelState = () => {
deferredPanelGenerationRef.current += 1;
intendedOpenThreadHeadIdRef.current = openThreadHeadIdRef.current;
onPendingOpenThreadHeadIdChange(null);
onOptimisticOpenThreadHeadIdChange(undefined);
if (deferredPanelFrameRef.current !== null) {
window.cancelAnimationFrame(deferredPanelFrameRef.current);
deferredPanelFrameRef.current = null;
}
if (deferredPanelTimerRef.current !== null) {
window.clearTimeout(deferredPanelTimerRef.current);
deferredPanelTimerRef.current = null;
}
};
window.addEventListener("buzz:navigation-intent", cancelDeferredPanelState);
return () => {
window.removeEventListener(
"buzz:navigation-intent",
cancelDeferredPanelState,
);
cancelDeferredPanelState();
};
}, [onOptimisticOpenThreadHeadIdChange, onPendingOpenThreadHeadIdChange]);
const handleCancelThreadReply = React.useCallback(() => {
setThreadReplyTargetId(openThreadHeadIdRef.current);
}, [setThreadReplyTargetId]);
const handleCloseThread = React.useCallback(() => {
intendedOpenThreadHeadIdRef.current = null;
onPendingOpenThreadHeadIdChange(null);
deferPanelState(() => {
onOptimisticOpenThreadHeadIdChange(null);
setOpenThreadHeadId(null);
@@ -130,6 +183,7 @@ export function useChannelPaneHandlers({
}, [
deferPanelState,
onOptimisticOpenThreadHeadIdChange,
onPendingOpenThreadHeadIdChange,
setExpandedThreadReplyIds,
setOpenThreadHeadId,
setThreadReplyTargetId,
@@ -200,7 +254,8 @@ export function useChannelPaneHandlers({
const handleOpenThread = React.useCallback(
(message: { id: string }) => {
if (openThreadHeadIdRef.current === message.id) {
if (intendedOpenThreadHeadIdRef.current === message.id) {
intendedOpenThreadHeadIdRef.current = null;
onPendingOpenThreadHeadIdChange(null);
deferPanelState(() => {
onOptimisticOpenThreadHeadIdChange(null);
@@ -213,6 +268,7 @@ export function useChannelPaneHandlers({
return;
}
intendedOpenThreadHeadIdRef.current = message.id;
onPendingOpenThreadHeadIdChange(message.id);
deferPanelState(() => {
onOptimisticOpenThreadHeadIdChange(message.id);
+197 -11
View File
@@ -2793,14 +2793,14 @@ test("sidebar selection paints before cached channel work starts", async ({
Object.assign(window, { __BUZZ_SIDEBAR_SELECTION_OBSERVED__: observed });
const observer = new MutationObserver(() => {
const random = document.querySelector('[data-testid="channel-random"]');
const title = document.querySelector('[data-testid="chat-title"]');
if (
random?.getAttribute("data-active") === "true" &&
title?.textContent === "general"
) {
observed.selectedBeforeRoute = true;
observer.disconnect();
}
if (random?.getAttribute("data-active") !== "true") return;
observer.disconnect();
requestAnimationFrame(() => {
requestAnimationFrame(() => {
const title = document.querySelector('[data-testid="chat-title"]');
observed.selectedBeforeRoute = title?.textContent === "general";
});
});
});
observer.observe(document.body, {
attributes: true,
@@ -2828,6 +2828,25 @@ test("sidebar selection paints before cached channel work starts", async ({
await expect(page.getByTestId("chat-title")).toHaveText("random");
});
test("superseded sidebar feedback cannot override newer navigation", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await page.getByTestId("channel-random").click();
await page.getByRole("button", { name: "Inbox" }).click();
await expect(page.getByTestId("home-inbox")).toBeVisible();
await page.waitForTimeout(100);
await expect(page.getByTestId("home-inbox")).toBeVisible();
await expect(page.getByTestId("channel-random")).toHaveAttribute(
"data-active",
"false",
);
});
test("thread open paints immediate feedback before deferred panel work", async ({
page,
}) => {
@@ -2845,10 +2864,17 @@ test("thread open paints immediate feedback before deferred panel work", async (
const observed = { loading: false };
Object.assign(window, { __BUZZ_THREAD_OPEN_OBSERVED__: observed });
const observer = new MutationObserver(() => {
if (document.querySelector('[data-testid="message-thread-loading"]')) {
observed.loading = true;
observer.disconnect();
if (!document.querySelector('[data-testid="message-thread-loading"]')) {
return;
}
observer.disconnect();
requestAnimationFrame(() => {
requestAnimationFrame(() => {
observed.loading =
document.querySelector('[data-testid="message-thread-loading"]') !==
null;
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
});
@@ -2869,6 +2895,166 @@ test("thread open paints immediate feedback before deferred panel work", async (
await expect(page.getByTestId("message-thread-panel")).toBeVisible();
});
test("rapid thread intents commit only the latest history entry", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const timeline = page.getByTestId("message-timeline");
const firstRoot = timeline.locator(
'[data-message-id="mock-general-welcome"]',
);
const secondRoot = timeline.locator('[data-message-id="mock-general-alice"]');
const historyLength = await page.evaluate(() => window.history.length);
await firstRoot.hover();
await secondRoot.hover();
await page.evaluate(() => {
const replyButtons = Array.from(
document.querySelectorAll<HTMLButtonElement>(
'[data-message-id="mock-general-welcome"] [aria-label="Reply"], [data-message-id="mock-general-alice"] [aria-label="Reply"]',
),
);
if (replyButtons.length !== 2) {
throw new Error("Expected both thread reply buttons");
}
replyButtons[0].click();
replyButtons[1].click();
});
const threadPanel = page.getByTestId("message-thread-panel");
await expect(threadPanel).toBeVisible();
await expect(threadPanel.getByTestId("message-thread-head")).toContainText(
"Hey team",
);
await expect(page).toHaveURL(/thread=mock-general-alice/);
await expect
.poll(() => page.evaluate(() => window.history.length))
.toBe(historyLength + 1);
});
test("rapid same-thread toggle leaves no panel or stale history entry", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const root = page
.getByTestId("message-timeline")
.locator('[data-message-id="mock-general-alice"]');
const historyLength = await page.evaluate(() => window.history.length);
await root.hover();
const reply = root.getByRole("button", { name: "Reply" });
await reply.dispatchEvent("click");
await reply.dispatchEvent("click");
await page.waitForTimeout(100);
await expect(page.getByTestId("message-thread-panel")).toHaveCount(0);
await expect(page).not.toHaveURL(/thread=/);
expect(await page.evaluate(() => window.history.length)).toBe(historyLength);
});
test("channel navigation supersedes a pending thread without stale history", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const root = page
.getByTestId("message-timeline")
.locator('[data-message-id="mock-general-alice"]');
const historyLength = await page.evaluate(() => window.history.length);
await root.hover();
await page.evaluate(() => {
document
.querySelector<HTMLButtonElement>(
'[data-message-id="mock-general-alice"] [aria-label="Reply"]',
)
?.click();
document
.querySelector<HTMLElement>('[data-testid="channel-random"]')
?.click();
});
await expect(page.getByTestId("chat-title")).toHaveText("random");
await page.waitForTimeout(100);
await expect(page.getByTestId("message-thread-panel")).toHaveCount(0);
await expect(page).not.toHaveURL(/thread=/);
expect(await page.evaluate(() => window.history.length)).toBe(
historyLength + 1,
);
});
test("back navigation supersedes a pending thread without stale history", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const root = page
.getByTestId("message-timeline")
.locator('[data-message-id="mock-general-alice"]');
const historyLength = await page.evaluate(() => window.history.length);
await root.hover();
await page.evaluate(() => {
document
.querySelector<HTMLButtonElement>(
'[data-message-id="mock-general-alice"] [aria-label="Reply"]',
)
?.click();
document
.querySelector<HTMLButtonElement>('[data-testid="global-back"]')
?.click();
});
await expect(page).toHaveURL(/\/$/);
await expect(page.getByTestId("home-inbox")).toBeVisible();
await page.waitForTimeout(100);
await expect(page.getByTestId("message-thread-panel")).toHaveCount(0);
await expect(page).not.toHaveURL(/thread=/);
expect(await page.evaluate(() => window.history.length)).toBe(historyLength);
});
test("forward navigation supersedes a pending thread without stale history", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await page.getByTestId("channel-random").click();
await expect(page.getByTestId("chat-title")).toHaveText("random");
await page.getByTestId("global-back").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const root = page
.getByTestId("message-timeline")
.locator('[data-message-id="mock-general-alice"]');
const historyLength = await page.evaluate(() => window.history.length);
await root.hover();
await page.evaluate(() => {
document
.querySelector<HTMLButtonElement>(
'[data-message-id="mock-general-alice"] [aria-label="Reply"]',
)
?.click();
document
.querySelector<HTMLButtonElement>('[data-testid="global-forward"]')
?.click();
});
await expect(page.getByTestId("chat-title")).toHaveText("random");
await expect(page).toHaveURL(/#\/channels\/[^?]+$/);
await page.waitForTimeout(100);
await expect(page.getByTestId("message-thread-panel")).toHaveCount(0);
await expect(page).not.toHaveURL(/thread=/);
expect(await page.evaluate(() => window.history.length)).toBe(historyLength);
});
test("thread composer is focused after clicking the reply icon", async ({
page,
}) => {