InstanceNotConnectedError and InstanceDisconnectedError now open the
ConnectInstanceModal instead of being swallowed silently. After the user
connects/reconnects, the friend request is retried automatically and a
success toast confirms both the connection and the sent request. All
other friend action errors (remove, cancel, accept, decline) now surface
as warning toasts instead of being silently discarded.
stopScreenShare() resets hwOverdrive: false, which clobbers the
user's intent when switching VP9 → H.264 HW mid-stream. Save
and restore the flag around the restart.
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.
- buildScreenShareOptions reads hwOverdrive from voiceStore instead of config.codec; both VP9 and H.264 paths now always get a VP8 SIMULCAST backup
- startScreenShare activates SDP override before WebRTC negotiation and deactivates on failure
- stopScreenShare and handleScreenShareUnpublished both deactivate the override and reset hwOverdrive: false
- scheduleEncoderDetection checks encoderImplementation in WebRTC stats after 4s and shows a warning toast if OpenH264 (software) is detected
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.
AcceleratedVideoEncoder only enables VA-API encoding on Linux.
Windows/macOS hardware H.264 encoding in Chromium's WebRTC is not
reliably controllable — VP9 via libvpx remains the recommended codec.
LiveKit uses screenShareEncoding (not videoEncoding) for screen share
tracks. Without it, both primary and backup fall back to the default
ScreenSharePresets.h1080fps15 (15fps). Primary was masked by our
overdrive mechanism, but backup had no overdrive and was stuck at 15fps.
- 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
- 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
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.
Calling e.preventDefault() on touchstart inside [data-context-menu]
elements prevented the browser from synthesizing click events
(touchstart → touchend → click chain). This broke tapping on spaces,
channels, DMs, and every other surface with data-context-menu.
Text selection prevention is already handled by CSS user-select: none
on [data-context-menu] elements — the JS preventDefault was redundant
and destructive. Reverted touchstart listener back to passive.
- 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)
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.
- Gaming mode uses H.264 primary (hardware NVENC encoding, zero CPU
impact on games). Text mode uses VP9 primary with H.264 backup
and SIMULCAST policy for Safari compatibility.
- Fix native mode starting at 30fps by decoupling frameRate constraint
from resolution constraint in both screenShare.ts overdrive and
useLiveKit.ts updateActiveTracks.
- Filter paused backup codec tracks in Connection Info stats so dead
0kbps entries don't show alongside active codec tracks.
MobileVoiceFullScreen and MobileVoiceMiniBar were missing voice_leave
(and dm_call_end) WS messages in their disconnect handlers, leaving
the server showing stale voice state. Now matches VoiceControls pattern.
iOS triggered both the app context menu and native text selection overlay
on long-press. Three-layer fix: CSS user-select:none on buttons/[role=button]/
[data-context-menu] elements; non-passive touchstart listener with conditional
preventDefault on context-menu targets; removeAllRanges() at timer fire to
clear any residual selection. Added data-context-menu to all mobile surfaces
that use onContextMenu handlers.
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.
Brings mute/deafen state resolution in line with VoiceChannel.tsx:
- Use LiveKit participant state (then WS fallback) for mute/deafen instead of only voiceUserStates
- Use local isMuted/isDeafened store values for the current user (self)
- Add spaceMutedUserIds, spaceDeafenedUserIds, permissionMutedUserIds checks
- Render deafened indicator (headphones-slash) when user is deafened vs muted
- Add LiveKit participant username and userId as terminal fallbacks in display name chain
Folder-aware layout resolution, folder icons in space strip,
bottom sheet folder flyout with rename/color/ungroup management.
Full folder CRUD via context menus: create, move to, remove from.
Add onContextMenu handlers for space icons (invite, folder management,
transfer, leave), channel items (settings, delete), and category headers
(settings, delete). Includes full folder management handlers.
Use channelToSpaceMap.get(channelId) for permission checks instead of
currentSpaceId from the navigation store. On mobile, UI navigation
decouples from the active voice session.
Add -webkit-touch-callout: none on mobile viewports to prevent the
native iOS copy/lookup popup from racing with our context menu.
Clear any text selection when the long-press timer fires.
Add useGlobalLongPress to ContextMenuRenderer that detects 500ms hold
with <10px movement and dispatches a synthetic contextmenu event on the
touched element. All existing onContextMenu handlers (spaces, channels,
voice users, messages, etc.) now work on touch devices automatically.
Remove per-message useLongPress integration from Message.tsx since the
global handler covers it.
ResizeObserver and media load callbacks were using isNearBottomRef
(5000px threshold) to decide whether to auto-scroll to bottom. On
mobile, inertial flick scrolling hadn't moved far enough to clear
that threshold before the observer fired, snapping the user back.
Now these callbacks check actual scroll distance at fire time with
a tight 150px threshold — only auto-scrolls if truly at the bottom.
1. Chat scroll: add flex flex-col to MobileChatScreen messages wrapper
so MessageList's flex-1 actually works (parent must be flex container)
2. Instance General panel: add fetchInstanceSettings/fetchStreamingLimits
to MobileInstancePanel mount (mirrors desktop InstancePanel useEffect)
3. Friends activity: add Activity tab to mobile FriendsPage showing
friends grouped by activity status (active, online, offline)