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
- 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.
One-time async migration runs after server is listening — generates 800px
WebP thumbnails for all existing image attachments that lack one. Gated by
a persistent flag in instance_settings so it never re-runs.
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.
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
- Add account deletion with tombstone (isDeleted flag), password/username
confirmation, owned-space guard, and full cleanup transaction
- Free deleted usernames by renaming to !deleted:<id> so they can be reused
- Add migration to retroactively free usernames from already-tombstoned users
- Add GET /api/auth/check-username endpoint with rate limiting for real-time
availability checking during registration
- Add debounced username availability indicator on registration Step 1
- Add DeleteAccountModal with federation-aware remote account cleanup
- Add federation ops utility for remote instance management
- Update sanitizeUser to anonymize deleted user profiles
- Add instance store improvements and connected instances modal updates
Refactor RegisterPage into a two-step flow: credentials first, then
personalization (display name, avatar upload, avatar color). Replace the
dual-panel sliding layout with conditional rendering and CSS keyframe
animations to eliminate overflow-hidden clipping of focus rings.
Supporting changes:
- Server accepts avatarColor on registration
- Auth store resets all user-scoped stores on login/register/logout
- Voice store gains resetSession() for full session cleanup
- Sync presence status to federated instances
- Propagate presence_update to socialStore regardless of origin
Federated users (e.g. youruser@nova browsing orbit) could not add
instances via Settings → Connections because connectToRemote assumed
window.location.host was the home instance, producing a double-@ username
like youruser@nova.ddns.net@orbit.ddns.net that failed server
validation. Now derives trueHomeHost/bareUsername/trueHomeUserId from the
user's actual homeInstance fields and branches the auth flow: login-only
with bare username when targeting home, register with correct namespacing
for third-party remotes. Also skips profile sync to home instance since
it's the source of truth.
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 server-side heartbeat using RFC 6455 protocol-level ping/pong frames
to detect abruptly disconnected clients (e.g. PC shutdown without TCP FIN).
Dead connections are terminated within ~65s, triggering the existing
cleanup chain to remove ghost users from voice channels and presence.
Add flex to speaking ring wrapper divs in VoiceUser and PictureInPicture
to eliminate baseline descender space that made the ring oval. Extract
useVoiceParticipantMeta hook to resolve real display names and avatars
for voice participants instead of showing raw usernames with null avatars.
Add showAt prop to Username component and use it in both UserProfilePopout
and UserProfileModal to consistently display @username for all users.
Also move avatar ring styling into Avatar component's ring prop.
Replace element-level opacity on banner fallback divs with rgba-based
gradient color stops via new mutedGradient() utility. This prevents
compositing layer artifacts that caused visible color seams at the
banner boundary in AccountPanel, UserProfilePopout, and UserProfileModal.
Move bg-surface-channel from info section to card wrapper in AccountPanel
so the avatar's negative margin overlaps into the banner correctly. Replace
accent color presets with 7×3 banner color palette, remove accent color
tinting from display names in popout/modal, and refactor gradient entries
to expose from/to hex stops.
Replace the hardcoded 16-color ACCENT_PRESETS array in AccountPanel with
a shared export from gradients.ts that extracts both hex stops from each
avatar gradient, producing 14 harmonized colors in a 7×2 grid.
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.
Friends/requests were only loaded when FriendsPage or ActivityPanel rendered,
so profile modals in space views always showed "Add Friend" even for existing
friends. Now loadFriends/loadRequests fire on every WS ready event.
Also adds canonicalUserMatch() with cascading ID/homeUserId/username+instance
fallback, replacing fragile homeUserId-only matching in getFriendshipStatus.
Space avatars in the mutual spaces tab now use getSpaceGradient() instead of
a flat grey background, matching the sidebar appearance. Mutual friends and
spaces from remote instances show a globe icon with the instance hostname.
Also wires up federated mutuals loading, correct API client routing for
remote user profiles, and the new mutuals utility.
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.
Space icon in Settings → Overview was using the user Avatar component
(wrong palette + hashed on name), now uses getSpaceGradient(id, name)
to match the sidebar.
Remove the unused USE_VOICE_ACTIVITY permission bit (was bit 25) and shift
STREAM to bit 25, DISCONNECT_MEMBERS to bit 26. Add a database migration to
remap stored permission values. Gate camera and screen share buttons in
VoiceControlBar behind canSpeak/canStream, matching VoiceControls behavior.
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.
Use neutral bg-white/20 for unwatched camera badge in grid tile instead
of bg-accent-rose/90, matching the local mute badge convention. Show
crossed-out camera icon in channel sidebar when a remote user's camera
is locally unwatched. Add local mute badge to both grid tile and sidebar.
Add ability to unsubscribe from remote camera tracks via context menu,
with unwatched state tracked in voiceStore. Fix click propagation through
React portals by adding onClick stopPropagation alongside onMouseDown on
both the main context menu and MoveToSubmenu flyout portal containers.
Use rose badge color for unwatched cameras (user choice) instead of amber
(reserved for server-enforced states).
- Add DISCONNECT_MEMBERS permission (bit 27) to disconnect users from voice
- Implement voice_disconnect WebSocket handler with permission checks
- Add disconnect button to voice user context menu
- Grant instance admins full permissions across all spaces
- Fix voice_disconnected handler to use federation-aware identity resolution
- Update CLAUDE.md with new event types and permission docs
Both the channel sidebar (VoiceChannel.tsx) and voice panel (VoiceUser.tsx)
now share one context menu with volume slider + conditional mod items,
eliminating duplicated portal/positioning/click-outside code from VoiceUser.
Replace the flat channel list with a single "Move to" trigger that opens
a cascading flyout on hover, preventing the menu from bloating vertically
when many voice channels exist.
Replace hardcoded Tailwind grid-cols breakpoints with a container-aware
layout algorithm that uses ResizeObserver to recompute optimal tile
arrangement. Maximizes tile area while maintaining 16:9 aspect ratio,
automatically adapting when VoiceChatPanel opens/closes, window resizes,
or fullscreen is toggled.
When a second browser joins voice, LiveKit disconnects the first browser
via identity collision. The first browser's leaveVoice() was optimistically
removing the user from voiceUsers, but the user is still in voice from
the other session. Add handleForceDisconnect() that clears local connection
state without touching voiceUsers, keeping the sidebar accurate.
Also fix deploy.sh to clean up stale renamed containers from failed recreates.
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.
- Add migration to convert legacy JSON array permissions to decimal strings
- Add runtime fallback in stringToPermissions for legacy format
- Show all permission toggles as enabled (dimmed) when Administrator is on
getMyUserIdForOrigin relied on instanceStore resolver which could return
the home user ID during connection errors. Cache the authoritative user
ID directly from the WS ready payload, ensuring effective-state
computations use the correct federated identity.
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.