- Add "MEMBERS" category header and drag-to-resize handle on the member
list section with height persisted to localStorage (min 80px, max 40vh)
- Add collapsible "DIRECT MESSAGES" section between channels and members
showing up to 5 online users as DM targets
- Remove members toggle button from ChatHeader (members are always in sidebar)
- Clean up onToggleMembers from ChatHeaderOptions and ChatArea
- Update unit and E2E tests to reflect the removed toggle
Add loadCollapsedCategories/saveCollapsedCategories persistence layer
keyed by server name. toggleCategory now auto-saves to localStorage.
On sidebar mount, collapsed state is loaded for the current server so
each server remembers its own collapsed/expanded sections across sessions.
Build DM conversation list from online members instead of empty array.
Each online member (excluding current user) appears as a DM target with
status indicator and "No messages yet" placeholder. Selection sets the
active DM user and keeps sidebar in DM mode. Sidebar re-renders on
member presence changes and active DM user switches.
Also loads per-server collapsed category state on sidebar mount.
The onSwitch callback now stores the target host in sessionStorage and
calls clearAuth(), which triggers the existing logout flow (voice leave,
WS disconnect, navigate to ConnectPage). On the ConnectPage side, a new
selectServer() method auto-fills the host and loads saved credentials.
The onAddServer callback similarly disconnects and lands on ConnectPage
so the user can add a new server profile.
Mount the MemberList component below the channel list in channel mode,
with admin callbacks (kick, ban, role change) and visibility toggled
via the existing ChatHeader member list button / uiStore subscription.
Tracked as channelModeExtras for proper cleanup on mode switch.
- Fix test regression: update theme option count 3→4 and midnight index [1]→[2] for neon-glow insertion
- Fix stale Voice & Audio tests: update select count 3→4 (video quality + device added), replace sensitivity slider test with meter bar render check
- Consolidate theme systems: applyTheme() in helpers.ts now delegates body class + persistence to applyThemeByName() so both paths write to owncord:theme:active
- Add input validation to loadCustomTheme(): reject non-object payloads and entries missing name/colors fields
- Remove dead CSS tokens --strip-width and --members-width from tokens.css
- Extend 800px responsive breakpoint to cover .unified-sidebar alongside .channel-sidebar
- Import restoreTheme from @lib/themes in main.ts
- Call restoreTheme() after applyStoredAppearance() so the body
class (theme-<name>) is applied before first render
- Add "neon-glow" entry to THEMES in settings/helpers.ts
- Update applyTheme() to also toggle theme-<name> class on document.body
- Update UiState and setTheme() in ui.store.ts to include "neon-glow"
- Delete ServerStrip.ts and its unit test
- Remove all .server-strip / .server-icon / .server-separator CSS rules
- Remove @media (max-width: 600px) responsive rule for .server-strip
Remove serverStripSlot and memberListSlot from the app layout in
MainPage.ts. Remove standalone MemberList creation, mounting, and
visibility subscription from ChatArea.ts. The member list is now
part of the unified sidebar (Task 8).
- Remove ServerStrip import and server strip slot from sidebar layout
- Add unified sidebar header with OC icon, server name, and online count
- Subscribe to uiStore.sidebarMode to swap between ChannelSidebar and
DmSidebar content views within the same DOM slot
- Wire UserBar onDisconnect to open QuickSwitchOverlay with saved profiles
- Add openQuickSwitch to SidebarAreaResult for external disconnect flow
- Add unified sidebar CSS classes to app.css
- Voice widget and user bar remain visible in both sidebar modes
Adds dmMode option (hides member-list toggle), exposes hashEl in refs,
and adds updateChatHeaderForDm helper that swaps # for @, sets username
and status when a DM recipient is active.
Adds optional onBack/serverName fields to DmSidebarOptions. When onBack
is provided, a clickable back header is rendered above the search bar
with arrow, title and subtitle. Supporting CSS added to app.css.
- Change UserBarOptions from Record<string,never> to interface with optional onDisconnect callback
- Render a "Switch server" button in the controls area when onDisconnect is provided
- Add log-out icon (Lucide path data) to icons.ts for the button
Implements lib/themes.ts with listThemeNames, applyThemeByName,
getActiveThemeName, saveCustomTheme, loadCustomTheme, deleteCustomTheme,
exportTheme, and restoreTheme. Built-in themes applied via body CSS
classes; custom themes applied via inline CSS variable overrides.
Active theme persisted to localStorage. All 7 unit tests pass (TDD).
Extends UiState with sidebarMode ("channels" | "dms") and
activeDmUserId (number | null). Adds setSidebarMode (which auto-clears
activeDmUserId when switching back to channels) and setActiveDmUser
actions. All three new tests pass (TDD: RED → GREEN).
Split layout redesign with deep indigo gradient left panel,
OC neon glow logo (orange→pink→purple→cyan), accent stripe
server cards, and full motion animations.
- Left panel: gradient background with ambient glow spots
- OC logo: SVG lettermark with Gaussian blur glow pulse
- Server cards: compact rows with purple accent stripe
- Inputs: 12px border-radius with focus glow ring
- Button: gradient with box-shadow and shimmer on hover
- Animations: panel slide-in, logo pulse, card stagger,
button shimmer, all respecting prefers-reduced-motion
The Rust save_credential command was accepting but ignoring the
password parameter (_password). Now stores it in the Windows
Credential Manager JSON blob alongside username and token.
DPAPI encrypts the blob at rest, tied to the Windows user —
plaintext is never written to disk.
Also wire the checkbox state to the profile's rememberPassword
field so it persists across sessions. Previously it was
hardcoded to false on every login.
- Collect stats from both publisher AND subscriber PeerConnections
(RTT is typically on the subscriber PC in LiveKit's SFU model)
- Accept candidate-pair with valid RTT regardless of state
(not just "succeeded" — subscriber may report "in-progress")
- Round raw byte values in formatBytes to avoid 15-digit floats
- Use max instead of sum for totalUp/totalDown across candidate
pairs to avoid double-counting across PCs
Add a signal-bars icon + ping text to the voice widget header
that shows real-time connection quality. Clicking it expands a
transport statistics pane with outgoing/incoming rates, packet
counts, RTT, and session totals.
- New lib/connectionStats.ts: polls WebRTC RTCPeerConnection
stats every 2s, computes quality level from RTT thresholds
- Signal icon with 4-tier coloring: green (<100ms), yellow
(100-200ms), red (>200ms), with 1-4 bars lit
- Expandable stats pane between header and controls
- Auto-starts/stops poller on voice connect/disconnect
- New createSignalIcon() in icons.ts for per-bar coloring
- getRoomForStats() export from livekitSession.ts
- Add stream quality selector (Low/Medium/High/Source) in Voice & Audio
settings with per-preset bitrate and resolution for camera + screenshare
- Use createLocalVideoTrack/createLocalScreenTracks + publishTrack for
explicit encoding control (bypasses LiveKit conservative defaults)
- Source quality: 8Mbps camera, 10Mbps screenshare, no adaptive/dynacast
- Nuclear mute: fully unpublish mic track when muting, re-publish on
unmute — guarantees SFU has no audio to forward
- Listen for LocalTrackPublished to re-enforce mute on renegotiation
- Store manually published tracks for explicit unpublish on disable
- VAD updatePipelineGain respects mute/deafen state
- Widen channel sidebar from 240px to 260px, add overflow handling
so voice user icons don't clip and cause horizontal scrollbar
- Remove invalid LiveKit server-side room config fields
- Deafen now also mutes the local microphone (privacy fix)
- Mute/deafen zero the audio pipeline GainNode to guarantee silence
when the replaced sender track bypasses LiveKit's track disable
- Send voice_leave on failed auto-reconnect to prevent ghost states
- Gate voice_join on LiveKit availability (reject if h.livekit == nil)
- IP-restrict /api/v1/livekit/webhook to admin CIDRs
- Warn at startup when LiveKit API keys are auto-generated (ephemeral)
- Fix connecting guard: move pending-join dispatch outside finally block
- Add volume slider + mute button overlay on remote video tiles
- Document token refresh limitation for 4h+ sessions
- Fix TS2306 in rnnoise-worklet test (ts-expect-error for worklet import)
Two specs for the video/screenshare experience:
1. SCREENSHARE-AUDIO.md — system audio capture + per-tile mute
2. VIDEO-FOCUS-MODE.md — Discord-style opt-in viewing with focus layout
- Add enableScreenshare/disableScreenshare to LiveKitSession with
proper LiveKit track publishing, error handling, and WS notification
- VoiceWidget screenshare button now shows active state (red highlight,
icon swap, aria-pressed) matching mute/deafen/camera pattern
- VideoModeController activates video grid for screenshare (not just
camera), with local self-view tile using ID offset to avoid collision
- MainPage voice store subscription now watches screenshare state
changes to trigger checkVideoMode automatically
- Reset localScreenshare on leaveVoice to prevent stale button state
- Add auto-reconnect on unexpected LiveKit disconnect (2 attempts with
3s delay, fresh token request on success)
- Fix pre-existing missing reapplyAudioProcessing mock in settings test
- 8 new tests covering screenshare button, video grid activation,
tile lifecycle, and state cleanup
Drag LEFT = easier for mic to pass (high sensitivity),
drag RIGHT = harder (low sensitivity). Inverts both the
threshold indicator position and pointer-to-value mapping.
Co-Authored-By: claude-flow <ruv@ruv.net>
- Add --autoplay-policy=no-user-gesture-required to WebView2 config
so remote participants' audio plays immediately on join (desktop app
doesn't need browser autoplay restrictions)
- Add AudioPlaybackStatusChanged handler with click-to-unlock fallback
for browsers that still block autoplay
- Replace broken VAD implementation that used setMicrophoneEnabled/
mediaStreamTrack.enabled (both fought LiveKit's track lifecycle) with
a unified GainNode audio pipeline:
rawMic → AnalyserNode (VAD) → GainNode (volume × gate) → sender
- VAD now gates by setting gain=0 instead of touching the track —
analyser always sees real audio, no stale track references
- Merge input volume and VAD into single pipeline (always active)
- Add voice settings UI: draggable sensitivity threshold on mic meter,
input/output volume sliders (0-200%), audio processing toggles
Co-Authored-By: claude-flow <ruv@ruv.net>
Server fixes:
- voice_leave: broadcast voice_leave even on DB error so peers don't see ghost users (H1)
- migrate: record migration inside transaction for atomicity (H2)
- password: use init() with panic for dummyHash to catch bcrypt init failures (M1)
- password: replace //nolint:errcheck with explicit _ = discard (M2)
- handlers: clarify edit permission comment, fix error message wording (M3)
- auth_handler: distinguish duplicate username (400) from DB error (500) (M4)
Client fixes:
- media: revert YouTube oEmbed to browser fetch — no need to disable cert verification (C1)
- messages.store: fix prependMessages cap to keep newest messages, not oldest (H5)
- ChannelSidebar: ref-count globalDragAc to prevent multi-instance teardown race (H6)
- attachments: replace console.error with project logger (M6)
- ws: clarify lastSeq reset comment to match actual behavior (M5)
CRITICAL:
- Add AuthMiddleware + rate limiting to /livekit/* proxy route (was unauthenticated)
- Remove well-known default LiveKit credentials from source; auto-generate unique
random keys on first run so voice works out of the box securely
- Reject the old "devkey"/"owncord-dev-secret-key-min-32chars" in NewLiveKitClient
HIGH:
- Add 5s timeouts to RemoveParticipant/ListParticipants SDK calls (goroutine leak)
- Fix config.yaml default file permissions from 0644 to 0600
- Fix voice store desync on unexpected LiveKit disconnect (phantom UI state)
- Add in-flight guard to handleVoiceToken (race on rapid channel switch)
- Fix lightbox listener leak on rapid reopen (orphaned mousemove/mouseup/keydown)
- Fix allTracked WeakRef set unbounded growth in media-visibility
- Replace debug console.log with createLogger in embeds.ts
- Stop persisting password in Windows credential blob (only token needed)
- Detach existing audio elements before attaching to prevent double playback on reconnects
- Remove webAudioMix to eliminate Web Audio overhead compounding with multiple participants
- Use participant.setVolume() for full 0-200% per-user volume range
- Add pli_throttle and active_loopback_prevention to LiveKit server config
- Bump version to 1.3.0
Server: suppress errcheck on deferred Close() calls, discard
resp.Body.Close error, remove unused voiceSpeakersPayload type
and buildVoiceSpeakers func.
Client: add unit tests for os-motion, livekitSession, and
safe-render to bring coverage from 74.16% to 76.06% (threshold 75%).