From cb20cb38fd45937407ca4ded544e9d259710f663 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Mon, 18 May 2026 14:20:35 -0400 Subject: [PATCH] text message rendering --- lib/controllers/power_level_controller.dart | 4 +- lib/models/content/membership.dart | 1 - lib/models/content/power_levels.dart | 1 - lib/widgets/chat_page/event_text.dart | 111 +++++++++++++++++++- lib/widgets/link_preview.dart | 61 +++++++++++ 5 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 lib/widgets/link_preview.dart diff --git a/lib/controllers/power_level_controller.dart b/lib/controllers/power_level_controller.dart index ea8a05e..d751378 100644 --- a/lib/controllers/power_level_controller.dart +++ b/lib/controllers/power_level_controller.dart @@ -27,7 +27,9 @@ class PowerLevelController extends Notifier { final content = event?.content is PowerLevelsContent ? event!.content : PowerLevelsContent(); - final user = ref.watch(ClientStateController.provider)?.userId; + final user = ref.watch( + ClientStateController.provider.select((value) => value?.userId), + ); if (user == null || content is! PowerLevelsContent) return false; int powerLevelOf(String userId) => diff --git a/lib/models/content/membership.dart b/lib/models/content/membership.dart index 7e00811..aa5a36d 100644 --- a/lib/models/content/membership.dart +++ b/lib/models/content/membership.dart @@ -7,7 +7,6 @@ part "membership.g.dart"; @freezed abstract class MembershipContent extends Content with _$MembershipContent { MembershipContent._(); - factory MembershipContent({ @JsonKey(name: "displayname") required String? displayName, @JsonKey(name: "membership") required MembershipStatus status, diff --git a/lib/models/content/power_levels.dart b/lib/models/content/power_levels.dart index bff41b0..3709c38 100644 --- a/lib/models/content/power_levels.dart +++ b/lib/models/content/power_levels.dart @@ -7,7 +7,6 @@ part "power_levels.g.dart"; @freezed abstract class PowerLevelsContent extends Content with _$PowerLevelsContent { PowerLevelsContent._(); - factory PowerLevelsContent({ @Default(IMap.empty()) IMap events, @Default(IMap.empty()) IMap users, diff --git a/lib/widgets/chat_page/event_text.dart b/lib/widgets/chat_page/event_text.dart index e35b06f..d4624be 100644 --- a/lib/widgets/chat_page/event_text.dart +++ b/lib/widgets/chat_page/event_text.dart @@ -1,13 +1,26 @@ +import "package:cross_cache/cross_cache.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/client_state_controller.dart"; +import "package:nexus/controllers/cross_cache_controller.dart"; +import "package:nexus/controllers/url_preview_controller.dart"; +import "package:nexus/helpers/extensions/better_when.dart"; +import "package:nexus/helpers/extensions/get_headers.dart"; +import "package:nexus/helpers/launch_helper.dart"; import "package:nexus/models/content/avatar.dart"; +import "package:nexus/models/content/content.dart"; import "package:nexus/models/content/message.dart"; import "package:nexus/models/event.dart"; +import "package:nexus/widgets/chat_page/html/html.dart"; +import "package:nexus/widgets/chat_page/html/quoted.dart"; import "package:nexus/widgets/chat_page/lazy_loading/message_avatar.dart"; import "package:nexus/widgets/chat_page/lazy_loading/message_displayname.dart"; +import "package:nexus/widgets/link_preview.dart"; import "package:timeago/timeago.dart"; +import "package:flutter_linkify/flutter_linkify.dart"; -class EventText extends StatelessWidget { +class EventText extends ConsumerWidget { final Event event; final bool textOnly; final bool isGrouped; @@ -25,8 +38,10 @@ class EventText extends StatelessWidget { }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final timestamp = Tooltip( message: event.timestamp.toString(), child: Text( @@ -55,7 +70,97 @@ class EventText extends StatelessWidget { Expanded(child: timestamp), ], ), - Text("data"), + ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(8)), + child: Container( + padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12), + decoration: BoxDecoration( + color: + ref.watch( + ClientStateController.provider.select( + (value) => value?.userId, + ), + ) == + event.sender + ? (event.eventId.startsWith("~") + ? colorScheme.onPrimary + : colorScheme.primaryContainer) + : colorScheme.surfaceContainer, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Quoted( // TODO: Show replies + // EventText(replyEvent textOnly: true, maxLines: 1,) + // ), + switch (event.content) { + Content(:final parseError?) => SelectableText( + "An error occurred while parsing this message:\n$parseError", + style: TextStyle(color: colorScheme.error), + ), + TextMessageContent( + :final body, + :final formattedBody, + :final format, + ) => + Column( + children: [ + format == "org.matrix.custom.html" + ? Html( + textStyle: + event.localContent?.bigEmoji == true + ? TextStyle(fontSize: 32) + : null, + formattedBody!.replaceAllMapped( + RegExp( + "(]*>.*?<\\/a>)|(\\bhttps?:\\/\\/[^\\s<]+)", + caseSensitive: false, + dotAll: true, + ), + (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"; + }, + ), + ) + : Linkify( + text: body, + options: LinkifyOptions( + humanize: false, + ), + onOpen: (link) => ref + .watch(LaunchHelper.provider) + .launchUrl(Uri.parse(link.url)), + linkStyle: TextStyle( + color: Theme.of( + context, + ).colorScheme.primary, + ), + ), + if (event.lastEditRowId != null) + Text( + "(edited)", + style: theme.textTheme.labelSmall, + ), + if (RegExp( + r'''https?://[^\s"'<>]+''', + ).allMatches(body).firstOrNull?.group(0) + case final link?) + LinkPreview(link), + ], + ), + _ => SizedBox.shrink(), + }, + ], + ), + ), + ), ], ), ), diff --git a/lib/widgets/link_preview.dart b/lib/widgets/link_preview.dart new file mode 100644 index 0000000..1186097 --- /dev/null +++ b/lib/widgets/link_preview.dart @@ -0,0 +1,61 @@ +import "package:cross_cache/cross_cache.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/cross_cache_controller.dart"; +import "package:nexus/controllers/url_preview_controller.dart"; +import "package:nexus/helpers/extensions/better_when.dart"; +import "package:nexus/helpers/extensions/get_headers.dart"; +import "package:nexus/helpers/launch_helper.dart"; + +class LinkPreview extends ConsumerWidget { + final String link; + const LinkPreview(this.link, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) => ref + .watch(UrlPreviewController.provider(link)) + .betterWhen( + data: (preview) => preview == null + ? SizedBox.shrink() + : InkWell( + onTap: () => + ref.watch(LaunchHelper.provider).launchUrl(Uri.parse(link)), + child: Card( + child: Column( + children: [ + if (preview.title != null) + Text( + preview.title!, + style: Theme.of(context).textTheme.labelLarge, + ), + if (preview.description != null) + Text(preview.description!), + if (preview.imageUrl != null) + Image( + errorBuilder: (_, _, _) => SizedBox.shrink(), + width: preview.width, + height: preview.height, + image: CachedNetworkImage( + preview.imageUrl!, + ref.watch(CrossCacheController.provider), + headers: ref.headers, + ), + fit: BoxFit.cover, + ), + ], + ), + // text: link, + // backgroundColor: isSentByMe + // ? colorScheme.inversePrimary + // : colorScheme.surfaceContainerLow, + // outsidePadding: EdgeInsets.only(top: 4), + // insidePadding: EdgeInsets.symmetric( + // vertical: 8, + // horizontal: 16, + // ), + // linkPreviewData: preview, + // onLinkPreviewDataFetched: (_) => null, + ), + ), + ); +}