From 991f86802f52b8b93d1886923822e9424cb4d5c2 Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 18 Jul 2025 10:35:13 +0200 Subject: [PATCH] scroll to responded message --- lib/src/views/chats/chat_messages.view.dart | 78 +++++++++++++++---- .../chat_list_entry.dart | 3 + .../response_container.dart | 75 +++++++++--------- pubspec.lock | 8 ++ pubspec.yaml | 1 + 5 files changed, 113 insertions(+), 52 deletions(-) diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 5330bbc..ab0be5c 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -6,6 +6,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:pie_menu/pie_menu.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; @@ -68,7 +69,8 @@ class ChatMessagesView extends StatefulWidget { State createState() => _ChatMessagesViewState(); } -class _ChatMessagesViewState extends State { +class _ChatMessagesViewState extends State + with SingleTickerProviderStateMixin { TextEditingController newMessageController = TextEditingController(); HashSet alreadyReportedOpened = HashSet(); late Contact user; @@ -82,6 +84,9 @@ class _ChatMessagesViewState extends State { GlobalKey verifyShieldKey = GlobalKey(); late FocusNode textFieldFocus; Timer? tutorial; + final ItemScrollController itemScrollController = ItemScrollController(); + int? focusedScrollItem; + late AnimationController _animationController; @override void initState() { @@ -90,6 +95,11 @@ class _ChatMessagesViewState extends State { textFieldFocus = FocusNode(); initStreams(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + tutorial = Timer(const Duration(seconds: 1), () async { tutorial = null; if (!mounted) return; @@ -248,6 +258,27 @@ class _ChatMessagesViewState extends State { setState(() {}); } + Future scrollToMessage(int messageId) async { + final index = messages.indexWhere( + (x) => x.isMessage && x.message!.message.messageId == messageId); + if (index == -1) return; + await itemScrollController.scrollTo( + index: index, + duration: const Duration(milliseconds: 400), + alignment: 0.5, + ); + setState(() { + focusedScrollItem = index; + _animationController.forward().then((_) { + _animationController.reverse().then((_) { + setState(() { + _animationController.value = 0.0; + }); + }); + }); + }); + } + @override Widget build(BuildContext context) { return GestureDetector( @@ -289,9 +320,10 @@ class _ChatMessagesViewState extends State { child: Column( children: [ Expanded( - child: ListView.builder( + child: ScrollablePositionedList.builder( reverse: true, itemCount: messages.length + 1, + itemScrollController: itemScrollController, itemBuilder: (context, i) { if (i == messages.length) { return const Padding( @@ -304,21 +336,33 @@ class _ChatMessagesViewState extends State { ); } else { final chatMessage = messages[i].message!; - return ChatListEntry( - key: Key(chatMessage.message.messageId.toString()), - chatMessage, - user, - galleryItems, - isLastMessageFromSameUser(messages, i), - emojiReactionsToMessageId[ - chatMessage.message.messageId] ?? - [], - onResponseTriggered: () { - setState(() { - responseToMessage = chatMessage.message; - }); - textFieldFocus.requestFocus(); - }, + return ScaleTransition( + scale: Tween( + begin: 1, + end: (focusedScrollItem == i) ? 1.03 : 1) + .animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ), + ), + child: ChatListEntry( + key: Key(chatMessage.message.messageId.toString()), + chatMessage, + user, + galleryItems, + isLastMessageFromSameUser(messages, i), + emojiReactionsToMessageId[ + chatMessage.message.messageId] ?? + [], + scrollToMessage: scrollToMessage, + onResponseTriggered: () { + setState(() { + responseToMessage = chatMessage.message; + }); + textFieldFocus.requestFocus(); + }, + ), ); } }, diff --git a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart index bb55a6a..21f49bf 100644 --- a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart @@ -20,6 +20,7 @@ class ChatListEntry extends StatefulWidget { this.lastMessageFromSameUser, this.otherReactions, { required this.onResponseTriggered, + required this.scrollToMessage, super.key, }); final ChatMessage msg; @@ -27,6 +28,7 @@ class ChatListEntry extends StatefulWidget { final bool lastMessageFromSameUser; final List otherReactions; final List galleryItems; + final void Function(int) scrollToMessage; final void Function() onResponseTriggered; @override @@ -78,6 +80,7 @@ class _ChatListEntryState extends State { ResponseContainer( msg: widget.msg, contact: widget.contact, + scrollToMessage: widget.scrollToMessage, child: (textMessage != null) ? ChatTextEntry( message: widget.msg.message, diff --git a/lib/src/views/chats/chat_messages_components/response_container.dart b/lib/src/views/chats/chat_messages_components/response_container.dart index 3c5612c..c05463a 100644 --- a/lib/src/views/chats/chat_messages_components/response_container.dart +++ b/lib/src/views/chats/chat_messages_components/response_container.dart @@ -15,12 +15,14 @@ class ResponseContainer extends StatefulWidget { required this.msg, required this.contact, required this.child, + required this.scrollToMessage, super.key, }); final ChatMessage msg; final Widget child; final Contact contact; + final void Function(int) scrollToMessage; @override State createState() => _ResponseContainerState(); @@ -57,44 +59,47 @@ class _ResponseContainerState extends State { if (widget.msg.responseTo == null) { return widget.child; } - return Container( - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.8, - ), - decoration: BoxDecoration( - color: getMessageColor(widget.msg.message), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 4, right: 4, left: 4), - child: Container( - key: _preview, - width: minWidth, - decoration: BoxDecoration( - color: context.color.surface.withAlpha(150), - borderRadius: const BorderRadius.only( - topRight: Radius.circular(8), - topLeft: Radius.circular(8), - bottomLeft: Radius.circular(4), - bottomRight: Radius.circular(4), + return GestureDetector( + onTap: () => widget.scrollToMessage(widget.msg.responseTo!.messageId), + child: Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.8, + ), + decoration: BoxDecoration( + color: getMessageColor(widget.msg.message), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 4, right: 4, left: 4), + child: Container( + key: _preview, + width: minWidth, + decoration: BoxDecoration( + color: context.color.surface.withAlpha(150), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(8), + topLeft: Radius.circular(8), + bottomLeft: Radius.circular(4), + bottomRight: Radius.circular(4), + ), + ), + child: ResponsePreview( + contact: widget.contact, + message: widget.msg.responseTo!, + showBorder: false, ), ), - child: ResponsePreview( - contact: widget.contact, - message: widget.msg.responseTo!, - showBorder: false, - ), ), - ), - SizedBox( - key: _message, - width: minWidth, - child: widget.child, - ), - ], + SizedBox( + key: _message, + width: minWidth, + child: widget.child, + ), + ], + ), ), ); } diff --git a/pubspec.lock b/pubspec.lock index 713e790..2ee3073 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1418,6 +1418,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + scrollable_positioned_list: + dependency: "direct main" + description: + name: scrollable_positioned_list + sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287" + url: "https://pub.dev" + source: hosted + version: "0.3.8" share_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 40d2793..b350f09 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,7 @@ dependencies: provider: ^6.1.2 restart_app: ^1.3.2 screenshot: ^3.0.0 + scrollable_positioned_list: ^0.3.8 share_plus: ^11.0.0 tutorial_coach_mark: ^1.3.0 url_launcher: ^6.3.1