diff --git a/apps/desktop/src/lib/friendNicknames.ts b/apps/desktop/src/lib/friendNicknames.ts new file mode 100644 index 0000000..547695b --- /dev/null +++ b/apps/desktop/src/lib/friendNicknames.ts @@ -0,0 +1,64 @@ +import { useSyncExternalStore } from 'react'; + +// Local-only friend nickname overrides. Stored in localStorage keyed by the +// peer's user-id. Empty/missing value = use the real display name. +// Local-only by design — friends never see your nickname for them. + +const STORAGE_KEY = 'chatapp.friendNicknames.v1'; + +let cache: Record | null = null; +const listeners = new Set<() => void>(); + +function load(): Record { + if (cache) return cache; + try { + const raw = window.localStorage.getItem(STORAGE_KEY); + if (!raw) { cache = {}; return cache; } + const parsed = JSON.parse(raw) as unknown; + if (parsed && typeof parsed === 'object') { + cache = {}; + for (const [k, v] of Object.entries(parsed as Record)) { + if (typeof v === 'string' && v.trim().length > 0) cache[k] = v; + } + return cache; + } + } catch { /* corrupted; fall through */ } + cache = {}; + return cache; +} + +function persist(): void { + try { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(cache ?? {})); } + catch { /* quota / private mode */ } + for (const l of listeners) l(); +} + +export function getNickname(userId: string): string | null { + return load()[userId] ?? null; +} + +export function setNickname(userId: string, nickname: string | null): void { + const store = load(); + const trimmed = nickname?.trim() ?? ''; + if (trimmed.length === 0) { + if (!(userId in store)) return; + delete store[userId]; + } else { + if (store[userId] === trimmed) return; + store[userId] = trimmed; + } + persist(); +} + +// Reactive hook: returns the current nickname for a user, or `fallback` +// when no nickname is set. Re-renders when ANY nickname changes (cheap, +// the set is small). +export function useNickname(userId: string | null | undefined, fallback: string): string { + const subscribe = (cb: () => void) => { + listeners.add(cb); + return () => { listeners.delete(cb); }; + }; + const getSnapshot = () => (userId ? getNickname(userId) : null); + const nickname = useSyncExternalStore(subscribe, getSnapshot, getSnapshot); + return nickname ?? fallback; +}