feat: DM unreachable member indicators and admin auto-accept peering toggle

This commit is contained in:
Jannis Braun
2026-04-09 13:58:45 +02:00
parent 4285e44d2d
commit 70a421864c
2 changed files with 41 additions and 3 deletions
@@ -3,6 +3,13 @@ import { Avatar } from '../ui/Avatar';
import { Tooltip } from '../ui/Tooltip'; import { Tooltip } from '../ui/Tooltip';
import { parseFederatedUsername, isSelf } from '../../utils/identity'; import { parseFederatedUsername, isSelf } from '../../utils/identity';
import { formatDmTimestamp, formatDmPreview } from '../../utils/dmFormatters'; import { formatDmTimestamp, formatDmPreview } from '../../utils/dmFormatters';
import { getRejectedPeerOrigins } from '../../hooks/useWebSocket';
function isMemberUnreachable(homeInstance: string | null | undefined): boolean {
if (!homeInstance) return false;
const normalized = homeInstance.startsWith('http') ? homeInstance : `https://${homeInstance}`;
return getRejectedPeerOrigins().has(normalized);
}
interface DmListItemProps { interface DmListItemProps {
dm: DmChannel; dm: DmChannel;
@@ -152,6 +159,13 @@ export function DmListItem({ dm, isActive, isUnread, user, onSelect, onClose, on
</svg> </svg>
</Tooltip> </Tooltip>
)} )}
{firstOther && isMemberUnreachable(firstOther.homeInstance) && (
<Tooltip content="Messages may not reach this user — their instance requires manual peering approval" position="top">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" className="text-accent-amber opacity-70 flex-shrink-0">
<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z" />
</svg>
</Tooltip>
)}
{dm.lastMessage && ( {dm.lastMessage && (
<span className={timestampClass}> <span className={timestampClass}>
{formatDmTimestamp(dm.lastMessage.createdAt)} {formatDmTimestamp(dm.lastMessage.createdAt)}
@@ -14,7 +14,7 @@ function FederationGlobalSettings() {
const updateInstanceSettings = useSettingsStore((s) => s.updateInstanceSettings); const updateInstanceSettings = useSettingsStore((s) => s.updateInstanceSettings);
const addToast = useUIStore((s) => s.addToast); const addToast = useUIStore((s) => s.addToast);
const [draft, setDraft] = useState<Pick<InstanceAdminSettings, 'federationRelayEnabled' | 'federationRelayTtlDays' | 'defaultAutoRotateIntervalDays'> | null>(null); const [draft, setDraft] = useState<Pick<InstanceAdminSettings, 'federationRelayEnabled' | 'federationRelayTtlDays' | 'defaultAutoRotateIntervalDays' | 'autoAcceptPeering'> | null>(null);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState(''); const [saveError, setSaveError] = useState('');
@@ -24,6 +24,7 @@ function FederationGlobalSettings() {
federationRelayEnabled: instanceSettings.federationRelayEnabled, federationRelayEnabled: instanceSettings.federationRelayEnabled,
federationRelayTtlDays: instanceSettings.federationRelayTtlDays, federationRelayTtlDays: instanceSettings.federationRelayTtlDays,
defaultAutoRotateIntervalDays: instanceSettings.defaultAutoRotateIntervalDays, defaultAutoRotateIntervalDays: instanceSettings.defaultAutoRotateIntervalDays,
autoAcceptPeering: instanceSettings.autoAcceptPeering,
}); });
} }
}, [instanceSettings]); }, [instanceSettings]);
@@ -33,7 +34,8 @@ function FederationGlobalSettings() {
const hasChanges = instanceSettings const hasChanges = instanceSettings
? draft.federationRelayEnabled !== instanceSettings.federationRelayEnabled || ? draft.federationRelayEnabled !== instanceSettings.federationRelayEnabled ||
draft.federationRelayTtlDays !== instanceSettings.federationRelayTtlDays || draft.federationRelayTtlDays !== instanceSettings.federationRelayTtlDays ||
draft.defaultAutoRotateIntervalDays !== instanceSettings.defaultAutoRotateIntervalDays draft.defaultAutoRotateIntervalDays !== instanceSettings.defaultAutoRotateIntervalDays ||
draft.autoAcceptPeering !== instanceSettings.autoAcceptPeering
: false; : false;
const handleSave = async () => { const handleSave = async () => {
@@ -55,6 +57,7 @@ function FederationGlobalSettings() {
federationRelayEnabled: instanceSettings.federationRelayEnabled, federationRelayEnabled: instanceSettings.federationRelayEnabled,
federationRelayTtlDays: instanceSettings.federationRelayTtlDays, federationRelayTtlDays: instanceSettings.federationRelayTtlDays,
defaultAutoRotateIntervalDays: instanceSettings.defaultAutoRotateIntervalDays, defaultAutoRotateIntervalDays: instanceSettings.defaultAutoRotateIntervalDays,
autoAcceptPeering: instanceSettings.autoAcceptPeering,
}); });
} }
setSaveError(''); setSaveError('');
@@ -75,6 +78,14 @@ function FederationGlobalSettings() {
<Toggle enabled={draft.federationRelayEnabled} onChange={(v) => setDraft({ ...draft, federationRelayEnabled: v })} /> <Toggle enabled={draft.federationRelayEnabled} onChange={(v) => setDraft({ ...draft, federationRelayEnabled: v })} />
</label> </label>
<label className="flex items-center justify-between cursor-pointer">
<div>
<div className="text-sm font-medium text-txt-primary">Auto-accept peering</div>
<div className="text-xs text-txt-tertiary mt-0.5">Automatically accept peering requests from other instances. When disabled, only manually initiated peering is allowed.</div>
</div>
<Toggle enabled={draft.autoAcceptPeering} onChange={(v) => setDraft({ ...draft, autoAcceptPeering: v })} />
</label>
<div> <div>
<div className="text-sm font-medium text-txt-primary mb-1">Relay TTL (days)</div> <div className="text-sm font-medium text-txt-primary mb-1">Relay TTL (days)</div>
<div className="text-xs text-txt-tertiary mb-2">How long relayed messages are retained in the outbox before cleanup</div> <div className="text-xs text-txt-tertiary mb-2">How long relayed messages are retained in the outbox before cleanup</div>
@@ -161,6 +172,7 @@ function peerStatusColor(status: string): string {
case 'active': return 'bg-status-online/15 text-status-online'; case 'active': return 'bg-status-online/15 text-status-online';
case 'pending': return 'bg-accent-lavender/15 text-accent-lavender'; case 'pending': return 'bg-accent-lavender/15 text-accent-lavender';
case 'unreachable': return 'bg-accent-amber/15 text-accent-amber'; case 'unreachable': return 'bg-accent-amber/15 text-accent-amber';
case 'rejected': return 'bg-accent-rose/15 text-accent-rose';
case 'revoked': return 'bg-white/5 text-txt-tertiary'; case 'revoked': return 'bg-white/5 text-txt-tertiary';
default: return 'bg-white/5 text-txt-tertiary'; default: return 'bg-white/5 text-txt-tertiary';
} }
@@ -171,10 +183,22 @@ function peerStatusDotColor(status: string): string {
case 'active': return 'bg-status-online'; case 'active': return 'bg-status-online';
case 'pending': return 'bg-accent-lavender'; case 'pending': return 'bg-accent-lavender';
case 'unreachable': return 'bg-accent-amber'; case 'unreachable': return 'bg-accent-amber';
case 'rejected': return 'bg-accent-rose';
default: return 'bg-txt-tertiary'; default: return 'bg-txt-tertiary';
} }
} }
function peerStatusLabel(status: string): string {
switch (status) {
case 'active': return 'Active';
case 'pending': return 'Pending';
case 'unreachable': return 'Unreachable';
case 'rejected': return 'Rejected (auto-peering denied)';
case 'revoked': return 'Revoked';
default: return status;
}
}
type PeerView = 'active' | 'revoked'; type PeerView = 'active' | 'revoked';
type SortBy = 'name' | 'lastSeen' | 'dateAdded' | 'failures'; type SortBy = 'name' | 'lastSeen' | 'dateAdded' | 'failures';
type StatusFilter = 'active' | 'unreachable' | 'pending'; type StatusFilter = 'active' | 'unreachable' | 'pending';
@@ -418,7 +442,7 @@ function PeerRow({ peer, view, expanded, onToggleExpand, onAction, defaultAutoRo
</div> </div>
<div className="flex items-center gap-2 shrink-0 ml-2"> <div className="flex items-center gap-2 shrink-0 ml-2">
<span className={`inline-flex items-center text-[10px] font-medium px-1.5 py-0.5 rounded ${peerStatusColor(peer.status)}`}> <span className={`inline-flex items-center text-[10px] font-medium px-1.5 py-0.5 rounded ${peerStatusColor(peer.status)}`}>
{peer.status} {peerStatusLabel(peer.status)}
</span> </span>
<span className="text-txt-tertiary text-xs">{expanded ? '▾' : '▸'}</span> <span className="text-txt-tertiary text-xs">{expanded ? '▾' : '▸'}</span>
</div> </div>