Commit Graph
59 Commits
Author SHA1 Message Date
devsyncwrld c70b0095a9 feat(voice): jump to the call from the voice panel
The channel name under 'Voice Connected' was a plain div. Making it navigate
needed more than an onClick: voiceStore never recorded which space the call
was in, and spaceStore.channels only holds the space currently being viewed —
so after navigating away the call's channel was unresolvable, which is also
why the label degraded to a generic 'Voice Channel'.

Capture space and channel name at join time (the only moment they are
reliable) and use them for both the label and the jump. Covers space calls
and DM calls.
2026-08-31 11:40:45 -03:00
Jannis Braun dbb9b2c34b wip(mobile): in-progress mobile + voice polish
Snapshot of in-progress work parked here so group-DM-polish can land
cleanly on main. Touches MainContent + mobile-ui.md which overlap with
group-DM-polish; rebase onto post-merge main and resolve conflicts on
those two files manually.

Files: MainContent, MessageInput, MobileVoiceFullScreen (+test), StreamTile,
VoiceUser, useLiveKit, useVisualViewportInset, AudioManager, voiceStore,
voice utils, mobile-ui.md, voice.md, mobile-parity handoff doc.
2026-05-10 21:26:40 +02:00
Jannis Braun 005559d95e feat(sounds): add streamWatchers map + recordStreamWatch/clearStreamWatchers/evictWatcher 2026-04-28 14:30:45 +02:00
Jannis Braun a3bd0313a8 feat(sounds): add messageSoundAllChannels preference to voiceStore 2026-04-28 14:26:41 +02:00
Jannis Braun 486448607e feat(voice-store): add pruneStaleDevices to clear stale persisted device ids 2026-04-27 20:08:42 +02:00
Jannis Braun 770a77a473 feat(voice-store): add cameraDeviceId persisted field 2026-04-27 20:07:23 +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 626fbfdba8 fix(federation): address code review findings for FED-009
- Resolve homeUserId from DB in sendFederatedCallStart/End (not raw userId)
- Clear existing timeout in createFederatedCall before overwriting
- Clear federatedCallToken/Url in leaveVoice and handleForceDisconnect
- Remove unnecessary `as any` cast in relay processor
2026-03-31 23:58:27 +02:00
Jannis Braun 24571edc34 feat(federation): client voice store handles federated call data (FED-009) 2026-03-31 23:40:11 +02:00
Jannis Braun 46c3838193 fix: reset hwOverdrive in all voice cleanup paths
Add hwOverdrive: false to resetSession(), leaveVoice(), and
handleForceDisconnect() store resets. Add deactivateHwOverdrive()
to disconnect callback and cleanup effect in useLiveKit.
Ensures "resets when stream ends" contract holds for edge cases
like server shutdown, identity collision, and logout.
2026-03-24 04:00:37 +01:00
Jannis Braun 94e955989a feat: remove codec from ScreenShareConfig, add transient hwOverdrive
VP9 is now always the default codec, so the persisted codec field is
removed. hwOverdrive is a transient boolean (excluded from partialize)
that drives hardware H.264 on demand without surviving page reload.
Persist version bumped 12→13 with migration to strip stale codec field.
2026-03-24 03:45:10 +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 8897a5894c feat: smart codec defaults + mid-stream codec switching
- Gaming mode defaults to H.264 (NVENC hardware encoding, zero CPU)
- Text mode defaults to VP9 (better compression, CPU is free)
- Changing mode auto-updates codec to smart default
- User can manually override codec via UI pills
- Changing codec mid-stream auto-restarts the stream (~1s interruption)
2026-03-23 23:35:33 +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 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 063d73cff1 feat(voice): add setMuted() idempotent setter and pttActive state for PTT keybinds 2026-03-22 20:23:23 +01:00
Jannis Braun a02b098fa7 feat: widen ScreenShareConfig types, bump persist to v12 2026-03-21 23:22:49 +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 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 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 07ef49eac0 feat: channel permissions UI, DM Sans font, private channel filtering, migration fix
- Rewrite ChannelSettingsModal with full tri-state permission override UI
  for roles and members (allow/neutral/deny per permission bit)
