mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
## Summary - defer foreground resume work until the activation task has returned, a frame has painted, and a trailing task gets a turn - centralize app-focus subscribers and remove the broad TanStack `refetchOnWindowFocus` fan-out - coalesce relay recovery and preserve an explicit deferred refresh only for workflow data without a polling/push freshness path - defer the notification permission native check while keeping blur and cheap correctness signals immediate ## Why Buzz Desktop 0.5.10 can spend roughly 1.5 seconds in the WebKit window-focus listener/microtask checkpoint before returning to the run loop. Focus currently fans out into query refetches, React polling updates, relay reconnect/replay, and native work in one activation turn. This patch establishes an interaction-first foreground boundary rather than letting those consumers compete with the activating input and first paint. ## Validation - focused foreground/workflow/relay tests: 18/18 passed before commit - `pnpm --dir desktop typecheck`: passed before commit - pre-commit desktop check and file-size gate: passed - pre-push desktop check, typecheck, and full desktop unit suite: 4,743/4,743 passed at `704e7b4b6618fafce655bb2b07c7a9fe0fc8c643` - Princess Donut independent adversarial review: PASS after two lifecycle/freshness blockers were resolved ## Manual test 1. Install the PR build and use Buzz long enough to populate channels, home, workflows, agents, and other polling surfaces. 2. Switch to another app for 30-60 seconds. 3. Return by clicking Buzz and immediately click a channel or scroll. 4. Confirm the first interaction and paint are prompt, then confirm channels/home/workflows refresh and a degraded relay reconnects after the activation boundary. 5. Repeat while rapidly switching away again to verify no resume work starts after focus has been lost. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
94 lines
2.7 KiB
TypeScript
94 lines
2.7 KiB
TypeScript
import * as React from "react";
|
|
|
|
import { setDesktopAppBadge } from "@/features/notifications/lib/desktop";
|
|
import { useForegroundQueryRefresh } from "@/features/workflows/hooks";
|
|
import { relayClient } from "@/shared/api/relayClient";
|
|
import { useRelayResumeTriggers } from "@/shared/api/useRelayResumeTriggers";
|
|
|
|
type AppShellLifecycleEffectsOptions = {
|
|
desktopBadgeEnabled: boolean;
|
|
homeBadgeCountExcludingHighPriority: number;
|
|
unreadChannelIds: ReadonlySet<string>;
|
|
unreadChannelNotificationCount: number;
|
|
};
|
|
|
|
export function useAppShellLifecycleEffects({
|
|
desktopBadgeEnabled,
|
|
homeBadgeCountExcludingHighPriority,
|
|
unreadChannelIds,
|
|
unreadChannelNotificationCount,
|
|
}: AppShellLifecycleEffectsOptions) {
|
|
// Event-driven reconnect: network online / focus / visibility short-circuit
|
|
// the backoff timer when the relay session is degraded (CMD+R gap G1).
|
|
useRelayResumeTriggers();
|
|
useForegroundQueryRefresh();
|
|
|
|
// Prevent webview file:/// navigation on file drop outside the composer.
|
|
// Scoped to file drags only (text drag-and-drop into inputs still works).
|
|
// Composer's onDrop fires first (React synthetic before window bubble).
|
|
React.useEffect(() => {
|
|
function preventNavigation(e: DragEvent) {
|
|
if (e.dataTransfer?.types.includes("Files")) {
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
window.addEventListener("dragover", preventNavigation);
|
|
window.addEventListener("drop", preventNavigation);
|
|
return () => {
|
|
window.removeEventListener("dragover", preventNavigation);
|
|
window.removeEventListener("drop", preventNavigation);
|
|
};
|
|
}, []);
|
|
|
|
React.useEffect(() => {
|
|
let isCancelled = false;
|
|
|
|
const startPreconnect = () => {
|
|
if (isCancelled) {
|
|
return;
|
|
}
|
|
|
|
void relayClient.preconnect().catch((error) => {
|
|
if (!isCancelled) {
|
|
console.error("Failed to preconnect to relay", error);
|
|
}
|
|
});
|
|
};
|
|
|
|
if ("requestIdleCallback" in window) {
|
|
const idleId = window.requestIdleCallback(startPreconnect, {
|
|
timeout: 1_500,
|
|
});
|
|
return () => {
|
|
isCancelled = true;
|
|
window.cancelIdleCallback(idleId);
|
|
};
|
|
}
|
|
|
|
const timeoutId = globalThis.setTimeout(startPreconnect, 250);
|
|
return () => {
|
|
isCancelled = true;
|
|
globalThis.clearTimeout(timeoutId);
|
|
};
|
|
}, []);
|
|
|
|
React.useEffect(() => {
|
|
if (!desktopBadgeEnabled) {
|
|
return;
|
|
}
|
|
|
|
const count =
|
|
unreadChannelNotificationCount + homeBadgeCountExcludingHighPriority;
|
|
void setDesktopAppBadge(
|
|
count
|
|
? { kind: "count", count }
|
|
: { kind: unreadChannelIds.size ? "dot" : "none" },
|
|
);
|
|
}, [
|
|
desktopBadgeEnabled,
|
|
homeBadgeCountExcludingHighPriority,
|
|
unreadChannelIds,
|
|
unreadChannelNotificationCount,
|
|
]);
|
|
}
|