From c9b5b3dda8f77af9dd00e3e3a2a1263a333532b1 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Mon, 18 May 2026 14:30:44 -0400 Subject: [PATCH] various fixes --- lib/controllers/user_controller.dart | 4 +- lib/widgets/chat_page/event_text.dart | 281 ++++++++++++++------------ lib/widgets/chat_page/room_chat.dart | 73 +++---- 3 files changed, 195 insertions(+), 163 deletions(-) diff --git a/lib/controllers/user_controller.dart b/lib/controllers/user_controller.dart index 4f0993b..e7f5fe0 100644 --- a/lib/controllers/user_controller.dart +++ b/lib/controllers/user_controller.dart @@ -21,8 +21,8 @@ class UserController extends AsyncNotifier { ), ); - if (member?.content is MembershipContent) { - return member!.content as MembershipContent; + if (member?.content case final MembershipContent content) { + return content; } final profile = await ref.watch(ProfileController.provider(userId).future); diff --git a/lib/widgets/chat_page/event_text.dart b/lib/widgets/chat_page/event_text.dart index cd957f6..d274dac 100644 --- a/lib/widgets/chat_page/event_text.dart +++ b/lib/widgets/chat_page/event_text.dart @@ -2,6 +2,7 @@ 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/helpers/extensions/show_context_menu.dart"; import "package:nexus/helpers/launch_helper.dart"; import "package:nexus/models/content/avatar.dart"; import "package:nexus/models/content/content.dart"; @@ -35,6 +36,7 @@ class EventText extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; + final errorStyle = TextStyle(color: colorScheme.error); final timestamp = Tooltip( message: event.timestamp.toString(), @@ -43,140 +45,165 @@ class EventText extends ConsumerWidget { style: theme.textTheme.labelSmall?.copyWith(color: Colors.grey), ), ); + final contextMenuCallback = getEventOptions == null + ? null + : (details) => context.showContextMenu( + globalPosition: details.globalPosition, + children: getEventOptions!(event).toList(), + ); - return switch (event.content) { - MessageContent() => Row( - spacing: 8, - children: [ - isGrouped ? SizedBox(width: 40) : MessageAvatar(event, height: 40), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isGrouped) - Row( - spacing: 4, - children: [ - MessageDisplayname( - event, - style: TextStyle(fontWeight: FontWeight.bold), - ), - Flexible(child: timestamp), - ], - ), - 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, + return GestureDetector( + onSecondaryTapUp: contextMenuCallback, + onLongPressStart: contextMenuCallback, + child: switch (event.content) { + MessageContent() => Row( + spacing: 8, + children: [ + isGrouped ? SizedBox(width: 40) : MessageAvatar(event, height: 40), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isGrouped) + Row( + spacing: 4, 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(), - }, + MessageDisplayname( + event, + style: TextStyle(fontWeight: FontWeight.bold), + ), + Flexible(child: timestamp), ], ), + 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: errorStyle, + ), + TextMessageContent( + :final body, + :final formattedBody, + :final format, + ) => + Column( + children: [ + format == "org.matrix.custom.html" && + !textOnly + ? 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, + maxLines: maxLines, + 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), + ], + ), + _ => + textOnly + ? Text( + "Unknown message type", + style: errorStyle, + ) + : SizedBox.shrink(), + }, + ], + ), + ), ), - ), - ], + ], + ), ), - ), - ], - ), - AvatarContent() => Row( - spacing: 4, - children: [ - SizedBox(width: 4), - Icon(Icons.numbers), - MessageDisplayname( - event, - style: TextStyle( - color: theme.colorScheme.primary, - fontWeight: FontWeight.bold, + ], + ), + AvatarContent() => Row( + spacing: 4, + children: [ + SizedBox(width: 4), + Icon(Icons.numbers), + MessageDisplayname( + event, + style: TextStyle( + color: theme.colorScheme.primary, + fontWeight: FontWeight.bold, + ), ), - ), - Text("changed the room avatar"), - ], - ), - _ => Text("AAAAA"), - }; + Text("changed the room avatar"), + ], + ), + _ => + textOnly + ? Text("Unknown event type", style: errorStyle) + : SizedBox.shrink(), + }, + ); } } diff --git a/lib/widgets/chat_page/room_chat.dart b/lib/widgets/chat_page/room_chat.dart index 41a5d08..7ca5efc 100644 --- a/lib/widgets/chat_page/room_chat.dart +++ b/lib/widgets/chat_page/room_chat.dart @@ -99,42 +99,47 @@ class RoomChat extends HookConsumerWidget { ), )) PopupMenuItem( - child: Row( - children: [ - ...{ - ...ref.watch( - AccountDataController.provider.select( - (value) => IList( - value["m.recent_emoji"]?.content["recent_emoji"] ?? - [], - ).map((entry) => entry["emoji"]), + enabled: false, + child: IconTheme( + data: theme.iconTheme, + child: Row( + children: [ + ...{ + ...ref.watch( + AccountDataController.provider.select( + (value) => IList( + value["m.recent_emoji"] + ?.content["recent_emoji"] ?? + [], + ).map((entry) => entry["emoji"]), + ), + ), + "👍", + "🤣", + "😭", + "🤔", + } + .toIList() + .sublist(0, 4) + .map( + (emoji) => IconButton( + onPressed: () async { + Navigator.of(context).pop(); + await notifier + .sendReaction(emoji, event) + .onError(showError); + }, + icon: Text(emoji), ), ), - "👍", - "🤣", - "😭", - "🤔", - } - .toIList() - .sublist(0, 4) - .map( - (emoji) => IconButton( - onPressed: () async { - Navigator.of(context).pop(); - await notifier - .sendReaction(emoji, event) - .onError(showError); - }, - icon: Text(emoji), - ), - ), - EmojiPickerButton( - context: context, - onPressed: Navigator.of(context).pop, - onSelection: (emoji) => - notifier.sendReaction(emoji, event).onError(showError), - ), - ], + EmojiPickerButton( + context: context, + onPressed: Navigator.of(context).pop, + onSelection: (emoji) => + notifier.sendReaction(emoji, event).onError(showError), + ), + ], + ), ), ), if (ref.watch(