Commit Graph
631 Commits
Author SHA1 Message Date
Jannis Braun d659637930 docs(message-list): note smooth-scroll exclusion; point sentinel comment at subsystem doc
Reviewer caught two small gaps after Task 3:
- Effect A's smooth-scroll path on new messages is intentionally NOT
  instrumented with the sentinel (the animation lands asynchronously
  across frames; no intermediate scrollTop is worth pinning to). The
  doc now records this so the reader's intuition matches the code.
- The sentinel-branch comment in MessageList.tsx pointed at "spec §2",
  which is the planning doc rather than the durable subsystem spec.
  Pointed at docs/systems/message-list.md instead.
2026-04-25 13:11:05 +02:00
Jannis Braun d18845acb5 fix(web): close handleScroll race that disabled auto-bottom on cold-cache image load
Tracks the post-clamp scrollTop of every programmatic scroll-to-bottom in
lastProgrammaticBottomScrollRef. handleScroll skips the at-bottom flip and
re-pins when the event's scrollTop matches the sentinel — i.e., the event
was queued by our own command and layout grew underneath. User scrolls
break the match (scrollTop changes) and flow through the normal path.

This complements the 2026-03-25 race-fix (which gated auxiliary effects on
isAtBottomRef) by also preventing handleScroll from flipping that ref to
false based on a post-growth distance measurement of our own scroll.
2026-04-25 13:02:01 +02:00
Jannis Braun 84ec8e04b7 fix(web): restore known-dimension reservation in ImageEmbed without letterbox fallback
Restores the dimension reservation reverted in dae6f2d, scoped to
the embed.width && embed.height case only. No fallback aspect-ratio
when dims are null — that was the source of the dark letterbox bars
on Tenor/Klipy GIFs and OG-less images that triggered the revert.

The server-side probe in embedResolver.ts:196 already populates dims
for all image-type embeds; this change makes the client honor them.
2026-04-25 12:53:11 +02:00
Jannis Braun 086158511c test(join-space): update stale assertions to match current UI and API
Four assertions drifted from the current JoinSpace modal, carried over
when the file was renamed from the old JoinServer component in fc06e25
without being updated:

- Placeholder was expanded to cover URL-form invite input
  ('e.g. abc123' → 'e.g. abc123 or https://instance.com/join/abc123').
- 'shows validation error when submitting empty code' asserted a code
  path that no longer exists: the submit button is now disabled when
  the trimmed input is empty (JoinSpace.tsx line 166), so clicking it
  is a no-op and the 'Invite code is required' error from the parser
  is unreachable from the rendered form. Replaced with an assertion
  that the button is disabled while the input is empty — the actual
  validation UX.
- joinByCode signature took on a second `origin` argument during the
  S2S DM unification + federated-join work (spaceStore.ts line 69).
  parseInviteInput returns { code, origin: undefined } for a bare
  code, so the call is `joinByCode('my-invite-code', undefined)`.
  Assertion updated to match exactly.

No code behavior change — tests now reflect actual behavior, which
was already correct and deployed. Closes backlog #28.
2026-04-23 02:52:00 +02:00
Jannis Braun 4d2e50b55b fix(web): extract cross-store resolvers into neutral utility to break TDZ
instanceStore registers three resolver functions at module load —
setApiForOriginResolver, setUserIdForOriginResolver,
setOriginFromHostnameResolver — whose backing `let` bindings used to
live in spaceStore. When the module graph was entered from
instanceStore (e.g. JoinSpaceModal importing useInstanceStore) the
order became spaceStore → chatStore → useWebSocket → socialStore →
instanceStore (top-level setter call) while spaceStore was still
paused on its line-8 chatStore import, so the backing `let` had not
been reached yet and the setter crashed with
`Cannot access '_getApiForOrigin' before initialization`. This left
InviteModal.test.tsx and JoinSpace.test.tsx unable to even load their
suites once AudioManager was mocked away.

Move the three `let` bindings, their setters, their pure getters, plus
the WS-populated user-ID cache (`_myUserIdByOrigin`, setMyUserIdForOrigin,
getCachedUserIdForOrigin, clearMyUserIdCache) into
`packages/web/src/utils/crossStoreResolvers.ts`. The utility imports
nothing from `./stores/*`, so no back-edge exists. spaceStore re-exports
the public surface for backward compatibility with the many existing
import sites; instanceStore imports the setters directly from the
utility (the in-cycle re-export path does not resolve at module-init
time under vite-ssr, so a direct import is required for the top-level
setter calls).

