import React, { useEffect } from 'react'; import { useUIKit, IconChevronRight } from '@tencentcloud/uikit-base-component-react'; import cs from 'classnames'; import { ContactStore, GroupStore } from 'tuikit-atomicx-react/chat'; import { ContactItemType, } from '../../types/contact'; import styles from './ContactList.module.scss'; import { ContactListItem } from './ContactListItem'; import { ContactSearch } from './ContactSearch'; import { useContactList } from './hooks/useContactList'; import { buildFriendSections } from './utils/buildFriendSections'; import type { ContactGroupItem, FriendApplicationInfo, GroupApplicationInfo, ContactListProps, ContactGroup, CustomGroupConfig, ContactLetterSection, ContactItem } from '../../types/contact'; export const ContactList: React.FC = (props) => { const { t, language, i18n, direction } = useUIKit(); const { activeContact, setActiveContact, setContactGroupTitles } = useContactList(); const { friendList, blackList, friendApplicationList, friendApplicationUnreadCount, loadFriends, loadBlackList, loadFriendApplications, acceptFriendApplication, refuseFriendApplication, clearFriendApplicationUnreadCount } = ContactStore(); const { joinedGroupList, applicationList, unreadApplicationCount: groupApplicationUnreadCount, loadJoinedGroups, loadApplications, acceptApplication, refuseApplication, clearApplicationUnreadCount } = GroupStore(); useEffect(() => { loadFriends(); loadBlackList(); loadFriendApplications(); loadJoinedGroups(); loadApplications(); }, []); const defaultGroupTitles: Partial> = React.useMemo(() => { const translate = i18n.getFixedT(language); return { [ContactItemType.FRIEND_REQUEST]: translate('ContactList.new_contacts'), [ContactItemType.GROUP_REQUEST]: translate('ContactList.group_applications'), [ContactItemType.FRIEND]: translate('ContactList.my_friends'), [ContactItemType.GROUP]: translate('ContactList.my_groups'), [ContactItemType.BLACK]: translate('ContactList.blacklist'), }; }, [i18n, language]); useEffect(() => { setContactGroupTitles(defaultGroupTitles); }, [defaultGroupTitles, setContactGroupTitles]); const { activeContactItem, enableSearch = true, groupConfig = {}, className, style, searchPlaceholder = t('ContactList.search_contacts'), searchInputPlaceholder, emptyText = t('ContactList.no_contacts'), ContactItem: CustomContactItem, ContactSearchComponent: CustomSearchComponent, GroupHeader, PlaceholderEmptyList, onContactItemClick, onFriendApplicationAction, onGroupApplicationAction, } = props; useEffect(() => { if (activeContactItem) { setActiveContact(activeContactItem); } }, [activeContactItem, setActiveContact]); const [expandedGroups, setExpandedGroups] = React.useState>( new Set(), ); const toggleGroupExpanded = (groupKey: string) => { const newExpanded = new Set(expandedGroups); const wasExpanded = newExpanded.has(groupKey); if (wasExpanded) { newExpanded.delete(groupKey); } else { newExpanded.add(groupKey); // Clear unread count when expanding if (groupKey === ContactItemType.FRIEND_REQUEST && friendApplicationUnreadCount > 0) { clearFriendApplicationUnreadCount(); } if (groupKey === ContactItemType.GROUP_REQUEST && groupApplicationUnreadCount > 0) { clearApplicationUnreadCount(); } } setExpandedGroups(newExpanded); }; const contactGroups: ContactGroup[] = React.useMemo(() => { const friendSections = buildFriendSections(friendList); const groupConfigs = [ { type: ContactItemType.FRIEND_REQUEST, title: defaultGroupTitles[ContactItemType.FRIEND_REQUEST], items: friendApplicationList, unreadCount: friendApplicationUnreadCount, showTotalCount: false, order: 1, }, { type: ContactItemType.GROUP_REQUEST, title: defaultGroupTitles[ContactItemType.GROUP_REQUEST], items: applicationList, unreadCount: groupApplicationUnreadCount, showTotalCount: false, order: 2, }, { type: ContactItemType.FRIEND, title: defaultGroupTitles[ContactItemType.FRIEND], items: friendList, count: friendList.length, sections: friendSections, showTotalCount: true, order: 3, }, { type: ContactItemType.GROUP, title: defaultGroupTitles[ContactItemType.GROUP], items: joinedGroupList, count: joinedGroupList.length, showTotalCount: true, order: 4, }, { type: ContactItemType.BLACK, title: defaultGroupTitles[ContactItemType.BLACK], items: blackList, count: blackList.length, showTotalCount: true, order: 5, }, ]; const customGroupConfig = groupConfig as Record; const groups = groupConfigs .filter(config => !customGroupConfig[config.type]?.hidden) .map(config => ({ key: config.type, type: config.type, title: customGroupConfig[config.type]?.title ?? config.title ?? '', items: config.items, ...(config.count !== undefined && { count: config.count }), ...(config.sections !== undefined && { sections: config.sections }), ...(config.unreadCount !== undefined && { unreadCount: config.unreadCount }), ...(config.showTotalCount !== undefined && { showTotalCount: config.showTotalCount }), isExpanded: expandedGroups.has(config.type), order: customGroupConfig[config.type]?.order ?? config.order, })) .sort((a, b) => a.order - b.order); return groups; }, [ defaultGroupTitles, friendApplicationList, friendApplicationUnreadCount, applicationList, friendList, joinedGroupList, blackList, groupConfig, expandedGroups, ]); const getItemId = (item: ContactItem): string => { if ('userID' in item) { return item.userID; } if ('groupID' in item) { return item.groupID; } return ''; }; const handleContactClick = (type: ContactItemType, item: ContactItem) => { const contactGroupItem: ContactGroupItem = { type, data: item }; onContactItemClick?.(contactGroupItem); setActiveContact(contactGroupItem); }; const handleFriendApplicationAction = async (action: 'accept' | 'refuse', application: FriendApplicationInfo) => { try { if (action === 'accept') { await acceptFriendApplication(application); } else { await refuseFriendApplication(application); } onFriendApplicationAction?.(action, application); } catch (error) { console.error(`Failed to ${action} friend application:`, error); } }; const handleGroupApplicationAction = async (action: 'accept' | 'refuse', application: GroupApplicationInfo) => { try { if (action === 'accept') { await acceptApplication(application); } else { await refuseApplication(application); } onGroupApplicationAction?.(action, application); } catch (error) { console.error(`Failed to ${action} group application:`, error); } }; const renderGroupHeader = (group: ContactGroup) => { if (GroupHeader) { return toggleGroupExpanded(group.key)} />; } return (
toggleGroupExpanded(group.key)} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { toggleGroupExpanded(group.key); } }} >
{group.title}
{group.showTotalCount && (
{group.count ?? 0}
)} {!group.showTotalCount && group.unreadCount !== undefined && group.unreadCount > 0 && (
{group.unreadCount > 99 ? '99+' : group.unreadCount}
)}
); }; const ContactItemComponent = CustomContactItem || ContactListItem; const renderContactItem = (item: ContactItem, type: ContactItemType, index: number) => ( ); const renderFriendSections = (sections: ContactLetterSection[]) => (
{sections.map(section => (
{section.title} ( {section.count} )
{section.items.map((friend, index) => renderContactItem(friend, ContactItemType.FRIEND, index))}
))}
); const renderGroupContent = (group: ContactGroup) => { if (!group.isExpanded) { return null; } if (group.type === ContactItemType.FRIEND && group.sections?.length) { return renderFriendSections(group.sections); } return (
{group.items.map((contactItem: ContactItem, index: number) => renderContactItem(contactItem, group.type, index))}
); }; const renderContactGroup = (group: ContactGroup) => (
{renderGroupHeader(group)} {renderGroupContent(group)}
); const renderPlaceholderEmpty = () => { if (PlaceholderEmptyList) { return PlaceholderEmptyList; } return (
{emptyText}
); }; const renderSearch = () => { if (!enableSearch) { return null; } const SearchComponent = CustomSearchComponent || ContactSearch; return (
{ handleContactClick?.(item.type, item.data); }} placeholder={searchPlaceholder} {...(searchInputPlaceholder !== undefined && { searchInputPlaceholder })} />
); }; return (
{renderSearch()}
{contactGroups.length > 0 ? ( contactGroups.map(renderContactGroup) ) : ( renderPlaceholderEmpty() )}
); }; export default ContactList;