make message flash when tapping reply

This commit is contained in:
Henry Hiles 2026-03-18 21:49:43 -04:00
commit a6aee7565a
No known key found for this signature in database
5 changed files with 127 additions and 102 deletions

View file

@ -66,6 +66,7 @@ class MessageController extends AsyncNotifier<Message?> {
),
).future,
),
"flashing": false,
"timelineId": event.timelineRowId,
"big": event.localContent?.bigEmoji == true,
"eventType": type,

View file

@ -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<InMemoryChatController> {
Future<void> scrollToMessage(Message message) async {
final controller = await future;
Future<void> 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);
}

View file

@ -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,
],
),
),
],
),
],
),
);
}

View file

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

View file

@ -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\\b[^>]*>.*?<\\/a>)|(\\bhttps?:\\/\\/[^\\s<]+)",
caseSensitive: false,
),
(m) {
// If it's already an <a> tag, leave it unchanged
if (m.group(1) != null) {
return m.group(1)!;
}
// Otherwise, wrap the bare URL
final url = m.group(2)!;
return "<a href=\"$url\">$url</a>";
},
)
.replaceAll("\n", "<br class=\"fake-break\"/>"),
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\\b[^>]*>.*?<\\/a>)|(\\bhttps?:\\/\\/[^\\s<]+)",
caseSensitive: false,
),
(m) {
// If it's already an <a> tag, leave it unchanged
if (m.group(1) != null) {
return m.group(1)!;
}
// Otherwise, wrap the bare URL
final url = m.group(2)!;
return "<a href=\"$url\">$url</a>";
},
)
.replaceAll("\n", "<br class=\"fake-break\"/>"),
),
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,
);
}
}