spaceStore's remaining wrappers (resolveUserOrigin, getLayoutHomeOrigin,
getMyUserIdForOrigin) stay where they are — they combine the utility's
pure lookups with authStore state — but now delegate to the utility.

Also adds the AudioManager mock to InviteModal.test.tsx and
JoinSpace.test.tsx so their suites actually load (same pattern already
used in 5 other test files). Net test-suite result: 127/131 pass (up
from 121/121 — +6 newly unlockable). The 4 remaining JoinSpace
failures are pre-existing stale UI-text assertions (the placeholder was
expanded and the submit button was made disable-when-empty) made
visible by the suite now loading; they're orthogonal to this change
and handed back for a separate triage.

Closes backlog #27.
2026-04-23 02:46:19 +02:00
Jannis Braun ae5bdaa338 test(friends): drop stale origin arg from addDmChannel assertion
The DM "Message button" test asserted addDmChannel was called with two
arguments — the channel and an empty-string origin — but the assertion
has been stale since commit 7f3ca4e ("route DM creation to home instance
with federated identity", 2026-04-01). That refactor made FriendsPage
always route DM creation through the home api client and dropped the
second argument from the addDmChannel call because the remote friend's
instanceOrigin no longer applies — home-created DMs don't need a
channelOriginMap entry (lookups default to '' for missing keys; remote-
delivered DMs still get their origin tagged by useWebSocket).

The two-arg assertion was introduced on 2026-03-25 (commit 277b69a)
against an intermediate form of the code that was later rewritten. Drop
the trailing '' so the assertion matches the current, intentional
one-arg call.
2026-04-23 02:28:57 +02:00
Jannis Braun 5a1e354ae1 feat(federation-ui): add needs_attention pill and Reset peering action
- peerStatusLabel/Color/DotColor gain a 'needs_attention' case (rose).
- StatusFilter row gains 'Needs Attention' toggle.
- PeerRow hides Rotate/Revoke and shows 'Reset Peering' when status is
  needs_attention, plus an Auth Failures stat.
- Parent panel routes 'reset' through a ConfirmDialog (danger variant)
  that spells out the destructive nature and the out-of-band re-peer step.
- Client FederationPeer interface gains consecutiveAuthFailures (Task 2
  extended the shared type but the web client's local mirror was stale).

Codifies the manual 'delete both sides, re-peer' workaround as a
first-class admin action.
2026-04-21 21:02:49 +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 072858cbbb fix: multiple federation peering bugs
1. queueOutboxEvent no longer creates pending peer placeholders when
   autoAcceptPeering is disabled — prevents bypassing the admin's
   peering control

2. Approval endpoint checks for 202 before response.ok — when the
   remote also has autoAcceptPeering off, sets peer to awaiting_approval
   instead of incorrectly activating it

3. awaiting_approval status added to Federation panel UI — status label,
   colors, filter options so these peers are visible and manageable
2026-04-20 17:54:00 +02:00
Jannis Braun b0d3ee93b0 fix: restore approvalCount state variable in FederationPanel 2026-04-20 15:11:47 +02:00
Jannis Braun 9e3c411e80 feat: badge count on Federation tab for pending approval requests 2026-04-20 15:10:56 +02:00
Jannis Braun 975ef93cc0 feat: pending approval requests section in Federation panel 2026-04-20 15:10:07 +02:00
Jannis Braun 39028ae56f feat: two-variant DM unreachable indicator for rejected vs awaiting_approval 2026-04-20 15:06:48 +02:00
Jannis Braun b434a736a8 fix: address code review findings (C1, I1, I2)
- C1: Include 'unreachable' peers in queueOutboxEvent query to prevent
  UNIQUE constraint violation when creating placeholders
- I1: Add 'rejected' to StatusFilter in FederationPanel so admins can
  see and manage rejected peers with delete/re-initiate actions
- I2: Map ensurePeered 'failed' to 'pending' in peer/ensure response
  to match spec and client expectations
2026-04-09 14:08:54 +02:00
Jannis Braun 70a421864c feat: DM unreachable member indicators and admin auto-accept peering toggle 2026-04-09 13:58:45 +02:00
Jannis Braun b03df992dc fix: SoundController isSelf must check BOTH local ID and homeUserId
The identity flip during disconnect: updateParticipants resolves
homeUserId → localSnowflake when activeDmCall is set, but reverts
to raw homeUserId when activeDmCall is cleared (before LiveKit
disconnect completes). SoundController sees the snowflake "leave"
and the homeUserId "join" — two phantom events for the same person.

Previous fix only checked homeUserId OR id. Now checks BOTH via
isSelf(id) which matches against a Set of {id, homeUserId}. This
recognizes the user as "self" regardless of which identity format
the participant currently has.
2026-04-08 15:33:51 +02:00
Jannis Braun 5ab1f7b89d fix: SoundController identity mismatch for federated users
Root cause: SoundController compared LiveKit participant p.userId
(which is homeUserId from the home instance) against currentUser.id
(local snowflake on the current instance). For federated users these
are different IDs, so the controller thought the user's own presence
was a stranger — playing user_join/user_leave for self.

Fix: use homeUserId || id for the self-check. This matches the
LiveKit identity format used in federated calls.
2026-04-08 15:25:15 +02:00
Jannis Braun 1468b68199 fix: skip participant leave sounds during self-disconnect
Added justDisconnected guard to the participant sound loop. When
the user hangs up, isLiveKitConnected transitions to false — but
in a separate or same subscription tick, the participants list
also empties. Without the guard, SoundController plays user_leave
for every departed participant AND the disconnect sound simultaneously.

Now: if justDisconnected is true, the entire participant loop is
skipped. Only the disconnect sound plays.
2026-04-08 15:05:49 +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 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 a1c47e071e fix: prevent connecting to own instance as a remote
Add shared isSelfOrigin() helper that normalizes origins before
comparing to window.location.origin. Fixes auto-connect treating
self-referencing replicatedInstances entries as remote connections,
causing duplicate friends/DMs/data. Also hides self-referencing
entries from the Connections panel UI.
2026-04-08 01:28:20 +02:00
Jannis Braun 02a44c201d fix: clean up replicatedInstances and registry on federation identity deletion
After deleting a federated identity, the server-side user_federation_registry
and users.replicated_instances were not cleaned up, causing "already connected"
errors when trying to re-federate. The deletion endpoint now authoritatively
removes both the registry row and the replicatedInstances entry, and bumps the
LWW timestamp to prevent stale client syncs from re-inserting them.

Also extends the endpoint to accept mode 'leave' (skip S2S, just clean up),
and enables the "Select instances..." scope option in DeleteIdentityDialog.
2026-04-03 04:56:25 +02:00
Jannis Braun 1d757ed839 fix(web): update DeleteIdentityDialog descriptions to match actual behavior 2026-04-03 03:57:34 +02:00
Jannis Braun c918199cee feat: wire up DeleteIdentityDialog with S2S deletion modes and scope
Update the dialog to support three deletion modes (leave/soft/full),
scope selector with disabled "Select instances..." option, loading
state during deletion, and per-instance error handling via toasts.
2026-04-03 02:48:35 +02:00
Jannis Braun e7be58f01d feat: implement deleteIdentity with S2S relay for soft/full modes 2026-04-03 02:43:55 +02:00
Jannis Braun 54b7877472 fix(web): fetch members on mount in TransferOwnershipModal
The modal read from the global spaceStore.members which only contains
members for the currently active space. Opening the modal via right-click
context menu on a space that hasn't been navigated to yet resulted in an
empty member list. Now fetches members independently via the dedicated
GET /api/spaces/:id/members endpoint with federation-aware API routing.
2026-04-03 01:37:03 +02:00
Jannis Braun d0864f13b9 fix(web): use Avatar component in TransferOwnershipModal for proper fallback colors
The modal was manually rendering avatars with a static bg-surface-input
background, bypassing the Avatar component's getAvatarGradient() logic.
Users without profile pictures got blank dark circles instead of their
hash-generated or user-set avatar color gradients.
2026-04-03 01:27:51 +02:00
Jannis Braun a07f2314ed fix(web): standardize modal button layout across all dialog modals
Apply symmetrical full-width button pattern to:
- Delete Identity dialog (ConnectedInstances)
- Transfer Ownership confirmation
- Image Crop modal
2026-04-03 01:15:14 +02:00
Jannis Braun 5b9b7fc107 fix(web): redesign ConfirmDialog with symmetrical full-width buttons
- Remove tinted description box, use plain text
- Buttons are now flex-1 equal width side by side
- Cancel gets visible bg-interactive-hover background
- Larger padding, rounded-xl, wider max-w
2026-04-03 01:07:31 +02:00
Jannis Braun 3152c66bd2 fix(web): restore original rounded separator style on friend rows 2026-04-03 01:02:53 +02:00
Jannis Braun 23517447c3 fix(web): flat separators with wrapper divs, first-row border, DM sidebar title case
- FriendItem/RequestItem: add plain wrapper div so divide-y border
  lands on a non-rounded element (fixes curved separator lines)
- Add border-t to divide-y containers so first row also gets top line
- Remove uppercase from "Direct Messages" in DM sidebar
2026-04-03 00:58:03 +02:00
Jannis Braun e16cedded7 feat(web): add confirmation dialog for unfriend action 2026-04-03 00:48:28 +02:00
Jannis Braun fbd69b09cf fix(web): use title case for all Friends page headers 2026-04-03 00:47:19 +02:00
Jannis Braun 33f5edb419 fix(web): use divide-y for flat friend row separators 2026-04-03 00:46:03 +02:00
Jannis Braun c5eddff084 fix(web): friends page header, button visibility, and status casing 2026-04-03 00:44:48 +02:00
Jannis Braun d7f82d5496 refactor(web): use DmListItem in ChannelSidebar, align nav item geometry
Replace ~105 lines of inline DM rendering with <DmListItem>.
Update Friends and Coming Soon nav items to rounded-[6px].
2026-04-02 23:50:48 +02:00
Jannis Braun f7b8770cab feat(web): extract DmListItem component with visual refinements
Extracts the inline DM list item rendering (~105 lines) from
ChannelSidebar.tsx into a standalone component. Applies cohesive
hover states, 6px border radius, 44px row height, and a selected-
state accent bar matching ChannelItem's pattern.
2026-04-02 23:44:03 +02:00
Jannis Braun c7c86007c4 feat(web): render smart timestamps and attachment-aware previews in DM sidebar 2026-04-02 18:00:02 +02:00
Jannis Braun 839197d4b8 fix(web): add re-authenticate flow, safe URL parsing, and minor UI fixes 2026-04-01 18:23:27 +02:00
Jannis Braun 95e4dd9fa8 feat(web): overhaul ConnectedInstances with persistent registry list, filters, expandable rows, and delete identity dialog 2026-04-01 18:08:14 +02:00
Jannis Braun d7e2f25c31 refactor(web): rename removeInstance to disconnectInstance, wire up registry on disconnect 2026-04-01 18:00:08 +02:00
Jannis Braun 7f3ca4e6d1 feat(web): route DM creation to home instance with federated identity
All DM creation and add-member call sites now use the home api client
and pass homeUserId/homeInstance instead of routing to the remote instance.
Also updates addMember in the API client to accept AddDmMemberRequest.
2026-04-01 12:45:13 +02:00
Jannis Braun f55238f8e5 feat(web): wire up confirm dialogs and action handlers for peer management 2026-04-01 01:53:32 +02:00
Jannis Braun a91f7b5df1 feat(web): implement expandable peer details with stats, rotation, and actions 2026-04-01 01:41:11 +02:00
Jannis Braun 72dba1c03e feat(web): add peer list with filtering, sorting, and compact rows 2026-04-01 01:39:19 +02:00
Jannis Braun 74ed5c44ce feat(web): implement FederationPanel global settings section 2026-04-01 01:26:18 +02:00
Jannis Braun 5ca5db7122 refactor(web): extract federation section from GeneralPanel 2026-04-01 01:25:26 +02:00
Jannis Braun f49c2b5488 feat(web): register Federation tab in instance settings 2026-04-01 01:23:56 +02:00
Jannis Braun a3a7527c9e chore: add system docs, specs, and misc updates from other sessions
- Add complete docs/systems/ reference (18 system docs)
- Add federation relay status doc and prior spec/plan docs
- Remove superseded docs/federation-dm-s2s.md (replaced by docs/systems/federation.md)
- CLAUDE.md updates
- Minor fixes in social.ts, types.ts, AddDmMemberModal, NewDmModal, UserSettings
2026-03-31 03:40:34 +02:00