From a6aee7565a71c5452d4ec3ea489410d9c9975b41 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Wed, 18 Mar 2026 21:49:43 -0400 Subject: [PATCH] make message flash when tapping reply --- lib/controllers/message_controller.dart | 1 + lib/controllers/room_chat_controller.dart | 12 ++ lib/widgets/chat_page/message_wrapper.dart | 68 +++++---- lib/widgets/chat_page/room_chat.dart | 17 +-- .../chat_page/text_message_wrapper.dart | 131 +++++++++--------- 5 files changed, 127 insertions(+), 102 deletions(-) diff --git a/lib/controllers/message_controller.dart b/lib/controllers/message_controller.dart index aba8a16..615306f 100644 --- a/lib/controllers/message_controller.dart +++ b/lib/controllers/message_controller.dart @@ -66,6 +66,7 @@ class MessageController extends AsyncNotifier { ), ).future, ), + "flashing": false, "timelineId": event.timelineRowId, "big": event.localContent?.bigEmoji == true, "eventType": type, diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index a1ee835..4a4dba2 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -1,3 +1,5 @@ +import "dart:async"; + import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; @@ -282,6 +284,16 @@ class RoomChatController extends AsyncNotifier { Future scrollToMessage(Message message) async { final controller = await future; + Future setFlashing(bool flashing) => controller.updateMessage( + message, + message.copyWith( + metadata: {...(message.metadata ?? {}), "flashing": flashing}, + ), + ); + + await setFlashing(true); + Timer(Duration(seconds: 1), () => setFlashing(false)); + return await controller.scrollToMessage(message.id); } diff --git a/lib/widgets/chat_page/message_wrapper.dart b/lib/widgets/chat_page/message_wrapper.dart index 20d4729..da53be0 100644 --- a/lib/widgets/chat_page/message_wrapper.dart +++ b/lib/widgets/chat_page/message_wrapper.dart @@ -9,34 +9,46 @@ class MessageWrapper extends StatelessWidget { const MessageWrapper(this.message, this.child, this.groupStatus, {super.key}); @override - Widget build(BuildContext context) => Row( - spacing: 8, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - groupStatus?.isFirst != false - ? AvatarOrHash( - Uri.parse(message.metadata?["avatarUrl"] ?? ""), - height: 40, - message.metadata?["displayName"] ?? "", - ) - : SizedBox(width: 40), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 4, - children: [ - if (groupStatus?.isFirst != false) - Text( - message.metadata?["displayName"] ?? message.authorId, - overflow: TextOverflow.ellipsis, - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), - ), - child, - ], - ), + Widget build(BuildContext context) => ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(12)), + child: AnimatedContainer( + padding: message.metadata?["flashing"] == true + ? EdgeInsets.all(8) + : EdgeInsets.all(0), + color: message.metadata?["flashing"] == true + ? Theme.of(context).colorScheme.onSurface.withAlpha(50) + : Colors.transparent, + duration: Duration(milliseconds: 250), + child: Row( + spacing: 8, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + groupStatus?.isFirst != false + ? AvatarOrHash( + Uri.parse(message.metadata?["avatarUrl"] ?? ""), + height: 40, + message.metadata?["displayName"] ?? "", + ) + : SizedBox(width: 40), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4, + children: [ + if (groupStatus?.isFirst != false) + Text( + message.metadata?["displayName"] ?? message.authorId, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + child, + ], + ), + ), + ], ), - ], + ), ); } diff --git a/lib/widgets/chat_page/room_chat.dart b/lib/widgets/chat_page/room_chat.dart index 1fb73e2..03c7dad 100644 --- a/lib/widgets/chat_page/room_chat.dart +++ b/lib/widgets/chat_page/room_chat.dart @@ -259,18 +259,13 @@ class RoomChat extends HookConsumerWidget { index, { required bool isSentByMe, MessageGroupStatus? groupStatus, - }) => MessageWrapper( + }) => TextMessageWrapper( message, - TextMessageWrapper( - message, - content: message.text, - groupStatus: groupStatus, - onTapReply: notifier.scrollToMessage, - updateMessage: controller.updateMessage, - isSentByMe: isSentByMe, - ), - - groupStatus, + content: message.text, + groupStatus: groupStatus, + onTapReply: notifier.scrollToMessage, + updateMessage: controller.updateMessage, + isSentByMe: isSentByMe, ), imageMessageBuilder: diff --git a/lib/widgets/chat_page/text_message_wrapper.dart b/lib/widgets/chat_page/text_message_wrapper.dart index 5f2a9f3..4873f84 100644 --- a/lib/widgets/chat_page/text_message_wrapper.dart +++ b/lib/widgets/chat_page/text_message_wrapper.dart @@ -2,6 +2,7 @@ import "package:flutter/material.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_link_previewer/flutter_link_previewer.dart"; import "package:nexus/widgets/chat_page/html/html.dart"; +import "package:nexus/widgets/chat_page/message_wrapper.dart"; import "package:nexus/widgets/chat_page/top_widget.dart"; class TextMessageWrapper extends StatelessWidget { @@ -31,75 +32,79 @@ class TextMessageWrapper extends StatelessWidget { final colorScheme = theme.colorScheme; final textMessage = message is TextMessage ? message as TextMessage : null; - return ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(8)), - child: Container( - padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12), - decoration: BoxDecoration( - color: isSentByMe - ? colorScheme.primaryContainer - : colorScheme.surfaceContainer, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TopWidget( - message, - groupStatus: groupStatus, - onTapReply: onTapReply, - ), - if (content != null) - Html( - textStyle: message.metadata?["big"] == true - ? TextStyle(fontSize: 32) - : null, - content! - .replaceAllMapped( - RegExp( - "(]*>.*?<\\/a>)|(\\bhttps?:\\/\\/[^\\s<]+)", - caseSensitive: false, - ), - (m) { - // If it's already an tag, leave it unchanged - if (m.group(1) != null) { - return m.group(1)!; - } - - // Otherwise, wrap the bare URL - final url = m.group(2)!; - return "$url"; - }, - ) - .replaceAll("\n", "
"), + return MessageWrapper( + message, + ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(8)), + child: Container( + padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12), + decoration: BoxDecoration( + color: isSentByMe + ? colorScheme.primaryContainer + : colorScheme.surfaceContainer, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TopWidget( + message, + groupStatus: groupStatus, + onTapReply: onTapReply, ), - if (textMessage?.editedAt != null) - Text("(edited)", style: theme.textTheme.labelSmall), - if (textMessage != null) - LinkPreview( - text: textMessage.text, - backgroundColor: isSentByMe - ? colorScheme.inversePrimary - : colorScheme.surfaceContainerLow, - outsidePadding: EdgeInsets.only(top: 4), - insidePadding: EdgeInsets.symmetric( - vertical: 8, - horizontal: 16, + if (content != null) + Html( + textStyle: message.metadata?["big"] == true + ? TextStyle(fontSize: 32) + : null, + content! + .replaceAllMapped( + RegExp( + "(]*>.*?<\\/a>)|(\\bhttps?:\\/\\/[^\\s<]+)", + caseSensitive: false, + ), + (m) { + // If it's already an tag, leave it unchanged + if (m.group(1) != null) { + return m.group(1)!; + } + + // Otherwise, wrap the bare URL + final url = m.group(2)!; + return "$url"; + }, + ) + .replaceAll("\n", "
"), ), - linkPreviewData: message.metadata?["linkPreviewData"], - onLinkPreviewDataFetched: (linkPreviewData) => updateMessage( - message, - message.copyWith( - metadata: { - ...(message.metadata ?? {}), - "linkPreviewData": linkPreviewData, - }, + if (textMessage?.editedAt != null) + Text("(edited)", style: theme.textTheme.labelSmall), + if (textMessage != null) + LinkPreview( + text: textMessage.text, + backgroundColor: isSentByMe + ? colorScheme.inversePrimary + : colorScheme.surfaceContainerLow, + outsidePadding: EdgeInsets.only(top: 4), + insidePadding: EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + linkPreviewData: message.metadata?["linkPreviewData"], + onLinkPreviewDataFetched: (linkPreviewData) => updateMessage( + message, + message.copyWith( + metadata: { + ...(message.metadata ?? {}), + "linkPreviewData": linkPreviewData, + }, + ), ), ), - ), - if (extra != null) extra!, - ], + if (extra != null) extra!, + ], + ), ), ), + groupStatus, ); } }