Fire a no-await profiles query in AuthContext on session establish to absorb
cold-connection latency before the first user-triggered request. Add
loading='lazy' default to the central Avatar component so all off-screen
avatars (chat list, friends list, popovers, message senders) skip eager
Supabase Storage fetches; set loading='eager' on ConversationHeader (active
conv header) and CallParticipantTile inline imgs (both AudioContent and
VideoStub) which are always above-the-fold when visible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A — Call core:
- Deafen now implies mute + remembers pre-deafen mic state so un-deafen
restores it (Discord-parity). Peers still see the headphones-off +
mic-off badges in sync via the existing data-channel broadcast.
- Self-join sound fires on the local peer's r.connect() too, not just
on remote ParticipantConnected, so the user gets the "I'm in" cue.
- New CallState.reconnecting holds the UI steady when LiveKit drops the
signaling socket and retries; duration keeps ticking, status label
switches to "Verbinde neu…". Full teardown only on terminal
Disconnected (after LK gives up).
- joinActiveCall falls back to connected after 5s if no peer arrived —
avoids hanging in "Verbinde…" when peers left the room mid-rejoin.
B — Ringtone:
- Oscillator base gain up (incoming 0.22 -> 0.4, outgoing 0.14 -> 0.22)
so the default pattern survives laptop speakers + background music.
- New ringtoneVolume slider in Settings, default 0.9, live-applies to
both the oscillator fallback and the custom-file <audio> element.
C — Participant tile:
- Split the speaking indicator: video tiles get the emerald border +
inset glow; audio tiles rely on the existing avatar pulse. No more
double-chrome when someone talks in grid/focus view.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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
Speaking ring:
- Switch useActiveSpeakers from LiveKit's smoothed isSpeaking / server-
batched ActiveSpeakersChanged to Web Audio API AnalyserNode on each
participant's raw audio MediaStreamTrack. Poll 50ms, RMS threshold 0.03,
250ms hold. Feels real-time vs the old ~500ms lag
- Defensive syncProbe on every tick so probes catch up if TrackPublished
missed (local mic publish race on join)
- Universal speaking overlay on tile (3px emerald border + inset glow,
z-10) so video mode shows the ring too, not just audio mode
Screen sharing:
- Separate "screen" tile per sharer so the sharer's avatar tile stays
intact with its speaking ring. Tile.id is kind-prefixed (user:xxx /
screen:xxx) so focus tracking distinguishes them
- New ScreenShareDialog (quality preset + fps override + displaySurface
hint) opens on the share button. startScreenShare / stopScreenShare
actions in CallContext replace the one-shot toggle
- ScreenShareViewer: plain CSS-only fullscreen overlay (Tauri WKWebView
doesn't implement requestFullscreen), always `h-full w-full
object-contain`, Esc exits
Camera:
- toggleCamera action in CallContext tracks isCameraEnabled
- VideoStub renders real <video> srcObject for the participant's camera
MediaStreamTrack; local preview is mirrored
- Tile video track resolves to Track.Source.Camera publications of the
LocalParticipant / each RemoteParticipant
- Room listens for TrackMuted / TrackUnmuted and re-publishes remote
state so peers switch to avatar placeholder when a camera is disabled
Deafen:
- New isDeafened state + toggleDeafen action. Sets `muted = true` on all
attached `<audio[data-livekit-track]>` plus mutes fresh ones on attach
via module-level flag
- Broadcast state over the LiveKit data channel
({type:'presence', deafened}) so peers can render the headphones-off
badge. Attributes API not used because the self-hosted server may run
older LiveKit versions
- remoteDeafen: Record<identity, bool> exposed via context, bumped on
DataReceived and re-broadcast on ParticipantConnected
Incoming video call:
- acceptIncoming takes an optional CallKind override so the receiver can
answer a video invite with audio only or promote an audio invite to
video on accept
- IncomingCallPanel shows two accept buttons (audio + video) when the
invite is a video call
Audio devices:
- audioSettings adds inputDeviceId + outputDeviceId, persisted
- CallContext uses them on setMicrophoneEnabled, plus new
setAudioInputDevice / setAudioOutputDevice hot-swap actions.
Output swap applies setSinkId to every attached remote-audio element
since LiveKit's own switchActiveDevice only tracks elements it
attached itself
- SettingsPage "Mikrofon" + "Ausgabegerät" selects with devicechange
listener and a permission-probe button
Fullscreen mode:
- Replaced absolute-positioned speaker + floating thumbnails with a real
flex layout. Default = even grid of all tiles. Clicking a tile flips
to big-speaker + horizontal thumbnail strip. Click focused tile =
back to grid
- Controls overlay pinned bottom; content wrapper has pb-24 so tiles
never sit behind the toolbar
- Grid now uses explicit grid-rows-* so cells get a defined 1fr height
(without it, video intrinsic dimensions blew tiles past the container
bounds on Windows)
UI chips:
- Mic-off badge combines isMuted flag AND
localParticipant.isMicrophoneEnabled, so a user with no mic / denied
permission sees the badge + the toolbar button red even though they
never pressed mute
- Deafen badge on tile chips for local + remote (remote driven by the
data-channel broadcast)
Visual:
- Light/dark theme via ThemeContext + CSS vars
- New design tokens (surface/fg/accent/line/etc.) across all pages
- Reusable Avatar component (img + letter fallback) wired into UserBar,
ConversationHeader, ChatsPage, FriendsPage, GroupInfoPanel, IncomingCallPanel
Calls:
- Split call UI: IncomingCallPanel, InCallPanel, CallControls,
CallParticipantTile, ScreenShareViewer
- Active speaker hook (useActiveSpeakers)
- Fix: ActiveCallBanner stayed hidden after hangup while peers in room.
- useCallPresence: bind presence callbacks only when we own subscribe
(Supabase forbids .on() after .subscribe() on shared dedup'd channels)
- useCallPresence: never removeChannel — channel is shared with CallContext
so tearing it down on ConversationHeader unmount killed live tracking
- ActiveCallBanner: lastCallConversationId fallback so banner shows
instantly after hangup, auto-dismiss when room confirmed empty
- Drop unused useAnyActiveCall
Bump tauri version 0.5.0 -> 0.6.0