Commit Graph
132 Commits
Author SHA1 Message Date
Jannis Braun e0861597bc fix(web): portal overlays into fullscreenElement so they render in voice fullscreen
requestFullscreen() on the voice container puts only its descendants in the
browser's top layer; overlays portaled to document.body were rendered outside
that layer and stayed invisible — most visibly the right-click context menu on
stream tiles and voice user panels.

Add usePortalContainer() hook returning document.fullscreenElement ?? document.body
and re-rendering on fullscreenchange. Migrate every overlay reachable during a
call: ContextMenuRenderer (desktop, submenu, mobile sheet), Tooltip,
ConfirmDialog, ConnectionInfoPopover, ScreenShareSettingsPopover, and
ScreenSharePicker (which previously rendered inline at App root).
2026-04-29 23:32:14 +02:00
Jannis Braun 081937a030 fix(sounds): drop singleton AudioManager from SoundController effect deps 2026-04-28 14:49:03 +02:00
Jannis Braun 494585adf2 fix(sounds): full SoundController rewrite — every file fires for the right audience
- stream_started/ended audible to all (was self-only)
- stream_user_joined/left wired to the watcher data-channel diff (was misused)
- effective-mute/deafen audible mid-call (was self-toggle only)
- message sound DM + federation-aware mention default (was every channel)
- self-stream-end suppresses per-watcher stream_user_left stack
2026-04-28 14:43:05 +02:00
Jannis Braun 7c0b67bcc9 feat(sounds): broadcast stream_watch from StreamTile click sites
Wire broadcastStreamWatch (LiveKit data-channel ping) to all three
explicit user-action sites in StreamTile: "Stop Watching" context-menu
item, "Watch Stream" context-menu item, and the handleWatch button
callback. Broadcast is intentionally confined to click sites only —
not wired in voiceStore actions — to prevent noisy stream_user_left
sounds on automatic teardown paths in useLiveKit.ts.
2026-04-28 14:38:39 +02:00
Jannis Braun 51ad54fda5 refactor(camera): unify toggle paths through handleCameraAction
- Voice-bar and mobile camera buttons now use the canonical handler
  (fixes mobile no-op and voice-bar wrong-preset bugs)
- Remove dead useLiveKit.toggleCamera
- Add _intentionalCameraOff flag with mark/consume helpers
2026-04-27 20:16:14 +02:00
Jannis Braun b03df992dc fix: SoundController isSelf must check BOTH local ID and homeUserId
The identity flip during disconnect: updateParticipants resolves
homeUserId → localSnowflake when activeDmCall is set, but reverts
to raw homeUserId when activeDmCall is cleared (before LiveKit
disconnect completes). SoundController sees the snowflake "leave"
and the homeUserId "join" — two phantom events for the same person.

Previous fix only checked homeUserId OR id. Now checks BOTH via
isSelf(id) which matches against a Set of {id, homeUserId}. This
recognizes the user as "self" regardless of which identity format
the participant currently has.
2026-04-08 15:33:51 +02:00
Jannis Braun 5ab1f7b89d fix: SoundController identity mismatch for federated users
Root cause: SoundController compared LiveKit participant p.userId
(which is homeUserId from the home instance) against currentUser.id
(local snowflake on the current instance). For federated users these
are different IDs, so the controller thought the user's own presence
was a stranger — playing user_join/user_leave for self.

Fix: use homeUserId || id for the self-check. This matches the
LiveKit identity format used in federated calls.
2026-04-08 15:25:15 +02:00
Jannis Braun 1468b68199 fix: skip participant leave sounds during self-disconnect
Added justDisconnected guard to the participant sound loop. When
the user hangs up, isLiveKitConnected transitions to false — but
in a separate or same subscription tick, the participants list
also empties. Without the guard, SoundController plays user_leave
for every departed participant AND the disconnect sound simultaneously.

