Розробка групового чата в мобільному додатку
Груповий чат складніший за приватний не в рази, а на порядок. У приватному чаті два учасники — всі события синхронізуються через одне WebSocket-з'єднання до однієї conversation. У груповому чаті на 200 учасників сервер повинен fanout кожного повідомлення в 199 з'єднань, правильно вичислювати непрочитані для кожного, не давити Redis під навантаженням та коректно працювати коли частина учасників офлайн. Це вже проблема архітектури, а не просто UI.
Серверна архітектура: fanout та presences
Розподіл повідомлень
Найболюча частина — доставка повідомлення всім учасникам групи. Синхронний fanout («відправили → прогнали по всім з'єднанням → відповідили клієнту») не масштабується: при групі на 500 людей ітерація по активним з'єднанням займає десятки мілісекунд, а якщо WS-серверів кілька — з'єднання учасників розподілені по різних нодах.
Правильна схема: клієнт → WebSocket-сервер → черга (Redis Pub/Sub або Kafka topic per group) → кожен WS-сервер читає з своєї черги та доставляє онлайн-учасникам → для офлайн-учасників — черга push-сповіщень.
Для груп до 100 учасників Redis Pub/Sub з channel-per-group працює добре. Для більших — Kafka або NATS JetStream з consumer groups.
Непрочитані повідомлення (unread counts)
Класична помилка — зберігати last_read_message_id у таблиці group_members та при кожному запиті рахувати SELECT COUNT(*) WHERE id > last_read_message_id. На групі з тисячами повідомлень та сотнями учасників це вбиває базу.
Робочий підхід: Redis Hash unread:{user_id}:{group_id} → інкремент на кожне нове повідомлення в групі, reset при відкритті чата. Суммарний badge — HVALS unread:{user_id} та суммування на клієнті. При перезапуску Redis — пересчет з PostgreSQL як fallback.
Ролі та права
Схема: owner, admin, member. Права гранулярно: can_send_messages, can_add_members, can_remove_members, can_edit_group_info. Зберігається в group_members.role + JSON-поле permissions для кастомних переопередень. Проверка на рівні API middleware до виконання action.
Мобільний UI: що технічно складно
Список учасників та упоминання
При вводі @ — popup з фільтрацією учасників. На iOS: UITextView + кастомний UIView-overlay позиціонований над клавіатурою через KeyboardLayoutGuide. При виборі учасника — вставка атрибутованої рядка з NSAttributedString та кастомним NSTextAttachment або просто кольоровий range.
У Jetpack Compose: BasicTextField з кастомним VisualTransformation для окраски упоминань + Popup з LazyColumn для выпадающого списку. Тригер @ — через TextFieldValue.text.lastIndexOf('@') з debounce 200ms.
На бекі при збереженні повідомлення — парсинг упоминань регуляркою, створення message_mentions[] записів, окремий push-сповіщення упомянутим учасникам навіть якщо вони відключили сповіщення групи.
Медіа та файли в групі
Фото, відео, документи — завантаження через presigned S3 URL як у приватному чаті, але з додатковою проверкою квот (ліміт хранилища на групу або на користувача). Медіагалерея групи — окремий екран з UICollectionView/LazyVerticalGrid, выборка з таблиці messages по type IN ('image','video') AND group_id = ? з пагінацією.
Превью посилань (link preview): на сервері при отриманні повідомлення з URL — асинхронний job (Sidekiq/Celery) парсит Open Graph метадані, кешує в Redis на 24h, клієнт отримує дані превю в событії message.updated.
Індикатор набору тексту
WS-event typing.start / typing.stop від клієнта → сервер рассилає в групу з user_id печатающого → клієнти показують «Іван набирає...». Проблема: при 20 одночасно набираючих учасниках UX ломається. Обмеження: показуємо максимум 3 імені, далі «і ще N людей набирають». Таймаут: якщо typing.stop не прийшов — автоматично скриваємо через 5 секунд.
Офлайн та синхронізація
Груповий чат вимагає локальної БД. SQLite через SQLCipher (шифрування) — схема: groups, messages, group_members. При старті додатку — sync з сервером: запит всіх груп з last_synced_at, потім для кожної групи — повідомлення після останнього message_id. Конфлікти при одночасному редагуванні — Last Write Wins по updated_at.
На iOS — GRDB.swift поверх SQLite, на Android — Room з Flow-підпиской для реактивного оновлення UI.
Типічні помилки при самостійній реалізації
-
N+1 при завантаженні списку груп: для кожної групи окремий запит
last_message. Рішення: JOIN з підзапитом або денормалізоване полеlast_message_preview. -
Push на всі повідомлення без урахування mute: учасник заглушив групу, але отримує пуш. Проверка
group_members.notifications_mutedна сервері перед відправкою FCM/APNs. - Видалення учасника без cleanup: після кика користувач технічно отримує WS-события якщо з'єднання не закрито. Потрібен примусовий disconnect через сигнал на WS-сервері.
- Відсутність optimistic updates: повідомлення з'являється у UI лише після ответу сервера. Правильно — показати одразу зі статусом «sending», оновити/откатити при ответе.
Терміни та склад робіт
Базовий груповий чат (створення груп, ролі admin/member, повідомлення з пагінацією, пуші) — 3-4 тижні. Повна функціональність (медіа, упоминання, link preview, офлайн-синхронізація, квоти хранилища, галерея групи) — 2-3 місяці. Для Flutter-проекту — приблизно на 30% швидше за рахунок єдиного UI-шару.
Вартість рассчитується після детального ТЗ: склад платформ, обсяг груп (ліміт учасників), вимоги до шифрування та офлайн-режиму істотно впливають на архітектурні рішення.







