import { useCallback, useEffect, useMemo, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { NavLink, useNavigate } from 'react-router'; import { createChannel, fetchChannels, guildChannelsQueryKey } from '@/api/guilds'; import type { Channel } from '@/api/types'; import { ChannelBadge } from '@/components/chat/ChannelBadge'; import { useChannelUnread } from '@/components/chat/useChannelUnread'; import { ErrorNotice } from '@/components/ui/ErrorNotice'; import { Field, SelectField } from '@/components/ui/Field'; import { Modal } from '@/components/ui/Modal'; import { Button } from '@/components/ui/primitives'; import { SkeletonLines } from '@/components/ui/Skeleton'; import { groupChannels } from '@/lib/channels'; import { canAdministerGuild, canManageGuild } from '@/lib/identity'; import { useCurrentUser } from '@/lib/hooks'; import { useSessionStore } from '@/stores/session'; import { useUiStore } from '@/stores/ui'; interface ChannelSidebarProps { guildId: string | null; channelId: string | null; } /** * Ссылка на комнату с бейджем непрочитанного. Отдельный компонент нужен, * чтобы подписка на стор сообщений жила на каждую комнату, а не на весь * сайдбар (иначе любое новое сообщение перерисовывало бы список целиком). */ function ChannelLink({ guildId, channel, selected, }: { guildId: string | null; channel: Channel; selected: boolean; }) { const { t } = useTranslation(); const { unread, mentions } = useChannelUnread(channel.id); const highlighted = !selected && (unread > 0 || mentions > 0); // aria-label ссылки перекрывает её содержимое, поэтому счётчики озвучиваем здесь. const label = mentions > 0 ? t('channels.selectMentions', { name: channel.name, count: mentions }) : unread > 0 ? t('channels.selectUnread', { name: channel.name, count: unread }) : t('channels.select', { name: channel.name }); return ( {channel.name} ); } /** Сайдбар комнат выбранного сервера: категории, текстовые и голосовые комнаты. */ export function ChannelSidebar({ guildId, channelId }: ChannelSidebarProps) { const { t } = useTranslation(); const navigate = useNavigate(); const queryClient = useQueryClient(); const currentUser = useCurrentUser(); const guild = useSessionStore( useCallback((state) => state.guilds.find((item) => item.id === guildId) ?? null, [guildId]), ); const setGuildChannels = useSessionStore((state) => state.setGuildChannels); const upsertChannel = useSessionStore((state) => state.upsertChannel); const openSettings = useUiStore((state) => state.openSettings); const [createOpen, setCreateOpen] = useState(false); const [channelName, setChannelName] = useState(''); const [channelType, setChannelType] = useState<'text' | 'voice'>('text'); const [parentId, setParentId] = useState(''); const channels = useMemo(() => guild?.channels ?? [], [guild]); const needsChannels = guildId !== null && guild !== null && channels.length === 0; const remoteChannels = useQuery({ queryKey: guildChannelsQueryKey(guildId ?? ''), queryFn: ({ signal }) => fetchChannels(guildId ?? '', signal), enabled: needsChannels, retry: 0, }); useEffect(() => { if (guildId === null || remoteChannels.data === undefined) { return; } setGuildChannels(guildId, remoteChannels.data); }, [guildId, remoteChannels.data, setGuildChannels]); const create = useMutation({ mutationFn: () => createChannel(guildId ?? '', { name: channelName.trim(), type: channelType, ...(parentId === '' ? {} : { parent_id: parentId }), }), onSuccess: (channel) => { if (guildId !== null) { upsertChannel(guildId, channel); } setCreateOpen(false); setChannelName(''); void queryClient.invalidateQueries({ queryKey: guildChannelsQueryKey(guildId ?? '') }); void navigate(`/app/${guildId ?? ''}/${channel.id}`); }, }); const groups = useMemo(() => groupChannels(channels), [channels]); const categories = useMemo( () => channels.filter((channel) => channel.type === 'category'), [channels], ); const canManage = guild !== null && canManageGuild(guild.my_permissions ?? [], guild.owner_id, currentUser.data?.id); // Шестерёнка настроек сервера — только владельцу и правам управления. const canManageSettings = guild !== null && canAdministerGuild( guild.my_permissions ?? [], guild.owner_id, currentUser.data?.id, currentUser.data?.is_instance_admin === true, ); // Каналы ещё едут по REST — показываем скелетон вместо «пусто». const channelsLoading = needsChannels && remoteChannels.isPending; if (guildId === null) { return