refactor(web): FriendsPage — toast on server errors, drop ConnectInstanceModal triggers

Removes try/catch on the deleted InstanceNotConnectedError/Disconnected
classes (T17). Server now returns structured error codes; client maps
them to human-readable toasts via the new mapServerErrorToMessage helper.

The friend-add flow no longer triggers ConnectInstanceModal — the server
handles all routing/peering/lookup. The modal itself stays for Connections
settings and space-join flows.
This commit is contained in:
Jannis Braun
2026-04-25 22:33:26 +02:00
parent 9d3f75b33c
commit 7309f44de5
2 changed files with 54 additions and 78 deletions
@@ -1,9 +1,9 @@
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useSocialStore, type TaggedFriend, type TaggedFriendRequest, type TaggedUser, InstanceNotConnectedError, InstanceDisconnectedError } from '../../stores/socialStore';
import { useSocialStore, type TaggedFriend, type TaggedFriendRequest, type TaggedUser } from '../../stores/socialStore';
import { useAuthStore } from '../../stores/authStore';
import { ConnectInstanceModal } from '../modals/ConnectInstanceModal';
import { useDiscoverStore, type TaggedDiscoverUser } from '../../stores/discoverStore';
import { mapServerErrorToMessage } from '../../utils/friendErrors';
import { useSpaceStore } from '../../stores/spaceStore';
import { useInstanceStore } from '../../stores/instanceStore';
import { useUIStore } from '../../stores/uiStore';
@@ -398,11 +398,6 @@ function AddFriendTab({
const [rawSearchResults, setRawSearchResults] = useState<TaggedUser[]>([]);
const [searchLoading, setSearchLoading] = useState(false);
const [directAddLoading, setDirectAddLoading] = useState(false);
const [connectModal, setConnectModal] = useState<{
domain: string;
isReconnect: boolean;
username: string;
} | null>(null);
// Fetch discover on mount
useEffect(() => {
@@ -481,34 +476,21 @@ function AddFriendTab({
addToast('Friend request sent!', 'success');
setQuery('');
} catch (err) {
if (err instanceof InstanceNotConnectedError) {
setConnectModal({ domain: err.domain, isReconnect: false, username: query.trim() });
} else if (err instanceof InstanceDisconnectedError) {
setConnectModal({ domain: err.domain, isReconnect: true, username: query.trim() });
} else {
addToast((err as Error).message, 'warning');
const errorBody = (err as { body?: unknown })?.body;
let code: string | undefined;
let message: string | undefined;
if (errorBody && typeof errorBody === 'object') {
code = (errorBody as { error?: string }).error;
message = (errorBody as { message?: string }).message;
}
// Fall back to the Error message if the server didn't send a structured body.
const fallback = message ?? (err instanceof Error ? err.message : undefined);
addToast(mapServerErrorToMessage(code, fallback, query.trim()), 'warning');
} finally {
setDirectAddLoading(false);
}
};
// Connect modal handler
const handleConnected = async (result: 'new' | 'reconnect') => {
const username = connectModal?.username;
const domain = connectModal?.domain;
setConnectModal(null);
if (!username) return;
try {
await sendFriendRequest(username);
const verb = result === 'reconnect' ? 'Reconnected to' : 'Connected to';
addToast(`${verb} ${domain} — friend request sent!`, 'success');
setQuery('');
} catch (err) {
addToast((err as Error).message, 'warning');
}
};
// No-op relationship change for search mode cards (useMemo re-derives from store)
const noopRelationshipChange = useCallback(() => {}, []);
@@ -618,15 +600,6 @@ function AddFriendTab({
)}
</div>
{connectModal && (
<ConnectInstanceModal
domain={connectModal.domain}
targetDisplayName={connectModal.username}
isReconnect={connectModal.isReconnect}
onConnected={handleConnected}
onCancel={() => setConnectModal(null)}
/>
)}
</div>
);
}
@@ -647,12 +620,6 @@ function UserDiscoverCard({
const openModal = useUIStore((s) => s.openModal);
const [actionLoading, setActionLoading] = useState(false);
const [error, setError] = useState('');
const addToast = useUIStore((s) => s.addToast);
const [connectModal, setConnectModal] = useState<{
domain: string;
isReconnect: boolean;
username: string;
} | null>(null);
const baseName = user.username.includes('@') ? user.username.split('@')[0]! : user.username;
const displayName = user.displayName ?? baseName;
@@ -676,32 +643,15 @@ function UserDiscoverCard({
const requestId = await sendFriendRequest(username);
onRelationshipChange(user.id, user._instanceOrigin, 'outbound_pending', requestId);
} catch (err) {
if (err instanceof InstanceNotConnectedError) {
setConnectModal({ domain: err.domain, isReconnect: false, username });
} else if (err instanceof InstanceDisconnectedError) {
setConnectModal({ domain: err.domain, isReconnect: true, username });
} else {
setError(err instanceof Error ? err.message : 'Failed to send request');
const errorBody = (err as { body?: unknown })?.body;
let code: string | undefined;
let message: string | undefined;
if (errorBody && typeof errorBody === 'object') {
code = (errorBody as { error?: string }).error;
message = (errorBody as { message?: string }).message;
}
} finally {
setActionLoading(false);
}
};
const handleDiscoverConnected = async (result: 'new' | 'reconnect') => {
const username = connectModal?.username;
const domain = connectModal?.domain;
setConnectModal(null);
if (!username) return;
setActionLoading(true);
try {
const requestId = await sendFriendRequest(username);
onRelationshipChange(user.id, user._instanceOrigin, 'outbound_pending', requestId);
const verb = result === 'reconnect' ? 'Reconnected to' : 'Connected to';
addToast(`${verb} ${domain} — friend request sent!`, 'success');
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to send request');
const fallback = message ?? (err instanceof Error ? err.message : 'Failed to send request');
setError(mapServerErrorToMessage(code, fallback, username));
} finally {
setActionLoading(false);
}
@@ -885,15 +835,6 @@ function UserDiscoverCard({
</button>
)}
</div>
{connectModal && (
<ConnectInstanceModal
domain={connectModal.domain}
targetDisplayName={user.displayName ?? baseName}
isReconnect={connectModal.isReconnect}
onConnected={handleDiscoverConnected}
onCancel={() => setConnectModal(null)}
/>
)}
</div>
);
}