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.
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.
When the same person registers on multiple federated instances, separate
DM channels are created for each identity, causing duplicate sidebar
entries (e.g., "Nova" appearing twice — once for local, once for
federated account).
Add deduplicateDmChannels() safety net that detects matching counterparts
using two layers: canonicalUserMatch (same ID/homeUserId/username) and
replicatedInstances cross-check (local user has a replicated account on
the federated user's home instance with the same base username). Keeps
the DM with the most recent activity, applied in both setDmChannels and
populateFromReady.
Investigation: queried both live instances (nova.ddns.net, orbit)
and confirmed root cause is same-person-two-accounts, not a server-side
creation bug or identity resolution gap.
Introduces a lightweight DmLastMessagePreview interface for the ready
payload and GET /api/dm response, and widens DmChannel.lastMessage to
accept either DmLastMessagePreview or DmMessageWithUser so both the
static preview path and real-time WS events can coexist without casts.
- disconnectInstance: save cached tokens BEFORE filtering the instance out, so
reconnectInstance can restore disconnected entries from localStorage
- reconnectInstance: if instance is not in active instances array, attempt to
restore it from cached localStorage token before proceeding with reconnect
- reconnectInstance: update registry to unreachable/auth_expired on failure
- forceRemoveEntry: now tears down WS, removes from instances array, purges
token from localStorage, and cleans up space store (was registry-only before)
Adds persistent federation registry storage endpoints under
/api/users/@me/federation-registry. GET returns all registry entries
with the stored updatedAt timestamp. PUT replaces the full registry via
LWW (409 on stale write) in an atomic transaction.
removeChannelStates cleared messages and readStates but not hasMore.
When a closed DM was reopened with the same channel ID, loadMessages
saw the stale hasMore entry and skipped loading — showing an empty
chat. Now clears hasMore so messages reload on reopen.
Three changes to cut deploy time:
1. deploy.sh: Pi and VM deploy concurrently (wall time = max, not sum)
2. deploy.sh: Build cache capped at 2GB instead of nuked after 24h,
so pnpm install layer is reused between deploys
3. Dockerfile: runtime stage uses --prod (skip devDeps). Moved tsx
from devDependencies to dependencies since it's used in CMD.
processDmTypingStartEvent and processDmTypingStopEvent handle
typing indicator relay from peers. Uses federatedId for channel
lookup, resolveLocalUser for ephemeral identity (no stub creation).
Also clears typing indicator in processCreateEvent when a relayed
message arrives — belt-and-suspenders for dropped relay packets.
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.
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.
DMs now come exclusively from the home instance's ready payload.
Remote origins' DM channels are ignored. The deduplication logic
that preferred home copies over remote copies is no longer needed.
Removed dead loadDmChannels() (zero call sites).
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.
Extend the add-member endpoint to resolve federated identity via
resolveOrCreateReplicatedUser() when homeUserId+homeInstance are provided,
falling back to the existing local userId lookup.