import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Link } from 'react-router-dom'; import { Avatar } from '@/app/Avatar'; import { Icon } from '@/app/Icon'; import { SelectionCheckbox } from '@/app/SelectionCheckbox'; import { Toast } from '@/app/Toast'; import { fetchAllPages } from '@/lib/api/client'; import { batchUnblockUsers, getBlocks, unblockUser } from '@/lib/api/friends'; import { formatEventTimestamp } from '@/lib/messages/time'; /** README §5.6. `getBlocks` nests the user under `.user` (see friends.ts's own comment on that * endpoint's real shape, a doc-drift bug caught during Phase 1-2's review). * * Multi-select + batch-unblock has no dedicated mock anywhere in this design handoff (04-contacts.dc.html's * own #1g only ever shows a single row, one 移出 button each) -- rather than invent a new interaction * pattern from nothing, this reuses the SAME 多选/已选择N项/取消 header convention FavoritesLayout.tsx * already established for its own multi-select, so it's consistent with something this app already does, * not a one-off design. */ export function BlocklistView() { const queryClient = useQueryClient(); const [error, setError] = useState(''); const [selectMode, setSelectMode] = useState(false); const [selected, setSelected] = useState>(new Set()); const query = useQuery({ queryKey: ['blocks'], queryFn: () => fetchAllPages((cursor) => getBlocks(cursor)) }); const unblockMutation = useMutation({ mutationFn: (userId: string) => unblockUser(userId), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['blocks'] }), onError: (err) => setError(err instanceof Error ? err.message : '移出黑名单失败'), }); const exitSelectMode = () => { setSelectMode(false); setSelected(new Set()); }; const bulkUnblockMutation = useMutation({ mutationFn: (userIds: string[]) => batchUnblockUsers(userIds), onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ['blocks'] }); // A 200 here only means the REQUEST was accepted -- the doc's own worked example returns a // per-item `failed[]` array alongside `items[]`, so some of the selected users can fail to // unblock while the rest succeed. Silently treating this as full success (the same pattern // already fixed for favorites/GroupAdminModal elsewhere in this app) would leave a user still // on the blocklist with no indication anything went wrong. if (data.failed?.length) setError(`${data.failed.length} 人移出失败,请重试`); exitSelectMode(); }, onError: (err) => setError(err instanceof Error ? err.message : '批量移出失败'), }); const toggleSelected = (userId: string) => setSelected((prev) => { const next = new Set(prev); if (next.has(userId)) next.delete(userId); else next.add(userId); return next; }); const items = query.data || []; return (
{selectMode ? (
已选择 {selected.size} 项
) : (
黑名单{items.length ? ` ${items.length}` : ''} {items.length > 1 ? ( ) : null}
)}
{query.isLoading ? (
加载中…
) : query.isError ? (
加载失败,请重试
) : items.length === 0 ? (
黑名单是空的
) : ( items.map((b) => (
toggleSelected(b.user.user_id) : undefined} onKeyDown={selectMode ? (e) => e.key === 'Enter' && toggleSelected(b.user.user_id) : undefined} className={`flex items-center gap-3 rounded-xl bg-white px-4 py-3 shadow-bubble ${selectMode ? 'cursor-pointer' : ''}`} > {selectMode ? : null} {b.user.display_name || b.user.user_id} 拉黑于 {formatEventTimestamp(new Date(b.blocked_at))} · 对方无法向你发送消息或发起通话 {selectMode ? null : ( )}
)) )} {items.length ? (

移出黑名单后恢复通信权限,但不会自动恢复已删除的好友关系。

) : null}
setError('')} />
); }