Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions desktop/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -86,6 +87,7 @@ import { relayClient } from "@/shared/api/relayClient";
import { useIdentityQuery } from "@/shared/api/hooks";
import { useRelayAutoHeal } from "@/shared/api/useRelayAutoHeal";
import { useDeferredStartup } from "@/shared/hooks/useDeferredStartup";
import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";
import { useWebviewScrollBoundaryLock } from "@/shared/hooks/useWebviewScrollBoundaryLock";
import { joinChannel } from "@/shared/api/tauri";
import type { Channel, ChannelVisibility, SearchHit } from "@/shared/api/types";
Expand All @@ -109,7 +111,7 @@ export function AppShell() {
handleHuddleStartPendingChange,
handleHuddleStarted,
handleHuddleVisibilityChange,
handleSidebarChannelSelect,
handleSidebarChannelSelect: selectSidebarChannel,
huddleBackingChannelIds,
revealedHuddleChannelIds,
isHuddleCompanionOpen,
Expand Down Expand Up @@ -153,6 +155,15 @@ export function AppShell() {
() => deriveShellRoute(location.pathname),
[location.pathname],
);
const {
pendingChannelId: pendingSidebarChannelId,
selectDeferred: handleSidebarChannelSelect,
} = useDeferredSidebarNavigation({
pathname: location.pathname,
selectedChannelId,
selectChannel: selectSidebarChannel,
});
const sidebarSelectedChannelId = pendingSidebarChannelId ?? selectedChannelId;
const {
removeCommunity: handleRemoveCommunity,
switchCommunity: handleSwitchCommunity,
Expand Down Expand Up @@ -880,8 +891,10 @@ export function AppShell() {
] ?? undefined)
: undefined
}
selectedChannelId={selectedChannelId}
selectedView={selectedView}
selectedChannelId={sidebarSelectedChannelId}
selectedView={
pendingSidebarChannelId ? "channel" : selectedView
}
unreadChannelIds={unreadChannelIds}
previewActivityChannelIds={unreadThreadChannelIds}
unreadChannelCounts={unreadChannelCounts}
Expand All @@ -899,7 +912,16 @@ export function AppShell() {
mainInsetRef={mainInsetRef}
terminal={<TerminalBootstrap {...terminalContext} />}
>
<Outlet />
{pendingSidebarChannelId ? (
<div
className="flex min-h-0 min-w-0 flex-1"
data-testid="pending-channel-skeleton"
>
<ViewLoadingFallback includeHeader kind="channel" />
</div>
) : (
<Outlet />
)}
</AppShellChannelSurface>
{!isHuddleRoom ? (
<RelayConnectionOverlay
Expand Down
3 changes: 3 additions & 0 deletions desktop/src/app/navigation/navigationIntent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function dispatchNavigationIntent(): void {
window.dispatchEvent(new Event("buzz:navigation-intent"));
}
5 changes: 5 additions & 0 deletions desktop/src/app/navigation/useAppNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -38,6 +39,7 @@ export function useAppNavigation() {
return false;
}

dispatchNavigationIntent();
await navigate({
...next,
replace: behavior.replace,
Expand Down Expand Up @@ -273,6 +275,7 @@ export function useAppNavigation() {

const closeSettings = React.useCallback(() => {
if (canGoBack) {
dispatchNavigationIntent();
router.history.back();
return;
}
Expand All @@ -282,6 +285,7 @@ export function useAppNavigation() {

const closeWorkflowDetail = React.useCallback(() => {
if (canGoBack) {
dispatchNavigationIntent();
router.history.back();
return;
}
Expand All @@ -292,6 +296,7 @@ export function useAppNavigation() {
const closeForumPost = React.useCallback(
(channelId: string) => {
if (canGoBack) {
dispatchNavigationIntent();
router.history.back();
return;
}
Expand Down
3 changes: 3 additions & 0 deletions desktop/src/app/navigation/useBackForwardControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -59,6 +60,7 @@ export function useBackForwardControls() {
return;
}

dispatchNavigationIntent();
router.history.back();
}, [canGoBack, router.history]);

Expand All @@ -67,6 +69,7 @@ export function useBackForwardControls() {
return;
}

dispatchNavigationIntent();
router.history.forward();
}, [canGoForward, router.history]);

Expand Down
134 changes: 134 additions & 0 deletions desktop/src/app/navigation/useDeferredSidebarNavigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import * as React from "react";

import { dispatchNavigationIntent } from "@/app/navigation/navigationIntent";

type DeferredSidebarNavigationOptions = {
pathname: string;
selectedChannelId: string | null;
selectChannel: (channelId: string) => Promise<unknown> | undefined;
};

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 isCommittingRef = React.useRef(false);
const ignoreNextNavigationIntentRef = React.useRef(false);
const pathnameRef = React.useRef(pathname);
pathnameRef.current = pathname;

const cancelDeferred = 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;
}
}, []);

const cancel = React.useCallback(() => {
isCommittingRef.current = false;
ignoreNextNavigationIntentRef.current = false;
cancelDeferred();
setPendingChannelId(null);
}, [cancelDeferred]);

React.useEffect(() => cancel, [cancel]);
React.useEffect(() => {
const handleNavigationIntent = () => {
if (ignoreNextNavigationIntentRef.current) {
ignoreNextNavigationIntentRef.current = false;
return;
}
cancel();
};
window.addEventListener("buzz:navigation-intent", handleNavigationIntent);
return () =>
window.removeEventListener(
"buzz:navigation-intent",
handleNavigationIntent,
);
}, [cancel]);
React.useEffect(() => {
// Keep the destination skeleton mounted through the route commit and one
// paint. Clearing state directly in this effect can be batched before the
// browser paints, exposing either the old outlet or expensive new outlet.
void pathname;
frameRef.current = window.requestAnimationFrame(() => {
frameRef.current = null;
cancel();
});
}, [cancel, pathname]);
React.useEffect(() => {
if (!isCommittingRef.current && pendingChannelId === selectedChannelId) {
setPendingChannelId(null);
}
}, [pendingChannelId, selectedChannelId]);

const selectDeferred = React.useCallback(
(channelId: string) => {
if (channelId === selectedChannelId) {
if (pendingChannelId !== null) {
dispatchNavigationIntent();
cancel();
}
const navigationResult = selectChannel(channelId);
if (navigationResult) void navigationResult.catch(() => undefined);
return;
}
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;
}
isCommittingRef.current = true;
ignoreNextNavigationIntentRef.current = true;
const navigationResult = selectChannel(channelId);
if (navigationResult) {
void navigationResult.catch(() => {
if (generationRef.current === generation) cancel();
});
}
}, 0);
});
});
},
[cancel, pendingChannelId, selectChannel, selectedChannelId],
);

