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
|
static let allowInviteExernalUsers: Bool = true
|
||||||
|
|
||||||
// MARK: - Side Menu
|
// MARK: - Side Menu
|
||||||
static let enableSideMenu: Bool = true
|
static let enableSideMenu: Bool = true && !newAppLayoutEnabled
|
||||||
static let sideMenuShowInviteFriends: Bool = true
|
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.
|
/// 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" : {
|
"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" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "home_fab_join_room.png",
|
"filename" : "home_my_spaces_action.svg",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "home_fab_join_room@2x.png",
|
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "home_fab_join_room@3x.png",
|
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "3x"
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
|
@ -21,6 +19,7 @@
|
||||||
"version" : 1
|
"version" : 1
|
||||||
},
|
},
|
||||||
"properties" : {
|
"properties" : {
|
||||||
|
"preserves-vector-representation" : true,
|
||||||
"template-rendering-intent" : "template"
|
"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_space_title" = "Create space";
|
||||||
"spaces_add_subspace_title" = "Create space within %@";
|
"spaces_add_subspace_title" = "Create space within %@";
|
||||||
"spaces_create_space_title" = "Create a space";
|
"spaces_create_space_title" = "Create a space";
|
||||||
|
"spaces_create_subspace_title" = "Create a subspace";
|
||||||
"spaces_left_panel_title" = "Spaces";
|
"spaces_left_panel_title" = "Spaces";
|
||||||
"leave_space_title" = "Leave %@";
|
"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?";
|
"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_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_title" = "What type of space do you want to create?";
|
||||||
"spaces_creation_visibility_message" = "To join an existing space, you need an invite.";
|
"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_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_settings_message" = "Add some details to help it stand out. You can change these at any point.";
|
||||||
"spaces_creation_address" = "Address";
|
"spaces_creation_address" = "Address";
|
||||||
|
@ -2158,7 +2161,7 @@ Tap the + to start adding people.";
|
||||||
|
|
||||||
"all_chats_title" = "All chats";
|
"all_chats_title" = "All chats";
|
||||||
"all_chats_section_title" = "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_recents" = "Recents";
|
||||||
"all_chats_edit_layout_unreads" = "Unreads";
|
"all_chats_edit_layout_unreads" = "Unreads";
|
||||||
"all_chats_edit_layout_add_section_title" = "Add section to home";
|
"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";
|
"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
|
// Mark: - Space Selector
|
||||||
|
|
||||||
"space_selector_title" = "Choose space";
|
"space_selector_title" = "My spaces";
|
||||||
|
|
||||||
// Mark: - Polls
|
// Mark: - Polls
|
||||||
|
|
||||||
|
|
|
@ -115,8 +115,7 @@ internal class Asset: NSObject {
|
||||||
internal static let roomActionPriorityLow = ImageAsset(name: "room_action_priority_low")
|
internal static let roomActionPriorityLow = ImageAsset(name: "room_action_priority_low")
|
||||||
internal static let homeEmptyScreenArtwork = ImageAsset(name: "home_empty_screen_artwork")
|
internal static let homeEmptyScreenArtwork = ImageAsset(name: "home_empty_screen_artwork")
|
||||||
internal static let homeEmptyScreenArtworkDark = ImageAsset(name: "home_empty_screen_artwork_dark")
|
internal static let homeEmptyScreenArtworkDark = ImageAsset(name: "home_empty_screen_artwork_dark")
|
||||||
internal static let homeFabCreateRoom = ImageAsset(name: "home_fab_create_room")
|
internal static let homeMySpacesAction = ImageAsset(name: "home_my_spaces_action")
|
||||||
internal static let homeFabJoinRoom = ImageAsset(name: "home_fab_join_room")
|
|
||||||
internal static let plusFloatingAction = ImageAsset(name: "plus_floating_action")
|
internal static let plusFloatingAction = ImageAsset(name: "plus_floating_action")
|
||||||
internal static let versionCheckCloseIcon = ImageAsset(name: "version_check_close_icon")
|
internal static let versionCheckCloseIcon = ImageAsset(name: "version_check_close_icon")
|
||||||
internal static let versionCheckInfoIcon = ImageAsset(name: "version_check_info_icon")
|
internal static let versionCheckInfoIcon = ImageAsset(name: "version_check_info_icon")
|
||||||
|
|
|
@ -119,7 +119,7 @@ public class VectorL10n: NSObject {
|
||||||
public static var allChatsAllFilter: String {
|
public static var allChatsAllFilter: String {
|
||||||
return VectorL10n.tr("Vector", "all_chats_all_filter")
|
return VectorL10n.tr("Vector", "all_chats_all_filter")
|
||||||
}
|
}
|
||||||
/// Edit layout
|
/// Layout preferences
|
||||||
public static var allChatsEditLayout: String {
|
public static var allChatsEditLayout: String {
|
||||||
return VectorL10n.tr("Vector", "all_chats_edit_layout")
|
return VectorL10n.tr("Vector", "all_chats_edit_layout")
|
||||||
}
|
}
|
||||||
|
@ -171,6 +171,14 @@ public class VectorL10n: NSObject {
|
||||||
public static var allChatsEditLayoutUnreads: String {
|
public static var allChatsEditLayoutUnreads: String {
|
||||||
return VectorL10n.tr("Vector", "all_chats_edit_layout_unreads")
|
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
|
/// Chats
|
||||||
public static var allChatsSectionTitle: String {
|
public static var allChatsSectionTitle: String {
|
||||||
return VectorL10n.tr("Vector", "all_chats_section_title")
|
return VectorL10n.tr("Vector", "all_chats_section_title")
|
||||||
|
@ -179,6 +187,10 @@ public class VectorL10n: NSObject {
|
||||||
public static var allChatsTitle: String {
|
public static var allChatsTitle: String {
|
||||||
return VectorL10n.tr("Vector", "all_chats_title")
|
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.
|
/// 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 {
|
public static func analyticsPromptMessageNewUser(_ p1: String) -> String {
|
||||||
return VectorL10n.tr("Vector", "analytics_prompt_message_new_user", p1)
|
return VectorL10n.tr("Vector", "analytics_prompt_message_new_user", p1)
|
||||||
|
@ -7779,7 +7791,7 @@ public class VectorL10n: NSObject {
|
||||||
public static var spacePublicJoinRuleDetail: String {
|
public static var spacePublicJoinRuleDetail: String {
|
||||||
return VectorL10n.tr("Vector", "space_public_join_rule_detail")
|
return VectorL10n.tr("Vector", "space_public_join_rule_detail")
|
||||||
}
|
}
|
||||||
/// Choose space
|
/// My spaces
|
||||||
public static var spaceSelectorTitle: String {
|
public static var spaceSelectorTitle: String {
|
||||||
return VectorL10n.tr("Vector", "space_selector_title")
|
return VectorL10n.tr("Vector", "space_selector_title")
|
||||||
}
|
}
|
||||||
|
@ -7839,6 +7851,10 @@ public class VectorL10n: NSObject {
|
||||||
public static var spacesCreateSpaceTitle: String {
|
public static var spacesCreateSpaceTitle: String {
|
||||||
return VectorL10n.tr("Vector", "spaces_create_space_title")
|
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.
|
/// As this space is just for you, no one will be informed. You can add more later.
|
||||||
public static var spacesCreationAddRoomsMessage: String {
|
public static var spacesCreationAddRoomsMessage: String {
|
||||||
return VectorL10n.tr("Vector", "spaces_creation_add_rooms_message")
|
return VectorL10n.tr("Vector", "spaces_creation_add_rooms_message")
|
||||||
|
@ -8071,6 +8087,14 @@ public class VectorL10n: NSObject {
|
||||||
public static var spacesNoRoomFoundDetail: String {
|
public static var spacesNoRoomFoundDetail: String {
|
||||||
return VectorL10n.tr("Vector", "spaces_no_room_found_detail")
|
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
|
/// Suggested
|
||||||
public static var spacesSuggestedRoom: String {
|
public static var spacesSuggestedRoom: String {
|
||||||
return VectorL10n.tr("Vector", "spaces_suggested_room")
|
return VectorL10n.tr("Vector", "spaces_suggested_room")
|
||||||
|
|
|
@ -146,8 +146,16 @@ class DarkTheme: NSObject, Theme {
|
||||||
|
|
||||||
navigationBar.standardAppearance = appearance
|
navigationBar.standardAppearance = appearance
|
||||||
|
|
||||||
navigationBar.scrollEdgeAppearance = modernScrollEdgeAppearance || BuildSettings.newAppLayoutEnabled ? nil : appearance
|
if BuildSettings.newAppLayoutEnabled {
|
||||||
} else {
|
appearance.configureWithOpaqueBackground()
|
||||||
|
appearance.backgroundColor = baseColor
|
||||||
|
appearance.shadowColor = nil
|
||||||
|
appearance.titleTextAttributes = [
|
||||||
|
NSAttributedString.Key.foregroundColor: textPrimaryColor
|
||||||
|
]
|
||||||
|
}
|
||||||
|
navigationBar.scrollEdgeAppearance = modernScrollEdgeAppearance ? nil : appearance
|
||||||
|
} else {
|
||||||
navigationBar.titleTextAttributes = [
|
navigationBar.titleTextAttributes = [
|
||||||
NSAttributedString.Key.foregroundColor: textPrimaryColor
|
NSAttributedString.Key.foregroundColor: textPrimaryColor
|
||||||
]
|
]
|
||||||
|
|
|
@ -149,7 +149,15 @@ class DefaultTheme: NSObject, Theme {
|
||||||
]
|
]
|
||||||
|
|
||||||
navigationBar.standardAppearance = appearance
|
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 {
|
} else {
|
||||||
navigationBar.titleTextAttributes = [
|
navigationBar.titleTextAttributes = [
|
||||||
NSAttributedString.Key.foregroundColor: textPrimaryColor
|
NSAttributedString.Key.foregroundColor: textPrimaryColor
|
||||||
|
|
|
@ -183,7 +183,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
|
||||||
[types addObject:@(RecentsDataSourceSectionTypeSecureBackupBanner)];
|
[types addObject:@(RecentsDataSourceSectionTypeSecureBackupBanner)];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.invitesCellDataArray.count > 0)
|
if (!BuildSettings.newAppLayoutEnabled && self.invitesCellDataArray.count > 0)
|
||||||
{
|
{
|
||||||
[types addObject:@(RecentsDataSourceSectionTypeInvites)];
|
[types addObject:@(RecentsDataSourceSectionTypeInvites)];
|
||||||
}
|
}
|
||||||
|
@ -229,7 +229,12 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
|
||||||
[types addObject:@(RecentsDataSourceSectionTypeAllChats)];
|
[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)];
|
[types addObject:@(RecentsDataSourceSectionTypeSuggestedRooms)];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1118,166 +1118,6 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
|
||||||
|
|
||||||
#pragma mark - Swipe actions
|
#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
|
- (void)leaveEditedRoom
|
||||||
{
|
{
|
||||||
if (editedRoomId)
|
if (editedRoomId)
|
||||||
|
|
|
@ -35,7 +35,7 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
|
||||||
|
|
||||||
private var invitedRoomListDataFetcher: MXRoomListDataFetcher? {
|
private var invitedRoomListDataFetcher: MXRoomListDataFetcher? {
|
||||||
switch mode {
|
switch mode {
|
||||||
case .home:
|
case .home, .allChats:
|
||||||
return invitedRoomListDataFetcherForHome
|
return invitedRoomListDataFetcherForHome
|
||||||
case .people:
|
case .people:
|
||||||
return invitedRoomListDataFetcherForPeople
|
return invitedRoomListDataFetcherForPeople
|
||||||
|
@ -548,7 +548,7 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
|
||||||
|
|
||||||
private func createCommonRoomListDataFetcher(withDataTypes dataTypes: MXRoomSummaryDataTypes = [],
|
private func createCommonRoomListDataFetcher(withDataTypes dataTypes: MXRoomSummaryDataTypes = [],
|
||||||
onlySuggested: Bool = false,
|
onlySuggested: Bool = false,
|
||||||
onlyRecents: Bool = false,
|
onlyBreadcrumbs: Bool = false,
|
||||||
paginate: Bool = true,
|
paginate: Bool = true,
|
||||||
strictMatches: Bool = false) -> MXRoomListDataFetcher {
|
strictMatches: Bool = false) -> MXRoomListDataFetcher {
|
||||||
guard let session = session else {
|
guard let session = session else {
|
||||||
|
@ -556,14 +556,14 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
|
||||||
}
|
}
|
||||||
let filterOptions = MXRoomListDataFilterOptions(dataTypes: dataTypes,
|
let filterOptions = MXRoomListDataFilterOptions(dataTypes: dataTypes,
|
||||||
onlySuggested: onlySuggested,
|
onlySuggested: onlySuggested,
|
||||||
onlyBreadcrumbs: onlyRecents,
|
onlyBreadcrumbs: onlyBreadcrumbs,
|
||||||
query: query,
|
query: query,
|
||||||
space: space,
|
space: space,
|
||||||
showAllRoomsInHomeSpace: showAllRoomsInHomeSpace,
|
showAllRoomsInHomeSpace: showAllRoomsInHomeSpace,
|
||||||
strictMatches: strictMatches)
|
strictMatches: strictMatches)
|
||||||
|
|
||||||
let fetchOptions = MXRoomListDataFetchOptions(filterOptions: filterOptions,
|
let fetchOptions = MXRoomListDataFetchOptions(filterOptions: filterOptions,
|
||||||
sortOptions: onlyRecents ? noSortOptions : sortOptions,
|
sortOptions: onlyBreadcrumbs ? noSortOptions : sortOptions,
|
||||||
async: true)
|
async: true)
|
||||||
let fetcher = session.roomListDataManager.fetcher(withOptions: fetchOptions)
|
let fetcher = session.roomListDataManager.fetcher(withOptions: fetchOptions)
|
||||||
if paginate {
|
if paginate {
|
||||||
|
@ -653,7 +653,7 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
|
||||||
lowPriorityRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.lowPriority])
|
lowPriorityRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.lowPriority])
|
||||||
serverNoticeRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.serverNotice])
|
serverNoticeRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.serverNotice])
|
||||||
suggestedRoomListDataFetcher = createCommonRoomListDataFetcher(onlySuggested: true)
|
suggestedRoomListDataFetcher = createCommonRoomListDataFetcher(onlySuggested: true)
|
||||||
breadcrumbsRoomListDataFetcher = createCommonRoomListDataFetcher(onlyRecents: true)
|
breadcrumbsRoomListDataFetcher = createCommonRoomListDataFetcher(onlyBreadcrumbs: true)
|
||||||
allChatsRoomListDataFetcher = createConversationRoomListDataFetcherForAllChats()
|
allChatsRoomListDataFetcher = createConversationRoomListDataFetcherForAllChats()
|
||||||
|
|
||||||
fetchersCreated = true
|
fetchersCreated = true
|
||||||
|
|
|
@ -74,7 +74,7 @@ class AllChatsFilterOptionListView: UIView, Themable {
|
||||||
// MARK: - Themable
|
// MARK: - Themable
|
||||||
|
|
||||||
func update(theme: Theme) {
|
func update(theme: Theme) {
|
||||||
backgroundColor = theme.colors.background.withAlphaComponent(0.3)
|
backgroundColor = theme.colors.background.withAlphaComponent(0.7)
|
||||||
|
|
||||||
tabListView.itemFont = theme.fonts.callout
|
tabListView.itemFont = theme.fonts.callout
|
||||||
tabListView.tintColor = theme.colors.accent
|
tabListView.tintColor = theme.colors.accent
|
||||||
|
|
|
@ -32,8 +32,8 @@ class AllChatsActionProvider {
|
||||||
self.recentsAction,
|
self.recentsAction,
|
||||||
self.filtersAction,
|
self.filtersAction,
|
||||||
UIMenu(title: "", options: .displayInline, children: [
|
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()))
|
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 {
|
static override func instantiate() -> Self {
|
||||||
let storyboard = UIStoryboard(name: "Main", bundle: .main)
|
let storyboard = UIStoryboard(name: "Main", bundle: .main)
|
||||||
guard let viewController = storyboard.instantiateViewController(withIdentifier: "AllChatsViewController") as? Self else {
|
guard let viewController = storyboard.instantiateViewController(withIdentifier: "AllChatsViewController") as? Self else {
|
||||||
|
@ -39,30 +33,52 @@ class AllChatsViewController: HomeViewController {
|
||||||
return viewController
|
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() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
editActionProvider.delegate = self
|
||||||
|
|
||||||
recentsTableView.tag = RecentsDataSourceMode.allChats.rawValue
|
recentsTableView.tag = RecentsDataSourceMode.allChats.rawValue
|
||||||
recentsTableView.clipsToBounds = false
|
recentsTableView.clipsToBounds = false
|
||||||
|
|
||||||
self.tabBarController?.title = VectorL10n.allChatsTitle
|
updateUI()
|
||||||
vc_setLargeTitleDisplayMode(.automatic)
|
vc_setLargeTitleDisplayMode(.automatic)
|
||||||
|
|
||||||
searchController.obscuresBackgroundDuringPresentation = false
|
searchController.obscuresBackgroundDuringPresentation = false
|
||||||
searchController.searchResultsUpdater = self
|
searchController.searchResultsUpdater = self
|
||||||
|
|
||||||
|
self.setupEditOptions()
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(self.setupEditOptions), name: AllChatsLayoutSettingsManager.didUpdateSettings, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(self.setupEditOptions), name: AllChatsLayoutSettingsManager.didUpdateSettings, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
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 {
|
if self.tabBarController?.navigationItem.searchController == nil {
|
||||||
self.tabBarController?.navigationItem.searchController = searchController
|
self.tabBarController?.navigationItem.searchController = searchController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
super.viewWillDisappear(animated)
|
||||||
|
|
||||||
|
self.navigationController?.isToolbarHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - HomeViewController
|
// MARK: - HomeViewController
|
||||||
|
|
||||||
override var recentsDataSourceMode: RecentsDataSourceMode {
|
override var recentsDataSourceMode: RecentsDataSourceMode {
|
||||||
|
@ -70,7 +86,7 @@ class AllChatsViewController: HomeViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func addFabButton() {
|
@objc private func addFabButton() {
|
||||||
self.setupEditOptions()
|
// Nothing to do. We don't need FAB
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func sections() -> Array<Int> {
|
@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
|
// MARK: - Private
|
||||||
|
|
||||||
@objc private func setupEditOptions() {
|
@objc private func setupEditOptions() {
|
||||||
// Note: updating toolbar items doesn't work as expected and has weird behaviour
|
self.tabBarController?.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "line.3.horizontal.decrease.circle"), menu: AllChatsActionProvider().menu)
|
||||||
// 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)
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// 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"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<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>
|
<connections>
|
||||||
<outlet property="recentsTableView" destination="orV-HH-88x" id="lgA-2k-pXJ"/>
|
<outlet property="recentsTableView" destination="orV-HH-88x" id="lgA-2k-pXJ"/>
|
||||||
<outlet property="stickyHeadersBottomContainer" destination="EXH-mK-0eB" id="95Y-KP-bwF"/>
|
<outlet property="stickyHeadersBottomContainer" destination="EXH-mK-0eB" id="95Y-KP-bwF"/>
|
||||||
<outlet property="stickyHeadersBottomContainerHeightConstraint" destination="SNq-Js-N7s" id="vom-iM-s6W"/>
|
<outlet property="stickyHeadersBottomContainerHeightConstraint" destination="SNq-Js-N7s" id="vom-iM-s6W"/>
|
||||||
<outlet property="stickyHeadersTopContainer" destination="JJC-Bw-6sa" id="JIy-sf-4Ya"/>
|
<outlet property="stickyHeadersTopContainer" destination="JJC-Bw-6sa" id="JIy-sf-4Ya"/>
|
||||||
<outlet property="stickyHeadersTopContainerHeightConstraint" destination="xT1-rL-nCC" id="VaK-0W-2Mi"/>
|
<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"/>
|
<outlet property="view" destination="iN0-l3-epB" id="NUQ-LI-M61"/>
|
||||||
</connections>
|
</connections>
|
||||||
</placeholder>
|
</placeholder>
|
||||||
|
@ -25,7 +24,7 @@
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<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">
|
<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"/>
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||||
</tableView>
|
</tableView>
|
||||||
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="JJC-Bw-6sa">
|
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="JJC-Bw-6sa">
|
||||||
|
@ -44,37 +43,22 @@
|
||||||
<constraint firstAttribute="height" id="SNq-Js-N7s"/>
|
<constraint firstAttribute="height" id="SNq-Js-N7s"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</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>
|
</subviews>
|
||||||
<viewLayoutGuide key="safeArea" id="4qf-KS-Fc9"/>
|
<viewLayoutGuide key="safeArea" id="4qf-KS-Fc9"/>
|
||||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="trailing" secondItem="orV-HH-88x" secondAttribute="trailing" id="3Np-64-AUe"/>
|
<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="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="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="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 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 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="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 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"/>
|
<constraint firstAttribute="trailing" secondItem="JJC-Bw-6sa" secondAttribute="trailing" id="trw-3x-yjO"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
<point key="canvasLocation" x="140" y="138"/>
|
<point key="canvasLocation" x="140" y="137.18140929535232"/>
|
||||||
</view>
|
</view>
|
||||||
</objects>
|
</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>
|
</document>
|
||||||
|
|
|
@ -265,7 +265,7 @@ final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let coordinator = SpaceCreationCoordinator(parameters: SpaceCreationCoordinatorParameters(session: session))
|
let coordinator = SpaceCreationCoordinator(parameters: SpaceCreationCoordinatorParameters(session: session, parentSpaceId: nil))
|
||||||
let presentable = coordinator.toPresentable()
|
let presentable = coordinator.toPresentable()
|
||||||
presentable.presentationController?.delegate = self
|
presentable.presentationController?.delegate = self
|
||||||
self.sideMenuViewController.present(presentable, animated: true, completion: nil)
|
self.sideMenuViewController.present(presentable, animated: true, completion: nil)
|
||||||
|
|
|
@ -737,6 +737,11 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
|
||||||
private weak var rightMenuAvatarView: AvatarView?
|
private weak var rightMenuAvatarView: AvatarView?
|
||||||
|
|
||||||
private func createLeftButtonItem(for viewController: UIViewController) {
|
private func createLeftButtonItem(for viewController: UIViewController) {
|
||||||
|
guard !BuildSettings.newAppLayoutEnabled else {
|
||||||
|
createAvatarButtonItem(for: viewController)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
guard BuildSettings.enableSideMenu else {
|
guard BuildSettings.enableSideMenu else {
|
||||||
let settingsBarButtonItem: MXKBarButtonItem = MXKBarButtonItem(image: Asset.Images.settingsIcon.image, style: .plain) { [weak self] in
|
let settingsBarButtonItem: MXKBarButtonItem = MXKBarButtonItem(image: Asset.Images.settingsIcon.image, style: .plain) { [weak self] in
|
||||||
self?.showSettings()
|
self?.showSettings()
|
||||||
|
@ -756,23 +761,21 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createRightButtonItem(for viewController: UIViewController) {
|
private func createRightButtonItem(for viewController: UIViewController) {
|
||||||
guard BuildSettings.newAppLayoutEnabled else {
|
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
|
|
||||||
|
|
||||||
return
|
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) {
|
private func createAvatarButtonItem(for viewController: UIViewController) {
|
||||||
var actions: [UIMenuElement] = []
|
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()
|
self?.showSettings()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -788,11 +791,6 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
|
||||||
})
|
})
|
||||||
|
|
||||||
actions.append(UIMenu(title: "", options: .displayInline, children: subMenuActions))
|
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: [
|
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
|
UIAction(title: VectorL10n.settingsSignOut, image: UIImage(systemName: "rectangle.portrait.and.arrow.right.fill"), attributes: .destructive) { [weak self] action in
|
||||||
self?.signOut()
|
self?.signOut()
|
||||||
|
@ -822,7 +820,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
|
||||||
avatarView.fill(with: avatar)
|
avatarView.fill(with: avatar)
|
||||||
}
|
}
|
||||||
|
|
||||||
viewController.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: view)
|
viewController.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: view)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateAvatarButtonItem() {
|
private func updateAvatarButtonItem() {
|
||||||
|
@ -924,6 +922,11 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
|
||||||
private var windowOverlay: WindowOverlayPresenter?
|
private var windowOverlay: WindowOverlayPresenter?
|
||||||
|
|
||||||
func showCoachMessageIfNeeded(with session: MXSession) {
|
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 {
|
if !RiotSettings.shared.slideMenuRoomsCoachMessageHasBeenDisplayed {
|
||||||
let isAuthenticated = MXKAccountManager.shared().activeAccounts.first != nil || MXKAccountManager.shared().accounts.first?.isSoftLogout == false
|
let isAuthenticated = MXKAccountManager.shared().activeAccounts.first != nil || MXKAccountManager.shared().accounts.first?.isSoftLogout == false
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,8 @@ enum MockAppScreens {
|
||||||
MockTemplateSimpleScreenScreenState.self,
|
MockTemplateSimpleScreenScreenState.self,
|
||||||
MockTemplateUserProfileScreenState.self,
|
MockTemplateUserProfileScreenState.self,
|
||||||
MockTemplateRoomListScreenState.self,
|
MockTemplateRoomListScreenState.self,
|
||||||
MockTemplateRoomChatScreenState.self
|
MockTemplateRoomChatScreenState.self,
|
||||||
|
MockSpaceSelectorScreenState.self
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,14 +46,24 @@ final class SpaceCreationCoordinator: Coordinator {
|
||||||
// MARK: - Setup
|
// MARK: - Setup
|
||||||
|
|
||||||
init(parameters: SpaceCreationCoordinatorParameters) {
|
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.parameters = parameters
|
||||||
self.spaceVisibilityMenuParameters = SpaceCreationMenuCoordinatorParameters(
|
self.spaceVisibilityMenuParameters = SpaceCreationMenuCoordinatorParameters(
|
||||||
session: parameters.session,
|
session: parameters.session,
|
||||||
creationParams: parameters.creationParameters,
|
creationParams: parameters.creationParameters,
|
||||||
navTitle: VectorL10n.spacesCreateSpaceTitle,
|
navTitle: VectorL10n.spacesCreateSpaceTitle,
|
||||||
showBackButton: false,
|
showBackButton: false,
|
||||||
title: VectorL10n.spacesCreationVisibilityTitle,
|
title: title,
|
||||||
detail: VectorL10n.spacesCreationVisibilityMessage,
|
detail: message,
|
||||||
options: [
|
options: [
|
||||||
SpaceCreationMenuRoomOption(id: .publicSpace, icon: Asset.Images.spaceCreationPublic.image, title: VectorL10n.public, detail: VectorL10n.spacePublicJoinRuleDetail),
|
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)
|
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 {
|
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
|
coordinator.callback = { [weak self] result in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
switch result {
|
switch result {
|
||||||
|
|
|
@ -26,6 +26,9 @@ struct SpaceCreationCoordinatorParameters {
|
||||||
/// The Matrix session
|
/// The Matrix session
|
||||||
let session: MXSession
|
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
|
/// Parameters needed to create the new space
|
||||||
let creationParameters: SpaceCreationParameters = SpaceCreationParameters()
|
let creationParameters: SpaceCreationParameters = SpaceCreationParameters()
|
||||||
|
|
||||||
|
@ -33,8 +36,10 @@ struct SpaceCreationCoordinatorParameters {
|
||||||
let navigationRouter: NavigationRouterType
|
let navigationRouter: NavigationRouterType
|
||||||
|
|
||||||
init(session: MXSession,
|
init(session: MXSession,
|
||||||
|
parentSpaceId: String?,
|
||||||
navigationRouter: NavigationRouterType? = nil) {
|
navigationRouter: NavigationRouterType? = nil) {
|
||||||
self.session = session
|
self.session = session
|
||||||
|
self.parentSpaceId = parentSpaceId
|
||||||
self.navigationRouter = navigationRouter ?? NavigationRouter(navigationController: RiotNavigationController())
|
self.navigationRouter = navigationRouter ?? NavigationRouter(navigationController: RiotNavigationController())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ final class SpaceCreationPostProcessCoordinator: Coordinator, Presentable {
|
||||||
|
|
||||||
init(parameters: SpaceCreationPostProcessCoordinatorParameters) {
|
init(parameters: SpaceCreationPostProcessCoordinatorParameters) {
|
||||||
self.parameters = parameters
|
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)
|
let view = SpaceCreationPostProcess(viewModel: viewModel.context)
|
||||||
.addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
|
.addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
|
||||||
spaceCreationPostProcessViewModel = viewModel
|
spaceCreationPostProcessViewModel = viewModel
|
||||||
|
|
|
@ -20,5 +20,6 @@ import Foundation
|
||||||
|
|
||||||
struct SpaceCreationPostProcessCoordinatorParameters {
|
struct SpaceCreationPostProcessCoordinatorParameters {
|
||||||
let session: MXSession
|
let session: MXSession
|
||||||
|
let parentSpaceId: String?
|
||||||
let creationParams: SpaceCreationParameters
|
let creationParams: SpaceCreationParameters
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol {
|
||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
private let session: MXSession
|
private let session: MXSession
|
||||||
|
private let parentSpaceId: String?
|
||||||
private let creationParams: SpaceCreationParameters
|
private let creationParams: SpaceCreationParameters
|
||||||
|
|
||||||
private var tasks: [SpaceCreationPostProcessTask] = []
|
private var tasks: [SpaceCreationPostProcessTask] = []
|
||||||
|
@ -66,8 +67,9 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol {
|
||||||
|
|
||||||
// MARK: - Setup
|
// MARK: - Setup
|
||||||
|
|
||||||
init(session: MXSession, creationParams: SpaceCreationParameters) {
|
init(session: MXSession, parentSpaceId: String?, creationParams: SpaceCreationParameters) {
|
||||||
self.session = session
|
self.session = session
|
||||||
|
self.parentSpaceId = parentSpaceId
|
||||||
self.creationParams = creationParams
|
self.creationParams = creationParams
|
||||||
self.tasks = Self.tasks(with: creationParams)
|
self.tasks = Self.tasks(with: creationParams)
|
||||||
self.tasksSubject = CurrentValueSubject(tasks)
|
self.tasksSubject = CurrentValueSubject(tasks)
|
||||||
|
@ -168,13 +170,25 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol {
|
||||||
let userIdInvites = creationParams.inviteType == .userId ? creationParams.userIdInvites : []
|
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
|
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 }
|
guard let self = self else { return }
|
||||||
|
|
||||||
if response.isFailure {
|
if response.isFailure {
|
||||||
self.updateCurrentTask(with: .failure)
|
self.updateCurrentTask(with: .failure)
|
||||||
} else {
|
} else {
|
||||||
self.creationParams.isModified = false
|
self.creationParams.isModified = false
|
||||||
self.createdSpace = response.value
|
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
|