diff --git a/lib/controllers/message_controller.dart b/lib/controllers/message_controller.dart index 1edf5e9..fb10945 100644 --- a/lib/controllers/message_controller.dart +++ b/lib/controllers/message_controller.dart @@ -46,6 +46,12 @@ class MessageController extends AsyncNotifier { final content = (event.decrypted ?? event.content); final type = (config.event.decryptedType ?? config.event.type); final newContent = content["m.new_content"] as Map?; + + final homeserver = ref.read(ClientStateController.provider)?.homeserverUrl; + final source = homeserver == null || content["url"] == null + ? "null" + : Uri.parse(content["url"]).mxcToHttps(homeserver).toString(); + final metadata = { "body": config.event.redactedBy == null ? (newContent?["body"] ?? content["body"] ?? "") @@ -72,6 +78,16 @@ class MessageController extends AsyncNotifier { ? author?.content["displayname"] : event.authorId.substring(1).split(":")[0], "txnId": config.event.transactionId, + "image": content["msgtype"] == "m.image" + ? Message.image( + id: "${config.event.eventId}-image", + authorId: event.authorId, + source: source, + replyToMessageId: replyId, + deliveredAt: config.event.timestamp, + blurhash: (content["info"] as Map?)?["xyz.amorgan.blurhash"], + ) + : null, }; if (!ref.mounted) return null; @@ -106,11 +122,6 @@ class MessageController extends AsyncNotifier { ) as TextMessage; - final homeserver = ref.read(ClientStateController.provider)?.homeserverUrl; - final source = homeserver == null || content["url"] == null - ? "null" - : Uri.parse(content["url"]).mxcToHttps(homeserver).toString(); - return switch (type) { "m.room.encrypted" => asText.copyWith( text: "Unable to decrypt message.", @@ -127,20 +138,6 @@ class MessageController extends AsyncNotifier { // authorId: senderId, // ), ("m.sticker" || "m.room.message") => switch (content["msgtype"]) { - (null || "m.image") => Message.image( - id: config.event.eventId, - metadata: metadata, - authorId: event.authorId, - text: - newContent?["formatted_body"] ?? - newContent?["body"] ?? - content["formatted_body"] ?? - content["body"], - source: source, - replyToMessageId: replyId, - deliveredAt: config.event.timestamp, - blurhash: (content["info"] as Map?)?["xyz.amorgan.blurhash"], - ), "m.audio" || "m.file" => Message.file( name: content["filename"].toString(), size: content["info"]["size"], diff --git a/lib/widgets/chat_page/message_wrapper.dart b/lib/widgets/chat_page/message_wrapper.dart new file mode 100644 index 0000000..fdce3cd --- /dev/null +++ b/lib/widgets/chat_page/message_wrapper.dart @@ -0,0 +1,39 @@ +import "package:flutter/material.dart"; +import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; + +class MessageWrapper extends StatelessWidget { + final Message message; + final Widget child; + final MessageGroupStatus? groupStatus; + 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), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + 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, + ], + ), + ], + ); +} diff --git a/lib/widgets/chat_page/room_chat.dart b/lib/widgets/chat_page/room_chat.dart index 46f07c8..2d12532 100644 --- a/lib/widgets/chat_page/room_chat.dart +++ b/lib/widgets/chat_page/room_chat.dart @@ -1,5 +1,4 @@ import "dart:math"; - import "package:cross_cache/cross_cache.dart"; import "package:flutter/material.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; @@ -21,10 +20,10 @@ import "package:nexus/helpers/extensions/get_headers.dart"; 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/avatar_or_hash.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/top_widget.dart"; import "package:nexus/widgets/form_text_input.dart"; @@ -52,37 +51,6 @@ class RoomChat extends HookConsumerWidget { final theme = Theme.of(context); final danger = theme.colorScheme.error; - Widget getTextWidget(TextMessage message) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Html( - textStyle: message.metadata?["big"] == true - ? TextStyle(fontSize: 32) - : null, - message.text - .replaceAllMapped( - RegExp( - "(]*>.*?<\\/a>)|(\\bhttps?:\\/\\/[^\\s<]+)", - caseSensitive: false, - ), - (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"; - }, - ) - .replaceAll("\n", "
"), - ), - if (message.editedAt != null) - Text("(edited)", style: theme.textTheme.labelSmall), - ], - ); - if (room == null || userId == null || room.metadata?.id == null) { return Center( child: Text( @@ -268,47 +236,6 @@ class RoomChat extends HookConsumerWidget { globalPosition: details.globalPosition, children: getMessageOptions(message), ), - onMessageTap: - ( - context, - message, { - required details, - required index, - }) { - if (message is ImageMessage) { - showDialog( - context: context, - builder: (_) => LayoutBuilder( - builder: (context, constraints) => Dialog( - backgroundColor: Colors.transparent, - insetPadding: EdgeInsets.all( - constraints.maxWidth / 100, - ), - child: ConstrainedBox( - constraints: BoxConstraints( - minWidth: min( - constraints.maxWidth, - 1000, - ), - ), - child: InteractiveViewer( - child: Image( - fit: BoxFit.contain, - image: CachedNetworkImage( - message.source, - ref.watch( - CrossCacheController.provider, - ), - headers: ref.headers, - ), - ), - ), - ), - ), - ), - ); - } - }, builders: Builders( loadMoreBuilder: (_) => Loading(), chatAnimatedListBuilder: (_, itemBuilder) => @@ -432,48 +359,161 @@ class RoomChat extends HookConsumerWidget { index, { required bool isSentByMe, MessageGroupStatus? groupStatus, - }) => Row( - spacing: 8, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - groupStatus?.isFirst != false - ? AvatarOrHash( - Uri.parse( - message.metadata?["avatarUrl"] ?? - "", - ), - height: 40, - message.metadata?["displayName"] ?? - "", - ) - : SizedBox(width: 40), + }) { + final image = + message.metadata?["image"] + as ImageMessage?; + return MessageWrapper( + message, Column( + spacing: 4, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (groupStatus?.isFirst != false) - Text( - message.metadata?["displayName"] ?? - message.authorId, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith( - fontWeight: FontWeight.bold, - ), - ), FlyerChatTextMessage( - showStatus: false, - customWidget: getTextWidget(message), - message: message, 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>)|(\\bhttps?:\\/\\/[^\\s<]+)", + caseSensitive: + false, + ), + (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"; + }, + ) + .replaceAll( + "\n", + "
", + ), + ), + if (message.editedAt != null) + Text( + "(edited)", + style: theme + .textTheme + .labelSmall, + ), + ], + ), + if (image != null) + InkWell( + onTap: () => showDialog( + context: context, + builder: (_) => LayoutBuilder( + builder: (context, constraints) => Dialog( + backgroundColor: + Colors.transparent, + insetPadding: + EdgeInsets.all( + constraints + .maxWidth / + 100, + ), + child: ConstrainedBox( + constraints: + BoxConstraints( + minWidth: min( + constraints + .maxWidth, + 1000, + ), + ), + child: InteractiveViewer( + child: Image( + fit: BoxFit + .contain, + image: CachedNetworkImage( + image.source, + ref.watch( + CrossCacheController + .provider, + ), + headers: + ref.headers, + ), + ), + ), + ), + ), + ), + ), + child: FlyerChatImageMessage( + customImageProvider: + CachedNetworkImage( + image.source, + ref.watch( + CrossCacheController + .provider, + ), + headers: ref.headers, + ), + errorBuilder: + ( + context, + error, + stackTrace, + ) => Center( + child: Text( + "Image Failed to Load", + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.error, + ), + ), + ), + message: image, + index: index, + ), + ), + ], + ), + topWidget: TopWidget( + message, + groupStatus: groupStatus, + onTapReply: + notifier.scrollToMessage, + ), + message: message, index: index, ), ], ), - ], - ), + groupStatus, + ); + }, linkPreviewBuilder: (_, message, isSentByMe) => LinkPreview( text: message.text, @@ -493,80 +533,6 @@ class RoomChat extends HookConsumerWidget { ), ), ), - imageMessageBuilder: - ( - _, - message, - index, { - required bool isSentByMe, - MessageGroupStatus? groupStatus, - }) { - final textMessage = - message.text?.isNotEmpty == true - ? TextMessage( - id: "${message.id}-text", - authorId: message.authorId, - text: message.text!, - ) - : null; - return Column( - spacing: 4, - crossAxisAlignment: isSentByMe - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - SizedBox(height: 12), - if (textMessage != null) - FlyerChatTextMessage( - customWidget: getTextWidget( - textMessage, - ), - topWidget: TopWidget( - message, - groupStatus: groupStatus, - onTapReply: - notifier.scrollToMessage, - alwaysShow: true, - ), - message: textMessage, - index: index, - ), - FlyerChatImageMessage( - topWidget: - message.text?.isNotEmpty == true - ? null - : TopWidget( - message, - groupStatus: groupStatus, - onTapReply: - notifier.scrollToMessage, - alwaysShow: true, - ), - customImageProvider: CachedNetworkImage( - message.source, - ref.watch( - CrossCacheController.provider, - ), - headers: ref.headers, - ), - errorBuilder: - (context, error, stackTrace) => - Center( - child: Text( - "Image Failed to Load", - style: TextStyle( - color: Theme.of( - context, - ).colorScheme.error, - ), - ), - ), - message: message, - index: index, - ), - ], - ); - }, fileMessageBuilder: ( _, @@ -574,22 +540,28 @@ class RoomChat extends HookConsumerWidget { index, { required bool isSentByMe, MessageGroupStatus? groupStatus, - }) => InkWell( - onTap: () => showDialog( - context: context, - builder: (_) => Dialog( - child: Text("TODO: Download Attachments"), + }) => MessageWrapper( + message, + InkWell( + onTap: () => showDialog( + context: context, + builder: (_) => Dialog( + child: Text( + "TODO: Download Attachments", + ), + ), + ), + child: FlyerChatFileMessage( + topWidget: TopWidget( + message, + onTapReply: notifier.scrollToMessage, + groupStatus: groupStatus, + ), + message: message, + index: index, ), ), - child: FlyerChatFileMessage( - topWidget: TopWidget( - message, - onTapReply: notifier.scrollToMessage, - groupStatus: groupStatus, - ), - message: message, - index: index, - ), + groupStatus, ), systemMessageBuilder: ( diff --git a/lib/widgets/chat_page/top_widget.dart b/lib/widgets/chat_page/top_widget.dart index 636bc84..2f4286a 100644 --- a/lib/widgets/chat_page/top_widget.dart +++ b/lib/widgets/chat_page/top_widget.dart @@ -19,107 +19,71 @@ class TopWidget extends ConsumerWidget { }); @override - Widget build(BuildContext context, WidgetRef ref) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Builder( - builder: (_) { - final replyMessage = message.metadata?["reply"] as Message?; + Widget build(BuildContext context, WidgetRef ref) { + final replyMessage = message.metadata?["reply"] as Message?; - if (replyMessage == null) return SizedBox.shrink(); + if (replyMessage == null) return SizedBox.shrink(); - final smallerText = - message is TextMessage && replyMessage.metadata!["body"] != null - ? replyMessage.metadata!["body"].substring( - 0, - min( - max( - max( - (message as TextMessage).text.length - - (replyMessage.metadata?["displayName"] as String) - .length - - 5, - message.metadata?["displayName"].length, - ), + final smallerText = + message is TextMessage && replyMessage.metadata!["body"] != null + ? replyMessage.metadata!["body"].substring( + 0, + min( + max( + max( + (message as TextMessage).text.length - + (replyMessage.metadata?["displayName"] as String).length - 5, - ), - replyMessage.metadata!["body"].length, - ), - ) - : null; - final replyText = - (smallerText == null || - smallerText.length == replyMessage.metadata!["body"].length) - ? replyMessage.metadata!["body"] - : "$smallerText..."; - - return Padding( - padding: EdgeInsets.only(bottom: 12), - child: InkWell( - onTap: () => onTapReply?.call(replyMessage), - child: Quoted( - Row( - mainAxisSize: MainAxisSize.min, - spacing: 8, - children: [ - AvatarOrHash( - Uri.tryParse(replyMessage.metadata?["avatarUrl"] ?? ""), - replyMessage.metadata?["displayName"] ?? "", - height: 16, - ), - Flexible( - child: Text( - replyMessage.metadata?["displayName"] ?? - replyMessage.authorId, - style: Theme.of(context).textTheme.labelMedium - ?.copyWith(fontWeight: FontWeight.bold), - overflow: TextOverflow.ellipsis, - ), - ), - Flexible( - child: Text( - replyText, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.labelMedium, - maxLines: 1, - ), - ), - ], + message.metadata?["displayName"].length, ), + 5, ), + replyMessage.metadata!["body"].length, ), - ); - }, - ), - if (alwaysShow || - groupStatus?.isFirst != false || - message.metadata?["reply"] != null) - InkWell( - onTap: () => showDialog( - context: context, - builder: (_) => - Dialog(child: Text("TODO: Show user profile")), // TODO - ), - child: Row( + ) + : null; + final replyText = + (smallerText == null || + smallerText.length == replyMessage.metadata!["body"].length) + ? replyMessage.metadata!["body"] + : "$smallerText..."; + + return Padding( + padding: EdgeInsets.only(bottom: 12), + child: InkWell( + onTap: () => onTapReply?.call(replyMessage), + child: Quoted( + Row( mainAxisSize: MainAxisSize.min, spacing: 8, children: [ AvatarOrHash( - Uri.parse(message.metadata?["avatarUrl"] ?? ""), - message.metadata?["displayName"] ?? "", + Uri.tryParse(replyMessage.metadata?["avatarUrl"] ?? ""), + replyMessage.metadata?["displayName"] ?? "", + height: 16, ), Flexible( child: Text( - message.metadata?["displayName"] ?? message.authorId, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium?.copyWith( + replyMessage.metadata?["displayName"] ?? + replyMessage.authorId, + style: Theme.of(context).textTheme.labelMedium?.copyWith( fontWeight: FontWeight.bold, ), + overflow: TextOverflow.ellipsis, + ), + ), + Flexible( + child: Text( + replyText, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.labelMedium, + maxLines: 1, ), ), ], ), ), - ], - ); + ), + ); + } }