Implement new space selector bottom sheet (#6518)
* Delight: Edit layout experiment #6079
|
@ -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.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 525 B |
Before Width: | Height: | Size: 874 B |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 569 B |
Before Width: | Height: | Size: 992 B |
Before Width: | Height: | Size: 1.4 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
6
Riot/Assets/Images.xcassets/Home/home_my_spaces_action.imageset/home_my_spaces_action.svg
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.75" y="0.75" width="7.5" height="7.5" rx="1.25" stroke="#0DBD8B" stroke-width="1.5"/>
|
||||
<rect x="0.75" y="11.8613" width="7.5" height="7.5" rx="1.25" stroke="#0DBD8B" stroke-width="1.5"/>
|
||||
<rect x="11.1111" width="9" height="9" rx="2" fill="#0DBD8B"/>
|
||||
<rect x="11.8611" y="11.8613" width="7.5" height="7.5" rx="1.25" stroke="#0DBD8B" stroke-width="1.5"/>
|
||||
</svg>
|
After Width: | Height: | Size: 466 B |
|
@ -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
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)];
|
||||
}
|
||||
|
|
|
@ -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<MXRoomTag*>* 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<UIContextualAction*> *actions = @[
|
||||
leaveAction,
|
||||
priorityAction,
|
||||
favouriteAction,
|
||||
muteAction,
|
||||
directChatAction
|
||||
];
|
||||
|
||||
UISwipeActionsConfiguration *swipeActionConfiguration = [UISwipeActionsConfiguration configurationWithActions:actions];
|
||||
swipeActionConfiguration.performsFirstActionWithFullSwipe = NO;
|
||||
return swipeActionConfiguration;
|
||||
}
|
||||
|
||||
- (void)leaveEditedRoom
|
||||
{
|
||||
if (editedRoomId)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -32,8 +32,8 @@ class AllChatsActionProvider {
|
|||
self.recentsAction,
|
||||
self.filtersAction,
|
||||
UIMenu(title: "", options: .displayInline, children: [
|
||||
alphabeticalOrderAction,
|
||||
activityOrderAction
|
||||
activityOrderAction,
|
||||
alphabeticalOrderAction
|
||||
])
|
||||
])
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Int> {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,14 +8,13 @@
|
|||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AllChatsViewController" customModule="Riot" customModuleProvider="target">
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AllChatsViewController" customModule="Element" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="recentsTableView" destination="orV-HH-88x" id="lgA-2k-pXJ"/>
|
||||
<outlet property="stickyHeadersBottomContainer" destination="EXH-mK-0eB" id="95Y-KP-bwF"/>
|
||||
<outlet property="stickyHeadersBottomContainerHeightConstraint" destination="SNq-Js-N7s" id="vom-iM-s6W"/>
|
||||
<outlet property="stickyHeadersTopContainer" destination="JJC-Bw-6sa" id="JIy-sf-4Ya"/>
|
||||
<outlet property="stickyHeadersTopContainerHeightConstraint" destination="xT1-rL-nCC" id="VaK-0W-2Mi"/>
|
||||
<outlet property="toolbar" destination="7eM-ZH-m1e" id="s2M-8i-KI8"/>
|
||||
<outlet property="view" destination="iN0-l3-epB" id="NUQ-LI-M61"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
|
@ -25,7 +24,7 @@
|
|||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="onDrag" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" translatesAutoresizingMaskIntoConstraints="NO" id="orV-HH-88x">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="623"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</tableView>
|
||||
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="JJC-Bw-6sa">
|
||||
|
@ -44,37 +43,22 @@
|
|||
<constraint firstAttribute="height" id="SNq-Js-N7s"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<toolbar opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7eM-ZH-m1e">
|
||||
<rect key="frame" x="0.0" y="623" width="375" height="44"/>
|
||||
<items>
|
||||
<barButtonItem image="ellipsis.circle" catalog="system" changesSelectionAsPrimaryAction="YES" id="Vkf-Sh-ckk"/>
|
||||
<barButtonItem style="plain" systemItem="flexibleSpace" id="7kJ-Np-DjU"/>
|
||||
<barButtonItem image="square.and.pencil" catalog="system" changesSelectionAsPrimaryAction="YES" id="Iam-Gs-cJP"/>
|
||||
</items>
|
||||
</toolbar>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="4qf-KS-Fc9"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="orV-HH-88x" secondAttribute="trailing" id="3Np-64-AUe"/>
|
||||
<constraint firstItem="7eM-ZH-m1e" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="Dfw-Ep-vIx"/>
|
||||
<constraint firstItem="4qf-KS-Fc9" firstAttribute="bottom" secondItem="orV-HH-88x" secondAttribute="bottom" id="Bka-Zz-CEr"/>
|
||||
<constraint firstItem="orV-HH-88x" firstAttribute="top" secondItem="JJC-Bw-6sa" secondAttribute="bottom" id="IMR-dV-gUS"/>
|
||||
<constraint firstItem="EXH-mK-0eB" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="bottom" id="Kmg-aC-GOO"/>
|
||||
<constraint firstItem="7eM-ZH-m1e" firstAttribute="trailing" secondItem="iN0-l3-epB" secondAttribute="trailing" id="NG9-KI-4ZF"/>
|
||||
<constraint firstItem="JJC-Bw-6sa" firstAttribute="top" secondItem="4qf-KS-Fc9" secondAttribute="top" id="OBu-sH-mqE"/>
|
||||
<constraint firstItem="4qf-KS-Fc9" firstAttribute="bottom" secondItem="7eM-ZH-m1e" secondAttribute="bottom" id="Ord-4E-Pvk"/>
|
||||
<constraint firstItem="EXH-mK-0eB" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="S3i-DW-PUB"/>
|
||||
<constraint firstAttribute="trailing" secondItem="EXH-mK-0eB" secondAttribute="trailing" id="bPP-yu-FTa"/>
|
||||
<constraint firstItem="7eM-ZH-m1e" firstAttribute="top" secondItem="orV-HH-88x" secondAttribute="bottom" id="clZ-LH-5Cr"/>
|
||||
<constraint firstItem="orV-HH-88x" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="dTn-zC-Axs"/>
|
||||
<constraint firstItem="JJC-Bw-6sa" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="l5C-zt-Env"/>
|
||||
<constraint firstAttribute="trailing" secondItem="JJC-Bw-6sa" secondAttribute="trailing" id="trw-3x-yjO"/>
|
||||
</constraints>
|
||||
<point key="canvasLocation" x="140" y="138"/>
|
||||
<point key="canvasLocation" x="140" y="137.18140929535232"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="ellipsis.circle" catalog="system" width="128" height="121"/>
|
||||
<image name="square.and.pencil" catalog="system" width="128" height="115"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -56,7 +56,8 @@ enum MockAppScreens {
|
|||
MockTemplateSimpleScreenScreenState.self,
|
||||
MockTemplateUserProfileScreenState.self,
|
||||
MockTemplateRoomListScreenState.self,
|
||||
MockTemplateRoomChatScreenState.self
|
||||
MockTemplateRoomChatScreenState.self,
|
||||
MockSpaceSelectorScreenState.self
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -20,5 +20,6 @@ import Foundation
|
|||
|
||||
struct SpaceCreationPostProcessCoordinatorParameters {
|
||||
let session: MXSession
|
||||
let parentSpaceId: String?
|
||||
let creationParams: SpaceCreationParameters
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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<String?, Never>
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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<String?, Never>
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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<String?, Never> { get }
|
||||
var selectedSpaceId: String? { get }
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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<SpaceSelectorViewState,
|
||||
Never,
|
||||
SpaceSelectorViewAction>
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
1
changelog.d/6410.change
Normal file
|
@ -0,0 +1 @@
|
|||
App Layout: Implemented the new Space selector bottom sheet
|