Commit Graph
205 Commits
Author SHA1 Message Date
Jannis Braun dc6caa0125 merge: wip(mobile): in-progress mobile + voice polish into main
Brings the parked mobile voice + screenshare polish from wip/mobile-polish
(commit dbb9b2c) into main. Conflicts in MainContent.tsx and mobile-ui.md
expected per the WIP commit message — resolved manually.
2026-05-12 13:36:08 +02:00
Jannis Braun 3c7bb02901 fix(dm): ownership transfer divergence after back-and-forth — canonicalize ownerHomeInstance + normalize authority checks
Manual ownership transfers between two federated instances diverged because
`dm_channels.ownerHomeInstance` was stored as a BARE host (`orbit.ddns.net`)
for federated owners — via `transferGroupDmOwnership` copying `users.homeInstance`
verbatim — while `sourceInstance` always arrives as a full URL on the wire.
`processOwnershipTransferEvent` and `processMemberRemoveEvent` then compared the
two with strict equality and rejected legitimate inbound events as
`unauthorized_source`, keeping ownership permanently divergent across peers.
Live DB inspection on the two test instances confirmed both rows (nova + orbit)
had a BARE `owner_home_instance`, matching the bug report exactly.

Three compounding fixes:

1. Receiver authority checks now compare via `normalizeOriginForCompare` so
   legacy bare-vs-full rows accept legitimate transfers (and kicks).
2. New `canonicalizeHomeInstance` helper in `federationAuth.ts`; every write
   site that persists `ownerHomeInstance` (`transferGroupDmOwnership`, group DM
   creation, lazy federation in member-add, `processMemberAddEvent` bootstrap,
   `processOwnershipTransferEvent` receiver storage) routes through it. Full URL
   is the canonical storage form, matching how `sourceInstance` arrives.
3. `dm_owner_updated` WS event extended with optional `newOwnerHomeUserId` and
   `newOwnerHomeInstance` fields. Client `updateDmOwner` writes them when
   present and leaves existing values untouched otherwise (legacy-server safe).
   Without this, `getOwnerInstanceForDm` returned the previous owner's home
   after a successful WS broadcast, routing the next owner-only op to the wrong
   instance.

Coverage: new `federation.ownershipTransfer.test.ts` (7 receiver tests including
the headline bare-vs-full regression and the dedup replay guard); new bare-vs-full
case in `federation.kick.test.ts`; two new client-side cases in
`groupDm.ownerRouting.test.ts` covering both the extended-payload write path and
the legacy-server passthrough. Tests: 1053 server + 364 web, all green.

Specs updated: `dm-system.md` historical bugs + frontend handler table + WS
state-change events table; `federation.md` `ownership_transfer` receiver flow;
`websocket.md` event-fields table.
2026-05-10 22:38:03 +02:00
Jannis Braun dbb9b2c34b wip(mobile): in-progress mobile + voice polish
Snapshot of in-progress work parked here so group-DM-polish can land
cleanly on main. Touches MainContent + mobile-ui.md which overlap with
group-DM-polish; rebase onto post-merge main and resolve conflicts on
those two files manually.

Files: MainContent, MessageInput, MobileVoiceFullScreen (+test), StreamTile,
VoiceUser, useLiveKit, useVisualViewportInset, AudioManager, voiceStore,
voice utils, mobile-ui.md, voice.md, mobile-parity handoff doc.
2026-05-10 21:26:40 +02:00
Jannis Braun d508fecfb0 feat(client): dm_channel_updated WS handler + updateDmMetadata store action 2026-05-10 19:42:05 +02:00
Jannis Braun 6fb38d391c feat(mobile): chat polish — floating composer + iOS keyboard handling + bottom-sheet drag-close + file-chip overflow
Multi-pass mobile chat polish landed across this session.

