diff --git a/lib/controllers/member_controller.dart b/lib/controllers/member_controller.dart new file mode 100644 index 0000000..97e120f --- /dev/null +++ b/lib/controllers/member_controller.dart @@ -0,0 +1,42 @@ +import "dart:async"; +import "package:collection/collection.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/members_controller.dart"; +import "package:nexus/models/configs/member_config.dart"; +import "package:nexus/models/membership.dart"; + +class MemberController extends AsyncNotifier { + final MemberConfig config; + MemberController(this.config); + + @override + FutureOr build() { + final member = ref.watch( + MembersController.provider(config.room).select( + (value) => value.firstWhereOrNull( + (membership) => membership.userId == config.userId, + ), + ), + ); + if (config.room.hasFetchedMembers || member != null) { + return member ?? + Membership( + avatarUrl: null, + displayName: config.userId, + userId: config.userId, + ); + } + return Membership( + avatarUrl: null, + displayName: config.userId, + userId: config.userId, + ); + + throw UnimplementedError(); + } + + static final provider = AsyncNotifierProvider.family + .autoDispose( + MemberController.new, + ); +} diff --git a/lib/controllers/members_controller.dart b/lib/controllers/members_controller.dart index 268a30d..acdabd1 100644 --- a/lib/controllers/members_controller.dart +++ b/lib/controllers/members_controller.dart @@ -1,25 +1,32 @@ import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/models/event.dart"; +import "package:nexus/models/membership.dart"; import "package:nexus/models/room.dart"; -class MembersController extends Notifier> { +class MembersController extends Notifier> { final Room room; MembersController(this.room); @override - IList build() => (room.state["m.room.member"]?.values ?? []) + IList build() => (room.state["m.room.member"]?.values ?? []) .map( (eventRowId) => room.events.firstWhereOrNull((event) => event.rowId == eventRowId), ) .nonNulls .where((member) => member.content["membership"] == "join") + .map( + (membership) => Membership( + avatarUrl: Uri.tryParse(membership.content["avatar_url"] ?? ""), + userId: membership.stateKey!, + displayName: membership.content["displayname"] ?? membership.stateKey, + ), + ) .toIList(); static final provider = NotifierProvider.family - .autoDispose, Room>( + .autoDispose, Room>( MembersController.new, ); } diff --git a/lib/controllers/message_controller.dart b/lib/controllers/message_controller.dart index f3ef13b..bf0a396 100644 --- a/lib/controllers/message_controller.dart +++ b/lib/controllers/message_controller.dart @@ -2,9 +2,8 @@ import "package:collection/collection.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client_state_controller.dart"; -import "package:nexus/controllers/members_controller.dart"; import "package:nexus/helpers/extensions/mxc_to_https.dart"; -import "package:nexus/models/message_config.dart"; +import "package:nexus/models/configs/message_config.dart"; class MessageController extends AsyncNotifier { final MessageConfig config; @@ -27,12 +26,6 @@ class MessageController extends AsyncNotifier { if (!ref.mounted) return null; - final members = ref.read(MembersController.provider(config.room)); - final author = members.firstWhereOrNull( - (member) => member.stateKey == event.authorId, - ); - if (!ref.mounted) return null; - final content = (event.decrypted ?? event.content); final type = (config.event.decryptedType ?? config.event.type); final newContent = content["m.new_content"] as Map?; @@ -52,14 +45,10 @@ class MessageController extends AsyncNotifier { "timelineId": event.timelineRowId, "big": event.localContent?.bigEmoji == true, "eventType": type, - "avatarUrl": author?.content["avatar_url"], "editSource": event.localContent?.editSource ?? newContent?["body"] ?? content["body"], - "displayName": author?.content["displayname"]?.isNotEmpty == true - ? author?.content["displayname"] - : event.authorId.substring(1).split(":")[0], "txnId": config.event.transactionId, }; diff --git a/lib/controllers/messages_controller.dart b/lib/controllers/messages_controller.dart index 83bd815..28885fb 100644 --- a/lib/controllers/messages_controller.dart +++ b/lib/controllers/messages_controller.dart @@ -2,8 +2,8 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/message_controller.dart"; -import "package:nexus/models/message_config.dart"; -import "package:nexus/models/messages_config.dart"; +import "package:nexus/models/configs/message_config.dart"; +import "package:nexus/models/configs/messages_config.dart"; class MessagesController extends AsyncNotifier> { final MessagesConfig config; diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index 4a4dba2..d737154 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -1,5 +1,4 @@ import "dart:async"; - import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; @@ -11,8 +10,8 @@ import "package:nexus/controllers/message_controller.dart"; import "package:nexus/controllers/messages_controller.dart"; import "package:nexus/controllers/new_events_controller.dart"; import "package:nexus/controllers/rooms_controller.dart"; -import "package:nexus/models/message_config.dart"; -import "package:nexus/models/messages_config.dart"; +import "package:nexus/models/configs/messages_config.dart"; +import "package:nexus/models/configs/message_config.dart"; import "package:nexus/models/requests/get_room_state_request.dart"; import "package:nexus/models/requests/paginate_request.dart"; import "package:nexus/models/requests/redact_event_request.dart"; @@ -31,11 +30,7 @@ class RoomChatController extends AsyncNotifier { if (room == null) return InMemoryChatController(); final state = await client.getRoomState( - GetRoomStateRequest( - roomId: roomId, - fetchMembers: room.metadata?.hasMemberList == false, - includeMembers: true, - ), + GetRoomStateRequest(roomId: roomId), ); ref diff --git a/lib/models/configs/member_config.dart b/lib/models/configs/member_config.dart new file mode 100644 index 0000000..179f780 --- /dev/null +++ b/lib/models/configs/member_config.dart @@ -0,0 +1,13 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:nexus/models/room.dart"; +part "member_config.freezed.dart"; +part "member_config.g.dart"; + +@freezed +abstract class MemberConfig with _$MemberConfig { + const factory MemberConfig({required Room room, required String userId}) = + _MemberConfig; + + factory MemberConfig.fromJson(Map json) => + _$MemberConfigFromJson(json); +} diff --git a/lib/models/message_config.dart b/lib/models/configs/message_config.dart similarity index 100% rename from lib/models/message_config.dart rename to lib/models/configs/message_config.dart diff --git a/lib/models/messages_config.dart b/lib/models/configs/messages_config.dart similarity index 100% rename from lib/models/messages_config.dart rename to lib/models/configs/messages_config.dart diff --git a/lib/models/membership.dart b/lib/models/membership.dart new file mode 100644 index 0000000..9703bbf --- /dev/null +++ b/lib/models/membership.dart @@ -0,0 +1,15 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +part "membership.freezed.dart"; +part "membership.g.dart"; + +@freezed +abstract class Membership with _$Membership { + const factory Membership({ + required Uri? avatarUrl, + required String displayName, + required String userId, + }) = _Membership; + + factory Membership.fromJson(Map json) => + _$MembershipFromJson(json); +} diff --git a/lib/models/requests/get_room_state_request.dart b/lib/models/requests/get_room_state_request.dart index a154d5f..de66b72 100644 --- a/lib/models/requests/get_room_state_request.dart +++ b/lib/models/requests/get_room_state_request.dart @@ -6,7 +6,7 @@ part "get_room_state_request.g.dart"; abstract class GetRoomStateRequest with _$GetRoomStateRequest { const factory GetRoomStateRequest({ required String roomId, - required bool fetchMembers, + @Default(false) bool fetchMembers, @Default(false) bool includeMembers, }) = _GetRoomStateRequest; diff --git a/lib/models/room.dart b/lib/models/room.dart index 3c3eec0..4cd371d 100644 --- a/lib/models/room.dart +++ b/lib/models/room.dart @@ -18,6 +18,7 @@ abstract class Room with _$Room { @Default(IMap.empty()) IMap> receipts, @Default(false) bool dismissNotifications, @Default(true) bool hasMore, + @Default(false) bool hasFetchedMembers, // required IList notifications, }) = _Room; diff --git a/lib/widgets/chat_page/chat_box.dart b/lib/widgets/chat_page/chat_box.dart index b9e7dbb..c1b8c5e 100644 --- a/lib/widgets/chat_page/chat_box.dart +++ b/lib/widgets/chat_page/chat_box.dart @@ -86,10 +86,11 @@ class ChatBox extends HookConsumerWidget { child: Column( children: [ RelationPreview( + relatedMessage, + room: room, shouldMention: shouldMention.value, toggleShouldMention: () => shouldMention.value = !shouldMention.value, - relatedMessage: relatedMessage, relationType: relationType, onDismiss: onDismiss, ), diff --git a/lib/widgets/chat_page/lazy_loading/message_avatar.dart b/lib/widgets/chat_page/lazy_loading/message_avatar.dart new file mode 100644 index 0000000..d68f5fd --- /dev/null +++ b/lib/widgets/chat_page/lazy_loading/message_avatar.dart @@ -0,0 +1,32 @@ +import "package:flutter/widgets.dart"; +import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/member_controller.dart"; +import "package:nexus/helpers/extensions/better_when.dart"; +import "package:nexus/models/configs/member_config.dart"; +import "package:nexus/models/room.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; + +class MessageAvatar extends ConsumerWidget { + final Message message; + final Room room; + final double height; + const MessageAvatar(this.message, this.room, {this.height = 16, super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) => ref + .watch( + MemberController.provider( + MemberConfig(room: room, userId: message.authorId), + ), + ) + .betterWhen( + data: (membership) => AvatarOrHash( + membership.avatarUrl, + membership.displayName, + height: height, + ), + loading: () => + AvatarOrHash(null, message.authorId.substring(1), height: height), + ); +} diff --git a/lib/widgets/chat_page/lazy_loading/message_displayname.dart b/lib/widgets/chat_page/lazy_loading/message_displayname.dart new file mode 100644 index 0000000..8c02d2e --- /dev/null +++ b/lib/widgets/chat_page/lazy_loading/message_displayname.dart @@ -0,0 +1,30 @@ +import "package:flutter/widgets.dart"; +import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/member_controller.dart"; +import "package:nexus/helpers/extensions/better_when.dart"; +import "package:nexus/models/configs/member_config.dart"; +import "package:nexus/models/room.dart"; + +class MessageDisplayname extends ConsumerWidget { + final Message message; + final Room room; + final TextStyle? style; + const MessageDisplayname(this.message, this.room, {this.style, super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) => ref + .watch( + MemberController.provider( + MemberConfig(room: room, userId: message.authorId), + ), + ) + .betterWhen( + data: (membership) => Text( + membership.displayName, + style: style, + overflow: TextOverflow.ellipsis, + ), + loading: SizedBox.shrink, + ); +} diff --git a/lib/widgets/chat_page/member_list.dart b/lib/widgets/chat_page/member_list.dart index 24d22e4..08785c6 100644 --- a/lib/widgets/chat_page/member_list.dart +++ b/lib/widgets/chat_page/member_list.dart @@ -36,18 +36,9 @@ class MemberList extends ConsumerWidget { builder: (context) => Dialog(child: Text("TODO: Open member popover")), ), - leading: AvatarOrHash( - Uri.tryParse(member.content["avatar_url"] ?? ""), - member.content["displayname"].toString(), - ), - title: Text( - member.content["displayname"].toString(), - overflow: TextOverflow.ellipsis, - ), - subtitle: Text( - member.stateKey ?? "Unknown User", - overflow: TextOverflow.ellipsis, - ), + leading: AvatarOrHash(member.avatarUrl, member.displayName), + title: Text(member.displayName, overflow: TextOverflow.ellipsis), + subtitle: Text(member.userId, overflow: TextOverflow.ellipsis), ), ), ], diff --git a/lib/widgets/chat_page/mention_overlay.dart b/lib/widgets/chat_page/mention_overlay.dart index 9858574..75473f0 100644 --- a/lib/widgets/chat_page/mention_overlay.dart +++ b/lib/widgets/chat_page/mention_overlay.dart @@ -40,39 +40,29 @@ class MentionOverlay extends ConsumerWidget { ? members : members.where( (member) => - member.stateKey?.toLowerCase().contains( + member.userId.toLowerCase().contains( query.toLowerCase(), ) == true || - (member.content["displayname"] as String?) - ?.toLowerCase() - .contains(query.toLowerCase()) == + member.displayName.toLowerCase().contains( + query.toLowerCase(), + ) == true, )) .map( (member) => ListTile( leading: AvatarOrHash( - Uri.tryParse( - member.content["avatar_url"] ?? "", - ), - member.content["displayname"] ?? "", + member.avatarUrl, + member.displayName, ), - title: Text( - member.content["displayname"] as String? ?? - member.stateKey ?? - "Unknown User", - ), - subtitle: member.stateKey != null - ? Text(member.stateKey!) - : null, + title: Text(member.displayName), + subtitle: Text(member.userId), onTap: () => addTag( - id: "[@${member.content["displayname"]}](https://matrix.to/#/${member.stateKey})", - name: - member.stateKey - ?.substring(1) - .split(":") - .first ?? - "Unknown User", + id: "[@${member.displayName}](https://matrix.to/#/${member.userId})", + name: member.userId + .substring(1) + .split(":") + .first, ), ), ) diff --git a/lib/widgets/chat_page/message_wrapper.dart b/lib/widgets/chat_page/message_wrapper.dart index da53be0..1be6c2b 100644 --- a/lib/widgets/chat_page/message_wrapper.dart +++ b/lib/widgets/chat_page/message_wrapper.dart @@ -1,12 +1,21 @@ import "package:flutter/material.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/models/room.dart"; +import "package:nexus/widgets/chat_page/lazy_loading/message_avatar.dart"; +import "package:nexus/widgets/chat_page/lazy_loading/message_displayname.dart"; class MessageWrapper extends StatelessWidget { final Message message; final Widget child; + final Room room; final MessageGroupStatus? groupStatus; - const MessageWrapper(this.message, this.child, this.groupStatus, {super.key}); + const MessageWrapper( + this.message, + this.child, + this.groupStatus, + this.room, { + super.key, + }); @override Widget build(BuildContext context) => ClipRRect( @@ -24,11 +33,7 @@ class MessageWrapper extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ groupStatus?.isFirst != false - ? AvatarOrHash( - Uri.parse(message.metadata?["avatarUrl"] ?? ""), - height: 40, - message.metadata?["displayName"] ?? "", - ) + ? MessageAvatar(message, room, height: 40) : SizedBox(width: 40), Expanded( child: Column( @@ -36,9 +41,9 @@ class MessageWrapper extends StatelessWidget { spacing: 4, children: [ if (groupStatus?.isFirst != false) - Text( - message.metadata?["displayName"] ?? message.authorId, - overflow: TextOverflow.ellipsis, + MessageDisplayname( + message, + room, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), diff --git a/lib/widgets/chat_page/relation_preview.dart b/lib/widgets/chat_page/relation_preview.dart index 7aa3ae8..7fded20 100644 --- a/lib/widgets/chat_page/relation_preview.dart +++ b/lib/widgets/chat_page/relation_preview.dart @@ -2,7 +2,9 @@ import "package:flutter/material.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/models/relation_type.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/models/room.dart"; +import "package:nexus/widgets/chat_page/lazy_loading/message_avatar.dart"; +import "package:nexus/widgets/chat_page/lazy_loading/message_displayname.dart"; class RelationPreview extends ConsumerWidget { final Message? relatedMessage; @@ -10,8 +12,11 @@ class RelationPreview extends ConsumerWidget { final VoidCallback onDismiss; final bool shouldMention; final VoidCallback toggleShouldMention; - const RelationPreview({ - required this.relatedMessage, + final Room room; + + const RelationPreview( + this.relatedMessage, { + required this.room, required this.relationType, required this.onDismiss, required this.shouldMention, @@ -36,14 +41,10 @@ class RelationPreview extends ConsumerWidget { "Editing message:", style: TextStyle(fontWeight: FontWeight.bold), ), - AvatarOrHash( - Uri.tryParse(relatedMessage?.metadata?["avatarUrl"] ?? ""), - relatedMessage?.metadata?["displayName"]?.toString() ?? "", - height: 16, - ), - Text( - relatedMessage!.metadata?["displayName"] ?? - relatedMessage!.authorId, + MessageAvatar(relatedMessage!, room), + MessageDisplayname( + relatedMessage!, + room, style: theme.textTheme.labelMedium?.copyWith( fontWeight: FontWeight.bold, ), diff --git a/lib/widgets/chat_page/reply_widget.dart b/lib/widgets/chat_page/reply_widget.dart index cd30acc..b7284bf 100644 --- a/lib/widgets/chat_page/reply_widget.dart +++ b/lib/widgets/chat_page/reply_widget.dart @@ -1,15 +1,15 @@ -import "dart:math"; import "package:flutter/material.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/event_controller.dart"; import "package:nexus/controllers/message_controller.dart"; import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/models/message_config.dart"; +import "package:nexus/models/configs/message_config.dart"; import "package:nexus/models/requests/get_event_request.dart"; import "package:nexus/models/room.dart"; -import "package:nexus/widgets/avatar_or_hash.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"; typedef OnTapReply = void Function(Message message)?; @@ -61,73 +61,28 @@ class ReplyWidget extends ConsumerWidget { 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, - ), - 5, - ), - replyMessage.metadata!["body"].length, - ), - ) - : null; - final replyText = - (smallerText == null || - smallerText.length == - replyMessage - .metadata!["body"] - .length) - ? replyMessage.metadata!["body"] - : "$smallerText..."; - return InkWell( onTap: () => onTapReply?.call(replyMessage), child: Row( mainAxisSize: MainAxisSize.min, spacing: 8, children: [ - AvatarOrHash( - Uri.tryParse( - replyMessage.metadata?["avatarUrl"] ?? - "", - ), - replyMessage.metadata?["displayName"] ?? - "", - height: 16, - ), + MessageAvatar(message, room), Flexible( - child: Text( - replyMessage - .metadata?["displayName"] ?? - replyMessage.authorId, + child: MessageDisplayname( + replyMessage, + room, style: Theme.of(context) .textTheme .labelMedium ?.copyWith( fontWeight: FontWeight.bold, ), - overflow: TextOverflow.ellipsis, ), ), Flexible( child: Text( - replyText, + replyMessage.metadata!["body"], overflow: TextOverflow.ellipsis, style: Theme.of( context, diff --git a/lib/widgets/chat_page/room_chat.dart b/lib/widgets/chat_page/room_chat.dart index 839109f..1627573 100644 --- a/lib/widgets/chat_page/room_chat.dart +++ b/lib/widgets/chat_page/room_chat.dart @@ -320,6 +320,7 @@ class RoomChat extends HookConsumerWidget { ), ), groupStatus, + room, ), systemMessageBuilder: diff --git a/lib/widgets/chat_page/text_message_wrapper.dart b/lib/widgets/chat_page/text_message_wrapper.dart index 9734a34..814deb4 100644 --- a/lib/widgets/chat_page/text_message_wrapper.dart +++ b/lib/widgets/chat_page/text_message_wrapper.dart @@ -109,6 +109,7 @@ class TextMessageWrapper extends StatelessWidget { ), ), groupStatus, + room, ); } }