Now: if justDisconnected is true, the entire participant loop is
skipped. Only the disconnect sound plays.
2026-04-08 15:05:49 +02:00
Jannis Braun c037803c5a fix: acceptor sets activeDmCall in click handler, not server response
Bug A: handleAccept relied on dm_call_accepted server response to set
activeDmCall. But connectFn's async AudioContext resume yields to the
event loop, dm_call_accepted arrives during the yield, finds
isLiveKitConnected=false (connectFn just reset it), and skips
setActiveDmCall. The acceptor connects to LiveKit but the UI never
shows the call. Fix: set activeDmCall and clear incomingCall
directly in the click handler.

Bug B: ready handler no longer sets activeDmCall for active calls.
On refresh/restart the client has no LiveKit connection — showing
"Connecting..." with no connection is broken. The call exists on
the server but this client session is disconnected.
2026-04-08 14:23:41 +02:00
Jannis Braun aae0b1a74e fix: comprehensive client-side session management for federated DM calls
Four fixes addressing the full state management problem:

1. Passive ready handler: no longer auto-connects to LiveKit on
   page refresh. Prevents identity conflicts when the same user
   has multiple sessions fighting for one LiveKit identity slot.
   The user must re-accept to join; state is shown but not acted on.

2. SoundController sync guard: incomingCallLoading/outgoingCallLoading
   refs prevent multiple playSound calls during async audio load.
   If call is cancelled while sound loads, stops it immediately on
   completion. Eliminates the "5 ringtones at once" bug.

3. Host dm_call_accepted broadcasts now include federatedCallId so
   all clients (including remote instances) can match the event.

4. Removed all diagnostic console.log statements.
2026-04-08 14:12:45 +02:00
Jannis Braun ba7b975bc8 feat: add federatedCallId and callOrigin to client call routing
Enable federated DM calls to route accept/reject/end through the correct
WebSocket connection using callOrigin, and include federatedCallId in all
dm_call payloads for server-side FederatedCallEntry lookup.
2026-04-08 03:26:48 +02:00
Jannis Braun f0104d19b7 feat: conditionally show bitrate slider based on instance allowCustomBitrate 2026-03-25 04:09:18 +01:00
Jannis Braun 78555a43f8 fix(voice): stabilize grid layout across focus-mode and fullscreen transitions
Refactor useGridLayout from RefObject to callback-ref API so the
ResizeObserver reattaches when the grid container remounts after
focus-mode toggle. Fixes tiles rendering at stale fullscreen dimensions
on 4:3 monitors.

Also clamp focus-mode participant strip height (max-h-[20vh] min-h-[80px])
to prevent it from crowding the focused stream on short viewports.
2026-03-25 03:08:45 +01:00
Jannis Braun 0d91df166a fix(context-menu): update all checkbox call sites to reactive subscribe+getChecked 2026-03-24 21:48:26 +01:00
Jannis Braun 8910de3053 feat(voice): add cachedUser carry-forward to ParticipantInfo
Extends ParticipantInfo with cachedUser: User | null. In updateParticipants,
snapshots the previous participant list and carries forward the cached User
object when spaceStore member lookup fails (user navigated to a different
space). Local user falls back to authStore. homeUserId is now derived from
cachedUser when the direct member lookup misses. Updates EMPTY_PARTICIPANT
in PictureInPicture.tsx.
2026-03-24 19:01:34 +01:00
Jannis Braun 8c2e20165a feat: rename codec buttons to Standard/NVIDIA · Apple with codec subtitles 2026-03-24 16:41:23 +01:00
Jannis Braun c6c44d1bdd feat: replace codec pills with VP9/H.264 HW, wire to hwOverdrive 2026-03-24 03:51:22 +01:00
Jannis Braun e53d3bcb98 fix: switch backup codec to VP8@30fps, dim H.264 pill with warning
- Backup codec changed from H.264 (OpenH264, slow) to VP8 (libvpx,
  fast) — same encoder family as VP9, lower dual-encode overhead
- Backup capped at 30fps with proportional bitrate to keep CPU
  overhead low during SIMULCAST dual-encoding
