Commit Graph
51 Commits
Author SHA1 Message Date
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 e809adff3e fix: sync channel sidebar voice users with LiveKit participant disconnects
The sidebar used voiceStore.voiceUsers (WebSocket-driven, 5s delay) while
voice panels used LiveKit's real-time participants. Now ParticipantDisconnected
also removes the user from voiceUsers for immediate sidebar updates.
2026-03-16 20:41:36 +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 fc72e424d6 refactor: rename "Server Mute/Deafen" to "Space Mute/Deafen" across entire stack
Aligns voice moderation terminology with Backspace's "Spaces" branding.
Renames WS protocol strings, backend handlers, frontend store/hooks/utils,
user-facing labels, and documentation — 15 files, zero functional changes.
2026-03-12 00:12:33 +01:00
Jannis Braun ce63c5ed36 feat: real-time SPEAK permission enforcement in voice channels
Permission changes now take effect immediately without requiring
disconnect/reconnect. Modeled as "permission mute" parallel to
server mute — server recomputes SPEAK for all voice participants
on role/override changes and broadcasts state via WebSocket.
Includes amber UI indicators and mic toggle blocking.
2026-03-10 14:50:01 +01:00
Jannis Braun f9376460d4 feat: add camera watch/unwatch with click propagation fix and correct badge color
Add ability to unsubscribe from remote camera tracks via context menu,
with unwatched state tracked in voiceStore. Fix click propagation through
React portals by adding onClick stopPropagation alongside onMouseDown on
both the main context menu and MoveToSubmenu flyout portal containers.
Use rose badge color for unwatched cameras (user choice) instead of amber
(reserved for server-enforced states).
2026-03-10 02:45:03 +01:00
Jannis Braun 2999ca95a0 fix: prevent stale voice sidebar after multi-session identity collision
When a second browser joins voice, LiveKit disconnects the first browser
via identity collision. The first browser's leaveVoice() was optimistically
removing the user from voiceUsers, but the user is still in voice from
the other session. Add handleForceDisconnect() that clears local connection
state without touching voiceUsers, keeping the sidebar accurate.

Also fix deploy.sh to clean up stale renamed containers from failed recreates.
2026-03-09 23:48:44 +01:00
Jannis Braun 907f8285cd fix: decouple voice intent from server enforcement to prevent involuntary unmute
When a moderator lifted a server mute/deafen, the client was involuntarily
turning on the user's microphone because isMuted/isDeafened conflated user
intent with server enforcement. Now intent (isMuted/isDeafened) is never
mutated by server events. Effective state (intent || serverEnforcement) is
computed at broadcast and hardware time via centralized helpers.
2026-03-09 22:25:56 +01:00
Jannis Braun fc06e25731 refactor: rename Server → Space across entire codebase
Renames all domain terminology from "Server" to "Space" throughout the
application — database schema, API routes, shared types, stores, components,
and UI strings. Files renamed: ServerSidebar → SpaceSidebar, CreateServer →
CreateSpace, JoinServer → JoinSpace, ServerSettings → SpaceSettings,
serverStore → spaceStore, routes/servers → routes/spaces.
2026-03-08 20:08:24 +01:00
Jannis Braun d0b0441f81 fix: revert echo gate, restore Chrome AEC during screen share
The echo gate architecture bypassed the serialized mic management chain
and introduced race conditions. Chrome's AEC handles echo properly for
both headphone and speaker users without custom intervention.
2026-03-05 00:09:03 +01:00
Jannis Braun dd02c1ed8d fix: prevent ghost users in voice sidebar after deploy
Send voice_join to WS server only after LiveKit successfully connects,
not immediately on joinVoiceChannel() or WS reconnect. Background tabs
can reconnect WebSocket but not WebRTC, causing phantom voice users.
2026-03-04 22:11:50 +01:00
Jannis Braun c7c3331dbf fix: persist homeUserId for federated users to ensure consistent avatar colors
Store the original home snowflake ID (homeUserId) during federation replication
so that avatar gradient colors resolve identically across instances. Previously,
replicated users got new snowflake IDs on each instance, causing different
gradient colors. Now Avatar, UserProfilePopout, VoiceUser, StreamTile, and
VoiceChannel all resolve through homeUserId when available. Includes backfill
logic for existing federated users missing the field.
2026-03-04 13:45:25 +01:00
Jannis Braun e37f9ea24c feat: route LiveKit token requests to correct instance for cross-instance voice 2026-03-03 00:37:20 +01:00
Jannis Braun 2184ded2c1 fix: use autoSubscribe:false to prevent LiveKit renegotiation storm on screen share
When a viewer joined while a screen share was active, autoSubscribe:true
caused a subscribe-then-unsubscribe dance for screen share tracks,
triggering cascading renegotiations, MaxListeners warnings, and
negotiation timeouts. Now the SFU starts with no subscriptions and we
explicitly subscribe only to non-screen-share tracks (audio, camera).
Screen shares remain controlled by the watch/unwatch UI flow.
2026-02-26 03:02:27 +01:00
Jannis Braun 4bb69851d3 fix: use LiveKit track.attach() for adaptive stream and switch screen share to VP9 single-layer
Camera was stuck at 180p because our custom <video> rendering bypassed
LiveKit's adaptive stream observer. Replaced manual srcObject binding
with track.attach()/detach() in VoiceUser, StreamTile, and PictureInPicture
so the SFU receives viewport dimensions and forwards the correct
H.264 simulcast layer.

