From 9e13bd8dfd5cc73364c5796e74d7b777a1bbd444 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sat, 12 Aug 2023 11:52:20 +0200 Subject: [PATCH] design: Big redesign of three column mode to advanced two column mode --- assets/l10n/intl_en.arb | 15 +- lib/config/routes.dart | 78 +- lib/config/themes.dart | 9 +- lib/pages/chat_details/chat_details.dart | 136 ++-- lib/pages/chat_details/chat_details_view.dart | 695 ++++++++---------- .../chat_details/participant_list_item.dart | 7 +- lib/pages/chat_list/chat_list_header.dart | 1 + lib/pages/chat_members/chat_members.dart | 76 ++ lib/pages/chat_members/chat_members_view.dart | 101 +++ .../chat_permissions_settings_view.dart | 10 +- .../device_settings/device_settings.dart | 2 - .../device_settings/device_settings_view.dart | 2 + .../invitation_selection.dart | 34 +- .../invitation_selection_view.dart | 231 +++--- .../new_private_chat_view.dart | 3 +- .../settings_chat/settings_chat_view.dart | 1 - .../settings_emotes/settings_emotes_view.dart | 201 +++-- .../settings_notifications_view.dart | 1 - .../settings_security_view.dart | 1 - .../settings_style/settings_style_view.dart | 1 - lib/widgets/fluffy_chat_app.dart | 5 +- lib/widgets/layouts/max_width_body.dart | 40 +- lib/widgets/layouts/side_view_layout.dart | 61 -- lib/widgets/lock_screen.dart | 4 +- 24 files changed, 944 insertions(+), 771 deletions(-) create mode 100644 lib/pages/chat_members/chat_members.dart create mode 100644 lib/pages/chat_members/chat_members_view.dart delete mode 100644 lib/widgets/layouts/side_view_layout.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index b1a5ea0c..1dc215b6 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -1037,6 +1037,14 @@ "type": "text", "placeholders": {} }, + "inviteContactToGroupQuestion": "Do you want to invite {contact} to the chat \"{groupName}\"?", + "@inviteContactToGroup": { + "type": "text", + "placeholders": { + "contact": {}, + "groupName": {} + } + }, "inviteContactToGroup": "Invite contact to {groupName}", "@inviteContactToGroup": { "type": "text", @@ -1044,6 +1052,10 @@ "groupName": {} } }, + "noGroupDescriptionYet": "No group description created yet.", + "anyoneCanKnock": "Anyone can knock", + "noOneCanJoin": "No one can join", + "tryAgain": "Try again", "invited": "Invited", "@invited": { "type": "text", @@ -2499,5 +2511,6 @@ }, "profileNotFound": "The user could not be found on the server. Maybe there is a connection problem or the user doesn't exist.", "setTheme": "Set theme:", - "setColorTheme": "Set color theme:" + "setColorTheme": "Set color theme:", + "invite": "Invite" } diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 92a93d1c..9c7eec9e 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -11,6 +11,7 @@ import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; +import 'package:fluffychat/pages/chat_members/chat_members.dart'; import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings.dart'; import 'package:fluffychat/pages/device_settings/device_settings.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart'; @@ -31,7 +32,6 @@ import 'package:fluffychat/pages/settings_stories/settings_stories.dart'; import 'package:fluffychat/pages/settings_style/settings_style.dart'; import 'package:fluffychat/pages/story/story_page.dart'; import 'package:fluffychat/widgets/layouts/empty_page.dart'; -import 'package:fluffychat/widgets/layouts/side_view_layout.dart'; import 'package:fluffychat/widgets/layouts/two_column_layout.dart'; import 'package:fluffychat/widgets/log_view.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -218,54 +218,42 @@ abstract class AppRoutes { ), ], ), - ShellRoute( - pageBuilder: (context, state, child) => defaultPageBuilder( + GoRoute( + path: ':roomid', + pageBuilder: (context, state) => defaultPageBuilder( context, - SideViewLayout( - mainView: ChatPage( - roomId: state.pathParameters['roomid']!, - ), - sideView: child, - hideSideView: state.fullPath == '/rooms/:roomid', - ), + ChatPage(roomId: state.pathParameters['roomid']!), ), + redirect: loggedOutRedirect, routes: [ GoRoute( - path: ':roomid', + path: 'encryption', pageBuilder: (context, state) => defaultPageBuilder( context, - const SizedBox.shrink(), + const ChatEncryptionSettings(), ), redirect: loggedOutRedirect, - routes: [ - GoRoute( - path: 'encryption', - pageBuilder: (context, state) => defaultPageBuilder( - context, - const ChatEncryptionSettings(), - ), - redirect: loggedOutRedirect, + ), + GoRoute( + path: 'invite', + pageBuilder: (context, state) => defaultPageBuilder( + context, + InvitationSelection( + roomId: state.pathParameters['roomid']!, ), - GoRoute( - path: 'invite', - pageBuilder: (context, state) => defaultPageBuilder( - context, - const InvitationSelection(), - ), - redirect: loggedOutRedirect, + ), + redirect: loggedOutRedirect, + ), + GoRoute( + path: 'details', + pageBuilder: (context, state) => defaultPageBuilder( + context, + ChatDetails( + roomId: state.pathParameters['roomid']!, ), - GoRoute( - path: 'details', - pageBuilder: (context, state) => defaultPageBuilder( - context, - ChatDetails( - roomId: state.pathParameters['roomid']!, - ), - ), - routes: _chatDetailsRoutes, - redirect: loggedOutRedirect, - ), - ], + ), + routes: _chatDetailsRoutes, + redirect: loggedOutRedirect, ), ], ), @@ -276,6 +264,16 @@ abstract class AppRoutes { ]; static final List _chatDetailsRoutes = [ + GoRoute( + path: 'members', + pageBuilder: (context, state) => defaultPageBuilder( + context, + ChatMembersPage( + roomId: state.pathParameters['roomid']!, + ), + ), + redirect: loggedOutRedirect, + ), GoRoute( path: 'permissions', pageBuilder: (context, state) => defaultPageBuilder( @@ -288,7 +286,7 @@ abstract class AppRoutes { path: 'invite', pageBuilder: (context, state) => defaultPageBuilder( context, - const InvitationSelection(), + InvitationSelection(roomId: state.pathParameters['roomid']!), ), redirect: loggedOutRedirect, ), diff --git a/lib/config/themes.dart b/lib/config/themes.dart index e7739a9d..f64dc96f 100644 --- a/lib/config/themes.dart +++ b/lib/config/themes.dart @@ -58,7 +58,11 @@ abstract class FluffyThemes { static const Duration animationDuration = Duration(milliseconds: 250); static const Curve animationCurve = Curves.easeInOut; - static ThemeData buildTheme(Brightness brightness, [Color? seed]) { + static ThemeData buildTheme( + BuildContext context, + Brightness brightness, [ + Color? seed, + ]) { final colorScheme = ColorScheme.fromSeed( brightness: brightness, seedColor: seed ?? AppConfig.colorSchemeSeed ?? AppConfig.primaryColor, @@ -92,6 +96,9 @@ abstract class FluffyThemes { filled: true, ), appBarTheme: AppBarTheme( + toolbarHeight: FluffyThemes.isColumnMode(context) ? 72 : 56, + shadowColor: Colors.grey.withAlpha(64), + surfaceTintColor: colorScheme.background, systemOverlayStyle: SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: brightness.reversed, diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index 8c0c7f0e..3b3f424b 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -8,6 +8,7 @@ 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:image_picker/image_picker.dart'; +import 'package:matrix/matrix.dart' as matrix; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/chat_details/chat_details_view.dart'; @@ -31,7 +32,6 @@ class ChatDetails extends StatefulWidget { } class ChatDetailsController extends State { - List? members; bool displaySettings = false; void toggleDisplaySettings() => @@ -42,7 +42,6 @@ class ChatDetailsController extends State { void setDisplaynameAction() async { final room = Matrix.of(context).client.getRoomById(roomId!)!; final input = await showTextInputDialog( - useRootNavigator: false, context: context, title: L10n.of(context)!.changeTheNameOfTheGroup, okLabel: L10n.of(context)!.ok, @@ -103,7 +102,6 @@ class ChatDetailsController extends State { return setAliasAction(); } final select = await showConfirmationDialog( - useRootNavigator: false, context: context, title: L10n.of(context)!.editRoomAliases, actions: [ @@ -176,7 +174,6 @@ class ChatDetailsController extends State { final domain = room.client.userID!.domain; final input = await showTextInputDialog( - useRootNavigator: false, context: context, title: L10n.of(context)!.setInvitationLink, okLabel: L10n.of(context)!.ok, @@ -201,17 +198,16 @@ class ChatDetailsController extends State { void setTopicAction() async { final room = Matrix.of(context).client.getRoomById(roomId!)!; final input = await showTextInputDialog( - useRootNavigator: false, context: context, title: L10n.of(context)!.setGroupDescription, okLabel: L10n.of(context)!.ok, cancelLabel: L10n.of(context)!.cancel, textFields: [ DialogTextField( - hintText: L10n.of(context)!.setGroupDescription, + hintText: L10n.of(context)!.noGroupDescriptionYet, initialText: room.topic, - minLines: 1, - maxLines: 4, + minLines: 4, + maxLines: 8, ) ], ); @@ -229,30 +225,90 @@ class ChatDetailsController extends State { } } - void setGuestAccessAction(GuestAccess guestAccess) => showFutureLoadingDialog( - context: context, - future: () => Matrix.of(context) - .client - .getRoomById(roomId!)! - .setGuestAccess(guestAccess), - ); + void setGuestAccess() async { + final room = Matrix.of(context).client.getRoomById(roomId!)!; + final currentGuestAccess = room.guestAccess; + final newGuestAccess = await showConfirmationDialog( + context: context, + title: L10n.of(context)!.whoIsAllowedToJoinThisGroup, + actions: GuestAccess.values + .map( + (guestAccess) => AlertDialogAction( + key: guestAccess, + label: guestAccess + .getLocalizedString(MatrixLocals(L10n.of(context)!)), + isDefaultAction: guestAccess == currentGuestAccess, + ), + ) + .toList(), + ); + if (newGuestAccess == null || newGuestAccess == currentGuestAccess) return; + await showFutureLoadingDialog( + context: context, + future: () => room.setGuestAccess(newGuestAccess), + ); + } - void setHistoryVisibilityAction(HistoryVisibility historyVisibility) => - showFutureLoadingDialog( - context: context, - future: () => Matrix.of(context) - .client - .getRoomById(roomId!)! - .setHistoryVisibility(historyVisibility), - ); + void setHistoryVisibility() async { + final room = Matrix.of(context).client.getRoomById(roomId!)!; + final currentHistoryVisibility = room.historyVisibility; + final newHistoryVisibility = + await showConfirmationDialog( + context: context, + title: L10n.of(context)!.whoIsAllowedToJoinThisGroup, + actions: HistoryVisibility.values + .map( + (visibility) => AlertDialogAction( + key: visibility, + label: visibility + .getLocalizedString(MatrixLocals(L10n.of(context)!)), + isDefaultAction: visibility == currentHistoryVisibility, + ), + ) + .toList(), + ); + if (newHistoryVisibility == null || + newHistoryVisibility == currentHistoryVisibility) return; + await showFutureLoadingDialog( + context: context, + future: () => room.setHistoryVisibility(newHistoryVisibility), + ); + } - void setJoinRulesAction(JoinRules joinRule) => showFutureLoadingDialog( - context: context, - future: () => Matrix.of(context) - .client - .getRoomById(roomId!)! - .setJoinRules(joinRule), - ); + void setJoinRules() async { + final room = Matrix.of(context).client.getRoomById(roomId!)!; + final currentJoinRule = room.joinRules; + final newJoinRule = await showConfirmationDialog( + context: context, + title: L10n.of(context)!.whoIsAllowedToJoinThisGroup, + actions: JoinRules.values + .map( + (joinRule) => AlertDialogAction( + key: joinRule, + label: + joinRule.getLocalizedString(MatrixLocals(L10n.of(context)!)), + isDefaultAction: joinRule == currentJoinRule, + ), + ) + .toList(), + ); + if (newJoinRule == null || newJoinRule == currentJoinRule) return; + await showFutureLoadingDialog( + context: context, + future: () async { + await room.setJoinRules(newJoinRule); + room.client.setRoomVisibilityOnDirectory( + roomId!, + visibility: { + JoinRules.public, + JoinRules.knock, + }.contains(newJoinRule) + ? matrix.Visibility.public + : matrix.Visibility.private, + ); + }, + ); + } void goToEmoteSettings() async { final room = Matrix.of(context).client.getRoomById(roomId!)!; @@ -337,26 +393,8 @@ class ChatDetailsController extends State { ); } - void requestMoreMembersAction() async { - final room = Matrix.of(context).client.getRoomById(roomId!); - final participants = await showFutureLoadingDialog( - context: context, - future: () => room!.requestParticipants(), - ); - if (participants.error == null) { - setState(() => members = participants.result); - } - } - static const fixedWidth = 360.0; @override - Widget build(BuildContext context) { - members ??= - Matrix.of(context).client.getRoomById(roomId!)!.getParticipants(); - return SizedBox( - width: fixedWidth, - child: ChatDetailsView(this), - ); - } + Widget build(BuildContext context) => ChatDetailsView(this); } diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index 3dbbbd49..3068b392 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -12,7 +12,6 @@ import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/chat_settings_popup_menu.dart'; -import 'package:fluffychat/widgets/content_banner.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/url_launcher.dart'; @@ -36,406 +35,366 @@ class ChatDetailsView extends StatelessWidget { ); } - controller.members!.removeWhere((u) => u.membership == Membership.leave); - final actualMembersCount = (room.summary.mInvitedMemberCount ?? 0) + - (room.summary.mJoinedMemberCount ?? 0); - final canRequestMoreMembers = - controller.members!.length < actualMembersCount; - final iconColor = Theme.of(context).textTheme.bodyLarge!.color; return StreamBuilder( stream: room.onUpdate.stream, builder: (context, snapshot) { + var members = room.getParticipants().toList() + ..sort((b, a) => a.powerLevel.compareTo(b.powerLevel)); + members = members.take(10).toList(); + final actualMembersCount = (room.summary.mInvitedMemberCount ?? 0) + + (room.summary.mJoinedMemberCount ?? 0); + final canRequestMoreMembers = members.length < actualMembersCount; + final iconColor = Theme.of(context).textTheme.bodyLarge!.color; + final displayname = room.getLocalizedDisplayname( + MatrixLocals(L10n.of(context)!), + ); return Scaffold( - body: NestedScrollView( - headerSliverBuilder: - (BuildContext context, bool innerBoxIsScrolled) => [ - SliverAppBar( - leading: IconButton( - icon: const Icon(Icons.close_outlined), - onPressed: () => - GoRouterState.of(context).uri.path.startsWith('/spaces/') - ? context.pop() - : context.go( - ['', 'rooms', controller.roomId!].join('/'), - ), - ), - elevation: Theme.of(context).appBarTheme.elevation, - expandedHeight: 300.0, - floating: true, - pinned: true, - actions: [ - if (room.canonicalAlias.isNotEmpty) - IconButton( - tooltip: L10n.of(context)!.share, - icon: Icon(Icons.adaptive.share_outlined), - onPressed: () => FluffyShare.share( - AppConfig.inviteLinkPrefix + room.canonicalAlias, - context, - ), - ), - ChatSettingsPopupMenu(room, false) - ], - title: Text( - room.getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), + appBar: AppBar( + leading: const Center(child: BackButton()), + elevation: Theme.of(context).appBarTheme.elevation, + actions: [ + if (room.canonicalAlias.isNotEmpty) + IconButton( + tooltip: L10n.of(context)!.share, + icon: Icon(Icons.adaptive.share_outlined), + onPressed: () => FluffyShare.share( + AppConfig.inviteLinkPrefix + room.canonicalAlias, + context, ), ), - backgroundColor: Theme.of(context).appBarTheme.backgroundColor, - flexibleSpace: FlexibleSpaceBar( - background: ContentBanner( - mxContent: room.avatar, - onEdit: room.canSendEvent('m.room.avatar') - ? controller.setAvatarAction - : null, - defaultIcon: Icons.group_outlined, - ), - ), - ), + ChatSettingsPopupMenu(room, false) ], - body: MaxWidthBody( - child: ListView.builder( - itemCount: controller.members!.length + - 1 + - (canRequestMoreMembers ? 1 : 0), - itemBuilder: (BuildContext context, int i) => i == 0 - ? Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ListTile( - onTap: room.canSendEvent(EventTypes.RoomTopic) - ? controller.setTopicAction - : null, - trailing: room.canSendEvent(EventTypes.RoomTopic) - ? Icon( - Icons.edit_outlined, - color: Theme.of(context) - .colorScheme - .onBackground, - ) - : null, - title: Text( - L10n.of(context)!.groupDescription, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - ), - ), - ), - if (room.topic.isNotEmpty) + title: Text(L10n.of(context)!.chatDetails), + backgroundColor: Theme.of(context).appBarTheme.backgroundColor, + ), + body: MaxWidthBody( + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: members.length + 1 + (canRequestMoreMembers ? 1 : 0), + itemBuilder: (BuildContext context, int i) => i == 0 + ? Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - ), - child: Linkify( - text: room.topic.isEmpty - ? L10n.of(context)!.addGroupDescription - : room.topic, - options: const LinkifyOptions(humanize: false), - linkStyle: - const TextStyle(color: Colors.blueAccent), - style: TextStyle( - fontSize: 14, - color: Theme.of(context) - .textTheme - .bodyMedium! - .color, - decorationColor: Theme.of(context) - .textTheme - .bodyMedium! - .color, - ), - onOpen: (url) => - UrlLauncher(context, url.url).launchUrl(), - ), - ), - const SizedBox(height: 8), - const Divider(height: 1), - ListTile( - title: Text( - L10n.of(context)!.settings, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - ), - ), - trailing: Icon( - controller.displaySettings - ? Icons.keyboard_arrow_down_outlined - : Icons.keyboard_arrow_right_outlined, - ), - onTap: controller.toggleDisplaySettings, - ), - if (controller.displaySettings) ...[ - if (room.canSendEvent('m.room.name')) - ListTile( - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: iconColor, - child: const Icon( - Icons.people_outline_outlined, - ), - ), - title: Text( - L10n.of(context)!.changeTheNameOfTheGroup, - ), - subtitle: Text( - room.getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), - ), - ), - onTap: controller.setDisplaynameAction, - ), - if (room.joinRules == JoinRules.public) - ListTile( - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: iconColor, - child: const Icon(Icons.link_outlined), - ), - onTap: controller.editAliases, - title: Text(L10n.of(context)!.editRoomAliases), - subtitle: Text( - (room.canonicalAlias.isNotEmpty) - ? room.canonicalAlias - : L10n.of(context)!.none, - ), - ), - ListTile( - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: iconColor, - child: const Icon( - Icons.insert_emoticon_outlined, - ), - ), - title: Text(L10n.of(context)!.emoteSettings), - subtitle: Text(L10n.of(context)!.setCustomEmotes), - onTap: controller.goToEmoteSettings, - ), - PopupMenuButton( - onSelected: controller.setJoinRulesAction, - itemBuilder: (BuildContext context) => - >[ - if (room.canChangeJoinRules) - PopupMenuItem( - value: JoinRules.public, - child: Text( - JoinRules.public.getLocalizedString( - MatrixLocals(L10n.of(context)!), + padding: const EdgeInsets.all(32.0), + child: Stack( + children: [ + 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, ), ), - ), - if (room.canChangeJoinRules) - PopupMenuItem( - value: JoinRules.invite, - child: Text( - JoinRules.invite.getLocalizedString( - MatrixLocals(L10n.of(context)!), - ), + child: Avatar( + mxContent: room.avatar, + name: displayname, + size: Avatar.defaultSize * 2.5, + fontSize: 18 * 2.5, ), ), - ], - child: ListTile( - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: iconColor, - child: const Icon(Icons.shield_outlined), - ), - title: Text( - L10n.of(context)!.whoIsAllowedToJoinThisGroup, - ), - subtitle: Text( - room.joinRules?.getLocalizedString( - MatrixLocals(L10n.of(context)!), - ) ?? - L10n.of(context)!.none, - ), - ), - ), - PopupMenuButton( - onSelected: controller.setHistoryVisibilityAction, - itemBuilder: (BuildContext context) => - >[ - if (room.canChangeHistoryVisibility) - PopupMenuItem( - value: HistoryVisibility.invited, - child: Text( - HistoryVisibility.invited - .getLocalizedString( - MatrixLocals(L10n.of(context)!), - ), - ), - ), - if (room.canChangeHistoryVisibility) - PopupMenuItem( - value: HistoryVisibility.joined, - child: Text( - HistoryVisibility.joined - .getLocalizedString( - MatrixLocals(L10n.of(context)!), - ), - ), - ), - if (room.canChangeHistoryVisibility) - PopupMenuItem( - value: HistoryVisibility.shared, - child: Text( - HistoryVisibility.shared - .getLocalizedString( - MatrixLocals(L10n.of(context)!), - ), - ), - ), - if (room.canChangeHistoryVisibility) - PopupMenuItem( - value: HistoryVisibility.worldReadable, - child: Text( - HistoryVisibility.worldReadable - .getLocalizedString( - MatrixLocals(L10n.of(context)!), - ), - ), - ), - ], - child: ListTile( - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: iconColor, - child: const Icon(Icons.visibility_outlined), - ), - title: Text( - L10n.of(context)!.visibilityOfTheChatHistory, - ), - subtitle: Text( - room.historyVisibility?.getLocalizedString( - MatrixLocals(L10n.of(context)!), - ) ?? - L10n.of(context)!.none, - ), - ), - ), - if (room.joinRules == JoinRules.public) - PopupMenuButton( - onSelected: controller.setGuestAccessAction, - itemBuilder: (BuildContext context) => - >[ - if (room.canChangeGuestAccess) - PopupMenuItem( - value: GuestAccess.canJoin, - child: Text( - GuestAccess.canJoin.getLocalizedString( - MatrixLocals( - L10n.of(context)!, - ), - ), - ), - ), - if (room.canChangeGuestAccess) - PopupMenuItem( - value: GuestAccess.forbidden, - child: Text( - GuestAccess.forbidden - .getLocalizedString( - MatrixLocals( - L10n.of(context)!, - ), + if (room.canChangeStateEvent( + EventTypes.RoomAvatar, + )) + Positioned( + bottom: 0, + right: 0, + child: FloatingActionButton.small( + onPressed: controller.setAvatarAction, + heroTag: null, + child: const Icon( + Icons.camera_alt_outlined, ), ), ), ], - child: ListTile( - leading: CircleAvatar( - backgroundColor: Theme.of(context) - .scaffoldBackgroundColor, - foregroundColor: iconColor, - child: const Icon( - Icons.person_add_alt_1_outlined, + ), + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextButton.icon( + onPressed: () => room.canChangeStateEvent( + EventTypes.RoomName, + ) + ? controller.setDisplaynameAction() + : FluffyShare.share( + displayname, + context, + copyOnly: true, + ), + icon: Icon( + room.canChangeStateEvent( + EventTypes.RoomName, + ) + ? Icons.edit_outlined + : Icons.copy_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), ), ), - title: Text( - L10n.of(context)!.areGuestsAllowedToJoin, - ), - subtitle: Text( - room.guestAccess.getLocalizedString( - MatrixLocals(L10n.of(context)!), + TextButton.icon( + onPressed: () => context.go( + '/rooms/${controller.roomId}/details/members', + ), + icon: const Icon( + Icons.group_outlined, + size: 14, + ), + style: TextButton.styleFrom( + foregroundColor: Theme.of(context) + .colorScheme + .secondary, + ), + label: Text( + L10n.of(context)!.countParticipants( + actualMembersCount, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + // style: const TextStyle(fontSize: 12), ), ), - ), + ], ), - ListTile( - title: - Text(L10n.of(context)!.editChatPermissions), - subtitle: Text( - L10n.of(context)!.whoCanPerformWhichAction, - ), - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: iconColor, - child: const Icon( - Icons.edit_attributes_outlined, - ), - ), - onTap: () => context - .go('/rooms/${room.id}/details/permissions'), ), ], - const Divider(height: 1), - ListTile( - title: Text( - actualMembersCount > 1 - ? L10n.of(context)!.countParticipants( - actualMembersCount.toString(), - ) - : L10n.of(context)!.emptyChat, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - ), + ), + Divider( + height: 1, + color: Theme.of(context).dividerColor, + ), + ListTile( + onTap: room.canSendEvent(EventTypes.RoomTopic) + ? controller.setTopicAction + : null, + trailing: room.canSendEvent(EventTypes.RoomTopic) + ? Icon( + Icons.edit_outlined, + color: Theme.of(context) + .colorScheme + .onBackground, + ) + : null, + title: Text( + L10n.of(context)!.groupDescription, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, ), ), - room.canInvite - ? ListTile( - title: Text(L10n.of(context)!.inviteContact), - leading: CircleAvatar( - backgroundColor: - Theme.of(context).primaryColor, - foregroundColor: Colors.white, - radius: Avatar.defaultSize / 2, - child: const Icon(Icons.add_outlined), - ), - onTap: () => - context.go('/rooms/${room.id}/invite'), - ) - : const SizedBox.shrink(), - ], - ) - : i < controller.members!.length + 1 - ? ParticipantListItem(controller.members![i - 1]) - : ListTile( - title: Text( - L10n.of(context)!.loadCountMoreParticipants( - (actualMembersCount - - controller.members!.length) - .toString(), - ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + child: Linkify( + text: room.topic.isEmpty + ? L10n.of(context)!.noGroupDescriptionYet + : room.topic, + options: const LinkifyOptions(humanize: false), + linkStyle: + const TextStyle(color: Colors.blueAccent), + style: TextStyle( + fontSize: 14, + fontStyle: room.topic.isEmpty + ? FontStyle.italic + : FontStyle.normal, + color: + Theme.of(context).textTheme.bodyMedium!.color, + decorationColor: + Theme.of(context).textTheme.bodyMedium!.color, ), + onOpen: (url) => + UrlLauncher(context, url.url).launchUrl(), + ), + ), + const SizedBox(height: 16), + Divider( + height: 1, + color: Theme.of(context).dividerColor, + ), + if (room.joinRules == JoinRules.public) + ListTile( leading: CircleAvatar( backgroundColor: Theme.of(context).scaffoldBackgroundColor, + foregroundColor: iconColor, + child: const Icon(Icons.link_outlined), + ), + trailing: const Icon(Icons.chevron_right_outlined), + onTap: controller.editAliases, + title: Text(L10n.of(context)!.editRoomAliases), + subtitle: Text( + (room.canonicalAlias.isNotEmpty) + ? room.canonicalAlias + : L10n.of(context)!.none, + ), + ), + ListTile( + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + foregroundColor: iconColor, + child: const Icon( + Icons.insert_emoticon_outlined, + ), + ), + title: Text(L10n.of(context)!.emoteSettings), + subtitle: Text(L10n.of(context)!.setCustomEmotes), + onTap: controller.goToEmoteSettings, + trailing: const Icon(Icons.chevron_right_outlined), + ), + ListTile( + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + foregroundColor: iconColor, + child: const Icon(Icons.shield_outlined), + ), + title: Text( + L10n.of(context)!.whoIsAllowedToJoinThisGroup, + ), + trailing: const Icon(Icons.chevron_right_outlined), + subtitle: Text( + room.joinRules?.getLocalizedString( + MatrixLocals(L10n.of(context)!), + ) ?? + L10n.of(context)!.none, + ), + onTap: controller.setJoinRules, + ), + ListTile( + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + foregroundColor: iconColor, + child: const Icon(Icons.visibility_outlined), + ), + trailing: const Icon(Icons.chevron_right_outlined), + title: Text( + L10n.of(context)!.visibilityOfTheChatHistory, + ), + subtitle: Text( + room.historyVisibility?.getLocalizedString( + MatrixLocals(L10n.of(context)!), + ) ?? + L10n.of(context)!.none, + ), + onTap: controller.setHistoryVisibility, + ), + if (room.joinRules == JoinRules.public) + ListTile( + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + foregroundColor: iconColor, child: const Icon( - Icons.refresh, - color: Colors.grey, + Icons.person_add_alt_1_outlined, ), ), - onTap: controller.requestMoreMembersAction, + trailing: const Icon(Icons.chevron_right_outlined), + title: Text( + L10n.of(context)!.areGuestsAllowedToJoin, + ), + subtitle: Text( + room.guestAccess.getLocalizedString( + MatrixLocals(L10n.of(context)!), + ), + ), + onTap: controller.setGuestAccess, ), - ), + ListTile( + title: Text(L10n.of(context)!.editChatPermissions), + subtitle: Text( + L10n.of(context)!.whoCanPerformWhichAction, + ), + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + foregroundColor: iconColor, + child: const Icon( + Icons.edit_attributes_outlined, + ), + ), + trailing: const Icon(Icons.chevron_right_outlined), + onTap: () => context + .go('/rooms/${room.id}/details/permissions'), + ), + Divider( + height: 1, + color: Theme.of(context).dividerColor, + ), + ListTile( + title: Text( + L10n.of(context)!.countParticipants( + actualMembersCount.toString(), + ), + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + ), + if (room.canInvite) + ListTile( + title: Text(L10n.of(context)!.inviteContact), + leading: CircleAvatar( + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + radius: Avatar.defaultSize / 2, + child: const Icon(Icons.add_outlined), + ), + trailing: const Icon(Icons.chevron_right_outlined), + onTap: () => context.go('/rooms/${room.id}/invite'), + ), + ], + ) + : i < members.length + 1 + ? ParticipantListItem(members[i - 1]) + : ListTile( + title: Text( + L10n.of(context)!.loadCountMoreParticipants( + (actualMembersCount - members.length).toString(), + ), + ), + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + child: const Icon( + Icons.group_outlined, + color: Colors.grey, + ), + ), + onTap: () => context.go( + '/rooms/${controller.roomId!}/details/members', + ), + trailing: const Icon(Icons.chevron_right_outlined), + ), ), ), ); diff --git a/lib/pages/chat_details/participant_list_item.dart b/lib/pages/chat_details/participant_list_item.dart index 63ce965b..11848cb6 100644 --- a/lib/pages/chat_details/participant_list_item.dart +++ b/lib/pages/chat_details/participant_list_item.dart @@ -38,7 +38,12 @@ class ParticipantListItem extends StatelessWidget { ), title: Row( children: [ - Text(user.calcDisplayname()), + Expanded( + child: Text( + user.calcDisplayname(), + overflow: TextOverflow.ellipsis, + ), + ), if (permissionBatch.isNotEmpty) Container( padding: const EdgeInsets.symmetric( diff --git a/lib/pages/chat_list/chat_list_header.dart b/lib/pages/chat_list/chat_list_header.dart index 51f68f96..0d49ea8a 100644 --- a/lib/pages/chat_list/chat_list_header.dart +++ b/lib/pages/chat_list/chat_list_header.dart @@ -18,6 +18,7 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { return SliverAppBar( floating: true, + toolbarHeight: Theme.of(context).appBarTheme.toolbarHeight ?? 56, pinned: FluffyThemes.isColumnMode(context) || selectMode != SelectMode.normal, scrolledUnderElevation: selectMode == SelectMode.normal ? 0 : null, diff --git a/lib/pages/chat_members/chat_members.dart b/lib/pages/chat_members/chat_members.dart new file mode 100644 index 00000000..5d3ca212 --- /dev/null +++ b/lib/pages/chat_members/chat_members.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; + +import 'package:matrix/matrix.dart'; + +import '../../widgets/matrix.dart'; +import 'chat_members_view.dart'; + +class ChatMembersPage extends StatefulWidget { + final String roomId; + const ChatMembersPage({required this.roomId, super.key}); + + @override + State createState() => ChatMembersController(); +} + +class ChatMembersController extends State { + List? members; + List? filteredMembers; + Object? error; + + final TextEditingController filterController = TextEditingController(); + + void setFilter([_]) async { + final filter = filterController.text.toLowerCase().trim(); + + if (filter.isEmpty) { + setState(() { + filteredMembers = members + ?..sort((b, a) => a.powerLevel.compareTo(b.powerLevel)); + }); + return; + } + setState(() { + filteredMembers = members + ?.where( + (user) => + user.displayName?.toLowerCase().contains(filter) ?? + user.id.toLowerCase().contains(filter), + ) + .toList() + ?..sort((b, a) => a.powerLevel.compareTo(b.powerLevel)); + }); + } + + void refreshMembers() async { + try { + setState(() { + error = null; + }); + final participants = await Matrix.of(context) + .client + .getRoomById(widget.roomId) + ?.requestParticipants(); + + setState(() { + members = participants; + }); + setFilter(); + } catch (e, s) { + Logs() + .d('Unable to request participants. Try again in 3 seconds...', e, s); + setState(() { + error = e; + }); + } + } + + @override + void initState() { + super.initState(); + refreshMembers(); + } + + @override + Widget build(BuildContext context) => ChatMembersView(this); +} diff --git a/lib/pages/chat_members/chat_members_view.dart b/lib/pages/chat_members/chat_members_view.dart new file mode 100644 index 00000000..f110b380 --- /dev/null +++ b/lib/pages/chat_members/chat_members_view.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; + +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import '../../widgets/layouts/max_width_body.dart'; +import '../../widgets/matrix.dart'; +import '../chat_details/participant_list_item.dart'; +import 'chat_members.dart'; + +class ChatMembersView extends StatelessWidget { + final ChatMembersController controller; + const ChatMembersView(this.controller, {super.key}); + + @override + Widget build(BuildContext context) { + final room = + Matrix.of(context).client.getRoomById(controller.widget.roomId); + if (room == null) { + return Scaffold( + appBar: AppBar( + title: Text(L10n.of(context)!.oopsSomethingWentWrong), + ), + body: Center( + child: Text(L10n.of(context)!.youAreNoLongerParticipatingInThisChat), + ), + ); + } + + final members = controller.filteredMembers; + + final roomCount = (room.summary.mJoinedMemberCount ?? 0) + + (room.summary.mInvitedMemberCount ?? 0); + + final error = controller.error; + + return Scaffold( + appBar: AppBar( + leading: const Center(child: BackButton()), + title: Text( + L10n.of(context)!.countParticipants(roomCount), + ), + actions: [ + IconButton( + onPressed: () => context.go('/rooms/{room.id}/invite'), + icon: const Icon( + Icons.person_add_outlined, + ), + ), + ], + ), + body: MaxWidthBody( + withScrolling: false, + child: error != null + ? Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.error_outline), + Text(error.toLocalizedString(context)), + const SizedBox(height: 8), + OutlinedButton.icon( + onPressed: controller.refreshMembers, + icon: const Icon(Icons.refresh_outlined), + label: Text(L10n.of(context)!.tryAgain), + ), + ], + ), + ), + ) + : members == null + ? const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator.adaptive(), + ), + ) + : ListView.builder( + shrinkWrap: true, + itemCount: members.length + 1, + itemBuilder: (context, i) => i == 0 + ? Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + controller: controller.filterController, + onChanged: controller.setFilter, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.search_outlined), + hintText: L10n.of(context)!.search, + ), + ), + ) + : ParticipantListItem(members[i - 1]), + ), + ), + ); + } +} diff --git a/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart b/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart index 6eb7005f..9e045342 100644 --- a/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart +++ b/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings.dart'; @@ -19,17 +18,10 @@ class ChatPermissionsSettingsView extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - leading: GoRouterState.of(context).uri.path.startsWith('/spaces/') - ? null - : IconButton( - icon: const Icon(Icons.close_outlined), - onPressed: () => - context.go(['', 'rooms', controller.roomId!].join('/')), - ), + leading: const Center(child: BackButton()), title: Text(L10n.of(context)!.editChatPermissions), ), body: MaxWidthBody( - withScrolling: true, child: StreamBuilder( stream: controller.onChanged, builder: (context, _) { diff --git a/lib/pages/device_settings/device_settings.dart b/lib/pages/device_settings/device_settings.dart index 294e2ed3..f0b541ca 100644 --- a/lib/pages/device_settings/device_settings.dart +++ b/lib/pages/device_settings/device_settings.dart @@ -33,7 +33,6 @@ class DevicesSettingsController extends State { void removeDevicesAction(List devices) async { if (await showOkCancelAlertDialog( - useRootNavigator: false, context: context, title: L10n.of(context)!.areYouSure, okLabel: L10n.of(context)!.yes, @@ -68,7 +67,6 @@ class DevicesSettingsController extends State { void renameDeviceAction(Device device) async { final displayName = await showTextInputDialog( - useRootNavigator: false, context: context, title: L10n.of(context)!.changeDeviceName, okLabel: L10n.of(context)!.ok, diff --git a/lib/pages/device_settings/device_settings_view.dart b/lib/pages/device_settings/device_settings_view.dart index 2c1a1839..aaab4cd9 100644 --- a/lib/pages/device_settings/device_settings_view.dart +++ b/lib/pages/device_settings/device_settings_view.dart @@ -39,6 +39,8 @@ class DevicesSettingsView extends StatelessWidget { ); } return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), itemCount: controller.notThisDevice.length + 1, itemBuilder: (BuildContext context, int i) { if (i == 0) { diff --git a/lib/pages/invitation_selection/invitation_selection.dart b/lib/pages/invitation_selection/invitation_selection.dart index e5e85616..74d30163 100644 --- a/lib/pages/invitation_selection/invitation_selection.dart +++ b/lib/pages/invitation_selection/invitation_selection.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.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/pages/invitation_selection/invitation_selection_view.dart'; @@ -14,7 +13,11 @@ import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/localized_exception_extension.dart'; class InvitationSelection extends StatefulWidget { - const InvitationSelection({Key? key}) : super(key: key); + final String roomId; + const InvitationSelection({ + Key? key, + required this.roomId, + }) : super(key: key); @override InvitationSelectionController createState() => @@ -28,7 +31,7 @@ class InvitationSelectionController extends State { List foundProfiles = []; Timer? coolDown; - String? get roomId => GoRouterState.of(context).pathParameters['roomid']; + String? get roomId => widget.roomId; Future> getContacts(BuildContext context) async { final client = Matrix.of(context).client; @@ -37,12 +40,10 @@ class InvitationSelectionController extends State { participants.removeWhere( (u) => ![Membership.join, Membership.invite].contains(u.membership), ); - final participantsIds = participants.map((p) => p.stateKey).toList(); final contacts = client.rooms .where((r) => r.isDirectChat) .map((r) => r.unsafeGetUserFromMemoryOrFallback(r.directChatMatrixID!)) - .toList() - ..removeWhere((u) => participantsIds.contains(u.stateKey)); + .toList(); contacts.sort( (a, b) => a.calcDisplayname().toLowerCase().compareTo( b.calcDisplayname().toLowerCase(), @@ -51,17 +52,19 @@ class InvitationSelectionController extends State { return contacts; } - void inviteAction(BuildContext context, String id) async { + void inviteAction(BuildContext context, String id, String displayname) async { final room = Matrix.of(context).client.getRoomById(roomId!)!; if (OkCancelResult.ok != await showOkCancelAlertDialog( context: context, - title: L10n.of(context)!.inviteContactToGroup( + title: L10n.of(context)!.inviteContact, + message: L10n.of(context)!.inviteContactToGroupQuestion( + displayname, room.getLocalizedDisplayname( MatrixLocals(L10n.of(context)!), ), ), - okLabel: L10n.of(context)!.yes, + okLabel: L10n.of(context)!.invite, cancelLabel: L10n.of(context)!.cancel, )) { return; @@ -118,19 +121,6 @@ class InvitationSelectionController extends State { ], ); } - final participants = Matrix.of(context) - .client - .getRoomById(roomId!)! - .getParticipants() - .where( - (user) => - [Membership.join, Membership.invite].contains(user.membership), - ) - .toList(); - foundProfiles.removeWhere( - (profile) => - participants.indexWhere((u) => u.id == profile.userId) != -1, - ); }); } diff --git a/lib/pages/invitation_selection/invitation_selection_view.dart b/lib/pages/invitation_selection/invitation_selection_view.dart index d2d5f1e1..07600669 100644 --- a/lib/pages/invitation_selection/invitation_selection_view.dart +++ b/lib/pages/invitation_selection/invitation_selection_view.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart'; @@ -20,104 +19,152 @@ class InvitationSelectionView extends StatelessWidget { final groupName = room.name.isEmpty ? L10n.of(context)!.group : room.name; return Scaffold( appBar: AppBar( - leading: GoRouterState.of(context).uri.path.startsWith('/spaces/') - ? null - : IconButton( - icon: const Icon(Icons.close_outlined), - onPressed: () => - context.go(['', 'rooms', controller.roomId!].join('/')), - ), + leading: const Center(child: BackButton()), titleSpacing: 0, - title: SizedBox( - height: 44, - child: Padding( - padding: const EdgeInsets.only(right: 12.0), - child: TextField( - textInputAction: TextInputAction.search, - decoration: InputDecoration( - hintText: L10n.of(context)!.inviteContactToGroup(groupName), - suffixIcon: controller.loading - ? const Padding( - padding: EdgeInsets.symmetric( - vertical: 10.0, - horizontal: 12, - ), - child: SizedBox.square( - dimension: 24, - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, + title: Text(L10n.of(context)!.inviteContact), + ), + body: MaxWidthBody( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + textInputAction: TextInputAction.search, + decoration: InputDecoration( + hintText: L10n.of(context)!.inviteContactToGroup(groupName), + prefixIcon: controller.loading + ? const Padding( + padding: EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 12, + ), + child: SizedBox.square( + dimension: 24, + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ), + ) + : const Icon(Icons.search_outlined), + ), + onChanged: controller.searchUserWithCoolDown, + ), + ), + StreamBuilder( + stream: room.onUpdate.stream, + builder: (context, snapshot) { + final participants = + room.getParticipants().map((user) => user.id).toSet(); + return controller.foundProfiles.isNotEmpty + ? ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: controller.foundProfiles.length, + itemBuilder: (BuildContext context, int i) => + _InviteContactListTile( + avatarUrl: controller.foundProfiles[i].avatarUrl, + displayname: controller + .foundProfiles[i].displayName ?? + controller.foundProfiles[i].userId.localpart ?? + L10n.of(context)!.user, + userId: controller.foundProfiles[i].userId, + isMember: participants + .contains(controller.foundProfiles[i].userId), + onTap: () => controller.inviteAction( + context, + controller.foundProfiles[i].userId, + controller.foundProfiles[i].displayName ?? + controller.foundProfiles[i].userId.localpart ?? + L10n.of(context)!.user, ), ), ) - : const Icon(Icons.search_outlined), - ), - onChanged: controller.searchUserWithCoolDown, + : FutureBuilder>( + future: controller.getContacts(context), + builder: (BuildContext context, snapshot) { + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ); + } + final contacts = snapshot.data!; + return ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: contacts.length, + itemBuilder: (BuildContext context, int i) => + _InviteContactListTile( + avatarUrl: contacts[i].avatarUrl, + displayname: contacts[i].displayName ?? + contacts[i].id.localpart ?? + L10n.of(context)!.user, + userId: contacts[i].id, + isMember: participants.contains(contacts[i].id), + onTap: () => controller.inviteAction( + context, + contacts[i].id, + contacts[i].displayName ?? + contacts[i].id.localpart ?? + L10n.of(context)!.user, + ), + ), + ); + }, + ); + }, ), - ), + ], ), ), - body: MaxWidthBody( - withScrolling: true, - child: controller.foundProfiles.isNotEmpty - ? ListView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: controller.foundProfiles.length, - itemBuilder: (BuildContext context, int i) => ListTile( - leading: Avatar( - mxContent: controller.foundProfiles[i].avatarUrl, - name: controller.foundProfiles[i].displayName ?? - controller.foundProfiles[i].userId, - ), - title: Text( - controller.foundProfiles[i].displayName ?? - controller.foundProfiles[i].userId.localpart!, - ), - subtitle: Text(controller.foundProfiles[i].userId), - onTap: () => controller.inviteAction( - context, - controller.foundProfiles[i].userId, - ), - ), - ) - : FutureBuilder>( - future: controller.getContacts(context), - builder: (BuildContext context, snapshot) { - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator.adaptive(strokeWidth: 2), - ); - } - final contacts = snapshot.data!; - return ListView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: contacts.length, - itemBuilder: (BuildContext context, int i) => ListTile( - leading: Avatar( - mxContent: contacts[i].avatarUrl, - name: contacts[i].calcDisplayname(), - ), - title: Text( - contacts[i].calcDisplayname(), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text( - contacts[i].id, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - ), - onTap: () => - controller.inviteAction(context, contacts[i].id), - ), - ); - }, - ), - ), + ); + } +} + +class _InviteContactListTile extends StatelessWidget { + final String userId; + final String displayname; + final Uri? avatarUrl; + final bool isMember; + final void Function() onTap; + + const _InviteContactListTile({ + Key? key, + required this.userId, + required this.displayname, + required this.avatarUrl, + required this.isMember, + required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Opacity( + opacity: isMember ? 0.5 : 1, + child: ListTile( + leading: Avatar( + mxContent: avatarUrl, + name: displayname, + ), + title: Text( + displayname, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + userId, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + ), + ), + onTap: isMember ? null : onTap, + trailing: isMember + ? Text(L10n.of(context)!.participant) + : const Icon(Icons.person_add_outlined), + ), ); } } diff --git a/lib/pages/new_private_chat/new_private_chat_view.dart b/lib/pages/new_private_chat/new_private_chat_view.dart index 69ea5b4b..97a94526 100644 --- a/lib/pages/new_private_chat/new_private_chat_view.dart +++ b/lib/pages/new_private_chat/new_private_chat_view.dart @@ -45,7 +45,7 @@ class NewPrivateChatView extends StatelessWidget { children: [ Expanded( child: MaxWidthBody( - withScrolling: true, + withFrame: false, child: Container( margin: const EdgeInsets.all(_qrCodePadding), alignment: Alignment.center, @@ -98,7 +98,6 @@ class NewPrivateChatView extends StatelessWidget { ), ), MaxWidthBody( - withScrolling: false, child: Padding( padding: const EdgeInsets.all(12.0), child: Form( diff --git a/lib/pages/settings_chat/settings_chat_view.dart b/lib/pages/settings_chat/settings_chat_view.dart index b62277cd..97970b17 100644 --- a/lib/pages/settings_chat/settings_chat_view.dart +++ b/lib/pages/settings_chat/settings_chat_view.dart @@ -24,7 +24,6 @@ class SettingsChatView extends StatelessWidget { body: ListTileTheme( iconColor: Theme.of(context).textTheme.bodyLarge!.color, child: MaxWidthBody( - withScrolling: true, child: Column( children: [ ListTile( diff --git a/lib/pages/settings_emotes/settings_emotes_view.dart b/lib/pages/settings_emotes/settings_emotes_view.dart index 64e8c3e3..d91b43bd 100644 --- a/lib/pages/settings_emotes/settings_emotes_view.dart +++ b/lib/pages/settings_emotes/settings_emotes_view.dart @@ -59,6 +59,7 @@ class EmotesSettingsView extends StatelessWidget { : null, body: MaxWidthBody( child: Column( + mainAxisSize: MainAxisSize.min, children: [ if (!controller.readonly) Container( @@ -116,120 +117,114 @@ class EmotesSettingsView extends StatelessWidget { onChanged: controller.setIsGloballyActive, ), if (!controller.readonly || controller.room != null) - Divider( - height: 2, - thickness: 2, - color: Theme.of(context).primaryColor, - ), - Expanded( - child: imageKeys.isEmpty - ? Center( - child: Padding( - padding: const EdgeInsets.all(16), - child: Text( - L10n.of(context)!.noEmotesFound, - style: const TextStyle(fontSize: 20), - ), + const Divider(thickness: 1), + imageKeys.isEmpty + ? Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + L10n.of(context)!.noEmotesFound, + style: const TextStyle(fontSize: 20), ), - ) - : ListView.separated( - separatorBuilder: (BuildContext context, int i) => - const SizedBox.shrink(), - itemCount: imageKeys.length + 1, - itemBuilder: (BuildContext context, int i) { - if (i >= imageKeys.length) { - return Container(height: 70); - } - final imageCode = imageKeys[i]; - final image = controller.pack!.images[imageCode]!; - final textEditingController = TextEditingController(); - textEditingController.text = imageCode; - final useShortCuts = - (PlatformInfos.isWeb || PlatformInfos.isDesktop); - return ListTile( - leading: Container( - width: 180.0, - height: 38, - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - borderRadius: - const BorderRadius.all(Radius.circular(10)), - color: Theme.of(context).secondaryHeaderColor, - ), - child: Shortcuts( - shortcuts: !useShortCuts + ), + ) + : ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (BuildContext context, int i) => + const SizedBox.shrink(), + itemCount: imageKeys.length + 1, + itemBuilder: (BuildContext context, int i) { + if (i >= imageKeys.length) { + return Container(height: 70); + } + final imageCode = imageKeys[i]; + final image = controller.pack!.images[imageCode]!; + final textEditingController = TextEditingController(); + textEditingController.text = imageCode; + final useShortCuts = + (PlatformInfos.isWeb || PlatformInfos.isDesktop); + return ListTile( + leading: Container( + width: 180.0, + height: 38, + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + borderRadius: + const BorderRadius.all(Radius.circular(10)), + color: Theme.of(context).secondaryHeaderColor, + ), + child: Shortcuts( + shortcuts: !useShortCuts + ? {} + : { + LogicalKeySet(LogicalKeyboardKey.enter): + SubmitLineIntent(), + }, + child: Actions( + actions: !useShortCuts ? {} : { - LogicalKeySet(LogicalKeyboardKey.enter): - SubmitLineIntent(), + SubmitLineIntent: CallbackAction( + onInvoke: (i) { + controller.submitImageAction( + imageCode, + textEditingController.text, + image, + textEditingController, + ); + return null; + }, + ), }, - child: Actions( - actions: !useShortCuts - ? {} - : { - SubmitLineIntent: CallbackAction( - onInvoke: (i) { - controller.submitImageAction( - imageCode, - textEditingController.text, - image, - textEditingController, - ); - return null; - }, - ), - }, - child: TextField( - readOnly: controller.readonly, - controller: textEditingController, - autocorrect: false, - minLines: 1, - maxLines: 1, - decoration: InputDecoration( - hintText: L10n.of(context)!.emoteShortcode, - prefixText: ': ', - suffixText: ':', - prefixStyle: TextStyle( - color: Theme.of(context) - .colorScheme - .secondary, - fontWeight: FontWeight.bold, - ), - suffixStyle: TextStyle( - color: Theme.of(context) - .colorScheme - .secondary, - fontWeight: FontWeight.bold, - ), - border: InputBorder.none, + child: TextField( + readOnly: controller.readonly, + controller: textEditingController, + autocorrect: false, + minLines: 1, + maxLines: 1, + decoration: InputDecoration( + hintText: L10n.of(context)!.emoteShortcode, + prefixText: ': ', + suffixText: ':', + prefixStyle: TextStyle( + color: + Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, ), - onSubmitted: (s) => - controller.submitImageAction( - imageCode, - s, - image, - textEditingController, + suffixStyle: TextStyle( + color: + Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, ), + border: InputBorder.none, + ), + onSubmitted: (s) => + controller.submitImageAction( + imageCode, + s, + image, + textEditingController, ), ), ), ), - title: _EmoteImage(image.url), - trailing: controller.readonly - ? null - : InkWell( - onTap: () => - controller.removeImageAction(imageCode), - child: const Icon( - Icons.delete_outlined, - color: Colors.red, - size: 32.0, - ), + ), + title: _EmoteImage(image.url), + trailing: controller.readonly + ? null + : InkWell( + onTap: () => + controller.removeImageAction(imageCode), + child: const Icon( + Icons.delete_outlined, + color: Colors.red, + size: 32.0, ), - ); - }, - ), - ), + ), + ); + }, + ), ], ), ), diff --git a/lib/pages/settings_notifications/settings_notifications_view.dart b/lib/pages/settings_notifications/settings_notifications_view.dart index 57160da4..5618c429 100644 --- a/lib/pages/settings_notifications/settings_notifications_view.dart +++ b/lib/pages/settings_notifications/settings_notifications_view.dart @@ -23,7 +23,6 @@ class SettingsNotificationsView extends StatelessWidget { title: Text(L10n.of(context)!.notifications), ), body: MaxWidthBody( - withScrolling: true, child: StreamBuilder( stream: Matrix.of(context) .client diff --git a/lib/pages/settings_security/settings_security_view.dart b/lib/pages/settings_security/settings_security_view.dart index bce60726..fc241a85 100644 --- a/lib/pages/settings_security/settings_security_view.dart +++ b/lib/pages/settings_security/settings_security_view.dart @@ -20,7 +20,6 @@ class SettingsSecurityView extends StatelessWidget { body: ListTileTheme( iconColor: Theme.of(context).colorScheme.onBackground, child: MaxWidthBody( - withScrolling: true, child: Column( children: [ ListTile( diff --git a/lib/pages/settings_style/settings_style_view.dart b/lib/pages/settings_style/settings_style_view.dart index c74176fb..912abb06 100644 --- a/lib/pages/settings_style/settings_style_view.dart +++ b/lib/pages/settings_style/settings_style_view.dart @@ -24,7 +24,6 @@ class SettingsStyleView extends StatelessWidget { ), backgroundColor: Theme.of(context).colorScheme.surface, body: MaxWidthBody( - withScrolling: true, child: Column( children: [ ListTile( diff --git a/lib/widgets/fluffy_chat_app.dart b/lib/widgets/fluffy_chat_app.dart index a8013d48..7207faf9 100644 --- a/lib/widgets/fluffy_chat_app.dart +++ b/lib/widgets/fluffy_chat_app.dart @@ -36,8 +36,9 @@ class FluffyChatApp extends StatelessWidget { builder: (context, themeMode, primaryColor) => MaterialApp.router( title: AppConfig.applicationName, themeMode: themeMode, - theme: FluffyThemes.buildTheme(Brightness.light, primaryColor), - darkTheme: FluffyThemes.buildTheme(Brightness.dark, primaryColor), + theme: FluffyThemes.buildTheme(context, Brightness.light, primaryColor), + darkTheme: + FluffyThemes.buildTheme(context, Brightness.dark, primaryColor), scrollBehavior: CustomScrollBehavior(), localizationsDelegates: L10n.localizationsDelegates, supportedLocales: L10n.supportedLocales, diff --git a/lib/widgets/layouts/max_width_body.dart b/lib/widgets/layouts/max_width_body.dart index d813ce2b..31f1e7be 100644 --- a/lib/widgets/layouts/max_width_body.dart +++ b/lib/widgets/layouts/max_width_body.dart @@ -2,15 +2,19 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:fluffychat/config/app_config.dart'; + class MaxWidthBody extends StatelessWidget { final Widget? child; final double maxWidth; + final bool withFrame; final bool withScrolling; const MaxWidthBody({ this.child, this.maxWidth = 600, - this.withScrolling = false, + this.withFrame = true, + this.withScrolling = true, Key? key, }) : super(key: key); @override @@ -18,21 +22,33 @@ class MaxWidthBody extends StatelessWidget { return SafeArea( child: LayoutBuilder( builder: (context, constraints) { + final paddingVal = max(0, (constraints.maxWidth - maxWidth) / 2); + final hasPadding = paddingVal > 0; final padding = EdgeInsets.symmetric( + vertical: hasPadding ? 32 : 0, horizontal: max(0, (constraints.maxWidth - maxWidth) / 2), ); - return withScrolling - ? SingleChildScrollView( - physics: const ScrollPhysics(), - child: Padding( - padding: padding, + final childWithPadding = Padding( + padding: padding, + child: withFrame && hasPadding + ? Material( + elevation: + Theme.of(context).appBarTheme.scrolledUnderElevation ?? + 4, + clipBehavior: Clip.hardEdge, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + shadowColor: Theme.of(context).appBarTheme.shadowColor, child: child, - ), - ) - : Padding( - padding: padding, - child: child, - ); + ) + : child, + ); + if (!withScrolling) return childWithPadding; + return SingleChildScrollView( + physics: const ScrollPhysics(), + child: childWithPadding, + ); }, ), ); diff --git a/lib/widgets/layouts/side_view_layout.dart b/lib/widgets/layouts/side_view_layout.dart deleted file mode 100644 index 0370a0c0..00000000 --- a/lib/widgets/layouts/side_view_layout.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:fluffychat/config/themes.dart'; - -class SideViewLayout extends StatelessWidget { - final Widget mainView; - final Widget sideView; - final bool hideSideView; - - const SideViewLayout({ - Key? key, - required this.mainView, - required this.sideView, - required this.hideSideView, - }) : super(key: key); - @override - Widget build(BuildContext context) { - final sideView = this.sideView; - const sideViewWidth = 360.0; - final threeColumnMode = FluffyThemes.isThreeColumnMode(context); - return Stack( - children: [ - AnimatedPositioned( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - top: 0, - left: 0, - bottom: 0, - right: !threeColumnMode || hideSideView ? 0 : sideViewWidth, - child: ClipRRect(child: mainView), - ), - AnimatedPositioned( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - bottom: 0, - top: 0, - right: 0, - left: !threeColumnMode && !hideSideView ? 0 : null, - width: hideSideView - ? 0 - : !threeColumnMode - ? null - : sideViewWidth, - child: hideSideView - ? const SizedBox.shrink() - : Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - border: Border( - left: BorderSide( - color: Theme.of(context).dividerColor, - ), - ), - ), - child: sideView, - ), - ), - ], - ); - } -} diff --git a/lib/widgets/lock_screen.dart b/lib/widgets/lock_screen.dart index 2b5473f2..70db44e0 100644 --- a/lib/widgets/lock_screen.dart +++ b/lib/widgets/lock_screen.dart @@ -25,8 +25,8 @@ class LockScreenState extends State { @override Widget build(BuildContext context) { return MaterialApp( - theme: FluffyThemes.buildTheme(Brightness.light), - darkTheme: FluffyThemes.buildTheme(Brightness.dark), + theme: FluffyThemes.buildTheme(context, Brightness.light), + darkTheme: FluffyThemes.buildTheme(context, Brightness.dark), localizationsDelegates: L10n.localizationsDelegates, supportedLocales: L10n.supportedLocales, home: Builder(