From 924e4bce2388ac2cc27366f42be399400b3ab5be Mon Sep 17 00:00:00 2001 From: krille-chan Date: Fri, 11 Aug 2023 07:55:15 +0200 Subject: [PATCH] design: Nicer user bottom sheet --- assets/l10n/intl_de.arb | 3 +- assets/l10n/intl_en.arb | 3 +- lib/pages/chat_list/chat_list_body.dart | 7 +- .../user_bottom_sheet/user_bottom_sheet.dart | 88 +++++++++-- .../user_bottom_sheet_view.dart | 148 +++++++++++++----- lib/utils/url_launcher.dart | 4 +- lib/widgets/profile_bottom_sheet.dart | 99 ------------ 7 files changed, 192 insertions(+), 160 deletions(-) delete mode 100644 lib/widgets/profile_bottom_sheet.dart diff --git a/assets/l10n/intl_de.arb b/assets/l10n/intl_de.arb index a81b21504..93d6c4185 100644 --- a/assets/l10n/intl_de.arb +++ b/assets/l10n/intl_de.arb @@ -2540,5 +2540,6 @@ "replace": "Ersetzen", "@replace": {}, "sendTypingNotifications": "Tippbenachrichtigungen senden", - "@sendTypingNotifications": {} + "@sendTypingNotifications": {}, + "profileNotFound": "Der Benutzer konnte auf dem Server nicht gefunden werden. Vielleicht gibt es ein Verbindungsproblem oder der Benutzer existiert nicht." } diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index c71821a07..25037b60c 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2500,5 +2500,6 @@ "placeholders": { "provider": {} } - } + }, + "profileNotFound": "The user could not be found on the server. Maybe there is a connection problem or the user doesn't exist." } diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index 53b48a0c9..69b438e8c 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -10,12 +10,12 @@ import 'package:fluffychat/pages/chat_list/chat_list_item.dart'; import 'package:fluffychat/pages/chat_list/search_title.dart'; import 'package:fluffychat/pages/chat_list/space_view.dart'; import 'package:fluffychat/pages/chat_list/stories_header.dart'; +import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/avatar.dart'; -import 'package:fluffychat/widgets/profile_bottom_sheet.dart'; import 'package:fluffychat/widgets/public_room_bottom_sheet.dart'; import '../../config/themes.dart'; import '../../widgets/connection_status_header.dart'; @@ -150,9 +150,8 @@ class ChatListViewBody extends StatelessWidget { userSearchResult.results[i].avatarUrl, onPressed: () => showAdaptiveBottomSheet( context: context, - builder: (c) => ProfileBottomSheet( - userId: - userSearchResult.results[i].userId, + builder: (c) => UserBottomSheet( + profile: userSearchResult.results[i], outerContext: context, ), ), diff --git a/lib/pages/user_bottom_sheet/user_bottom_sheet.dart b/lib/pages/user_bottom_sheet/user_bottom_sheet.dart index 4fd0cc231..270681086 100644 --- a/lib/pages/user_bottom_sheet/user_bottom_sheet.dart +++ b/lib/pages/user_bottom_sheet/user_bottom_sheet.dart @@ -21,17 +21,66 @@ enum UserBottomSheetAction { ignore, } +class LoadProfileBottomSheet extends StatelessWidget { + final String userId; + final BuildContext outerContext; + + const LoadProfileBottomSheet({ + super.key, + required this.userId, + required this.outerContext, + }); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: Matrix.of(context) + .client + .getUserProfile(userId) + .timeout(const Duration(seconds: 3)), + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return Scaffold( + appBar: AppBar( + leading: CloseButton( + onPressed: Navigator.of(context, rootNavigator: false).pop, + ), + ), + body: const Center( + child: CircularProgressIndicator.adaptive(), + ), + ); + } + return UserBottomSheet( + outerContext: outerContext, + profile: Profile( + userId: userId, + avatarUrl: snapshot.data?.avatarUrl, + displayName: snapshot.data?.displayname, + ), + profileSearchError: snapshot.error, + ); + }, + ); + } +} + class UserBottomSheet extends StatefulWidget { - final User user; + final User? user; + final Profile? profile; final Function? onMention; final BuildContext outerContext; + final Object? profileSearchError; const UserBottomSheet({ Key? key, - required this.user, + this.user, + this.profile, required this.outerContext, this.onMention, - }) : super(key: key); + this.profileSearchError, + }) : assert(user != null || profile != null), + super(key: key); @override UserBottomSheetController createState() => UserBottomSheetController(); @@ -39,6 +88,9 @@ class UserBottomSheet extends StatefulWidget { class UserBottomSheetController extends State { void participantAction(UserBottomSheetAction action) async { + final user = widget.user; + final userId = user?.id ?? widget.profile?.userId; + if (userId == null) throw ('user or profile must not be null!'); // ignore: prefer_function_declarations_over_variables final Function askConfirmation = () async => (await showOkCancelAlertDialog( useRootNavigator: false, @@ -50,7 +102,8 @@ class UserBottomSheetController extends State { OkCancelResult.ok); switch (action) { case UserBottomSheetAction.report: - final event = widget.user; + if (user == null) throw ('User must not be null for this action!'); + final score = await showConfirmationDialog( context: context, title: L10n.of(context)!.reportUser, @@ -85,8 +138,8 @@ class UserBottomSheetController extends State { final result = await showFutureLoadingDialog( context: context, future: () => Matrix.of(context).client.reportContent( - event.roomId!, - event.eventId, + user.roomId!, + user.eventId, reason: reason.single, score: score, ), @@ -97,46 +150,51 @@ class UserBottomSheetController extends State { ); break; case UserBottomSheetAction.mention: + if (user == null) throw ('User must not be null for this action!'); Navigator.of(context, rootNavigator: false).pop(); widget.onMention!(); break; case UserBottomSheetAction.ban: + if (user == null) throw ('User must not be null for this action!'); if (await askConfirmation()) { await showFutureLoadingDialog( context: context, - future: () => widget.user.ban(), + future: () => user.ban(), ); Navigator.of(context, rootNavigator: false).pop(); } break; case UserBottomSheetAction.unban: + if (user == null) throw ('User must not be null for this action!'); if (await askConfirmation()) { await showFutureLoadingDialog( context: context, - future: () => widget.user.unban(), + future: () => user.unban(), ); Navigator.of(context, rootNavigator: false).pop(); } break; case UserBottomSheetAction.kick: + if (user == null) throw ('User must not be null for this action!'); if (await askConfirmation()) { await showFutureLoadingDialog( context: context, - future: () => widget.user.kick(), + future: () => user.kick(), ); Navigator.of(context, rootNavigator: false).pop(); } break; case UserBottomSheetAction.permission: + if (user == null) throw ('User must not be null for this action!'); final newPermission = await showPermissionChooser( context, - currentLevel: widget.user.powerLevel, + currentLevel: user.powerLevel, ); if (newPermission != null) { if (newPermission == 100 && await askConfirmation() == false) break; await showFutureLoadingDialog( context: context, - future: () => widget.user.setPower(newPermission), + future: () => user.setPower(newPermission), ); Navigator.of(context, rootNavigator: false).pop(); } @@ -144,7 +202,9 @@ class UserBottomSheetController extends State { case UserBottomSheetAction.message: final roomIdResult = await showFutureLoadingDialog( context: context, - future: () => widget.user.startDirectChat(), + future: () => Matrix.of(context) + .client + .startDirectChat(user?.id ?? widget.profile!.userId), ); if (roomIdResult.error != null) return; widget.outerContext.go(['', 'rooms', roomIdResult.result!].join('/')); @@ -154,7 +214,9 @@ class UserBottomSheetController extends State { if (await askConfirmation()) { await showFutureLoadingDialog( context: context, - future: () => Matrix.of(context).client.ignoreUser(widget.user.id), + future: () => Matrix.of(context) + .client + .ignoreUser(user?.id ?? widget.profile!.userId), ); } } diff --git a/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart b/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart index 420b411d3..12e0525cc 100644 --- a/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart +++ b/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart @@ -5,7 +5,6 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/widgets/avatar.dart'; -import '../../utils/matrix_sdk_extensions/presence_extension.dart'; import '../../widgets/matrix.dart'; import 'user_bottom_sheet.dart'; @@ -17,24 +16,38 @@ class UserBottomSheetView extends StatelessWidget { @override Widget build(BuildContext context) { final user = controller.widget.user; + final userId = (user?.id ?? controller.widget.profile?.userId)!; + final displayname = (user?.calcDisplayname() ?? + controller.widget.profile?.displayName ?? + controller.widget.profile?.userId.localpart)!; + final avatarUrl = user?.avatarUrl ?? controller.widget.profile?.avatarUrl; + final client = Matrix.of(context).client; - final presence = client.presences[user.id]; + final profileSearchError = controller.widget.profileSearchError; return SafeArea( child: Scaffold( appBar: AppBar( leading: CloseButton( onPressed: Navigator.of(context, rootNavigator: false).pop, ), - title: Text(user.calcDisplayname()), actions: [ - if (user.id != client.userID) + if (userId != client.userID && + !client.ignoredUsers.contains(userId)) Padding( padding: const EdgeInsets.all(8.0), child: OutlinedButton.icon( + label: Text( + L10n.of(context)!.ignore, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ), + icon: Icon( + Icons.shield_outlined, + color: Theme.of(context).colorScheme.error, + ), onPressed: () => controller - .participantAction(UserBottomSheetAction.message), - icon: const Icon(Icons.forum_outlined), - label: Text(L10n.of(context)!.sendAMessage), + .participantAction(UserBottomSheetAction.ignore), ), ), ], @@ -45,31 +58,81 @@ class UserBottomSheetView extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.all(16.0), - child: Avatar( - mxContent: user.avatarUrl, - name: user.calcDisplayname(), - size: Avatar.defaultSize * 2, - fontSize: 24, + child: Material( + elevation: + Theme.of(context).appBarTheme.scrolledUnderElevation ?? + 4, + shadowColor: Theme.of(context).appBarTheme.shadowColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).dividerColor, + ), + borderRadius: BorderRadius.circular( + Avatar.defaultSize * 2.5, + ), + ), + child: Avatar( + mxContent: avatarUrl, + name: displayname, + size: Avatar.defaultSize * 2.5, + fontSize: 18 * 2.5, + ), ), ), Expanded( - child: ListTile( - contentPadding: const EdgeInsets.only(right: 16.0), - title: Text(user.id), - subtitle: presence == null - ? null - : Text(presence.getLocalizedLastActiveAgo(context)), - trailing: IconButton( - icon: Icon(Icons.adaptive.share), - onPressed: () => FluffyShare.share( - user.id, - context, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextButton.icon( + onPressed: () => FluffyShare.share(userId, context), + icon: Icon( + Icons.adaptive.share_outlined, + size: 16, + ), + style: TextButton.styleFrom( + foregroundColor: + Theme.of(context).colorScheme.onBackground, + ), + label: Text( + displayname, + maxLines: 1, + overflow: TextOverflow.ellipsis, + // style: const TextStyle(fontSize: 18), + ), ), - ), + TextButton.icon( + onPressed: () => FluffyShare.share(userId, context), + icon: const Icon( + Icons.copy_outlined, + size: 14, + ), + style: TextButton.styleFrom( + foregroundColor: + Theme.of(context).colorScheme.secondary, + ), + label: Text( + userId, + maxLines: 1, + overflow: TextOverflow.ellipsis, + // style: const TextStyle(fontSize: 12), + ), + ), + ], ), ), ], ), + if (userId != client.userID) + Padding( + padding: const EdgeInsets.all(12.0), + child: ElevatedButton.icon( + onPressed: () => controller + .participantAction(UserBottomSheetAction.message), + icon: const Icon(Icons.forum_outlined), + label: Text(L10n.of(context)!.sendAMessage), + ), + ), if (controller.widget.onMention != null) ListTile( leading: const Icon(Icons.alternate_email_outlined), @@ -77,53 +140,58 @@ class UserBottomSheetView extends StatelessWidget { onTap: () => controller.participantAction(UserBottomSheetAction.mention), ), - if (user.canChangePowerLevel) + if (user != null && user.canChangePowerLevel) ListTile( title: Text(L10n.of(context)!.setPermissionsLevel), leading: const Icon(Icons.edit_attributes_outlined), onTap: () => controller .participantAction(UserBottomSheetAction.permission), ), - if (user.canKick) + if (user != null && user.canKick) ListTile( title: Text(L10n.of(context)!.kickFromChat), leading: const Icon(Icons.exit_to_app_outlined), onTap: () => controller.participantAction(UserBottomSheetAction.kick), ), - if (user.canBan && user.membership != Membership.ban) + if (user != null && + user.canBan && + user.membership != Membership.ban) ListTile( title: Text(L10n.of(context)!.banFromChat), leading: const Icon(Icons.warning_sharp), onTap: () => controller.participantAction(UserBottomSheetAction.ban), ) - else if (user.canBan && user.membership == Membership.ban) + else if (user != null && + user.canBan && + user.membership == Membership.ban) ListTile( title: Text(L10n.of(context)!.unbanFromChat), leading: const Icon(Icons.warning_outlined), onTap: () => controller.participantAction(UserBottomSheetAction.unban), ), - if (user.id != client.userID && - !client.ignoredUsers.contains(user.id)) + if (user != null && user.id != client.userID) ListTile( textColor: Theme.of(context).colorScheme.onErrorContainer, iconColor: Theme.of(context).colorScheme.onErrorContainer, - title: Text(L10n.of(context)!.ignore), - leading: const Icon(Icons.block), - onTap: () => - controller.participantAction(UserBottomSheetAction.ignore), - ), - if (user.id != client.userID) - ListTile( - textColor: Theme.of(context).colorScheme.error, - iconColor: Theme.of(context).colorScheme.error, title: Text(L10n.of(context)!.reportUser), - leading: const Icon(Icons.shield_outlined), + leading: const Icon(Icons.report_outlined), onTap: () => controller.participantAction(UserBottomSheetAction.report), ), + if (profileSearchError != null) + ListTile( + leading: const Icon( + Icons.warning_outlined, + color: Colors.orange, + ), + subtitle: Text( + L10n.of(context)!.profileNotFound, + style: const TextStyle(color: Colors.orange), + ), + ), ], ), ), diff --git a/lib/utils/url_launcher.dart b/lib/utils/url_launcher.dart index c48b2793d..8d76d6912 100644 --- a/lib/utils/url_launcher.dart +++ b/lib/utils/url_launcher.dart @@ -11,9 +11,9 @@ import 'package:punycode/punycode.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:fluffychat/widgets/profile_bottom_sheet.dart'; import 'package:fluffychat/widgets/public_room_bottom_sheet.dart'; import 'platform_infos.dart'; @@ -233,7 +233,7 @@ class UrlLauncher { } else if (identityParts.primaryIdentifier.sigil == '@') { await showAdaptiveBottomSheet( context: context, - builder: (c) => ProfileBottomSheet( + builder: (c) => LoadProfileBottomSheet( userId: identityParts.primaryIdentifier, outerContext: context, ), diff --git a/lib/widgets/profile_bottom_sheet.dart b/lib/widgets/profile_bottom_sheet.dart deleted file mode 100644 index 0afb51cb5..000000000 --- a/lib/widgets/profile_bottom_sheet.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; -import 'package:go_router/go_router.dart'; -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/widgets/avatar.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class ProfileBottomSheet extends StatelessWidget { - final String userId; - final BuildContext outerContext; - - const ProfileBottomSheet({ - required this.userId, - required this.outerContext, - Key? key, - }) : super(key: key); - - void _startDirectChat(BuildContext context) async { - final client = Matrix.of(context).client; - final result = await showFutureLoadingDialog( - context: context, - future: () => client.startDirectChat(userId), - ); - if (result.error == null) { - context.go(['', 'rooms', result.result!].join('/')); - - Navigator.of(context, rootNavigator: false).pop(); - return; - } - } - - @override - Widget build(BuildContext context) { - return SafeArea( - child: FutureBuilder( - future: Matrix.of(context).client.getProfileFromUserId(userId), - builder: (context, snapshot) { - final profile = snapshot.data; - return Scaffold( - appBar: AppBar( - leading: CloseButton( - onPressed: Navigator.of(context, rootNavigator: false).pop, - ), - title: ListTile( - contentPadding: const EdgeInsets.only(right: 16.0), - title: Text( - profile?.displayName ?? userId.localpart ?? userId, - style: const TextStyle(fontSize: 18), - ), - subtitle: Text( - userId, - style: const TextStyle(fontSize: 12), - ), - ), - actions: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: OutlinedButton.icon( - onPressed: () => _startDirectChat(context), - icon: Icon(Icons.adaptive.share_outlined), - label: Text(L10n.of(context)!.share), - ), - ), - ], - ), - body: ListView( - children: [ - Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Avatar( - mxContent: profile?.avatarUrl, - name: profile?.displayName ?? userId, - size: Avatar.defaultSize * 3, - fontSize: 36, - ), - ), - ), - Container( - width: double.infinity, - padding: const EdgeInsets.all(12), - child: FloatingActionButton.extended( - onPressed: () => _startDirectChat(context), - label: Text(L10n.of(context)!.newChat), - icon: const Icon(Icons.send_outlined), - ), - ), - const SizedBox(height: 8), - ], - ), - ); - }, - ), - ); - } -}