mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): quiet gate for workspace switches instead of boot splash (#1449)
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
+43
-2
@@ -7,6 +7,7 @@ import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
@@ -26,6 +27,7 @@ import { isSharedIdentity as isSharedIdentityCmd } from "@/shared/api/tauri";
|
||||
import { listenForDeepLinks } from "@/shared/deep-link";
|
||||
import { useSystemColorScheme } from "@/shared/theme/useSystemColorScheme";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Spinner } from "@/shared/ui/spinner";
|
||||
import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion";
|
||||
import { StepProgress } from "@/shared/ui/step-progress";
|
||||
|
||||
@@ -54,6 +56,31 @@ function AppLoadingGate() {
|
||||
);
|
||||
}
|
||||
|
||||
// Quiet gate for switching between already-set-up workspaces: visually empty
|
||||
// unless the switch takes long, so fast switches don't flash the boot splash.
|
||||
function WorkspaceSwitchGate() {
|
||||
const [showSpinner, setShowSpinner] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => setShowSpinner(true), 300);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex min-h-dvh items-center justify-center bg-background"
|
||||
data-testid="workspace-switch-gate"
|
||||
role="status"
|
||||
>
|
||||
<StartupWindowDragRegion />
|
||||
<span className="sr-only">Switching workspace…</span>
|
||||
{showSpinner ? (
|
||||
<Spinner aria-hidden="true" className="text-muted-foreground" />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OnboardingLoadingGate() {
|
||||
const systemColorScheme = useSystemColorScheme();
|
||||
|
||||
@@ -155,12 +182,14 @@ function AppReady({
|
||||
canBackToWorkspaceSetup,
|
||||
isCompletingFirstRunWorkspace,
|
||||
isSharedIdentity,
|
||||
isWorkspaceSwitch,
|
||||
onFirstRunWorkspaceSettled,
|
||||
onBackToWorkspaceSetup,
|
||||
}: {
|
||||
canBackToWorkspaceSetup: boolean;
|
||||
isCompletingFirstRunWorkspace: boolean;
|
||||
isSharedIdentity: boolean;
|
||||
isWorkspaceSwitch: boolean;
|
||||
onFirstRunWorkspaceSettled: () => void;
|
||||
onBackToWorkspaceSetup: () => void;
|
||||
}) {
|
||||
@@ -193,7 +222,7 @@ function AppReady({
|
||||
return <OnboardingLoadingGate />;
|
||||
}
|
||||
|
||||
return <AppLoadingGate />;
|
||||
return isWorkspaceSwitch ? <WorkspaceSwitchGate /> : <AppLoadingGate />;
|
||||
}
|
||||
|
||||
return <RouterProvider router={router} />;
|
||||
@@ -251,6 +280,17 @@ export function App() {
|
||||
// Composite key: changes when workspace ID changes OR when
|
||||
// the active workspace's config is updated (relayUrl/token).
|
||||
const workspaceKey = `${activeWorkspace?.id ?? "none"}-${reinitKey}`;
|
||||
|
||||
// Latch once the workspace key deviates from its cold-boot value: from then
|
||||
// on, loading phases are in-app switches and get the quiet gate instead of
|
||||
// the full "Setting up your workspace" splash.
|
||||
const initialWorkspaceKeyRef = useRef(workspaceKey);
|
||||
const hasSwitchedWorkspaceRef = useRef(false);
|
||||
if (workspaceKey !== initialWorkspaceKeyRef.current) {
|
||||
hasSwitchedWorkspaceRef.current = true;
|
||||
}
|
||||
const isWorkspaceSwitch = hasSwitchedWorkspaceRef.current;
|
||||
|
||||
const workspace = useWorkspaceInit(
|
||||
activeWorkspace,
|
||||
workspaceKey,
|
||||
@@ -306,7 +346,7 @@ export function App() {
|
||||
return <OnboardingLoadingGate />;
|
||||
}
|
||||
|
||||
return <AppLoadingGate />;
|
||||
return isWorkspaceSwitch ? <WorkspaceSwitchGate /> : <AppLoadingGate />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -314,6 +354,7 @@ export function App() {
|
||||
<AppReady
|
||||
canBackToWorkspaceSetup={canBackToWorkspaceSetup}
|
||||
isCompletingFirstRunWorkspace={isCompletingFirstRunWorkspace}
|
||||
isWorkspaceSwitch={isWorkspaceSwitch}
|
||||
key={workspaceKey}
|
||||
isSharedIdentity={sharedIdentity}
|
||||
onFirstRunWorkspaceSettled={handleFirstRunWorkspaceSettled}
|
||||
|
||||
@@ -93,6 +93,9 @@ type E2eConfig = {
|
||||
channelsReadError?: string;
|
||||
feedReadError?: string;
|
||||
canvasReadError?: string;
|
||||
/** Delay (ms) for `apply_workspace` so e2e tests can observe the
|
||||
* workspace-switch gate. 0/undefined = instant. */
|
||||
applyWorkspaceDelayMs?: number;
|
||||
openDmDelayMs?: number;
|
||||
sendMessageDelayMs?: number;
|
||||
usersBatchDelayMs?: number;
|
||||
@@ -7462,8 +7465,15 @@ export function maybeInstallE2eTauriMocks() {
|
||||
return importMockIdentity(
|
||||
(payload as { nsec?: string } | null)?.nsec ?? "",
|
||||
);
|
||||
case "apply_workspace":
|
||||
case "apply_workspace": {
|
||||
const applyDelayMs = activeConfig?.mock?.applyWorkspaceDelayMs ?? 0;
|
||||
if (applyDelayMs > 0) {
|
||||
return new Promise((resolve) =>
|
||||
window.setTimeout(resolve, applyDelayMs),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "get_profile":
|
||||
return handleGetProfile(activeConfig);
|
||||
case "update_profile":
|
||||
|
||||
@@ -73,6 +73,34 @@ test.describe("workspace rail", () => {
|
||||
.toBe(WORKSPACE_B.id);
|
||||
});
|
||||
|
||||
test("shows the quiet switch gate, not the boot splash, while switching", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Slow down apply_workspace so the loading phase is observable.
|
||||
await installMockBridge(
|
||||
page,
|
||||
{ applyWorkspaceDelayMs: 800 },
|
||||
{ skipWorkspaceSeed: true },
|
||||
);
|
||||
await seedWorkspaces(page, [WORKSPACE_A, WORKSPACE_B], WORKSPACE_A.id);
|
||||
await page.goto("/");
|
||||
|
||||
// Cold boot still uses the full splash.
|
||||
await expect(page.getByTestId("app-loading-gate")).toBeVisible();
|
||||
const buttonB = page.getByTestId(`workspace-rail-button-${WORKSPACE_B.id}`);
|
||||
await expect(buttonB).toBeVisible();
|
||||
|
||||
await buttonB.click();
|
||||
|
||||
// The switch renders the quiet gate; the "Setting up your workspace"
|
||||
// splash must not reappear.
|
||||
await expect(page.getByTestId("workspace-switch-gate")).toBeVisible();
|
||||
await expect(page.getByTestId("app-loading-gate")).toHaveCount(0);
|
||||
|
||||
// The app settles into the new workspace once apply completes.
|
||||
await expect(buttonB).toHaveAttribute("aria-current", "true");
|
||||
});
|
||||
|
||||
test("hides the rail with a single workspace", async ({ page }) => {
|
||||
await installMockBridge(page, undefined, { skipWorkspaceSeed: true });
|
||||
await seedWorkspaces(page, [WORKSPACE_A], WORKSPACE_A.id);
|
||||
|
||||
@@ -111,6 +111,8 @@ type MockBridgeOptions = {
|
||||
channelsReadError?: string;
|
||||
feedReadError?: string;
|
||||
canvasReadError?: string;
|
||||
/** Delay (ms) for `apply_workspace`; see e2eBridge mock config. */
|
||||
applyWorkspaceDelayMs?: number;
|
||||
openDmDelayMs?: number;
|
||||
sendMessageDelayMs?: number;
|
||||
usersBatchDelayMs?: number;
|
||||
|
||||
Reference in New Issue
Block a user