import { useEffect, useState } from 'react'; import { useMutation, useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { acceptFriendRequest, searchUsers, sendFriendRequest, userSearchQueryKey, type RelationshipUser, } from '@/api/friends'; import { Avatar } from '@/components/ui/Avatar'; import { ErrorNotice } from '@/components/ui/ErrorNotice'; import { Field } from '@/components/ui/Field'; import { Button } from '@/components/ui/primitives'; import { SkeletonLines } from '@/components/ui/Skeleton'; import { useOpenDirectChannel } from '@/components/friends/useOpenDirectChannel'; import { useFriendsStore } from '@/stores/friends'; /** Пауза перед запросом: пользователь обычно печатает логин целиком. */ export const SEARCH_DEBOUNCE_MS = 400; /** Сервер требует минимум два символа (`q` в `GET /users/search`). */ export const SEARCH_MIN_LENGTH = 2; interface UserSearchProps { onClose: () => void; } /** * Поиск пользователей по логину с debounce и заявкой в друзья. * * Состояние связи берём из стора друзей, а не из ответа поиска: после * «Добавить в друзья» кнопка сразу становится «Заявка отправлена». * Компонент монтируется только при открытом поиске — состояние не залипает. */ export function UserSearch({ onClose }: UserSearchProps) { const { t } = useTranslation(); const [value, setValue] = useState(''); const [query, setQuery] = useState(''); const friends = useFriendsStore((state) => state.friends); const outgoing = useFriendsStore((state) => state.outgoing); const reload = useFriendsStore((state) => state.reload); const openDm = useOpenDirectChannel(); useEffect(() => { const trimmed = value.trim(); const timer = window.setTimeout(() => setQuery(trimmed), SEARCH_DEBOUNCE_MS); return () => { window.clearTimeout(timer); }; }, [value]); const search = useQuery({ queryKey: userSearchQueryKey(query), queryFn: ({ signal }) => searchUsers(query, signal), enabled: query.length >= SEARCH_MIN_LENGTH, retry: 0, }); const add = useMutation({ mutationFn: (user: RelationshipUser) => sendFriendRequest({ username: user.username }), onSuccess: () => reload(), }); const accept = useMutation({ mutationFn: (userId: string) => acceptFriendRequest(userId), onSuccess: () => reload(), }); const results = search.data ?? []; const tooShort = value.trim().length > 0 && value.trim().length < SEARCH_MIN_LENGTH; return (
setValue(event.target.value)} onKeyDown={(event) => { if (event.key === 'Escape') { onClose(); } }} hint={tooShort ? t('friends.search.hint') : undefined} />
{search.isFetching ? : null} {search.isError ? ( { void search.refetch(); }} /> ) : null} {!search.isFetching && query.length >= SEARCH_MIN_LENGTH && results.length === 0 ? (

{t('friends.search.empty')}

) : null}
    {results.map((user) => { const alreadyFriend = friends.some((item) => item.user_id === user.user_id); const requested = !alreadyFriend && outgoing.some((item) => item.user_id === user.user_id); const incoming = !alreadyFriend && user.relationship === 'incoming'; return (
  • {user.display_name} @{user.username} {alreadyFriend ? ( {t('friends.search.friend')} ) : requested ? ( {t('friends.search.requested')} ) : incoming ? ( ) : ( )}
  • ); })}
{add.isError ? : null} {accept.isError ? : null} {openDm.isError ? : null}
); }