- H.264 pill visually dimmed with amber warning when selected
2026-03-24 01:14:47 +01:00
Jannis Braun a54dd9084a feat: show encoder implementation in Connection Info, VP9 recommended hint
- Connection Info now shows encoder impl (e.g. libvpx, OpenH264,
  ExternalEncoder) on send-side video tracks alongside quality
  limitation reason
- Amber warning "VP9 is recommended" when H.264 is selected
2026-03-24 01:06:19 +01:00
Jannis Braun 65a1fc1402 fix: VP9 default for both modes, clean up codec pill UI
VP9 empirically outperforms H.264 in gaming (Chrome may not use
NVENC for screen share H.264). Codec pills now match other pill
styles — simple labels, no description text. Mode change no longer
auto-switches codec; user's codec choice is independent.
2026-03-24 00:59:04 +01:00
Jannis Braun 894ce68194 feat: add manual codec selection (VP9/H.264) to stream settings
VP9 is default — better quality per bit with H.264 SIMULCAST backup
for Safari. H.264 option available for zero-CPU hardware encoding.
Codec choice is independent of gaming/text mode, which still controls
contentHint and degradationPreference.
2026-03-23 22:44:31 +01:00
Jannis Braun 161e876df8 fix: IncomingCallModal calls connectFn directly for iOS gesture context 2026-03-23 21:09:37 +01:00
Jannis Braun 241451ca18 feat: gesture-aware voice connection, remove AppLayout auto-connect
Replace the useEffect-based auto-connect pattern in AppLayout with
direct connect/disconnect calls from user gesture contexts. This is
required for iOS (AudioContext.resume + getUserMedia must happen in
a gesture handler) and aligns with tightening autoplay policies on
desktop browsers.

Architecture:
- voiceStore gains connectFn/disconnectFn refs, registered by AppLayout
  from the single useLiveKit() instance.
- All voice join paths (ChannelSidebar, MobileSpacesScreen, MainContent,
  voice_moved WS handler) pass connectFn to joinVoiceChannel().
- All disconnect paths (VoiceControls, voiceActions, MobileVoiceFullScreen,
  MobileVoiceMiniBar, dm_call_ended/rejected WS handlers, ready handler)
  call disconnectFn() directly.
- dm_call_accepted WS handler calls connectFn() to initiate the DM call
  LiveKit connection.
- The 55-line auto-connect useEffect and lastAttemptedRef are removed.
2026-03-23 21:06:19 +01:00
Jannis Braun 8aa66816d8 feat: add MobileVoiceJoinSheet bottom sheet for voice preview 2026-03-23 20:59:26 +01:00
Jannis Braun 7c1ec5e846 refactor: use VoiceUserRow in desktop VoiceChannel 2026-03-23 20:46:47 +01:00
Jannis Braun ba11eefb0c fix: add isUnwatchedCamera prop and remove unused React import
Adds camera slash overlay for unwatched cameras, matching
VoiceChannel.tsx. Removes unnecessary React import (Vite
automatic JSX transform).
2026-03-23 20:44:44 +01:00
Jannis Braun 00c5757311 feat: add VoiceUserRow pure UI component 2026-03-23 20:40:29 +01:00
Jannis Braun 8f104e1c5b fix: derive spaceId from voice channel in buildVoiceModMenuItems
Use channelToSpaceMap.get(channelId) for permission checks instead of
currentSpaceId from the navigation store. On mobile, UI navigation
decouples from the active voice session.
2026-03-23 16:32:47 +01:00
Jannis Braun 11081dbf5a refactor(voice): extract action handlers into voiceActions.ts, remove hard-coded M/D shortcuts
Move mute/deafen/camera/screen-share/disconnect logic out of VoiceControlBar into a shared voiceActions.ts utility so the same handlers can be called from both UI buttons and the upcoming keybind dispatcher.
2026-03-22 20:27:10 +01:00
Jannis Braun 09bfd1ccab fix: use grid layout for stream settings pills to prevent uneven wrapping 2026-03-21 23:41:01 +01:00
Jannis Braun 5ac0917d81 feat: dynamic resolution/framerate pills from instance limits 2026-03-21 23:30:14 +01:00
Jannis Braun ebe34a710b refactor: update VoiceChannel to use useDragManager hook-provided handlers 2026-03-20 20:46:49 +01:00
Jannis Braun 68378c7d83 fix: deduplicate VolumeSliderItem and add missing stopPropagation
- Extract VolumeSliderItem to voiceMenuItems.tsx (was duplicated in
  VoiceUser.tsx and VoiceChannel.tsx)
