diff --git a/lib/config/routes.dart b/lib/config/routes.dart index b839dd3f3..f12b27fdd 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -20,7 +20,6 @@ import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart' import 'package:fluffychat/pages/login/login.dart'; import 'package:fluffychat/pages/new_group/new_group.dart'; import 'package:fluffychat/pages/new_private_chat/new_private_chat.dart'; -import 'package:fluffychat/pages/new_space/new_space.dart'; import 'package:fluffychat/pages/settings/settings.dart'; import 'package:fluffychat/pages/settings_3pid/settings_3pid.dart'; import 'package:fluffychat/pages/settings_chat/settings_chat.dart'; @@ -163,7 +162,7 @@ abstract class AppRoutes { pageBuilder: (context, state) => defaultPageBuilder( context, state, - const NewSpace(), + const NewGroup(createGroupType: CreateGroupType.space), ), redirect: loggedOutRedirect, ), diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 22b4e5186..22868fa54 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -98,10 +98,6 @@ class ChatListController extends State StreamSubscription? _intentUriStreamSubscription; - void createNewSpace() { - context.push('/rooms/newspace'); - } - ActiveFilter activeFilter = AppConfig.separateChatTypes ? ActiveFilter.messages : ActiveFilter.allChats; diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 543df5a21..1fb0fc8a4 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -157,7 +157,12 @@ class ChatListItem extends StatelessWidget { right: 0, child: Avatar( border: space == null - ? null + ? room.isSpace + ? BorderSide( + width: 1, + color: theme.dividerColor, + ) + : null : BorderSide( width: 2, color: backgroundColor ?? @@ -251,11 +256,6 @@ class ChatListItem extends StatelessWidget { ), ), ), - if (room.isSpace) - const Icon( - Icons.arrow_circle_right_outlined, - size: 18, - ), ], ), subtitle: Row( diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 921ef6b83..041b83826 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -38,16 +38,6 @@ class ClientChooserButton extends StatelessWidget { ], ), ), - PopupMenuItem( - value: SettingsAction.newSpace, - child: Row( - children: [ - const Icon(Icons.workspaces_outlined), - const SizedBox(width: 18), - Text(L10n.of(context).createNewSpace), - ], - ), - ), PopupMenuItem( value: SettingsAction.setStatus, child: Row( @@ -260,9 +250,6 @@ class ClientChooserButton extends StatelessWidget { case SettingsAction.newGroup: context.go('/rooms/newgroup'); break; - case SettingsAction.newSpace: - controller.createNewSpace(); - break; case SettingsAction.invite: FluffyShare.shareInviteLink(context); break; @@ -352,7 +339,6 @@ class ClientChooserButton extends StatelessWidget { enum SettingsAction { addAccount, newGroup, - newSpace, setStatus, invite, settings, diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index 31beb6779..1fc59b18e 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -4,13 +4,18 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart' as sdk; +import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/new_group/new_group_view.dart'; import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/widgets/matrix.dart'; class NewGroup extends StatefulWidget { - const NewGroup({super.key}); + final CreateGroupType createGroupType; + const NewGroup({ + this.createGroupType = CreateGroupType.group, + super.key, + }); @override NewGroupController createState() => NewGroupController(); @@ -30,6 +35,14 @@ class NewGroupController extends State { bool loading = false; + CreateGroupType get createGroupType => + _createGroupType ?? widget.createGroupType; + + CreateGroupType? _createGroupType; + + void setCreateGroupType(Set b) => + setState(() => _createGroupType = b.single); + void setPublicGroup(bool b) => setState(() => publicGroup = b); void setGroupCanBeFound(bool b) => setState(() => groupCanBeFound = b); @@ -48,6 +61,52 @@ class NewGroupController extends State { }); } + Future _createGroup() async { + if (!mounted) return; + final roomId = await Matrix.of(context).client.createGroupChat( + visibility: + groupCanBeFound ? sdk.Visibility.public : sdk.Visibility.private, + preset: publicGroup + ? sdk.CreateRoomPreset.publicChat + : sdk.CreateRoomPreset.privateChat, + groupName: nameController.text.isNotEmpty ? nameController.text : null, + initialState: [ + if (avatar != null) + sdk.StateEvent( + type: sdk.EventTypes.RoomAvatar, + content: {'url': avatarUrl.toString()}, + ), + ], + ); + if (!mounted) return; + context.go('/rooms/$roomId/invite'); + } + + Future _createSpace() async { + if (!mounted) return; + final spaceId = await Matrix.of(context).client.createRoom( + preset: publicGroup + ? sdk.CreateRoomPreset.publicChat + : sdk.CreateRoomPreset.privateChat, + creationContent: {'type': RoomCreationTypes.mSpace}, + visibility: publicGroup ? sdk.Visibility.public : null, + roomAliasName: publicGroup + ? nameController.text.trim().toLowerCase().replaceAll(' ', '_') + : null, + name: nameController.text.trim(), + powerLevelContentOverride: {'events_default': 100}, + initialState: [ + if (avatar != null) + sdk.StateEvent( + type: sdk.EventTypes.RoomAvatar, + content: {'url': avatarUrl.toString()}, + ), + ], + ); + if (!mounted) return; + context.pop(spaceId); + } + void submitAction([_]) async { final client = Matrix.of(context).client; @@ -62,23 +121,12 @@ class NewGroupController extends State { if (!mounted) return; - final roomId = await client.createGroupChat( - visibility: - groupCanBeFound ? sdk.Visibility.public : sdk.Visibility.private, - preset: publicGroup - ? sdk.CreateRoomPreset.publicChat - : sdk.CreateRoomPreset.privateChat, - groupName: nameController.text.isNotEmpty ? nameController.text : null, - initialState: [ - if (avatar != null) - sdk.StateEvent( - type: sdk.EventTypes.RoomAvatar, - content: {'url': avatarUrl.toString()}, - ), - ], - ); - if (!mounted) return; - context.go('/rooms/$roomId/invite'); + switch (createGroupType) { + case CreateGroupType.group: + await _createGroup(); + case CreateGroupType.space: + await _createSpace(); + } } catch (e, s) { sdk.Logs().d('Unable to create group', e, s); setState(() { @@ -91,3 +139,5 @@ class NewGroupController extends State { @override Widget build(BuildContext context) => NewGroupView(this); } + +enum CreateGroupType { group, space } diff --git a/lib/pages/new_group/new_group_view.dart b/lib/pages/new_group/new_group_view.dart index 932fd2958..f4cfe3464 100644 --- a/lib/pages/new_group/new_group_view.dart +++ b/lib/pages/new_group/new_group_view.dart @@ -26,12 +26,33 @@ class NewGroupView extends StatelessWidget { onPressed: controller.loading ? null : Navigator.of(context).pop, ), ), - title: Text(L10n.of(context).createGroup), + title: Text( + controller.createGroupType == CreateGroupType.space + ? L10n.of(context).newSpace + : L10n.of(context).createGroup, + ), ), body: MaxWidthBody( child: Column( mainAxisSize: MainAxisSize.min, children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: SegmentedButton( + selected: {controller.createGroupType}, + onSelectionChanged: controller.setCreateGroupType, + segments: [ + ButtonSegment( + value: CreateGroupType.group, + label: Text(L10n.of(context).group), + ), + ButtonSegment( + value: CreateGroupType.space, + label: Text(L10n.of(context).space), + ), + ], + ), + ), const SizedBox(height: 16), InkWell( borderRadius: BorderRadius.circular(90), @@ -44,8 +65,8 @@ class NewGroupView extends StatelessWidget { borderRadius: BorderRadius.circular(90), child: Image.memory( avatar, - width: Avatar.defaultSize, - height: Avatar.defaultSize, + width: Avatar.defaultSize * 2, + height: Avatar.defaultSize * 2, fit: BoxFit.cover, ), ), @@ -53,7 +74,7 @@ class NewGroupView extends StatelessWidget { ), const SizedBox(height: 32), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), + padding: const EdgeInsets.symmetric(horizontal: 24.0), child: TextField( autofocus: true, controller: controller.nameController, @@ -61,7 +82,9 @@ class NewGroupView extends StatelessWidget { readOnly: controller.loading, decoration: InputDecoration( prefixIcon: const Icon(Icons.people_outlined), - labelText: L10n.of(context).groupName, + labelText: controller.createGroupType == CreateGroupType.space + ? L10n.of(context).spaceName + : L10n.of(context).groupName, ), ), ), @@ -69,12 +92,17 @@ class NewGroupView extends StatelessWidget { SwitchListTile.adaptive( contentPadding: const EdgeInsets.symmetric(horizontal: 32), secondary: const Icon(Icons.public_outlined), - title: Text(L10n.of(context).groupIsPublic), + title: Text( + controller.createGroupType == CreateGroupType.space + ? L10n.of(context).spaceIsPublic + : L10n.of(context).groupIsPublic, + ), value: controller.publicGroup, onChanged: controller.loading ? null : controller.setPublicGroup, ), AnimatedSize( duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, child: controller.publicGroup ? SwitchListTile.adaptive( contentPadding: @@ -88,20 +116,42 @@ class NewGroupView extends StatelessWidget { ) : const SizedBox.shrink(), ), - SwitchListTile.adaptive( - contentPadding: const EdgeInsets.symmetric(horizontal: 32), - secondary: Icon( - Icons.lock_outlined, - color: theme.colorScheme.onSurface, - ), - title: Text( - L10n.of(context).enableEncryption, - style: TextStyle( - color: theme.colorScheme.onSurface, - ), - ), - value: !controller.publicGroup, - onChanged: null, + AnimatedSize( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + child: controller.createGroupType == CreateGroupType.space + ? const SizedBox.shrink() + : SwitchListTile.adaptive( + contentPadding: + const EdgeInsets.symmetric(horizontal: 32), + secondary: Icon( + Icons.lock_outlined, + color: theme.colorScheme.onSurface, + ), + title: Text( + L10n.of(context).enableEncryption, + style: TextStyle( + color: theme.colorScheme.onSurface, + ), + ), + value: !controller.publicGroup, + onChanged: null, + ), + ), + AnimatedSize( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + child: controller.createGroupType == CreateGroupType.space + ? ListTile( + contentPadding: + const EdgeInsets.symmetric(horizontal: 32), + trailing: const Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0), + child: Icon(Icons.info_outlined), + ), + subtitle: Text(L10n.of(context).newSpaceDescription), + ) + : const SizedBox.shrink(), ), Padding( padding: const EdgeInsets.all(16.0), @@ -112,12 +162,17 @@ class NewGroupView extends StatelessWidget { controller.loading ? null : controller.submitAction, child: controller.loading ? const LinearProgressIndicator() - : Text(L10n.of(context).createGroupAndInviteUsers), + : Text( + controller.createGroupType == CreateGroupType.space + ? L10n.of(context).createNewSpace + : L10n.of(context).createGroupAndInviteUsers, + ), ), ), ), AnimatedSize( duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, child: error == null ? const SizedBox.shrink() : ListTile( diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart deleted file mode 100644 index 12bb4cc17..000000000 --- a/lib/pages/new_space/new_space.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'dart:typed_data'; - -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' as sdk; -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/pages/new_space/new_space_view.dart'; -import 'package:fluffychat/utils/file_selector.dart'; -import 'package:fluffychat/utils/localized_exception_extension.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class NewSpace extends StatefulWidget { - const NewSpace({super.key}); - - @override - NewSpaceController createState() => NewSpaceController(); -} - -class NewSpaceController extends State { - TextEditingController nameController = TextEditingController(); - TextEditingController topicController = TextEditingController(); - bool publicGroup = false; - bool loading = false; - String? nameError; - String? topicError; - - Uint8List? avatar; - - Uri? avatarUrl; - - void selectPhoto() async { - final photo = await selectFiles( - context, - type: FileSelectorType.images, - ); - final bytes = await photo.firstOrNull?.readAsBytes(); - setState(() { - avatarUrl = null; - avatar = bytes; - }); - } - - void setPublicGroup(bool b) => setState(() => publicGroup = b); - - void submitAction([_]) async { - final client = Matrix.of(context).client; - setState(() { - nameError = topicError = null; - }); - if (nameController.text.isEmpty) { - setState(() { - nameError = L10n.of(context).pleaseChoose; - }); - return; - } - setState(() { - loading = true; - }); - try { - final avatar = this.avatar; - avatarUrl ??= avatar == null ? null : await client.uploadContent(avatar); - - final spaceId = await client.createRoom( - preset: publicGroup - ? sdk.CreateRoomPreset.publicChat - : sdk.CreateRoomPreset.privateChat, - creationContent: {'type': RoomCreationTypes.mSpace}, - visibility: publicGroup ? sdk.Visibility.public : null, - roomAliasName: publicGroup - ? nameController.text.trim().toLowerCase().replaceAll(' ', '_') - : null, - name: nameController.text.trim(), - topic: topicController.text.isEmpty ? null : topicController.text, - powerLevelContentOverride: {'events_default': 100}, - initialState: [ - if (avatar != null) - sdk.StateEvent( - type: sdk.EventTypes.RoomAvatar, - content: {'url': avatarUrl.toString()}, - ), - ], - ); - if (!mounted) return; - context.pop(spaceId); - } catch (e) { - setState(() { - topicError = e.toLocalizedString(context); - }); - } finally { - setState(() { - loading = false; - }); - } - // TODO: Go to spaces - } - - @override - Widget build(BuildContext context) => NewSpaceView(this); -} diff --git a/lib/pages/new_space/new_space_view.dart b/lib/pages/new_space/new_space_view.dart deleted file mode 100644 index 0f5813952..000000000 --- a/lib/pages/new_space/new_space_view.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -import 'package:fluffychat/widgets/avatar.dart'; -import 'package:fluffychat/widgets/layouts/max_width_body.dart'; -import 'new_space.dart'; - -class NewSpaceView extends StatelessWidget { - final NewSpaceController controller; - - const NewSpaceView(this.controller, {super.key}); - - @override - Widget build(BuildContext context) { - final avatar = controller.avatar; - return Scaffold( - appBar: AppBar( - title: Text(L10n.of(context).createNewSpace), - ), - body: MaxWidthBody( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 16), - InkWell( - borderRadius: BorderRadius.circular(90), - onTap: controller.loading ? null : controller.selectPhoto, - child: CircleAvatar( - radius: Avatar.defaultSize, - child: avatar == null - ? const Icon(Icons.add_a_photo_outlined) - : ClipRRect( - borderRadius: BorderRadius.circular(90), - child: Image.memory( - avatar, - width: Avatar.defaultSize, - height: Avatar.defaultSize, - fit: BoxFit.cover, - ), - ), - ), - ), - const SizedBox(height: 32), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: TextField( - autofocus: true, - controller: controller.nameController, - autocorrect: false, - readOnly: controller.loading, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.people_outlined), - labelText: L10n.of(context).spaceName, - errorText: controller.nameError, - ), - ), - ), - const SizedBox(height: 16), - SwitchListTile.adaptive( - contentPadding: const EdgeInsets.symmetric(horizontal: 32), - title: Text(L10n.of(context).spaceIsPublic), - value: controller.publicGroup, - onChanged: controller.setPublicGroup, - ), - ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 32), - trailing: const Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0), - child: Icon(Icons.info_outlined), - ), - subtitle: Text(L10n.of(context).newSpaceDescription), - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: - controller.loading ? null : controller.submitAction, - child: controller.loading - ? const LinearProgressIndicator() - : Text(L10n.of(context).createNewSpace), - ), - ), - ), - ], - ), - ), - ); - } -}