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.
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 ban/unban functionality with BansPanel in space settings, voice
moderation context menu (mute/deafen/disconnect), and fix federated
space settings panels to use origin-aware API client. Show domain
indicators for federated members in MembersPanel.
Fix three federation bugs in Space Settings: icon URL was double-wrapped
through home API, upload targeted home instance instead of remote, and
updateSpace used home API client. Also restyle settings panels (User,
Space, Members, Roles, Connected Instances) with consistent section
headers, grouped cards, and improved layout.
Restructure SpaceSettings modal from 778-line monolith into thin
orchestrator with extracted panel components. Add full role CRUD with
permission editor (create, edit name/color/permissions, delete).
Backend: expose role permissions in GET response, accept permissions
in POST/PATCH role endpoints with BigInt validation, broadcast
pushReadyPayload to all space members on role mutations.