- MessageInput is now a floating glass-bubble (`position: absolute`) on both desktop and mobile — last messages scroll *behind* the translucent bubble. MobileChatScreen wraps MessageList + MessageInput in a `relative` parent so absolute positioning resolves. Removed the prior in-flow mobile branch that clipped message-list bottom against an invisible barrier.
- iOS PWA keyboard handling: new `useVisualViewportInset` hook subscribes to `visualViewport.resize/scroll` AND polls `vv.height` for ~600ms after focusin (iOS PWA standalone often fails to dispatch resize for keyboard transitions). MobileShell sizes its container to `vv.height` when keyboard is open — `bottom: 0` on the composer naturally lands flush with the keyboard top, regardless of how reliably resize events fire. Composer uses 6px gap above home indicator (keyboard closed) and 0px gap above keyboard (keyboard open). Added `interactive-widget=resizes-content` viewport meta as the cleaner native equivalent for Chrome/Android.
- MessageList bottom padding is dynamic via `--composer-clearance` CSS variable. MessageInput writes `composerHeight + bottomOffset + 12px` to its parent via ResizeObserver — re-fires on textarea autosize, reply banner, attachment tile growth, parent resize. Last message always has 12px breathing room above the bubble regardless of composer state.
- AttachmentRenderer generic file chip: `max-w-full sm:max-w-[400px]` on outer + `min-w-0` + `flex-shrink-0` on icon + `flex-1` on text + `flex-wrap` on badge row. Long filenames now ellipsize cleanly on narrow viewports instead of pushing the chip off-screen.
- New `useDragToClose` hook: shared bottom-sheet drag-down-to-dismiss gesture. Spread on handle/header only (body scrolling unaffected). 6px deadzone, 100px or 0.5px/ms velocity threshold, 200ms `cubic-bezier(0.22, 1, 0.36, 1)` close-out animation, rAF-staged transform for a stable from-value. `hasInteracted` latch prevents the open keyframe from re-firing mid-close (the bounce-up-then-vanish bug). Wired into InputPopover (emoji/GIF), MobileVoiceJoinSheet, MobileFolderSheet.

Specs: docs/systems/mobile-ui.md (Floating Composer + Drag-to-Close sections), docs/systems/message-list.md (--composer-clearance), docs/systems/design-system.md (glass-bubble row references).
2026-05-08 10:23:32 +02:00
Jannis Braun d793ab8f78 fix(message-list): sticky pagination skeleton on channel switch
Two cooperating bugs let the pagination skeleton stick at the top of the
chat across channel switches: (1) the browser's post-clamp scroll event
on channel switch fired the load-more block on the new channel, and
(2) useDelayedLoading's threshold timer refreshed displayStart on every
fire, extending the minDisplay window unboundedly when isLoading cycled.

- useDelayedLoading: stamp displayStart only on the false→true show
  transition (functional setShow form to avoid stale closures).
- MessageList: suppressNextLoadMoreRef armed in Effect 3, consumed by
  the load-more block on the next scroll event, with a 250 ms fallback
  disarm so legitimate user scrolls aren't silently dropped. Suppression
  scoped to the load-more block only.
- Regression test for the displayStart refresh bug.
- Updated docs/systems/message-list.md history.
2026-05-05 19:56:26 +02:00
Jannis Braun 49e9047005 feat(client-federation): user-view cache for cross-instance DM render
Fixes a render bug where a federated user (e.g. axel@nova) appeared with
the federation globe icon and a broken avatar when viewed on his own home
instance. Root cause: `populateFromReady` is first-wins by federatedId and
discards the entire skipped DM payload — including its `members` array —
so when a sibling instance's ready arrived first, the home instance's view
of every shared user was dropped on the floor.

Adds a render-only `userViews` cache that mirrors the `dmAlternatives`
philosophy: information from skipped ready payloads is preserved for
rendering. Every wire surface that delivers a User upserts into the cache
regardless of dedup outcome; render sites read through a Zustand selector
hook to surface the home view when one is loaded. The DM channel ingestion
race is left untouched — the existing no-flapping invariant on origin
reconnect is intentional and load-bearing for failover.

Layered changes:

- `identity.ts`: `normalizeOriginToHost`, `canonicalUserKey`,
  `isDeliveryFromHome`, `isFederationGlobeApplicable` — single helpers
  for origin/host normalization and the home/stub tier decision.
- `spaceStore.ts`: `userViews` Map, `UserViewEntry` type, `upsertUserView`
  action with the home-wins preference rule, prune by `deliveredBy` in
  `removeInstanceSpaces` (mirrors `dmAlternatives` cleanup), `reset`
  clears.
- `userViewLookup.ts`: `useCanonicalUserView` (Zustand selector hook for
  React) + `getCanonicalUserView` (sync getter for non-React paths).
  Render reactivity is structural via the selector, not coincidence on
  legacy update paths.
- `populateFromReady` upsert pass runs BEFORE the federatedId dedup so
  members of skipped DMs still reach the cache.
- WS handlers (dm_message_*, message_*, user_updated, member_joined,
  friend_request_*, dm_channel_created, dm_member_added) and REST
  hydrators (socialStore, discoverStore, mutuals) feed the cache with
  their delivering origin.
- Render-site routing through `useCanonicalUserView` at every audited
  user-rendering site (sidebar, header, search, message bubble, reply
  chips, profile popout/modal, group settings, voice tiles, mention
  chips, member lists, friends, invites). Self-rendering sites compose
  alongside via existing `isSelf`/`resolveDisplayIdentity`.
- Globe predicate hoisted to `isFederationGlobeApplicable` and applied
  at three sites, gating on `domain !== window.location.host` so we
  never show the globe for users whose home IS our own.

Tests: 31 new unit tests across `identity`, `userViews` store, and
`userViewLookup`. Full suite 276/276.

Docs: `client-federation.md` §3 gains a "User View Cache" section
parallel to "DM Origin Failover"; `dm-system.md` notes the new store
action and WS handler upserts.

Bug 3 (federation profile-sync gap — orbit's stale profile data on
nova-Axel after a clear/color-change on nova never propagated)
remains open. The user-view cache routes around it for the common case
(home instance is connected), but the underlying S2S relay gap is its
own diagnosis and follows in a separate branch.
2026-05-05 01:59:05 +02:00
Jannis Braun 19d42445b0 polish(web): clear device lists on permission revoke; document probe-constraint omission 2026-05-03 02:09:13 +02:00
Jannis Braun 8907bb305d fix(web): mic-loss recovery uses 'could not be restored' for unclassified errors 2026-05-03 01:35:07 +02:00
Jannis Braun 63bf9e684c fix(web): mic-loss detection observes upstream stream, not published clone
The previous Task 4 handler installed onended on the *published* mic track,
but that track is a clone of AudioManager's MediaStreamAudioDestinationNode
output — destination-node tracks never end on upstream loss, they just go
silent. The handler also called setInputDevice for silent-recovery and assumed
syncMic would re-publish, but syncMic's dep array does not depend on
streamGeneration, so the recovery never republished.

This commit moves loss detection into AudioManager (where the upstream
getUserMedia track lives) via a new onInputTrackEnded subscription, extracts
republishMicrophone from syncMic into a module-level helper that both the
normal device-change path and the recovery path call directly, and removes
the published-track Microphone branch from RoomEvent.LocalTrackPublished.
The plan and Task 9 spec text are updated to match.
2026-05-03 01:20:13 +02:00
Jannis Braun 9a26513009 feat(web): mic-track-loss recovery — probe, attempt re-acquire, toast 2026-05-03 01:09:04 +02:00
Jannis Braun a1ebb23264 feat(web): add useAudioDevices hook for permission-aware audio enumeration 2026-05-03 00:51:20 +02:00
Jannis Braun 5128a625b8 feat(web): pending-message orchestrator (TTL discard, deferred send, online retry) 2026-05-02 16:00:30 +02:00
Jannis Braun e0861597bc fix(web): portal overlays into fullscreenElement so they render in voice fullscreen
requestFullscreen() on the voice container puts only its descendants in the
browser's top layer; overlays portaled to document.body were rendered outside
that layer and stayed invisible — most visibly the right-click context menu on
stream tiles and voice user panels.

