From cff580dee2fd35b4393b69c7a5e1bb3f06e8a757 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Wed, 20 May 2026 16:06:17 -0400 Subject: [PATCH] general fixups, plus adding colors for names --- lib/controllers/author_controller.dart | 5 +- lib/controllers/user_controller.dart | 33 +++++++-- lib/main.dart | 4 +- lib/models/configs/user_config.dart | 12 ++++ lib/widgets/composer/relation_preview.dart | 32 ++------- lib/widgets/event_preview.dart | 36 ++++++++++ lib/widgets/html/html.dart | 9 ++- lib/widgets/html/mention_chip.dart | 10 ++- lib/widgets/lazy_loading/message_avatar.dart | 2 +- .../lazy_loading/message_displayname.dart | 13 +++- lib/widgets/member_list.dart | 25 +++++-- lib/widgets/renderers/event.dart | 58 ++++----------- lib/widgets/renderers/membership.dart | 71 +++++++++++-------- pubspec.lock | 8 +++ 14 files changed, 196 insertions(+), 122 deletions(-) create mode 100644 lib/models/configs/user_config.dart create mode 100644 lib/widgets/event_preview.dart diff --git a/lib/controllers/author_controller.dart b/lib/controllers/author_controller.dart index 8499775..70070e1 100644 --- a/lib/controllers/author_controller.dart +++ b/lib/controllers/author_controller.dart @@ -1,6 +1,7 @@ import "dart:async"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/user_controller.dart"; +import "package:nexus/models/configs/user_config.dart"; import "package:nexus/models/content/membership.dart"; import "package:nexus/models/event.dart"; @@ -11,7 +12,9 @@ class AuthorController extends AsyncNotifier { @override Future build() async { final member = await ref.watch( - UserController.provider(event.sender).future, + UserController.provider( + UserConfig(roomId: event.roomId, userId: event.sender), + ).future, ); return MembershipContent( diff --git a/lib/controllers/user_controller.dart b/lib/controllers/user_controller.dart index 16bfea7..5a47ba6 100644 --- a/lib/controllers/user_controller.dart +++ b/lib/controllers/user_controller.dart @@ -4,25 +4,44 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/members_controller.dart"; import "package:nexus/controllers/profile_controller.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; +import "package:nexus/models/configs/user_config.dart"; import "package:nexus/models/content/membership.dart"; import "package:nexus/models/membership_status.dart"; class UserController extends AsyncNotifier { - final String userId; - UserController(this.userId); + final UserConfig config; + UserController(this.config); @override Future build() async { - final profile = await ref.watch(ProfileController.provider(userId).future); + final member = config.roomId == null + ? null + : await ref.watch( + MembersController.provider(config.roomId!).selectAsync( + (value) => value.firstWhereOrNull( + (membership) => membership.stateKey == config.userId, + ), + ), + ); + + if (member?.content case final MembershipContent content) { + return content; + } + + final profile = await ref.watch( + ProfileController.provider(config.userId).future, + ); return MembershipContent( status: MembershipStatus.leave, avatarUrl: profile.avatarUrl, - displayName: profile.displayName ?? userId.localpart, + displayName: profile.displayName ?? config.userId.localpart, ); } static final provider = - AsyncNotifierProvider.family( - UserController.new, - ); + AsyncNotifierProvider.family< + UserController, + MembershipContent, + UserConfig + >(UserController.new); } diff --git a/lib/main.dart b/lib/main.dart index b687ebd..834aeef 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -67,8 +67,8 @@ void main() async { await windowManager.setMinimumSize(Size.square(500)); } - FlutterError.onError = (FlutterErrorDetails details) => - showError(details.exception.toString(), details.stack); + // FlutterError.onError = (FlutterErrorDetails details) => + // showError(details.exception.toString(), details.stack); runApp( ProviderScope( diff --git a/lib/models/configs/user_config.dart b/lib/models/configs/user_config.dart new file mode 100644 index 0000000..4f3f8ff --- /dev/null +++ b/lib/models/configs/user_config.dart @@ -0,0 +1,12 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +part "user_config.freezed.dart"; +part "user_config.g.dart"; + +@freezed +abstract class UserConfig with _$UserConfig { + const factory UserConfig({required String? roomId, required String userId}) = + _UserConfig; + + factory UserConfig.fromJson(Map json) => + _$UserConfigFromJson(json); +} diff --git a/lib/widgets/composer/relation_preview.dart b/lib/widgets/composer/relation_preview.dart index 028e412..f2bcaf6 100644 --- a/lib/widgets/composer/relation_preview.dart +++ b/lib/widgets/composer/relation_preview.dart @@ -2,9 +2,7 @@ import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/relation_type.dart"; -import "package:nexus/widgets/renderers/event.dart"; -import "package:nexus/widgets/lazy_loading/message_avatar.dart"; -import "package:nexus/widgets/lazy_loading/message_displayname.dart"; +import "package:nexus/widgets/event_preview.dart"; class RelationPreview extends ConsumerWidget { final Event? relatedEvent; @@ -29,7 +27,7 @@ class RelationPreview extends ConsumerWidget { return Container( color: theme.colorScheme.surfaceContainerHigh, - padding: EdgeInsets.symmetric(horizontal: 8), + padding: EdgeInsets.symmetric(horizontal: 12), child: Row( spacing: 8, children: [ @@ -39,30 +37,10 @@ class RelationPreview extends ConsumerWidget { style: TextStyle(fontWeight: FontWeight.bold), ), - MessageAvatar(relatedEvent!), - Expanded( - child: Row( - spacing: 8, - children: [ - Flexible( - child: MessageDisplayname( - relatedEvent!, - style: theme.textTheme.labelMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ), - Expanded( - child: IgnorePointer( - child: EventRenderer( - relatedEvent!, - textOnly: true, - maxLines: 1, - ), - ), - ), - ], + child: Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: EventPreview(relatedEvent!), ), ), diff --git a/lib/widgets/event_preview.dart b/lib/widgets/event_preview.dart new file mode 100644 index 0000000..df289bb --- /dev/null +++ b/lib/widgets/event_preview.dart @@ -0,0 +1,36 @@ +import "package:flutter/material.dart"; +import "package:nexus/models/content/message.dart"; +import "package:nexus/models/event.dart"; +import "package:nexus/widgets/lazy_loading/message_avatar.dart"; +import "package:nexus/widgets/lazy_loading/message_displayname.dart"; +import "package:nexus/widgets/renderers/event.dart"; + +class EventPreview extends StatelessWidget { + final Event event; + const EventPreview(this.event, {super.key}); + + @override + Widget build(BuildContext context) => IgnorePointer( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 4), + child: Row( + spacing: 12, + children: [ + if (event.content is MessageContent) MessageAvatar(event), + + Expanded( + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 4, + runSpacing: 2, + children: [ + if (event.content is MessageContent) MessageDisplayname(event), + EventRenderer(event, textOnly: true, maxLines: 1), + ], + ), + ), + ], + ), + ), + ); +} diff --git a/lib/widgets/html/html.dart b/lib/widgets/html/html.dart index e889aff..449f400 100644 --- a/lib/widgets/html/html.dart +++ b/lib/widgets/html/html.dart @@ -17,8 +17,9 @@ import "package:nexus/widgets/html/quoted.dart"; class Html extends ConsumerWidget { final String html; + final String? roomId; final TextStyle? textStyle; - const Html(this.html, {this.textStyle, super.key}); + const Html(this.html, {this.roomId, this.textStyle, super.key}); @override Widget build(BuildContext context, WidgetRef ref) => HtmlWidget( @@ -59,13 +60,15 @@ class Html extends ConsumerWidget { ) : null, - "blockquote" => Quoted(Html(element.innerHtml)), + "blockquote" => Quoted( + Html(element.innerHtml, textStyle: textStyle, roomId: roomId), + ), "a" => element.attributes["href"]?.mention == null ? null : InlineCustomWidget( - child: MentionChip(element.attributes["href"]!), + child: MentionChip(element.attributes["href"]!, roomId), ), "img" => diff --git a/lib/widgets/html/mention_chip.dart b/lib/widgets/html/mention_chip.dart index 059b997..4791ed8 100644 --- a/lib/widgets/html/mention_chip.dart +++ b/lib/widgets/html/mention_chip.dart @@ -3,17 +3,23 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/user_controller.dart"; import "package:nexus/helpers/extensions/link_to_mention.dart"; import "package:nexus/helpers/extensions/show_user_popover.dart"; +import "package:nexus/models/configs/user_config.dart"; class MentionChip extends ConsumerWidget { + final String? roomId; final String content; - const MentionChip(this.content, {super.key}); + const MentionChip(this.content, this.roomId, {super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final mention = content.mention; final membership = mention?.startsWith("@") == true ? ref - .watch(UserController.provider(mention!)) + .watch( + UserController.provider( + UserConfig(roomId: roomId, userId: mention!), + ), + ) .whenOrNull(data: (data) => data) : null; diff --git a/lib/widgets/lazy_loading/message_avatar.dart b/lib/widgets/lazy_loading/message_avatar.dart index 529edaa..215bb35 100644 --- a/lib/widgets/lazy_loading/message_avatar.dart +++ b/lib/widgets/lazy_loading/message_avatar.dart @@ -10,7 +10,7 @@ import "package:nexus/widgets/avatar_or_hash.dart"; class MessageAvatar extends ConsumerWidget { final Event event; final double height; - const MessageAvatar(this.event, {this.height = 16, super.key}); + const MessageAvatar(this.event, {this.height = 24, super.key}); @override Widget build(BuildContext context, WidgetRef ref) => ref diff --git a/lib/widgets/lazy_loading/message_displayname.dart b/lib/widgets/lazy_loading/message_displayname.dart index 9c6abd1..e388fe7 100644 --- a/lib/widgets/lazy_loading/message_displayname.dart +++ b/lib/widgets/lazy_loading/message_displayname.dart @@ -1,3 +1,4 @@ +import "package:color_hash/color_hash.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/author_controller.dart"; @@ -31,7 +32,17 @@ class MessageDisplayname extends ConsumerWidget { : null, child: Text( "${membership.displayName ?? event.sender.localpart}${event.pmp == null ? "" : " (via ${event.sender})"}", - style: style, + style: + style ?? + TextStyle( + color: ColorHash( + event.sender, + lightness: .7, + saturation: .7, + ).color, + fontWeight: FontWeight.bold, + ), + maxLines: 1, overflow: TextOverflow.ellipsis, ), ), diff --git a/lib/widgets/member_list.dart b/lib/widgets/member_list.dart index 6ee322e..d943f8a 100644 --- a/lib/widgets/member_list.dart +++ b/lib/widgets/member_list.dart @@ -1,14 +1,16 @@ +import "package:color_hash/color_hash.dart"; import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/members_by_status_controller.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/helpers/extensions/show_user_popover.dart"; import "package:nexus/models/configs/members_by_status_config.dart"; import "package:nexus/models/content/membership.dart"; import "package:nexus/models/membership_status.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/error_dialog.dart"; +import "package:nexus/widgets/loading.dart"; class MemberList extends HookConsumerWidget { final String roomId; @@ -63,10 +65,14 @@ class MemberList extends HookConsumerWidget { ), ], ), - membersProvider.betterWhen( - data: (members) => Expanded( + switch (membersProvider) { + AsyncError(:final error, :final stackTrace) => ErrorDialog( + error, + stackTrace, + ), + AsyncData(:final value) || AsyncLoading(:final value?) => Expanded( child: ListView( - children: members + children: value .map( (member) => switch (member.content) { MembershipContent( @@ -87,6 +93,14 @@ class MemberList extends HookConsumerWidget { title: Text( displayName ?? member.stateKey!.localpart, overflow: TextOverflow.ellipsis, + style: TextStyle( + color: ColorHash( + member.stateKey!, + lightness: .7, + saturation: .8, + ).color, + fontWeight: FontWeight.bold, + ), ), subtitle: Text( member.stateKey!, @@ -100,7 +114,8 @@ class MemberList extends HookConsumerWidget { .toList(), ), ), - ), + AsyncLoading _ => Loading(), + }, ], ), ); diff --git a/lib/widgets/renderers/event.dart b/lib/widgets/renderers/event.dart index bfccb2b..0a9cf73 100644 --- a/lib/widgets/renderers/event.dart +++ b/lib/widgets/renderers/event.dart @@ -19,6 +19,7 @@ import "package:nexus/models/content/membership.dart"; import "package:nexus/models/content/message.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/requests/get_event_request.dart"; +import "package:nexus/widgets/event_preview.dart"; import "package:nexus/widgets/expandable_image.dart"; import "package:nexus/widgets/html/html.dart"; import "package:nexus/widgets/lazy_loading/message_avatar.dart"; @@ -59,6 +60,8 @@ class EventRenderer extends ConsumerWidget { message: event.timestamp.toString(), child: Text( format(event.timestamp), + maxLines: 1, + overflow: TextOverflow.ellipsis, style: theme.textTheme.labelSmall?.copyWith(color: Colors.grey), ), ); @@ -83,6 +86,7 @@ class EventRenderer extends ConsumerWidget { ), MessageContent() || EncryptedContent() => Row( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, spacing: 8, children: [ if (!textOnly) @@ -90,7 +94,7 @@ class EventRenderer extends ConsumerWidget { SizedBox(width: 40) else MessageAvatar(event, height: 40), - Expanded( + Flexible( child: Column( spacing: 4, crossAxisAlignment: CrossAxisAlignment.start, @@ -99,16 +103,14 @@ class EventRenderer extends ConsumerWidget { Row( spacing: 4, children: [ - Flexible( - child: MessageDisplayname( - event, - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - Flexible(child: timestamp), + Flexible(child: MessageDisplayname(event)), + Flexible(flex: 0, child: timestamp), ], ), Card( + margin: textOnly + ? EdgeInsets.zero + : EdgeInsets.only(bottom: 4), color: textOnly ? Colors.transparent : ref.watch( @@ -152,32 +154,7 @@ class EventRenderer extends ConsumerWidget { AsyncData(:final value?) || AsyncLoading( :final value?, - ) => IgnorePointer( - child: Row( - spacing: 8, - children: [ - MessageAvatar(value, height: 24), - Flexible( - child: MessageDisplayname( - value, - style: TextStyle( - color: theme - .colorScheme - .primary, - fontWeight: FontWeight.bold, - ), - ), - ), - Expanded( - child: EventRenderer( - value, - textOnly: true, - maxLines: 1, - ), - ), - ], - ), - ), + ) => EventPreview(value), AsyncError _ => Text( "An error occurred while fetching the reply", style: errorStyle, @@ -233,6 +210,7 @@ class EventRenderer extends ConsumerWidget { children: [ format == MessageFormat.html && !textOnly ? Html( + roomId: event.roomId, textStyle: textStyle, formattedBody!.replaceAllMapped( RegExp( @@ -291,7 +269,7 @@ class EventRenderer extends ConsumerWidget { )) { final url? => ConstrainedBox( constraints: BoxConstraints.loose( - Size.fromWidth(500), + Size.square(500), ), child: switch (event.content) { VideoMessageContent( @@ -425,14 +403,8 @@ class EventRenderer extends ConsumerWidget { padding: EdgeInsets.symmetric(horizontal: 4), child: Icon(Icons.numbers), ), - MessageDisplayname( - event, - style: TextStyle( - color: theme.colorScheme.primary, - fontWeight: FontWeight.bold, - ), - ), - Text("changed the room avatar"), + Flexible(child: MessageDisplayname(event)), + Expanded(child: Text("changed the room avatar")), ], ), _ => null, diff --git a/lib/widgets/renderers/membership.dart b/lib/widgets/renderers/membership.dart index 5330fea..61dc582 100644 --- a/lib/widgets/renderers/membership.dart +++ b/lib/widgets/renderers/membership.dart @@ -19,43 +19,54 @@ class MembershipRenderer extends StatelessWidget { return switch (event.content) { MembershipContent content => Row( - spacing: 4, + spacing: 8, children: [ Padding( padding: EdgeInsets.symmetric(horizontal: 4), child: Icon(Icons.people), ), - InkWell( - onTapUp: (details) => context.showUserPopover( - content, - event.stateKey!, - globalPosition: details.globalPosition, - ), - child: Text( - content.displayName ?? event.stateKey!.localpart, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, - ), + Expanded( + child: Wrap( + spacing: 4, + children: [ + InkWell( + onTapUp: (details) => context.showUserPopover( + content, + event.stateKey!, + globalPosition: details.globalPosition, + ), + child: Text( + overflow: TextOverflow.ellipsis, + content.displayName ?? event.stateKey!.localpart, + maxLines: 1, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + ), + Text( + overflow: TextOverflow.ellipsis, + maxLines: 1, + "${switch (content.status) { + MembershipStatus.invite => "was invited to", + MembershipStatus.join => "joined", + MembershipStatus.leave => event.sender == event.stateKey ? "left" : (event.unsigned["prev_content"]?["membership"] == "ban" ? "was unbanned from" : "was kicked from"), + MembershipStatus.ban => "was banned from", + MembershipStatus.knock => "asked to join", + }} the room${content.reason == null ? "" : "because ${content.reason}"}${event.sender == event.stateKey ? "" : " by "}", + ), + if (event.sender != event.stateKey) + MessageDisplayname( + event, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + ], ), ), - Text( - "${switch (content.status) { - MembershipStatus.invite => "was invited to", - MembershipStatus.join => "joined", - MembershipStatus.leave => event.sender == event.stateKey ? "left" : (event.unsigned["prev_content"]?["membership"] == "ban" ? "was unbanned from" : "was kicked from"), - MembershipStatus.ban => "was banned from", - MembershipStatus.knock => "asked to join", - }} the room${content.reason == null ? "" : "because ${content.reason}"}${event.sender == event.stateKey ? "" : " by "}", - ), - if (event.sender != event.stateKey) - MessageDisplayname( - event, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, - ), - ), ], ), _ => SizedBox.shrink(), diff --git a/pubspec.lock b/pubspec.lock index 6d3aa22..df05714 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -73,6 +73,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + boxy: + dependency: "direct main" + description: + name: boxy + sha256: "42ccafe13b2893878042acc5b7e2446025328e11a3197b0bb78db42ff76aa3f0" + url: "https://pub.dev" + source: hosted + version: "2.3.0" build: dependency: transitive description: