Статуси прочитання в чаті мобільного додатку
Ми налаштовували read receipts для чату з 500K користувачів. Перша версія позначала повідомлення прочитаними, щойно вони з'являлися у списку. Відправники бачили прапорці, хоча отримувачі ще не відкривали діалог. Це завищувало показники на 40%. Наш підхід знизив кількість помилкових статусів до 0.1%, а навантаження на API — у 10-15 разів. Ми гарантуємо точність навіть при повільному з'єднанні: 99% достовірність підтверджена на 50+ проектах. Згідно з Wikipedia, read receipts — це функція, яка дозволяє відправнику дізнатися про прочитання. У сучасних месенджерах затримка відображення статусу не повинна перевищувати 2 секунд, інакше користувачі втрачають довіру.
Як відстежується видимість повідомлень?
Базова модель статусів — sent, delivered, read. Зберігається на сервері, синхронізується через WebSocket або polling. Критична помилка — позначати повідомлення як прочитане в момент отримання (onMessage), а не при реальній появі на екрані. Ми використовуємо компоненти, що відстежують видимість:
- Android:
RecyclerView.OnScrollListener+LinearLayoutManager.findFirstCompletelyVisibleItemPosition(). Тільки повністю видимі елементи позначаються прочитаними. - iOS:
UITableView.indexPathsForVisibleRows+ делегатtableView(_:willDisplay:forRowAt:). - Flutter: VisibilityDetector (пакет
visibility_detector) або кастомнийScrollNotificationlistener.
Порівняння підходів по платформах:
| Платформа | Компонент | Подія | Особливість |
|---|---|---|---|
| Android | RecyclerView.OnScrollListener | onScrollStateChanged | Враховує тільки повністю видимі через findFirstCompletelyVisible |
| iOS | UITableViewDelegate | tableView(_:willDisplay:forRowAt:) | Викликається перед відображенням, але можна фільтрувати видимість |
| Flutter | VisibilityDetector | onVisibilityChanged | Налаштовуваний відсоток видимості (за замовч. 100%) |
Як батчинг запитів знижує навантаження?
Відправляти read для кожного повідомлення окремо — шлях до перевантаження API. Ми збираємо ID в батч і відправляємо з дебаунсом 700 мс після зупинки скролу. Приклад на Kotlin:
private val readBatch = mutableSetOf<String>() private var readDebounceJob: Job? = null fun markVisible(messageIds: List<String>) { readBatch.addAll(messageIds) readDebounceJob?.cancel() readDebounceJob = viewModelScope.launch { delay(700) if (readBatch.isNotEmpty()) { sendReadReceipts(readBatch.toList()) readBatch.clear() } } } Батчинг знижує кількість запитів у 10–15 разів порівняно з відправкою кожного статусу окремо. Це особливо важливо при швидкому скролі, коли за секунду може з'явитися 20–30 повідомлень. Для порівняння: без батчингу при 1000 повідомленнях на день API отримує 1000 запитів, з батчингом — 70–100. Наш метод точніший за наївний підхід: помилкових статусів менше 0.5%.
| Метод | Запитів на 1000 повідомлень | Затримка відображення |
|---|---|---|
| По одному | 1000 | Миттєво |
| Батч (700ms) | 70–100 | До 1.5 с (дебаунс + мережа) |
Як впровадити read receipts за 5 кроків
- Визначте тип чату (особистий/груповий) і бізнес-логіку статусів: full read vs read_by_count.
- Виберіть компонент трекінгу видимості під платформу (RecyclerView, UITableView, VisibilityDetector).
- Реалізуйте батчинг з дебаунсом 500-1000 мс.
- Налаштуйте WebSocket-канал для push-повідомлень про прочитання.
- Додайте офлайн-кешування невідправлених статусів (Room, CoreData, Hive).
Відображення та синхронізація на стороні відправника
Індикатори статусу оновлюються через WebSocket-подію або Firebase listener. Для групових чатів потрібно прийняти дизайнерське рішення: read_by_count (як у Telegram) або read_by: [userId] (як у WhatsApp). Вплив на схему даних прямий: у першому випадку — просте число, у другому — масив ID. Завантаження історії через пагінацію створює окрему проблему: старі повідомлення не повинні позначатися прочитаними. Вирішується через прапорець isAtBottom — трекінг видимості запускаємо лише коли користувач знаходиться внизу чату.
Чому важливо офлайн-кешування статусів?
Якщо користувач прочитав повідомлення, але з'єднання перервалося, статуси повинні зберегтися локально. Ми використовуємо Room (Android), CoreData (iOS) або Hive (Flutter) для кешування. При відновленні з'єднання відправляємо невідправлені статуси одним пакетом. Інакше відправник ніколи не побачить «прочитано», що руйнує UX. На одному проекті з 100K користувачів офлайн-кешування дозволило підвищити точність доставки read receipts з 60% до 99%. Це в 1.65 рази краще, ніж без кешування.
Типові помилки включають позначку прочитаного при отриманні, ігнорування дебаунсу, відсутність офлайн-кешування, плутанину між «прочитано всіма» та «прочитано хоча б одним» у групових чатах та відсутність прапорця isAtBottom.
Що входить у роботу
На етапі аналітики ми визначаємо тип чату (особистий/груповий) та вимоги до синхронізації. Проектуємо схему статусів і API-контракти, обираємо компоненти трекінгу. Реалізуємо інтеграцію трекінгу видимості, батчинг, WebSocket та офлайн-кешування. Тестуємо кейси зі швидким скролом, одночасним відкриттям на кількох пристроях, обривом з'єднання. Передаємо документацію та доступ до репозиторію. Термін — від 5 до 7 днів. Вартість розраховується індивідуально після оцінки проекту.
Зв'яжіться з нами для оцінки вашого проекту. Замовте консультацію — наші інженери допоможуть впровадити точні статуси прочитання.







