Фронтенд — список диалогов
Перед началом: бэкенд-ядро (Модули 1–7). Инструменты: React, TypeScript, effector, FSD. Трудоёмкость: ~6 ч.
🔥 Инцидент: «Бейджи удвоились после реконнекта»
Студент показывает баг: «Открываю ноутбук после обеда — у диалога с Бобом бейдж "8", хотя сообщений три». Разгадка в двух строках кода: непрочитанные считались инкрементом при каждом WS-событии — а после реконнекта sync доиграл те же события заново. Инкремент поверх уже загруженной истории — почти классика. В этом модуле строим список диалогов так, чтобы источником правды были серверные числа, а WS только двигал их с известной точки.
📖 Теория
8.1 Типы из контракта, а не руками
В бэкенде живёт api/openapi.yaml — единственный источник правды. Генерируем:
bashnpx openapi-typescript ../api/openapi.yaml -o src/shared/api/schema.d.ts
Дальше shared/api экспортирует алиасы (ConversationListItem,
Message, UserCard) из сгенерированных типов. Дрейф типов исчезает как
класс: изменили путь в спеке — фронт не собрался, а не «почему-то 404 в
рантайме».
8.2 Слайсы FSD под список
Раскладка под наш экран:
entities/conversation — модель списка: $conversations, события WS, селекторы
entities/session — текущий пользователь, токен
features/start-dialog — «написать» из контактов (создание direct)
features/create-group — мастер группы (название + участники)
widgets/conversation-list — рендер списка с бейджами и статусами
pages/chats — master-detail: список + область диалога
Правило импортов не меняется: pages → widgets → features → entities → shared, только вниз.
8.3 Единственный источник правды для непрочитанных — сервер
Модель состояния диалогов в entities/conversation:
- при загрузке/синке:
$conversationsперезаписывается серверными значениями (unreadуже посчитан бэкендом какlast_seq - last_read_seq); - live-событие
message.newдля диалога не в фокусе →unread + 1(это инкремент поверх серверной базы, а не поверх «своей» истории); - событие
message.readот собеседника →unreadне трогаем (это про наши отправленные), но запоминаемlast_read_seqдля галочек; - после
sync.completeникаких повторных инкрементов: догон применяется к истории сообщений, а список просто перечитывается с сервера.
Простейший надёжный приём: события живут после sync.complete; всё,
что пришло из догона — уже учтено серверным состоянием списка.
8.4 Сортировка и превью
Список сортируется по last_at (серверное поле), сообщения «из будущего»
невозможны. Превью последнего сообщения для системных — рендерим текстом
по payload (та же логика, что в ленте, переиспользуем функцию из
entities/message). Активный диалог подсвечивается по URL — источник
активности один: роутер.
💻 Практика
-
Сгенерируйте типы из спецификации и подключите в
shared/api(скриптnpm run gen:apiвpackage.json). -
entities/conversation/model.ts:
ts// события conversationsLoaded // ответ REST /conversations messageNew // WS: обновить превью, сорт, unread если не активен readStateChanged // WS message.read: запомнить last_read_seq собеседника presenceChanged // WS presence: статус собеседника в списке // сторы $conversations // список с серверными полями $activeId // активный диалог (из роутера)
-
widgets/conversation-list: аватар, имя (для direct — peer, для группы — title), превью последнего, время (HH:MMсегодня, иначе дата), бейдж непрочитанных, точка присутствия у direct. -
features/start-dialog: изGET /contacts— поиск и кнопка «Написать»; послеPOST /conversations {type: direct}— навигация в диалог. -
features/create-group: мини-мастер — название, мультивыбор из контактов, создание; после создания — в новый диалог. -
Связка с sync из Модуля 6: при коннекте грузим список (
GET /conversations), отправляемsync, и только послеsync.completeпомечаем поток «живым» (флаг$live). Проверьте инцидент дня: тот же сценарий реконнекта — бейджи больше не удваиваются.
Чек-лист модуля: типы генерируются из спеки; список показывает непрочитанные/превью/присутствие; старт диалога и создание группы работают; после реконнекта бейджи не «плывут».
🧩 Quiz
sync.complete в модели списка?✅ Итоги модуля
- Контракт → типы → код: openapi-typescript убирает дрейф типов.
- Список диалогов — селекторы поверх серверного состояния плюс живые события после sync.complete.
- FSD-раскладка: entities (данные), features (сценарии), widgets (композиция), pages (маршрутизация).
- Присутствие и непрочитанные стали видимыми — список ожил.
🔗 Мостик к Модулю 9
Список приводит в диалог — теперь делаем сам диалог: ленту с догрузкой истории вверх, статусы собственных сообщений (отправляется → отправлено → прочитано), индикатор набора, системные сообщения и панель участников группы с действиями по матрице прав.