Add usePortalContainer() hook returning document.fullscreenElement ?? document.body
and re-rendering on fullscreenchange. Migrate every overlay reachable during a
call: ContextMenuRenderer (desktop, submenu, mobile sheet), Tooltip,
ConfirmDialog, ConnectionInfoPopover, ScreenShareSettingsPopover, and
ScreenSharePicker (which previously rendered inline at App root).
2026-04-29 23:32:14 +02:00
Jannis Braun 62aa87c737 feat(sounds): receive stream_watch pings + evict watchers on participant disconnect 2026-04-28 14:35:14 +02:00
Jannis Braun 786823946a feat(camera): handle camera-track end with branched toast (unplug vs permission revoke)
LocalTrackPublished registers a one-shot onended on the camera track's
MediaStreamTrack. The handler:
  - skips when consumeIntentionalCameraOff() flag is set (user-initiated)
  - re-probes getUserMedia to distinguish NotAllowedError (permission
    revoked) from NotFoundError (disconnected) from other errors
  - tears down camera state via the unified path

Also reset _intentionalCameraOff in voiceActions if setCameraEnabled(false)
rejects, so a failed disable doesn't poison the next genuine unplug.
2026-04-27 20:29:32 +02:00
Jannis Braun e8534339fb feat(camera): syncCamera effect hot-swaps publication on cameraDeviceId change
Uses room.switchActiveDevice('videoinput', id) to swap the underlying
MediaStreamTrack without re-publishing. Compares against the published
track's actual getSettings().deviceId so null→explicit-same-device is a
no-op. Rolls back on failure with a 'Could not switch camera' toast.
2026-04-27 20:23:00 +02:00
Jannis Braun 51ad54fda5 refactor(camera): unify toggle paths through handleCameraAction
- Voice-bar and mobile camera buttons now use the canonical handler
  (fixes mobile no-op and voice-bar wrong-preset bugs)
- Remove dead useLiveKit.toggleCamera
- Add _intentionalCameraOff flag with mark/consume helpers
2026-04-27 20:16:14 +02:00
Jannis Braun cebbd5c859 feat(web): user-facing pending peering subscriptions and outcome notifications
Two new inline sections in the user-facing federation/connections settings
panel: 'Recent peering outcomes' (terminal-state notifications with
Retry-for-approved + Dismiss) and 'Pending peering approvals' (active
subscriber rows the user is waiting on, with Cancel). New WS handlers for
peering_subscription_changed and peering_notification_received refresh the
lists in real-time and surface a transient toast for online users. Retry
deep-link for friend_add prefills the friend-add input with the original
target handle (other reasons get Dismiss only — the gate doesn't wire
those paths yet).
2026-04-26 22:42:53 +02:00
Jannis Braun 9d3f75b33c feat(web): WS handlers for friend_request_sent + friend_request_relay_failed
Multi-tab sync: friend_request_sent appends the new outbound request to
socialStore (deduped by id+origin), no toast.

