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": "This server has reached its member limit.",
"channel.not_found": "Channel not found.",
"validation.failed": "Check the fields you filled in.",
"rate.limited": "Too many requests. Please wait a moment."
"rate.limited": "Too many requests. Please wait a moment.",
"perm.denied": "You do not have permission to do that.",
"rate_limited": "Too many requests. Please wait a moment.",
"message.edit_window_expired": "This message can no longer be edited: more than 24 hours passed.",
"file.too_large": "The file exceeds the allowed size.",
"request.bad": "Malformed request.",
"file.not_found": "File not found."
},
"theme": {
"switchToLight": "Light theme",
@@ -210,7 +216,100 @@
"notInGuild": {
"title": "Server unavailable",
"body": "Looks like you are not a member of this server."
}
},
"selectUnread": "Open channel {{name}}, {{count}} unread",
"selectMentions": "Open channel {{name}}, {{count}} mentions",
"slowmode": "slowmode {{seconds}}s"
},
"chat": {
"log": "Message log of {{name}}",
"loadMore": "Load more",
"loadingMore": "Loading…",
"today": "Today",
"yesterday": "Yesterday",
"edited": "(edited)",
"pending": "sending…",
"newMessages": "New messages",
"adminBadge": "admin",
"unknownAuthor": "unknown",
"start": {
"title": "This is the start of #{{name}}",
"body": "There are no messages here yet. Say something — everyone in the channel will see it."
},
"message": {
"by": "Message from {{name}} in {{channel}}",
"sentAt": "Sent {{time}}"
},
"reply": {
"to": "reply to {{name}}",
"missing": "deleted message",
"composer": "Replying to {{name}}",
"cancel": "Cancel reply"
},
"actions": {
"menu": "Message actions",
"reply": "Reply",
"react": "Add reaction",
"pin": "Pin",
"unpin": "Unpin",
"edit": "Edit",
"delete": "Delete",
"deleteConfirm": "Delete this message?",
"copy": "Copy text"
},
"reactions": {
"title": "Reactions",
"add": "React with {{emoji}}",
"toggle": "Reaction {{emoji}}: {{count}}"
},
"emoji": {
"title": "Emoji picker",
"quick": "Quick reactions",
"popular": "Popular emoji",
"pick": "React with {{emoji}}"
},
"edit": {
"label": "Edit message",
"hint": "Enter to save, Escape to cancel",
"save": "Save",
"cancel": "Cancel"
},
"composer": {
"label": "Message to {{name}}",
"placeholder": "Message #{{name}}",
"send": "Send",
"hint": "Enter to send, Shift+Enter for a new line",
"attach": "Attach a file",
"fileInput": "Choose files",
"uploading": "Uploading attachments…",
"attachments": "Message attachments",
"removeAttachment": "Remove attachment {{name}}",
"counter": "{{count}}/{{max}}",
"tooLarge": "File “{{name}}” exceeds the {{limit}} limit",
"rateLimited": "Too many messages. You can send again in {{seconds}} s.",
"denied": "You are not allowed to send messages in this channel.",
"readOnly": "This channel is read-only for you."
},
"typing_one": "{{names}} is typing…",
"typing_few": "{{names}} are typing…",
"typing_many": "{{names}} are typing…",
"typing_other": "{{names}} are typing…",
"pins": {
"open": "Pinned messages",
"title": "Pinned in #{{name}}",
"empty": "No pinned messages in this channel yet.",
"jump": "Jump"
},
"search": {
"open": "Search this channel",
"title": "Message search",
"label": "Search field",
"placeholder": "Search messages",
"hint": "Type at least two characters.",
"empty": "Nothing found."
},
"unread": "Unread messages: {{count}}",
"mentions": "Mentions: {{count}}"
},
"user": {
"panelLabel": "User panel",
+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": "Панель пользователя",