Merge pull request #4897 from vector-im/spaces

Spaces first RC
This commit is contained in:
Gil Eluard 2021-09-24 11:48:39 +02:00 committed by GitHub
commit dc8bcbf567
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
198 changed files with 8402 additions and 292 deletions

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "side_menu_notif_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "side_menu_notif_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "side_menu_notif_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "space_home_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "space_home_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "space_home_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 B

View file

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "space_menu_close.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "space_menu_close@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "space_menu_close@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 B

View file

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "space_menu_leave.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "space_menu_leave@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "space_menu_leave@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

View file

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "space_menu_members.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "space_menu_members@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "space_menu_members@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "space_menu_rooms.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "space_menu_rooms@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "space_menu_rooms@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "space_room_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "space_room_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "space_room_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 B

View file

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "space_type_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "space_type_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "space_type_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "space_user_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "space_user_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "space_user_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "spaces_more.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "spaces_more@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "spaces_more@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

View file

@ -63,6 +63,7 @@
"switch" = "Switch";
"more" = "More";
"less" = "Less";
"open" = "Open";
"done" = "Done";
// Call Bar
@ -190,6 +191,7 @@
"room_recents_low_priority_section" = "LOW PRIORITY";
"room_recents_server_notice_section" = "SYSTEM ALERTS";
"room_recents_invites_section" = "INVITES";
"room_recents_suggested_rooms_section" = "SUGGESTED ROOMS";
"room_recents_start_chat_with" = "Start chat";
"room_recents_create_empty_room" = "Create room";
"room_recents_join_room" = "Join room";
@ -1695,6 +1697,36 @@ Tap the + to start adding people.";
"space_beta_announce_subtitle" = "The new version of communities";
"space_beta_announce_information" = "Spaces are a new way to group rooms and people. Theyre not on iOS yet, but you can use them now on Web and Desktop.";
"spaces_home_space_title" = "Home";
"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?";
"leave_space_message_admin_warning" = "You are admin of this space, ensure that you have transferred admin right to another member before leaving.";
"leave_space_only_action" = "Don't leave any rooms";
"leave_space_and_all_rooms_action" = "Leave all rooms and spaces";
"spaces_explore_rooms" = "Explore rooms";
"spaces_suggested_room" = "Suggested";
"space_tag" = "space";
"spaces_empty_space_title" = "This space has no rooms (yet)";
"spaces_empty_space_detail" = "Some rooms may be hidden because theyre private and you need an invite.";
"spaces_no_result_found_title" = "No results found";
"spaces_no_room_found_detail" = "Some results may be hidden because theyre private and you need an invite to join them.";
"spaces_no_member_found_detail" = "Looking for someone not in %@? For now, you can invite them on web or desktop.";
"spaces_coming_soon_title" = "Coming soon";
"spaces_add_rooms_coming_soon_title" = "Adding rooms coming soon";
"spaces_invites_coming_soon_title" = "Invites coming soon";
"spaces_coming_soon_detail" = "This feature hasnt been implemented here, but its on the way. For now, you can do that with Element on your computer.";
"space_participants_action_remove" = "Remove from this space";
"space_participants_action_ban" = "Ban from this space";
"space_private_join_rule" = "Private space";
"space_public_join_rule" = "Public space";
// Mark: Avatar
"space_avatar_view_accessibility_label" = "avatar";
"space_avatar_view_accessibility_hint" = "Change space avatar";
// Mark: - User avatar view
"user_avatar_view_accessibility_label" = "avatar";

View file

@ -0,0 +1,32 @@
//
// 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
extension MXKImageView {
@objc func vc_setRoomAvatarImage(with url: String?, displayName: String, mediaManager: MXMediaManager) {
// Use the display name to prepare the default avatar image.
let avatarImage = AvatarGenerator.generateAvatar(forText: displayName)
if let avatarUrl = url {
self.enableInMemoryCache = true
self.setImageURI(avatarUrl, withType: nil, andImageOrientation: .up, toFitViewSize: self.frame.size, with: MXThumbnailingMethodCrop, previewImage: avatarImage, mediaManager: mediaManager)
} else {
self.image = avatarImage
}
self.contentMode = .scaleAspectFill
}
}

View file

