Реализация ответов на сообщения (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, два лейбла. Если исходное сообщение удалено — показываем текст «Сообщение удалено» серым курсивом.
Прокрутка к исходному сообщению — по тапу на 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. Стоимость рассчитывается индивидуально после анализа требований.







