Repository Public
1/**2 * In-app notifications. Bodies are generic by default and never contain terminal content.3 * Web Push is NOT implemented in this milestone (the `webPush` setting is only stored).4 */5import type { Notification } from '@cr/shared';6import type { Db } from '../../db.js';7import type { EventHub } from '../../events-hub.js';8import { getSettings } from '../settings/store.js';910export interface NotificationRow {11 id: string;12 kind: string;13 title: string;14 body: string;15 link: string | null;16 read_at: Date | null;17 created_at: Date;18}19export const NOTIF_COLS = 'id, kind, title, body, link, read_at, created_at';20export const notificationDto = (r: NotificationRow): Notification => ({21 id: r.id,22 kind: r.kind as Notification['kind'],23 title: r.title,24 body: r.body,25 link: r.link,26 readAt: r.read_at?.toISOString() ?? null,27 createdAt: r.created_at.toISOString(),28});2930export class Notifier {31 constructor(32 private readonly db: Db,33 private readonly hub: EventHub,34 ) {}3536 /** `subject` (e.g. a session/task title) is included only when the user chose detail = 'titles'. */37 async notify(userId: string, kind: Notification['kind'], generic: string, subject: string | null, link: string | null): Promise<void> {38 const settings = await getSettings(this.db, userId);39 if (!settings.notifications.inApp) return;40 const body = settings.notifications.detail === 'titles' && subject ? subject.slice(0, 200) : '';41 const row = await this.db.one<NotificationRow>(42 `INSERT INTO notifications (user_id, kind, title, body, link) VALUES ($1, $2, $3, $4, $5) RETURNING ${NOTIF_COLS}`,43 [userId, kind, generic, body, link],44 );45 if (row) this.hub.publish(userId, { type: 'notification', notification: notificationDto(row) });46 }47}48