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: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<ChatMessagesView> createState() => _ChatMessagesViewState();
}
class _ChatMessagesViewState extends State<ChatMessagesView> {
class _ChatMessagesViewState extends State<ChatMessagesView>
with SingleTickerProviderStateMixin {
TextEditingController newMessageController = TextEditingController();
HashSet<int> alreadyReportedOpened = HashSet<int>();
late Contact user;
@ -82,6 +84,9 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
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<ChatMessagesView> {
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<ChatMessagesView> {
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
Widget build(BuildContext context) {
return GestureDetector(
@ -289,9 +320,10 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
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,7 +336,17 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
);
} else {
final chatMessage = messages[i].message!;
return ChatListEntry(
return ScaleTransition(
scale: Tween<double>(
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,
@ -313,12 +355,14 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
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.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<Message> otherReactions;
final List<MemoryItem> galleryItems;
final void Function(int) scrollToMessage;
final void Function() onResponseTriggered;
@override
@ -78,6 +80,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
ResponseContainer(
msg: widget.msg,
contact: widget.contact,
scrollToMessage: widget.scrollToMessage,
child: (textMessage != null)
? ChatTextEntry(
message: widget.msg.message,

View file

@ -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<ResponseContainer> createState() => _ResponseContainerState();
@ -57,7 +59,9 @@ class _ResponseContainerState extends State<ResponseContainer> {
if (widget.msg.responseTo == null) {
return widget.child;
}
return Container(
return GestureDetector(
onTap: () => widget.scrollToMessage(widget.msg.responseTo!.messageId),
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.8,
),
@ -96,6 +100,7 @@ class _ResponseContainerState extends State<ResponseContainer> {
),
],
),
),
);
}
}

View file

@ -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:

View file

@ -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