feat(web): вход с 2FA сразу, панель пользователя, модальные настройки, markdown
По правкам пользователя: - вход: поле «Код 2FA или резервный код» видно сразу, принимает 14-символьные резервные коды (раньше влезало 6 цифр), пустое значение не ломает вход; - нижняя панель: одна подложка, слева аватар и имя с мини-профилем и выбором статуса, справа микрофон, камера, наушники (переключатели, стрелка с выбором устройства и громкости, переход в «Аудио-видео») и шестерёнка; - кнопка «Покинуть сервер» из сайдбара убрана (выход — в «Опасной зоне»); - настройки открываются модальным окном со списком пунктов; «Профиль» имеет вкладки «Основной» и «Сервер» (список серверов, ник и «Применить ко всем серверам»), аватар загружается и удаляется, есть часовой пояс (МСК по умолчанию); - markdown: добавлены ==выделение==, __жирный__, ***оба***, ||спойлер||, #канал; - двойной клик по сообщению ставит ответ (не срабатывает на ссылках, кнопках и при выделенном тексте); - 219 тестов Vitest (было 176), check/lint/test/build зелёные.
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, NavLink, useNavigate } from 'react-router';
|
||||
import { NavLink, useNavigate } from 'react-router';
|
||||
|
||||
import { createChannel, fetchChannels, guildChannelsQueryKey, leaveGuild } from '@/api/guilds';
|
||||
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 { myGuildsQueryKey } from '@/api/users';
|
||||
import { ErrorNotice } from '@/components/ui/ErrorNotice';
|
||||
import { Field, SelectField } from '@/components/ui/Field';
|
||||
import { Modal } from '@/components/ui/Modal';
|
||||
@@ -17,6 +16,7 @@ import { groupChannels } from '@/lib/channels';
|
||||
import { canManageGuild, canManageGuildSettings } from '@/lib/identity';
|
||||
import { useCurrentUser } from '@/lib/hooks';
|
||||
import { useSessionStore } from '@/stores/session';
|
||||
import { useUiStore } from '@/stores/ui';
|
||||
|
||||
interface ChannelSidebarProps {
|
||||
guildId: string | null;
|
||||
@@ -79,7 +79,7 @@ export function ChannelSidebar({ guildId, channelId }: ChannelSidebarProps) {
|
||||
);
|
||||
const setGuildChannels = useSessionStore((state) => state.setGuildChannels);
|
||||
const upsertChannel = useSessionStore((state) => state.upsertChannel);
|
||||
const removeGuild = useSessionStore((state) => state.removeGuild);
|
||||
const openSettings = useUiStore((state) => state.openSettings);
|
||||
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [channelName, setChannelName] = useState('');
|
||||
@@ -121,17 +121,6 @@ export function ChannelSidebar({ guildId, channelId }: ChannelSidebarProps) {
|
||||
},
|
||||
});
|
||||
|
||||
const leave = useMutation({
|
||||
mutationFn: () => leaveGuild(guildId ?? ''),
|
||||
onSuccess: () => {
|
||||
if (guildId !== null) {
|
||||
removeGuild(guildId);
|
||||
}
|
||||
void queryClient.invalidateQueries({ queryKey: myGuildsQueryKey });
|
||||
void navigate('/app/empty', { replace: true });
|
||||
},
|
||||
});
|
||||
|
||||
const groups = useMemo(() => groupChannels(channels), [channels]);
|
||||
const categories = useMemo(
|
||||
() => channels.filter((channel) => channel.type === 'category'),
|
||||
@@ -163,14 +152,15 @@ export function ChannelSidebar({ guildId, channelId }: ChannelSidebarProps) {
|
||||
</h1>
|
||||
<div className="flex items-center gap-1">
|
||||
{canManageSettings ? (
|
||||
<Link
|
||||
to={`/settings/servers/${guildId}`}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('channels.settings')}
|
||||
title={t('channels.settings')}
|
||||
className="rounded-[var(--radius-sm)] px-1 text-lg leading-none text-fg-muted transition-[color,background-color,transform] duration-[var(--duration-fast)] hover:bg-surface-3 hover:text-fg active:scale-95"
|
||||
onClick={() => openSettings({ section: 'profile', tab: 'server', guildId })}
|
||||
>
|
||||
⚙
|
||||
</Link>
|
||||
</button>
|
||||
) : null}
|
||||
{canManage ? (
|
||||
<button
|
||||
@@ -228,19 +218,7 @@ export function ChannelSidebar({ guildId, channelId }: ChannelSidebarProps) {
|
||||
</div>
|
||||
|
||||
<footer className="border-t border-border/40 px-2 py-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full"
|
||||
disabled={leave.isPending}
|
||||
onClick={() => {
|
||||
if (window.confirm(t('guilds.leaveConfirm', { name: guild?.name ?? '' }))) {
|
||||
leave.mutate();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('guilds.leave')}
|
||||
</Button>
|
||||
{leave.isError ? <ErrorNotice className="mt-2" error={leave.error} /> : null}
|
||||
<p className="px-1 text-xs text-fg-muted">{t('channels.leaveHint')}</p>
|
||||
</footer>
|
||||
|
||||
<Modal
|
||||
|
||||
Reference in New Issue
Block a user