custom text message wrapper

This commit is contained in:
Henry Hiles 2026-03-13 19:10:07 -04:00
commit 3e0d8304b6
No known key found for this signature in database
6 changed files with 210 additions and 200 deletions

View file

@ -104,7 +104,7 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
);
if (oldMessage == null || message == null || !ref.mounted) return;
return await updateMessage(
return await controller.updateMessage(
oldMessage,
message.copyWith(
id: oldMessage.id,
@ -225,9 +225,6 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
);
}
Future<void> updateMessage(Message message, Message newMessage) async =>
(await future).updateMessage(message, newMessage);
Future<void> send(
String message, {
bool shouldMention = true,

View file

@ -4,11 +4,9 @@ import "package:flutter/material.dart";
import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:flutter_chat_ui/flutter_chat_ui.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:flutter_link_previewer/flutter_link_previewer.dart";
import "package:flyer_chat_file_message/flyer_chat_file_message.dart";
import "package:flyer_chat_image_message/flyer_chat_image_message.dart";
import "package:flyer_chat_system_message/flyer_chat_system_message.dart";
import "package:flyer_chat_text_message/flyer_chat_text_message.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/client_controller.dart";
import "package:nexus/controllers/client_state_controller.dart";
@ -21,10 +19,10 @@ import "package:nexus/helpers/extensions/show_context_menu.dart";
import "package:nexus/models/relation_type.dart";
import "package:nexus/models/requests/report_request.dart";
import "package:nexus/widgets/chat_page/chat_box.dart";
import "package:nexus/widgets/chat_page/html/html.dart";
import "package:nexus/widgets/chat_page/member_list.dart";
import "package:nexus/widgets/chat_page/message_wrapper.dart";
import "package:nexus/widgets/chat_page/room_appbar.dart";
import "package:nexus/widgets/chat_page/text_message_wrapper.dart";
import "package:nexus/widgets/chat_page/top_widget.dart";
import "package:nexus/widgets/form_text_input.dart";
import "package:nexus/widgets/loading.dart";
@ -187,6 +185,13 @@ class RoomChat extends HookConsumerWidget {
];
}
final chatTheme = ChatTheme.fromThemeData(theme).copyWith(
colors: ChatColors.fromThemeData(theme).copyWith(
primary: theme.colorScheme.primaryContainer,
onPrimary: theme.colorScheme.onPrimaryContainer,
),
);
return Scaffold(
appBar: RoomAppbar(
room,
@ -208,12 +213,7 @@ class RoomChat extends HookConsumerWidget {
.betterWhen(
data: (controller) => Chat(
currentUserId: userId,
theme: ChatTheme.fromThemeData(theme).copyWith(
colors: ChatColors.fromThemeData(theme).copyWith(
primary: theme.colorScheme.primaryContainer,
onPrimary: theme.colorScheme.onPrimaryContainer,
),
),
theme: chatTheme,
onMessageSecondaryTap:
(
context,
@ -359,80 +359,53 @@ class RoomChat extends HookConsumerWidget {
index, {
required bool isSentByMe,
MessageGroupStatus? groupStatus,
}) {
final image =
message.metadata?["image"]
as ImageMessage?;
return MessageWrapper(
}) => MessageWrapper(
message,
Column(
spacing: 4,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
FlyerChatTextMessage(
showTime: true,
showStatus: false,
customWidget: Column(
spacing: 4,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Html(
textStyle:
message.metadata?["big"] ==
true
? TextStyle(
fontSize: 32,
)
: null,
message.text
.replaceAllMapped(
RegExp(
"(<a\\b[^>]*>.*?<\\/a>)|(\\bhttps?:\\/\\/[^\\s<]+)",
caseSensitive:
false,
TextMessageWrapper(
message,
content: message.text,
groupStatus: groupStatus,
onTapReply: notifier.scrollToMessage,
updateMessage: controller.updateMessage,
isSentByMe: isSentByMe,
),
(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\"/>",
groupStatus,
),
),
if (message.editedAt != null)
Text(
"(edited)",
style: theme
.textTheme
.labelSmall,
),
],
),
if (image != null)
InkWell(
imageMessageBuilder:
(
context,
message,
index, {
required bool isSentByMe,
MessageGroupStatus? groupStatus,
}) {
final text =
message.metadata?["text"] as TextMessage?;
return MessageWrapper(
text ?? message,
Column(
spacing: 4,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
TextMessageWrapper(
message,
content: message.text,
groupStatus: groupStatus,
onTapReply: notifier.scrollToMessage,
updateMessage:
controller.updateMessage,
isSentByMe: isSentByMe,
extra: InkWell(
onTap: () => showDialog(
context: context,
builder: (_) => LayoutBuilder(
builder: (context, constraints) => Dialog(
builder:
(
context,
constraints,
) => Dialog(
backgroundColor:
Colors.transparent,
insetPadding:
@ -452,10 +425,9 @@ class RoomChat extends HookConsumerWidget {
),
child: InteractiveViewer(
child: Image(
fit: BoxFit
.contain,
fit: BoxFit.contain,
image: CachedNetworkImage(
image.source,
message.source,
ref.watch(
CrossCacheController
.provider,
@ -472,7 +444,7 @@ class RoomChat extends HookConsumerWidget {
child: FlyerChatImageMessage(
customImageProvider:
CachedNetworkImage(
image.source,
message.source,
ref.watch(
CrossCacheController
.provider,
@ -494,45 +466,16 @@ class RoomChat extends HookConsumerWidget {
),
),
),
message: image,
index: index,
),
),
],
),
topWidget: TopWidget(
message,
groupStatus: groupStatus,
onTapReply:
notifier.scrollToMessage,
),
message: message,
index: index,
),
),
),
],
),
groupStatus,
);
},
linkPreviewBuilder: (_, message, isSentByMe) =>
LinkPreview(
text: message.text,
backgroundColor: isSentByMe
? theme.colorScheme.inversePrimary
: theme.colorScheme.surfaceContainerLow,
insidePadding: EdgeInsets.symmetric(
vertical: 8,
horizontal: 16,
),
linkPreviewData: message.linkPreviewData,
onLinkPreviewDataFetched: (linkPreviewData) =>
notifier.updateMessage(
message,
message.copyWith(
linkPreviewData: linkPreviewData,
),
),
),
fileMessageBuilder:
(
_,

View file

@ -0,0 +1,105 @@
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/top_widget.dart";
class TextMessageWrapper extends StatelessWidget {
final Message message;
final String? content;
final MessageGroupStatus? groupStatus;
final Future<void> Function(Message oldMessage, Message newMessage)
updateMessage;
final bool isSentByMe;
final Widget? extra;
final OnTapReply onTapReply;
const TextMessageWrapper(
this.message, {
this.content,
this.onTapReply,
required this.updateMessage,
required this.groupStatus,
required this.isSentByMe,
this.extra,
super.key,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
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\"/>"),
),
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!,
],
),
),
);
}
}

View file

@ -5,11 +5,13 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/widgets/chat_page/html/quoted.dart";
typedef OnTapReply = void Function(Message message)?;
class TopWidget extends ConsumerWidget {
final Message message;
final bool alwaysShow;
final MessageGroupStatus? groupStatus;
final void Function(Message message)? onTapReply;
final OnTapReply onTapReply;
const TopWidget(
this.message, {
required this.groupStatus,

View file

@ -509,14 +509,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_math_fork:
dependency: transitive
description:
name: flutter_math_fork
sha256: "6d5f2f1aa57ae539ffb0a04bb39d2da67af74601d685a161aff7ce5bda5fa407"
url: "https://pub.dev"
source: hosted
version: "0.7.4"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -591,15 +583,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
flyer_chat_text_message:
dependency: "direct main"
description:
path: "packages/flyer_chat_text_message"
ref: HEAD
resolved-ref: "03be67c8c81c8f637672ee03dd8f082d2c223627"
url: "https://github.com/Henry-Hiles/flutter_chat_ui"
source: git
version: "2.6.0"
freezed:
dependency: "direct dev"
description:
@ -640,14 +623,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.3"
gpt_markdown:
dependency: transitive
description:
name: gpt_markdown
sha256: "9b88dfaffea644070b648c204ca4a55745a49f4ad0b58ed0ab70913ad593c7a1"
url: "https://pub.dev"
source: hosted
version: "1.1.5"
graphs:
dependency: transitive
description:
@ -1381,14 +1356,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.0+1"
tuple:
dependency: transitive
description:
name: tuple
sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
url: "https://pub.dev"
source: hosted
version: "2.0.2"
typed_data:
dependency: transitive
description:

View file

@ -42,10 +42,6 @@ dependencies:
flyer_chat_image_message: ^2.2.2
flyer_chat_system_message: ^2.1.13
flyer_chat_file_message: ^2.3.1
flyer_chat_text_message:
git:
url: https://github.com/Henry-Hiles/flutter_chat_ui
path: packages/flyer_chat_text_message
flutter_chat_ui:
git:
url: https://github.com/Henry-Hiles/flutter_chat_ui