diff --git a/apps/desktop/public/sw.js b/apps/desktop/public/sw.js new file mode 100644 index 0000000..eb73de7 --- /dev/null +++ b/apps/desktop/public/sw.js @@ -0,0 +1,67 @@ +// Web Push service worker. +// +// Handles browser-delivered push events when the app tab is closed or in the +// background. Tauri desktop does not install service workers; native OS +// notifications are routed through the Tauri notification plugin instead +// (see src/lib/osNotify.ts). +// +// Payload contract — server sends JSON of shape: +// { title: string, body?: string, conversationId?: string, kind?: 'message' | 'call' } +// Body is intentionally generic; message ciphertext is never included. + +self.addEventListener('install', (event) => { + // Activate immediately so updates apply on next page load. + event.waitUntil(self.skipWaiting()); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener('push', (event) => { + let data = { title: 'Neue Nachricht', body: '' }; + try { + if (event.data) { + data = { ...data, ...event.data.json() }; + } + } catch (_err) { + /* malformed payload — fall back to defaults */ + } + + const opts = { + body: data.body || '', + icon: '/favicon.svg', + badge: '/favicon.svg', + tag: data.conversationId || 'default', + renotify: true, + data: { + conversationId: data.conversationId, + kind: data.kind, + }, + }; + + event.waitUntil(self.registration.showNotification(data.title, opts)); +}); + +self.addEventListener('notificationclick', (event) => { + event.notification.close(); + const conversationId = event.notification.data && event.notification.data.conversationId; + const target = conversationId ? '/chats/' + conversationId : '/'; + + event.waitUntil( + self.clients + .matchAll({ type: 'window', includeUncontrolled: true }) + .then((clientList) => { + for (const client of clientList) { + if ('focus' in client) { + client.postMessage({ type: 'navigate', to: target }); + return client.focus(); + } + } + if (self.clients.openWindow) { + return self.clients.openWindow(target); + } + return undefined; + }), + ); +}); diff --git a/apps/desktop/src/components/AttachmentAudio.tsx b/apps/desktop/src/components/AttachmentAudio.tsx new file mode 100644 index 0000000..fdc3d3d --- /dev/null +++ b/apps/desktop/src/components/AttachmentAudio.tsx @@ -0,0 +1,239 @@ +import { type AttachmentHandle, downloadAndDecryptAttachment } from '@chat-app/shared/chat'; +import { useEffect, useMemo, useRef, useState } from 'react'; + +import { supabase } from '../lib/supabase'; +import { AlertIcon, MicIcon, SpinnerIcon } from './icons'; + +interface Props { + handle: AttachmentHandle; +} + +const BAR_COUNT = 48; + +// Custom voice-message player with waveform visualisation. Decoded peaks are +// computed once per blob via OfflineAudioContext so playback only carries the +// rendered DOM. Falls back to a rectangular bar if decoding fails (e.g. the +// blob mime is recognised by