Implement new space selector bottom sheet (#6518)

* Delight: Edit layout experiment #6079
This commit is contained in:
Gil Eluard 2022-08-05 13:39:45 +02:00 committed by GitHub
parent 6cb076badb
commit a809185407
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 1705 additions and 295 deletions

View file

@ -235,7 +235,7 @@ final class BuildSettings: NSObject {
static let allowInviteExernalUsers: Bool = true
// MARK: - Side Menu
static let enableSideMenu: Bool = true
static let enableSideMenu: Bool = true && !newAppLayoutEnabled
static let sideMenuShowInviteFriends: Bool = true
/// Whether to read the `io.element.functional_members` state event and exclude any service members when computing a room's name and avatar.

View file

@ -1,6 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}
}

View file

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 569 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 992 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,17 +1,15 @@
{
"images" : [
{
"filename" : "home_fab_join_room.png",
"filename" : "home_my_spaces_action.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "home_fab_join_room@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "home_fab_join_room@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
@ -21,6 +19,7 @@
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}

View 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

View file

@ -1998,6 +1998,7 @@ Tap the + to start adding people.";
"spaces_add_space_title" = "Create space";
"spaces_add_subspace_title" = "Create space within %@";
"spaces_create_space_title" = "Create a space";
"spaces_create_subspace_title" = "Create a subspace";
"spaces_left_panel_title" = "Spaces";
"leave_space_title" = "Leave %@";
"leave_space_message" = "Are you sure you want to leave %@? Do you also want to leave all rooms and spaces of this space?";
@ -2043,6 +2044,8 @@ Tap the + to start adding people.";
"spaces_creation_hint" = "Spaces are a new way to group rooms and people.";
"spaces_creation_visibility_title" = "What type of space do you want to create?";
"spaces_creation_visibility_message" = "To join an existing space, you need an invite.";
"spaces_subspace_creation_visibility_title" = "What type of subspace do you want to create?";
"spaces_subspace_creation_visibility_message" = "The created space will be added to %@.";
"spaces_creation_footer" = "You can change this later";
"spaces_creation_settings_message" = "Add some details to help it stand out. You can change these at any point.";
"spaces_creation_address" = "Address";
@ -2158,7 +2161,7 @@ Tap the + to start adding people.";
"all_chats_title" = "All chats";
"all_chats_section_title" = "Chats";
"all_chats_edit_layout" = "Edit layout";
"all_chats_edit_layout" = "Layout preferences";
"all_chats_edit_layout_recents" = "Recents";
"all_chats_edit_layout_unreads" = "Unreads";
"all_chats_edit_layout_add_section_title" = "Add section to home";
@ -2176,9 +2179,14 @@ Tap the + to start adding people.";
"room_recents_recently_viewed_section" = "Recently viewed";
"all_chats_user_menu_settings" = "User settings";
"all_chats_edit_menu_leave_space" = "Leave %@";
"all_chats_edit_menu_space_settings" = "Space settings";
// Mark: - Space Selector
"space_selector_title" = "Choose space";
"space_selector_title" = "My spaces";
// Mark: - Polls

View file

@ -115,8 +115,7 @@ internal class Asset: NSObject {
internal static let roomActionPriorityLow = ImageAsset(name: "room_action_priority_low")
internal static let homeEmptyScreenArtwork = ImageAsset(name: "home_empty_screen_artwork")
internal static let homeEmptyScreenArtworkDark = ImageAsset(name: "home_empty_screen_artwork_dark")
internal static let homeFabCreateRoom = ImageAsset(name: "home_fab_create_room")
internal static let homeFabJoinRoom = ImageAsset(name: "home_fab_join_room")
internal static let homeMySpacesAction = ImageAsset(name: "home_my_spaces_action")
internal static let plusFloatingAction = ImageAsset(name: "plus_floating_action")
internal static let versionCheckCloseIcon = ImageAsset(name: "version_check_close_icon")
internal static let versionCheckInfoIcon = ImageAsset(name: "version_check_info_icon")

View file

@ -119,7 +119,7 @@ public class VectorL10n: NSObject {
public static var allChatsAllFilter: String {
return VectorL10n.tr("Vector", "all_chats_all_filter")
}
/// Edit layout
/// Layout preferences
public static var allChatsEditLayout: String {
return VectorL10n.tr("Vector", "all_chats_edit_layout")
}
@ -171,6 +171,14 @@ public class VectorL10n: NSObject {
public static var allChatsEditLayoutUnreads: String {
return VectorL10n.tr("Vector", "all_chats_edit_layout_unreads")
}
/// Leave %@
public static func allChatsEditMenuLeaveSpace(_ p1: String) -> String {
return VectorL10n.tr("Vector", "all_chats_edit_menu_leave_space", p1)
}
/// Space settings
public static var allChatsEditMenuSpaceSettings: String {
return VectorL10n.tr("Vector", "all_chats_edit_menu_space_settings")
}
/// Chats
public static var allChatsSectionTitle: String {
return VectorL10n.tr("Vector", "all_chats_section_title")
@ -179,6 +187,10 @@ public class VectorL10n: NSObject {
public static var allChatsTitle: String {
return VectorL10n.tr("Vector", "all_chats_title")
}
/// User settings
public static var allChatsUserMenuSettings: String {
return VectorL10n.tr("Vector", "all_chats_user_menu_settings")
}
/// Help us identify issues and improve %@ by sharing anonymous usage data. To understand how people use multiple devices, well generate a random identifier, shared by your devices.
public static func analyticsPromptMessageNewUser(_ p1: String) -> String {
return VectorL10n.tr("Vector", "analytics_prompt_message_new_user", p1)
@ -7779,7 +7791,7 @@ public class VectorL10n: NSObject {
public static var spacePublicJoinRuleDetail: String {
return VectorL10n.tr("Vector", "space_public_join_rule_detail")
}
/// Choose space
/// My spaces
public static var spaceSelectorTitle: String {
return VectorL10n.tr("Vector", "space_selector_title")
}
@ -7839,6 +7851,10 @@ public class VectorL10n: NSObject {
public static var spacesCreateSpaceTitle: String {
return VectorL10n.tr("Vector", "spaces_create_space_title")
}
/// Create a subspace
public static var spacesCreateSubspaceTitle: String {
return VectorL10n.tr("Vector", "spaces_create_subspace_title")
}
/// As this space is just for you, no one will be informed. You can add more later.
public static var spacesCreationAddRoomsMessage: String {
return VectorL10n.tr("Vector", "spaces_creation_add_rooms_message")
@ -8071,6 +8087,14 @@ public class VectorL10n: NSObject {
public static var spacesNoRoomFoundDetail: String {
return VectorL10n.tr("Vector", "spaces_no_room_found_detail")
}
/// The created space will be added to %@.
public static func spacesSubspaceCreationVisibilityMessage(_ p1: String) -> String {
return VectorL10n.tr("Vector", "spaces_subspace_creation_visibility_message", p1)
}
/// What type of subspace do you want to create?
public static var spacesSubspaceCreationVisibilityTitle: String {
return VectorL10n.tr("Vector", "spaces_subspace_creation_visibility_title")
}
/// Suggested
public static var spacesSuggestedRoom: String {
return VectorL10n.tr("Vector", "spaces_suggested_room")

View file

@ -146,8 +146,16 @@ class DarkTheme: NSObject, Theme {
navigationBar.standardAppearance = appearance
navigationBar.scrollEdgeAppearance = modernScrollEdgeAppearance || BuildSettings.newAppLayoutEnabled ? nil : appearance
} else {
if BuildSettings.newAppLayoutEnabled {
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = baseColor
appearance.shadowColor = nil
appearance.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: textPrimaryColor
]
}
navigationBar.scrollEdgeAppearance = modernScrollEdgeAppearance ? nil : appearance
} else {
navigationBar.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: textPrimaryColor
]

View file

@ -149,7 +149,15 @@ class DefaultTheme: NSObject, Theme {
]
navigationBar.standardAppearance = appearance
navigationBar.scrollEdgeAppearance = modernScrollEdgeAppearance || BuildSettings.newAppLayoutEnabled ? nil : appearance
if BuildSettings.newAppLayoutEnabled {
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = baseColor
appearance.shadowColor = nil
appearance.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: textPrimaryColor
]
}
navigationBar.scrollEdgeAppearance = modernScrollEdgeAppearance ? nil : appearance
} else {
navigationBar.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: textPrimaryColor

View file

@ -183,7 +183,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
[types addObject:@(RecentsDataSourceSectionTypeSecureBackupBanner)];
}
if (self.invitesCellDataArray.count > 0)
if (!BuildSettings.newAppLayoutEnabled && self.invitesCellDataArray.count > 0)
{
[types addObject:@(RecentsDataSourceSectionTypeInvites)];
}
@ -229,7 +229,12 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
[types addObject:@(RecentsDataSourceSectionTypeAllChats)];
}
if (self.suggestedRoomCellDataArray.count > 0)
if (self.currentSpace == nil && BuildSettings.newAppLayoutEnabled && self.invitesCellDataArray.count > 0)
{
[types addObject:@(RecentsDataSourceSectionTypeInvites)];
}
if (self.currentSpace != nil && self.suggestedRoomCellDataArray.count > 0)
{
[types addObject:@(RecentsDataSourceSectionTypeSuggestedRooms)];
}

View file

@ -1118,166 +1118,6 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
#pragma mark - Swipe actions
- (void)tableView:(UITableView*)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath
{
[self cancelEditionMode:isRefreshPending];
}
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{
return UITableViewCellEditingStyleNone;
}
- (nullable UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
{
MXRoom *room = [self.dataSource getRoomAtIndexPath:indexPath];
if (!room)
{
return nil;
}
// Display no action for the invited room
if (room.summary.membership == MXMembershipInvite)
{
return nil;
}
// Store the identifier of the room related to the edited cell.
editedRoomId = room.roomId;
UIColor *selectedColor = ThemeService.shared.theme.tintColor;
UIColor *unselectedColor = ThemeService.shared.theme.tabBarUnselectedItemTintColor;
UIColor *actionBackgroundColor = ThemeService.shared.theme.baseColor;
NSString* title = @" ";
// Direct chat toggle
BOOL isDirect = room.isDirect;
UIContextualAction *directChatAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive
title:title
handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
[self makeDirectEditedRoom:!isDirect];
completionHandler(YES);
}];
directChatAction.backgroundColor = actionBackgroundColor;
UIImage *directChatImage = AssetImages.roomActionDirectChat.image;
directChatImage = [directChatImage vc_tintedImageUsingColor:isDirect ? selectedColor : unselectedColor];
directChatAction.image = [directChatImage vc_notRenderedImage];
// Notification toggle
BOOL isMuted = room.isMute || room.isMentionsOnly;
UIContextualAction *muteAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive
title:title
handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
if ([BuildSettings showNotificationsV2])
{
[self changeEditedRoomNotificationSettings];
}
else
{
[self muteEditedRoomNotifications:!isMuted];
}
completionHandler(YES);
}];
muteAction.backgroundColor = actionBackgroundColor;
UIImage *notificationImage;
if([BuildSettings showNotificationsV2] && isMuted)
{
notificationImage = AssetImages.roomActionNotificationMuted.image;
}
else
{
notificationImage = AssetImages.roomActionNotification.image;
}
notificationImage = [notificationImage vc_tintedImageUsingColor:isMuted ? unselectedColor : selectedColor];
muteAction.image = [notificationImage vc_notRenderedImage];
// Favorites management
MXRoomTag* currentTag = nil;
// Get the room tag (use only the first one).
if (room.accountData.tags)
{
NSArray<MXRoomTag*>* tags = room.accountData.tags.allValues;
if (tags.count)
{
currentTag = tags[0];
}
}
BOOL isFavourite = (currentTag && [kMXRoomTagFavourite isEqualToString:currentTag.name]);
UIContextualAction *favouriteAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive
title:title
handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
NSString *favouriteTag = isFavourite ? nil : kMXRoomTagFavourite;
[self updateEditedRoomTag:favouriteTag];
completionHandler(YES);
}];
favouriteAction.backgroundColor = actionBackgroundColor;
UIImage *favouriteImage = AssetImages.roomActionFavourite.image;
favouriteImage = [favouriteImage vc_tintedImageUsingColor:isFavourite ? selectedColor : unselectedColor];
favouriteAction.image = [favouriteImage vc_notRenderedImage];
// Priority toggle
BOOL isInLowPriority = (currentTag && [kMXRoomTagLowPriority isEqualToString:currentTag.name]);
UIContextualAction *priorityAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive
title:title
handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
NSString *priorityTag = isInLowPriority ? nil : kMXRoomTagLowPriority;
[self updateEditedRoomTag:priorityTag];
completionHandler(YES);
}];
priorityAction.backgroundColor = actionBackgroundColor;
UIImage *priorityImage = isInLowPriority ? AssetImages.roomActionPriorityHigh.image : AssetImages.roomActionPriorityLow.image;
priorityImage = [priorityImage vc_tintedImageUsingColor:unselectedColor];
priorityAction.image = [priorityImage vc_notRenderedImage];
// Leave action
UIContextualAction *leaveAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive
title:title
handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
[self leaveEditedRoom];
completionHandler(YES);
}];
leaveAction.backgroundColor = actionBackgroundColor;
UIImage *leaveImage = AssetImages.roomActionLeave.image;
leaveImage = [leaveImage vc_tintedImageUsingColor:unselectedColor];
leaveAction.image = [leaveImage vc_notRenderedImage];
// Create swipe action configuration
NSArray<UIContextualAction*> *actions = @[
leaveAction,
priorityAction,
favouriteAction,
muteAction,
directChatAction
];
UISwipeActionsConfiguration *swipeActionConfiguration = [UISwipeActionsConfiguration configurationWithActions:actions];
swipeActionConfiguration.performsFirstActionWithFullSwipe = NO;
return swipeActionConfiguration;
}
- (void)leaveEditedRoom
{
if (editedRoomId)

View file

@ -35,7 +35,7 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
private var invitedRoomListDataFetcher: MXRoomListDataFetcher? {
switch mode {
case .home:
case .home, .allChats:
return invitedRoomListDataFetcherForHome
case .people:
return invitedRoomListDataFetcherForPeople
@ -548,7 +548,7 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
private func createCommonRoomListDataFetcher(withDataTypes dataTypes: MXRoomSummaryDataTypes = [],
onlySuggested: Bool = false,
onlyRecents: Bool = false,
onlyBreadcrumbs: Bool = false,
paginate: Bool = true,
strictMatches: Bool = false) -> MXRoomListDataFetcher {
guard let session = session else {
@ -556,14 +556,14 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
}
let filterOptions = MXRoomListDataFilterOptions(dataTypes: dataTypes,
onlySuggested: onlySuggested,
onlyBreadcrumbs: onlyRecents,
onlyBreadcrumbs: onlyBreadcrumbs,
query: query,
space: space,
showAllRoomsInHomeSpace: showAllRoomsInHomeSpace,
strictMatches: strictMatches)
let fetchOptions = MXRoomListDataFetchOptions(filterOptions: filterOptions,
sortOptions: onlyRecents ? noSortOptions : sortOptions,
sortOptions: onlyBreadcrumbs ? noSortOptions : sortOptions,
async: true)
let fetcher = session.roomListDataManager.fetcher(withOptions: fetchOptions)
if paginate {
@ -653,7 +653,7 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
lowPriorityRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.lowPriority])
serverNoticeRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.serverNotice])
suggestedRoomListDataFetcher = createCommonRoomListDataFetcher(onlySuggested: true)
breadcrumbsRoomListDataFetcher = createCommonRoomListDataFetcher(onlyRecents: true)
breadcrumbsRoomListDataFetcher = createCommonRoomListDataFetcher(onlyBreadcrumbs: true)
allChatsRoomListDataFetcher = createConversationRoomListDataFetcherForAllChats()
fetchersCreated = true

View file

@ -74,7 +74,7 @@ class AllChatsFilterOptionListView: UIView, Themable {
// MARK: - Themable
func update(theme: Theme) {
backgroundColor = theme.colors.background.withAlphaComponent(0.3)
backgroundColor = theme.colors.background.withAlphaComponent(0.7)
tabListView.itemFont = theme.fonts.callout
tabListView.tintColor = theme.colors.accent

View file

@ -32,8 +32,8 @@ class AllChatsActionProvider {
self.recentsAction,
self.filtersAction,
UIMenu(title: "", options: .displayInline, children: [
alphabeticalOrderAction,
activityOrderAction
activityOrderAction,
alphabeticalOrderAction
])
])
}

View file

@ -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)
}
}
}

