Messenger/packages/web/public/sw.js
Заид Омар Медхат | Zaid Omar Medhat 47fbf861ee Phase 6.5: notifications service + in-app toasts; in-app voice/video messages; SVG icons
Notifications (web push now, Android FCM ready, iOS prepared):
- device_tokens / notification_settings / conversation_mutes (migration 006)
- REST: register/list/unregister devices, get/update settings (quiet hours,
  timezone, enable), mute/unmute conversation, VAPID public key
- BullMQ queue: backend enqueues a job per new message; a dedicated stateless
  `notifications` worker service drains it
- Delivery gating: skip sender, muted chats, disabled users, quiet hours, and
  ONLINE users (they get the message live + an in-app toast) — offline → push
- Providers behind one interface: Web Push (VAPID/web-push) + FCM (firebase-admin,
  HTTP v1; Android + iOS via the same path). Dead tokens (404/410/unregistered)
  auto-retired; transient failures recorded
- Web: service worker (push display + tap-to-open the originating chat), push
  registration/unregistration, in-app toast stack, deep-link via ?conversation=

In-app voice & video messages: MediaRecorder capture in the composer (mic/video),
live preview + timer, upload via the existing presigned-media path.

No emojis anywhere: replaced all glyphs with inline SVG icons (shared/ui) so the
UI renders identically across web/desktop/mobile; notification text is plain.

Chores: dedupe ioredis (BullMQ) + pin uuid>=11.1.1 (audit clean) via pnpm overrides;
reusable Centrifugo client factory for the worker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFRyKxkKEjfAgpXygzoNiD
2026-07-10 19:05:39 +05:00

48 lines
1.6 KiB
JavaScript

/* Altricade service worker — Web Push display + Telegram-style tap-to-open.
Plain JS (runs in the SW global scope, outside the TS/React build). */
self.addEventListener('push', (event) => {
let payload = {};
try {
payload = event.data ? event.data.json() : {};
} catch {
payload = {};
}
const title = typeof payload.title === 'string' ? payload.title : 'New message';
const body = typeof payload.body === 'string' ? payload.body : '';
const conversationId = typeof payload.conversationId === 'string' ? payload.conversationId : '';
event.waitUntil(
self.registration.showNotification(title, {
body,
// Collapse multiple messages from the same chat into one notification.
tag: conversationId || undefined,
renotify: Boolean(conversationId),
data: { conversationId },
}),
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const data = event.notification.data || {};
const conversationId = typeof data.conversationId === 'string' ? data.conversationId : '';
const url = conversationId ? `/?conversation=${conversationId}` : '/';
event.waitUntil(
(async () => {
const clientList = await self.clients.matchAll({
type: 'window',
includeUncontrolled: true,
});
for (const client of clientList) {
if ('focus' in client) {
await client.focus();
client.postMessage({ type: 'notification.open', conversationId });
return;
}
}
if (self.clients.openWindow) {
await self.clients.openWindow(url);
}
})(),
);
});