feat: stateless federated identity resolver + federation UX improvements

Add identity.ts with isSelf() and resolveDisplayIdentity() — pure
stateless functions that detect replicated-self using the immutable
(username, homeInstance) composite key. No store lookups, no data
mutation. Fixes wrong avatar gradient and missing edit/delete on
own messages in remote channels.

Also includes: optimistic message dedup fix for cross-instance
messages (content-only matching), federation toast notifications,
Username component with @domain display, invite parser, and
deploy script simplification.
This commit is contained in:
Jannis Braun
2026-03-04 03:37:49 +01:00
parent 64fd8edfc9
commit dca9c4dc83
19 changed files with 713 additions and 110 deletions
@@ -0,0 +1,47 @@
import { useEffect, useRef } from 'react';
import { useInstanceStore, type ConnectedInstance } from '../stores/instanceStore';
import { useUIStore } from '../stores/uiStore';
/**
* Watches instanceStore for status changes on remote instances and fires
* toast notifications when connections are lost or restored.
*/
export function useFederationToasts() {
const instances = useInstanceStore((s) => s.instances);
const addToast = useUIStore((s) => s.addToast);
const prevStatuses = useRef<Map<string, ConnectedInstance['status']>>(new Map());
useEffect(() => {
const prev = prevStatuses.current;
for (const inst of instances) {
const prevStatus = prev.get(inst.origin);
if (prevStatus === undefined) {
// First time seeing this instance — record but don't toast
continue;
}
if (prevStatus === inst.status) continue;
const label = inst.label || (() => { try { return new URL(inst.origin).host; } catch { return inst.origin; } })();
if (
prevStatus === 'connected' &&
(inst.status === 'disconnected' || inst.status === 'error')
) {
addToast(`Lost connection to ${label} — reconnecting...`, 'warning');
} else if (
(prevStatus === 'disconnected' || prevStatus === 'error' || prevStatus === 'connecting') &&
inst.status === 'connected'
) {
addToast(`Reconnected to ${label}`, 'success');
}
}
// Update prev statuses
const next = new Map<string, ConnectedInstance['status']>();
for (const inst of instances) {
next.set(inst.origin, inst.status);
}
prevStatuses.current = next;
}, [instances, addToast]);
}
+13
View File
@@ -107,6 +107,13 @@ function handleEvent(origin: string, event: ServerEvent): void {
populateFromReady(origin, event.servers, event.folders, event.dmChannels);
// Mark remote instance as connected in instanceStore
if (!isHome) {
import('../stores/instanceStore').then(({ useInstanceStore }) => {
useInstanceStore.getState().setInstanceStatus(origin, 'connected');
});
}
if (isHome && currentServerId) {
loadServerDetail(currentServerId);
}
@@ -518,6 +525,12 @@ function connectToOrigin(origin: string, token: string): void {
ws.onclose = () => {
conn.ws = null;
stopHeartbeat(conn);
// Mark remote instance as disconnected in instanceStore
if (origin !== HOME_ORIGIN) {
import('../stores/instanceStore').then(({ useInstanceStore }) => {
useInstanceStore.getState().setInstanceStatus(origin, 'disconnected', 'Connection lost — reconnecting');
});
}
// Only reconnect if the connection is still registered (not explicitly disconnected)
if (connections.has(origin) && conn.token) {
const delay = Math.min(1000 * Math.pow(2, conn.reconnectAttempts), 30000);