- Add e.stopPropagation() to all imperative context menu handlers
  for consistency with useContextMenu hook behavior
2026-03-20 18:55:58 +01:00
Jannis Braun 443259a527 chore: delete old context menu components replaced by centralized system 2026-03-20 18:48:40 +01:00
Jannis Braun 8eb3fc84a5 refactor: migrate voice context menus to centralized store 2026-03-20 18:47:45 +01:00
Jannis Braun 1c806be94f fix: clear message cache on reconnect to prevent stale scroll position
After a server restart, navigating to a previously-visited channel showed
messages at a stale middle position instead of scrolling to the bottom.
The in-memory message cache survived the reconnect, so loadMessages()
bailed (cache hit) and the scroll-to-bottom logic never fired.

Now the ready handler clears the messages and hasMore maps for all
channels belonging to the reconnecting origin (including DMs for home).
The currently open channel is force-reloaded immediately; other channels
get fresh-fetched on next visit, triggering proper scroll-to-bottom.

Also fixes voice channel settings gear icon placement to match text
channels (flex-1 pushes icon to right edge).
2026-03-16 22:34:21 +01:00
Jannis Braun bfecb41c66 fix: derive voice sidebar from LiveKit participants to eliminate desync
The channel sidebar voice user list was maintained by a separate
voiceUsers Map (fed by WS events + fragile hydration code) that diverged
from reality after server restarts — users shown in wrong channels,
duplicated across channels. The VoiceGrid was always correct because it
reads LiveKit participants directly.

Now VoiceChannel.tsx derives its user list from LiveKit participants for
the connected channel (single source of truth) and only falls back to
server-provided voiceUsers for channels the user is not connected to.

Removed all hydration band-aids that tried to sync the two systems:
- useLiveKit ParticipantDisconnected → removeVoiceUser
- useLiveKit ConnectionStateChanged → addVoiceUser hydration loop
- useWebSocket ready handler → dynamic import LiveKit hydration

Also includes: voice channel settings gear icon on hover, persist
per-user volume/mute prefs across sessions, default screen share
audio off on Electron (no system audio capture support).
2026-03-16 22:10:53 +01:00
Jannis Braun 0815963e1b fix: update screen share audio warning to recommend Chrome browser 2026-03-16 19:27:12 +01:00
Jannis Braun bda7930e61 fix: restore setDisplayMediaRequestHandler with useSystemPicker
Electron requires setDisplayMediaRequestHandler for getDisplayMedia() to
work — removing it broke screen sharing entirely. Restored the handler
with useSystemPicker: true, which on macOS 15+ uses the native system
picker (honoring restrictOwnAudio), while Windows/Linux fall back to
the custom picker with the shareAudio toggle for echo control.
2026-03-16 18:34:50 +01:00
Jannis Braun 9ff3761640 fix: remove setDisplayMediaRequestHandler to let restrictOwnAudio work natively
The custom screen share picker intercepted getDisplayMedia() and created
a raw loopback stream, bypassing Chromium's constraint pipeline entirely.
restrictOwnAudio was silently discarded. Removing the handler lets
Chromium 144's native getDisplayMedia run end-to-end with restrictOwnAudio
applied, eliminating the audio feedback loop in the desktop app.
2026-03-16 18:18:42 +01:00
Jannis Braun 46f55643ae fix: eliminate screen share audio feedback loop + upgrade Electron 33→40
Screen sharing with audio captured the app's own voice playback, causing
users to hear themselves echoed back. Fixed via two layers:

- Add restrictOwnAudio constraint (Chrome 141+/Chromium 144) to exclude
  the app's own audio from system audio capture
