Fix connection accept: stale cache, hidden button, and false 409 error

- incomingQuery: staleTime:0 + refetchOnMount:'always' so pending
  requests are always fresh when components mount (was inheriting
  5-min global staleTime, causing empty pendingRequestIds on nav)
- NotificationsPage: show Accept button while incoming data loads
  (was hidden during async gap); disable with spinner until ready
- Both toast and page: treat 409 as success ("already accepted")
  instead of showing error (fixes race when both fire respond)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-05 17:37:21 +08:00
parent 2fb41e0cf4
commit 2139ea8077
3 changed files with 24 additions and 8 deletions

View File

@ -36,9 +36,15 @@ export default function NotificationToaster() {
await respondRef.current({ requestId, action });
toast.dismiss(loadingId);
toast.success(action === 'accept' ? 'Connection accepted' : 'Request declined');
} catch (err) {
} catch (err: any) {
toast.dismiss(loadingId);
// 409 means the request was already resolved (e.g. accepted via notification center)
const status = err?.response?.status;
if (status === 409) {
toast.success(action === 'accept' ? 'Connection already accepted' : 'Request already resolved');
} else {
toast.error(getErrorMessage(err, 'Failed to respond to request'));
}
} finally {
respondingRef.current.delete(requestId);
}

View File

@ -31,7 +31,7 @@ export default function NotificationsPage() {
deleteNotification,
} = useNotifications();
const { incomingRequests, respond, isResponding } = useConnections();
const { incomingRequests, respond, isResponding, isLoadingIncoming } = useConnections();
const queryClient = useQueryClient();
const navigate = useNavigate();
const [filter, setFilter] = useState<Filter>('all');
@ -93,8 +93,14 @@ export default function NotificationsPage() {
}
toast.success(action === 'accept' ? 'Connection accepted' : 'Request declined');
} catch (err) {
// 409 means the request was already resolved (e.g. accepted via toast)
const status = (err as any)?.response?.status;
if (status === 409) {
toast.success(action === 'accept' ? 'Connection already accepted' : 'Request already resolved');
} else {
toast.error(getErrorMessage(err, 'Failed to respond'));
}
}
};
const handleNotificationClick = async (notification: AppNotification) => {
@ -218,22 +224,23 @@ export default function NotificationsPage() {
{/* Connection request actions (inline) */}
{notification.type === 'connection_request' &&
notification.source_id &&
pendingRequestIds.has(notification.source_id) && (
!notification.is_read &&
(isLoadingIncoming || pendingRequestIds.has(notification.source_id)) && (
<div className="flex items-center gap-1.5 shrink-0">
<Button
size="sm"
onClick={(e) => { e.stopPropagation(); handleConnectionRespond(notification, 'accept'); }}
disabled={isResponding}
disabled={isResponding || isLoadingIncoming}
className="gap-1 h-7 text-xs"
>
{isResponding ? <Loader2 className="h-3 w-3 animate-spin" /> : <Check className="h-3 w-3" />}
{(isResponding || isLoadingIncoming) ? <Loader2 className="h-3 w-3 animate-spin" /> : <Check className="h-3 w-3" />}
Accept
</Button>
<Button
variant="ghost"
size="sm"
onClick={(e) => { e.stopPropagation(); handleConnectionRespond(notification, 'reject'); }}
disabled={isResponding}
disabled={isResponding || isLoadingIncoming}
className="h-7 text-xs"
>
<X className="h-3 w-3" />

View File

@ -20,6 +20,8 @@ export function useConnections() {
const { data } = await api.get<ConnectionRequest[]>('/connections/requests/incoming');
return data;
},
staleTime: 0,
refetchOnMount: 'always',
});
const outgoingQuery = useQuery({
@ -95,6 +97,7 @@ export function useConnections() {
incomingRequests: incomingQuery.data ?? [],
outgoingRequests: outgoingQuery.data ?? [],
isLoading: connectionsQuery.isLoading,
isLoadingIncoming: incomingQuery.isLoading,
search: searchMutation.mutateAsync,
isSearching: searchMutation.isPending,
sendRequest: sendRequestMutation.mutateAsync,