diff --git a/README.md b/README.md index a603886..b97e9e7 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. - [ ] Notifications using UnifiedPush ([#35](https://git.federated.nexus/Nexus/nexus/issues/35)) - [ ] Group calls using [MSC4195](https://github.com/matrix-org/matrix-spec-proposals/pull/4195) - [ ] Invites -- [ ] Settings ([#37](https://git.federated.nexus/Nexus/nexus/issues/37)) +- [ ] Settings - [ ] Matrix: URIs vs Matrix.to links - [ ] Light/Dark mode - [ ] Remote Gomuks instance diff --git a/lib/helpers/extensions/show_user_popover.dart b/lib/helpers/extensions/show_user_popover.dart index 1ea3015..e703721 100644 --- a/lib/helpers/extensions/show_user_popover.dart +++ b/lib/helpers/extensions/show_user_popover.dart @@ -1,18 +1,25 @@ import "package:flutter/material.dart"; +import "package:nexus/helpers/extensions/show_context_menu.dart"; import "package:nexus/models/content/membership.dart"; -import "package:nexus/widgets/user_bottom_sheet.dart"; +import "package:nexus/widgets/user_popover.dart"; extension ShowUserPopover on BuildContext { void showUserPopover( MembershipContent member, String userId, { String? roomId, - }) => showModalBottomSheet( - constraints: BoxConstraints.loose( - Size(500, View.of(this).physicalSize.height - 80), - ), - isScrollControlled: true, - context: this, - builder: (context) => UserBottomSheet(member, userId, roomId: roomId), + required Offset globalPosition, + }) => showContextMenu( + globalPosition: globalPosition, + children: [ + PopupMenuItem( + enabled: false, + padding: .symmetric(horizontal: 16, vertical: 8), + child: IconTheme( + data: .new(), + child: UserPopover(member, userId, roomId: roomId), + ), + ), + ], ); } diff --git a/lib/widgets/avatar_or_hash.dart b/lib/widgets/avatar_or_hash.dart index 20f4eac..4931684 100644 --- a/lib/widgets/avatar_or_hash.dart +++ b/lib/widgets/avatar_or_hash.dart @@ -24,7 +24,7 @@ class AvatarOrHash extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final box = ColoredBox( color: ColorHash(title).color, - child: Center(child: Icon(Icons.person, size: height / 2)), + child: Center(child: Text(title.isEmpty ? "" : title[0])), ); final parsedAvatar = avatar?.mxcToHttps( diff --git a/lib/widgets/highlight_wrapper.dart b/lib/widgets/flash_wrapper.dart similarity index 61% rename from lib/widgets/highlight_wrapper.dart rename to lib/widgets/flash_wrapper.dart index 920db9e..057565e 100644 --- a/lib/widgets/highlight_wrapper.dart +++ b/lib/widgets/flash_wrapper.dart @@ -1,16 +1,16 @@ import "package:flutter/material.dart"; -class HighlightWrapper extends StatelessWidget { +class FlashWrapper extends StatelessWidget { final Widget child; - final bool isHighlighted; - const HighlightWrapper(this.child, {this.isHighlighted = false, super.key}); + final bool isFlashing; + const FlashWrapper(this.child, {this.isFlashing = false, super.key}); @override Widget build(BuildContext context) => ClipRRect( borderRadius: .all(.circular(12)), child: AnimatedContainer( - padding: isHighlighted ? .all(8) : .all(0), - color: isHighlighted + padding: isFlashing ? .all(8) : .all(0), + color: isFlashing ? Theme.of(context).colorScheme.onSurface.withAlpha(50) : Colors.transparent, duration: .new(milliseconds: 250), diff --git a/lib/widgets/html/mention_chip.dart b/lib/widgets/html/mention_chip.dart index 8e0ce11..c757c3f 100644 --- a/lib/widgets/html/mention_chip.dart +++ b/lib/widgets/html/mention_chip.dart @@ -23,9 +23,14 @@ class MentionChip extends ConsumerWidget { return mention == null ? SizedBox.shrink() : InkWell( - onTap: () { + onTapUp: (details) { if (membership != null) { - context.showUserPopover(membership, mention, roomId: roomId); + context.showUserPopover( + membership, + mention, + roomId: roomId, + globalPosition: details.globalPosition, + ); } }, child: IgnorePointer( diff --git a/lib/widgets/lazy_loading/message_avatar.dart b/lib/widgets/lazy_loading/message_avatar.dart index b65e377..e7c7a77 100644 --- a/lib/widgets/lazy_loading/message_avatar.dart +++ b/lib/widgets/lazy_loading/message_avatar.dart @@ -12,18 +12,21 @@ class MessageAvatar extends ConsumerWidget { const MessageAvatar(this.event, {this.height = 24, super.key}); @override - Widget build(BuildContext context, WidgetRef ref) => switch (ref.watch( - AuthorController.provider(event), - )) { - AsyncData(:final value) || AsyncLoading(:final value?) => InkWell( - onTap: () => - context.showUserPopover(value, event.sender, roomId: event.roomId), - child: AvatarOrHash( - value.avatarUrl, - value.displayName ?? event.sender.localpart, - height: height, - ), - ), - _ => AvatarOrHash(null, event.sender.localpart, height: height), - }; + Widget build(BuildContext context, WidgetRef ref) => + switch (ref.watch(AuthorController.provider(event))) { + AsyncData(:final value) || AsyncLoading(:final value?) => InkWell( + onTapUp: (details) => context.showUserPopover( + value, + event.sender, + roomId: event.roomId, + globalPosition: details.globalPosition, + ), + child: AvatarOrHash( + value.avatarUrl, + value.displayName ?? event.sender.localpart, + height: height, + ), + ), + _ => AvatarOrHash(null, event.sender.localpart, height: height), + }; } diff --git a/lib/widgets/lazy_loading/message_displayname.dart b/lib/widgets/lazy_loading/message_displayname.dart index a817122..ffa5dc0 100644 --- a/lib/widgets/lazy_loading/message_displayname.dart +++ b/lib/widgets/lazy_loading/message_displayname.dart @@ -22,11 +22,12 @@ class MessageDisplayname extends ConsumerWidget { AuthorController.provider(event), )) { AsyncData(:final value) || AsyncLoading(:final value?) => InkWell( - onTap: clickable - ? () => context.showUserPopover( + onTapUp: clickable + ? (details) => context.showUserPopover( value, event.sender, roomId: event.roomId, + globalPosition: details.globalPosition, ) : null, child: Wrap( diff --git a/lib/widgets/member_list.dart b/lib/widgets/member_list.dart index 6326421..d3c21b6 100644 --- a/lib/widgets/member_list.dart +++ b/lib/widgets/member_list.dart @@ -14,7 +14,6 @@ import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/divider_text.dart"; import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/loading.dart"; -import "package:nexus/widgets/user_bottom_sheet.dart"; class MemberList extends HookConsumerWidget { final String roomId; @@ -139,11 +138,8 @@ class MemberList extends HookConsumerWidget { ), leading: AvatarOrHash( avatarUrl, - height: 36, displayName ?? - members[index] - .stateKey! - .localpart, + members[index].sender.localpart, ), ), _ => throw Exception( @@ -151,25 +147,12 @@ class MemberList extends HookConsumerWidget { ), }, onTap: (index) { - final member = members[index]; - if (member.content - case MembershipContent content) { - showModalBottomSheet( - constraints: BoxConstraints.loose( - Size( - 500, - (context.size?.height ?? 1000) - 80, - ), - ), - isScrollControlled: true, - context: context, - builder: (context) => UserBottomSheet( - content, - member.stateKey!, - roomId: roomId, - ), - ); - } + // context.showUserPopover( + // member.content as MembershipContent, + // member.stateKey!, + // roomId: roomId, + // globalPosition: details.globalPosition, + // ), }, ), ], diff --git a/lib/widgets/renderers/event.dart b/lib/widgets/renderers/event.dart index bcca961..a4f659c 100644 --- a/lib/widgets/renderers/event.dart +++ b/lib/widgets/renderers/event.dart @@ -1,7 +1,6 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/helpers/extensions/show_context_menu.dart"; import "package:nexus/models/content/avatar.dart"; import "package:nexus/models/content/canonical_alias.dart"; @@ -25,7 +24,7 @@ import "package:nexus/widgets/reaction_row.dart"; import "package:nexus/widgets/renderers/membership.dart"; import "package:nexus/widgets/renderers/generic_event.dart"; -class EventRenderer extends HookConsumerWidget { +class EventRenderer extends ConsumerWidget { final Event event; final bool textOnly; final bool isGrouped; @@ -47,8 +46,6 @@ class EventRenderer extends HookConsumerWidget { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final errorStyle = TextStyle(color: colorScheme.error); - final focusNode = useFocusNode(); - useListenable(focusNode); final child = event.redactedBy != null || event.relationType == "m.replace" ? null @@ -171,44 +168,12 @@ class EventRenderer extends HookConsumerWidget { if (textOnly) child else ...[ - Builder( - builder: (context) => FocusableActionDetector( - focusNode: focusNode, - actions: contextMenuCallback == null - ? null - : { - ActivateIntent: CallbackAction( - onInvoke: (_) { - final renderBox = - context.findRenderObject() as RenderBox; - final topLeft = renderBox.localToGlobal( - Offset.zero, - ); - context.showContextMenu( - globalPosition: topLeft, - children: getEventOptions!(event).toList(), - ); - return null; - }, - ), - }, - child: Container( - decoration: BoxDecoration( - color: focusNode.hasPrimaryFocus - ? theme.colorScheme.surfaceContainerHighest - : null, - ), - child: GestureDetector( - onSecondaryTapUp: contextMenuCallback, - onLongPressStart: contextMenuCallback, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: 8, - ).copyWith(top: isGrouped ? 0 : 8), - child: child, - ), - ), - ), + GestureDetector( + onSecondaryTapUp: contextMenuCallback, + onLongPressStart: contextMenuCallback, + child: Padding( + padding: isGrouped ? .zero : .only(top: 8), + child: child, ), ), diff --git a/lib/widgets/renderers/membership.dart b/lib/widgets/renderers/membership.dart index b2835b4..c8c91ba 100644 --- a/lib/widgets/renderers/membership.dart +++ b/lib/widgets/renderers/membership.dart @@ -21,10 +21,11 @@ class MembershipRenderer extends StatelessWidget { return switch (event.content) { MembershipContent content => GenericEventRenderer(Icons.people, [ InkWell( - onTap: () => context.showUserPopover( + onTapUp: (details) => context.showUserPopover( content, event.stateKey!, roomId: event.roomId, + globalPosition: details.globalPosition, ), child: Text( overflow: .ellipsis, diff --git a/lib/widgets/room_chat.dart b/lib/widgets/room_chat.dart index 2817172..2ab2cf7 100644 --- a/lib/widgets/room_chat.dart +++ b/lib/widgets/room_chat.dart @@ -20,7 +20,7 @@ import "package:nexus/widgets/emoji_picker_button.dart"; import "package:nexus/widgets/renderers/event.dart"; import "package:nexus/widgets/member_list.dart"; import "package:nexus/widgets/room_appbar.dart"; -import "package:nexus/widgets/highlight_wrapper.dart"; +import "package:nexus/widgets/flash_wrapper.dart"; import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/main.dart"; import "package:nexus/widgets/loading.dart"; @@ -41,7 +41,7 @@ class RoomChat extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final relatedEvent = useState(null); final relationType = useState(RelationType.reply); - final highlightedEvent = useState(null); + final flashingEvent = useState(null); final composerSize = useState(64); @@ -400,7 +400,7 @@ class RoomChat extends HookConsumerWidget { children: [ Positioned.fill( child: Padding( - padding: .symmetric(horizontal: 4), + padding: .symmetric(horizontal: 12), child: switch (controllerData) { AsyncData(:final value?) || AsyncLoading(:final value?) => CustomScrollView( @@ -426,7 +426,7 @@ class RoomChat extends HookConsumerWidget { itemBuilder: (_, index) { final event = value[index]; final previousEvent = value.getOrNull(index - 1); - return HighlightWrapper( + return FlashWrapper( EventRenderer( event, onTapReply: () async { @@ -440,10 +440,10 @@ class RoomChat extends HookConsumerWidget { duration: (_) => .new(milliseconds: 700), curve: (_) => Curves.easeInOut, ); - highlightedEvent.value = replyId; + flashingEvent.value = replyId; await Future.delayed(.new(seconds: 1), () { - if (highlightedEvent.value == replyId) { - highlightedEvent.value = null; + if (flashingEvent.value == replyId) { + flashingEvent.value = null; } }); }, @@ -457,8 +457,8 @@ class RoomChat extends HookConsumerWidget { "${event.sender}${event.pmp?.id}" == "${previousEvent?.sender}${previousEvent?.pmp?.id}", ), - isHighlighted: - highlightedEvent.value == event.eventId, + isFlashing: + flashingEvent.value == event.eventId, ); }, ), diff --git a/lib/widgets/sidebar.dart b/lib/widgets/sidebar.dart index 56f8d57..591e25f 100644 --- a/lib/widgets/sidebar.dart +++ b/lib/widgets/sidebar.dart @@ -132,7 +132,6 @@ class Sidebar extends HookConsumerWidget { }, short: true, icon: AvatarOrHash( - height: 28, space.room?.metadata?.avatar, fallback: space.icon == null ? null @@ -210,11 +209,7 @@ class Sidebar extends HookConsumerWidget { actions: [ RoomMenu( selectedSpace.room, - children: selectedSpace.children.addAll( - selectedSpace.subSpaces - .map((element) => element.children) - .flattened, - ), + children: selectedSpace.children, ), ], ), diff --git a/lib/widgets/user_bottom_sheet.dart b/lib/widgets/user_bottom_sheet.dart deleted file mode 100644 index d422e01..0000000 --- a/lib/widgets/user_bottom_sheet.dart +++ /dev/null @@ -1,264 +0,0 @@ -import "package:collection/collection.dart"; -import "package:flutter/material.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:intl/intl.dart"; -import "package:m3e_buttons/m3e_buttons.dart"; -import "package:nexus/controllers/client_controller.dart"; -import "package:nexus/controllers/client_state_controller.dart"; -import "package:nexus/controllers/power_level_controller.dart"; -import "package:nexus/controllers/profile_controller.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/helpers/extensions/get_localpart.dart"; -import "package:nexus/helpers/extensions/mxc_to_https.dart"; -import "package:nexus/models/content/membership.dart"; -import "package:nexus/models/requests/membership_action.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; -import "package:nexus/main.dart"; -import "package:nexus/widgets/expandable_image.dart"; - -class UserBottomSheet extends ConsumerWidget { - final MembershipContent member; - final String userId; - final String? roomId; - const UserBottomSheet(this.member, this.userId, {this.roomId, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context); - final textTheme = theme.textTheme; - final client = ref.watch(ClientController.provider.notifier); - - void showMembershipDialog(MembershipAction action) => showDialog( - context: context, - builder: (context) => HookBuilder( - builder: (context) { - final actionReasonController = useTextEditingController(); - return AlertDialog( - title: Text("${toBeginningOfSentenceCase(action.name)} $userId"), - content: Column( - mainAxisSize: .min, - crossAxisAlignment: .start, - children: [ - Text("Are you sure you want to ${action.name} $userId?"), - SizedBox(height: 12), - TextField( - textCapitalization: .sentences, - controller: actionReasonController, - decoration: .new( - labelText: "Reason for ${action.name} (optional)", - ), - ), - ], - ), - actions: [ - TextButton( - onPressed: Navigator.of(context).pop, - child: Text("Cancel"), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - client - .setMembership( - .new( - userId: userId, - roomId: roomId!, - action: action, - reason: actionReasonController.text, - ), - ) - .onError(showError); - }, - child: Text(toBeginningOfSentenceCase(action.name)), - ), - ], - ); - }, - ), - ); - - return Padding( - padding: .all(42), - child: Column( - spacing: 4, - mainAxisSize: .min, - crossAxisAlignment: .center, - children: [ - Row( - mainAxisAlignment: .end, - children: [ - M3EButton( - onPressed: Navigator.of(context).pop, - child: Icon(Icons.close), - ), - ], - ), - SizedBox(height: 18), - - ExpandableImage( - member.avatarUrl - ?.mxcToHttps( - ref.watch( - ClientStateController.provider.select( - (value) => value!.homeserverUrl!, - ), - ), - ) - .toString(), - child: AvatarOrHash( - member.avatarUrl, - member.displayName ?? userId.localpart, - height: 200, - ), - ), - - SizedBox(height: 8), - - SelectableText( - member.displayName ?? userId.localpart, - style: textTheme.headlineLarge, - textAlign: .center, - ), - SelectableText( - userId, - textAlign: .center, - style: textTheme.titleSmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - - ref - .watch(ProfileController.provider(userId)) - .betterWhen( - loading: () => Text(""), - data: (profile) => Column( - children: [ - if (profile.timezone == null && profile.pronouns.isEmpty) - Text(""), - Wrap( - crossAxisAlignment: .center, - alignment: .center, - spacing: 4, - runSpacing: 4, - children: [ - ...profile.pronouns - .where( - // TODO: Check system language (l10n) - (pronoun) => pronoun.language == "en", - ) - .mapIndexed( - (index, pronoun) => [ - if (index != 0) - Icon( - Icons.circle, - size: 4, - color: theme.colorScheme.onSurfaceVariant, - ), - Text( - pronoun.summary, - textAlign: .center, - style: textTheme.titleSmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ], - ) - .flattened, - - if (profile.timezone != null) ...[ - if (profile.pronouns.isNotEmpty) - SizedBox( - height: 16, - child: VerticalDivider( - thickness: 1.5, - width: 4, - color: theme.colorScheme.onSurfaceVariant, - ), - ), - Text( - profile.timezone!, - textAlign: .center, - style: textTheme.titleSmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ], - ], - ), - ], - ), - ), - - SizedBox(height: 8), - if (userId != ref.watch(ClientStateController.provider)?.userId && - roomId != null) ...[ - Row( - children: [ - Expanded( - child: M3EButton.icon( - onPressed: null, - shape: .square, - style: .tonal, - icon: Icon(Icons.message), - label: Text("Message"), - ), - ), - ], - ), - - if (ref.watch( - PowerLevelController.provider( - .membershipAction( - action: .kick, - roomId: roomId!, - targetUser: userId, - ), - ), - ) && - member.status == .join || - member.status == .invite) - Padding( - padding: .only(top: 4), - child: Row( - mainAxisSize: MainAxisSize.max, - spacing: 8, - children: [ - M3EButton.icon( - onPressed: () => showMembershipDialog(.kick), - shape: .square, - icon: Icon(Icons.sports_martial_arts), - label: Text("Kick"), - decoration: .new( - backgroundColor: WidgetStatePropertyAll( - theme.colorScheme.error, - ), - foregroundColor: WidgetStatePropertyAll( - theme.colorScheme.onError, - ), - ), - ), - - M3EButton.icon( - onPressed: () => showMembershipDialog(.ban), - shape: .square, - icon: Icon(Icons.gavel), - label: Text("Ban"), - decoration: .new( - backgroundColor: WidgetStatePropertyAll( - theme.colorScheme.errorContainer, - ), - foregroundColor: WidgetStatePropertyAll( - theme.colorScheme.onErrorContainer, - ), - ), - ), - ].map((e) => Expanded(child: e)).toList(), - ), - ), - ], - ], - ), - ); - } -} diff --git a/lib/widgets/user_popover.dart b/lib/widgets/user_popover.dart new file mode 100644 index 0000000..97d88c4 --- /dev/null +++ b/lib/widgets/user_popover.dart @@ -0,0 +1,224 @@ +import "package:flutter/material.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:intl/intl.dart"; +import "package:nexus/controllers/client_controller.dart"; +import "package:nexus/controllers/client_state_controller.dart"; +import "package:nexus/controllers/power_level_controller.dart"; +import "package:nexus/controllers/profile_controller.dart"; +import "package:nexus/helpers/extensions/better_when.dart"; +import "package:nexus/helpers/extensions/get_localpart.dart"; +import "package:nexus/helpers/extensions/mxc_to_https.dart"; +import "package:nexus/models/content/membership.dart"; +import "package:nexus/models/requests/membership_action.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/main.dart"; +import "package:nexus/widgets/expandable_image.dart"; + +class UserPopover extends ConsumerWidget { + final MembershipContent member; + final String userId; + final String? roomId; + const UserPopover(this.member, this.userId, {this.roomId, super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final client = ref.watch(ClientController.provider.notifier); + + void showMembershipDialog(MembershipAction action) => showDialog( + context: context, + builder: (context) => HookBuilder( + builder: (context) { + final actionReasonController = useTextEditingController(); + return AlertDialog( + title: Text("${toBeginningOfSentenceCase(action.name)} $userId"), + content: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Text("Are you sure you want to ${action.name} $userId?"), + SizedBox(height: 12), + TextField( + textCapitalization: .sentences, + controller: actionReasonController, + decoration: .new( + labelText: "Reason for ${action.name} (optional)", + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: Text("Cancel"), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + client + .setMembership( + .new( + userId: userId, + roomId: roomId!, + action: action, + reason: actionReasonController.text, + ), + ) + .onError(showError); + }, + child: Text(toBeginningOfSentenceCase(action.name)), + ), + ], + ); + }, + ), + ); + + final actionButton = ButtonStyle( + padding: WidgetStatePropertyAll(.symmetric(horizontal: 24, vertical: 18)), + shape: WidgetStatePropertyAll( + RoundedRectangleBorder(borderRadius: .circular(8)), + ), + ); + + return Column( + spacing: 16, + crossAxisAlignment: .stretch, + children: [ + Wrap( + alignment: .center, + spacing: 16, + runSpacing: 8, + children: [ + ExpandableImage( + member.avatarUrl + ?.mxcToHttps( + ref.watch( + ClientStateController.provider.select( + (value) => value!.homeserverUrl!, + ), + ), + ) + .toString(), + child: AvatarOrHash( + member.avatarUrl, + member.displayName ?? userId.localpart, + height: 80, + ), + ), + Column( + children: [ + SelectableText( + member.displayName ?? userId.localpart, + style: textTheme.headlineSmall, + ), + SelectableText(userId, style: textTheme.titleSmall), + SizedBox(height: 4), + ref + .watch(ProfileController.provider(userId)) + .betterWhen( + loading: SizedBox.shrink, + data: (profile) => Wrap( + alignment: .center, + spacing: 4, + runSpacing: 4, + children: [ + for (final pronoun in profile.pronouns.where( + (pronoun) => pronoun.language == "en", + )) + Chip( + label: Text(pronoun.summary), + labelStyle: .new( + color: theme.colorScheme.onPrimary, + ), + color: WidgetStatePropertyAll( + theme.colorScheme.primary, + ), + ), + if (profile.timezone != null) + Chip( + label: Text(profile.timezone!), + labelStyle: .new( + color: theme.colorScheme.onPrimary, + ), + color: WidgetStatePropertyAll( + theme.colorScheme.primary, + ), + ), + ], + ), + ), + ], + ), + ], + ), + if (userId != ref.watch(ClientStateController.provider)?.userId && + roomId != null) + Wrap( + alignment: .center, + spacing: 8, + runSpacing: 8, + children: [ + FilledButton.icon( + onPressed: null, + label: Text("Message"), + icon: Icon(Icons.message), + style: actionButton, + ), + + if (ref.watch( + PowerLevelController.provider( + .membershipAction( + action: .kick, + roomId: roomId!, + targetUser: userId, + ), + ), + ) && + member.status == .join || + member.status == .invite) + FilledButton.icon( + onPressed: () => showMembershipDialog(.kick), + label: Text("Kick"), + style: actionButton.copyWith( + backgroundColor: WidgetStatePropertyAll( + theme.colorScheme.error, + ), + foregroundColor: WidgetStatePropertyAll( + theme.colorScheme.onError, + ), + ), + icon: Icon(Icons.sports_martial_arts), + ), + if (ref.watch( + PowerLevelController.provider( + .membershipAction( + roomId: roomId!, + action: .ban, + targetUser: userId, + ), + ), + )) + ElevatedButton.icon( + onPressed: () => showMembershipDialog( + member.status == .ban ? .unban : .ban, + ), + icon: Icon(Icons.gavel), + label: Text(member.status == .ban ? "Unban" : "Ban"), + style: actionButton.copyWith( + backgroundColor: WidgetStatePropertyAll( + theme.colorScheme.errorContainer, + ), + foregroundColor: WidgetStatePropertyAll( + theme.colorScheme.onErrorContainer, + ), + ), + ), + ], + ), + ], + ); + } +}