- Switch font from Inter to self-hosted DM Sans (woff2 variable fonts)
- Add client-side VIEW_CHANNEL filtering in ChannelSidebar for private channels
- Broadcast isPrivate flag on channel override changes
- Fix voice permission bit migration: gate behind persistent flag to prevent
  repeated re-runs that stripped STREAM from @everyone roles
- Add speakingUserIds set to voice store for efficient user-level lookups
- Clear current channel view when a channel is deleted
- Move .glass-strip to @layer utilities for proper CSS specificity
- Simplify avatar initials font size to proportional formula
2026-03-13 02:47:32 +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 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 c8e2945c07 feat: two-step registration with avatar/color picker, fix focus ring clipping
Refactor RegisterPage into a two-step flow: credentials first, then
personalization (display name, avatar upload, avatar color). Replace the
dual-panel sliding layout with conditional rendering and CSS keyframe
animations to eliminate overflow-hidden clipping of focus rings.

Supporting changes:
- Server accepts avatarColor on registration
- Auth store resets all user-scoped stores on login/register/logout
- Voice store gains resetSession() for full session cleanup
- Sync presence status to federated instances
- Propagate presence_update to socialStore regardless of origin
2026-03-11 03:43:20 +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 3d37c70e50 feat: grey badges for local-only actions and sidebar unwatched camera state
Use neutral bg-white/20 for unwatched camera badge in grid tile instead
of bg-accent-rose/90, matching the local mute badge convention. Show
crossed-out camera icon in channel sidebar when a remote user's camera
is locally unwatched. Add local mute badge to both grid tile and sidebar.
2026-03-10 03:19:15 +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 db1909d785 fix: resolve federated identity checks for server mute/deafen pipeline
- The client now dynamically resolves the user's federated identity via `getMyUserIdForOrigin` when evaluating incoming `voice_server_muted` and `voice_server_deafened` events. Previously, the client incorrectly compared the remote event's federated `userId` against the local `authStore` home `userId`, causing federated users to silently drop restriction events.
- Client-side mic/deafen toggles (`toggleMic`, `toggleDeafen`) now accurately evaluate the user's origin-specific ID against the restriction sets, preventing federated users from bypassing locks.
- UI state selectors (`VoiceControlBar`, `ChannelSidebar`) now compute `myOriginId` to correctly render the yellow server-lockdown indicators for cross-instance users.
2026-03-09 19:54:15 +01:00
Jannis Braun 6a12fe2024 fix: rearchitect server mute/deafen pipeline to scope restrictions by spaceId
- Replaces global `userId` tracking with `spaceId:userId` composite keys across both backend and frontend, fixing the issue where server-muting a user in one space bled into others.
- Modifies client-side `ready` event hydration to merge voice states per-origin instead of completely overwriting the store, preventing federated connections from wiping out home instance mutes.
- Excludes server voice restrictions from `zustand/persist` so stale client caches don't override the server's authority on reload.
- Fixes React component reactivity by using reactive store selections for `spaceId` instead of imperative `getState()` calls, ensuring UI lockdown indicators accurately reflect the initial websocket handshake.
2026-03-09 19:44:37 +01:00
Jannis Braun c2ddfe0cb7 fix: persist server mute/deafen state across reloads and prevent client bypass
Server-side: add DB persistence for voice restrictions (schema, migration,
ready payload, cleanup on leave). Client-side: fix four bugs that wiped or
bypassed server restriction state — leaveVoice() no longer clears global
restriction Sets, voice_state_update leave no longer drops amber icons,
toggleMic/toggleDeafen now guard against server restrictions, and force-mute/
deafen uses direct setState instead of fragile toggle calls.
2026-03-09 18:36:17 +01:00
Jannis Braun f6cdfe993f fix: server-deafen now shows amber mute icon across all UI surfaces
When a moderator server-deafens a user, the implied mute is server-imposed
and should display amber (not red) everywhere. Updated VoiceControlBar,
ChannelSidebar, VoiceChannel, and VoiceUser to check isServerDeafened
alongside isServerMuted for amber color and cursor-not-allowed state.

