smoother response animation
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run

This commit is contained in:
otsmr 2026-04-10 15:30:44 +02:00
parent 527bf51bff
commit f419b3709d
4 changed files with 72 additions and 34 deletions

View file

@ -20,7 +20,7 @@ import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_dat
import 'package:twonly/src/views/chats/chat_messages_components/message_input.dart';
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/blink.component.dart';
import 'package:twonly/src/views/chats/chat_messages_components/blink.component.dart';
import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/verified_shield.dart';

View file

@ -19,7 +19,7 @@ import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_med
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_text_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_unkown.entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/common.dart';
import 'package:twonly/src/views/chats/chat_messages_components/message_actions.dart';
import 'package:twonly/src/views/chats/chat_messages_components/message_reply_drag.dart';
import 'package:twonly/src/views/chats/chat_messages_components/message_context_menu.dart';
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
@ -74,8 +74,9 @@ class _ChatListEntryState extends State<ChatListEntry> {
Future<void> initAsync() async {
if (widget.message.mediaId != null) {
final mediaFileStream =
twonlyDB.mediaFilesDao.watchMedia(widget.message.mediaId!);
final mediaFileStream = twonlyDB.mediaFilesDao.watchMedia(
widget.message.mediaId!,
);
mediaFileSub = mediaFileStream.listen((mediaFiles) {
if (mediaFiles != null) {
mediaService = MediaFileService(mediaFiles);
@ -87,8 +88,9 @@ class _ChatListEntryState extends State<ChatListEntry> {
}
});
}
final stream =
twonlyDB.reactionsDao.watchReactions(widget.message.messageId);
final stream = twonlyDB.reactionsDao.watchReactions(
widget.message.messageId,
);
reactionsSub = stream.listen((update) {
setState(() {
@ -159,8 +161,10 @@ class _ChatListEntryState extends State<ChatListEntry> {
);
final seen = <String>{};
var reactionsForWidth =
reactions.where((t) => seen.add(t.emoji)).toList().length;
var reactionsForWidth = reactions
.where((t) => seen.add(t.emoji))
.toList()
.length;
if (reactionsForWidth > 4) reactionsForWidth = 4;
Widget child = Stack(
@ -205,7 +209,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
);
if (widget.onResponseTriggered != null) {
child = MessageActions(
child = MessageReplyDrag(
message: widget.message,
onResponseTriggered: widget.onResponseTriggered!,
child: child,
@ -228,8 +232,9 @@ class _ChatListEntryState extends State<ChatListEntry> {
child: Padding(
padding: padding,
child: Row(
mainAxisAlignment:
right ? MainAxisAlignment.end : MainAxisAlignment.start,
mainAxisAlignment: right
? MainAxisAlignment.end
: MainAxisAlignment.start,
children: [
if (!right && !widget.group.isDirectChat)
hideContactAvatar
@ -306,6 +311,6 @@ class _ChatListEntryState extends State<ChatListEntry> {
bottomRight: Radius.circular(bottomRight),
bottomLeft: Radius.circular(bottomLeft),
),
hideContactAvatar
hideContactAvatar,
);
}

View file

@ -5,8 +5,8 @@ import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/database/twonly.db.dart';
class MessageActions extends StatefulWidget {
const MessageActions({
class MessageReplyDrag extends StatefulWidget {
const MessageReplyDrag({
required this.child,
required this.message,
required this.onResponseTriggered,
@ -17,27 +17,49 @@ class MessageActions extends StatefulWidget {
final VoidCallback onResponseTriggered;
@override
State<MessageActions> createState() => _SlidingResponseWidgetState();
State<MessageReplyDrag> createState() => _SlidingResponseWidgetState();
}
class _SlidingResponseWidgetState extends State<MessageActions> {
class _SlidingResponseWidgetState extends State<MessageReplyDrag> {
double _offsetX = 0;
bool gotFeedback = false;
double _dragProgress = 0;
double _animatedScale = 1;
Future<void> _triggerPopAnimation() async {
setState(() {
_animatedScale = 1.3;
});
await Future.delayed(Duration(milliseconds: 50));
if (mounted) {
setState(() {
_animatedScale = 1.0;
});
}
}
void _onHorizontalDragUpdate(DragUpdateDetails details) {
setState(() {
_offsetX += details.delta.dx;
if (_offsetX <= 0) _offsetX = 0;
if (_offsetX > 40) {
_offsetX = 40;
if (!gotFeedback) {
unawaited(HapticFeedback.heavyImpact());
gotFeedback = true;
unawaited(_triggerPopAnimation());
}
_dragProgress = 1;
} else {
_dragProgress = _offsetX / 40;
}
if (_offsetX < 30) {
gotFeedback = false;
}
if (_offsetX <= 0) _offsetX = 0;
if (_offsetX > 50) {
_offsetX = 50;
}
});
}
@ -47,6 +69,7 @@ class _SlidingResponseWidgetState extends State<MessageActions> {
}
setState(() {
_offsetX = 0.0;
_dragProgress = 0;
});
}
@ -54,6 +77,32 @@ class _SlidingResponseWidgetState extends State<MessageActions> {
Widget build(BuildContext context) {
return Stack(
children: [
if (_dragProgress > 0.2)
Positioned(
left: _dragProgress * 10,
top: 0,
bottom: 0,
child: Transform.scale(
scale: 1 * _dragProgress,
child: AnimatedScale(
duration: const Duration(milliseconds: 50),
scale: _animatedScale,
curve: Curves.easeInOut,
child: Opacity(
opacity: 1 * _dragProgress,
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FaIcon(
FontAwesomeIcons.reply,
size: 14,
),
],
),
),
),
),
),
Transform.translate(
offset: Offset(_offsetX, 0),
child: GestureDetector(
@ -62,22 +111,6 @@ class _SlidingResponseWidgetState extends State<MessageActions> {
child: widget.child,
),
),
if (_offsetX >= 40)
const Positioned(
left: 20,
top: 0,
bottom: 0,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FaIcon(
FontAwesomeIcons.reply,
size: 14,
// color: Colors.green,
),
],
),
),
],
);
}