View file

@ -25,12 +25,6 @@ class AllChatsViewController: HomeViewController {
return UINib(nibName: String(describing: self), bundle: Bundle(for: self.classForCoder()))
}
// MARK: - Private
@IBOutlet private weak var toolbar: UIToolbar!
private let searchController = UISearchController(searchResultsController: nil)
static override func instantiate() -> Self {
let storyboard = UIStoryboard(name: "Main", bundle: .main)
guard let viewController = storyboard.instantiateViewController(withIdentifier: "AllChatsViewController") as? Self else {
@ -39,30 +33,52 @@ class AllChatsViewController: HomeViewController {
return viewController
}
// MARK: - Private
private let searchController = UISearchController(searchResultsController: nil)
private let editActionProvider = AllChatsEditActionProvider()
private var spaceSelectorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter?
private var childCoordinators: [Coordinator] = []
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
editActionProvider.delegate = self
recentsTableView.tag = RecentsDataSourceMode.allChats.rawValue
recentsTableView.clipsToBounds = false
self.tabBarController?.title = VectorL10n.allChatsTitle
updateUI()
vc_setLargeTitleDisplayMode(.automatic)
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchResultsUpdater = self
self.setupEditOptions()
NotificationCenter.default.addObserver(self, selector: #selector(self.setupEditOptions), name: AllChatsLayoutSettingsManager.didUpdateSettings, object: nil)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
toolbar.tintColor = ThemeService.shared().theme.colors.accent
self.navigationController?.isToolbarHidden = false
self.navigationController?.toolbar.tintColor = ThemeService.shared().theme.colors.accent
if self.tabBarController?.navigationItem.searchController == nil {
self.tabBarController?.navigationItem.searchController = searchController
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.navigationController?.isToolbarHidden = true
}
// MARK: - HomeViewController
override var recentsDataSourceMode: RecentsDataSourceMode {
@ -70,7 +86,7 @@ class AllChatsViewController: HomeViewController {
}
@objc private func addFabButton() {
self.setupEditOptions()
// Nothing to do. We don't need FAB
}
@objc private func sections() -> Array<Int> {
@ -87,39 +103,227 @@ class AllChatsViewController: HomeViewController {
]
}
// MARK: - Actions
@objc private func showSpaceSelectorAction(sender: AnyObject) {
let currentSpaceId = self.dataSource.currentSpace?.spaceId ?? SpaceSelectorConstants.homeSpaceId
let spaceSelectorBridgePresenter = SpaceSelectorBottomSheetCoordinatorBridgePresenter(session: self.mainSession, selectedSpaceId: currentSpaceId, showHomeSpace: true)
spaceSelectorBridgePresenter.present(from: self, animated: true)
spaceSelectorBridgePresenter.delegate = self
self.spaceSelectorBridgePresenter = spaceSelectorBridgePresenter
}
// MARK: - Toolbar animation
private var lastScrollPosition: Double = 0
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
lastScrollPosition = self.recentsTableView.contentOffset.y
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
super.scrollViewDidScroll(scrollView)
if self.recentsTableView.contentOffset.y == 0 {
self.navigationController?.setToolbarHidden(false, animated: true)
}
guard self.recentsTableView.isDragging else {
return
}
let scrollPosition = max(self.recentsTableView.contentOffset.y, 0)
guard scrollPosition < self.recentsTableView.contentSize.height - self.recentsTableView.bounds.height else {
return
}
self.navigationController?.setToolbarHidden(scrollPosition - lastScrollPosition > 0, animated: true)
lastScrollPosition = scrollPosition
}
// MARK: - Private
@objc private func setupEditOptions() {
// Note: updating toolbar items doesn't work as expected and has weird behaviour
// Also this piece of code is going to be updated in the next PR
let editMenu = UIMenu(children: [
UIAction(title: VectorL10n.roomRecentsJoinRoom,
image: Asset.Images.homeFabJoinRoom.image,
discoverabilityTitle: VectorL10n.roomRecentsJoinRoom,
handler: { [weak self] action in
self?.joinARoom()
}),
UIAction(title: VectorL10n.roomRecentsCreateEmptyRoom,
image: Asset.Images.homeFabCreateRoom.image,
discoverabilityTitle: VectorL10n.roomRecentsCreateEmptyRoom,
handler: { [weak self] action in
self?.createNewRoom()
}),
UIAction(title: VectorL10n.roomRecentsStartChatWith,
image: Asset.Images.sideMenuActionIconFeedback.image,
discoverabilityTitle: VectorL10n.roomRecentsStartChatWith,
handler: { [weak self] action in
self?.startChat()
})
])
toolbar.items = [
UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), menu: AllChatsActionProvider().menu),
UIBarButtonItem.flexibleSpace(),
UIBarButtonItem(image: UIImage(systemName: "square.and.pencil"), menu: editMenu)
]
self.tabBarController?.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "line.3.horizontal.decrease.circle"), menu: AllChatsActionProvider().menu)
}
private func updateUI() {
let currentSpace = self.dataSource?.currentSpace
self.tabBarController?.title = currentSpace?.summary?.displayname ?? VectorL10n.allChatsTitle
updateToolbar(with: editActionProvider.updateMenu(with: mainSession, parentSpace: currentSpace, completion: { [weak self] menu in
self?.updateToolbar(with: menu)
}))
}
private func updateToolbar(with menu: UIMenu) {
let currentSpace = self.dataSource?.currentSpace
self.navigationController?.isToolbarHidden = false
self.tabBarController?.setToolbarItems([
UIBarButtonItem(image: Asset.Images.homeMySpacesAction.image, style: .done, target: self, action: #selector(self.showSpaceSelectorAction(sender: ))),
UIBarButtonItem.flexibleSpace(),
UIBarButtonItem(image: UIImage(systemName: currentSpace == nil ? "square.and.pencil" : "ellipsis.circle"), menu: menu)
], animated: true)
}
private func showCreateSpace(parentSpaceId: String?) {
let coordinator = SpaceCreationCoordinator(parameters: SpaceCreationCoordinatorParameters(session: self.mainSession, parentSpaceId: parentSpaceId))
let presentable = coordinator.toPresentable()
self.present(presentable, animated: true, completion: nil)
coordinator.callback = { [weak self] result in
guard let self = self else {
return
}
coordinator.toPresentable().dismiss(animated: true) {
self.remove(childCoordinator: coordinator)
switch result {
case .cancel:
break
case .done(let spaceId):
self.switchSpace(withId: spaceId)
}
}
}
add(childCoordinator: coordinator)
coordinator.start()
}
private func switchSpace(withId spaceId: String?) {
searchController.isActive = false
guard let spaceId = spaceId else {
self.dataSource.currentSpace = nil
updateUI()
return
}
guard let space = self.mainSession.spaceService.getSpace(withId: spaceId) else {
MXLog.warning("[AllChatsViewController] switchSpace: no space found with id \(spaceId)")
return
}
self.dataSource.currentSpace = space
updateUI()
self.recentsTableView.setContentOffset(.zero, animated: true)
}
private func add(childCoordinator: Coordinator) {
self.childCoordinators.append(childCoordinator)
}
private func remove(childCoordinator: Coordinator) {
self.childCoordinators.append(childCoordinator)
}
private func showSpaceInvite() {
guard let session = mainSession, let spaceRoom = dataSource.currentSpace?.room else {
return
}
let coordinator = ContactsPickerCoordinator(session: session, room: spaceRoom, initialSearchText: nil, actualParticipants: nil, invitedParticipants: nil, userParticipant: nil)
coordinator.delegate = self
coordinator.start()
add(childCoordinator: coordinator)
present(coordinator.toPresentable(), animated: true)
}
private func showSpaceMembers() {
guard let session = mainSession, let spaceId = dataSource.currentSpace?.spaceId else {
return
}
let coordinator = SpaceMembersCoordinator(parameters: SpaceMembersCoordinatorParameters(userSessionsService: UserSessionsService.shared, session: session, spaceId: spaceId))
coordinator.delegate = self
let presentable = coordinator.toPresentable()
presentable.presentationController?.delegate = self
coordinator.start()
add(childCoordinator: coordinator)
present(presentable, animated: true, completion: nil)
}
private func showSpaceSettings() {
guard let session = mainSession, let spaceId = dataSource.currentSpace?.spaceId else {
return
}
let coordinator = SpaceSettingsModalCoordinator(parameters: SpaceSettingsModalCoordinatorParameters(session: session, spaceId: spaceId, parentSpaceId: nil))
coordinator.callback = { [weak self] result in
guard let self = self else { return }
coordinator.toPresentable().dismiss(animated: true) {
self.remove(childCoordinator: coordinator)
}
}
let presentable = coordinator.toPresentable()
presentable.presentationController?.delegate = self
present(presentable, animated: true, completion: nil)
coordinator.start()
add(childCoordinator: coordinator)
}
private func showLeaveSpace() {
guard let session = mainSession, let spaceSummary = dataSource.currentSpace?.summary else {
return
}
let name = spaceSummary.displayname ?? VectorL10n.spaceTag
let selectionHeader = MatrixItemChooserSelectionHeader(title: VectorL10n.leaveSpaceSelectionTitle,
selectAllTitle: VectorL10n.leaveSpaceSelectionAllRooms,
selectNoneTitle: VectorL10n.leaveSpaceSelectionNoRooms)
let paramaters = MatrixItemChooserCoordinatorParameters(session: session,
title: VectorL10n.leaveSpaceTitle(name),
detail: VectorL10n.leaveSpaceMessage(name),
selectionHeader: selectionHeader,
viewProvider: LeaveSpaceViewProvider(navTitle: nil),
itemsProcessor: LeaveSpaceItemsProcessor(spaceId: spaceSummary.roomId, session: session))
let coordinator = MatrixItemChooserCoordinator(parameters: paramaters)
coordinator.toPresentable().presentationController?.delegate = self
coordinator.start()
add(childCoordinator: coordinator)
coordinator.completion = { [weak self] result in
coordinator.toPresentable().dismiss(animated: true) {
self?.remove(childCoordinator: coordinator)
}
}
present(coordinator.toPresentable(), animated: true)
}
}
// MARK: - SpaceSelectorBottomSheetCoordinatorBridgePresenterDelegate
extension AllChatsViewController: SpaceSelectorBottomSheetCoordinatorBridgePresenterDelegate {
func spaceSelectorBottomSheetCoordinatorBridgePresenterDidCancel(_ coordinatorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter) {
self.spaceSelectorBridgePresenter = nil
}
func spaceSelectorBottomSheetCoordinatorBridgePresenterDidSelectHome(_ coordinatorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter) {
coordinatorBridgePresenter.dismiss(animated: true) {
self.spaceSelectorBridgePresenter = nil
}
switchSpace(withId: nil)
}
func spaceSelectorBottomSheetCoordinatorBridgePresenter(_ coordinatorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter, didSelectSpaceWithId spaceId: String) {
coordinatorBridgePresenter.dismiss(animated: true) {
self.spaceSelectorBridgePresenter = nil
}
switchSpace(withId: spaceId)
}
func spaceSelectorBottomSheetCoordinatorBridgePresenter(_ coordinatorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter, didCreateSpaceWithinSpaceWithId parentSpaceId: String?) {
coordinatorBridgePresenter.dismiss(animated: true) {
self.spaceSelectorBridgePresenter = nil
}
self.showCreateSpace(parentSpaceId: parentSpaceId)
}
}
// MARK: - UISearchResultsUpdating
@ -135,3 +339,67 @@ extension AllChatsViewController: UISearchResultsUpdating {
}
}
// MARK: - UIAdaptivePresentationControllerDelegate
extension AllChatsViewController: UIAdaptivePresentationControllerDelegate {
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
guard let coordinator = childCoordinators.last else {
return
}
remove(childCoordinator: coordinator)
}
}
// MARK: - AllChatsEditActionProviderDelegate
extension AllChatsViewController: AllChatsEditActionProviderDelegate {
func allChatsEditActionProvider(_ actionProvider: AllChatsEditActionProvider, didSelect option: AllChatsEditActionProviderOption) {
switch option {
case .exploreRooms:
joinARoom()
case .createRoom:
createNewRoom()
case .startChat:
startChat()
case .invitePeople:
showSpaceInvite()
case .spaceMembers:
showSpaceMembers()
case .spaceSettings:
showSpaceSettings()
case .leaveSpace:
showLeaveSpace()
case .createSpace:
showCreateSpace(parentSpaceId: dataSource.currentSpace?.spaceId)
}
}
}
// MARK: - ContactsPickerCoordinatorDelegate
extension AllChatsViewController: ContactsPickerCoordinatorDelegate {
func contactsPickerCoordinatorDidStartLoading(_ coordinator: ContactsPickerCoordinatorProtocol) {
}
func contactsPickerCoordinatorDidEndLoading(_ coordinator: ContactsPickerCoordinatorProtocol) {
}
func contactsPickerCoordinatorDidClose(_ coordinator: ContactsPickerCoordinatorProtocol) {
remove(childCoordinator: coordinator)
}
}
// MARK: - SpaceMembersCoordinatorDelegate
extension AllChatsViewController: SpaceMembersCoordinatorDelegate {
func spaceMembersCoordinatorDidCancel(_ coordinator: SpaceMembersCoordinatorType) {
coordinator.toPresentable().dismiss(animated: true) {
self.remove(childCoordinator: coordinator)
}
}
}

