From 7223581b97ab0eec191b32fe2df6fe1f05af652d Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 19 Nov 2023 14:10:28 +0100 Subject: [PATCH] feat: New UX design for create group chat --- assets/l10n/intl_en.arb | 7 +- lib/pages/new_group/new_group.dart | 93 +++++++++--- lib/pages/new_group/new_group_view.dart | 137 ++++++++++++++++-- .../new_private_chat_view.dart | 25 ++-- 4 files changed, 216 insertions(+), 46 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 05e8d861..b8498fb4 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -76,7 +76,7 @@ "mxid": {} } }, - "addChatDescription": "Add a chat description", + "addChatDescription": "Add a chat description...", "addToSpace": "Add to space", "@addToSpace": {}, "admin": "Admin", @@ -2559,5 +2559,8 @@ "query": {} } }, - "searchChatsRooms": "Search for #chats, @users..." + "searchChatsRooms": "Search for #chats, @users...", + "groupName": "Group name", + "createGroupAndInviteUsers": "Create a group and invite users", + "groupCanBeFoundViaSearch": "Group can be found via search" } diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index d00a71e8..584a3f88 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -1,6 +1,8 @@ +import 'dart:typed_data'; + import 'package:flutter/material.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart' as sdk; @@ -15,29 +17,86 @@ class NewGroup extends StatefulWidget { } class NewGroupController extends State { - TextEditingController controller = TextEditingController(); + TextEditingController nameController = TextEditingController(); + + TextEditingController topicController = TextEditingController(); + bool publicGroup = false; + bool groupCanBeFound = true; + + Uint8List? avatar; + + Uri? avatarUrl; + + Object? error; + + bool loading = false; void setPublicGroup(bool b) => setState(() => publicGroup = b); + void setGroupCanBeFound(bool b) => setState(() => groupCanBeFound = b); + + void selectPhoto() async { + final photo = await FilePicker.platform.pickFiles( + type: FileType.image, + allowMultiple: false, + withData: true, + ); + + setState(() { + avatarUrl = null; + avatar = photo?.files.singleOrNull?.bytes; + }); + } + void submitAction([_]) async { final client = Matrix.of(context).client; - final roomID = await showFutureLoadingDialog( - context: context, - future: () async { - final roomId = await client.createGroupChat( - visibility: - publicGroup ? sdk.Visibility.public : sdk.Visibility.private, - preset: publicGroup - ? sdk.CreateRoomPreset.publicChat - : sdk.CreateRoomPreset.privateChat, - groupName: controller.text.isNotEmpty ? controller.text : null, + + try { + setState(() { + loading = true; + error = null; + }); + + final avatar = this.avatar; + avatarUrl ??= avatar == null ? null : await client.uploadContent(avatar); + + if (!mounted) return; + + final roomId = await client.createGroupChat( + visibility: + publicGroup ? sdk.Visibility.public : sdk.Visibility.private, + preset: publicGroup + ? sdk.CreateRoomPreset.publicChat + : sdk.CreateRoomPreset.privateChat, + groupName: nameController.text.isNotEmpty ? nameController.text : null, + initialState: [ + if (topicController.text.isNotEmpty) + sdk.StateEvent( + type: sdk.EventTypes.RoomTopic, + content: {'topic': topicController.text}, + ), + if (avatar != null) + sdk.StateEvent( + type: sdk.EventTypes.RoomAvatar, + content: {'url': avatarUrl.toString()}, + ), + ], + ); + if (!mounted) return; + if (publicGroup && groupCanBeFound) { + await client.setRoomVisibilityOnDirectory( + roomId, + visibility: sdk.Visibility.public, ); - return roomId; - }, - ); - if (roomID.error == null) { - context.go('/rooms/${roomID.result!}/invite'); + } + context.go('/rooms/$roomId/invite'); + } catch (e, s) { + sdk.Logs().d('Unable to create group', e, s); + setState(() { + error = e; + loading = false; + }); } } diff --git a/lib/pages/new_group/new_group_view.dart b/lib/pages/new_group/new_group_view.dart index ee947e6f..34d44365 100644 --- a/lib/pages/new_group/new_group_view.dart +++ b/lib/pages/new_group/new_group_view.dart @@ -2,7 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/new_group/new_group.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; class NewGroupView extends StatelessWidget { @@ -12,48 +15,150 @@ class NewGroupView extends StatelessWidget { @override Widget build(BuildContext context) { + final avatar = controller.avatar; + final error = controller.error; return Scaffold( appBar: AppBar( + leading: Center( + child: BackButton( + onPressed: controller.loading ? null : Navigator.of(context).pop, + ), + ), title: Text(L10n.of(context)!.createGroup), ), body: MaxWidthBody( child: Column( mainAxisSize: MainAxisSize.min, children: [ + const SizedBox(height: 16), Padding( - padding: const EdgeInsets.all(12.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + InkWell( + borderRadius: BorderRadius.circular(90), + onTap: controller.loading ? null : controller.selectPhoto, + child: CircleAvatar( + radius: Avatar.defaultSize / 2, + child: avatar == null + ? const Icon(Icons.camera_alt_outlined) + : Image.memory( + avatar, + width: Avatar.defaultSize, + height: Avatar.defaultSize, + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: TextField( + controller: controller.nameController, + autocorrect: false, + readOnly: controller.loading, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.people_outlined), + hintText: L10n.of(context)!.groupName, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), child: TextField( - controller: controller.controller, - autofocus: true, - autocorrect: false, - textInputAction: TextInputAction.go, - onSubmitted: controller.submitAction, + controller: controller.topicController, + minLines: 4, + maxLines: 4, + maxLength: 255, + readOnly: controller.loading, decoration: InputDecoration( - labelText: L10n.of(context)!.optionalGroupName, - prefixIcon: const Icon(Icons.people_outlined), - hintText: L10n.of(context)!.enterAGroupName, + hintText: L10n.of(context)!.addChatDescription, ), ), ), + const SizedBox(height: 16), SwitchListTile.adaptive( secondary: const Icon(Icons.public_outlined), title: Text(L10n.of(context)!.groupIsPublic), value: controller.publicGroup, - onChanged: controller.setPublicGroup, + onChanged: controller.loading ? null : controller.setPublicGroup, + ), + AnimatedSize( + duration: FluffyThemes.animationDuration, + child: controller.publicGroup + ? SwitchListTile.adaptive( + secondary: const Icon(Icons.search_outlined), + title: Text(L10n.of(context)!.groupCanBeFoundViaSearch), + value: controller.groupCanBeFound, + onChanged: controller.loading + ? null + : controller.setGroupCanBeFound, + ) + : const SizedBox.shrink(), ), SwitchListTile.adaptive( - secondary: const Icon(Icons.lock_outlined), - title: Text(L10n.of(context)!.enableEncryption), + secondary: Icon( + Icons.lock_outlined, + color: Theme.of(context).colorScheme.onBackground, + ), + title: Text( + L10n.of(context)!.enableEncryption, + style: TextStyle( + color: Theme.of(context).colorScheme.onBackground, + ), + ), value: !controller.publicGroup, onChanged: null, ), + Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.onPrimary, + backgroundColor: Theme.of(context).colorScheme.primary, + ), + onPressed: + controller.loading ? null : controller.submitAction, + child: controller.loading + ? const LinearProgressIndicator() + : Row( + children: [ + Expanded( + child: Text( + L10n.of(context)!.createGroupAndInviteUsers, + ), + ), + Icon(Icons.adaptive.arrow_forward_outlined), + ], + ), + ), + ), + ), + AnimatedSize( + duration: FluffyThemes.animationDuration, + child: error == null + ? const SizedBox.shrink() + : ListTile( + leading: Icon( + Icons.warning_outlined, + color: Theme.of(context).colorScheme.error, + ), + title: Text( + error.toLocalizedString(context), + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ), + ), + ), ], ), ), - floatingActionButton: FloatingActionButton( - onPressed: controller.submitAction, - child: const Icon(Icons.arrow_forward_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 619145ee..bc38b012 100644 --- a/lib/pages/new_private_chat/new_private_chat_view.dart +++ b/lib/pages/new_private_chat/new_private_chat_view.dart @@ -11,6 +11,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/new_private_chat/new_private_chat.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; @@ -28,6 +29,7 @@ class NewPrivateChatView extends StatelessWidget { min(MediaQuery.of(context).size.width - 16, 256).toDouble(); return Scaffold( appBar: AppBar( + scrolledUnderElevation: Theme.of(context).appBarTheme.elevation, leading: const Center(child: BackButton()), title: Text(L10n.of(context)!.newChat), backgroundColor: Theme.of(context).scaffoldBackgroundColor, @@ -121,18 +123,18 @@ class NewPrivateChatView extends StatelessWidget { onPressed: controller.copyUserId, ), ), - //if (PlatformInfos.isMobile) - ListTile( - leading: CircleAvatar( - backgroundColor: - Theme.of(context).colorScheme.primaryContainer, - foregroundColor: - Theme.of(context).colorScheme.onPrimaryContainer, - child: const Icon(Icons.qr_code_scanner_outlined), + if (PlatformInfos.isMobile) + ListTile( + leading: CircleAvatar( + backgroundColor: + Theme.of(context).colorScheme.primaryContainer, + foregroundColor: + Theme.of(context).colorScheme.onPrimaryContainer, + child: const Icon(Icons.qr_code_scanner_outlined), + ), + title: Text(L10n.of(context)!.scanQrCode), + onTap: controller.openScannerAction, ), - title: Text(L10n.of(context)!.scanQrCode), - onTap: controller.openScannerAction, - ), ListTile( leading: CircleAvatar( backgroundColor: @@ -234,6 +236,7 @@ class NewPrivateChatView extends StatelessWidget { leading: Avatar( name: displayname, mxContent: contact.avatarUrl, + presenceUserId: contact.userId, ), title: Text(displayname), subtitle: Text(contact.userId),