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).
Use titleBarStyle: 'hidden' + titleBarOverlay on Win/Linux to remove the
ugly native title bar and menu bar while keeping OS-rendered min/max/close
buttons. Hidden Edit menu preserves keyboard shortcuts (Ctrl+C/V/X/Z/A).
Title bar drag region and separator line rendered via web frontend, with
colors matching the Aether Drift design system.
Also adds electron-builder metadata (description, author, homepage,
artifactName) and multi-size icons for cross-platform builds.
- Custom screen share picker for Electron (ScreenSharePicker.tsx) with
Aether Drift design: glass-bubble footer, adaptive grid, pill tabs,
border-based selection (avoids overflow clipping), hover brightness
- Instance URL picker so Electron connects to any Backspace server
- System audio loopback on Windows/Linux via desktopCapturer callback
- macOS: video-only callback (OS blocks system audio capture)
- IPC bridge for screen source enumeration and selection
- Purge stale service worker caches on Electron launch
- Dev/prod URL auto-detection (Vite 5173 in dev, server 3000 in prod)
- Typed IPC bridge via preload (notifications, badge, window controls, updates, deep links)
- Native OS notifications via NotificationController with window focus suppression
- Auto-update via electron-updater with UpdateToast UI
- Deep linking (backspace:// protocol) for macOS and Windows/Linux
- Window state persistence (position, size, maximize across restarts)
- Tray icon with graceful fallback when icon asset missing
- Suppress PWA service worker polling/reloads inside Electron
- Platform detection layer (isElectron, getElectronAPI)
- Root workspace scripts (dev:desktop, build:desktop)
- Document BACKSPACE_URL and BACKSPACE_UPDATE_URL env vars
- 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
- Add subtle border + inset shadow to input tiers for resting-state visibility
- Fix DmSearchBar and SearchPopover containers missing input depth treatment
- Fix focus ring clipping in settings panel scroll container
- Swap search and member list toggle button positions in channel/DM headers
Replace the static placeholder button with a fully functional search bar
that filters existing DM conversations instantly and searches for users
via the API with debounce. Supports keyboard navigation, federation-aware
DM creation, group DM display, and portal-based glass dropdown.
Make /join/:code a public route with a standalone JoinPage that shows a
space preview and handles authenticated, unauthenticated, and cross-instance
users. Adds GET /api/spaces/invite/:code/preview (no auth) endpoint,
?redirect= param support on login/register, and cleans up dead invite
handling from AppLayout and JoinSpace modal.
- 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
Guard autoConnectAll against connecting to window.location.origin,
send perspective-correct replicatedInstances lists so remotes never
store self-references, and deduplicate unaccounted spaces in sidebar.
Context menu no longer closes the flyout when right-clicking a space inside
a folder. Added data-flyout-safe attribute so click-outside detection skips
portaled context menu elements. Added drag-and-drop reordering within folder
flyouts with drop indicators and layout persistence.
Replace fixed h-12 (48px) with py-3 + min-h-8 padding pattern to match
MainContent's 57px header height (12+32+12+1), fixing the 9px border
misalignment between sidebar and content headers.
Add catch-all onDragOver/onDrop to parent <nav> so drops landing in
the 6px mb-1.5 margin gaps between items are handled. Use a ref to
mirror dropIndicator state for guaranteed freshness in the drop
handler (eliminates React 18 batching stale-closure race). Add
stopPropagation to prevent double persistLayout calls from bubbling.
Remove FK constraint on space_folder_members.space_id so federated
space IDs (which don't exist in the local spaces table) can be added
to folders without silently failing. Add migration to recreate the
table for existing databases and explicit cleanup on space deletion.
Offset drop indicator lines by 3px into the mb-1.5 gap so adjacent
items share one visual position instead of showing two lines.
Extract TransferOwnershipModal (~165 lines) to its own file.
Folders now always occupy a single icon slot with a glass-pill styled
icon. Clicking opens a flyout popover to the right showing contained
spaces with navigation, unread dots, and federation badges. Removes
the collapsed/expanded toggle and inline rendering in favor of a
condensed, Discord-style folder UX.
Add user_space_layout table and PUT /api/users/@me/space-layout endpoint
for persisting per-user sidebar ordering. Spaces can be freely reordered
via drag-and-drop, folders created by dragging one space onto another,
and folders auto-dissolve when they have fewer than 2 members. Includes
folder context menu (rename, color, ungroup), collapsed folder mini-grid
icons, multi-tab sync via WebSocket, and localStorage collapse state.
Removes the rigid native/federated split — federated spaces now intermix
freely while keeping their globe badge.
Add stale-channel guard in AppLayout that redirects to the space root
when the URL's channelId no longer exists in the channel list. Remove
the hardcoded navigate('/channels/@me') from ChannelSettingsModal and
the redundant setCurrentChannel fallback from the WS channel_deleted
handler — AppLayout's guard now handles all cases uniformly.
Context menu insertion before floatingPanel shifted its fragment index,
causing React to unmount/recreate the DOM element on DM↔space transitions.
The ResizeObserver was left observing the old detached element, so
paddingBottom never adjusted and channels scrolled behind the panel.
- Reorder fragment children so floatingPanel is at index 1 in both views
- Replace useRef with callback ref so ResizeObserver reattaches on remount
- Add stopPropagation to ContextMenu to prevent sidebar menu conflicts
- Restyle sidebar create channel/category buttons (smaller, separated)
Add CreateCategory modal (replaces browser prompt) and right-click
"Delete Category" context menu on category headers with confirmation
dialog explaining channels will be uncategorized, not deleted.
Voice channels already support video/screen share, so the separate video
type was redundant. Adds migration to convert existing video channels.
Also adds border-border-soft to CreateChannel input fields for visibility.
Add drag-and-drop support for moving users between voice channels
(MOVE_MEMBERS permission required). Fix voice_moved handler using
wrong user ID for federated users — now uses the same isHome/
getMyUserIdForOrigin pattern as adjacent voice handlers.
Add .glass-modal CSS class (0.82 opacity) for dialogs and apply
consistent glass tiers across all floating UI: modals, context menus,
tooltips, popovers, and call cards. Replaces ad-hoc bg-surface-elevated
and inline styles with the documented 5-tier glass hierarchy.
Add avatarColor field to spaces, matching the user avatar color system.
Spaces get a random color on creation and owners can change it in space
settings. The color controls the fallback gradient when no icon is uploaded,
replacing the old deterministic hash-based gradient. Includes full
federation support, explore page, mutual spaces, and color picker in both
create and settings modals.
Enforce lowercase usernames at registration/login, migrate existing
usernames, and fix ownership checks for federated spaces by resolving
user identity per-instance with getMyUserIdForOrigin.
- Use custom Tooltip on all space sidebar items instead of browser title
- Add transferOwnership action to spaceStore (federation-aware)
- Add "Transfer Ownership" context menu item for space owners
- Add TransferOwnershipModal with member search and confirmation
- Add transfer ownership option in SpaceSettings > Danger Zone
Use getMyUserIdForOrigin() in leaveSpace() so federated spaces send
the correct remote user ID instead of the home ID. Show context menu
for all spaces with "Invite People" action; "Leave Space" only for
non-owners.
Replace ContextMenu wrapper with a single portal-based SpaceContextMenu
that renders via createPortal to document.body. Fixes pill indicator
positioning, menu overflow clipping, multiple-menu-open bugs, and
inconsistent DOM structure between owner/non-owner spaces. Also normalize
federated space icons in addSpaceFromReady() for discovery page joins.
- Add fileCleanup utility to delete uploaded files from disk on message/account deletion
- Add migration to clean orphaned DM channels, attachments, reactions, read states, and stale moderator refs
- Add FK constraints on dm_message_id in attachments and dm_reactions schema
- Make bans.bannedBy and voiceRestrictions.moderatorId nullable for deleted moderators
- Transfer group DM ownership on member leave or account deletion
- Fully clean up orphaned DM channels (zero members) including files
- Add "Leave Group" context menu for group DMs in ChannelSidebar
- Add leaveDm action to spaceStore
Broadcast user_updated events over WebSocket when profile fields change,
updating members, DM participants, friends, and cached messages in real time.
Widen useVoiceParticipantMeta to return the full user object and add a
standalone avatarColor prop to Avatar so all ~16 callsites now resolve
the user's chosen gradient color instead of falling back to hash-based colors.
Remove placeholder buttons (Threads, Inbox, Help) from channel and DM
headers. Add full-text message search with backend endpoints for both
space channels and DMs, supporting filters (from, has, before, after)
and pagination. Search popover with debounced input, highlighted matches,
and jump-to-message that scrolls with a highlight animation. Includes
messages/around endpoints for loading context when jumping to uncached
messages.
Add avatarColor as a stored, user-selectable field (mint, sky, lavender,
coral, rose, teal, amber). Randomly assigned on registration, changeable
in profile settings. Existing users keep hash-based fallback until they
choose a color. Includes DB migration, API validation, gradient map,
live preview in settings, and banner fallback integration.
Add banner image, accent color, and bio fields to user profiles with
full-stack support: schema migration, API validation (hex color format,
190-char bio limit), sanitizeUser propagation, and new GET /users/:id/mutuals
endpoint. Rewrite AccountPanel with live preview card, avatar/banner upload
via ImageCropModal, 16-preset accent color picker, and bio editor. Enhance
UserProfilePopout with banner display, accent-colored names, bio rendering,
and mutual counts. Add new UserProfileModal with About/Mutual Friends/Mutual
Spaces tabs and friend action buttons.
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.
Split monolithic UserSettings into AccountPanel, VoicePanel, ConnectionsPanel,
and InstancePanel tabs. Remove standalone InstanceSettings modal. Add reusable
Toggle component fixing size deformation, color inconsistency (green→purple),
and flex-shrink issues. Fix custom status clearing by always sending the field
to the server. Wrap Log Out button in glass bubble. Add subtle card depth with
borders.
Add isServerDeafened to mic icon slash conditions in VoiceControlBar and
ChannelSidebar. Restructure VoiceUser badge rendering to gate on effective
state (intent OR server enforcement) instead of only LiveKit participant
intent, fixing missing badges in the voice grid for server-muted users.
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.
- 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.