0a8544b6fc
Generic notification engine: reacts to message.sent and runs three gates before delivering — policy (DM always; group only on @mention or reply-to-you), presence (skip if the recipient is focused on that thread), mute (per-thread). Delivery via a swappable NotificationPort (Web Push/VAPID adapter); a 'gone' (404/410) prunes the sub. - PresenceService + gateway `focus_thread` signal + disconnect cleanup (room membership != viewing, since the sidebar joins every thread room). - IiosNotificationSubscription table + `muted` on IiosThreadParticipant (migration). - Endpoints: GET vapid-public-key, POST/DELETE subscribe, POST threads/:id/mute|unmute; listThreads returns the caller's `muted`. - DM-vs-group read from the opaque `membership` attribute in the notification POLICY only — kernel stays generic (grep-verified). Tests: 13 new (3 gates + reply-to-you + prune + web-push adapter states); full suite 205 green. Verified live: module boots, subscribe stored, mute/unmute round-trips. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
26 lines
933 B
TypeScript
26 lines
933 B
TypeScript
import { Injectable } from '@nestjs/common';
|
|
|
|
/**
|
|
* In-memory focus tracker (single instance). Keyed on userId (the stable externalId the
|
|
* gateway has as principal.userId). NOTE: room membership ≠ viewing — the sidebar joins
|
|
* every thread room for live updates, so presence uses an explicit `focus_thread` signal.
|
|
* Prod (multi-replica): back this with Redis.
|
|
*/
|
|
@Injectable()
|
|
export class PresenceService {
|
|
private readonly focus = new Map<string, { userId: string; threadId: string | null }>(); // socketId → focus
|
|
|
|
setFocus(socketId: string, userId: string, threadId: string | null): void {
|
|
this.focus.set(socketId, { userId, threadId });
|
|
}
|
|
|
|
clearSocket(socketId: string): void {
|
|
this.focus.delete(socketId);
|
|
}
|
|
|
|
isViewing(userId: string, threadId: string): boolean {
|
|
for (const f of this.focus.values()) if (f.userId === userId && f.threadId === threadId) return true;
|
|
return false;
|
|
}
|
|
}
|