atagen

malice

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

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

db4e552594cbatagen2026-10-26 04:43 UTC1
import type { MessageKind, RoomSummary, TimelineItem } from './types';
db4e552594cbatagen2026-10-26 04:43 UTC2
db4e552594cbatagen2026-10-26 04:43 UTC3
const RECENT_MS = 5 * 60 * 1000;
db4e552594cbatagen2026-10-26 04:43 UTC4
db4e552594cbatagen2026-10-26 04:43 UTC5
export interface NotificationSource {
db4e552594cbatagen2026-10-26 04:43 UTC6
  senderId: string | null;
db4e552594cbatagen2026-10-26 04:43 UTC7
  senderName: string | null;
db4e552594cbatagen2026-10-26 04:43 UTC8
  senderAvatar: string | null;
db4e552594cbatagen2026-10-26 04:43 UTC9
  kind: MessageKind | null;
db4e552594cbatagen2026-10-26 04:43 UTC10
  body: string | null;
db4e552594cbatagen2026-10-26 04:43 UTC11
  undecryptable: boolean;
db4e552594cbatagen2026-10-26 04:43 UTC12
  highlight: boolean;
db4e552594cbatagen2026-10-26 04:43 UTC13
}
db4e552594cbatagen2026-10-26 04:43 UTC14
db4e552594cbatagen2026-10-26 04:43 UTC15
export interface NotificationPlan {
db4e552594cbatagen2026-10-26 04:43 UTC16
  title: string;
db4e552594cbatagen2026-10-26 04:43 UTC17
  body: string;
db4e552594cbatagen2026-10-26 04:43 UTC18
  tag: string;
db4e552594cbatagen2026-10-26 04:43 UTC19
  renotify: boolean;
db4e552594cbatagen2026-10-26 04:43 UTC20
  roomId: string;
db4e552594cbatagen2026-10-26 04:43 UTC21
  iconMxc: string | null;
db4e552594cbatagen2026-10-26 04:43 UTC22
}
db4e552594cbatagen2026-10-26 04:43 UTC23
db4e552594cbatagen2026-10-26 04:43 UTC24
export function notifyingCount(room: RoomSummary): number {
db4e552594cbatagen2026-10-26 04:43 UTC25
  if (room.space || room.membership !== 'Joined' || room.notification_mode === 'Mute') return 0;
db4e552594cbatagen2026-10-26 04:43 UTC26
  if (room.notification_mode === 'MentionsAndKeywordsOnly') return room.unread_mentions;
db4e552594cbatagen2026-10-26 04:43 UTC27
  return room.unread_messages + room.unread_mentions;
db4e552594cbatagen2026-10-26 04:43 UTC28
}
db4e552594cbatagen2026-10-26 04:43 UTC29
db4e552594cbatagen2026-10-26 04:43 UTC30
export function badgeTotal(rooms: RoomSummary[]): number {
db4e552594cbatagen2026-10-26 04:43 UTC31
  return rooms.reduce((sum, room) => sum + notifyingCount(room), 0);
db4e552594cbatagen2026-10-26 04:43 UTC32
}
db4e552594cbatagen2026-10-26 04:43 UTC33
db4e552594cbatagen2026-10-26 04:43 UTC34
export function roomTag(roomId: string): string {
db4e552594cbatagen2026-10-26 04:43 UTC35
  return `room:${roomId}`;
db4e552594cbatagen2026-10-26 04:43 UTC36
}
db4e552594cbatagen2026-10-26 04:43 UTC37
db4e552594cbatagen2026-10-26 04:43 UTC38
export interface Arrival {
db4e552594cbatagen2026-10-26 04:43 UTC39
  mention: boolean;
db4e552594cbatagen2026-10-26 04:43 UTC40
}
db4e552594cbatagen2026-10-26 04:43 UTC41
db4e552594cbatagen2026-10-26 04:43 UTC42
export function arrivalIn(previous: RoomSummary | undefined, next: RoomSummary, now: number): Arrival | null {
db4e552594cbatagen2026-10-26 04:43 UTC43
  if (!previous || notifyingCount(next) <= notifyingCount(previous)) return null;
db4e552594cbatagen2026-10-26 04:43 UTC44
  const at = next.latest_event_timestamp_ms;
db4e552594cbatagen2026-10-26 04:43 UTC45
  if (at !== null && now - at > RECENT_MS) return null;
db4e552594cbatagen2026-10-26 04:43 UTC46
  return { mention: next.unread_mentions > previous.unread_mentions };
db4e552594cbatagen2026-10-26 04:43 UTC47
}
db4e552594cbatagen2026-10-26 04:43 UTC48
db4e552594cbatagen2026-10-26 04:43 UTC49
export function isRoomWatched(roomId: string, openRoomId: string | null, visible: boolean, focused: boolean): boolean {
db4e552594cbatagen2026-10-26 04:43 UTC50
  return visible && focused && roomId === openRoomId;
db4e552594cbatagen2026-10-26 04:43 UTC51
}
db4e552594cbatagen2026-10-26 04:43 UTC52
db4e552594cbatagen2026-10-26 04:43 UTC53
export function sourceFromTimeline(items: TimelineItem[] | undefined): NotificationSource | null {
db4e552594cbatagen2026-10-26 04:43 UTC54
  if (!items) return null;
db4e552594cbatagen2026-10-26 04:43 UTC55
  for (let i = items.length - 1; i >= 0; i--) {
db4e552594cbatagen2026-10-26 04:43 UTC56
    const item = items[i];
db4e552594cbatagen2026-10-26 04:43 UTC57
    if (item.kind !== 'event') continue;
db4e552594cbatagen2026-10-26 04:43 UTC58
    const { event } = item;
db4e552594cbatagen2026-10-26 04:43 UTC59
    if (event.own) return null;
db4e552594cbatagen2026-10-26 04:43 UTC60
    const content = event.content;
db4e552594cbatagen2026-10-26 04:43 UTC61
    const common = {
db4e552594cbatagen2026-10-26 04:43 UTC62
      senderId: event.sender.user_id,
db4e552594cbatagen2026-10-26 04:43 UTC63
      senderName: event.sender.display_name,
db4e552594cbatagen2026-10-26 04:43 UTC64
      senderAvatar: event.sender.avatar
db4e552594cbatagen2026-10-26 04:43 UTC65
    };
db4e552594cbatagen2026-10-26 04:43 UTC66
    if (content.kind === 'Message') {
db4e552594cbatagen2026-10-26 04:43 UTC67
      return { ...common, kind: content.message.kind, body: content.message.body, undecryptable: false, highlight: event.highlight === true };
db4e552594cbatagen2026-10-26 04:43 UTC68
    }
db4e552594cbatagen2026-10-26 04:43 UTC69
    if (content.kind === 'Sticker') return { ...common, kind: null, body: content.body, undecryptable: false, highlight: event.highlight === true };
db4e552594cbatagen2026-10-26 04:43 UTC70
    if (content.kind === 'UnableToDecrypt') return { ...common, kind: null, body: null, undecryptable: true, highlight: false };
db4e552594cbatagen2026-10-26 04:43 UTC71
  }
db4e552594cbatagen2026-10-26 04:43 UTC72
  return null;
db4e552594cbatagen2026-10-26 04:43 UTC73
}
db4e552594cbatagen2026-10-26 04:43 UTC74
db4e552594cbatagen2026-10-26 04:43 UTC75
export function sourceFromSummary(room: RoomSummary): NotificationSource | null {
db4e552594cbatagen2026-10-26 04:43 UTC76
  const latest = room.latest_event;
db4e552594cbatagen2026-10-26 04:43 UTC77
  if (!latest || latest.send_state.kind !== 'Sent') return null;
db4e552594cbatagen2026-10-26 04:43 UTC78
  const hero = room.heroes.find((h) => h.user_id === latest.sender);
db4e552594cbatagen2026-10-26 04:43 UTC79
  return {
db4e552594cbatagen2026-10-26 04:43 UTC80
    senderId: latest.sender,
db4e552594cbatagen2026-10-26 04:43 UTC81
    senderName: hero?.display_name ?? null,
db4e552594cbatagen2026-10-26 04:43 UTC82
    senderAvatar: hero?.avatar ?? null,
db4e552594cbatagen2026-10-26 04:43 UTC83
    kind: latest.kind,
db4e552594cbatagen2026-10-26 04:43 UTC84
    body: latest.body,
db4e552594cbatagen2026-10-26 04:43 UTC85
    undecryptable: false,
db4e552594cbatagen2026-10-26 04:43 UTC86
    highlight: false
db4e552594cbatagen2026-10-26 04:43 UTC87
  };
db4e552594cbatagen2026-10-26 04:43 UTC88
}
db4e552594cbatagen2026-10-26 04:43 UTC89
db4e552594cbatagen2026-10-26 04:43 UTC90
const MEDIA_LABEL: Partial<Record<string, string>> = {
db4e552594cbatagen2026-10-26 04:43 UTC91
  Image: 'Sent an image',
db4e552594cbatagen2026-10-26 04:43 UTC92
  Video: 'Sent a video',
db4e552594cbatagen2026-10-26 04:43 UTC93
  Audio: 'Sent an audio message',
db4e552594cbatagen2026-10-26 04:43 UTC94
  File: 'Sent a file',
db4e552594cbatagen2026-10-26 04:43 UTC95
  Location: 'Shared a location'
db4e552594cbatagen2026-10-26 04:43 UTC96
};
db4e552594cbatagen2026-10-26 04:43 UTC97
db4e552594cbatagen2026-10-26 04:43 UTC98
function previewText(source: NotificationSource, sender: string): string | null {
db4e552594cbatagen2026-10-26 04:43 UTC99
  if (source.undecryptable) return null;
db4e552594cbatagen2026-10-26 04:43 UTC100
  const kind = typeof source.kind === 'string' ? source.kind : null;
db4e552594cbatagen2026-10-26 04:43 UTC101
  const label = kind ? MEDIA_LABEL[kind] : undefined;
db4e552594cbatagen2026-10-26 04:43 UTC102
  const text = source.body?.trim();
db4e552594cbatagen2026-10-26 04:43 UTC103
  if (label) return label;
db4e552594cbatagen2026-10-26 04:43 UTC104
  if (!text) return null;
db4e552594cbatagen2026-10-26 04:43 UTC105
  return kind === 'Emote' ? `${sender} ${text}` : text;
db4e552594cbatagen2026-10-26 04:43 UTC106
}
db4e552594cbatagen2026-10-26 04:43 UTC107
db4e552594cbatagen2026-10-26 04:43 UTC108
function localPart(userId: string): string {
db4e552594cbatagen2026-10-26 04:43 UTC109
  const name = userId.startsWith('@') ? userId.slice(1) : userId;
db4e552594cbatagen2026-10-26 04:43 UTC110
  const colon = name.indexOf(':');
db4e552594cbatagen2026-10-26 04:43 UTC111
  return colon > 0 ? name.slice(0, colon) : name;
db4e552594cbatagen2026-10-26 04:43 UTC112
}
db4e552594cbatagen2026-10-26 04:43 UTC113
db4e552594cbatagen2026-10-26 04:43 UTC114
export function planNotification(
db4e552594cbatagen2026-10-26 04:43 UTC115
  room: RoomSummary,
db4e552594cbatagen2026-10-26 04:43 UTC116
  source: NotificationSource | null,
db4e552594cbatagen2026-10-26 04:43 UTC117
  arrival: Arrival
db4e552594cbatagen2026-10-26 04:43 UTC118
): NotificationPlan {
db4e552594cbatagen2026-10-26 04:43 UTC119
  const sender = source?.senderName ?? (source?.senderId ? localPart(source.senderId) : null);
db4e552594cbatagen2026-10-26 04:43 UTC120
  const text = source && sender ? previewText(source, sender) : null;
db4e552594cbatagen2026-10-26 04:43 UTC121
  const generic = room.direct ? 'New message' : 'New messages';
db4e552594cbatagen2026-10-26 04:43 UTC122
  let body: string;
db4e552594cbatagen2026-10-26 04:43 UTC123
  if (!text) body = sender && !room.direct ? `${sender}: ${generic}` : generic;
db4e552594cbatagen2026-10-26 04:43 UTC124
  else body = room.direct || !sender || source?.kind === 'Emote' ? text : `${sender}: ${text}`;
db4e552594cbatagen2026-10-26 04:43 UTC125
  return {
db4e552594cbatagen2026-10-26 04:43 UTC126
    title: room.direct && sender ? sender : room.name,
db4e552594cbatagen2026-10-26 04:43 UTC127
    body,
db4e552594cbatagen2026-10-26 04:43 UTC128
    tag: roomTag(room.room_id),
db4e552594cbatagen2026-10-26 04:43 UTC129
    renotify: arrival.mention || source?.highlight === true,
db4e552594cbatagen2026-10-26 04:43 UTC130
    roomId: room.room_id,
db4e552594cbatagen2026-10-26 04:43 UTC131
    iconMxc: (room.direct ? source?.senderAvatar : null) ?? room.avatar
db4e552594cbatagen2026-10-26 04:43 UTC132
  };
db4e552594cbatagen2026-10-26 04:43 UTC133
}