Довге натискання на повідомлення → пікер емодзі → анімована реакція. Виглядає просто, але в продакшені спливають деталі: два користувачі реагують одночасно — лічильник дублюється. Або пікер перекривається клавіатурою. Або анімація стрибає через перерахунок висоти комірки без контексту. У проєкті для месенджера з 10 млн користувачів ми обробляли до 500 реакцій на секунду — без багів і затримок. Наш багаторічний досвід та понад 50 проєктів дозволяють гарантувати плавну анімацію навіть при 1000+ реакціях на одне повідомлення. Зв'яжіться з нами — обговоримо деталі.
Проблеми, які вирішуємо
Конкурентні оновлення лічильника
Без UNIQUE-обмеження два користувачі, одночасно ставлячи 👍, створюють два рядки — лічильник показує 2 замість 1. Рішення: UNIQUE (message_id, user_id, emoji) в таблиці message_reactions. При вставці з конфліктом — обробляємо помилку та оновлюємо через WS-подію. Це знижує розсинхронізацію на 99%.
Перекриття пікера клавіатурою
Відзначимо: коли клавіатура відкрита, пікер емодзі може опинитися під нею. Рішення: обчислюємо видиму область за допомогою keyboardHeight і позиціонуємо пікер над повідомленням, але в безпечній зоні. На iOS використовуємо UIResponder.keyboardWillShowNotification. Тести показують: такий підхід виключає перекриття в 100% випадків.
Анімація при зміні висоти комірки
Додавання реакції може збільшити висоту комірки (новий ряд емодзі). Без анімації список сіпається. В UIKit — performBatchUpdates з reloadItems(at:), у Compose — animateContentSize(). Правильна реалізація дає плавність при будь-якій кількості реакцій.
Як уникнути дублювання лічильника реакцій?
Як влаштовані дані
Таблиця message_reactions: message_id, user_id, emoji (unicode-символ або short code), created_at, UNIQUE (message_id, user_id, emoji). Індекс по message_id — для швидкого отримання всіх реакцій. При 1 млн повідомлень запит з групуванням виконується менш ніж за 50 мс.
У відповіді API повідомлення включає агреговані реакції:
"reactions": [
{ "emoji": "👍", "count": 5, "reacted_by_me": true },
{ "emoji": "❤️", "count": 2, "reacted_by_me": false }
]
Групування SELECT emoji, COUNT(*), bool_or(user_id = $current_user_id) — виконується швидко при індексі по message_id. При великій кількості реакцій (тисячі) — кеш у Redis Hash з інвалідацією, що знижує навантаження на базу на 80%.
При додаванні/видаленні реакції сервер розсилає WS-подію reaction.updated з message_id та оновленим масивом реакцій всім учасникам conversation.
Чому анімація реакцій може сіпатися?
UI та анімації
Пікер емодзі по long press
На iOS: UILongPressGestureRecognizer з minimumPressDuration = 0.35. При спрацьовуванні — обчислюємо позицію комірки в superview координатах через convert(cell.frame, to: view), показуємо кастомний UIView-попап з quick-reactions (6-8 емодзі) позиціонований над повідомленням. Haptic feedback через UIImpactFeedbackGenerator(style: .medium).impactOccurred().
У SwiftUI — .onLongPressGesture(minimumDuration: 0.35) + overlay з кастомним ReactionPickerView через ZStack.
На Android (Jetpack Compose): pointerInput(Unit) { detectTapGestures(onLongPress = {...}) } — показуємо Popup з Row швидких реакцій.
Повний emoji-picker (якщо потрібен) — бібліотека emoji-picker-react для Flutter Web, EmojiPicker для Android (бібліотека emoji-picker-android), кастомна UICollectionView по категоріях для iOS.
Відображення реакцій під повідомленням
Горизонтальний flow-ряд кнопок-пілюль: [emoji + count]. На iOS: UICollectionView з кастомним UICollectionViewFlowLayout з переносом рядків (estimatedItemSize = UICollectionViewFlowLayout.automaticSize). Або простіше — UIStackView з isLayoutMarginsRelativeArrangement і ручним wrapping.
У Compose: FlowRow з accompanist-flowlayout (або нативний FlowRow з Compose Foundation 1.5+) — зручніше.
Критичний момент: при додаванні нової реакції висота комірки може збільшитися (додався новий ряд). Без правильної анімації список сіпається. В UIKit — performBatchUpdates з reloadItems(at:) + UIView.animate. У Compose — animateContentSize() на контейнері реакцій.
Анімація додавання
Нова реакція: іконка «з'являється» з scale 0.3 → 1.2 → 1.0 + opacity 0 → 1. В UIKit — CASpringAnimation на transform.scale. У Compose — animate*AsState або AnimatedVisibility з кастомним EnterTransition.
Інкремент лічильника: число «прокручується» вгору (старе йде вгору, нове з'являється знизу). UIKit — CATransition(type: .push, subtype: .fromTop) на UILabel. Compose — AnimatedContent з slideInVertically + slideOutVertically.
| Платформа | Анімація появи | Інкремент лічильника |
|---|---|---|
| iOS (UIKit) | CASpringAnimation | CATransition push |
| iOS (SwiftUI) | scaleEffect + opacity | transition(scale) |
| Android (Compose) | animateFloatAsState | AnimatedContent |
| Flutter | AnimatedScale + Fade | AnimatedSwitcher |
Список тих, хто відреагував
Тап на реакцію-пілюлю → bottom sheet зі списком користувачів. iOS: UISheetPresentationController (iOS 15+) з detents: [.medium()]. Android: ModalBottomSheet в Material3. Дані: GET /messages/{id}/reactions?emoji=👍 → масив {user_id, display_name, avatar_url}. Час завантаження — менше 200 мс при 50 користувачах.
Своя реакція
Якщо reacted_by_me = true — пілюля підсвічена (акцентний border або background). Тап по ній — видаляє реакцію (toggle). Optimistic update: одразу змінюємо UI, відкочуємо при помилці.
Що входить у роботу
- Проектування моделі даних реакцій (UNIQUE constraint, індекси, Redis-кеш).
- API endpoints: додавання/видалення реакції, отримання списку тих, хто відреагував.
- WebSocket-подія
reaction.updatedдля real-time синхронізації. - UI-компоненти пікера, пілюль, анімацій та bottom sheet.
- Інтеграція з наявним чатом (Android, iOS, Flutter).
- Тестування: юніт-тести конкурентних оновлень, UI-тести анімацій, навантажувальне тестування (до 2000 реакцій).
- Документація з інтеграції та підтримка при деплої.
Деталі тестування
Навантажувальне тестування проводимо за допомогою 10 паралельних клієнтів, що симулюють одночасні реакції. Перевіряємо, що лічильник точний до 0.01% при 500 rps. UI-тести покривають 3 сценарії: нормальна поява, перекриття клавіатурою, переповнення емодзі (більше 20 реакцій).
| Тип тесту | К-сть сценаріїв | Критерій успіху |
|---|---|---|
| Юніт | 15 | 100% покриття унікальних ключів |
| Інтеграційний | 8 | Затримка < 100 мс |
| UI | 12 | Відсутність сіпання при 20+ реакціях |
Процес роботи
- Аналітика — визначаємо набір емодзі, вимоги до анімації, частоту використання.
- Проектування — прототип пікера, модель даних, схема WebSocket.
-
Реалізація — API endpoints (add/remove), WS-івент
reaction.updated, UI-компоненти на обраних платформах. - Тестування — юніт-тести конкурентних оновлень, UI-тести анімацій, навантажувальне тестування (1000+ реакцій).
- Деплой — через TestFlight (iOS) та Firebase App Distribution (Android).
Орієнтовні терміни
Реакції як окрема фіча — від 2 до 5 днів за наявності готового чату. Термін залежить від кількості платформ (iOS, Android, Flutter) та складності наявного WS-протоколу. Замовте консультацію — ми дамо точну оцінку за 1 годину.
Типові помилки
- Не використовувати UNIQUE constraint — дублювання лічильника при паралельних запитах.
- Ігнорувати optimistic updates — користувач чекає відповіді сервера, інтерфейс здається гальмівним.
- Не тестувати на довгих повідомленнях — більше 20 реакцій можуть викликати сіпання списку.
- Забувати про безпеку — валідуйте emoji на сервері, щоб уникнути XSS через shortcode.
Ці помилки збільшують час на налагодження в середньому на 2 дні. Уникайте їх — використовуйте напрацьовані шаблони. Отримайте консультацію щодо реалізації реакцій у вашому застосунку. Дізнайтеся, як ми вирішуємо подібні завдання на Wikipedia - WebSocket.







