1. Video aspect ratio: remove container border/overflow-hidden, use
preload="metadata" so browser knows dimensions before play
2. YouTube Error 153: remove sandbox attr (too restrictive), add full
allow permissions (encrypted-media, accelerometer, gyroscope, etc.)
3. Google Images not displaying: detect image Content-Type from HTTP
response in metadataFetcher, override classifier to create image
embed for URLs that serve image/* content
4. Audio seeking broken: add HTTP Range request support in uploads
route (Accept-Ranges, Content-Range, 206 Partial Content)
- Fix ?? to || in metadataFetcher.ts to handle empty strings from Cheerio
- Fix stale embeds on message edit: delete old embeds synchronously before
broadcast, then resolve new ones async (all 4 edit paths: REST+WS, msg+DM)
- Revert unrelated MessageList.tsx scroll threshold change (5000 not 150)
- Remove duplicate embed indexes from migrateAddIndexes (kept standalone ones)
- chatStore: normalize embeds to [] in addMessage, addRealtimeMessage, and updateMessage to guard against missing field from older servers/federation
- useWebSocket: add embeds_resolved and dm_embeds_resolved cases that patch the in-memory message cache when the server broadcasts resolved embed data
- useWebSocket: normalize embed image URLs for remote origins in message_created, message_updated, dm_message_created, dm_message_updated, and the new embeds_resolved handlers
Switches Message.tsx from client-side URL metadata fetching (old Embed.tsx)
to server-driven embed objects via message.embeds[]. Delegates attachment
rendering to AttachmentRenderer. Removes firstUrl extraction and urlRegex.
Integrate embed infrastructure into the complete message flow:
- messages.ts: batch-fetch embeds in GET, resolve on POST, re-resolve on PATCH
- dm.ts: same pattern for DM messages with isDm=true
- search.ts: include embeds in all 4 search/around endpoints
- events.ts: embed resolution in WS message create/edit for both space and DM
- Fix embedClassifier.ts type errors (regex match undefined → null)
- Add embeds: [] to all inline MessageWithUser/DmMessageWithUser constructions
- Add embeds: [] to chatStore optimistic message
handleVoiceDropZoneDrop called stopPropagation and clearState
unconditionally, preventing container's handleContainerDrop from
firing when dropping a channel onto a voice channel. Add early
return for non-voiceUser drags so channel/category drops bubble
through to the container handler correctly.
Port the SpaceSidebar normalization pattern: convert 'before B'
to 'after A' so only one drop indicator renders at any gap.
Fixes double-line visual glitch and unreliable drops when cursor
is between two channels. Also adjust indicator offsets from
top-0/bottom-0 to -top-[1px]/-bottom-[1px] to center in the gap.
Remove activeDrag from drag-start dependency arrays to prevent
unnecessary re-renders during drag. Wrap containerHandlers in
useMemo for consistent reference stability.
When a user joins a voice channel while already connected from another
tab, the server now sends a 'displaced' reason on voice_disconnected.
The client tears down LiveKit and shows an informational toast.
- Extract VolumeSliderItem to voiceMenuItems.tsx (was duplicated in
VoiceUser.tsx and VoiceChannel.tsx)
- Add e.stopPropagation() to all imperative context menu handlers
for consistency with useContextMenu hook behavior
Add auto-launch settings (start at boot + start minimized) to the
Desktop section in Account settings, using Electron's built-in
setLoginItemSettings API across macOS, Windows, and Linux.
Replace the fallback colored-circle tray icon with proper B logo assets:
template images for macOS (adapts to light/dark menu bar) and colored
icons for Windows/Linux. Fix BGRA channel order bug in fallback generator
and move tray icons to resources/ so they're packaged into the app.
- Save the top-visible message ID on channel leave, restore via
scrollIntoView on return (immune to lazy-loaded image reflow)
- Add floating glass-bubble "Jump to Present" button when scrolled
5000px+ from bottom
- Clear stale scroll anchors when user returns to bottom
- Evict scroll positions alongside channel cache eviction
ResizeObserver alone misses scroll adjustments when multiple images load
in quick succession due to browser layout-loop suppression. A capture-phase
load event listener on the content wrapper reliably catches all descendant
image loads and re-scrolls to bottom.
profileSync stored avatar/banner paths with /api/uploads/ prefix on remote
instances, causing resolveAssetUrl to produce double-path URLs like
https://remote/api/uploads//api/uploads/file.jpg that 404'd. Store bare
filenames instead, strip prefix defensively in resolveAssetUrl and server-side
for existing data self-healing.
Voice channels rendered with VoiceChannel component have no text reading/acking
UI, so messages in them created phantom unread indicators on the space sidebar
that users could never clear. Root cause was a message in the counter-strike
voice channel with no read state.
Three-layer fix:
- spaceStore: track voiceChannelIds set, exclude voice channels from
channelLastMessageIds so setReadStates never marks them unread
- useWebSocket: skip markChannelUnread for voice channels on message_created,
prune orphaned unreads on every ready event
- chatStore: validate preserved unreads against channelToSpaceMap to drop
orphans that don't map to any known channel
resetUserStores() was abusing populateFromReady('', [], [], []) to clear the
space store. Its LWW timestamp logic fired an async pushLayoutToOrigin when
_layoutUpdatedAt > 0, which read a null token from localStorage (already
removed on logout, not yet set on login). The 401 response triggered
handleUnauthorized(), deleting the freshly-stored login token and forcing a
full page reload — requiring users to log in twice.
Replace with a proper reset() method that synchronously sets all state to
initial values with no LWW comparison or API side effects.
After a server restart, navigating to a previously-visited channel showed
messages at a stale middle position instead of scrolling to the bottom.
The in-memory message cache survived the reconnect, so loadMessages()
bailed (cache hit) and the scroll-to-bottom logic never fired.
Now the ready handler clears the messages and hasMore maps for all
channels belonging to the reconnecting origin (including DMs for home).
The currently open channel is force-reloaded immediately; other channels
get fresh-fetched on next visit, triggering proper scroll-to-bottom.
Also fixes voice channel settings gear icon placement to match text
channels (flex-1 pushes icon to right edge).
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).
The sidebar used voiceStore.voiceUsers (WebSocket-driven, 5s delay) while
voice panels used LiveKit's real-time participants. Now ParticipantDisconnected
also removes the user from voiceUsers for immediate sidebar updates.
Root cause: own messages echoed by the server marked channels unread when
the user had already navigated away. Seven related bugs compounded the
problem — stale read states, missing cleanup on space/DM removal, REST
broadcast ignoring VIEW_CHANNEL, and no validation on channel_ack writes.
Frontend:
- Skip markChannelUnread for the user's own messages (federation-aware)
- Walk backward past temp_ IDs in ackChannel instead of bailing
- Re-fire ack timer when temp message is replaced by server-confirmed ID
- Add removeChannelStates to clean up unread/read/message caches
- Clean up chatStore on removeSpace, removeDmChannel, removeInstanceSpaces
Server:
- Use sendToChannel instead of sendToSpace for REST message creation
- Clean up read_states on space deletion, member kick/leave, and ban
- Validate channel membership before accepting channel_ack writes
- Clean up read_states on DM leave and DM channel deletion
useSystemPicker triggers applyConstraints to re-open the system picker
(Electron #44684) and doesn't reliably pass audio capture. All platforms
now use the custom Aether Drift picker with explicit loopback control.
Electron requires setDisplayMediaRequestHandler for getDisplayMedia() to
work — removing it broke screen sharing entirely. Restored the handler
with useSystemPicker: true, which on macOS 15+ uses the native system
picker (honoring restrictOwnAudio), while Windows/Linux fall back to
the custom picker with the shareAudio toggle for echo control.
The custom screen share picker intercepted getDisplayMedia() and created
a raw loopback stream, bypassing Chromium's constraint pipeline entirely.
restrictOwnAudio was silently discarded. Removing the handler lets
Chromium 144's native getDisplayMedia run end-to-end with restrictOwnAudio
applied, eliminating the audio feedback loop in the desktop app.
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+)
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.
Replace the Swift squircle hack with the correct Apple approach:
generate a proper .icns using sips + iconutil, then replace
electron.icns in the Electron bundle before launch. macOS applies
its native squircle mask + shadow from the .icns automatically.
macOS only applies the squircle mask to packaged .app bundles. For dev
mode, generate a pre-masked icon-dock.png via Swift/AppKit at launch
and set it with app.dock.setIcon(). Silently skipped on non-macOS.
Regenerate all web icons (favicon, PWA, apple-touch) from master 1024x1024
icon.png. Add BrowserWindow icon property for Windows/Linux taskbar icon
in Electron dev mode.
Point prebuild script at the 1024x1024 master icon.png at project root.
electron-builder auto-generates .icns (macOS), .ico (Windows), and
multi-size PNGs (Linux) from build/icon.png during packaging.
- 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
Registration avatar upload raced with AuthRedirect — setting the Zustand
token triggered navigation before the upload could finish. Now the token
is stored in localStorage (for API auth) but not in Zustand until the
avatar upload completes, so the page stays mounted throughout.
Extracts initSession() from login/register for reuse.
- Resize avatars/icons to 256px and banners to 1280px (client crop + server safety net)
- Add server-side resizeProfileImage() for federation/API uploads without crop modal
- Fix unconstrained crop on RegisterPage and CreateSpace (was missing maxOutputDimension)
- PWA: switch to autoUpdate with skipWaiting/clientsClaim for seamless deploys
- Storage janitor: exclude profile images from unlinked cleanup, delete stale thumbnails
- Add deleteAttachmentByFilename() to clean orphaned attachment records for profile images
- Migration: one-time cleanup of stale profile image attachment records
- GeneralPanel: wrap in <form> to prevent implicit submission
Click trash icon to arm (morphs to green checkmark), click again to
confirm. Auto-cancels after 3s or 2s after mouse leaves. Uses CSS
scale+opacity transitions for a smooth icon swap animation.
Remove width: 100% !important override that fought emoji-mart's shadow
DOM grid. Container now uses w-fit to wrap content tightly. GIF picker
gets explicit w-[390px] to maintain its own width independently.
Replace hard-edged ring ripples with blurred radial gradient orbs, add
subtle glass refraction shimmer, and use gentler glow/breathing curves
for a calmer incoming call experience.
isValidAssetUrl() was rejecting bare filenames (e.g. "1234567890.webp") which
is the established convention the frontend sends. Now accepts bare filenames
while still blocking path traversal and unsafe schemes.
Also updates client-side password validation to match server's 8-char minimum.
- 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 extension-based mimetype fallback in uploads route so thumbnail
files serve correct Content-Type (image/webp) instead of falling back
to application/octet-stream when DB lookup misses
- Skip animated images (metadata.pages > 1) during thumbnail generation
to preserve GIF/WebP animations instead of flattening to static frame
- Remove redundant explicit thumbnail deletion in storageJanitor since
deleteUploadFile() already auto-deletes the thumbnail variant
- 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