db4e552594cb | atagen | 2026-10-26 04:43 UTC | 1 | import type { MessageKind, RoomSummary, TimelineItem } from './types'; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 2 | |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 3 | const RECENT_MS = 5 * 60 * 1000; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 4 | |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 5 | export interface NotificationSource { |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 6 | senderId: string | null; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 7 | senderName: string | null; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 8 | senderAvatar: string | null; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 9 | kind: MessageKind | null; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 10 | body: string | null; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 11 | undecryptable: boolean; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 12 | highlight: boolean; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 13 | } |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 14 | |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 15 | export interface NotificationPlan { |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 16 | title: string; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 17 | body: string; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 18 | tag: string; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 19 | renotify: boolean; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 20 | roomId: string; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 21 | iconMxc: string | null; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 22 | } |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 23 | |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 24 | export function notifyingCount(room: RoomSummary): number { |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 25 | if (room.space || room.membership !== 'Joined' || room.notification_mode === 'Mute') return 0; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 26 | if (room.notification_mode === 'MentionsAndKeywordsOnly') return room.unread_mentions; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 27 | return room.unread_messages + room.unread_mentions; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 28 | } |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 29 | |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 30 | export function badgeTotal(rooms: RoomSummary[]): number { |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 31 | return rooms.reduce((sum, room) => sum + notifyingCount(room), 0); |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 32 | } |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 33 | |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 34 | export function roomTag(roomId: string): string { |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 35 | return `room:${roomId}`; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 36 | } |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 37 | |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 38 | export interface Arrival { |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 39 | mention: boolean; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 40 | } |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 41 | |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 42 | export function arrivalIn(previous: RoomSummary | undefined, next: RoomSummary, now: number): Arrival | null { |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 43 | if (!previous || notifyingCount(next) <= notifyingCount(previous)) return null; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 44 | const at = next.latest_event_timestamp_ms; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 45 | if (at !== null && now - at > RECENT_MS) return null; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 46 | return { mention: next.unread_mentions > previous.unread_mentions }; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 47 | } |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 48 | |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 49 | export function isRoomWatched(roomId: string, openRoomId: string | null, visible: boolean, focused: boolean): boolean { |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 50 | return visible && focused && roomId === openRoomId; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 51 | } |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 52 | |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 53 | export function sourceFromTimeline(items: TimelineItem[] | undefined): NotificationSource | null { |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 54 | if (!items) return null; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 55 | for (let i = items.length - 1; i >= 0; i--) { |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 56 | const item = items[i]; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 57 | if (item.kind !== 'event') continue; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 58 | const { event } = item; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 59 | if (event.own) return null; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 60 | const content = event.content; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 61 | const common = { |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 62 | senderId: event.sender.user_id, |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 63 | senderName: event.sender.display_name, |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 64 | senderAvatar: event.sender.avatar |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 65 | }; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 66 | if (content.kind === 'Message') { |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 67 | return { ...common, kind: content.message.kind, body: content.message.body, undecryptable: false, highlight: event.highlight === true }; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 68 | } |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 69 | if (content.kind === 'Sticker') return { ...common, kind: null, body: content.body, undecryptable: false, highlight: event.highlight === true }; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 70 | if (content.kind === 'UnableToDecrypt') return { ...common, kind: null, body: null, undecryptable: true, highlight: false }; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 71 | } |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 72 | return null; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 73 | } |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 74 | |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 75 | export function sourceFromSummary(room: RoomSummary): NotificationSource | null { |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 76 | const latest = room.latest_event; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 77 | if (!latest || latest.send_state.kind !== 'Sent') return null; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 78 | const hero = room.heroes.find((h) => h.user_id === latest.sender); |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 79 | return { |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 80 | senderId: latest.sender, |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 81 | senderName: hero?.display_name ?? null, |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 82 | senderAvatar: hero?.avatar ?? null, |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 83 | kind: latest.kind, |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 84 | body: latest.body, |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 85 | undecryptable: false, |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 86 | highlight: false |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 87 | }; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 88 | } |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 89 | |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 90 | const MEDIA_LABEL: Partial<Record<string, string>> = { |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 91 | Image: 'Sent an image', |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 92 | Video: 'Sent a video', |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 93 | Audio: 'Sent an audio message', |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 94 | File: 'Sent a file', |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 95 | Location: 'Shared a location' |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 96 | }; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 97 | |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 98 | function previewText(source: NotificationSource, sender: string): string | null { |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 99 | if (source.undecryptable) return null; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 100 | const kind = typeof source.kind === 'string' ? source.kind : null; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 101 | const label = kind ? MEDIA_LABEL[kind] : undefined; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 102 | const text = source.body?.trim(); |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 103 | if (label) return label; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 104 | if (!text) return null; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 105 | return kind === 'Emote' ? `${sender} ${text}` : text; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 106 | } |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 107 | |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 108 | function localPart(userId: string): string { |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 109 | const name = userId.startsWith('@') ? userId.slice(1) : userId; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 110 | const colon = name.indexOf(':'); |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 111 | return colon > 0 ? name.slice(0, colon) : name; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 112 | } |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 113 | |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 114 | export function planNotification( |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 115 | room: RoomSummary, |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 116 | source: NotificationSource | null, |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 117 | arrival: Arrival |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 118 | ): NotificationPlan { |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 119 | const sender = source?.senderName ?? (source?.senderId ? localPart(source.senderId) : null); |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 120 | const text = source && sender ? previewText(source, sender) : null; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 121 | const generic = room.direct ? 'New message' : 'New messages'; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 122 | let body: string; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 123 | if (!text) body = sender && !room.direct ? `${sender}: ${generic}` : generic; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 124 | else body = room.direct || !sender || source?.kind === 'Emote' ? text : `${sender}: ${text}`; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 125 | return { |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 126 | title: room.direct && sender ? sender : room.name, |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 127 | body, |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 128 | tag: roomTag(room.room_id), |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 129 | renotify: arrival.mention || source?.highlight === true, |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 130 | roomId: room.room_id, |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 131 | iconMxc: (room.direct ? source?.senderAvatar : null) ?? room.avatar |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 132 | }; |
db4e552594cb | atagen | 2026-10-26 04:43 UTC | 133 | } |