{"release_id":"01a1998d-9802-0000-b0f7-33a23307f4ed","chunk_id":"01a1998d-9819-7ea0-898a-23b236c2e022","file":"BGe1idH8.mjs","mappings":"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","names":["unreadNotifications","writable","unreadChat","notificationPrefs","applyMuteFromToast","target","key","p","arr","latestToast","notifySignedIn","activeUserId","unsub","initGlobalRealtime","userId","disposeGlobalRealtime","realtime","event","dispatchEvent","currentPathname","isToastMuted","eventType","ctx","prefs","get","n","inner","notificationTitle","notificationHref","notificationInitial","initialOf","value","c","type","payload","id","postId","code"],"sources":["../../../../../../src/lib/stores/unreadNotifications.ts","../../../../../../src/lib/stores/global-realtime.ts"],"sourcesContent":["import { writable } from 'svelte/store';\n\n/**\n * RAT-272: shared unread-notifications counter.\n *\n * `NotificationBell.svelte` polls `/api/notifications/unread-count` every 5s\n * for its own badge — `BottomNav.svelte` needs the same number to drive the\n * Profile-slot coral dot. Rather than have both components fetch in parallel\n * (doubling the request rate), the bell owns the fetch and writes here; the\n * bottom nav just subscribes.\n *\n * Initial value 0 means a brief \"no badge\" flash on first paint, same as the\n * bell's local state — RAT-247 explicitly accepted that to keep the SSR\n * layout from depending on a transient lookup.\n *\n * RAT-300: chat unread piggy-backs on the same poll endpoint (which now\n * returns `{ count, notifications, chat }`) so the BottomNav messages-slot\n * dot stays consistent with the bell — same single fetch fans out to two\n * stores via the bell's $effect.\n */\nexport const unreadNotifications = writable<number>(0);\nexport const unreadChat = writable<number>(0);\n","/** RAT-360 (F4): single entry point for realtime events that need to drive\n *  cross-cutting UI state (badge counters, toast notifications).\n *\n *  Subscribed once at the root layout (`+layout.svelte`) on the logged-in\n *  branch, dispatches into the existing `unreadNotifications` / `unreadChat`\n *  writables that the bell + BottomNav already read from, plus a new\n *  `latestToast` for the minimal toast UI.\n *\n *  Components that need more than the counter + toast (e.g. NotificationBell\n *  prepending fresh notifications to its dropdown items list) subscribe to\n *  the primitive directly — `realtime.subscribe('user:${userId}', ...)`.\n *  The primitive refcounts SDK channels, so multiple subs on the same name\n *  share the one underlying socket channel; no extra connection cost. */\n\nimport { writable, get } from 'svelte/store';\nimport { realtime } from '$lib/realtime/client';\nimport type { RealtimeEvent } from '$lib/realtime/types';\nimport { unreadNotifications, unreadChat } from './unreadNotifications';\n\n/** RAT-365: per-user toast mute toggles. Default = empty (everything fires).\n *  The root layout effect syncs this from data.notificationPrefs on every\n *  layout server load (session change, invalidateAll, etc.); the bell's\n *  settings panel writes optimistically here + POSTs to the API.\n *\n *  Note: mute only kills the toast + the latestToast emission. Unread\n *  counters keep bumping — muting is about reducing interruption, not\n *  hiding the fact that something arrived. */\nexport interface NotificationPrefs {\n\tmuteAll?: boolean;\n\tmuteDMs?: boolean;\n\tmuteGroups?: boolean;\n\tmuteNotifications?: boolean;\n\t/** RAT-366: per-conversation mute. The arrays hold chatThread.id (DMs)\n\t *  or groupRoom.id (rooms). isToastMuted checks both before falling\n\t *  through to the category flags. */\n\tmutedThreadIds?: string[];\n\tmutedRoomIds?: string[];\n}\n\nexport const notificationPrefs = writable<NotificationPrefs>({});\n\n/** RAT-369: handler for the quick-mute button on toasts. Optimistic store\n *  write + non-blocking POST to whichever endpoint owns the target. */\nexport async function applyMuteFromToast(target: MuteTarget): Promise<void> {\n\tif (target.kind === 'thread' || target.kind === 'room') {\n\t\tconst key = target.kind === 'thread' ? 'mutedThreadIds' : 'mutedRoomIds';\n\t\tnotificationPrefs.update((p) => {\n\t\t\tconst arr = p[key] ?? [];\n\t\t\treturn { ...p, [key]: arr.includes(target.id) ? arr : [...arr, target.id] };\n\t\t});\n\t\ttry {\n\t\t\tawait fetch('/api/profile/notification-prefs/mute', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: { 'content-type': 'application/json' },\n\t\t\t\tbody: JSON.stringify({ kind: target.kind, id: target.id, muted: true })\n\t\t\t});\n\t\t} catch {\n\t\t\t// optimistic — server reconciles on next load\n\t\t}\n\t\treturn;\n\t}\n\tif (target.kind === 'category-notifications') {\n\t\tnotificationPrefs.update((p) => ({ ...p, muteNotifications: true }));\n\t\ttry {\n\t\t\tawait fetch('/api/profile/notification-prefs', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: { 'content-type': 'application/json' },\n\t\t\t\tbody: JSON.stringify({ muteNotifications: true })\n\t\t\t});\n\t\t} catch {\n\t\t\t// optimistic — server reconciles on next load\n\t\t}\n\t}\n}\n\n/** What the toast component renders. Body is intentionally omitted from\n *  chat events (lock-screen / screenshare privacy — see F4 ticket). Only\n *  the metadata + a route that gets us to the source. */\nexport interface ToastEvent {\n\tid: string;\n\tkind: 'chat' | 'group' | 'notification' | 'rate_card' | 'auth';\n\ttitle: string;\n\t/** Where to navigate when the user taps the toast. Null = no-op tap. */\n\thref: string | null;\n\tavatarPath: string | null;\n\t/** RAT-364: shown inside the avatar circle when avatarPath is null. The\n\t *  first letter of the sender's nickname for chat/group, or a notification-\n\t *  type-specific glyph. Null falls back to a blank dot. */\n\tfallbackInitial: string | null;\n\t/** RAT-369: optional quick-mute target. When present, the toast renders\n\t *  a small bell-slash button that, on tap, silences future events of\n\t *  this kind. For chat / group it's a per-conversation mute (RAT-366);\n\t *  for notifications it flips the category-mute flag (RAT-365). */\n\tmuteTarget: MuteTarget | null;\n\tat: number;\n}\n\nexport type MuteTarget =\n\t| { kind: 'thread'; id: string }\n\t| { kind: 'room'; id: string }\n\t| { kind: 'category-notifications' };\n\nexport const latestToast = writable<ToastEvent | null>(null);\n\n/** RAT-491: pop a one-off \"you're signed in\" confirmation toast. Used by the\n *  OAuth-return handlers on pages that run no resume flow of their own (`/`,\n *  `/c/[nickname]`) so a successful google sign-in is actually visible instead\n *  of dropping the user back on a page that looks unchanged. Reuses the same\n *  minimal toast surface as realtime events (globally mounted for authed\n *  viewers), so no new chrome. */\nexport function notifySignedIn(): void {\n\tlatestToast.set({\n\t\tid: `auth-${crypto.randomUUID()}`,\n\t\tkind: 'auth',\n\t\ttitle: \"you're signed in.\",\n\t\thref: null,\n\t\tavatarPath: null,\n\t\tfallbackInitial: '✓',\n\t\tmuteTarget: null,\n\t\tat: Date.now()\n\t});\n}\n\nlet activeUserId: string | null = null;\nlet unsub: (() => void) | null = null;\n\n/** Wire up the global subscription. Idempotent — re-calling with the same\n *  userId is a no-op; switching userId tears down and re-subscribes. Called\n *  from the root layout effect. */\nexport function initGlobalRealtime(userId: string): void {\n\tif (activeUserId === userId) return;\n\tdisposeGlobalRealtime();\n\tactiveUserId = userId;\n\tunsub = realtime.subscribe(`user:${userId}`, (event) => dispatchEvent(event));\n}\n\nexport function disposeGlobalRealtime(): void {\n\tif (unsub) unsub();\n\tunsub = null;\n\tactiveUserId = null;\n}\n\n/** RAT-364: read the current pathname so we can suppress toast + counter\n *  bumps when the user is already looking at the source chat. SSR-safe\n *  via the typeof guard. */\nfunction currentPathname(): string {\n\treturn typeof window !== 'undefined' ? window.location.pathname : '';\n}\n\n/** RAT-365 / RAT-366: check whether the user has muted the channel a given\n *  event came from. muteAll wins over everything; per-conversation mutes\n *  (RAT-366) win over the category flags (RAT-365). Returns true ⇒ no\n *  toast (badge counters still bump separately).\n *\n *  Per-conversation context (threadId / roomId) is passed in by the\n *  caller because the event payload shape differs between DMs and groups. */\nfunction isToastMuted(\n\teventType: string,\n\tctx: { threadId?: string | null; roomId?: string | null } = {}\n): boolean {\n\tconst prefs = get(notificationPrefs);\n\tif (prefs.muteAll) return true;\n\tif (\n\t\teventType === 'chat_message_new' &&\n\t\tctx.threadId &&\n\t\tprefs.mutedThreadIds?.includes(ctx.threadId)\n\t) {\n\t\treturn true;\n\t}\n\tif (eventType === 'group_message_new' && ctx.roomId && prefs.mutedRoomIds?.includes(ctx.roomId)) {\n\t\treturn true;\n\t}\n\tif (eventType === 'chat_message_new' && prefs.muteDMs) return true;\n\tif (eventType === 'group_message_new' && prefs.muteGroups) return true;\n\tif (eventType === 'notification_new' && prefs.muteNotifications) return true;\n\treturn false;\n}\n\nfunction dispatchEvent(event: RealtimeEvent) {\n\tswitch (event.type) {\n\t\tcase 'notification_new': {\n\t\t\tunreadNotifications.update((n) => n + 1);\n\t\t\tif (isToastMuted('notification_new')) break;\n\t\t\tconst p = event.payload as {\n\t\t\t\tnotificationId?: string;\n\t\t\t\ttype?: string;\n\t\t\t\tpayload?: Record<string, unknown>;\n\t\t\t};\n\t\t\tconst inner = (p.payload ?? {}) as Record<string, unknown>;\n\t\t\tlatestToast.set({\n\t\t\t\tid: event.id,\n\t\t\t\tkind: 'notification',\n\t\t\t\ttitle: notificationTitle(p.type ?? 'notification', inner),\n\t\t\t\thref: notificationHref(p.type ?? 'notification', inner),\n\t\t\t\tavatarPath: typeof inner.senderAvatarPath === 'string' ? inner.senderAvatarPath : null,\n\t\t\t\tfallbackInitial: notificationInitial(p.type ?? 'notification', inner),\n\t\t\t\tmuteTarget: { kind: 'category-notifications' },\n\t\t\t\tat: Date.now()\n\t\t\t});\n\t\t\tbreak;\n\t\t}\n\t\tcase 'chat_message_new': {\n\t\t\tconst p = event.payload as {\n\t\t\t\tthreadId?: string;\n\t\t\t\tsenderNickname?: string | null;\n\t\t\t\tsenderAvatarPath?: string | null;\n\t\t\t};\n\t\t\t// RAT-364: user is already viewing this DM thread — the thread page\n\t\t\t// handles append + mark-read itself. Skipping bump + toast prevents\n\t\t\t// the badge flash + redundant cue.\n\t\t\tif (p.senderNickname && currentPathname() === `/c/${p.senderNickname}`) break;\n\t\t\tunreadChat.update((n) => n + 1);\n\t\t\tif (isToastMuted('chat_message_new', { threadId: p.threadId ?? null })) break;\n\t\t\tlatestToast.set({\n\t\t\t\tid: event.id,\n\t\t\t\tkind: 'chat',\n\t\t\t\t// Privacy-first: never include the body, just who sent.\n\t\t\t\ttitle: `new message — ${p.senderNickname ?? 'someone'}`,\n\t\t\t\thref: p.senderNickname ? `/c/${p.senderNickname}` : null,\n\t\t\t\tavatarPath: p.senderAvatarPath ?? null,\n\t\t\t\tfallbackInitial: initialOf(p.senderNickname),\n\t\t\t\tmuteTarget: p.threadId ? { kind: 'thread', id: p.threadId } : null,\n\t\t\t\tat: Date.now()\n\t\t\t});\n\t\t\tbreak;\n\t\t}\n\t\tcase 'group_message_new': {\n\t\t\tconst p = event.payload as {\n\t\t\t\troomId?: string;\n\t\t\t\tinviteCode?: string | null;\n\t\t\t\tsenderNickname?: string | null;\n\t\t\t\tsenderAvatarPath?: string | null;\n\t\t\t};\n\t\t\t// RAT-364: suppress when already in this room. inviteCode added to\n\t\t\t// the broadcast payload by group-chat.ts dispatchGroupMessageNew.\n\t\t\tif (p.inviteCode && currentPathname() === `/g/${p.inviteCode}`) break;\n\t\t\tunreadChat.update((n) => n + 1);\n\t\t\tif (isToastMuted('group_message_new', { roomId: p.roomId ?? null })) break;\n\t\t\tlatestToast.set({\n\t\t\t\tid: event.id,\n\t\t\t\tkind: 'group',\n\t\t\t\ttitle: `new group message — ${p.senderNickname ?? 'someone'}`,\n\t\t\t\thref: p.inviteCode ? `/g/${p.inviteCode}` : null,\n\t\t\t\tavatarPath: p.senderAvatarPath ?? null,\n\t\t\t\tfallbackInitial: initialOf(p.senderNickname),\n\t\t\t\tmuteTarget: p.roomId ? { kind: 'room', id: p.roomId } : null,\n\t\t\t\tat: Date.now()\n\t\t\t});\n\t\t\tbreak;\n\t\t}\n\t\tcase 'rate_card_flip': {\n\t\t\t// Requester got rated. No counter bump (the rate flip isn't an\n\t\t\t// \"unread thing\" the bell tracks), but a toast is the right cue.\n\t\t\tlatestToast.set({\n\t\t\t\tid: event.id,\n\t\t\t\tkind: 'rate_card',\n\t\t\t\ttitle: 'your rate is in',\n\t\t\t\thref: null,\n\t\t\t\tavatarPath: null,\n\t\t\t\tfallbackInitial: '★',\n\t\t\t\tmuteTarget: null,\n\t\t\t\tat: Date.now()\n\t\t\t});\n\t\t\tbreak;\n\t\t}\n\t\t// chat_message_new / group_message_new / notification_new are the only\n\t\t// types we centralize. thread:* and room:* events go straight to the\n\t\t// page-level subscribers via the primitive.\n\t}\n}\n\n/** RAT-364: first letter of `value` uppercased, or null when empty. Drives\n *  the avatar-circle fallback when the sender has no avatar. */\nfunction initialOf(value: string | null | undefined): string | null {\n\tif (!value) return null;\n\tconst c = value.trim().charAt(0);\n\treturn c ? c.toUpperCase() : null;\n}\n\n/** Notification glyph that fits the type when the sender is anonymous /\n *  has no avatar. Falls back to the first letter of an inner nickname,\n *  then to a neutral bell. */\nfunction notificationInitial(type: string, payload: Record<string, unknown>): string | null {\n\tswitch (type) {\n\t\tcase 'new_follower':\n\t\t\treturn initialOf(payload.followerNickname as string | undefined);\n\t\tcase 'chat_mention':\n\t\t\treturn initialOf(payload.sender as string | undefined);\n\t\tcase 'duel_challenge':\n\t\t\treturn '⚔';\n\t\tcase 'rate_request_queued':\n\t\tcase 'rate_session_started':\n\t\tcase 'peer_rate_request':\n\t\t\treturn initialOf(payload.requesterNickname as string | undefined) ?? '★';\n\t\tcase 'new_post':\n\t\t\treturn '✎';\n\t\tcase 'beat_new_comment':\n\t\t\treturn '💬';\n\t\tcase 'beat_accepted':\n\t\tcase 'beat_closed':\n\t\t\treturn '⚔';\n\t\tcase 'arena_admirers':\n\t\t\treturn '♥';\n\t\tcase 'animation_ready':\n\t\t\treturn '▶';\n\t\tcase 'tod_invite':\n\t\tcase 'tod_started':\n\t\tcase 'tod_turn':\n\t\tcase 'tod_player_joined':\n\t\tcase 'tod_can_start':\n\t\tcase 'tod_table_full':\n\t\tcase 'tod_over':\n\t\t\treturn '🍾';\n\t\tdefault:\n\t\t\treturn null;\n\t}\n}\n\n/** Human-readable label for the notification types defined in\n *  $lib/server/notifications. Kept terse and privacy-respecting. */\nfunction notificationTitle(type: string, payload: Record<string, unknown>): string {\n\tswitch (type) {\n\t\tcase 'duel_challenge':\n\t\t\treturn 'new duel challenge';\n\t\tcase 'new_follower': {\n\t\t\tconst n = typeof payload.followerNickname === 'string' ? payload.followerNickname : null;\n\t\t\treturn n ? `new follower: ${n}` : 'new follower';\n\t\t}\n\t\tcase 'rate_request_queued':\n\t\t\treturn 'new rate request in your queue';\n\t\tcase 'peer_rate_request': {\n\t\t\tconst n = typeof payload.requesterNickname === 'string' ? payload.requesterNickname : null;\n\t\t\treturn n ? `${n} wants you to rate them` : 'new rate request';\n\t\t}\n\t\tcase 'rate_session_started':\n\t\t\treturn 'rate session started';\n\t\tcase 'new_post':\n\t\t\treturn 'new post in your feed';\n\t\tcase 'beat_new_comment':\n\t\t\treturn 'new comment on your duel';\n\t\tcase 'beat_accepted':\n\t\t\treturn 'someone took your beat';\n\t\tcase 'beat_closed':\n\t\t\treturn payload.result === 'won'\n\t\t\t\t? 'you won your beat'\n\t\t\t\t: payload.result === 'tie'\n\t\t\t\t\t? 'your beat ended in a tie'\n\t\t\t\t\t: 'your beat is over';\n\t\tcase 'arena_admirers': {\n\t\t\tconst c = typeof payload.count === 'number' ? payload.count : 1;\n\t\t\treturn `${c === 1 ? '1 person' : `${c} people`} liked you`;\n\t\t}\n\t\tcase 'chat_mention': {\n\t\t\tconst n = typeof payload.sender === 'string' ? payload.sender : null;\n\t\t\treturn n ? `${n} mentioned you` : 'mentioned in a chat';\n\t\t}\n\t\tcase 'animation_ready':\n\t\t\treturn 'your animation is ready';\n\t\tcase 'animation_failed':\n\t\t\treturn 'animation failed — tokens refunded';\n\t\t// truth or dare\n\t\tcase 'tod_invite': {\n\t\t\tconst n = typeof payload.hostNickname === 'string' ? payload.hostNickname : null;\n\t\t\treturn n ? `${n} wants you at their table` : 'truth or dare invite';\n\t\t}\n\t\tcase 'tod_started':\n\t\t\treturn 'your table just started';\n\t\tcase 'tod_turn':\n\t\t\treturn 'the bottle landed on you';\n\t\tcase 'tod_player_joined': {\n\t\t\tconst c = typeof payload.count === 'number' ? payload.count : null;\n\t\t\treturn c ? `${c} at your table` : 'someone sat down at your table';\n\t\t}\n\t\tcase 'tod_can_start':\n\t\t\treturn 'three at the table · starts in 10 min';\n\t\tcase 'tod_table_full':\n\t\t\treturn 'table is full · start it';\n\t\tcase 'tod_over':\n\t\t\treturn 'match over';\n\t\tdefault:\n\t\t\treturn 'new notification';\n\t}\n}\n\nfunction notificationHref(type: string, payload: Record<string, unknown>): string | null {\n\tswitch (type) {\n\t\tcase 'animation_ready':\n\t\tcase 'animation_failed': {\n\t\t\tconst id = typeof payload.animationId === 'string' ? payload.animationId : null;\n\t\t\tconst postId = typeof payload.postId === 'string' ? payload.postId : null;\n\t\t\treturn id ? `/animations/${id}` : postId ? `/post/${postId}` : null;\n\t\t}\n\t\tcase 'duel_challenge': {\n\t\t\tconst code = typeof payload.duelCode === 'string' ? payload.duelCode : null;\n\t\t\treturn code ? `/d/${code}` : null;\n\t\t}\n\t\tcase 'new_follower': {\n\t\t\tconst n = typeof payload.followerNickname === 'string' ? payload.followerNickname : null;\n\t\t\treturn n ? `/u/${n}` : null;\n\t\t}\n\t\tcase 'peer_rate_request': {\n\t\t\tconst n = typeof payload.requesterNickname === 'string' ? payload.requesterNickname : null;\n\t\t\treturn n ? `/c/${n}` : null;\n\t\t}\n\t\tcase 'new_post': {\n\t\t\tconst id = typeof payload.postId === 'string' ? payload.postId : null;\n\t\t\treturn id ? `/post/${id}` : null;\n\t\t}\n\t\tcase 'beat_new_comment':\n\t\tcase 'beat_accepted':\n\t\tcase 'beat_closed': {\n\t\t\tconst code = typeof payload.challengeCode === 'string' ? payload.challengeCode : null;\n\t\t\treturn code ? `/b/${code}` : null;\n\t\t}\n\t\tcase 'arena_admirers':\n\t\t\treturn '/a?admirers=first';\n\t\tcase 'chat_mention': {\n\t\t\t// Either DM (senderNickname) or group (inviteCode). Audit notes\n\t\t\t// chatKind discriminates.\n\t\t\tconst kind = typeof payload.chatKind === 'string' ? payload.chatKind : null;\n\t\t\tif (kind === 'group') {\n\t\t\t\tconst code = typeof payload.inviteCode === 'string' ? payload.inviteCode : null;\n\t\t\t\treturn code ? `/g/${code}` : null;\n\t\t\t}\n\t\t\tconst n = typeof payload.senderNickname === 'string' ? payload.senderNickname : null;\n\t\t\treturn n ? `/c/${n}` : null;\n\t\t}\n\t\tdefault:\n\t\t\treturn null;\n\t}\n}\n"],"version":3}