Merge pull request #5316 from vector-im/gil/5225_invite_to_space_in_room_landing

Invite to space in room landing
This commit is contained in:
Gil Eluard 2022-02-04 11:13:12 +01:00 committed by GitHub
commit 3051a81e77
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
76 changed files with 2426 additions and 364 deletions

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "space_menu_plus_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "space_menu_plus_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "space_menu_plus_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: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 B

View file

@ -286,6 +286,7 @@ Tap the + to start adding people.";
"room_participants_remove_third_party_invite_prompt_msg" = "Are you sure you want to revoke this invite?";
"room_participants_invite_prompt_title" = "Confirmation";
"room_participants_invite_prompt_msg" = "Are you sure you want to invite %@ to this chat?";
"room_participants_invite_prompt_to_msg" = "Are you sure you want to invite %@ to %@?";
"room_participants_filter_room_members" = "Filter room members";
"room_participants_filter_room_members_for_dm" = "Filter members";
"room_participants_invite_another_user" = "Search / invite by User ID, Name or email";
@ -1718,6 +1719,12 @@ Tap the + to start adding people.";
"invite_friends_action" = "Invite friends to %@";
"invite_friends_share_text" = "Hey, talk to me on %@: %@";
// MARK: - Share invite link
"share_invite_link_action" = "Share invite link";
"share_invite_link_room_text" = "Hey, join this room on %@";
"share_invite_link_space_text" = "Hey, join this space on %@";
// Mark: - Room avatar view
"room_avatar_view_accessibility_label" = "avatar";
@ -1740,6 +1747,15 @@ Tap the + to start adding people.";
"room_intro_cell_information_dm_sentence2" = "Only the two of you are in this conversation, no one else can join.";
"room_intro_cell_information_multiple_dm_sentence2" = "Only you are in this conversation, unless any of you invites someone to join.";
// Mark: - Room invite
"room_invite_to_space_option_title" = "To %@";
"room_invite_to_space_option_detail" = "They can explore %@, but wont be a member of %@.";
"room_invite_to_room_option_title" = "To just this room";
"room_invite_to_room_option_detail" = "They wont be a part of %@.";
"room_invite_not_enough_permission" = "You do not have permission to invite people to this room";
"space_invite_not_enough_permission" = "You do not have permission to invite people to this space";
// Mark: - Spaces
"space_feature_unavailable_title" = "Spaces arent here yet";
@ -1779,6 +1795,9 @@ Tap the + to start adding people.";
"space_private_join_rule" = "Private space";
"space_private_join_rule_detail" = "Invite only, best for yourself or teams";
"space_public_join_rule" = "Public space";
"spaces_invite_people" = "Invite people";
"spaces_add_room" = "Add room";
"spaces_add_space" = "Add space";
"space_public_join_rule_detail" = "Open to anyone, best for communities";
"space_topic" = "description";

View file

