When a WS connection receives its ready event, push cached myActivities
to that instance so federated users see activities immediately without
waiting for the next debounce cycle.
Switch pushActivities and setShowActivity from wsSend (home-only) to
wsSendAll (all instances). Add myActivities cache for re-push on late
connections.
Inserts a new tier 3 between DM member lookup and identity-only fallback.
When the current space's member list doesn't contain the voice participant
(user navigated to a different space/instance), the hook now falls back
to the cached User object from ParticipantInfo. This preserves avatar,
avatarColor, displayName, and homeUserId for federated users in PiP.
The isLocal branch was falling through to cachedUser.id when
homeUserId was null, diverging from the null-for-home-instance
contract used by the memberMatch and carry-forward branches.
Avatar.tsx already falls back to user.id internally.
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.
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)
Previously, voice cleanup only happened in finalizeDisconnect after a
5-second grace period, and only when ALL connections closed. If a user
was logged in on multiple devices and closed the one in voice, the
voice state was never cleaned up — creating a permanent ghost in the
channel sidebar.
Now removeConnection checks if the closing socket is the voice-active
one (tracked via voiceWs map) and immediately cleans up voice state,
broadcasts the leave event, and notifies remaining tabs.
Adds device-switch guardrail to handleVoiceJoin: if the user already
has a voice session on a different WebSocket, that old socket receives
a voice_disconnected/displaced event before the new session takes over.
Also binds voiceWs on the caller socket in handleDmCallStart and on
the acceptor socket in handleDmCallAccept.
Create useLongPress hook with 500ms delay, 10px movement cancellation
(Euclidean distance), and ghost click suppression. Integrate into
Message component to trigger the existing context menu bottom sheet
on touch devices.
Create MobileMembersScreen that renders space members grouped by role
without depending on desktop memberListOpen flag or hidden md:block.
Add members button to voice full screen header for space channels.
Replace inline InstancePanel render with MobileInstancePanel that shows
a navigation list for General, Streaming, Storage, and Users sub-panels.
Each sub-panel pushes onto the mobile screen stack with MobileScreenHeader.
Add overflow-hidden to MobileScreenStack stacked screen wrapper to
establish block formatting context. Add min-h-0 to MobileChatScreen
messages wrapper to override flex min-height: auto default.
Adds uploadWithProgress() using XMLHttpRequest for real-time upload
progress events. MessageInput now shows per-file progress bars with
percentage overlay during upload. Failed uploads show toast warnings
with the specific error instead of silently failing. Upload timeout
raised to 10 minutes for large files.
The 500 MB cap was arbitrary. Self-hosted admins should decide based
on their own disk space. Global multipart ceiling raised to 5 GB,
validation cap to 5120 MB.
Replaces the layout-shifting error banner at the bottom of the panel
with non-intrusive toast notifications. Upload limit save, cleanup
success, and all error feedback now use the existing toast system.
Cleanup preview results stay inline as expected contextual responses.
Reads max_upload_size_bytes from instance_settings per-request and
passes it to request.file() so Fastify kills the stream at the
admin's configured limit. Checks file.truncated to properly reject
files that exceed the limit instead of saving corrupted data.
Adds cleanupOldMedia() to the storage janitor and a new endpoint
POST /api/admin/storage/cleanup-media { maxAgeDays, dryRun }.
Deletes chat attachments older than the specified threshold while
preserving profile images.
Rebuilds the attachments table with ON DELETE CASCADE on dm_message_id.
Dangling records are excluded during copy — their files become standard
disk orphans detectable by the storage janitor. Also updates the raw SQL
in index.ts to include width/height/duration columns for fresh installs.
Adds getDanglingAttachments() to find attachment records whose
messageId/dmMessageId points to a deleted message. Updates stats
to show dangling count and cleanup to delete them.
Replace the give-up-after-3-retries path with a persistent background
scheduler: 10 retries every 30s (5 min), then 12 retries every 5 min
(1 hr). Active timers are keyed by origin so rapid password changes
cancel and replace the previous loop. All timers are cleared on logout
via clearPasswordSyncTimers() called from instanceStore.reset().
Add `pendingSyncOrigins: string[]` to InstanceState so components re-render
when pending password-sync flags change. Hydrate from localStorage on
autoConnectAll, clear on reset, and remove the localStorage-only read path
from `hasPendingPasswordSync`.
When a federated user's local password hash is stale (e.g. they changed
their password on the home instance and sync failed), the login handler
now falls back to verifying credentials against the home instance. If the
home instance accepts the password, the local hash is silently updated
without touching passwordChangedAt, so existing valid JWTs remain valid.
Add installers/ to .gitignore and remove from tracking — binaries
should not be in git.
Also includes:
- Re-push Electron activities after WebSocket reconnect (sleep/wake)
- Merge activity map instead of replacing to preserve local state
- Nori mascot design plan