scroll to responded message

This commit is contained in:
otsmr 2025-07-18 10:35:13 +02:00
parent 90d6b048f3
commit 991f86802f
5 changed files with 113 additions and 52 deletions

View file

@ -6,6 +6,7 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:pie_menu/pie_menu.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/globals.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/tables/messages_table.dart';
@ -68,7 +69,8 @@ class ChatMessagesView extends StatefulWidget {
State<ChatMessagesView> createState() => _ChatMessagesViewState(); State<ChatMessagesView> createState() => _ChatMessagesViewState();
} }
class _ChatMessagesViewState extends State<ChatMessagesView> { class _ChatMessagesViewState extends State<ChatMessagesView>
with SingleTickerProviderStateMixin {
TextEditingController newMessageController = TextEditingController(); TextEditingController newMessageController = TextEditingController();
HashSet<int> alreadyReportedOpened = HashSet<int>(); HashSet<int> alreadyReportedOpened = HashSet<int>();
late Contact user; late Contact user;
@ -82,6 +84,9 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
GlobalKey verifyShieldKey = GlobalKey(); GlobalKey verifyShieldKey = GlobalKey();
late FocusNode textFieldFocus; late FocusNode textFieldFocus;
Timer? tutorial; Timer? tutorial;
final ItemScrollController itemScrollController = ItemScrollController();
int? focusedScrollItem;
late AnimationController _animationController;
@override @override
void initState() { void initState() {
@ -90,6 +95,11 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
textFieldFocus = FocusNode(); textFieldFocus = FocusNode();
initStreams(); initStreams();
_animationController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
tutorial = Timer(const Duration(seconds: 1), () async { tutorial = Timer(const Duration(seconds: 1), () async {
tutorial = null; tutorial = null;
if (!mounted) return; if (!mounted) return;
@ -248,6 +258,27 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
setState(() {}); setState(() {});
} }
Future<void> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
@ -289,9 +320,10 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
child: Column( child: Column(
children: [ children: [
Expanded( Expanded(
child: ListView.builder( child: ScrollablePositionedList.builder(
reverse: true, reverse: true,
itemCount: messages.length + 1, itemCount: messages.length + 1,
itemScrollController: itemScrollController,
itemBuilder: (context, i) { itemBuilder: (context, i) {
if (i == messages.length) { if (i == messages.length) {
return const Padding( return const Padding(
@ -304,21 +336,33 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
); );
} else { } else {
final chatMessage = messages[i].message!; final chatMessage = messages[i].message!;
return ChatListEntry( return ScaleTransition(
key: Key(chatMessage.message.messageId.toString()), scale: Tween<double>(
chatMessage, begin: 1,
user, end: (focusedScrollItem == i) ? 1.03 : 1)
galleryItems, .animate(
isLastMessageFromSameUser(messages, i), CurvedAnimation(
emojiReactionsToMessageId[ parent: _animationController,
chatMessage.message.messageId] ?? curve: Curves.easeInOut,
[], ),
onResponseTriggered: () { ),
setState(() { child: ChatListEntry(
responseToMessage = chatMessage.message; key: Key(chatMessage.message.messageId.toString()),
}); chatMessage,
textFieldFocus.requestFocus(); user,
}, galleryItems,
isLastMessageFromSameUser(messages, i),
emojiReactionsToMessageId[
chatMessage.message.messageId] ??
[],
scrollToMessage: scrollToMessage,
onResponseTriggered: () {
setState(() {
responseToMessage = chatMessage.message;
});
textFieldFocus.requestFocus();
},
),
); );
} }
}, },

View file

@ -20,6 +20,7 @@ class ChatListEntry extends StatefulWidget {
this.lastMessageFromSameUser, this.lastMessageFromSameUser,
this.otherReactions, { this.otherReactions, {
required this.onResponseTriggered, required this.onResponseTriggered,
required this.scrollToMessage,
super.key, super.key,
}); });
final ChatMessage msg; final ChatMessage msg;
@ -27,6 +28,7 @@ class ChatListEntry extends StatefulWidget {
final bool lastMessageFromSameUser; final bool lastMessageFromSameUser;
final List<Message> otherReactions; final List<Message> otherReactions;
final List<MemoryItem> galleryItems; final List<MemoryItem> galleryItems;
final void Function(int) scrollToMessage;
final void Function() onResponseTriggered; final void Function() onResponseTriggered;
@override @override
@ -78,6 +80,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
ResponseContainer( ResponseContainer(
msg: widget.msg, msg: widget.msg,
contact: widget.contact, contact: widget.contact,
scrollToMessage: widget.scrollToMessage,
child: (textMessage != null) child: (textMessage != null)
? ChatTextEntry( ? ChatTextEntry(
message: widget.msg.message, message: widget.msg.message,

View file

@ -15,12 +15,14 @@ class ResponseContainer extends StatefulWidget {
required this.msg, required this.msg,
required this.contact, required this.contact,
required this.child, required this.child,
required this.scrollToMessage,
super.key, super.key,
}); });
final ChatMessage msg; final ChatMessage msg;
final Widget child; final Widget child;
final Contact contact; final Contact contact;
final void Function(int) scrollToMessage;
@override @override
State<ResponseContainer> createState() => _ResponseContainerState(); State<ResponseContainer> createState() => _ResponseContainerState();
@ -57,44 +59,47 @@ class _ResponseContainerState extends State<ResponseContainer> {
if (widget.msg.responseTo == null) { if (widget.msg.responseTo == null) {
return widget.child; return widget.child;
} }
return Container( return GestureDetector(
constraints: BoxConstraints( onTap: () => widget.scrollToMessage(widget.msg.responseTo!.messageId),
maxWidth: MediaQuery.of(context).size.width * 0.8, child: Container(
), constraints: BoxConstraints(
decoration: BoxDecoration( maxWidth: MediaQuery.of(context).size.width * 0.8,
color: getMessageColor(widget.msg.message), ),
borderRadius: BorderRadius.circular(12), decoration: BoxDecoration(
), color: getMessageColor(widget.msg.message),
child: Column( borderRadius: BorderRadius.circular(12),
crossAxisAlignment: CrossAxisAlignment.start, ),
children: [ child: Column(
Padding( crossAxisAlignment: CrossAxisAlignment.start,
padding: const EdgeInsets.only(top: 4, right: 4, left: 4), children: [
child: Container( Padding(
key: _preview, padding: const EdgeInsets.only(top: 4, right: 4, left: 4),
width: minWidth, child: Container(
decoration: BoxDecoration( key: _preview,
color: context.color.surface.withAlpha(150), width: minWidth,
borderRadius: const BorderRadius.only( decoration: BoxDecoration(
topRight: Radius.circular(8), color: context.color.surface.withAlpha(150),
topLeft: Radius.circular(8), borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(4), topRight: Radius.circular(8),
bottomRight: Radius.circular(4), 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(
SizedBox( key: _message,
key: _message, width: minWidth,
width: minWidth, child: widget.child,
child: widget.child, ),
), ],
], ),
), ),
); );
} }

View file

@ -1418,6 +1418,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" 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: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -64,6 +64,7 @@ dependencies:
provider: ^6.1.2 provider: ^6.1.2
restart_app: ^1.3.2 restart_app: ^1.3.2
screenshot: ^3.0.0 screenshot: ^3.0.0
scrollable_positioned_list: ^0.3.8
share_plus: ^11.0.0 share_plus: ^11.0.0
tutorial_coach_mark: ^1.3.0 tutorial_coach_mark: ^1.3.0
url_launcher: ^6.3.1 url_launcher: ^6.3.1