text message rendering

This commit is contained in:
Henry Hiles 2026-05-18 14:20:35 -04:00
commit cb20cb38fd
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
5 changed files with 172 additions and 6 deletions

View file

@ -27,7 +27,9 @@ class PowerLevelController extends Notifier<bool> {
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) =>

View file

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

View file

@ -7,7 +7,6 @@ part "power_levels.g.dart";
@freezed
abstract class PowerLevelsContent extends Content with _$PowerLevelsContent {
PowerLevelsContent._();
factory PowerLevelsContent({
@Default(IMap.empty()) IMap<String, int> events,
@Default(IMap.empty()) IMap<String, int> users,

View file

@ -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\\b[^>]*>.*?<\\/a>)|(\\bhttps?:\\/\\/[^\\s<]+)",
caseSensitive: false,
dotAll: true,
),
(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>";
},
),
)
: 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(),
},
],
),
),
),
],
),
),

View file

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