- Add shareAudio toggle so users can disable system audio entirely
- Remove outdated macOS audio block (now supported via ScreenCaptureKit)
- Upgrade Electron 33→40 (Chromium 130→144) so restrictOwnAudio works
  natively in the desktop app
- Add NSAudioCaptureUsageDescription for macOS 14.2+ audio capture
- Add GTK 3 fallback for Linux GNOME compatibility (Electron 36+)
2026-03-16 18:01:40 +01:00
Jannis Braun 95ec3ff952 feat: Electron screen share picker, instance selector, and system audio loopback
- Custom screen share picker for Electron (ScreenSharePicker.tsx) with
  Aether Drift design: glass-bubble footer, adaptive grid, pill tabs,
  border-based selection (avoids overflow clipping), hover brightness
- Instance URL picker so Electron connects to any Backspace server
- System audio loopback on Windows/Linux via desktopCapturer callback
- macOS: video-only callback (OS blocks system audio capture)
- IPC bridge for screen source enumeration and selection
- Purge stale service worker caches on Electron launch
2026-03-16 03:50:07 +01:00
Jannis Braun 7113f47b17 fix: soften incoming call animations with liquid ripple and refraction effects
Replace hard-edged ring ripples with blurred radial gradient orbs, add
subtle glass refraction shimmer, and use gentler glow/breathing curves
for a calmer incoming call experience.
2026-03-15 00:33:05 +01:00
Jannis Braun 7c544c1ff4 feat: security hardening, DB indexes, token revocation, and input validation
- SSRF protection: DNS resolution + private IP blocking on metadata fetcher
- Upload security: CSP/X-Frame-Options headers, SVG forced download, nosniff
- Auth hardening: JWT secret min length, password min 8 chars, token revocation via password_changed_at
- Attachment ownership verification before linking to messages
- Message length limit (4000 chars) enforced on client and server
- Asset URL validation on avatar/banner updates
- Federation instance validation (domain regex, origin scheme, length limits)
- DB indexes on all FK columns for query performance
- Migrations: nullable moderator columns, dm_messages reply_to FK constraint
- File cleanup on avatar/banner replacement and space deletion
- Fastify trustProxy, AbortController on fetches, typing map size cap
2026-03-15 00:06:15 +01:00
Jannis Braun afbc4b5e31 fix: use consistent picture frame icon for video quality button in VoiceControlBar 2026-03-13 23:07:14 +01:00
Jannis Braun 2b810055f7 fix: PiP selection logic — last speaker persistence + focused stream support
Track last speaker via ref so avatar fallback doesn't snap to array-order
first remote when everyone stops talking. Parse :stream suffix from
focusedParticipantId so clicking a stream tile in the grid pins that
screen share to PiP. Remember last focus via ref so PiP content persists
when grid focus is cleared.
2026-03-13 02:05:46 +01:00
Jannis Braun 44fd74c7c6 fix: remove redundant status dot from voice channel avatars
Being in a voice channel already implies online status, so the dot
is unnecessary and creates a cutout artifact on the speaking ring.
2026-03-13 01:50:22 +01:00
Jannis Braun 5f5433eb67 fix: speaking indicator layout shift — use box-shadow ring instead of container growth 2026-03-13 01:43:35 +01:00
Jannis Braun 770e0490dd feat: increase perceived audio loudness — +3dB master boost, configurable SFX volume
Three changes to bring output volume closer to native apps:
- Insert masterBoost GainNode (+3dB) before the compressor/limiter
- Raise default system sound volume from 0.5 to 0.8
- Add configurable Sound Effects Volume slider (0–200%) in Voice settings
2026-03-12 23:42:11 +01:00
Jannis Braun b6d44f1568 refactor: remove video channel type, fix invisible CreateChannel inputs
Voice channels already support video/screen share, so the separate video
type was redundant. Adds migration to convert existing video channels.
Also adds border-border-soft to CreateChannel input fields for visibility.
2026-03-12 00:46:15 +01:00