populateFromReady() built the federatedId dedup set from ALL existing
DMs, including those belonging to the reconnecting origin. Incoming DMs
then matched their own stale entries and were skipped as "duplicates."
The subsequent origin-removal step deleted the old copies, leaving no
DMs from that origin in state.
Scope the dedup set to DMs from OTHER origins only, so reconnecting
origins replace their DMs cleanly while cross-instance dedup still works.
Three bugs that combined to corrupt DM identities during initial sync:
1. Sync endpoint omitted federatedId for group DMs, causing the receiver
to treat them as 1-on-1 DMs and compute wrong pair hashes — creating
phantom channels that collide with real ones.
2. backfillHomeUserId unconditionally overwrote existing homeUserIds,
so a single wrong match would permanently corrupt a user's identity
and cascade to all subsequent lookups.
3. Migration duplicate-stub Criterion 1 ("shared 1-on-1 DM membership")
incorrectly merged different users from the same domain who were
simply having a conversation, destroying one user's identity.
The relay broadcast loop skipped members whose homeInstance matched the
source instance, assuming they already received the message on their home
server. This broke delivery for federated users (e.g. youruser@nova on
orbit) who are actively connected to the remote instance.
Client-side dedup in addRealtimeMessage already handles double delivery
via sourceMessageId cross-matching, making the server-side skip both
unnecessary and harmful.
Queue dm_close relay in DELETE /api/dm/:id after broadcasting locally,
and queue dm_reopen relay inside the myDm.closed === 1 branch of POST
/api/dm so peers are notified only on actual reopen, not new DM creation.
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.
Remove home-only restriction on DM events. Add federatedId
dedup check to dm_channel_created to prevent duplicate sidebar
entries for cross-instance DMs.
Replace home-only DM filter with unified DM list. DMs from all
connected instances are merged, deduplicated by federatedId.
First-loaded copy wins; both copies are in sync via S2S relay.
Receive read_state_update events from peers, translate message
coordinates to local IDs via sourceInstance/sourceMessageId
mapping, update read_states with timestamp-only LWW.
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.
profileUpdatedAt ?? createdAt treated freshly registered users as having
"newer" profiles than users with real edit history, because createdAt is
always NOW at registration time. This broke federation profile sync:
the client correctly pushed home → remote, but the remote server's LWW
guard rejected the write (stored createdAt > incoming profileUpdatedAt).
A null profileUpdatedAt means "never edited" — that's timestamp 0, not
the user's creation time.
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.
Add removeRequestsForUser (socialStore), removeUser (discoverStore), and
clearTypingForUser (chatStore) to support isDeleted cleanup in the
user_updated WS handler.
Switch from manual space-ID collection to collectDeletionBroadcastTargets and
add user_updated broadcast so clients patch their caches when a federated user
is deleted via S2S. Force-disconnect moved after broadcasts so other tabs
receive events before the connection is torn down.
- Orphaned DM channels (zero members) are unreachable garbage — clean
them up regardless of purgeContent mode, not just in full/nuke mode.
- Full/nuke mode now also deletes the user's space messages, their
attachments, and embeds. This is the meaningful distinction: "Delete
User" preserves all content as "Deleted User", "Nuke" removes it.
After a prior deletion + re-federation, multiple user records share
the same homeUserId (one deleted, one live). The unfiltered .get()
returned the older deleted record, causing the idempotency check to
short-circuit and miss the live record entirely.
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.
When a federated user's identity has been tombstoned (isDeleted=1),
findFederatedUser filters them out, causing resolveOrCreateReplicatedUser
to silently create a new stub — a "zombie" resurrection. This guard checks
for a deleted row before creating a stub and returns null instead.
All 11 call sites across federation.ts and dm.ts have been updated with
appropriate null guards: federation relay handlers reject or skip the event
(participant_not_found / accepted no-op), while dm.ts routes convert null
to undefined so the existing 404 path handles it.
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.
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.
- 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