Screen share VP9 SVC with L3T3 spatial layers failed because hardware
VP9 encoders (NVENC, QSV, VCE) don't support spatial scalability —
Chrome silently degrades to L1T1. Reverted to VP9 single-layer
(simulcast: false, no scalabilityMode). Also targets encodings[length-1]
in applyOverdrive() for correct simulcast layer targeting.
2026-02-26 02:33:18 +01:00
Jannis Braun 33e9198de6 perf: enable adaptiveStream and camera simulcast for SFU bandwidth optimization
- adaptiveStream: true — SFU adjusts quality per subscriber viewport size
- Camera simulcast: true — publisher encodes 3 quality layers, SFU picks per viewer
- Screen share keeps simulcast: false (text readability)
- Server-side: livekit.yaml updated with 40 Mbps bandwidth cap, room cleanup,
  playout delay, and A/V sync (applied directly to Pi, not in this commit)
2026-02-26 01:09:05 +01:00
Jannis Braun 36e27121da fix: harden data integrity, connection stability, and memory management
Wrap all multi-write DB operations in atomic transactions (server/channel
creation, message+attachment linking, DM creation, friend acceptance,
cascading deletes) to prevent partial-write corruption.

Batch N+1 queries in WS ready payload into O(1) bulk fetches with
chunked inArray() to respect SQLite's variable limit.

Fix chat history regression where background WS messages bypassed
channel load by switching the guard from messages.has() to hasMore.has().

Add LRU channel eviction (20 cached, evict to 15) and per-channel
message cap (200) to bound client memory growth.

Shorten WS heartbeat from 30s to 15s for aggressive proxy/NAT
environments. Clear all user-scoped stores on logout to prevent
cross-session data leaks.

Extract LiveKit internal accessors into shared livekitInternals utility.
2026-02-24 03:52:22 +01:00
Jannis Braun f808a204e7 refactor: dynamic screen share engine with independent resolution/fps/mode axes
Replace rigid SCREEN_QUALITY_MAP (6 hardcoded VideoPreset strings) with a
builder function that computes bitrate, degradation preference, and content
hint from three independent axes (height, fps, content mode). Camera is
decoupled onto a fixed 720p30 preset so screen share changes no longer
affect camera quality. New ScreenShareSettingsPopover replaces the old
VideoQualityPopover with pill-style selectors. Store migrated to v5 with
backwards-compatible migration from videoQuality string.
2026-02-24 02:16:15 +01:00
Jannis Braun 9b0d319ab2 refactor: per-track WebRTC diagnostic engine replacing flat stats model
Extract stats polling into useTrackStats hook with per-sender/receiver
stats via RTCRtpSender.getStats(), delta-based FPS (fixes 25fps overwrite
bug with camera+screenshare), and track-to-source matching via LiveKit
TrackPublication identity. ConnectionInfoPopover becomes a pure display
component with Network/Audio/Video sections. Remove soft-launch diagnostic
loop from useLiveKit.
2026-02-24 00:36:31 +01:00
Jannis Braun e9ec61db43 fix: revert removeAllListeners() from destroyRoom() to stop SDK teardown errors
Closure guards on all 15 handlers already isolate React state from stale
rooms — the sledgehammer removeAllListeners() was stripping the SDK's own
internal listeners (PCManager, SignalClient), causing "Tried to add a track
for a participant that's not present" errors on rapid channel switches.
2026-02-23 21:30:07 +01:00
Jannis Braun 61e607204f fix: prevent ghost Room memory leak by stripping listeners before disconnect
LiveKit's Room.disconnect() tears down WebRTC but leaves .on() handlers
attached. Every connect() registers ~15 event handlers, which accumulate
on orphaned Room instances during rapid channel switches or HMR, causing
MaxListenersExceededWarning. Added destroyRoom() helper that calls
removeAllListeners() before disconnect() at all four teardown sites.
2026-02-23 21:13:35 +01:00
Jannis Braun bc51fc6f7e refactor: unify DM calls with server voice architecture
Merge connectDm() into connect() with isDm flag, eliminating ~145 lines
of duplicated LiveKit room setup. DM calls now inherit all event handlers
(SpeakingDetector cleanup, deafen broadcasts, metadata changes). Replace
monolithic DmCallView with shared VoiceGrid + VoiceControlBar components,
making DM calls group-DM-ready with full feature parity.
2026-02-23 20:52:58 +01:00
Jannis Braun 7e7de41718 fix: centralize screen share pipeline to fix 270p resolution ramp-up failure
Screen sharing was publishing at h360 (640x360) and never ramping to target
resolution due to stale closure in setTimeout, wrong initial quality anchor,
and 5 competing code paths with inconsistent bitrates.

