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:
@@ -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",
|
||||
|
||||
@@ -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": "Панель пользователя",
|
||||
|
||||
Reference in New Issue
Block a user