00ca0561eced | atagen | 2026-10-26 05:50 UTC | 1 | import type { RoomSummary } from './types'; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 2 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 3 | export type RoomMode = 'AllMessages' | 'MentionsAndKeywordsOnly' | 'Mute'; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 4 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 5 | export interface MutedSpaces { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 6 | spaces: string[]; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 7 | applied: Record<string, Record<string, RoomMode | null>>; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 8 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 9 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 10 | export const NO_MUTED_SPACES: MutedSpaces = { spaces: [], applied: {} }; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 11 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 12 | function isRecord(value: unknown): value is Record<string, unknown> { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 13 | return typeof value === 'object' && value !== null && !Array.isArray(value); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 14 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 15 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 16 | function isMode(value: unknown): value is RoomMode { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 17 | return value === 'AllMessages' || value === 'MentionsAndKeywordsOnly' || value === 'Mute'; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 18 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 19 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 20 | export function parseMutedSpaces(raw: unknown): MutedSpaces { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 21 | if (!isRecord(raw)) return NO_MUTED_SPACES; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 22 | const spaces = Array.isArray(raw['spaces']) ? [...new Set(raw['spaces'].filter((s): s is string => typeof s === 'string'))] : []; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 23 | const applied: MutedSpaces['applied'] = {}; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 24 | if (isRecord(raw['applied'])) { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 25 | for (const [spaceId, rooms] of Object.entries(raw['applied'])) { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 26 | if (!spaces.includes(spaceId) || !isRecord(rooms)) continue; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 27 | const entry: Record<string, RoomMode | null> = {}; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 28 | for (const [roomId, mode] of Object.entries(rooms)) if (mode === null || isMode(mode)) entry[roomId] = mode; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 29 | applied[spaceId] = entry; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 30 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 31 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 32 | return { spaces, applied }; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 33 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 34 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 35 | function childMap(rooms: RoomSummary[]): Map<string, Set<string>> { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 36 | const map = new Map<string, Set<string>>(); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 37 | const add = (space: string, child: string) => { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 38 | let set = map.get(space); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 39 | if (!set) map.set(space, (set = new Set())); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 40 | set.add(child); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 41 | }; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 42 | for (const room of rooms) { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 43 | for (const child of room.children) add(room.room_id, child); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 44 | for (const parent of room.parent_spaces) add(parent, room.room_id); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 45 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 46 | return map; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 47 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 48 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 49 | function descendants(spaceId: string, children: Map<string, Set<string>>, spaceIds: Set<string>): Set<string> { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 50 | const out = new Set<string>(); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 51 | const seen = new Set<string>([spaceId]); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 52 | const queue = [spaceId]; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 53 | for (let at = queue.pop(); at !== undefined; at = queue.pop()) { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 54 | for (const child of children.get(at) ?? []) { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 55 | if (spaceIds.has(child)) { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 56 | if (!seen.has(child)) { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 57 | seen.add(child); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 58 | queue.push(child); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 59 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 60 | } else out.add(child); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 61 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 62 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 63 | return out; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 64 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 65 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 66 | export function roomsUnderSpace(spaceId: string, rooms: RoomSummary[]): Set<string> { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 67 | const spaceIds = new Set(rooms.filter((r) => r.space).map((r) => r.room_id)); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 68 | return descendants(spaceId, childMap(rooms), spaceIds); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 69 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 70 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 71 | export function spaceMutedRoomIds(rooms: RoomSummary[], mutedSpaces: string[]): Set<string> { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 72 | const out = new Set<string>(); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 73 | if (mutedSpaces.length === 0) return out; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 74 | const children = childMap(rooms); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 75 | const spaceIds = new Set(rooms.filter((r) => r.space).map((r) => r.room_id)); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 76 | for (const id of mutedSpaces) for (const room of descendants(id, children, spaceIds)) out.add(room); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 77 | return out; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 78 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 79 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 80 | export function isRoomMuted(room: RoomSummary, spaceMuted: Set<string>): boolean { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 81 | return room.notification_mode === 'Mute' || spaceMuted.has(room.room_id); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 82 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 83 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 84 | export function withSpaceMutes(rooms: RoomSummary[], spaceMuted: Set<string>): RoomSummary[] { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 85 | if (spaceMuted.size === 0) return rooms; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 86 | return rooms.map((room) => (spaceMuted.has(room.room_id) && room.notification_mode !== 'Mute' ? { ...room, notification_mode: 'Mute' } : room)); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 87 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 88 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 89 | export interface RoomChange { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 90 | roomId: string; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 91 | mode: RoomMode | null; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 92 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 93 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 94 | export interface Plan { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 95 | state: MutedSpaces; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 96 | changes: RoomChange[]; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 97 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 98 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 99 | function priorMode(room: RoomSummary): RoomMode | null { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 100 | const mode = room.notification_mode; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 101 | return room.notification_mode_custom && (mode === 'AllMessages' || mode === 'MentionsAndKeywordsOnly') ? mode : null; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 102 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 103 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 104 | function claimed(state: MutedSpaces): Set<string> { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 105 | const out = new Set<string>(); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 106 | for (const entry of Object.values(state.applied)) for (const roomId of Object.keys(entry)) out.add(roomId); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 107 | return out; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 108 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 109 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 110 | function claim(state: MutedSpaces, spaceId: string, targets: RoomSummary[]): Plan { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 111 | const skip = claimed(state); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 112 | const fresh = targets.filter((room) => room.notification_mode !== 'Mute' && room.notification_mode !== null && !skip.has(room.room_id)); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 113 | const entry = { ...state.applied[spaceId] }; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 114 | for (const room of fresh) entry[room.room_id] = priorMode(room); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 115 | return { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 116 | state: fresh.length === 0 ? state : { ...state, applied: { ...state.applied, [spaceId]: entry } }, |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 117 | changes: fresh.map((room) => ({ roomId: room.room_id, mode: 'Mute' })) |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 118 | }; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 119 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 120 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 121 | function joinedUnder(spaceId: string, rooms: RoomSummary[]): RoomSummary[] { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 122 | const ids = roomsUnderSpace(spaceId, rooms); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 123 | return rooms.filter((r) => ids.has(r.room_id) && !r.space && r.membership === 'Joined'); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 124 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 125 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 126 | export function muteSpacePlan(state: MutedSpaces, spaceId: string, rooms: RoomSummary[]): Plan { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 127 | const base = state.spaces.includes(spaceId) ? state : { ...state, spaces: [...state.spaces, spaceId] }; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 128 | return claim(base, spaceId, joinedUnder(spaceId, rooms)); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 129 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 130 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 131 | export function reconcilePlan(state: MutedSpaces, rooms: RoomSummary[]): Plan { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 132 | let current = state; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 133 | const changes: RoomChange[] = []; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 134 | for (const spaceId of state.spaces) { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 135 | const plan = claim(current, spaceId, joinedUnder(spaceId, rooms)); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 136 | current = plan.state; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 137 | changes.push(...plan.changes); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 138 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 139 | return { state: current, changes }; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 140 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 141 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 142 | export function unmuteSpacePlan(state: MutedSpaces, spaceId: string, rooms: RoomSummary[]): Plan { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 143 | const spaces = state.spaces.filter((s) => s !== spaceId); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 144 | const { [spaceId]: released = {}, ...applied } = state.applied; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 145 | const next: MutedSpaces = { spaces, applied }; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 146 | const byId = new Map(rooms.map((r) => [r.room_id, r])); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 147 | const changes: RoomChange[] = []; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 148 | for (const [roomId, prior] of Object.entries(released)) { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 149 | const room = byId.get(roomId); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 150 | if (!room || room.notification_mode !== 'Mute') continue; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 151 | const heir = spaces.find((s) => roomsUnderSpace(s, rooms).has(roomId)); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 152 | if (heir) { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 153 | next.applied = { ...next.applied, [heir]: { ...next.applied[heir], [roomId]: prior } }; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 154 | continue; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 155 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 156 | changes.push({ roomId, mode: prior }); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 157 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 158 | return { state: next, changes }; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 159 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 160 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 161 | export function mutingSpace(state: MutedSpaces, rooms: RoomSummary[], roomId: string): string | null { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 162 | return state.spaces.find((id) => roomsUnderSpace(id, rooms).has(roomId)) ?? null; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 163 | } |