Commit Graph
127 Commits
Author SHA1 Message Date
Jannis Braun 7351b3d90d fix(dm): surface dm.name on chat header/placeholder; collapse unnamed-group placeholder
Three layered bugs all manifesting as "the group name doesn't show / the
placeholder is a 40-character wall of names":

1. **WS ready payload was missing `name`/`icon`.** The handler serialized
   DmChannel rows with only `id, federatedId, ownerId, createdAt, members,
   lastMessage`. The optional metadata fields were silently dropped, so
   the client store never received `dm.name` until a subsequent
   `dm_channel_updated` event fired (i.e. only mid-session renames worked,
   never the initial render). `ownerHomeUserId`, `ownerHomeInstance`, and
   `metadataUpdatedAt` were also missing — added too because federated
   routing depends on `ownerHomeInstance` (`getDmOwnerHomeInstance`).

2. **Header surfaces silently dropped `dm.name`.** `MainContent` (desktop
   chat header) and `MobileChatScreen` always rendered the joined member
   names, even when `dm.name` was set. Five other surfaces (`DmListItem`,
   `MobileDmsScreen`, `MessageList` welcome hero, `MobileGroupDmInfo`,
   `GroupDmSettings`) honored it correctly, so a renamed group showed
   different titles depending on which surface you looked at.

3. **Message-input placeholder rendered joined names.** Once a group
   has 4+ members "Message #Alice, Bob, Charlie, Dave" overflows the
   textarea and obscures the call-to-action.

Consolidates the display-name logic behind two utilities in
`dmFormatters.ts`:

  - `formatDmHeaderName(dm, currentUser)` — `dm.name` verbatim if set,
    else joined names (excluding self); falls back to `'Group'` /
    `'Direct Message'`. Used by all 5 header surfaces (was inlined
    5 different ways).
  - `formatDmInputLabel(dm, currentUser)` — `'#<name>'` if set,
    `'the group'` for unnamed groups (collapses the unreadable
    joined-names form), `'@<partner>'` for 1-on-1.

`MessageInput` accepts an optional `placeholder` prop that bypasses the
default `Message {#|@}<channelName>` derivation; DM call sites use it
to inject the `formatDmInputLabel`-based form. 1-on-1 DMs keep the
canonical-view lookup so replicated aliases still surface the home
account's displayName; the placeholder reuses the canonical `dmName`
so header + placeholder stay aligned even when raw partner ≠ canonical.

13 new unit tests covering `formatDmHeaderName` (8 cases: named, blank,
joined, federated-username base, empty group, 1-on-1, no-displayName,
no-partner) and `formatDmInputLabel` (4 cases: named, unnamed,
whitespace-only, 1-on-1). 365 → 377 web tests, 1053 server tests,
typecheck clean.
2026-05-10 23:41:11 +02:00
Jannis Braun ad1a0f7164 fix(presence): broadcast presence_update to friends + DM members + space members
Six WS sites that previously broadcast presence_update to spaces only now use
collectProfileBroadcastTargetIds (the same recipient set as user_updated):
  - ws/handler.ts finalizeDisconnect (offline)
  - ws/handler.ts auth path (online)
  - ws/events.ts handlePresenceUpdate (manual idle/dnd/online)
  - ws/events.ts handleActivityUpdate (rich activity changes)
  - routes/users.ts showActivity-toggle clear
  - routes/users.ts status PATCH

Friends with no shared space + DM-only co-members now see each other's
online/offline transitions live, matching user_updated semantics. Federated
stub presence broadcasts (Task B3) use the same helper, so cross-instance
recipients are uniform.

Updates one assertion in social.federated.test.ts that asserted the old
snowflake-style stub username (now realname-based per A1).
2026-05-05 16:06:45 +02:00
Jannis Braun 613424e1c7 feat(federation): queue S2S presence_update on auth/disconnect/status/activity changes
New FederationPresenceUpdatePayload + queuePresenceRelay() helper. Five WS
sites now project the native user's status (and optional activities) to all
active peers via the outbox: WS auth-success, finalizeDisconnect,
manual presence_update, activity_update, showActivity-toggle clear.

Outbox-only (no mutation-log entry) — presence is ephemeral; the upcoming
peer-activation hook re-emits a fresh snapshot so peers recovering from
unreachable converge without history replay. No-op for replicated users.
2026-05-05 16:01:28 +02:00
Jannis Braun 1ed70a90b1 fix(dm): render system messages in sidebar preview instead of raw JSON
DmLastMessagePreview lacked a `type` field, so the sidebar rendered
`lastMessage.content` verbatim — surfacing JSON like
`{"event":"space_invite",...}` for space invites and member-add events.