Also includes smart mute/deafen toggle logic (Discord-style coupling),
server-side enforcement of mute/deafen bypass, and cleanup of server
voice state on user departure.
2026-03-09 17:12:00 +01:00
Jannis Braun e2c18ad2b0 feat: bans system, voice moderation, and federated space settings fixes
Add ban/unban functionality with BansPanel in space settings, voice
moderation context menu (mute/deafen/disconnect), and fix federated
space settings panels to use origin-aware API client. Show domain
indicators for federated members in MembersPanel.
2026-03-09 15:56:46 +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 d18d9c51f7 fix: resolve duplicate voice user in federated channel sidebar
Use origin-aware user ID resolution for optimistic voice add/remove
instead of always using the home instance ID. Adds getMyUserIdForOrigin
resolver (same pattern as getApiForOrigin) so joinVoiceChannel and
leaveVoice use the correct federated user ID. Also fixes WS reconnect
voice re-registration, invite URL for remote servers, and chatStore
early-load guard for unmapped channels.
2026-03-07 14:33:37 +01:00
Jannis Braun 09b9d1d55a feat: enable auto gain control by default for new users 2026-03-05 00:19:39 +01:00
Jannis Braun a5073f89fd fix: optimistic voice state so user appears in sidebar immediately
Previously, the voice channel sidebar only showed users after a server
round-trip (voice_state_update broadcast). After a deploy/reconnect,
this left the user invisible in the sidebar despite being connected.
Now joinVoiceChannel, leaveVoice, and the WS ready handler all
optimistically update voiceUsers for the local user immediately.
2026-03-04 21:48:39 +01:00
Jannis Braun 64fd8edfc9 feat: federation batch 1+2 — bug fixes and password enforcement
Batch 1 — Bug fixes:
- Fix stale voice state on remote reconnect (clearVoiceUsersForOrigin)
- Fix logout not cleaning remote servers from store
- Fix reply-to asset normalization for remote messages
- Fix HTTPS hardcoded in autoConnectAll (store full origin, legacy fallback)

Batch 2 — Password enforcement + replication flow:
- Add connectToRemote() with home password verification before remote auth
- Auto-cascade: verify home password → register on remote → login fallback
- Replace Register/Login tabs with single password field in ConnectedInstances
- Add DifferentPasswordError for typed fallback-login UI transition
- Fallback login form shown only when remote has different password
2026-03-04 01:51:38 +01:00
Jannis Braun e219229b63 feat: add Backspace design prototype and rebrand from Opencord
- Add Backspace-design-prototype.html: finalized "Aether Drift" design
  language with warm matte surfaces and subtle frosted glass accents
- Update CLAUDE.md with DESIGN SYSTEM section and remove Discord clone references
- Rename all Opencord references to Backspace across the full codebase
- Archive outdated design experiments and Discord-specific assets
- Add science-backed accessibility fallback (prefers-reduced-transparency)
2026-03-01 21:26:57 +01:00
Jannis Braun 2186235d65 feat: manual bitrate override slider for screen share settings
- Add customBitrateKbps to ScreenShareConfig (null = auto matrix lookup)
- Slider in Stream Settings popover: 500 kbps–20 Mbps, step 500 kbps
- "Reset to Auto" clears override back to preset-derived bitrate
- Live updates via existing applyOverdrive() pipeline on active streams
- Persist version 5 → 6 with migration
2026-02-26 00:28:20 +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 5e34b39b78 fix: broadcast camera & screen share status via WebSocket for sidebar visibility
Camera and LIVE badges in the channel sidebar were only visible to users
who had joined the same LiveKit room. Widen the voice_status WS event
from {isMuted, isDeafened} to {isMuted, isDeafened, isCameraOn, isScreenSharing}
so all server members see camera/screenshare indicators without joining voice.
2026-02-23 20:07:42 +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 5747267a6b refactor: remove browser NS toggle, default RNNoise on, rework voice settings
- RNNoise (AI Noise Suppression) now enabled by default for all users
- Remove redundant browser Noise Suppression toggle from VoiceControls
  and UserSettings — AudioManager handles it automatically as fallback
- Add AI Noise Suppression toggle to UserSettings panel
- Rename toggleRnnoise → setRnnoiseEnabled for clearer API
- Store migration v3→v4: enable RNNoise for existing users
- Keep Echo Cancellation and Auto Gain Control (orthogonal features)
2026-02-23 00:49:42 +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 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 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 3a441d5abd Fix room switching and audio consistency issues 2026-02-19 22:57:16 +01:00