Фронтенд — окно диалога, группы, участники
Перед началом: список диалогов (Модуль 8). Инструменты: React, effector, WS. Трудоёмкость: ~7 ч.
🔥 Инцидент: «Прочитал — а бейдж остался»
Тестировщик открывает диалог с 12 непрочитанными, читает всё, бейдж в списке остаётся «12». Второй сценарий: бейдж погас, а сообщения в ленте остались неподсвеченными. Обе поломки — про одно: «прочитано» отправлялось по наитию (например, при монтировании окна), а не по честному правилу «сообщение показано пользователю». В этом модуле собираем окно диалога целиком: лента, статусы, receipts, typing, системные сообщения, панель участников — и одно честное определение «прочитано».
📖 Теория
9.1 Лента: догрузка вверх и якорь скролла
История грузится страницами по before_seq (Модуль 3) и приклеивается
сверху. Классический баг «лента прыгает при подгрузке» лечится якорем:
запоминаем scrollHeight до вставки и восстанавливаем позицию после:
tsconst before = el.scrollHeight prependMessages(older) requestAnimationFrame(() => { el.scrollTop = el.scrollHeight - before })
Виртуализацию большого списка оставляем как упражнение (в ui-kit есть
VirtualList — см. курс ui-kit).
9.2 Статусы собственных сообщений
Три состояния, каждое — из своего источника:
| Статус | Источник |
|---|---|
sending | оптимистичная запись до ответа REST |
sent | ответ 201 с серверным id/seq (или WS-эхо с нашим id) |
read | message.read от собеседника: min(last_read_seq других) ≥ seq сообщения |
Оптимистичная запись дедуплицируется по client_msg_id → при приходе
серверного id заменяется каноничной записью (не «добавляется рядом»).
Ошибка отправки — статус error и кнопка «повторить» (тот же
client_msg_id — безопасно, Модуль 3).
9.3 «Прочитано» — это правило, а не событие монтирования
Честное определение: сообщение прочитано, когда лента открыта,
диалог активен, вкладка видима и скролл у нижней границы. Тогда —
отправка read с текущим last_seq диалога. Условия проверяются:
- при новых сообщениях (если видимо и внизу — сразу read);
- при
visibilitychange(вернулись во вкладку); - при доскролле к низу (debounce ~300 мс).
Разделитель «непрочитанные» в ленте: запоминаем last_read_seq на момент
открытия диалога и рисуем линию перед первым непрочитанным — тот же
WhatsApp/Telegram-паттерн.
9.4 Typing: дебаунс туда, таймер обратно
Клиент: событие typing не чаще, чем раз в 2 секунды на диалог (иначе
зальём канал). Получатель: рисует «печатает…» и гасит по таймеру 3–4
секунды от последнего события — новые события продлевают таймер. Тишина —
индикатор исчез. Не забываем исключать себя.
9.5 Системные сообщения и панель участников
Системные сообщения рендерятся из payload (Модуль 5): центрированная
строка «Алиса добавила Боба», без аватара и пузыря. Своё имя — «Вы».
Панель участников (для групп): список с ролями, действия по той же
матрице прав, что на бэкенде — кнопки не показываем там, где сервер
ответит 403. Это дублирование правил — нормальная цена: UX не должен
водить пользователя в отказ. Единый источник: константа can(action, role) из entities/conversation, захардкоженная зеркально бэкенду
(в будущем — из контракта).
💻 Практика
-
entities/message: добавьте статусы (sending | sent | read | error), привязкуclient_msg_id, селекторlastOwnReadTile(по receipts). -
widgets/chat-windowv2:- лента с догрузкой вверх по
before_seqи якорем скролла; - пузыри: свои/чужие, время, галочки статусов; системные — по центру;
- разделитель «непрочитанные» по правилу §9.3;
- шапка диалога: имя (peer/title), typing-индикатор, статус присутствия у direct, кнопка «Участники» у групп.
- лента с догрузкой вверх по
-
features/mark-read: правило §9.3 целиком (активность + видимость + низ скролла; debounce). Проверьте инцидент дня: открыли диалог сверху (не у низа) — бейдж не должен сгорать до честного прочтения. -
features/typing-indicator: отправка (§9.4) и отображение. -
widgets/member-panel: список участников, роли, добавление/удаление, передача владения, приглашение по ссылке (копирование), мьют диалога. Кнопки — по матрице §5.1. -
Сквозной сценарий на двух браузерных профилях (главный тест курса):
- Алиса пишет Бобу → у Боба бейдж «1», у Алисы галочка
sent; - Боб открывает диалог → галочка
read; свайп не нужен — правило честное; - Боб печатает → Алиса видит индикатор, через 4 секунды он гаснет;
- группа: создаём втроём, Боб добавляет Кэрол → системное сообщение у всех; Боб выгнан → его окно переходит в состояние «доступ закрыт».
- Алиса пишет Бобу → у Боба бейдж «1», у Алисы галочка
Чек-лист модуля: лента не прыгает при подгрузке; статусы честные; read отправляется по правилу; typing живёт и гаснет; системные сообщения локализованы клиентом; действия в панели совпадают с правами сервера.
🧩 Quiz
✅ Итоги модуля
- Лента: страницы по seq, якорь скролла, разделитель непрочитанных.
- Статусы сообщений собраны из честных источников: REST-ответ, receipts по last_read_seq, локальная ошибка с повтором.
- «Прочитано» — правило (активен + видим + низ), а не факт открытия.
- Typing: дебаунс на отправке, таймер на приёме.
- Панель участников повторяет матрицу прав бэкенда; выгнанный пользователь получает явное состояние.
🔗 Мостик к Модулю 10
Всё живёт, пока вкладка открыта. Закрыл ноутбук, ушёл из дома — и не узнаешь о сообщении, пока не вернёшься. Следующий модуль — уведомления: Web Push для офлайн-получателей, правила «кого будить» (mute, mentions) и то, почему эти правила обязан применять сервер, а не клиент.