return { pendingChannelId, selectDeferred };
}
9 changes: 4 additions & 5 deletions desktop/src/app/useHuddlePresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,15 +298,15 @@ export function useHuddlePresentation() {
void queryClient.invalidateQueries({
queryKey: channelWindowKey(ephemeralChannelId),
});
void goChannel(ephemeralChannelId);
return goChannel(ephemeralChannelId);
},
[goChannel, queryClient, revealHuddleChannel],
);
const showHuddleInMainApp = React.useCallback(
(ephemeralChannelId: string) => {
activeHuddleChannelIdRef.current = ephemeralChannelId;
trackHuddleBackingChannel(ephemeralChannelId);
viewHuddleChannel(ephemeralChannelId);
return viewHuddleChannel(ephemeralChannelId);
},
[trackHuddleBackingChannel, viewHuddleChannel],
);
Expand All @@ -316,10 +316,9 @@ export function useHuddlePresentation() {
isHuddleDrawerOpen &&
channelId === activeHuddleChannelIdRef.current
) {
showHuddleInMainApp(channelId);
return;
return showHuddleInMainApp(channelId);
}
void goChannel(channelId);
return goChannel(channelId);
},
[goChannel, isHuddleDrawerOpen, showHuddleInMainApp],
);
Expand Down
29 changes: 15 additions & 14 deletions desktop/src/features/channels/ui/ChannelPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,7 @@ export const ChannelPane = React.memo(function ChannelPane({
>
{isHuddleTranscript ? null : header}
<MessageTimeline
key={activeChannelId ?? "none"}
ref={messageTimelineRef}
channelId={activeChannel?.id}
channelIntro={channelIntro}
Expand Down Expand Up @@ -781,6 +782,20 @@ export const ChannelPane = React.memo(function ChannelPane({
useSplitAuxiliaryPane={useSplitAuxiliaryPane}
transparentChrome={hasSplitAuxiliaryPane}
/>
) : shouldShowThreadSkeleton ? (
(() => {
if (isHuddleTranscript) {
return wrapThreadPanel(<HuddleStartingView />);
}
const panel = (
<MessageThreadPanelSkeleton
{...threadLayoutProps}
onClose={onCloseThread}
widthPx={threadPanelWidthPx}
/>
);
return wrapThreadPanel(panel);
})()
) : threadHeadMessage ? (
(() => {
const panel = (
Expand Down Expand Up @@ -853,20 +868,6 @@ export const ChannelPane = React.memo(function ChannelPane({
);
return wrapThreadPanel(panel);
})()
) : shouldShowThreadSkeleton ? (
(() => {
if (isHuddleTranscript) {
return wrapThreadPanel(<HuddleStartingView />);
}
const panel = (
<MessageThreadPanelSkeleton
{...threadLayoutProps}
onClose={onCloseThread}
widthPx={threadPanelWidthPx}
/>
);
return wrapThreadPanel(panel);
})()
) : activeChannel && selectedAgent ? (
(() => {
// When the panel was opened from a different channel than the
Expand Down
Loading
Loading