atagen

malice

public
Star0
Watch0
4ac384dcce6c4c0b2d9806e2fa8512728546430d
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/src/lib/mute-model.ts

00ca0561ecedatagen2026-10-26 05:50 UTC1
import type { RoomSummary } from './types';
00ca0561ecedatagen2026-10-26 05:50 UTC2
00ca0561ecedatagen2026-10-26 05:50 UTC3
export type RoomMode = 'AllMessages' | 'MentionsAndKeywordsOnly' | 'Mute';
00ca0561ecedatagen2026-10-26 05:50 UTC4
00ca0561ecedatagen2026-10-26 05:50 UTC5
export interface MutedSpaces {
00ca0561ecedatagen2026-10-26 05:50 UTC6
  spaces: string[];
00ca0561ecedatagen2026-10-26 05:50 UTC7
  applied: Record<string, Record<string, RoomMode | null>>;
00ca0561ecedatagen2026-10-26 05:50 UTC8
}
00ca0561ecedatagen2026-10-26 05:50 UTC9
00ca0561ecedatagen2026-10-26 05:50 UTC10
export const NO_MUTED_SPACES: MutedSpaces = { spaces: [], applied: {} };
00ca0561ecedatagen2026-10-26 05:50 UTC11
00ca0561ecedatagen2026-10-26 05:50 UTC12
function isRecord(value: unknown): value is Record<string, unknown> {
00ca0561ecedatagen2026-10-26 05:50 UTC13
  return typeof value === 'object' && value !== null && !Array.isArray(value);
00ca0561ecedatagen2026-10-26 05:50 UTC14
}
00ca0561ecedatagen2026-10-26 05:50 UTC15
00ca0561ecedatagen2026-10-26 05:50 UTC16
function isMode(value: unknown): value is RoomMode {
00ca0561ecedatagen2026-10-26 05:50 UTC17
  return value === 'AllMessages' || value === 'MentionsAndKeywordsOnly' || value === 'Mute';
00ca0561ecedatagen2026-10-26 05:50 UTC18
}
00ca0561ecedatagen2026-10-26 05:50 UTC19
00ca0561ecedatagen2026-10-26 05:50 UTC20
export function parseMutedSpaces(raw: unknown): MutedSpaces {
00ca0561ecedatagen2026-10-26 05:50 UTC21
  if (!isRecord(raw)) return NO_MUTED_SPACES;
00ca0561ecedatagen2026-10-26 05:50 UTC22
  const spaces = Array.isArray(raw['spaces']) ? [...new Set(raw['spaces'].filter((s): s is string => typeof s === 'string'))] : [];
00ca0561ecedatagen2026-10-26 05:50 UTC23
  const applied: MutedSpaces['applied'] = {};
00ca0561ecedatagen2026-10-26 05:50 UTC24
  if (isRecord(raw['applied'])) {
00ca0561ecedatagen2026-10-26 05:50 UTC25
    for (const [spaceId, rooms] of Object.entries(raw['applied'])) {
00ca0561ecedatagen2026-10-26 05:50 UTC26
      if (!spaces.includes(spaceId) || !isRecord(rooms)) continue;
00ca0561ecedatagen2026-10-26 05:50 UTC27
      const entry: Record<string, RoomMode | null> = {};
00ca0561ecedatagen2026-10-26 05:50 UTC28
      for (const [roomId, mode] of Object.entries(rooms)) if (mode === null || isMode(mode)) entry[roomId] = mode;
00ca0561ecedatagen2026-10-26 05:50 UTC29
      applied[spaceId] = entry;
00ca0561ecedatagen2026-10-26 05:50 UTC30
    }
00ca0561ecedatagen2026-10-26 05:50 UTC31
  }
00ca0561ecedatagen2026-10-26 05:50 UTC32
  return { spaces, applied };
00ca0561ecedatagen2026-10-26 05:50 UTC33
}
00ca0561ecedatagen2026-10-26 05:50 UTC34
00ca0561ecedatagen2026-10-26 05:50 UTC35
function childMap(rooms: RoomSummary[]): Map<string, Set<string>> {
00ca0561ecedatagen2026-10-26 05:50 UTC36
  const map = new Map<string, Set<string>>();
00ca0561ecedatagen2026-10-26 05:50 UTC37
  const add = (space: string, child: string) => {
00ca0561ecedatagen2026-10-26 05:50 UTC38
    let set = map.get(space);
00ca0561ecedatagen2026-10-26 05:50 UTC39
    if (!set) map.set(space, (set = new Set()));
00ca0561ecedatagen2026-10-26 05:50 UTC40
    set.add(child);
00ca0561ecedatagen2026-10-26 05:50 UTC41
  };
00ca0561ecedatagen2026-10-26 05:50 UTC42
  for (const room of rooms) {
00ca0561ecedatagen2026-10-26 05:50 UTC43
    for (const child of room.children) add(room.room_id, child);
00ca0561ecedatagen2026-10-26 05:50 UTC44
    for (const parent of room.parent_spaces) add(parent, room.room_id);
00ca0561ecedatagen2026-10-26 05:50 UTC45
  }
00ca0561ecedatagen2026-10-26 05:50 UTC46
  return map;
00ca0561ecedatagen2026-10-26 05:50 UTC47
}
00ca0561ecedatagen2026-10-26 05:50 UTC48
00ca0561ecedatagen2026-10-26 05:50 UTC49
function descendants(spaceId: string, children: Map<string, Set<string>>, spaceIds: Set<string>): Set<string> {
00ca0561ecedatagen2026-10-26 05:50 UTC50
  const out = new Set<string>();
00ca0561ecedatagen2026-10-26 05:50 UTC51
  const seen = new Set<string>([spaceId]);
00ca0561ecedatagen2026-10-26 05:50 UTC52
  const queue = [spaceId];
00ca0561ecedatagen2026-10-26 05:50 UTC53
  for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
00ca0561ecedatagen2026-10-26 05:50 UTC54
    for (const child of children.get(at) ?? []) {
00ca0561ecedatagen2026-10-26 05:50 UTC55
      if (spaceIds.has(child)) {
00ca0561ecedatagen2026-10-26 05:50 UTC56
        if (!seen.has(child)) {
00ca0561ecedatagen2026-10-26 05:50 UTC57
          seen.add(child);
00ca0561ecedatagen2026-10-26 05:50 UTC58
          queue.push(child);
00ca0561ecedatagen2026-10-26 05:50 UTC59
        }
00ca0561ecedatagen2026-10-26 05:50 UTC60
      } else out.add(child);
00ca0561ecedatagen2026-10-26 05:50 UTC61
    }
00ca0561ecedatagen2026-10-26 05:50 UTC62
  }
