From bcfc275bb3cef82442f98a7cc09af49a5f74c641 Mon Sep 17 00:00:00 2001 From: grendervill Date: Sun, 20 Sep 2026 01:52:27 +0300 Subject: [PATCH] =?UTF-8?q?feat(web):=20=D0=B4=D1=80=D1=83=D0=B7=D1=8C?= =?UTF-8?q?=D1=8F,=20=D0=BB=D0=B8=D1=87=D0=BD=D1=8B=D0=B5=20=D0=B1=D0=B5?= =?UTF-8?q?=D1=81=D0=B5=D0=B4=D1=8B=20=D0=B8=20=D0=B2=D1=85=D0=BE=D0=B4=20?= =?UTF-8?q?=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=BF=D0=BE=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=B3=D0=BB=D0=B0=D1=88=D0=B5=D0=BD=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раздел «Друзья» по запросу пользователя: - первая иконка рейки — «Друзья», серверы ниже за разделителем; личные беседы не засоряют список серверов; - сайдбар друзей: беседы с точками статуса и бейджами непрочитанного, список друзей с кликом на беседу; - контент: пустое состояние «Добавь друзей для общения» с поиском, поиск по логину (debounce), добавление в друзья, входящие и исходящие заявки, меню «…» (удалить/заблокировать), поповер последнего входа в часовом поясе пользователя (по умолчанию МСК), блок «Присоединиться по приглашению» с предпросмотром сервера; - личный чат на существующих компонентах: права по can_send канала, заголовок с именем и статусом собеседника, поиск, пины, ack и typing работают; - realtime: READY dm_channels, события DM_CHANNEL_CREATE, RELATIONSHIP_UPDATE, PRESENCE_UPDATE и MESSAGE_CREATE в беседах; - новый пользователь: «У вас пока нет серверов», создание сервера и вход по приглашению (автовступления нет); - 239 тестов Vitest (было 219), тест паритета ключей i18n (527 ключей). --- web/src/components/friends/FriendRow.tsx | 7 + web/src/components/friends/FriendsView.tsx | 2 +- web/src/components/friends/useDmUnread.ts | 27 +- web/src/i18n/locales/en.json | 6 +- web/src/i18n/locales/ru.json | 6 +- web/src/pages/app/FriendsSidebar.tsx | 4 +- web/src/pages/app/GuildRail.tsx | 2 +- web/src/pages/app/NoGuildView.tsx | 2 +- web/src/stores/gateway.ts | 2 + web/src/stores/session.ts | 15 + web/tests/friends.test.tsx | 802 +++++++++++++++++++++ web/tests/helpers.tsx | 48 +- web/tests/i18n.test.ts | 45 ++ web/tests/login.test.tsx | 6 +- web/tests/shell.test.tsx | 80 +- 15 files changed, 967 insertions(+), 87 deletions(-) create mode 100644 web/tests/friends.test.tsx create mode 100644 web/tests/i18n.test.ts diff --git a/web/src/components/friends/FriendRow.tsx b/web/src/components/friends/FriendRow.tsx index e1dac59..79e472b 100644 --- a/web/src/components/friends/FriendRow.tsx +++ b/web/src/components/friends/FriendRow.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import { removeRelationship, type RelationshipUser } from '@/api/friends'; import { Avatar } from '@/components/ui/Avatar'; +import { ErrorNotice } from '@/components/ui/ErrorNotice'; import { Menu, MenuItem, MenuSeparator } from '@/components/ui/Menu'; import { Popover } from '@/components/ui/Popover'; import { Button } from '@/components/ui/primitives'; @@ -131,6 +132,12 @@ export function FriendRow({ friend }: { friend: RelationshipUser }) {

{hint}

+ {openDm.isError || remove.isError ? ( + + + + ) : null} + setSearchOpen((value) => !value)} > - {t('friends.empty.search')} + {t('friends.search.open')} diff --git a/web/src/components/friends/useDmUnread.ts b/web/src/components/friends/useDmUnread.ts index 0d5a81f..dc2934b 100644 --- a/web/src/components/friends/useDmUnread.ts +++ b/web/src/components/friends/useDmUnread.ts @@ -6,10 +6,14 @@ import { unreadCount, useMessagesStore, type ChannelMessagesState } from '@/stor /** * Непрочитанные личной беседы. * - * Для беседы с загруженной историей счётчик считается как у комнат - * (`unreadCount`). Пока история не загружена, сравниваем `last_message_id` - * канала с отметкой чтения из READY/`READ_STATE_UPDATE`: точного числа в этом - * случае нет, поэтому `exact = false` — в сайдбаре рисуем точку, а не цифру. + * Два сигнала, как у комнат: + * - загруженная история — точное число (`unreadCount`); + * - `last_message_id` канала против отметки чтения из READY / + * `READ_STATE_UPDATE` — когда история ещё не загружена, точного числа нет, + * поэтому в сайдбаре рисуем точку (`exact = false`), а не цифру. + * + * Сигналы объединяются через максимум: сообщение в уже открытой беседе могло + * прийти событием, когда история загружена, а отметки чтения ещё нет. */ export function useDmUnread(channel: Channel): { unread: number; @@ -22,14 +26,17 @@ export function useDmUnread(channel: Channel): { [channel.id], ), ); + // Отметки чтения известны после снапшота READY: отсутствие записи для + // беседы означает «не читалась». + const snapshotApplied = useMessagesStore((state) => state.snapshotApplied); const mentions = record?.mentionCount ?? 0; - if (record !== undefined && record.messages.length > 0) { - return { unread: unreadCount(record), mentions, exact: true }; - } - - const known = record?.readStateKnown === true; + const hasHistory = record !== undefined && record.messages.length > 0; + const fromHistory = hasHistory ? unreadCount(record) : 0; + const known = record?.readStateKnown === true || snapshotApplied; const marker = record?.lastReadMessageId ?? null; const hasNewer = channel.last_message_id !== undefined && channel.last_message_id !== marker; - return { unread: known && hasNewer ? 1 : 0, mentions, exact: false }; + const unread = Math.max(fromHistory, known && hasNewer ? 1 : 0); + + return { unread, mentions, exact: fromHistory > 0 }; } diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 56dce3c..c1cc698 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -176,7 +176,8 @@ }, "empty": { "title": "You have no servers yet", - "body": "Create your own server or join one with an invite." + "body": "Create your own server or join one with an invite.", + "create": "Create your own server" }, "leave": "Leave server", "leaveConfirm": "Leave the server “{{name}}”?", @@ -249,7 +250,8 @@ "adding": "Sending…", "requested": "Request sent", "friend": "Already friends", - "message": "Message" + "message": "Message", + "open": "Search friends" }, "requests": { "incoming": "Incoming requests", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index 506aec1..f1986c1 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -176,7 +176,8 @@ }, "empty": { "title": "У вас пока нет серверов", - "body": "Создайте свой сервер или присоединитесь по приглашению." + "body": "Создайте свой сервер или присоединитесь по приглашению.", + "create": "Создать свой сервер" }, "leave": "Покинуть сервер", "leaveConfirm": "Покинуть сервер «{{name}}»?", @@ -249,7 +250,8 @@ "adding": "Отправляем…", "requested": "Заявка отправлена", "friend": "Уже в друзьях", - "message": "Написать" + "message": "Написать", + "open": "Поиск друзей" }, "requests": { "incoming": "Входящие заявки", diff --git a/web/src/pages/app/FriendsSidebar.tsx b/web/src/pages/app/FriendsSidebar.tsx index bbd227e..5de10a3 100644 --- a/web/src/pages/app/FriendsSidebar.tsx +++ b/web/src/pages/app/FriendsSidebar.tsx @@ -137,7 +137,9 @@ export function FriendsSidebar({ channelId }: { channelId: string | null }) { {t('friends.dmSection')} {direct.length === 0 ? ( -

{t('friends.dmEmpty')}

+

+ {t('friends.dmEmpty')} +

) : (