import type { ChatMessage } from '../types'; import { createFaviconBadge, type FaviconBadgeOptions } from './faviconBadge'; import { createTitleRotator, type TitleRotatorOptions } from './titleRotator'; import type { ChatNotifier } from './types'; export interface BrowserNotifierOptions { /** Title rotation config. Pass `false` to disable title mutation. */ title?: TitleRotatorOptions | false; /** Favicon badge config. Pass `false` to disable favicon mutation. */ favicon?: FaviconBadgeOptions | false; } const NOOP: ChatNotifier = { setUnread() {}, clear() {} }; /** * Facebook-style unread notifier: alternates `document.title` between * the base title and an alert, plus paints a small badge over the * favicon. Both surfaces are optional and individually toggleable. * * Returns a no-op in SSR or non-DOM environments — safe to construct * unconditionally. * * The notifier itself is **stateless w.r.t. visibility** by design; * the hook layer (`useChatUnreadNotifier`) decides when to call * `setUnread` vs `clear` based on page focus. Keeping the policy in * the hook lets hosts swap in their own notifier (Wails dock badge, * cross-tab Zustand broadcaster) without duplicating the gating logic. */ export function createBrowserNotifier(opts: BrowserNotifierOptions = {}): ChatNotifier { if (typeof document === 'undefined') return NOOP; const title = opts.title === false ? null : createTitleRotator(opts.title); const favicon = opts.favicon === false ? null : createFaviconBadge(opts.favicon); let active = false; return { setUnread(count: number, latest?: ChatMessage | null) { if (count <= 0) { this.clear(); return; } if (active) { title?.update(count, latest); favicon?.set(count); } else { active = true; title?.start(count, latest); favicon?.set(count); } }, clear() { if (!active) return; active = false; title?.stop(); favicon?.clear(); }, }; } export function createNoopNotifier(): ChatNotifier { return NOOP; }