View file

@ -8,14 +8,13 @@
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AllChatsViewController" customModule="Riot" customModuleProvider="target">
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AllChatsViewController" customModule="Element" customModuleProvider="target">
<connections>
<outlet property="recentsTableView" destination="orV-HH-88x" id="lgA-2k-pXJ"/>
<outlet property="stickyHeadersBottomContainer" destination="EXH-mK-0eB" id="95Y-KP-bwF"/>
<outlet property="stickyHeadersBottomContainerHeightConstraint" destination="SNq-Js-N7s" id="vom-iM-s6W"/>
<outlet property="stickyHeadersTopContainer" destination="JJC-Bw-6sa" id="JIy-sf-4Ya"/>
<outlet property="stickyHeadersTopContainerHeightConstraint" destination="xT1-rL-nCC" id="VaK-0W-2Mi"/>
<outlet property="toolbar" destination="7eM-ZH-m1e" id="s2M-8i-KI8"/>
<outlet property="view" destination="iN0-l3-epB" id="NUQ-LI-M61"/>
</connections>
</placeholder>
@ -25,7 +24,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="onDrag" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" translatesAutoresizingMaskIntoConstraints="NO" id="orV-HH-88x">
<rect key="frame" x="0.0" y="0.0" width="375" height="623"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</tableView>
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="JJC-Bw-6sa">
@ -44,37 +43,22 @@
<constraint firstAttribute="height" id="SNq-Js-N7s"/>
</constraints>
</view>
<toolbar opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7eM-ZH-m1e">
<rect key="frame" x="0.0" y="623" width="375" height="44"/>
<items>
<barButtonItem image="ellipsis.circle" catalog="system" changesSelectionAsPrimaryAction="YES" id="Vkf-Sh-ckk"/>
<barButtonItem style="plain" systemItem="flexibleSpace" id="7kJ-Np-DjU"/>
<barButtonItem image="square.and.pencil" catalog="system" changesSelectionAsPrimaryAction="YES" id="Iam-Gs-cJP"/>
</items>
</toolbar>
</subviews>
<viewLayoutGuide key="safeArea" id="4qf-KS-Fc9"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="orV-HH-88x" secondAttribute="trailing" id="3Np-64-AUe"/>
<constraint firstItem="7eM-ZH-m1e" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="Dfw-Ep-vIx"/>
<constraint firstItem="4qf-KS-Fc9" firstAttribute="bottom" secondItem="orV-HH-88x" secondAttribute="bottom" id="Bka-Zz-CEr"/>
<constraint firstItem="orV-HH-88x" firstAttribute="top" secondItem="JJC-Bw-6sa" secondAttribute="bottom" id="IMR-dV-gUS"/>
<constraint firstItem="EXH-mK-0eB" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="bottom" id="Kmg-aC-GOO"/>
<constraint firstItem="7eM-ZH-m1e" firstAttribute="trailing" secondItem="iN0-l3-epB" secondAttribute="trailing" id="NG9-KI-4ZF"/>
<constraint firstItem="JJC-Bw-6sa" firstAttribute="top" secondItem="4qf-KS-Fc9" secondAttribute="top" id="OBu-sH-mqE"/>
<constraint firstItem="4qf-KS-Fc9" firstAttribute="bottom" secondItem="7eM-ZH-m1e" secondAttribute="bottom" id="Ord-4E-Pvk"/>
<constraint firstItem="EXH-mK-0eB" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="S3i-DW-PUB"/>
<constraint firstAttribute="trailing" secondItem="EXH-mK-0eB" secondAttribute="trailing" id="bPP-yu-FTa"/>
<constraint firstItem="7eM-ZH-m1e" firstAttribute="top" secondItem="orV-HH-88x" secondAttribute="bottom" id="clZ-LH-5Cr"/>
<constraint firstItem="orV-HH-88x" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="dTn-zC-Axs"/>
<constraint firstItem="JJC-Bw-6sa" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="l5C-zt-Env"/>
<constraint firstAttribute="trailing" secondItem="JJC-Bw-6sa" secondAttribute="trailing" id="trw-3x-yjO"/>
</constraints>
<point key="canvasLocation" x="140" y="138"/>
<point key="canvasLocation" x="140" y="137.18140929535232"/>
</view>
</objects>
<resources>
<image name="ellipsis.circle" catalog="system" width="128" height="121"/>
<image name="square.and.pencil" catalog="system" width="128" height="115"/>
</resources>
</document>

