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:
@@ -40,6 +40,31 @@ export function apiErrorCode(error: unknown): string | null {
|
||||
return isApiErrorPayload(payload) ? payload.code : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Дополнительное поле конверта ошибки (например, `retry_after_ms` у 429).
|
||||
*
|
||||
* Сервер кладёт детали рядом с `code`/`message`, а не во вложенный `details`,
|
||||
* поэтому ищем значение в исходном конверте; `details` остаётся запасным
|
||||
* вариантом для ошибок, собранных вручную.
|
||||
*/
|
||||
export function apiErrorField(error: unknown, key: string): unknown {
|
||||
if (error === null || typeof error !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
const record = error as { apiError?: unknown; details?: unknown };
|
||||
if (isApiErrorPayload(record.apiError)) {
|
||||
const value = (record.apiError as unknown as Record<string, unknown>)[key];
|
||||
if (value !== undefined) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
const details = record.details;
|
||||
if (typeof details === 'object' && details !== null) {
|
||||
return (details as Record<string, unknown>)[key];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export interface RequestOptions {
|
||||
method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
||||
body?: unknown;
|
||||
|
||||
Reference in New Issue
Block a user