- Create utils/screenShare.ts as single source of truth for all screen share ops
- Publish at target resolution from the start (not h360 → ramp)
- Read store at call time in timers (eliminates stale closure bug)
- Use maintain-resolution for screen content, maintain-framerate for camera
- Fix OS-level "Stop sharing" not resetting store or restoring AEC
- Enable dynacast for SFU quality signaling
- Reconcile QUALITY_MAP to canonical bitrates across all 7 files
2026-02-23 03:25:52 +01:00
Jannis Braun 82eee04946 fix: client-side speaking detection replacing LiveKit's conservative server VAD
LiveKit's server-side VAD has a high, non-configurable threshold that misses
conversational speech. Replace it with a SpeakingDetector singleton that uses
Web Audio AnalyserNodes to read actual RMS levels per participant (50ms poll,
0.008 threshold, 250ms hysteresis hold).
2026-02-23 02:53:40 +01:00
Jannis Braun 9091f08ced fix: reliable speaking indicator via direct store writes + polling safety net
Eliminate double-state architecture (useState → useEffect bridge → store)
that lost speaking events due to React 18 batching. ActiveSpeakersChanged
now writes speakingParticipantIds directly to voiceStore; 200ms poll
catches missed SDK events. Each VoiceUser subscribes to its own identity
via fine-grained selector for minimal re-renders. Also adds connection
quality indicator and ConnectionInfoPopover.
2026-02-23 02:36:39 +01:00
Jannis Braun ccf2001047 feat: RNNoise WASM AudioWorklet for ML-based noise suppression (Phase 3)
Inject @sapphi-red/web-noise-suppressor into AudioManager's input pipeline
as a toggleable AudioWorkletNode. The worklet is loaded lazily on first
enable, then kept alive — toggling bypasses by rewiring the graph without
destroying the WASM instance. Browser NS is forced off when RNNoise is
active to avoid double-processing. InputGain forced to stereo up-mix to
prevent mono-left-only output from the worklet. Also wires Phase 2 output
device routing (setSinkId) into AppLayout/ChannelSidebar.
2026-02-23 00:28:29 +01:00
Jannis Braun dbebd38576 refactor: replace unpublishTrack mute with setMicrophoneEnabled (Phase 1 RNNoise prep)
- syncMic now uses setMicrophoneEnabled(false/true) to mute/unmute the mic
  track in-place instead of tearing down and re-publishing via unpublishTrack().
  This eliminates WebRTC renegotiation on every mute cycle and preserves the
  Web Audio pipeline for future AudioWorklet injection (RNNoise WASM).
- Unify DmCallView mute/deafen with the primary syncMic path — removed direct
  setMicrophoneEnabled calls, added missing auto-mute/unmute on deafen toggle.
- Strip redundant applyConstraints from VoiceControls noise suppression toggle
  that was hardcoding echoCancellation:true/autoGainControl:true and fighting
  AudioManager's constraint pipeline.
