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.
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.
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.
- Fix DM welcome header avatar using home identity for correct gradient color
- Register DM channel IDs in channelOriginMap so federated DM operations
(reactions, messages, typing) route to the correct instance
- Pass origin when creating DM channels from friends list and WS events
- Add server discovery/explore page with public server listings
- Add server visibility and description fields
PiP positioning:
- Replace hardcoded layout constants with dynamic DOM measurement system
- Obstacle elements declare themselves via data-pip-obstacle="left|bottom"
- getPipBounds() queries actual element rects at runtime via getBoundingClientRect
- MutationObserver + ResizeObserver re-clamp PiP when obstacles appear/resize
- PiP reappears after close when navigating away from voice channel
Reactions:
- Align reaction pills with Aether Drift prototype
- Own-reaction accent: purple → mint (bg, border, count color)
- Default pills: subtle white overlay bg + border-soft border
- Count text: 12px/semibold/txt-secondary per prototype spec
Bug fix:
- Deduplicate channel insertion in createChannel store action
- Prevents double-add race between REST response and WS broadcast
Avatar fallback gradients were hashed by display name alone when no
user prop was passed, causing the same person to appear in different
colors across messages, replies, member lists, voice panels, etc.
Added userId prop to Avatar and wired it through all 17 call sites
so the gradient always hashes by user ID.
Migrate all 10 voice/video components from Discord visual language to
Aether Drift design system. Zero discord-* classes or hardcoded hex
values remain in components/voice/.
Key changes:
- Status indicators use pastel accents: mint (connected/speaking),
amber (connecting/idle), rose (muted/deafened/error)
- VoiceControlBar converted to frosted glass-bubble material
- ConnectionInfoPopover and ScreenShareSettingsPopover use glass popovers
- Speaking ring glow changed from Discord green to mint (#86efac)
- Slider accents use lavender instead of blurple
- All bg/text/border tokens mapped to surface-*/txt-*/interactive-* system
Camera was stuck at 180p because our custom <video> rendering bypassed
LiveKit's adaptive stream observer. Replaced manual srcObject binding
with track.attach()/detach() in VoiceUser, StreamTile, and PictureInPicture
so the SFU receives viewport dimensions and forwards the correct
H.264 simulcast layer.
Screen share VP9 SVC with L3T3 spatial layers failed because hardware
VP9 encoders (NVENC, QSV, VCE) don't support spatial scalability —
Chrome silently degrades to L1T1. Reverted to VP9 single-layer
(simulcast: false, no scalabilityMode). Also targets encodings[length-1]
in applyOverdrive() for correct simulcast layer targeting.
Eliminate double-state architecture (useState → useEffect bridge → store)
that lost speaking events due to React 18 batching. ActiveSpeakersChanged
now writes speakingParticipantIds directly to voiceStore; 200ms poll
catches missed SDK events. Each VoiceUser subscribes to its own identity
via fine-grained selector for minimal re-renders. Also adds connection
quality indicator and ConnectionInfoPopover.
Streams now appear as separate tiles in the voice grid alongside the
user's camera/avatar tile, matching Discord's model. Each stream tile
has independent volume, mute, watch/unwatch controls, quality badges,
and stream attenuation that ducks audio when someone speaks.
Shift entire color palette from Discord's Ash theme to the darker Dark theme.
Redesign VoiceControlBar as floating auto-show pill, add gradient Join Voice
screen, move invite icon to server header, add voice channel user status
badges, update auth pages with subtle gradient backgrounds, and sweep all
hardcoded hex colors across 14 files to match the new darker palette.
- New PictureInPicture component: draggable floating window that shows
active stream when navigating away from voice channel, with LIVE badge,
snap-to-edge, click-to-navigate-back
- Fix VoiceUser to check actual track presence instead of boolean flags
for video rendering
- Add LocalTrackPublished/LocalTrackUnpublished events to useLiveKit
so local screen share and camera show immediately in VoiceGrid
- Add pipCollapsed state to uiStore