Cancel deferred sidebar navigation on newer intent

Subscribe the sidebar deferral to the shared navigation-intent signal so
back, forward, deep links, and every other navigation source supersede stale
channel work. Keep optimistic selection until the committed route catches up,
and cover immediate competing intents plus traversal races.

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:56:18 -06:00
co-authored by Carl
parent 79601009fa
commit 4b399d8a91
4 changed files with 86 additions and 23 deletions
+7 -18
View File
@@ -155,7 +155,6 @@ export function AppShell() {
[location.pathname],
);
const {
cancel: cancelPendingSidebarNavigation,
pendingChannelId: pendingSidebarChannelId,
selectDeferred: handleSidebarChannelSelect,
} = useDeferredSidebarNavigation({
@@ -858,10 +857,7 @@ export function AppShell() {
});
await goChannel(directMessage.id);
}}
onSelectAgents={() => {
cancelPendingSidebarNavigation();
void goAgents();
}}
onSelectAgents={() => void goAgents()}
onSelectChannel={handleSidebarChannelSelect}
onOpenSearchResult={handleOpenSearchResult}
searchChannels={channels}
@@ -869,18 +865,9 @@ export function AppShell() {
searchFocusRequest,
scopeSearchFocusRequest,
]}
onSelectHome={() => {
cancelPendingSidebarNavigation();
void goHome();
}}
onSelectProjects={() => {
cancelPendingSidebarNavigation();
void goProjects();
}}
onSelectPulse={() => {
cancelPendingSidebarNavigation();
void goPulse();
}}
onSelectHome={() => void goHome()}
onSelectProjects={() => void goProjects()}
onSelectPulse={() => void goPulse()}
onSelectSettings={handleOpenSettings}
onSelectWorkflows={() => void goWorkflows()}
onSetPresenceStatus={(status) =>
@@ -904,7 +891,9 @@ export function AppShell() {
: undefined
}
selectedChannelId={sidebarSelectedChannelId}
selectedView={selectedView}
selectedView={
pendingSidebarChannelId ? "channel" : selectedView
}
unreadChannelIds={unreadChannelIds}
previewActivityChannelIds={unreadThreadChannelIds}
unreadChannelCounts={unreadChannelCounts}
@@ -22,7 +22,7 @@ export function useDeferredSidebarNavigation({
const pathnameRef = React.useRef(pathname);
pathnameRef.current = pathname;
const cancel = React.useCallback(() => {
const cancelDeferred = React.useCallback(() => {
generationRef.current += 1;
if (frameRef.current !== null) {
window.cancelAnimationFrame(frameRef.current);
@@ -32,10 +32,23 @@ export function useDeferredSidebarNavigation({
window.clearTimeout(timerRef.current);
timerRef.current = null;
}
setPendingChannelId(null);
}, []);
const cancel = React.useCallback(() => {
cancelDeferred();
setPendingChannelId(null);
}, [cancelDeferred]);
React.useEffect(() => cancel, [cancel]);
React.useEffect(() => {
const handleNavigationIntent = () => cancelDeferred();
window.addEventListener("buzz:navigation-intent", handleNavigationIntent);
return () =>
window.removeEventListener(
"buzz:navigation-intent",
handleNavigationIntent,
);
}, [cancelDeferred]);
React.useEffect(() => {
// A committed route change supersedes any deferred sidebar intent.
void pathname;
@@ -84,5 +97,5 @@ export function useDeferredSidebarNavigation({
[cancel, selectChannel],
);
return { cancel, pendingChannelId, selectDeferred };
return { pendingChannelId, selectDeferred };
}
@@ -105,6 +105,7 @@ export function AppSidebarPrimaryMenu({
<SidebarMenuItem>
<SidebarMenuButton
className="data-[active=true]:font-normal"
data-testid="open-home-view"
isActive={selectedView === "home"}
onClick={onSelectHome}
tooltip="Inbox"
+62 -2
View File
@@ -2835,8 +2835,16 @@ test("superseded sidebar feedback cannot override newer navigation", async ({
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 page.evaluate(() => {
document
.querySelector<HTMLElement>('[data-testid="channel-random"]')
?.click();
const inboxButton = document.querySelector<HTMLButtonElement>(
'[data-testid="open-home-view"]',
);
if (!inboxButton) throw new Error("Expected Inbox button");
inboxButton.click();
});
await expect(page.getByTestId("home-inbox")).toBeVisible();
await page.waitForTimeout(100);
@@ -2847,6 +2855,58 @@ test("superseded sidebar feedback cannot override newer navigation", async ({
);
});
test("back navigation supersedes pending sidebar navigation", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const historyLength = await page.evaluate(() => window.history.length);
await page.evaluate(() => {
document
.querySelector<HTMLElement>('[data-testid="channel-random"]')
?.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("home-inbox")).toBeVisible();
expect(await page.evaluate(() => window.history.length)).toBe(historyLength);
});
test("forward navigation supersedes pending sidebar 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 expect(page.getByTestId("chat-title")).toHaveText("random");
await page.getByTestId("global-back").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const historyLength = await page.evaluate(() => window.history.length);
await page.evaluate(() => {
document
.querySelector<HTMLElement>('[data-testid="channel-watercooler"]')
?.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("chat-title")).toHaveText("random");
expect(await page.evaluate(() => window.history.length)).toBe(historyLength);
});
test("thread open paints immediate feedback before deferred panel work", async ({
page,
}) => {