2026-02-22 23:17:16 +01:00
Jannis Braun 708bdf5468 fix: auto-disable AEC during screen share to prevent Chrome voice ducking
Chrome's AEC uses getDisplayMedia audio as a reference signal and
aggressively ducks the microphone even when headphones are used.
This adds a screenShareActive flag to AudioManager that forces
echoCancellation off during screen share, plus Chromium-specific
goog* constraints as belt-and-suspenders.
2026-02-20 15:22:20 +01:00
Jannis Braun 25f72aef3b fix: address sender-side voice ducking with voice processing controls
Disable AGC by default to prevent Chrome from crushing mic sensitivity
when stream audio is playing. Add user-facing toggles for echo
cancellation, noise suppression, and auto gain control. Decouple voice
and stream audio by routing through ctx.destination instead of shared
compressor. Track mic stream generation to re-publish when settings change.
2026-02-20 15:00:08 +01:00
Jannis Braun 5c38f076d3 fix: eliminate voice ducking caused by rogue LiveKit audio elements
Add MutationObserver to neutralize LiveKit's re-attached <audio> elements,
mark our keep-alive elements with data-opencord, detach tracks on unsubscribe,
remove dangerous blanket .play(), and soften compressor to transparent limiter.
2026-02-20 07:03:41 +01:00
Jannis Braun 6f777f97ce fix: local stream video visibility and audio double-playback
Show local user's own screen share in their stream tile by bypassing
the isSubscribed check for local participants and auto-watching/unwatching
local streams on publish/unpublish. Eliminate audio double-playback by
detaching LiveKit's auto-attached audio elements so GlobalAudioRenderer
is the sole playback path, making attenuation and volume controls effective.
Fix GainNode leak in useAudioTrackPlayer cleanup paths.
2026-02-20 04:47:50 +01:00
Jannis Braun 1aa40cca15 feat: implement Discord-like stream widget system with separate tiles
Streams now appear as separate tiles in the voice grid alongside the
user's camera/avatar tile, matching Discord's model. Each stream tile
has independent volume, mute, watch/unwatch controls, quality badges,
and stream attenuation that ducks audio when someone speaks.
2026-02-20 03:59:28 +01:00
Jannis Braun a8656e6a3b feat: add screen share audio support
Pass audio: true to setScreenShareEnabled so the browser offers the
"Share audio" checkbox. Track ScreenShareAudio from remote participants
and play it through a dedicated audio element with the same volume
pipeline (including boost >100%).
2026-02-20 03:16:34 +01:00
Jannis Braun 140c7c38ab Milestone: First fully usable version with robust audio and screen sharing 2026-02-19 23:09:03 +01:00
Jannis Braun 3a441d5abd Fix room switching and audio consistency issues 2026-02-19 22:57:16 +01:00
Jannis Braun 95217f0343 Overhaul audio architecture for robust cross-browser reliability 2026-02-19 22:13:48 +01:00
Jannis Braun 8afd960a43 Fix Chrome audio reliability and prevent double audio 2026-02-19 20:49:59 +01:00
Jannis Braun 5ba64d2aea Fix double audio, Chrome reload silence, and restore input gain functionality 2026-02-19 19:43:41 +01:00
Jannis Braun 6deed231ab Ensure voice mute/deafen status persists across disconnects and re-joins 2026-02-19 18:02:00 +01:00
Jannis Braun 5fae725675 Fix missing isDeafened declaration in useLiveKit hook 2026-02-19 17:43:26 +01:00
Jannis Braun a18e087e24 Fix voice status persistence and global visibility across channels 2026-02-19 17:37:30 +01:00
Jannis Braun c7608e0655 Update voice control components and sidebar logic 2026-02-19 17:11:16 +01:00
Jannis Braun 0da4a530d6 feat: voice status visibility + sidebar persistence fixes
- Add WebSocket voice_status/voice_status_update events so mute/deafen
  icons are visible in the sidebar without joining the voice channel
- Server tracks voiceUserStates and includes them in the ready payload
- Re-register voice channel on WebSocket reconnect to prevent sidebar
  users from disappearing after idle timeout
- Re-broadcast deafen state to late joiners via LiveKit data channel
- Fix black grid tile when video stops (enabled-flag guards)
- Remove duplicate mute/deafen from VoiceControls (replaced with
  Video Quality + Noise Suppression)
