diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift
index 359e321bb..0e6209163 100644
--- a/Config/BuildSettings.swift
+++ b/Config/BuildSettings.swift
@@ -235,7 +235,7 @@ final class BuildSettings: NSObject {
static let allowInviteExernalUsers: Bool = true
// MARK: - Side Menu
- static let enableSideMenu: Bool = true
+ static let enableSideMenu: Bool = true && !newAppLayoutEnabled
static let sideMenuShowInviteFriends: Bool = true
/// Whether to read the `io.element.functional_members` state event and exclude any service members when computing a room's name and avatar.
diff --git a/Riot/Assets/Images.xcassets/Home/RoomContextualMenu/Contents.json b/Riot/Assets/Images.xcassets/Home/RoomContextualMenu/Contents.json
index da4a164c9..73c00596a 100644
--- a/Riot/Assets/Images.xcassets/Home/RoomContextualMenu/Contents.json
+++ b/Riot/Assets/Images.xcassets/Home/RoomContextualMenu/Contents.json
@@ -1,6 +1,6 @@
{
"info" : {
- "version" : 1,
- "author" : "xcode"
+ "author" : "xcode",
+ "version" : 1
}
-}
\ No newline at end of file
+}
diff --git a/Riot/Assets/Images.xcassets/Home/home_fab_create_room.imageset/Contents.json b/Riot/Assets/Images.xcassets/Home/home_fab_create_room.imageset/Contents.json
deleted file mode 100644
index 5b6d4a656..000000000
--- a/Riot/Assets/Images.xcassets/Home/home_fab_create_room.imageset/Contents.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "images" : [
- {
- "filename" : "home_fab_create_room.png",
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "filename" : "home_fab_create_room@2x.png",
- "idiom" : "universal",
- "scale" : "2x"
- },
- {
- "filename" : "home_fab_create_room@3x.png",
- "idiom" : "universal",
- "scale" : "3x"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- },
- "properties" : {
- "template-rendering-intent" : "template"
- }
-}
diff --git a/Riot/Assets/Images.xcassets/Home/home_fab_create_room.imageset/home_fab_create_room.png b/Riot/Assets/Images.xcassets/Home/home_fab_create_room.imageset/home_fab_create_room.png
deleted file mode 100644
index ec25c6d82..000000000
Binary files a/Riot/Assets/Images.xcassets/Home/home_fab_create_room.imageset/home_fab_create_room.png and /dev/null differ
diff --git a/Riot/Assets/Images.xcassets/Home/home_fab_create_room.imageset/home_fab_create_room@2x.png b/Riot/Assets/Images.xcassets/Home/home_fab_create_room.imageset/home_fab_create_room@2x.png
deleted file mode 100644
index 47d024dc1..000000000
Binary files a/Riot/Assets/Images.xcassets/Home/home_fab_create_room.imageset/home_fab_create_room@2x.png and /dev/null differ
diff --git a/Riot/Assets/Images.xcassets/Home/home_fab_create_room.imageset/home_fab_create_room@3x.png b/Riot/Assets/Images.xcassets/Home/home_fab_create_room.imageset/home_fab_create_room@3x.png
deleted file mode 100644
index fef140a3b..000000000
Binary files a/Riot/Assets/Images.xcassets/Home/home_fab_create_room.imageset/home_fab_create_room@3x.png and /dev/null differ
diff --git a/Riot/Assets/Images.xcassets/Home/home_fab_join_room.imageset/home_fab_join_room.png b/Riot/Assets/Images.xcassets/Home/home_fab_join_room.imageset/home_fab_join_room.png
deleted file mode 100644
index e74a2d4db..000000000
Binary files a/Riot/Assets/Images.xcassets/Home/home_fab_join_room.imageset/home_fab_join_room.png and /dev/null differ
diff --git a/Riot/Assets/Images.xcassets/Home/home_fab_join_room.imageset/home_fab_join_room@2x.png b/Riot/Assets/Images.xcassets/Home/home_fab_join_room.imageset/home_fab_join_room@2x.png
deleted file mode 100644
index 38cf26a4e..000000000
Binary files a/Riot/Assets/Images.xcassets/Home/home_fab_join_room.imageset/home_fab_join_room@2x.png and /dev/null differ
diff --git a/Riot/Assets/Images.xcassets/Home/home_fab_join_room.imageset/home_fab_join_room@3x.png b/Riot/Assets/Images.xcassets/Home/home_fab_join_room.imageset/home_fab_join_room@3x.png
deleted file mode 100644
index ac964984c..000000000
Binary files a/Riot/Assets/Images.xcassets/Home/home_fab_join_room.imageset/home_fab_join_room@3x.png and /dev/null differ
diff --git a/Riot/Assets/Images.xcassets/Home/home_fab_join_room.imageset/Contents.json b/Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/Contents.json
similarity index 70%
rename from Riot/Assets/Images.xcassets/Home/home_fab_join_room.imageset/Contents.json
rename to Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/Contents.json
index b0b8fb364..d50c95b02 100644
--- a/Riot/Assets/Images.xcassets/Home/home_fab_join_room.imageset/Contents.json
+++ b/Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/Contents.json
@@ -1,17 +1,15 @@
{
"images" : [
{
- "filename" : "home_fab_join_room.png",
+ "filename" : "home_my_spaces_action.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
- "filename" : "home_fab_join_room@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
- "filename" : "home_fab_join_room@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
@@ -21,6 +19,7 @@
"version" : 1
},
"properties" : {
+ "preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
diff --git a/Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/home_my_spaces_action.svg b/Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/home_my_spaces_action.svg
new file mode 100644
index 000000000..3b956bb6b
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/home_my_spaces_action.svg
@@ -0,0 +1,6 @@
+
diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings
index b0e17ec47..7ff7f0697 100644
--- a/Riot/Assets/en.lproj/Vector.strings
+++ b/Riot/Assets/en.lproj/Vector.strings
@@ -1998,6 +1998,7 @@ Tap the + to start adding people.";
"spaces_add_space_title" = "Create space";
"spaces_add_subspace_title" = "Create space within %@";
"spaces_create_space_title" = "Create a space";
+"spaces_create_subspace_title" = "Create a subspace";
"spaces_left_panel_title" = "Spaces";
"leave_space_title" = "Leave %@";
"leave_space_message" = "Are you sure you want to leave %@? Do you also want to leave all rooms and spaces of this space?";
@@ -2043,6 +2044,8 @@ Tap the + to start adding people.";
"spaces_creation_hint" = "Spaces are a new way to group rooms and people.";
"spaces_creation_visibility_title" = "What type of space do you want to create?";
"spaces_creation_visibility_message" = "To join an existing space, you need an invite.";
+"spaces_subspace_creation_visibility_title" = "What type of subspace do you want to create?";
+"spaces_subspace_creation_visibility_message" = "The created space will be added to %@.";
"spaces_creation_footer" = "You can change this later";
"spaces_creation_settings_message" = "Add some details to help it stand out. You can change these at any point.";
"spaces_creation_address" = "Address";
@@ -2158,7 +2161,7 @@ Tap the + to start adding people.";
"all_chats_title" = "All chats";
"all_chats_section_title" = "Chats";
-"all_chats_edit_layout" = "Edit layout";
+"all_chats_edit_layout" = "Layout preferences";
"all_chats_edit_layout_recents" = "Recents";
"all_chats_edit_layout_unreads" = "Unreads";
"all_chats_edit_layout_add_section_title" = "Add section to home";
@@ -2176,9 +2179,14 @@ Tap the + to start adding people.";
"room_recents_recently_viewed_section" = "Recently viewed";
+"all_chats_user_menu_settings" = "User settings";
+
+"all_chats_edit_menu_leave_space" = "Leave %@";
+"all_chats_edit_menu_space_settings" = "Space settings";
+
// Mark: - Space Selector
-"space_selector_title" = "Choose space";
+"space_selector_title" = "My spaces";
// Mark: - Polls
diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift
index 511cdb1d3..d0373b94f 100644
--- a/Riot/Generated/Images.swift
+++ b/Riot/Generated/Images.swift
@@ -115,8 +115,7 @@ internal class Asset: NSObject {
internal static let roomActionPriorityLow = ImageAsset(name: "room_action_priority_low")
internal static let homeEmptyScreenArtwork = ImageAsset(name: "home_empty_screen_artwork")
internal static let homeEmptyScreenArtworkDark = ImageAsset(name: "home_empty_screen_artwork_dark")
- internal static let homeFabCreateRoom = ImageAsset(name: "home_fab_create_room")
- internal static let homeFabJoinRoom = ImageAsset(name: "home_fab_join_room")
+ internal static let homeMySpacesAction = ImageAsset(name: "home_my_spaces_action")
internal static let plusFloatingAction = ImageAsset(name: "plus_floating_action")
internal static let versionCheckCloseIcon = ImageAsset(name: "version_check_close_icon")
internal static let versionCheckInfoIcon = ImageAsset(name: "version_check_info_icon")
diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift
index 36c46a9ac..d4323c05d 100644
--- a/Riot/Generated/Strings.swift
+++ b/Riot/Generated/Strings.swift
@@ -119,7 +119,7 @@ public class VectorL10n: NSObject {
public static var allChatsAllFilter: String {
return VectorL10n.tr("Vector", "all_chats_all_filter")
}
- /// Edit layout
+ /// Layout preferences
public static var allChatsEditLayout: String {
return VectorL10n.tr("Vector", "all_chats_edit_layout")
}
@@ -171,6 +171,14 @@ public class VectorL10n: NSObject {
public static var allChatsEditLayoutUnreads: String {
return VectorL10n.tr("Vector", "all_chats_edit_layout_unreads")
}
+ /// Leave %@
+ public static func allChatsEditMenuLeaveSpace(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "all_chats_edit_menu_leave_space", p1)
+ }
+ /// Space settings
+ public static var allChatsEditMenuSpaceSettings: String {
+ return VectorL10n.tr("Vector", "all_chats_edit_menu_space_settings")
+ }
/// Chats
public static var allChatsSectionTitle: String {
return VectorL10n.tr("Vector", "all_chats_section_title")
@@ -179,6 +187,10 @@ public class VectorL10n: NSObject {
public static var allChatsTitle: String {
return VectorL10n.tr("Vector", "all_chats_title")
}
+ /// User settings
+ public static var allChatsUserMenuSettings: String {
+ return VectorL10n.tr("Vector", "all_chats_user_menu_settings")
+ }
/// Help us identify issues and improve %@ by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices.
public static func analyticsPromptMessageNewUser(_ p1: String) -> String {
return VectorL10n.tr("Vector", "analytics_prompt_message_new_user", p1)
@@ -7779,7 +7791,7 @@ public class VectorL10n: NSObject {
public static var spacePublicJoinRuleDetail: String {
return VectorL10n.tr("Vector", "space_public_join_rule_detail")
}
- /// Choose space
+ /// My spaces
public static var spaceSelectorTitle: String {
return VectorL10n.tr("Vector", "space_selector_title")
}
@@ -7839,6 +7851,10 @@ public class VectorL10n: NSObject {
public static var spacesCreateSpaceTitle: String {
return VectorL10n.tr("Vector", "spaces_create_space_title")
}
+ /// Create a subspace
+ public static var spacesCreateSubspaceTitle: String {
+ return VectorL10n.tr("Vector", "spaces_create_subspace_title")
+ }
/// As this space is just for you, no one will be informed. You can add more later.
public static var spacesCreationAddRoomsMessage: String {
return VectorL10n.tr("Vector", "spaces_creation_add_rooms_message")
@@ -8071,6 +8087,14 @@ public class VectorL10n: NSObject {
public static var spacesNoRoomFoundDetail: String {
return VectorL10n.tr("Vector", "spaces_no_room_found_detail")
}
+ /// The created space will be added to %@.
+ public static func spacesSubspaceCreationVisibilityMessage(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "spaces_subspace_creation_visibility_message", p1)
+ }
+ /// What type of subspace do you want to create?
+ public static var spacesSubspaceCreationVisibilityTitle: String {
+ return VectorL10n.tr("Vector", "spaces_subspace_creation_visibility_title")
+ }
/// Suggested
public static var spacesSuggestedRoom: String {
return VectorL10n.tr("Vector", "spaces_suggested_room")
diff --git a/Riot/Managers/Theme/Themes/DarkTheme.swift b/Riot/Managers/Theme/Themes/DarkTheme.swift
index fbf265729..fb4a2a3d7 100644
--- a/Riot/Managers/Theme/Themes/DarkTheme.swift
+++ b/Riot/Managers/Theme/Themes/DarkTheme.swift
@@ -146,8 +146,16 @@ class DarkTheme: NSObject, Theme {
navigationBar.standardAppearance = appearance
- navigationBar.scrollEdgeAppearance = modernScrollEdgeAppearance || BuildSettings.newAppLayoutEnabled ? nil : appearance
- } else {
+ if BuildSettings.newAppLayoutEnabled {
+ appearance.configureWithOpaqueBackground()
+ appearance.backgroundColor = baseColor
+ appearance.shadowColor = nil
+ appearance.titleTextAttributes = [
+ NSAttributedString.Key.foregroundColor: textPrimaryColor
+ ]
+ }
+ navigationBar.scrollEdgeAppearance = modernScrollEdgeAppearance ? nil : appearance
+ } else {
navigationBar.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: textPrimaryColor
]
diff --git a/Riot/Managers/Theme/Themes/DefaultTheme.swift b/Riot/Managers/Theme/Themes/DefaultTheme.swift
index 289aabf0f..d1207330a 100644
--- a/Riot/Managers/Theme/Themes/DefaultTheme.swift
+++ b/Riot/Managers/Theme/Themes/DefaultTheme.swift
@@ -149,7 +149,15 @@ class DefaultTheme: NSObject, Theme {
]
navigationBar.standardAppearance = appearance
- navigationBar.scrollEdgeAppearance = modernScrollEdgeAppearance || BuildSettings.newAppLayoutEnabled ? nil : appearance
+ if BuildSettings.newAppLayoutEnabled {
+ appearance.configureWithOpaqueBackground()
+ appearance.backgroundColor = baseColor
+ appearance.shadowColor = nil
+ appearance.titleTextAttributes = [
+ NSAttributedString.Key.foregroundColor: textPrimaryColor
+ ]
+ }
+ navigationBar.scrollEdgeAppearance = modernScrollEdgeAppearance ? nil : appearance
} else {
navigationBar.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: textPrimaryColor
diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m
index 1d59793d4..29aac7808 100644
--- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m
+++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m
@@ -183,7 +183,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
[types addObject:@(RecentsDataSourceSectionTypeSecureBackupBanner)];
}
- if (self.invitesCellDataArray.count > 0)
+ if (!BuildSettings.newAppLayoutEnabled && self.invitesCellDataArray.count > 0)
{
[types addObject:@(RecentsDataSourceSectionTypeInvites)];
}
@@ -229,7 +229,12 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
[types addObject:@(RecentsDataSourceSectionTypeAllChats)];
}
- if (self.suggestedRoomCellDataArray.count > 0)
+ if (self.currentSpace == nil && BuildSettings.newAppLayoutEnabled && self.invitesCellDataArray.count > 0)
+ {
+ [types addObject:@(RecentsDataSourceSectionTypeInvites)];
+ }
+
+ if (self.currentSpace != nil && self.suggestedRoomCellDataArray.count > 0)
{
[types addObject:@(RecentsDataSourceSectionTypeSuggestedRooms)];
}
diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m
index aeacb789e..ba23d57c4 100644
--- a/Riot/Modules/Common/Recents/RecentsViewController.m
+++ b/Riot/Modules/Common/Recents/RecentsViewController.m
@@ -1118,166 +1118,6 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
#pragma mark - Swipe actions
-- (void)tableView:(UITableView*)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath
-{
- [self cancelEditionMode:isRefreshPending];
-}
-
-- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
-{
- return UITableViewCellEditingStyleNone;
-}
-
-- (nullable UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
-{
- MXRoom *room = [self.dataSource getRoomAtIndexPath:indexPath];
-
- if (!room)
- {
- return nil;
- }
-
- // Display no action for the invited room
- if (room.summary.membership == MXMembershipInvite)
- {
- return nil;
- }
-
- // Store the identifier of the room related to the edited cell.
- editedRoomId = room.roomId;
-
- UIColor *selectedColor = ThemeService.shared.theme.tintColor;
- UIColor *unselectedColor = ThemeService.shared.theme.tabBarUnselectedItemTintColor;
- UIColor *actionBackgroundColor = ThemeService.shared.theme.baseColor;
-
- NSString* title = @" ";
-
- // Direct chat toggle
-
- BOOL isDirect = room.isDirect;
-
- UIContextualAction *directChatAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive
- title:title
- handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
- [self makeDirectEditedRoom:!isDirect];
- completionHandler(YES);
- }];
- directChatAction.backgroundColor = actionBackgroundColor;
-
- UIImage *directChatImage = AssetImages.roomActionDirectChat.image;
- directChatImage = [directChatImage vc_tintedImageUsingColor:isDirect ? selectedColor : unselectedColor];
- directChatAction.image = [directChatImage vc_notRenderedImage];
-
- // Notification toggle
-
- BOOL isMuted = room.isMute || room.isMentionsOnly;
-
- UIContextualAction *muteAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive
- title:title
- handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
-
- if ([BuildSettings showNotificationsV2])
- {
- [self changeEditedRoomNotificationSettings];
- }
- else
- {
- [self muteEditedRoomNotifications:!isMuted];
- }
-
-
- completionHandler(YES);
- }];
- muteAction.backgroundColor = actionBackgroundColor;
-
- UIImage *notificationImage;
- if([BuildSettings showNotificationsV2] && isMuted)
- {
- notificationImage = AssetImages.roomActionNotificationMuted.image;
- }
- else
- {
- notificationImage = AssetImages.roomActionNotification.image;
- }
-
- notificationImage = [notificationImage vc_tintedImageUsingColor:isMuted ? unselectedColor : selectedColor];
- muteAction.image = [notificationImage vc_notRenderedImage];
-
- // Favorites management
-
- MXRoomTag* currentTag = nil;
-
- // Get the room tag (use only the first one).
- if (room.accountData.tags)
- {
- NSArray* tags = room.accountData.tags.allValues;
- if (tags.count)
- {
- currentTag = tags[0];
- }
- }
-
- BOOL isFavourite = (currentTag && [kMXRoomTagFavourite isEqualToString:currentTag.name]);
-
- UIContextualAction *favouriteAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive
- title:title
- handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
- NSString *favouriteTag = isFavourite ? nil : kMXRoomTagFavourite;
- [self updateEditedRoomTag:favouriteTag];
- completionHandler(YES);
- }];
- favouriteAction.backgroundColor = actionBackgroundColor;
-
- UIImage *favouriteImage = AssetImages.roomActionFavourite.image;
- favouriteImage = [favouriteImage vc_tintedImageUsingColor:isFavourite ? selectedColor : unselectedColor];
- favouriteAction.image = [favouriteImage vc_notRenderedImage];
-
- // Priority toggle
-
- BOOL isInLowPriority = (currentTag && [kMXRoomTagLowPriority isEqualToString:currentTag.name]);
-
- UIContextualAction *priorityAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive
- title:title
- handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
- NSString *priorityTag = isInLowPriority ? nil : kMXRoomTagLowPriority;
- [self updateEditedRoomTag:priorityTag];
- completionHandler(YES);
- }];
- priorityAction.backgroundColor = actionBackgroundColor;
-
- UIImage *priorityImage = isInLowPriority ? AssetImages.roomActionPriorityHigh.image : AssetImages.roomActionPriorityLow.image;
- priorityImage = [priorityImage vc_tintedImageUsingColor:unselectedColor];
- priorityAction.image = [priorityImage vc_notRenderedImage];
-
- // Leave action
-
- UIContextualAction *leaveAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive
- title:title
- handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
- [self leaveEditedRoom];
- completionHandler(YES);
- }];
- leaveAction.backgroundColor = actionBackgroundColor;
-
- UIImage *leaveImage = AssetImages.roomActionLeave.image;
- leaveImage = [leaveImage vc_tintedImageUsingColor:unselectedColor];
- leaveAction.image = [leaveImage vc_notRenderedImage];
-
- // Create swipe action configuration
-
- NSArray *actions = @[
- leaveAction,
- priorityAction,
- favouriteAction,
- muteAction,
- directChatAction
- ];
-
- UISwipeActionsConfiguration *swipeActionConfiguration = [UISwipeActionsConfiguration configurationWithActions:actions];
- swipeActionConfiguration.performsFirstActionWithFullSwipe = NO;
- return swipeActionConfiguration;
-}
-
- (void)leaveEditedRoom
{
if (editedRoomId)
diff --git a/Riot/Modules/Common/Recents/Service/MatrixSDK/RecentsListService.swift b/Riot/Modules/Common/Recents/Service/MatrixSDK/RecentsListService.swift
index e0820fa7e..c0474a17b 100644
--- a/Riot/Modules/Common/Recents/Service/MatrixSDK/RecentsListService.swift
+++ b/Riot/Modules/Common/Recents/Service/MatrixSDK/RecentsListService.swift
@@ -35,7 +35,7 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
private var invitedRoomListDataFetcher: MXRoomListDataFetcher? {
switch mode {
- case .home:
+ case .home, .allChats:
return invitedRoomListDataFetcherForHome
case .people:
return invitedRoomListDataFetcherForPeople
@@ -548,7 +548,7 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
private func createCommonRoomListDataFetcher(withDataTypes dataTypes: MXRoomSummaryDataTypes = [],
onlySuggested: Bool = false,
- onlyRecents: Bool = false,
+ onlyBreadcrumbs: Bool = false,
paginate: Bool = true,
strictMatches: Bool = false) -> MXRoomListDataFetcher {
guard let session = session else {
@@ -556,14 +556,14 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
}
let filterOptions = MXRoomListDataFilterOptions(dataTypes: dataTypes,
onlySuggested: onlySuggested,
- onlyBreadcrumbs: onlyRecents,
+ onlyBreadcrumbs: onlyBreadcrumbs,
query: query,
space: space,
showAllRoomsInHomeSpace: showAllRoomsInHomeSpace,
strictMatches: strictMatches)
let fetchOptions = MXRoomListDataFetchOptions(filterOptions: filterOptions,
- sortOptions: onlyRecents ? noSortOptions : sortOptions,
+ sortOptions: onlyBreadcrumbs ? noSortOptions : sortOptions,
async: true)
let fetcher = session.roomListDataManager.fetcher(withOptions: fetchOptions)
if paginate {
@@ -653,7 +653,7 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
lowPriorityRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.lowPriority])
serverNoticeRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.serverNotice])
suggestedRoomListDataFetcher = createCommonRoomListDataFetcher(onlySuggested: true)
- breadcrumbsRoomListDataFetcher = createCommonRoomListDataFetcher(onlyRecents: true)
+ breadcrumbsRoomListDataFetcher = createCommonRoomListDataFetcher(onlyBreadcrumbs: true)
allChatsRoomListDataFetcher = createConversationRoomListDataFetcherForAllChats()
fetchersCreated = true
diff --git a/Riot/Modules/Common/SectionHeaders/AllChatsFilterOptionListView.swift b/Riot/Modules/Common/SectionHeaders/AllChatsFilterOptionListView.swift
index 5d1f67190..f0c681636 100644
--- a/Riot/Modules/Common/SectionHeaders/AllChatsFilterOptionListView.swift
+++ b/Riot/Modules/Common/SectionHeaders/AllChatsFilterOptionListView.swift
@@ -74,7 +74,7 @@ class AllChatsFilterOptionListView: UIView, Themable {
// MARK: - Themable
func update(theme: Theme) {
- backgroundColor = theme.colors.background.withAlphaComponent(0.3)
+ backgroundColor = theme.colors.background.withAlphaComponent(0.7)
tabListView.itemFont = theme.fonts.callout
tabListView.tintColor = theme.colors.accent
diff --git a/Riot/Modules/ContextMenu/ActionProviders/AllChatsActionProvider.swift b/Riot/Modules/ContextMenu/ActionProviders/AllChatsActionProvider.swift
index be1ad4f8c..620d903ea 100644
--- a/Riot/Modules/ContextMenu/ActionProviders/AllChatsActionProvider.swift
+++ b/Riot/Modules/ContextMenu/ActionProviders/AllChatsActionProvider.swift
@@ -32,8 +32,8 @@ class AllChatsActionProvider {
self.recentsAction,
self.filtersAction,
UIMenu(title: "", options: .displayInline, children: [
- alphabeticalOrderAction,
- activityOrderAction
+ activityOrderAction,
+ alphabeticalOrderAction
])
])
}
diff --git a/Riot/Modules/ContextMenu/ActionProviders/AllChatsEditActionProvider.swift b/Riot/Modules/ContextMenu/ActionProviders/AllChatsEditActionProvider.swift
new file mode 100644
index 000000000..2b2c74e4c
--- /dev/null
+++ b/Riot/Modules/ContextMenu/ActionProviders/AllChatsEditActionProvider.swift
@@ -0,0 +1,198 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import UIKit
+import MatrixSDK
+
+enum AllChatsEditActionProviderOption {
+ case exploreRooms
+ case createRoom
+ case startChat
+ case invitePeople
+ case spaceMembers
+ case spaceSettings
+ case leaveSpace
+ case createSpace
+}
+
+protocol AllChatsEditActionProviderDelegate: AnyObject {
+ func allChatsEditActionProvider(_ actionProvider: AllChatsEditActionProvider, didSelect option: AllChatsEditActionProviderOption)
+}
+
+/// `AllChatsEditActionProvider` provides the menu for accessing edit screens according to the current parent space
+class AllChatsEditActionProvider {
+
+ // MARK: - Properties
+
+ weak var delegate: AllChatsEditActionProviderDelegate?
+
+ // MARK: - Private
+
+ private var parentSpace: MXSpace? {
+ didSet {
+ parentName = parentSpace?.summary?.displayname ?? VectorL10n.spaceTag
+ }
+ }
+ private var parentName: String = VectorL10n.spaceTag
+ private var isInviteAvailable: Bool = false
+ private var isAddRoomAvailable: Bool = true
+
+ // MARK: - RoomActionProviderProtocol
+
+ var menu: UIMenu {
+ guard parentSpace != nil else {
+ return UIMenu(title: VectorL10n.allChatsTitle, children: [
+ self.exploreRoomsAction,
+ UIMenu(title: "", options: .displayInline, children: [
+ self.startChatAction,
+ self.createRoomAction,
+ self.createSpaceAction
+ ])
+ ])
+ }
+
+ return UIMenu(title: parentName, children: [
+ UIMenu(title: "", options: .displayInline, children: [
+ self.spaceMembersAction,
+ self.exploreRoomsAction,
+ self.spaceSettingsAction
+ ]),
+ UIMenu(title: "", options: .displayInline, children: [
+ self.invitePeopleAction,
+ self.createRoomAction,
+ self.createSpaceAction
+ ]),
+ self.leaveSpaceAction
+ ])
+ }
+
+ // MARK: - Public
+
+ /// Returns an instance of the updated menu accordingly to the given parameters.
+ ///
+ /// Some menu items can be disabled depending on the required power levels of the `parentSpace`. Therefore, `updateMenu()` first returns a temporary context menu
+ /// with all sensible items disabled, asynchronously fetches power levels of the `parentSpace`, then gives a new instance of the menu with, potentially, all sensible items
+ /// enabled via the `completion` callback.
+ ///
+ /// - Parameters:
+ /// - session: The current `MXSession` instance
+ /// - parentSpace: The current parent space (`nil` for home space)
+ /// - completion: callback called once the power levels of the `parentSpace` have been fetched and the menu items have been computed accordingly.
+ /// - Returns: If the `parentSpace` is `nil`, the context menu, the temporary context menu otherwise.
+ func updateMenu(with session: MXSession?, parentSpace: MXSpace?, completion: @escaping (UIMenu) -> Void) -> UIMenu {
+ self.parentSpace = parentSpace
+ isInviteAvailable = false
+ isAddRoomAvailable = parentSpace == nil
+
+ guard let parentSpace = parentSpace, let spaceRoom = parentSpace.room, let session = session else {
+ return self.menu
+ }
+
+ spaceRoom.state { [weak self] roomState in
+ guard let self = self else { return }
+
+ guard let powerLevels = roomState?.powerLevels, let userId = session.myUserId else {
+ return
+ }
+ let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: userId)
+
+ self.isInviteAvailable = userPowerLevel >= powerLevels.invite
+ self.isAddRoomAvailable = userPowerLevel >= parentSpace.minimumPowerLevelForAddingRoom(with: powerLevels)
+
+ completion(self.menu)
+ }
+
+ return self.menu
+ }
+
+ // MARK: - Private
+
+ private var exploreRoomsAction: UIAction {
+ UIAction(title: VectorL10n.spacesExploreRooms,
+ image: parentSpace == nil ? UIImage(systemName: "list.bullet") : UIImage(systemName: "square.fill.text.grid.1x2")) { [weak self] action in
+ guard let self = self else { return }
+
+ self.delegate?.allChatsEditActionProvider(self, didSelect: .exploreRooms)
+ }
+ }
+
+ private var createRoomAction: UIAction {
+ UIAction(title: parentSpace == nil ? VectorL10n.roomRecentsCreateEmptyRoom : VectorL10n.spacesAddRoom,
+ image: UIImage(systemName: "number"),
+ attributes: isAddRoomAvailable ? [] : .disabled) { [weak self] action in
+ guard let self = self else { return }
+
+ self.delegate?.allChatsEditActionProvider(self, didSelect: .createRoom)
+ }
+ }
+
+ private var startChatAction: UIAction {
+ UIAction(title: VectorL10n.roomRecentsStartChatWith,
+ image: UIImage(systemName: "person")) { [weak self] action in
+ guard let self = self else { return }
+
+ self.delegate?.allChatsEditActionProvider(self, didSelect: .startChat)
+ }
+ }
+
+ private var createSpaceAction: UIAction {
+ UIAction(title: parentSpace == nil ? VectorL10n.spacesCreateSpaceTitle : VectorL10n.spacesCreateSubspaceTitle,
+ image: UIImage(systemName: "plus"),
+ attributes: isAddRoomAvailable ? [] : .disabled) { [weak self] action in
+ guard let self = self else { return }
+
+ self.delegate?.allChatsEditActionProvider(self, didSelect: .createSpace)
+ }
+ }
+
+ private var invitePeopleAction: UIAction {
+ UIAction(title: VectorL10n.spacesInvitePeople,
+ image: UIImage(systemName: "person.badge.plus"),
+ attributes: isInviteAvailable ? [] : .disabled) { [weak self] action in
+ guard let self = self else { return }
+
+ self.delegate?.allChatsEditActionProvider(self, didSelect: .invitePeople)
+ }
+ }
+
+ private var spaceMembersAction: UIAction {
+ UIAction(title: VectorL10n.roomDetailsPeople,
+ image: UIImage(systemName: "person.3")) { [weak self] action in
+ guard let self = self else { return }
+
+ self.delegate?.allChatsEditActionProvider(self, didSelect: .spaceMembers)
+ }
+ }
+
+ private var spaceSettingsAction: UIAction {
+ UIAction(title: VectorL10n.allChatsEditMenuSpaceSettings,
+ image: UIImage(systemName: "gearshape")) { [weak self] action in
+ guard let self = self else { return }
+
+ self.delegate?.allChatsEditActionProvider(self, didSelect: .spaceSettings)
+ }
+ }
+
+ private var leaveSpaceAction: UIAction {
+ UIAction(title: VectorL10n.allChatsEditMenuLeaveSpace(parentName),
+ image: UIImage(systemName: "rectangle.portrait.and.arrow.right.fill"),
+ attributes: .destructive) { [weak self] action in
+ guard let self = self else { return }
+
+ self.delegate?.allChatsEditActionProvider(self, didSelect: .leaveSpace)
+ }
+ }
+}
diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift
index 5d8f5ab6c..fe22bdb40 100644
--- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift
+++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift
@@ -25,12 +25,6 @@ class AllChatsViewController: HomeViewController {
return UINib(nibName: String(describing: self), bundle: Bundle(for: self.classForCoder()))
}
- // MARK: - Private
-
- @IBOutlet private weak var toolbar: UIToolbar!
-
- private let searchController = UISearchController(searchResultsController: nil)
-
static override func instantiate() -> Self {
let storyboard = UIStoryboard(name: "Main", bundle: .main)
guard let viewController = storyboard.instantiateViewController(withIdentifier: "AllChatsViewController") as? Self else {
@@ -39,30 +33,52 @@ class AllChatsViewController: HomeViewController {
return viewController
}
+ // MARK: - Private
+
+ private let searchController = UISearchController(searchResultsController: nil)
+
+ private let editActionProvider = AllChatsEditActionProvider()
+
+ private var spaceSelectorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter?
+
+ private var childCoordinators: [Coordinator] = []
+
+ // MARK: - Lifecycle
+
override func viewDidLoad() {
super.viewDidLoad()
+ editActionProvider.delegate = self
+
recentsTableView.tag = RecentsDataSourceMode.allChats.rawValue
recentsTableView.clipsToBounds = false
- self.tabBarController?.title = VectorL10n.allChatsTitle
+ updateUI()
vc_setLargeTitleDisplayMode(.automatic)
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchResultsUpdater = self
+ self.setupEditOptions()
NotificationCenter.default.addObserver(self, selector: #selector(self.setupEditOptions), name: AllChatsLayoutSettingsManager.didUpdateSettings, object: nil)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
- toolbar.tintColor = ThemeService.shared().theme.colors.accent
+ self.navigationController?.isToolbarHidden = false
+ self.navigationController?.toolbar.tintColor = ThemeService.shared().theme.colors.accent
if self.tabBarController?.navigationItem.searchController == nil {
self.tabBarController?.navigationItem.searchController = searchController
}
}
+ override func viewWillDisappear(_ animated: Bool) {
+ super.viewWillDisappear(animated)
+
+ self.navigationController?.isToolbarHidden = true
+ }
+
// MARK: - HomeViewController
override var recentsDataSourceMode: RecentsDataSourceMode {
@@ -70,7 +86,7 @@ class AllChatsViewController: HomeViewController {
}
@objc private func addFabButton() {
- self.setupEditOptions()
+ // Nothing to do. We don't need FAB
}
@objc private func sections() -> Array {
@@ -87,39 +103,227 @@ class AllChatsViewController: HomeViewController {
]
}
+ // MARK: - Actions
+
+ @objc private func showSpaceSelectorAction(sender: AnyObject) {
+ let currentSpaceId = self.dataSource.currentSpace?.spaceId ?? SpaceSelectorConstants.homeSpaceId
+ let spaceSelectorBridgePresenter = SpaceSelectorBottomSheetCoordinatorBridgePresenter(session: self.mainSession, selectedSpaceId: currentSpaceId, showHomeSpace: true)
+ spaceSelectorBridgePresenter.present(from: self, animated: true)
+ spaceSelectorBridgePresenter.delegate = self
+ self.spaceSelectorBridgePresenter = spaceSelectorBridgePresenter
+ }
+
+ // MARK: - Toolbar animation
+
+ private var lastScrollPosition: Double = 0
+
+ override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
+ lastScrollPosition = self.recentsTableView.contentOffset.y
+ }
+
+ override func scrollViewDidScroll(_ scrollView: UIScrollView) {
+ super.scrollViewDidScroll(scrollView)
+
+ if self.recentsTableView.contentOffset.y == 0 {
+ self.navigationController?.setToolbarHidden(false, animated: true)
+ }
+
+ guard self.recentsTableView.isDragging else {
+ return
+ }
+
+ let scrollPosition = max(self.recentsTableView.contentOffset.y, 0)
+ guard scrollPosition < self.recentsTableView.contentSize.height - self.recentsTableView.bounds.height else {
+ return
+ }
+
+ self.navigationController?.setToolbarHidden(scrollPosition - lastScrollPosition > 0, animated: true)
+ lastScrollPosition = scrollPosition
+ }
+
// MARK: - Private
@objc private func setupEditOptions() {
- // Note: updating toolbar items doesn't work as expected and has weird behaviour
- // Also this piece of code is going to be updated in the next PR
-
- let editMenu = UIMenu(children: [
- UIAction(title: VectorL10n.roomRecentsJoinRoom,
- image: Asset.Images.homeFabJoinRoom.image,
- discoverabilityTitle: VectorL10n.roomRecentsJoinRoom,
- handler: { [weak self] action in
- self?.joinARoom()
- }),
- UIAction(title: VectorL10n.roomRecentsCreateEmptyRoom,
- image: Asset.Images.homeFabCreateRoom.image,
- discoverabilityTitle: VectorL10n.roomRecentsCreateEmptyRoom,
- handler: { [weak self] action in
- self?.createNewRoom()
- }),
- UIAction(title: VectorL10n.roomRecentsStartChatWith,
- image: Asset.Images.sideMenuActionIconFeedback.image,
- discoverabilityTitle: VectorL10n.roomRecentsStartChatWith,
- handler: { [weak self] action in
- self?.startChat()
- })
- ])
-
- toolbar.items = [
- UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), menu: AllChatsActionProvider().menu),
- UIBarButtonItem.flexibleSpace(),
- UIBarButtonItem(image: UIImage(systemName: "square.and.pencil"), menu: editMenu)
- ]
+ self.tabBarController?.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "line.3.horizontal.decrease.circle"), menu: AllChatsActionProvider().menu)
}
+
+ private func updateUI() {
+ let currentSpace = self.dataSource?.currentSpace
+ self.tabBarController?.title = currentSpace?.summary?.displayname ?? VectorL10n.allChatsTitle
+
+ updateToolbar(with: editActionProvider.updateMenu(with: mainSession, parentSpace: currentSpace, completion: { [weak self] menu in
+ self?.updateToolbar(with: menu)
+ }))
+ }
+
+ private func updateToolbar(with menu: UIMenu) {
+ let currentSpace = self.dataSource?.currentSpace
+ self.navigationController?.isToolbarHidden = false
+ self.tabBarController?.setToolbarItems([
+ UIBarButtonItem(image: Asset.Images.homeMySpacesAction.image, style: .done, target: self, action: #selector(self.showSpaceSelectorAction(sender: ))),
+ UIBarButtonItem.flexibleSpace(),
+ UIBarButtonItem(image: UIImage(systemName: currentSpace == nil ? "square.and.pencil" : "ellipsis.circle"), menu: menu)
+ ], animated: true)
+ }
+
+ private func showCreateSpace(parentSpaceId: String?) {
+ let coordinator = SpaceCreationCoordinator(parameters: SpaceCreationCoordinatorParameters(session: self.mainSession, parentSpaceId: parentSpaceId))
+ let presentable = coordinator.toPresentable()
+ self.present(presentable, animated: true, completion: nil)
+ coordinator.callback = { [weak self] result in
+ guard let self = self else {
+ return
+ }
+
+ coordinator.toPresentable().dismiss(animated: true) {
+ self.remove(childCoordinator: coordinator)
+ switch result {
+ case .cancel:
+ break
+ case .done(let spaceId):
+ self.switchSpace(withId: spaceId)
+ }
+ }
+ }
+ add(childCoordinator: coordinator)
+ coordinator.start()
+ }
+
+ private func switchSpace(withId spaceId: String?) {
+ searchController.isActive = false
+
+ guard let spaceId = spaceId else {
+ self.dataSource.currentSpace = nil
+ updateUI()
+
+ return
+ }
+
+ guard let space = self.mainSession.spaceService.getSpace(withId: spaceId) else {
+ MXLog.warning("[AllChatsViewController] switchSpace: no space found with id \(spaceId)")
+ return
+ }
+
+ self.dataSource.currentSpace = space
+ updateUI()
+
+ self.recentsTableView.setContentOffset(.zero, animated: true)
+ }
+
+ private func add(childCoordinator: Coordinator) {
+ self.childCoordinators.append(childCoordinator)
+ }
+
+ private func remove(childCoordinator: Coordinator) {
+ self.childCoordinators.append(childCoordinator)
+ }
+
+ private func showSpaceInvite() {
+ guard let session = mainSession, let spaceRoom = dataSource.currentSpace?.room else {
+ return
+ }
+
+ let coordinator = ContactsPickerCoordinator(session: session, room: spaceRoom, initialSearchText: nil, actualParticipants: nil, invitedParticipants: nil, userParticipant: nil)
+ coordinator.delegate = self
+ coordinator.start()
+ add(childCoordinator: coordinator)
+ present(coordinator.toPresentable(), animated: true)
+ }
+
+ private func showSpaceMembers() {
+ guard let session = mainSession, let spaceId = dataSource.currentSpace?.spaceId else {
+ return
+ }
+
+ let coordinator = SpaceMembersCoordinator(parameters: SpaceMembersCoordinatorParameters(userSessionsService: UserSessionsService.shared, session: session, spaceId: spaceId))
+ coordinator.delegate = self
+ let presentable = coordinator.toPresentable()
+ presentable.presentationController?.delegate = self
+ coordinator.start()
+ add(childCoordinator: coordinator)
+ present(presentable, animated: true, completion: nil)
+ }
+
+ private func showSpaceSettings() {
+ guard let session = mainSession, let spaceId = dataSource.currentSpace?.spaceId else {
+ return
+ }
+
+ let coordinator = SpaceSettingsModalCoordinator(parameters: SpaceSettingsModalCoordinatorParameters(session: session, spaceId: spaceId, parentSpaceId: nil))
+ coordinator.callback = { [weak self] result in
+ guard let self = self else { return }
+
+ coordinator.toPresentable().dismiss(animated: true) {
+ self.remove(childCoordinator: coordinator)
+ }
+ }
+
+ let presentable = coordinator.toPresentable()
+ presentable.presentationController?.delegate = self
+ present(presentable, animated: true, completion: nil)
+ coordinator.start()
+ add(childCoordinator: coordinator)
+ }
+
+ private func showLeaveSpace() {
+ guard let session = mainSession, let spaceSummary = dataSource.currentSpace?.summary else {
+ return
+ }
+
+ let name = spaceSummary.displayname ?? VectorL10n.spaceTag
+
+ let selectionHeader = MatrixItemChooserSelectionHeader(title: VectorL10n.leaveSpaceSelectionTitle,
+ selectAllTitle: VectorL10n.leaveSpaceSelectionAllRooms,
+ selectNoneTitle: VectorL10n.leaveSpaceSelectionNoRooms)
+ let paramaters = MatrixItemChooserCoordinatorParameters(session: session,
+ title: VectorL10n.leaveSpaceTitle(name),
+ detail: VectorL10n.leaveSpaceMessage(name),
+ selectionHeader: selectionHeader,
+ viewProvider: LeaveSpaceViewProvider(navTitle: nil),
+ itemsProcessor: LeaveSpaceItemsProcessor(spaceId: spaceSummary.roomId, session: session))
+ let coordinator = MatrixItemChooserCoordinator(parameters: paramaters)
+ coordinator.toPresentable().presentationController?.delegate = self
+ coordinator.start()
+ add(childCoordinator: coordinator)
+ coordinator.completion = { [weak self] result in
+ coordinator.toPresentable().dismiss(animated: true) {
+ self?.remove(childCoordinator: coordinator)
+ }
+ }
+ present(coordinator.toPresentable(), animated: true)
+ }
+}
+
+// MARK: - SpaceSelectorBottomSheetCoordinatorBridgePresenterDelegate
+extension AllChatsViewController: SpaceSelectorBottomSheetCoordinatorBridgePresenterDelegate {
+
+ func spaceSelectorBottomSheetCoordinatorBridgePresenterDidCancel(_ coordinatorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter) {
+ self.spaceSelectorBridgePresenter = nil
+ }
+
+ func spaceSelectorBottomSheetCoordinatorBridgePresenterDidSelectHome(_ coordinatorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter) {
+ coordinatorBridgePresenter.dismiss(animated: true) {
+ self.spaceSelectorBridgePresenter = nil
+ }
+
+ switchSpace(withId: nil)
+ }
+
+ func spaceSelectorBottomSheetCoordinatorBridgePresenter(_ coordinatorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter, didSelectSpaceWithId spaceId: String) {
+ coordinatorBridgePresenter.dismiss(animated: true) {
+ self.spaceSelectorBridgePresenter = nil
+ }
+
+ switchSpace(withId: spaceId)
+ }
+
+ func spaceSelectorBottomSheetCoordinatorBridgePresenter(_ coordinatorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter, didCreateSpaceWithinSpaceWithId parentSpaceId: String?) {
+ coordinatorBridgePresenter.dismiss(animated: true) {
+ self.spaceSelectorBridgePresenter = nil
+ }
+ self.showCreateSpace(parentSpaceId: parentSpaceId)
+ }
+
}
// MARK: - UISearchResultsUpdating
@@ -135,3 +339,67 @@ extension AllChatsViewController: UISearchResultsUpdating {
}
}
+
+// MARK: - UIAdaptivePresentationControllerDelegate
+extension AllChatsViewController: UIAdaptivePresentationControllerDelegate {
+
+ func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
+ guard let coordinator = childCoordinators.last else {
+ return
+ }
+
+ remove(childCoordinator: coordinator)
+ }
+}
+
+// MARK: - AllChatsEditActionProviderDelegate
+extension AllChatsViewController: AllChatsEditActionProviderDelegate {
+
+ func allChatsEditActionProvider(_ actionProvider: AllChatsEditActionProvider, didSelect option: AllChatsEditActionProviderOption) {
+ switch option {
+ case .exploreRooms:
+ joinARoom()
+ case .createRoom:
+ createNewRoom()
+ case .startChat:
+ startChat()
+ case .invitePeople:
+ showSpaceInvite()
+ case .spaceMembers:
+ showSpaceMembers()
+ case .spaceSettings:
+ showSpaceSettings()
+ case .leaveSpace:
+ showLeaveSpace()
+ case .createSpace:
+ showCreateSpace(parentSpaceId: dataSource.currentSpace?.spaceId)
+ }
+ }
+
+}
+
+// MARK: - ContactsPickerCoordinatorDelegate
+extension AllChatsViewController: ContactsPickerCoordinatorDelegate {
+
+ func contactsPickerCoordinatorDidStartLoading(_ coordinator: ContactsPickerCoordinatorProtocol) {
+ }
+
+ func contactsPickerCoordinatorDidEndLoading(_ coordinator: ContactsPickerCoordinatorProtocol) {
+ }
+
+ func contactsPickerCoordinatorDidClose(_ coordinator: ContactsPickerCoordinatorProtocol) {
+ remove(childCoordinator: coordinator)
+ }
+
+}
+
+// MARK: - SpaceMembersCoordinatorDelegate
+extension AllChatsViewController: SpaceMembersCoordinatorDelegate {
+
+ func spaceMembersCoordinatorDidCancel(_ coordinator: SpaceMembersCoordinatorType) {
+ coordinator.toPresentable().dismiss(animated: true) {
+ self.remove(childCoordinator: coordinator)
+ }
+ }
+
+}
diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.xib b/Riot/Modules/Home/AllChats/AllChatsViewController.xib
index f13d8779d..20803b426 100644
--- a/Riot/Modules/Home/AllChats/AllChatsViewController.xib
+++ b/Riot/Modules/Home/AllChats/AllChatsViewController.xib
@@ -8,14 +8,13 @@
-
+
-
@@ -25,7 +24,7 @@
-
+
@@ -44,37 +43,22 @@
-
-
-
-
-
-
-
-
-
+
-
-
-
-
+
-
-
-
-
diff --git a/Riot/Modules/SideMenu/SideMenuCoordinator.swift b/Riot/Modules/SideMenu/SideMenuCoordinator.swift
index 76ad8e548..a0ffbd62c 100644
--- a/Riot/Modules/SideMenu/SideMenuCoordinator.swift
+++ b/Riot/Modules/SideMenu/SideMenuCoordinator.swift
@@ -265,7 +265,7 @@ final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType {
return
}
- let coordinator = SpaceCreationCoordinator(parameters: SpaceCreationCoordinatorParameters(session: session))
+ let coordinator = SpaceCreationCoordinator(parameters: SpaceCreationCoordinatorParameters(session: session, parentSpaceId: nil))
let presentable = coordinator.toPresentable()
presentable.presentationController?.delegate = self
self.sideMenuViewController.present(presentable, animated: true, completion: nil)
diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift
index b7ea062e0..f0ee0b435 100644
--- a/Riot/Modules/TabBar/TabBarCoordinator.swift
+++ b/Riot/Modules/TabBar/TabBarCoordinator.swift
@@ -737,6 +737,11 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
private weak var rightMenuAvatarView: AvatarView?
private func createLeftButtonItem(for viewController: UIViewController) {
+ guard !BuildSettings.newAppLayoutEnabled else {
+ createAvatarButtonItem(for: viewController)
+ return
+ }
+
guard BuildSettings.enableSideMenu else {
let settingsBarButtonItem: MXKBarButtonItem = MXKBarButtonItem(image: Asset.Images.settingsIcon.image, style: .plain) { [weak self] in
self?.showSettings()
@@ -756,23 +761,21 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
}
private func createRightButtonItem(for viewController: UIViewController) {
- guard BuildSettings.newAppLayoutEnabled else {
- let searchBarButtonItem: MXKBarButtonItem = MXKBarButtonItem(image: Asset.Images.searchIcon.image, style: .plain) { [weak self] in
- self?.showUnifiedSearch()
- }
- searchBarButtonItem.accessibilityLabel = VectorL10n.searchDefaultPlaceholder
- viewController.navigationItem.rightBarButtonItem = searchBarButtonItem
-
+ guard !BuildSettings.newAppLayoutEnabled else {
return
}
-
- createAvatarButtonItem(for: viewController)
+
+ let searchBarButtonItem: MXKBarButtonItem = MXKBarButtonItem(image: Asset.Images.searchIcon.image, style: .plain) { [weak self] in
+ self?.showUnifiedSearch()
+ }
+ searchBarButtonItem.accessibilityLabel = VectorL10n.searchDefaultPlaceholder
+ viewController.navigationItem.rightBarButtonItem = searchBarButtonItem
}
private func createAvatarButtonItem(for viewController: UIViewController) {
var actions: [UIMenuElement] = []
- actions.append(UIAction(title: VectorL10n.settings, image: UIImage(systemName: "gearshape")) { [weak self] action in
+ actions.append(UIAction(title: VectorL10n.allChatsUserMenuSettings, image: UIImage(systemName: "gearshape")) { [weak self] action in
self?.showSettings()
})
@@ -788,11 +791,6 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
})
actions.append(UIMenu(title: "", options: .displayInline, children: subMenuActions))
-
- actions.append(UIAction(title: VectorL10n.roomAccessibilitySearch, image: UIImage(systemName: "magnifyingglass")) { [weak self] action in
- self?.showUnifiedSearch()
- })
-
actions.append(UIMenu(title: "", options: .displayInline, children: [
UIAction(title: VectorL10n.settingsSignOut, image: UIImage(systemName: "rectangle.portrait.and.arrow.right.fill"), attributes: .destructive) { [weak self] action in
self?.signOut()
@@ -822,7 +820,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
avatarView.fill(with: avatar)
}
- viewController.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: view)
+ viewController.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: view)
}
private func updateAvatarButtonItem() {
@@ -924,6 +922,11 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
private var windowOverlay: WindowOverlayPresenter?
func showCoachMessageIfNeeded(with session: MXSession) {
+ guard !BuildSettings.newAppLayoutEnabled else {
+ // Showing coach message makes no sense with the new App Layout
+ return
+ }
+
if !RiotSettings.shared.slideMenuRoomsCoachMessageHasBeenDisplayed {
let isAuthenticated = MXKAccountManager.shared().activeAccounts.first != nil || MXKAccountManager.shared().accounts.first?.isSoftLogout == false
diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
index c53ee2bdc..a2f2b6602 100644
--- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
+++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
@@ -56,7 +56,8 @@ enum MockAppScreens {
MockTemplateSimpleScreenScreenState.self,
MockTemplateUserProfileScreenState.self,
MockTemplateRoomListScreenState.self,
- MockTemplateRoomChatScreenState.self
+ MockTemplateRoomChatScreenState.self,
+ MockSpaceSelectorScreenState.self
]
}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift
index c71a085ff..12f021459 100644
--- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift
+++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift
@@ -46,14 +46,24 @@ final class SpaceCreationCoordinator: Coordinator {
// MARK: - Setup
init(parameters: SpaceCreationCoordinatorParameters) {
+ let title: String
+ let message: String
+ if let parentSpaceId = parameters.parentSpaceId, let parentSpaceName = parameters.session.spaceService.getSpace(withId: parentSpaceId)?.summary?.displayname {
+ title = VectorL10n.spacesSubspaceCreationVisibilityTitle
+ message = VectorL10n.spacesSubspaceCreationVisibilityMessage(parentSpaceName)
+ } else {
+ title = VectorL10n.spacesCreationVisibilityTitle
+ message = VectorL10n.spacesCreationVisibilityMessage
+ }
+
self.parameters = parameters
self.spaceVisibilityMenuParameters = SpaceCreationMenuCoordinatorParameters(
session: parameters.session,
creationParams: parameters.creationParameters,
navTitle: VectorL10n.spacesCreateSpaceTitle,
showBackButton: false,
- title: VectorL10n.spacesCreationVisibilityTitle,
- detail: VectorL10n.spacesCreationVisibilityMessage,
+ title: title,
+ detail: message,
options: [
SpaceCreationMenuRoomOption(id: .publicSpace, icon: Asset.Images.spaceCreationPublic.image, title: VectorL10n.public, detail: VectorL10n.spacePublicJoinRuleDetail),
SpaceCreationMenuRoomOption(id: .privateSpace, icon: Asset.Images.spaceCreationPrivate.image, title: VectorL10n.private, detail: VectorL10n.spacePrivateJoinRuleDetail)
@@ -245,7 +255,7 @@ final class SpaceCreationCoordinator: Coordinator {
}
private func createPostProcessCoordinator() -> SpaceCreationPostProcessCoordinator {
- let coordinator = SpaceCreationPostProcessCoordinator(parameters: SpaceCreationPostProcessCoordinatorParameters(session: parameters.session, creationParams: parameters.creationParameters))
+ let coordinator = SpaceCreationPostProcessCoordinator(parameters: SpaceCreationPostProcessCoordinatorParameters(session: parameters.session, parentSpaceId: parameters.parentSpaceId, creationParams: parameters.creationParameters))
coordinator.callback = { [weak self] result in
guard let self = self else { return }
switch result {
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinatorParameters.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinatorParameters.swift
index 0106cef34..f965b81ab 100644
--- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinatorParameters.swift
+++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinatorParameters.swift
@@ -26,6 +26,9 @@ struct SpaceCreationCoordinatorParameters {
/// The Matrix session
let session: MXSession
+ /// The identifier of the parent space. `nil` for creating a root space
+ let parentSpaceId: String?
+
/// Parameters needed to create the new space
let creationParameters: SpaceCreationParameters = SpaceCreationParameters()
@@ -33,8 +36,10 @@ struct SpaceCreationCoordinatorParameters {
let navigationRouter: NavigationRouterType
init(session: MXSession,
+ parentSpaceId: String?,
navigationRouter: NavigationRouterType? = nil) {
self.session = session
+ self.parentSpaceId = parentSpaceId
self.navigationRouter = navigationRouter ?? NavigationRouter(navigationController: RiotNavigationController())
}
}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinator.swift
index 92848c116..f5c46f7e6 100644
--- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinator.swift
+++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinator.swift
@@ -40,7 +40,7 @@ final class SpaceCreationPostProcessCoordinator: Coordinator, Presentable {
init(parameters: SpaceCreationPostProcessCoordinatorParameters) {
self.parameters = parameters
- let viewModel = SpaceCreationPostProcessViewModel.makeSpaceCreationPostProcessViewModel(spaceCreationPostProcessService: SpaceCreationPostProcessService(session: parameters.session, creationParams: parameters.creationParams))
+ let viewModel = SpaceCreationPostProcessViewModel.makeSpaceCreationPostProcessViewModel(spaceCreationPostProcessService: SpaceCreationPostProcessService(session: parameters.session, parentSpaceId: parameters.parentSpaceId, creationParams: parameters.creationParams))
let view = SpaceCreationPostProcess(viewModel: viewModel.context)
.addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
spaceCreationPostProcessViewModel = viewModel
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinatorParameters.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinatorParameters.swift
index be2857b70..68a99c879 100644
--- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinatorParameters.swift
+++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinatorParameters.swift
@@ -20,5 +20,6 @@ import Foundation
struct SpaceCreationPostProcessCoordinatorParameters {
let session: MXSession
+ let parentSpaceId: String?
let creationParams: SpaceCreationParameters
}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/MatrixSDK/SpaceCreationPostProcessService.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/MatrixSDK/SpaceCreationPostProcessService.swift
index 0dae6f360..99a45c3cd 100644
--- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/MatrixSDK/SpaceCreationPostProcessService.swift
+++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/MatrixSDK/SpaceCreationPostProcessService.swift
@@ -27,6 +27,7 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol {
// MARK: Private
private let session: MXSession
+ private let parentSpaceId: String?
private let creationParams: SpaceCreationParameters
private var tasks: [SpaceCreationPostProcessTask] = []
@@ -66,8 +67,9 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol {
// MARK: - Setup
- init(session: MXSession, creationParams: SpaceCreationParameters) {
+ init(session: MXSession, parentSpaceId: String?, creationParams: SpaceCreationParameters) {
self.session = session
+ self.parentSpaceId = parentSpaceId
self.creationParams = creationParams
self.tasks = Self.tasks(with: creationParams)
self.tasksSubject = CurrentValueSubject(tasks)
@@ -168,13 +170,25 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol {
let userIdInvites = creationParams.inviteType == .userId ? creationParams.userIdInvites : []
session.spaceService.createSpace(withName: creationParams.name, topic: creationParams.topic, isPublic: creationParams.isPublic, aliasLocalPart: alias, inviteArray: userIdInvites) { [weak self] response in
guard let self = self else { return }
+
if response.isFailure {
self.updateCurrentTask(with: .failure)
} else {
self.creationParams.isModified = false
self.createdSpace = response.value
- self.updateCurrentTask(with: .success)
- self.runNextTask()
+
+ guard let createdSpaceId = self.createdSpace?.spaceId, let parentSpaceId = self.parentSpaceId, let parentSpace = self.session.spaceService.getSpace(withId: parentSpaceId) else {
+ self.updateCurrentTask(with: .success)
+ self.runNextTask()
+ return
+ }
+
+ parentSpace.addChild(roomId: createdSpaceId) { [weak self] response in
+ guard let self = self else { return }
+
+ self.updateCurrentTask(with: .success)
+ self.runNextTask()
+ }
}
}
}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomCoordinator.swift
new file mode 100644
index 000000000..c649ed558
--- /dev/null
+++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomCoordinator.swift
@@ -0,0 +1,152 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import SwiftUI
+
+enum SpaceSelectorBottomSheetCoordinatorResult {
+ case cancel
+ case homeSelected
+ case spaceSelected(_ item: SpaceSelectorListItemData)
+ case createSpace(_ parentSpaceId: String?)
+}
+
+struct SpaceSelectorBottomSheetCoordinatorParameters {
+ let session: MXSession
+ let selectedSpaceId: String?
+ let showHomeSpace: Bool
+
+ init(session: MXSession,
+ selectedSpaceId: String? = nil,
+ showHomeSpace: Bool = false) {
+ self.session = session
+ self.selectedSpaceId = selectedSpaceId
+ self.showHomeSpace = showHomeSpace
+ }
+}
+
+final class SpaceSelectorBottomSheetCoordinator: Coordinator, Presentable {
+
+ // MARK: - Properties
+
+ private let parameters: SpaceSelectorBottomSheetCoordinatorParameters
+
+ private let navigationRouter: NavigationRouterType
+ private var spaceIdStack: [String]
+
+ private weak var roomDetailCoordinator: SpaceChildRoomDetailCoordinator?
+ private weak var currentSpaceSelectorCoordinator: SpaceSelectorCoordinator?
+
+ // MARK: - Public
+
+ // Must be used only internally
+ var childCoordinators: [Coordinator] = []
+ var completion: ((SpaceSelectorBottomSheetCoordinatorResult) -> Void)?
+
+ // MARK: - Setup
+
+ init(parameters: SpaceSelectorBottomSheetCoordinatorParameters,
+ navigationRouter: NavigationRouterType = NavigationRouter(navigationController: RiotNavigationController())) {
+ self.parameters = parameters
+ self.navigationRouter = navigationRouter
+ self.spaceIdStack = []
+ self.setupNavigationRouter()
+ }
+
+ // MARK: - Public
+
+ func start() {
+ pushSpace(withId: nil)
+ }
+
+ func toPresentable() -> UIViewController {
+ return self.navigationRouter.toPresentable()
+ }
+
+ // MARK: - Private
+
+ private func setupNavigationRouter() {
+ guard #available(iOS 15.0, *) else { return }
+
+ guard let sheetController = self.navigationRouter.toPresentable().sheetPresentationController else {
+ MXLog.debug("[SpaceSelectorBottomSheetCoordinator] setup: no sheetPresentationController found")
+ return
+ }
+
+ sheetController.detents = [.medium(), .large()]
+ sheetController.prefersGrabberVisible = true
+ sheetController.selectedDetentIdentifier = .medium
+ sheetController.prefersScrollingExpandsWhenScrolledToEdge = true
+ }
+
+ private func createSpaceSelectorCoordinator(parentSpaceId: String?) -> SpaceSelectorCoordinator {
+ let parameters = SpaceSelectorCoordinatorParameters(session: parameters.session,
+ parentSpaceId: parentSpaceId,
+ selectedSpaceId: parameters.selectedSpaceId,
+ showHomeSpace: parameters.showHomeSpace)
+ let coordinator = SpaceSelectorCoordinator(parameters: parameters)
+ coordinator.completion = { [weak self] result in
+ guard let self = self else { return }
+
+ switch result {
+ case .cancel:
+ self.completion?(.cancel)
+ case .homeSelected:
+ self.trackSpaceSelection(with: nil)
+ self.completion?(.homeSelected)
+ case .spaceSelected(let item):
+ self.trackSpaceSelection(with: item.id)
+ self.completion?(.spaceSelected(item))
+ case .spaceDisclosure(let item):
+ self.pushSpace(withId: item.id)
+ case .createSpace(let parentSpaceId):
+ self.completion?(.createSpace(parentSpaceId))
+ }
+ }
+
+ return coordinator
+ }
+
+ private func pushSpace(withId spaceId: String?) {
+ let coordinator = self.createSpaceSelectorCoordinator(parentSpaceId: spaceId)
+
+ coordinator.start()
+
+ self.add(childCoordinator: coordinator)
+ self.currentSpaceSelectorCoordinator = coordinator
+
+ if let spaceId = spaceId {
+ self.spaceIdStack.append(spaceId)
+ }
+
+ if self.navigationRouter.modules.isEmpty {
+ self.navigationRouter.setRootModule(coordinator)
+ } else {
+ self.navigationRouter.push(coordinator.toPresentable(), animated: true) {
+ self.remove(childCoordinator: coordinator)
+ self.spaceIdStack.removeLast()
+ }
+ }
+ }
+
+ private func trackSpaceSelection(with spaceId: String?) {
+ guard parameters.selectedSpaceId != spaceId else {
+ Analytics.shared.trackInteraction(.spacePanelSelectedSpace)
+ return
+ }
+
+ Analytics.shared.trackInteraction(.spacePanelSwitchSpace)
+ }
+}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomCoordinatorBridgePresenter.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomCoordinatorBridgePresenter.swift
new file mode 100644
index 000000000..6226cd7a9
--- /dev/null
+++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomCoordinatorBridgePresenter.swift
@@ -0,0 +1,93 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+@objc protocol SpaceSelectorBottomSheetCoordinatorBridgePresenterDelegate {
+ func spaceSelectorBottomSheetCoordinatorBridgePresenterDidCancel(_ coordinatorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter)
+ func spaceSelectorBottomSheetCoordinatorBridgePresenterDidSelectHome(_ coordinatorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter)
+ func spaceSelectorBottomSheetCoordinatorBridgePresenter(_ coordinatorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter, didSelectSpaceWithId spaceId: String)
+ func spaceSelectorBottomSheetCoordinatorBridgePresenter(_ coordinatorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter, didCreateSpaceWithinSpaceWithId parentSpaceId: String?)
+}
+
+/// `SpaceSelectorBottomSheetCoordinatorBridgePresenter` enables to start `SpaceSelectorBottomSheetCoordinator` from a view controller.
+/// This bridge is used while waiting for global usage of coordinator pattern.
+/// It breaks the Coordinator abstraction and it has been introduced for Objective-C compatibility (mainly for integration in legacy view controllers).
+/// Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator.
+@objcMembers
+final class SpaceSelectorBottomSheetCoordinatorBridgePresenter: NSObject {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private let session: MXSession
+ private let selectedSpaceId: String?
+ private let showHomeSpace: Bool
+ private var coordinator: SpaceSelectorBottomSheetCoordinator?
+
+ // MARK: Public
+
+ weak var delegate: SpaceSelectorBottomSheetCoordinatorBridgePresenterDelegate?
+
+ // MARK: - Setup
+
+ init(session: MXSession, selectedSpaceId: String?, showHomeSpace: Bool) {
+ self.session = session
+ self.selectedSpaceId = selectedSpaceId
+ self.showHomeSpace = showHomeSpace
+
+ super.init()
+ }
+
+ // MARK: - Public
+
+ func present(from viewController: UIViewController, animated: Bool) {
+ let parameters = SpaceSelectorBottomSheetCoordinatorParameters(session: session, selectedSpaceId: selectedSpaceId, showHomeSpace: showHomeSpace)
+ let coordinator = SpaceSelectorBottomSheetCoordinator(parameters: parameters)
+ coordinator.completion = { [weak self] result in
+ guard let self = self else { return }
+
+ switch result {
+ case .cancel:
+ self.delegate?.spaceSelectorBottomSheetCoordinatorBridgePresenterDidCancel(self)
+ case .homeSelected:
+ self.delegate?.spaceSelectorBottomSheetCoordinatorBridgePresenterDidSelectHome(self)
+ case .spaceSelected(let item):
+ self.delegate?.spaceSelectorBottomSheetCoordinatorBridgePresenter(self, didSelectSpaceWithId: item.id)
+ case .createSpace(let parentSpaceId):
+ self.delegate?.spaceSelectorBottomSheetCoordinatorBridgePresenter(self, didCreateSpaceWithinSpaceWithId: parentSpaceId)
+ }
+ }
+ let presentable = coordinator.toPresentable()
+ viewController.present(presentable, animated: animated, completion: nil)
+ coordinator.start()
+
+ self.coordinator = coordinator
+ }
+
+ func dismiss(animated: Bool, completion: (() -> Void)?) {
+ guard let coordinator = self.coordinator else {
+ return
+ }
+ coordinator.toPresentable().dismiss(animated: animated) {
+ self.coordinator = nil
+
+ completion?()
+ }
+ }
+}
+
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift
new file mode 100644
index 000000000..51766d9f5
--- /dev/null
+++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift
@@ -0,0 +1,112 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import SwiftUI
+import CommonKit
+
+struct SpaceSelectorCoordinatorParameters {
+ let session: MXSession
+ let parentSpaceId: String?
+ let selectedSpaceId: String?
+ let showHomeSpace: Bool
+
+ init(session: MXSession,
+ parentSpaceId: String? = nil,
+ selectedSpaceId: String? = nil,
+ showHomeSpace: Bool = false) {
+ self.session = session
+ self.parentSpaceId = parentSpaceId
+ self.selectedSpaceId = selectedSpaceId
+ self.showHomeSpace = showHomeSpace
+ }
+}
+
+final class SpaceSelectorCoordinator: Coordinator, Presentable {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private let parameters: SpaceSelectorCoordinatorParameters
+ private let hostingViewController: UIViewController
+ private var viewModel: SpaceSelectorViewModelProtocol
+
+ private var indicatorPresenter: UserIndicatorTypePresenterProtocol
+ private var loadingIndicator: UserIndicator?
+
+ // MARK: Public
+
+ // Must be used only internally
+ var childCoordinators: [Coordinator] = []
+ var completion: ((SpaceSelectorCoordinatorResult) -> Void)?
+
+ // MARK: - Setup
+
+ init(parameters: SpaceSelectorCoordinatorParameters) {
+ self.parameters = parameters
+ let service = SpaceSelectorService(session: parameters.session, parentSpaceId: parameters.parentSpaceId, showHomeSpace: parameters.showHomeSpace, selectedSpaceId: parameters.selectedSpaceId)
+ let viewModel = SpaceSelectorViewModel.makeViewModel(service: service)
+ let view = SpaceSelector(viewModel: viewModel.context)
+ .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
+ self.viewModel = viewModel
+ let hostingViewController = VectorHostingController(rootView: view)
+ hostingViewController.hidesBackTitleWhenPushed = true
+ self.hostingViewController = hostingViewController
+
+ indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: self.hostingViewController)
+ }
+
+ // MARK: - Public
+
+ func start() {
+ MXLog.debug("[SpaceSelectorCoordinator] did start.")
+ viewModel.completion = { [weak self] result in
+ guard let self = self else { return }
+ MXLog.debug("[SpaceSheetCoordinator] SpaceSelectorViewModel did complete with result: \(result).")
+ switch result {
+ case .cancel:
+ self.completion?(.cancel)
+ case .homeSelected:
+ self.completion?(.homeSelected)
+ case .spaceSelected(let item):
+ self.completion?(.spaceSelected(item))
+ case .spaceDisclosure(let item):
+ self.completion?(.spaceDisclosure(item))
+ case .createSpace:
+ self.completion?(.createSpace(self.parameters.parentSpaceId))
+ }
+ }
+ }
+
+ func toPresentable() -> UIViewController {
+ return self.hostingViewController
+ }
+
+ // MARK: - Private
+
+ /// Show an activity indicator whilst loading.
+ /// - Parameters:
+ /// - label: The label to show on the indicator.
+ /// - isInteractionBlocking: Whether the indicator should block any user interaction.
+ private func startLoading(label: String = VectorL10n.loading, isInteractionBlocking: Bool = true) {
+ loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
+ }
+
+ /// Hide the currently displayed activity indicator.
+ private func stopLoading() {
+ loadingIndicator = nil
+ }
+}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/MockSpaceSelectorScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/MockSpaceSelectorScreenState.swift
new file mode 100644
index 000000000..261c39a90
--- /dev/null
+++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/MockSpaceSelectorScreenState.swift
@@ -0,0 +1,60 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import SwiftUI
+
+/// Using an enum for the screen allows you define the different state cases with
+/// the relevant associated data for each case.
+enum MockSpaceSelectorScreenState: MockScreenState, CaseIterable {
+ // A case for each state you want to represent
+ // with specific, minimal associated data that will allow you
+ // mock that screen.
+ case initialList
+ case emptyList
+ case selection
+
+ /// The associated screen
+ var screenType: Any.Type {
+ SpaceSelector.self
+ }
+
+ /// A list of screen state definitions
+ static var allCases: [MockSpaceSelectorScreenState] {
+ [.initialList, .emptyList, .selection]
+ }
+
+ /// Generate the view struct for the screen state.
+ var screenView: ([Any], AnyView) {
+ let service: MockSpaceSelectorService
+ switch self {
+ case .initialList:
+ service = MockSpaceSelectorService()
+ case .emptyList:
+ service = MockSpaceSelectorService(spaceList: [MockSpaceSelectorService.homeItem])
+ case .selection:
+ service = MockSpaceSelectorService(selectedSpaceId: MockSpaceSelectorService.defaultSpaceList[2].id)
+ }
+ let viewModel = SpaceSelectorViewModel.makeViewModel(service: service)
+
+ // can simulate service and viewModel actions here if needs be.
+
+ return (
+ [service, viewModel],
+ AnyView(SpaceSelector(viewModel: viewModel.context))
+ )
+ }
+}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/MatrixSDK/SpaceSelectorService.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/MatrixSDK/SpaceSelectorService.swift
new file mode 100644
index 000000000..da40b076f
--- /dev/null
+++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/MatrixSDK/SpaceSelectorService.swift
@@ -0,0 +1,92 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import Combine
+import MatrixSDK
+
+class SpaceSelectorService: SpaceSelectorServiceProtocol {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private let session: MXSession
+ private let parentSpaceId: String?
+ private let showHomeSpace: Bool
+
+ private var spaceList: [SpaceSelectorListItemData] {
+ var itemList = showHomeSpace && parentSpaceId == nil ? [SpaceSelectorListItemData(id: SpaceSelectorConstants.homeSpaceId, icon: Asset.Images.sideMenuActionIconFeedback.image, displayName: VectorL10n.allChatsTitle)] : []
+
+ let notificationCounter = session.spaceService.notificationCounter
+
+ if let parentSpaceId = parentSpaceId, let parentSpace = session.spaceService.getSpace(withId: parentSpaceId) {
+ itemList.append(contentsOf: parentSpace.childSpaces.compactMap { space in
+ SpaceSelectorListItemData.itemData(with: space, notificationCounter: notificationCounter)
+ })
+ } else {
+ itemList.append(contentsOf: session.spaceService.rootSpaces.compactMap { space in
+ SpaceSelectorListItemData.itemData(with: space, notificationCounter: notificationCounter)
+ })
+ }
+ return itemList
+ }
+
+ private var parentSpaceName: String? {
+ guard let parentSpaceId = parentSpaceId, let summary = session.roomSummary(withRoomId: parentSpaceId) else {
+ return nil
+ }
+
+ return summary.displayname
+ }
+
+ // MARK: Public
+
+ private(set) var spaceListSubject: CurrentValueSubject<[SpaceSelectorListItemData], Never>
+ private(set) var parentSpaceNameSubject: CurrentValueSubject
+ private(set) var selectedSpaceId: String?
+
+ // MARK: - Setup
+
+ init(session: MXSession, parentSpaceId: String?, showHomeSpace: Bool, selectedSpaceId: String?) {
+ self.session = session
+ self.parentSpaceId = parentSpaceId
+ self.showHomeSpace = showHomeSpace
+ self.spaceListSubject = CurrentValueSubject([])
+ self.parentSpaceNameSubject = CurrentValueSubject(nil)
+ self.selectedSpaceId = selectedSpaceId
+
+ spaceListSubject.send(spaceList)
+ parentSpaceNameSubject.send(parentSpaceName)
+ }
+}
+
+fileprivate extension SpaceSelectorListItemData {
+ static func itemData(with space: MXSpace, notificationCounter: MXSpaceNotificationCounter) -> SpaceSelectorListItemData? {
+ guard let summary = space.summary else {
+ return nil
+ }
+
+ let notificationState = notificationCounter.notificationState(forSpaceWithId: space.spaceId)
+
+ return SpaceSelectorListItemData(id:summary.roomId,
+ avatar: summary.room.avatarData,
+ displayName: summary.displayname,
+ notificationCount: notificationState?.groupMissedDiscussionsCount ?? 0,
+ highlightedNotificationCount: notificationState?.groupMissedDiscussionsHighlightedCount ?? 0,
+ hasSubItems: !space.childSpaces.isEmpty)
+ }
+}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/Mock/MockSpaceSelectorService.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/Mock/MockSpaceSelectorService.swift
new file mode 100644
index 000000000..d5bf023b3
--- /dev/null
+++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/Mock/MockSpaceSelectorService.swift
@@ -0,0 +1,41 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import Combine
+import UIKit
+
+class MockSpaceSelectorService: SpaceSelectorServiceProtocol {
+
+ static let homeItem = SpaceSelectorListItemData(id: SpaceSelectorConstants.homeSpaceId, avatar: nil, icon: UIImage(systemName: "house"), displayName: "All Chats", notificationCount: 0, highlightedNotificationCount: 0, hasSubItems: false)
+ static let defaultSpaceList = [
+ homeItem,
+ SpaceSelectorListItemData(id: "!aaabaa:matrix.org", avatar: nil, icon: UIImage(systemName: "number"), displayName: "Default Space", notificationCount: 0, highlightedNotificationCount: 0, hasSubItems: false),
+ SpaceSelectorListItemData(id: "!zzasds:matrix.org", avatar: nil, icon: UIImage(systemName: "number"), displayName: "Space with sub items", notificationCount: 0, highlightedNotificationCount: 0, hasSubItems: true),
+ SpaceSelectorListItemData(id: "!scthve:matrix.org", avatar: nil, icon: UIImage(systemName: "number"), displayName: "Space with notifications", notificationCount: 55, highlightedNotificationCount: 0, hasSubItems: true),
+ SpaceSelectorListItemData(id: "!ferggs:matrix.org", avatar: nil, icon: UIImage(systemName: "number"), displayName: "Space with highlight", notificationCount: 99, highlightedNotificationCount: 50, hasSubItems: false)
+ ]
+
+ var spaceListSubject: CurrentValueSubject<[SpaceSelectorListItemData], Never>
+ var parentSpaceNameSubject: CurrentValueSubject
+ var selectedSpaceId: String?
+
+ init(spaceList: [SpaceSelectorListItemData] = defaultSpaceList, parentSpaceName: String? = nil, selectedSpaceId: String = SpaceSelectorConstants.homeSpaceId) {
+ self.spaceListSubject = CurrentValueSubject(spaceList)
+ self.parentSpaceNameSubject = CurrentValueSubject(parentSpaceName)
+ self.selectedSpaceId = selectedSpaceId
+ }
+}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/SpaceSelectorServiceProtocol.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/SpaceSelectorServiceProtocol.swift
new file mode 100644
index 000000000..48ebec3e6
--- /dev/null
+++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/SpaceSelectorServiceProtocol.swift
@@ -0,0 +1,24 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import Combine
+
+protocol SpaceSelectorServiceProtocol {
+ var spaceListSubject: CurrentValueSubject<[SpaceSelectorListItemData], Never> { get }
+ var parentSpaceNameSubject: CurrentValueSubject { get }
+ var selectedSpaceId: String? { get }
+}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorModels.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorModels.swift
new file mode 100644
index 000000000..ade9a78f0
--- /dev/null
+++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorModels.swift
@@ -0,0 +1,110 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+// MARK: - Coordinator
+
+enum SpaceSelectorCoordinatorResult {
+ /// Cancel button has been presed
+ case cancel
+ /// Home Space (aka "All Chats") has been selected -> the app should switch to the home space
+ case homeSelected
+ /// A space has been selected -> the app should switch to this space
+ case spaceSelected(_ item: SpaceSelectorListItemData)
+ /// The disclosure button of a space has been pressed -> the parent coordinator should navigate to its sub-spaces
+ case spaceDisclosure(_ item: SpaceSelectorListItemData)
+ /// The create space button has been pressed
+ case createSpace(_ parentSpaceId: String?)
+}
+
+// MARK: View model
+
+enum SpaceSelectorConstants {
+ /// Arbitrary ID for the home space (aka "All Chats")
+ static let homeSpaceId = "SpaceSelectorListItemDataHomeSpaceId"
+}
+
+/// This structure contains all the data to display the information about a space
+struct SpaceSelectorListItemData {
+ /// Id of the space (`SpaceSelectorConstants.homeSpaceId` for the home space)
+ let id: String
+ /// avatar data of the space: set this property to `nil` if you want to display a space with a hardcoded icon
+ let avatar: AvatarInput?
+ /// hardcoded icon: only used if the avatar is not set
+ let icon: UIImage?
+ /// Displayname of the space
+ let displayName: String?
+ /// total number of notifications for this space
+ let notificationCount: UInt
+ /// total number of highlights for this space
+ let highlightedNotificationCount: UInt
+ /// Indicates if the space has sub spaces (condition the display of the disclosure button)
+ let hasSubItems: Bool
+
+ init(id: String,
+ avatar: AvatarInput? = nil,
+ icon: UIImage? = nil,
+ displayName: String?,
+ notificationCount: UInt = 0,
+ highlightedNotificationCount: UInt = 0,
+ hasSubItems: Bool = false) {
+ self.id = id
+ self.avatar = avatar
+ self.icon = icon
+ self.displayName = displayName
+ self.notificationCount = notificationCount
+ self.highlightedNotificationCount = highlightedNotificationCount
+ self.hasSubItems = hasSubItems
+ }
+}
+
+extension SpaceSelectorListItemData: Identifiable, Equatable {}
+
+enum SpaceSelectorViewModelResult {
+ /// Cancel button has been presed
+ case cancel
+ /// Home Space (aka "All Chats") has been selected -> the app should switch to the home space
+ case homeSelected
+ /// A space has been selected -> the app should switch to this space
+ case spaceSelected(_ item: SpaceSelectorListItemData)
+ /// The disclosure button of a space has been pressed -> the parent coordinator should navigate to its sub-spaces
+ case spaceDisclosure(_ item: SpaceSelectorListItemData)
+ /// The create space button has been pressed
+ case createSpace
+}
+
+// MARK: View
+
+struct SpaceSelectorViewState: BindableState {
+ /// List of items that represents the list of sub space of the current space
+ var items: [SpaceSelectorListItemData]
+ /// Id of the currently selected space if there is a current space in the app
+ var selectedSpaceId: String?
+ /// String to be displayed as title for the navigation bar
+ var navigationTitle: String
+}
+
+enum SpaceSelectorViewAction {
+ /// Cancel button has been presed
+ case cancel
+ /// A space has been selected
+ case spaceSelected(_ item: SpaceSelectorListItemData)
+ /// The disclosure button of a space has been pressed
+ case spaceDisclosure(_ item: SpaceSelectorListItemData)
+ /// The create space button has been pressed
+ case createSpace
+}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModel.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModel.swift
new file mode 100644
index 000000000..fe3abd531
--- /dev/null
+++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModel.swift
@@ -0,0 +1,72 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import SwiftUI
+import Combine
+
+typealias SpaceSelectorViewModelType = StateStoreViewModel
+
+class SpaceSelectorViewModel: SpaceSelectorViewModelType, SpaceSelectorViewModelProtocol {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private let service: SpaceSelectorServiceProtocol
+
+ // MARK: Public
+
+ var completion: ((SpaceSelectorViewModelResult) -> Void)?
+
+ // MARK: - Setup
+
+ static func makeViewModel(service: SpaceSelectorServiceProtocol) -> SpaceSelectorViewModelProtocol {
+ return SpaceSelectorViewModel(service: service)
+ }
+
+ private init(service: SpaceSelectorServiceProtocol) {
+ self.service = service
+ super.init(initialViewState: Self.defaultState(service: service))
+ }
+
+ private static func defaultState(service: SpaceSelectorServiceProtocol) -> SpaceSelectorViewState {
+ let parentName = service.parentSpaceNameSubject.value
+ return SpaceSelectorViewState(items: service.spaceListSubject.value,
+ selectedSpaceId: service.selectedSpaceId,
+ navigationTitle: parentName ?? VectorL10n.spaceSelectorTitle)
+ }
+
+ // MARK: - Public
+
+ override func process(viewAction: SpaceSelectorViewAction) {
+ switch viewAction {
+ case .cancel:
+ completion?(.cancel)
+ case .spaceSelected(let item):
+ if item.id == SpaceSelectorConstants.homeSpaceId {
+ completion?(.homeSelected)
+ } else {
+ completion?(.spaceSelected(item))
+ }
+ case .spaceDisclosure(let item):
+ completion?(.spaceDisclosure(item))
+ case .createSpace:
+ completion?(.createSpace)
+ }
+ }
+}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModelProtocol.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModelProtocol.swift
new file mode 100644
index 000000000..eb53edc93
--- /dev/null
+++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModelProtocol.swift
@@ -0,0 +1,24 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+protocol SpaceSelectorViewModelProtocol {
+
+ var completion: ((SpaceSelectorViewModelResult) -> Void)? { get set }
+ static func makeViewModel(service: SpaceSelectorServiceProtocol) -> SpaceSelectorViewModelProtocol
+ var context: SpaceSelectorViewModelType.Context { get }
+}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/UI/SpaceSelectorUITests.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/UI/SpaceSelectorUITests.swift
new file mode 100644
index 000000000..4770a7602
--- /dev/null
+++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/UI/SpaceSelectorUITests.swift
@@ -0,0 +1,42 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import XCTest
+import RiotSwiftUI
+
+class SpaceSelectorUITests: MockScreenTestCase {
+
+ func testInitialDisplay() {
+ app.goToScreenWithIdentifier(MockSpaceSelectorScreenState.initialList.title)
+
+ let disclosureButtons = app.buttons.matching(identifier: "disclosureButton").allElementsBoundByIndex
+ XCTAssertEqual(disclosureButtons.count, MockSpaceSelectorService.defaultSpaceList.filter { $0.hasSubItems }.count)
+
+ let notificationBadges = app.staticTexts.matching(identifier: "notificationBadge").allElementsBoundByIndex
+ let itemsWithNotifications = MockSpaceSelectorService.defaultSpaceList.filter { $0.notificationCount > 0 }
+ XCTAssertEqual(notificationBadges.count, itemsWithNotifications.count)
+ for (index, notificationBadge) in notificationBadges.enumerated() {
+ XCTAssertEqual("\(itemsWithNotifications[index].notificationCount)", notificationBadge.label)
+ }
+
+ let spaceItemNameList = app.staticTexts.matching(identifier: "itemName").allElementsBoundByIndex
+ XCTAssertEqual(spaceItemNameList.count, MockSpaceSelectorService.defaultSpaceList.count)
+ for (index, item) in MockSpaceSelectorService.defaultSpaceList.enumerated() {
+ XCTAssertEqual(item.displayName, spaceItemNameList[index].label)
+ }
+ }
+
+}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/Unit/SpaceSelectorViewModelTests.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/Unit/SpaceSelectorViewModelTests.swift
new file mode 100644
index 000000000..4d590a8ad
--- /dev/null
+++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/Unit/SpaceSelectorViewModelTests.swift
@@ -0,0 +1,41 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import XCTest
+import Combine
+
+@testable import RiotSwiftUI
+
+class SpaceSelectorViewModelTests: XCTestCase {
+
+ var service: MockSpaceSelectorService!
+ var viewModel: SpaceSelectorViewModelProtocol!
+ var context: SpaceSelectorViewModelType.Context!
+ var cancellables = Set()
+
+ override func setUpWithError() throws {
+ service = MockSpaceSelectorService()
+ viewModel = SpaceSelectorViewModel.makeViewModel(service: service)
+ context = viewModel.context
+ }
+
+ func testInitialState() {
+ XCTAssertEqual(context.viewState.selectedSpaceId, MockSpaceSelectorService.homeItem.id)
+ XCTAssertEqual(context.viewState.items, MockSpaceSelectorService.defaultSpaceList)
+ XCTAssertEqual(context.viewState.navigationTitle, VectorL10n.spaceSelectorTitle)
+ }
+
+}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelector.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelector.swift
new file mode 100644
index 000000000..f87f4eff1
--- /dev/null
+++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelector.swift
@@ -0,0 +1,76 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import SwiftUI
+
+struct SpaceSelector: View {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ @Environment(\.theme) private var theme: ThemeSwiftUI
+
+ @ViewBuilder
+ private var rightButton: some View {
+ Button(VectorL10n.create) {
+ viewModel.send(viewAction: .createSpace)
+ }
+ }
+
+ // MARK: Public
+
+ @ObservedObject var viewModel: SpaceSelectorViewModel.Context
+
+ var body: some View {
+ ScrollView {
+ LazyVStack {
+ ForEach(viewModel.viewState.items) { item in
+ SpaceSelectorListRow(avatar: item.avatar,
+ icon: item.icon,
+ displayName: item.displayName,
+ hasSubItems: item.hasSubItems,
+ isSelected: item.id == viewModel.viewState.selectedSpaceId,
+ notificationCount: item.notificationCount,
+ highlightedNotificationCount: item.highlightedNotificationCount,
+ disclosureAction: {
+ viewModel.send(viewAction: .spaceDisclosure(item))
+ }
+ )
+ .onTapGesture {
+ viewModel.send(viewAction: .spaceSelected(item))
+ }
+ }
+ }
+ }
+ .frame(maxHeight: .infinity)
+ .background(theme.colors.background.edgesIgnoringSafeArea(.all))
+ .navigationTitle(viewModel.viewState.navigationTitle)
+ .navigationBarItems(
+ trailing: rightButton
+ )
+ .accentColor(theme.colors.accent)
+ }
+}
+
+// MARK: - Previews
+
+struct SpaceSelector_Previews: PreviewProvider {
+ static let stateRenderer = MockSpaceSelectorScreenState.stateRenderer
+ static var previews: some View {
+ stateRenderer.screenGroup()
+ }
+}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelectorListRow.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelectorListRow.swift
new file mode 100644
index 000000000..611475c52
--- /dev/null
+++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelectorListRow.swift
@@ -0,0 +1,115 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import SwiftUI
+
+struct SpaceSelectorListRow: View {
+ // MARK: - Properties
+
+ // MARK: Private
+
+ @Environment(\.theme) private var theme: ThemeSwiftUI
+
+ // MARK: Public
+
+ let avatar: AvatarInputProtocol?
+ let icon: UIImage?
+ let displayName: String?
+ let hasSubItems: Bool
+ let isSelected: Bool
+ let notificationCount: UInt
+ let highlightedNotificationCount: UInt
+ let disclosureAction: (() -> Void)?
+
+ @ViewBuilder
+ var body: some View {
+ ZStack {
+ if isSelected {
+ RoundedRectangle(cornerRadius: 8)
+ .fill(theme.colors.system)
+ .padding(.horizontal, 8)
+ }
+ VStack {
+ HStack {
+ if let avatar = avatar {
+ SpaceAvatarImage(avatarData: avatar, size: .xSmall)
+ }
+ if let icon = icon {
+ Image(uiImage: icon)
+ .renderingMode(.template)
+ .foregroundColor(theme.colors.primaryContent)
+ .frame(width: 32, height: 32)
+ .background(theme.colors.quinaryContent)
+ .clipShape(RoundedRectangle(cornerRadius: 8))
+ }
+ Text(displayName ?? "")
+ .foregroundColor(theme.colors.primaryContent)
+ .font(theme.fonts.bodySB)
+ .accessibility(identifier: "itemName")
+ Spacer()
+ if notificationCount > 0 {
+ Text("\(notificationCount)")
+ .multilineTextAlignment(.center)
+ .foregroundColor(theme.colors.background)
+ .font(theme.fonts.footnote)
+ .padding(.vertical, 2)
+ .padding(.horizontal, 6)
+ .background(highlightedNotificationCount > 0 ? theme.colors.alert : theme.colors.secondaryContent)
+ .clipShape(Capsule())
+ .accessibility(identifier: "notificationBadge")
+ }
+ if hasSubItems {
+ Button {
+ disclosureAction?()
+ } label: {
+ Image(systemName: "chevron.right")
+ .renderingMode(.template)
+ .foregroundColor(theme.colors.accent)
+ }
+ .accessibility(identifier: "disclosureButton")
+ }
+ }
+ .padding(.vertical, 8)
+ }
+ .padding(.horizontal)
+ }
+ .padding(.horizontal, 8)
+ .frame(maxWidth: .infinity)
+ .background(theme.colors.background)
+ }
+
+}
+
+// MARK: - Previews
+
+struct SpaceSelectorListRow_Previews: PreviewProvider {
+
+ static var previews: some View {
+ sampleView.theme(.light).preferredColorScheme(.light)
+ sampleView.theme(.dark).preferredColorScheme(.dark)
+ }
+
+ static var sampleView: some View {
+ VStack(spacing: 8) {
+ SpaceSelectorListRow(avatar: nil, icon: UIImage(systemName: "house"), displayName: "Space name", hasSubItems: false, isSelected: false, notificationCount: 0, highlightedNotificationCount: 0, disclosureAction: nil)
+ SpaceSelectorListRow(avatar: nil, icon: UIImage(systemName: "house"), displayName: "Space name", hasSubItems: true, isSelected: false, notificationCount: 0, highlightedNotificationCount: 0, disclosureAction: nil)
+ SpaceSelectorListRow(avatar: nil, icon: UIImage(systemName: "house"), displayName: "Space name", hasSubItems: true, isSelected: false, notificationCount: 99, highlightedNotificationCount: 0, disclosureAction: nil)
+ SpaceSelectorListRow(avatar: nil, icon: UIImage(systemName: "house"), displayName: "Space name", hasSubItems: false, isSelected: false, notificationCount: 99, highlightedNotificationCount: 1, disclosureAction: nil)
+ SpaceSelectorListRow(avatar: nil, icon: UIImage(systemName: "house"), displayName: "Space name", hasSubItems: true, isSelected: true, notificationCount: 99, highlightedNotificationCount: 1, disclosureAction: nil)
+ }
+ }
+
+}
diff --git a/changelog.d/6410.change b/changelog.d/6410.change
new file mode 100644
index 000000000..9ea313d80
--- /dev/null
+++ b/changelog.d/6410.change
@@ -0,0 +1 @@
+App Layout: Implemented the new Space selector bottom sheet