@ -206,14 +206,17 @@ internal enum Asset {
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 spaceAddRoom = ImageAsset(name: "space_add_room")
internal static let spaceCreationCamera = ImageAsset(name: "space_creation_camera")
internal static let spaceCreationPrivate = ImageAsset(name: "space_creation_private")
internal static let spaceCreationPublic = ImageAsset(name: "space_creation_public")
internal static let spaceHomeIconDark = ImageAsset(name: "space_home_icon_dark")
internal static let spaceHomeIconLight = ImageAsset(name: "space_home_icon_light")
internal static let spaceInviteUser = ImageAsset(name: "space_invite_user")
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 spaceMenuPlusIcon = ImageAsset(name: "space_menu_plus_icon")
internal static let spaceMenuRooms = ImageAsset(name: "space_menu_rooms")
internal static let spacePrivateIcon = ImageAsset(name: "space_private_icon")
internal static let spaceRoomIcon = ImageAsset(name: "space_room_icon")

View file

@ -147,6 +147,11 @@ internal enum StoryboardScene {
internal static let initialScene = InitialSceneType<Riot.MajorUpdateViewController>(storyboard: MajorUpdateViewController.self)
}
internal enum OptionListViewController: StoryboardType {
internal static let storyboardName = "OptionListViewController"
internal static let initialScene = InitialSceneType<Riot.OptionListViewController>(storyboard: OptionListViewController.self)
}
internal enum QRCodeReaderViewController: StoryboardType {
internal static let storyboardName = "QRCodeReaderViewController"

View file

@ -3299,6 +3299,26 @@ public class VectorL10n: NSObject {
public static var roomIntroCellInformationRoomWithoutTopicSentence2Part2: String {
return VectorL10n.tr("Vector", "room_intro_cell_information_room_without_topic_sentence2_part2")
}
/// You do not have permission to invite people to this room
public static var roomInviteNotEnoughPermission: String {
return VectorL10n.tr("Vector", "room_invite_not_enough_permission")
}
/// They wont be a part of %@.
public static func roomInviteToRoomOptionDetail(_ p1: String) -> String {
return VectorL10n.tr("Vector", "room_invite_to_room_option_detail", p1)
}
/// To just this room
public static var roomInviteToRoomOptionTitle: String {
return VectorL10n.tr("Vector", "room_invite_to_room_option_title")
}
/// They can explore %@, but wont be a member of %@.
public static func roomInviteToSpaceOptionDetail(_ p1: String, _ p2: String) -> String {
return VectorL10n.tr("Vector", "room_invite_to_space_option_detail", p1, p2)
}
/// To %@
public static func roomInviteToSpaceOptionTitle(_ p1: String) -> String {
return VectorL10n.tr("Vector", "room_invite_to_space_option_title", p1)
}
/// Join
public static var roomJoinGroupCall: String {
return VectorL10n.tr("Vector", "room_join_group_call")
@ -3579,6 +3599,10 @@ public class VectorL10n: NSObject {
public static var roomParticipantsInvitePromptTitle: String {
return VectorL10n.tr("Vector", "room_participants_invite_prompt_title")
}
/// Are you sure you want to invite %@ to %@?
public static func roomParticipantsInvitePromptToMsg(_ p1: String, _ p2: String) -> String {
return VectorL10n.tr("Vector", "room_participants_invite_prompt_to_msg", p1, p2)
}
/// INVITED
public static var roomParticipantsInvitedSection: String {
return VectorL10n.tr("Vector", "room_participants_invited_section")
@ -5071,6 +5095,18 @@ public class VectorL10n: NSObject {
public static var shareExtensionSendNow: String {
return VectorL10n.tr("Vector", "share_extension_send_now")
}
/// Share invite link
public static var shareInviteLinkAction: String {
return VectorL10n.tr("Vector", "share_invite_link_action")
}
/// Hey, join this room on %@
public static func shareInviteLinkRoomText(_ p1: String) -> String {
return VectorL10n.tr("Vector", "share_invite_link_room_text", p1)
}
/// Hey, join this space on %@
public static func shareInviteLinkSpaceText(_ p1: String) -> String {
return VectorL10n.tr("Vector", "share_invite_link_space_text", p1)
}
/// Feedback
public static var sideMenuActionFeedback: String {
return VectorL10n.tr("Vector", "side_menu_action_feedback")
@ -5211,6 +5247,10 @@ public class VectorL10n: NSObject {
public static var spaceHomeShowAllRooms: String {
return VectorL10n.tr("Vector", "space_home_show_all_rooms")
}
/// You do not have permission to invite people to this space
public static var spaceInviteNotEnoughPermission: String {
return VectorL10n.tr("Vector", "space_invite_not_enough_permission")
}
/// Ban from this space
public static var spaceParticipantsActionBan: String {
return VectorL10n.tr("Vector", "space_participants_action_ban")
@ -5243,10 +5283,18 @@ public class VectorL10n: NSObject {
public static var spaceTopic: String {
return VectorL10n.tr("Vector", "space_topic")
}
/// Add room
public static var spacesAddRoom: String {
return VectorL10n.tr("Vector", "spaces_add_room")
}
/// Adding rooms coming soon
public static var spacesAddRoomsComingSoonTitle: String {
return VectorL10n.tr("Vector", "spaces_add_rooms_coming_soon_title")
}
/// Add space
public static var spacesAddSpace: String {
return VectorL10n.tr("Vector", "spaces_add_space")
}
/// Create space
public static var spacesAddSpaceTitle: String {
return VectorL10n.tr("Vector", "spaces_add_space_title")
@ -5459,6 +5507,10 @@ public class VectorL10n: NSObject {
public static var spacesHomeSpaceTitle: String {
return VectorL10n.tr("Vector", "spaces_home_space_title")
}
/// Invite people
public static var spacesInvitePeople: String {
return VectorL10n.tr("Vector", "spaces_invite_people")
}
/// Invites coming soon
public static var spacesInvitesComingSoonTitle: String {
return VectorL10n.tr("Vector", "spaces_invites_coming_soon_title")

View file

@ -195,6 +195,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
- (void)setCurrentSpace:(MXSpace *)currentSpace
{
super.currentSpace = currentSpace;
[self.recentsListService updateSpace:currentSpace];
}

View file

@ -141,6 +141,11 @@ FOUNDATION_EXPORT NSString *const RecentsViewControllerDataReadyNotification;
*/
- (void)onPlusButtonPressed;
/**
Open screen to create a new chat room.
*/
- (void)startChat;
/**
Open screen to create a new room.
*/

View file

@ -1867,7 +1867,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
typeof(self) self = weakSelf;
self->currentAlert = nil;
[self performSegueWithIdentifier:@"presentStartChat" sender:self];
[self startChat];
}
}]];
@ -1977,6 +1977,10 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
self.customSizedPresentationController = nil;
}
- (void)startChat {
[self performSegueWithIdentifier:@"presentStartChat" sender:self];
}
- (void)createNewRoom
{
// Sanity check

View file

@ -258,72 +258,7 @@
[self cancelEditionMode:YES];
}
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:[VectorL10n spacesExploreRooms]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->currentAlert = nil;
[self showRoomDirectory];
}
}]];
[currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomDetailsPeople]
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:[MatrixKitL10n 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];
[super onPlusButtonPressed];
}
- (void)cancelEditionMode:(BOOL)forceRefresh
@ -361,6 +296,31 @@
[self updateEmptyView];
}
- (void)createNewRoom
{
if (recentsDataSource.currentSpace)
{
[[AppDelegate theDelegate] showAlertWithTitle:VectorL10n.spacesAddRoomsComingSoonTitle message:[VectorL10n spacesComingSoonDetail:AppInfo.current.displayName]];
}
else
{
[super createNewRoom];
}
}
- (void)startChat {
if (recentsDataSource.currentSpace)
{
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
{
[super startChat];
}
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView

View file

@ -68,8 +68,9 @@
// This will be used by the shared RecentsDataSource instance for sanity checks (see UITableViewDataSource methods).
self.recentsTableView.tag = RecentsDataSourceModePeople;
UIImage *fabImage = self.dataSource.currentSpace == nil ? [UIImage imageNamed:@"people_floating_action"] : [UIImage imageNamed:@"add_member_floating_action"];
// Add the (+) button programmatically
plusButtonImageView = [self vc_addFABWithImage:[UIImage imageNamed:@"people_floating_action"]
plusButtonImageView = [self vc_addFABWithImage:fabImage
target:self
action:@selector(onPlusButtonPressed)];
}

View file

@ -16,10 +16,9 @@
#import "SegmentedViewController.h"
#import "ContactsTableViewController.h"
@class Contact;
@class RoomParticipantsViewController;
@class AnalyticsScreenTimer;
/**
`RoomParticipantsViewController` delegate.
@ -42,7 +41,7 @@
'RoomParticipantsViewController' instance is used to edit members of the room defined by the property 'mxRoom'.
When this property is nil, the view controller is empty.
*/
@interface RoomParticipantsViewController : MXKViewController <UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, UIGestureRecognizerDelegate, MXKRoomMemberDetailsViewControllerDelegate, ContactsTableViewControllerDelegate>
@interface RoomParticipantsViewController : MXKViewController <UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, UIGestureRecognizerDelegate, MXKRoomMemberDetailsViewControllerDelegate>
{
@protected
/**
@ -79,6 +78,11 @@
*/
@property (nonatomic) MXRoom *mxRoom;
/**
The ID of the parent space. `nil` for home space
*/
@property (nonatomic) NSString *parentSpaceId;
/**
Enable mention option in member details view. NO by default
*/
@ -86,6 +90,7 @@
@property (nonatomic) BOOL showCancelBarButtonItem;
@property (nonatomic) BOOL showParticipantCustomAccessoryView;
@property (nonatomic) BOOL showInviteUserFab;
/**
The delegate for the view controller.

View file

@ -29,7 +29,7 @@
#import "RageShakeManager.h"
@interface RoomParticipantsViewController () <UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, UIGestureRecognizerDelegate, MXKRoomMemberDetailsViewControllerDelegate, ContactsTableViewControllerDelegate>
@interface RoomParticipantsViewController () <UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, UIGestureRecognizerDelegate, MXKRoomMemberDetailsViewControllerDelegate, RoomParticipantsInviteCoordinatorBridgePresenterDelegate>
{
// Search result
NSString *currentSearchText;
@ -49,12 +49,13 @@
id roomDidFlushDataNotificationObserver;
RoomMemberDetailsViewController *memberDetailsViewController;
ContactsTableViewController *contactsPickerViewController;
UIAlertController *currentAlert;
// Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change.
id kThemeServiceDidChangeThemeNotificationObserver;
RoomParticipantsInviteCoordinatorBridgePresenter *invitePresenter;
}
@end
@ -85,6 +86,7 @@
self.enableBarTintColorStatusChange = NO;
self.rageShakeManager = [RageShakeManager sharedManager];
self.showParticipantCustomAccessoryView = YES;
self.showInviteUserFab = YES;
}
- (void)viewDidLoad
@ -140,11 +142,13 @@
[self.tableView registerClass:ContactTableViewCell.class forCellReuseIdentifier:@"ParticipantTableViewCellId"];
// Add invite members button programmatically
[self vc_addFABWithImage:[UIImage imageNamed:@"add_member_floating_action"]
target:self
action:@selector(onAddParticipantButtonPressed)];
if (_showInviteUserFab)
{
// Add invite members button programmatically
[self vc_addFABWithImage:[UIImage imageNamed:@"add_member_floating_action"]
target:self
action:@selector(onAddParticipantButtonPressed)];
}
// Observe user interface theme change.
kThemeServiceDidChangeThemeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kThemeServiceDidChangeThemeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
@ -259,12 +263,6 @@
[memberDetailsViewController destroy];
memberDetailsViewController = nil;
}
if (contactsPickerViewController)
{
[contactsPickerViewController destroy];
contactsPickerViewController = nil;
}
[self.screenTimer start];
}
@ -547,50 +545,9 @@
- (void)onAddParticipantButtonPressed
{
// Push the contacts picker.
contactsPickerViewController = [ContactsTableViewController contactsTableViewController];
// Set delegate to handle action on member (start chat, mention)
contactsPickerViewController.contactsTableViewControllerDelegate = self;
// Prepare its data source
ContactsDataSource *contactsDataSource = [[ContactsDataSource alloc] initWithMatrixSession:self.mxRoom.mxSession];
contactsDataSource.areSectionsShrinkable = YES;
contactsDataSource.displaySearchInputInContactsList = YES;
contactsDataSource.forceMatrixIdInDisplayName = YES;
// Add a plus icon to the contact cell in the contacts picker, in order to make it more understandable for the end user.
contactsDataSource.contactCellAccessoryImage = [[UIImage imageNamed:@"plus_icon"] vc_tintedImageUsingColor:ThemeService.shared.theme.textPrimaryColor];
// List all the participants matrix user id to ignore them during the contacts search.
for (Contact *contact in actualParticipants)
{
contactsDataSource.ignoredContactsByMatrixId[contact.mxMember.userId] = contact;
}
for (Contact *contact in invitedParticipants)
{
if (contact.mxMember)
{
contactsDataSource.ignoredContactsByMatrixId[contact.mxMember.userId] = contact;
}
}
if (userParticipant)
{
contactsDataSource.ignoredContactsByMatrixId[userParticipant.mxMember.userId] = userParticipant;
}
[contactsPickerViewController showSearch:YES];
contactsPickerViewController.searchBar.placeholder = [VectorL10n roomParticipantsInviteAnotherUser];
// Apply the search pattern if any
if (currentSearchText)
{
contactsPickerViewController.searchBar.text = currentSearchText;
[contactsDataSource searchWithPattern:currentSearchText forceReset:YES];
}
[contactsPickerViewController displayList:contactsDataSource];
[self pushViewController:contactsPickerViewController];
self->invitePresenter = [[RoomParticipantsInviteCoordinatorBridgePresenter alloc] initWithSession:self.mxRoom.mxSession room:self.mxRoom parentSpaceId:self.parentSpaceId currentSearchText:currentSearchText actualParticipants:actualParticipants invitedParticipants:invitedParticipants userParticipant:userParticipant];
self->invitePresenter.delegate = self;
[self->invitePresenter presentFrom:self animated:true];
}
- (void)refreshParticipantsFromRoomMembers
@ -1265,13 +1222,6 @@
}
}
#pragma mark - ContactsTableViewControllerDelegate
- (void)contactsTableViewController:(ContactsTableViewController *)contactsTableViewController didSelectContact:(MXKContact*)contact
{
[self didSelectInvitableContact:contact];
}
#pragma mark - Actions
- (void)onDeleteAt:(NSIndexPath*)path
@ -1496,149 +1446,6 @@
[self withdrawViewControllerAnimated:YES completion:nil];
}
#pragma mark -
- (void)didSelectInvitableContact:(MXKContact*)contact
{
__weak typeof(self) weakSelf = self;
if (currentAlert)
{
[currentAlert dismissViewControllerAnimated:NO completion:nil];
currentAlert = nil;
}
// Invite ?
NSString *promptMsg = [VectorL10n roomParticipantsInvitePromptMsg:contact.displayName];
currentAlert = [UIAlertController alertControllerWithTitle:[VectorL10n roomParticipantsInvitePromptTitle]
message:promptMsg
preferredStyle:UIAlertControllerStyleAlert];
[currentAlert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n cancel]
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->currentAlert = nil;
}
}]];
[currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n invite]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->currentAlert = nil;
NSArray *identifiers = contact.matrixIdentifiers;
NSString *participantId;
if (identifiers.count)
{
participantId = identifiers.firstObject;
// Invite this user if a room is defined
[self addPendingActionMask];
[self.mxRoom inviteUser:participantId success:^{
__strong __typeof(weakSelf)self = weakSelf;
[self removePendingActionMask];
// Refresh display by removing the contacts picker
[self->contactsPickerViewController withdrawViewControllerAnimated:YES completion:nil];
} failure:^(NSError *error) {
__strong __typeof(weakSelf)self = weakSelf;
[self removePendingActionMask];
MXLogDebug(@"[RoomParticipantsVC] Invite %@ failed", participantId);
// Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
else
{
if (contact.emailAddresses.count)
{
// This is a local contact, consider the first email by default.
// TODO: Prompt the user to select the right email.
MXKEmail *email = contact.emailAddresses.firstObject;
participantId = email.emailAddress;
}
else
{
// This is the text filled by the user.
participantId = contact.displayName;
}
// Is it an email or a Matrix user ID?
if ([MXTools isEmailAddress:participantId])
{
[self addPendingActionMask];
[self.mxRoom inviteUserByEmail:participantId success:^{
__strong __typeof(weakSelf)self = weakSelf;
[self removePendingActionMask];
// Refresh display by removing the contacts picker
[self->contactsPickerViewController withdrawViewControllerAnimated:YES completion:nil];
} failure:^(NSError *error) {
__strong __typeof(weakSelf)self = weakSelf;
[self removePendingActionMask];
MXLogDebug(@"[RoomParticipantsVC] Invite be email %@ failed", participantId);
// Alert user
if ([error.domain isEqualToString:kMXRestClientErrorDomain]
&& error.code == MXRestClientErrorMissingIdentityServer)
{
NSString *message = [VectorL10n errorInvite3pidWithNoIdentityServer];
[[AppDelegate theDelegate] showAlertWithTitle:message message:nil];
}
else
{
[[AppDelegate theDelegate] showErrorAsAlert:error];
}
}];
}
else //if ([MXTools isMatrixUserIdentifier:participantId])
{
[self addPendingActionMask];
[self.mxRoom inviteUser:participantId success:^{
__strong __typeof(weakSelf)self = weakSelf;
[self removePendingActionMask];
// Refresh display by removing the contacts picker
[self->contactsPickerViewController withdrawViewControllerAnimated:YES completion:nil];
} failure:^(NSError *error) {
__strong __typeof(weakSelf)self = weakSelf;
[self removePendingActionMask];
MXLogDebug(@"[RoomParticipantsVC] Invite %@ failed", participantId);
// Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
}
}
}]];
[currentAlert mxk_setAccessibilityIdentifier:@"RoomParticipantsVCInviteAlert"];
[self presentViewController:currentAlert animated:YES completion:nil];
}
#pragma mark - UISearchBar delegate
- (void)refreshSearchBarItemsColor:(UISearchBar *)searchBar
@ -1768,4 +1575,21 @@
[searchBar resignFirstResponder];
}
#pragma mark - RoomParticipantsInviteCoordinatorBridgePresenterDelegate
- (void)roomParticipantsInviteCoordinatorBridgePresenterDidComplete:(RoomParticipantsInviteCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
self->invitePresenter = nil;
}
- (void)roomParticipantsInviteCoordinatorBridgePresenterDidStartLoading:(RoomParticipantsInviteCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
[self addPendingActionMask];
}
- (void)roomParticipantsInviteCoordinatorBridgePresenterDidEndLoading:(RoomParticipantsInviteCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
[self removePendingActionMask];
}
@end

View file

@ -0,0 +1,141 @@
//
// 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 ContactsPickerCoordinator: ContactsPickerCoordinatorProtocol {
private weak var currentAlert: UIAlertController?
// MARK: - Private
private let session: MXSession?
private let room: MXRoom?
private let initialSearchText: String?
private var actualParticipants: [Contact]?
private var invitedParticipants: [Contact]?
private var userParticipant: Contact?
private let navigationRouter: NavigationRouterType
private weak var contactsPickerViewController: ContactsTableViewController?
private var viewModel: ContactsPickerViewModelProtocol?
// MARK: Public
internal var childCoordinators: [Coordinator] = []
weak var delegate: ContactsPickerCoordinatorDelegate?
// MARK: - Setup
init(session: MXSession, room: MXRoom, initialSearchText: String?, actualParticipants: [Contact]?, invitedParticipants: [Contact]?, userParticipant: Contact?, navigationRouter: NavigationRouterType? = nil) {
self.session = session
self.room = room
self.initialSearchText = initialSearchText
self.actualParticipants = actualParticipants
self.invitedParticipants = invitedParticipants
self.userParticipant = userParticipant
if let navigationRouter = navigationRouter {
self.navigationRouter = navigationRouter
} else {
self.navigationRouter = NavigationRouter(navigationController: RiotNavigationController())
}
}
// MARK: - Public methods
func start() {
guard let room = self.room else {
MXLog.error("[ContactsCoordinator] start: no room")
return
}
let viewModel = ContactsPickerViewModel(room: room, actualParticipants: self.actualParticipants, invitedParticipants: self.invitedParticipants, userParticipant: self.userParticipant)
viewModel.coordinatorDelegate = self
self.viewModel = viewModel
guard viewModel.areParticipantsLoaded else {
viewModel.loadParticipants()
return
}
startWithParticipants()
}
func toPresentable() -> UIViewController {
return self.navigationRouter.toPresentable()
}
// MARK: - Private methods
private func startWithParticipants() {
// Push the contacts picker.
let contactsViewController = RoomInviteViewController()
viewModel?.prepare(contactsViewController: contactsViewController, currentSearchText: initialSearchText)
self.navigationRouter.push(contactsViewController, animated: true) { [weak self] in
guard let self = self else { return }
self.delegate?.contactsPickerCoordinatorDidClose(self)
}
contactsPickerViewController = contactsViewController
}
}
// MARK: - ContactsViewModelCoordinatorDelegate
extension ContactsPickerCoordinator: ContactsPickerViewModelCoordinatorDelegate {
func contactsPickerViewModelDidStartLoading(_ viewModel: ContactsPickerViewModelProtocol) {
delegate?.contactsPickerCoordinatorDidStartLoading(self)
}
func contactsPickerViewModelDidEndLoading(_ viewModel: ContactsPickerViewModelProtocol) {
delegate?.contactsPickerCoordinatorDidEndLoading(self)
startWithParticipants()
}
func contactsPickerViewModelDidStartInvite(_ viewModel: ContactsPickerViewModelProtocol) {
contactsPickerViewController?.startActivityIndicator()
}
func contactsPickerViewModelDidEndInvite(_ viewModel: ContactsPickerViewModelProtocol) {
contactsPickerViewController?.stopActivityIndicator()
contactsPickerViewController?.withdrawViewController(animated: true, completion: {
self.delegate?.contactsPickerCoordinatorDidClose(self)
})
}
func contactsPickerViewModel(_ viewModel: ContactsPickerViewModelProtocol, inviteFailedWithError error: Error?) {
contactsPickerViewController?.stopActivityIndicator()
if let error = error {
AppDelegate.theDelegate().showError(asAlert: error)
}
}
func contactsPickerViewModel(_ viewModel: ContactsPickerViewModelProtocol, display message: String, title: String, actions: [UIAlertAction]) {
currentAlert?.dismiss(animated: false, completion: nil)
currentAlert = nil
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
for action in actions {
alert.addAction(action)
}
alert.mxk_setAccessibilityIdentifier("RoomParticipantsVCInviteAlert")
navigationRouter.present(alert, animated: true)
currentAlert = alert
}
}

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
protocol ContactsPickerCoordinatorDelegate: AnyObject {
func contactsPickerCoordinatorDidStartLoading(_ coordinator: ContactsPickerCoordinatorProtocol)
func contactsPickerCoordinatorDidEndLoading(_ coordinator: ContactsPickerCoordinatorProtocol)
func contactsPickerCoordinatorDidClose(_ coordinator: ContactsPickerCoordinatorProtocol)
}
protocol ContactsPickerCoordinatorProtocol: Coordinator, Presentable {
var delegate: ContactsPickerCoordinatorDelegate? { get }
}

View file

@ -0,0 +1,283 @@
//
// 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 ContactsPickerViewModel: NSObject, ContactsPickerViewModelProtocol {
private class RoomMembers {
var actualParticipants: [Contact] = []
var invitedParticipants: [Contact] = []
var userParticipant: Contact?
}
// MARK: - Properties
weak var coordinatorDelegate: ContactsPickerViewModelCoordinatorDelegate?
private(set) var areParticipantsLoaded: Bool = false
// MARK: - Private
private let room: MXRoom
private var actualParticipants: [Contact]?
private var invitedParticipants: [Contact]?
private var userParticipant: Contact?
// MARK: - Setup
init(room: MXRoom, actualParticipants: [Contact]?, invitedParticipants: [Contact]?, userParticipant: Contact?) {
self.room = room
self.actualParticipants = actualParticipants
self.invitedParticipants = invitedParticipants
self.userParticipant = userParticipant
areParticipantsLoaded = actualParticipants != nil && invitedParticipants != nil && userParticipant != nil
super.init()
}
// MARK: - Public
func loadParticipants() {
coordinatorDelegate?.contactsPickerViewModelDidStartLoading(self)
let roomMembers = RoomMembers()
// Retrieve the current members from the room state
room.state { [weak self] roomState in
guard let self = self else {
return
}
guard let roomState = roomState, let members = roomState.members.membersWithoutConferenceUser(), let session = self.room.mxSession, let myUserId = session.myUserId, let roomThirdPartyInvites = roomState.thirdPartyInvites else {
self.finalize(participants: roomMembers)
return
}
for member in members {
if member.userId == myUserId {
if member.membership == .join || member.membership == .invite {
let displayName = VectorL10n.you
if let participant = Contact(matrixContactWithDisplayName: displayName, andMatrixID: myUserId) {
participant.mxMember = roomState.members.member(withUserId: myUserId)
roomMembers.userParticipant = participant
}
}
} else {
self.handle(roomMember: member, session: session, members: roomMembers)
}
}
for invite in roomThirdPartyInvites {
self.add(thirdPartyParticipant: invite, roomState: roomState, members: roomMembers)
}
self.finalize(participants: roomMembers)
}
}
func prepare(contactsViewController: RoomInviteViewController, currentSearchText: String?) -> Bool {
contactsViewController.room = self.room
// Set delegate to handle action on member (start chat, mention)
contactsViewController.contactsTableViewControllerDelegate = self
// Prepare its data source
guard let contactsDataSource = ContactsDataSource(matrixSession: room.mxSession) else {
MXLog.error("[ContactsPickerViewModel] prepare: failed to instantiate ContactsDataSource")
return false
}
contactsDataSource.areSectionsShrinkable = true
contactsDataSource.displaySearchInputInContactsList = true
contactsDataSource.forceMatrixIdInDisplayName = true
// Add a plus icon to the contact cell in the contacts picker, in order to make it more understandable for the end user.
contactsDataSource.contactCellAccessoryImage = Asset.Images.plusIcon.image.vc_tintedImage(usingColor: ThemeService.shared().theme.textPrimaryColor)
// List all the participants matrix user id to ignore them during the contacts search.
for contact in actualParticipants ?? [] {
if let userId = contact.mxMember.userId {
contactsDataSource.ignoredContactsByMatrixId[userId] = contact
}
}
for contact in invitedParticipants ?? [] {
if let userId = contact.mxMember?.userId {
contactsDataSource.ignoredContactsByMatrixId[userId] = contact
}
}
if let userParticipantId = self.userParticipant?.mxMember.userId {
contactsDataSource.ignoredContactsByMatrixId[userParticipantId] = userParticipant
}
contactsViewController.showSearch(true)
contactsViewController.searchBar.placeholder = VectorL10n.roomParticipantsInviteAnotherUser
// Apply the search pattern if any
if currentSearchText != nil {
contactsViewController.searchBar.text = currentSearchText
contactsDataSource.search(withPattern: currentSearchText, forceReset: true)
}
contactsViewController.displayList(contactsDataSource)
return true
}
// MARK: - Private
private func handle(roomMember: MXRoomMember, session: MXSession, members: RoomMembers) {
// Add this member after checking his status
guard roomMember.membership == .join || roomMember.membership == .invite else {
return
}
// Prepare the display name of this member
var displayName = roomMember.displayname
if displayName.isEmptyOrNil {
// Look for the corresponding MXUser in matrix session
if let user = session.user(withUserId: roomMember.userId) {
displayName = user.displayname.isEmptyOrNil ? user.userId : user.displayname
} else {
displayName = roomMember.userId
}
}
// Create the contact related to this member
if let contact = Contact(matrixContactWithDisplayName: displayName, andMatrixID: roomMember.userId) {
contact.mxMember = roomMember
if roomMember.membership == .invite {
members.invitedParticipants.append(contact)
} else {
members.actualParticipants.append(contact)
}
}
}
private func add(thirdPartyParticipant invite: MXRoomThirdPartyInvite, roomState: MXRoomState, members: RoomMembers) {
// If the homeserver has converted the 3pid invite into a room member, do no show it
// If the invite has been revoked (null display name), do not show it too.
guard let displayName = invite.displayname, roomState.member(withThirdPartyInviteToken: invite.token) == nil else {
return
}
if let contact = Contact(matrixContactWithDisplayName: displayName, andMatrixID: nil) {
contact.isThirdPartyInvite = true
contact.mxThirdPartyInvite = invite
members.invitedParticipants.append(contact)
}
}
private func finalize(participants roomMembers: RoomMembers) {
self.actualParticipants = roomMembers.actualParticipants
self.invitedParticipants = roomMembers.invitedParticipants
self.userParticipant = roomMembers.userParticipant
self.coordinatorDelegate?.contactsPickerViewModelDidEndLoading(self)
}
}
// MARK: - ContactsTableViewControllerDelegate
extension ContactsPickerViewModel: ContactsTableViewControllerDelegate {
func contactsTableViewController(_ contactsTableViewController: ContactsTableViewController!, didSelect contact: MXKContact?) {
guard let contact = contact else {
MXLog.error("[ContactsPickerViewModel] contactsTableViewController: nil contact found")
return
}
let roomName = room.displayName ?? VectorL10n.spaceTag
let message = VectorL10n.roomParticipantsInvitePromptToMsg(contact.displayName, roomName)
coordinatorDelegate?.contactsPickerViewModel(self, display: message, title: VectorL10n.roomParticipantsInvitePromptTitle, actions: [
UIAlertAction(title: MatrixKitL10n.cancel, style: .cancel, handler: nil),
UIAlertAction(title: VectorL10n.invite, style: .default, handler: { [weak self] action in
self?.invite(contact: contact)
})
])
}
private func invite(contact: MXKContact) {
if let identifiers = contact.matrixIdentifiers as? [String], let participantId = identifiers.first {
// Invite this user if a room is defined
self.coordinatorDelegate?.contactsPickerViewModelDidStartInvite(self)
room.invite(.userId(participantId)) { [weak self] response in
guard let self = self else { return }
switch response {
case .success:
self.coordinatorDelegate?.contactsPickerViewModelDidEndInvite(self)
case .failure:
MXLog.error("[ContactsPickerViewModel] Failed to invite \(participantId) due to error; \(response.error ?? "nil")")
self.coordinatorDelegate?.contactsPickerViewModel(self, inviteFailedWithError: response.error)
}
}
} else {
let _participantId: String?
if let emailAddresses = contact.emailAddresses as? [MXKEmail], let email = emailAddresses.first {
// This is a local contact, consider the first email by default.
// TODO: Prompt the user to select the right email.
_participantId = email.emailAddress
} else {
// This is the text filled by the user.
_participantId = contact.displayName
}
guard let participantId = _participantId else {
MXLog.error("[ContactsPickerViewModel] invite: unexpectedly found participantId nil")
return
}
self.coordinatorDelegate?.contactsPickerViewModelDidStartInvite(self)
// Is it an email or a Matrix user ID?
if MXTools.isEmailAddress(participantId) {
room.invite(.email(participantId)) { [weak self] response in
guard let self = self else { return }
switch response {
case .success:
self.coordinatorDelegate?.contactsPickerViewModelDidEndInvite(self)
case .failure:
MXLog.error("[ContactsPickerViewModel] Failed to invite \(participantId) by email due to error; \(response.error ?? "nil")")
if let error = response.error as NSError?, error.domain == kMXRestClientErrorDomain, error.code == MXRestClientErrorMissingIdentityServer {
self.coordinatorDelegate?.contactsPickerViewModel(self, inviteFailedWithError: nil)
AppDelegate.theDelegate().showAlert(withTitle: VectorL10n.errorInvite3pidWithNoIdentityServer, message: nil)
} else {
self.coordinatorDelegate?.contactsPickerViewModel(self, inviteFailedWithError: response.error)
}
}
}
} else {
room.invite(.userId(participantId)) { [weak self] response in
guard let self = self else { return }
switch response {
case .success:
self.coordinatorDelegate?.contactsPickerViewModelDidEndInvite(self)
case .failure:
MXLog.error("[ContactsPickerViewModel] Failed to invite \(participantId) due to error; \(response.error ?? "nil")")
self.coordinatorDelegate?.contactsPickerViewModel(self, inviteFailedWithError: response.error)
}
}
}
}
}
}

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
protocol ContactsPickerViewModelCoordinatorDelegate: AnyObject {
func contactsPickerViewModelDidStartLoading(_ viewModel: ContactsPickerViewModelProtocol)
func contactsPickerViewModelDidEndLoading(_ viewModel: ContactsPickerViewModelProtocol)
func contactsPickerViewModelDidStartInvite(_ viewModel: ContactsPickerViewModelProtocol)
func contactsPickerViewModelDidEndInvite(_ viewModel: ContactsPickerViewModelProtocol)
func contactsPickerViewModel(_ viewModel: ContactsPickerViewModelProtocol, inviteFailedWithError error: Error?)
func contactsPickerViewModel(_ viewModel: ContactsPickerViewModelProtocol, display message: String, title: String, actions: [UIAlertAction])
}
protocol ContactsPickerViewModelProtocol {
var coordinatorDelegate: ContactsPickerViewModelCoordinatorDelegate? { get set }
var areParticipantsLoaded: Bool { get }
func loadParticipants()
@discardableResult func prepare(contactsViewController: RoomInviteViewController, currentSearchText: String?) -> Bool
}

View file

@ -0,0 +1,57 @@
//
// 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 RoomInviteViewController: ContactsTableViewController {
var room: MXRoom?
var roomAlias: String?
private lazy var shareLinkPresenter: ShareInviteLinkPresenter = ShareInviteLinkPresenter()
override func viewDidLoad() {
super.viewDidLoad()
roomAlias = room?.summary?.aliases?.first
setupShareInviteLinkHeader()
}
private func setupShareInviteLinkHeader() {
guard roomAlias != nil, RiotSettings.shared.allowInviteExernalUsers else {
contactsTableView.tableHeaderView = nil
return
}
let inviteHeaderView = ShareInviteLinkHeaderView.instantiate()
inviteHeaderView.delegate = self
contactsTableView.tableHeaderView = inviteHeaderView
}
private func showInviteLink(from sourceView: UIView?) {
guard let room = room else {
return
}
shareLinkPresenter.present(for: room, from: self, sourceView: sourceView, animated: true)
}
}
// MARK: - ShareInviteLinkHeaderViewDelegate
extension RoomInviteViewController: ShareInviteLinkHeaderViewDelegate {
func shareInviteLinkHeaderView(_ headerView: ShareInviteLinkHeaderView, didTapButton button: UIButton) {
showInviteLink(from: button)
}
}

View file

@ -0,0 +1,82 @@
// File created from ScreenTemplate
// $ createScreen.sh Room/ParticipantsInviteModal/OptionList OptionList
/*
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 UIKit
final class OptionListCoordinator: OptionListCoordinatorProtocol {
// MARK: - Properties
// MARK: Private
private let parameters: OptionListCoordinatorParameters
private var optionListViewModel: OptionListViewModelProtocol
private let optionListViewController: OptionListViewController
private lazy var slidingModalPresenter: SlidingModalPresenter = SlidingModalPresenter()
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
weak var delegate: OptionListCoordinatorDelegate?
// MARK: - Setup
init(parameters: OptionListCoordinatorParameters) {
self.parameters = parameters
let optionListViewModel = OptionListViewModel(title: self.parameters.title, options: self.parameters.options)
let optionListViewController = OptionListViewController.instantiate(with: optionListViewModel)
self.optionListViewModel = optionListViewModel
self.optionListViewController = optionListViewController
}
// MARK: - Public
func start() {
self.optionListViewModel.coordinatorDelegate = self
if let rootViewController = self.parameters.navigationRouter?.toPresentable() {
slidingModalPresenter.present(optionListViewController, from: rootViewController, animated: true, completion: nil)
}
}
func dismiss(animated: Bool, completion: (() -> Void)?) {
slidingModalPresenter.dismiss(animated: animated, completion: completion)
}
func toPresentable() -> UIViewController {
return self.optionListViewController
}
}
// MARK: - OptionListViewModelCoordinatorDelegate
extension OptionListCoordinator: OptionListViewModelCoordinatorDelegate {
func optionListViewModel(_ viewModel: OptionListViewModelProtocol, didSelectOptionAt index: Int) {
dismiss(animated: false) {
self.delegate?.optionListCoordinator(self, didSelectOptionAt: index)
}
}
func optionListViewModelDidCancel(_ viewModel: OptionListViewModelProtocol) {
dismiss(animated: true) {
self.delegate?.optionListCoordinatorDidCancel(self)
}
}
}

View file

@ -0,0 +1,29 @@
// File created from ScreenTemplate
// $ createScreen.sh Room/ParticipantsInviteModal/OptionList OptionList
/*
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
/// OptionListCoordinator input parameters
struct OptionListCoordinatorParameters {
let title: String?
let options: [OptionListItemViewData]
let navigationRouter: NavigationRouterType?
}

View file

@ -0,0 +1,29 @@
// File created from ScreenTemplate
// $ createScreen.sh Room/ParticipantsInviteModal/OptionList OptionList
/*
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 OptionListCoordinatorDelegate: AnyObject {
func optionListCoordinator(_ coordinator: OptionListCoordinatorProtocol, didSelectOptionAt index: Int)
func optionListCoordinatorDidCancel(_ coordinator: OptionListCoordinatorProtocol)
}
/// `OptionListCoordinatorProtocol` is a protocol describing a Coordinator that handle invite options screen navigation flow.
protocol OptionListCoordinatorProtocol: Coordinator, Presentable {
var delegate: OptionListCoordinatorDelegate? { get }
}

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 UIKit
class OptionListItemViewData {
let title: String?
let detail: String?
let image: UIImage?
let accessoryImage: UIImage?
let enabled: Bool
init(title: String? = nil,
detail: String? = nil,
image: UIImage? = nil,
accessoryImage: UIImage? = Asset.Images.chevron.image,
enabled: Bool = true) {
self.title = title
self.detail = detail
self.image = image
self.accessoryImage = accessoryImage
self.enabled = enabled
}
}

View file

@ -0,0 +1,26 @@
// File created from ScreenTemplate
// $ createScreen.sh Room/ParticipantsInviteModal/OptionList OptionList
/*
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
/// OptionListViewController view actions exposed to view model
enum OptionListViewAction {
case loadData
case selected(_ index: Int)
case cancel
}

View file

@ -0,0 +1,85 @@
//
// 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
class OptionListViewCell: UITableViewCell, NibReusable {
// MARK: - Properties
@IBOutlet private weak var iconView: UIImageView!
@IBOutlet private weak var titleLabel: UILabel!
@IBOutlet private weak var detailLabel: UILabel!
@IBOutlet private weak var selectionView: UIView!
@IBOutlet private weak var chevronView: UIImageView!
var isEnabled: Bool = true {
didSet {
self.contentView.alpha = isEnabled ? 1 : 0.3
}
}
// MARK: - Private
private var theme: Theme?
// MARK: - Life cycle
override func awakeFromNib() {
super.awakeFromNib()
self.selectionStyle = .none
self.selectionView.layer.cornerRadius = 8.0
self.selectionView.layer.masksToBounds = true
}
override func setSelected(_ selected: Bool, animated: Bool) {
if isEnabled {
super.setSelected(selected, animated: animated)
UIView.animate(withDuration: animated ? 0.2 : 0.0) {
self.selectionView.transform = selected ? .init(scaleX: 0.95, y: 0.95) : .identity
}
}
}
// MARK: - Public
func update(with viewData: OptionListItemViewData) {
self.iconView.image = viewData.image?.withRenderingMode(.alwaysTemplate)
self.titleLabel.text = viewData.title
self.detailLabel.text = viewData.detail
self.chevronView.image = viewData.accessoryImage?.withRenderingMode(.alwaysTemplate)
self.isEnabled = viewData.enabled
}
func update(theme: Theme) {
self.theme = theme
self.backgroundColor = theme.colors.background
self.iconView.tintColor = theme.colors.secondaryContent
self.titleLabel.textColor = theme.colors.primaryContent
self.titleLabel.font = theme.fonts.bodySB
self.detailLabel.textColor = theme.colors.secondaryContent
self.detailLabel.font = theme.fonts.footnote
self.selectionView.backgroundColor = theme.colors.quinaryContent
self.chevronView.tintColor = theme.colors.quarterlyContent
}
}

View file

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<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>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="80" id="tOd-GW-k0x" customClass="OptionListViewCell" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="80"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="tOd-GW-k0x" id="kRV-oW-j2b">
<rect key="frame" x="0.0" y="0.0" width="320" height="80"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="YQF-X2-MAf">
<rect key="frame" x="0.0" y="8" width="320" height="64"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="height" constant="64" id="f1L-gQ-B6g"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7jb-2j-CHW">
<rect key="frame" x="48" y="18" width="248" height="44.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SSn-E2-PZK">
<rect key="frame" x="0.0" y="0.0" width="248" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<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="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cSw-r7-kht">
<rect key="frame" x="0.0" y="24" width="248" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="cSw-r7-kht" secondAttribute="bottom" id="GP4-p1-ujO"/>
<constraint firstItem="SSn-E2-PZK" firstAttribute="top" secondItem="7jb-2j-CHW" secondAttribute="top" id="VGr-Pf-3g6"/>
<constraint firstItem="cSw-r7-kht" firstAttribute="top" secondItem="SSn-E2-PZK" secondAttribute="bottom" constant="3" id="VYd-8e-6av"/>
<constraint firstAttribute="trailing" secondItem="SSn-E2-PZK" secondAttribute="trailing" id="bvj-r4-dUU"/>
<constraint firstAttribute="trailing" secondItem="cSw-r7-kht" secondAttribute="trailing" id="kpw-Ec-cv6"/>
<constraint firstItem="SSn-E2-PZK" firstAttribute="leading" secondItem="7jb-2j-CHW" secondAttribute="leading" id="quW-O4-sbk"/>
<constraint firstItem="cSw-r7-kht" firstAttribute="leading" secondItem="7jb-2j-CHW" secondAttribute="leading" id="xDJ-qG-rEq"/>
</constraints>
</view>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="lGB-4z-4VR">
<rect key="frame" x="12" y="28" width="24" height="24"/>
<constraints>
<constraint firstAttribute="width" constant="24" id="Ic9-Pt-qeE"/>
<constraint firstAttribute="height" constant="24" id="TKp-E4-NgB"/>
</constraints>
</imageView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="252" verticalHuggingPriority="251" image="chevron" translatesAutoresizingMaskIntoConstraints="NO" id="fIt-Up-cIW">
<rect key="frame" x="304" y="31.5" width="8" height="17"/>
</imageView>
</subviews>
<constraints>
<constraint firstItem="YQF-X2-MAf" firstAttribute="top" secondItem="kRV-oW-j2b" secondAttribute="top" constant="8" id="0nO-xV-wrC"/>
<constraint firstAttribute="trailing" secondItem="YQF-X2-MAf" secondAttribute="trailing" id="4pu-1l-NUB"/>
<constraint firstItem="fIt-Up-cIW" firstAttribute="centerY" secondItem="kRV-oW-j2b" secondAttribute="centerY" id="8JT-77-2Bn"/>
<constraint firstItem="lGB-4z-4VR" firstAttribute="leading" secondItem="kRV-oW-j2b" secondAttribute="leading" constant="12" id="9mJ-4Y-VXF"/>
<constraint firstItem="7jb-2j-CHW" firstAttribute="leading" secondItem="lGB-4z-4VR" secondAttribute="trailing" constant="12" id="BfR-X1-M1A"/>
<constraint firstItem="YQF-X2-MAf" firstAttribute="leading" secondItem="kRV-oW-j2b" secondAttribute="leading" id="Dlx-2U-0pd"/>
<constraint firstItem="7jb-2j-CHW" firstAttribute="centerY" secondItem="kRV-oW-j2b" secondAttribute="centerY" id="Nep-ep-waU"/>
<constraint firstAttribute="trailing" secondItem="fIt-Up-cIW" secondAttribute="trailing" constant="8" id="d75-6P-yTi"/>
<constraint firstAttribute="bottom" secondItem="YQF-X2-MAf" secondAttribute="bottom" constant="8" id="fTw-ef-WYp"/>
<constraint firstItem="fIt-Up-cIW" firstAttribute="leading" secondItem="7jb-2j-CHW" secondAttribute="trailing" constant="8" id="q3m-MZ-OfJ"/>
<constraint firstItem="lGB-4z-4VR" firstAttribute="centerY" secondItem="kRV-oW-j2b" secondAttribute="centerY" id="rCq-Wx-cnB"/>
</constraints>
</tableViewCellContentView>
<viewLayoutGuide key="safeArea" id="0Tk-ek-Uxc"/>
<connections>
<outlet property="chevronView" destination="fIt-Up-cIW" id="d80-M2-cn4"/>
<outlet property="detailLabel" destination="cSw-r7-kht" id="da9-15-dKs"/>
<outlet property="iconView" destination="lGB-4z-4VR" id="tEM-vI-I3c"/>
<outlet property="selectionView" destination="YQF-X2-MAf" id="Y3z-Ug-Lrc"/>
<outlet property="titleLabel" destination="SSn-E2-PZK" id="bS7-F2-Tfb"/>
</connections>
<point key="canvasLocation" x="137.68115942028987" y="109.82142857142857"/>
</tableViewCell>
</objects>
<resources>
<image name="chevron" width="8" height="17"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View file

@ -0,0 +1,84 @@
<?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="V8j-Lb-PgC">
<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>
<!--Option List View Controller-->
<scene sceneID="mt5-wz-YKA">
<objects>
<viewController extendedLayoutIncludesOpaqueBars="YES" automaticallyAdjustsScrollViewInsets="NO" id="V8j-Lb-PgC" customClass="OptionListViewController" customModule="Riot" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="EL9-GA-lwo">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WA9-CE-nGs">
<rect key="frame" x="16" y="60" width="342" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Lsk-jp-94m">
<rect key="frame" x="374" y="58.5" width="24" height="24"/>
<color key="backgroundColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="24" id="HY5-88-cuU"/>
<constraint firstAttribute="width" constant="24" id="PfC-lM-WEY"/>
</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="closeAction:" destination="V8j-Lb-PgC" eventType="touchUpInside" id="QNT-8E-uLB"/>
</connections>
</button>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="hBP-rT-V1I">
<rect key="frame" x="16" y="89" width="382" height="765"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<connections>
<outlet property="dataSource" destination="V8j-Lb-PgC" id="B18-Qe-eXW"/>
<outlet property="delegate" destination="V8j-Lb-PgC" id="Zuo-J0-Ssv"/>
</connections>
</tableView>
</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 firstItem="WA9-CE-nGs" firstAttribute="leading" secondItem="bFg-jh-JZB" secondAttribute="leading" constant="16" id="2MG-kw-FCN"/>
<constraint firstItem="bFg-jh-JZB" firstAttribute="bottom" secondItem="hBP-rT-V1I" secondAttribute="bottom" constant="8" id="7Tq-0y-W34"/>
<constraint firstItem="hBP-rT-V1I" firstAttribute="top" secondItem="WA9-CE-nGs" secondAttribute="bottom" constant="8" id="DBV-6W-nDd"/>
<constraint firstItem="WA9-CE-nGs" firstAttribute="top" secondItem="bFg-jh-JZB" secondAttribute="top" constant="16" id="M2h-yE-iSP"/>
<constraint firstItem="hBP-rT-V1I" firstAttribute="leading" secondItem="bFg-jh-JZB" secondAttribute="leading" constant="16" id="XYs-gC-Vly"/>
<constraint firstItem="Lsk-jp-94m" firstAttribute="centerY" secondItem="WA9-CE-nGs" secondAttribute="centerY" id="ltK-14-FrO"/>
<constraint firstItem="bFg-jh-JZB" firstAttribute="trailing" secondItem="Lsk-jp-94m" secondAttribute="trailing" constant="16" id="mQ8-fV-Apl"/>
<constraint firstItem="hBP-rT-V1I" firstAttribute="trailing" secondItem="bFg-jh-JZB" secondAttribute="trailing" constant="-16" id="vcz-Ag-tJx"/>
<constraint firstItem="Lsk-jp-94m" firstAttribute="leading" secondItem="WA9-CE-nGs" secondAttribute="trailing" constant="16" id="zPJ-gP-KrQ"/>
</constraints>
</view>
<connections>
<outlet property="bottomMargin" destination="7Tq-0y-W34" id="1c7-TO-JuJ"/>
<outlet property="closeButton" destination="Lsk-jp-94m" id="y6e-gi-Syb"/>
<outlet property="tableView" destination="hBP-rT-V1I" id="wm7-Vn-ChJ"/>
<outlet property="titleLabel" destination="WA9-CE-nGs" id="m0L-mK-3Xr"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="zK0-v6-7Wt" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-3198" y="-647"/>
</scene>
</scenes>
<resources>
<image name="space_menu_close" width="10" height="10.5"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View file

@ -0,0 +1,214 @@
// File created from ScreenTemplate
// $ createScreen.sh Room/ParticipantsInviteModal/OptionList OptionList
/*
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 UIKit
final class OptionListViewController: UIViewController {
// MARK: - Constants
private enum Constants {
static let estimatedRowHeight: CGFloat = 80.0
}
// MARK: - Properties
// MARK: Outlets
@IBOutlet private weak var titleLabel: UILabel!
@IBOutlet private weak var closeButton: UIButton!
@IBOutlet private weak var tableView: UITableView!
@IBOutlet private weak var bottomMargin: NSLayoutConstraint!
// MARK: Private
private var viewModel: OptionListViewModelProtocol!
private var theme: Theme!
private var keyboardAvoider: KeyboardAvoider?
private var errorPresenter: MXKErrorPresentation!
private var activityPresenter: ActivityIndicatorPresenter!
private var options: [OptionListItemViewData] = []
// MARK: - Setup
class func instantiate(with viewModel: OptionListViewModelProtocol) -> OptionListViewController {
let viewController = StoryboardScene.OptionListViewController.initialScene.instantiate()
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.keyboardAvoider = KeyboardAvoider(scrollViewContainerView: self.view, scrollView: self.tableView)
self.activityPresenter = ActivityIndicatorPresenter()
self.errorPresenter = MXKErrorAlertPresentation()
self.registerThemeServiceDidChangeThemeNotification()
self.update(theme: self.theme)
self.viewModel.viewDelegate = self
self.viewModel.process(viewAction: .loadData)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.keyboardAvoider?.startAvoiding()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.keyboardAvoider?.stopAvoiding()
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return self.theme.statusBarStyle
}
// MARK: - Private
private func update(theme: Theme) {
self.theme = theme
self.view.backgroundColor = theme.backgroundColor
self.tableView.backgroundColor = theme.backgroundColor
self.titleLabel.textColor = theme.colors.primaryContent
self.titleLabel.font = theme.fonts.title3SB
self.closeButton.backgroundColor = theme.roomInputTextBorder
self.closeButton.tintColor = theme.noticeSecondaryColor
self.tableView.reloadData()
}
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.setupTableView()
self.closeButton.layer.masksToBounds = true
self.closeButton.layer.cornerRadius = self.closeButton.bounds.height / 2
}
private func setupTableView() {
self.tableView.separatorStyle = .none
self.tableView.rowHeight = UITableView.automaticDimension
self.tableView.estimatedRowHeight = Constants.estimatedRowHeight
self.tableView.allowsSelection = true
self.tableView.register(cellType: OptionListViewCell.self)
self.tableView.tableFooterView = UIView()
}
private func render(viewState: OptionListViewState) {
switch viewState {
case .idle:
break
case .loading:
self.renderLoading()
case .loaded(let title, let options):
self.renderLoaded(title: title, options: options)
case .error(let error):
self.render(error: error)
}
}
private func renderLoading() {
self.activityPresenter.presentActivityIndicator(on: self.view, animated: true)
}
private func renderLoaded(title: String?, options: [OptionListItemViewData]) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.titleLabel.text = title
self.options = options
}
private func render(error: Error) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil)
}
// MARK: - Actions
@IBAction private func closeAction(_ sender: Any) {
self.viewModel.process(viewAction: .cancel)
}
}
// MARK: - OptionListViewModelViewDelegate
extension OptionListViewController: OptionListViewModelViewDelegate {
func optionListViewModel(_ viewModel: OptionListViewModelProtocol, didUpdateViewState viewSate: OptionListViewState) {
self.render(viewState: viewSate)
}
}
// MARK: - SlidingModalPresentable
extension OptionListViewController: SlidingModalPresentable {
func allowsDismissOnBackgroundTap() -> Bool {
return true
}
func layoutHeightFittingWidth(_ width: CGFloat) -> CGFloat {
return tableView.frame.minY + Constants.estimatedRowHeight * CGFloat(options.count) + bottomMargin.constant
}
}
// MARK: - UITableViewDataSource
extension OptionListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return options.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let viewData = options[indexPath.row]
let cell = tableView.dequeueReusableCell(for: indexPath, cellType: OptionListViewCell.self)
cell.update(theme: self.theme)
cell.update(with: viewData)
return cell
}
}
// MARK: - UITableViewDelegate
extension OptionListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if options[indexPath.row].enabled {
viewModel.process(viewAction: .selected(indexPath.row))
}
}
}

View file

@ -0,0 +1,66 @@
// File created from ScreenTemplate
// $ createScreen.sh Room/ParticipantsInviteModal/OptionList OptionList
/*
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
final class OptionListViewModel: OptionListViewModelProtocol {
// MARK: - Properties
// MARK: Private
private let title: String?
private let options: [OptionListItemViewData]
// MARK: Public
weak var viewDelegate: OptionListViewModelViewDelegate?
weak var coordinatorDelegate: OptionListViewModelCoordinatorDelegate?
private(set) var viewState: OptionListViewState = .idle {
didSet {
self.viewDelegate?.optionListViewModel(self, didUpdateViewState: viewState)
}
}
// MARK: - Setup
init(title: String?, options: [OptionListItemViewData]) {
self.title = title
self.options = options
}
// MARK: - Public
func process(viewAction: OptionListViewAction) {
switch viewAction {
case .loadData:
self.loadData()
case .selected(let index):
self.coordinatorDelegate?.optionListViewModel(self, didSelectOptionAt: index)
case .cancel:
self.coordinatorDelegate?.optionListViewModelDidCancel(self)
}
}
// MARK: - Private
private func loadData() {
self.viewState = .loaded(title, options)
}
}

View file

@ -0,0 +1,39 @@
// File created from ScreenTemplate
// $ createScreen.sh Room/ParticipantsInviteModal/OptionList OptionList
/*
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 OptionListViewModelViewDelegate: AnyObject {
func optionListViewModel(_ viewModel: OptionListViewModelProtocol, didUpdateViewState viewSate: OptionListViewState)
}
protocol OptionListViewModelCoordinatorDelegate: AnyObject {
func optionListViewModel(_ viewModel: OptionListViewModelProtocol, didSelectOptionAt index: Int)
func optionListViewModelDidCancel(_ viewModel: OptionListViewModelProtocol)
}
/// Protocol describing the view model used by `OptionListViewController`
protocol OptionListViewModelProtocol {
var viewDelegate: OptionListViewModelViewDelegate? { get set }
var coordinatorDelegate: OptionListViewModelCoordinatorDelegate? { get set }
func process(viewAction: OptionListViewAction)
var viewState: OptionListViewState { get }
}

View file

@ -0,0 +1,27 @@
// File created from ScreenTemplate
// $ createScreen.sh Room/ParticipantsInviteModal/OptionList OptionList
/*
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
/// OptionListViewController view state
enum OptionListViewState {
case idle
case loading
case loaded(_ title: String?, _ options: [OptionListItemViewData])
case error(Error)
}

View file

@ -0,0 +1,214 @@
//
// 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
@objc protocol RoomParticipantsInviteCoordinatorBridgePresenterDelegate {
func roomParticipantsInviteCoordinatorBridgePresenterDidStartLoading(_ coordinatorBridgePresenter: RoomParticipantsInviteCoordinatorBridgePresenter)
func roomParticipantsInviteCoordinatorBridgePresenterDidEndLoading(_ coordinatorBridgePresenter: RoomParticipantsInviteCoordinatorBridgePresenter)
func roomParticipantsInviteCoordinatorBridgePresenterDidComplete(_ coordinatorBridgePresenter: RoomParticipantsInviteCoordinatorBridgePresenter)
}
/// RoomParticipantsInviteCoordinatorBridgePresenter enables to start ContactsPickerCoordinator from a view controller.
/// This bridge is used while waiting for global usage of coordinator pattern.
@objcMembers
final class RoomParticipantsInviteCoordinatorBridgePresenter: NSObject {
// MARK: - Properties
// MARK: Private
private let session: MXSession?
private let room: MXRoom?
private let parentSpaceId: String?
private let currentSearchText: String?
private var actualParticipants: [Contact]?
private var invitedParticipants: [Contact]?
private var userParticipant: Contact?
private var roomOptions: [RoomOptionListItemViewData] = []
private weak var contactsPickerViewController: ContactsTableViewController?
private weak var currentAlert: UIAlertController?
private var contactPickerCoordinator: ContactsPickerCoordinator?
private var optionListCoordinator: OptionListCoordinator?
private var navigationRouter: NavigationRouterType?
// MARK: Public
weak var delegate: RoomParticipantsInviteCoordinatorBridgePresenterDelegate?
// MARK: - Setup
init(session: MXSession?, room: MXRoom?, parentSpaceId: String?) {
self.session = session
self.room = room
self.parentSpaceId = parentSpaceId
self.currentSearchText = nil
self.actualParticipants = nil
self.invitedParticipants = nil
self.userParticipant = nil
super.init()
}
init(session: MXSession?, room: MXRoom?, parentSpaceId: String?, currentSearchText: String? = nil, actualParticipants: [Contact]? = nil, invitedParticipants: [Contact]? = nil, userParticipant: Contact? = nil) {
self.session = session
self.room = room
self.parentSpaceId = parentSpaceId
self.currentSearchText = currentSearchText
self.actualParticipants = actualParticipants
self.invitedParticipants = invitedParticipants
self.userParticipant = userParticipant
super.init()
}
func present(from viewController: UIViewController, animated: Bool) {
guard let room = self.room else {
MXLog.error("[RoomParticipantsInviteCoordinatorBridgePresenter] present: nil room found")
return
}
if let navigationController = viewController.navigationController {
self.navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController)
} else {
self.navigationRouter = nil
}
if let spaceId = self.parentSpaceId, let spaceRoom = self.session?.spaceService.getSpace(withId: spaceId)?.room {
self.presentRoomSelector(between: room, and: spaceRoom)
return
}
self.pushContactsPicker(for: room)
}
// MARK: - Private
private class RoomOptionListItemViewData: OptionListItemViewData {
let room: MXRoom
init(title: String? = nil,
detail: String? = nil,
image: UIImage? = nil,
room: MXRoom,
accessoryImage: UIImage? = Asset.Images.chevron.image,
enabled: Bool = true) {
self.room = room
super.init(title: title, detail: detail, image: image, accessoryImage: accessoryImage, enabled: enabled)
}
}
private func presentRoomSelector(between room: MXRoom, and spaceRoom: MXRoom) {
let roomName = room.displayName ?? ""
let spaceName = spaceRoom.displayName ?? ""
self.roomOptions = [
RoomOptionListItemViewData(title: VectorL10n.roomInviteToSpaceOptionTitle(spaceName),
detail: VectorL10n.roomInviteToSpaceOptionDetail(spaceName, roomName),
image: Asset.Images.addParticipants.image, room: spaceRoom,
accessoryImage: Asset.Images.chevron.image),
RoomOptionListItemViewData(title: VectorL10n.roomInviteToRoomOptionTitle,
detail: VectorL10n.roomInviteToRoomOptionDetail(spaceName),
image: Asset.Images.addParticipants.image, room: room,
accessoryImage: Asset.Images.chevron.image)
]
let coordinator = OptionListCoordinator(parameters: OptionListCoordinatorParameters(title: VectorL10n.roomIntroCellAddParticipantsAction, options: self.roomOptions, navigationRouter: self.navigationRouter))
coordinator.delegate = self
coordinator.start()
self.optionListCoordinator = coordinator
}
private func pushContactsPicker(for room: MXRoom) {
guard let session = self.session else {
MXLog.error("[RoomParticipantsInviteCoordinatorBridgePresenter] pushContactsPicker: nil session found")
return
}
canInvite(to: room) { [weak self] canInvite in
guard let self = self else { return }
guard canInvite else {
let message = room.summary?.roomType == .space ? VectorL10n.spaceInviteNotEnoughPermission : VectorL10n.roomInviteNotEnoughPermission
let alert = UIAlertController(title: VectorL10n.spacesInvitePeople, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: VectorL10n.ok, style: .default, handler: nil))
self.navigationRouter?.present(alert, animated: true)
return
}
let coordinator = ContactsPickerCoordinator(session: session,
room: room,
initialSearchText: self.currentSearchText,
actualParticipants: self.actualParticipants,
invitedParticipants: self.invitedParticipants,
userParticipant: self.userParticipant,
navigationRouter: self.navigationRouter)
coordinator.delegate = self
coordinator.start()
self.contactPickerCoordinator = coordinator
}
}
private func canInvite(to room: MXRoom, completion: @escaping (Bool) -> Void) {
guard let userId = self.session?.myUserId else {
MXLog.error("[RoomParticipantsInviteCoordinatorBridgePresenter] canInvite: userId not found")
completion(false)
return
}
room.state { roomState in
guard let powerLevels = roomState?.powerLevels else {
MXLog.error("[RoomParticipantsInviteCoordinatorBridgePresenter] canInvite: room powerLevels not found")
completion(false)
return
}
let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: userId)
completion(userPowerLevel >= powerLevels.invite)
}
}
}
extension RoomParticipantsInviteCoordinatorBridgePresenter: ContactsPickerCoordinatorDelegate {
func contactsPickerCoordinatorDidStartLoading(_ coordinator: ContactsPickerCoordinatorProtocol) {
delegate?.roomParticipantsInviteCoordinatorBridgePresenterDidStartLoading(self)
}
func contactsPickerCoordinatorDidEndLoading(_ coordinator: ContactsPickerCoordinatorProtocol) {
delegate?.roomParticipantsInviteCoordinatorBridgePresenterDidEndLoading(self)
}
func contactsPickerCoordinatorDidClose(_ coordinator: ContactsPickerCoordinatorProtocol) {
delegate?.roomParticipantsInviteCoordinatorBridgePresenterDidComplete(self)
}
}
extension RoomParticipantsInviteCoordinatorBridgePresenter: OptionListCoordinatorDelegate {
func optionListCoordinator(_ coordinator: OptionListCoordinatorProtocol, didSelectOptionAt index: Int) {
optionListCoordinator = nil
self.pushContactsPicker(for: roomOptions[index].room)
}
func optionListCoordinator(_ coordinator: OptionListCoordinatorProtocol, didCompleteWithUserDisplayName userDisplayName: String?) {
optionListCoordinator = nil
}
func optionListCoordinatorDidCancel(_ coordinator: OptionListCoordinatorProtocol) {
optionListCoordinator = nil
}
}

View file

@ -0,0 +1,77 @@
//
// 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 UIKit
import Reusable
@objc
protocol ShareInviteLinkHeaderViewDelegate: AnyObject {
func shareInviteLinkHeaderView(_ headerView: ShareInviteLinkHeaderView, didTapButton button: UIButton)
}
@objcMembers
final class ShareInviteLinkHeaderView: UIView, NibLoadable, Themable {
// MARK: - Constants
private enum Constants {
static let buttonHighlightedAlpha: CGFloat = 0.2
}
// MARK: - Properties
@IBOutlet private weak var button: CustomRoundedButton!
weak var delegate: ShareInviteLinkHeaderViewDelegate?
// MARK: - Setup
static func instantiate() -> ShareInviteLinkHeaderView {
let view = ShareInviteLinkHeaderView.loadFromNib()
view.update(theme: ThemeService.shared().theme)
return view
}
// MARK: - Life cycle
override func awakeFromNib() {
super.awakeFromNib()
button.setTitle(VectorL10n.shareInviteLinkAction, for: .normal)
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
button.layer.cornerRadius = 8
button.layer.borderWidth = 2
}
// MARK: - Public
func update(theme: Theme) {
button.layer.borderColor = theme.tintColor.cgColor
button.setTitleColor(theme.tintColor, for: .normal)
button.setTitleColor(theme.tintColor.withAlphaComponent(Constants.buttonHighlightedAlpha), for: .highlighted)
button.vc_setBackgroundColor(theme.baseColor, for: .normal)
let buttonImage = Asset.Images.shareActionButton.image.vc_tintedImage(usingColor: theme.tintColor)
button.setImage(buttonImage, for: .normal)
}
// MARK: - Action
@objc private func buttonAction(_ sender: UIButton) {
delegate?.shareInviteLinkHeaderView(self, didTapButton: button)
}
}

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<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="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="cxh-dz-aGG" customClass="ShareInviteLinkHeaderView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="414" height="64"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="K8C-8y-oEb" customClass="CustomRoundedButton" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="35.5" y="10" width="343" height="44"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="JuE-b9-RNu"/>
<constraint firstAttribute="width" priority="750" constant="343" id="ujQ-vj-edr"/>
</constraints>
<inset key="contentEdgeInsets" minX="20" minY="0.0" maxX="20" maxY="0.0"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="20" maxY="0.0"/>
<state key="normal" title="Invite friends" image="share_action_button">
<color key="titleColor" red="0.47843137250000001" green="0.78823529410000004" blue="0.63137254899999995" alpha="1" colorSpace="calibratedRGB"/>
</state>
<state key="disabled">
<color key="titleColor" red="0.47843137250000001" green="0.78823529410000004" blue="0.63137254899999995" alpha="0.5" colorSpace="calibratedRGB"/>
</state>
</button>
</subviews>
<viewLayoutGuide key="safeArea" id="Ehk-Sf-ESZ"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="K8C-8y-oEb" firstAttribute="top" secondItem="cxh-dz-aGG" secondAttribute="top" constant="10" id="dLb-B4-eBJ"/>
<constraint firstItem="K8C-8y-oEb" firstAttribute="centerX" secondItem="cxh-dz-aGG" secondAttribute="centerX" id="hNU-VW-Hbx"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="K8C-8y-oEb" secondAttribute="trailing" constant="16" id="nXF-QG-u1t"/>
<constraint firstItem="K8C-8y-oEb" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="cxh-dz-aGG" secondAttribute="leading" constant="16" id="rZm-C4-mTe"/>
<constraint firstAttribute="bottom" secondItem="K8C-8y-oEb" secondAttribute="bottom" constant="10" id="tDj-72-Eek"/>
</constraints>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="button" destination="K8C-8y-oEb" id="xU3-t7-lLR"/>
</connections>
<point key="canvasLocation" x="114.49275362318842" y="-639.50892857142856"/>
</view>
</objects>
<resources>
<image name="share_action_button" width="24" height="24"/>
</resources>
</document>

View file

@ -0,0 +1,87 @@
//
// 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
/// ShareInviteLinkPresenter enables to share room alias to someone else
@objcMembers
final class ShareInviteLinkPresenter: NSObject {
// MARK: - Constants
// MARK: - Properties
// MARK: Private
private weak var presentingViewController: UIViewController?
private weak var sourceView: UIView?
// MARK: - Public
func present(for room: MXRoom,
from viewController: UIViewController,
sourceView: UIView?,
animated: Bool) {
self.presentingViewController = viewController
self.sourceView = sourceView
self.shareInvite(from: room)
}
func dismiss(animated: Bool, completion: (() -> Void)?) {
self.presentingViewController?.dismiss(animated: animated, completion: completion)
}
// MARK: - Private
private func shareInvite(from room: MXRoom) {
let shareText = self.buildShareText(with: room)
// Set up activity view controller
let activityItems: [Any] = [ shareText ]
let activityViewController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
self.present(activityViewController, animated: true)
}
private func buildShareText(with room: MXRoom) -> String {
let roomAliasOrId: String
if let alias = room.summary?.aliases?.first {
roomAliasOrId = alias
} else {
roomAliasOrId = room.matrixItemId
}
if room.summary?.roomType == .space {
return VectorL10n.shareInviteLinkSpaceText(MXTools.permalink(toRoom: roomAliasOrId))
} else {
return VectorL10n.shareInviteLinkRoomText(MXTools.permalink(toRoom: roomAliasOrId))
}
}
private func present(_ viewController: UIViewController, animated: Bool) {
// Configure source view when view controller is presented with a 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)
}
}

View file

@ -75,6 +75,8 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol {
self.roomViewController = RoomViewController.instantiate()
self.activityIndicatorPresenter = ActivityIndicatorPresenter()
self.roomViewController.parentSpaceId = parameters.parentSpaceId
if #available(iOS 14, *) {
TimelinePollProvider.shared.session = parameters.session
}

View file

@ -31,6 +31,9 @@ class RoomCoordinatorBridgePresenterParameters: NSObject {
/// The room identifier
let roomId: String
/// The identifier of the parent space. `nil` for home space
let parentSpaceId: String?
/// If not nil, the room will be opened on this event.
let eventId: String?
@ -39,10 +42,12 @@ class RoomCoordinatorBridgePresenterParameters: NSObject {
init(session: MXSession,
roomId: String,
parentSpaceId: String?,
eventId: String?,
previewData: RoomPreviewData?) {
self.session = session
self.roomId = roomId
self.parentSpaceId = parentSpaceId
self.eventId = eventId
self.previewData = previewData
}
@ -76,7 +81,7 @@ final class RoomCoordinatorBridgePresenter: NSObject {
func present(from viewController: UIViewController, animated: Bool) {
let coordinator = self.createRoomCoordinator()
let coordinator = self.createRoomCoordinator(parentSpaceId: bridgeParameters.parentSpaceId)
coordinator.delegate = self
let presentable = coordinator.toPresentable()
presentable.modalPresentationStyle = .formSheet
@ -90,7 +95,7 @@ final class RoomCoordinatorBridgePresenter: NSObject {
let navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController)
let coordinator = self.createRoomCoordinator(with: navigationRouter)
let coordinator = self.createRoomCoordinator(with: navigationRouter, parentSpaceId: bridgeParameters.parentSpaceId)
coordinator.delegate = self
coordinator.start() // Will trigger view controller push
@ -110,14 +115,14 @@ final class RoomCoordinatorBridgePresenter: NSObject {
// MARK: - Private
private func createRoomCoordinator(with navigationRouter: NavigationRouterType = NavigationRouter(navigationController: RiotNavigationController())) -> RoomCoordinator {
private func createRoomCoordinator(with navigationRouter: NavigationRouterType = NavigationRouter(navigationController: RiotNavigationController()), parentSpaceId: String?) -> RoomCoordinator {
let coordinatorParameters: RoomCoordinatorParameters
if let previewData = self.bridgeParameters.previewData {
coordinatorParameters = RoomCoordinatorParameters(navigationRouter: navigationRouter, previewData: previewData)
coordinatorParameters = RoomCoordinatorParameters(navigationRouter: navigationRouter, parentSpaceId: parentSpaceId, previewData: previewData)
} else {
coordinatorParameters = RoomCoordinatorParameters(navigationRouter: navigationRouter, session: self.bridgeParameters.session, roomId: self.bridgeParameters.roomId, eventId: self.bridgeParameters.eventId)
coordinatorParameters = RoomCoordinatorParameters(navigationRouter: navigationRouter, session: self.bridgeParameters.session, parentSpaceId: parentSpaceId, roomId: self.bridgeParameters.roomId, eventId: self.bridgeParameters.eventId)
}
return RoomCoordinator(parameters: coordinatorParameters)

View file

@ -34,6 +34,9 @@ struct RoomCoordinatorParameters {
/// The room identifier
let roomId: String
/// The identifier of the parent space. `nil` for home space
let parentSpaceId: String?
/// If not nil, the room will be opened on this event.
let eventId: String?
@ -46,12 +49,14 @@ struct RoomCoordinatorParameters {
navigationRouterStore: NavigationRouterStoreProtocol?,
session: MXSession,
roomId: String,
parentSpaceId: String?,
eventId: String?,
previewData: RoomPreviewData?) {
self.navigationRouter = navigationRouter
self.navigationRouterStore = navigationRouterStore
self.session = session
self.roomId = roomId
self.parentSpaceId = parentSpaceId
self.eventId = eventId
self.previewData = previewData
}
@ -60,17 +65,19 @@ struct RoomCoordinatorParameters {
init(navigationRouter: NavigationRouterType? = nil,
navigationRouterStore: NavigationRouterStoreProtocol? = nil,
session: MXSession,
parentSpaceId: String?,
roomId: String,
eventId: String? = nil) {
self.init(navigationRouter: navigationRouter, navigationRouterStore: navigationRouterStore, session: session, roomId: roomId, eventId: eventId, previewData: nil)
self.init(navigationRouter: navigationRouter, navigationRouterStore: navigationRouterStore, session: session, roomId: roomId, parentSpaceId: parentSpaceId, eventId: eventId, previewData: nil)
}
/// Init to present a room preview
init(navigationRouter: NavigationRouterType? = nil,
navigationRouterStore: NavigationRouterStoreProtocol? = nil,
parentSpaceId: String?,
previewData: RoomPreviewData) {
self.init(navigationRouter: navigationRouter, navigationRouterStore: navigationRouterStore, session: previewData.mxSession, roomId: previewData.roomId, eventId: nil, previewData: previewData)
self.init(navigationRouter: navigationRouter, navigationRouterStore: navigationRouterStore, session: previewData.mxSession, roomId: previewData.roomId, parentSpaceId: parentSpaceId, eventId: nil, previewData: previewData)
}
}

View file

@ -28,6 +28,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
private let navigationRouter: NavigationRouterType
private let session: MXSession
private let room: MXRoom
private let parentSpaceId: String?
private let initialSection: RoomInfoSection
private weak var roomSettingsViewController: RoomSettingsViewController?
@ -38,6 +39,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
participants.finalizeInit()
participants.enableMention = true
participants.mxRoom = self.room
participants.parentSpaceId = self.parentSpaceId
participants.delegate = self
participants.screenTimer = AnalyticsScreenTimer(screen: .roomMembers)
@ -98,6 +100,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
self.session = parameters.session
self.room = parameters.room
self.parentSpaceId = parameters.parentSpaceId
self.initialSection = parameters.initialSection
}

View file

@ -29,16 +29,18 @@ class RoomInfoCoordinatorParameters: NSObject {
let session: MXSession
let room: MXRoom
let parentSpaceId: String?
let initialSection: RoomInfoSection
init(session: MXSession, room: MXRoom, initialSection: RoomInfoSection) {
init(session: MXSession, room: MXRoom, parentSpaceId: String?, initialSection: RoomInfoSection) {
self.session = session
self.room = room
self.parentSpaceId = parentSpaceId
self.initialSection = initialSection
super.init()
}
convenience init(session: MXSession, room: MXRoom) {
self.init(session: session, room: room, initialSection: .none)
convenience init(session: MXSession, room: MXRoom, parentSpaceId: String?) {
self.init(session: session, room: room, parentSpaceId: parentSpaceId, initialSection: .none)
}
}

View file

@ -78,6 +78,11 @@ extern NSNotificationName const RoomGroupCallTileTappedNotification;
*/
@property (nonatomic) BOOL showMissedDiscussionsBadge;
/**
ID of the parent space. `nil` for home space.
*/
@property (nonatomic, nullable) NSString *parentSpaceId;
/**
Display the preview of a room that is unknown for the user.

View file

@ -93,7 +93,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, SpaceDetailPresenterDelegate, UserSuggestionCoordinatorBridgeDelegate>
RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, UserSuggestionCoordinatorBridgeDelegate, RoomParticipantsInviteCoordinatorBridgePresenterDelegate>
{
// The preview header
@ -198,6 +198,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
@property (nonatomic, strong) RoomCreationModalCoordinatorBridgePresenter *roomCreationModalCoordinatorBridgePresenter;
@property (nonatomic, strong) RoomInfoCoordinatorBridgePresenter *roomInfoCoordinatorBridgePresenter;
@property (nonatomic, strong) CustomSizedPresentationController *customSizedPresentationController;
@property (nonatomic, strong) RoomParticipantsInviteCoordinatorBridgePresenter *participantsInvitePresenter;
@property (nonatomic, getter=isActivitiesViewExpanded) BOOL activitiesViewExpanded;
@property (nonatomic, getter=isScrollToBottomHidden) BOOL scrollToBottomHidden;
@property (nonatomic, getter=isMissedDiscussionsBadgeHidden) BOOL missedDiscussionsBadgeHidden;
@ -1843,7 +1844,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
- (void)showAddParticipants
{
[self showRoomInfoWithInitialSection:RoomInfoSectionAddParticipants];
self.participantsInvitePresenter = [[RoomParticipantsInviteCoordinatorBridgePresenter alloc] initWithSession:self.roomDataSource.mxSession room:self.roomDataSource.room parentSpaceId:self.parentSpaceId];
self.participantsInvitePresenter.delegate = self;
[self.participantsInvitePresenter presentFrom:self animated:YES];
}
- (void)showRoomTopicChange
@ -1858,7 +1861,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
- (void)showRoomInfoWithInitialSection:(RoomInfoSection)roomInfoSection
{
RoomInfoCoordinatorParameters *parameters = [[RoomInfoCoordinatorParameters alloc] initWithSession:self.roomDataSource.mxSession room:self.roomDataSource.room initialSection:roomInfoSection];
RoomInfoCoordinatorParameters *parameters = [[RoomInfoCoordinatorParameters alloc] initWithSession:self.roomDataSource.mxSession room:self.roomDataSource.room parentSpaceId:self.parentSpaceId initialSection:roomInfoSection];
self.roomInfoCoordinatorBridgePresenter = [[RoomInfoCoordinatorBridgePresenter alloc] initWithParameters:parameters];
@ -6563,4 +6566,22 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
[self mention:member];
}
#pragma mark - RoomParticipantsInviteCoordinatorBridgePresenterDelegate
- (void)roomParticipantsInviteCoordinatorBridgePresenterDidComplete:(RoomParticipantsInviteCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
self.participantsInvitePresenter = nil;
}
- (void)roomParticipantsInviteCoordinatorBridgePresenterDidStartLoading:(RoomParticipantsInviteCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
[self startActivityIndicator];
}
- (void)roomParticipantsInviteCoordinatorBridgePresenterDidEndLoading:(RoomParticipantsInviteCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
[self stopActivityIndicator];
}
@end

View file

@ -379,6 +379,12 @@ extension SideMenuCoordinator: SpaceMenuPresenterDelegate {
self.showExploreRooms(spaceId: spaceId, session: session)
case .exploreMembers:
self.showMembers(spaceId: spaceId, session: session)
case .addRoom:
AppDelegate.theDelegate().showAlert(withTitle: VectorL10n.spacesAddRoom, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName))
case .addSpace:
AppDelegate.theDelegate().showAlert(withTitle: VectorL10n.spacesAddSpace, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName))
case .settings:
AppDelegate.theDelegate().showAlert(withTitle: VectorL10n.sideMenuActionSettings, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName))
}
}
}

View file

@ -0,0 +1,90 @@
//
// 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 UIKit
import Reusable
@objc
protocol AddItemHeaderViewDelegate: AnyObject {
func addItemHeaderView(_ headerView: AddItemHeaderView, didTapButton button: UIButton)
}
/// `AddItemHeaderView` is a generic view used as a header view for UITableView.
/// With this view we can add an extra action cell with icon and text as for SpaceMemberList and SpaceExploreRooms
@objcMembers
final class AddItemHeaderView: UIView, NibLoadable, Themable {
// MARK: - Constants
private enum Constants {
static let buttonHighlightedAlpha: CGFloat = 0.2
}
// MARK: - Properties
@IBOutlet private weak var button: UIButton!
@IBOutlet private weak var iconBackgroundView: UIView!
@IBOutlet private weak var iconView: UIImageView!
@IBOutlet private weak var titleLabel: UILabel!
weak var delegate: AddItemHeaderViewDelegate?
private var title: String? {
didSet {
titleLabel.text = title
}
}
private var icon: UIImage? {
didSet {
iconView.image = icon
}
}
// MARK: - Setup
static func instantiate(title: String?, icon: UIImage?) -> AddItemHeaderView {
let view = AddItemHeaderView.loadFromNib()
view.icon = icon
view.title = title
view.update(theme: ThemeService.shared().theme)
return view
}
// MARK: - Life cycle
override func awakeFromNib() {
super.awakeFromNib()
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
iconBackgroundView.layer.masksToBounds = true
iconBackgroundView.layer.cornerRadius = iconBackgroundView.bounds.width / 2
}
// MARK: - Public
func update(theme: Theme) {
iconBackgroundView.layer.backgroundColor = theme.colors.quinaryContent.cgColor
iconView.tintColor = theme.colors.secondaryContent
titleLabel.textColor = theme.colors.primaryContent
titleLabel.font = theme.fonts.headline
}
// MARK: - Action
@objc private func buttonAction(_ sender: UIButton) {
delegate?.addItemHeaderView(self, didTapButton: button)
}
}

View file

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<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="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="cxh-dz-aGG" customClass="AddItemHeaderView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="414" height="77"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="K8C-8y-oEb">
<rect key="frame" x="0.0" y="0.0" width="414" height="77"/>
<constraints>
<constraint firstAttribute="height" constant="77" id="JuE-b9-RNu"/>
</constraints>
<inset key="contentEdgeInsets" minX="20" minY="0.0" maxX="20" maxY="0.0"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="20" maxY="0.0"/>
<state key="normal">
<color key="titleColor" red="0.47843137250000001" green="0.78823529410000004" blue="0.63137254899999995" alpha="1" colorSpace="calibratedRGB"/>
</state>
<state key="disabled">
<color key="titleColor" red="0.47843137250000001" green="0.78823529410000004" blue="0.63137254899999995" alpha="0.5" colorSpace="calibratedRGB"/>
</state>
</button>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="5Ob-zl-Yhb">
<rect key="frame" x="13" y="17.5" width="42" height="42"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Kik-Yj-tb0">
<rect key="frame" x="0.0" y="0.0" width="42" height="42"/>
</imageView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Kik-Yj-tb0" firstAttribute="leading" secondItem="5Ob-zl-Yhb" secondAttribute="leading" id="8x3-3e-gtx"/>
<constraint firstAttribute="trailing" secondItem="Kik-Yj-tb0" secondAttribute="trailing" id="AJT-WT-ytj"/>
<constraint firstAttribute="bottom" secondItem="Kik-Yj-tb0" secondAttribute="bottom" id="ELg-cy-SKj"/>
<constraint firstAttribute="height" constant="42" id="cY8-gc-vLW"/>
<constraint firstAttribute="width" constant="42" id="fJr-GR-ahN"/>
<constraint firstItem="Kik-Yj-tb0" firstAttribute="top" secondItem="5Ob-zl-Yhb" secondAttribute="top" id="m8Y-Fu-iJd"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Mfm-61-xzF">
<rect key="frame" x="69" y="28" width="331" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="Ehk-Sf-ESZ"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="5Ob-zl-Yhb" firstAttribute="centerY" secondItem="cxh-dz-aGG" secondAttribute="centerY" id="7fq-2U-Q7B"/>
<constraint firstItem="Mfm-61-xzF" firstAttribute="centerY" secondItem="cxh-dz-aGG" secondAttribute="centerY" id="8Th-Y1-glF"/>
<constraint firstItem="5Ob-zl-Yhb" firstAttribute="leading" secondItem="cxh-dz-aGG" secondAttribute="leading" constant="13" id="Ec0-ux-5ZW"/>
<constraint firstItem="Ehk-Sf-ESZ" firstAttribute="trailing" secondItem="Mfm-61-xzF" secondAttribute="trailing" constant="14" id="HCD-YR-0ip"/>
<constraint firstItem="K8C-8y-oEb" firstAttribute="top" secondItem="cxh-dz-aGG" secondAttribute="top" id="dLb-B4-eBJ"/>
<constraint firstAttribute="trailing" secondItem="K8C-8y-oEb" secondAttribute="trailing" id="nXF-QG-u1t"/>
<constraint firstItem="K8C-8y-oEb" firstAttribute="leading" secondItem="cxh-dz-aGG" secondAttribute="leading" id="rZm-C4-mTe"/>
<constraint firstAttribute="bottom" secondItem="K8C-8y-oEb" secondAttribute="bottom" id="tDj-72-Eek"/>
<constraint firstItem="Mfm-61-xzF" firstAttribute="leading" secondItem="5Ob-zl-Yhb" secondAttribute="trailing" constant="14" id="wWl-9y-vew"/>
</constraints>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="button" destination="K8C-8y-oEb" id="xU3-t7-lLR"/>
<outlet property="iconBackgroundView" destination="5Ob-zl-Yhb" id="O8Y-re-hFp"/>
<outlet property="iconView" destination="Kik-Yj-tb0" id="lE3-da-2mt"/>
<outlet property="titleLabel" destination="Mfm-61-xzF" id="27Q-vu-oPf"/>
</connections>
<point key="canvasLocation" x="114.49275362318842" y="-639.50892857142856"/>
</view>
</objects>
</document>

View file

@ -70,4 +70,8 @@ extension SpaceMemberListCoordinator: SpaceMemberListViewModelCoordinatorDelegat
func spaceMemberListViewModelDidCancel(_ viewModel: SpaceMemberListViewModelType) {
self.delegate?.spaceMemberListCoordinatorDidCancel(self)
}
func spaceMemberListViewModelShowInvite(_ viewModel: SpaceMemberListViewModelType) {
self.delegate?.spaceMemberListCoordinatorShowInvite(self)
}
}

View file

@ -21,6 +21,7 @@ import Foundation
protocol SpaceMemberListCoordinatorDelegate: AnyObject {
func spaceMemberListCoordinator(_ coordinator: SpaceMemberListCoordinatorType, didSelect member: MXRoomMember, from sourceView: UIView?)
func spaceMemberListCoordinatorDidCancel(_ coordinator: SpaceMemberListCoordinatorType)
func spaceMemberListCoordinatorShowInvite(_ coordinator: SpaceMemberListCoordinatorType)
}
/// `SpaceMemberListCoordinatorType` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow.

View file

@ -23,4 +23,5 @@ enum SpaceMemberListViewAction {
case loadData
case complete(_ selectedMember: MXRoomMember, _ sourceView: UIView?)
case cancel
case invite
}

View file

@ -35,20 +35,16 @@ final class SpaceMemberListViewController: RoomParticipantsViewController {
private var errorPresenter: MXKErrorPresentation!
private var activityPresenter: ActivityIndicatorPresenter!
private var titleView: MainTitleView!
private var emptyView: SearchEmptyView!
private let inviteHeaderView = AddItemHeaderView.instantiate(title: VectorL10n.spacesInvitePeople, icon: Asset.Images.spaceInviteUser.image)
private var emptyViewArtwork: UIImage {
return ThemeService.shared().isCurrentThemeDark() ? Asset.Images.peopleEmptyScreenArtworkDark.image : Asset.Images.peopleEmptyScreenArtwork.image
}
// MARK: - Setup
class func instantiate(with viewModel: SpaceMemberListViewModelType) -> SpaceMemberListViewController {
let viewController = SpaceMemberListViewController()
viewController.viewModel = viewModel
viewController.showParticipantCustomAccessoryView = false
viewController.showInviteUserFab = false
viewController.theme = ThemeService.shared().theme
viewController.emptyView = SearchEmptyView()
return viewController
}
@ -71,14 +67,21 @@ final class SpaceMemberListViewController: RoomParticipantsViewController {
self.viewModel.process(viewAction: .loadData)
self.title = ""
self.setupTableViewHeader()
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return self.theme.statusBarStyle
}
// MARK: - Private
private func setupTableViewHeader() {
inviteHeaderView.delegate = self
tableView.tableHeaderView = inviteHeaderView
}
private func update(theme: Theme) {
self.theme = theme
@ -90,7 +93,8 @@ final class SpaceMemberListViewController: RoomParticipantsViewController {
theme.applyStyle(onSearchBar: self.searchBarView)
self.titleView.update(theme: theme)
self.emptyView.update(theme: theme)
self.inviteHeaderView.update(theme: theme)
}
private func registerThemeServiceDidChangeThemeNotification() {
@ -111,11 +115,6 @@ final class SpaceMemberListViewController: RoomParticipantsViewController {
self.titleView = MainTitleView()
self.titleView.titleLabel.text = VectorL10n.roomDetailsPeople
self.navigationItem.titleView = self.titleView
self.emptyView.frame = CGRect(x: Constants.emptySearchViewMargin, y: self.searchBarView.frame.maxY + 2 * Constants.emptySearchViewMargin, width: self.view.bounds.width - 2 * Constants.emptySearchViewMargin, height: 0)
self.emptyView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
self.emptyView.alpha = 0
self.view.insertSubview(self.emptyView, at: 0)
}
private func render(viewState: SpaceMemberListViewState) {
@ -137,9 +136,6 @@ final class SpaceMemberListViewController: RoomParticipantsViewController {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.mxRoom = space.room
self.titleView.subtitleLabel.text = space.summary?.displayname
self.emptyView.titleLabel.text = VectorL10n.spacesNoResultFoundTitle
self.emptyView.detailLabel.text = VectorL10n.spacesNoMemberFoundDetail(space.summary?.displayname ?? "")
self.emptyView.layoutIfNeeded()
}
private func render(error: Error) {
@ -154,7 +150,7 @@ final class SpaceMemberListViewController: RoomParticipantsViewController {
// MARK: - Actions
@objc private func onAddParticipantButtonPressed() {
self.errorPresenter.presentError(from: self, title: VectorL10n.spacesInvitesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName), animated: true, handler: nil)
self.viewModel.process(viewAction: .invite)
}
private func cancelButtonAction() {
@ -173,11 +169,6 @@ final class SpaceMemberListViewController: RoomParticipantsViewController {
override func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
super.searchBar(searchBar, textDidChange: searchText)
UIView.animate(withDuration: 0.2) {
self.emptyView.alpha = self.tableView.numberOfSections == 0 ? 1 : 0
self.tableView.alpha = self.tableView.numberOfSections == 0 ? 0 : 1
}
}
// MARK: - MXKRoomMemberDetailsViewControllerDelegate
@ -200,3 +191,10 @@ extension SpaceMemberListViewController: SpaceMemberListViewModelViewDelegate {
self.render(viewState: viewSate)
}
}
// MARK: - SpaceMemberListViewModelViewDelegate
extension SpaceMemberListViewController: AddItemHeaderViewDelegate {
func addItemHeaderView(_ headerView: AddItemHeaderView, didTapButton button: UIButton) {
self.viewModel.process(viewAction: .invite)
}
}

View file

@ -57,6 +57,8 @@ final class SpaceMemberListViewModel: SpaceMemberListViewModelType {
case .cancel:
self.cancelOperations()
self.coordinatorDelegate?.spaceMemberListViewModelDidCancel(self)
case .invite:
self.coordinatorDelegate?.spaceMemberListViewModelShowInvite(self)
}
}

View file

@ -25,6 +25,7 @@ protocol SpaceMemberListViewModelViewDelegate: AnyObject {
protocol SpaceMemberListViewModelCoordinatorDelegate: AnyObject {
func spaceMemberListViewModel(_ viewModel: SpaceMemberListViewModelType, didSelect member: MXRoomMember, from sourceView: UIView?)
func spaceMemberListViewModelDidCancel(_ viewModel: SpaceMemberListViewModelType)
func spaceMemberListViewModelShowInvite(_ viewModel: SpaceMemberListViewModelType)
}
/// Protocol describing the view model used by `SpaceMemberListViewController`

View file

@ -131,8 +131,51 @@ extension SpaceMembersCoordinator: SpaceMemberListCoordinatorDelegate {
func spaceMemberListCoordinatorDidCancel(_ coordinator: SpaceMemberListCoordinatorType) {
self.delegate?.spaceMembersCoordinatorDidCancel(self)
}
func spaceMemberListCoordinatorShowInvite(_ coordinator: SpaceMemberListCoordinatorType) {
guard let space = parameters.session.spaceService.getSpace(withId: parameters.spaceId), let spaceRoom = space.room else {
MXLog.error("[SpaceMembersCoordinator] spaceMemberListCoordinatorShowInvite: failed to find space with id \(parameters.spaceId)")
return
}
spaceRoom.state { [weak self] roomState in
guard let self = self else { return }
guard let powerLevels = roomState?.powerLevels, let userId = self.parameters.session.myUserId else {
MXLog.error("[SpaceMembersCoordinator] spaceMemberListCoordinatorShowInvite: failed to find powerLevels for room")
return
}
let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: userId)
guard userPowerLevel >= powerLevels.invite else {
let alert = UIAlertController(title: VectorL10n.spacesInvitePeople, message: VectorL10n.spaceInviteNotEnoughPermission, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: VectorL10n.ok, style: .default, handler: nil))
self.navigationRouter.present(alert, animated: true)
return
}
let coordinator = ContactsPickerCoordinator(session: self.parameters.session, room: spaceRoom, initialSearchText: nil, actualParticipants: nil, invitedParticipants: nil, userParticipant: nil, navigationRouter: self.navigationRouter)
coordinator.delegate = self
coordinator.start()
self.childCoordinators.append(coordinator)
}
}
}
// MARK: - ContactsPickerCoordinatorDelegate
extension SpaceMembersCoordinator: ContactsPickerCoordinatorDelegate {
func contactsPickerCoordinatorDidStartLoading(_ coordinator: ContactsPickerCoordinatorProtocol) {
}
func contactsPickerCoordinatorDidEndLoading(_ coordinator: ContactsPickerCoordinatorProtocol) {
}
func contactsPickerCoordinatorDidClose(_ coordinator: ContactsPickerCoordinatorProtocol) {
remove(childCoordinator: coordinator)
}
}
// MARK: - SpaceMemberDetailCoordinatorDelegate
extension SpaceMembersCoordinator: SpaceMemberDetailCoordinatorDelegate {
func spaceMemberDetailCoordinator(_ coordinator: SpaceMemberDetailCoordinatorType, showRoomWithId roomId: String) {
if !UIDevice.current.isPhone, let memberDetailCoordinator = self.memberDetailCoordinator {

View file

@ -21,6 +21,9 @@ enum SpaceMenuListItemAction {
case showAllRoomsInHomeSpace
case exploreSpaceMembers
case exploreSpaceRooms
case addRoom
case addSpace
case settings
case leaveSpace
}
@ -42,6 +45,8 @@ class SpaceMenuListItemViewData {
let style: SpaceMenuListItemStyle
let title: String?
let icon: UIImage?
let isBeta: Bool
/// Any value related to the type of data (e.g. `Bool` for `boolean` style, `nil` for `normal` and `destructive` style)
var value: Any? {
didSet {
@ -50,11 +55,12 @@ class SpaceMenuListItemViewData {
}
weak var delegate: SpaceMenuListItemViewDataDelegate?
init(action: SpaceMenuListItemAction, style: SpaceMenuListItemStyle, title: String?, icon: UIImage?, value: Any?) {
init(action: SpaceMenuListItemAction, style: SpaceMenuListItemStyle, title: String?, icon: UIImage?, value: Any?, isBeta: Bool = false) {
self.action = action
self.style = style
self.title = title
self.icon = icon
self.value = value
self.isBeta = isBeta
}
}

View file

@ -23,6 +23,8 @@ class SpaceMenuListViewCell: UITableViewCell, SpaceMenuCell, NibReusable {
@IBOutlet private weak var iconView: UIImageView!
@IBOutlet private weak var titleLabel: UILabel!
@IBOutlet private weak var betaView: UIView!
@IBOutlet private weak var betaLabel: UILabel!
@IBOutlet private weak var selectionView: UIView!
// MARK: - Private
@ -64,6 +66,10 @@ class SpaceMenuListViewCell: UITableViewCell, SpaceMenuCell, NibReusable {
self.titleLabel.textColor = theme.colors.primaryContent
self.iconView.tintColor = theme.colors.secondaryContent
}
self.betaView.layer.masksToBounds = true
self.betaView.layer.cornerRadius = 4
self.betaView.isHidden = !viewData.isBeta
}
func update(theme: Theme) {
@ -73,5 +79,8 @@ class SpaceMenuListViewCell: UITableViewCell, SpaceMenuCell, NibReusable {
self.titleLabel.textColor = theme.colors.primaryContent
self.titleLabel.font = theme.fonts.body
self.selectionView.backgroundColor = theme.colors.separator
self.betaLabel.font = theme.fonts.caption2SB
self.betaLabel.textColor = theme.colors.secondaryContent
self.betaView.backgroundColor = theme.colors.quinaryContent
}
}

View file

@ -25,12 +25,30 @@
<constraint firstAttribute="height" constant="48" id="f1L-gQ-B6g"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SSn-E2-PZK">
<rect key="frame" x="54" y="21.5" width="250" height="21"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SSn-E2-PZK">
<rect key="frame" x="54" y="21.5" width="193" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="geg-4t-FiC">
<rect key="frame" x="255" y="19.5" width="49" height="25"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="BETA" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dzE-nd-zEE">
<rect key="frame" x="4" y="2" width="41" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="dzE-nd-zEE" firstAttribute="leading" secondItem="geg-4t-FiC" secondAttribute="leading" constant="4" id="G6W-P8-1qV"/>
<constraint firstAttribute="trailing" secondItem="dzE-nd-zEE" secondAttribute="trailing" constant="4" id="Ka7-Y4-F62"/>
<constraint firstAttribute="bottom" secondItem="dzE-nd-zEE" secondAttribute="bottom" constant="2" id="awT-GP-Omf"/>
<constraint firstItem="dzE-nd-zEE" firstAttribute="top" secondItem="geg-4t-FiC" secondAttribute="top" constant="2" id="fte-cV-0l2"/>
</constraints>
</view>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="lGB-4z-4VR">
<rect key="frame" x="16" y="21" width="22" height="22"/>
<constraints>
@ -42,10 +60,12 @@
<constraints>
<constraint firstItem="YQF-X2-MAf" firstAttribute="top" secondItem="kRV-oW-j2b" secondAttribute="top" constant="8" id="0nO-xV-wrC"/>
<constraint firstAttribute="trailing" secondItem="YQF-X2-MAf" secondAttribute="trailing" constant="8" id="4pu-1l-NUB"/>
<constraint firstAttribute="trailing" secondItem="SSn-E2-PZK" secondAttribute="trailing" constant="16" id="9Ql-U0-iqV"/>
<constraint firstItem="lGB-4z-4VR" firstAttribute="leading" secondItem="kRV-oW-j2b" secondAttribute="leading" constant="16" id="9mJ-4Y-VXF"/>
<constraint firstItem="YQF-X2-MAf" firstAttribute="leading" secondItem="kRV-oW-j2b" secondAttribute="leading" constant="8" id="Dlx-2U-0pd"/>
<constraint firstAttribute="trailing" secondItem="geg-4t-FiC" secondAttribute="trailing" constant="16" id="MCa-Cr-Ytx"/>
<constraint firstItem="geg-4t-FiC" firstAttribute="centerY" secondItem="SSn-E2-PZK" secondAttribute="centerY" id="ccE-qZ-Nj3"/>
<constraint firstAttribute="bottom" secondItem="YQF-X2-MAf" secondAttribute="bottom" constant="8" id="fTw-ef-WYp"/>
<constraint firstItem="geg-4t-FiC" firstAttribute="leading" secondItem="SSn-E2-PZK" secondAttribute="trailing" constant="8" id="jOq-4W-byc"/>
<constraint firstItem="SSn-E2-PZK" firstAttribute="centerY" secondItem="kRV-oW-j2b" secondAttribute="centerY" id="jwX-PQ-zct"/>
<constraint firstItem="lGB-4z-4VR" firstAttribute="centerY" secondItem="kRV-oW-j2b" secondAttribute="centerY" id="rCq-Wx-cnB"/>
<constraint firstItem="SSn-E2-PZK" firstAttribute="leading" secondItem="lGB-4z-4VR" secondAttribute="trailing" constant="16" id="u3q-k9-r0F"/>
@ -53,6 +73,8 @@
</tableViewCellContentView>
<viewLayoutGuide key="safeArea" id="0Tk-ek-Uxc"/>
<connections>
<outlet property="betaLabel" destination="dzE-nd-zEE" id="j1O-pO-ym6"/>
<outlet property="betaView" destination="geg-4t-FiC" id="c7b-Hw-TZ8"/>
<outlet property="iconView" destination="lGB-4z-4VR" id="tEM-vI-I3c"/>
<outlet property="selectionView" destination="YQF-X2-MAf" id="Y3z-Ug-Lrc"/>
<outlet property="titleLabel" destination="SSn-E2-PZK" id="bS7-F2-Tfb"/>

View file

@ -24,6 +24,9 @@ class SpaceMenuPresenter: NSObject {
enum Actions {
case exploreRooms
case exploreMembers
case addRoom
case addSpace
case settings
}
// MARK: - Properties
@ -108,6 +111,12 @@ extension SpaceMenuPresenter: SpaceMenuModelViewModelCoordinatorDelegate {
self.delegate?.spaceMenuPresenter(self, didCompleteWith: .exploreMembers, forSpaceWithId: self.spaceId, with: self.session)
case .exploreSpaceRooms:
self.delegate?.spaceMenuPresenter(self, didCompleteWith: .exploreRooms, forSpaceWithId: self.spaceId, with: self.session)
case .addRoom:
self.delegate?.spaceMenuPresenter(self, didCompleteWith: .addRoom, forSpaceWithId: self.spaceId, with: self.session)
case .addSpace:
self.delegate?.spaceMenuPresenter(self, didCompleteWith: .addSpace, forSpaceWithId: self.spaceId, with: self.session)
case .settings:
self.delegate?.spaceMenuPresenter(self, didCompleteWith: .settings, forSpaceWithId: self.spaceId, with: self.session)
default:
MXLog.error("[SpaceMenuPresenter] spaceListViewModel didSelectItem: invalid action \(action)")
}

View file

@ -27,6 +27,9 @@ class SpaceMenuViewModel: SpaceMenuViewModelType {
private let spaceMenuItems: [SpaceMenuListItemViewData] = [
SpaceMenuListItemViewData(action: .exploreSpaceMembers, style: .normal, title: VectorL10n.roomDetailsPeople, icon: Asset.Images.spaceMenuMembers.image, value: nil),
SpaceMenuListItemViewData(action: .exploreSpaceRooms, style: .normal, title: VectorL10n.spacesExploreRooms, icon: Asset.Images.spaceMenuRooms.image, value: nil),
SpaceMenuListItemViewData(action: .addRoom, style: .normal, title: VectorL10n.spacesAddRoom, icon: Asset.Images.spaceMenuPlusIcon.image, value: nil),
SpaceMenuListItemViewData(action: .addSpace, style: .normal, title: VectorL10n.spacesAddSpace, icon: Asset.Images.spaceMenuPlusIcon.image, value: nil, isBeta: true),
SpaceMenuListItemViewData(action: .settings, style: .normal, title: VectorL10n.sideMenuActionSettings, icon: Asset.Images.sideMenuActionIconSettings.image, value: nil),
SpaceMenuListItemViewData(action: .leaveSpace, style: .destructive, title: VectorL10n.leave, icon: Asset.Images.spaceMenuLeave.image, value: nil)
]

View file

@ -26,24 +26,24 @@
</constraints>
</view>
<view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="GVJ-S7-stu" customClass="SpaceAvatarView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="18" y="11" width="32" height="32"/>
<rect key="frame" x="16" y="11" width="40" height="40"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="width" secondItem="GVJ-S7-stu" secondAttribute="height" multiplier="1:1" id="87c-7u-7ge"/>
<constraint firstAttribute="height" constant="32" id="zvP-oT-8po"/>
<constraint firstAttribute="height" constant="40" id="zvP-oT-8po"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="XbP-0o-uBP">
<rect key="frame" x="66" y="7.5" width="238" height="39"/>
<rect key="frame" x="72" y="11.5" width="232" height="39"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gVa-kK-bqa">
<rect key="frame" x="0.0" y="0.0" width="198" height="17"/>
<rect key="frame" x="0.0" y="0.0" width="192" 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="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xhg-rs-E5l">
<rect key="frame" x="202" y="0.0" width="36" height="17"/>
<rect key="frame" x="196" y="0.0" width="36" height="17"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@ -120,7 +120,7 @@
<constraint firstItem="4Ic-S2-Ph6" firstAttribute="leading" secondItem="YoL-49-1Hj" secondAttribute="leading" constant="11" id="BzO-a8-pjD"/>
<constraint firstAttribute="bottom" secondItem="GVJ-S7-stu" secondAttribute="bottom" constant="11" id="CCO-dh-iNA"/>
<constraint firstItem="XbP-0o-uBP" firstAttribute="centerY" secondItem="GVJ-S7-stu" secondAttribute="centerY" id="GUt-0Z-6Px"/>
<constraint firstItem="GVJ-S7-stu" firstAttribute="leading" secondItem="YoL-49-1Hj" secondAttribute="leading" constant="18" id="TmX-fw-4A1"/>
<constraint firstItem="GVJ-S7-stu" firstAttribute="leading" secondItem="YoL-49-1Hj" secondAttribute="leading" constant="16" id="TmX-fw-4A1"/>
<constraint firstAttribute="trailing" secondItem="XbP-0o-uBP" secondAttribute="trailing" constant="16" id="WHX-0h-KAF"/>
<constraint firstItem="4Ic-S2-Ph6" firstAttribute="top" secondItem="YoL-49-1Hj" secondAttribute="top" constant="1" id="ZBt-T2-SNc"/>
<constraint firstAttribute="bottom" secondItem="4Ic-S2-Ph6" secondAttribute="bottom" constant="1" id="aaA-eT-Ma1"/>

View file

@ -26,18 +26,18 @@
</constraints>
</view>
<view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="GVJ-S7-stu" customClass="RoomAvatarView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="18" y="11" width="32" height="32"/>
<rect key="frame" x="16" y="11" width="40" height="40"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="width" secondItem="GVJ-S7-stu" secondAttribute="height" multiplier="1:1" id="87c-7u-7ge"/>
<constraint firstAttribute="height" constant="32" id="zvP-oT-8po"/>
<constraint firstAttribute="height" constant="40" id="zvP-oT-8po"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="XbP-0o-uBP">
<rect key="frame" x="66" y="7.5" width="238" height="39"/>
<rect key="frame" x="72" y="11.5" width="232" height="39"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gVa-kK-bqa">
<rect key="frame" x="0.0" y="0.0" width="198" height="17"/>
<rect key="frame" x="0.0" y="0.0" width="192" height="17"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@ -56,13 +56,13 @@
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="Description" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cKk-HO-IfB">
<rect key="frame" x="41" y="23" width="197" height="16"/>
<rect key="frame" x="41" y="23" width="191" height="16"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCallout"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xhg-rs-E5l">
<rect key="frame" x="202" y="0.0" width="36" height="17"/>
<rect key="frame" x="196" y="0.0" width="36" height="17"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@ -92,7 +92,7 @@
<constraint firstItem="4Ic-S2-Ph6" firstAttribute="leading" secondItem="YoL-49-1Hj" secondAttribute="leading" constant="11" id="BzO-a8-pjD"/>
<constraint firstAttribute="bottom" secondItem="GVJ-S7-stu" secondAttribute="bottom" constant="11" id="CCO-dh-iNA"/>
<constraint firstItem="XbP-0o-uBP" firstAttribute="centerY" secondItem="GVJ-S7-stu" secondAttribute="centerY" id="GUt-0Z-6Px"/>
<constraint firstItem="GVJ-S7-stu" firstAttribute="leading" secondItem="YoL-49-1Hj" secondAttribute="leading" constant="18" id="TmX-fw-4A1"/>
<constraint firstItem="GVJ-S7-stu" firstAttribute="leading" secondItem="YoL-49-1Hj" secondAttribute="leading" constant="16" id="TmX-fw-4A1"/>
<constraint firstAttribute="trailing" secondItem="XbP-0o-uBP" secondAttribute="trailing" constant="16" id="WHX-0h-KAF"/>
<constraint firstItem="4Ic-S2-Ph6" firstAttribute="top" secondItem="YoL-49-1Hj" secondAttribute="top" constant="1" id="ZBt-T2-SNc"/>
<constraint firstAttribute="bottom" secondItem="4Ic-S2-Ph6" secondAttribute="bottom" constant="1" id="aaA-eT-Ma1"/>

View file

@ -24,7 +24,7 @@
<outlet property="delegate" destination="V8j-Lb-PgC" id="MCU-dH-BVp"/>
</connections>
</searchBar>
<tableView clipsSubviews="YES" alpha="0.0" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="Ky8-xS-gvY">
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="Ky8-xS-gvY">
<rect key="frame" x="0.0" y="95" width="414" height="801"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<connections>

View file

@ -39,10 +39,9 @@ final class SpaceExploreRoomViewController: UIViewController {
private var errorPresenter: MXKErrorPresentation!
private var activityPresenter: ActivityIndicatorPresenter!
private var titleView: MainTitleView!
private var emptyView: RootTabEmptyView!
private var plusButtonImageView: UIImageView!
private var hasMore: Bool = false
private let addRoomHeaderView = AddItemHeaderView.instantiate(title: VectorL10n.spacesAddRoom, icon: Asset.Images.spaceAddRoom.image)
private var itemDataList: [SpaceExploreRoomListItemViewData] = [] {
didSet {
self.tableView.reloadData()
@ -53,22 +52,12 @@ final class SpaceExploreRoomViewController: UIViewController {
return ThemeService.shared().isCurrentThemeDark() ? Asset.Images.roomsEmptyScreenArtworkDark.image : Asset.Images.roomsEmptyScreenArtwork.image
}
private var scrollViewHidden = true {
didSet {
UIView.animate(withDuration: 0.2) {
self.tableView.alpha = self.scrollViewHidden ? 0 : 1
self.emptyView.alpha = self.scrollViewHidden ? 1 : 0
}
}
}
// MARK: - Setup
class func instantiate(with viewModel: SpaceExploreRoomViewModelType) -> SpaceExploreRoomViewController {
let viewController = StoryboardScene.SpaceExploreRoomViewController.initialScene.instantiate()
viewController.viewModel = viewModel
viewController.theme = ThemeService.shared().theme
viewController.emptyView = RootTabEmptyView.instantiate()
return viewController
}
@ -122,8 +111,9 @@ final class SpaceExploreRoomViewController: UIViewController {
self.titleView.update(theme: theme)
self.tableView.backgroundColor = theme.colors.background
self.tableView.reloadData()
self.emptyView.update(theme: theme)
theme.applyStyle(onSearchBar: self.tableSearchBar)
self.addRoomHeaderView.update(theme: theme)
}
private func registerThemeServiceDidChangeThemeNotification() {
@ -152,16 +142,14 @@ final class SpaceExploreRoomViewController: UIViewController {
self.tableView.keyboardDismissMode = .interactive
self.setupTableView()
self.emptyView.fill(with: self.emptyViewArtwork, title: VectorL10n.roomsEmptyViewTitle, informationText: VectorL10n.roomsEmptyViewInformation)
self.plusButtonImageView = self.vc_addFAB(withImage: Asset.Images.roomsFloatingAction.image, target: self, action: #selector(addRoomAction(semder:)))
self.emptyView.frame = CGRect(x: 0, y: self.tableSearchBar.frame.maxY, width: self.view.bounds.width, height: self.view.bounds.height - self.tableSearchBar.frame.maxY)
self.emptyView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
self.emptyView.alpha = 0
self.view.insertSubview(self.emptyView, at: 0)
self.setupTableViewHeader()
}
private func setupTableViewHeader() {
addRoomHeaderView.delegate = self
tableView.tableHeaderView = addRoomHeaderView
}
private func setupTableView() {
self.tableView.separatorStyle = .none
self.tableView.rowHeight = UITableView.automaticDimension
@ -198,7 +186,6 @@ final class SpaceExploreRoomViewController: UIViewController {
private func renderLoaded(children: [SpaceExploreRoomListItemViewData]) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.itemDataList = children
self.scrollViewHidden = false
}
private func render(error: Error) {
@ -207,15 +194,11 @@ final class SpaceExploreRoomViewController: UIViewController {
}
private func renderEmptySpace() {
self.emptyView.fill(with: self.emptyViewArtwork, title: VectorL10n.spacesEmptySpaceTitle, informationText: VectorL10n.spacesEmptySpaceDetail)
self.scrollViewHidden = true
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.renderLoaded(children: [])
}
private func renderEmptyFilterResult() {
self.emptyView.fill(with: self.emptyViewArtwork, title: VectorL10n.spacesNoResultFoundTitle, informationText: VectorL10n.spacesNoRoomFoundDetail)
self.scrollViewHidden = true
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.renderLoaded(children: [])
}
// MARK: - Actions
@ -286,3 +269,12 @@ extension SpaceExploreRoomViewController: SpaceExploreRoomViewModelViewDelegate
self.render(viewState: viewSate)
}
}
// MARK: - SpaceMemberListViewModelViewDelegate
extension SpaceExploreRoomViewController: AddItemHeaderViewDelegate {
func addItemHeaderView(_ headerView: AddItemHeaderView, didTapButton button: UIButton) {
self.errorPresenter.presentError(from: self, title: VectorL10n.spacesAddRoomsComingSoonTitle, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName), animated: true, handler: nil)
}
}

View file

@ -28,6 +28,8 @@ final class ExploreRoomCoordinator: ExploreRoomCoordinatorType {
private let navigationRouter: NavigationRouterType
private let session: MXSession
private let spaceId: String
// We need to stack the ID of visited space and subspaces so we know what is the current space ID when navigating to a room
private var spaceIdStack: [String]
private weak var roomDetailCoordinator: SpaceChildRoomDetailCoordinator?
private lazy var slidingModalPresenter: SlidingModalPresenter = {
@ -47,6 +49,7 @@ final class ExploreRoomCoordinator: ExploreRoomCoordinatorType {
self.navigationRouter = NavigationRouter(navigationController: RiotNavigationController())
self.session = session
self.spaceId = spaceId
self.spaceIdStack = [spaceId]
}
// MARK: - Public methods
@ -72,8 +75,10 @@ final class ExploreRoomCoordinator: ExploreRoomCoordinatorType {
let coordinator = self.createShowSpaceExploreRoomCoordinator(session: self.session, spaceId: item.childInfo.childRoomId, spaceName: item.childInfo.name)
coordinator.start()
self.add(childCoordinator: coordinator)
self.spaceIdStack.append(item.childInfo.childRoomId)
self.navigationRouter.push(coordinator.toPresentable(), animated: true) {
self.remove(childCoordinator: coordinator)
self.spaceIdStack.removeLast()
}
}
@ -134,6 +139,7 @@ final class ExploreRoomCoordinator: ExploreRoomCoordinatorType {
}
self?.navigationRouter.push(roomViewController, animated: true, popCompletion: nil)
roomViewController.parentSpaceId = self?.spaceIdStack.last
roomViewController.displayRoom(roomDataSource)
roomViewController.navigationItem.leftItemsSupplementBackButton = true
roomViewController.showMissedDiscussionsBadge = false

View file

@ -394,6 +394,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
let roomCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared,
session: roomNavigationParameters.mxSession,
parentSpaceId: self.currentSpaceId,
roomId: roomNavigationParameters.roomId,
eventId: roomNavigationParameters.eventId)
@ -407,7 +408,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
// RoomCoordinator will be presented by the split view.
// As we don't know which navigation controller instance will be used,
// give the NavigationRouterStore instance and let it find the associated navigation controller
let roomCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared, session: matrixSession, roomId: roomId, eventId: eventId)
let roomCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared, session: matrixSession, parentSpaceId: self.currentSpaceId, roomId: roomId, eventId: eventId)
self.showRoom(with: roomCoordinatorParameters, completion: completion)
}
@ -417,7 +418,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
// RoomCoordinator will be presented by the split view
// We don't which navigation controller instance will be used
// Give the NavigationRouterStore instance and let it find the associated navigation controller if needed
let roomCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared, previewData: previewData)
let roomCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared, parentSpaceId: self.currentSpaceId, previewData: previewData)
self.showRoom(with: roomCoordinatorParameters)
}
@ -425,7 +426,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
private func showRoomPreview(withNavigationParameters roomPreviewNavigationParameters: RoomPreviewNavigationParameters, completion: (() -> Void)?) {
let roomCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared,
previewData: roomPreviewNavigationParameters.previewData)
parentSpaceId: self.currentSpaceId, previewData: roomPreviewNavigationParameters.previewData)
self.showRoom(with: roomCoordinatorParameters,
stackOnSplitViewDetail: roomPreviewNavigationParameters.presentationParameters.stackAboveVisibleViews,

View file

@ -45,6 +45,7 @@
#import "RoomInputToolbarView.h"
#import "NSArray+Element.h"
#import "ShareItemSender.h"
#import "Contact.h"
#import "HTMLFormatter.h"
#import "RoomTimelineCellProvider.h"
#import "PlainRoomTimelineCellProvider.h"

1
changelog.d/5225.feature Normal file
View file

@ -0,0 +1 @@
Invite to Space in room landing

1
changelog.d/5226.feature Normal file
View file

@ -0,0 +1 @@
Implement FAB journeys & rough edge warnings

1
changelog.d/5227.feature Normal file
View file

@ -0,0 +1 @@
Space panel overflow journeys & rough edge warnings