import { useEffect, useId, useState } from 'react'; import { useMutation } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { acceptFriendRequest, removeRelationship, unblockUser, type RelationshipUser, } from '@/api/friends'; import { FriendRow } from '@/components/friends/FriendRow'; import { FramedAvatar, Nickname } from '@/components/profile/Cosmetics'; import { InviteJoin } from '@/components/friends/InviteJoin'; import { UserSearch } from '@/components/friends/UserSearch'; import { Avatar } from '@/components/ui/Avatar'; import { ErrorNotice } from '@/components/ui/ErrorNotice'; import { SkeletonLines } from '@/components/ui/Skeleton'; import { Button } from '@/components/ui/primitives'; import { useFriendsStore } from '@/stores/friends'; import { useSessionStore } from '@/stores/session'; import { useUiStore } from '@/stores/ui'; /** Имя для заявки: список связей не хранит отдельного «ника». */ function displayName(user: RelationshipUser): string { return user.display_name === '' ? user.username : user.display_name; } /** * Раздел «Друзья»: заявки, список друзей, поиск по логину и присоединение * по приглашению. Личные беседы открываются в области контента (см. * `DirectMessageView`), а их список живёт в сайдбаре раздела. */ export function FriendsView() { const { t } = useTranslation(); const friends = useFriendsStore((state) => state.friends); const incoming = useFriendsStore((state) => state.incoming); const outgoing = useFriendsStore((state) => state.outgoing); const blocked = useFriendsStore((state) => state.blocked); const status = useFriendsStore((state) => state.status); const error = useFriendsStore((state) => state.error); const load = useFriendsStore((state) => state.load); const reload = useFriendsStore((state) => state.reload); const dmChannels = useSessionStore((state) => state.dmChannels); const listOpen = useUiStore((state) => state.friendsListOpen); const toggleFriendsList = useUiStore((state) => state.toggleFriendsList); const listId = useId(); const [searchOpen, setSearchOpen] = useState(false); useEffect(() => { void load(); }, [load]); // Снятие блокировки: список перечитывается, человек снова может писать. const unblock = useMutation({ mutationFn: (userId: string) => unblockUser(userId), onSuccess: () => reload(), }); const respond = useMutation({ mutationFn: ({ kind, userId }: { kind: 'accept' | 'decline' | 'cancel'; userId: string }) => kind === 'accept' ? acceptFriendRequest(userId) : removeRelationship(userId), onSuccess: () => reload(), }); // Пусто — только когда нет ни друзей, ни бесед, ни заявок: иначе заявку // было бы не увидеть. const isEmpty = friends.length === 0 && dmChannels.length === 0 && incoming.length === 0 && outgoing.length === 0; return (

{t('friends.title')}

{searchOpen ? setSearchOpen(false)} /> : null}
{status === 'error' ? ( { void reload(); }} /> ) : null} {status === 'loading' && friends.length === 0 ? : null} {incoming.length === 0 ? null : (

{t('friends.requests.incoming')}

    {incoming.map((user) => (
  • {displayName(user)} @{user.username}
  • ))}
)} {outgoing.length === 0 ? null : (

{t('friends.requests.outgoing')}

    {outgoing.map((user) => (
  • {displayName(user)} @{user.username}
  • ))}
)} {isEmpty ? (

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

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

) : (
{/* Список свёрнут по умолчанию: раскрывается снизу вверх, состояние переживает перезагрузку страницы (zustand + localStorage). */}
{!listOpen ? null : friends.length === 0 ? (

{t('friends.friendsEmpty')}

) : (
    {friends.map((friend) => ( ))}
)}
)} {blocked.length === 0 ? null : (

{t('friends.blocked.section', { count: blocked.length })}

    {blocked.map((user) => (
  • @{user.username}
  • ))}
)} {respond.isError ? : null} {unblock.isError ? : null}
); }