feat: DM unreachable member indicators and admin auto-accept peering toggle
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user