/** * Page-visibility helpers. SSR-safe — every entry point guards on * `typeof document`. * * Why `hidden || !hasFocus` instead of just `hidden`: * - `document.hidden` flips true only when the tab is fully * backgrounded (other tab, minimised window). * - A focused window in the foreground with the chat tab visible but * the user typing into a different app still counts as "watching" * for Chrome's purposes. We treat that as away too, so unread * notifications kick in when you alt-tab to your editor. * * Inside Wails / Electron with a single window, `document.hidden` is * always false and `document.hasFocus()` tracks the OS window — which * is exactly what we want for a desktop dock badge if a host ever * decides to reuse the browser notifier (most won't; they'll ship * their own ChatNotifier). */ export function isPageHidden(): boolean { if (typeof document === 'undefined') return false; if (document.hidden) return true; if (typeof document.hasFocus === 'function' && !document.hasFocus()) return true; return false; } /** * Subscribe to visibility/focus changes. Returns an unsubscribe fn. * No-op in SSR. */ export function onVisibilityChange(cb: (hidden: boolean) => void): () => void { if (typeof document === 'undefined') return () => {}; const fire = () => cb(isPageHidden()); document.addEventListener('visibilitychange', fire); // `focus` / `blur` on window catch alt-tab-without-visibility-change // (Chrome on macOS notably keeps `visibilityState === 'visible'` for // background-but-on-screen tabs). window.addEventListener('focus', fire); window.addEventListener('blur', fire); return () => { document.removeEventListener('visibilitychange', fire); window.removeEventListener('focus', fire); window.removeEventListener('blur', fire); }; }