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 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
- Add "Discover People" section to Add Friend tab with user cards, mutual counts, and inline actions
- Add discoverStore for fetching/searching discoverable users across local and federated instances
- Add PrivacyPanel to user settings with discoverability toggle
- Add is_discoverable column to users table with migration
- Fix "Send Friend Request" button vertical alignment using transform centering
Cancelled requests now disappear from receiver's UI instantly, and
declined requests revert the sender's discover card from "Request
Pending" to "Send Friend Request" — no page refresh needed.
Also includes the discover endpoint and sendFriendRequest return type
changes from the prior session.
Avatars/banners now resize to max 512px/1920px and convert to WebP before
upload (zero server cost). Chat image uploads generate an 800px-wide WebP
thumbnail via Sharp; the feed shows the thumbnail, click opens the full-res
original. Adds lazy loading to avatars. Federation-compatible: remote
instances without this feature fall back gracefully.
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.
- Add spaceId to InvitePreview so the client can navigate to the space
- Detect "already a member" error and show green success card with auto-redirect
- Add "I use another instance" link for authenticated users alongside "Not you? Log in"
The link was bouncing back to JoinPage because AuthRedirect on /login
saw the existing token and redirected right back. Now calls logout()
first so the login form actually renders.
Add avatar/name/username identity card in the federation connect phase
so users can confirm their account before entering their password.
Promote "I use another instance" from a hidden <details> collapsible to
an equal-weight button with an "or" divider, using a dedicated
'other-instance' phase with Back navigation.
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
Track last speaker via ref so avatar fallback doesn't snap to array-order
first remote when everyone stops talking. Parse :stream suffix from
focusedParticipantId so clicking a stream tile in the grid pins that
screen share to PiP. Remember last focus via ref so PiP content persists
when grid focus is cleared.
Gutter column was 56px (w-14) with the 16px gap baked into the width.
This worked for avatars but left only 4px between compact hover
timestamps and message text. Switch to w-10 (40px) gutter + gap-4
(16px flex gap), preserving the same 76px content offset while giving
timestamps proper breathing room.
Three changes to bring output volume closer to native apps:
- Insert masterBoost GainNode (+3dB) before the compressor/limiter
- Raise default system sound volume from 0.5 to 0.8
- Add configurable Sound Effects Volume slider (0–200%) in Voice settings
Add a cross-instance self-ID registry to identity.ts so isSelf() can
recognize the current user's Snowflake IDs from all connected instances.
Previously, federated DMs showed the user themselves as the other party
because remote-instance IDs didn't match the home user ID.
- Register user IDs from every WS ready event (home + remote)
- Clear the registry on session reset (login/logout/register/delete)
- Fix isSelf() username comparison to parse both sides as federated
- Replace naive ID check in MessageList WelcomeHeader with isSelf()
Profile and space layout changes on remote instances were being
overwritten by stale data on reconnect. Adds Last-Writer-Wins
timestamps so the client-relay mesh rejects stale writes:
- profile_updated_at column on users table with migration + backfill
- Server LWW guards on PATCH /users/@me and PUT /space-layout
- Bidirectional profileSync: pulls newer remote profiles to home
- LWW layout sync replaces home-authoritative _layoutFromTrueHome flag
- Layout pushes to ALL connected instances in parallel
Federated users now have their sidebar layout synced from their true
home instance instead of each browsing instance maintaining a separate
disconnected layout. Layout saves route to the true home API with
automatic fallback to the browsing instance if unreachable.
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.
Add Delete Channel button to channel settings modal with ConfirmDialog
confirmation. Fix backend DELETE route to disconnect voice users, clean
up attachment files from disk, and remove orphaned read_states. Make
deleteChannel federation-aware in spaceStore and clean up voiceUsers
on channel_deleted WebSocket event.
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.
Bring CLAUDE.md into full alignment with the production codebase after months
of active development. Adds 3 new DB tables, 8 user columns, 18+ API endpoints,
10 WS events, and 30+ new source files to the documentation. Updates deployment
section for Caddy/multi-instance setup and adds 8 new feature entries.
Also applies glass material system to UserProfileModal (backdrop + card).
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.