00ca0561ecedatagen2026-10-26 05:50 UTC63
  return out;
00ca0561ecedatagen2026-10-26 05:50 UTC64
}
00ca0561ecedatagen2026-10-26 05:50 UTC65
00ca0561ecedatagen2026-10-26 05:50 UTC66
export function roomsUnderSpace(spaceId: string, rooms: RoomSummary[]): Set<string> {
00ca0561ecedatagen2026-10-26 05:50 UTC67
  const spaceIds = new Set(rooms.filter((r) => r.space).map((r) => r.room_id));
00ca0561ecedatagen2026-10-26 05:50 UTC68
  return descendants(spaceId, childMap(rooms), spaceIds);
00ca0561ecedatagen2026-10-26 05:50 UTC69
}
00ca0561ecedatagen2026-10-26 05:50 UTC70
00ca0561ecedatagen2026-10-26 05:50 UTC71
export function spaceMutedRoomIds(rooms: RoomSummary[], mutedSpaces: string[]): Set<string> {
00ca0561ecedatagen2026-10-26 05:50 UTC72
  const out = new Set<string>();
00ca0561ecedatagen2026-10-26 05:50 UTC73
  if (mutedSpaces.length === 0) return out;
00ca0561ecedatagen2026-10-26 05:50 UTC74
  const children = childMap(rooms);
00ca0561ecedatagen2026-10-26 05:50 UTC75
  const spaceIds = new Set(rooms.filter((r) => r.space).map((r) => r.room_id));
00ca0561ecedatagen2026-10-26 05:50 UTC76
  for (const id of mutedSpaces) for (const room of descendants(id, children, spaceIds)) out.add(room);
00ca0561ecedatagen2026-10-26 05:50 UTC77
  return out;
00ca0561ecedatagen2026-10-26 05:50 UTC78
}
00ca0561ecedatagen2026-10-26 05:50 UTC79
00ca0561ecedatagen2026-10-26 05:50 UTC80
export function isRoomMuted(room: RoomSummary, spaceMuted: Set<string>): boolean {
00ca0561ecedatagen2026-10-26 05:50 UTC81
  return room.notification_mode === 'Mute' || spaceMuted.has(room.room_id);
00ca0561ecedatagen2026-10-26 05:50 UTC82
}
00ca0561ecedatagen2026-10-26 05:50 UTC83
00ca0561ecedatagen2026-10-26 05:50 UTC84
export function withSpaceMutes(rooms: RoomSummary[], spaceMuted: Set<string>): RoomSummary[] {
00ca0561ecedatagen2026-10-26 05:50 UTC85
  if (spaceMuted.size === 0) return rooms;
00ca0561ecedatagen2026-10-26 05:50 UTC86
  return rooms.map((room) => (spaceMuted.has(room.room_id) && room.notification_mode !== 'Mute' ? { ...room, notification_mode: 'Mute' } : room));
00ca0561ecedatagen2026-10-26 05:50 UTC87
}
00ca0561ecedatagen2026-10-26 05:50 UTC88
00ca0561ecedatagen2026-10-26 05:50 UTC89
export interface RoomChange {
00ca0561ecedatagen2026-10-26 05:50 UTC90
  roomId: string;
00ca0561ecedatagen2026-10-26 05:50 UTC91
  mode: RoomMode | null;
00ca0561ecedatagen2026-10-26 05:50 UTC92
}
00ca0561ecedatagen2026-10-26 05:50 UTC93
00ca0561ecedatagen2026-10-26 05:50 UTC94
export interface Plan {
00ca0561ecedatagen2026-10-26 05:50 UTC95
  state: MutedSpaces;
00ca0561ecedatagen2026-10-26 05:50 UTC96
  changes: RoomChange[];
00ca0561ecedatagen2026-10-26 05:50 UTC97
}
00ca0561ecedatagen2026-10-26 05:50 UTC98
00ca0561ecedatagen2026-10-26 05:50 UTC99
function priorMode(room: RoomSummary): RoomMode | null {
00ca0561ecedatagen2026-10-26 05:50 UTC100
  const mode = room.notification_mode;
00ca0561ecedatagen2026-10-26 05:50 UTC101
  return room.notification_mode_custom && (mode === 'AllMessages' || mode === 'MentionsAndKeywordsOnly') ? mode : null;
00ca0561ecedatagen2026-10-26 05:50 UTC102
}
00ca0561ecedatagen2026-10-26 05:50 UTC103
00ca0561ecedatagen2026-10-26 05:50 UTC104
function claimed(state: MutedSpaces): Set<string> {
00ca0561ecedatagen2026-10-26 05:50 UTC105
  const out = new Set<string>();
00ca0561ecedatagen2026-10-26 05:50 UTC106
  for (const entry of Object.values(state.applied)) for (const roomId of Object.keys(entry)) out.add(roomId);
00ca0561ecedatagen2026-10-26 05:50 UTC107
  return out;
00ca0561ecedatagen2026-10-26 05:50 UTC108
}
00ca0561ecedatagen2026-10-26 05:50 UTC109
00ca0561ecedatagen2026-10-26 05:50 UTC110
function claim(state: MutedSpaces, spaceId: string, targets: RoomSummary[]): Plan {
00ca0561ecedatagen2026-10-26 05:50 UTC111
  const skip = claimed(state);
00ca0561ecedatagen2026-10-26 05:50 UTC112
  const fresh = targets.filter((room) => room.notification_mode !== 'Mute' && room.notification_mode !== null && !skip.has(room.room_id));
00ca0561ecedatagen2026-10-26 05:50 UTC113
  const entry = { ...state.applied[spaceId] };
00ca0561ecedatagen2026-10-26 05:50 UTC114
  for (const room of fresh) entry[room.room_id] = priorMode(room);
00ca0561ecedatagen2026-10-26 05:50 UTC115
  return {
00ca0561ecedatagen2026-10-26 05:50 UTC116
    state: fresh.length === 0 ? state : { ...state, applied: { ...state.applied, [spaceId]: entry } },
00ca0561ecedatagen2026-10-26 05:50 UTC117
    changes: fresh.map((room) => ({ roomId: room.room_id, mode: 'Mute' }))
00ca0561ecedatagen2026-10-26 05:50 UTC118
  };
00ca0561ecedatagen2026-10-26 05:50 UTC119
}
00ca0561ecedatagen2026-10-26 05:50 UTC120
00ca0561ecedatagen2026-10-26 05:50 UTC121
function joinedUnder(spaceId: string, rooms: RoomSummary[]): RoomSummary[] {
00ca0561ecedatagen2026-10-26 05:50 UTC122
  const ids = roomsUnderSpace(spaceId, rooms);
00ca0561ecedatagen2026-10-26 05:50 UTC123
  return rooms.filter((r) => ids.has(r.room_id) && !r.space && r.membership === 'Joined');
00ca0561ecedatagen2026-10-26 05:50 UTC124
}
00ca0561ecedatagen2026-10-26 05:50 UTC125
00ca0561ecedatagen2026-10-26 05:50 UTC126
export function muteSpacePlan(state: MutedSpaces, spaceId: string, rooms: RoomSummary[]): Plan {
00ca0561ecedatagen2026-10-26 05:50 UTC127
  const base = state.spaces.includes(spaceId) ? state : { ...state, spaces: [...state.spaces, spaceId] };
00ca0561ecedatagen2026-10-26 05:50 UTC128
  return claim(base, spaceId, joinedUnder(spaceId, rooms));
00ca0561ecedatagen2026-10-26 05:50 UTC129
}
00ca0561ecedatagen2026-10-26 05:50 UTC130
00ca0561ecedatagen2026-10-26 05:50 UTC131
export function reconcilePlan(state: MutedSpaces, rooms: RoomSummary[]): Plan {
00ca0561ecedatagen2026-10-26 05:50 UTC132
  let current = state;
00ca0561ecedatagen2026-10-26 05:50 UTC133
  const changes: RoomChange[] = [];
00ca0561ecedatagen2026-10-26 05:50 UTC134
  for (const spaceId of state.spaces) {
00ca0561ecedatagen2026-10-26 05:50 UTC135
    const plan = claim(current, spaceId, joinedUnder(spaceId, rooms));
00ca0561ecedatagen2026-10-26 05:50 UTC136
    current = plan.state;
00ca0561ecedatagen2026-10-26 05:50 UTC137
    changes.push(...plan.changes);
00ca0561ecedatagen2026-10-26 05:50 UTC138
  }
00ca0561ecedatagen2026-10-26 05:50 UTC139
  return { state: current, changes };
00ca0561ecedatagen2026-10-26 05:50 UTC140
}
00ca0561ecedatagen2026-10-26 05:50 UTC141
00ca0561ecedatagen2026-10-26 05:50 UTC142
export function unmuteSpacePlan(state: MutedSpaces, spaceId: string, rooms: RoomSummary[]): Plan {
00ca0561ecedatagen2026-10-26 05:50 UTC143
  const spaces = state.spaces.filter((s) => s !== spaceId);
00ca0561ecedatagen2026-10-26 05:50 UTC144
  const { [spaceId]: released = {}, ...applied } = state.applied;
00ca0561ecedatagen2026-10-26 05:50 UTC145
  const next: MutedSpaces = { spaces, applied };
00ca0561ecedatagen2026-10-26 05:50 UTC146
  const byId = new Map(rooms.map((r) => [r.room_id, r]));
00ca0561ecedatagen2026-10-26 05:50 UTC147
  const changes: RoomChange[] = [];
00ca0561ecedatagen2026-10-26 05:50 UTC148
  for (const [roomId, prior] of Object.entries(released)) {
00ca0561ecedatagen2026-10-26 05:50 UTC149
    const room = byId.get(roomId);
00ca0561ecedatagen2026-10-26 05:50 UTC150
    if (!room || room.notification_mode !== 'Mute') continue;
00ca0561ecedatagen2026-10-26 05:50 UTC151
    const heir = spaces.find((s) => roomsUnderSpace(s, rooms).has(roomId));
00ca0561ecedatagen2026-10-26 05:50 UTC152
    if (heir) {
00ca0561ecedatagen2026-10-26 05:50 UTC153
      next.applied = { ...next.applied, [heir]: { ...next.applied[heir], [roomId]: prior } };
00ca0561ecedatagen2026-10-26 05:50 UTC154
      continue;
00ca0561ecedatagen2026-10-26 05:50 UTC155
    }
00ca0561ecedatagen2026-10-26 05:50 UTC156
    changes.push({ roomId, mode: prior });
00ca0561ecedatagen2026-10-26 05:50 UTC157
  }
00ca0561ecedatagen2026-10-26 05:50 UTC158
  return { state: next, changes };
00ca0561ecedatagen2026-10-26 05:50 UTC159
}
00ca0561ecedatagen2026-10-26 05:50 UTC160
00ca0561ecedatagen2026-10-26 05:50 UTC161
export function mutingSpace(state: MutedSpaces, rooms: RoomSummary[], roomId: string): string | null {
00ca0561ecedatagen2026-10-26 05:50 UTC162
  return state.spaces.find((id) => roomsUnderSpace(id, rooms).has(roomId)) ?? null;
00ca0561ecedatagen2026-10-26 05:50 UTC163
}