diff --git a/.gitignore b/.gitignore index 7fdb9b2ac..630d1f08d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ build/.last_build_id widgets/matrix_widget_api/build/.last_build_id widgets/calendar/android/local.properties .vscode/settings.json -.idea/ \ No newline at end of file +.idea/ +/build diff --git a/commet/lib/client/matrix/extensions/matrix_client_extensions.dart b/commet/lib/client/matrix/extensions/matrix_client_extensions.dart index 7af9dffd9..2ce5a670f 100644 --- a/commet/lib/client/matrix/extensions/matrix_client_extensions.dart +++ b/commet/lib/client/matrix/extensions/matrix_client_extensions.dart @@ -1,7 +1,6 @@ import 'package:commet/client/client.dart' as commet; import 'package:commet/client/matrix/components/emoticon/matrix_emoticon_component.dart'; import 'package:commet/client/matrix/matrix_mxc_image_provider.dart'; -import 'package:commet/client/room.dart' show RoomVisibility; import 'package:commet/client/room_preview.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; @@ -42,11 +41,12 @@ extension MatrixExtensions on Client { }; var visibility = switch (joinRule) { - "public" => RoomVisibility.public, - "knock" => RoomVisibility.knock, - "invite" => RoomVisibility.invite, - "private" => RoomVisibility.private, - _ => RoomVisibility.private, + "public" => commet.RoomVisibilityPublic(), + "knock" => commet.RoomVisibilityPrivate(), + "invite" => commet.RoomVisibilityPrivate(), + "private" => commet.RoomVisibilityPrivate(), + "restricted" => commet.RoomVisibilityRestricted([]), + _ => commet.RoomVisibilityPrivate(), }; if (name != null && id != null) { diff --git a/commet/lib/client/matrix/matrix_client.dart b/commet/lib/client/matrix/matrix_client.dart index 041c66b7e..2a86b28bd 100644 --- a/commet/lib/client/matrix/matrix_client.dart +++ b/commet/lib/client/matrix/matrix_client.dart @@ -464,14 +464,44 @@ class MatrixClient extends Client { ]; } + var visibility = switch (args.visibility) { + final RoomVisibilityPrivate _ => matrix.Visibility.private, + final RoomVisibilityPublic _ => matrix.Visibility.public, + final RoomVisibilityRestricted _ => null, + _ => matrix.Visibility.private, + }; + + if (args.visibility case RoomVisibilityRestricted restricted) { + initialState ??= List.empty(growable: true); + + initialState = [ + ...initialState, + for (var i in restricted.spaces) + matrix.StateEvent( + stateKey: i, + type: matrix.EventTypes.SpaceParent, + content: { + "canonical": true, + "via": [ + if (self?.identifier.domain != null) self?.identifier.domain + ] + }), + matrix.StateEvent(content: { + "join_rule": "restricted", + "allow": [ + for (var i in restricted.spaces) + {"room_id": i, "type": "m.room_membership"}, + ] + }, type: matrix.EventTypes.RoomJoinRules) + ]; + } + var id = await _matrixClient.createRoom( creationContent: creationContent, name: args.name, initialState: initialState, topic: args.topic, - visibility: args.visibility == RoomVisibility.private - ? matrix.Visibility.private - : matrix.Visibility.public, + visibility: visibility, ); await _matrixClient.waitForRoomInSync(id); @@ -492,7 +522,7 @@ class MatrixClient extends Client { var id = await _matrixClient.createSpace( name: args.name, waitForSync: true, - visibility: args.visibility == RoomVisibility.private + visibility: args.visibility is RoomVisibilityPrivate ? matrix.Visibility.private : matrix.Visibility.public, ); diff --git a/commet/lib/client/matrix/matrix_room.dart b/commet/lib/client/matrix/matrix_room.dart index 622c91a97..0b1c3028e 100644 --- a/commet/lib/client/matrix/matrix_room.dart +++ b/commet/lib/client/matrix/matrix_room.dart @@ -748,7 +748,7 @@ class MatrixRoom extends Room { case matrix.JoinRules.restricted: if (_client.spaces.any((e) => - e.visibility == RoomVisibility.public && + e.visibility is RoomVisibilityPublic && e.containsRoom(_matrixRoom.id))) { // if any public space contains this room, consider the room public // this is kind of flawed, because there could be public spaces we are not a member of @@ -847,4 +847,54 @@ class MatrixRoom extends Room { await tl.setReadMarker(); } + + @override + RoomVisibility get visibility { + switch (_matrixRoom.joinRules) { + case matrix.JoinRules.public: + return RoomVisibilityPublic(); + case matrix.JoinRules.knock: + return RoomVisibilityPrivate(); + case matrix.JoinRules.invite: + return RoomVisibilityPrivate(); + case matrix.JoinRules.private: + return RoomVisibilityPrivate(); + case matrix.JoinRules.restricted: + return RoomVisibilityRestricted(matrixRoom + .getState(matrix.EventTypes.RoomJoinRules) + ?.content + .tryGetList>("allow") + ?.map((i) => i.tryGet("room_id")) + .nonNulls + .toList() ?? + []); + case matrix.JoinRules.knockRestricted: + return RoomVisibilityPrivate(); + case null: + return RoomVisibilityPublic(); + } + } + + @override + Future setVisibility(RoomVisibility visibility) async { + var state = switch (visibility) { + final RoomVisibilityPrivate _ => matrix.StateEvent(content: { + "join_rule": "invite", + }, type: matrix.EventTypes.RoomJoinRules), + final RoomVisibilityPublic _ => matrix.StateEvent( + content: {"join_rule": "public"}, + type: matrix.EventTypes.RoomJoinRules), + final RoomVisibilityRestricted restricted => matrix.StateEvent(content: { + "join_rule": "restricted", + "allow": [ + for (var i in restricted.spaces) + {"room_id": i, "type": "m.room_membership"}, + ] + }, type: matrix.EventTypes.RoomJoinRules), + RoomVisibility() => throw UnimplementedError(), + }; + + await _matrixRoom.client + .setRoomStateWithKey(_matrixRoom.id, state.type, "", state.content); + } } diff --git a/commet/lib/client/matrix/matrix_room_permissions.dart b/commet/lib/client/matrix/matrix_room_permissions.dart index 0ec5f739b..a1ee34757 100644 --- a/commet/lib/client/matrix/matrix_room_permissions.dart +++ b/commet/lib/client/matrix/matrix_room_permissions.dart @@ -44,4 +44,8 @@ class MatrixRoomPermissions extends Permissions { @override bool get canChangeRoles => room.canChangePowerLevel; + + @override + bool get canChangeVisibility => + room.canChangeStateEvent(matrix.EventTypes.RoomJoinRules); } diff --git a/commet/lib/client/matrix/matrix_space.dart b/commet/lib/client/matrix/matrix_space.dart index 5ab62cb96..18d9fd28c 100644 --- a/commet/lib/client/matrix/matrix_space.dart +++ b/commet/lib/client/matrix/matrix_space.dart @@ -100,15 +100,19 @@ class MatrixSpace extends Space { RoomVisibility get visibility { switch (_matrixRoom.joinRules) { case matrix.JoinRules.public: - return RoomVisibility.public; + return RoomVisibilityPublic(); case matrix.JoinRules.knock: - return RoomVisibility.knock; + return RoomVisibilityPrivate(); case matrix.JoinRules.invite: - return RoomVisibility.invite; + return RoomVisibilityPrivate(); case matrix.JoinRules.private: - return RoomVisibility.private; - default: - return RoomVisibility.private; + return RoomVisibilityPrivate(); + case matrix.JoinRules.restricted: + return RoomVisibilityRestricted([]); + case matrix.JoinRules.knockRestricted: + return RoomVisibilityPrivate(); + case null: + return RoomVisibilityPublic(); } } @@ -449,6 +453,16 @@ class MatrixSpace extends Space { final update = event.rooms?.join; if (update == null) return; + var thisRoom = update[_matrixRoom.id]; + if (thisRoom != null) { + if (thisRoom.timeline?.events + ?.any((i) => i.type == matrix.EventTypes.SpaceChild) == + true) { + print("A child of this space has been modified!"); + loadExtra(); + } + } + for (var id in update.keys) { if (roomsWithChildren.any((i) => i.identifier == id)) { _updateTopLevelStatus(); diff --git a/commet/lib/client/matrix_background/matrix_background_room.dart b/commet/lib/client/matrix_background/matrix_background_room.dart index 26a4342f5..67513dafd 100644 --- a/commet/lib/client/matrix_background/matrix_background_room.dart +++ b/commet/lib/client/matrix_background/matrix_background_room.dart @@ -349,4 +349,14 @@ class MatrixBackgroundRoom implements Room { // TODO: implement markAsRead throw UnimplementedError(); } + + @override + // TODO: implement visibility + RoomVisibility get visibility => throw UnimplementedError(); + + @override + Future setVisibility(RoomVisibility visibility) { + // TODO: implement setVisibility + throw UnimplementedError(); + } } diff --git a/commet/lib/client/permissions.dart b/commet/lib/client/permissions.dart index d4dab685d..c8c334a40 100644 --- a/commet/lib/client/permissions.dart +++ b/commet/lib/client/permissions.dart @@ -20,6 +20,8 @@ abstract class Permissions { bool get canEnableE2EE => false; + bool get canChangeVisibility => false; + bool get canEditRoomSecurity => canEnableE2EE; bool get canChangeNotificationSettings => true; diff --git a/commet/lib/client/room.dart b/commet/lib/client/room.dart index 1778926af..5a3173a7b 100644 --- a/commet/lib/client/room.dart +++ b/commet/lib/client/room.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:collection/collection.dart'; import 'package:commet/client/client.dart'; import 'package:commet/client/components/calendar_room/calendar_room_component.dart'; import 'package:commet/client/components/direct_messages/direct_message_component.dart'; @@ -14,7 +15,50 @@ import 'package:flutter/material.dart'; import 'attachment.dart'; import 'permissions.dart'; -enum RoomVisibility { public, private, invite, knock } +// enum RoomVisibility { public, private, invite, knock } + +abstract class RoomVisibility { + static IconData icon(RoomVisibility? visibility) { + return switch (visibility) { + final RoomVisibilityPublic _ => Icons.public, + final RoomVisibilityPrivate _ => Icons.lock, + final RoomVisibilityRestricted _ => Icons.shield, + _ => Icons.question_mark, + }; + } +} + +class RoomVisibilityPrivate implements RoomVisibility { + @override + bool operator ==(Object other) { + if (other is RoomVisibilityPrivate) return true; + if (identical(this, other)) return true; + return false; + } +} + +class RoomVisibilityPublic implements RoomVisibility { + @override + bool operator ==(Object other) { + if (other is RoomVisibilityPublic) return true; + if (identical(this, other)) return true; + return false; + } +} + +class RoomVisibilityRestricted implements RoomVisibility { + final List spaces; + + @override + bool operator ==(Object other) { + if (other is! RoomVisibilityRestricted) return false; + if (identical(this, other)) return true; + + return ListEquality().equals(this.spaces, other.spaces); + } + + RoomVisibilityRestricted(this.spaces); +} enum PushRule { notify, mentionsOnly, dontNotify } @@ -104,6 +148,8 @@ abstract class Room { int get displayHighlightedNotificationCount => pushRule != PushRule.dontNotify ? highlightedNotificationCount : 0; + RoomVisibility get visibility; + /// Send a message in this room Future sendMessage({ String? message, @@ -151,6 +197,8 @@ abstract class Room { /// Set a notification push rule Future setPushRule(PushRule rule); + Future setVisibility(RoomVisibility visibility); + /// Gets the color of a user based on their ID Color getColorOfUser(String userId); diff --git a/commet/lib/ui/atoms/room_preview.dart b/commet/lib/ui/atoms/room_preview.dart index e3c690cfb..dd4aed4e0 100644 --- a/commet/lib/ui/atoms/room_preview.dart +++ b/commet/lib/ui/atoms/room_preview.dart @@ -46,10 +46,10 @@ class RoomPreviewView extends StatelessWidget { size: 15, switch (previewData.visibility) { null => Icons.lock, - RoomVisibility.public => Icons.public, - RoomVisibility.private => Icons.lock, - RoomVisibility.invite => Icons.lock, - RoomVisibility.knock => Icons.lock, + final RoomVisibilityPublic _ => Icons.public, + final RoomVisibilityPrivate _ => Icons.lock, + final RoomVisibilityRestricted _ => Icons.shield, + _ => Icons.question_mark, }) ], ), diff --git a/commet/lib/ui/atoms/space_list.dart b/commet/lib/ui/atoms/space_list.dart index 8efcec2df..4fb419423 100644 --- a/commet/lib/ui/atoms/space_list.dart +++ b/commet/lib/ui/atoms/space_list.dart @@ -4,6 +4,7 @@ import 'package:commet/client/client.dart'; import 'package:commet/client/room_preview.dart'; import 'package:commet/client/space_child.dart'; import 'package:commet/main.dart'; +import 'package:commet/ui/atoms/adaptive_context_menu.dart'; import 'package:commet/ui/atoms/room_preview_text_button.dart'; import 'package:commet/ui/atoms/room_text_button.dart'; import 'package:commet/ui/navigation/adaptive_dialog.dart'; @@ -166,22 +167,36 @@ class _SpaceListState extends State { Widget buildChild(SpaceChild child) { if (child case SpaceChildSpace _) { if (widget.currentDepth < widget.maxDepth) { - return tiamat.TextButtonExpander(child.child.displayName, - initiallyExpanded: true, - childrenPadding: const EdgeInsets.fromLTRB(2, 0, 0, 0), - iconColor: Theme.of(context).colorScheme.secondary, - textColor: Theme.of(context).colorScheme.secondary, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 8), - child: SpaceList( - child.child, - isTopLevel: false, - currentDepth: widget.currentDepth + 1, - onRoomSelected: widget.onRoomSelected, - ), + return AdaptiveContextMenu( + items: [ + if (widget.space.permissions.canEditChildren) + tiamat.ContextMenuItem( + icon: Icons.remove_circle, + text: "Remove from ${widget.space.displayName}", + onPressed: () async { + if (await AdaptiveDialog.confirmation(context) == true) { + widget.space.removeChild(child); + } + }, ) - ]); + ], + child: tiamat.TextButtonExpander(child.child.displayName, + initiallyExpanded: true, + childrenPadding: const EdgeInsets.fromLTRB(2, 0, 0, 0), + iconColor: Theme.of(context).colorScheme.secondary, + textColor: Theme.of(context).colorScheme.secondary, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 8), + child: SpaceList( + child.child, + isTopLevel: false, + currentDepth: widget.currentDepth + 1, + onRoomSelected: widget.onRoomSelected, + ), + ) + ]), + ); } else { return tiamat.TextButton(widget.space.displayName); } diff --git a/commet/lib/ui/organisms/space_summary/space_summary.dart b/commet/lib/ui/organisms/space_summary/space_summary.dart index 492987257..95ec162c1 100644 --- a/commet/lib/ui/organisms/space_summary/space_summary.dart +++ b/commet/lib/ui/organisms/space_summary/space_summary.dart @@ -92,13 +92,18 @@ class _SpaceSummaryState extends State { void openRoomSettings(Room room) { NavigationUtils.navigateTo( - context, RoomSettingsPage(room: room, onLeaveRoom: widget.onLeaveRoom)); + context, + RoomSettingsPage( + room: room, + contextSpace: widget.space, + onLeaveRoom: widget.onLeaveRoom)); } void onAddRoomButtonTap() async { var room = await GetOrCreateRoom.show( widget.space.client, context, + currentSpace: widget.space, joinRoom: false, showAllRoomTypes: true, existingRoomsRemoveWhere: (child) { diff --git a/commet/lib/ui/organisms/space_summary/space_summary_view.dart b/commet/lib/ui/organisms/space_summary/space_summary_view.dart index a993d345f..2dc56b9ae 100644 --- a/commet/lib/ui/organisms/space_summary/space_summary_view.dart +++ b/commet/lib/ui/organisms/space_summary/space_summary_view.dart @@ -8,7 +8,6 @@ import 'package:commet/client/room_preview.dart'; import 'package:commet/client/space_child.dart'; import 'package:commet/config/build_config.dart'; import 'package:commet/config/layout_config.dart'; -import 'package:commet/ui/atoms/adaptive_context_menu.dart'; import 'package:commet/ui/atoms/room_panel.dart'; import 'package:commet/ui/atoms/scaled_safe_area.dart'; import 'package:commet/utils/common_strings.dart'; @@ -100,6 +99,10 @@ class SpaceSummaryViewState extends State { desc: "Label to display that the space is private", name: "labelSpaceVisibilityPrivate"); + String get labelSpaceVisibilityRestricted => Intl.message("Restricted space", + desc: "Label to display that the space is restricted", + name: "labelSpaceVisibilityRestricted"); + String labelSpaceGettingText(spaceName) => Intl.message("Welcome to \n\n # $spaceName", args: [spaceName], @@ -394,11 +397,13 @@ class SpaceSummaryViewState extends State { } Widget spaceVisibility() { - IconData data = - widget.visibility == RoomVisibility.public ? Icons.public : Icons.lock; - String text = widget.visibility == RoomVisibility.public - ? labelSpaceVisibilityPublic - : labelSpaceVisibilityPrivate; + IconData data = RoomVisibility.icon(widget.visibility); + String text = switch (widget.visibility) { + final RoomVisibilityPublic _ => labelSpaceVisibilityPublic, + final RoomVisibilityPrivate _ => labelSpaceVisibilityPrivate, + final RoomVisibilityRestricted _ => labelSpaceVisibilityRestricted, + _ => "", + }; return Row( children: [ Icon(data), @@ -599,16 +604,6 @@ class SpaceSummaryViewState extends State { result = Container(); } - return AdaptiveContextMenu( - items: [ - ContextMenuItem( - text: "Remove from ${parent.displayName}", - onPressed: () { - parent.removeChild(item); - }, - ) - ], - child: result, - ); + return result; } } diff --git a/commet/lib/ui/pages/get_or_create_room/get_or_create_room.dart b/commet/lib/ui/pages/get_or_create_room/get_or_create_room.dart index f8e5abee6..58d164553 100644 --- a/commet/lib/ui/pages/get_or_create_room/get_or_create_room.dart +++ b/commet/lib/ui/pages/get_or_create_room/get_or_create_room.dart @@ -56,6 +56,7 @@ class GetOrCreateRoom extends StatefulWidget { bool createPhotoRoom = false, bool createCalendar = false, String? initialRoomAddress, + Space? currentSpace, bool Function(SpaceChild child)? existingRoomsRemoveWhere, }) async { if (client == null) { @@ -78,7 +79,7 @@ class GetOrCreateRoom extends StatefulWidget { fields: [ RoomFieldName(), RoomFieldTopic(), - RoomFieldVisibility(), + RoomFieldVisibility(client: client!, currentSpace: currentSpace), RoomFieldEncryption( canEnableEncryption: true, defaultEnabled: true), ], @@ -96,7 +97,7 @@ class GetOrCreateRoom extends StatefulWidget { fields: [ RoomFieldName(), RoomFieldTopic(), - RoomFieldVisibility(), + RoomFieldVisibility(client: client!, currentSpace: currentSpace), RoomFieldEncryption( canEnableEncryption: false, defaultEnabled: false), RoomFieldType(RoomType.voipRoom), @@ -115,7 +116,7 @@ class GetOrCreateRoom extends StatefulWidget { fields: [ RoomFieldName(), RoomFieldTopic(), - RoomFieldVisibility(), + RoomFieldVisibility(client: client!, currentSpace: currentSpace), RoomFieldEncryption(defaultEnabled: true), RoomFieldType(RoomType.photoAlbum), ], @@ -133,7 +134,7 @@ class GetOrCreateRoom extends StatefulWidget { fields: [ RoomFieldName(), RoomFieldTopic(), - RoomFieldVisibility(), + RoomFieldVisibility(client: client!, currentSpace: currentSpace), RoomFieldEncryption(defaultEnabled: true), RoomFieldType(RoomType.calendar), ], @@ -151,7 +152,7 @@ class GetOrCreateRoom extends StatefulWidget { fields: [ RoomFieldName(), RoomFieldTopic(), - RoomFieldVisibility(), + RoomFieldVisibility(client: client!, currentSpace: currentSpace), ], ), create: (args) async { diff --git a/commet/lib/ui/pages/get_or_create_room/room_creator.dart b/commet/lib/ui/pages/get_or_create_room/room_creator.dart index 004249aff..b3f708a90 100644 --- a/commet/lib/ui/pages/get_or_create_room/room_creator.dart +++ b/commet/lib/ui/pages/get_or_create_room/room_creator.dart @@ -1,4 +1,5 @@ import 'package:commet/client/client.dart'; +import 'package:commet/client/matrix/matrix_peer.dart'; import 'package:commet/config/layout_config.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -157,33 +158,54 @@ class RoomFieldEncryption implements RoomField { } class RoomFieldVisibility implements RoomField { - String get roomVisibilityPrivateExplanation => Intl.message( + Space? currentSpace; + Client client; + + RoomFieldVisibility({required this.client, this.currentSpace}); + + static String get roomVisibilityPrivateExplanation => Intl.message( "This room will only be accessible by invitation", name: "roomVisibilityPrivateExplanation", desc: "Explains what 'private' room visibility means", ); - String get roomVisibilityPublicExplanation => Intl.message( + static String get roomVisibilityPublicExplanation => Intl.message( "This room will be publically accessible by anyone on the internet", name: "roomVisibilityPublicExplanation", desc: "Explains what 'public' visibility means", ); - String get labelVisibilityPrivate => Intl.message( + static String get labelVisibilityPrivate => Intl.message( "Private", name: "labelVisibilityPrivate", desc: "Short label for room visibility private", ); - String get labelVisibilityPublic => Intl.message( + static String get labelVisibilityPublic => Intl.message( "Public", name: "labelVisibilityPublic", desc: "Short label for room visibility public", ); + static String get roomVisibilityRestrictedExplanation => Intl.message( + "This room will be available to anyone who is a member of it's parent spaces", + name: "roomVisibilityRestrictedExplanation", + desc: "Explains what 'restricted' visibility means", + ); + + static String get labelVisibilityRestricted => Intl.message( + "Restricted", + name: "labelVisibilityRestricted", + desc: "Short label for room visibility restricted", + ); + @override void setDefaults(CreateRoomArgs args) { - args.visibility = RoomVisibility.private; + if (currentSpace != null) { + args.visibility = RoomVisibilityRestricted([currentSpace!.identifier]); + } else { + args.visibility = RoomVisibilityPrivate(); + } } @override @@ -195,67 +217,97 @@ class RoomFieldVisibility implements RoomField { Widget build(CreateRoomArgs args, Function() onArgsChanged) { return SizedBox( height: 90, - child: tiamat.DropdownSelector( + child: tiamat.DropdownSelector( itemHeight: 80, value: args.visibility, items: [ - RoomVisibility.public, - RoomVisibility.private, + RoomVisibilityPrivate(), + RoomVisibilityPublic(), + if (currentSpace != null) + RoomVisibilityRestricted([currentSpace!.identifier]) ], onItemSelected: (item) { args.visibility = item; onArgsChanged(); }, itemBuilder: (item) { - String? title; - IconData? icon; - String? subtitle; - switch (item) { - case RoomVisibility.public: - title = labelVisibilityPublic; - icon = Icons.public; - subtitle = roomVisibilityPublicExplanation; - break; - case RoomVisibility.private: - case RoomVisibility.invite: - case RoomVisibility.knock: - title = labelVisibilityPrivate; - icon = Icons.lock; - subtitle = roomVisibilityPrivateExplanation; - - break; - case null: - break; - } - - return Padding( - padding: const EdgeInsets.all(0.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, + return buildRoomVisibility(client, item); + }, + ), + ); + } + + static Widget buildRoomVisibility(Client client, RoomVisibility? item) { + String? title; + Widget icon = Icon(RoomVisibility.icon(item)); + String? subtitle; + switch (item) { + case final RoomVisibilityPublic _: + title = labelVisibilityPublic; + subtitle = roomVisibilityPublicExplanation; + break; + case final RoomVisibilityPrivate _: + title = labelVisibilityPrivate; + subtitle = roomVisibilityPrivateExplanation; + break; + case final RoomVisibilityRestricted restricted: + title = labelVisibilityRestricted; + subtitle = roomVisibilityRestrictedExplanation; + + icon = Row( + spacing: 4, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + for (var i in restricted.spaces) buildSpaceIcon(client, i), + ]); + break; + case null: + break; + } + + return Padding( + padding: const EdgeInsets.all(0.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(0, 8, 0, 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + textBaseline: TextBaseline.alphabetic, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(0, 2, 0, 2), - child: Icon(icon), - ), - tiamat.Text.label(title!), - ], - ), - tiamat.Text.labelLow( - subtitle!, - overflow: TextOverflow.fade, + Padding( + padding: const EdgeInsets.fromLTRB(0, 2, 8, 0), + child: icon, ), + tiamat.Text.label(title!), ], ), - ); - }, + ), + tiamat.Text.labelLow( + subtitle!, + overflow: TextOverflow.fade, + ), + ], ), ); } + + static Widget buildSpaceIcon(Client client, String i) { + Space? space = client.getSpace(i); + + return tiamat.Tooltip( + text: space?.displayName ?? i, + child: tiamat.Avatar( + radius: 13, + image: space?.avatar, + placeholderText: space?.displayName ?? i, + placeholderColor: space?.color ?? MatrixPeer.hashColor(i)), + ); + } } class RoomCreatorWidget extends StatefulWidget { diff --git a/commet/lib/ui/pages/main/main_page.dart b/commet/lib/ui/pages/main/main_page.dart index 8c6f0b755..96d3fecc2 100644 --- a/commet/lib/ui/pages/main/main_page.dart +++ b/commet/lib/ui/pages/main/main_page.dart @@ -355,6 +355,7 @@ class MainPageState extends State { context, RoomSettingsPage( room: currentRoom!, + contextSpace: currentSpace, onLeaveRoom: clearRoomSelection, )); } diff --git a/commet/lib/ui/pages/settings/categories/room/security/room_security_settings_page.dart b/commet/lib/ui/pages/settings/categories/room/security/room_security_settings_page.dart index 9b90b0444..5a6215562 100644 --- a/commet/lib/ui/pages/settings/categories/room/security/room_security_settings_page.dart +++ b/commet/lib/ui/pages/settings/categories/room/security/room_security_settings_page.dart @@ -1,10 +1,24 @@ import 'package:commet/client/client.dart'; -import 'package:commet/ui/pages/settings/categories/room/security/room_security_settings_view.dart'; +import 'package:commet/ui/navigation/adaptive_dialog.dart'; +import 'package:commet/ui/pages/get_or_create_room/room_creator.dart'; +import 'package:commet/utils/error_utils.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:tiamat/atoms/tile.dart'; + +import 'package:tiamat/tiamat.dart' as tiamat; class RoomSecuritySettingsPage extends StatefulWidget { - const RoomSecuritySettingsPage({super.key, required this.room}); + const RoomSecuritySettingsPage({ + required this.room, + this.contextSpace, + this.showEncryptionToggle = true, + super.key, + }); final Room room; + final Space? contextSpace; + final bool showEncryptionToggle; @override State createState() => @@ -12,16 +26,146 @@ class RoomSecuritySettingsPage extends StatefulWidget { } class _RoomSecuritySettingsPageState extends State { + late bool isE2EEEnabled; + late RoomVisibility visibility; + + String get promptEnableEncryptionRoomSettings => + Intl.message("Enable Encryption", + name: "promptEnableEncryptionRoomSettings", + desc: "Short prompt to enable encryption for a room"); + + String get encryptionCannotBeDisabledExplanationRoomSettings => + Intl.message("If enabled, encryption cannot be disabled later", + name: "encryptionCannotBeDisabledExplanationRoomSettings", + desc: "Explains that encryption cannot be disabled once enabled"); + + @override + void initState() { + isE2EEEnabled = widget.room.isE2EE; + visibility = widget.room.visibility; + super.initState(); + } + @override Widget build(BuildContext context) { - return RoomSecuritySettingsView( - isE2EEEnabled: widget.room.isE2EE, - enableE2EE: enableE2EE, - supportsE2EE: widget.room.client.supportsE2EE, + return Column( + spacing: 8, + children: [ + if (widget.room.client.supportsE2EE && widget.showEncryptionToggle) + buildE2EEToggle(), + buildRoomVisibility(), + ], + ); + } + + Widget buildE2EEToggle() { + return tiamat.Panel( + mode: tiamat.TileType.surfaceContainerLow, + child: Opacity( + opacity: widget.room.permissions.canEnableE2EE ? 1 : 0.5, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + tiamat.Text.labelEmphasised( + promptEnableEncryptionRoomSettings), + tiamat.Text.labelLow( + encryptionCannotBeDisabledExplanationRoomSettings) + ]), + IgnorePointer( + ignoring: isE2EEEnabled || !widget.room.permissions.canEnableE2EE, + child: tiamat.Switch( + state: isE2EEEnabled, + onChanged: (value) { + if (value != true) return; + setState(() { + isE2EEEnabled = true; + widget.room.enableE2EE(); + }); + }, + ), + ) + ], + ), + ), ); } - void enableE2EE() { - widget.room.enableE2EE(); + Widget buildRoomVisibility() { + return IgnorePointer( + ignoring: !widget.room.permissions.canChangeVisibility, + child: tiamat.Panel( + header: "Room Visibility", + mode: TileType.surfaceContainerLow, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () async { + List spaces = List.empty(growable: true); + if (widget.room.visibility + case RoomVisibilityRestricted restricted) { + spaces.addAll(restricted.spaces); + } + + if (widget.contextSpace != null && + !spaces.contains(widget.contextSpace?.identifier)) { + spaces.add(widget.contextSpace!.identifier); + } + + if (spaces.isEmpty) { + var parents = widget.room.client.spaces.where((i) => i.subspaces + .any((i) => i.identifier == widget.room.identifier)); + + for (var p in parents) { + spaces.add(p.identifier); + } + } + + var items = [ + if (spaces.isNotEmpty) RoomVisibilityRestricted(spaces), + RoomVisibilityPrivate(), + RoomVisibilityPublic(), + ]; + + var newVisibility = await AdaptiveDialog.pickOne( + title: "Set Visibility", + context, + items: items, + itemBuilder: (context, item, callback) { + return Material( + borderRadius: BorderRadius.circular(8), + clipBehavior: Clip.antiAlias, + color: Colors.transparent, + child: InkWell( + onTap: callback, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: RoomFieldVisibility.buildRoomVisibility( + widget.room.client, item), + ), + ), + ); + }, + ); + + if (newVisibility != null) { + ErrorUtils.tryRun(context, () async { + await widget.room.setVisibility(newVisibility); + + setState(() { + visibility = newVisibility; + }); + }); + } + }, + child: RoomFieldVisibility.buildRoomVisibility( + widget.room.client, visibility), + ), + ), + ), + ); } } diff --git a/commet/lib/ui/pages/settings/categories/room/security/room_security_settings_view.dart b/commet/lib/ui/pages/settings/categories/room/security/room_security_settings_view.dart deleted file mode 100644 index 0a05357dc..000000000 --- a/commet/lib/ui/pages/settings/categories/room/security/room_security_settings_view.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:intl/intl.dart'; - -import 'package:tiamat/tiamat.dart' as tiamat; - -class RoomSecuritySettingsView extends StatefulWidget { - const RoomSecuritySettingsView( - {super.key, - this.enableE2EE, - this.isE2EEEnabled, - this.supportsE2EE = false}); - final bool? isE2EEEnabled; - final bool supportsE2EE; - final Function()? enableE2EE; - - @override - State createState() => - _RoomSecuritySettingsViewState(); -} - -class _RoomSecuritySettingsViewState extends State { - late bool isE2EEEnabled; - - String get promptEnableEncryptionRoomSettings => - Intl.message("Enable Encryption", - name: "promptEnableEncryptionRoomSettings", - desc: "Short prompt to enable encryption for a room"); - - String get encryptionCannotBeDisabledExplanationRoomSettings => - Intl.message("If enabled, encryption cannot be disabled later", - name: "encryptionCannotBeDisabledExplanationRoomSettings", - desc: "Explains that encryption cannot be disabled once enabled"); - - @override - void initState() { - isE2EEEnabled = widget.isE2EEEnabled ?? false; - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - if (widget.supportsE2EE) buildE2EEToggle(), - ], - ); - } - - Widget buildE2EEToggle() { - return tiamat.Panel( - mode: tiamat.TileType.surfaceContainerLow, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - tiamat.Text.labelEmphasised(promptEnableEncryptionRoomSettings), - tiamat.Text.labelLow( - encryptionCannotBeDisabledExplanationRoomSettings) - ]), - IgnorePointer( - ignoring: isE2EEEnabled, - child: tiamat.Switch( - state: isE2EEEnabled, - onChanged: (value) { - if (value != true) return; - setState(() { - isE2EEEnabled = true; - widget.enableE2EE?.call(); - }); - }, - ), - ) - ], - ), - ); - } -} diff --git a/commet/lib/ui/pages/settings/categories/room/settings_category_room.dart b/commet/lib/ui/pages/settings/categories/room/settings_category_room.dart index 5b830633f..e35a2a1c0 100644 --- a/commet/lib/ui/pages/settings/categories/room/settings_category_room.dart +++ b/commet/lib/ui/pages/settings/categories/room/settings_category_room.dart @@ -16,8 +16,9 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; class SettingsCategoryRoom implements SettingsCategory { - SettingsCategoryRoom(this.room); + SettingsCategoryRoom(this.room, this.contextSpace); Room room; + Space? contextSpace; String get labelRoomSettingsGeneral => Intl.message( "General", @@ -94,14 +95,16 @@ class SettingsCategoryRoom implements SettingsCategory { return RoomAppearanceSettingsPage(room: room); }, ), - if (room.permissions.canEditRoomSecurity) - SettingsTab( - label: labelRoomSettingsSecurity, - icon: Icons.lock, - pageBuilder: (context) { - return RoomSecuritySettingsPage(room: room); - }, - ), + SettingsTab( + label: labelRoomSettingsSecurity, + icon: Icons.lock, + pageBuilder: (context) { + return RoomSecuritySettingsPage( + room: room, + contextSpace: contextSpace, + ); + }, + ), if (emoticons != null && (room.permissions.canEditRoomEmoticons || emoticons.ownedPacks.isNotEmpty)) diff --git a/commet/lib/ui/pages/settings/categories/space/settings_category_space.dart b/commet/lib/ui/pages/settings/categories/space/settings_category_space.dart index a7f2f2106..ab34c2f16 100644 --- a/commet/lib/ui/pages/settings/categories/space/settings_category_space.dart +++ b/commet/lib/ui/pages/settings/categories/space/settings_category_space.dart @@ -6,6 +6,7 @@ import 'package:commet/client/matrix/matrix_space.dart'; import 'package:commet/main.dart'; import 'package:commet/ui/molecules/user_list.dart'; import 'package:commet/ui/pages/settings/categories/room/permissions/matrix/matrix_room_permissions_page.dart'; +import 'package:commet/ui/pages/settings/categories/room/security/room_security_settings_page.dart'; import 'package:commet/ui/pages/settings/categories/space/space_developer_settings_view.dart'; import 'package:commet/ui/pages/settings/categories/space/space_emoji_pack_settings.dart'; import 'package:commet/ui/pages/settings/categories/space/space_general_settings_page.dart'; @@ -70,6 +71,18 @@ class SettingsCategorySpace implements SettingsCategory { space: space, ); }), + if (space case MatrixSpace s) + SettingsTab( + label: "Security", + icon: Icons.lock, + makeScrollable: false, + pageBuilder: (context) { + return RoomSecuritySettingsPage( + showEncryptionToggle: false, + room: MatrixRoom(space.client as MatrixClient, s.matrixRoom, + s.matrixRoom.client)); + }, + ), if (emoticons != null && (space.permissions.canEditRoomEmoticons || emoticons.ownedPacks.isNotEmpty)) @@ -105,7 +118,7 @@ class SettingsCategorySpace implements SettingsCategory { return RoomMemberList(MatrixRoom(space.client as MatrixClient, s.matrixRoom, s.matrixRoom.client)); }, - ) + ), ]); } } diff --git a/commet/lib/ui/pages/settings/room_settings_page.dart b/commet/lib/ui/pages/settings/room_settings_page.dart index a330f8ed1..409a92674 100644 --- a/commet/lib/ui/pages/settings/room_settings_page.dart +++ b/commet/lib/ui/pages/settings/room_settings_page.dart @@ -7,8 +7,10 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; class RoomSettingsPage extends StatefulWidget { - const RoomSettingsPage({super.key, required this.room, this.onLeaveRoom}); + const RoomSettingsPage( + {super.key, this.contextSpace, required this.room, this.onLeaveRoom}); final Room room; + final Space? contextSpace; final Function()? onLeaveRoom; @override @@ -31,6 +33,7 @@ class _RoomSettingsPageState extends State { settings: [ SettingsCategoryRoom( widget.room, + widget.contextSpace, ), ], buttons: [