- Fix missing users in sidebar voice list (identity matching + fallback)
2026-02-19 07:58:50 +01:00
Jannis Braun cd48261f54 feat: Voice UI overhaul — fix black tiles, focus mode, track lifecycle
- Fix black screen when streams end by checking track readyState === 'live'
- Add track 'ended' event listeners in VoiceUser for immediate fallback to avatar
- Add visible "Grid" button to exit focus mode (replaces undiscoverable double-click)
- Auto-focus screen sharers, auto-unfocus when they stop sharing
- Add LIVE badge, speaking glow shadow, screen share object-contain
- Add TrackMuted/TrackUnmuted/ActiveSpeakersChanged Room events
- Fix PiP video attachment with shouldShow dependency
2026-02-19 05:45:31 +01:00
Jannis Braun 2e309bf959 feat: Add Picture-in-Picture floating video + fix local track visibility
- New PictureInPicture component: draggable floating window that shows
  active stream when navigating away from voice channel, with LIVE badge,
  snap-to-edge, click-to-navigate-back
- Fix VoiceUser to check actual track presence instead of boolean flags
  for video rendering
- Add LocalTrackPublished/LocalTrackUnpublished events to useLiveKit
  so local screen share and camera show immediately in VoiceGrid
- Add pipCollapsed state to uiStore
2026-02-19 05:26:06 +01:00
Jannis Braun d88c72aa62 feat: Restore Overdrive v22 Soft-Launch Protocol
Restores the working smooth resolution ramp-up pipeline that starts at 360p
to clear the WebRTC handshake, then upgrades to target quality after 2 seconds.
2026-02-19 04:56:50 +01:00
Jannis Braun 35c1531038 feat: Finalize 720p60 Default Pipeline (v18)
- Removed 'Auto' preset from UI
- Defaulted video quality to 720p60 (Golden Config)
- Implemented 'Resolution Lock' with 3.5Mbps floor
- Hardened sidebar connection status sync
- Cleaned up diagnostic logging
2026-02-19 03:58:45 +01:00
Jannis Braun 7ae3e8c687 feat: Optimize WebRTC pipeline for 60fps screen sharing
- Implemented 'Overdrive' logic to force high bitrates on Chrome
- Fixed 'Auto' preset to default to stable 720p60
- Added persistent 'Triple-Kick' hammer to prevent bitrate throttling
- Fixed sidebar connection status sync
- Added comprehensive diagnostic logger
2026-02-19 03:34:20 +01:00
Jannis Braun 4f65ea9c86 fix: resolve LiveKit voice/video stability issues
- Guard Disconnected/ConnectionStateChanged event handlers against stale rooms:
  old room events no longer nuke the new room's state (root cause of buttons
  failing, mute getting stuck, DUPLICATE_IDENTITY cascades)
- Reset media state (isMuted/isCameraOn/isScreenSharing) on connect to prevent
  desync after reconnects
- Add voiceStates to WS ready payload so users see who's in voice on page load
- Wire VoiceControls buttons to check getActiveRoom() before SDK calls
- Guard ChannelSidebar against re-joining the same voice channel
- Switch LIVEKIT_URL to wss://nova.ddns.net/livekit for HTTPS secure context
  (required for getUserMedia in Safari)
2026-02-18 07:52:25 +01:00
Jannis Braun 5ef502f2e3 fix: repair invite links, social features, messaging + Discord UI overhaul
Phase 1 - Feature Repair:
- Fix member kick/leave: add missing db.delete() call in servers.ts
- Stabilize invite codes: return existing code instead of regenerating
- Fix user search: use LIKE instead of exact match in social.ts
- Wire DM button on FriendsPage to create/navigate to DM channels
- Add cancel outgoing friend request (DELETE endpoint + frontend)
- Add accept/decline friend request actions with WS real-time events
- Fix replyToId persistence in message creation
- Hydrate reactions and replyTo in message queries
- Add joinByCode to API client and serverStore
- Add friend_request_received/accepted WebSocket events

Phase 2 - Discord UI Overhaul:
- Remove stray borders between layout columns
- Replace shadow-sm with shadow-header on content headers
- Replace all bg-gray-*/text-gray-* with Discord color tokens
- Ensure flat color contrast (#1E1F22, #2B2D31, #313338)

Testing:
- Set up vitest + @testing-library/react + jsdom
- Add 17 tests across InviteModal, JoinServer, FriendsPage (all passing)
- Fix vite resolve.extensions to prefer .tsx over stale .js files
2026-02-18 05:34:45 +01:00