feat(web): чат Фазы 2 — лента, композер, реакции, ответы, поиск и вложения

Полноценный текстовый чат в веб-клиенте:

- стор сообщений по комнатам (кэш 200, оптимистичная отправка с nonce,
  применение всех событий Gateway, пагинация курсором, непрочитанные и
  упоминания, сброс при логауте и INVALID_SESSION);
- лента: группировка подряд идущих сообщений одного автора, разделители дат,
  разделитель «новые сообщения», подгрузка истории вверх, пустые состояния;
- разметка: жирный/курсив/код/блок кода/зачёркнутый/цитата/ссылки (только
  http(s)) и упоминания <@id> — безопасный рендер React-узлами без
  dangerouslySetInnerHTML;
- реакции чипами с подсветкой своих, быстрые эмодзи и пикер, панель действий и
  контекстное меню (ответ, реакция, закрепление, правка, удаление, копирование);
- ответы с переходом к оригиналу, inline-правка, «печатает…» с POST /typing раз
  в 3 с, пины в модалке, поиск по комнате с подсветкой и переходом;
- композер: автовысота, Enter/Shift+Enter, лимит 4000, drag&drop и скрепка
  (POST /channels/{id}/files → attachment_ids), обработка 429 с таймером и
  perm.denied с причиной, read-only без SEND_MESSAGES;
- бейджи непрочитанных и упоминаний в сайдбаре, авто-POST /ack при активном окне;
- 176 тестов Vitest (было 100), check/lint/test/build зелёные.
This commit is contained in:
2026-09-20 00:15:18 +03:00
parent b22db8f048
commit b01eb1f34c
34 changed files with 6417 additions and 52 deletions
+101 -2
View File
@@ -50,7 +50,13 @@
"guild.member_limit_reached": "На сервере достигнут лимит участников.",
"channel.not_found": "Комната не найдена.",
"validation.failed": "Проверьте заполненные поля.",
"rate.limited": "Слишком много запросов. Подождите немного."
"rate.limited": "Слишком много запросов. Подождите немного.",
"perm.denied": "Недостаточно прав для этого действия.",
"rate_limited": "Слишком много запросов. Подождите немного.",
"message.edit_window_expired": "Сообщение нельзя изменить: прошло больше 24 часов.",
"file.too_large": "Файл больше допустимого размера.",
"request.bad": "Некорректный запрос.",
"file.not_found": "Файл не найден."
},
"theme": {
"switchToLight": "Светлая тема",
@@ -210,7 +216,100 @@
"notInGuild": {
"title": "Сервер недоступен",
"body": "Похоже, вы не состоите в этом сервере."
}
},
"selectUnread": "Открыть комнату {{name}}, непрочитанных: {{count}}",
"selectMentions": "Открыть комнату {{name}}, упоминаний: {{count}}",
"slowmode": "медленный режим {{seconds}} с"
},
"chat": {
"log": "Лента сообщений комнаты {{name}}",
"loadMore": "Загрузить ещё",
"loadingMore": "Загружаем…",
"today": "Сегодня",
"yesterday": "Вчера",
"edited": "(изменено)",
"pending": "отправляется…",
"newMessages": "Новые сообщения",
"adminBadge": "админ",
"unknownAuthor": "неизвестный",
"start": {
"title": "Это начало комнаты #{{name}}",
"body": "Здесь пока нет сообщений. Напишите первым — сообщение увидят все участники комнаты."
},
"message": {
"by": "Сообщение от {{name}} в комнате {{channel}}",
"sentAt": "Отправлено {{time}}"
},
"reply": {
"to": "ответ {{name}}",
"missing": "сообщение удалено",
"composer": "Ответ {{name}}",
"cancel": "Отменить ответ"
},
"actions": {
"menu": "Действия с сообщением",
"reply": "Ответить",
"react": "Реакция",
"pin": "Закрепить",
"unpin": "Открепить",
"edit": "Изменить",
"delete": "Удалить",
"deleteConfirm": "Удалить это сообщение?",
"copy": "Копировать текст"
},
"reactions": {
"title": "Реакции",
"add": "Поставить реакцию {{emoji}}",
"toggle": "Реакция {{emoji}}: {{count}}"
},
"emoji": {
"title": "Выбор эмодзи",
"quick": "Быстрые реакции",
"popular": "Популярные эмодзи",
"pick": "Поставить реакцию {{emoji}}"
},
"edit": {
"label": "Редактирование сообщения",
"hint": "Enter — сохранить, Escape — отменить",
"save": "Сохранить",
"cancel": "Отмена"
},
"composer": {
"label": "Сообщение в комнату {{name}}",
"placeholder": "Написать в #{{name}}",
"send": "Отправить",
"hint": "Enter — отправить, Shift+Enter — новая строка",
"attach": "Прикрепить файл",
"fileInput": "Выбрать файлы",
"uploading": "Загружаем вложения…",
"attachments": "Вложения к сообщению",
"removeAttachment": "Убрать вложение {{name}}",
"counter": "{{count}}/{{max}}",
"tooLarge": "Файл «{{name}}» больше допустимых {{limit}}",
"rateLimited": "Слишком часто. Отправить можно будет через {{seconds}} с.",
"denied": "Нет права отправлять сообщения в эту комнату.",
"readOnly": "В эту комнату нельзя писать."
},
"typing_one": "{{names}} печатает…",
"typing_few": "{{names}} печатают…",
"typing_many": "{{names}} печатают…",
"typing_other": "{{names}} печатают…",
"pins": {
"open": "Закреплённые сообщения",
"title": "Закреплённые в #{{name}}",
"empty": "В этой комнате пока нет закреплённых сообщений.",
"jump": "Перейти"
},
"search": {
"open": "Поиск по комнате",
"title": "Поиск по сообщениям",
"label": "Строка поиска",
"placeholder": "Поиск по сообщениям",
"hint": "Введите не меньше двух символов.",
"empty": "Ничего не найдено."
},
"unread": "Непрочитанных сообщений: {{count}}",
"mentions": "Упоминаний: {{count}}"
},
"user": {
"panelLabel": "Панель пользователя",