diff --git a/packages/web/src/components/chat/FriendsPage.tsx b/packages/web/src/components/chat/FriendsPage.tsx index a1978597..b3d78fc3 100644 --- a/packages/web/src/components/chat/FriendsPage.tsx +++ b/packages/web/src/components/chat/FriendsPage.tsx @@ -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([]); 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({ )} - {connectModal && ( - setConnectModal(null)} - /> - )} ); } @@ -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({ )} - {connectModal && ( - setConnectModal(null)} - /> - )} ); } diff --git a/packages/web/src/utils/friendErrors.ts b/packages/web/src/utils/friendErrors.ts new file mode 100644 index 00000000..ec1531a5 --- /dev/null +++ b/packages/web/src/utils/friendErrors.ts @@ -0,0 +1,35 @@ +/** + * Map a server error code (from POST /api/social/requests) to a human-readable + * toast message. The server emits these codes; the client renders them. + * + * Used by FriendsPage and UserProfileModal when the server returns an error + * from the friend-add flow. + */ +export function mapServerErrorToMessage( + code: string | undefined, + fallback: string | undefined, + handle: string, +): string { + switch (code) { + case 'username_required': return 'Enter a username.'; + case 'cannot_friend_self': return "You can't friend yourself."; + case 'peer_rejected': + return `Instance has rejected federation. Contact your admin.`; + case 'user_not_found': + return `No user "${handle}" on the remote instance.`; + case 'already_friends': return "You're already friends with this user."; + case 'peer_pending_approval': + return "The remote instance's admin needs to approve federation. Try again later."; + case 'peer_pending': + return 'Connecting to the remote instance — try again in a moment.'; + case 'incoming_request_exists': + return `${handle} has already sent you a request — open the Pending tab.`; + case 'lookup_rate_limited': return 'Too many lookups; try again in a minute.'; + case 'peer_unreachable': return 'The remote instance is currently unreachable.'; + case 'invalid_target_domain': return 'Invalid target domain.'; + case 'not_authoritative_for_sender': + // Should not happen in normal client usage — internal protocol violation. + return 'Could not send friend request (authority error).'; + default: return fallback ?? 'Could not send friend request.'; + } +}