From a04ecf7a1927b95405dcd406847145c3f7d3075c Mon Sep 17 00:00:00 2001 From: Dennis Landmann Date: Tue, 21 Apr 2026 01:14:16 +0200 Subject: [PATCH] feat: voice messages, offline queue, delivery ticks, volume slider, admin + scaling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Voice messages: MediaRecorder → encrypted attachment, custom waveform player via OfflineAudioContext, 60s limit + live mic-level meter - Offline message queue: localStorage outbox, exponential backoff retries, optimistic pending bubble with retry/discard - Delivery indicator: message_deliveries table + RLS (reciprocal receipts), ✓ / ✓✓ / ✓✓-blue tick states, group-aware (all members must ack) - Per-participant volume slider in calls via right-click tile menu, persisted to localStorage, applied to attached audio elements - Group call scaling: grid up to 12 tiles with pagination, active-speaker auto-promotion in fullscreen - Push notifications scaffolding: service worker, VAPID subscription registration, notify-push edge function skeleton - Backup recovery code: 24-char base32 code (~120 bits entropy) as alternative decrypt path, restore UI with mode toggle - Admin panel: conversations list, audit log (admin_audit_log table + admin_log_action RPC), audit entry on user flag toggle - Search v2: sender filter, attachment-only toggle, date range - Reactions pop animation (scale 0.4→1.15→1 on count change) - Message list windowing (150 default, expand via IntersectionObserver) - Stub cleanup: removed dead ScreenshareStub from CallParticipantTile Fixes: - Focus-triggered flicker: dropped window.focus listeners in three spots, throttled visibilitychange/online wake-refreshes to 30s, keep existing data visible during background re-syncs (no more spinner on every click) - Voice attachment audio element collapsed to 0px on peer side — now forces 280px min-width on bubble Migrations (push required): 20260421000001_message_deliveries.sql 20260421000002_admin_audit_log.sql Server TODO: VAPID keys + notify-push edge function deploy --- apps/desktop/public/sw.js | 67 ++ .../src/components/AttachmentAudio.tsx | 239 +++++++ .../src/components/BackupExportDialog.tsx | 109 ++- .../src/components/CallParticipantTile.tsx | 72 +- apps/desktop/src/components/DeviceRestore.tsx | 44 +- apps/desktop/src/components/InCallPanel.tsx | 173 ++++- apps/desktop/src/components/MessageBubble.tsx | 68 +- .../src/components/ParticipantVolumeMenu.tsx | 87 +++ apps/desktop/src/components/VoiceRecorder.tsx | 252 +++++++ apps/desktop/src/context/AuthContext.tsx | 8 + apps/desktop/src/context/CallContext.tsx | 7 +- .../src/context/ConversationsContext.tsx | 11 +- apps/desktop/src/lib/deviceBackup.ts | 56 ++ apps/desktop/src/lib/messageOutbox.ts | 164 +++++ apps/desktop/src/lib/participantVolumes.ts | 91 +++ .../src/lib/useConversationMessages.ts | 186 ++++- apps/desktop/src/lib/useFriendships.ts | 9 +- apps/desktop/src/lib/useGroupReceipts.ts | 87 +++ apps/desktop/src/lib/useMessageDeliveries.ts | 61 ++ apps/desktop/src/lib/webPush.ts | 87 +++ apps/desktop/src/pages/AdminPage.tsx | 122 +++- apps/desktop/src/pages/ConversationPage.tsx | 415 +++++++++-- apps/desktop/tailwind.config.js | 6 + design_handoff_chatapp/ChatApp Redesign.html | 6 + design_handoff_chatapp/README.md | 526 ++++++++------ design_handoff_chatapp/components/app.jsx | 24 +- design_handoff_chatapp/components/icons.jsx | 12 + design_handoff_chatapp/components/pages.jsx | 591 ++++++++++++++++ .../screenshots/12-friends-light.png | Bin 0 -> 33333 bytes .../screenshots/13-admin-light.png | Bin 0 -> 38080 bytes .../screenshots/14-settings-light.png | Bin 0 -> 39481 bytes .../screenshots/15-friends-dark.png | Bin 0 -> 32178 bytes .../screenshots/16-admin-dark.png | Bin 0 -> 36952 bytes .../screenshots/17-settings-dark.png | Bin 0 -> 38113 bytes design_handoff_chatapp/styles/pages.css | 651 ++++++++++++++++++ packages/shared/src/admin/index.ts | 138 ++++ packages/shared/src/chat/messages.ts | 128 ++++ supabase/functions/notify-push/index.ts | 102 +++ .../20260421000001_message_deliveries.sql | 54 ++ .../20260421000002_admin_audit_log.sql | 63 ++ 40 files changed, 4286 insertions(+), 430 deletions(-) create mode 100644 apps/desktop/public/sw.js create mode 100644 apps/desktop/src/components/AttachmentAudio.tsx create mode 100644 apps/desktop/src/components/ParticipantVolumeMenu.tsx create mode 100644 apps/desktop/src/components/VoiceRecorder.tsx create mode 100644 apps/desktop/src/lib/messageOutbox.ts create mode 100644 apps/desktop/src/lib/participantVolumes.ts create mode 100644 apps/desktop/src/lib/useGroupReceipts.ts create mode 100644 apps/desktop/src/lib/useMessageDeliveries.ts create mode 100644 apps/desktop/src/lib/webPush.ts create mode 100644 design_handoff_chatapp/components/pages.jsx create mode 100644 design_handoff_chatapp/screenshots/12-friends-light.png create mode 100644 design_handoff_chatapp/screenshots/13-admin-light.png create mode 100644 design_handoff_chatapp/screenshots/14-settings-light.png create mode 100644 design_handoff_chatapp/screenshots/15-friends-dark.png create mode 100644 design_handoff_chatapp/screenshots/16-admin-dark.png create mode 100644 design_handoff_chatapp/screenshots/17-settings-dark.png create mode 100644 design_handoff_chatapp/styles/pages.css create mode 100644 supabase/functions/notify-push/index.ts create mode 100644 supabase/migrations/20260421000001_message_deliveries.sql create mode 100644 supabase/migrations/20260421000002_admin_audit_log.sql 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