import type React from 'react'; import { useCallback, useState } from 'react'; import { IconSearch, useUIKit, } from '@tencentcloud/uikit-base-component-react'; import cs from 'classnames'; import { Avatar, useContactStore, useGroupStore } from 'tuikit-atomicx-react/chat'; import { Input } from '../../../baseComp/Input'; import { HighlightText } from '../shared/HighlightText'; import { handleSearchError } from '../shared/utils/handleSearchError'; import { renderSearchResultItem } from '../shared/renderSearchResultItem'; import styles from './ExactSearch.module.scss'; import type { ContactInfo, GroupInfo, UserProfile, GroupSearchInfo } from 'tuikit-atomicx-react/chat'; import type { SearchResultItemComponent } from '../shared/renderSearchResultItem'; import { SearchType as UISearchType } from '../../../types/search'; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- /** * Discriminated union returned by onResultItemClick. * data types are the raw SDK types — no intermediate mapping. * Consumers narrow with `result.type === 'user'` for full type inference. */ export type ExactSearchResult = | { type: 'user'; data: ContactInfo } | { type: 'group'; data: GroupInfo }; export interface ExactSearchProps { /** Called when a result row is clicked. */ onResultItemClick?: (result: ExactSearchResult) => void; /** Called on every keyword change. */ onKeywordChange?: (keyword: string) => void; /** * When provided, a cancel button is rendered next to the search input. * Clicking it calls this callback (e.g. to hide the search UI). */ onCancel?: () => void; /** Label for the cancel button. Only shown when onCancel is provided. */ cancelText?: string; /** * Placeholder for the search input. Defaults to the i18n key * `Search.exact_search_placeholder` ("Search by User ID or Group ID"). */ searchInputPlaceholder?: string; SearchResultItem?: SearchResultItemComponent | undefined; className?: string; style?: React.CSSProperties; autoFocus?: boolean; } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export function ExactSearch({ onResultItemClick, onKeywordChange, onCancel, cancelText, searchInputPlaceholder, SearchResultItem, className, style, autoFocus = false, }: ExactSearchProps) { const { t } = useUIKit(); const { getContactInfo } = useContactStore(); const { getGroupInfo } = useGroupStore(); const [keyword, setKeyword] = useState(''); const [userResults, setUserResults] = useState([]); const [groupResults, setGroupResults] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); // Distinguishes "not searched yet" (show nothing) from "searched, got zero results" (show empty). const [hasSearched, setHasSearched] = useState(false); const resetResults = useCallback(() => { setUserResults([]); setGroupResults([]); setHasSearched(false); setError(null); }, []); const doSearch = useCallback(async (kw: string) => { const trimmed = kw.trim(); if (!trimmed) { return; } setIsLoading(true); setError(null); setUserResults([]); setGroupResults([]); setHasSearched(true); try { const [usersResult, groupResult] = await Promise.allSettled([ getContactInfo([trimmed]), getGroupInfo(trimmed), ]); if (usersResult.status === 'fulfilled') { setUserResults(usersResult.value.filter(u => !!u.userID)); } if (groupResult.status === 'fulfilled' && groupResult.value?.groupID) { setGroupResults([groupResult.value]); } } catch (err) { setError(handleSearchError(t, err)); } finally { setIsLoading(false); } }, [getContactInfo, getGroupInfo, t]); // Input expects ChangeEvent. const handleChange = useCallback((e: React.ChangeEvent) => { const val = e.target.value; setKeyword(val); resetResults(); onKeywordChange?.(val); }, [onKeywordChange, resetResults]); // Input expects KeyboardEvent. const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === 'Enter') { doSearch(keyword); } }, [doSearch, keyword]); const handleClear = useCallback(() => { setKeyword(''); resetResults(); onKeywordChange?.(''); }, [onKeywordChange, resetResults]); const isEmpty = hasSearched && !isLoading && !error && userResults.length === 0 && groupResults.length === 0; return (
{/* Search bar row: [input] [cancel button?] */}
} clearable value={keyword} onChange={handleChange} onKeyDown={handleKeyDown} onClear={handleClear} placeholder={searchInputPlaceholder ?? t('Search.exact_search_placeholder')} autoFocus={autoFocus} enterKeyHint="search" /> {onCancel && ( {cancelText} )}
{/* State views */} {isLoading && (
{t('Search.loading')}
)} {error && (
{error}
)} {isEmpty && (
{t('Search.no_result')}
)} {/* Results */} {!isLoading && !error && (userResults.length > 0 || groupResults.length > 0) && (
{userResults.length > 0 && (

{t('Search.Users')}

{userResults.map((user: ContactInfo) => renderSearchResultItem( SearchResultItem, { keyword, data: { profile: user as unknown as UserProfile }, type: UISearchType.USER, onClick: () => onResultItemClick?.({ type: 'user', data: user }), },
onResultItemClick?.({ type: 'user', data: user })} >
{user.aboutMe && (
)}
, ))}
)} {groupResults.length > 0 && (

{t('Search.Groups')}

{groupResults.map(group => renderSearchResultItem( SearchResultItem, { keyword, data: { groupInfo: group as unknown as GroupSearchInfo }, type: UISearchType.GROUP, onClick: () => onResultItemClick?.({ type: 'group', data: group }), },
onResultItemClick?.({ type: 'group', data: group })} >
{group.notification && (
)}
, ))}
)}
)}
); } export default ExactSearch;