Async rollback: friend_request_relay_failed removes the row by id and
surfaces a warning toast with the target handle and reason. Wires the
client side of the rollback hook from T10.
2026-04-25 22:29:45 +02:00
Jannis Braun b16ece93b8 feat(web): no_recipient toast copy arm (#18)
buildCallUndeliverableToast renders "{peerLabel} couldn't ring anyone."
for the single-failure terminal case; multi-failure + non-terminal paths
fall through to existing lines (which already fold the new reason in by
peer label). TDD — four new assertions.
2026-04-24 21:15:21 +02:00
Jannis Braun 5e509cf3df feat(web): host_unreachable phase copy for dm_call_undeliverable (TDD) 2026-04-24 00:51:30 +02:00
Jannis Braun 6a5b02b1a0 feat(web): phase-aware dm_call_undeliverable toast copy (TDD) 2026-04-23 23:21:40 +02:00
Jannis Braun b1844e126f feat(federation): dmAlternatives fallback in dm_message_created
Adds a new resolution step before the legacy 2-member-identity fallback:
if the event's dmChannelId is an alternate-origin local id for a DM
whose primary is in dmChannels, route the message to the primary via
resolveDmChannelId. Covers 1-on-1 AND group DMs uniformly — closes a
pre-existing phantom-sidebar-entry bug for group DMs in multi-instance
sessions and handles post-failover routing when the reconnected original
origin's WS still addresses the DM by its old local id.
2026-04-23 01:29:28 +02:00
Jannis Braun 6a7d1fb38c feat(web): handle dm_call_undeliverable — toast + tear down outgoing call on terminal 2026-04-21 14:02:08 +02:00
Jannis Braun 3d8709d20a feat: real-time Federation panel updates via WS events
Added federation_peers_changed (no-payload signal) broadcast from every
peer state mutation, and federation_approval_request_received when a new
approval request is queued. Client subscribes via onFederationPeersChanged
callback registry. FederationPanel and PendingApprovals debounce-refetch
on any event. sendToAdmins helper broadcasts only to admin users.
2026-04-20 18:28:10 +02:00
Jannis Braun 0aec716d4c fix: gate all client DM events on active S2S peer status
The client's direct WS connection to remote instances (via Connections)
delivered DM events independently of S2S peering. Added activePeerOrigins
allowlist to ready payload — all DM event handlers now silently drop
events from non-home origins without an active peer. This prevents
notifications, sounds, previews, typing indicators, calls, and channel
updates from instances where peering was revoked or never established.
2026-04-20 17:05:57 +02:00
Jannis Braun c3fe1bc9d4 feat: track awaitingApprovalPeerOrigins and show login toast for pending approvals 2026-04-20 15:06:00 +02:00
Jannis Braun 4285e44d2d feat: client ensurePeered API, connection flow swap, WS event handlers for peer rejection 2026-04-09 13:56:18 +02:00
Jannis Braun 016ca2c59b fix: use canonical identity for federated friend/request dedup
The socialStore WS-driven handlers (addFriendFromAccepted,
addIncomingRequest, removeFriendLocally, removeRequestById,
updateFriendPresence) used instance-local id:origin composite keys
for deduplication. When the client is connected to multiple instances,
both fire WS events for the same federated user with different local
IDs, bypassing the dedup and creating duplicate entries.

Switch all handlers to use homeUserId??id (canonical identity),
matching the pattern loadFriends/loadRequests already use. Also
replace the loadRequests() re-fetch in updateFriendRequest with
optimistic canonical removal to avoid racing S2S relay propagation.
2026-04-09 01:23:49 +02:00
Jannis Braun a1a7c2ff18 fix: null roomRef before destroyRoom to suppress teardown sounds
During room.disconnect(), LiveKit fires ParticipantDisconnected for
each remote participant BEFORE the final Disconnected event. Because
roomRef was still set, guardedUpdate() called updateParticipants(),
which updated the voiceStore while isLiveKitConnected was still true.
SoundController played user_leave for each departing participant
alongside the disconnect sound.

Fix: set roomRef.current = null before calling destroyRoom(). This
causes guardedUpdate() to return early for all teardown events.
The disconnect function handles cleanup after destroyRoom resolves.
2026-04-08 15:39:21 +02:00
Jannis Braun 22aa1e3f66 fix: batch participants + isLiveKitConnected on disconnect to prevent double sound
RoomEvent.Disconnected handler set participants=[] and
isLiveKitConnected=false in separate setState calls. SoundController
subscription fired between them — saw empty participants while still
"connected" → played user_leave, then saw disconnected → played
disconnect. Both sounds played simultaneously.

Batching into one setState ensures SoundController sees the final
state atomically: participants gone AND disconnected in one update.
2026-04-08 14:59:08 +02:00
Jannis Braun c037803c5a fix: acceptor sets activeDmCall in click handler, not server response
Bug A: handleAccept relied on dm_call_accepted server response to set
activeDmCall. But connectFn's async AudioContext resume yields to the
event loop, dm_call_accepted arrives during the yield, finds
isLiveKitConnected=false (connectFn just reset it), and skips
setActiveDmCall. The acceptor connects to LiveKit but the UI never
shows the call. Fix: set activeDmCall and clear incomingCall
directly in the click handler.

Bug B: ready handler no longer sets activeDmCall for active calls.
On refresh/restart the client has no LiveKit connection — showing
"Connecting..." with no connection is broken. The call exists on
the server but this client session is disconnected.
2026-04-08 14:23:41 +02:00
Jannis Braun aae0b1a74e fix: comprehensive client-side session management for federated DM calls
Four fixes addressing the full state management problem:

1. Passive ready handler: no longer auto-connects to LiveKit on
   page refresh. Prevents identity conflicts when the same user
   has multiple sessions fighting for one LiveKit identity slot.
   The user must re-accept to join; state is shown but not acted on.

2. SoundController sync guard: incomingCallLoading/outgoingCallLoading
   refs prevent multiple playSound calls during async audio load.
   If call is cancelled while sound loads, stops it immediately on
   completion. Eliminates the "5 ringtones at once" bug.

3. Host dm_call_accepted broadcasts now include federatedCallId so
   all clients (including remote instances) can match the event.

4. Removed all diagnostic console.log statements.
2026-04-08 14:12:45 +02:00
Jannis Braun 86fe713a7c fix: cross-instance event contamination in federated DM calls
Root cause: sendToFederatedCallUsers used sendToDmMembers when dmChannelId
was set, which broadcast to ALL DM members including the caller's replicated
stub. The caller's multi-instance WS received dm_call_accepted with the
REMOTE instance's dmChannelId, causing token request for a non-existent
channel (403) and preventing the caller from connecting.

Fix 1: sendToFederatedCallUsers always uses ringedUserIds (exact recipients)
instead of sendToDmMembers (all members including caller stub).

Fix 2: dm_call_accepted handler only sets activeDmCall if the client is the
caller (wasOutgoingCall) or already connected to LiveKit. Other instances of
the same user just clear ringing without entering stuck "Connecting..." state.
2026-04-08 13:50:25 +02:00
Jannis Braun df2701b17f fix: route call accept/reject through delivering WS, not host URL
callOrigin was set to event.callOrigin (the HOST instance URL), which
routed accept/reject through the multi-instance WS connection. On
mobile hotspot or when the multi-instance WS drops, the accept is
silently lost — the host never knows, the call stays ringing forever.

Now callOrigin = origin (the WS that delivered dm_call_incoming).
This is always connected. The server on that instance finds the
FederatedCallEntry and relays to the host via S2S HTTP, which is
reliable and independent of client WS state.
2026-04-08 13:15:38 +02:00
Jannis Braun 5265117d29 fix: three root causes for federated DM call failures
1. Accept/reject/end from remote instance now resolves federatedId
   to local dmChannelId via DB lookup, so the host can find its
   VoiceRoom when the event arrives with only a federatedCallId.
   Previously silently failed with "No active call" error.

2. Batch all dm_call_incoming state updates into a single
   useVoiceStore.setState() call. Prevents SoundController from
   starting multiple ringtone instances (async playSound guard
   race when 4 separate set() calls each triggered the subscription).

3. Always overwrite callOrigin/federatedCallId (with null if absent)
   on dm_call_incoming. Prevents stale values from a previous
   federated call routing local accepts to the wrong instance.
2026-04-08 12:53:58 +02:00
Jannis Braun 9ad240495f fix: prevent auto-connect on dm_call_accepted for non-caller instances
Three fixes for multi-instance call state consistency:

1. Client dm_call_accepted handler only auto-connects to LiveKit if
   the user was the caller (outgoingCall was set). Other instances of
   the same user just clear ringing state without connecting.

2. Server processDmCallAcceptEvent remote path skips duplicate
   broadcast when FederatedCallEntry is already active (prevents
   state conflicts from host fan-out arriving after local accept).

3. Ready payload handler clears stuck incomingCall when restoring
   an already-active call after page refresh.
2026-04-08 12:21:27 +02:00
Jannis Braun ba7b975bc8 feat: add federatedCallId and callOrigin to client call routing
Enable federated DM calls to route accept/reject/end through the correct
WebSocket connection using callOrigin, and include federatedCallId in all
dm_call payloads for server-side FederatedCallEntry lookup.
2026-04-08 03:26:48 +02:00
Jannis Braun 95f97c1526 feat: process DM WS events from all origins
Remove home-only restriction on DM events. Add federatedId
dedup check to dm_channel_created to prevent duplicate sidebar
entries for cross-instance DMs.
2026-04-07 19:55:08 +02:00
Jannis Braun 4c2e75b80c feat: handle deleted user cleanup in user_updated WebSocket handler 2026-04-03 04:23:18 +02:00
Jannis Braun b77aad61eb refactor(web): unify DM sorting with sortDmChannels across all triggers 2026-04-02 17:56:26 +02:00
Jannis Braun 187f71e68e feat: add dm_typing_stop event to clear typing indicator on message send
Broadcasts dm_typing_stop to DM members before dm_message_created,
so the typing indicator clears immediately when a message arrives
instead of lingering for up to 3 seconds after delivery.
2026-04-01 12:50:03 +02:00
Jannis Braun 2f211e0588 feat(federation): useLiveKit connects to remote LiveKit for federated calls (FED-009) 2026-03-31 23:41:50 +02:00
Jannis Braun 24571edc34 feat(federation): client voice store handles federated call data (FED-009) 2026-03-31 23:40:11 +02:00
Jannis Braun c3191be4e7 feat: system messages, group DM welcome header, and dm_owner_updated event
- Render system messages (member_added, member_removed, owner_changed) inline
  in MessageList with icon + human-readable text; system messages never group
  with adjacent user messages
- Rewrite WelcomeHeader to branch on ownerId: group DMs show overlapping avatars,
  group name, creator attribution, federated privacy note, and a Leave Group button
- Add dm_owner_updated ServerEvent; broadcast from dm.ts leave handler and
  federation processOwnershipTransferEvent so all clients update ownerId in real-time
- Add updateDmOwner action to spaceStore and handle dm_owner_updated in useWebSocket
- Add leaveGroup alias to API client dm namespace
2026-03-27 05:43:30 +01:00
Jannis Braun 589af25b45 feat(federation): show toast on file_rejected federation event 2026-03-27 04:43:23 +01:00
Jannis Braun b8a41c4484 fix: update discoverStore on friend accept and remove WS events
The friend_request_accepted and friend_removed WS handlers updated
socialStore but not discoverStore, so the discovery page buttons
stayed stale until page refresh. Decline/cancel already updated
both stores correctly.
2026-03-27 01:24:14 +01:00
Jannis Braun f362f5530c fix(federation): prevent duplicate DM sidebar entries from relay broadcasts
When a relay-created dm_message_created arrives for an unknown channel,
check if an existing DM has the same member (by homeUserId). If so,
route the message to the existing channel instead of adding a duplicate
sidebar entry. Prevents the transient duplicate that required page
refresh to dedup.
2026-03-26 14:45:04 +01:00
Jannis Braun 78555a43f8 fix(voice): stabilize grid layout across focus-mode and fullscreen transitions
Refactor useGridLayout from RefObject to callback-ref API so the
ResizeObserver reattaches when the grid container remounts after
focus-mode toggle. Fixes tiles rendering at stale fullscreen dimensions
on 4:3 monitors.

Also clamp focus-mode participant strip height (max-h-[20vh] min-h-[80px])
to prevent it from crowding the focused stream on short viewports.
2026-03-25 03:08:45 +01:00