Реалізація відповідей на повідомлення (Reply) у мобільному чаті
Reply-механіка — одна з тих функцій, яку недооцінюють на етапі планування. На поверхні: показати цитату над полем введення, надіслати parent_message_id на сервер, відрисувати превью в стрічці. На практиці: три платформи (iOS/Android/Flutter), різні стани UI, прокрутка до вихідного повідомлення через 500+ позицій та edge-케이си типу відповіді на видалене повідомлення.
Схема даних та API
Повідомлення з reply зберігає parent_id (nullable FK на себе). Сервер повертає або повний об'єкт батька (parent_message embedded), або тільки parent_id — тоді клієнт підтягує його окремо. Перший варіант простіше для рендеру, але розпухає payload при довгих цитатах. Компроміс: повертати обрізану снимок батька — { id, text_preview, sender_name, attachment_type } — без повного тіла.
Важливий момент: якщо користувач відповідає на повідомлення, яке саме є reply, в UI показуємо тільки один рівень вкладеності. Рекурсивні цитати — плутанина, WhatsApp і Telegram обидва на це вирішили давно.
Реалізація на iOS (UIKit / SwiftUI)
На UIKit поле введення — кастомний inputAccessoryView. При виборі reply додаємо preview-підкладку вище UITextView: окремий UIView з UILabel (ім'я відправника), UILabel (текст превью, обрізаний до 80 символів через NSLineBreakMode.byTruncatingTail), UIButton для скасування. Анімація появи — зміна inputAccessoryView.frame.size.height з UIView.animate(withDuration: 0.2), інакше клавіатура стрибає.
У клітинці повідомлення reply-блок малюємо окремим UIView над bubble: ліва кольорова смужка через CALayer з backgroundColor, два label. Якщо вихідне повідомлення видалено — показуємо текст «Повідомлення видалено» сірим курсивом.
Прокрутка до вихідного повідомлення — по тапу на reply-блок. Якщо повідомлення є в поточному dataSource — collectionView.scrollToItem(at:, at: .centeredVertically, animated: true). Якщо ні (не все завантажено) — запитуємо сторінку з потрібним message_id через API, підгружаємо, прокручуємо. Після прокрутки підсвічуємо клітинку: змінюємо backgroundColor на .systemYellow.withAlphaComponent(0.3), прибираємо через 1.2 секунди з UIView.animate.
SwiftUI — ScrollViewProxy.scrollTo(_:anchor:) в withAnimation. Простіше, але потребує iOS 14+.
Реалізація на Android (Jetpack Compose)
Reply preview над TextField — окремий composable, який з'являється через AnimatedVisibility(visible = replyState != null, enter = slideInVertically + fadeIn). Кнопка закриття очищає replyState у ViewModel.
У LazyColumn кожне повідомлення перевіряє parentMessage != null — якщо так, перед bubble рендеримо ReplyPreview composable з вертикальною кольоровою смужкою через Box з Modifier.fillMaxHeight().width(3.dp).background(color).
Прокрутка до оригіналу: LazyListState.animateScrollToItem(index). Індекс шукаємо в snapshot через items.indexOfFirst { it.id == parentId }. Якщо не знайшли — триггеримо підгрузку через PagingSource з початковим ключем parentId.
Flutter
reply_state — у ChatCubit або ChangeNotifier. Preview над TextField — звичайний AnimatedContainer з Curve.easeOut. У ListView.builder / CustomScrollView з SliverList reply-блок — окремий ReplyPreviewWidget всередину Column з bubble.
Прокрутка: якщо використовуємо flutter_chat_ui — там є вбудований callback onMessageTap, можна додати reply scroll через ItemScrollController з scrollable_positioned_list. Без сторонніх пакетів — ScrollController.animateTo з попереднім розрахунком offset за висотою клітинок (нестабільно при різних розмірах) або Scrollable.ensureVisible для конкретного віджета.
Етапи роботи
Проектування схеми API та станів UI → розроблення бекенд-частини (підтримка parent_id, снимок батька) → реалізація UI на потрібній платформі → обробка edge-케йсів (видалене повідомлення, медіа-цитата, прокрутка через підгрузку) → тестування на довгих тредах.
Строки
1-3 робочих дня в залежності від платформи (одна або кілька) та наявності готового API. Вартість розраховується індивідуально після аналізу вимог.







