Merge pull request #5316 from vector-im/gil/5225_invite_to_space_in_room_landing
Invite to space in room landing
23
Riot/Assets/Images.xcassets/Spaces/space_add_room.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Spaces/space_add_room.imageset/space_add_room.png
vendored
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
Riot/Assets/Images.xcassets/Spaces/space_add_room.imageset/space_add_room@2x.png
vendored
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
Riot/Assets/Images.xcassets/Spaces/space_add_room.imageset/space_add_room@3x.png
vendored
Normal file
After Width: | Height: | Size: 2.3 KiB |
26
Riot/Assets/Images.xcassets/Spaces/space_invite_user.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Spaces/space_invite_user.imageset/space_invite_user.png
vendored
Normal file
After Width: | Height: | Size: 496 B |
BIN
Riot/Assets/Images.xcassets/Spaces/space_invite_user.imageset/space_invite_user@2x.png
vendored
Normal file
After Width: | Height: | Size: 865 B |
BIN
Riot/Assets/Images.xcassets/Spaces/space_invite_user.imageset/space_invite_user@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.2 KiB |
26
Riot/Assets/Images.xcassets/Spaces/space_menu_plus_icon.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Spaces/space_menu_plus_icon.imageset/space_menu_plus_icon.png
vendored
Normal file
After Width: | Height: | Size: 321 B |
BIN
Riot/Assets/Images.xcassets/Spaces/space_menu_plus_icon.imageset/space_menu_plus_icon@2x.png
vendored
Normal file
After Width: | Height: | Size: 443 B |
BIN
Riot/Assets/Images.xcassets/Spaces/space_menu_plus_icon.imageset/space_menu_plus_icon@3x.png
vendored
Normal file
After Width: | Height: | Size: 563 B |
|
@ -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 won’t be a member of %@.";
|
||||
"room_invite_to_room_option_title" = "To just this room";
|
||||
"room_invite_to_room_option_detail" = "They won’t 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 aren’t 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";
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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 won’t 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 won’t 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")
|
||||
|
|
|
@ -195,6 +195,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
|
|||
|
||||
- (void)setCurrentSpace:(MXSpace *)currentSpace
|
||||
{
|
||||
super.currentSpace = currentSpace;
|
||||
[self.recentsListService updateSpace:currentSpace];
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)];
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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?
|
||||
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -70,4 +70,8 @@ extension SpaceMemberListCoordinator: SpaceMemberListViewModelCoordinatorDelegat
|
|||
func spaceMemberListViewModelDidCancel(_ viewModel: SpaceMemberListViewModelType) {
|
||||
self.delegate?.spaceMemberListCoordinatorDidCancel(self)
|
||||
}
|
||||
|
||||
func spaceMemberListViewModelShowInvite(_ viewModel: SpaceMemberListViewModelType) {
|
||||
self.delegate?.spaceMemberListCoordinatorShowInvite(self)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -23,4 +23,5 @@ enum SpaceMemberListViewAction {
|
|||
case loadData
|
||||
case complete(_ selectedMember: MXRoomMember, _ sourceView: UIView?)
|
||||
case cancel
|
||||
case invite
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,6 +57,8 @@ final class SpaceMemberListViewModel: SpaceMemberListViewModelType {
|
|||
case .cancel:
|
||||
self.cancelOperations()
|
||||
self.coordinatorDelegate?.spaceMemberListViewModelDidCancel(self)
|
||||
case .invite:
|
||||
self.coordinatorDelegate?.spaceMemberListViewModelShowInvite(self)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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)")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
]
|
||||
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
@ -0,0 +1 @@
|
|||
Invite to Space in room landing
|
1
changelog.d/5226.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Implement FAB journeys & rough edge warnings
|
1
changelog.d/5227.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Space panel overflow journeys & rough edge warnings
|