Adds `type` to the preview payload (populated server-side from
`dm_messages.type`) and routes all sidebar call sites through a single
`formatDmSidebarPreview` helper that renders human-readable text for
each system event and skips the group `Sender:` prefix on system rows.
2026-04-29 23:13:09 +02:00
Jannis Braun 26a4925032 feat(server): reclassify undeliverable targeted-peer as no_recipient failure (#18)
sendFederatedCallStart now treats a 200-with-undeliverable-messageId as
a peer-failure instead of unconditional success. Feeds the existing
failures[] array and terminal-determination machinery from #16.
New sendFederatedCallStartForTest export mirrors the existing
handleDm*ForTest pattern. TDD — three tests cover single-peer terminal
no_recipient, group-DM mixed delivered+undeliverable non-terminal, and
the happy-path (empty undeliverable → no event).

Also hardens sendCallRelay's response parse: validates undeliverable
is an Array and entries are well-shaped, logs protocol drift at warn/debug
rather than silently falling back to old-peer semantics.
2026-04-24 21:11:11 +02:00
Jannis Braun 7d2137b6d4 feat(server): sendCallRelay surfaces undeliverable messageIds (#18)
CallRelayResult success arm gains undeliverable: string[]. sendCallRelay
parses FederationRelayResponse.undeliverable (when present) and returns
the messageIds so sendFederatedCallStart can reclassify per-peer results.
Old peers that omit the field → empty array → today's behavior.
TDD — three tests cover old-peer, new-peer-with-undeliverable, and 5xx paths.
2026-04-24 21:05:28 +02:00
Jannis Braun 3b61380a1e feat(server): ConnectionManager.evictFederatedCallsForHost (TDD) 2026-04-24 00:37:44 +02:00
Jannis Braun 1e59e7012c fix(server): scope accept-rollback terminal to the acceptor only
Code-review catch: the Path-2 accept-rollback previously emitted
dm_call_undeliverable { terminal: true } via sendToFederatedCallUsers,
which broadcasts to every ringedUserIds entry. In a group DM this
would prematurely tear down non-accepting ringees whose own accept /
reject / timeout paths should govern their state. Switch to
sendToUser(acceptorId) so only the acting user gets the terminal
signal. Reorder the clearFederatedCall to happen before the emit so a
concurrent end-handler sees a cleared entry (clearFederatedCall is
idempotent). Spec updated, test extended to assert the scoping with a
two-ringee group-DM fixture.
2026-04-23 23:38:31 +02:00
Jannis Braun f6252b8ce1 feat(server): fan dm_call_end out on host ring timeout 2026-04-23 23:19:42 +02:00
Jannis Braun 6cd7728f3e feat(server): fanOutCallEvent returns failures; surface to host-side caller 2026-04-23 23:14:50 +02:00
Jannis Braun 3cb80d110d feat(server): aggregate Path-1 call fan-out failures and surface to originator 2026-04-23 23:12:55 +02:00
Jannis Braun 13241345de feat(server): surface dm_call_end relay failure to originator (TDD) 2026-04-23 23:11:20 +02:00
Jannis Braun 5170d316ba feat(server): surface dm_call_reject relay failure to rejector (TDD) 2026-04-23 23:10:18 +02:00
Jannis Braun 07c5b0e7de feat(server): surface dm_call_accept relay failure to acceptor (TDD) 2026-04-23 23:09:18 +02:00
Jannis Braun 06e1ed92e7 refactor(server): add buildFailureFromResult + CallFanoutFailure types 2026-04-23 23:07:07 +02:00
Jannis Braun 44a44163af polish(server): consolidate federation_peers queries, narrow targetedPeers map, use return values from Promise.all to drop non-null assertion 2026-04-21 14:00:29 +02:00
Jannis Braun f83c2af357 feat(server): aggregate call-start failures into dm_call_undeliverable
sendFederatedCallStart now collects per-targeted-peer results and emits
a single dm_call_undeliverable event to the caller when any targeted
peer relay fails. Destroys the local ring room when no plausible
recipient remains (no targeted success + no connected local ringee).

LiveKit pre-flight also emits via this path with reason
'livekit_unavailable' instead of a silent console.warn, closing the
60s hang for unconfigured instances.

Guards against phantom toasts when the caller cancels mid-race by
checking getRoom() before emitting.
2026-04-21 13:53:34 +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 33c45e3184 feat: add awaitingApprovalPeerOrigins and pendingApprovalCount to ready payload 2026-04-20 15:03:29 +02:00
Jannis Braun 5f700c1a51 feat: expose autoAcceptPeering in admin settings and add rejectedPeerOrigins to ready payload 2026-04-09 13:53:26 +02:00
Jannis Braun 5b1c847e57 fix: exclude acting user from federated call end/reject broadcast
sendToFederatedCallUsers sent dm_call_ended/rejected back to the user
who initiated the action. They already disconnected in their click
handler — the redundant event triggered disconnectFn() again, causing
connect and disconnect sounds to play simultaneously.

Added excludeUserId parameter to sendToFederatedCallUsers, used in
handleDmCallEnd and handleDmCallReject Path 2.
2026-04-08 14:49:29 +02:00
Jannis Braun 9e7e068df7 chore: remove federated call diagnostic logging 2026-04-08 14:44:27 +02:00
Jannis Braun 782f5d280d debug: log federated call relay params 2026-04-08 14:38:05 +02:00
Jannis Braun 27514e1596 fix: use wss:// LiveKit URL in federated call relay, not https://
sendFederatedCallStart was sending `https://${domain}/livekit` as the
LiveKit URL. The LiveKit SDK requires `wss://` for WebSocket connections.
The caller (local) worked because it gets the URL from config.livekit.url
(wss://). The federated acceptor failed because it used the relay URL
(https://) — the SDK can't connect over HTTPS.

Now uses config.livekit.url directly, falling back to wss:// if unset.
2026-04-08 14:30:48 +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 70dbe04e6d debug: add diagnostic logging to DM call accept flow 2026-04-08 13:29:45 +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 07edb25d12 feat: update DM call handlers and processors for federatedCallId lookup 2026-04-08 03:18:25 +02:00
Jannis Braun 1690295db5 feat: universal call relay — broadcast to all active federation peers 2026-04-08 03:11:56 +02:00
Jannis Braun 2538bb5e4c fix: remove federated user gate for DM call events
Federated users can now send dm_call_start/accept/reject/end.
Handlers already branch correctly for local vs federated call state.
2026-04-08 03:04:51 +02:00
Jannis Braun 365c64794d refactor: re-key FederatedCallEntry by federatedId, add ringedUserIds 2026-04-08 03:03:44 +02:00
Jannis Braun 23f2863bdc fix: seed DM read states for federated users on first access
When a federated user first accesses DMs on a remote instance,
all channels appear unread because no read_states rows exist.
Seed missing read states to the latest message during the ready
payload build. The S2S relay keeps things in sync going forward.
2026-04-07 21:38:11 +02:00
Jannis Braun 1ada46baff fix: relay mark_unread to peers, fix docs and timestamp consistency
- Add queueReadStateRelay call in handleMarkUnread (skip '0' sentinel)
- Fix double Date.now() in queueReadStateRelay (use single const)
- Fix federation.md: read state relay uses outbox (not fire-and-forget),
  correct payload schema to match implementation
2026-04-07 20:08:31 +02:00
Jannis Braun 32ccd9c410 feat: outbound S2S read state relay
Queue read_state_update events when users ack DM messages on
channels with a federatedId. Translates local message IDs to
federation coordinates using sourceInstance/sourceMessageId.
2026-04-07 19:49:52 +02:00
Jannis Braun 662143bf08 feat: lift DM gates for federated users
Remove requireLocalUser from DM routes, include DMs in federated
ready payload, replace blanket dm_* WS gate with call-only blocklist.
DM calls remain gated (separate scope).
2026-04-07 19:48:07 +02:00
Jannis Braun 5e778f400b fix(server): use two-step MAX(created_at) for DM last message in ready payload
Aligns the ready payload query with the GET /api/dm approach:
get MAX(created_at) per channel first, then fetch the actual
message rows. Avoids issues with federated relay messages whose
local snowflake IDs don't match chronological order.
2026-04-03 00:04:40 +02:00
Jannis Braun 6e5792ff72 feat(server): include attachment metadata in ready payload DM lastMessage 2026-04-02 17:51:37 +02:00
Jannis Braun 1d81838868 feat(server): filter DM read states from ready payload for federated users 2026-04-02 11:33:59 +02:00
Jannis Braun b1ba7b35d2 feat(server): skip DM channel queries in ready payload for federated users 2026-04-02 11:33:51 +02:00
Jannis Braun 54bce681f9 feat(server): add isFederated flag and visibleChannelIdSet to ready payload builder 2026-04-02 11:33:43 +02:00
Jannis Braun c94e1644a8 feat(server): add WS federation gating for DM events and shared handlers 2026-04-02 10:51:20 +02:00
Jannis Braun 32d9f16a15 feat(server): cache isFederated flag at WS auth, pass to event handler 2026-04-02 10:47:52 +02:00
Jannis Braun b3011fb3da feat(federation): add fire-and-forget S2S typing relay
sendTypingRelay() mirrors sendCallRelay() — direct POST to peers,
no outbox, no retry. Uses federatedId for cross-instance channel
identification. Wired into handleDmTypingStart() for typing_start
and broadcastDmMessage() for typing_stop.
2026-04-01 12:53:55 +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 626fbfdba8 fix(federation): address code review findings for FED-009
- Resolve homeUserId from DB in sendFederatedCallStart/End (not raw userId)
- Clear existing timeout in createFederatedCall before overwriting
- Clear federatedCallToken/Url in leaveVoice and handleForceDisconnect
- Remove unnecessary `as any` cast in relay processor
2026-03-31 23:58:27 +02:00
Jannis Braun 2f47d32136 feat(federation): handleDmCallAccept/Reject/End with federated call support (FED-009) 2026-03-31 23:30:27 +02:00
Jannis Braun f2affb0f1d feat(federation): handleDmCallStart sends S2S to remote instances (FED-009) 2026-03-31 23:27:42 +02:00
Jannis Braun cbcb324e24 feat(federation): add federated call registry with ringing timeout (FED-009) 2026-03-31 23:24:45 +02:00