@ -32,6 +32,8 @@ typedef NS_ENUM(NSUInteger, RoomEncryptionTrustLevel) {
*/
@interface MXRoomSummary (Riot)
@property(nonatomic, readonly) BOOL isJoined;
/**
Set the room avatar in the dedicated MXKImageView.
The riot style implies to use in order :

View file

@ -77,4 +77,9 @@
return roomEncryptionTrustLevel;
}
- (BOOL)isJoined
{
return self.membership == MXMembershipJoin || self.membershipTransitionState == MXMembershipTransitionStateJoined;
}
@end

View file

@ -32,6 +32,16 @@ extension UIView {
}
}
/// Add a subview matching the safe area of the parent view using autolayout
@objc func vc_addSubViewMatchingParentSafeArea(_ subView: UIView) {
self.addSubview(subView)
subView.translatesAutoresizingMaskIntoConstraints = false
subView.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor).isActive = true
subView.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor).isActive = true
subView.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor).isActive = true
subView.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor).isActive = true
}
@objc func vc_removeAllSubviews() {
for subView in self.subviews {
subView.removeFromSuperview()

View file

@ -184,8 +184,18 @@ internal enum Asset {
internal static let sideMenuActionIconSettings = ImageAsset(name: "side_menu_action_icon_settings")
internal static let sideMenuActionIconShare = ImageAsset(name: "side_menu_action_icon_share")
internal static let sideMenuIcon = ImageAsset(name: "side_menu_icon")
internal static let sideMenuNotifIcon = ImageAsset(name: "side_menu_notif_icon")
internal static let featureUnavaibleArtwork = ImageAsset(name: "feature_unavaible_artwork")
internal static let featureUnavaibleArtworkDark = ImageAsset(name: "feature_unavaible_artwork_dark")
internal static let spaceHomeIcon = ImageAsset(name: "space_home_icon")
internal static let spaceMenuClose = ImageAsset(name: "space_menu_close")
internal static let spaceMenuLeave = ImageAsset(name: "space_menu_leave")
internal static let spaceMenuMembers = ImageAsset(name: "space_menu_members")
internal static let spaceMenuRooms = ImageAsset(name: "space_menu_rooms")
internal static let spaceRoomIcon = ImageAsset(name: "space_room_icon")
internal static let spaceTypeIcon = ImageAsset(name: "space_type_icon")
internal static let spaceUserIcon = ImageAsset(name: "space_user_icon")
internal static let spacesMore = ImageAsset(name: "spaces_more")
internal static let tabFavourites = ImageAsset(name: "tab_favourites")
internal static let tabGroups = ImageAsset(name: "tab_groups")
internal static let tabHome = ImageAsset(name: "tab_home")

View file

@ -244,11 +244,36 @@ internal enum StoryboardScene {
internal static let initialScene = InitialSceneType<Riot.SimpleScreenTemplateViewController>(storyboard: SimpleScreenTemplateViewController.self)
}
internal enum SpaceChildRoomDetailViewController: StoryboardType {
internal static let storyboardName = "SpaceChildRoomDetailViewController"
internal static let initialScene = InitialSceneType<Riot.SpaceChildRoomDetailViewController>(storyboard: SpaceChildRoomDetailViewController.self)
}
internal enum SpaceDetailViewController: StoryboardType {
internal static let storyboardName = "SpaceDetailViewController"
internal static let initialScene = InitialSceneType<Riot.SpaceDetailViewController>(storyboard: SpaceDetailViewController.self)
}
internal enum SpaceExploreRoomViewController: StoryboardType {
internal static let storyboardName = "SpaceExploreRoomViewController"
internal static let initialScene = InitialSceneType<Riot.SpaceExploreRoomViewController>(storyboard: SpaceExploreRoomViewController.self)
}
internal enum SpaceFeatureUnaivableViewController: StoryboardType {
internal static let storyboardName = "SpaceFeatureUnaivableViewController"
internal static let initialScene = InitialSceneType<Riot.SpaceFeatureUnaivableViewController>(storyboard: SpaceFeatureUnaivableViewController.self)
}
internal enum SpaceListViewController: StoryboardType {
internal static let storyboardName = "SpaceListViewController"
internal static let initialScene = InitialSceneType<Riot.SpaceListViewController>(storyboard: SpaceListViewController.self)
}
internal enum SpaceMenuViewController: StoryboardType {
internal static let storyboardName = "SpaceMenuViewController"
internal static let initialScene = InitialSceneType<Riot.SpaceMenuViewController>(storyboard: SpaceMenuViewController.self)
}
internal enum TemplateScreenViewController: StoryboardType {
internal static let storyboardName = "TemplateScreenViewController"

View file

@ -2083,6 +2083,26 @@ public class VectorL10n: NSObject {
public static var leave: String {
return VectorL10n.tr("Vector", "leave")
}
/// Leave all rooms and spaces
public static var leaveSpaceAndAllRoomsAction: String {
return VectorL10n.tr("Vector", "leave_space_and_all_rooms_action")
}
/// Are you sure you want to leave %@? Do you also want to leave all rooms and spaces of this space?
public static func leaveSpaceMessage(_ p1: String) -> String {
return VectorL10n.tr("Vector", "leave_space_message", p1)
}
/// You are admin of this space, ensure that you have transferred admin right to another member before leaving.
public static var leaveSpaceMessageAdminWarning: String {
return VectorL10n.tr("Vector", "leave_space_message_admin_warning")
}
/// Don't leave any rooms
public static var leaveSpaceOnlyAction: String {
return VectorL10n.tr("Vector", "leave_space_only_action")
}
/// Leave %@
public static func leaveSpaceTitle(_ p1: String) -> String {
return VectorL10n.tr("Vector", "leave_space_title", p1)
}
/// Less
public static var less: String {
return VectorL10n.tr("Vector", "less")
@ -2191,6 +2211,10 @@ public class VectorL10n: NSObject {
public static var on: String {
return VectorL10n.tr("Vector", "on")
}
/// Open
public static var `open`: String {
return VectorL10n.tr("Vector", "open")
}
/// or
public static var or: String {
return VectorL10n.tr("Vector", "or")
@ -3419,6 +3443,10 @@ public class VectorL10n: NSObject {
public static var roomRecentsStartChatWith: String {
return VectorL10n.tr("Vector", "room_recents_start_chat_with")
}
/// SUGGESTED ROOMS
public static var roomRecentsSuggestedRoomsSection: String {
return VectorL10n.tr("Vector", "room_recents_suggested_rooms_section")
}
/// Can't find this room. Make sure it exists
public static var roomRecentsUnknownRoomErrorMessage: String {
return VectorL10n.tr("Vector", "room_recents_unknown_room_error_message")
@ -4791,6 +4819,14 @@ public class VectorL10n: NSObject {
public static var socialLoginListTitleSignUp: String {
return VectorL10n.tr("Vector", "social_login_list_title_sign_up")
}
/// Change space avatar
public static var spaceAvatarViewAccessibilityHint: String {
return VectorL10n.tr("Vector", "space_avatar_view_accessibility_hint")
}
/// avatar
public static var spaceAvatarViewAccessibilityLabel: String {
return VectorL10n.tr("Vector", "space_avatar_view_accessibility_label")
}
/// BETA
public static var spaceBetaAnnounceBadge: String {
return VectorL10n.tr("Vector", "space_beta_announce_badge")
@ -4819,6 +4855,78 @@ public class VectorL10n: NSObject {
public static var spaceFeatureUnavailableTitle: String {
return VectorL10n.tr("Vector", "space_feature_unavailable_title")
}
/// Ban from this space
public static var spaceParticipantsActionBan: String {
return VectorL10n.tr("Vector", "space_participants_action_ban")
}
/// Remove from this space
public static var spaceParticipantsActionRemove: String {
return VectorL10n.tr("Vector", "space_participants_action_remove")
}
/// Private space
public static var spacePrivateJoinRule: String {
return VectorL10n.tr("Vector", "space_private_join_rule")
}
/// Public space
public static var spacePublicJoinRule: String {
return VectorL10n.tr("Vector", "space_public_join_rule")
}
/// space
public static var spaceTag: String {
return VectorL10n.tr("Vector", "space_tag")
}
/// Adding rooms coming soon
public static var spacesAddRoomsComingSoonTitle: String {
return VectorL10n.tr("Vector", "spaces_add_rooms_coming_soon_title")
}
/// This feature hasnt been implemented here, but its on the way. For now, you can do that with Element on your computer.
public static var spacesComingSoonDetail: String {
return VectorL10n.tr("Vector", "spaces_coming_soon_detail")
}
/// Coming soon
public static var spacesComingSoonTitle: String {
return VectorL10n.tr("Vector", "spaces_coming_soon_title")
}
/// Some rooms may be hidden because theyre private and you need an invite.
public static var spacesEmptySpaceDetail: String {
return VectorL10n.tr("Vector", "spaces_empty_space_detail")
}
/// This space has no rooms (yet)
public static var spacesEmptySpaceTitle: String {
return VectorL10n.tr("Vector", "spaces_empty_space_title")
}
/// Explore rooms
public static var spacesExploreRooms: String {
return VectorL10n.tr("Vector", "spaces_explore_rooms")
}
/// Home
public static var spacesHomeSpaceTitle: String {
return VectorL10n.tr("Vector", "spaces_home_space_title")
}
/// Invites coming soon
public static var spacesInvitesComingSoonTitle: String {
return VectorL10n.tr("Vector", "spaces_invites_coming_soon_title")
}
/// Spaces
public static var spacesLeftPanelTitle: String {
return VectorL10n.tr("Vector", "spaces_left_panel_title")
}
/// Looking for someone not in %@? For now, you can invite them on web or desktop.
public static func spacesNoMemberFoundDetail(_ p1: String) -> String {
return VectorL10n.tr("Vector", "spaces_no_member_found_detail", p1)
}
/// No results found
public static var spacesNoResultFoundTitle: String {
return VectorL10n.tr("Vector", "spaces_no_result_found_title")
}
/// Some results may be hidden because theyre private and you need an invite to join them.
public static var spacesNoRoomFoundDetail: String {
return VectorL10n.tr("Vector", "spaces_no_room_found_detail")
}
/// Suggested
public static var spacesSuggestedRoom: String {
return VectorL10n.tr("Vector", "spaces_suggested_room")
}
/// Start
public static var start: String {
return VectorL10n.tr("Vector", "start")

View file

@ -34,6 +34,10 @@ extension UserSessionsService {
@objcMembers
class UserSessionsService: NSObject {
// MARK: - Singleton
static public let shared: UserSessionsService = UserSessionsService()
// MARK: - Properties
// MARK: Private

View file

@ -97,6 +97,14 @@
*/
- (instancetype)initWithPublicRoom:(MXPublicRoom*)publicRoom andSession:(MXSession*)mxSession;
/**
Contructors.
@param childInfo MXSpaceChildInfo instance that describes the child.
@param mxSession the session to open the room preview with.
*/
- (instancetype)initWithSpaceChildInfo:(MXSpaceChildInfo*)childInfo andSession:(MXSession*)mxSession;
/**
Attempt to peek into the room to get room data (state, messages history, etc).

View file

@ -16,6 +16,7 @@
*/
#import "RoomPreviewData.h"
#import <MatrixSDK-Swift.h>
@implementation RoomPreviewData
@ -79,6 +80,21 @@
return self;
}
- (instancetype)initWithSpaceChildInfo:(MXSpaceChildInfo*)childInfo andSession:(MXSession*)mxSession
{
self = [self init];
if (self)
{
_roomId = childInfo.childRoomId;
_roomName = childInfo.name;
_roomAvatarUrl = childInfo.avatarUrl;
_roomTopic = childInfo.topic;
_numJoinedMembers = childInfo.activeMemberCount;
_mxSession = mxSession;
}
return self;
}
- (void)dealloc
{
if (_roomDataSource)

View file

@ -55,6 +55,8 @@ final class AppCoordinator: NSObject, AppCoordinatorType {
private var mainMatrixSession: MXSession? {
return self.userSessionsService.mainUserSession?.matrixSession
}
private var currentSpaceId: String?
// MARK: Public
@ -65,7 +67,7 @@ final class AppCoordinator: NSObject, AppCoordinatorType {
init(router: RootRouterType, window: UIWindow) {
self.rootRouter = router
self.customSchemeURLParser = CustomSchemeURLParser()
self.userSessionsService = UserSessionsService()
self.userSessionsService = UserSessionsService.shared
super.init()
@ -148,7 +150,7 @@ final class AppCoordinator: NSObject, AppCoordinatorType {
private func addSideMenu() {
let appInfo = AppInfo.current
let coordinatorParameters = SideMenuCoordinatorParameters(userSessionsService: self.userSessionsService, appInfo: appInfo)
let coordinatorParameters = SideMenuCoordinatorParameters(appNavigator: self.appNavigator, userSessionsService: self.userSessionsService, appInfo: appInfo)
let coordinator = SideMenuCoordinator(parameters: coordinatorParameters)
coordinator.delegate = self
@ -187,6 +189,29 @@ final class AppCoordinator: NSObject, AppCoordinatorType {
FLEXManager.shared.showExplorer()
#endif
}
fileprivate func navigate(to destination: AppNavigatorDestination) {
switch destination {
case .homeSpace:
MXLog.verbose("Switch to home space")
self.navigateToSpace(with: nil)
case .space(let spaceId):
MXLog.verbose("Switch to space with id: \(spaceId)")
self.navigateToSpace(with: spaceId)
}
}
private func navigateToSpace(with spaceId: String?) {
guard spaceId != self.currentSpaceId else {
MXLog.verbose("Space with id: \(String(describing: spaceId)) is already selected")
return
}
self.currentSpaceId = spaceId
// Reload split view with selected space id
self.splitViewCoordinator?.start(with: spaceId)
}
}
// MARK: - LegacyAppDelegateDelegate
@ -218,6 +243,10 @@ extension AppCoordinator: LegacyAppDelegateDelegate {
func legacyAppDelegate(_ legacyAppDelegate: LegacyAppDelegate!, didRemove account: MXKAccount!) {
self.userSessionsService.removeUserSession(relatedToAccount: account)
}
func legacyAppDelegate(_ legacyAppDelegate: LegacyAppDelegate!, didNavigateToSpaceWithId spaceId: String!) {
self.sideMenuCoordinator?.select(spaceWithId: spaceId)
}
}
// MARK: - SplitViewCoordinatorDelegate
@ -239,6 +268,8 @@ extension AppCoordinator: SideMenuCoordinatorDelegate {
fileprivate class AppNavigator: AppNavigatorProtocol {
// swiftlint:enable private_over_fileprivate
// MARK: - Properties
private unowned let appCoordinator: AppCoordinator
let alert: AlertPresentable
@ -251,8 +282,16 @@ fileprivate class AppNavigator: AppNavigatorProtocol {
return SideMenuPresenter(sideMenuCoordinator: sideMenuCoordinator)
}()
// MARK: - Setup
init(appCoordinator: AppCoordinator) {
self.appCoordinator = appCoordinator
self.alert = AppAlertPresenter(legacyAppDelegate: appCoordinator.legacyAppDelegate)
}
// MARK: - Public
func navigate(to destination: AppNavigatorDestination) {
self.appCoordinator.navigate(to: destination)
}
}

View file

@ -18,9 +18,13 @@ import Foundation
/// AppNavigatorProtocol abstract a navigator at app level.
/// It enables to perform the navigation within the global app scope (open the side menu, open a room and so on)
/// Note: Use a destination enum like presented here https://www.swiftbysundell.com/articles/navigation-in-swift/#where-to-navigator or use simple methods like Element Android Navigator
/// Note: Presentation of the pattern here https://www.swiftbysundell.com/articles/navigation-in-swift/#where-to-navigator
protocol AppNavigatorProtocol {
var sideMenu: SideMenuPresentable { get }
var alert: AlertPresentable { get }
/// Navigate to a destination screen or a state
/// Do not use protocol with associatedtype for the moment like presented here https://www.swiftbysundell.com/articles/navigation-in-swift/#where-to-navigator use a separate enum
func navigate(to destination: AppNavigatorDestination)
}

View file

@ -0,0 +1,27 @@
//
// 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
/// Supported destinations used by AppNavigator to navigate in screen hierarchy
enum AppNavigatorDestination {
/// Show home space
case homeSpace
/// Show a space with specific id
case space(_ spaceId: String)
}

View file

@ -241,6 +241,29 @@ UINavigationControllerDelegate
*/
- (BOOL)handleUniversalLinkURL:(NSURL*)universalLinkURL;
/**
Extract params from the URL fragment part (after '#') of a vector.im Universal link:
The fragment can contain a '?'. So there are two kinds of parameters: path params and query params.
It is in the form of /[pathParam1]/[pathParam2]?[queryParam1Key]=[queryParam1Value]&[queryParam2Key]=[queryParam2Value]
@note this method should be private but is used by RoomViewController. This should be moved to a univresal link parser class
@param fragment the fragment to parse.
@param outPathParams the decoded path params.
@param outQueryParams the decoded query params. If there is no query params, it will be nil.
*/
- (void)parseUniversalLinkFragment:(NSString*)fragment outPathParams:(NSArray<NSString*> **)outPathParams outQueryParams:(NSMutableDictionary **)outQueryParams;
/**
Open the dedicated space with the given ID.
This method will open only joined or invited spaces.
@note this method is temporary and should be moved to a dedicated coordinator
@param spaceId ID of the space.
*/
- (void)openSpaceWithId:(NSString*)spaceId;
#pragma mark - App version management
/**
@ -271,4 +294,6 @@ UINavigationControllerDelegate
- (void)legacyAppDelegate:(LegacyAppDelegate*)legacyAppDelegate didRemoveAccount:(MXKAccount*)account;
- (void)legacyAppDelegate:(LegacyAppDelegate*)legacyAppDelegate didNavigateToSpaceWithId:(NSString*)spaceId;
@end

View file

@ -87,7 +87,7 @@ NSString *const AppDelegateDidValidateEmailNotificationClientSecretKey = @"AppDe
NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUniversalLinkDidChangeNotification";
@interface LegacyAppDelegate () <GDPRConsentViewControllerDelegate, KeyVerificationCoordinatorBridgePresenterDelegate, ServiceTermsModalCoordinatorBridgePresenterDelegate, PushNotificationServiceDelegate, SetPinCoordinatorBridgePresenterDelegate, CallPresenterDelegate>
@interface LegacyAppDelegate () <GDPRConsentViewControllerDelegate, KeyVerificationCoordinatorBridgePresenterDelegate, ServiceTermsModalCoordinatorBridgePresenterDelegate, PushNotificationServiceDelegate, SetPinCoordinatorBridgePresenterDelegate, CallPresenterDelegate, SpaceDetailPresenterDelegate>
{
/**
Reachability observer
@ -204,6 +204,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
@property (nonatomic, strong) ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter;
@property (nonatomic, strong) SlidingModalPresenter *slidingModalPresenter;
@property (nonatomic, strong) SetPinCoordinatorBridgePresenter *setPinCoordinatorBridgePresenter;
@property (nonatomic, strong) SpaceDetailPresenter *spaceDetailPresenter;
/**
Used to manage on boarding steps, like create DM with riot bot
@ -1344,8 +1345,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
if (room.summary.roomType == MXRoomTypeSpace)
{
// Indicates that spaces are not supported
[self.spaceFeatureUnavailablePresenter presentUnavailableFeatureFrom:self.presentedViewController animated:YES];
[self restoreInitialDisplay:^{
self.spaceDetailPresenter = [SpaceDetailPresenter new];
self.spaceDetailPresenter.delegate = self;
[self.spaceDetailPresenter presentForSpaceWithId:room.roomId from:self.masterNavigationController sourceView:nil session:account.mxSession animated:YES];
}];
}
else
{
@ -1361,9 +1365,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
// So, come back to the home VC and show its loading wheel while processing
[self restoreInitialDisplay:^{
if ([_masterTabBarController.selectedViewController isKindOfClass:MXKViewController.class])
if ([_masterTabBarController.selectedViewController isKindOfClass:MXKActivityHandlingViewController.class])
{
MXKViewController *homeViewController = (MXKViewController*)_masterTabBarController.selectedViewController;
MXKActivityHandlingViewController *homeViewController = (MXKActivityHandlingViewController*)_masterTabBarController.selectedViewController;
[homeViewController startActivityIndicator];
@ -1458,24 +1462,21 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
roomPreviewData.viaServers = queryParams[@"via"];
}
// Is it a link to an event of a room?
// If yes, the event will be displayed once the room is joined
roomPreviewData.eventId = (pathParams.count >= 3) ? pathParams[2] : nil;
// Try to get more information about the room before opening its preview
[roomPreviewData peekInRoom:^(BOOL succeeded) {
// Note: the activity indicator will not disappear if the session is not ready
[homeViewController stopActivityIndicator];
// If no data is available for this room, we name it with the known room alias (if any).
if (!succeeded && universalLinkFragmentPendingRoomAlias[roomIdOrAlias])
[account.mxSession.matrixRestClient roomSummaryWith:roomIdOrAlias via:roomPreviewData.viaServers success:^(MXPublicRoom *room) {
if ([room.roomTypeString isEqualToString:MXRoomTypeStringSpace])
{
roomPreviewData.roomName = universalLinkFragmentPendingRoomAlias[roomIdOrAlias];
[homeViewController stopActivityIndicator];
self.spaceDetailPresenter = [SpaceDetailPresenter new];
self.spaceDetailPresenter.delegate = self;
[self.spaceDetailPresenter presentForSpaceWithPublicRoom:room from:self.masterNavigationController sourceView:nil session:account.mxSession animated:YES];
}
universalLinkFragmentPendingRoomAlias = nil;
[self showRoomPreview:roomPreviewData];
else
{
[self peekInRoomWithId:roomIdOrAlias forPreviewData:roomPreviewData params:pathParams];
}
} failure:^(NSError *error) {
[self peekInRoomWithId:roomIdOrAlias forPreviewData:roomPreviewData params:pathParams];
}];
}
@ -1607,6 +1608,33 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
}
}
- (void)peekInRoomWithId:(NSString*)roomIdOrAlias forPreviewData:(RoomPreviewData *)roomPreviewData params:(NSArray<NSString*> *)pathParams
{
// Is it a link to an event of a room?
// If yes, the event will be displayed once the room is joined
roomPreviewData.eventId = (pathParams.count >= 3) ? pathParams[2] : nil;
MXWeakify(self);
// Try to get more information about the room before opening its preview
[roomPreviewData peekInRoom:^(BOOL succeeded) {
MXStrongifyAndReturnIfNil(self);
MXKViewController *homeViewController = (MXKViewController*)self.masterTabBarController.selectedViewController;
// Note: the activity indicator will not disappear if the session is not ready
[homeViewController stopActivityIndicator];
// If no data is available for this room, we name it with the known room alias (if any).
if (!succeeded && self->universalLinkFragmentPendingRoomAlias[roomIdOrAlias])
{
roomPreviewData.roomName = self->universalLinkFragmentPendingRoomAlias[roomIdOrAlias];
}
self->universalLinkFragmentPendingRoomAlias = nil;
[self showRoomPreview:roomPreviewData];
}];
}
/**
Extract params from the URL fragment part (after '#') of a vector.im Universal link:
@ -4382,4 +4410,50 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
[URLPreviewService.shared clearStore];
}
#pragma mark - Spaces
-(void)openSpaceWithId:(NSString *)spaceId
{
MXSession *session = mxSessionArray.firstObject;
if ([session.spaceService getSpaceWithId:spaceId]) {
[self restoreInitialDisplay:^{
[self.delegate legacyAppDelegate:self didNavigateToSpaceWithId:spaceId];
}];
}
else
{
MXWeakify(self);
__block __weak id observer = [[NSNotificationCenter defaultCenter] addObserverForName:MXSpaceService.didBuildSpaceGraph object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
MXStrongifyAndReturnIfNil(self);
[[NSNotificationCenter defaultCenter] removeObserver:observer];
if ([session.spaceService getSpaceWithId:spaceId]) {
[self restoreInitialDisplay:^{
[self.delegate legacyAppDelegate:self didNavigateToSpaceWithId:spaceId];
}];
}
}];
}
}
#pragma mark - SpaceDetailPresenterDelegate
- (void)spaceDetailPresenterDidComplete:(SpaceDetailPresenter *)presenter
{
self.spaceDetailPresenter = nil;
}
- (void)spaceDetailPresenter:(SpaceDetailPresenter *)presenter didOpenSpaceWithId:(NSString *)spaceId
{
self.spaceDetailPresenter = nil;
[self openSpaceWithId:spaceId];
}
- (void)spaceDetailPresenter:(SpaceDetailPresenter *)presenter didJoinSpaceWithId:(NSString *)spaceId
{
self.spaceDetailPresenter = nil;
[self openSpaceWithId:spaceId];
}
@end

View file

@ -32,7 +32,7 @@ class AvatarView: UIView, Themable {
// MARK: Private
private var theme: Theme?
private(set) var theme: Theme?
// MARK: Public
@ -106,7 +106,18 @@ class AvatarView: UIView, Themable {
return
}
let defaultavatarImage = AvatarGenerator.generateAvatar(forMatrixItem: viewData.matrixItemId, withDisplayName: viewData.displayName)
let defaultAvatarImage: UIImage?
var defaultAvatarImageContentMode: UIView.ContentMode = .scaleAspectFill
switch viewData.fallbackImage {
case .matrixItem(let matrixItemId, let matrixItemDisplayName):
defaultAvatarImage = AvatarGenerator.generateAvatar(forMatrixItem: matrixItemId, withDisplayName: matrixItemDisplayName)
case .image(let image, let contentMode):
defaultAvatarImage = image
defaultAvatarImageContentMode = contentMode ?? .scaleAspectFill
case .none:
defaultAvatarImage = nil
}
if let avatarUrl = viewData.avatarUrl {
avatarImageView.setImageURI(avatarUrl,
@ -114,13 +125,13 @@ class AvatarView: UIView, Themable {
andImageOrientation: .up,
toFitViewSize: avatarImageView.frame.size,
with: MXThumbnailingMethodScale,
previewImage: defaultavatarImage,
previewImage: defaultAvatarImage,
mediaManager: viewData.mediaManager)
avatarImageView.contentMode = .scaleAspectFill
} else {
avatarImageView.image = defaultavatarImage
avatarImageView.image = defaultAvatarImage
avatarImageView.contentMode = defaultAvatarImageContentMode
}
avatarImageView.contentMode = .scaleAspectFill
}
func updateView() {

View file

@ -0,0 +1,34 @@
//
// 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
struct AvatarViewData: AvatarViewDataProtocol {
/// Matrix item identifier (user id or room id)
var matrixItemId: String
/// Matrix item display name (user or room display name)
var displayName: String?
/// Matrix item avatar URL (user or room avatar url)
var avatarUrl: String?
/// Matrix media handler
var mediaManager: MXMediaManager
/// Fallback image used when avatarUrl is nil
var fallbackImage: AvatarFallbackImage?
}

View file

@ -14,7 +14,18 @@
// limitations under the License.
//
import Foundation
import UIKit
enum AvatarFallbackImage {
/// matrixItem represent a Matrix item like a room, space, user
/// matrixItemId: Matrix item identifier (user id or room id)
/// displayName: Matrix item display name (user or room display name)
case matrixItem(_ matrixItemId: String, _ displayName: String?)
/// Normal image with optional content mode
case image(_ image: UIImage, _ contentMode: UIView.ContentMode? = nil)
}
/// AvatarViewDataProtocol describe a view data that should be given to an AvatarView sublcass
protocol AvatarViewDataProtocol: AvatarProtocol {
@ -25,8 +36,11 @@ protocol AvatarViewDataProtocol: AvatarProtocol {
var displayName: String? { get }
/// Matrix item avatar URL (user or room avatar url)
var avatarUrl: String? { get }
var avatarUrl: String? { get }
/// Matrix media handler
var mediaManager: MXMediaManager { get }
/// Fallback image used when avatarUrl is nil
var fallbackImage: AvatarFallbackImage? { get }
}

View file

@ -18,6 +18,7 @@
#import "RecentCellData.h"
#import "MXRoom+Riot.h"
#import "MatrixSDK-Swift.h"
@implementation RecentCellData
// trick to hide the mother class property as it is readonly one.
@ -57,7 +58,7 @@
- (void)update
{
[super update];
roomDisplayname = self.roomSummary.displayname;
roomDisplayname = self.spaceChildInfo ? self.spaceChildInfo.name: self.roomSummary.displayname;
if (!roomDisplayname.length)
{
roomDisplayname = [NSBundle mxk_localizedStringForKey:@"room_displayname_empty_room"];

View file

@ -19,6 +19,8 @@
#import "PublicRoomsDirectoryDataSource.h"
@class MXSpace;
/**
List the different modes used to prepare the recents data source.
Each mode corresponds to an application tab: Home, Favourites, People and Rooms.
@ -72,6 +74,7 @@ extern NSString *const kRecentsDataSourceTapOnDirectoryServerChange;
@property (nonatomic) NSInteger conversationSection;
@property (nonatomic) NSInteger lowPrioritySection;
@property (nonatomic) NSInteger serverNoticeSection;
@property (nonatomic) NSInteger suggestedRoomsSection;
@property (nonatomic, readonly) NSArray* invitesCellDataArray;
@property (nonatomic, readonly) NSArray* favoriteCellDataArray;
@ -79,6 +82,7 @@ extern NSString *const kRecentsDataSourceTapOnDirectoryServerChange;
@property (nonatomic, readonly) NSArray* conversationCellDataArray;
@property (nonatomic, readonly) NSArray* lowPriorityCellDataArray;
@property (nonatomic, readonly) NSArray* serverNoticeCellDataArray;
@property (nonatomic, readonly) NSArray* suggestedRoomCellDataArray;
@property (nonatomic, readonly) SecureBackupBannerDisplay secureBackupBannerDisplay;
@property (nonatomic, readonly) CrossSigningBannerDisplay crossSigningBannerDisplay;

View file

@ -34,6 +34,7 @@
#define RECENTSDATASOURCE_SECTION_LOWPRIORITY 0x10
#define RECENTSDATASOURCE_SECTION_SERVERNOTICE 0x20
#define RECENTSDATASOURCE_SECTION_PEOPLE 0x40
#define RECENTSDATASOURCE_SECTION_SUGGESTED 0x80
#define RECENTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT 30.0
@ -60,7 +61,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
@end
@implementation RecentsDataSource
@synthesize directorySection, invitesSection, favoritesSection, peopleSection, conversationSection, lowPrioritySection, serverNoticeSection, secureBackupBannerSection, crossSigningBannerSection;
@synthesize directorySection, invitesSection, favoritesSection, peopleSection, conversationSection, lowPrioritySection, serverNoticeSection, suggestedRoomsSection, secureBackupBannerSection, crossSigningBannerSection;
@synthesize hiddenCellIndexPath, droppingCellIndexPath, droppingCellBackGroundView;
- (instancetype)init
@ -84,10 +85,17 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
// Set default data and view classes
[self registerCellDataClass:RecentCellData.class forCellIdentifier:kMXKRecentCellIdentifier];
[self registerSpaceServiceDidBuildGraphNotification];
}
return self;
}
- (void)dealloc
{
[self unregisterSpaceServiceDidBuildGraphNotification];
}
- (void)resetSectionIndexes
{
crossSigningBannerSection = -1;
@ -99,9 +107,9 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
conversationSection = -1;
lowPrioritySection = -1;
serverNoticeSection = -1;
suggestedRoomsSection = -1;
}
#pragma mark - Properties
- (NSArray *)invitesCellDataArray
@ -128,6 +136,10 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
{
return state.serverNoticeCellDataArray;
}
- (NSArray *)suggestedRoomCellDataArray
{
return state.suggestedRoomCellDataArray;
}
- (NSUInteger)missedFavouriteDiscussionsCount
{
@ -165,7 +177,6 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
return state.unsentMessagesGroupDiscussionsCount;
}
#pragma mark -
- (void)setDelegate:(id<MXKDataSourceDelegate>)delegate andRecentsDataSourceMode:(RecentsDataSourceMode)recentsDataSourceMode
@ -212,6 +223,23 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
return stickyHeader;
}
#pragma mark - Space Service notifications
- (void)registerSpaceServiceDidBuildGraphNotification
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(spaceServiceDidBuildGraphNotification:) name:MXSpaceService.didBuildSpaceGraph object:nil];
}
- (void)spaceServiceDidBuildGraphNotification:(NSNotification*)notification
{
[self forceRefresh];
}
- (void)unregisterSpaceServiceDidBuildGraphNotification
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:MXSpaceService.didBuildSpaceGraph object:nil];
}
#pragma mark - Key backup setup banner
- (void)registerKeyBackupStateDidChangeNotification
@ -491,6 +519,11 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
{
serverNoticeSection = sectionsCount++;
}
if (self.suggestedRoomCellDataArray.count > 0)
{
suggestedRoomsSection = sectionsCount++;
}
}
return sectionsCount;
@ -543,6 +576,10 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
{
count = self.invitesCellDataArray.count;
}
else if (section == suggestedRoomsSection && !(shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_SUGGESTED))
{
count = self.suggestedRoomCellDataArray.count;
}
// Adjust this count according to the potential dragged cell.
if ([self isMovingCellSection:section])
@ -624,6 +661,11 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
title = NSLocalizedStringFromTable(@"room_recents_invites_section", @"Vector", nil);
}
}
else if (section == suggestedRoomsSection)
{
count = self.suggestedRoomCellDataArray.count;
title = NSLocalizedStringFromTable(@"room_recents_suggested_rooms_section", @"Vector", nil);
}
if (count)
{
@ -675,6 +717,10 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
{
sectionArray = self.serverNoticeCellDataArray;
}
else if (section == suggestedRoomsSection)
{
sectionArray = self.suggestedRoomCellDataArray;
}
BOOL highlight = NO;
for (id<MXKRecentCellDataStoring> cellData in sectionArray)
@ -760,6 +806,10 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
{
sectionBitwise = RECENTSDATASOURCE_SECTION_INVITES;
}
else if (section == suggestedRoomsSection)
{
sectionBitwise = RECENTSDATASOURCE_SECTION_SUGGESTED;
}
}
if (sectionBitwise)
@ -957,6 +1007,13 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
cellData = self.invitesCellDataArray[cellDataIndex];
}
}
else if (tableSection == suggestedRoomsSection)
{
if (cellDataIndex < self.suggestedRoomCellDataArray.count)
{
cellData = self.suggestedRoomCellDataArray[cellDataIndex];
}
}
return cellData;
}
@ -1013,9 +1070,19 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
{
id<MXKRecentCellDataStoring> cellDataStoring = cellDataArray[index];
if ([roomId isEqualToString:cellDataStoring.roomSummary.roomId] && (matrixSession == cellDataStoring.roomSummary.room.mxSession))
if (cellDataStoring.roomSummary)
{
return index;
if ([roomId isEqualToString:cellDataStoring.roomSummary.roomId] && (matrixSession == cellDataStoring.roomSummary.room.mxSession))
{
return index;
}
}
else if (cellDataStoring.spaceChildInfo)
{
if ([roomId isEqualToString:cellDataStoring.spaceChildInfo.name])
{
return index;
}
}
}
}
@ -1117,7 +1184,22 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
indexPath = [NSIndexPath indexPathForRow:index inSection:serverNoticeSection];
}
}
if (!indexPath && (suggestedRoomsSection >= 0))
{
index = [self cellIndexPosWithRoomId:roomId andMatrixSession:matrixSession within:self.serverNoticeCellDataArray];
if (index != NSNotFound)
{
// Check whether the low priority rooms are shrinked
if (shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_SUGGESTED)
{
return nil;
}
indexPath = [NSIndexPath indexPathForRow:index inSection:serverNoticeSection];
}
}
return indexPath;
}
@ -1133,13 +1215,13 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
NSMutableArray<id<MXKRecentCellDataStoring>> *cells = [NSMutableArray new];
NSInteger count = recentsDataSource.numberOfCells;
for (NSUInteger index = 0; index < count; index++)
{
id<MXKRecentCellDataStoring> cell = [recentsDataSource cellDataAtIndex:index];
[cells addObject:cell];
}
MXWeakify(self);
[self computeStateAsyncWithCells:cells recentsDataSourceMode:self.recentsDataSourceMode matrixSession:recentsDataSource.mxSession onComplete:^(RecentsDataSourceState *newState) {
MXStrongifyAndReturnIfNil(self);
@ -1179,7 +1261,8 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
NSMutableArray<id<MXKRecentCellDataStoring>> *conversationCellDataArray = [NSMutableArray new];
NSMutableArray<id<MXKRecentCellDataStoring>> *lowPriorityCellDataArray = [NSMutableArray new];
NSMutableArray<id<MXKRecentCellDataStoring>> *serverNoticeCellDataArray = [NSMutableArray new];
NSMutableArray<id<MXKRecentCellDataStoring>> *suggestedRoomCellDataArray = [NSMutableArray new];
MissedDiscussionsCount *favouriteMissedDiscussionsCount = [MissedDiscussionsCount new];
MissedDiscussionsCount *directMissedDiscussionsCount = [MissedDiscussionsCount new];
MissedDiscussionsCount *groupMissedDiscussionsCount = [MissedDiscussionsCount new];
@ -1206,7 +1289,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
}
else if (room.summary.membership == MXMembershipInvite)
{
if (!MXSDKOptions.sharedInstance.autoAcceptRoomInvites)
if (room.summary.roomType != MXRoomTypeSpace && !MXSDKOptions.sharedInstance.autoAcceptRoomInvites)
{
[invitesCellDataArray addObject:recentCellDataStoring];
}
@ -1215,6 +1298,14 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
{
[peopleCellDataArray addObject:recentCellDataStoring];
}
else if (recentCellDataStoring.isSuggestedRoom)
{
MXRoomSummary *roomSummary = [mxSession roomSummaryWithRoomId:recentCellDataStoring.spaceChildInfo.childRoomId];
if (!roomSummary.isJoined)
{
[suggestedRoomCellDataArray addObject:recentCellDataStoring];
}
}
else
{
// Hide spaces from home (keep space invites)
@ -1253,13 +1344,22 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
}
else if (recentsDataSourceMode == RecentsDataSourceModeRooms)
{
if (recentCellDataStoring.isSuggestedRoom)
{
MXRoomSummary *roomSummary = [mxSession roomSummaryWithRoomId:recentCellDataStoring.spaceChildInfo.childRoomId];
BOOL isJoined = roomSummary.membership == MXMembershipJoin || roomSummary.membershipTransitionState == MXMembershipTransitionStateJoined;
if (!isJoined)
{
[suggestedRoomCellDataArray addObject:recentCellDataStoring];
}
}
// Consider only non direct rooms.
if (!room.isDirect)
else if (!room.isDirect)
{
// Keep only the invites, the favourites and the rooms without tag and room type different from space
if (room.summary.membership == MXMembershipInvite)
{
if (!MXSDKOptions.sharedInstance.autoAcceptRoomInvites)
if (room.summary.roomType != MXRoomTypeSpace && !MXSDKOptions.sharedInstance.autoAcceptRoomInvites)
{
[invitesCellDataArray addObject:recentCellDataStoring];
}
@ -1348,6 +1448,15 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
// Sort each rooms collection by considering first the rooms with some missed notifs, the rooms with unread, then the others.
comparator = ^NSComparisonResult(id<MXKRecentCellDataStoring> recentCellData1, id<MXKRecentCellDataStoring> recentCellData2) {
if (recentCellData1.spaceChildInfo && !recentCellData2.spaceChildInfo)
{
return NSOrderedDescending;
}
if (recentCellData2.spaceChildInfo && !recentCellData1.spaceChildInfo)
{
return NSOrderedAscending;
}
if (recentCellData1.roomSummary.room.sentStatus != RoomSentStatusOk
&& recentCellData2.roomSummary.room.sentStatus == RoomSentStatusOk)
{
@ -1417,6 +1526,15 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
// Sort each rooms collection by considering first the rooms with some unread messages then the others.
comparator = ^NSComparisonResult(id<MXKRecentCellDataStoring> recentCellData1, id<MXKRecentCellDataStoring> recentCellData2) {
if (recentCellData1.spaceChildInfo && !recentCellData2.spaceChildInfo)
{
return NSOrderedDescending;
}
if (recentCellData2.spaceChildInfo && !recentCellData1.spaceChildInfo)
{
return NSOrderedAscending;
}
if (recentCellData1.roomSummary.room.sentStatus != RoomSentStatusOk
&& recentCellData2.roomSummary.room.sentStatus == RoomSentStatusOk)
{
@ -1489,6 +1607,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
}
MXLogDebug(@"[RecentsDataSource] refreshRoomsSections: Done in %.0fms", [[NSDate date] timeIntervalSinceDate:startDate] * 1000);
MXLogDebug(@"[Spaces] refreshRoomsSections with %ld suggested room", suggestedRoomCellDataArray.count);
return [[RecentsDataSourceState alloc]
initWithInvitesCellDataArray:invitesCellDataArray
@ -1497,6 +1616,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
conversationCellDataArray:conversationCellDataArray
lowPriorityCellDataArray:lowPriorityCellDataArray
serverNoticeCellDataArray:serverNoticeCellDataArray
suggestedRoomCellDataArray:suggestedRoomCellDataArray
favouriteMissedDiscussionsCount:favouriteMissedDiscussionsCount
directMissedDiscussionsCount:directMissedDiscussionsCount
groupMissedDiscussionsCount:groupMissedDiscussionsCount

View file

@ -29,6 +29,7 @@ class RecentsDataSourceState: NSObject {
let conversationCellDataArray: [MXKRecentCellDataStoring]
let lowPriorityCellDataArray: [MXKRecentCellDataStoring]
let serverNoticeCellDataArray: [MXKRecentCellDataStoring]
let suggestedRoomCellDataArray: [MXKRecentCellDataStoring]
// MARK: Notifications counts
let favouriteMissedDiscussionsCount: MissedDiscussionsCount
@ -47,6 +48,7 @@ class RecentsDataSourceState: NSObject {
conversationCellDataArray: [MXKRecentCellDataStoring],
lowPriorityCellDataArray: [MXKRecentCellDataStoring],
serverNoticeCellDataArray: [MXKRecentCellDataStoring],
suggestedRoomCellDataArray: [MXKRecentCellDataStoring],
favouriteMissedDiscussionsCount: MissedDiscussionsCount,
directMissedDiscussionsCount: MissedDiscussionsCount,
groupMissedDiscussionsCount: MissedDiscussionsCount,
@ -58,6 +60,7 @@ class RecentsDataSourceState: NSObject {
self.conversationCellDataArray = conversationCellDataArray
self.lowPriorityCellDataArray = lowPriorityCellDataArray
self.serverNoticeCellDataArray = serverNoticeCellDataArray
self.suggestedRoomCellDataArray = suggestedRoomCellDataArray
self.favouriteMissedDiscussionsCount = favouriteMissedDiscussionsCount
self.directMissedDiscussionsCount = directMissedDiscussionsCount
self.groupMissedDiscussionsCount = groupMissedDiscussionsCount

View file

@ -72,6 +72,8 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
@property (nonatomic, strong) RoomsDirectoryCoordinatorBridgePresenter *roomsDirectoryCoordinatorBridgePresenter;
@property (nonatomic, strong) ExploreRoomCoordinatorBridgePresenter *exploreRoomsCoordinatorBridgePresenter;
@property (nonatomic, strong) SpaceFeatureUnavailablePresenter *spaceFeatureUnavailablePresenter;
@property (nonatomic, strong) CustomSizedPresentationController *customSizedPresentationController;
@ -1950,7 +1952,13 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
return;
}
if (RiotSettings.shared.roomsAllowToJoinPublicRooms)
if (self.dataSource.currentSpace)
{
self.exploreRoomsCoordinatorBridgePresenter = [[ExploreRoomCoordinatorBridgePresenter alloc] initWithSession:self.mainSession spaceId:self.dataSource.currentSpace.spaceId];
self.exploreRoomsCoordinatorBridgePresenter.delegate = self;
[self.exploreRoomsCoordinatorBridgePresenter presentFrom:self animated:YES];
}
else if (RiotSettings.shared.roomsAllowToJoinPublicRooms)
{
self.roomsDirectoryCoordinatorBridgePresenter = [[RoomsDirectoryCoordinatorBridgePresenter alloc] initWithSession:self.mainSession dataSource:[self.recentsDataSource.publicRoomsDirectoryDataSource copy]];
self.roomsDirectoryCoordinatorBridgePresenter.delegate = self;
@ -2064,6 +2072,18 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
[self dispayRoomWithRoomId:roomId inMatrixSession:matrixSession];
}
- (void)recentListViewController:(MXKRecentListViewController *)recentListViewController didSelectSuggestedRoom:(MXSpaceChildInfo *)childInfo
{
RoomPreviewData *previewData = [[RoomPreviewData alloc] initWithSpaceChildInfo:childInfo andSession:self.mainSession];
[self startActivityIndicator];
MXWeakify(self);
[previewData peekInRoom:^(BOOL succeeded) {
MXStrongifyAndReturnIfNil(self);
[self stopActivityIndicator];
[[AppDelegate theDelegate].masterTabBarController showRoomPreview:previewData];
}];
}
#pragma mark - UISearchBarDelegate
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
@ -2270,6 +2290,16 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
}
}
#pragma mark - ExploreRoomCoordinatorBridgePresenterDelegate
- (void)exploreRoomCoordinatorBridgePresenterDelegateDidComplete:(ExploreRoomCoordinatorBridgePresenter *)coordinatorBridgePresenter {
MXWeakify(self);
[coordinatorBridgePresenter dismissWithAnimated:YES completion:^{
MXStrongifyAndReturnIfNil(self);
self.exploreRoomsCoordinatorBridgePresenter = nil;
}];
}
#pragma mark - RoomNotificationSettingsCoordinatorBridgePresenterDelegate
-(void)roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete:(RoomNotificationSettingsCoordinatorBridgePresenter *)coordinatorBridgePresenter
{

View file

@ -76,7 +76,7 @@
self.lastEventDate.text = roomCellData.lastEventDate;
// Manage lastEventAttributedTextMessage optional property
if ([roomCellData respondsToSelector:@selector(lastEventAttributedTextMessage)])
if (!roomCellData.spaceChildInfo && [roomCellData respondsToSelector:@selector(lastEventAttributedTextMessage)])
{
// Force the default text color for the last message (cancel highlighted message color)
NSMutableAttributedString *lastEventDescription = [[NSMutableAttributedString alloc] initWithAttributedString:roomCellData.lastEventAttributedTextMessage];
@ -124,7 +124,14 @@
self.roomTitle.font = [UIFont systemFontOfSize:17 weight:UIFontWeightMedium];
}
[roomCellData.roomSummary setRoomAvatarImageIn:self.roomAvatar];
if (roomCellData.spaceChildInfo)
{
[self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.spaceChildInfo.avatarUrl displayName:roomCellData.spaceChildInfo.displayName mediaManager:roomCellData.recentsDataSource.mxSession.mediaManager];
}
else
{
[self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.roomSummary.avatar displayName:roomCellData.roomSummary.displayname mediaManager:roomCellData.roomSummary.mxSession.mediaManager];
}
}
else
{

View file

@ -86,16 +86,7 @@
{
_showCustomAccessoryView = show;
if (show)
{
self.customAccessViewWidthConstraint.constant = 25;
self.customAccessoryViewLeadingConstraint.constant = 13;
}
else
{
self.customAccessViewWidthConstraint.constant = 0;
self.customAccessoryViewLeadingConstraint.constant = 0;
}
self.customAccessViewWidthConstraint.constant = show ? 25 : 0;
}
- (void)setShowMatrixIdInDisplayName:(BOOL)showMatrixIdInDisplayName

View file

@ -26,7 +26,7 @@
#import "MXRoom+Riot.h"
@interface HomeViewController () <SecureBackupSetupCoordinatorBridgePresenterDelegate>
@interface HomeViewController () <SecureBackupSetupCoordinatorBridgePresenterDelegate, SpaceMembersCoordinatorBridgePresenterDelegate>
{
RecentsDataSource *recentsDataSource;
@ -48,6 +48,8 @@
@property (nonatomic, assign, readwrite) BOOL roomListDataReady;
@property(nonatomic) SpaceMembersCoordinatorBridgePresenter *spaceMembersCoordinatorBridgePresenter;
@end
@implementation HomeViewController
@ -254,7 +256,72 @@
[self cancelEditionMode:YES];
}
[super onPlusButtonPressed];
if (recentsDataSource.currentSpace != nil)
{
[self showPlusMenuForSpace];
}
else
{
[super onPlusButtonPressed];
}
}
- (void)showPlusMenuForSpace
{
__weak typeof(self) weakSelf = self;
[currentAlert dismissViewControllerAnimated:NO completion:nil];
currentAlert = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
[currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"spaces_explore_rooms", @"Vector", nil)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->currentAlert = nil;
[self showRoomDirectory];
}
}]];
[currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_details_people", @"Vector", nil)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->currentAlert = nil;
self.spaceMembersCoordinatorBridgePresenter = [[SpaceMembersCoordinatorBridgePresenter alloc] initWithUserSessionsService:[UserSessionsService shared] session:self.mainSession spaceId:self.dataSource.currentSpace.spaceId];
self.spaceMembersCoordinatorBridgePresenter.delegate = self;
[self.spaceMembersCoordinatorBridgePresenter presentFrom:self animated:YES];
}
}]];
[currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"]
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->currentAlert = nil;
}
}]];
[currentAlert popoverPresentationController].sourceView = plusButtonImageView;
[currentAlert popoverPresentationController].sourceRect = plusButtonImageView.bounds;
[currentAlert mxk_setAccessibilityIdentifier:@"RecentsVCCreateRoomAlert"];
[self presentViewController:currentAlert animated:YES completion:nil];
}
- (void)cancelEditionMode:(BOOL)forceRefresh
@ -562,7 +629,14 @@
id<MXKRecentCellDataStoring> renderedCellData = (id<MXKRecentCellDataStoring>)roomCollectionViewCell.renderedCellData;
[self.delegate recentListViewController:self didSelectRoom:renderedCellData.roomSummary.roomId inMatrixSession:renderedCellData.roomSummary.room.mxSession];
if (renderedCellData.isSuggestedRoom)
{
[self.delegate recentListViewController:self didSelectSuggestedRoom:renderedCellData.spaceChildInfo];
}
else
{
[self.delegate recentListViewController:self didSelectRoom:renderedCellData.roomSummary.roomId inMatrixSession:renderedCellData.roomSummary.room.mxSession];
}
}
// Hide the keyboard when user select a room
@ -861,7 +935,17 @@
+ recentsDataSource.peopleCellDataArray.count
+ recentsDataSource.conversationCellDataArray.count
+ recentsDataSource.lowPriorityCellDataArray.count
+ recentsDataSource.serverNoticeCellDataArray.count;
+ recentsDataSource.serverNoticeCellDataArray.count
+ recentsDataSource.suggestedRoomCellDataArray.count;
}
#pragma mark - SpaceMembersCoordinatorBridgePresenterDelegate
- (void)spaceMembersCoordinatorBridgePresenterDelegateDidComplete:(SpaceMembersCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
[coordinatorBridgePresenter dismissWithAnimated:YES completion:^{
self.spaceMembersCoordinatorBridgePresenter = nil;
}];
}
@end

View file

@ -16,7 +16,7 @@
import Foundation
class HomeViewControllerWithBannerWrapperViewController: UIViewController, BannerPresentationProtocol {
class HomeViewControllerWithBannerWrapperViewController: MXKActivityHandlingViewController, BannerPresentationProtocol {
@objc let homeViewController: HomeViewController
private var bannerContainerView: UIView!
@ -41,6 +41,8 @@ class HomeViewControllerWithBannerWrapperViewController: UIViewController, Banne
override func viewDidLoad() {
super.viewDidLoad()
homeViewController.willMove(toParent: self)
view.backgroundColor = .clear
stackView = UIStackView()
@ -49,7 +51,7 @@ class HomeViewControllerWithBannerWrapperViewController: UIViewController, Banne
stackView.alignment = .fill
view.vc_addSubViewMatchingParent(stackView)
addChild(homeViewController)
stackView.addArrangedSubview(homeViewController.view)
homeViewController.didMove(toParent: self)

View file

@ -130,7 +130,14 @@
}
[roomCellData.roomSummary setRoomAvatarImageIn:self.roomAvatar];
if (roomCellData.roomSummary)
{
[self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.roomSummary.avatar displayName:roomCellData.roomSummary.displayname mediaManager:roomCellData.roomSummary.mxSession.mediaManager];
}
else
{
[self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.spaceChildInfo.avatarUrl displayName:roomCellData.spaceChildInfo.displayName mediaManager:roomCellData.recentsDataSource.mxSession.mediaManager];
}
}
}
@ -171,7 +178,7 @@
{
if (roomCellData)
{
return roomCellData.roomSummary.roomId;
return roomCellData.spaceChildInfo ? roomCellData.spaceChildInfo.childRoomId : roomCellData.roomSummary.roomId;
}
return nil;
}

View file

@ -26,12 +26,14 @@
#import "Riot-Swift.h"
@interface PeopleViewController ()
@interface PeopleViewController () <SpaceMembersCoordinatorBridgePresenterDelegate>
{
NSInteger directRoomsSectionNumber;
RecentsDataSource *recentsDataSource;
}
@property(nonatomic) SpaceMembersCoordinatorBridgePresenter *spaceMembersCoordinatorBridgePresenter;
@end
@implementation PeopleViewController
@ -119,7 +121,16 @@
- (void)onPlusButtonPressed
{
[self performSegueWithIdentifier:@"presentStartChat" sender:self];
if (self.dataSource.currentSpace != nil)
{
self.spaceMembersCoordinatorBridgePresenter = [[SpaceMembersCoordinatorBridgePresenter alloc] initWithUserSessionsService:[UserSessionsService shared] session:self.mainSession spaceId:self.dataSource.currentSpace.spaceId];
self.spaceMembersCoordinatorBridgePresenter.delegate = self;
[self.spaceMembersCoordinatorBridgePresenter presentFrom:self animated:YES];
}
else
{
[self performSegueWithIdentifier:@"presentStartChat" sender:self];
}
}
#pragma mark -
@ -172,4 +183,13 @@
+ recentsDataSource.conversationCellDataArray.count;
}
#pragma mark - SpaceMembersCoordinatorBridgePresenterDelegate
- (void)spaceMembersCoordinatorBridgePresenterDelegateDidComplete:(SpaceMembersCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
[coordinatorBridgePresenter dismissWithAnimated:YES completion:^{
self.spaceMembersCoordinatorBridgePresenter = nil;
}];
}
@end

View file

@ -465,6 +465,11 @@
[[AppDelegate theDelegate] presentCompleteSecurityForSession:self.mainSession];
}
- (void)showRoomWithId:(NSString*)roomId
{
[[AppDelegate theDelegate] showRoom:roomId andEventId:nil withMatrixSession:self.mainSession];
}
#pragma mark - Hide/Show navigation bar border
- (void)hideNavigationBarBorder:(BOOL)isHidden
@ -518,7 +523,10 @@
{
isOneself = YES;
[otherActionsArray addObject:@(MXKRoomMemberDetailsActionLeave)];
if (self.enableLeave)
{
[otherActionsArray addObject:@(MXKRoomMemberDetailsActionLeave)];
}
if (oneSelfPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsStateEvent:kMXEventTypeStringRoomPowerLevels])
{
@ -754,10 +762,24 @@
title = NSLocalizedStringFromTable(@"room_participants_action_leave", @"Vector", nil);
break;
case MXKRoomMemberDetailsActionKick:
title = NSLocalizedStringFromTable(@"room_participants_action_remove", @"Vector", nil);
if (self.mxRoom.summary.roomType == MXRoomTypeSpace)
{
title = NSLocalizedStringFromTable(@"space_participants_action_remove", @"Vector", nil);
}
else
{
title = NSLocalizedStringFromTable(@"room_participants_action_remove", @"Vector", nil);
}
break;
case MXKRoomMemberDetailsActionBan:
title = NSLocalizedStringFromTable(@"room_participants_action_ban", @"Vector", nil);
if (self.mxRoom.summary.roomType == MXRoomTypeSpace)
{
title = NSLocalizedStringFromTable(@"space_participants_action_ban", @"Vector", nil);
}
else
{
title = NSLocalizedStringFromTable(@"room_participants_action_ban", @"Vector", nil);
}
break;
case MXKRoomMemberDetailsActionUnban:
title = NSLocalizedStringFromTable(@"room_participants_action_unban", @"Vector", nil);
@ -1047,7 +1069,7 @@
if (indexPath.row < directChatsArray.count)
{
// Open this room
[[AppDelegate theDelegate] showRoom:directChatsArray[indexPath.row] andEventId:nil withMatrixSession:self.mainSession];
[self showRoomWithId:directChatsArray[indexPath.row]];
}
else
{

View file

@ -85,6 +85,7 @@
@property (nonatomic) BOOL enableMention;
@property (nonatomic) BOOL showCancelBarButtonItem;
@property (nonatomic) BOOL showParticipantCustomAccessoryView;
/**
The delegate for the view controller.

View file

@ -84,6 +84,7 @@
// Setup `MXKViewControllerHandling` properties
self.enableBarTintColorStatusChange = NO;
self.rageShakeManager = [RageShakeManager sharedManager];
self.showParticipantCustomAccessoryView = YES;
}
- (void)viewDidLoad
@ -113,7 +114,11 @@
self.navigationItem.title = NSLocalizedStringFromTable(@"room_participants_title", @"Vector", nil);
if (self.mxRoom.isDirect)
if (self.mxRoom.summary.roomType == MXRoomTypeSpace)
{
_searchBarView.placeholder = NSLocalizedStringFromTable(@"search_default_placeholder", @"Vector", nil);
}
else if (self.mxRoom.isDirect)
{
_searchBarView.placeholder = NSLocalizedStringFromTable(@"room_participants_filter_room_members_for_dm", @"Vector", nil);
}
@ -340,7 +345,11 @@
{
self.searchBarHeader.hidden = NO;
if (self.mxRoom.isDirect)
if (self.mxRoom.summary.roomType == MXRoomTypeSpace)
{
self.searchBarView.placeholder = NSLocalizedStringFromTable(@"search_default_placeholder", @"Vector", nil);
}
else if (self.mxRoom.isDirect)
{
self.searchBarView.placeholder = NSLocalizedStringFromTable(@"room_participants_filter_room_members_for_dm", @"Vector", nil);
}
@ -870,6 +879,19 @@
}
}
- (void)showDetailFor:(MXRoomMember* _Nonnull)member from:(UIView* _Nullable)sourceView {
memberDetailsViewController = [RoomMemberDetailsViewController roomMemberDetailsViewController];
// Set delegate to handle action on member (start chat, mention)
memberDetailsViewController.delegate = self;
memberDetailsViewController.enableMention = _enableMention;
memberDetailsViewController.enableVoipCall = NO;
[memberDetailsViewController displayRoomMember:member withMatrixRoom:self.mxRoom];
[self pushViewController:memberDetailsViewController];
}
#pragma mark - UITableView data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
@ -948,6 +970,7 @@
{
ContactTableViewCell* participantCell = [tableView dequeueReusableCellWithIdentifier:@"ParticipantTableViewCellId" forIndexPath:indexPath];
participantCell.selectionStyle = UITableViewCellSelectionStyleNone;
participantCell.showCustomAccessoryView = self.showParticipantCustomAccessoryView;
participantCell.mxRoom = self.mxRoom;
@ -1185,16 +1208,8 @@
if (contact.mxMember)
{
memberDetailsViewController = [RoomMemberDetailsViewController roomMemberDetailsViewController];
// Set delegate to handle action on member (start chat, mention)
memberDetailsViewController.delegate = self;
memberDetailsViewController.enableMention = _enableMention;
memberDetailsViewController.enableVoipCall = NO;
[memberDetailsViewController displayRoomMember:contact.mxMember withMatrixRoom:self.mxRoom];
[self pushViewController:memberDetailsViewController];
UITableViewCell *selectedCell = [tableView cellForRowAtIndexPath:indexPath];
[self showDetailFor:contact.mxMember from:selectedCell];
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];

View file

@ -19,26 +19,18 @@ import Reusable
class RoomNotificationSettingsAvatarView: UIView {
@IBOutlet weak var avatarView: MXKImageView!
@IBOutlet weak var avatarView: RoomAvatarView!
@IBOutlet weak var nameLabel: UILabel!
func configure(viewData: AvatarViewDataProtocol) {
let avatarImage = AvatarGenerator.generateAvatar(forMatrixItem: viewData.matrixItemId, withDisplayName: viewData.displayName)
avatarView.fill(with: viewData)
if let avatarUrl = viewData.avatarUrl {
avatarView.enableInMemoryCache = true
avatarView.setImageURI(avatarUrl,
withType: nil,
andImageOrientation: .up,
toFitViewSize: avatarView.frame.size,
with: MXThumbnailingMethodCrop,
previewImage: avatarImage,
mediaManager: viewData.mediaManager)
} else {
avatarView.image = avatarImage
switch viewData.fallbackImage {
case .matrixItem(_, let matrixItemDisplayName):
nameLabel.text = matrixItemDisplayName
default:
nameLabel.text = nil
}
nameLabel.text = viewData.displayName
}
}

View file

@ -14,7 +14,7 @@
<rect key="frame" x="0.0" y="0.0" width="414" height="192"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES"/>
<subviews>
<view autoresizesSubviews="NO" clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="q3Z-S1-Py9" customClass="MXKImageView">
<view autoresizesSubviews="NO" clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="q3Z-S1-Py9" customClass="RoomAvatarView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="167" y="20" width="80" height="80"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>

View file

@ -0,0 +1,127 @@
//
// 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
/// this extension is temprorary and implements navigation to the Space bootom sheet. This should be moved to an universal link flow coordinator
extension RoomViewController {
@objc func handleSpaceUniversalLink(with url: URL) {
let url = Tools.fixURL(withSeveralHashKeys: url)
var pathParamsObjc: NSArray?
var queryParamsObjc: NSMutableDictionary?
AppDelegate.theDelegate().parseUniversalLinkFragment(url?.fragment, outPathParams: &pathParamsObjc, outQueryParams: &queryParamsObjc)
// Sanity check
guard let pathParams = pathParamsObjc as? [String], pathParams.count > 0 else {
MXLog.error("[RoomViewController] Universal link: Error: No path parameters")
return
}
var roomIdOrAliasParam: String?
var eventIdParam: String?
var userIdParam: String?
var groupIdParam: String?
// Check permalink to room or event
if pathParams[0] == "room" && pathParams.count >= 2 {
// The link is the form of "/room/[roomIdOrAlias]" or "/room/[roomIdOrAlias]/[eventId]"
roomIdOrAliasParam = pathParams[1]
// Is it a link to an event of a room?
eventIdParam = pathParams.count >= 3 ? pathParams[2] : nil
} else if pathParams[0] == "group" && pathParams.count >= 2 {
// The link is the form of "/group/[groupId]"
groupIdParam = pathParams[1]
} else if (pathParams[0].hasPrefix("#") || pathParams[0].hasPrefix("!")) && pathParams.count >= 1 {
// The link is the form of "/#/[roomIdOrAlias]" or "/#/[roomIdOrAlias]/[eventId]"
// Such links come from matrix.to permalinks
roomIdOrAliasParam = pathParams[0]
eventIdParam = pathParams.count >= 2 ? pathParams[1] : nil
} else if pathParams[0] == "user" && pathParams.count == 2 { // Check permalink to a user
// The link is the form of "/user/userId"
userIdParam = pathParams[1]
} else if pathParams[0].hasPrefix("@") && pathParams.count == 1 {
// The link is the form of "/#/[userId]"
// Such links come from matrix.to permalinks
userIdParam = pathParams[0]
}
guard let roomIdOrAlias = roomIdOrAliasParam else {
AppDelegate.theDelegate().handleUniversalLinkURL(url)
return
}
self.startActivityIndicator()
var viaServers: [String] = []
if let queryParams = queryParamsObjc as? [String: Any], let via = queryParams["via"] as? [String] {
viaServers = via
}
if roomIdOrAlias.hasPrefix("#") {
self.mainSession.matrixRestClient.roomId(forRoomAlias: roomIdOrAlias) { [weak self] response in
guard let self = self else {
return
}
guard let roomId = response.value else {
self.stopActivityIndicator()
if response.error != nil {
let errorMessage = VectorL10n.roomDoesNotExist(roomIdOrAlias)
AppDelegate.theDelegate().showAlert(withTitle: nil, message: errorMessage)
}
return
}
self.requestSummaryAndShowSpaceDetail(forRoomWithId: roomId, via: viaServers, from: url)
}
} else {
self.requestSummaryAndShowSpaceDetail(forRoomWithId: roomIdOrAlias, via: viaServers, from: url)
}
}
private func requestSummaryAndShowSpaceDetail(forRoomWithId roomId: String, via: [String], from url: URL?) {
if self.mainSession.spaceService.getSpace(withId: roomId) != nil {
self.stopActivityIndicator()
self.showSpaceDetail(withId: roomId)
return
}
self.mainSession.matrixRestClient.roomSummary(with: roomId, via: via) { [weak self] response in
guard let self = self else {
return
}
self.stopActivityIndicator()
guard let publicRoom = response.value, publicRoom.roomTypeString == MXRoomTypeString.space.rawValue else {
AppDelegate.theDelegate().handleUniversalLinkURL(url)
return
}
self.showSpaceDetail(with: publicRoom)
}
}
}

View file

@ -86,6 +86,10 @@ extern NSNotificationName const RoomGroupCallTileTappedNotification;
*/
- (void)displayRoomPreview:(RoomPreviewData*)roomPreviewData;
- (void)showSpaceDetailWithPublicRoom:(MXPublicRoom *)publicRoom;
- (void)showSpaceDetailWithId:(NSString *)spaceId;
/**
Action used to handle some buttons.
*/

View file

@ -137,7 +137,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
@interface RoomViewController () <UISearchBarDelegate, UIGestureRecognizerDelegate, UIScrollViewAccessibilityDelegate, RoomTitleViewTapGestureDelegate, RoomParticipantsViewControllerDelegate, MXKRoomMemberDetailsViewControllerDelegate, ContactsTableViewControllerDelegate, MXServerNoticesDelegate, RoomContextualMenuViewControllerDelegate,
ReactionsMenuViewModelCoordinatorDelegate, EditHistoryCoordinatorBridgePresenterDelegate, MXKDocumentPickerPresenterDelegate, EmojiPickerCoordinatorBridgePresenterDelegate,
ReactionHistoryCoordinatorBridgePresenterDelegate, CameraPresenterDelegate, MediaPickerCoordinatorBridgePresenterDelegate,
RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate>
RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate>
{
// The preview header
@ -244,8 +244,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
@property (nonatomic, strong) CustomSizedPresentationController *customSizedPresentationController;
@property (nonatomic, getter=isActivitiesViewExpanded) BOOL activitiesViewExpanded;
@property (nonatomic, getter=isScrollToBottomHidden) BOOL scrollToBottomHidden;
@property (nonatomic, getter=isMissedDiscussionsBadgeHidden) BOOL missedDiscussionsBadgeHidden;
@property (nonatomic, strong) VoiceMessageController *voiceMessageController;
@property (nonatomic, strong) SpaceDetailPresenter *spaceDetailPresenter;
@end
@ -1423,12 +1425,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
}
}
- (void)setShowMissedDiscussionsBadge:(BOOL)showMissedDiscussionsBadge
{
missedDiscussionsBadgeLabel.hidden = !showMissedDiscussionsBadge;
missedDiscussionsDotView.hidden = !showMissedDiscussionsBadge;
}
- (void)setScrollToBottomHidden:(BOOL)scrollToBottomHidden
{
if (_scrollToBottomHidden != scrollToBottomHidden)
@ -1452,6 +1448,13 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
}];
}
- (void)setMissedDiscussionsBadgeHidden:(BOOL)missedDiscussionsBadgeHidden{
_missedDiscussionsBadgeHidden = missedDiscussionsBadgeHidden;
missedDiscussionsBadgeLabel.hidden = missedDiscussionsBadgeHidden;
missedDiscussionsDotView.hidden = missedDiscussionsBadgeHidden;
}
#pragma mark - Internals
- (UIBarButtonItem *)videoCallBarButtonItem
@ -2190,7 +2193,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
}
else
{
return [[AppDelegate theDelegate] handleUniversalLinkURL:universalLinkURL];
[self handleSpaceUniversalLinkWith:universalLinkURL];
return YES;
}
}
@ -4961,16 +4965,16 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
- (void)refreshMissedDiscussionsCount:(BOOL)force
{
// Ignore this action when no room is displayed
if (!self.roomDataSource || !missedDiscussionsBadgeLabel
if (!self.showMissedDiscussionsBadge || !self.roomDataSource || !missedDiscussionsBadgeLabel
|| [UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPhone
|| ([[UIScreen mainScreen] nativeBounds].size.height > 2532 && UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)))
{
self.showMissedDiscussionsBadge = NO;
self.missedDiscussionsBadgeHidden = YES;
return;
}
self.showMissedDiscussionsBadge = YES;
self.missedDiscussionsBadgeHidden = NO;
NSUInteger highlightCount = 0;
NSUInteger missedCount = [[AppDelegate theDelegate].masterTabBarController missedDiscussionsCount];
@ -6478,4 +6482,37 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
}];
}
- (void)showSpaceDetailWithPublicRoom:(MXPublicRoom *)publicRoom
{
self.spaceDetailPresenter = [SpaceDetailPresenter new];
self.spaceDetailPresenter.delegate = self;
[self.spaceDetailPresenter presentForSpaceWithPublicRoom:publicRoom from:self sourceView:nil session:self.mainSession animated:YES];
}
- (void)showSpaceDetailWithId:(NSString *)spaceId
{
self.spaceDetailPresenter = [SpaceDetailPresenter new];
self.spaceDetailPresenter.delegate = self;
[self.spaceDetailPresenter presentForSpaceWithId:spaceId from:self sourceView:nil session:self.mainSession animated:YES];
}
#pragma mark - SpaceDetailPresenterDelegate
- (void)spaceDetailPresenterDidComplete:(SpaceDetailPresenter *)presenter
{
self.spaceDetailPresenter = nil;
}
- (void)spaceDetailPresenter:(SpaceDetailPresenter *)presenter didOpenSpaceWithId:(NSString *)spaceId
{
self.spaceDetailPresenter = nil;
[[LegacyAppDelegate theDelegate] openSpaceWithId:spaceId];
}
- (void)spaceDetailPresenter:(SpaceDetailPresenter *)presenter didJoinSpaceWithId:(NSString *)spaceId
{
self.spaceDetailPresenter = nil;
[[LegacyAppDelegate theDelegate] openSpaceWithId:spaceId];
}
@end

