From 249538c20b0d445475626123bf49a3dd4956570d Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Mon, 2 Jun 2025 14:48:39 -0400 Subject: [PATCH] feat: redesign of space access page (#2903) --- assets/l10n/intl_en.arb | 15 +- lib/config/routes.dart | 21 +- lib/pages/chat/chat.dart | 2 +- .../chat_access_settings_controller.dart | 21 +- lib/pages/chat_details/chat_details.dart | 97 ++------ lib/pages/chat_list/chat_list.dart | 81 +++---- lib/pages/chat_list/chat_list_view.dart | 36 +-- lib/pages/chat_list/space_view.dart | 3 +- .../invitation_selection_view.dart | 9 +- lib/pages/new_group/new_group.dart | 25 +- lib/pages/new_group/new_group_view.dart | 41 ++-- .../new_private_chat_view.dart | 44 ++-- .../activity_room_selection.dart | 8 +- .../chat/constants/default_power_level.dart | 134 ++++++----- .../utils/chat_list_handle_space_tap.dart | 2 +- .../pages/pangea_chat_access_settings.dart | 221 ++++++++++++++++++ .../pages/pangea_chat_details.dart | 4 +- .../widgets/space_invite_buttons.dart | 2 +- .../widgets/visibility_toggle.dart | 149 ------------ .../common/controllers/pangea_controller.dart | 8 +- .../extensions/pangea_room_extension.dart | 1 - .../room_children_and_parents_extension.dart | 33 ++- .../room_space_settings_extension.dart | 11 +- .../spaces/utils/client_spaces_extension.dart | 103 ++++++-- .../spaces/widgets/add_room_dialog.dart | 141 ----------- lib/utils/fluffy_share.dart | 12 +- lib/widgets/adaptive_dialogs/user_dialog.dart | 7 +- lib/widgets/future_loading_dialog.dart | 4 +- 28 files changed, 617 insertions(+), 618 deletions(-) create mode 100644 lib/pangea/chat_settings/pages/pangea_chat_access_settings.dart delete mode 100644 lib/pangea/chat_settings/widgets/visibility_toggle.dart delete mode 100644 lib/pangea/spaces/widgets/add_room_dialog.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 8628fc059..54e658afb 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4962,5 +4962,18 @@ "access": "Access", "addSubspace": "Add subspace", "botSettings": "Bot settings", - "activitySuggestionTimeoutMessage": "We are working hard to generate activties for you, please check back in a minute" + "activitySuggestionTimeoutMessage": "We are working hard to generate activties for you, please check back in a minute", + "accessSettingsWarning": "Oops! It looks like you don't have permission to set the Access rules of this room. You should check these to make sure they're what you need and talk to a room admin if you need to change them", + "howSpaceCanBeFound": "How this space can be found", + "private": "Private", + "cannotBeFoundInSearch": "Cannot be found in search", + "public": "Public", + "visibleToCommunity": "Visible to the broader Pangea Chat community via \"Find your people\"", + "howSpaceCanBeJoined": "How this space can be joined", + "restricted": "Restricted", + "canBeFoundVia": "Can be found via:", + "canBeFoundViaInvitation": "\u2022 invitation", + "canBeFoundViaCodeOrLink": "\u2022 code or link", + "canBeFoundViaKnock": "\u2022 request to join and admin approval", + "anyoneCanJoin": "Anyone can join! However, admin can kick and ban whoever misbehaves. Those who are banned may not return!" } diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 1438efa58..ec16f5c64 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -202,7 +202,6 @@ abstract class AppRoutes { activeChat: state.pathParameters['roomid'], // #Pangea activeSpaceId: state.uri.queryParameters['spaceId'], - activeFilter: state.uri.queryParameters['filter'], // Pangea# displayNavigationRail: state.path?.startsWith('/rooms/settings') != true, @@ -245,7 +244,6 @@ abstract class AppRoutes { activeChat: state.pathParameters['roomid'], // #Pangea activeSpaceId: state.uri.queryParameters['spaceId'], - activeFilter: state.uri.queryParameters['filter'], // Pangea# ), ), @@ -287,22 +285,12 @@ abstract class AppRoutes { pageBuilder: (context, state) => defaultPageBuilder( context, state, - const NewGroup(), + // #Pangea + // const NewGroup(), + NewGroup(spaceId: state.uri.queryParameters['space']), + // Pangea# ), redirect: loggedOutRedirect, - // #Pangea - routes: [ - GoRoute( - path: ':spaceid', - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - NewGroup(spaceId: state.pathParameters['spaceid']!), - ), - redirect: loggedOutRedirect, - ), - ], - // Pangea# ), GoRoute( path: 'newspace', @@ -775,7 +763,6 @@ abstract class AppRoutes { mainView: ChatList( activeChat: state.pathParameters['roomid'], activeSpaceId: state.uri.queryParameters['spaceId'], - activeFilter: state.uri.queryParameters['filter'], displayNavigationRail: state.path?.startsWith('/rooms/settings') != true, ), diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index cc016a335..27a865cb0 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -901,7 +901,7 @@ class ChatController extends State pangeaEditingEvent = previousEdit; } - final spaceCode = room.classCode(context); + final spaceCode = room.classCode; if (spaceCode != null) { GoogleAnalytics.sendMessage( room.id, diff --git a/lib/pages/chat_access_settings/chat_access_settings_controller.dart b/lib/pages/chat_access_settings/chat_access_settings_controller.dart index 7dad6dfeb..fba78e2a8 100644 --- a/lib/pages/chat_access_settings/chat_access_settings_controller.dart +++ b/lib/pages/chat_access_settings/chat_access_settings_controller.dart @@ -4,7 +4,8 @@ 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_access_settings/chat_access_settings_page.dart'; +import 'package:fluffychat/pangea/chat_settings/pages/pangea_chat_access_settings.dart'; +import 'package:fluffychat/pangea/spaces/utils/client_spaces_extension.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; @@ -58,13 +59,22 @@ class ChatAccessSettingsController extends State { } void setJoinRule(JoinRules? newJoinRules) async { - if (newJoinRules == null) return; + // #Pangea + // if (newJoinRules == null) return; + if (newJoinRules == null || room.joinRules == newJoinRules) return; + // Pangea# setState(() { joinRulesLoading = true; }); try { - await room.setJoinRules(newJoinRules); + // #Pangea + // await room.setJoinRules(newJoinRules); + await room.client.pangeaSetJoinRules( + room.id, + newJoinRules.toString().replaceAll('JoinRules.', ''), + ); + // Pangea# } catch (e, s) { Logs().w('Unable to change join rules', e, s); if (mounted) { @@ -295,6 +305,9 @@ class ChatAccessSettingsController extends State { @override Widget build(BuildContext context) { - return ChatAccessSettingsPageView(this); + // #Pangea + // return ChatAccessSettingsPageView(this); + return PangeaChatAccessSettingsPageView(this); + // Pangea# } } diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index 5846241c5..fa178d409 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -6,18 +6,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:matrix/matrix.dart' as sdk; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/settings/settings.dart'; +import 'package:fluffychat/pangea/chat/constants/default_power_level.dart'; import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart'; import 'package:fluffychat/pangea/chat_settings/pages/pangea_chat_details.dart'; import 'package:fluffychat/pangea/chat_settings/utils/download_chat.dart'; import 'package:fluffychat/pangea/chat_settings/utils/download_file.dart'; +import 'package:fluffychat/pangea/common/constants/model_keys.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/spaces/utils/set_class_name.dart'; +import 'package:fluffychat/pangea/spaces/utils/space_code.dart'; import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart'; @@ -209,70 +211,6 @@ class ChatDetailsController extends State { // Pangea# // #Pangea - bool showEditNameIcon = false; - void hoverEditNameIcon(bool hovering) => - setState(() => showEditNameIcon = !showEditNameIcon); - - Future setJoinRules(JoinRules joinRules) async { - if (roomId == null) return; - final room = Matrix.of(context).client.getRoomById(roomId!); - if (room == null) return; - - final content = room.getState(EventTypes.RoomJoinRules)?.content ?? {}; - content['join_rule'] = joinRules.toString().replaceAll('JoinRules.', ''); - await showFutureLoadingDialog( - context: context, - future: () async { - await room.client.setRoomStateWithKey( - roomId!, - EventTypes.RoomJoinRules, - '', - content, - ); - }, - ); - } - - Future setVisibility(sdk.Visibility visibility) async { - if (roomId == null) return; - final room = Matrix.of(context).client.getRoomById(roomId!); - if (room == null) return; - - await showFutureLoadingDialog( - context: context, - future: () async { - await room.client.setRoomVisibilityOnDirectory( - room.id, - visibility: visibility, - ); - }, - ); - setState(() {}); - } - - Future toggleMute() async { - final client = Matrix.of(context).client; - final Room? room = client.getRoomById(roomId!); - if (room == null) return; - await showFutureLoadingDialog( - context: context, - future: () async { - await (room.pushRuleState == PushRuleState.notify - ? room.setPushRuleState(PushRuleState.mentionsOnly) - : room.setPushRuleState(PushRuleState.notify)); - }, - ); - - // wait for push rule update in sync - await client.onSync.stream.firstWhere( - (sync) => - sync.accountData != null && - sync.accountData!.isNotEmpty && - sync.accountData!.any((e) => e.type == 'm.push_rules'), - ); - if (mounted) setState(() {}); - } - void downloadChatAction() async { if (roomId == null) return; final Room? room = Matrix.of(context).client.getRoomById(roomId!); @@ -389,22 +327,37 @@ class ChatDetailsController extends State { ); if (names == null) return; final client = Matrix.of(context).client; - final result = await showFutureLoadingDialog( + await showFutureLoadingDialog( context: context, future: () async { final activeSpace = client.getRoomById(roomId!)!; await activeSpace.postLoad(); + final accessCode = await SpaceCodeUtil.generateSpaceCode(client); - final resp = await client.createSpace( + final resp = await client.createRoom( name: names, - visibility: activeSpace.joinRules == JoinRules.public - ? sdk.Visibility.public - : sdk.Visibility.private, + visibility: RoomDefaults.spaceChildVisibility, + creationContent: {'type': 'm.space'}, + initialState: [ + RoomDefaults.defaultSpacePowerLevels(client.userID!), + StateEvent( + type: EventTypes.RoomJoinRules, + content: { + 'join_rule': 'knock_restricted', + 'allow': [ + { + "type": "m.room_membership", + "room_id": roomId!, + } + ], + ModelKey.accessCode: accessCode, + }, + ), + ], ); - await activeSpace.pangeaSetSpaceChild(resp); + await activeSpace.addToSpace(resp); }, ); - if (result.error != null) return; } // Pangea# } diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index f7486b5cf..99fabc7d0 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -24,6 +24,7 @@ import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/spaces/utils/client_spaces_extension.dart'; import 'package:fluffychat/pangea/subscription/widgets/subscription_snackbar.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; @@ -83,7 +84,6 @@ class ChatList extends StatefulWidget { final String? activeChat; // #Pangea final String? activeSpaceId; - final String? activeFilter; // Pangea# final bool displayNavigationRail; @@ -92,7 +92,6 @@ class ChatList extends StatefulWidget { required this.activeChat, // #Pangea this.activeSpaceId, - this.activeFilter, // Pangea# this.displayNavigationRail = false, }); @@ -535,7 +534,7 @@ class ChatListController extends State // #Pangea final String? justInputtedCode = MatrixState.pangeaController.classController.justInputtedCode(); - final newSpaceCode = space?.classCode(context); + final newSpaceCode = space?.classCode; if (newSpaceCode?.toLowerCase() == justInputtedCode?.toLowerCase()) { return; } @@ -625,12 +624,6 @@ class ChatListController extends State _activeSpaceId = widget.activeSpaceId == 'clear' ? null : widget.activeSpaceId; - - if (widget.activeFilter == 'groups') { - activeFilter = AppConfig.separateChatTypes - ? ActiveFilter.groups - : ActiveFilter.allChats; - } // Pangea# super.initState(); @@ -640,15 +633,6 @@ class ChatListController extends State @override void didUpdateWidget(ChatList oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.activeFilter != oldWidget.activeFilter && - widget.activeFilter == 'groups') { - setActiveFilter( - AppConfig.separateChatTypes - ? ActiveFilter.groups - : ActiveFilter.allChats, - ); - } - if (widget.activeSpaceId != oldWidget.activeSpaceId && widget.activeSpaceId != null) { widget.activeSpaceId == 'clear' @@ -822,12 +806,11 @@ class ChatListController extends State ], ), ), - if (spacesWithPowerLevels.isNotEmpty - // #Pangea - && - !room.isSpace - // Pangea# - ) + // #Pangea + // if (spacesWithPowerLevels.isNotEmpty) + if (spacesWithPowerLevels.isNotEmpty && + room.canChangeStateEvent(EventTypes.SpaceParent)) + // Pangea# PopupMenuItem( value: ChatContextAction.addToSpace, child: Row( @@ -846,8 +829,11 @@ class ChatListController extends State // if the room has a parent for which the user has a high enough power level // to set parent's space child events, show option to remove the room from the space if (room.spaceParents.isNotEmpty && + room.canChangeStateEvent(EventTypes.SpaceParent) && room.pangeaSpaceParents.any( - (r) => r.canChangeStateEvent(EventTypes.SpaceChild), + (r) => + r.canChangeStateEvent(EventTypes.SpaceChild) && + r.id == activeSpaceId, ) && activeSpaceId != null) PopupMenuItem( @@ -1022,21 +1008,40 @@ class ChatListController extends State context: context, // #Pangea // future: () => space.setSpaceChild(room.id), - future: () => space.pangeaSetSpaceChild(room.id), + future: () => space.addToSpace(room.id), // Pangea# ); // #Pangea + try { + await space.client.setSpaceChildAccess(room.id); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context).accessSettingsWarning), + duration: const Duration(seconds: 10), + ), + ); + } return; case ChatContextAction.removeFromSpace: await showFutureLoadingDialog( context: context, future: () async { - final futures = room.pangeaSpaceParents - .where((r) => r.canChangeStateEvent(EventTypes.SpaceChild)) - .map((space) => removeSpaceChild(space, room.id)); - await Future.wait(futures); + final activeSpace = room.client.getRoomById(activeSpaceId!); + if (activeSpace == null) return; + await activeSpace.removeSpaceChild(room.id); }, ); + try { + await room.client.resetSpaceChildAccess(room.id); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context).accessSettingsWarning), + duration: const Duration(seconds: 10), + ), + ); + } return; case ChatContextAction.delete: if (room.isSpace) { @@ -1077,22 +1082,6 @@ class ChatListController extends State } } - // #Pangea - /// Remove a room from a space. Often, the user will have permission to set - /// the SpaceChild event for the parent space, but not the SpaceParent event. - /// This would cause a permissions error, but the child will still be removed - /// via the SpaceChild event. If that's the case, silence the error. - Future removeSpaceChild(Room space, String roomId) async { - try { - await space.removeSpaceChild(roomId); - } catch (err) { - if ((err as MatrixException).error != MatrixError.M_FORBIDDEN) { - rethrow; - } - } - } - // Pangea# - void dismissStatusList() async { final result = await showOkCancelAlertDialog( title: L10n.of(context).hidePresences, diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index f69d52840..f2ed0c1c1 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -7,7 +7,6 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pangea/chat_list/widgets/chat_list_view_body_wrapper.dart'; -import 'package:fluffychat/pangea/spaces/widgets/space_floating_actions_buttons.dart'; import 'package:fluffychat/widgets/navigation_rail.dart'; class ChatListView extends StatelessWidget { @@ -58,30 +57,17 @@ class ChatListView extends StatelessWidget { // body: ChatListViewBody(controller), body: ChatListViewBodyWrapper(controller: controller), // Pangea# - floatingActionButton: - // #Pangea - // !controller.isSearchMode && controller.activeSpaceId == null - controller.activeFilter == ActiveFilter.spaces && - controller.activeSpaceId == null && - !controller.isSearchMode - ? const SpaceFloatingActionButtons() - : !controller.isSearchMode && - controller.activeSpaceId == null - // Pangea# - ? FloatingActionButton.extended( - // #Pangea - // onPressed: () => context.go('/rooms/newprivatechat'), - onPressed: () => context.go( - '/rooms/newgroup/${controller.activeSpaceId ?? ''}', - ), - // Pangea# - icon: const Icon(Icons.add_outlined), - label: Text( - L10n.of(context).chat, - overflow: TextOverflow.fade, - ), - ) - : const SizedBox.shrink(), + floatingActionButton: !controller.isSearchMode && + controller.activeSpaceId == null + ? FloatingActionButton.extended( + onPressed: () => context.go('/rooms/newprivatechat'), + icon: const Icon(Icons.add_outlined), + label: Text( + L10n.of(context).chat, + overflow: TextOverflow.fade, + ), + ) + : const SizedBox.shrink(), ), ), ), diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index d1d6dbf0d..1bde3fb21 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -678,7 +678,8 @@ class _SpaceViewState extends State { // #Pangea // onPressed: _addChatOrSubspace, // label: Text(L10n.of(context).group), - onPressed: () => context.go("/rooms/newgroup/${widget.spaceId}"), + onPressed: () => + context.go("/rooms/newgroup?space=${widget.spaceId}"), label: Text(L10n.of(context).chat), // Pangea# icon: const Icon(Icons.group_add_outlined), diff --git a/lib/pages/invitation_selection/invitation_selection_view.dart b/lib/pages/invitation_selection/invitation_selection_view.dart index 4638c4213..449a1c016 100644 --- a/lib/pages/invitation_selection/invitation_selection_view.dart +++ b/lib/pages/invitation_selection/invitation_selection_view.dart @@ -54,17 +54,17 @@ class InvitationSelectionView extends StatelessWidget { title: Text(L10n.of(context).inviteContact), // #Pangea actions: [ - if (room.isSpace && room.classCode(context) != null) + if (room.isSpace && room.classCode != null) PopupMenuButton( icon: const Icon(Icons.share_outlined), onSelected: (value) async { - final spaceCode = room.classCode(context)!; + final spaceCode = room.classCode!; String toCopy = spaceCode; if (value == 0) { final String initialUrl = kIsWeb ? html.window.origin! : Environment.frontendURL; toCopy = - "$initialUrl/#/join_with_link?${SpaceConstants.classCode}=${room.classCode(context)}"; + "$initialUrl/#/join_with_link?${SpaceConstants.classCode}=${room.classCode}"; } await Clipboard.setData(ClipboardData(text: toCopy)); @@ -92,8 +92,7 @@ class InvitationSelectionView extends StatelessWidget { child: ListTile( leading: const Icon(Icons.share_outlined), title: Text( - L10n.of(context) - .shareInviteCode(room.classCode(context)!), + L10n.of(context).shareInviteCode(room.classCode!), ), contentPadding: const EdgeInsets.all(0), ), diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index b2146b55f..169e37832 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -140,11 +140,22 @@ class NewGroupController extends State { content: {'url': avatarUrl.toString()}, ), // #Pangea - StateEvent( - type: EventTypes.RoomPowerLevels, - stateKey: '', - content: defaultPowerLevels(Matrix.of(context).client.userID!), + RoomDefaults.defaultPowerLevels( + Matrix.of(context).client.userID!, ), + if (widget.spaceId != null) + StateEvent( + type: EventTypes.RoomJoinRules, + content: { + 'join_rule': 'knock_restricted', + 'allow': [ + { + "type": "m.room_membership", + "room_id": widget.spaceId, + } + ], + }, + ), // Pangea# ], // #Pangea @@ -164,7 +175,7 @@ class NewGroupController extends State { if (widget.spaceId != null) { try { final space = client.getRoomById(widget.spaceId!); - await space?.pangeaSetSpaceChild(room.id); + await space?.addToSpace(room.id); } catch (err) { ErrorHandler.logError( e: "Failed to add room to space", @@ -187,7 +198,7 @@ class NewGroupController extends State { ); } } - context.go('/rooms/$roomId/invite?filter=groups'); + context.go('/rooms/$roomId/invite'); // Pangea# } @@ -231,7 +242,7 @@ class NewGroupController extends State { final room = Matrix.of(context).client.getRoomById(spaceId); if (room == null) return; - final spaceCode = room.classCode(context); + final spaceCode = room.classCode; if (spaceCode != null) { GoogleAnalytics.createClass(room.name, spaceCode); } diff --git a/lib/pages/new_group/new_group_view.dart b/lib/pages/new_group/new_group_view.dart index 6bbae6fe4..b060eb4ec 100644 --- a/lib/pages/new_group/new_group_view.dart +++ b/lib/pages/new_group/new_group_view.dart @@ -65,27 +65,26 @@ class NewGroupView extends StatelessWidget { 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, - // #Pangea - // label: Text(L10n.of(context).group), - label: Text(L10n.of(context).chat), - // Pangea# - ), - ButtonSegment( - value: CreateGroupType.space, - label: Text(L10n.of(context).space), - ), - ], - ), - ), - const SizedBox(height: 16), + // #Pangea + // 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), + // Pangea# InkWell( borderRadius: BorderRadius.circular(90), onTap: controller.loading ? null : controller.selectPhoto, 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 6dd01a13f..f5274e59f 100644 --- a/lib/pages/new_private_chat/new_private_chat_view.dart +++ b/lib/pages/new_private_chat/new_private_chat_view.dart @@ -1,16 +1,15 @@ 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:pretty_qr_code/pretty_qr_code.dart'; 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/pangea/common/config/environment.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'; import 'package:fluffychat/widgets/matrix.dart'; @@ -33,13 +32,15 @@ class NewPrivateChatView extends StatelessWidget { leading: const Center(child: BackButton()), title: Text(L10n.of(context).newChat), backgroundColor: theme.scaffoldBackgroundColor, - actions: [ - TextButton( - onPressed: - UrlLauncher(context, AppConfig.startChatTutorial).launchUrl, - child: Text(L10n.of(context).help), - ), - ], + // #Pangea + // actions: [ + // TextButton( + // onPressed: + // UrlLauncher(context, AppConfig.startChatTutorial).launchUrl, + // child: Text(L10n.of(context).help), + // ), + // ], + // Pangea# ), body: MaxWidthBody( withScrolling: false, @@ -138,15 +139,17 @@ class NewPrivateChatView extends StatelessWidget { title: Text(L10n.of(context).shareInviteLink), onTap: controller.inviteAction, ), - ListTile( - leading: CircleAvatar( - backgroundColor: theme.colorScheme.tertiaryContainer, - foregroundColor: theme.colorScheme.onTertiaryContainer, - child: const Icon(Icons.group_add_outlined), - ), - title: Text(L10n.of(context).createGroup), - onTap: () => context.go('/rooms/newgroup'), - ), + // #Pangea + // ListTile( + // leading: CircleAvatar( + // backgroundColor: theme.colorScheme.tertiaryContainer, + // foregroundColor: theme.colorScheme.onTertiaryContainer, + // child: const Icon(Icons.group_add_outlined), + // ), + // title: Text(L10n.of(context).createGroup), + // onTap: () => context.go('/rooms/newgroup'), + // ), + // Pangea# if (PlatformInfos.isMobile) ListTile( leading: CircleAvatar( @@ -181,7 +184,10 @@ class NewPrivateChatView extends StatelessWidget { constraints: const BoxConstraints(maxWidth: 256), child: PrettyQrView.data( - data: 'https://matrix.to/#/$userId', + // #Pangea + // data: 'https://matrix.to/#/$userId', + data: Environment.frontendURL, + // Pangea# decoration: PrettyQrDecoration( shape: PrettyQrSmoothSymbol( roundFactor: 1, diff --git a/lib/pangea/activity_suggestions/activity_room_selection.dart b/lib/pangea/activity_suggestions/activity_room_selection.dart index a2523b7d1..cc1ed8ad4 100644 --- a/lib/pangea/activity_suggestions/activity_room_selection.dart +++ b/lib/pangea/activity_suggestions/activity_room_selection.dart @@ -179,12 +179,8 @@ class ActivityRoomSelectionState extends State { preset: CreateRoomPreset.trustedPrivateChat, initialState: [ BotOptionsModel(mode: BotMode.directChat).toStateEvent, - StateEvent( - type: EventTypes.RoomPowerLevels, - stateKey: '', - content: defaultPowerLevels( - Matrix.of(context).client.userID!, - ), + RoomDefaults.defaultPowerLevels( + Matrix.of(context).client.userID!, ), if (avatar != null && avatarUrl != null) StateEvent( diff --git a/lib/pangea/chat/constants/default_power_level.dart b/lib/pangea/chat/constants/default_power_level.dart index 1459199e6..c52c65b53 100644 --- a/lib/pangea/chat/constants/default_power_level.dart +++ b/lib/pangea/chat/constants/default_power_level.dart @@ -1,60 +1,78 @@ -Map defaultPowerLevels(String userID) => { - "ban": 50, - "kick": 50, - "invite": 50, - "redact": 50, - "events": { - "m.room.power_levels": 100, - "m.room.pinned_events": 50, - }, - "events_default": 0, - "state_default": 50, - "users": { - userID: 100, - }, - "users_default": 0, - "notifications": { - "room": 50, - }, - }; +import 'package:matrix/matrix.dart'; -Map restrictedPowerLevels(String userID) => { - "ban": 50, - "kick": 50, - "invite": 50, - "redact": 50, - "events": { - "m.room.power_levels": 100, - "m.room.pinned_events": 50, - }, - "events_default": 50, - "state_default": 50, - "users": { - userID: 100, - }, - "users_default": 0, - "notifications": { - "room": 50, - }, - }; +class RoomDefaults { + static StateEvent defaultPowerLevels(String userID) => StateEvent( + type: EventTypes.RoomPowerLevels, + stateKey: '', + content: { + "ban": 50, + "kick": 50, + "invite": 50, + "redact": 50, + "events": { + "m.room.power_levels": 100, + "m.room.pinned_events": 50, + }, + "events_default": 0, + "state_default": 50, + "users": { + userID: 100, + }, + "users_default": 0, + "notifications": { + "room": 50, + }, + }, + ); -Map defaultSpacePowerLevels(String userID) => { - "ban": 50, - "kick": 50, - "invite": 50, - "redact": 50, - "events": { - "m.room.power_levels": 100, - "m.room.join_rules": 100, - "m.space.child": 50, - }, - "events_default": 0, - "state_default": 50, - "users": { - userID: 100, - }, - "users_default": 0, - "notifications": { - "room": 50, - }, - }; + static StateEvent restrictedPowerLevels(String userID) => StateEvent( + type: EventTypes.RoomPowerLevels, + stateKey: '', + content: { + "ban": 50, + "kick": 50, + "invite": 50, + "redact": 50, + "events": { + "m.room.power_levels": 100, + "m.room.pinned_events": 50, + }, + "events_default": 50, + "state_default": 50, + "users": { + userID: 100, + }, + "users_default": 0, + "notifications": { + "room": 50, + }, + }, + ); + + static StateEvent defaultSpacePowerLevels(String userID) => StateEvent( + type: EventTypes.RoomPowerLevels, + stateKey: '', + content: { + "ban": 50, + "kick": 50, + "invite": 50, + "redact": 50, + "events": { + "m.room.power_levels": 100, + "m.room.join_rules": 100, + "m.space.child": 50, + }, + "events_default": 0, + "state_default": 50, + "users": { + userID: 100, + }, + "users_default": 0, + "notifications": { + "room": 50, + }, + }, + ); + + static Visibility spaceChildVisibility = Visibility.private; +} diff --git a/lib/pangea/chat_list/utils/chat_list_handle_space_tap.dart b/lib/pangea/chat_list/utils/chat_list_handle_space_tap.dart index 4ef8af2b5..35d2bba74 100644 --- a/lib/pangea/chat_list/utils/chat_list_handle_space_tap.dart +++ b/lib/pangea/chat_list/utils/chat_list_handle_space_tap.dart @@ -54,7 +54,7 @@ void chatListHandleSpaceTap( if (rooms.any((s) => s.spaceChildren.any((c) => c.roomId == space.id))) { autoJoin(space); } else if (justInputtedCode != null && - justInputtedCode == space.classCode(context)) { + justInputtedCode == space.classCode) { // do nothing } else { controller.showInviteDialog(space); diff --git a/lib/pangea/chat_settings/pages/pangea_chat_access_settings.dart b/lib/pangea/chat_settings/pages/pangea_chat_access_settings.dart new file mode 100644 index 000000000..def795a42 --- /dev/null +++ b/lib/pangea/chat_settings/pages/pangea_chat_access_settings.dart @@ -0,0 +1,221 @@ +import 'package:flutter/material.dart' hide Visibility; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat_access_settings/chat_access_settings_controller.dart'; +import 'package:fluffychat/widgets/layouts/max_width_body.dart'; + +class PangeaChatAccessSettingsPageView extends StatelessWidget { + final ChatAccessSettingsController controller; + const PangeaChatAccessSettingsPageView(this.controller, {super.key}); + + @override + Widget build(BuildContext context) { + final room = controller.room; + return Scaffold( + appBar: AppBar( + leading: const Center(child: BackButton()), + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.shield_outlined), + const SizedBox(width: 8), + Text(L10n.of(context).access), + ], + ), + ), + body: MaxWidthBody( + showBorder: false, + child: StreamBuilder( + stream: room.client.onRoomState.stream + .where((update) => update.roomId == controller.room.id), + builder: (context, snapshot) { + return Container( + width: 400.0, + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + child: FutureBuilder( + future: room.client.getRoomVisibilityOnDirectory(room.id), + builder: (context, snapshot) { + return Column( + spacing: 16.0, + mainAxisSize: MainAxisSize.min, + children: [ + ChatAccessTitle( + icon: Icons.search_outlined, + title: L10n.of(context).howSpaceCanBeFound, + ), + ChatAccessTile( + emoji: "🏡", + title: L10n.of(context).private, + description: L10n.of(context).cannotBeFoundInSearch, + selected: snapshot.data == Visibility.private, + onTap: () { + if (snapshot.data == Visibility.private) return; + controller.setChatVisibilityOnDirectory(false); + }, + ), + ChatAccessTile( + emoji: "🌏", + title: L10n.of(context).public, + description: L10n.of(context).visibleToCommunity, + selected: snapshot.data == Visibility.public, + onTap: () { + if (snapshot.data == Visibility.public) return; + controller.setChatVisibilityOnDirectory(true); + }, + ), + const SizedBox(height: 8.0), + ChatAccessTitle( + icon: Icons.key_outlined, + title: L10n.of(context).howSpaceCanBeJoined, + ), + ChatAccessTile( + emoji: "🤝", + title: L10n.of(context).restricted, + descriptionWidget: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(L10n.of(context).canBeFoundVia), + Text(L10n.of(context).canBeFoundViaInvitation), + Text(L10n.of(context).canBeFoundViaCodeOrLink), + Text(L10n.of(context).canBeFoundViaKnock), + ], + ), + selected: room.joinRules == JoinRules.knock, + onTap: () => controller.setJoinRule(JoinRules.knock), + ), + ChatAccessTile( + emoji: "👐", + title: L10n.of(context).open, + description: L10n.of(context).anyoneCanJoin, + selected: room.joinRules == JoinRules.public, + onTap: () => controller.setJoinRule(JoinRules.public), + ), + ], + ); + }, + ), + ); + }, + ), + ), + ); + } +} + +class ChatAccessTitle extends StatelessWidget { + final IconData icon; + final String title; + + const ChatAccessTitle({ + super.key, + required this.icon, + required this.title, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isColumnMode = FluffyThemes.isColumnMode(context); + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: isColumnMode ? 32.0 : 24.0, + ), + SizedBox(width: isColumnMode ? 32.0 : 16.0), + Text( + title, + style: isColumnMode + ? theme.textTheme.titleLarge + : theme.textTheme.titleMedium, + ), + ], + ); + } +} + +class ChatAccessTile extends StatelessWidget { + final String emoji; + final String title; + + final String? description; + final Widget? descriptionWidget; + + final bool selected; + final VoidCallback? onTap; + + const ChatAccessTile({ + super.key, + required this.emoji, + required this.title, + this.description, + this.descriptionWidget, + this.selected = false, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isColumnMode = FluffyThemes.isColumnMode(context); + + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + child: Opacity( + opacity: selected ? 1.0 : 0.5, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + border: Border.all( + color: selected + ? theme.colorScheme.primaryContainer + : theme.colorScheme.outline, + width: 2, + ), + color: selected + ? theme.colorScheme.primaryContainer.withAlpha(50) + : null, + ), + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Text( + emoji, + style: isColumnMode + ? theme.textTheme.displayMedium + : theme.textTheme.displaySmall, + ), + const SizedBox(width: 16.0), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: isColumnMode + ? theme.textTheme.titleLarge + : theme.textTheme.titleMedium, + ), + description != null + ? Text(description!) + : descriptionWidget != null + ? descriptionWidget! + : const SizedBox.shrink(), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pangea/chat_settings/pages/pangea_chat_details.dart b/lib/pangea/chat_settings/pages/pangea_chat_details.dart index 5a742e648..8bd24277c 100644 --- a/lib/pangea/chat_settings/pages/pangea_chat_details.dart +++ b/lib/pangea/chat_settings/pages/pangea_chat_details.dart @@ -330,7 +330,7 @@ class RoomDetailsButtonRowState extends State { title: l10n.access, icon: const Icon(Icons.shield_outlined), onPressed: () => context.go('/rooms/${room.id}/details/access'), - visible: room.isSpace, + visible: room.isSpace && room.spaceParents.isEmpty, enabled: room.isSpace && room.isRoomAdmin, ), ButtonDetails( @@ -364,7 +364,7 @@ class RoomDetailsButtonRowState extends State { icon: const Icon(Icons.add_outlined), onPressed: widget.controller.addSubspace, visible: room.isSpace && - room.canSendEvent( + room.canChangeStateEvent( EventTypes.SpaceChild, ), showInMainView: false, diff --git a/lib/pangea/chat_settings/widgets/space_invite_buttons.dart b/lib/pangea/chat_settings/widgets/space_invite_buttons.dart index eb18a8a95..f504270d5 100644 --- a/lib/pangea/chat_settings/widgets/space_invite_buttons.dart +++ b/lib/pangea/chat_settings/widgets/space_invite_buttons.dart @@ -50,7 +50,7 @@ class SpaceInviteButtonsController extends State { @override Widget build(BuildContext context) { - final spaceCode = widget.room.classCode(context); + final spaceCode = widget.room.classCode; if (!widget.room.isSpace || spaceCode == null) { return const SizedBox.shrink(); } diff --git a/lib/pangea/chat_settings/widgets/visibility_toggle.dart b/lib/pangea/chat_settings/widgets/visibility_toggle.dart deleted file mode 100644 index fdbbe8a25..000000000 --- a/lib/pangea/chat_settings/widgets/visibility_toggle.dart +++ /dev/null @@ -1,149 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart' as matrix; -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/widgets/future_loading_dialog.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class VisibilityToggle extends StatefulWidget { - final Room room; - final Color? iconColor; - final Future Function(matrix.Visibility) setVisibility; - final Future Function(JoinRules) setJoinRules; - - const VisibilityToggle({ - required this.setVisibility, - required this.setJoinRules, - required this.room, - this.iconColor, - super.key, - }); - - @override - State createState() => VisibilityToggleState(); -} - -class VisibilityToggleState extends State { - Room get room => widget.room; - - bool get _isPublic => room.joinRules == matrix.JoinRules.public; - - matrix.Visibility? _visibility; - bool _loading = true; - String? _error; - - @override - void initState() { - super.initState(); - _getVisibility(); - } - - Future _getVisibility() async { - try { - final resp = await Matrix.of(context).client.getRoomVisibilityOnDirectory( - room.id, - ); - _visibility = resp ?? matrix.Visibility.private; - } catch (e) { - _error = e.toString(); - } finally { - setState(() { - _loading = false; - }); - } - } - - Future _setVisibility(matrix.Visibility visibility) async { - try { - await widget.setVisibility(visibility); - _visibility = visibility; - } catch (e) { - _error = e.toString(); - } finally { - if (mounted) { - setState(() {}); - } - } - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - SwitchListTile.adaptive( - activeColor: AppConfig.activeToggleColor, - title: Text( - L10n.of(context).requireCodeToJoin, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - ), - ), - secondary: CircleAvatar( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - foregroundColor: widget.iconColor, - child: const Icon(Icons.key_outlined), - ), - value: !_isPublic, - onChanged: (value) => - widget.setJoinRules(value ? JoinRules.knock : JoinRules.public), - ), - ListTile( - title: Text( - L10n.of(context).canFindInSearch, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - ), - ), - leading: CircleAvatar( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - foregroundColor: widget.iconColor, - child: const Icon(Icons.search_outlined), - ), - onTap: _visibility != null - ? () => showFutureLoadingDialog( - future: () async { - _setVisibility( - _visibility == matrix.Visibility.public - ? matrix.Visibility.private - : matrix.Visibility.public, - ); - }, - context: context, - ) - : null, - trailing: _loading || _error != null - ? SizedBox( - height: 24.0, - width: 24.0, - child: _error != null - ? Icon( - Icons.error, - color: Theme.of(context).colorScheme.error, - ) - : const CircularProgressIndicator.adaptive(), - ) - : Switch.adaptive( - activeColor: AppConfig.activeToggleColor, - value: _visibility == matrix.Visibility.public, - onChanged: (value) => showFutureLoadingDialog( - future: () async { - _setVisibility( - value - ? matrix.Visibility.public - : matrix.Visibility.private, - ); - }, - context: context, - ), - ), - ), - ], - ); - } -} diff --git a/lib/pangea/common/controllers/pangea_controller.dart b/lib/pangea/common/controllers/pangea_controller.dart index 5018d6b17..c1c609697 100644 --- a/lib/pangea/common/controllers/pangea_controller.dart +++ b/lib/pangea/common/controllers/pangea_controller.dart @@ -269,12 +269,8 @@ class PangeaController { preset: CreateRoomPreset.trustedPrivateChat, initialState: [ BotOptionsModel(mode: BotMode.directChat).toStateEvent, - StateEvent( - type: EventTypes.RoomPowerLevels, - stateKey: '', - content: defaultPowerLevels( - matrixState.client.userID!, - ), + RoomDefaults.defaultPowerLevels( + matrixState.client.userID!, ), ], ); diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart index d6150cfa0..316ef7199 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -10,7 +10,6 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:html_unescape/html_unescape.dart'; import 'package:http/http.dart' as http; -import 'package:matrix/matrix.dart' as matrix; import 'package:matrix/matrix.dart'; import 'package:matrix/src/utils/markdown.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; diff --git a/lib/pangea/extensions/room_children_and_parents_extension.dart b/lib/pangea/extensions/room_children_and_parents_extension.dart index 73aee8a12..3a8fe73b8 100644 --- a/lib/pangea/extensions/room_children_and_parents_extension.dart +++ b/lib/pangea/extensions/room_children_and_parents_extension.dart @@ -15,7 +15,7 @@ extension ChildrenAndParentsRoomExtension on Room { /// Wrapper around call to setSpaceChild with added functionality /// to prevent adding one room to multiple spaces, and resets the /// subspace's JoinRules and Visibility to defaults. - Future pangeaSetSpaceChild( + Future addToSpace( String roomId, { bool? suggested, }) async { @@ -38,11 +38,9 @@ extension ChildrenAndParentsRoomExtension on Room { } try { - await setSpaceChild(roomId, suggested: suggested); - await child.setJoinRules(JoinRules.public); - await child.client.setRoomVisibilityOnDirectory( + await _trySetSpaceChild( roomId, - visibility: matrix.Visibility.private, + suggested: suggested, ); } catch (err, stack) { ErrorHandler.logError( @@ -57,6 +55,31 @@ extension ChildrenAndParentsRoomExtension on Room { } } + Future _trySetSpaceChild( + String roomId, { + bool? suggested, + int retries = 0, + }) async { + final Room? child = client.getRoomById(roomId); + if (child == null) return; + + try { + await setSpaceChild(roomId, suggested: suggested); + } catch (err) { + retries++; + if (retries < 3) { + await Future.delayed(const Duration(seconds: 1)); + return _trySetSpaceChild( + roomId, + suggested: suggested, + retries: retries, + ); + } else { + rethrow; + } + } + } + /// A map of child suggestion status for a space. Map get spaceChildSuggestionStatus { if (!isSpace) return {}; diff --git a/lib/pangea/extensions/room_space_settings_extension.dart b/lib/pangea/extensions/room_space_settings_extension.dart index 37551150a..333539d6b 100644 --- a/lib/pangea/extensions/room_space_settings_extension.dart +++ b/lib/pangea/extensions/room_space_settings_extension.dart @@ -1,15 +1,8 @@ part of "pangea_room_extension.dart"; extension SpaceRoomExtension on Room { - String? classCode(BuildContext context) { - if (!isSpace) { - for (final Room potentialClassRoom in pangeaSpaceParents) { - if (potentialClassRoom.isSpace) { - return SpaceRoomExtension(potentialClassRoom).classCode(context); - } - } - return null; - } + String? get classCode { + if (!isSpace) return null; final roomJoinRules = getState(EventTypes.RoomJoinRules, ""); if (roomJoinRules != null) { final accessCode = roomJoinRules.content.tryGet(ModelKey.accessCode); diff --git a/lib/pangea/spaces/utils/client_spaces_extension.dart b/lib/pangea/spaces/utils/client_spaces_extension.dart index 08e1c74a4..e94efd224 100644 --- a/lib/pangea/spaces/utils/client_spaces_extension.dart +++ b/lib/pangea/spaces/utils/client_spaces_extension.dart @@ -122,10 +122,18 @@ extension SpacesClientExtension on Client { type: EventTypes.RoomAvatar, content: {'url': introChatUploadURL.toString()}, ), + RoomDefaults.defaultPowerLevels(userID!), StateEvent( - type: EventTypes.RoomPowerLevels, - stateKey: '', - content: defaultPowerLevels(userID!), + type: EventTypes.RoomJoinRules, + content: { + 'join_rule': 'knock_restricted', + 'allow': [ + { + "type": "m.room_membership", + "room_id": space.id, + } + ], + }, ), ], ); @@ -142,10 +150,18 @@ extension SpacesClientExtension on Client { type: EventTypes.RoomAvatar, content: {'url': announcementsChatUploadURL.toString()}, ), + RoomDefaults.restrictedPowerLevels(userID!), StateEvent( - type: EventTypes.RoomPowerLevels, - stateKey: '', - content: restrictedPowerLevels(userID!), + type: EventTypes.RoomJoinRules, + content: { + 'join_rule': 'knock_restricted', + 'allow': [ + { + "type": "m.room_membership", + "room_id": space.id, + } + ], + }, ), ], ); @@ -166,11 +182,11 @@ extension SpacesClientExtension on Client { } } - final addIntroChatFuture = space.pangeaSetSpaceChild( + final addIntroChatFuture = space.addToSpace( roomIds[0], ); - final addAnnouncementsChatFuture = space.pangeaSetSpaceChild( + final addAnnouncementsChatFuture = space.addToSpace( roomIds[1], ); @@ -186,11 +202,7 @@ extension SpacesClientExtension on Client { required JoinRules joinRules, }) { return [ - StateEvent( - type: EventTypes.RoomPowerLevels, - stateKey: '', - content: defaultSpacePowerLevels(userID), - ), + RoomDefaults.defaultSpacePowerLevels(userID), StateEvent( type: EventTypes.RoomJoinRules, content: { @@ -200,4 +212,69 @@ extension SpacesClientExtension on Client { ), ]; } + + /// Keep the room's current join rule state event content (except for what's intentionally replaced) + /// since space's access codes were stored there. Don't want to accidentally remove them. + Future pangeaSetJoinRules( + String roomId, + String joinRule, { + List>? allow, + }) async { + final room = getRoomById(roomId); + if (room == null) { + throw Exception('Room not found for user ID: $userID'); + } + + final currentJoinRule = room + .getState( + EventTypes.RoomJoinRules, + ) + ?.content ?? + {}; + + if (currentJoinRule[ModelKey.joinRule] == joinRule && + (currentJoinRule['allow'] == allow)) { + return; // No change needed + } + + currentJoinRule[ModelKey.joinRule] = joinRule; + currentJoinRule['allow'] = allow; + + await setRoomStateWithKey( + roomId, + EventTypes.RoomJoinRules, + '', + currentJoinRule, + ); + } + + Future setSpaceChildAccess(String roomId) async { + await pangeaSetJoinRules( + roomId, + 'knock_restricted', + allow: [ + { + "type": "m.room_membership", + "room_id": id, + } + ], + ); + + await setRoomVisibilityOnDirectory( + roomId, + visibility: Visibility.private, + ); + } + + Future resetSpaceChildAccess(String roomId) async { + await pangeaSetJoinRules( + roomId, + JoinRules.knock.toString().replaceAll('JoinRules.', ''), + ); + + await setRoomVisibilityOnDirectory( + roomId, + visibility: Visibility.private, + ); + } } diff --git a/lib/pangea/spaces/widgets/add_room_dialog.dart b/lib/pangea/spaces/widgets/add_room_dialog.dart deleted file mode 100644 index f798d99eb..000000000 --- a/lib/pangea/spaces/widgets/add_room_dialog.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart' as matrix; -import 'package:matrix/matrix.dart'; - -class AddRoomDialog extends StatefulWidget { - const AddRoomDialog({ - super.key, - }); - - @override - AddRoomDialogState createState() => AddRoomDialogState(); -} - -class AddRoomDialogState extends State { - final _formKey = GlobalKey(); - final TextEditingController _roomNameController = TextEditingController(); - final TextEditingController _roomDescriptionController = - TextEditingController(); - - @override - void dispose() { - _roomNameController.dispose(); - _roomDescriptionController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Dialog( - child: Form( - key: _formKey, - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 400, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(20), - child: Column( - children: [ - Text( - style: Theme.of(context).textTheme.headlineSmall, - L10n.of(context).createChat, - ), - const SizedBox(height: 20), - TextFormField( - controller: _roomNameController, - decoration: InputDecoration( - hintText: L10n.of(context).chatName, - ), - minLines: 1, - maxLines: 1, - maxLength: 64, - validator: (text) { - if (text == null || text.isEmpty) { - return L10n.of(context).pleaseChoose; - } - return null; - }, - onTapOutside: (_) => - FocusManager.instance.primaryFocus?.unfocus(), - ), - const SizedBox(height: 20), - TextFormField( - controller: _roomDescriptionController, - decoration: InputDecoration( - hintText: L10n.of(context).chatDescription, - ), - minLines: 4, - maxLines: 8, - maxLength: 255, - onTapOutside: (_) => - FocusManager.instance.primaryFocus?.unfocus(), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.all(20), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(null); - }, - child: Text(L10n.of(context).cancel), - ), - const SizedBox(width: 20), - TextButton( - onPressed: () async { - final isValid = _formKey.currentState!.validate(); - if (!isValid) return; - Navigator.of(context).pop( - RoomResponse( - roomName: _roomNameController.text, - roomDescription: _roomDescriptionController.text, - joinRules: JoinRules.public, - visibility: matrix.Visibility.private, - ), - ); - }, - child: Text(L10n.of(context).confirm), - ), - ], - ), - ), - ], - ), - ), - ), - ); - } -} - -class RoomResponse { - final String roomName; - final String roomDescription; - final JoinRules joinRules; - final matrix.Visibility visibility; - - RoomResponse({ - required this.roomName, - required this.roomDescription, - required this.joinRules, - required this.visibility, - }); - - Map toJson() { - return { - 'roomName': roomName, - 'roomDescripion': roomDescription, - 'joinRules': joinRules, - 'visibility': visibility, - }; - } -} diff --git a/lib/utils/fluffy_share.dart b/lib/utils/fluffy_share.dart index 4fd7967c5..e4d7b7645 100644 --- a/lib/utils/fluffy_share.dart +++ b/lib/utils/fluffy_share.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:share_plus/share_plus.dart'; +import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import '../widgets/matrix.dart'; @@ -34,10 +35,13 @@ abstract class FluffyShare { final client = Matrix.of(context).client; final ownProfile = await client.fetchOwnProfile(); await FluffyShare.share( - L10n.of(context).inviteText( - ownProfile.displayName ?? client.userID!, - 'https://matrix.to/#/${client.userID}?client=im.fluffychat', - ), + // #Pangea + // L10n.of(context).inviteText( + // ownProfile.displayName ?? client.userID!, + // 'https://matrix.to/#/${client.userID}?client=im.fluffychat', + // ), + "${ownProfile.displayName ?? client.userID!} invited you to Pangea Chat.\nOpen the invite link: \n ${Environment.frontendURL}", + // Pangea# context, ); } diff --git a/lib/widgets/adaptive_dialogs/user_dialog.dart b/lib/widgets/adaptive_dialogs/user_dialog.dart index 254a43e15..ed5846112 100644 --- a/lib/widgets/adaptive_dialogs/user_dialog.dart +++ b/lib/widgets/adaptive_dialogs/user_dialog.dart @@ -183,7 +183,9 @@ class UserDialog extends StatelessWidget { bigButtons: true, onPressed: () async { final router = GoRouter.of(context); - Navigator.of(context).pop(); + // #Pangea + // Navigator.of(context).pop(); + // Pangea# final roomIdResult = await showFutureLoadingDialog( context: context, // #Pangea @@ -194,6 +196,9 @@ class UserDialog extends StatelessWidget { ), // Pangea# ); + // #Pangea + Navigator.of(context).pop(); + // Pangea# final roomId = roomIdResult.result; if (roomId == null) return; router.go('/rooms/$roomId'); diff --git a/lib/widgets/future_loading_dialog.dart b/lib/widgets/future_loading_dialog.dart index f5101e0a3..355511c3c 100644 --- a/lib/widgets/future_loading_dialog.dart +++ b/lib/widgets/future_loading_dialog.dart @@ -22,7 +22,7 @@ Future> showFutureLoadingDialog({ ExceptionContext? exceptionContext, bool ignoreError = false, // #Pangea - String? Function(Object, StackTrace?)? onError, + Object? Function(Object, StackTrace?)? onError, String? Function()? onSuccess, VoidCallback? onDismiss, // Pangea# @@ -82,7 +82,7 @@ class LoadingDialog extends StatefulWidget { final Future future; final ExceptionContext? exceptionContext; // #Pangea - final String? Function(Object, StackTrace?)? onError; + final Object? Function(Object, StackTrace?)? onError; final String? Function()? onSuccess; final VoidCallback? onDismiss; // Pangea#