View file

@ -265,7 +265,7 @@ final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType {
return
}
let coordinator = SpaceCreationCoordinator(parameters: SpaceCreationCoordinatorParameters(session: session))
let coordinator = SpaceCreationCoordinator(parameters: SpaceCreationCoordinatorParameters(session: session, parentSpaceId: nil))
let presentable = coordinator.toPresentable()
presentable.presentationController?.delegate = self
self.sideMenuViewController.present(presentable, animated: true, completion: nil)

View file

@ -737,6 +737,11 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
private weak var rightMenuAvatarView: AvatarView?
private func createLeftButtonItem(for viewController: UIViewController) {
guard !BuildSettings.newAppLayoutEnabled else {
createAvatarButtonItem(for: viewController)
return
}
guard BuildSettings.enableSideMenu else {
let settingsBarButtonItem: MXKBarButtonItem = MXKBarButtonItem(image: Asset.Images.settingsIcon.image, style: .plain) { [weak self] in
self?.showSettings()
@ -756,23 +761,21 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
}
private func createRightButtonItem(for viewController: UIViewController) {
guard BuildSettings.newAppLayoutEnabled else {
let searchBarButtonItem: MXKBarButtonItem = MXKBarButtonItem(image: Asset.Images.searchIcon.image, style: .plain) { [weak self] in
self?.showUnifiedSearch()
}
searchBarButtonItem.accessibilityLabel = VectorL10n.searchDefaultPlaceholder
viewController.navigationItem.rightBarButtonItem = searchBarButtonItem
guard !BuildSettings.newAppLayoutEnabled else {
return
}
createAvatarButtonItem(for: viewController)
let searchBarButtonItem: MXKBarButtonItem = MXKBarButtonItem(image: Asset.Images.searchIcon.image, style: .plain) { [weak self] in
self?.showUnifiedSearch()
}
searchBarButtonItem.accessibilityLabel = VectorL10n.searchDefaultPlaceholder
viewController.navigationItem.rightBarButtonItem = searchBarButtonItem
}
private func createAvatarButtonItem(for viewController: UIViewController) {
var actions: [UIMenuElement] = []
actions.append(UIAction(title: VectorL10n.settings, image: UIImage(systemName: "gearshape")) { [weak self] action in
actions.append(UIAction(title: VectorL10n.allChatsUserMenuSettings, image: UIImage(systemName: "gearshape")) { [weak self] action in
self?.showSettings()
})
@ -788,11 +791,6 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
})
actions.append(UIMenu(title: "", options: .displayInline, children: subMenuActions))
actions.append(UIAction(title: VectorL10n.roomAccessibilitySearch, image: UIImage(systemName: "magnifyingglass")) { [weak self] action in
self?.showUnifiedSearch()
})
actions.append(UIMenu(title: "", options: .displayInline, children: [
UIAction(title: VectorL10n.settingsSignOut, image: UIImage(systemName: "rectangle.portrait.and.arrow.right.fill"), attributes: .destructive) { [weak self] action in
self?.signOut()
@ -822,7 +820,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
avatarView.fill(with: avatar)
}
viewController.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: view)
viewController.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: view)
}
private func updateAvatarButtonItem() {
@ -924,6 +922,11 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
private var windowOverlay: WindowOverlayPresenter?
func showCoachMessageIfNeeded(with session: MXSession) {
guard !BuildSettings.newAppLayoutEnabled else {
// Showing coach message makes no sense with the new App Layout
return
}
if !RiotSettings.shared.slideMenuRoomsCoachMessageHasBeenDisplayed {
let isAuthenticated = MXKAccountManager.shared().activeAccounts.first != nil || MXKAccountManager.shared().accounts.first?.isSoftLogout == false

View file

@ -56,7 +56,8 @@ enum MockAppScreens {
MockTemplateSimpleScreenScreenState.self,
MockTemplateUserProfileScreenState.self,
MockTemplateRoomListScreenState.self,
MockTemplateRoomChatScreenState.self
MockTemplateRoomChatScreenState.self,
MockSpaceSelectorScreenState.self
]
}

View file

@ -46,14 +46,24 @@ final class SpaceCreationCoordinator: Coordinator {
// MARK: - Setup
init(parameters: SpaceCreationCoordinatorParameters) {
let title: String
let message: String
if let parentSpaceId = parameters.parentSpaceId, let parentSpaceName = parameters.session.spaceService.getSpace(withId: parentSpaceId)?.summary?.displayname {
title = VectorL10n.spacesSubspaceCreationVisibilityTitle
message = VectorL10n.spacesSubspaceCreationVisibilityMessage(parentSpaceName)
} else {
title = VectorL10n.spacesCreationVisibilityTitle
message = VectorL10n.spacesCreationVisibilityMessage
}
self.parameters = parameters
self.spaceVisibilityMenuParameters = SpaceCreationMenuCoordinatorParameters(
session: parameters.session,
creationParams: parameters.creationParameters,
navTitle: VectorL10n.spacesCreateSpaceTitle,
showBackButton: false,
title: VectorL10n.spacesCreationVisibilityTitle,
detail: VectorL10n.spacesCreationVisibilityMessage,
title: title,
detail: message,
options: [
SpaceCreationMenuRoomOption(id: .publicSpace, icon: Asset.Images.spaceCreationPublic.image, title: VectorL10n.public, detail: VectorL10n.spacePublicJoinRuleDetail),
SpaceCreationMenuRoomOption(id: .privateSpace, icon: Asset.Images.spaceCreationPrivate.image, title: VectorL10n.private, detail: VectorL10n.spacePrivateJoinRuleDetail)
@ -245,7 +255,7 @@ final class SpaceCreationCoordinator: Coordinator {
}
private func createPostProcessCoordinator() -> SpaceCreationPostProcessCoordinator {
let coordinator = SpaceCreationPostProcessCoordinator(parameters: SpaceCreationPostProcessCoordinatorParameters(session: parameters.session, creationParams: parameters.creationParameters))
let coordinator = SpaceCreationPostProcessCoordinator(parameters: SpaceCreationPostProcessCoordinatorParameters(session: parameters.session, parentSpaceId: parameters.parentSpaceId, creationParams: parameters.creationParameters))
coordinator.callback = { [weak self] result in
guard let self = self else { return }
switch result {

View file

@ -26,6 +26,9 @@ struct SpaceCreationCoordinatorParameters {
/// The Matrix session
let session: MXSession
/// The identifier of the parent space. `nil` for creating a root space
let parentSpaceId: String?
/// Parameters needed to create the new space
let creationParameters: SpaceCreationParameters = SpaceCreationParameters()
@ -33,8 +36,10 @@ struct SpaceCreationCoordinatorParameters {
let navigationRouter: NavigationRouterType
init(session: MXSession,
parentSpaceId: String?,
navigationRouter: NavigationRouterType? = nil) {
self.session = session
self.parentSpaceId = parentSpaceId
self.navigationRouter = navigationRouter ?? NavigationRouter(navigationController: RiotNavigationController())
}
}

View file

@ -40,7 +40,7 @@ final class SpaceCreationPostProcessCoordinator: Coordinator, Presentable {
init(parameters: SpaceCreationPostProcessCoordinatorParameters) {
self.parameters = parameters
let viewModel = SpaceCreationPostProcessViewModel.makeSpaceCreationPostProcessViewModel(spaceCreationPostProcessService: SpaceCreationPostProcessService(session: parameters.session, creationParams: parameters.creationParams))
let viewModel = SpaceCreationPostProcessViewModel.makeSpaceCreationPostProcessViewModel(spaceCreationPostProcessService: SpaceCreationPostProcessService(session: parameters.session, parentSpaceId: parameters.parentSpaceId, creationParams: parameters.creationParams))
let view = SpaceCreationPostProcess(viewModel: viewModel.context)
.addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
spaceCreationPostProcessViewModel = viewModel

View file

@ -20,5 +20,6 @@ import Foundation
struct SpaceCreationPostProcessCoordinatorParameters {
let session: MXSession
let parentSpaceId: String?
let creationParams: SpaceCreationParameters
}

View file

@ -27,6 +27,7 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol {
// MARK: Private
private let session: MXSession
private let parentSpaceId: String?
private let creationParams: SpaceCreationParameters
private var tasks: [SpaceCreationPostProcessTask] = []
@ -66,8 +67,9 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol {
// MARK: - Setup
init(session: MXSession, creationParams: SpaceCreationParameters) {
init(session: MXSession, parentSpaceId: String?, creationParams: SpaceCreationParameters) {
self.session = session
self.parentSpaceId = parentSpaceId
self.creationParams = creationParams
self.tasks = Self.tasks(with: creationParams)
self.tasksSubject = CurrentValueSubject(tasks)
@ -168,13 +170,25 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol {
let userIdInvites = creationParams.inviteType == .userId ? creationParams.userIdInvites : []
session.spaceService.createSpace(withName: creationParams.name, topic: creationParams.topic, isPublic: creationParams.isPublic, aliasLocalPart: alias, inviteArray: userIdInvites) { [weak self] response in
guard let self = self else { return }
if response.isFailure {
self.updateCurrentTask(with: .failure)
} else {
self.creationParams.isModified = false
self.createdSpace = response.value
self.updateCurrentTask(with: .success)
self.runNextTask()
guard let createdSpaceId = self.createdSpace?.spaceId, let parentSpaceId = self.parentSpaceId, let parentSpace = self.session.spaceService.getSpace(withId: parentSpaceId) else {
self.updateCurrentTask(with: .success)
self.runNextTask()
return
}
parentSpace.addChild(roomId: createdSpaceId) { [weak self] response in
guard let self = self else { return }
self.updateCurrentTask(with: .success)
self.runNextTask()
}
}
}
}

View file

@ -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)
}
}

View file

@ -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?()
}
}
}

View file

@ -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
}
}

View file

@ -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))
)
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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 }
}

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -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 }
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}

View file

@ -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()
}
}

View file

@ -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
View file

@ -0,0 +1 @@
App Layout: Implemented the new Space selector bottom sheet