View file

@ -25,7 +25,11 @@ final class RoomAvatarView: AvatarView, NibOwnerLoadable {
@IBOutlet private weak var cameraBadgeContainerView: UIView!
// MARK: Setup
// MARK: Public
var showCameraBadgeOnFallbackImage: Bool = false
// MARK: - Setup
private func commonInit() {
}
@ -74,6 +78,14 @@ final class RoomAvatarView: AvatarView, NibOwnerLoadable {
override func updateAvatarImageView(with viewData: AvatarViewDataProtocol) {
super.updateAvatarImageView(with: viewData)
self.cameraBadgeContainerView.isHidden = viewData.avatarUrl != nil
let hideCameraBadge: Bool
if self.showCameraBadgeOnFallbackImage {
hideCameraBadge = viewData.avatarUrl != nil
} else {
hideCameraBadge = true
}
self.cameraBadgeContainerView.isHidden = hideCameraBadge
}
}

View file

@ -25,4 +25,8 @@ struct RoomAvatarViewData: AvatarViewDataProtocol {
var matrixItemId: String {
return roomId
}
var fallbackImage: AvatarFallbackImage? {
return .matrixItem(matrixItemId, displayName)
}
}

View file

@ -70,6 +70,8 @@ final class RoomCreationIntroCellContentView: UIView, NibLoadable, Themable {
self.addParticipantsButton.addTarget(self, action: #selector(socialButtonAction(_:)), for: .touchUpInside)
self.addParticipantsLabel.text = VectorL10n.roomIntroCellAddParticipantsAction
self.roomAvatarView.showCameraBadgeOnFallbackImage = true
}
override func layoutSubviews() {

View file

@ -103,8 +103,13 @@
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
// Hide the header to merge Invites and Rooms into a single list.
return 0.0;
if ([tableView numberOfSections] <= 1)
{
// Hide the header to merge Invites and Rooms into a single list.
return 0.0;
}
return [super tableView:tableView heightForHeaderInSection:section];
}
#pragma mark -
@ -155,7 +160,8 @@
- (NSUInteger)totalItemCounts
{
return recentsDataSource.conversationCellDataArray.count
+ recentsDataSource.invitesCellDataArray.count;
+ recentsDataSource.invitesCellDataArray.count
+ recentsDataSource.suggestedRoomCellDataArray.count;
}
@end

View file

@ -27,7 +27,19 @@ struct DirectoryRoomTableViewCellVM {
// TODO: Use AvatarView subclass in the cell view
func setAvatar(in avatarImageView: MXKImageView) {
let avatarImage = AvatarGenerator.generateAvatar(forMatrixItem: self.avatarViewData.matrixItemId, withDisplayName: title)
let defaultAvatarImage: UIImage?
var defaultAvatarImageContentMode: UIView.ContentMode = .scaleAspectFill
switch self.avatarViewData.fallbackImage {
case .matrixItem(let matrixItemId, let matrixItemDisplayName):
defaultAvatarImage = AvatarGenerator.generateAvatar(forMatrixItem: matrixItemId, withDisplayName: matrixItemDisplayName)
case .image(let image, let contentMode):
defaultAvatarImage = image
defaultAvatarImageContentMode = contentMode ?? .scaleAspectFill
case .none:
defaultAvatarImage = nil
}
if let avatarUrl = self.avatarViewData.avatarUrl {
avatarImageView.enableInMemoryCache = true
@ -37,10 +49,12 @@ struct DirectoryRoomTableViewCellVM {
andImageOrientation: .up,
toFitViewSize: avatarImageView.frame.size,
with: MXThumbnailingMethodCrop,
previewImage: avatarImage,
previewImage: defaultAvatarImage,
mediaManager: self.avatarViewData.mediaManager)
avatarImageView.contentMode = .scaleAspectFill
} else {
avatarImageView.image = avatarImage
avatarImageView.image = defaultAvatarImage
avatarImageView.contentMode = defaultAvatarImageContentMode
}
}

View file

@ -22,16 +22,27 @@ import SideMenu
import SafariServices
class SideMenuCoordinatorParameters {
let appNavigator: AppNavigatorProtocol
let userSessionsService: UserSessionsService
let appInfo: AppInfo
init(userSessionsService: UserSessionsService, appInfo: AppInfo) {
init(appNavigator: AppNavigatorProtocol,
userSessionsService: UserSessionsService,
appInfo: AppInfo) {
self.appNavigator = appNavigator
self.userSessionsService = userSessionsService
self.appInfo = appInfo
}
}
final class SideMenuCoordinator: SideMenuCoordinatorType {
final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType {
// MARK: - Constants
private enum SideMenu {
static let widthRatio: CGFloat = 0.82
static let maxWidthiPad: CGFloat = 320.0
}
// MARK: - Properties
@ -40,12 +51,20 @@ final class SideMenuCoordinator: SideMenuCoordinatorType {
private let parameters: SideMenuCoordinatorParameters
private var sideMenuViewModel: SideMenuViewModelType
private weak var spaceListCoordinator: SpaceListCoordinatorType?
private lazy var sideMenuNavigationViewController: SideMenuNavigationController = {
return self.createSideMenuNavigationController(with: self.sideMenuViewController)
}()
private let sideMenuViewController: SideMenuViewController
let spaceMenuPresenter = SpaceMenuPresenter()
let spaceDetailPresenter = SpaceDetailPresenter()
private var exploreRoomCoordinator: ExploreRoomCoordinator?
private var membersCoordinator: SpaceMembersCoordinator?
// MARK: Public
// Must be used only internally
@ -69,9 +88,13 @@ final class SideMenuCoordinator: SideMenuCoordinatorType {
self.sideMenuViewModel.coordinatorDelegate = self
self.sideMenuNavigationViewController.sideMenuDelegate = self
self.sideMenuNavigationViewController.dismissOnRotation = false
// Set the sideMenuNavigationViewController as default left menu
SideMenuManager.default.leftMenuNavigationController = self.sideMenuNavigationViewController
self.addSpaceListIfNeeded()
self.registerUserSessionsServiceNotifications()
}
func toPresentable() -> UIViewController {
@ -86,21 +109,73 @@ final class SideMenuCoordinator: SideMenuCoordinatorType {
return self.sideMenuNavigationViewController.sideMenuManager.addPanGestureToPresent(toView: view)
}
func select(spaceWithId spaceId: String) {
self.spaceListCoordinator?.select(spaceWithId: spaceId)
}
// MARK: - Private
private func createSideMenuNavigationController(with rootViewController: UIViewController) -> SideMenuNavigationController {
var sideMenuSettings = SideMenuSettings()
sideMenuSettings.presentationStyle = .viewSlideOut
sideMenuSettings.menuWidth = self.getMenuWidth()
let navigationController = SideMenuNavigationController(rootViewController: rootViewController, settings: sideMenuSettings)
// Present side menu to the left
navigationController.leftSide = true
// FIX: SideMenuSettings are not taken into account at init apply them again
navigationController.settings = sideMenuSettings
return navigationController
}
private func getMenuWidth() -> CGFloat {
let appScreenRect = UIApplication.shared.keyWindow?.bounds ?? UIWindow().bounds
let minimumSize = min(appScreenRect.width, appScreenRect.height)
let menuWidth: CGFloat
if UIDevice.current.isPhone {
menuWidth = round(minimumSize * SideMenu.widthRatio)
} else {
// Set a max menu width on iPad
menuWidth = min(round(minimumSize * SideMenu.widthRatio), SideMenu.maxWidthiPad * SideMenu.widthRatio)
}
return menuWidth
}
private func addSpaceListIfNeeded() {
guard self.spaceListCoordinator == nil else {
return
}
guard let mainMatrixSession = self.parameters.userSessionsService.mainUserSession?.matrixSession else {
return
}
self.addSpaceList(with: mainMatrixSession)
}
private func addSpaceList(with matrixSession: MXSession) {
let parameters = SpaceListCoordinatorParameters(session: matrixSession)
let spaceListCoordinator = SpaceListCoordinator(parameters: parameters)
spaceListCoordinator.delegate = self
spaceListCoordinator.start()
let spaceListPresentable = spaceListCoordinator.toPresentable()
// sideMenuViewController.spaceListContainerView can be nil, load controller view to avoid this case
self.sideMenuViewController.loadViewIfNeeded()
self.sideMenuViewController.vc_addChildViewController(viewController: spaceListPresentable, onView: self.sideMenuViewController.spaceListContainerView)
self.add(childCoordinator: spaceListCoordinator)
self.spaceListCoordinator = spaceListCoordinator
}
private func createSettingsViewController() -> SettingsViewController {
let viewController: SettingsViewController = SettingsViewController.instantiate()
viewController.loadViewIfNeeded()
@ -136,12 +211,65 @@ final class SideMenuCoordinator: SideMenuCoordinatorType {
self.sideMenuNavigationViewController.present(safariViewController, animated: true, completion: nil)
}
private func showExploreRooms(spaceId: String, session: MXSession) {
let exploreRoomCoordinator = ExploreRoomCoordinator(session: session, spaceId: spaceId)
exploreRoomCoordinator.delegate = self
let presentable = exploreRoomCoordinator.toPresentable()
presentable.presentationController?.delegate = self
self.sideMenuViewController.present(presentable, animated: true, completion: nil)
exploreRoomCoordinator.start()
self.exploreRoomCoordinator = exploreRoomCoordinator
}
private func showMembers(spaceId: String, session: MXSession) {
let parameters = SpaceMembersCoordinatorParameters(userSessionsService: self.parameters.userSessionsService, session: session, spaceId: spaceId)
let spaceMembersCoordinator = SpaceMembersCoordinator(parameters: parameters)
spaceMembersCoordinator.delegate = self
let presentable = spaceMembersCoordinator.toPresentable()
presentable.presentationController?.delegate = self
self.sideMenuViewController.present(presentable, animated: true, completion: nil)
spaceMembersCoordinator.start()
self.membersCoordinator = spaceMembersCoordinator
}
private func showInviteFriends(from sourceView: UIView?) {
let myUserId = self.parameters.userSessionsService.mainUserSession?.userId ?? ""
let inviteFriendsPresenter = InviteFriendsPresenter()
inviteFriendsPresenter.present(for: myUserId, from: self.sideMenuViewController, sourceView: sourceView, animated: true)
}
private func showMenu(forSpaceWithId spaceId: String, from sourceView: UIView?) {
guard let session = self.parameters.userSessionsService.mainUserSession?.matrixSession else {
return
}
self.spaceMenuPresenter.delegate = self
self.spaceMenuPresenter.present(forSpaceWithId: spaceId, from: self.sideMenuViewController, sourceView: sourceView, session: session, animated: true)
}
private func showSpaceDetail(forSpaceWithId spaceId: String, from sourceView: UIView?) {
guard let session = self.parameters.userSessionsService.mainUserSession?.matrixSession else {
return
}
self.spaceDetailPresenter.delegate = self
self.spaceDetailPresenter.present(forSpaceWithId: spaceId, from: self.sideMenuViewController, sourceView: sourceView, session: session, animated: true)
}
// MARK: UserSessions management
private func registerUserSessionsServiceNotifications() {
// Listen only notifications from the current UserSessionsService instance
let userSessionService = self.parameters.userSessionsService
NotificationCenter.default.addObserver(self, selector: #selector(userSessionsServiceDidAddUserSession(_:)), name: UserSessionsService.didAddUserSession, object: userSessionService)
}
@objc private func userSessionsServiceDidAddUserSession(_ notification: Notification) {
self.addSpaceListIfNeeded()
}
}
// MARK: - SideMenuViewModelCoordinatorDelegate
@ -179,3 +307,84 @@ extension SideMenuCoordinator: SideMenuNavigationControllerDelegate {
func sideMenuDidDisappear(menu: SideMenuNavigationController, animated: Bool) {
}
}
// MARK: - SideMenuNavigationControllerDelegate
extension SideMenuCoordinator: SpaceListCoordinatorDelegate {
func spaceListCoordinatorDidSelectHomeSpace(_ coordinator: SpaceListCoordinatorType) {
self.parameters.appNavigator.sideMenu.dismiss(animated: true) {
}
self.parameters.appNavigator.navigate(to: .homeSpace)
}
func spaceListCoordinator(_ coordinator: SpaceListCoordinatorType, didSelectSpaceWithId spaceId: String) {
self.parameters.appNavigator.sideMenu.dismiss(animated: true) {
}
self.parameters.appNavigator.navigate(to: .space(spaceId))
}
func spaceListCoordinator(_ coordinator: SpaceListCoordinatorType, didSelectInviteWithId spaceId: String, from sourceView: UIView?) {
self.showSpaceDetail(forSpaceWithId: spaceId, from: sourceView)
}
func spaceListCoordinator(_ coordinator: SpaceListCoordinatorType, didPressMoreForSpaceWithId spaceId: String, from sourceView: UIView) {
self.showMenu(forSpaceWithId: spaceId, from: sourceView)
}
}
// MARK: - SpaceMenuPresenterDelegate
extension SideMenuCoordinator: SpaceMenuPresenterDelegate {
func spaceMenuPresenter(_ presenter: SpaceMenuPresenter, didCompleteWith action: SpaceMenuPresenter.Actions, forSpaceWithId spaceId: String, with session: MXSession) {
presenter.dismiss(animated: false) {
switch action {
case .exploreRooms:
self.showExploreRooms(spaceId: spaceId, session: session)
case .exploreMembers:
self.showMembers(spaceId: spaceId, session: session)
}
}
}
}
extension SideMenuCoordinator: SpaceDetailPresenterDelegate {
func spaceDetailPresenter(_ presenter: SpaceDetailPresenter, didJoinSpaceWithId spaceId: String) {
self.spaceListCoordinator?.select(spaceWithId: spaceId)
}
func spaceDetailPresenter(_ presenter: SpaceDetailPresenter, didOpenSpaceWithId spaceId: String) {
// this use case cannot happen here as the space list open directly joined spaces on tap
self.spaceListCoordinator?.revertItemSelection()
}
func spaceDetailPresenterDidComplete(_ presenter: SpaceDetailPresenter) {
self.spaceListCoordinator?.revertItemSelection()
}
}
// MARK: - ExploreRoomCoordinatorDelegate
extension SideMenuCoordinator: ExploreRoomCoordinatorDelegate {
func exploreRoomCoordinatorDidComplete(_ coordinator: ExploreRoomCoordinatorType) {
self.exploreRoomCoordinator?.toPresentable().dismiss(animated: true) {
self.exploreRoomCoordinator = nil
}
}
}
// MARK: - SpaceMembersCoordinatorDelegate
extension SideMenuCoordinator: SpaceMembersCoordinatorDelegate {
func spaceMembersCoordinatorDidCancel(_ coordinator: SpaceMembersCoordinatorType) {
self.membersCoordinator?.toPresentable().dismiss(animated: true) {
self.membersCoordinator = nil
}
}
}
// MARK: - UIAdaptivePresentationControllerDelegate
extension SideMenuCoordinator: UIAdaptivePresentationControllerDelegate {
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
self.exploreRoomCoordinator = nil
self.membersCoordinator = nil
}
}

View file

@ -28,4 +28,5 @@ protocol SideMenuCoordinatorType: Coordinator, Presentable {
@discardableResult func addScreenEdgePanGesturesToPresent(to view: UIView) -> UIScreenEdgePanGestureRecognizer
@discardableResult func addPanGestureToPresent(to view: UIView) -> UIPanGestureRecognizer
func select(spaceWithId spaceId: String)
}

View file

@ -16,126 +16,97 @@
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9U2-KL-ZVA">
<rect key="frame" x="0.0" y="44" width="414" height="852"/>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="uTs-MO-piF">
<rect key="frame" x="0.0" y="44" width="414" height="92"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="e7g-um-WO4">
<rect key="frame" x="0.0" y="0.0" width="414" height="222"/>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="NuF-pw-IzO" customClass="UserAvatarView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="20" y="20" width="52" height="52"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="width" secondItem="NuF-pw-IzO" secondAttribute="height" multiplier="1:1" id="aPR-9H-XC7"/>
<constraint firstAttribute="width" constant="52" id="v7Z-9B-ROI"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="pj0-XK-IJ2">
<rect key="frame" x="87" y="23" width="307" height="46"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="voD-3Q-ryt">
<rect key="frame" x="0.0" y="0.0" width="414" height="222"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="uTs-MO-piF">
<rect key="frame" x="0.0" y="0.0" width="414" height="158"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="NuF-pw-IzO" customClass="UserAvatarView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="20" y="30" width="52" height="52"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="width" secondItem="NuF-pw-IzO" secondAttribute="height" multiplier="1:1" id="aPR-9H-XC7"/>
<constraint firstAttribute="width" constant="52" id="v7Z-9B-ROI"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="pj0-XK-IJ2">
<rect key="frame" x="20" y="92" width="374" height="46"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bbo-IX-VUb">
<rect key="frame" x="0.0" y="0.0" width="374" height="24"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle3"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="VWw-Gn-nd0">
<rect key="frame" x="0.0" y="28" width="374" height="18"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="pj0-XK-IJ2" secondAttribute="bottom" constant="20" id="GyA-NG-zuK"/>
<constraint firstAttribute="trailing" secondItem="pj0-XK-IJ2" secondAttribute="trailing" constant="20" id="Y1J-eh-n41"/>
<constraint firstItem="pj0-XK-IJ2" firstAttribute="leading" secondItem="uTs-MO-piF" secondAttribute="leading" constant="20" id="oqk-zx-vwa"/>
<constraint firstItem="NuF-pw-IzO" firstAttribute="leading" secondItem="uTs-MO-piF" secondAttribute="leading" constant="20" id="rSh-ot-aqo"/>
<constraint firstItem="pj0-XK-IJ2" firstAttribute="top" secondItem="NuF-pw-IzO" secondAttribute="bottom" constant="10" id="wMq-kI-hIR"/>
<constraint firstItem="NuF-pw-IzO" firstAttribute="top" secondItem="uTs-MO-piF" secondAttribute="top" constant="30" id="woS-eb-vCr"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="M9u-6y-ybq">
<rect key="frame" x="0.0" y="158" width="414" height="64"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="jh2-Rr-gGK">
<rect key="frame" x="20" y="0.0" width="374" height="44"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="d2f-GW-Q3t" customClass="SideMenuActionView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="RaC-Fc-LVI"/>
</constraints>
</view>
</subviews>
<constraints>
<constraint firstItem="d2f-GW-Q3t" firstAttribute="width" secondItem="jh2-Rr-gGK" secondAttribute="width" id="OtD-wt-AZr"/>
</constraints>
</stackView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="jh2-Rr-gGK" firstAttribute="leading" secondItem="M9u-6y-ybq" secondAttribute="leading" constant="20" id="4H1-E7-NKg"/>
<constraint firstAttribute="bottom" secondItem="jh2-Rr-gGK" secondAttribute="bottom" constant="20" id="Ml9-0O-ZAG"/>
<constraint firstItem="jh2-Rr-gGK" firstAttribute="top" secondItem="M9u-6y-ybq" secondAttribute="top" id="dTl-ZO-glj"/>
<constraint firstAttribute="trailing" secondItem="jh2-Rr-gGK" secondAttribute="trailing" constant="20" id="uW2-nD-nhl"/>
</constraints>
</view>
</subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bbo-IX-VUb">
<rect key="frame" x="0.0" y="0.0" width="307" height="24"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle3"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="VWw-Gn-nd0">
<rect key="frame" x="0.0" y="28" width="307" height="18"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="pj0-XK-IJ2" secondAttribute="bottom" constant="20" id="GyA-NG-zuK"/>
<constraint firstItem="pj0-XK-IJ2" firstAttribute="centerY" secondItem="NuF-pw-IzO" secondAttribute="centerY" id="Ryy-Un-b4P"/>
<constraint firstAttribute="trailing" secondItem="pj0-XK-IJ2" secondAttribute="trailing" constant="20" id="Y1J-eh-n41"/>
<constraint firstItem="pj0-XK-IJ2" firstAttribute="leading" secondItem="NuF-pw-IzO" secondAttribute="trailing" constant="15" id="dY6-O4-aq7"/>
<constraint firstItem="NuF-pw-IzO" firstAttribute="leading" secondItem="uTs-MO-piF" secondAttribute="leading" constant="20" id="rSh-ot-aqo"/>
<constraint firstItem="NuF-pw-IzO" firstAttribute="top" secondItem="uTs-MO-piF" secondAttribute="top" constant="20" id="woS-eb-vCr"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="d7a-wj-MoP">
<rect key="frame" x="0.0" y="136" width="414" height="672"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="M9u-6y-ybq">
<rect key="frame" x="0.0" y="808" width="414" height="54"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="jh2-Rr-gGK">
<rect key="frame" x="20" y="0.0" width="374" height="44"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="d2f-GW-Q3t" customClass="SideMenuActionView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="M9u-6y-ybq" firstAttribute="leading" secondItem="voD-3Q-ryt" secondAttribute="leading" id="BY4-9k-kWW"/>
<constraint firstAttribute="bottom" secondItem="M9u-6y-ybq" secondAttribute="bottom" id="JgN-P3-Gr2"/>
<constraint firstItem="M9u-6y-ybq" firstAttribute="top" secondItem="uTs-MO-piF" secondAttribute="bottom" id="MuD-JP-iy9"/>
<constraint firstAttribute="width" priority="750" constant="500" id="glD-Sz-73O"/>
<constraint firstItem="uTs-MO-piF" firstAttribute="leading" secondItem="voD-3Q-ryt" secondAttribute="leading" id="kQ7-oa-oSs"/>
<constraint firstItem="uTs-MO-piF" firstAttribute="top" secondItem="voD-3Q-ryt" secondAttribute="top" id="m0n-kQ-UAA"/>
<constraint firstAttribute="trailing" secondItem="uTs-MO-piF" secondAttribute="trailing" id="oWE-b2-UKq"/>
<constraint firstAttribute="trailing" secondItem="M9u-6y-ybq" secondAttribute="trailing" id="wLj-aM-UyK"/>
<constraint firstAttribute="height" constant="44" id="RaC-Fc-LVI"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="voD-3Q-ryt" secondAttribute="bottom" id="63a-5e-ptU"/>
<constraint firstItem="voD-3Q-ryt" firstAttribute="centerX" secondItem="e7g-um-WO4" secondAttribute="centerX" id="P2G-mq-gQW"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="voD-3Q-ryt" secondAttribute="trailing" id="QgV-SO-5yf"/>
<constraint firstItem="voD-3Q-ryt" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="e7g-um-WO4" secondAttribute="leading" id="YPo-u1-PtT"/>
<constraint firstItem="voD-3Q-ryt" firstAttribute="top" secondItem="e7g-um-WO4" secondAttribute="top" id="rhQ-96-szL"/>
<constraint firstItem="d2f-GW-Q3t" firstAttribute="width" secondItem="jh2-Rr-gGK" secondAttribute="width" id="OtD-wt-AZr"/>
</constraints>
</view>
</stackView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="e7g-um-WO4" secondAttribute="trailing" id="GyG-Fh-PME"/>
<constraint firstItem="e7g-um-WO4" firstAttribute="width" secondItem="9U2-KL-ZVA" secondAttribute="width" id="Ok2-WQ-Zgc"/>
<constraint firstAttribute="bottom" secondItem="e7g-um-WO4" secondAttribute="bottom" id="Y46-NP-zAc"/>
<constraint firstItem="e7g-um-WO4" firstAttribute="leading" secondItem="9U2-KL-ZVA" secondAttribute="leading" id="aoV-Yh-AcD"/>
<constraint firstItem="e7g-um-WO4" firstAttribute="top" secondItem="9U2-KL-ZVA" secondAttribute="top" id="pFN-bA-SHw"/>
<constraint firstItem="jh2-Rr-gGK" firstAttribute="leading" secondItem="M9u-6y-ybq" secondAttribute="leading" constant="20" id="4H1-E7-NKg"/>
<constraint firstAttribute="bottom" secondItem="jh2-Rr-gGK" secondAttribute="bottom" constant="10" id="Ml9-0O-ZAG"/>
<constraint firstItem="jh2-Rr-gGK" firstAttribute="top" secondItem="M9u-6y-ybq" secondAttribute="top" id="dTl-ZO-glj"/>
<constraint firstAttribute="trailing" secondItem="jh2-Rr-gGK" secondAttribute="trailing" constant="20" id="uW2-nD-nhl"/>
</constraints>
</scrollView>
</view>
</subviews>
<viewLayoutGuide key="safeArea" id="bFg-jh-JZB"/>
<color key="backgroundColor" red="0.94509803921568625" green="0.96078431372549022" blue="0.97254901960784312" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="9U2-KL-ZVA" secondAttribute="bottom" id="7Cb-nY-CsO"/>
<constraint firstItem="9U2-KL-ZVA" firstAttribute="leading" secondItem="bFg-jh-JZB" secondAttribute="leading" id="GdQ-hK-muG"/>
<constraint firstItem="bFg-jh-JZB" firstAttribute="trailing" secondItem="9U2-KL-ZVA" secondAttribute="trailing" id="sbD-ek-vGJ"/>
<constraint firstItem="bFg-jh-JZB" firstAttribute="top" secondItem="9U2-KL-ZVA" secondAttribute="top" id="wTB-V6-IHV"/>
<constraint firstItem="bFg-jh-JZB" firstAttribute="bottom" secondItem="M9u-6y-ybq" secondAttribute="bottom" id="39t-nU-ci3"/>
<constraint firstItem="uTs-MO-piF" firstAttribute="leading" secondItem="bFg-jh-JZB" secondAttribute="leading" id="NaR-V6-0ns"/>
<constraint firstItem="d7a-wj-MoP" firstAttribute="top" secondItem="pj0-XK-IJ2" secondAttribute="bottom" priority="250" constant="20" id="TPa-Kf-kaT"/>
<constraint firstAttribute="trailing" secondItem="d7a-wj-MoP" secondAttribute="trailing" id="UEj-UR-cGJ"/>
<constraint firstItem="d7a-wj-MoP" firstAttribute="top" secondItem="uTs-MO-piF" secondAttribute="bottom" id="epm-HJ-Glc"/>
<constraint firstItem="d7a-wj-MoP" firstAttribute="leading" secondItem="EL9-GA-lwo" secondAttribute="leading" id="f1d-7J-hNw"/>
<constraint firstItem="M9u-6y-ybq" firstAttribute="leading" secondItem="bFg-jh-JZB" secondAttribute="leading" id="iFr-i7-3RB"/>
<constraint firstItem="bFg-jh-JZB" firstAttribute="trailing" secondItem="uTs-MO-piF" secondAttribute="trailing" id="keO-Hx-S2U"/>
<constraint firstItem="M9u-6y-ybq" firstAttribute="top" secondItem="d7a-wj-MoP" secondAttribute="bottom" id="nNw-XQ-3Mu"/>
<constraint firstItem="bFg-jh-JZB" firstAttribute="top" secondItem="uTs-MO-piF" secondAttribute="top" id="nRc-Xs-kjp"/>
<constraint firstItem="d7a-wj-MoP" firstAttribute="top" relation="greaterThanOrEqual" secondItem="NuF-pw-IzO" secondAttribute="bottom" constant="20" id="oOb-84-BE1"/>
<constraint firstItem="M9u-6y-ybq" firstAttribute="trailing" secondItem="bFg-jh-JZB" secondAttribute="trailing" id="vxI-Qt-760"/>
</constraints>
</view>
<connections>
<outlet property="menuItemsStackView" destination="jh2-Rr-gGK" id="mTS-AO-avQ"/>
<outlet property="scrollView" destination="9U2-KL-ZVA" id="ojG-2y-X7b"/>
<outlet property="spaceListContainerView" destination="d7a-wj-MoP" id="TzM-3u-PqG"/>
<outlet property="userAvatarView" destination="NuF-pw-IzO" id="Xyh-Rl-hW4"/>
<outlet property="userDisplayNameLabel" destination="bbo-IX-VUb" id="8vG-CB-Fgo"/>
<outlet property="userIdLabel" destination="VWw-Gn-nd0" id="4gK-yt-uR9"/>

View file

@ -29,8 +29,8 @@ final class SideMenuViewController: UIViewController {
// MARK: - Properties
// MARK: Outlets
@IBOutlet private weak var scrollView: UIScrollView!
@IBOutlet weak var spaceListContainerView: UIView!
// User info
@IBOutlet private weak var userAvatarView: UserAvatarView!
@ -103,7 +103,7 @@ final class SideMenuViewController: UIViewController {
theme.applyStyle(onNavigationBar: navigationBar)
}
self.view.backgroundColor = theme.headerBackgroundColor
self.view.backgroundColor = theme.colors.background
self.userAvatarView.update(theme: theme)
self.userDisplayNameLabel.textColor = theme.textPrimaryColor

View file

@ -71,11 +71,12 @@ final class SideMenuViewModel: SideMenuViewModelType {
}
private func userAvatarViewData(from mxSession: MXSession) -> UserAvatarViewData? {
guard let userId = mxSession.myUserId, let mediaManager = mxSession.mediaManager else {
guard let userId = mxSession.myUserId, let mediaManager = mxSession.mediaManager, let myUser = mxSession.myUser else {
return nil
}
let userDisplayName = mxSession.myUser.displayname
let avatarUrl = mxSession.myUser.avatarUrl
let userDisplayName = myUser.displayname
let avatarUrl = myUser.avatarUrl
return UserAvatarViewData(userId: userId,
displayName: userDisplayName,
@ -103,7 +104,8 @@ final class SideMenuViewModel: SideMenuViewModelType {
.feedback
]
let appVersion = self.appInfo.appVersion?.bundleShortVersion
// Hide app version
let appVersion: String? = nil
let viewData = SideMenuViewData(userAvatarViewData: userAvatarViewData, sideMenuItems: sideMenuItems, appVersion: appVersion)

View file

@ -0,0 +1,103 @@
//
// 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 Reusable
final class SpaceAvatarView: AvatarView, NibOwnerLoadable {
// MARK: - Constants
private enum Constants {
static let cornerRadius: CGFloat = 8.0
}
// MARK: - Properties
// MARK: Outlets
@IBOutlet private weak var cameraBadgeContainerView: UIView!
// MARK: Public
var showCameraBadgeOnFallbackImage: Bool = false
// MARK: - Setup
private func commonInit() {
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.loadNibContent()
self.commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.loadNibContent()
self.commonInit()
}
// MARK: - Lifecycle
override func layoutSubviews() {
super.layoutSubviews()
self.avatarImageView.layer.cornerRadius = Constants.cornerRadius
}
// MARK: - Public
override func fill(with viewData: AvatarViewDataProtocol) {
self.updateAvatarImageView(with: viewData)
// Fix layoutSubviews not triggered issue
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.setNeedsLayout()
}
}
// MARK: - Overrides
override func updateAccessibilityTraits() {
if self.isUserInteractionEnabled {
self.vc_setupAccessibilityTraitsButton(withTitle: VectorL10n.spaceAvatarViewAccessibilityLabel, hint: VectorL10n.spaceAvatarViewAccessibilityHint, isEnabled: true)
} else {
self.vc_setupAccessibilityTraitsImage(withTitle: VectorL10n.spaceAvatarViewAccessibilityLabel)
}
}
override func updateAvatarImageView(with viewData: AvatarViewDataProtocol) {
super.updateAvatarImageView(with: viewData)
let hideCameraBadge: Bool
if self.showCameraBadgeOnFallbackImage {
hideCameraBadge = viewData.avatarUrl != nil
} else {
hideCameraBadge = true
}
self.cameraBadgeContainerView.isHidden = hideCameraBadge
}
override func update(theme: Theme) {
super.update(theme: theme)
self.avatarImageView.defaultBackgroundColor = theme.colors.tile
}
}

View file

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="SpaceAvatarView" customModule="Riot" customModuleProvider="target">
<connections>
<outlet property="avatarImageView" destination="ln9-Sd-GKd" id="9Zd-LM-hgl"/>
<outlet property="cameraBadgeContainerView" destination="0YT-CK-WjK" id="T31-nq-8PB"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="80" height="80"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="iK1-yG-fEu">
<rect key="frame" x="0.0" y="0.0" width="80" height="80"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ln9-Sd-GKd" customClass="MXKImageView">
<rect key="frame" x="0.0" y="0.0" width="80" height="80"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="0YT-CK-WjK">
<rect key="frame" x="56" y="56" width="24" height="24"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="capture_avatar" translatesAutoresizingMaskIntoConstraints="NO" id="vGE-Mx-xPX">
<rect key="frame" x="0.0" y="0.0" width="24" height="24"/>
</imageView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="24" id="AIT-k4-SJZ"/>
<constraint firstItem="vGE-Mx-xPX" firstAttribute="leading" secondItem="0YT-CK-WjK" secondAttribute="leading" id="IoL-FC-x3t"/>
<constraint firstAttribute="trailing" secondItem="vGE-Mx-xPX" secondAttribute="trailing" id="KDL-CV-LNm"/>
<constraint firstItem="vGE-Mx-xPX" firstAttribute="top" secondItem="0YT-CK-WjK" secondAttribute="top" id="PLP-FV-9fe"/>
<constraint firstAttribute="bottom" secondItem="vGE-Mx-xPX" secondAttribute="bottom" id="bIo-ge-7Ud"/>
<constraint firstAttribute="width" constant="24" id="ugV-gr-fbC"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="0YT-CK-WjK" secondAttribute="trailing" id="8HT-uc-Dd2"/>
<constraint firstAttribute="trailing" secondItem="ln9-Sd-GKd" secondAttribute="trailing" id="ABF-Wz-ZZy"/>
<constraint firstAttribute="height" constant="80" id="GdE-dy-bxN"/>
<constraint firstAttribute="width" constant="80" id="NCu-Xg-4p3"/>
<constraint firstAttribute="bottom" secondItem="ln9-Sd-GKd" secondAttribute="bottom" id="NZp-ao-R0Q"/>
<constraint firstItem="ln9-Sd-GKd" firstAttribute="top" secondItem="iK1-yG-fEu" secondAttribute="top" id="VZt-Uu-toa"/>
<constraint firstItem="ln9-Sd-GKd" firstAttribute="leading" secondItem="iK1-yG-fEu" secondAttribute="leading" id="jEs-Ac-ock"/>
<constraint firstAttribute="bottom" secondItem="0YT-CK-WjK" secondAttribute="bottom" id="wIN-TR-RZm"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="iK1-yG-fEu" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="GZx-C7-FF8"/>
<constraint firstAttribute="trailing" secondItem="iK1-yG-fEu" secondAttribute="trailing" id="UPO-MZ-XUZ"/>
<constraint firstAttribute="bottom" secondItem="iK1-yG-fEu" secondAttribute="bottom" id="g7y-SL-f3s"/>
<constraint firstItem="iK1-yG-fEu" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="imn-uW-TTP"/>
</constraints>
<nil key="simulatedTopBarMetrics"/>
<nil key="simulatedBottomBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="-433" y="-802"/>
</view>
</objects>
<resources>
<image name="capture_avatar" width="25" height="25"/>
</resources>
</document>

View file

@ -0,0 +1,139 @@
//
// 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
/// Presenter for space detail screen
class SpaceDetailPresenter: NSObject {
// MARK: - Constants
enum Actions {
case exploreRooms
case exploreMembers
}
// MARK: - Properties
@objc public weak var delegate: SpaceDetailPresenterDelegate?
// MARK: Private
private weak var presentingViewController: UIViewController?
private var viewModel: SpaceDetailViewModel!
private weak var sourceView: UIView?
private lazy var slidingModalPresenter: SlidingModalPresenter = {
return SlidingModalPresenter()
}()
private var session: MXSession!
private var spaceId: String!
// MARK: - Public
@objc func present(forSpaceWithId spaceId: String,
from viewController: UIViewController,
sourceView: UIView?,
session: MXSession,
animated: Bool) {
self.session = session
self.spaceId = spaceId
self.viewModel = SpaceDetailViewModel(session: session, spaceId: spaceId)
self.viewModel.coordinatorDelegate = self
self.presentingViewController = viewController
self.sourceView = sourceView
self.show(with: session)
}
@objc func present(forSpaceWithPublicRoom publicRoom: MXPublicRoom,
from viewController: UIViewController,
sourceView: UIView?,
session: MXSession,
animated: Bool) {
self.session = session
self.spaceId = publicRoom.roomId
self.viewModel = SpaceDetailViewModel(session: session, publicRoom: publicRoom)
self.viewModel.coordinatorDelegate = self
self.presentingViewController = viewController
self.sourceView = sourceView
self.show(with: session)
}
func dismiss(animated: Bool, completion: (() -> Void)?) {
self.presentingViewController?.dismiss(animated: animated, completion: completion)
}
// MARK: - Private
private func show(with session: MXSession) {
let viewController = SpaceDetailViewController.instantiate(mediaManager: session.mediaManager, viewModel: self.viewModel)
self.present(viewController, animated: true)
}
private func present(_ viewController: SpaceDetailViewController, animated: Bool) {
if UIDevice.current.isPhone {
guard let rootViewController = self.presentingViewController else {
MXLog.error("[SpaceDetailPresenter] present no rootViewController found")
return
}
slidingModalPresenter.present(viewController, from: rootViewController.presentedViewController ?? rootViewController, animated: true, completion: nil)
} else {
// Configure source view when view controller is presented with a popover
viewController.modalPresentationStyle = .popover
if let sourceView = self.sourceView, let popoverPresentationController = viewController.popoverPresentationController {
popoverPresentationController.sourceView = sourceView
popoverPresentationController.sourceRect = sourceView.bounds
}
self.presentingViewController?.present(viewController, animated: animated, completion: nil)
}
}
}
// MARK: - SpaceDetailModelViewModelCoordinatorDelegate
extension SpaceDetailPresenter: SpaceDetailModelViewModelCoordinatorDelegate {
func spaceDetailViewModelDidJoin(_ viewModel: SpaceDetailViewModelType) {
self.dismiss(animated: true) {
self.delegate?.spaceDetailPresenter(self, didJoinSpaceWithId: self.spaceId)
}
}
func spaceDetailViewModelDidOpen(_ viewModel: SpaceDetailViewModelType) {
self.dismiss(animated: false) {
self.delegate?.spaceDetailPresenter(self, didOpenSpaceWithId: self.spaceId)
}
}
func spaceDetailViewModelDidCancel(_ viewModel: SpaceDetailViewModelType) {
self.dismiss(animated: true, completion: nil)
}
func spaceDetailViewModelDidDismiss(_ viewModel: SpaceDetailViewModelType) {
self.delegate?.spaceDetailPresenterDidComplete(self)
}
}
@objc protocol SpaceDetailPresenterDelegate: AnyObject {
func spaceDetailPresenterDidComplete(_ presenter: SpaceDetailPresenter)
func spaceDetailPresenter(_ presenter: SpaceDetailPresenter, didJoinSpaceWithId spaceId: String)
func spaceDetailPresenter(_ presenter: SpaceDetailPresenter, didOpenSpaceWithId spaceId: String)
}

View file

@ -0,0 +1,27 @@
//
// 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
/// `SpaceDetailViewController` view actions exposed to view model
enum SpaceDetailViewAction {
case loadData
case join
case open
case leave
case dismiss
case dismissed
}

View file

@ -0,0 +1,238 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Y6W-OH-hqX">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Space Detail View Controller-->
<scene sceneID="s0d-6b-0kx">
<objects>
<viewController extendedLayoutIncludesOpaqueBars="YES" id="Y6W-OH-hqX" customClass="SpaceDetailViewController" customModule="Riot" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
<rect key="frame" x="0.0" y="0.0" width="414" height="842"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="22X-aK-4D2">
<rect key="frame" x="16" y="16" width="382" height="76"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="yVi-9K-5iE" customClass="RoomAvatarView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="4" width="32" height="32"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="32" id="OZZ-dW-Uuc"/>
<constraint firstAttribute="width" constant="32" id="qAF-jw-btk"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="display name invited you" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" adjustsFontForContentSizeCategory="YES" translatesAutoresizingMaskIntoConstraints="NO" id="oZk-F6-3nn">
<rect key="frame" x="44" y="0.0" width="306" height="17"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="@userid:matrix.org" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" adjustsFontForContentSizeCategory="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Vw0-9q-U23">
<rect key="frame" x="44" y="19" width="306" height="17"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="GbA-LS-7G8">
<rect key="frame" x="0.0" y="50" width="382" height="1"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="height" constant="1" id="PSu-zU-1Vr"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="Vw0-9q-U23" secondAttribute="trailing" constant="32" id="35Q-0D-eaP"/>
<constraint firstItem="Vw0-9q-U23" firstAttribute="leading" secondItem="yVi-9K-5iE" secondAttribute="trailing" constant="12" id="5jU-Wj-fli"/>
<constraint firstAttribute="height" constant="76" id="GXB-6p-EIC"/>
<constraint firstAttribute="trailing" secondItem="GbA-LS-7G8" secondAttribute="trailing" id="HI3-0y-tyY"/>
<constraint firstItem="Vw0-9q-U23" firstAttribute="bottom" secondItem="yVi-9K-5iE" secondAttribute="bottom" id="IxE-pA-5bx"/>
<constraint firstItem="oZk-F6-3nn" firstAttribute="leading" secondItem="yVi-9K-5iE" secondAttribute="trailing" constant="12" id="UAC-PF-u3I"/>
<constraint firstItem="yVi-9K-5iE" firstAttribute="leading" secondItem="22X-aK-4D2" secondAttribute="leading" id="VDT-b0-SqV"/>
<constraint firstItem="oZk-F6-3nn" firstAttribute="top" secondItem="22X-aK-4D2" secondAttribute="top" id="Xkw-oz-SUD"/>
<constraint firstItem="GbA-LS-7G8" firstAttribute="top" secondItem="yVi-9K-5iE" secondAttribute="bottom" constant="14" id="eWu-Q8-50q"/>
<constraint firstAttribute="trailing" secondItem="oZk-F6-3nn" secondAttribute="trailing" constant="32" id="hSf-zB-m6e"/>
<constraint firstItem="GbA-LS-7G8" firstAttribute="leading" secondItem="22X-aK-4D2" secondAttribute="leading" id="o4Q-41-AfC"/>
<constraint firstItem="yVi-9K-5iE" firstAttribute="top" secondItem="22X-aK-4D2" secondAttribute="top" constant="4" id="x9I-eQ-C0t"/>
</constraints>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dxd-y5-bn4">
<rect key="frame" x="374" y="16" width="24" height="24"/>
<color key="backgroundColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="24" id="TTq-xS-O71"/>
<constraint firstAttribute="width" constant="24" id="gmo-Ip-cjr"/>
</constraints>
<state key="normal" image="space_menu_close"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
<integer key="value" value="14"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
<connections>
<action selector="closeActionWithSender:" destination="Y6W-OH-hqX" eventType="touchUpInside" id="TIh-gS-svg"/>
</connections>
</button>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="aSn-OV-epF" customClass="SpaceAvatarView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="16" y="92" width="66" height="66"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="66" id="N0q-nk-kG6"/>
<constraint firstAttribute="width" secondItem="aSn-OV-epF" secondAttribute="height" multiplier="1:1" id="X3x-O0-Cpp"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="1000" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3Mp-yr-jUa">
<rect key="frame" x="16" y="182" width="382" height="17"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="space_type_icon" translatesAutoresizingMaskIntoConstraints="NO" id="5eT-si-nJh">
<rect key="frame" x="16" y="212" width="16" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="16" id="Hmz-Ud-9NT"/>
<constraint firstAttribute="height" constant="16" id="sak-fG-0Id"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" text="44" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ko6-Oy-KB4">
<rect key="frame" x="37" y="212" width="17" height="16"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCallout"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="VRt-iQ-AXx">
<rect key="frame" x="16" y="244" width="382" height="488"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Rg1-rU-wKD">
<rect key="frame" x="0.0" y="0.0" width="382" height="488"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="Rg1-rU-wKD" firstAttribute="height" relation="greaterThanOrEqual" secondItem="VRt-iQ-AXx" secondAttribute="height" id="AWs-5c-xxU"/>
<constraint firstItem="Rg1-rU-wKD" firstAttribute="bottom" secondItem="ynp-n0-iet" secondAttribute="bottom" id="EqK-uS-dLH"/>
<constraint firstItem="Rg1-rU-wKD" firstAttribute="width" secondItem="VRt-iQ-AXx" secondAttribute="width" id="F6n-H6-yE4"/>
<constraint firstItem="Rg1-rU-wKD" firstAttribute="leading" secondItem="ynp-n0-iet" secondAttribute="leading" id="Rsa-EB-og8"/>
<constraint firstItem="Rg1-rU-wKD" firstAttribute="trailing" secondItem="ynp-n0-iet" secondAttribute="trailing" constant="382" id="X2l-nM-1cX"/>
<constraint firstItem="Rg1-rU-wKD" firstAttribute="top" secondItem="ynp-n0-iet" secondAttribute="top" id="XXS-M9-hY3"/>
</constraints>
<viewLayoutGuide key="contentLayoutGuide" id="ynp-n0-iet"/>
<viewLayoutGuide key="frameLayoutGuide" id="REH-HY-tM4"/>
</scrollView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xQH-D8-TVA">
<rect key="frame" x="16" y="748" width="382" height="44"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="UHd-DI-8BX"/>
</constraints>
<state key="normal" title="OK"/>
<connections>
<action selector="joinActionWithSender:" destination="Y6W-OH-hqX" eventType="touchUpInside" id="mE9-eR-UXZ"/>
</connections>
</button>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="N1F-Ko-xvc">
<rect key="frame" x="16" y="748" width="382" height="44"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="utA-Mz-rmH">
<rect key="frame" x="0.0" y="0.0" width="189" height="44"/>
<state key="normal" title="DECLINE"/>
<connections>
<action selector="leaveActionWithSender:" destination="Y6W-OH-hqX" eventType="touchUpInside" id="O7a-Cd-3oX"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="JPi-uh-vpV">
<rect key="frame" x="205" y="0.0" width="177" height="44"/>
<state key="normal" title="ACCEPT"/>
<connections>
<action selector="joinActionWithSender:" destination="Y6W-OH-hqX" eventType="touchUpInside" id="4eZ-Um-eDh"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="JPi-uh-vpV" firstAttribute="leading" secondItem="utA-Mz-rmH" secondAttribute="trailing" constant="16" id="7Ga-c0-OCJ"/>
<constraint firstItem="utA-Mz-rmH" firstAttribute="width" secondItem="JPi-uh-vpV" secondAttribute="width" multiplier="1.06897" id="8QQ-go-9xV"/>
<constraint firstAttribute="trailing" secondItem="JPi-uh-vpV" secondAttribute="trailing" id="I13-lD-tED"/>
<constraint firstAttribute="bottom" secondItem="utA-Mz-rmH" secondAttribute="bottom" id="QiF-ea-a0Y"/>
<constraint firstItem="utA-Mz-rmH" firstAttribute="top" secondItem="N1F-Ko-xvc" secondAttribute="top" id="U4Y-G0-YSz"/>
<constraint firstItem="JPi-uh-vpV" firstAttribute="top" secondItem="N1F-Ko-xvc" secondAttribute="top" id="aPS-mC-5DE"/>
<constraint firstAttribute="bottom" secondItem="JPi-uh-vpV" secondAttribute="bottom" id="nIm-Mj-Ich"/>
<constraint firstItem="utA-Mz-rmH" firstAttribute="leading" secondItem="N1F-Ko-xvc" secondAttribute="leading" id="yib-Eb-TGj"/>
</constraints>
</view>
</subviews>
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="5eT-si-nJh" firstAttribute="leading" secondItem="3Mp-yr-jUa" secondAttribute="leading" id="0Mu-pd-nWz"/>
<constraint firstItem="vDu-zF-Fre" firstAttribute="trailing" secondItem="N1F-Ko-xvc" secondAttribute="trailing" constant="16" id="0pd-eF-og1"/>
<constraint firstItem="5eT-si-nJh" firstAttribute="top" secondItem="3Mp-yr-jUa" secondAttribute="bottom" constant="13" id="0z7-5Y-ha9"/>
<constraint firstItem="vDu-zF-Fre" firstAttribute="trailing" secondItem="xQH-D8-TVA" secondAttribute="trailing" constant="16" id="2GI-ap-K0l"/>
<constraint firstItem="vDu-zF-Fre" firstAttribute="trailing" secondItem="dxd-y5-bn4" secondAttribute="trailing" constant="16" id="3FU-wC-Uy7"/>
<constraint firstItem="aSn-OV-epF" firstAttribute="width" secondItem="aSn-OV-epF" secondAttribute="height" multiplier="1:1" id="6ng-Dc-MPf"/>
<constraint firstItem="ko6-Oy-KB4" firstAttribute="centerY" secondItem="5eT-si-nJh" secondAttribute="centerY" id="9D6-Kl-bei"/>
<constraint firstItem="3Mp-yr-jUa" firstAttribute="top" secondItem="aSn-OV-epF" secondAttribute="bottom" constant="24" id="9h6-Ic-hmQ"/>
<constraint firstItem="N1F-Ko-xvc" firstAttribute="bottom" secondItem="xQH-D8-TVA" secondAttribute="bottom" id="HfP-a7-LJV"/>
<constraint firstItem="3Mp-yr-jUa" firstAttribute="leading" secondItem="vDu-zF-Fre" secondAttribute="leading" constant="16" id="HxH-M7-Fy1"/>
<constraint firstItem="xQH-D8-TVA" firstAttribute="top" secondItem="VRt-iQ-AXx" secondAttribute="bottom" constant="16" id="MfS-3y-K9f"/>
<constraint firstItem="vDu-zF-Fre" firstAttribute="bottom" secondItem="xQH-D8-TVA" secondAttribute="bottom" constant="16" id="MhW-nH-ei4"/>
<constraint firstItem="VRt-iQ-AXx" firstAttribute="leading" secondItem="vDu-zF-Fre" secondAttribute="leading" constant="16" id="OWC-of-jEz"/>
<constraint firstItem="VRt-iQ-AXx" firstAttribute="top" secondItem="5eT-si-nJh" secondAttribute="bottom" constant="16" id="OsF-8y-uSK"/>
<constraint firstItem="xQH-D8-TVA" firstAttribute="leading" secondItem="vDu-zF-Fre" secondAttribute="leading" constant="16" id="PRN-1R-lXK"/>
<constraint firstItem="vDu-zF-Fre" firstAttribute="trailing" secondItem="3Mp-yr-jUa" secondAttribute="trailing" constant="16" id="Rg7-2n-fPo"/>
<constraint firstItem="vDu-zF-Fre" firstAttribute="trailing" secondItem="VRt-iQ-AXx" secondAttribute="trailing" constant="16" id="T3h-BY-ke9"/>
<constraint firstItem="N1F-Ko-xvc" firstAttribute="leading" secondItem="vDu-zF-Fre" secondAttribute="leading" constant="16" id="XIm-NG-zh8"/>
<constraint firstItem="ko6-Oy-KB4" firstAttribute="leading" secondItem="5eT-si-nJh" secondAttribute="trailing" constant="5" id="e98-Ro-OCy"/>
<constraint firstItem="dxd-y5-bn4" firstAttribute="top" secondItem="vDu-zF-Fre" secondAttribute="top" constant="16" id="fBu-Zb-akl"/>
<constraint firstItem="aSn-OV-epF" firstAttribute="top" secondItem="22X-aK-4D2" secondAttribute="bottom" id="g4t-WH-Kbz"/>
<constraint firstItem="N1F-Ko-xvc" firstAttribute="top" secondItem="xQH-D8-TVA" secondAttribute="top" id="hE9-gh-tH4"/>
<constraint firstItem="aSn-OV-epF" firstAttribute="leading" secondItem="vDu-zF-Fre" secondAttribute="leading" constant="16" id="hem-oS-VnE"/>
<constraint firstItem="vDu-zF-Fre" firstAttribute="trailing" secondItem="22X-aK-4D2" secondAttribute="trailing" constant="16" id="q8c-eN-9im"/>
<constraint firstItem="22X-aK-4D2" firstAttribute="top" secondItem="vDu-zF-Fre" secondAttribute="top" constant="16" id="ufQ-NF-PUt"/>
<constraint firstItem="22X-aK-4D2" firstAttribute="leading" secondItem="vDu-zF-Fre" secondAttribute="leading" constant="16" id="vdh-02-FdI"/>
</constraints>
</view>
<modalPageSheetSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="acceptButton" destination="JPi-uh-vpV" id="MuZ-ah-AIm"/>
<outlet property="avatarView" destination="aSn-OV-epF" id="kgk-RU-l5L"/>
<outlet property="closeButton" destination="dxd-y5-bn4" id="T5W-Ah-JMq"/>
<outlet property="declineButton" destination="utA-Mz-rmH" id="UwR-LU-rv5"/>
<outlet property="inviteActionPanel" destination="N1F-Ko-xvc" id="yjc-4a-nkf"/>
<outlet property="inviterAvatarView" destination="yVi-9K-5iE" id="qBp-MT-d3U"/>
<outlet property="inviterIdLabel" destination="Vw0-9q-U23" id="RxX-Zo-frz"/>
<outlet property="inviterPanelHeight" destination="GXB-6p-EIC" id="VCL-wF-kiK"/>
<outlet property="inviterSeparatorView" destination="GbA-LS-7G8" id="wzH-LB-Hsv"/>
<outlet property="inviterTitleLabel" destination="oZk-F6-3nn" id="1Ih-UD-XYM"/>
<outlet property="joinButton" destination="xQH-D8-TVA" id="PUa-fv-FOK"/>
<outlet property="joinButtonBottomMargin" destination="MhW-nH-ei4" id="w7A-jz-twK"/>
<outlet property="joinButtonTopMargin" destination="MfS-3y-K9f" id="90t-7l-MPe"/>
<outlet property="spaceTypeIconView" destination="5eT-si-nJh" id="AIS-HH-xrs"/>
<outlet property="spaceTypeLabel" destination="ko6-Oy-KB4" id="QhM-7w-ipS"/>
<outlet property="titleLabel" destination="3Mp-yr-jUa" id="Dhq-d3-4lb"/>
<outlet property="topicLabel" destination="Rg1-rU-wKD" id="EYy-cs-M08"/>
<outlet property="topicScrollView" destination="VRt-iQ-AXx" id="6Ti-Cm-gcP"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-117.39130434782609" y="69.642857142857139"/>
</scene>
</scenes>
<resources>
<image name="space_menu_close" width="10" height="10.5"/>
<image name="space_type_icon" width="12" height="12.5"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View file

@ -0,0 +1,278 @@
//
// 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
class SpaceDetailViewController: UIViewController {
// MARK: - Constants
private enum Constants {
static let popoverWidth: CGFloat = 320
static let topicMaxHeight: CGFloat = 105
}
// MARK: Private
private var theme: Theme!
private var mediaManager: MXMediaManager!
private var viewModel: SpaceDetailViewModelType!
private var errorPresenter: MXKErrorPresentation!
private var activityPresenter: ActivityIndicatorPresenter!
private var isJoined: Bool = false
// MARK: Outlets
@IBOutlet private weak var inviterPanelHeight: NSLayoutConstraint!
@IBOutlet private weak var inviterAvatarView: RoomAvatarView!
@IBOutlet private weak var inviterTitleLabel: UILabel!
@IBOutlet private weak var inviterIdLabel: UILabel!
@IBOutlet private weak var inviterSeparatorView: UIView!
@IBOutlet private weak var avatarView: SpaceAvatarView!
@IBOutlet private weak var titleLabel: UILabel!
@IBOutlet private weak var closeButton: UIButton!
@IBOutlet private weak var spaceTypeIconView: UIImageView!
@IBOutlet private weak var spaceTypeLabel: UILabel!
@IBOutlet private weak var topicLabel: UILabel!
@IBOutlet private weak var topicScrollView: UIScrollView!
@IBOutlet private weak var joinButtonTopMargin: NSLayoutConstraint!
@IBOutlet private weak var joinButtonBottomMargin: NSLayoutConstraint!
@IBOutlet private weak var joinButton: UIButton!
@IBOutlet private weak var declineButton: UIButton!
@IBOutlet private weak var acceptButton: UIButton!
@IBOutlet private weak var inviteActionPanel: UIView!
// MARK: - Setup
class func instantiate(mediaManager: MXMediaManager, viewModel: SpaceDetailViewModelType!) -> SpaceDetailViewController {
let viewController = StoryboardScene.SpaceDetailViewController.initialScene.instantiate()
viewController.mediaManager = mediaManager
viewController.viewModel = viewModel
viewController.theme = ThemeService.shared().theme
return viewController
}
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.setupViews()
self.activityPresenter = ActivityIndicatorPresenter()
self.errorPresenter = MXKErrorAlertPresentation()
self.registerThemeServiceDidChangeThemeNotification()
self.update(theme: self.theme)
self.viewModel.viewDelegate = self
self.viewModel.process(viewAction: .loadData)
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return self.theme.statusBarStyle
}
override var preferredContentSize: CGSize {
get {
return CGSize(width: Constants.popoverWidth, height: self.intrisicHeight(with: Constants.popoverWidth))
}
set {
super.preferredContentSize = newValue
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.viewModel.process(viewAction: .dismissed)
}
// MARK: - IBActions
@IBAction private func closeAction(sender: UIButton) {
self.viewModel.process(viewAction: .dismiss)
}
@IBAction private func joinAction(sender: UIButton) {
if isJoined {
self.viewModel.process(viewAction: .open)
} else {
self.viewModel.process(viewAction: .join)
}
}
@IBAction private func leaveAction(sender: UIButton) {
self.viewModel.process(viewAction: .leave)
}
// MARK: - Private
private func update(theme: Theme) {
self.theme = theme
self.view.backgroundColor = theme.colors.background
self.inviterAvatarView.update(theme: theme)
self.inviterTitleLabel.textColor = theme.colors.secondaryContent
self.inviterTitleLabel.font = theme.fonts.calloutSB
self.inviterIdLabel.textColor = theme.colors.secondaryContent
self.inviterIdLabel.font = theme.fonts.footnote
self.inviterSeparatorView.backgroundColor = theme.colors.navigation
self.titleLabel.textColor = theme.colors.primaryContent
self.titleLabel.font = theme.fonts.title3SB
self.closeButton.backgroundColor = theme.roomInputTextBorder
self.closeButton.tintColor = theme.noticeSecondaryColor
self.avatarView.update(theme: theme)
self.spaceTypeIconView.tintColor = theme.colors.tertiaryContent
self.spaceTypeLabel.font = theme.fonts.callout
self.spaceTypeLabel.textColor = theme.colors.tertiaryContent
self.topicLabel.font = theme.fonts.caption1
self.topicLabel.textColor = theme.colors.tertiaryContent
apply(theme: theme, on: self.joinButton)
apply(theme: theme, on: self.acceptButton)
self.declineButton.layer.borderColor = theme.colors.alert.cgColor
self.declineButton.tintColor = theme.colors.alert
self.declineButton.setTitleColor(theme.colors.alert, for: .normal)
self.declineButton.titleLabel?.font = theme.fonts.body
}
private func apply(theme: Theme, on button: UIButton) {
button.backgroundColor = theme.colors.accent
button.tintColor = theme.colors.background
button.setTitleColor(theme.colors.background, for: .normal)
button.titleLabel?.font = theme.fonts.bodySB
}
private func registerThemeServiceDidChangeThemeNotification() {
NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil)
}
@objc private func themeDidChange() {
self.update(theme: ThemeService.shared().theme)
}
private func setupViews() {
self.closeButton.layer.masksToBounds = true
self.closeButton.layer.cornerRadius = self.closeButton.bounds.height / 2
self.setup(button: self.joinButton, withTitle: VectorL10n.join)
self.setup(button: self.acceptButton, withTitle: VectorL10n.accept)
self.setup(button: self.declineButton, withTitle: VectorL10n.decline)
self.declineButton.layer.borderWidth = 1.0
}
private func setup(button: UIButton, withTitle title: String) {
button.layer.masksToBounds = true
button.layer.cornerRadius = 8.0
button.setTitle(title.uppercased(), for: .normal)
}
private func render(viewState: SpaceDetailViewState) {
switch viewState {
case .loading:
self.renderLoading()
case .loaded(let parameters):
self.renderLoaded(parameters: parameters)
case .error(let error):
self.render(error: error)
}
}
private func renderLoading() {
self.activityPresenter.presentActivityIndicator(on: self.view, animated: true)
}
private func renderLoaded(parameters: SpaceDetailLoadedParameters) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
switch parameters.membership {
case .invite:
self.joinButton.isHidden = true
self.inviteActionPanel.isHidden = false
case .join:
self.inviterPanelHeight.constant = 0
self.joinButton.setTitle(VectorL10n.open, for: .normal)
self.isJoined = true
default:
self.inviterPanelHeight.constant = 0
}
let avatarViewData = AvatarViewData(matrixItemId: parameters.spaceId, displayName: parameters.displayName, avatarUrl: parameters.avatarUrl, mediaManager: self.mediaManager, fallbackImage: .matrixItem(parameters.spaceId, parameters.displayName))
self.titleLabel.text = parameters.displayName
self.avatarView.fill(with: avatarViewData)
self.topicLabel.text = parameters.topic
let joinRuleString = parameters.joinRule == .public ? VectorL10n.spacePublicJoinRule : VectorL10n.spacePrivateJoinRule
let membersCount = parameters.membersCount
let membersString = membersCount == 1 ? VectorL10n.roomTitleOneMember : VectorL10n.roomTitleMembers("\(membersCount)")
self.spaceTypeLabel.text = "\(joinRuleString) · \(membersString)"
self.inviterIdLabel.text = parameters.inviterId
if let inviterId = parameters.inviterId {
self.inviterTitleLabel.text = "\(parameters.inviter?.displayname ?? inviterId) invited you"
if let inviter = parameters.inviter {
let avatarViewData = AvatarViewData(matrixItemId: inviter.userId, displayName: inviter.displayname, avatarUrl: inviter.avatarUrl, mediaManager: self.mediaManager, fallbackImage: .matrixItem(inviter.userId, inviter.displayname))
self.inviterAvatarView.fill(with: avatarViewData)
}
}
view.layoutIfNeeded()
}
private func render(error: Error) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil)
}
private func intrisicHeight(with width: CGFloat) -> CGFloat {
let topicHeight = min(self.topicLabel.sizeThatFits(CGSize(width: width - self.topicScrollView.frame.minX * 2, height: 0)).height, Constants.topicMaxHeight)
return self.topicScrollView.frame.minY + topicHeight + self.joinButton.frame.height + self.joinButtonTopMargin.constant + self.joinButtonBottomMargin.constant
}
}
// MARK: - SlidingModalPresentable
extension SpaceDetailViewController: SlidingModalPresentable {
func allowsDismissOnBackgroundTap() -> Bool {
return true
}
func layoutHeightFittingWidth(_ width: CGFloat) -> CGFloat {
return self.intrisicHeight(with: width) + self.joinButtonTopMargin.constant + self.joinButtonBottomMargin.constant
}
}
// MARK: - SpaceDetailViewModelViewDelegate
extension SpaceDetailViewController: SpaceDetailViewModelViewDelegate {
func spaceDetailViewModel(_ viewModel: SpaceDetailViewModelType, didUpdateViewState viewSate: SpaceDetailViewState) {
self.render(viewState: viewSate)
}
}

View file

@ -0,0 +1,130 @@
//
// 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
/// View model used by `SpaceDetailViewController`
class SpaceDetailViewModel: SpaceDetailViewModelType {
// MARK: - Properties
weak var coordinatorDelegate: SpaceDetailModelViewModelCoordinatorDelegate?
weak var viewDelegate: SpaceDetailViewModelViewDelegate?
private let session: MXSession
private let spaceId: String
private let publicRoom: MXPublicRoom?
// MARK: - Setup
init(session: MXSession, spaceId: String) {
self.session = session
self.spaceId = spaceId
self.publicRoom = nil
}
init(session: MXSession, publicRoom: MXPublicRoom) {
self.session = session
self.publicRoom = publicRoom
self.spaceId = publicRoom.roomId
}
// MARK: - Public
func process(viewAction: SpaceDetailViewAction) {
switch viewAction {
case .loadData:
self.loadData()
case .join:
self.join()
case .leave:
self.leave()
case .open:
self.coordinatorDelegate?.spaceDetailViewModelDidOpen(self)
case .dismiss:
self.coordinatorDelegate?.spaceDetailViewModelDidCancel(self)
case .dismissed:
self.coordinatorDelegate?.spaceDetailViewModelDidDismiss(self)
}
}
// MARK: - Private
private func update(viewState: SpaceDetailViewState) {
self.viewDelegate?.spaceDetailViewModel(self, didUpdateViewState: viewState)
}
private func loadData() {
if let publicRoom = self.publicRoom {
self.update(viewState: .loaded(SpaceDetailLoadedParameters(spaceId: publicRoom.roomId, displayName: publicRoom.displayname(), topic: publicRoom.topic, avatarUrl: publicRoom.avatarUrl, joinRule: publicRoom.worldReadable ? .public : .private, membership: .unknown, inviterId: nil, inviter: nil, membersCount: UInt(publicRoom.numJoinedMembers))))
} else {
guard let space = self.session.spaceService.getSpace(withId: self.spaceId), let summary = space.summary else {
MXLog.error("[SpaceDetailViewModel] setupViews: no space found")
return
}
let parameters = SpaceDetailLoadedParameters(spaceId: space.spaceId, displayName: summary.displayname, topic: summary.topic, avatarUrl: summary.avatar, joinRule: nil, membership: summary.membership, inviterId: nil, inviter: nil, membersCount: 0)
self.update(viewState: .loaded(parameters))
self.update(viewState: .loading)
space.room.state { state in
let joinRule = state?.joinRule
let membersCount = summary.membersCount.members
var inviterId: String?
var inviter: MXUser?
state?.stateEvents.forEach({ event in
if event.wireEventType == .roomMember && event.stateKey == self.session.myUserId {
guard let userId = event.sender else {
return
}
inviterId = userId
inviter = self.session.user(withUserId: userId)
}
})
let parameters = SpaceDetailLoadedParameters(spaceId: space.spaceId, displayName: summary.displayname, topic: summary.topic, avatarUrl: summary.avatar, joinRule: joinRule, membership: summary.membership, inviterId: inviterId, inviter: inviter, membersCount: membersCount)
self.update(viewState: .loaded(parameters))
}
}
}
private func join() {
self.update(viewState: .loading)
self.session.joinRoom(self.spaceId) { [weak self] (response) in
guard let self = self else { return }
switch response {
case .success:
self.coordinatorDelegate?.spaceDetailViewModelDidJoin(self)
case .failure(let error):
self.update(viewState: .error(error))
}
}
}
private func leave() {
self.update(viewState: .loading)
self.session.leaveRoom(self.spaceId) { [weak self] (response) in
guard let self = self else { return }
switch response {
case .success:
self.process(viewAction: .dismiss)
case .failure(let error):
self.update(viewState: .error(error))
}
}
}
}

View file

@ -0,0 +1,37 @@
//
// 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 SpaceDetailViewModelViewDelegate: AnyObject {
func spaceDetailViewModel(_ viewModel: SpaceDetailViewModelType, didUpdateViewState viewSate: SpaceDetailViewState)
}
protocol SpaceDetailModelViewModelCoordinatorDelegate: AnyObject {
func spaceDetailViewModelDidCancel(_ viewModel: SpaceDetailViewModelType)
func spaceDetailViewModelDidDismiss(_ viewModel: SpaceDetailViewModelType)
func spaceDetailViewModelDidOpen(_ viewModel: SpaceDetailViewModelType)
func spaceDetailViewModelDidJoin(_ viewModel: SpaceDetailViewModelType)
}
/// Protocol describing the view model used by `SpaceDetailViewController`
protocol SpaceDetailViewModelType {
var viewDelegate: SpaceDetailViewModelViewDelegate? { get set }
var coordinatorDelegate: SpaceDetailModelViewModelCoordinatorDelegate? { get set }
func process(viewAction: SpaceDetailViewAction)
}

View file

@ -0,0 +1,36 @@
//
// 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
struct SpaceDetailLoadedParameters {
let spaceId: String
let displayName: String?
let topic: String?
let avatarUrl: String?
let joinRule: MXRoomJoinRule?
let membership: MXMembership
let inviterId: String?
let inviter: MXUser?
let membersCount: UInt
}
/// SpaceDetailViewController view state
enum SpaceDetailViewState {
case loading
case loaded(_ paremeters: SpaceDetailLoadedParameters)
case error(Error)
}

View file

@ -0,0 +1,88 @@
// File created from ScreenTemplate
// $ createScreen.sh Spaces/SpaceList SpaceList
/*
Copyright 2020 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 UIKit
/// Side menu space list
final class SpaceListCoordinator: SpaceListCoordinatorType {
// MARK: - Properties
// MARK: Private
private let parameters: SpaceListCoordinatorParameters
private var spaceListViewModel: SpaceListViewModelType
private let spaceListViewController: SpaceListViewController
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
weak var delegate: SpaceListCoordinatorDelegate?
// MARK: - Setup
init(parameters: SpaceListCoordinatorParameters) {
self.parameters = parameters
let spaceListViewModel = SpaceListViewModel(session: self.parameters.session)
let spaceListViewController = SpaceListViewController.instantiate(with: spaceListViewModel)
self.spaceListViewModel = spaceListViewModel
self.spaceListViewController = spaceListViewController
}
// MARK: - Public methods
func start() {
self.spaceListViewModel.coordinatorDelegate = self
}
func toPresentable() -> UIViewController {
return self.spaceListViewController
}
func revertItemSelection() {
self.spaceListViewModel.revertItemSelection()
}
func select(spaceWithId spaceId: String) {
self.spaceListViewModel.select(spaceWithId: spaceId)
}
}
// MARK: - SpaceListViewModelCoordinatorDelegate
extension SpaceListCoordinator: SpaceListViewModelCoordinatorDelegate {
func spaceListViewModelDidSelectHomeSpace(_ viewModel: SpaceListViewModelType) {
self.delegate?.spaceListCoordinatorDidSelectHomeSpace(self)
}
func spaceListViewModel(_ viewModel: SpaceListViewModelType, didSelectSpaceWithId spaceId: String) {
self.delegate?.spaceListCoordinator(self, didSelectSpaceWithId: spaceId)
}
func spaceListViewModel(_ viewModel: SpaceListViewModelType, didSelectInviteWithId spaceId: String, from sourceView: UIView?) {
self.delegate?.spaceListCoordinator(self, didSelectInviteWithId: spaceId, from: sourceView)
}
func spaceListViewModel(_ viewModel: SpaceListViewModelType, didPressMoreForSpaceWithId spaceId: String, from sourceView: UIView) {
self.delegate?.spaceListCoordinator(self, didPressMoreForSpaceWithId: spaceId, from: sourceView)
}
}

View file

@ -0,0 +1,25 @@
//
// 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
class SpaceListCoordinatorParameters {
let session: MXSession
init(session: MXSession) {
self.session = session
}
}

View file

@ -0,0 +1,33 @@
// File created from ScreenTemplate
// $ createScreen.sh Spaces/SpaceList SpaceList
/*
Copyright 2020 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 SpaceListCoordinatorDelegate: AnyObject {
func spaceListCoordinatorDidSelectHomeSpace(_ coordinator: SpaceListCoordinatorType)
func spaceListCoordinator(_ coordinator: SpaceListCoordinatorType, didSelectSpaceWithId spaceId: String)
func spaceListCoordinator(_ coordinator: SpaceListCoordinatorType, didSelectInviteWithId spaceId: String, from sourceView: UIView?)
func spaceListCoordinator(_ coordinator: SpaceListCoordinatorType, didPressMoreForSpaceWithId spaceId: String, from sourceView: UIView)
}
/// `SpaceListCoordinatorType` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow.
protocol SpaceListCoordinatorType: Coordinator, Presentable {
var delegate: SpaceListCoordinatorDelegate? { get }
func revertItemSelection()
func select(spaceWithId spaceId: String)
}

Some files were not shown because too many files have changed in this diff Show more