import { useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Link, useParams } from 'react-router-dom'; import { Avatar } from '@/app/Avatar'; import { Icon } from '@/app/Icon'; import { AddFriendModal } from '@/features/contacts/AddFriendModal'; import { fetchAllPages } from '@/lib/api/client'; import { getBlocks, getFriendRequests, getFriends } from '@/lib/api/friends'; const FIXED_ENTRIES = [ { key: 'requests', route: '/contacts/requests', icon: 'person_add', label: '新朋友', colorClass: 'bg-warn-bg text-warn' }, { key: 'groups', route: '/contacts/groups', icon: 'groups', label: '群聊', colorClass: 'bg-accent-soft-bg text-accent' }, { key: 'blocklist', route: '/contacts/blocklist', icon: 'block', label: '黑名单', colorClass: 'bg-[#f3efff] text-[#7c5cf6]' }, // README §5.7: mobile-only -- desktop already reaches this via ChatList's own pinned row (§2.1), // §5.1's desktop entry list doesn't include it. { key: 'system', route: '/messages/system', icon: 'notifications', label: '系统消息', colorClass: 'bg-warn-bg text-warn', mobileOnly: true }, ] as const; /** Server-computed `initial` (doc line 1417) sorts CJK names correctly without a client-side pinyin * library; falls back to the display name's own first character for the rare item missing one. */ function groupInitial(name: string, initial: string | undefined): string { const raw = (initial || name || '').trim().charAt(0).toUpperCase(); return /[A-Z]/.test(raw) ? raw : '#'; } /** * README §5.1 (320px): search + fixed entries (新朋友/群聊/黑名单, 文件助手 removed -- see * ChatListColumn.tsx's PINNED_ENTRIES doc comment for why) + letter-grouped * friend list + a right-side letter index. Friend list is fetched once (fetchAllPages) and both * grouped and searched client-side, same convention ChatListColumn already uses for conversations * -- friend lists are the same small-to-medium scale, no reason to hit the server per keystroke. */ export function ContactsListColumn() { const { userId } = useParams(); const [query, setQuery] = useState(''); const [addingFriend, setAddingFriend] = useState(false); const friendsQuery = useQuery({ queryKey: ['friends'], queryFn: () => fetchAllPages((cursor) => getFriends('', cursor)) }); // Unlike friends/blocks (both already fetchAllPages), this used to read only page 1 -- the request // COUNT BADGE would silently undercount for any account with more than one page of pending requests. const requestsQuery = useQuery({ queryKey: ['friend-requests', 'inbound', 'pending'], queryFn: () => fetchAllPages((cursor) => getFriendRequests('inbound', 'pending', cursor)), }); const blocksQuery = useQuery({ queryKey: ['blocks'], queryFn: () => fetchAllPages((cursor) => getBlocks(cursor)) }); const trimmedQuery = query.trim().toLowerCase(); const friends = friendsQuery.data || []; const filtered = trimmedQuery ? friends.filter((f) => (f.remark || f.display_name || f.user_id).toLowerCase().includes(trimmedQuery)) : friends; const groups = useMemo(() => { const byLetter = new Map(); for (const f of [...filtered].sort((a, b) => (a.remark || a.display_name || '').localeCompare(b.remark || b.display_name || '', 'zh'))) { const letter = groupInitial(f.remark || f.display_name || f.user_id, f.initial); byLetter.set(letter, [...(byLetter.get(letter) || []), f]); } return [...byLetter.entries()].sort(([a], [b]) => (a === '#' ? 1 : b === '#' ? -1 : a.localeCompare(b))); }, [filtered]); const pendingRequestCount = requestsQuery.data?.length ?? 0; return ( ); }