// App-wide store of open floating chat windows (the Messenger-style dock, bottom-right). // A window can be opened from anywhere (a conversation's "pop out" button, or automatically // when a message arrives) and stays open across page navigation. The dock state (which chats // are open + their minimised state) is persisted per user so it survives a reload. import { LS } from "./storage"; import { createStore } from "./store"; export interface DockChat { partnerId: number; name: string; avatar: string | null; minimized: boolean; flash: number; // bumped to trigger a one-shot attention flash on the window } type PartnerLike = { id: number; name: string; avatar_url: string | null }; const MAX_OPEN = 3; // keep the dock from overflowing the corner const dock = createStore([]); let userId: number | null = null; function storageKey(): string | null { return userId != null ? LS.chatDock(userId) : null; } function persist(): void { const k = storageKey(); if (!k) return; try { // Don't persist the transient flash counter. localStorage.setItem(k, JSON.stringify(dock.get().map(({ flash, ...c }) => c))); } catch { /* ignore quota/serialization errors */ } } function emit(next: DockChat[]): void { dock.set(next); persist(); } // Bind the dock to the signed-in user and restore their persisted windows (once per user). export function initDock(uid: number): void { if (userId === uid) return; userId = uid; let restored: DockChat[] = []; try { const raw = localStorage.getItem(storageKey()!); if (raw) restored = (JSON.parse(raw) as Omit[]).map((c) => ({ ...c, flash: 0 })); } catch { restored = []; } dock.set(restored.slice(0, MAX_OPEN)); } export function openChat(partner: PartnerLike): void { const existing = dock.get().find((c) => c.partnerId === partner.id); if (existing) { // Re-opening focuses it: ensure expanded and move to the front. emit([ { ...existing, minimized: false }, ...dock.get().filter((c) => c.partnerId !== partner.id), ]); return; } emit( [ { partnerId: partner.id, name: partner.name, avatar: partner.avatar_url, minimized: false, flash: 0, }, ...dock.get(), ].slice(0, MAX_OPEN), ); } // An incoming message arrived from `partner`: open the window if it's closed, otherwise just // flash it (whether expanded or minimised — don't disturb the minimised state). export function notifyIncoming(partner: PartnerLike): void { const existing = dock.get().find((c) => c.partnerId === partner.id); if (!existing) { emit( [ { partnerId: partner.id, name: partner.name, avatar: partner.avatar_url, minimized: false, flash: 1, }, ...dock.get(), ].slice(0, MAX_OPEN), ); return; } emit(dock.get().map((c) => (c.partnerId === partner.id ? { ...c, flash: c.flash + 1 } : c))); } export function closeChat(partnerId: number): void { emit(dock.get().filter((c) => c.partnerId !== partnerId)); } export function toggleMinimize(partnerId: number): void { emit(dock.get().map((c) => (c.partnerId === partnerId ? { ...c, minimized: !c.minimized } : c))); } export function useDockChats(): DockChat[] { return dock.useValue(); }