From ccdb571821c18bf81747c9c825199d7cd83af127 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 23 Jul 2021 16:34:43 +0100 Subject: [PATCH 001/276] Add quinary colour to themes. --- Riot/Managers/Theme/Theme.swift | 1 + Riot/Managers/Theme/Themes/DarkTheme.swift | 1 + Riot/Managers/Theme/Themes/DefaultTheme.swift | 1 + 3 files changed, 3 insertions(+) diff --git a/Riot/Managers/Theme/Theme.swift b/Riot/Managers/Theme/Theme.swift index 72d2d1ab8..a8b8c9ccd 100644 --- a/Riot/Managers/Theme/Theme.swift +++ b/Riot/Managers/Theme/Theme.swift @@ -41,6 +41,7 @@ import DesignKit var textPrimaryColor: UIColor { get } var textSecondaryColor: UIColor { get } var textTertiaryColor: UIColor { get } + var textQuinaryColor: UIColor { get } var tintColor: UIColor { get } var tintBackgroundColor: UIColor { get } diff --git a/Riot/Managers/Theme/Themes/DarkTheme.swift b/Riot/Managers/Theme/Themes/DarkTheme.swift index 7ce49c214..9a99f55e6 100644 --- a/Riot/Managers/Theme/Themes/DarkTheme.swift +++ b/Riot/Managers/Theme/Themes/DarkTheme.swift @@ -42,6 +42,7 @@ class DarkTheme: NSObject, Theme { var textPrimaryColor: UIColor = UIColor(rgb: 0xFFFFFF) var textSecondaryColor: UIColor = UIColor(rgb: 0xA9B2BC) var textTertiaryColor: UIColor = UIColor(rgb: 0x8E99A4) + var textQuinaryColor: UIColor = UIColor(rgb: 0x394049) var tintColor: UIColor = UIColor(displayP3Red: 0.05098039216, green: 0.7450980392, blue: 0.5450980392, alpha: 1.0) var tintBackgroundColor: UIColor = UIColor(rgb: 0x1F6954) diff --git a/Riot/Managers/Theme/Themes/DefaultTheme.swift b/Riot/Managers/Theme/Themes/DefaultTheme.swift index 770f33a90..91fc69921 100644 --- a/Riot/Managers/Theme/Themes/DefaultTheme.swift +++ b/Riot/Managers/Theme/Themes/DefaultTheme.swift @@ -42,6 +42,7 @@ class DefaultTheme: NSObject, Theme { var textPrimaryColor: UIColor = UIColor(rgb: 0x17191C) var textSecondaryColor: UIColor = UIColor(rgb: 0x737D8C) var textTertiaryColor: UIColor = UIColor(rgb: 0x8D99A5) + var textQuinaryColor: UIColor = UIColor(rgb: 0xE3E8F0) var tintColor: UIColor = UIColor(displayP3Red: 0.05098039216, green: 0.7450980392, blue: 0.5450980392, alpha: 1.0) var tintBackgroundColor: UIColor = UIColor(rgb: 0xe9fff9) From 45084810d0606f1af503461917b052576ef1ac71 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 23 Jul 2021 16:43:39 +0100 Subject: [PATCH 002/276] Add RequestContactsAccessFooterView. Update InviteFriendsHeaderView and StartChatViewController styles to match Figma doc. --- .../Contacts/DataSources/ContactsDataSource.m | 6 +- .../StartChat/InviteFriendsHeaderView.swift | 21 ++-- .../StartChat/InviteFriendsHeaderView.xib | 18 ++-- .../RequestContactsAccessFooterView.swift | 66 ++++++++++++ .../RequestContactsAccessFooterView.xib | 101 ++++++++++++++++++ .../StartChat/StartChatViewController.m | 87 +++++++++++++-- .../StartChat/StartChatViewController.xib | 21 ++-- 7 files changed, 280 insertions(+), 40 deletions(-) create mode 100644 Riot/Modules/StartChat/RequestContactsAccessFooterView.swift create mode 100644 Riot/Modules/StartChat/RequestContactsAccessFooterView.xib diff --git a/Riot/Modules/Contacts/DataSources/ContactsDataSource.m b/Riot/Modules/Contacts/DataSources/ContactsDataSource.m index 52a68f130..233c72130 100644 --- a/Riot/Modules/Contacts/DataSources/ContactsDataSource.m +++ b/Riot/Modules/Contacts/DataSources/ContactsDataSource.m @@ -466,8 +466,8 @@ searchInputSection = count++; } - // Keep visible the header for the both contact sections, even if their are empty. - if (BuildSettings.allowLocalContactsAccess) + // Keep visible the header for the both contact sections, even if they're are empty. + if (BuildSettings.allowLocalContactsAccess && [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized) { filteredLocalContactsSection = count++; } @@ -482,7 +482,7 @@ } // Keep visible the local contact header, even if the section is empty. - if (BuildSettings.allowLocalContactsAccess) + if (BuildSettings.allowLocalContactsAccess && [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized) { filteredLocalContactsSection = count++; } diff --git a/Riot/Modules/StartChat/InviteFriendsHeaderView.swift b/Riot/Modules/StartChat/InviteFriendsHeaderView.swift index c1d81ea06..e59539910 100644 --- a/Riot/Modules/StartChat/InviteFriendsHeaderView.swift +++ b/Riot/Modules/StartChat/InviteFriendsHeaderView.swift @@ -50,27 +50,30 @@ final class InviteFriendsHeaderView: UIView, NibLoadable, Themable { override func awakeFromNib() { super.awakeFromNib() - self.button.setTitle(VectorL10n.inviteFriendsAction(BuildSettings.bundleDisplayName), for: .normal) - self.button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside) + button.setTitle(VectorL10n.inviteFriendsAction(BuildSettings.bundleDisplayName), for: .normal) + button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside) + button.layer.cornerRadius = 8 + button.layer.borderWidth = 2 } // MARK: - Public func update(theme: Theme) { - self.backgroundColor = theme.backgroundColor + backgroundColor = theme.backgroundColor - self.button.setTitleColor(theme.baseTextPrimaryColor, for: .normal) - self.button.setTitleColor(theme.baseTextPrimaryColor.withAlphaComponent(Constants.buttonHighlightedAlpha), for: .highlighted) - self.button.vc_setBackgroundColor(theme.tintColor, for: .normal) + 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.backgroundColor, for: .normal) - let buttonImage = Asset.Images.shareActionButton.image.vc_tintedImage(usingColor: theme.baseIconPrimaryColor) + let buttonImage = Asset.Images.shareActionButton.image.vc_tintedImage(usingColor: theme.tintColor) - self.button.setImage(buttonImage, for: .normal) + button.setImage(buttonImage, for: .normal) } // MARK: - Action @objc private func buttonAction(_ sender: UIButton) { - self.delegate?.inviteFriendsHeaderView(self, didTapButton: button) + delegate?.inviteFriendsHeaderView(self, didTapButton: button) } } diff --git a/Riot/Modules/StartChat/InviteFriendsHeaderView.xib b/Riot/Modules/StartChat/InviteFriendsHeaderView.xib index fa0d1a1f6..3f62c617e 100644 --- a/Riot/Modules/StartChat/InviteFriendsHeaderView.xib +++ b/Riot/Modules/StartChat/InviteFriendsHeaderView.xib @@ -1,9 +1,9 @@ - + - + @@ -11,14 +11,14 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/StartChat/StartChatViewController.m b/Riot/Modules/StartChat/StartChatViewController.m index 428fa13fc..5cadcde1f 100644 --- a/Riot/Modules/StartChat/StartChatViewController.m +++ b/Riot/Modules/StartChat/StartChatViewController.m @@ -20,7 +20,7 @@ #import "Riot-Swift.h" #import "MXSession+Riot.h" -@interface StartChatViewController () +@interface StartChatViewController () { // The contact used to describe the current user. MXKContact *userContact; @@ -49,6 +49,8 @@ @property (nonatomic, strong) InviteFriendsPresenter *inviteFriendsPresenter; @property (nonatomic, weak) InviteFriendsHeaderView *inviteFriendsHeaderView; +@property (nonatomic, strong) RequestContactsAccessFooterView *requestContactsAccessFooterView; + @end @implementation StartChatViewController @@ -120,9 +122,6 @@ _searchBarView.autocapitalizationType = UITextAutocapitalizationTypeNone; [self refreshSearchBarItemsColor:_searchBarView]; - // Hide line separators of empty cells - self.contactsTableView.tableFooterView = [[UIView alloc] init]; - [self.contactsTableView registerClass:ContactTableViewCell.class forCellReuseIdentifier:@"ParticipantTableViewCellId"]; // Redirect table data source @@ -161,16 +160,34 @@ } } +- (void)updateFooterView +{ + if (!RiotSettings.shared.allowInviteExernalUsers + || [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized + || self.contactsTableView.numberOfSections > 0) + { + // Hide line separators of empty cells + // FIXME: Store this? + self.contactsTableView.tableFooterView = [[UIView alloc] init]; + return; + } + + RequestContactsAccessFooterView *contactsAccessView = self.requestContactsAccessFooterView ?: [RequestContactsAccessFooterView instantiate]; + contactsAccessView.delegate = self; + self.contactsTableView.tableFooterView = contactsAccessView; + + self.requestContactsAccessFooterView = contactsAccessView; +} + - (void)userInterfaceThemeDidChange { [super userInterfaceThemeDidChange]; [self refreshSearchBarItemsColor:_searchBarView]; - _searchBarHeaderBorder.backgroundColor = ThemeService.shared.theme.headerBorderColor; - // Check the table view style to select its bg color. self.contactsTableView.backgroundColor = ((self.contactsTableView.style == UITableViewStylePlain) ? ThemeService.shared.theme.backgroundColor : ThemeService.shared.theme.headerBackgroundColor); + self.navigationController.navigationBar.barTintColor = self.contactsTableView.backgroundColor; self.view.backgroundColor = self.contactsTableView.backgroundColor; self.contactsTableView.separatorColor = ThemeService.shared.theme.lineBreakColor; @@ -230,6 +247,8 @@ // Refresh display [self refreshContactsTable]; } + + [self updateFooterView]; } - (void)viewDidDisappear:(BOOL)animated @@ -244,6 +263,31 @@ { [super viewDidLayoutSubviews]; [self.contactsTableView vc_relayoutHeaderView]; + [self layoutRequestContactsAccessFooterView]; +} + +- (void)layoutRequestContactsAccessFooterView +{ + // FIXME: add nil checks + if (self.contactsTableView.tableFooterView == self.requestContactsAccessFooterView) + { + CGSize footerSize = [self.requestContactsAccessFooterView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; + CGFloat gapHeight = self.contactsTableView.bounds.size.height - self.contactsTableView.adjustedContentInset.top - self.contactsTableView.adjustedContentInset.bottom - self.inviteFriendsHeaderView.frame.size.height; + if (gapHeight > footerSize.height) + { + self.requestContactsAccessFooterView.frame = CGRectMake(self.requestContactsAccessFooterView.frame.origin.x, + self.requestContactsAccessFooterView.frame.origin.y, + self.requestContactsAccessFooterView.frame.size.width, + gapHeight); + } + else + { + self.requestContactsAccessFooterView.frame = CGRectMake(self.requestContactsAccessFooterView.frame.origin.x, + self.requestContactsAccessFooterView.frame.origin.y, + footerSize.width, + footerSize.height); + } + } } #pragma mark - @@ -693,7 +737,10 @@ leftImageView.image = [leftImageView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; leftImageView.tintColor = ThemeService.shared.theme.tintColor; - // remove the gray background color + // Use the theme's grey color. + // The effect views are needed due to minimal style. + // With default style there is a border above the search bar. + searchBarTextField.backgroundColor = ThemeService.shared.theme.textQuinaryColor; UIView *effectBackgroundTop = [searchBarTextField valueForKey:@"_effectBackgroundTop"]; UIView *effectBackgroundBottom = [searchBarTextField valueForKey:@"_effectBackgroundBottom"]; effectBackgroundTop.hidden = YES; @@ -712,13 +759,18 @@ } - (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar -{ +{ self.isAddParticipantSearchBarEditing = YES; searchBar.showsCancelButton = NO; return YES; } +- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar +{ + [self updateFooterView]; +} + - (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar { searchBar.text = nil; @@ -731,6 +783,11 @@ [searchBar resignFirstResponder]; } +- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar +{ + [self updateFooterView]; +} + #pragma mark - ContactsTableViewControllerDelegate - (void)contactsTableViewController:(ContactsTableViewController *)contactsTableViewController didSelectContact:(MXKContact*)contact @@ -785,4 +842,18 @@ [self showInviteFriendsFromSourceView:button]; } +#pragma mark - RequestContactsAccessFooterViewDelegate + +- (void)didRequestContactsAccess +{ + [MXKTools checkAccessForContacts:@"Doug" showPopUpInViewController:self completionHandler:^(BOOL granted) { + if (granted) + { + [self updateFooterView]; + // FIXME: Local contacts are refreshed but not matched + [MXKContactManager.sharedManager refreshLocalContacts]; + } + }]; +} + @end diff --git a/Riot/Modules/StartChat/StartChatViewController.xib b/Riot/Modules/StartChat/StartChatViewController.xib index a3e924401..958aaf904 100644 --- a/Riot/Modules/StartChat/StartChatViewController.xib +++ b/Riot/Modules/StartChat/StartChatViewController.xib @@ -1,11 +1,9 @@ - - - - + + - + @@ -25,7 +23,7 @@ - + @@ -36,10 +34,10 @@ - + - + @@ -60,9 +58,9 @@ - + - + @@ -70,6 +68,7 @@ + @@ -80,7 +79,7 @@ - + From 19f35940622a405ea66cb8f929560d80999665e5 Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 26 Jul 2021 12:20:10 +0100 Subject: [PATCH 003/276] Enable local contact sync if granted access. Improve layout. --- .../StartChat/StartChatViewController.m | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/Riot/Modules/StartChat/StartChatViewController.m b/Riot/Modules/StartChat/StartChatViewController.m index 5cadcde1f..4708fee0f 100644 --- a/Riot/Modules/StartChat/StartChatViewController.m +++ b/Riot/Modules/StartChat/StartChatViewController.m @@ -263,16 +263,21 @@ { [super viewDidLayoutSubviews]; [self.contactsTableView vc_relayoutHeaderView]; - [self layoutRequestContactsAccessFooterView]; + [self updateRequestContactsAccessFooterViewHeight]; } -- (void)layoutRequestContactsAccessFooterView +- (void)updateRequestContactsAccessFooterViewHeight { - // FIXME: add nil checks - if (self.contactsTableView.tableFooterView == self.requestContactsAccessFooterView) + if (self.requestContactsAccessFooterView && self.requestContactsAccessFooterView == self.contactsTableView.tableFooterView) { CGSize footerSize = [self.requestContactsAccessFooterView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; - CGFloat gapHeight = self.contactsTableView.bounds.size.height - self.contactsTableView.adjustedContentInset.top - self.contactsTableView.adjustedContentInset.bottom - self.inviteFriendsHeaderView.frame.size.height; + CGFloat gapHeight = self.contactsTableView.bounds.size.height - self.contactsTableView.adjustedContentInset.top - self.contactsTableView.adjustedContentInset.bottom; + + if (self.contactsTableView.tableHeaderView) + { + gapHeight -= self.contactsTableView.tableHeaderView.frame.size.height; + } + if (gapHeight > footerSize.height) { self.requestContactsAccessFooterView.frame = CGRectMake(self.requestContactsAccessFooterView.frame.origin.x, @@ -284,7 +289,7 @@ { self.requestContactsAccessFooterView.frame = CGRectMake(self.requestContactsAccessFooterView.frame.origin.x, self.requestContactsAccessFooterView.frame.origin.y, - footerSize.width, + self.requestContactsAccessFooterView.frame.size.width, footerSize.height); } } @@ -846,11 +851,14 @@ - (void)didRequestContactsAccess { - [MXKTools checkAccessForContacts:@"Doug" showPopUpInViewController:self completionHandler:^(BOOL granted) { + [MXKTools checkAccessForContacts:@"Contacts access has been disabled in the Settings app." showPopUpInViewController:self completionHandler:^(BOOL granted) { if (granted) { + // Hide the request access view. [self updateFooterView]; - // FIXME: Local contacts are refreshed but not matched + + // Enable sync local contacts and refresh the contacts manager. + MXKAppSettings.standardAppSettings.syncLocalContacts = YES; [MXKContactManager.sharedManager refreshLocalContacts]; } }]; From e585041d044ed6dce743c44e1b8892b6f2fced17 Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 26 Jul 2021 12:54:24 +0100 Subject: [PATCH 004/276] Update search bar string and appearance. --- Riot/Assets/en.lproj/Vector.strings | 2 +- Riot/Generated/Strings.swift | 2 +- Riot/Modules/StartChat/StartChatViewController.m | 8 +------- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 80c55d989..8410a5f8d 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -176,7 +176,7 @@ "room_creation_keep_private" = "Keep private"; "room_creation_make_private" = "Make private"; "room_creation_wait_for_creation" = "A room is already being created. Please wait."; -"room_creation_invite_another_user" = "Search / invite by User ID, Name or email"; +"room_creation_invite_another_user" = "Invite by User ID, Name or email"; "room_creation_error_invite_user_by_email_without_identity_server" = "No identity server is configured so you cannot add a participant with an email."; "room_creation_dm_error" = "We couldn't create your DM. Please check the users you want to invite and try again."; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 3de870902..a9c21c129 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2418,7 +2418,7 @@ internal enum VectorL10n { internal static var roomCreationErrorInviteUserByEmailWithoutIdentityServer: String { return VectorL10n.tr("Vector", "room_creation_error_invite_user_by_email_without_identity_server") } - /// Search / invite by User ID, Name or email + /// Invite by User ID, Name or email internal static var roomCreationInviteAnotherUser: String { return VectorL10n.tr("Vector", "room_creation_invite_another_user") } diff --git a/Riot/Modules/StartChat/StartChatViewController.m b/Riot/Modules/StartChat/StartChatViewController.m index 4708fee0f..4af062baf 100644 --- a/Riot/Modules/StartChat/StartChatViewController.m +++ b/Riot/Modules/StartChat/StartChatViewController.m @@ -740,7 +740,7 @@ // Magnifying glass icon. UIImageView *leftImageView = (UIImageView *)searchBarTextField.leftView; leftImageView.image = [leftImageView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - leftImageView.tintColor = ThemeService.shared.theme.tintColor; + leftImageView.tintColor = ThemeService.shared.theme.textSecondaryColor; // Use the theme's grey color. // The effect views are needed due to minimal style. @@ -750,12 +750,6 @@ UIView *effectBackgroundBottom = [searchBarTextField valueForKey:@"_effectBackgroundBottom"]; effectBackgroundTop.hidden = YES; effectBackgroundBottom.hidden = YES; - - // place holder - if (searchBarTextField.placeholder) - { - searchBarTextField.textColor = ThemeService.shared.theme.placeholderTextColor; - } } - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText From 19f3bbd78bec2af1a4510a74b7abd433b93e1019 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 5 Aug 2021 17:34:09 +0100 Subject: [PATCH 005/276] Move RequestContactsAccessFooterView into ContactsTableViewController. Remove automatic triggering of contacts access. --- .../Images.xcassets/Contacts/Contents.json | 6 + .../Contents.json | 23 +++ .../facepile.png | Bin 0 -> 13107 bytes .../facepile@2x.png | Bin 0 -> 43651 bytes .../facepile@3x.png | Bin 0 -> 88835 bytes Riot/Generated/Images.swift | 1 + .../Contacts/ContactsTableViewController.m | 166 +++++++++++++----- .../Contacts/DataSources/ContactsDataSource.h | 5 + .../Contacts/DataSources/ContactsDataSource.m | 5 + .../RequestContactsAccessFooterView.swift | 0 .../RequestContactsAccessFooterView.xib | 26 +-- .../StartChat/StartChatViewController.m | 82 +-------- 12 files changed, 178 insertions(+), 136 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/Contacts/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Contacts/contacts_request_facepile.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Contacts/contacts_request_facepile.imageset/facepile.png create mode 100644 Riot/Assets/Images.xcassets/Contacts/contacts_request_facepile.imageset/facepile@2x.png create mode 100644 Riot/Assets/Images.xcassets/Contacts/contacts_request_facepile.imageset/facepile@3x.png rename Riot/Modules/{StartChat => Contacts/Views}/RequestContactsAccessFooterView.swift (100%) rename Riot/Modules/{StartChat => Contacts/Views}/RequestContactsAccessFooterView.xib (87%) diff --git a/Riot/Assets/Images.xcassets/Contacts/Contents.json b/Riot/Assets/Images.xcassets/Contacts/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Contacts/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Contacts/contacts_request_facepile.imageset/Contents.json b/Riot/Assets/Images.xcassets/Contacts/contacts_request_facepile.imageset/Contents.json new file mode 100644 index 000000000..824fd548a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Contacts/contacts_request_facepile.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "facepile.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "facepile@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "facepile@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Contacts/contacts_request_facepile.imageset/facepile.png b/Riot/Assets/Images.xcassets/Contacts/contacts_request_facepile.imageset/facepile.png new file mode 100644 index 0000000000000000000000000000000000000000..9e5b49a61985621b5631a9da124c7e35d954340c GIT binary patch literal 13107 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 N002ovPDHLkV1nmUGfw~j literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Contacts/contacts_request_facepile.imageset/facepile@2x.png b/Riot/Assets/Images.xcassets/Contacts/contacts_request_facepile.imageset/facepile@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..90ad6b27d9f6489c10ee477a8b255f59452f47f5 GIT binary patch literal 43651 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 literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Contacts/contacts_request_facepile.imageset/facepile@3x.png b/Riot/Assets/Images.xcassets/Contacts/contacts_request_facepile.imageset/facepile@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..f2324c7eeef12ca7eb49fbc86a2dc147981d0158 GIT binary patch literal 88835 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

-5w(2 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 z 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 { /** Observe kAppDelegateDidTapStatusBarNotification to handle tap on clock status bar. @@ -41,6 +41,9 @@ id kThemeServiceDidChangeThemeNotificationObserver; } +@property (nonatomic, strong) RequestContactsAccessFooterView *requestContactsAccessFooterView; +@property (nonatomic) BOOL shouldHideFooterView; + @end @implementation ContactsTableViewController @@ -92,6 +95,7 @@ // Hide line separators of empty cells self.contactsTableView.tableFooterView = [[UIView alloc] init]; + self.shouldHideFooterView = NO; // Observe user interface theme change. kThemeServiceDidChangeThemeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kThemeServiceDidChangeThemeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { @@ -150,20 +154,6 @@ // Screen tracking [[Analytics sharedInstance] trackScreen:_screenName]; - if (BuildSettings.allowLocalContactsAccess) - { - // Check whether the access to the local contacts has not been already asked - // and check that the user has decided to use or not to use an identity server - if ([CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusNotDetermined - || !contactsDataSource.mxSession.hasAccountDataIdentityServerValue) - { - // Allow by default the local contacts sync in order to discover matrix users. - // This setting change will trigger the loading of the local contacts, which will automatically - // ask user permission to access their local contacts. - [MXKAppSettings standardAppSettings].syncLocalContacts = YES; - } - } - // Observe kAppDelegateDidTapStatusBarNotification. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { @@ -179,8 +169,14 @@ [super viewDidAppear:animated]; // Load the local contacts for display. - // In viewDidAppear as it may trigger a request for contacts access. [self refreshLocalContacts]; + [self updateFooterView]; +} + +- (void)viewDidLayoutSubviews +{ + [super viewDidLayoutSubviews]; + [self updateRequestContactsAccessFooterViewHeight]; } - (void)viewWillDisappear:(BOOL)animated @@ -203,6 +199,71 @@ #pragma mark - +- (RequestContactsAccessFooterView*)makeFooterView +{ + RequestContactsAccessFooterView *footerView = [RequestContactsAccessFooterView instantiate]; + footerView.delegate = self; + + self.requestContactsAccessFooterView = footerView; + + return footerView; +} + +- (void)updateFooterView +{ + if (!RiotSettings.shared.allowInviteExernalUsers || self->contactsDataSource.hasLocalContacts) + { + self.contactsTableView.tableFooterView = nil; + return; + } + + if ([CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized + && MXKAppSettings.standardAppSettings.syncLocalContacts + && contactsDataSource.mxSession.hasAccountDataIdentityServerValue) + { + self.contactsTableView.tableFooterView = nil; + return; + } + + if (self.shouldHideFooterView) + { + self.contactsTableView.tableFooterView = nil; + return; + } + + self.contactsTableView.tableFooterView = self.requestContactsAccessFooterView ?: [self makeFooterView]; + [self updateRequestContactsAccessFooterViewHeight]; +} + +- (void)updateRequestContactsAccessFooterViewHeight +{ + if (self.requestContactsAccessFooterView && self.requestContactsAccessFooterView == self.contactsTableView.tableFooterView) + { + CGSize footerSize = [self.requestContactsAccessFooterView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; + CGFloat gapHeight = self.contactsTableView.bounds.size.height - self.contactsTableView.adjustedContentInset.top - self.contactsTableView.adjustedContentInset.bottom; + + if (self.contactsTableView.tableHeaderView) + { + gapHeight -= self.contactsTableView.tableHeaderView.frame.size.height; + } + + if (gapHeight > footerSize.height) + { + self.requestContactsAccessFooterView.frame = CGRectMake(self.requestContactsAccessFooterView.frame.origin.x, + self.requestContactsAccessFooterView.frame.origin.y, + self.requestContactsAccessFooterView.frame.size.width, + gapHeight); + } + else + { + self.requestContactsAccessFooterView.frame = CGRectMake(self.requestContactsAccessFooterView.frame.origin.x, + self.requestContactsAccessFooterView.frame.origin.y, + self.requestContactsAccessFooterView.frame.size.width, + footerSize.height); + } + } +} + - (void)displayList:(ContactsDataSource*)listDataSource { // Cancel registration on existing dataSource if any @@ -228,39 +289,25 @@ return; } - // Do not scan local contacts in background if the user has not decided yet about using - // an identity server - BOOL doRefreshLocalContacts = NO; - for (MXSession *session in self.mxSessions) - { - if (session.hasAccountDataIdentityServerValue) - { - doRefreshLocalContacts = YES; - break; - } - } - + // Check whether the user has not decided yet about using an identity server // Check whether the application is allowed to access the local contacts. - if (doRefreshLocalContacts + if (contactsDataSource.mxSession.hasAccountDataIdentityServerValue && [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized) { - // Check the user permission for syncing local contacts. This permission was handled independently on previous application version. + // If the user hasn't enabled local contact sync in the app... if (![MXKAppSettings standardAppSettings].syncLocalContacts) { - // Check whether it was not requested yet. - if (![MXKAppSettings standardAppSettings].syncLocalContactsPermissionRequested) + // ... Check whether they have been directed to the Settings app to enable contact access. + if ([MXKAppSettings standardAppSettings].syncLocalContactsPermissionOpenedSystemSettings) { - [MXKAppSettings standardAppSettings].syncLocalContactsPermissionRequested = YES; - - [MXKContactManager requestUserConfirmationForLocalContactsSyncInViewController:self completionHandler:^(BOOL granted) { - - if (granted) - { - // Allow local contacts sync in order to discover matrix users. - [MXKAppSettings standardAppSettings].syncLocalContacts = YES; - } - - }]; + // If they have enable local contact sync and reset the system settings app flag. + [MXKAppSettings standardAppSettings].syncLocalContacts = YES; + [MXKAppSettings standardAppSettings].syncLocalContactsPermissionOpenedSystemSettings = NO; + } + else + { + // Otherwise local contact sync is disabled so we're done. + return; } } @@ -426,6 +473,18 @@ - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { [contactsDataSource searchWithPattern:searchText forceReset:NO]; + + // FIXME: This should be based off of the data source as it doesn't work in StartChat. + if (searchText.length && self.contactsTableView.tableFooterView) + { + self.shouldHideFooterView = YES; + [self updateFooterView]; + } + else if (!searchText.length && !self.contactsTableView.tableFooterView) + { + self.shouldHideFooterView = NO; + [self updateFooterView]; + } } - (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar @@ -460,4 +519,25 @@ [self withdrawViewControllerAnimated:YES completion:nil]; } +#pragma mark - RequestContactsAccessFooterViewDelegate + +- (void)didRequestContactsAccess +{ + + [MXKTools checkAccessForContacts:@"Contacts disabled" + withManualChangeMessage:@"To enable contacts, go to your device settings." + showPopUpInViewController:self + completionHandler:^(BOOL granted) { + if (granted) + { + // Hide the request access view. + [self updateFooterView]; + + // Enable sync local contacts and refresh the contacts manager. + MXKAppSettings.standardAppSettings.syncLocalContacts = YES; + [self refreshLocalContacts]; + } + }]; +} + @end diff --git a/Riot/Modules/Contacts/DataSources/ContactsDataSource.h b/Riot/Modules/Contacts/DataSources/ContactsDataSource.h index 4e8005f0c..da132b86e 100644 --- a/Riot/Modules/Contacts/DataSources/ContactsDataSource.h +++ b/Riot/Modules/Contacts/DataSources/ContactsDataSource.h @@ -50,6 +50,11 @@ typedef enum : NSUInteger NSMutableArray *filteredMatrixContacts; } +/** + Whether or not the data source has any local contacts loaded. + */ +- (BOOL)hasLocalContacts; + /** Get the contact at the given index path. diff --git a/Riot/Modules/Contacts/DataSources/ContactsDataSource.m b/Riot/Modules/Contacts/DataSources/ContactsDataSource.m index 233c72130..cdd131c59 100644 --- a/Riot/Modules/Contacts/DataSources/ContactsDataSource.m +++ b/Riot/Modules/Contacts/DataSources/ContactsDataSource.m @@ -660,6 +660,11 @@ #pragma mark - +- (BOOL)hasLocalContacts +{ + return filteredLocalContacts.count; +} + -(MXKContact *)contactAtIndexPath:(NSIndexPath*)indexPath { NSInteger row = indexPath.row; diff --git a/Riot/Modules/StartChat/RequestContactsAccessFooterView.swift b/Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.swift similarity index 100% rename from Riot/Modules/StartChat/RequestContactsAccessFooterView.swift rename to Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.swift diff --git a/Riot/Modules/StartChat/RequestContactsAccessFooterView.xib b/Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.xib similarity index 87% rename from Riot/Modules/StartChat/RequestContactsAccessFooterView.xib rename to Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.xib index 2126affdf..38f6a40b7 100644 --- a/Riot/Modules/StartChat/RequestContactsAccessFooterView.xib +++ b/Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.xib @@ -16,25 +16,25 @@ - + - - + + - - + diff --git a/Riot/Modules/StartChat/StartChatViewController.m b/Riot/Modules/StartChat/StartChatViewController.m index 4af062baf..be8e7a62a 100644 --- a/Riot/Modules/StartChat/StartChatViewController.m +++ b/Riot/Modules/StartChat/StartChatViewController.m @@ -20,7 +20,7 @@ #import "Riot-Swift.h" #import "MXSession+Riot.h" -@interface StartChatViewController () +@interface StartChatViewController () { // The contact used to describe the current user. MXKContact *userContact; @@ -49,8 +49,6 @@ @property (nonatomic, strong) InviteFriendsPresenter *inviteFriendsPresenter; @property (nonatomic, weak) InviteFriendsHeaderView *inviteFriendsHeaderView; -@property (nonatomic, strong) RequestContactsAccessFooterView *requestContactsAccessFooterView; - @end @implementation StartChatViewController @@ -160,25 +158,6 @@ } } -- (void)updateFooterView -{ - if (!RiotSettings.shared.allowInviteExernalUsers - || [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized - || self.contactsTableView.numberOfSections > 0) - { - // Hide line separators of empty cells - // FIXME: Store this? - self.contactsTableView.tableFooterView = [[UIView alloc] init]; - return; - } - - RequestContactsAccessFooterView *contactsAccessView = self.requestContactsAccessFooterView ?: [RequestContactsAccessFooterView instantiate]; - contactsAccessView.delegate = self; - self.contactsTableView.tableFooterView = contactsAccessView; - - self.requestContactsAccessFooterView = contactsAccessView; -} - - (void)userInterfaceThemeDidChange { [super userInterfaceThemeDidChange]; @@ -247,8 +226,6 @@ // Refresh display [self refreshContactsTable]; } - - [self updateFooterView]; } - (void)viewDidDisappear:(BOOL)animated @@ -263,36 +240,6 @@ { [super viewDidLayoutSubviews]; [self.contactsTableView vc_relayoutHeaderView]; - [self updateRequestContactsAccessFooterViewHeight]; -} - -- (void)updateRequestContactsAccessFooterViewHeight -{ - if (self.requestContactsAccessFooterView && self.requestContactsAccessFooterView == self.contactsTableView.tableFooterView) - { - CGSize footerSize = [self.requestContactsAccessFooterView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; - CGFloat gapHeight = self.contactsTableView.bounds.size.height - self.contactsTableView.adjustedContentInset.top - self.contactsTableView.adjustedContentInset.bottom; - - if (self.contactsTableView.tableHeaderView) - { - gapHeight -= self.contactsTableView.tableHeaderView.frame.size.height; - } - - if (gapHeight > footerSize.height) - { - self.requestContactsAccessFooterView.frame = CGRectMake(self.requestContactsAccessFooterView.frame.origin.x, - self.requestContactsAccessFooterView.frame.origin.y, - self.requestContactsAccessFooterView.frame.size.width, - gapHeight); - } - else - { - self.requestContactsAccessFooterView.frame = CGRectMake(self.requestContactsAccessFooterView.frame.origin.x, - self.requestContactsAccessFooterView.frame.origin.y, - self.requestContactsAccessFooterView.frame.size.width, - footerSize.height); - } - } } #pragma mark - @@ -765,11 +712,6 @@ return YES; } -- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar -{ - [self updateFooterView]; -} - - (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar { searchBar.text = nil; @@ -782,11 +724,6 @@ [searchBar resignFirstResponder]; } -- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar -{ - [self updateFooterView]; -} - #pragma mark - ContactsTableViewControllerDelegate - (void)contactsTableViewController:(ContactsTableViewController *)contactsTableViewController didSelectContact:(MXKContact*)contact @@ -841,21 +778,4 @@ [self showInviteFriendsFromSourceView:button]; } -#pragma mark - RequestContactsAccessFooterViewDelegate - -- (void)didRequestContactsAccess -{ - [MXKTools checkAccessForContacts:@"Contacts access has been disabled in the Settings app." showPopUpInViewController:self completionHandler:^(BOOL granted) { - if (granted) - { - // Hide the request access view. - [self updateFooterView]; - - // Enable sync local contacts and refresh the contacts manager. - MXKAppSettings.standardAppSettings.syncLocalContacts = YES; - [MXKContactManager.sharedManager refreshLocalContacts]; - } - }]; -} - @end From 92da98785e4d24c2f8fef922cb179bf1b18c5427 Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 9 Aug 2021 12:22:56 +0100 Subject: [PATCH 006/276] Fix image name. --- .../Contacts/Views/RequestContactsAccessFooterView.xib | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.xib b/Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.xib index 38f6a40b7..2e795926f 100644 --- a/Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.xib +++ b/Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.xib @@ -18,7 +18,7 @@ - + - + From 1d9044a8ff4557bfb526e5273daaa158f2f6104b Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 9 Aug 2021 12:58:18 +0100 Subject: [PATCH 007/276] Replace hasLocalContacts with showLocalContacts. Update the value of showLocalContacts in ContactsTableViewController. --- .../Contacts/ContactsTableViewController.m | 20 ++++++++++++------- .../Contacts/DataSources/ContactsDataSource.h | 5 +++-- .../Contacts/DataSources/ContactsDataSource.m | 9 ++------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/Riot/Modules/Contacts/ContactsTableViewController.m b/Riot/Modules/Contacts/ContactsTableViewController.m index 13d5c7402..c16a8b021 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -211,20 +211,22 @@ - (void)updateFooterView { - if (!RiotSettings.shared.allowInviteExernalUsers || self->contactsDataSource.hasLocalContacts) + if (!BuildSettings.allowLocalContactsAccess) { - self.contactsTableView.tableFooterView = nil; + self.contactsTableView.tableFooterView = [[UIView alloc] init]; return; } + // With contacts access granted, contact sync enabled and an identity server, the footer can be hidden. if ([CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized && MXKAppSettings.standardAppSettings.syncLocalContacts && contactsDataSource.mxSession.hasAccountDataIdentityServerValue) { - self.contactsTableView.tableFooterView = nil; + self.contactsTableView.tableFooterView = [[UIView alloc] init]; return; } + // If the footer should be shown, don't show it when searching the identity server. if (self.shouldHideFooterView) { self.contactsTableView.tableFooterView = nil; @@ -274,6 +276,7 @@ contactsDataSource = listDataSource; contactsDataSource.delegate = self; + contactsDataSource.showLocalContacts = MXKAppSettings.standardAppSettings.syncLocalContacts; if (self.contactsTableView) { @@ -530,12 +533,15 @@ completionHandler:^(BOOL granted) { if (granted) { + // Enable local contacts sync and display. + MXKAppSettings.standardAppSettings.syncLocalContacts = YES; + self->contactsDataSource.showLocalContacts = YES; + + // Refresh the contacts manager. + [self refreshLocalContacts]; + // Hide the request access view. [self updateFooterView]; - - // Enable sync local contacts and refresh the contacts manager. - MXKAppSettings.standardAppSettings.syncLocalContacts = YES; - [self refreshLocalContacts]; } }]; } diff --git a/Riot/Modules/Contacts/DataSources/ContactsDataSource.h b/Riot/Modules/Contacts/DataSources/ContactsDataSource.h index da132b86e..f81a83f25 100644 --- a/Riot/Modules/Contacts/DataSources/ContactsDataSource.h +++ b/Riot/Modules/Contacts/DataSources/ContactsDataSource.h @@ -51,9 +51,10 @@ typedef enum : NSUInteger } /** - Whether or not the data source has any local contacts loaded. + Whether the data source should include local contacts in the table view. + Note: Updating this property does not automatically refresh the data source. */ -- (BOOL)hasLocalContacts; +@property (nonatomic) BOOL showLocalContacts; /** Get the contact at the given index path. diff --git a/Riot/Modules/Contacts/DataSources/ContactsDataSource.m b/Riot/Modules/Contacts/DataSources/ContactsDataSource.m index cdd131c59..d4a00b4ce 100644 --- a/Riot/Modules/Contacts/DataSources/ContactsDataSource.m +++ b/Riot/Modules/Contacts/DataSources/ContactsDataSource.m @@ -467,7 +467,7 @@ } // Keep visible the header for the both contact sections, even if they're are empty. - if (BuildSettings.allowLocalContactsAccess && [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized) + if (BuildSettings.allowLocalContactsAccess && self.showLocalContacts && [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized) { filteredLocalContactsSection = count++; } @@ -482,7 +482,7 @@ } // Keep visible the local contact header, even if the section is empty. - if (BuildSettings.allowLocalContactsAccess && [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized) + if (BuildSettings.allowLocalContactsAccess && self.showLocalContacts && [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized) { filteredLocalContactsSection = count++; } @@ -660,11 +660,6 @@ #pragma mark - -- (BOOL)hasLocalContacts -{ - return filteredLocalContacts.count; -} - -(MXKContact *)contactAtIndexPath:(NSIndexPath*)indexPath { NSInteger row = indexPath.row; From 3d958d95c2f3d02499114e4ef1aa5f07eff3bda6 Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 9 Aug 2021 15:08:05 +0100 Subject: [PATCH 008/276] Hide local contacts in Unified Search. --- Riot/Modules/Contacts/ContactsTableViewController.m | 1 - Riot/Modules/Contacts/DataSources/ContactsDataSource.h | 6 ++++-- Riot/Modules/Contacts/DataSources/ContactsDataSource.m | 1 + Riot/Modules/GlobalSearch/UnifiedSearchViewController.m | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Riot/Modules/Contacts/ContactsTableViewController.m b/Riot/Modules/Contacts/ContactsTableViewController.m index c16a8b021..ee60995da 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -276,7 +276,6 @@ contactsDataSource = listDataSource; contactsDataSource.delegate = self; - contactsDataSource.showLocalContacts = MXKAppSettings.standardAppSettings.syncLocalContacts; if (self.contactsTableView) { diff --git a/Riot/Modules/Contacts/DataSources/ContactsDataSource.h b/Riot/Modules/Contacts/DataSources/ContactsDataSource.h index f81a83f25..ea6db9c2e 100644 --- a/Riot/Modules/Contacts/DataSources/ContactsDataSource.h +++ b/Riot/Modules/Contacts/DataSources/ContactsDataSource.h @@ -51,8 +51,10 @@ typedef enum : NSUInteger } /** - Whether the data source should include local contacts in the table view. - Note: Updating this property does not automatically refresh the data source. + Whether the data source should include local contacts in the table view. The default + value is set at initialisation to match the `MXKAppSettings` value for `syncLocalContacts`. + Note: After updating this property, the table view's data will need to be reloaded for it to have + any effect. */ @property (nonatomic) BOOL showLocalContacts; diff --git a/Riot/Modules/Contacts/DataSources/ContactsDataSource.m b/Riot/Modules/Contacts/DataSources/ContactsDataSource.m index d4a00b4ce..7909dea8c 100644 --- a/Riot/Modules/Contacts/DataSources/ContactsDataSource.m +++ b/Riot/Modules/Contacts/DataSources/ContactsDataSource.m @@ -80,6 +80,7 @@ _areSectionsShrinkable = NO; shrinkedSectionsBitMask = 0; + _showLocalContacts = MXKAppSettings.standardAppSettings.syncLocalContacts; hideNonMatrixEnabledContacts = NO; _displaySearchInputInContactsList = NO; diff --git a/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m b/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m index e79e9a055..bec3f836f 100644 --- a/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m +++ b/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m @@ -245,6 +245,7 @@ // Init the search for people peopleSearchDataSource = [[ContactsDataSource alloc] initWithMatrixSession:mainSession]; + peopleSearchDataSource.showLocalContacts = NO; peopleSearchDataSource.areSectionsShrinkable = YES; peopleSearchDataSource.displaySearchInputInContactsList = YES; peopleSearchDataSource.contactCellAccessoryImage = [[UIImage imageNamed: @"disclosure_icon"] vc_tintedImageUsingColor:ThemeService.shared.theme.textSecondaryColor];; From 82cb8bc4886da9909f6c9e76c95fed00ec57b106 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 10 Aug 2021 12:03:28 +0100 Subject: [PATCH 009/276] Take into account the identity server when setting the default value of showLocalContacts. --- .../Modules/Contacts/DataSources/ContactsDataSource.m | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Riot/Modules/Contacts/DataSources/ContactsDataSource.m b/Riot/Modules/Contacts/DataSources/ContactsDataSource.m index 7909dea8c..2395472c4 100644 --- a/Riot/Modules/Contacts/DataSources/ContactsDataSource.m +++ b/Riot/Modules/Contacts/DataSources/ContactsDataSource.m @@ -80,7 +80,6 @@ _areSectionsShrinkable = NO; shrinkedSectionsBitMask = 0; - _showLocalContacts = MXKAppSettings.standardAppSettings.syncLocalContacts; hideNonMatrixEnabledContacts = NO; _displaySearchInputInContactsList = NO; @@ -93,6 +92,16 @@ return self; } +- (instancetype)initWithMatrixSession:(MXSession *)mxSession +{ + self = [super initWithMatrixSession:mxSession]; + if (self) { + // Only show local contacts when contact sync is enabled and the identity server terms of service have been accepted. + _showLocalContacts = MXKAppSettings.standardAppSettings.syncLocalContacts && self.mxSession.hasAccountDataIdentityServerValue; + } + return self; +} + - (void)destroy { [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXKContactManagerDidUpdateMatrixContactsNotification object:nil]; From e293279a53e20ee5d4e91e4e16ed0b0b4f4cb0b3 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 10 Aug 2021 12:32:07 +0100 Subject: [PATCH 010/276] Disable the contact access footer view in Unified Search. --- Riot/Modules/Contacts/ContactsTableViewController.h | 7 +++++++ Riot/Modules/Contacts/ContactsTableViewController.m | 5 ++++- Riot/Modules/GlobalSearch/UnifiedSearchViewController.m | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Riot/Modules/Contacts/ContactsTableViewController.h b/Riot/Modules/Contacts/ContactsTableViewController.h index a196bd788..eeb3185a8 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.h +++ b/Riot/Modules/Contacts/ContactsTableViewController.h @@ -66,6 +66,13 @@ */ @property (weak, nonatomic) IBOutlet UITableView *contactsTableView; +/** + When true, the footer that allows the user to grant access to their contacts will + never be shown. When false, the footer will shown when the required access is + not available. + */ +@property (nonatomic) BOOL hideRequestContactAccessFooter; + /** If YES, the table view will scroll at the top on the next data source refresh. It comes back to NO after each refresh. diff --git a/Riot/Modules/Contacts/ContactsTableViewController.m b/Riot/Modules/Contacts/ContactsTableViewController.m index ee60995da..03e0e7107 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -68,6 +68,9 @@ { [super finalizeInit]; + // Allow the contact access footer to be shown when necessary. + self.hideRequestContactAccessFooter = NO; + // Setup `MXKViewControllerHandling` properties self.enableBarTintColorStatusChange = NO; self.rageShakeManager = [RageShakeManager sharedManager]; @@ -211,7 +214,7 @@ - (void)updateFooterView { - if (!BuildSettings.allowLocalContactsAccess) + if (!BuildSettings.allowLocalContactsAccess || self.hideRequestContactAccessFooter) { self.contactsTableView.tableFooterView = [[UIView alloc] init]; return; diff --git a/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m b/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m index bec3f836f..12a63f041 100644 --- a/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m +++ b/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m @@ -91,6 +91,7 @@ [titles addObject: NSLocalizedStringFromTable(@"search_people", @"Vector", nil)]; peopleSearchViewController = [ContactsTableViewController contactsTableViewController]; peopleSearchViewController.contactsTableViewControllerDelegate = self; + peopleSearchViewController.hideRequestContactAccessFooter = YES; [viewControllers addObject:peopleSearchViewController]; // add Files tab From cb665e4b215617e4ec02a44cdc128bacfde926be Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 10 Aug 2021 13:05:11 +0100 Subject: [PATCH 011/276] Match StartChatViewController colours to latest Figma. --- .../Views/RequestContactsAccessFooterView.xib | 4 ---- Riot/Modules/StartChat/InviteFriendsHeaderView.swift | 4 +--- Riot/Modules/StartChat/StartChatViewController.m | 12 ++++++------ 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.xib b/Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.xib index 2e795926f..d0128d78c 100644 --- a/Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.xib +++ b/Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.xib @@ -72,7 +72,6 @@ - @@ -96,8 +95,5 @@ - - - diff --git a/Riot/Modules/StartChat/InviteFriendsHeaderView.swift b/Riot/Modules/StartChat/InviteFriendsHeaderView.swift index e59539910..30325e704 100644 --- a/Riot/Modules/StartChat/InviteFriendsHeaderView.swift +++ b/Riot/Modules/StartChat/InviteFriendsHeaderView.swift @@ -59,12 +59,10 @@ final class InviteFriendsHeaderView: UIView, NibLoadable, Themable { // MARK: - Public func update(theme: Theme) { - backgroundColor = theme.backgroundColor - 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.backgroundColor, for: .normal) + button.vc_setBackgroundColor(theme.baseColor, for: .normal) let buttonImage = Asset.Images.shareActionButton.image.vc_tintedImage(usingColor: theme.tintColor) diff --git a/Riot/Modules/StartChat/StartChatViewController.m b/Riot/Modules/StartChat/StartChatViewController.m index be8e7a62a..d48db5666 100644 --- a/Riot/Modules/StartChat/StartChatViewController.m +++ b/Riot/Modules/StartChat/StartChatViewController.m @@ -115,7 +115,7 @@ createBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedStringFromTable(@"start", @"Vector", nil) style:UIBarButtonItemStylePlain target:self action:@selector(onButtonPressed:)]; self.navigationItem.rightBarButtonItem = createBarButtonItem; - _searchBarView.placeholder = NSLocalizedStringFromTable(@"room_creation_invite_another_user", @"Vector", nil); + _searchBarView.placeholder = NSLocalizedStringFromTable(@"search_default_placeholder", @"Vector", nil); _searchBarView.returnKeyType = UIReturnKeyDone; _searchBarView.autocapitalizationType = UITextAutocapitalizationTypeNone; [self refreshSearchBarItemsColor:_searchBarView]; @@ -165,11 +165,12 @@ [self refreshSearchBarItemsColor:_searchBarView]; // Check the table view style to select its bg color. - self.contactsTableView.backgroundColor = ((self.contactsTableView.style == UITableViewStylePlain) ? ThemeService.shared.theme.backgroundColor : ThemeService.shared.theme.headerBackgroundColor); - self.navigationController.navigationBar.barTintColor = self.contactsTableView.backgroundColor; + self.contactsTableView.backgroundColor = ((self.contactsTableView.style == UITableViewStylePlain) ? ThemeService.shared.theme.baseColor : ThemeService.shared.theme.headerBackgroundColor); self.view.backgroundColor = self.contactsTableView.backgroundColor; self.contactsTableView.separatorColor = ThemeService.shared.theme.lineBreakColor; + _searchBarHeaderBorder.backgroundColor = self.contactsTableView.backgroundColor; + if (self.contactsTableView.dataSource) { [self.contactsTableView reloadData]; @@ -676,7 +677,6 @@ { // bar tint color searchBar.barTintColor = searchBar.tintColor = ThemeService.shared.theme.tintColor; - searchBar.tintColor = ThemeService.shared.theme.tintColor; // FIXME: this all seems incredibly fragile and tied to gutwrenching the current UISearchBar internals. @@ -693,8 +693,8 @@ // The effect views are needed due to minimal style. // With default style there is a border above the search bar. searchBarTextField.backgroundColor = ThemeService.shared.theme.textQuinaryColor; - UIView *effectBackgroundTop = [searchBarTextField valueForKey:@"_effectBackgroundTop"]; - UIView *effectBackgroundBottom = [searchBarTextField valueForKey:@"_effectBackgroundBottom"]; + UIView *effectBackgroundTop = [searchBarTextField valueForKey:@"_effectBackgroundTop"]; + UIView *effectBackgroundBottom = [searchBarTextField valueForKey:@"_effectBackgroundBottom"]; effectBackgroundTop.hidden = YES; effectBackgroundBottom.hidden = YES; } From ad2a5f8fca2990d441d7e3a5392d01596f291919 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 11 Aug 2021 17:13:15 +0100 Subject: [PATCH 012/276] Use areAllTermsAgreed instead of hasAccountDataIdentityServerValue. Move identity server terms modal from LegacyAppDelegate into ContactsTableViewController. --- Riot/Modules/Application/LegacyAppDelegate.m | 82 +---------- .../Contacts/ContactsTableViewController.m | 138 ++++++++++++++++-- .../Contacts/DataSources/ContactsDataSource.m | 2 +- 3 files changed, 128 insertions(+), 94 deletions(-) diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index a6df6cb48..166e6e764 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -87,7 +87,7 @@ NSString *const AppDelegateDidValidateEmailNotificationClientSecretKey = @"AppDe NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUniversalLinkDidChangeNotification"; -@interface LegacyAppDelegate () +@interface LegacyAppDelegate () { /** Reachability observer @@ -201,7 +201,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni @property (weak, nonatomic) UIAlertController *incomingKeyVerificationRequestAlertController; -@property (nonatomic, strong) ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter; @property (nonatomic, strong) SlidingModalPresenter *slidingModalPresenter; @property (nonatomic, strong) SetPinCoordinatorBridgePresenter *setPinCoordinatorBridgePresenter; @@ -659,9 +658,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Register to GDPR consent not given notification [self registerUserConsentNotGivenNotification]; - // Register to identity server terms not signed notification - [self registerIdentityServiceTermsNotSignedNotification]; - // Start monitoring reachability [[AFNetworkReachabilityManager sharedManager] setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) { @@ -4043,82 +4039,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } } -#pragma mark - Identity server service terms - -// Observe identity server terms not signed notification -- (void)registerIdentityServiceTermsNotSignedNotification -{ - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleIdentityServiceTermsNotSignedNotification:) name:MXIdentityServiceTermsNotSignedNotification object:nil]; -} - -- (void)handleIdentityServiceTermsNotSignedNotification:(NSNotification*)notification -{ - MXLogDebug(@"[AppDelegate] IS Terms: handleIdentityServiceTermsNotSignedNotification."); - - NSString *baseURL; - NSString *accessToken; - - MXJSONModelSetString(baseURL, notification.userInfo[MXIdentityServiceNotificationIdentityServerKey]); - MXJSONModelSetString(accessToken, notification.userInfo[MXIdentityServiceNotificationAccessTokenKey]); - - [self presentIdentityServerTermsWithBaseURL:baseURL andAccessToken:accessToken]; -} - -- (void)presentIdentityServerTermsWithBaseURL:(NSString*)baseURL andAccessToken:(NSString*)accessToken -{ - MXSession *mxSession = self.mxSessions.firstObject; - - if (!mxSession || !baseURL || !accessToken || self.serviceTermsModalCoordinatorBridgePresenter.isPresenting) - { - return; - } - - ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter = [[ServiceTermsModalCoordinatorBridgePresenter alloc] initWithSession:mxSession - baseUrl:baseURL - serviceType:MXServiceTypeIdentityService - outOfContext:YES - accessToken:accessToken]; - - serviceTermsModalCoordinatorBridgePresenter.delegate = self; - - [serviceTermsModalCoordinatorBridgePresenter presentFrom:self.presentedViewController animated:YES]; - self.serviceTermsModalCoordinatorBridgePresenter = serviceTermsModalCoordinatorBridgePresenter; -} - -- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidAccept:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter -{ - [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ - - }]; - self.serviceTermsModalCoordinatorBridgePresenter = nil; -} - -- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline:(ServiceTermsModalCoordinatorBridgePresenter *)coordinatorBridgePresenter session:(MXSession *)session -{ - MXLogDebug(@"[AppDelegate] IS Terms: User has declined the use of the default IS."); - - // The user does not want to use the proposed IS. - // Disable IS feature on user's account - [session setIdentityServer:nil andAccessToken:nil]; - [session setAccountDataIdentityServer:nil success:^{ - } failure:^(NSError *error) { - MXLogDebug(@"[AppDelegate] IS Terms: Error: %@", error); - }]; - - [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ - - }]; - self.serviceTermsModalCoordinatorBridgePresenter = nil; -} - -- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidCancel:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter -{ - [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ - - }]; - self.serviceTermsModalCoordinatorBridgePresenter = nil; -} - #pragma mark - Settings - (void)setupUserDefaults diff --git a/Riot/Modules/Contacts/ContactsTableViewController.m b/Riot/Modules/Contacts/ContactsTableViewController.m index 03e0e7107..fd8f879e3 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -28,7 +28,7 @@ #define CONTACTS_TABLEVC_DEFAULT_SECTION_HEADER_HEIGHT 30.0 #define CONTACTS_TABLEVC_LOCALCONTACTS_SECTION_HEADER_HEIGHT 65.0 -@interface ContactsTableViewController () +@interface ContactsTableViewController () { /** Observe kAppDelegateDidTapStatusBarNotification to handle tap on clock status bar. @@ -44,6 +44,8 @@ @property (nonatomic, strong) RequestContactsAccessFooterView *requestContactsAccessFooterView; @property (nonatomic) BOOL shouldHideFooterView; +@property (nonatomic, strong) ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter; + @end @implementation ContactsTableViewController @@ -223,7 +225,7 @@ // With contacts access granted, contact sync enabled and an identity server, the footer can be hidden. if ([CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized && MXKAppSettings.standardAppSettings.syncLocalContacts - && contactsDataSource.mxSession.hasAccountDataIdentityServerValue) + && contactsDataSource.mxSession.identityService.areAllTermsAgreed) { self.contactsTableView.tableFooterView = [[UIView alloc] init]; return; @@ -296,7 +298,7 @@ // Check whether the user has not decided yet about using an identity server // Check whether the application is allowed to access the local contacts. - if (contactsDataSource.mxSession.hasAccountDataIdentityServerValue + if (contactsDataSource.mxSession.identityService.areAllTermsAgreed && [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized) { // If the user hasn't enabled local contact sync in the app... @@ -528,24 +530,136 @@ - (void)didRequestContactsAccess { + MXWeakify(self); + // Check for contacts access, showing a pop-up if necessary. [MXKTools checkAccessForContacts:@"Contacts disabled" withManualChangeMessage:@"To enable contacts, go to your device settings." showPopUpInViewController:self completionHandler:^(BOOL granted) { + + MXStrongifyAndReturnIfNil(self); + if (granted) { - // Enable local contacts sync and display. - MXKAppSettings.standardAppSettings.syncLocalContacts = YES; - self->contactsDataSource.showLocalContacts = YES; - - // Refresh the contacts manager. - [self refreshLocalContacts]; - - // Hide the request access view. - [self updateFooterView]; + // If granted and the identity service terms have already been accepted, show the local contacts. + if (self->contactsDataSource.mxSession.identityService.areAllTermsAgreed) + { + [self showLocalContacts]; + } + else + { + // Otherwise, get a valid identity service. + MXSession *session = self->contactsDataSource.mxSession; + MXIdentityService *identityService = session.identityService; + + if (!identityService) + { + NSString *baseURL = session.accountDataIdentityServer ?: RiotSettings.shared.identityServerUrlString; + identityService = [[MXIdentityService alloc] initWithIdentityServer:baseURL + accessToken:nil + andHomeserverRestClient:session.matrixRestClient]; + } + + // Get the identity service's access token. + [identityService accessTokenWithSuccess:^(NSString * _Nullable accessToken) { + MXWeakify(session); + + // Set the identity server in the session and account data as this will be nil if + // the terms were previously declined. These will be reverted if declined once more. + [session setIdentityServer:identityService.identityServer andAccessToken:accessToken]; + [session setAccountDataIdentityServer:identityService.identityServer success:^{ + + MXStrongifyAndReturnIfNil(session); + + // Present the terms of the identity server. + [self presentIdentityServerTermsWithSession:session + baseURL:identityService.identityServer + andAccessToken:accessToken]; + + } failure:^(NSError *error) { + // Something went wrong setting the account data identity service + MXLogError(@"[ContactsTableViewController] Error preparing to display identity server terms: %@", error); + }]; + } failure:^(NSError * _Nonnull error) { + // Something went wrong getting the identity service's access token. + MXLogError(@"[ContactsTableViewController] Error preparing to display identity server terms: %@", error); + }]; + } } }]; } +- (void)showLocalContacts +{ + // Enable local contacts sync and display. + MXKAppSettings.standardAppSettings.syncLocalContacts = YES; + self->contactsDataSource.showLocalContacts = YES; + + // Attempt to refresh the contacts manager - triggers identity server if necessary. + [self refreshLocalContacts]; + + // Hide the request access view. + [self updateFooterView]; +} + +#pragma mark - Identity server service terms + +- (void)presentIdentityServerTermsWithSession:(MXSession*)mxSession baseURL:(NSString*)baseURL andAccessToken:(NSString*)accessToken +{ + if (!mxSession || !baseURL || !accessToken || self.serviceTermsModalCoordinatorBridgePresenter.isPresenting) + { + return; + } + + ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter = [[ServiceTermsModalCoordinatorBridgePresenter alloc] initWithSession:mxSession + baseUrl:baseURL + serviceType:MXServiceTypeIdentityService + outOfContext:YES + accessToken:accessToken]; + + serviceTermsModalCoordinatorBridgePresenter.delegate = self; + + [serviceTermsModalCoordinatorBridgePresenter presentFrom:self animated:YES]; + self.serviceTermsModalCoordinatorBridgePresenter = serviceTermsModalCoordinatorBridgePresenter; +} + +#pragma mark ServiceTermsModalCoordinatorBridgePresenterDelegate + +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidAccept:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter +{ + [self showLocalContacts]; + + [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ + + }]; + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline:(ServiceTermsModalCoordinatorBridgePresenter *)coordinatorBridgePresenter session:(MXSession *)session +{ + MXLogDebug(@"[ContactsTableViewController] IS Terms: User has declined the use of the default IS."); + + // The user does not want to use the proposed IS. + // Disable IS feature on user's account + [session setIdentityServer:nil andAccessToken:nil]; + [session setAccountDataIdentityServer:nil success:^{ + } failure:^(NSError *error) { + MXLogDebug(@"[ContactsTableViewController] IS Terms: Error: %@", error); + }]; + + [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ + + }]; + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidCancel:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter +{ + [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ + + }]; + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + @end diff --git a/Riot/Modules/Contacts/DataSources/ContactsDataSource.m b/Riot/Modules/Contacts/DataSources/ContactsDataSource.m index 2395472c4..eb0d9d204 100644 --- a/Riot/Modules/Contacts/DataSources/ContactsDataSource.m +++ b/Riot/Modules/Contacts/DataSources/ContactsDataSource.m @@ -97,7 +97,7 @@ self = [super initWithMatrixSession:mxSession]; if (self) { // Only show local contacts when contact sync is enabled and the identity server terms of service have been accepted. - _showLocalContacts = MXKAppSettings.standardAppSettings.syncLocalContacts && self.mxSession.hasAccountDataIdentityServerValue; + _showLocalContacts = MXKAppSettings.standardAppSettings.syncLocalContacts && self.mxSession.identityService.areAllTermsAgreed; } return self; } From 8380f07dff5242dcb61cef5d9b19533151c5c982 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 12 Aug 2021 12:20:08 +0100 Subject: [PATCH 013/276] Fix bug where RequestContactsAccessFooterView could still be visible beneath the contacts. --- .../Modal/Modal/ServiceTermsModalScreenViewModel.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewModel.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewModel.swift index a49d6954f..a05b08cf2 100644 --- a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewModel.swift +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewModel.swift @@ -99,6 +99,12 @@ final class ServiceTermsModalScreenViewModel: ServiceTermsModalScreenViewModelTy return } self.update(viewState: .accepted) + + // Send a notification to update the identity service immediately. + let userInfo = [MXIdentityServiceNotificationIdentityServerKey: self.serviceTerms.baseUrl] + NotificationCenter.default.post(name: .MXIdentityServiceTermsAccepted, object: nil, userInfo: userInfo) + + // Notify the delegate. self.coordinatorDelegate?.serviceTermsModalScreenViewModelDidAccept(self) }, failure: { [weak self] (error) in From 4bdecb1e4817ccbbf88b3ee2b3559645d5ce29c2 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 12 Aug 2021 12:29:20 +0100 Subject: [PATCH 014/276] Localise contacts disabled alert strings. --- Riot/Assets/en.lproj/Vector.strings | 2 ++ Riot/Generated/Strings.swift | 10 +++++++++- Riot/Modules/Contacts/ContactsTableViewController.m | 4 ++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index ff3e6a899..90a4fc67b 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -241,6 +241,8 @@ Tap the + to start adding people."; "contacts_address_book_no_contact" = "No local contacts"; "contacts_address_book_permission_required" = "Permission required to access local contacts"; "contacts_address_book_permission_denied" = "You didn't allow Element to access your local contacts"; +"contacts_address_book_permission_denied_alert_title" = "Contacts disabled"; +"contacts_address_book_permission_denied_alert_message" = "To enable contacts, go to your device settings."; "contacts_user_directory_section" = "USER DIRECTORY"; "contacts_user_directory_offline_section" = "USER DIRECTORY (offline)"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index ccbd7f10f..10fd6c90b 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -558,6 +558,14 @@ internal enum VectorL10n { internal static var contactsAddressBookPermissionDenied: String { return VectorL10n.tr("Vector", "contacts_address_book_permission_denied") } + /// To enable contacts, go to your device settings. + internal static var contactsAddressBookPermissionDeniedAlertMessage: String { + return VectorL10n.tr("Vector", "contacts_address_book_permission_denied_alert_message") + } + /// Contacts disabled + internal static var contactsAddressBookPermissionDeniedAlertTitle: String { + return VectorL10n.tr("Vector", "contacts_address_book_permission_denied_alert_title") + } /// Permission required to access local contacts internal static var contactsAddressBookPermissionRequired: String { return VectorL10n.tr("Vector", "contacts_address_book_permission_required") @@ -4014,7 +4022,7 @@ internal enum VectorL10n { internal static var settingsAdd3pidPasswordMessage: String { return VectorL10n.tr("Vector", "settings_add_3pid_password_message") } - /// Add email adress + /// Add email address internal static var settingsAdd3pidPasswordTitleEmail: String { return VectorL10n.tr("Vector", "settings_add_3pid_password_title_email") } diff --git a/Riot/Modules/Contacts/ContactsTableViewController.m b/Riot/Modules/Contacts/ContactsTableViewController.m index fd8f879e3..09efc9032 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -533,8 +533,8 @@ MXWeakify(self); // Check for contacts access, showing a pop-up if necessary. - [MXKTools checkAccessForContacts:@"Contacts disabled" - withManualChangeMessage:@"To enable contacts, go to your device settings." + [MXKTools checkAccessForContacts:NSLocalizedStringFromTable(@"contacts_address_book_permission_denied_alert_title", @"Vector", nil) + withManualChangeMessage:NSLocalizedStringFromTable(@"contacts_address_book_permission_denied_alert_message", @"Vector", nil) showPopUpInViewController:self completionHandler:^(BOOL granted) { From 9c1710c14fb1cfc905ad4843cd55b8e57186dfb4 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 13 Aug 2021 14:06:28 +0100 Subject: [PATCH 015/276] Match NSContactsUsageDescription to Figma design. --- Riot/Assets/en.lproj/InfoPlist.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/en.lproj/InfoPlist.strings b/Riot/Assets/en.lproj/InfoPlist.strings index 468d42cce..e4d9358f0 100644 --- a/Riot/Assets/en.lproj/InfoPlist.strings +++ b/Riot/Assets/en.lproj/InfoPlist.strings @@ -18,6 +18,6 @@ "NSCameraUsageDescription" = "The camera is used to take photos and videos, make video calls."; "NSPhotoLibraryUsageDescription" = "The photo library is used to send photos and videos."; "NSMicrophoneUsageDescription" = "Element needs to access your microphone to make and receive calls, take videos, and record voice messages."; -"NSContactsUsageDescription" = "To discover contacts already using Matrix, Element can send email addresses and phone numbers in your address book to your chosen Matrix identity server. Where supported, personal data is hashed before sending - please check your identity server's privacy policy for more details."; +"NSContactsUsageDescription" = "Element will show your contacts here so you can invite them to talk."; "NSCalendarsUsageDescription" = "See your scheduled meetings in the app."; "NSFaceIDUsageDescription" = "Face ID is used to access your app."; From f55605cf1c6a0273f947e22abb1b7c1f6b24c0fe Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 13 Aug 2021 14:15:42 +0100 Subject: [PATCH 016/276] Load contacts and/or the footer view before the view appears. --- Riot/Modules/Contacts/ContactsTableViewController.m | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Riot/Modules/Contacts/ContactsTableViewController.m b/Riot/Modules/Contacts/ContactsTableViewController.m index 09efc9032..e0448e6f6 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -166,15 +166,11 @@ }]; - [self refreshContactsTable]; -} - -- (void)viewDidAppear:(BOOL)animated -{ - [super viewDidAppear:animated]; - // Load the local contacts for display. [self refreshLocalContacts]; + [self refreshContactsTable]; + + // Show the contacts access footer if necessary. [self updateFooterView]; } From 85f38a0163ee7668414fb910c1181d8dc48c5524 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 13 Aug 2021 14:38:24 +0100 Subject: [PATCH 017/276] Fix the footer when searching from StartChatViewController. --- .../Contacts/ContactsTableViewController.h | 7 +++++ .../Contacts/ContactsTableViewController.m | 29 ++++++++++--------- .../StartChat/StartChatViewController.m | 9 ++++++ 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/Riot/Modules/Contacts/ContactsTableViewController.h b/Riot/Modules/Contacts/ContactsTableViewController.h index eeb3185a8..19e7f4abc 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.h +++ b/Riot/Modules/Contacts/ContactsTableViewController.h @@ -73,6 +73,13 @@ */ @property (nonatomic) BOOL hideRequestContactAccessFooter; +/** + Indicates when there's an active search. This is used to indicate that the contacts + access footer should be hidden as even without local contacts, there will still be + results to be shown from the server. + */ +@property (nonatomic) BOOL contactsAreFilteredWithSearch; + /** If YES, the table view will scroll at the top on the next data source refresh. It comes back to NO after each refresh. diff --git a/Riot/Modules/Contacts/ContactsTableViewController.m b/Riot/Modules/Contacts/ContactsTableViewController.m index e0448e6f6..254f71f91 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -42,7 +42,6 @@ } @property (nonatomic, strong) RequestContactsAccessFooterView *requestContactsAccessFooterView; -@property (nonatomic) BOOL shouldHideFooterView; @property (nonatomic, strong) ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter; @@ -70,7 +69,8 @@ { [super finalizeInit]; - // Allow the contact access footer to be shown when necessary. + // By default, allow the contact access footer to be shown + // when sufficient permissions are not available. self.hideRequestContactAccessFooter = NO; // Setup `MXKViewControllerHandling` properties @@ -100,7 +100,7 @@ // Hide line separators of empty cells self.contactsTableView.tableFooterView = [[UIView alloc] init]; - self.shouldHideFooterView = NO; + self.contactsAreFilteredWithSearch = NO; // Observe user interface theme change. kThemeServiceDidChangeThemeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kThemeServiceDidChangeThemeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { @@ -200,6 +200,12 @@ #pragma mark - +- (void)setContactsAreFilteredWithSearch:(BOOL)contactsAreFilteredWithSearch +{ + _contactsAreFilteredWithSearch = contactsAreFilteredWithSearch; + [self updateFooterView]; +} + - (RequestContactsAccessFooterView*)makeFooterView { RequestContactsAccessFooterView *footerView = [RequestContactsAccessFooterView instantiate]; @@ -227,10 +233,10 @@ return; } - // If the footer should be shown, don't show it when searching the identity server. - if (self.shouldHideFooterView) + // If the footer is to be shown, hide it when there's an active search. + if (self.contactsAreFilteredWithSearch) { - self.contactsTableView.tableFooterView = nil; + self.contactsTableView.tableFooterView = [[UIView alloc] init]; return; } @@ -477,16 +483,13 @@ { [contactsDataSource searchWithPattern:searchText forceReset:NO]; - // FIXME: This should be based off of the data source as it doesn't work in StartChat. - if (searchText.length && self.contactsTableView.tableFooterView) + if (searchText.length && !self.contactsAreFilteredWithSearch) { - self.shouldHideFooterView = YES; - [self updateFooterView]; + self.contactsAreFilteredWithSearch = YES; } - else if (!searchText.length && !self.contactsTableView.tableFooterView) + else if (!searchText.length && self.contactsAreFilteredWithSearch) { - self.shouldHideFooterView = NO; - [self updateFooterView]; + self.contactsAreFilteredWithSearch = NO; } } diff --git a/Riot/Modules/StartChat/StartChatViewController.m b/Riot/Modules/StartChat/StartChatViewController.m index d48db5666..d4c410291 100644 --- a/Riot/Modules/StartChat/StartChatViewController.m +++ b/Riot/Modules/StartChat/StartChatViewController.m @@ -702,6 +702,15 @@ - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { [contactsDataSource searchWithPattern:searchText forceReset:NO]; + + if (searchText.length && !self.contactsAreFilteredWithSearch) + { + self.contactsAreFilteredWithSearch = YES; + } + else if (!searchText.length && self.contactsAreFilteredWithSearch) + { + self.contactsAreFilteredWithSearch = NO; + } } - (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar From 0d5c85f56ca7275fdaaffe053026e8e36d051aeb Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 13 Aug 2021 14:44:23 +0100 Subject: [PATCH 018/276] Check for unnecessary assignments to contactsAreFilteredWithSearch in the setter. --- .../Contacts/ContactsTableViewController.m | 17 +++++++---------- .../Modules/StartChat/StartChatViewController.m | 9 +-------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/Riot/Modules/Contacts/ContactsTableViewController.m b/Riot/Modules/Contacts/ContactsTableViewController.m index 254f71f91..997218224 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -202,8 +202,12 @@ - (void)setContactsAreFilteredWithSearch:(BOOL)contactsAreFilteredWithSearch { - _contactsAreFilteredWithSearch = contactsAreFilteredWithSearch; - [self updateFooterView]; + // Filter out redundant assignments. + if (_contactsAreFilteredWithSearch != contactsAreFilteredWithSearch) + { + _contactsAreFilteredWithSearch = contactsAreFilteredWithSearch; + [self updateFooterView]; + } } - (RequestContactsAccessFooterView*)makeFooterView @@ -483,14 +487,7 @@ { [contactsDataSource searchWithPattern:searchText forceReset:NO]; - if (searchText.length && !self.contactsAreFilteredWithSearch) - { - self.contactsAreFilteredWithSearch = YES; - } - else if (!searchText.length && self.contactsAreFilteredWithSearch) - { - self.contactsAreFilteredWithSearch = NO; - } + self.contactsAreFilteredWithSearch = searchText.length ? YES : NO; } - (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar diff --git a/Riot/Modules/StartChat/StartChatViewController.m b/Riot/Modules/StartChat/StartChatViewController.m index d4c410291..f0c58414b 100644 --- a/Riot/Modules/StartChat/StartChatViewController.m +++ b/Riot/Modules/StartChat/StartChatViewController.m @@ -703,14 +703,7 @@ { [contactsDataSource searchWithPattern:searchText forceReset:NO]; - if (searchText.length && !self.contactsAreFilteredWithSearch) - { - self.contactsAreFilteredWithSearch = YES; - } - else if (!searchText.length && self.contactsAreFilteredWithSearch) - { - self.contactsAreFilteredWithSearch = NO; - } + self.contactsAreFilteredWithSearch = searchText.length ? YES : NO; } - (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar From ce843010412bde42515135cfd18be5785bc94b05 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 13 Aug 2021 14:45:22 +0100 Subject: [PATCH 019/276] Move function. --- .../Contacts/ContactsTableViewController.m | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Riot/Modules/Contacts/ContactsTableViewController.m b/Riot/Modules/Contacts/ContactsTableViewController.m index 997218224..1cf9d280e 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -200,16 +200,6 @@ #pragma mark - -- (void)setContactsAreFilteredWithSearch:(BOOL)contactsAreFilteredWithSearch -{ - // Filter out redundant assignments. - if (_contactsAreFilteredWithSearch != contactsAreFilteredWithSearch) - { - _contactsAreFilteredWithSearch = contactsAreFilteredWithSearch; - [self updateFooterView]; - } -} - - (RequestContactsAccessFooterView*)makeFooterView { RequestContactsAccessFooterView *footerView = [RequestContactsAccessFooterView instantiate]; @@ -381,6 +371,16 @@ } } +- (void)setContactsAreFilteredWithSearch:(BOOL)contactsAreFilteredWithSearch +{ + // Filter out redundant assignments. + if (_contactsAreFilteredWithSearch != contactsAreFilteredWithSearch) + { + _contactsAreFilteredWithSearch = contactsAreFilteredWithSearch; + [self updateFooterView]; + } +} + #pragma mark - MXKDataSourceDelegate - (Class)cellViewClassForCellData:(MXKCellData*)cellData From 76107b4ba454ed00552bf8f26c78ccaa43c840c6 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 9 Sep 2021 13:08:50 +0100 Subject: [PATCH 020/276] Update strings and add localise. Use theme v2. --- Riot/Assets/en.lproj/Vector.strings | 4 +++ Riot/Generated/Strings.swift | 16 ++++++++++ .../RequestContactsAccessFooterView.swift | 29 ++++++++++++----- .../Views/RequestContactsAccessFooterView.xib | 31 ++++++++++--------- 4 files changed, 59 insertions(+), 21 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index b28568493..b4301df10 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -245,6 +245,10 @@ Tap the + to start adding people."; "contacts_address_book_permission_denied_alert_message" = "To enable contacts, go to your device settings."; "contacts_user_directory_section" = "USER DIRECTORY"; "contacts_user_directory_offline_section" = "USER DIRECTORY (offline)"; +"contacts_access_footer_title" = "Start by listing your contacts"; +"contacts_access_footer_description" = "Let %@ show your contacts so you can quickly start chatting with those you know best."; +"contacts_access_footer_button_title" = "Allow contacts access"; +"contacts_access_footer_footer" = "This can be disabled anytime from settings."; // Chat participants "room_participants_title" = "Participants"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 8b527a188..91e23d994 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -542,6 +542,22 @@ internal enum VectorL10n { internal static var collapse: String { return VectorL10n.tr("Vector", "collapse") } + /// Allow contacts access + internal static var contactsAccessFooterButtonTitle: String { + return VectorL10n.tr("Vector", "contacts_access_footer_button_title") + } + /// Let %@ show your contacts so you can quickly start chatting with those you know best. + internal static func contactsAccessFooterDescription(_ p1: String) -> String { + return VectorL10n.tr("Vector", "contacts_access_footer_description", p1) + } + /// This can be disabled anytime from settings. + internal static var contactsAccessFooterFooter: String { + return VectorL10n.tr("Vector", "contacts_access_footer_footer") + } + /// Start by listing your contacts + internal static var contactsAccessFooterTitle: String { + return VectorL10n.tr("Vector", "contacts_access_footer_title") + } /// Matrix users only internal static var contactsAddressBookMatrixUsersToggle: String { return VectorL10n.tr("Vector", "contacts_address_book_matrix_users_toggle") diff --git a/Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.swift b/Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.swift index 72df51aa0..e3849184f 100644 --- a/Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.swift +++ b/Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.swift @@ -24,6 +24,8 @@ import Reusable @objcMembers class RequestContactsAccessFooterView: UIView, NibLoadable, Themable { + // MARK: - Properties + weak var delegate: RequestContactsAccessFooterViewDelegate? @IBOutlet weak var containerView: UIView! @@ -45,21 +47,34 @@ class RequestContactsAccessFooterView: UIView, NibLoadable, Themable { containerView.layer.cornerRadius = 8 requestAccessButton.layer.cornerRadius = 8 + + titleLabel.text = VectorL10n.contactsAccessFooterTitle + descriptionLabel.text = VectorL10n.contactsAccessFooterDescription(BuildSettings.bundleDisplayName) + requestAccessButton.setTitle(VectorL10n.contactsAccessFooterButtonTitle, for: .normal) + footerLabel.text = VectorL10n.contactsAccessFooterFooter } func update(theme: Theme) { - tintColor = theme.tintColor + tintColor = theme.colors.accent - containerView.backgroundColor = theme.textQuinaryColor + containerView.backgroundColor = theme.colors.quinaryContent - titleLabel.textColor = theme.textPrimaryColor - descriptionLabel.textColor = theme.textSecondaryColor - footerLabel.textColor = theme.textTertiaryColor + titleLabel.font = theme.fonts.bodySB + titleLabel.textColor = theme.colors.primaryContent - requestAccessButton.backgroundColor = theme.tintColor - requestAccessButton.setTitleColor(theme.backgroundColor, for: .normal) + descriptionLabel.font = theme.fonts.body + descriptionLabel.textColor = theme.colors.secondaryContent + + requestAccessButton.titleLabel?.font = theme.fonts.body + requestAccessButton.backgroundColor = theme.colors.accent + requestAccessButton.setTitleColor(theme.colors.background, for: .normal) + + footerLabel.font = theme.fonts.footnote.withSize(13) + footerLabel.textColor = theme.colors.tertiaryContent } + // MARK: - Action + @IBAction private func requestContactsAccess(_ sender: Any) { delegate?.didRequestContactsAccess() } diff --git a/Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.xib b/Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.xib index d0128d78c..93579168b 100644 --- a/Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.xib +++ b/Riot/Modules/Contacts/Views/RequestContactsAccessFooterView.xib @@ -12,39 +12,42 @@ - + - + - + - @@ -87,7 +90,7 @@ - + From 9e8da35bd80219e8ca908c84bb1188021d3390cf Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 9 Sep 2021 14:41:03 +0100 Subject: [PATCH 021/276] Revert search bar placeholder. --- Riot/Assets/en.lproj/Vector.strings | 2 +- Riot/Generated/Strings.swift | 2 +- Riot/Modules/StartChat/StartChatViewController.m | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index b4301df10..b61949f5f 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -176,7 +176,7 @@ "room_creation_keep_private" = "Keep private"; "room_creation_make_private" = "Make private"; "room_creation_wait_for_creation" = "A room is already being created. Please wait."; -"room_creation_invite_another_user" = "Invite by User ID, Name or email"; +"room_creation_invite_another_user" = "User ID, name or email"; "room_creation_error_invite_user_by_email_without_identity_server" = "No identity server is configured so you cannot add a participant with an email."; "room_creation_dm_error" = "We couldn't create your DM. Please check the users you want to invite and try again."; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 91e23d994..65f3a37b0 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2454,7 +2454,7 @@ internal enum VectorL10n { internal static var roomCreationErrorInviteUserByEmailWithoutIdentityServer: String { return VectorL10n.tr("Vector", "room_creation_error_invite_user_by_email_without_identity_server") } - /// Invite by User ID, Name or email + /// User ID, name or email internal static var roomCreationInviteAnotherUser: String { return VectorL10n.tr("Vector", "room_creation_invite_another_user") } diff --git a/Riot/Modules/StartChat/StartChatViewController.m b/Riot/Modules/StartChat/StartChatViewController.m index f0c58414b..be6995687 100644 --- a/Riot/Modules/StartChat/StartChatViewController.m +++ b/Riot/Modules/StartChat/StartChatViewController.m @@ -115,7 +115,7 @@ createBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedStringFromTable(@"start", @"Vector", nil) style:UIBarButtonItemStylePlain target:self action:@selector(onButtonPressed:)]; self.navigationItem.rightBarButtonItem = createBarButtonItem; - _searchBarView.placeholder = NSLocalizedStringFromTable(@"search_default_placeholder", @"Vector", nil); + _searchBarView.placeholder = NSLocalizedStringFromTable(@"room_creation_invite_another_user", @"Vector", nil); _searchBarView.returnKeyType = UIReturnKeyDone; _searchBarView.autocapitalizationType = UITextAutocapitalizationTypeNone; [self refreshSearchBarItemsColor:_searchBarView]; From bdf5aa3868b629fcd811f5360cdc336b6874ce62 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 9 Sep 2021 15:17:29 +0100 Subject: [PATCH 022/276] Update strings in SettingsViewController. --- Riot/Assets/en.lproj/Vector.strings | 6 ++++-- Riot/Generated/Strings.swift | 16 ++++++++++---- .../Modules/Settings/SettingsViewController.m | 21 ++++++++++++++++--- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index b61949f5f..26fd77b37 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -464,7 +464,8 @@ Tap the + to start adding people."; "settings_integrations" = "INTEGRATIONS"; "settings_user_interface" = "USER INTERFACE"; "settings_ignored_users" = "IGNORED USERS"; -"settings_contacts" = "LOCAL CONTACTS"; +"settings_contacts" = "DEVICE CONTACTS"; +"settings_phone_contacts" = "PHONE CONTACTS"; "settings_advanced" = "ADVANCED"; "settings_other" = "OTHER"; "settings_labs" = "LABS"; @@ -552,8 +553,9 @@ Tap the + to start adding people."; "settings_unignore_user" = "Show all messages from %@?"; -"settings_contacts_discover_matrix_users" = "Use emails and phone numbers to discover users"; +"settings_contacts_enable_sync" = "Find your contacts"; "settings_contacts_phonebook_country" = "Phonebook country"; +"settings_contacts_enable_sync_description" = "This will use your identity server to connect you with your contacts, and help them find you."; "settings_labs_e2e_encryption" = "End-to-End Encryption"; "settings_labs_e2e_encryption_prompt_message" = "To finish setting up encryption you must log in again."; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 65f3a37b0..fcf6c5bc6 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4114,13 +4114,17 @@ internal enum VectorL10n { internal static var settingsConfirmPassword: String { return VectorL10n.tr("Vector", "settings_confirm_password") } - /// LOCAL CONTACTS + /// DEVICE CONTACTS internal static var settingsContacts: String { return VectorL10n.tr("Vector", "settings_contacts") } - /// Use emails and phone numbers to discover users - internal static var settingsContactsDiscoverMatrixUsers: String { - return VectorL10n.tr("Vector", "settings_contacts_discover_matrix_users") + /// Find your contacts + internal static var settingsContactsEnableSync: String { + return VectorL10n.tr("Vector", "settings_contacts_enable_sync") + } + /// This will use your identity server to connect you with your contacts, and help them find you. + internal static var settingsContactsEnableSyncDescription: String { + return VectorL10n.tr("Vector", "settings_contacts_enable_sync_description") } /// Phonebook country internal static var settingsContactsPhonebookCountry: String { @@ -4526,6 +4530,10 @@ internal enum VectorL10n { internal static var settingsPasswordUpdated: String { return VectorL10n.tr("Vector", "settings_password_updated") } + /// PHONE CONTACTS + internal static var settingsPhoneContacts: String { + return VectorL10n.tr("Vector", "settings_phone_contacts") + } /// Phone internal static var settingsPhoneNumber: String { return VectorL10n.tr("Vector", "settings_phone_number") diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 7fe3b62ac..e8fcb9b96 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -120,7 +120,8 @@ enum enum { LOCAL_CONTACTS_SYNC_INDEX, - LOCAL_CONTACTS_PHONEBOOK_COUNTRY_INDEX + LOCAL_CONTACTS_PHONEBOOK_COUNTRY_INDEX, + LOCAL_CONTACTS_SYNC_DESCRIPTION_INDEX }; enum @@ -451,7 +452,12 @@ TableViewSectionsDelegate> { [sectionLocalContacts addRowWithTag:LOCAL_CONTACTS_PHONEBOOK_COUNTRY_INDEX]; } - sectionLocalContacts.headerTitle = NSLocalizedStringFromTable(@"settings_contacts", @"Vector", nil); + else + { + [sectionLocalContacts addRowWithTag:LOCAL_CONTACTS_SYNC_DESCRIPTION_INDEX]; + } + NSString *localizedStringKey = UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone ? @"settings_phone_contacts" : @"settings_contacts"; + sectionLocalContacts.headerTitle = NSLocalizedStringFromTable(localizedStringKey, @"Vector", nil); [tmpSections addObject:sectionLocalContacts]; } @@ -2164,7 +2170,7 @@ TableViewSectionsDelegate> MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; labelAndSwitchCell.mxkLabel.numberOfLines = 0; - labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_contacts_discover_matrix_users", @"Vector", nil); + labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_contacts_enable_sync", @"Vector", nil); labelAndSwitchCell.mxkSwitch.on = [MXKAppSettings standardAppSettings].syncLocalContacts; labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; labelAndSwitchCell.mxkSwitch.enabled = YES; @@ -2192,6 +2198,15 @@ TableViewSectionsDelegate> [cell vc_setAccessoryDisclosureIndicatorWithCurrentTheme]; cell.selectionStyle = UITableViewCellSelectionStyleDefault; } + else if (row == LOCAL_CONTACTS_SYNC_DESCRIPTION_INDEX) + { + MXKTableViewCell *descriptionCell = [self getDefaultTableViewCell:tableView]; + descriptionCell.textLabel.text = NSLocalizedStringFromTable(@"settings_contacts_enable_sync_description", @"Vector", nil); + descriptionCell.textLabel.numberOfLines = 0; + descriptionCell.selectionStyle = UITableViewCellSelectionStyleNone; + + cell = descriptionCell; + } } else if (section == SECTION_TAG_ADVANCED) { From 4a4bf7d4dc6c28ef949a9bf4c7f1e6e42bff413b Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 13 Sep 2021 11:46:31 +0100 Subject: [PATCH 023/276] Update the Service Terms modal from the latest Figma. Checkboxes have been removed from the individual policies. Reverse flow so that the service terms are show before the contacts access popup. Removes outOfContext from the modal as it will only be presented when requested. Fixes second presentation on swipe to dismiss of the modal. --- Riot/Assets/en.lproj/InfoPlist.strings | 2 +- Riot/Assets/en.lproj/Vector.strings | 13 +- Riot/Generated/Strings.swift | 36 +-- .../Contacts/ContactsTableViewController.m | 90 +++---- .../IntegrationManagerViewController.m | 14 +- .../WidgetPicker/WidgetPickerViewController.m | 12 +- .../Widgets/WidgetViewController.m | 16 +- .../ServiceTermsModalScreenCoordinator.swift | 8 +- ...rviceTermsModalScreenCoordinatorType.swift | 1 - .../ServiceTermsModalScreenViewAction.swift | 1 - ...eTermsModalScreenViewController.storyboard | 127 ++++++---- ...erviceTermsModalScreenViewController.swift | 219 ++++++++---------- .../ServiceTermsModalScreenViewModel.swift | 12 +- ...ServiceTermsModalScreenViewModelType.swift | 4 - .../Modal/ServiceTermsModalCoordinator.swift | 27 ++- ...TermsModalCoordinatorBridgePresenter.swift | 19 +- .../ServiceTermsModalCoordinatorType.swift | 1 - ...SettingsIdentityServerViewController.swift | 2 +- .../Modules/Settings/SettingsViewController.m | 91 +++++++- 19 files changed, 364 insertions(+), 331 deletions(-) diff --git a/Riot/Assets/en.lproj/InfoPlist.strings b/Riot/Assets/en.lproj/InfoPlist.strings index e4d9358f0..6d3378a2a 100644 --- a/Riot/Assets/en.lproj/InfoPlist.strings +++ b/Riot/Assets/en.lproj/InfoPlist.strings @@ -18,6 +18,6 @@ "NSCameraUsageDescription" = "The camera is used to take photos and videos, make video calls."; "NSPhotoLibraryUsageDescription" = "The photo library is used to send photos and videos."; "NSMicrophoneUsageDescription" = "Element needs to access your microphone to make and receive calls, take videos, and record voice messages."; -"NSContactsUsageDescription" = "Element will show your contacts here so you can invite them to talk."; +"NSContactsUsageDescription" = "Element will show your contacts so you can invite them to chat."; "NSCalendarsUsageDescription" = "See your scheduled meetings in the app."; "NSFaceIDUsageDescription" = "Face ID is used to access your app."; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 26fd77b37..2d8483abd 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -1024,18 +1024,13 @@ Tap the + to start adding people."; "gdpr_consent_not_given_alert_review_now_action" = "Review now"; // Service terms -"service_terms_modal_title" = "Terms Of Service"; -"service_terms_modal_message" = "To continue you need to accept the terms of this service (%@)."; +"service_terms_modal_title_message" = "To continue, accept the below terms and conditions"; "service_terms_modal_accept_button" = "Accept"; "service_terms_modal_decline_button" = "Decline"; +"service_terms_modal_footer" = "This can be disabled anytime in settings."; -"service_terms_modal_description_for_identity_server_1" = "Find others by phone or email"; -"service_terms_modal_description_for_identity_server_2" = "Be found by phone or email"; -"service_terms_modal_description_for_integration_manager" = "Use Bots, bridges, widgets and sticker packs"; - -// Service terms - Variant for identity server when displayed out of a context -"service_terms_modal_title_identity_server" = "Contact discovery"; -"service_terms_modal_message_identity_server" = "Accept the terms of the identity server (%@) to discover contacts."; +"service_terms_modal_description_identity_server" = "This will allow someone to find you if they have your phone number or email saved in their phone contacts."; +"service_terms_modal_description_integration_manager" = "This will allow you to use bots, bridges, widgets and sticker packs."; "service_terms_modal_policy_checkbox_accessibility_hint" = "Check to accept %@"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index fcf6c5bc6..4b6164d20 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -3998,37 +3998,25 @@ internal enum VectorL10n { internal static var serviceTermsModalDeclineButton: String { return VectorL10n.tr("Vector", "service_terms_modal_decline_button") } - /// Find others by phone or email - internal static var serviceTermsModalDescriptionForIdentityServer1: String { - return VectorL10n.tr("Vector", "service_terms_modal_description_for_identity_server_1") + /// This will allow someone to find you if they have your phone number or email saved in their phone contacts. + internal static var serviceTermsModalDescriptionIdentityServer: String { + return VectorL10n.tr("Vector", "service_terms_modal_description_identity_server") } - /// Be found by phone or email - internal static var serviceTermsModalDescriptionForIdentityServer2: String { - return VectorL10n.tr("Vector", "service_terms_modal_description_for_identity_server_2") + /// This will allow you to use bots, bridges, widgets and sticker packs. + internal static var serviceTermsModalDescriptionIntegrationManager: String { + return VectorL10n.tr("Vector", "service_terms_modal_description_integration_manager") } - /// Use Bots, bridges, widgets and sticker packs - internal static var serviceTermsModalDescriptionForIntegrationManager: String { - return VectorL10n.tr("Vector", "service_terms_modal_description_for_integration_manager") - } - /// To continue you need to accept the terms of this service (%@). - internal static func serviceTermsModalMessage(_ p1: String) -> String { - return VectorL10n.tr("Vector", "service_terms_modal_message", p1) - } - /// Accept the terms of the identity server (%@) to discover contacts. - internal static func serviceTermsModalMessageIdentityServer(_ p1: String) -> String { - return VectorL10n.tr("Vector", "service_terms_modal_message_identity_server", p1) + /// This can be disabled anytime in settings. + internal static var serviceTermsModalFooter: String { + return VectorL10n.tr("Vector", "service_terms_modal_footer") } /// Check to accept %@ internal static func serviceTermsModalPolicyCheckboxAccessibilityHint(_ p1: String) -> String { return VectorL10n.tr("Vector", "service_terms_modal_policy_checkbox_accessibility_hint", p1) } - /// Terms Of Service - internal static var serviceTermsModalTitle: String { - return VectorL10n.tr("Vector", "service_terms_modal_title") - } - /// Contact discovery - internal static var serviceTermsModalTitleIdentityServer: String { - return VectorL10n.tr("Vector", "service_terms_modal_title_identity_server") + /// To continue, accept the below terms and conditions + internal static var serviceTermsModalTitleMessage: String { + return VectorL10n.tr("Vector", "service_terms_modal_title_message") } /// Invalid credentials internal static var settingsAdd3pidInvalidPasswordMessage: String { diff --git a/Riot/Modules/Contacts/ContactsTableViewController.m b/Riot/Modules/Contacts/ContactsTableViewController.m index 1cf9d280e..8c81125ea 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -525,6 +525,28 @@ #pragma mark - RequestContactsAccessFooterViewDelegate - (void)didRequestContactsAccess +{ + // First check the identity if service terms have already been accepted + if (self->contactsDataSource.mxSession.identityService.areAllTermsAgreed) + { + // If they have we only require local contacts access. + [self checkAccessForContacts]; + } + else + { + MXWeakify(self); + + [self->contactsDataSource.mxSession prepareIdentityServiceForTermsWithDefault:RiotSettings.shared.identityServerUrlString + completion:^(MXSession *session, NSString *baseURL, NSString *accessToken) { + MXStrongifyAndReturnIfNil(self); + + // Present the terms of the identity server. + [self presentIdentityServerTermsWithSession:session baseURL:baseURL andAccessToken:accessToken]; + }]; + } +} + + - (void)checkAccessForContacts { MXWeakify(self); @@ -538,50 +560,8 @@ if (granted) { - // If granted and the identity service terms have already been accepted, show the local contacts. - if (self->contactsDataSource.mxSession.identityService.areAllTermsAgreed) - { - [self showLocalContacts]; - } - else - { - // Otherwise, get a valid identity service. - MXSession *session = self->contactsDataSource.mxSession; - MXIdentityService *identityService = session.identityService; - - if (!identityService) - { - NSString *baseURL = session.accountDataIdentityServer ?: RiotSettings.shared.identityServerUrlString; - identityService = [[MXIdentityService alloc] initWithIdentityServer:baseURL - accessToken:nil - andHomeserverRestClient:session.matrixRestClient]; - } - - // Get the identity service's access token. - [identityService accessTokenWithSuccess:^(NSString * _Nullable accessToken) { - MXWeakify(session); - - // Set the identity server in the session and account data as this will be nil if - // the terms were previously declined. These will be reverted if declined once more. - [session setIdentityServer:identityService.identityServer andAccessToken:accessToken]; - [session setAccountDataIdentityServer:identityService.identityServer success:^{ - - MXStrongifyAndReturnIfNil(session); - - // Present the terms of the identity server. - [self presentIdentityServerTermsWithSession:session - baseURL:identityService.identityServer - andAccessToken:accessToken]; - - } failure:^(NSError *error) { - // Something went wrong setting the account data identity service - MXLogError(@"[ContactsTableViewController] Error preparing to display identity server terms: %@", error); - }]; - } failure:^(NSError * _Nonnull error) { - // Something went wrong getting the identity service's access token. - MXLogError(@"[ContactsTableViewController] Error preparing to display identity server terms: %@", error); - }]; - } + // When granted, local contacts can be shown. + [self showLocalContacts]; } }]; } @@ -592,7 +572,7 @@ MXKAppSettings.standardAppSettings.syncLocalContacts = YES; self->contactsDataSource.showLocalContacts = YES; - // Attempt to refresh the contacts manager - triggers identity server if necessary. + // Attempt to refresh the contacts manager. [self refreshLocalContacts]; // Hide the request access view. @@ -611,7 +591,6 @@ ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter = [[ServiceTermsModalCoordinatorBridgePresenter alloc] initWithSession:mxSession baseUrl:baseURL serviceType:MXServiceTypeIdentityService - outOfContext:YES accessToken:accessToken]; serviceTermsModalCoordinatorBridgePresenter.delegate = self; @@ -624,37 +603,22 @@ - (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidAccept:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter { - [self showLocalContacts]; - [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ - + [self checkAccessForContacts]; }]; self.serviceTermsModalCoordinatorBridgePresenter = nil; } - (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline:(ServiceTermsModalCoordinatorBridgePresenter *)coordinatorBridgePresenter session:(MXSession *)session { - MXLogDebug(@"[ContactsTableViewController] IS Terms: User has declined the use of the default IS."); - - // The user does not want to use the proposed IS. - // Disable IS feature on user's account - [session setIdentityServer:nil andAccessToken:nil]; - [session setAccountDataIdentityServer:nil success:^{ - } failure:^(NSError *error) { - MXLogDebug(@"[ContactsTableViewController] IS Terms: Error: %@", error); - }]; - [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ }]; self.serviceTermsModalCoordinatorBridgePresenter = nil; } -- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidCancel:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidClose:(ServiceTermsModalCoordinatorBridgePresenter *)coordinatorBridgePresenter { - [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ - - }]; self.serviceTermsModalCoordinatorBridgePresenter = nil; } diff --git a/Riot/Modules/Integrations/IntegrationManagerViewController.m b/Riot/Modules/Integrations/IntegrationManagerViewController.m index e4b48d09c..ae695c773 100644 --- a/Riot/Modules/Integrations/IntegrationManagerViewController.m +++ b/Riot/Modules/Integrations/IntegrationManagerViewController.m @@ -745,7 +745,6 @@ NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ"; ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter = [[ServiceTermsModalCoordinatorBridgePresenter alloc] initWithSession:mxSession baseUrl:config.baseUrl serviceType:MXServiceTypeIntegrationManager - outOfContext:NO accessToken:config.scalarToken]; serviceTermsModalCoordinatorBridgePresenter.delegate = self; @@ -762,14 +761,6 @@ NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ"; self.serviceTermsModalCoordinatorBridgePresenter = nil; } -- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidCancel:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter -{ - [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ - [self withdrawViewControllerAnimated:YES completion:nil]; - }]; - self.serviceTermsModalCoordinatorBridgePresenter = nil; -} - - (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter session:(MXSession * _Nonnull)session { [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ @@ -778,4 +769,9 @@ NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ"; self.serviceTermsModalCoordinatorBridgePresenter = nil; } +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidClose:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter +{ + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + @end diff --git a/Riot/Modules/Integrations/WidgetPicker/WidgetPickerViewController.m b/Riot/Modules/Integrations/WidgetPicker/WidgetPickerViewController.m index 32bb833a0..71f1901f5 100644 --- a/Riot/Modules/Integrations/WidgetPicker/WidgetPickerViewController.m +++ b/Riot/Modules/Integrations/WidgetPicker/WidgetPickerViewController.m @@ -151,7 +151,6 @@ ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter = [[ServiceTermsModalCoordinatorBridgePresenter alloc] initWithSession:widget.mxSession baseUrl:config.baseUrl serviceType:MXServiceTypeIntegrationManager - outOfContext:NO accessToken:config.scalarToken]; serviceTermsModalCoordinatorBridgePresenter.delegate = self; @@ -173,16 +172,15 @@ self.serviceTermsModalCoordinatorBridgePresenter = nil; } -- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidCancel:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter -{ - [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; - self.serviceTermsModalCoordinatorBridgePresenter = nil; -} - - (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter session:(MXSession * _Nonnull)session { [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; self.serviceTermsModalCoordinatorBridgePresenter = nil; } +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidClose:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter +{ + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + @end diff --git a/Riot/Modules/Integrations/Widgets/WidgetViewController.m b/Riot/Modules/Integrations/Widgets/WidgetViewController.m index 9eb26718f..d03be3aea 100644 --- a/Riot/Modules/Integrations/Widgets/WidgetViewController.m +++ b/Riot/Modules/Integrations/Widgets/WidgetViewController.m @@ -662,8 +662,7 @@ NSString *const kJavascriptSendResponseToPostMessageAPI = @"riotIOS.sendResponse MXLogDebug(@"[WidgetVC] presentTerms for %@", config.baseUrl); ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter = [[ServiceTermsModalCoordinatorBridgePresenter alloc] initWithSession:widget.mxSession baseUrl:config.baseUrl - serviceType:MXServiceTypeIntegrationManager - outOfContext:NO + serviceType:MXServiceTypeIntegrationManager accessToken:config.scalarToken]; serviceTermsModalCoordinatorBridgePresenter.delegate = self; @@ -683,14 +682,6 @@ NSString *const kJavascriptSendResponseToPostMessageAPI = @"riotIOS.sendResponse self.serviceTermsModalCoordinatorBridgePresenter = nil; } -- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidCancel:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter -{ - [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ - [self withdrawViewControllerAnimated:YES completion:nil]; - }]; - self.serviceTermsModalCoordinatorBridgePresenter = nil; -} - - (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter session:(MXSession * _Nonnull)session { [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ @@ -699,4 +690,9 @@ NSString *const kJavascriptSendResponseToPostMessageAPI = @"riotIOS.sendResponse self.serviceTermsModalCoordinatorBridgePresenter = nil; } +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidClose:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter +{ + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + @end diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinator.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinator.swift index 5fcd0e632..e572e92c2 100644 --- a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinator.swift +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinator.swift @@ -37,9 +37,9 @@ final class ServiceTermsModalScreenCoordinator: ServiceTermsModalScreenCoordinat // MARK: - Setup - init(serviceTerms: MXServiceTerms, outOfContext: Bool = false) { + init(serviceTerms: MXServiceTerms) { - let serviceTermsModalScreenViewModel = ServiceTermsModalScreenViewModel(serviceTerms: serviceTerms, outOfContext: outOfContext) + let serviceTermsModalScreenViewModel = ServiceTermsModalScreenViewModel(serviceTerms: serviceTerms) let serviceTermsModalScreenViewController = ServiceTermsModalScreenViewController.instantiate(with: serviceTermsModalScreenViewModel) self.serviceTermsModalScreenViewModel = serviceTermsModalScreenViewModel self.serviceTermsModalScreenViewController = serviceTermsModalScreenViewController @@ -70,8 +70,4 @@ extension ServiceTermsModalScreenCoordinator: ServiceTermsModalScreenViewModelCo func serviceTermsModalScreenViewModelDidDecline(_ viewModel: ServiceTermsModalScreenViewModelType) { self.delegate?.serviceTermsModalScreenCoordinatorDidDecline(self) } - - func serviceTermsModalScreenViewModelDidCancel(_ viewModel: ServiceTermsModalScreenViewModelType) { - self.delegate?.serviceTermsModalScreenCoordinatorDidCancel(self) - } } diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinatorType.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinatorType.swift index 909a0b33f..55969be50 100644 --- a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinatorType.swift +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinatorType.swift @@ -22,7 +22,6 @@ protocol ServiceTermsModalScreenCoordinatorDelegate: AnyObject { func serviceTermsModalScreenCoordinatorDidAccept(_ coordinator: ServiceTermsModalScreenCoordinatorType) func serviceTermsModalScreenCoordinator(_ coordinator: ServiceTermsModalScreenCoordinatorType, displayPolicy policy: MXLoginPolicyData) func serviceTermsModalScreenCoordinatorDidDecline(_ coordinator: ServiceTermsModalScreenCoordinatorType) - func serviceTermsModalScreenCoordinatorDidCancel(_ coordinator: ServiceTermsModalScreenCoordinatorType) } /// `ServiceTermsModalScreenCoordinatorType` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow. diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewAction.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewAction.swift index 27bb11e05..7c10a2aaf 100644 --- a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewAction.swift +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewAction.swift @@ -24,5 +24,4 @@ enum ServiceTermsModalScreenViewAction { case display(MXLoginPolicyData) case accept case decline - case cancel } diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard index f7c793731..4258b26be 100644 --- a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard @@ -1,12 +1,11 @@ - - - - + + - + + @@ -15,48 +14,90 @@ - + - + - + - + - - - - - - - + + - - + - - + - + diff --git a/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m b/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m index 12a63f041..19cfb5a8b 100644 --- a/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m +++ b/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m @@ -91,7 +91,7 @@ [titles addObject: NSLocalizedStringFromTable(@"search_people", @"Vector", nil)]; peopleSearchViewController = [ContactsTableViewController contactsTableViewController]; peopleSearchViewController.contactsTableViewControllerDelegate = self; - peopleSearchViewController.hideRequestContactAccessFooter = YES; + peopleSearchViewController.disableFindYourContactsFooter = YES; [viewControllers addObject:peopleSearchViewController]; // add Files tab diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard index 4258b26be..fc40bdc45 100644 --- a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard @@ -29,7 +29,7 @@ - + @@ -149,7 +151,7 @@ - + @@ -166,7 +168,7 @@ - + diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift index 4b35e7a08..7220846f9 100644 --- a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift @@ -78,6 +78,18 @@ final class ServiceTermsModalScreenViewController: UIViewController { self.viewModel.process(viewAction: .load) } + override func viewWillAppear(_ animated: Bool) { + if view.frame.size.height < 568 { + navigationController?.setNavigationBarHidden(true, animated: animated) + } + } + + override func viewWillDisappear(_ animated: Bool) { + if navigationController?.isNavigationBarHidden == true { + navigationController?.setNavigationBarHidden(false, animated: animated) + } + } + override var preferredStatusBarStyle: UIStatusBarStyle { return self.theme.statusBarStyle } From 7693a5aef5d2b4b1b5182ddea109555ae498ac06 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 16 Sep 2021 13:08:21 +0100 Subject: [PATCH 031/276] Fix landscape layout of ServiceTermsModalScreenViewController. --- ...eTermsModalScreenViewController.storyboard | 204 +++++++++--------- ...erviceTermsModalScreenViewController.swift | 16 +- 2 files changed, 103 insertions(+), 117 deletions(-) diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard index 77015ddcc..3f4cd5f2c 100644 --- a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard @@ -20,129 +20,121 @@ - - + + - - + + - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + - - - - - - + + + + + + + @@ -151,7 +143,6 @@ - @@ -163,6 +154,7 @@ + diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift index 7220846f9..818f94109 100644 --- a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift @@ -38,7 +38,8 @@ final class ServiceTermsModalScreenViewController: UIViewController { @IBOutlet private weak var tableView: UITableView! @IBOutlet private weak var acceptButton: UIButton! @IBOutlet private weak var declineButton: UIButton! - + @IBOutlet private weak var tableViewHeightConstraint: NSLayoutConstraint! + // MARK: Private private var viewModel: ServiceTermsModalScreenViewModelType! @@ -78,16 +79,9 @@ final class ServiceTermsModalScreenViewController: UIViewController { self.viewModel.process(viewAction: .load) } - override func viewWillAppear(_ animated: Bool) { - if view.frame.size.height < 568 { - navigationController?.setNavigationBarHidden(true, animated: animated) - } - } - - override func viewWillDisappear(_ animated: Bool) { - if navigationController?.isNavigationBarHidden == true { - navigationController?.setNavigationBarHidden(false, animated: animated) - } + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + tableViewHeightConstraint.constant = max(120, tableView.contentSize.height) } override var preferredStatusBarStyle: UIStatusBarStyle { From e6804935f15e23eea73bcb7a2959b4e2801a4641 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Wed, 22 Sep 2021 11:38:50 +0300 Subject: [PATCH 032/276] Implement NSArray mappers --- Riot/Categories/NSArray+Element.h | 31 +++++++++++++++++ Riot/Categories/NSArray+Element.m | 55 +++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 Riot/Categories/NSArray+Element.h create mode 100644 Riot/Categories/NSArray+Element.m diff --git a/Riot/Categories/NSArray+Element.h b/Riot/Categories/NSArray+Element.h new file mode 100644 index 000000000..f8ef1a857 --- /dev/null +++ b/Riot/Categories/NSArray+Element.h @@ -0,0 +1,31 @@ +// +// 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface NSArray (Element) + +- (NSArray *)vc_map:(id (^)(id obj))block; + +- (NSArray *)vc_compactMap:(id _Nullable (^)(id obj))block; + +- (NSArray *)vc_flatMap:(NSArray* (^)(id obj))block; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Riot/Categories/NSArray+Element.m b/Riot/Categories/NSArray+Element.m new file mode 100644 index 000000000..35427c9df --- /dev/null +++ b/Riot/Categories/NSArray+Element.m @@ -0,0 +1,55 @@ +// +// 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 "NSArray+Element.h" + +@implementation NSArray (Element) + +- (NSArray *)vc_map:(id (^)(id obj))block +{ + NSMutableArray *result = [NSMutableArray arrayWithCapacity:self.count]; + [self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) + { + [result addObject:block(obj)]; + }]; + return result; +} + +- (NSArray *)vc_compactMap:(id _Nullable (^)(id obj))block +{ + NSMutableArray *result = [NSMutableArray arrayWithCapacity:self.count]; + [self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) + { + id mappedObject = block(obj); + if (mappedObject) + { + [result addObject:mappedObject]; + } + }]; + return result; +} + +- (NSArray *)vc_flatMap:(NSArray* (^)(id obj))block +{ + NSMutableArray *result = [NSMutableArray arrayWithCapacity:self.count]; + [self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) + { + [result addObjectsFromArray:block(obj)]; + }]; + return result; +} + +@end From c7b40f408b9ef8957c01f70bde3b612450e5a6a0 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Wed, 22 Sep 2021 11:41:22 +0300 Subject: [PATCH 033/276] Adapt sentStatus changes --- Riot/Categories/MXRoom+Riot.h | 10 ------- Riot/Categories/MXRoom+Riot.m | 26 ------------------- .../Recents/Views/RecentTableViewCell.m | 5 ++-- .../Home/Views/RoomCollectionViewCell.m | 7 ++--- Riot/Modules/Room/RoomViewController.m | 10 +++---- 5 files changed, 12 insertions(+), 46 deletions(-) diff --git a/Riot/Categories/MXRoom+Riot.h b/Riot/Categories/MXRoom+Riot.h index dcf4860a3..42a6a6c01 100644 --- a/Riot/Categories/MXRoom+Riot.h +++ b/Riot/Categories/MXRoom+Riot.h @@ -19,13 +19,6 @@ #import "UserEncryptionTrustLevel.h" -typedef NS_ENUM(NSUInteger, RoomSentStatus) -{ - RoomSentStatusOk, - RoomSentStatusSentFailed, - RoomSentStatusSentFailedDueToUnknownDevices -}; - /** Define a `MXRoom` category at Riot level. */ @@ -51,9 +44,6 @@ typedef NS_ENUM(NSUInteger, RoomSentStatus) */ @property (nonatomic) id notificationCenterDidUpdateObserver; -/// Check if all messages have been sent. -@property (nonatomic, readonly) RoomSentStatus sentStatus; - /** Update the room tag. diff --git a/Riot/Categories/MXRoom+Riot.m b/Riot/Categories/MXRoom+Riot.m index 273f86a82..5bef645be 100644 --- a/Riot/Categories/MXRoom+Riot.m +++ b/Riot/Categories/MXRoom+Riot.m @@ -656,30 +656,4 @@ return objc_getAssociatedObject(self, @selector(notificationCenterDidUpdateObserver)); } -#pragma mark - Unread messages - -- (RoomSentStatus)sentStatus -{ - RoomSentStatus status = RoomSentStatusOk; - NSArray *outgoingMsgs = self.outgoingMessages; - - for (MXEvent *event in outgoingMsgs) - { - if (event.sentState == MXEventSentStateFailed) - { - status = RoomSentStatusSentFailed; - - // Check if the error is due to unknown devices - if ([event.sentError.domain isEqualToString:MXEncryptingErrorDomain] - && event.sentError.code == MXEncryptingErrorUnknownDeviceCode) - { - status = RoomSentStatusSentFailedDueToUnknownDevices; - break; - } - } - } - - return status; -} - @end diff --git a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m index 337c39cba..b5c5add9f 100644 --- a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m +++ b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m @@ -88,7 +88,8 @@ self.lastEventDescription.text = roomCellData.lastEventTextMessage; } - self.unsentImageView.hidden = roomCellData.roomSummary.room.sentStatus == RoomSentStatusOk; + MXRoom *room = [roomCellData.mxSession roomWithRoomId:roomCellData.roomSummary.roomId]; + self.unsentImageView.hidden = roomCellData.roomSummary.sentStatus == MXRoomSummarySentStatusOk; self.lastEventDecriptionLabelTrailingConstraint.constant = self.unsentImageView.hidden ? 10 : 30; // Notify unreads and bing @@ -124,7 +125,7 @@ self.roomTitle.font = [UIFont systemFontOfSize:17 weight:UIFontWeightMedium]; } - [roomCellData.roomSummary setRoomAvatarImageIn:self.roomAvatar]; + [room.summary setRoomAvatarImageIn:self.roomAvatar]; } else { diff --git a/Riot/Modules/Home/Views/RoomCollectionViewCell.m b/Riot/Modules/Home/Views/RoomCollectionViewCell.m index 33b263a80..d438b2b9e 100644 --- a/Riot/Modules/Home/Views/RoomCollectionViewCell.m +++ b/Riot/Modules/Home/Views/RoomCollectionViewCell.m @@ -101,8 +101,8 @@ } // Notify unreads and bing - if (roomCellData.roomSummary.room.summary.membership == MXMembershipInvite - || roomCellData.roomSummary.room.sentStatus != RoomSentStatusOk) + if (roomCellData.roomSummary.membership == MXMembershipInvite + || roomCellData.roomSummary.sentStatus != MXRoomSummarySentStatusOk) { self.badgeLabel.hidden = NO; self.badgeLabel.badgeColor = ThemeService.shared.theme.noticeColor; @@ -130,7 +130,8 @@ } - [roomCellData.roomSummary setRoomAvatarImageIn:self.roomAvatar]; + MXRoom *room = [roomCellData.mxSession roomWithRoomId:roomCellData.roomSummary.roomId]; + [room.summary setRoomAvatarImageIn:self.roomAvatar]; } } diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 301c568ba..314f7fe55 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -4993,14 +4993,14 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; -(BOOL)checkUnsentMessages { - RoomSentStatus sentStatus = RoomSentStatusOk; + MXRoomSummarySentStatus sentStatus = MXRoomSummarySentStatusOk; if ([self.activitiesView isKindOfClass:RoomActivitiesView.class]) { - sentStatus = self.roomDataSource.room.sentStatus; + sentStatus = self.roomDataSource.room.summary.sentStatus; - if (sentStatus != RoomSentStatusOk) + if (sentStatus != MXRoomSummarySentStatusOk) { - NSString *notification = sentStatus == RoomSentStatusSentFailedDueToUnknownDevices ? + NSString *notification = sentStatus == MXRoomSummarySentStatusSentFailedDueToUnknownDevices ? NSLocalizedStringFromTable(@"room_unsent_messages_unknown_devices_notification", @"Vector", nil) : NSLocalizedStringFromTable(@"room_unsent_messages_notification", @"Vector", nil); @@ -5071,7 +5071,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } } - return sentStatus != RoomSentStatusOk; + return sentStatus != MXRoomSummarySentStatusOk; } - (void)eventDidChangeSentState:(NSNotification *)notif From 83130b9772d72bfe858b31dc2e08195e3641a23e Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Wed, 22 Sep 2021 11:43:07 +0300 Subject: [PATCH 034/276] Change method parameter --- .../Common/Recents/RecentsViewController+RoomInvite.swift | 5 +++-- Riot/Modules/Common/Recents/RecentsViewController.m | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Riot/Modules/Common/Recents/RecentsViewController+RoomInvite.swift b/Riot/Modules/Common/Recents/RecentsViewController+RoomInvite.swift index 32bf4dada..681a24874 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController+RoomInvite.swift +++ b/Riot/Modules/Common/Recents/RecentsViewController+RoomInvite.swift @@ -18,8 +18,9 @@ import Foundation extension RecentsViewController { - @objc func canShowRoomPreview(for room: MXRoom) -> Bool { - let membershipTransitionState = room.summary.membershipTransitionState + @objc + func canShowRoomPreview(for summary: MXRoomSummaryProtocol) -> Bool { + let membershipTransitionState = summary.membershipTransitionState // NOTE: For the moment do not offer the possibility to show room preview when invitation action is in progress diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index 7e89bd215..76d54ee95 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -1452,7 +1452,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro [self showSpaceInviteNotAvailable]; } // Check if can show preview for the invited room - else if ([self canShowRoomPreviewFor:invitedRoom]) + else if ([self canShowRoomPreviewFor:cellData.roomSummary]) { // Display the room preview [self dispayRoomWithRoomId:invitedRoom.roomId inMatrixSession:invitedRoom.mxSession]; From 83e5c7ad6e3eaeec8caca0e13923dd25d44b0123 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Wed, 22 Sep 2021 11:46:12 +0300 Subject: [PATCH 035/276] Adapt to cell data changes --- Riot/Modules/Common/Recents/CellData/RecentCellData.m | 3 ++- Riot/Modules/Common/Recents/RecentsViewController.m | 7 +++---- Riot/Modules/Home/HomeViewController.m | 2 +- .../InviteRecentTableViewCell+ButtonViewsUpdate.swift | 4 ++-- Riot/Modules/People/Views/InviteRecentTableViewCell.m | 11 +++-------- .../Share/Listing/Views/RecentRoomTableViewCell.m | 3 ++- 6 files changed, 13 insertions(+), 17 deletions(-) diff --git a/Riot/Modules/Common/Recents/CellData/RecentCellData.m b/Riot/Modules/Common/Recents/CellData/RecentCellData.m index a9def48fd..6b691fd28 100644 --- a/Riot/Modules/Common/Recents/CellData/RecentCellData.m +++ b/Riot/Modules/Common/Recents/CellData/RecentCellData.m @@ -44,8 +44,9 @@ - (NSUInteger)notificationCount { + MXRoom *room = [self.mxSession roomWithRoomId:self.roomSummary.roomId]; // Ignore the regular notification count if the room is in 'mentions only" mode at the Riot level. - if (self.roomSummary.room.isMentionsOnly) + if (room.isMentionsOnly) { // Only the highlighted missed messages must be considered here. return self.roomSummary.highlightCount; diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index 76d54ee95..36b995e2d 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -894,7 +894,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro { id cellDataStoring = (id )cellData; - if (cellDataStoring.roomSummary.room.summary.membership != MXMembershipInvite) + if (cellDataStoring.roomSummary.membership != MXMembershipInvite) { return RecentTableViewCell.class; } @@ -1444,9 +1444,8 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro id cellData = [self.dataSource cellDataAtIndexPath:indexPath]; // Retrieve the invited room - MXRoom* invitedRoom = cellData.roomSummary.room; - if (invitedRoom.summary.roomType == MXRoomTypeSpace) + if (cellData.roomSummary.roomType == MXRoomTypeSpace) { // Indicates that spaces are not supported [self showSpaceInviteNotAvailable]; @@ -1455,7 +1454,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro else if ([self canShowRoomPreviewFor:cellData.roomSummary]) { // Display the room preview - [self dispayRoomWithRoomId:invitedRoom.roomId inMatrixSession:invitedRoom.mxSession]; + [self dispayRoomWithRoomId:cellData.roomSummary.roomId inMatrixSession:cellData.mxSession]; } else { diff --git a/Riot/Modules/Home/HomeViewController.m b/Riot/Modules/Home/HomeViewController.m index 21ae2e64c..ea504c2e8 100644 --- a/Riot/Modules/Home/HomeViewController.m +++ b/Riot/Modules/Home/HomeViewController.m @@ -562,7 +562,7 @@ id renderedCellData = (id)roomCollectionViewCell.renderedCellData; - [self.delegate recentListViewController:self didSelectRoom:renderedCellData.roomSummary.roomId inMatrixSession:renderedCellData.roomSummary.room.mxSession]; + [self.delegate recentListViewController:self didSelectRoom:renderedCellData.roomSummary.roomId inMatrixSession:renderedCellData.mxSession]; } // Hide the keyboard when user select a room diff --git a/Riot/Modules/People/Views/InviteRecentTableViewCell+ButtonViewsUpdate.swift b/Riot/Modules/People/Views/InviteRecentTableViewCell+ButtonViewsUpdate.swift index 7c5ce4825..27d319da7 100644 --- a/Riot/Modules/People/Views/InviteRecentTableViewCell+ButtonViewsUpdate.swift +++ b/Riot/Modules/People/Views/InviteRecentTableViewCell+ButtonViewsUpdate.swift @@ -26,8 +26,8 @@ extension InviteRecentTableViewCell { } /// Update buttons according to current MXMembershipChangeState of the room - @objc func updateButtonViews(with room: MXRoom) { - let membershipTransitionState = room.summary.membershipTransitionState + @objc func updateButtonViews(with summary: MXRoomSummaryProtocol) { + let membershipTransitionState = summary.membershipTransitionState var joinButtonIsLoading = false var leaveButtonIsLoading = false diff --git a/Riot/Modules/People/Views/InviteRecentTableViewCell.m b/Riot/Modules/People/Views/InviteRecentTableViewCell.m index 796e1d938..9401ab495 100644 --- a/Riot/Modules/People/Views/InviteRecentTableViewCell.m +++ b/Riot/Modules/People/Views/InviteRecentTableViewCell.m @@ -87,7 +87,7 @@ NSString *const kInviteRecentTableViewCellRoomKey = @"kInviteRecentTableViewCell { if (self.delegate) { - MXRoom *room = roomCellData.roomSummary.room; + MXRoom *room = [roomCellData.mxSession roomWithRoomId:roomCellData.roomSummary.roomId]; if (room) { @@ -99,16 +99,11 @@ NSString *const kInviteRecentTableViewCellRoomKey = @"kInviteRecentTableViewCell - (void)render:(MXKCellData *)cellData { [super render:cellData]; - - MXRoom *room = roomCellData.roomSummary.room; - if (room.roomId) - { - [self updateViewsWithRoom:room showPreviewButton:NO]; - } + [self updateViewsWithRoom:roomCellData.roomSummary showPreviewButton:NO]; } -- (void)updateViewsWithRoom:(MXRoom*)room showPreviewButton:(BOOL)showPreviewButton +- (void)updateViewsWithRoom:(id)room showPreviewButton:(BOOL)showPreviewButton { NSString *rightButtonTitle; diff --git a/RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.m b/RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.m index 386d25835..52ed17ee8 100644 --- a/RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.m +++ b/RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.m @@ -70,7 +70,8 @@ roomCellData = (id)cellData; if (roomCellData) { - [roomCellData.roomSummary setRoomAvatarImageIn:self.avatarImageView]; + MXRoomSummary *roomSummary = [roomCellData.mxSession roomSummaryWithRoomId:roomCellData.roomSummary.roomId]; + [roomSummary setRoomAvatarImageIn:self.avatarImageView]; self.roomTitleLabel.text = roomCellData.roomSummary.displayname; if (!self.roomTitleLabel.text.length) From c7c1a651b2e63360833a2551c65bebf6b0e1c341 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 23 Sep 2021 16:55:00 +0300 Subject: [PATCH 036/276] Post a notification when RiotSettings updated --- Riot/Managers/Settings/RiotSettings.swift | 8 +++++--- .../UserDefaultsBackedPropertyWrapper.swift | 9 +++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 1872db213..3a070c03d 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -22,10 +22,12 @@ final class RiotSettings: NSObject { // MARK: - Constants - private enum UserDefaultsKeys { + public enum UserDefaultsKeys { static let enableCrashReport = "enableCrashReport" static let notificationsShowDecryptedContent = "showDecryptedContent" static let allowStunServerFallback = "allowStunServerFallback" + static let pinRoomsWithMissedNotificationsOnHome = "pinRoomsWithMissedNotif" + static let pinRoomsWithUnreadMessagesOnHome = "pinRoomsWithUnread" } static let shared = RiotSettings() @@ -79,11 +81,11 @@ final class RiotSettings: NSObject { var showDecryptedContentInNotifications /// Indicate if rooms with missed notifications should be displayed first on home screen. - @UserDefault(key: "pinRoomsWithMissedNotif", defaultValue: false, storage: defaults) + @UserDefault(key: UserDefaultsKeys.pinRoomsWithMissedNotificationsOnHome, defaultValue: false, storage: defaults) var pinRoomsWithMissedNotificationsOnHome /// Indicate if rooms with unread messages should be displayed first on home screen. - @UserDefault(key: "pinRoomsWithUnread", defaultValue: false, storage: defaults) + @UserDefault(key: UserDefaultsKeys.pinRoomsWithUnreadMessagesOnHome, defaultValue: false, storage: defaults) var pinRoomsWithUnreadMessagesOnHome /// Indicate to show Not Safe For Work public rooms. diff --git a/Riot/PropertyWrappers/UserDefaultsBackedPropertyWrapper.swift b/Riot/PropertyWrappers/UserDefaultsBackedPropertyWrapper.swift index bcf40b573..2f761cd15 100644 --- a/Riot/PropertyWrappers/UserDefaultsBackedPropertyWrapper.swift +++ b/Riot/PropertyWrappers/UserDefaultsBackedPropertyWrapper.swift @@ -42,6 +42,11 @@ struct UserDefault { } else { storage.setValue(newValue, forKey: key) } + let key = key + DispatchQueue.main.async { + NotificationCenter.default.post(name: .userDefaultValueUpdated, + object: key) + } } } } @@ -59,3 +64,7 @@ private protocol AnyOptional { extension Optional: AnyOptional { var isNil: Bool { self == nil } } + +extension Notification.Name { + static let userDefaultValueUpdated = Notification.Name("userDefaultValueUpdated") +} From 878291f50c3d942ead31650383847c821685b26b Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 23 Sep 2021 16:55:58 +0300 Subject: [PATCH 037/276] Adapt store method changes --- .../Modules/Share/DataSources/ShareDataSource.m | 13 ++++++++----- SiriIntents/IntentHandler.m | 16 ++++++++-------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/RiotShareExtension/Modules/Share/DataSources/ShareDataSource.m b/RiotShareExtension/Modules/Share/DataSources/ShareDataSource.m index e841769ea..8c71e6a52 100644 --- a/RiotShareExtension/Modules/Share/DataSources/ShareDataSource.m +++ b/RiotShareExtension/Modules/Share/DataSources/ShareDataSource.m @@ -24,10 +24,12 @@ @property NSArray *recentCellDatas; @property NSMutableArray *visibleRoomCellDatas; +@property (nonatomic, strong) MXSession *mxSession; @end @implementation ShareDataSource +@synthesize mxSession; - (instancetype)initWithMode:(ShareDataSourceMode)dataSourceMode { @@ -53,20 +55,21 @@ - (void)loadCellData { - [[ShareExtensionManager sharedManager].fileStore asyncRoomsSummaries:^(NSArray * _Nonnull roomsSummaries) { + [[ShareExtensionManager sharedManager].fileStore asyncRoomsSummaries:^(NSArray> * _Nonnull roomsSummaries) { NSMutableArray *cellData = [NSMutableArray array]; // Add a fake matrix session to each room summary to provide it a REST client (used to handle correctly the room avatar). - MXSession *session = [[MXSession alloc] initWithMatrixRestClient:[[MXRestClient alloc] initWithCredentials:[ShareExtensionManager sharedManager].userAccount.mxCredentials andOnUnrecognizedCertificateBlock:nil]]; + self.mxSession = [[MXSession alloc] initWithMatrixRestClient:[[MXRestClient alloc] initWithCredentials:[ShareExtensionManager sharedManager].userAccount.mxCredentials andOnUnrecognizedCertificateBlock:nil]]; - for (MXRoomSummary *roomSummary in roomsSummaries) + for (id roomSummary in roomsSummaries) { if (!roomSummary.hiddenFromUser && ((self.dataSourceMode == DataSourceModeRooms) ^ roomSummary.isDirect)) { - [roomSummary setMatrixSession:session]; + [roomSummary setMatrixSession:self.mxSession]; - MXKRecentCellData *recentCellData = [[MXKRecentCellData alloc] initWithRoomSummary:roomSummary andRecentListDataSource:nil]; + MXKRecentCellData *recentCellData = [[MXKRecentCellData alloc] initWithRoomSummary:roomSummary + dataSource:self]; [cellData addObject:recentCellData]; } diff --git a/SiriIntents/IntentHandler.m b/SiriIntents/IntentHandler.m index 908ed40ed..c55a5357e 100644 --- a/SiriIntents/IntentHandler.m +++ b/SiriIntents/IntentHandler.m @@ -217,11 +217,11 @@ { MXKAccount *account = [MXKAccountManager sharedManager].activeAccounts.firstObject; MXFileStore *fileStore = [[MXFileStore alloc] initWithCredentials:account.mxCredentials]; - [fileStore asyncRoomsSummaries:^(NSArray * _Nonnull roomsSummaries) { + [fileStore asyncRoomsSummaries:^(NSArray> * _Nonnull roomsSummaries) { NSString *roomID = person.customIdentifier; BOOL isEncrypted = NO; - for (MXRoomSummary *roomSummary in roomsSummaries) + for (id roomSummary in roomsSummaries) { if ([roomSummary.roomId isEqualToString:roomID]) { @@ -318,16 +318,16 @@ if (account) { MXFileStore *fileStore = [[MXFileStore alloc] initWithCredentials:account.mxCredentials]; - [fileStore asyncRoomsSummaries:^(NSArray * _Nonnull roomsSummaries) { + [fileStore asyncRoomsSummaries:^(NSArray> * _Nonnull roomsSummaries) { // Contains userIds of all users with whom the current user has direct chats // Use set to avoid duplicates NSMutableSet *directUserIds = [NSMutableSet set]; // Contains room summaries for all direct rooms connected with particular userId - NSMutableDictionary *> *roomSummaries = [NSMutableDictionary dictionary]; + NSMutableDictionary> *> *roomSummaries = [NSMutableDictionary dictionary]; - for (MXRoomSummary *summary in roomsSummaries) + for (id summary in roomsSummaries) { // TODO: We also need to check if joined room members count equals 2 // It is pointlessly to save rooms with 1 joined member or room with more than 2 joined members @@ -343,7 +343,7 @@ [directUserIds addObject:diretUserId]; // Save associated with diretUserId room summary - NSMutableArray *userRoomSummaries = roomSummaries[diretUserId]; + NSMutableArray> *userRoomSummaries = roomSummaries[diretUserId]; if (userRoomSummaries) [userRoomSummaries addObject:summary]; else @@ -373,8 +373,8 @@ MXUser *user = matchingUsers.firstObject; // Provide to the user a list of direct rooms to choose from - NSArray *summaries = roomSummaries[user.userId]; - for (MXRoomSummary *summary in summaries) + NSArray> *summaries = roomSummaries[user.userId]; + for (id summary in summaries) { INPersonHandle *personHandle = [[INPersonHandle alloc] initWithValue:user.userId type:INPersonHandleTypeUnknown]; From 8a8160e04368270eee9d697c79e24bcd67884107 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 23 Sep 2021 16:56:39 +0300 Subject: [PATCH 038/276] Fetch room from session when needed --- Riot/Modules/Common/Recents/Views/RecentTableViewCell.m | 4 ++-- Riot/Modules/Home/Views/RoomCollectionViewCell.m | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m index b5c5add9f..f000c2d71 100644 --- a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m +++ b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m @@ -88,7 +88,6 @@ self.lastEventDescription.text = roomCellData.lastEventTextMessage; } - MXRoom *room = [roomCellData.mxSession roomWithRoomId:roomCellData.roomSummary.roomId]; self.unsentImageView.hidden = roomCellData.roomSummary.sentStatus == MXRoomSummarySentStatusOk; self.lastEventDecriptionLabelTrailingConstraint.constant = self.unsentImageView.hidden ? 10 : 30; @@ -125,7 +124,8 @@ self.roomTitle.font = [UIFont systemFontOfSize:17 weight:UIFontWeightMedium]; } - [room.summary setRoomAvatarImageIn:self.roomAvatar]; + MXRoomSummary *summary = [roomCellData.mxSession roomSummaryWithRoomId:roomCellData.roomSummary.roomId]; + [summary setRoomAvatarImageIn:self.roomAvatar]; } else { diff --git a/Riot/Modules/Home/Views/RoomCollectionViewCell.m b/Riot/Modules/Home/Views/RoomCollectionViewCell.m index d438b2b9e..1c284d7d5 100644 --- a/Riot/Modules/Home/Views/RoomCollectionViewCell.m +++ b/Riot/Modules/Home/Views/RoomCollectionViewCell.m @@ -130,8 +130,8 @@ } - MXRoom *room = [roomCellData.mxSession roomWithRoomId:roomCellData.roomSummary.roomId]; - [room.summary setRoomAvatarImageIn:self.roomAvatar]; + MXRoomSummary *summary = [roomCellData.mxSession roomSummaryWithRoomId:roomCellData.roomSummary.roomId]; + [summary setRoomAvatarImageIn:self.roomAvatar]; } } From e3e713ab1c0f0e12b591cba094e8009d683ef88f Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 24 Sep 2021 18:50:41 +0300 Subject: [PATCH 039/276] Refactor recents data source to use new room list data fetchers --- .../DataSources/DiscussionsCount.swift | 60 ++ .../Recents/DataSources/RecentsDataSource.h | 56 +- .../Recents/DataSources/RecentsDataSource.m | 552 ++++-------------- .../DataSources/RecentsDataSourceState.swift | 34 +- .../RecentsRoomListContainer.swift | 430 ++++++++++++++ Riot/Modules/People/PeopleViewController.m | 2 +- Riot/Modules/TabBar/MasterTabBarController.m | 20 +- 7 files changed, 624 insertions(+), 530 deletions(-) create mode 100644 Riot/Modules/Common/Recents/DataSources/DiscussionsCount.swift create mode 100644 Riot/Modules/Common/Recents/DataSources/RecentsRoomListContainer.swift diff --git a/Riot/Modules/Common/Recents/DataSources/DiscussionsCount.swift b/Riot/Modules/Common/Recents/DataSources/DiscussionsCount.swift new file mode 100644 index 000000000..7d8e751ff --- /dev/null +++ b/Riot/Modules/Common/Recents/DataSources/DiscussionsCount.swift @@ -0,0 +1,60 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// Noticiations counts per section +@objcMembers +public class DiscussionsCount: NSObject { + /// Number of notified rooms with regular notifications + public let numberOfNotified: Int + + /// Number of highlighted rooms with mentions like notications + public let numberOfHighlighted: Int + + /// Number of rooms that has unsent messages in it + public let numberOfUnsent: Int + + /// Flag indicating is there any unsent + public var hasUnsent: Bool { + return numberOfUnsent > 0 + } + + /// Flag indicating is there any highlight + public var hasHighlight: Bool { + return numberOfHighlighted > 0 + } + + public static let zero: DiscussionsCount = DiscussionsCount(numberOfNotified: 0, + numberOfHighlighted: 0, + numberOfUnsent: 0) + + public init(numberOfNotified: Int, + numberOfHighlighted: Int, + numberOfUnsent: Int) { + self.numberOfNotified = numberOfNotified + self.numberOfHighlighted = numberOfHighlighted + self.numberOfUnsent = numberOfUnsent + super.init() + } + + public init(withRoomListDataCounts counts: MXRoomListDataCounts) { + self.numberOfNotified = counts.numberOfNotifiedRooms + self.numberOfHighlighted = counts.numberOfHighlightedRooms + counts.numberOfInvitedRooms + self.numberOfUnsent = counts.numberOfUnsentRooms + super.init() + } +} diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h index 2e1510949..822868e8c 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h @@ -19,18 +19,19 @@ #import "PublicRoomsDirectoryDataSource.h" +@protocol MXKRecentCellDataStoring; +@class DiscussionsCount; /** List the different modes used to prepare the recents data source. Each mode corresponds to an application tab: Home, Favourites, People and Rooms. */ -typedef enum : NSUInteger +typedef NS_ENUM(NSInteger, RecentsDataSourceMode) { RecentsDataSourceModeHome, RecentsDataSourceModeFavourites, RecentsDataSourceModePeople, RecentsDataSourceModeRooms - -} RecentsDataSourceMode; +}; /** List the different secure backup banners that could be displayed. @@ -73,12 +74,12 @@ extern NSString *const kRecentsDataSourceTapOnDirectoryServerChange; @property (nonatomic) NSInteger lowPrioritySection; @property (nonatomic) NSInteger serverNoticeSection; -@property (nonatomic, readonly) NSArray* invitesCellDataArray; -@property (nonatomic, readonly) NSArray* favoriteCellDataArray; -@property (nonatomic, readonly) NSArray* peopleCellDataArray; -@property (nonatomic, readonly) NSArray* conversationCellDataArray; -@property (nonatomic, readonly) NSArray* lowPriorityCellDataArray; -@property (nonatomic, readonly) NSArray* serverNoticeCellDataArray; +@property (nonatomic, readonly) NSArray> *invitesCellDataArray; +@property (nonatomic, readonly) NSArray> *favoriteCellDataArray; +@property (nonatomic, readonly) NSArray> *peopleCellDataArray; +@property (nonatomic, readonly) NSArray> *conversationCellDataArray; +@property (nonatomic, readonly) NSArray> *lowPriorityCellDataArray; +@property (nonatomic, readonly) NSArray> *serverNoticeCellDataArray; @property (nonatomic, readonly) SecureBackupBannerDisplay secureBackupBannerDisplay; @property (nonatomic, readonly) CrossSigningBannerDisplay crossSigningBannerDisplay; @@ -160,43 +161,18 @@ extern NSString *const kRecentsDataSourceTapOnDirectoryServerChange; - (void)moveRoomCell:(MXRoom*)room from:(NSIndexPath*)oldPath to:(NSIndexPath*)newPath success:(void (^)(void))moveSuccess failure:(void (^)(NSError *error))moveFailure; /** - The current number of the favourite rooms with missed notifications. + Counts for favorited rooms. */ -@property (nonatomic, readonly) NSUInteger missedFavouriteDiscussionsCount; +@property (nonatomic, readonly) DiscussionsCount *favoriteMissedDiscussionsCount; /** - The current number of the favourite rooms with unread highlighted messages. + Counts for direct rooms. */ -@property (nonatomic, readonly) NSUInteger missedHighlightFavouriteDiscussionsCount; +@property (nonatomic, readonly) DiscussionsCount *directMissedDiscussionsCount; /** - The current number of the direct chats with missed notifications, including the invites. + Counts for group rooms. */ -@property (nonatomic, readonly) NSUInteger missedDirectDiscussionsCount; - -/** - The current number of the direct chats with unread highlighted messages. - */ -@property (nonatomic, readonly) NSUInteger missedHighlightDirectDiscussionsCount; - -/** - The current number of the direct chats with unsent messages. - */ -@property (nonatomic, readonly) NSUInteger unsentMessagesDirectDiscussionsCount; - -/** - The current number of the group chats with missed notifications, including the invites. - */ -@property (nonatomic, readonly) NSUInteger missedGroupDiscussionsCount; - -/** - The current number of the group chats with unread highlighted messages. - */ -@property (nonatomic, readonly) NSUInteger missedHighlightGroupDiscussionsCount; - -/** - The current number of the group chats with unsent messages. - */ -@property (nonatomic, readonly) NSUInteger unsentMessagesGroupDiscussionsCount; +@property (nonatomic, readonly) DiscussionsCount *groupMissedDiscussionsCount; @end diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m index 123d9da69..6c6919bc2 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -24,6 +24,7 @@ #import "MXRoom+Riot.h" #import "MXSession+Riot.h" +#import "NSArray+Element.h" #import "Riot-Swift.h" @@ -39,7 +40,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSourceTapOnDirectoryServerChange"; -@interface RecentsDataSource() +@interface RecentsDataSource() { RecentsDataSourceState *state; dispatch_queue_t processingQueue; @@ -50,6 +51,8 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou // Timer to not refresh publicRoomsDirectoryDataSource on every keystroke. NSTimer *publicRoomsTriggerTimer; + + RecentsRoomListFetchersContainer *fetchersContainer; } @property (nonatomic, assign, readwrite) SecureBackupBannerDisplay secureBackupBannerDisplay; @@ -88,6 +91,16 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou return self; } +- (void)finalizeInitialization +{ + [super finalizeInitialization]; + + fetchersContainer = [[RecentsRoomListFetchersContainer alloc] initWithSession:self.mxSession + mode:_recentsDataSourceMode + query:nil]; + [fetchersContainer addDelegate:self]; +} + - (void)resetSectionIndexes { crossSigningBannerSection = -1; @@ -104,67 +117,77 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou #pragma mark - Properties -- (NSArray *)invitesCellDataArray +- (NSArray> *)invitesCellDataArray { - return state.invitesCellDataArray; + if (!fetchersContainer.shouldShowInvited) + { + return nil; + } + return [self mapRoomSummaries:fetchersContainer.invitedRoomListDataFetcher.data.rooms]; } -- (NSArray *)favoriteCellDataArray +- (NSArray> *)favoriteCellDataArray { - return state.favoriteCellDataArray; + if (!fetchersContainer.shouldShowFavorited) + { + return nil; + } + return [self mapRoomSummaries:fetchersContainer.favoritedRoomListDataFetcher.data.rooms]; } -- (NSArray *)peopleCellDataArray +- (NSArray> *)peopleCellDataArray { - return state.peopleCellDataArray; + if (!fetchersContainer.shouldShowDirect) + { + return nil; + } + return [self mapRoomSummaries:fetchersContainer.directRoomListDataFetcher.data.rooms]; } -- (NSArray *)conversationCellDataArray +- (NSArray> *)conversationCellDataArray { - return state.conversationCellDataArray; + if (!fetchersContainer.shouldShowConversation) + { + return nil; + } + return [self mapRoomSummaries:fetchersContainer.conversationRoomListDataFetcher.data.rooms]; } -- (NSArray *)lowPriorityCellDataArray +- (NSArray> *)lowPriorityCellDataArray { - return state.lowPriorityCellDataArray; + if (!fetchersContainer.shouldShowLowPriority) + { + return nil; + } + return [self mapRoomSummaries:fetchersContainer.lowPriorityRoomListDataFetcher.data.rooms]; } -- (NSArray *)serverNoticeCellDataArray +- (NSArray> *)serverNoticeCellDataArray { - return state.serverNoticeCellDataArray; + if (!fetchersContainer.shouldShowServerNotice) + { + return nil; + } + return [self mapRoomSummaries:fetchersContainer.serverNoticeRoomListDataFetcher.data.rooms]; } -- (NSUInteger)missedFavouriteDiscussionsCount +- (DiscussionsCount *)favoriteMissedDiscussionsCount { - return state.favouriteMissedDiscussionsCount.count; -} -- (NSUInteger)missedHighlightFavouriteDiscussionsCount -{ - return state.favouriteMissedDiscussionsCount.highlightCount; + return fetchersContainer.favoritedMissedDiscussionsCount; } -- (NSUInteger)missedDirectDiscussionsCount +- (DiscussionsCount *)directMissedDiscussionsCount { - return state.directMissedDiscussionsCount.count; -} -- (NSUInteger)missedHighlightDirectDiscussionsCount -{ - return state.directMissedDiscussionsCount.highlightCount; + return fetchersContainer.directMissedDiscussionsCount; } -- (NSUInteger)missedGroupDiscussionsCount +- (DiscussionsCount *)groupMissedDiscussionsCount { - return state.groupMissedDiscussionsCount.count; -} -- (NSUInteger)groupMissedDiscussionsCount -{ - return state.favouriteMissedDiscussionsCount.highlightCount; + return fetchersContainer.conversationMissedDiscussionsCount; } -- (NSUInteger)unsentMessagesDirectDiscussionsCount +- (NSArray> *)mapRoomSummaries:(NSArray> *)summaries { - return state.unsentMessagesDirectDiscussionsCount; + return [summaries vc_map:^id _Nonnull(id _Nonnull summary) { + return [[MXKRecentCellData alloc] initWithRoomSummary:summary + dataSource:self]; + }]; } -- (NSUInteger)unsentMessagesGroupDiscussionsCount -{ - return state.unsentMessagesGroupDiscussionsCount; -} - #pragma mark - @@ -190,8 +213,8 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou } [self updateSecureBackupBanner]; - [self forceRefresh]; [self refreshCrossSigningBannerDisplay]; + [fetchersContainer updateMode:_recentsDataSourceMode]; } - (UIView *)viewForStickyHeaderInSection:(NSInteger)section withFrame:(CGRect)frame @@ -418,10 +441,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou // Refresh is disabled during drag&drop animation" if (!self.droppingCellIndexPath) { - [self refreshRoomsSection:^{ - // And inform the delegate about the update - [self.delegate dataSource:self didCellChange:nil]; - }]; + [fetchersContainer refresh]; } } @@ -471,13 +491,13 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou favoritesSection = sectionsCount++; } - if (_recentsDataSourceMode == RecentsDataSourceModeHome) + if (self.peopleCellDataArray.count > 0 || _recentsDataSourceMode == RecentsDataSourceModeHome) { peopleSection = sectionsCount++; } // Keep visible the main rooms section even if it is empty, except on favourites screen. - if (_recentsDataSourceMode != RecentsDataSourceModeFavourites) + if (self.conversationCellDataArray.count > 0 || _recentsDataSourceMode == RecentsDataSourceModeHome) { conversationSection = sectionsCount++; } @@ -651,57 +671,56 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou - (UIView *)badgeViewForHeaderTitleInHomeSection:(NSInteger)section { // Prepare a badge to display the total of missed notifications in this section. - NSUInteger count = 0; - NSArray *sectionArray; + NSUInteger totalNotificationCount = 0; + NSUInteger totalHighlightCount = 0; UIView *missedNotifAndUnreadBadgeBgView = nil; if (section == favoritesSection) { - sectionArray = self.favoriteCellDataArray; + totalNotificationCount = fetchersContainer.favoritedRoomListDataFetcher.data.counts.totalNotificationCount; + totalHighlightCount = fetchersContainer.favoritedRoomListDataFetcher.data.counts.totalHighlightCount; } else if (section == peopleSection) { - sectionArray = self.peopleCellDataArray; + totalNotificationCount = fetchersContainer.directRoomListDataFetcher.data.counts.totalNotificationCount; + totalHighlightCount = fetchersContainer.directRoomListDataFetcher.data.counts.totalHighlightCount; } else if (section == conversationSection) { - sectionArray = self.conversationCellDataArray; + totalNotificationCount = fetchersContainer.conversationRoomListDataFetcher.data.counts.totalNotificationCount; + totalHighlightCount = fetchersContainer.conversationRoomListDataFetcher.data.counts.totalHighlightCount; } else if (section == lowPrioritySection) { - sectionArray = self.lowPriorityCellDataArray; + totalNotificationCount = fetchersContainer.lowPriorityRoomListDataFetcher.data.counts.totalNotificationCount; + totalHighlightCount = fetchersContainer.lowPriorityRoomListDataFetcher.data.counts.totalHighlightCount; } else if (section == serverNoticeSection) { - sectionArray = self.serverNoticeCellDataArray; + totalNotificationCount = fetchersContainer.serverNoticeRoomListDataFetcher.data.counts.totalNotificationCount; + totalHighlightCount = fetchersContainer.serverNoticeRoomListDataFetcher.data.counts.totalHighlightCount; } - BOOL highlight = NO; - for (id cellData in sectionArray) - { - count += cellData.notificationCount; - highlight |= (cellData.highlightCount > 0); - } - - if (count) + if (totalNotificationCount) { UILabel *missedNotifAndUnreadBadgeLabel = [[UILabel alloc] init]; missedNotifAndUnreadBadgeLabel.textColor = ThemeService.shared.theme.baseTextPrimaryColor; missedNotifAndUnreadBadgeLabel.font = [UIFont boldSystemFontOfSize:14]; - if (count > 1000) + if (totalNotificationCount > 1000) { - CGFloat value = count / 1000.0; + CGFloat value = totalNotificationCount / 1000.0; missedNotifAndUnreadBadgeLabel.text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"large_badge_value_k_format", @"Vector", nil), value]; } else { - missedNotifAndUnreadBadgeLabel.text = [NSString stringWithFormat:@"%tu", count]; + missedNotifAndUnreadBadgeLabel.text = [NSString stringWithFormat:@"%tu", totalNotificationCount]; } [missedNotifAndUnreadBadgeLabel sizeToFit]; CGFloat bgViewWidth = missedNotifAndUnreadBadgeLabel.frame.size.width + 18; + BOOL highlight = totalHighlightCount > 0; missedNotifAndUnreadBadgeBgView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, bgViewWidth, 20)]; [missedNotifAndUnreadBadgeBgView.layer setCornerRadius:10]; missedNotifAndUnreadBadgeBgView.backgroundColor = highlight ? ThemeService.shared.theme.noticeColor : ThemeService.shared.theme.noticeSecondaryColor; @@ -1013,7 +1032,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou { id cellDataStoring = cellDataArray[index]; - if ([roomId isEqualToString:cellDataStoring.roomSummary.roomId] && (matrixSession == cellDataStoring.roomSummary.room.mxSession)) + if ([roomId isEqualToString:cellDataStoring.roomSummary.roomId] && cellDataStoring.mxSession == matrixSession) { return index; } @@ -1121,389 +1140,8 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou return indexPath; } - #pragma mark - MXKDataSourceDelegate -- (void)refreshRoomsSection:(void (^)(void))onComplete -{ - if (displayedRecentsDataSourceArray.count > 0) - { - // FIXME manage multi accounts - MXKSessionRecentsDataSource *recentsDataSource = displayedRecentsDataSourceArray[0]; - - NSMutableArray> *cells = [NSMutableArray new]; - NSInteger count = recentsDataSource.numberOfCells; - - for (NSUInteger index = 0; index < count; index++) - { - id cell = [recentsDataSource cellDataAtIndex:index]; - [cells addObject:cell]; - } - - MXWeakify(self); - [self computeStateAsyncWithCells:cells recentsDataSourceMode:self.recentsDataSourceMode matrixSession:recentsDataSource.mxSession onComplete:^(RecentsDataSourceState *newState) { - MXStrongifyAndReturnIfNil(self); - - self->state = newState; - onComplete(); - }]; - } - else - { - onComplete(); - } -} - -- (void)computeStateAsyncWithCells:(NSArray> *)cells - recentsDataSourceMode:(RecentsDataSourceMode)recentsDataSourceMode - matrixSession:(MXSession*)mxSession - onComplete:(void (^)(RecentsDataSourceState *newState))onComplete -{ - dispatch_async(processingQueue, ^{ - RecentsDataSourceState *newState = [RecentsDataSource computeStateWithCells:cells recentsDataSourceMode:recentsDataSourceMode matrixSession:mxSession]; - dispatch_async(dispatch_get_main_queue(), ^{ - onComplete(newState); - }); - }); -} - -+ (RecentsDataSourceState *)computeStateWithCells:(NSArray> *)cells - recentsDataSourceMode:(RecentsDataSourceMode)recentsDataSourceMode - matrixSession:(MXSession*)mxSession -{ - NSDate *startDate = [NSDate date]; - - NSMutableArray> *invitesCellDataArray = [NSMutableArray new]; - NSMutableArray> *favoriteCellDataArray = [NSMutableArray new]; - NSMutableArray> *peopleCellDataArray = [NSMutableArray new]; - NSMutableArray> *conversationCellDataArray = [NSMutableArray new]; - NSMutableArray> *lowPriorityCellDataArray = [NSMutableArray new]; - NSMutableArray> *serverNoticeCellDataArray = [NSMutableArray new]; - - MissedDiscussionsCount *favouriteMissedDiscussionsCount = [MissedDiscussionsCount new]; - MissedDiscussionsCount *directMissedDiscussionsCount = [MissedDiscussionsCount new]; - MissedDiscussionsCount *groupMissedDiscussionsCount = [MissedDiscussionsCount new]; - NSUInteger unsentMessagesDirectDiscussionsCount = 0; - NSUInteger unsentMessagesGroupDiscussionsCount = 0; - - for (id recentCellDataStoring in cells) - { - MXRoom* room = recentCellDataStoring.roomSummary.room; - - if (recentsDataSourceMode == RecentsDataSourceModeHome) - { - if (room.accountData.tags[kMXRoomTagServerNotice]) - { - [serverNoticeCellDataArray addObject:recentCellDataStoring]; - } - else if (room.accountData.tags[kMXRoomTagFavourite]) - { - [favoriteCellDataArray addObject:recentCellDataStoring]; - } - else if (room.accountData.tags[kMXRoomTagLowPriority]) - { - [lowPriorityCellDataArray addObject:recentCellDataStoring]; - } - else if (room.summary.membership == MXMembershipInvite) - { - if (!MXSDKOptions.sharedInstance.autoAcceptRoomInvites) - { - [invitesCellDataArray addObject:recentCellDataStoring]; - } - } - else if (room.isDirect) - { - [peopleCellDataArray addObject:recentCellDataStoring]; - } - else - { - // Hide spaces from home (keep space invites) - if (room.summary.roomType != MXRoomTypeSpace) - { - [conversationCellDataArray addObject:recentCellDataStoring]; - } - } - } - else if (recentsDataSourceMode == RecentsDataSourceModeFavourites) - { - // Keep only the favourites rooms. - if (room.accountData.tags[kMXRoomTagFavourite]) - { - [favoriteCellDataArray addObject:recentCellDataStoring]; - } - } - else if (recentsDataSourceMode == RecentsDataSourceModePeople) - { - // Keep only the direct rooms which are not low priority - if (room.isDirect && !room.accountData.tags[kMXRoomTagLowPriority]) - { - if (room.summary.membership == MXMembershipInvite) - { - if (!MXSDKOptions.sharedInstance.autoAcceptRoomInvites) - { - [invitesCellDataArray addObject:recentCellDataStoring]; - } - - } - else - { - [conversationCellDataArray addObject:recentCellDataStoring]; - } - } - } - else if (recentsDataSourceMode == RecentsDataSourceModeRooms) - { - // Consider only non direct rooms. - if (!room.isDirect) - { - // Keep only the invites, the favourites and the rooms without tag and room type different from space - if (room.summary.membership == MXMembershipInvite) - { - if (!MXSDKOptions.sharedInstance.autoAcceptRoomInvites) - { - [invitesCellDataArray addObject:recentCellDataStoring]; - } - } - else if ((!room.accountData.tags.count || room.accountData.tags[kMXRoomTagFavourite]) && room.summary.roomType != MXRoomTypeSpace) - { - [conversationCellDataArray addObject:recentCellDataStoring]; - } - } - } - - // Update missed conversations counts - NSUInteger notificationCount = recentCellDataStoring.roomSummary.notificationCount; - - // Ignore the regular notification count if the room is in 'mentions only" mode at the Riot level. - if (room.isMentionsOnly) - { - // Only the highlighted missed messages must be considered here. - notificationCount = recentCellDataStoring.roomSummary.highlightCount; - } - - if (notificationCount) - { - if (room.accountData.tags[kMXRoomTagFavourite]) - { - favouriteMissedDiscussionsCount.count ++; - - if (recentCellDataStoring.roomSummary.highlightCount) - { - favouriteMissedDiscussionsCount.highlightCount ++; - } - } - - if (room.isDirect) - { - directMissedDiscussionsCount.count ++; - - if (recentCellDataStoring.roomSummary.highlightCount) - { - directMissedDiscussionsCount.highlightCount ++; - } - } - else if (!room.accountData.tags.count || room.accountData.tags[kMXRoomTagFavourite]) - { - groupMissedDiscussionsCount.count ++; - - if (recentCellDataStoring.roomSummary.highlightCount) - { - groupMissedDiscussionsCount.highlightCount ++; - } - } - } - else if (room.summary.membership == MXMembershipInvite) - { - if (room.isDirect) - { - directMissedDiscussionsCount.count ++; - } - else - { - groupMissedDiscussionsCount.highlightCount ++; - } - } - - if (room.sentStatus != RoomSentStatusOk) - { - if (room.isDirect) - { - unsentMessagesDirectDiscussionsCount ++; - } - else - { - unsentMessagesGroupDiscussionsCount ++; - } - } - } - - if (recentsDataSourceMode == RecentsDataSourceModeHome) - { - BOOL pinMissedNotif = RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome; - BOOL pinUnread = RiotSettings.shared.pinRoomsWithUnreadMessagesOnHome; - NSComparator comparator = nil; - - if (pinMissedNotif) - { - // Sort each rooms collection by considering first the rooms with some missed notifs, the rooms with unread, then the others. - comparator = ^NSComparisonResult(id recentCellData1, id recentCellData2) { - - if (recentCellData1.roomSummary.room.sentStatus != RoomSentStatusOk - && recentCellData2.roomSummary.room.sentStatus == RoomSentStatusOk) - { - return NSOrderedAscending; - } - - if (recentCellData2.roomSummary.room.sentStatus != RoomSentStatusOk - && recentCellData1.roomSummary.room.sentStatus == RoomSentStatusOk) - { - return NSOrderedDescending; - } - - if (recentCellData1.highlightCount) - { - if (recentCellData2.highlightCount) - { - return NSOrderedSame; - } - else - { - return NSOrderedAscending; - } - } - else if (recentCellData2.highlightCount) - { - return NSOrderedDescending; - } - else if (recentCellData1.notificationCount) - { - if (recentCellData2.notificationCount) - { - return NSOrderedSame; - } - else - { - return NSOrderedAscending; - } - } - else if (recentCellData2.notificationCount) - { - return NSOrderedDescending; - } - else if (pinUnread) - { - if (recentCellData1.hasUnread) - { - if (recentCellData2.hasUnread) - { - return NSOrderedSame; - } - else - { - return NSOrderedAscending; - } - } - else if (recentCellData2.hasUnread) - { - return NSOrderedDescending; - } - } - - return NSOrderedSame; - }; - } - else if (pinUnread) - { - // Sort each rooms collection by considering first the rooms with some unread messages then the others. - comparator = ^NSComparisonResult(id recentCellData1, id recentCellData2) { - - if (recentCellData1.roomSummary.room.sentStatus != RoomSentStatusOk - && recentCellData2.roomSummary.room.sentStatus == RoomSentStatusOk) - { - return NSOrderedAscending; - } - - if (recentCellData2.roomSummary.room.sentStatus != RoomSentStatusOk - && recentCellData1.roomSummary.room.sentStatus == RoomSentStatusOk) - { - return NSOrderedDescending; - } - - if (recentCellData1.hasUnread) - { - if (recentCellData2.hasUnread) - { - return NSOrderedSame; - } - else - { - return NSOrderedAscending; - } - } - else if (recentCellData2.hasUnread) - { - return NSOrderedDescending; - } - - return NSOrderedSame; - }; - } - - if (comparator) - { - // Sort the rooms collections - [favoriteCellDataArray sortUsingComparator:comparator]; - [peopleCellDataArray sortUsingComparator:comparator]; - [conversationCellDataArray sortUsingComparator:comparator]; - [lowPriorityCellDataArray sortUsingComparator:comparator]; - [serverNoticeCellDataArray sortUsingComparator:comparator]; - } - } - else if (favoriteCellDataArray.count > 0 && recentsDataSourceMode == RecentsDataSourceModeFavourites) - { - // Sort them according to their tag order - [favoriteCellDataArray sortUsingComparator:^NSComparisonResult(id recentCellData1, id recentCellData2) { - - return [mxSession compareRoomsByTag:kMXRoomTagFavourite room1:recentCellData1.roomSummary.room room2:recentCellData2.roomSummary.room]; - - }]; - } - else if (conversationCellDataArray.count > 0 && (recentsDataSourceMode == RecentsDataSourceModeRooms || recentsDataSourceMode == RecentsDataSourceModePeople)) - { - [conversationCellDataArray sortUsingComparator:^NSComparisonResult(id recentCellData1, id recentCellData2) { - - if (recentCellData1.roomSummary.room.sentStatus != RoomSentStatusOk - && recentCellData2.roomSummary.room.sentStatus == RoomSentStatusOk) - { - return NSOrderedAscending; - } - - if (recentCellData2.roomSummary.room.sentStatus != RoomSentStatusOk - && recentCellData1.roomSummary.room.sentStatus == RoomSentStatusOk) - { - return NSOrderedDescending; - } - - return NSOrderedAscending; - }]; - } - - MXLogDebug(@"[RecentsDataSource] refreshRoomsSections: Done in %.0fms", [[NSDate date] timeIntervalSinceDate:startDate] * 1000); - - return [[RecentsDataSourceState alloc] - initWithInvitesCellDataArray:invitesCellDataArray - favoriteCellDataArray:favoriteCellDataArray - peopleCellDataArray:peopleCellDataArray - conversationCellDataArray:conversationCellDataArray - lowPriorityCellDataArray:lowPriorityCellDataArray - serverNoticeCellDataArray:serverNoticeCellDataArray - favouriteMissedDiscussionsCount:favouriteMissedDiscussionsCount - directMissedDiscussionsCount:directMissedDiscussionsCount - groupMissedDiscussionsCount:groupMissedDiscussionsCount - unsentMessagesDirectDiscussionsCount:unsentMessagesDirectDiscussionsCount - unsentMessagesGroupDiscussionsCount:unsentMessagesGroupDiscussionsCount]; -} - - (void)dataSource:(MXKDataSource*)dataSource didCellChange:(id)changes { // Refresh is disabled during drag&drop animation @@ -1523,12 +1161,8 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou // 1 - call [super thisNewMethod] // 2 - call [self refreshRoomsSections] - // refresh the sections - [self refreshRoomsSection:^{ - // Call super to keep update readyRecentsDataSourceArray. - [super dataSource:dataSource didCellChange:changes]; - }]; - + // Call super to keep update readyRecentsDataSourceArray. + [super dataSource:dataSource didCellChange:changes]; } #pragma mark - Drag & Drop handling @@ -1597,6 +1231,8 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou [publicRoomsTriggerTimer invalidate]; publicRoomsTriggerTimer = nil; + + [fetchersContainer stop]; } #pragma mark - Override MXKRecentsDataSource @@ -1604,11 +1240,13 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou - (void)searchWithPatterns:(NSArray *)patternsList { [super searchWithPatterns:patternsList]; + + NSString *searchPattern = [patternsList componentsJoinedByString:@" "]; + + [fetchersContainer updateQuery:searchPattern]; if (_publicRoomsDirectoryDataSource) { - NSString *searchPattern = [patternsList componentsJoinedByString:@" "]; - // Do not send a /publicRooms request for every keystroke // Let user finish typing [publicRoomsTriggerTimer invalidate]; @@ -1755,4 +1393,12 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou [self hideCrossSigningBannerWithDisplay:self.crossSigningBannerDisplay]; } +#pragma mark - MXRoomListDataFetcherDelegate + +- (void)fetcherDidChangeData:(id)fetcher +{ + // TODO: Update only updated sections + [self.delegate dataSource:self didCellChange:nil]; +} + @end diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSourceState.swift b/Riot/Modules/Common/Recents/DataSources/RecentsDataSourceState.swift index 5f7b376dd..510cebebe 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSourceState.swift +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSourceState.swift @@ -30,15 +30,10 @@ class RecentsDataSourceState: NSObject { let lowPriorityCellDataArray: [MXKRecentCellDataStoring] let serverNoticeCellDataArray: [MXKRecentCellDataStoring] - // MARK: Notifications counts - let favouriteMissedDiscussionsCount: MissedDiscussionsCount - let directMissedDiscussionsCount: MissedDiscussionsCount - let groupMissedDiscussionsCount: MissedDiscussionsCount - - // MARK: Unsent counts - let unsentMessagesDirectDiscussionsCount: UInt - let unsentMessagesGroupDiscussionsCount: UInt - + // MARK: Discussion counts + let favouriteMissedDiscussionsCount: DiscussionsCount + let directMissedDiscussionsCount: DiscussionsCount + let groupMissedDiscussionsCount: DiscussionsCount // MARK: - Setup init(invitesCellDataArray: [MXKRecentCellDataStoring], @@ -47,11 +42,9 @@ class RecentsDataSourceState: NSObject { conversationCellDataArray: [MXKRecentCellDataStoring], lowPriorityCellDataArray: [MXKRecentCellDataStoring], serverNoticeCellDataArray: [MXKRecentCellDataStoring], - favouriteMissedDiscussionsCount: MissedDiscussionsCount, - directMissedDiscussionsCount: MissedDiscussionsCount, - groupMissedDiscussionsCount: MissedDiscussionsCount, - unsentMessagesDirectDiscussionsCount: UInt, - unsentMessagesGroupDiscussionsCount: UInt) { + favouriteMissedDiscussionsCount: DiscussionsCount, + directMissedDiscussionsCount: DiscussionsCount, + groupMissedDiscussionsCount: DiscussionsCount) { self.invitesCellDataArray = invitesCellDataArray self.favoriteCellDataArray = favoriteCellDataArray self.peopleCellDataArray = peopleCellDataArray @@ -61,19 +54,6 @@ class RecentsDataSourceState: NSObject { self.favouriteMissedDiscussionsCount = favouriteMissedDiscussionsCount self.directMissedDiscussionsCount = directMissedDiscussionsCount self.groupMissedDiscussionsCount = groupMissedDiscussionsCount - self.unsentMessagesDirectDiscussionsCount = unsentMessagesDirectDiscussionsCount - self.unsentMessagesGroupDiscussionsCount = unsentMessagesGroupDiscussionsCount super.init() } } - - -/// Noticiations counts per section -@objcMembers -class MissedDiscussionsCount: NSObject { - /// Regular notifications - var count: UInt = 0 - - /// Mentions like notications - var highlightCount: UInt = 0 -} diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsRoomListContainer.swift b/Riot/Modules/Common/Recents/DataSources/RecentsRoomListContainer.swift new file mode 100644 index 000000000..396f41e5b --- /dev/null +++ b/Riot/Modules/Common/Recents/DataSources/RecentsRoomListContainer.swift @@ -0,0 +1,430 @@ +// +// 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 + +@objcMembers +public class RecentsRoomListFetchersContainer: NSObject { + + public let session: MXSession + public private(set) var mode: RecentsDataSourceMode + public private(set) var query: String? + + // MARK: - Fetchers + + public private(set) var invitedRoomListDataFetcher: MXRoomListDataFetcher? + public private(set) var favoritedRoomListDataFetcher: MXRoomListDataFetcher? + public var directRoomListDataFetcher: MXRoomListDataFetcher? { + switch mode { + case .home: + return directRoomListDataFetcherForHome + case .people: + return directRoomListDataFetcherForPeople + default: + return nil + } + } + public var conversationRoomListDataFetcher: MXRoomListDataFetcher? { + switch mode { + case .home: + return conversationRoomListDataFetcherForHome + case .rooms: + return conversationRoomListDataFetcherForRooms + default: + return nil + } + } + public private(set) var lowPriorityRoomListDataFetcher: MXRoomListDataFetcher? + public private(set) var serverNoticeRoomListDataFetcher: MXRoomListDataFetcher? + + private var conversationRoomListDataFetcherForHome: MXRoomListDataFetcher? + private var conversationRoomListDataFetcherForRooms: MXRoomListDataFetcher? + private var directRoomListDataFetcherForHome: MXRoomListDataFetcher? + private var directRoomListDataFetcherForPeople: MXRoomListDataFetcher? + + // MARK: - Private + + private var fetcherTypesForMode: [RecentsDataSourceMode: FetcherTypes] = [ + .home: [.invited, .favorited, .directHome, .conversationHome, .lowPriority, .serverNotice], + .favourites: [.favorited], + .people: [.directPeople], + .rooms: [.conversationRooms] + ] + + private var allFetchers: [MXRoomListDataFetcher] { + var result: [MXRoomListDataFetcher] = [] + if let fetcher = invitedRoomListDataFetcher { + result.append(fetcher) + } + if let fetcher = favoritedRoomListDataFetcher { + result.append(fetcher) + } + if let fetcher = directRoomListDataFetcherForHome { + result.append(fetcher) + } + if let fetcher = directRoomListDataFetcherForPeople { + result.append(fetcher) + } + if let fetcher = conversationRoomListDataFetcherForHome { + result.append(fetcher) + } + if let fetcher = conversationRoomListDataFetcherForRooms { + result.append(fetcher) + } + if let fetcher = lowPriorityRoomListDataFetcher { + result.append(fetcher) + } + if let fetcher = serverNoticeRoomListDataFetcher { + result.append(fetcher) + } + return result + } + + private var hideInvitedSection: Bool { + return MXSDKOptions.sharedInstance().autoAcceptRoomInvites + } + + private var visibleFetchers: [MXRoomListDataFetcher] { + guard let fetcherTypes = fetcherTypesForMode[mode] else { + return [] + } + var result: [MXRoomListDataFetcher] = [] + if let fetcher = invitedRoomListDataFetcher, fetcherTypes.contains(.invited) { + result.append(fetcher) + } + if let fetcher = favoritedRoomListDataFetcher, fetcherTypes.contains(.favorited) { + result.append(fetcher) + } + if let fetcher = directRoomListDataFetcherForHome, fetcherTypes.contains(.directHome) { + result.append(fetcher) + } + if let fetcher = directRoomListDataFetcherForPeople, fetcherTypes.contains(.directPeople) { + result.append(fetcher) + } + if let fetcher = conversationRoomListDataFetcherForHome, fetcherTypes.contains(.conversationHome) { + result.append(fetcher) + } + if let fetcher = conversationRoomListDataFetcherForRooms, fetcherTypes.contains(.conversationRooms) { + result.append(fetcher) + } + if let fetcher = lowPriorityRoomListDataFetcher, fetcherTypes.contains(.lowPriority) { + result.append(fetcher) + } + if let fetcher = serverNoticeRoomListDataFetcher, fetcherTypes.contains(.serverNotice) { + result.append(fetcher) + } + return result + } + + // swiftlint:disable weak_delegate + private let multicastDelegate: MXMulticastDelegate = MXMulticastDelegate() + // swiftlint:enable weak_delegate + + private var sortOptions: MXRoomListDataSortOptions { + switch mode { + case .home: + let pinMissed = RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome + let pinUnread = RiotSettings.shared.pinRoomsWithUnreadMessagesOnHome + return MXRoomListDataSortOptions(missedNotificationsFirst: pinMissed, + unreadMessagesFirst: pinUnread) + default: + return MXRoomListDataSortOptions(missedNotificationsFirst: false, + unreadMessagesFirst: false) + } + } + + // MARK: - Public API + + public init(withSession session: MXSession, + mode: RecentsDataSourceMode = .home, + query: String? = nil) { + self.session = session + self.mode = mode + self.query = query + super.init() + createFetchers() + addRiotSettingsObserver() + } + + public var favoritedMissedDiscussionsCount: DiscussionsCount { + guard let data = favoritedRoomListDataFetcher?.data else { + return .zero + } + return DiscussionsCount(withRoomListDataCounts: data.counts) + } + + public var directMissedDiscussionsCount: DiscussionsCount { + guard let data = directRoomListDataFetcherForPeople?.data else { + return .zero + } + return DiscussionsCount(withRoomListDataCounts: data.counts) + } + + public var conversationMissedDiscussionsCount: DiscussionsCount { + guard let data = conversationRoomListDataFetcherForRooms?.data else { + return .zero + } + return DiscussionsCount(withRoomListDataCounts: data.counts) + } + + public var shouldShowInvited: Bool { + return fetcherTypesForMode[mode]?.contains(.invited) ?? false + } + + public var shouldShowFavorited: Bool { + return fetcherTypesForMode[mode]?.contains(.favorited) ?? false + } + + public var shouldShowDirect: Bool { + switch mode { + case .home: + return fetcherTypesForMode[mode]?.contains(.directHome) ?? false + case .people: + return fetcherTypesForMode[mode]?.contains(.directPeople) ?? false + default: + return false + } + } + + public var shouldShowConversation: Bool { + switch mode { + case .home: + return fetcherTypesForMode[mode]?.contains(.conversationHome) ?? false + case .rooms: + return fetcherTypesForMode[mode]?.contains(.conversationRooms) ?? false + default: + return false + } + } + + public var shouldShowLowPriority: Bool { + return fetcherTypesForMode[mode]?.contains(.lowPriority) ?? false + } + + public var shouldShowServerNotice: Bool { + return fetcherTypesForMode[mode]?.contains(.serverNotice) ?? false + } + + public func updateMode(_ mode: RecentsDataSourceMode) { + self.mode = mode + if let fetcher = favoritedRoomListDataFetcher { + updateFavoritedFetcher(fetcher, for: mode) + } + allFetchers.forEach({ notifyDataChange(on: $0) }) + } + + public func updateQuery(_ query: String?) { + self.query = query + visibleFetchers.forEach({ $0.fetchOptions.filterOptions.query = query }) + } + + public func refresh() { + allFetchers.forEach({ $0.fetchOptions.sortOptions = sortOptions }) + } + + public func stop() { + removeRiotSettingsObserver() + removeAllDelegates() + allFetchers.forEach({ $0.stop() }) + } + + // MARK: - Delegate + + public func addDelegate(_ delegate: MXRoomListDataFetcherDelegate) { + multicastDelegate.addDelegate(delegate) + } + + public func removeDelegate(_ delegate: MXRoomListDataFetcherDelegate) { + multicastDelegate.removeDelegate(delegate) + } + + public func removeAllDelegates() { + multicastDelegate.removeAllDelegates() + } + + // MARK: - Private + + private func addRiotSettingsObserver() { + NotificationCenter.default.addObserver(self, + selector: #selector(userDefaultsUpdated(_:)), + name: .userDefaultValueUpdated, + object: nil) + } + + private func removeRiotSettingsObserver() { + NotificationCenter.default.removeObserver(self, + name: .userDefaultValueUpdated, + object: nil) + } + + @objc + private func userDefaultsUpdated(_ notification: Notification) { + guard let key = notification.object as? String else { + return + } + switch key { + case RiotSettings.UserDefaultsKeys.pinRoomsWithMissedNotificationsOnHome, + RiotSettings.UserDefaultsKeys.pinRoomsWithUnreadMessagesOnHome: + refresh() + default: + break + } + } + + private func createCommonRoomListDataFetcher(withDataTypes dataTypes: MXRoomSummaryDataTypes, + paginate: Bool = true) -> MXRoomListDataFetcher { + let filterOptions = MXRoomListDataFilterOptions(dataTypes: dataTypes, + query: query) + + let fetchOptions = MXRoomListDataFetchOptions(filterOptions: filterOptions, + sortOptions: sortOptions, + async: false) + let fetcher = session.roomListDataManager.fetcher(withOptions: fetchOptions) + if paginate { + fetcher.addDelegate(self) + fetcher.paginate() + } + return fetcher + } + + private func createDirectRoomListDataFetcherForHome() -> MXRoomListDataFetcher? { + let fetcher = createCommonRoomListDataFetcher(withDataTypes: [.direct], paginate: false) + updateDirectFetcher(fetcher, for: .home) + fetcher.addDelegate(self) + fetcher.paginate() + return fetcher + } + + private func createDirectRoomListDataFetcherForPeople() -> MXRoomListDataFetcher? { + let fetcher = createCommonRoomListDataFetcher(withDataTypes: [.direct], paginate: false) + updateDirectFetcher(fetcher, for: .people) + fetcher.addDelegate(self) + fetcher.paginate() + return fetcher + } + + private func createConversationRoomListDataFetcherForHome() -> MXRoomListDataFetcher? { + let fetcher = createCommonRoomListDataFetcher(withDataTypes: [], paginate: false) + updateConversationFetcher(fetcher, for: .home) + fetcher.addDelegate(self) + fetcher.paginate() + return fetcher + } + + private func createConversationRoomListDataFetcherForRooms() -> MXRoomListDataFetcher? { + let fetcher = createCommonRoomListDataFetcher(withDataTypes: [], paginate: false) + updateConversationFetcher(fetcher, for: .rooms) + fetcher.addDelegate(self) + fetcher.paginate() + return fetcher + } + + private func createFetchers() { + if !hideInvitedSection { + invitedRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.invited]) + } + favoritedRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.favorited]) + directRoomListDataFetcherForHome = createDirectRoomListDataFetcherForHome() + directRoomListDataFetcherForPeople = createDirectRoomListDataFetcherForPeople() + conversationRoomListDataFetcherForHome = createConversationRoomListDataFetcherForHome() + conversationRoomListDataFetcherForRooms = createConversationRoomListDataFetcherForRooms() + lowPriorityRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.lowPriority]) + serverNoticeRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.serverNotice]) + } + + private func updateDirectFetcher(_ fetcher: MXRoomListDataFetcher, for mode: RecentsDataSourceMode) { + switch mode { + case .home: + fetcher.fetchOptions.filterOptions.notDataTypes = [.invited, .lowPriority] + case .people: + fetcher.fetchOptions.filterOptions.notDataTypes = [.lowPriority] + default: + break + } + } + + private func updateFavoritedFetcher(_ fetcher: MXRoomListDataFetcher, for mode: RecentsDataSourceMode) { + switch mode { + case .home: + fetcher.fetchOptions.sortOptions = sortOptions + case .favourites: + let newSortOptions = sortOptions + newSortOptions.favoriteTag = true + fetcher.fetchOptions.sortOptions = newSortOptions + default: + break + } + } + + private func updateConversationFetcher(_ fetcher: MXRoomListDataFetcher, for mode: RecentsDataSourceMode) { + var notDataTypes: MXRoomSummaryDataTypes = [.hidden, .conferenceUser, .direct, .lowPriority, .serverNotice] + switch mode { + case .home: + notDataTypes.insert([.invited, .favorited]) + fetcher.fetchOptions.filterOptions.notDataTypes = notDataTypes + case .rooms: + if hideInvitedSection { + notDataTypes.insert(.invited) + } + fetcher.fetchOptions.filterOptions.notDataTypes = notDataTypes + default: + break + } + } + + private func notifyDataChange(on fetcher: MXRoomListDataFetcher) { + multicastDelegate.invoke(invocation: { $0.fetcherDidChangeData(fetcher) }) + } + + deinit { + stop() + } + +} + +// MARK: - MXRoomListDataFetcherDelegate + +extension RecentsRoomListFetchersContainer: MXRoomListDataFetcherDelegate { + + public func fetcherDidChangeData(_ fetcher: MXRoomListDataFetcher) { + notifyDataChange(on: fetcher) + } + +} + +// MARK: - FetcherTypes + +private struct FetcherTypes: OptionSet { + typealias RawValue = Int + let rawValue: RawValue + + init(rawValue: RawValue) { + self.rawValue = rawValue + } + + static let invited = FetcherTypes(rawValue: 1 << 0) + static let favorited = FetcherTypes(rawValue: 1 << 1) + static let directHome = FetcherTypes(rawValue: 1 << 2) + static let directPeople = FetcherTypes(rawValue: 1 << 3) + static let conversationHome = FetcherTypes(rawValue: 1 << 4) + static let conversationRooms = FetcherTypes(rawValue: 1 << 5) + static let lowPriority = FetcherTypes(rawValue: 1 << 6) + static let serverNotice = FetcherTypes(rawValue: 1 << 7) + + static let none: FetcherTypes = [] + static let all: FetcherTypes = [ + .invited, .favorited, .directHome, .directPeople, .conversationHome, .conversationRooms, .lowPriority, .serverNotice] +} diff --git a/Riot/Modules/People/PeopleViewController.m b/Riot/Modules/People/PeopleViewController.m index 26b0ca3f0..b4e501b02 100644 --- a/Riot/Modules/People/PeopleViewController.m +++ b/Riot/Modules/People/PeopleViewController.m @@ -169,7 +169,7 @@ - (NSUInteger)totalItemCounts { return recentsDataSource.invitesCellDataArray.count - + recentsDataSource.conversationCellDataArray.count; + + recentsDataSource.peopleCellDataArray.count; } @end diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m index 8d93f7f78..910e350ac 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -352,6 +352,8 @@ } } } + + [recentsDataSource finalizeInitialization]; } } @@ -907,15 +909,15 @@ // Use a middle dot to signal missed notif in favourites if (RiotSettings.shared.homeScreenShowFavouritesTab) { - [self setMissedDiscussionsMark:(recentsDataSource.missedFavouriteDiscussionsCount? @"\u00B7": nil) + [self setMissedDiscussionsMark:(recentsDataSource.favoriteMissedDiscussionsCount.numberOfNotified ? @"\u00B7": nil) onTabBarItem:TABBAR_FAVOURITES_INDEX - withBadgeColor:(recentsDataSource.missedHighlightFavouriteDiscussionsCount ? ThemeService.shared.theme.noticeColor : ThemeService.shared.theme.noticeSecondaryColor)]; + withBadgeColor:(recentsDataSource.favoriteMissedDiscussionsCount.hasHighlight ? ThemeService.shared.theme.noticeColor : ThemeService.shared.theme.noticeSecondaryColor)]; } // Update the badge on People and Rooms tabs if (RiotSettings.shared.homeScreenShowPeopleTab) { - if (recentsDataSource.unsentMessagesDirectDiscussionsCount) + if (recentsDataSource.directMissedDiscussionsCount.hasUnsent) { [self setBadgeValue:@"!" onTabBarItem:TABBAR_PEOPLE_INDEX @@ -923,25 +925,25 @@ } else { - [self setMissedDiscussionsCount:recentsDataSource.missedDirectDiscussionsCount + [self setMissedDiscussionsCount:recentsDataSource.directMissedDiscussionsCount.numberOfNotified onTabBarItem:TABBAR_PEOPLE_INDEX - withBadgeColor:(recentsDataSource.missedHighlightDirectDiscussionsCount ? ThemeService.shared.theme.noticeColor : ThemeService.shared.theme.noticeSecondaryColor)]; + withBadgeColor:(recentsDataSource.directMissedDiscussionsCount.hasHighlight ? ThemeService.shared.theme.noticeColor : ThemeService.shared.theme.noticeSecondaryColor)]; } } if (RiotSettings.shared.homeScreenShowRoomsTab) { - if (recentsDataSource.unsentMessagesGroupDiscussionsCount) + if (recentsDataSource.groupMissedDiscussionsCount.hasUnsent) { - [self setMissedDiscussionsCount:recentsDataSource.unsentMessagesGroupDiscussionsCount + [self setMissedDiscussionsCount:recentsDataSource.groupMissedDiscussionsCount.numberOfUnsent onTabBarItem:TABBAR_ROOMS_INDEX withBadgeColor:ThemeService.shared.theme.noticeColor]; } else { - [self setMissedDiscussionsCount:recentsDataSource.missedGroupDiscussionsCount + [self setMissedDiscussionsCount:recentsDataSource.groupMissedDiscussionsCount.numberOfNotified onTabBarItem:TABBAR_ROOMS_INDEX - withBadgeColor:(recentsDataSource.missedHighlightGroupDiscussionsCount ? ThemeService.shared.theme.noticeColor : ThemeService.shared.theme.noticeSecondaryColor)]; + withBadgeColor:(recentsDataSource.groupMissedDiscussionsCount.hasHighlight ? ThemeService.shared.theme.noticeColor : ThemeService.shared.theme.noticeSecondaryColor)]; } } } From b31cd38e009c5356617ece332e26c04b9e46d920 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Wed, 29 Sep 2021 15:10:41 +0300 Subject: [PATCH 040/276] Add spaces option --- .../Common/Recents/DataSources/RecentsDataSource.m | 8 +++++++- .../DataSources/RecentsRoomListContainer.swift | 12 ++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m index 689742450..7517f0f79 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -100,7 +100,8 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou fetchersContainer = [[RecentsRoomListFetchersContainer alloc] initWithSession:self.mxSession mode:_recentsDataSourceMode - query:nil]; + query:nil + space:nil]; [fetchersContainer addDelegate:self]; } @@ -229,6 +230,11 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou [fetchersContainer updateMode:_recentsDataSourceMode]; } +- (void)setCurrentSpace:(MXSpace *)currentSpace +{ + [fetchersContainer updateSpace:currentSpace]; +} + - (UIView *)viewForStickyHeaderInSection:(NSInteger)section withFrame:(CGRect)frame { UIView *stickyHeader; diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsRoomListContainer.swift b/Riot/Modules/Common/Recents/DataSources/RecentsRoomListContainer.swift index e23807652..2085f9237 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsRoomListContainer.swift +++ b/Riot/Modules/Common/Recents/DataSources/RecentsRoomListContainer.swift @@ -22,6 +22,7 @@ public class RecentsRoomListFetchersContainer: NSObject { public let session: MXSession public private(set) var mode: RecentsDataSourceMode public private(set) var query: String? + public private(set) var space: MXSpace? // MARK: - Fetchers @@ -158,10 +159,12 @@ public class RecentsRoomListFetchersContainer: NSObject { public init(withSession session: MXSession, mode: RecentsDataSourceMode = .home, - query: String? = nil) { + query: String? = nil, + space: MXSpace? = nil) { self.session = session self.mode = mode self.query = query + self.space = space super.init() createFetchers() addRiotSettingsObserver() @@ -239,6 +242,11 @@ public class RecentsRoomListFetchersContainer: NSObject { visibleFetchers.forEach({ $0.fetchOptions.filterOptions.query = query }) } + public func updateSpace(_ space: MXSpace?) { + self.space = space + allFetchers.forEach({ $0.fetchOptions.filterOptions.space = space }) + } + public func refresh() { allFetchers.forEach({ $0.fetchOptions.sortOptions = sortOptions }) } @@ -378,7 +386,7 @@ public class RecentsRoomListFetchersContainer: NSObject { } private func updateConversationFetcher(_ fetcher: MXRoomListDataFetcher, for mode: RecentsDataSourceMode) { - var notDataTypes: MXRoomSummaryDataTypes = [.hidden, .conferenceUser, .direct, .lowPriority, .serverNotice] + var notDataTypes: MXRoomSummaryDataTypes = [.hidden, .conferenceUser, .direct, .lowPriority, .serverNotice, .space] switch mode { case .home: notDataTypes.insert([.invited, .favorited]) From 5d3c3cf5d71a2943e3ea93e21c75d3b3c4946b73 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 29 Sep 2021 17:40:37 +0100 Subject: [PATCH 041/276] Add an image for the integration manager to ServiceTermsModalScreenViewController. --- .../Integrations/Contents.json | 6 + .../Contents.json | 15 + .../integration_manager_iconpile.pdf | 522 ++++++++++++++++++ Riot/Generated/Images.swift | 1 + ...eTermsModalScreenViewController.storyboard | 1 + ...erviceTermsModalScreenViewController.swift | 4 +- 6 files changed, 548 insertions(+), 1 deletion(-) create mode 100644 Riot/Assets/Images.xcassets/Integrations/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Integrations/integration_manager_iconpile.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Integrations/integration_manager_iconpile.imageset/integration_manager_iconpile.pdf diff --git a/Riot/Assets/Images.xcassets/Integrations/Contents.json b/Riot/Assets/Images.xcassets/Integrations/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Integrations/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Integrations/integration_manager_iconpile.imageset/Contents.json b/Riot/Assets/Images.xcassets/Integrations/integration_manager_iconpile.imageset/Contents.json new file mode 100644 index 000000000..034126ed8 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Integrations/integration_manager_iconpile.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "integration_manager_iconpile.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Riot/Assets/Images.xcassets/Integrations/integration_manager_iconpile.imageset/integration_manager_iconpile.pdf b/Riot/Assets/Images.xcassets/Integrations/integration_manager_iconpile.imageset/integration_manager_iconpile.pdf new file mode 100644 index 000000000..86db4d93f --- /dev/null +++ b/Riot/Assets/Images.xcassets/Integrations/integration_manager_iconpile.imageset/integration_manager_iconpile.pdf @@ -0,0 +1,522 @@ +%PDF-1.7 + +1 0 obj + << /BBox [ 0.000000 0.000000 110.000000 46.000000 ] + /Resources << >> + /Subtype /Form + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Type /XObject + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +q +1.000000 0.000000 -0.000000 1.000000 46.000000 13.067383 cm +0.450980 0.490196 0.549020 scn +8.832680 0.000023 m +3.859828 0.578173 0.000000 4.804639 0.000000 9.932617 c +0.000000 15.455465 4.477152 19.932617 10.000000 19.932617 c +15.152602 19.932617 19.395014 16.035631 19.940670 11.028222 c +19.906002 11.044100 19.870073 11.058149 19.832964 11.070212 c +16.719622 12.082235 13.238947 11.455502 10.848575 9.043321 c +8.464049 6.637041 7.840186 3.135887 8.832680 0.000023 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 46.000000 13.067383 cm +0.450980 0.490196 0.549020 scn +10.722207 0.654825 m +19.219536 9.152152 l +16.732798 9.960095 14.059622 9.433351 12.260846 7.618164 c +10.476121 5.817157 9.948320 3.147501 10.722207 0.654825 c +h +f +n +Q +54.832680 13.067406 m +49.859829 13.645554 46.000000 17.872021 46.000000 23.000000 c +46.000000 28.522848 50.477154 33.000000 56.000000 33.000000 c +61.152603 33.000000 65.395012 29.103014 65.940674 24.095604 c +65.906006 24.111483 65.870071 24.125532 65.832962 24.137596 c +62.719620 25.149618 59.238945 24.522884 56.848576 22.110703 c +54.464050 19.704424 53.840187 16.203270 54.832680 13.067406 c +h +W* +n +56.722206 13.722206 m +65.219536 22.219536 l +62.732796 23.027477 60.059624 22.500732 58.260845 20.685547 c +56.476120 18.884541 55.948318 16.214884 56.722206 13.722206 c +h +W* +n +q +1.000000 0.000000 -0.000000 1.000000 46.000000 13.067383 cm +0.450980 0.490196 0.549020 scn +8.832680 0.000023 m +8.717499 -0.990683 l +9.053570 -1.029755 9.386534 -0.895788 9.601898 -0.634851 c +9.817264 -0.373913 9.885659 -0.021587 9.783568 0.300978 c +8.832680 0.000023 l +h +19.940670 11.028222 m +19.525362 10.121425 l +19.851667 9.971979 20.233105 10.009373 20.524176 10.219343 c +20.815245 10.429314 20.971058 10.779479 20.932178 11.136267 c +19.940670 11.028222 l +h +19.832964 11.070212 m +20.141296 12.018734 l +20.141291 12.018736 l +19.832964 11.070212 l +h +10.848575 9.043321 m +11.557022 8.341278 l +11.557022 8.341278 l +10.848575 9.043321 l +h +10.722207 0.654825 m +9.769680 0.359100 l +9.873262 0.025461 10.143859 -0.229664 10.483012 -0.313446 c +10.822165 -0.397228 11.180433 -0.297455 11.427460 -0.050428 c +10.722207 0.654825 l +h +19.219536 9.152152 m +19.924788 8.446899 l +20.170168 8.692279 20.270367 9.047563 20.189354 9.384994 c +20.108341 9.722424 19.857763 9.993490 19.527727 10.100719 c +19.219536 9.152152 l +h +12.260846 7.618164 m +12.969294 6.916121 l +12.969294 6.916121 l +12.260846 7.618164 l +h +8.947861 0.990728 m +4.472036 1.511093 0.997378 5.316795 0.997378 9.932617 c +-0.997378 9.932617 l +-0.997378 4.292482 3.247620 -0.354748 8.717499 -0.990683 c +8.947861 0.990728 l +h +0.997378 9.932617 m +0.997378 14.904629 5.027989 18.935240 10.000000 18.935240 c +10.000000 20.929995 l +3.926316 20.929995 -0.997378 16.006302 -0.997378 9.932617 c +0.997378 9.932617 l +h +10.000000 18.935240 m +14.638034 18.935240 18.458044 15.427099 18.949162 10.920177 c +20.932178 11.136267 l +20.331984 16.644163 15.667171 20.929995 10.000000 20.929995 c +10.000000 18.935240 l +h +20.355978 11.935019 m +20.286608 11.966791 20.214968 11.994786 20.141296 12.018734 c +19.524632 10.121691 l +19.525362 10.121425 l +20.355978 11.935019 l +h +20.141291 12.018736 m +16.712317 13.133358 12.825830 12.455570 10.140127 9.745363 c +11.557022 8.341278 l +13.652063 10.455433 16.726927 11.031113 19.524637 10.121689 c +20.141291 12.018736 l +h +10.140127 9.745363 m +7.462934 7.043747 6.791740 3.143171 7.881791 -0.300932 c +9.783568 0.300978 l +8.888631 3.128603 9.465164 6.230335 11.557022 8.341278 c +10.140127 9.745363 l +h +11.427460 -0.050428 m +19.924788 8.446899 l +18.514284 9.857405 l +10.016954 1.360079 l +11.427460 -0.050428 l +h +19.527727 10.100719 m +16.723921 11.011679 13.645474 10.432379 11.552399 8.320207 c +12.969294 6.916121 l +14.473769 8.434322 16.741674 8.908512 18.911345 8.203585 c +19.527727 10.100719 l +h +11.552399 8.320207 m +9.477654 6.226535 8.899933 3.160538 9.769680 0.359100 c +11.674734 0.950550 l +10.996708 3.134464 11.474587 5.407779 12.969294 6.916121 c +11.552399 8.320207 l +h +f +n +Q +Q + +endstream +endobj + +2 0 obj + 3998 +endobj + +3 0 obj + << /BBox [ 0.000000 0.000000 110.000000 46.000000 ] + /Resources << >> + /Subtype /Form + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Type /XObject + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 39.257812 0.000000 cm +0.890196 0.909804 0.941176 scn +37.742256 23.000000 m +37.742256 10.297451 27.444805 0.000000 14.742256 0.000000 c +9.132510 0.000000 3.991834 2.008327 0.000000 5.345207 c +4.760142 9.730907 7.742256 16.017200 7.742256 23.000000 c +7.742256 29.982801 4.760141 36.269093 0.000000 40.654793 c +3.991834 43.991673 9.132510 46.000000 14.742256 46.000000 c +27.444805 46.000000 37.742256 35.702549 37.742256 23.000000 c +h +f* +n +Q + +endstream +endobj + +4 0 obj + 506 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 71.212158 0.000000 cm +0.890196 0.909804 0.941176 scn +-0.000005 6.274433 m +4.200984 10.596851 6.787903 16.496323 6.787903 23.000000 c +6.787903 29.503679 4.200984 35.403149 -0.000005 39.725567 c +4.119314 43.615345 9.675125 46.000000 15.787903 46.000000 c +28.490452 46.000000 38.787903 35.702549 38.787903 23.000000 c +38.787903 10.297451 28.490452 0.000000 15.787903 0.000000 c +9.675125 0.000000 4.119314 2.384655 -0.000005 6.274433 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 39.257812 0.000000 cm +0.890196 0.909804 0.941176 scn +37.742264 23.000000 m +37.742264 10.297451 27.444813 0.000000 14.742264 0.000000 c +9.132518 0.000000 3.991842 2.008327 0.000008 5.345207 c +4.760150 9.730907 7.742264 16.017200 7.742264 23.000000 c +7.742264 29.982801 4.760149 36.269093 0.000008 40.654793 c +3.991842 43.991673 9.132518 46.000000 14.742264 46.000000 c +27.444813 46.000000 37.742264 35.702549 37.742264 23.000000 c +h +f* +n +Q +q +/E1 gs +/X1 Do +Q +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.890196 0.909804 0.941176 scn +46.000000 23.000000 m +46.000000 10.297451 35.702549 0.000000 23.000000 0.000000 c +10.297451 0.000000 0.000000 10.297451 0.000000 23.000000 c +0.000000 35.702549 10.297451 46.000000 23.000000 46.000000 c +35.702549 46.000000 46.000000 35.702549 46.000000 23.000000 c +h +f* +n +Q +q +q +1.000000 -0.000000 -0.000000 1.000000 79.000000 12.999998 cm +0.450980 0.490196 0.549020 scn +0.000000 16.000000 m +0.000000 18.209139 1.790861 20.000000 4.000000 20.000000 c +15.999997 20.000000 l +18.209137 20.000000 19.999998 18.209139 19.999998 16.000000 c +19.999998 4.000003 l +19.999998 1.790863 18.209137 0.000002 15.999998 0.000002 c +4.000000 0.000002 l +1.790862 0.000002 0.000000 1.790863 0.000000 4.000002 c +0.000000 16.000000 l +h +f +n +Q +79.000000 29.000000 m +79.000000 31.209137 80.790863 33.000000 83.000000 33.000000 c +95.000000 33.000000 l +97.209137 33.000000 99.000000 31.209139 99.000000 29.000000 c +99.000000 17.000000 l +99.000000 14.790861 97.209137 13.000000 95.000000 13.000000 c +83.000000 13.000000 l +80.790863 13.000000 79.000000 14.790861 79.000000 17.000000 c +79.000000 29.000000 l +h +W* +n +q +1.000000 -0.000000 -0.000000 1.000000 79.000000 12.999998 cm +0.450980 0.490196 0.549020 scn +4.000000 18.000000 m +15.999997 18.000000 l +15.999997 22.000000 l +4.000000 22.000000 l +4.000000 18.000000 l +h +17.999998 16.000000 m +17.999998 4.000003 l +21.999998 4.000003 l +21.999998 16.000000 l +17.999998 16.000000 l +h +15.999998 2.000002 m +4.000000 2.000002 l +4.000000 -1.999998 l +15.999998 -1.999998 l +15.999998 2.000002 l +h +2.000000 4.000002 m +2.000000 16.000000 l +-2.000000 16.000000 l +-2.000000 4.000002 l +2.000000 4.000002 l +h +4.000000 2.000002 m +2.895431 2.000002 2.000000 2.895433 2.000000 4.000002 c +-2.000000 4.000002 l +-2.000000 0.686293 0.686293 -1.999998 4.000000 -1.999998 c +4.000000 2.000002 l +h +17.999998 4.000003 m +17.999998 2.895433 17.104567 2.000002 15.999998 2.000002 c +15.999998 -1.999998 l +19.313707 -1.999998 21.999998 0.686295 21.999998 4.000003 c +17.999998 4.000003 l +h +15.999997 18.000000 m +17.104567 18.000000 17.999998 17.104568 17.999998 16.000000 c +21.999998 16.000000 l +21.999998 19.313709 19.313705 22.000000 15.999997 22.000000 c +15.999997 18.000000 l +h +4.000000 22.000000 m +0.686291 22.000000 -2.000000 19.313707 -2.000000 16.000000 c +2.000000 16.000000 l +2.000000 17.104568 2.895431 18.000000 4.000000 18.000000 c +4.000000 22.000000 l +h +f +n +Q +Q +q +1.000000 -0.000000 -0.000000 1.000000 81.000000 14.999998 cm +1.000000 1.000000 1.000000 scn +16.000000 8.000000 m +16.000000 3.581722 12.418278 0.000000 8.000000 0.000000 c +3.581722 0.000000 0.000000 3.581722 0.000000 8.000000 c +0.000000 12.418278 3.581722 16.000000 8.000000 16.000000 c +12.418278 16.000000 16.000000 12.418278 16.000000 8.000000 c +h +f +n +Q +q +1.000000 -0.000000 -0.000000 1.000000 89.000000 19.896606 cm +0.450980 0.490196 0.549020 scn +0.750000 7.103394 m +0.750000 7.517607 0.414214 7.853394 0.000000 7.853394 c +-0.414214 7.853394 -0.750000 7.517607 -0.750000 7.103394 c +0.750000 7.103394 l +h +0.000000 3.290894 m +-0.750000 3.290894 l +-0.750000 3.041655 -0.626185 2.808699 -0.419605 2.669257 c +0.000000 3.290894 l +h +2.080395 0.981757 m +2.423716 0.750016 2.889895 0.840468 3.121636 1.183789 c +3.353378 1.527109 3.262925 1.993289 2.919605 2.225030 c +2.080395 0.981757 l +h +-0.750000 7.103394 m +-0.750000 3.290894 l +0.750000 3.290894 l +0.750000 7.103394 l +-0.750000 7.103394 l +h +-0.419605 2.669257 m +2.080395 0.981757 l +2.919605 2.225030 l +0.419605 3.912530 l +-0.419605 2.669257 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 14.000000 15.000000 cm +0.450980 0.490196 0.549020 scn +0.000000 14.727272 m +0.000000 16.534750 1.465250 18.000000 3.272727 18.000000 c +14.727273 18.000000 l +16.534750 18.000000 18.000000 16.534750 18.000000 14.727272 c +18.000000 3.272727 l +18.000000 1.465250 16.534750 0.000000 14.727273 0.000000 c +3.272727 0.000000 l +1.465250 0.000000 0.000000 1.465250 0.000000 3.272727 c +0.000000 14.727272 l +h +8.181818 12.681818 m +8.181818 11.100275 6.899724 9.818182 5.318182 9.818182 c +3.736639 9.818182 2.454545 11.100275 2.454545 12.681818 c +2.454545 14.263361 3.736639 15.545454 5.318182 15.545454 c +6.899724 15.545454 8.181818 14.263361 8.181818 12.681818 c +h +5.318182 2.454546 m +6.899724 2.454546 8.181818 3.736639 8.181818 5.318182 c +8.181818 6.899724 6.899724 8.181818 5.318182 8.181818 c +3.736639 8.181818 2.454545 6.899724 2.454545 5.318182 c +2.454545 3.736639 3.736639 2.454546 5.318182 2.454546 c +h +15.545454 5.318182 m +15.545454 3.736639 14.263361 2.454546 12.681818 2.454546 c +11.100276 2.454546 9.818182 3.736639 9.818182 5.318182 c +9.818182 6.899724 11.100276 8.181818 12.681818 8.181818 c +14.263361 8.181818 15.545454 6.899724 15.545454 5.318182 c +h +12.681818 9.818182 m +14.263361 9.818182 15.545454 11.100275 15.545454 12.681818 c +15.545454 14.263361 14.263361 15.545454 12.681818 15.545454 c +11.100276 15.545454 9.818182 14.263361 9.818182 12.681818 c +9.818182 11.100275 11.100276 9.818182 12.681818 9.818182 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 14.000000 15.000000 cm +0.450980 0.490196 0.549020 scn +0.000000 14.727272 m +0.000000 16.534750 1.465250 18.000000 3.272727 18.000000 c +14.727273 18.000000 l +16.534750 18.000000 18.000000 16.534750 18.000000 14.727272 c +18.000000 3.272727 l +18.000000 1.465250 16.534750 0.000000 14.727273 0.000000 c +3.272727 0.000000 l +1.465250 0.000000 0.000000 1.465250 0.000000 3.272727 c +0.000000 14.727272 l +h +8.181818 12.681818 m +8.181818 11.100275 6.899724 9.818182 5.318182 9.818182 c +3.736639 9.818182 2.454545 11.100275 2.454545 12.681818 c +2.454545 14.263361 3.736639 15.545454 5.318182 15.545454 c +6.899724 15.545454 8.181818 14.263361 8.181818 12.681818 c +h +5.318182 2.454546 m +6.899724 2.454546 8.181818 3.736639 8.181818 5.318182 c +8.181818 6.899724 6.899724 8.181818 5.318182 8.181818 c +3.736639 8.181818 2.454545 6.899724 2.454545 5.318182 c +2.454545 3.736639 3.736639 2.454546 5.318182 2.454546 c +h +15.545454 5.318182 m +15.545454 3.736639 14.263361 2.454546 12.681818 2.454546 c +11.100276 2.454546 9.818182 3.736639 9.818182 5.318182 c +9.818182 6.899724 11.100276 8.181818 12.681818 8.181818 c +14.263361 8.181818 15.545454 6.899724 15.545454 5.318182 c +h +12.681818 9.818182 m +14.263361 9.818182 15.545454 11.100275 15.545454 12.681818 c +15.545454 14.263361 14.263361 15.545454 12.681818 15.545454 c +11.100276 15.545454 9.818182 14.263361 9.818182 12.681818 c +9.818182 11.100275 11.100276 9.818182 12.681818 9.818182 c +h +f* +n +Q + +endstream +endobj + +7 0 obj + 7483 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 110.000000 46.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Type /Catalog + /Pages 9 0 R + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000004257 00000 n +0000004280 00000 n +0000005035 00000 n +0000005057 00000 n +0000005355 00000 n +0000012894 00000 n +0000012917 00000 n +0000013091 00000 n +0000013165 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +13225 +%%EOF \ No newline at end of file diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index f5c0a2f67..bb0bcdafe 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -97,6 +97,7 @@ internal enum Asset { internal static let plusFloatingAction = ImageAsset(name: "plus_floating_action") internal static let versionCheckCloseIcon = ImageAsset(name: "version_check_close_icon") internal static let versionCheckInfoIcon = ImageAsset(name: "version_check_info_icon") + internal static let integrationManagerIconpile = ImageAsset(name: "integration_manager_iconpile") internal static let closeBanner = ImageAsset(name: "close_banner") internal static let importFilesButton = ImageAsset(name: "import_files_button") internal static let keyBackupLogo = ImageAsset(name: "key_backup_logo") diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard index 3f4cd5f2c..cbb3f012b 100644 --- a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard @@ -152,6 +152,7 @@ + diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift index 818f94109..7a95b36fc 100644 --- a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift @@ -32,6 +32,7 @@ final class ServiceTermsModalScreenViewController: UIViewController { @IBOutlet private weak var scrollView: UIScrollView! + @IBOutlet private weak var imageView: UIImageView! @IBOutlet private weak var titleLabel: UILabel! @IBOutlet private weak var descriptionLabel: UILabel! @IBOutlet private weak var footerLabel: UILabel! @@ -148,10 +149,11 @@ final class ServiceTermsModalScreenViewController: UIViewController { if self.viewModel.serviceType == MXServiceTypeIdentityService { self.descriptionLabel.text = VectorL10n.serviceTermsModalDescriptionIdentityServer self.tableHeaderView.titleLabel.text = VectorL10n.serviceTermsModalTableHeaderIdentityServer + self.imageView.image = Asset.Images.findYourContactsFacepile.image } else { self.descriptionLabel.text = VectorL10n.serviceTermsModalDescriptionIntegrationManager self.tableHeaderView.titleLabel.text = VectorL10n.serviceTermsModalTableHeaderIntegrationManager - // TODO: Set a different image for the integration manager. + self.imageView.image = Asset.Images.integrationManagerIconpile.image } } From 56703c272a87375ce5c48154e09c692454a6e226 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 30 Sep 2021 12:43:49 +0300 Subject: [PATCH 042/276] Fix merge conflict --- Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m index f90a83696..9c5a91ea4 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -762,7 +762,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou missedNotifAndUnreadBadgeLabel.font = [UIFont boldSystemFontOfSize:14]; if (totalNotificationCount > 1000) { - CGFloat value = count / 1000.0; + CGFloat value = totalNotificationCount / 1000.0; missedNotifAndUnreadBadgeLabel.text = [VectorL10n largeBadgeValueKFormat:value]; } else From 1e7afe5180f5d3743d4a426d6605b922a0e8b40c Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 30 Sep 2021 16:00:52 +0300 Subject: [PATCH 043/276] Refactor recent cell data --- .../Common/Recents/CellData/RecentCellData.m | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Riot/Modules/Common/Recents/CellData/RecentCellData.m b/Riot/Modules/Common/Recents/CellData/RecentCellData.m index a69615782..be2d88d47 100644 --- a/Riot/Modules/Common/Recents/CellData/RecentCellData.m +++ b/Riot/Modules/Common/Recents/CellData/RecentCellData.m @@ -27,10 +27,8 @@ #endif @implementation RecentCellData -// trick to hide the mother class property as it is readonly one. -// self.roomDisplayname returns this value instead of the mother class. -@synthesize roomDisplayname; +// Adds K handling to super implementation - (NSString*)notificationCountStringValue { NSString *stringValue; @@ -49,6 +47,7 @@ return stringValue; } +// Adds mentions-only handling to super implementation - (NSUInteger)notificationCount { MXRoom *room = [self.mxSession roomWithRoomId:self.roomSummary.roomId]; @@ -56,20 +55,21 @@ if (room.isMentionsOnly) { // Only the highlighted missed messages must be considered here. - return self.roomSummary.highlightCount; + return super.highlightCount; } - return self.roomSummary.notificationCount; + return super.notificationCount; } -- (void)update +// Adds "Empty Room" case to super implementation +- (NSString *)roomDisplayname { - [super update]; - roomDisplayname = self.spaceChildInfo ? self.spaceChildInfo.name: self.roomSummary.displayname; - if (!roomDisplayname.length) + NSString *result = [super roomDisplayname]; + if (!result.length) { - roomDisplayname = [MatrixKitL10n roomDisplaynameEmptyRoom]; + result = [MatrixKitL10n roomDisplaynameEmptyRoom]; } + return result; } @end From fd871fbb88a86b50c120696bf783e36ee54b2028 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 30 Sep 2021 16:01:26 +0300 Subject: [PATCH 044/276] Add suggested rooms fetcher --- .../Common/Recents/DataSources/RecentsRoomListContainer.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsRoomListContainer.swift b/Riot/Modules/Common/Recents/DataSources/RecentsRoomListContainer.swift index 2085f9237..c61a246dc 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsRoomListContainer.swift +++ b/Riot/Modules/Common/Recents/DataSources/RecentsRoomListContainer.swift @@ -359,6 +359,7 @@ public class RecentsRoomListFetchersContainer: NSObject { conversationRoomListDataFetcherForRooms = createConversationRoomListDataFetcherForRooms() lowPriorityRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.lowPriority]) serverNoticeRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.serverNotice]) + suggestedRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.suggested]) } private func updateDirectFetcher(_ fetcher: MXRoomListDataFetcher, for mode: RecentsDataSourceMode) { From 634424d288b7bd11bfb921c964a14834d6ac6990 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 30 Sep 2021 14:14:15 +0100 Subject: [PATCH 045/276] Small tweaks plus changelog entry. Use the new generated localisation strings. Add comments. Address feedback from PR review. --- Riot/Assets/en.lproj/Vector.strings | 6 ++ Riot/Generated/Strings.swift | 100 +++++++++++++----- .../Contacts/ContactsTableViewController.h | 5 +- .../Contacts/ContactsTableViewController.m | 30 ++++-- .../Views/FindYourContactsFooterView.swift | 22 ++-- .../Views/FindYourContactsFooterView.xib | 4 +- ...erviceTermsModalScreenViewController.swift | 29 ++++- .../ServiceTermsModalTableHeaderView.swift | 7 ++ .../Modules/Settings/SettingsViewController.m | 24 ++--- changelog.d/4484.change | 1 + 10 files changed, 167 insertions(+), 61 deletions(-) create mode 100644 changelog.d/4484.change diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index ed66f5b89..67d390007 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -1039,6 +1039,12 @@ Tap the + to start adding people."; "service_terms_modal_description_identity_server" = "This will allow someone to find you if they have your phone number or email saved in their phone contacts."; "service_terms_modal_description_integration_manager" = "This will allow you to use bots, bridges, widgets and sticker packs."; +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "Identity Server"; +"service_terms_modal_information_title_integration_manager" = "Integration Manager"; +"service_terms_modal_information_description_identity_server" = "An identity server allows %@ to find users on Matrix by looking up their phone number or email address."; +"service_terms_modal_information_description_integration_manager" = "An integration manager allows %@ to ..."; + "service_terms_modal_policy_checkbox_accessibility_hint" = "Check to accept %@"; // Deactivate account diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 6891446ae..cf7f1b935 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -559,6 +559,14 @@ public class VectorL10n: NSObject { public static func contactsAddressBookPermissionDenied(_ p1: String) -> String { return VectorL10n.tr("Vector", "contacts_address_book_permission_denied", p1) } + /// To enable contacts, go to your device settings. + public static var contactsAddressBookPermissionDeniedAlertMessage: String { + return VectorL10n.tr("Vector", "contacts_address_book_permission_denied_alert_message") + } + /// Contacts disabled + public static var contactsAddressBookPermissionDeniedAlertTitle: String { + return VectorL10n.tr("Vector", "contacts_address_book_permission_denied_alert_title") + } /// Permission required to access local contacts public static var contactsAddressBookPermissionRequired: String { return VectorL10n.tr("Vector", "contacts_address_book_permission_required") @@ -1403,6 +1411,26 @@ public class VectorL10n: NSObject { public static var fileUploadErrorUnsupportedFileTypeMessage: String { return VectorL10n.tr("Vector", "file_upload_error_unsupported_file_type_message") } + /// Find your contacts + public static var findYourContactsButtonTitle: String { + return VectorL10n.tr("Vector", "find_your_contacts_button_title") + } + /// This can be disabled anytime from settings. + public static var findYourContactsFooter: String { + return VectorL10n.tr("Vector", "find_your_contacts_footer") + } + /// Unable to connect to the identity server. + public static var findYourContactsIdentityServiceError: String { + return VectorL10n.tr("Vector", "find_your_contacts_identity_service_error") + } + /// Let %@ show your contacts so you can quickly start chatting with those you know best. + public static func findYourContactsMessage(_ p1: String) -> String { + return VectorL10n.tr("Vector", "find_your_contacts_message", p1) + } + /// Start by listing your contacts + public static var findYourContactsTitle: String { + return VectorL10n.tr("Vector", "find_your_contacts_title") + } /// To continue using the %@ homeserver you must review and agree to the terms and conditions. public static func gdprConsentNotGivenAlertMessage(_ p1: String) -> String { return VectorL10n.tr("Vector", "gdpr_consent_not_given_alert_message", p1) @@ -2463,7 +2491,7 @@ public class VectorL10n: NSObject { public static var roomCreationErrorInviteUserByEmailWithoutIdentityServer: String { return VectorL10n.tr("Vector", "room_creation_error_invite_user_by_email_without_identity_server") } - /// Search / invite by User ID, Name or email + /// User ID, name or email public static var roomCreationInviteAnotherUser: String { return VectorL10n.tr("Vector", "room_creation_invite_another_user") } @@ -4011,37 +4039,49 @@ public class VectorL10n: NSObject { public static var serviceTermsModalDeclineButton: String { return VectorL10n.tr("Vector", "service_terms_modal_decline_button") } - /// Find others by phone or email - public static var serviceTermsModalDescriptionForIdentityServer1: String { - return VectorL10n.tr("Vector", "service_terms_modal_description_for_identity_server_1") + /// This will allow someone to find you if they have your phone number or email saved in their phone contacts. + public static var serviceTermsModalDescriptionIdentityServer: String { + return VectorL10n.tr("Vector", "service_terms_modal_description_identity_server") } - /// Be found by phone or email - public static var serviceTermsModalDescriptionForIdentityServer2: String { - return VectorL10n.tr("Vector", "service_terms_modal_description_for_identity_server_2") + /// This will allow you to use bots, bridges, widgets and sticker packs. + public static var serviceTermsModalDescriptionIntegrationManager: String { + return VectorL10n.tr("Vector", "service_terms_modal_description_integration_manager") } - /// Use Bots, bridges, widgets and sticker packs - public static var serviceTermsModalDescriptionForIntegrationManager: String { - return VectorL10n.tr("Vector", "service_terms_modal_description_for_integration_manager") + /// This can be disabled anytime in settings. + public static var serviceTermsModalFooter: String { + return VectorL10n.tr("Vector", "service_terms_modal_footer") } - /// To continue you need to accept the terms of this service (%@). - public static func serviceTermsModalMessage(_ p1: String) -> String { - return VectorL10n.tr("Vector", "service_terms_modal_message", p1) + /// An identity server allows %@ to find users on Matrix by looking up their phone number or email address. + public static func serviceTermsModalInformationDescriptionIdentityServer(_ p1: String) -> String { + return VectorL10n.tr("Vector", "service_terms_modal_information_description_identity_server", p1) } - /// Accept the terms of the identity server (%@) to discover contacts. - public static func serviceTermsModalMessageIdentityServer(_ p1: String) -> String { - return VectorL10n.tr("Vector", "service_terms_modal_message_identity_server", p1) + /// An integration manager allows %@ to ... + public static func serviceTermsModalInformationDescriptionIntegrationManager(_ p1: String) -> String { + return VectorL10n.tr("Vector", "service_terms_modal_information_description_integration_manager", p1) + } + /// Identity Server + public static var serviceTermsModalInformationTitleIdentityServer: String { + return VectorL10n.tr("Vector", "service_terms_modal_information_title_identity_server") + } + /// Integration Manager + public static var serviceTermsModalInformationTitleIntegrationManager: String { + return VectorL10n.tr("Vector", "service_terms_modal_information_title_integration_manager") } /// Check to accept %@ public static func serviceTermsModalPolicyCheckboxAccessibilityHint(_ p1: String) -> String { return VectorL10n.tr("Vector", "service_terms_modal_policy_checkbox_accessibility_hint", p1) } - /// Terms Of Service - public static var serviceTermsModalTitle: String { - return VectorL10n.tr("Vector", "service_terms_modal_title") + /// IDENTITY SERVER TERMS + public static var serviceTermsModalTableHeaderIdentityServer: String { + return VectorL10n.tr("Vector", "service_terms_modal_table_header_identity_server") } - /// Contact discovery - public static var serviceTermsModalTitleIdentityServer: String { - return VectorL10n.tr("Vector", "service_terms_modal_title_identity_server") + /// INTEGRATION MANAGER TERMS + public static var serviceTermsModalTableHeaderIntegrationManager: String { + return VectorL10n.tr("Vector", "service_terms_modal_table_header_integration_manager") + } + /// To continue, accept the below terms and conditions + public static var serviceTermsModalTitleMessage: String { + return VectorL10n.tr("Vector", "service_terms_modal_title_message") } /// Invalid credentials public static var settingsAdd3pidInvalidPasswordMessage: String { @@ -4127,13 +4167,17 @@ public class VectorL10n: NSObject { public static var settingsConfirmPassword: String { return VectorL10n.tr("Vector", "settings_confirm_password") } - /// LOCAL CONTACTS + /// DEVICE CONTACTS public static var settingsContacts: String { return VectorL10n.tr("Vector", "settings_contacts") } - /// Use emails and phone numbers to discover users - public static var settingsContactsDiscoverMatrixUsers: String { - return VectorL10n.tr("Vector", "settings_contacts_discover_matrix_users") + /// Find your contacts + public static var settingsContactsEnableSync: String { + return VectorL10n.tr("Vector", "settings_contacts_enable_sync") + } + /// This will use your identity server to connect you with your contacts, and help them find you. + public static var settingsContactsEnableSyncDescription: String { + return VectorL10n.tr("Vector", "settings_contacts_enable_sync_description") } /// Phonebook country public static var settingsContactsPhonebookCountry: String { @@ -4543,6 +4587,10 @@ public class VectorL10n: NSObject { public static var settingsPasswordUpdated: String { return VectorL10n.tr("Vector", "settings_password_updated") } + /// PHONE CONTACTS + public static var settingsPhoneContacts: String { + return VectorL10n.tr("Vector", "settings_phone_contacts") + } /// Phone public static var settingsPhoneNumber: String { return VectorL10n.tr("Vector", "settings_phone_number") diff --git a/Riot/Modules/Contacts/ContactsTableViewController.h b/Riot/Modules/Contacts/ContactsTableViewController.h index 707070e1d..8b535495b 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.h +++ b/Riot/Modules/Contacts/ContactsTableViewController.h @@ -74,9 +74,8 @@ @property (nonatomic) BOOL disableFindYourContactsFooter; /** - Indicates when there's an active search. This is used to indicate that the contacts - access footer should be hidden as even without local contacts, there will still be - results to be shown from the server. + Indicates when there's an active search. This is used to determine when the contacts + access footer should be hidden in order to list the results from the server. */ @property (nonatomic) BOOL contactsAreFilteredWithSearch; diff --git a/Riot/Modules/Contacts/ContactsTableViewController.m b/Riot/Modules/Contacts/ContactsTableViewController.m index 5043d28cc..49342c959 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -200,6 +200,10 @@ #pragma mark - +/** + Creates a new `FindYourContactsFooterView` and caches it in + the `findYourContactsFooterView` property before returning it for use. + */ - (FindYourContactsFooterView*)makeFooterView { FindYourContactsFooterView *footerView = [FindYourContactsFooterView instantiate]; @@ -210,6 +214,11 @@ return footerView; } +/** + Checks whether local contacts sync is ready to use or if there are any search results + in the table, hiding the find your contacts footer if so. Otherwise the footer is shown + so long as it hasn't been disabled. + */ - (void)updateFooterViewVisibility { if (!BuildSettings.allowLocalContactsAccess || self.disableFindYourContactsFooter) @@ -238,6 +247,9 @@ [self updateFooterViewHeight]; } +/** + Updates the height of the find your contacts footer to fill all available space. + */ - (void)updateFooterViewHeight { if (self.findYourContactsFooterView && self.findYourContactsFooterView == self.contactsTableView.tableFooterView) @@ -504,7 +516,7 @@ #pragma mark - FindYourContactsFooterViewDelegate -- (void)didTapEnableContactsSync +- (void)contactsFooterViewDidRequestFindContacts:(FindYourContactsFooterView *)footerView { // First check the identity if service terms have already been accepted if (self->contactsDataSource.mxSession.identityService.areAllTermsAgreed) @@ -518,12 +530,14 @@ // The preparation can take some time so indicate this to the user [self startActivityIndicator]; + footerView.isActionEnabled = NO; [self->contactsDataSource.mxSession prepareIdentityServiceForTermsWithDefault:RiotSettings.shared.identityServerUrlString success:^(MXSession *session, NSString *baseURL, NSString *accessToken) { MXStrongifyAndReturnIfNil(self); [self stopActivityIndicator]; + footerView.isActionEnabled = YES; // Present the terms of the identity server. [self presentIdentityServerTermsWithSession:session baseURL:baseURL andAccessToken:accessToken]; @@ -532,12 +546,14 @@ MXStrongifyAndReturnIfNil(self); [self stopActivityIndicator]; + footerView.isActionEnabled = YES; - UIAlertController *alertController = [UIAlertController alertControllerWithTitle:NSLocalizedStringFromTable(@"find_your_contacts_identity_service_error", @"Vector", nil) + // Alert the user that something went wrong. + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:VectorL10n.findYourContactsIdentityServiceError message:nil preferredStyle:UIAlertControllerStyleAlert]; - [alertController addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"] + [alertController addAction:[UIAlertAction actionWithTitle:MatrixKitL10n.ok style:UIAlertActionStyleDefault handler:nil]]; @@ -551,8 +567,8 @@ MXWeakify(self); // Check for contacts access, showing a pop-up if necessary. - [MXKTools checkAccessForContacts:NSLocalizedStringFromTable(@"contacts_address_book_permission_denied_alert_title", @"Vector", nil) - withManualChangeMessage:NSLocalizedStringFromTable(@"contacts_address_book_permission_denied_alert_message", @"Vector", nil) + [MXKTools checkAccessForContacts:VectorL10n.contactsAddressBookPermissionDeniedAlertTitle + withManualChangeMessage:VectorL10n.contactsAddressBookPermissionDeniedAlertMessage showPopUpInViewController:self completionHandler:^(BOOL granted) { @@ -611,9 +627,7 @@ - (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline:(ServiceTermsModalCoordinatorBridgePresenter *)coordinatorBridgePresenter session:(MXSession *)session { - [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ - - }]; + [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; self.serviceTermsModalCoordinatorBridgePresenter = nil; } diff --git a/Riot/Modules/Contacts/Views/FindYourContactsFooterView.swift b/Riot/Modules/Contacts/Views/FindYourContactsFooterView.swift index edaf04468..ba0e9cf8a 100644 --- a/Riot/Modules/Contacts/Views/FindYourContactsFooterView.swift +++ b/Riot/Modules/Contacts/Views/FindYourContactsFooterView.swift @@ -18,7 +18,7 @@ import UIKit import Reusable @objc protocol FindYourContactsFooterViewDelegate { - func didTapEnableContactsSync() + func contactsFooterViewDidRequestFindContacts(_ footerView: FindYourContactsFooterView) } @objcMembers @@ -28,11 +28,17 @@ class FindYourContactsFooterView: UIView, NibLoadable, Themable { weak var delegate: FindYourContactsFooterViewDelegate? - @IBOutlet weak var containerView: UIView! - @IBOutlet weak var titleLabel: UILabel! - @IBOutlet weak var messageLabel: UILabel! - @IBOutlet weak var button: CustomRoundedButton! - @IBOutlet weak var footerLabel: UILabel! + /// Whether or not the view's button responds to taps. + var isActionEnabled: Bool { + get { button.isEnabled } + set { button.isEnabled = newValue } + } + + @IBOutlet weak private var containerView: UIView! + @IBOutlet weak private var titleLabel: UILabel! + @IBOutlet weak private var messageLabel: UILabel! + @IBOutlet weak private var button: CustomRoundedButton! + @IBOutlet weak private var footerLabel: UILabel! // MARK: - Setup @@ -75,7 +81,7 @@ class FindYourContactsFooterView: UIView, NibLoadable, Themable { // MARK: - Action - @IBAction private func enableContactsSync(_ sender: Any) { - delegate?.didTapEnableContactsSync() + @IBAction private func buttonAction(_ sender: Any) { + delegate?.contactsFooterViewDidRequestFindContacts(self) } } diff --git a/Riot/Modules/Contacts/Views/FindYourContactsFooterView.xib b/Riot/Modules/Contacts/Views/FindYourContactsFooterView.xib index eb5faea58..86cc2734b 100644 --- a/Riot/Modules/Contacts/Views/FindYourContactsFooterView.xib +++ b/Riot/Modules/Contacts/Views/FindYourContactsFooterView.xib @@ -1,8 +1,6 @@ - - @@ -40,7 +38,7 @@ - +

  • + WeakDictionary (https://github.com/nicholascross/WeakDictionary/) +

    + MIT License +

    + Copyright (c) 2016 Nicholas Cross +

    + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: +

    + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. +

    + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +

    +
  • From 11eca2a84e1c14b13f02949cedfbeaba6054bb15 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 6 Oct 2021 16:59:56 +0200 Subject: [PATCH 083/276] Templates: Upate FlowTemplateCoordinator. --- .../FlowTemplateCoordinator.swift | 13 ++++++++++++- .../FlowTemplateCoordinatorBridgePresenter.swift | 13 ++++--------- .../FlowTemplateCoordinatorProtocol.swift | 3 +++ 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift index 773c7cf6e..dbbfa540d 100644 --- a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift +++ b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift @@ -17,7 +17,7 @@ import UIKit @objcMembers -final class FlowTemplateCoordinator: FlowTemplateCoordinatorProtocol { +final class FlowTemplateCoordinator: NSObject, FlowTemplateCoordinatorProtocol { // MARK: - Properties @@ -52,6 +52,9 @@ final class FlowTemplateCoordinator: FlowTemplateCoordinatorProtocol { self.add(childCoordinator: rootCoordinator) + // Detect when view controller has been dismissed by gesture when presented modally (not in full screen). + self.navigationRouter.toPresentable().presentationController?.delegate = self + if self.navigationRouter.modules.isEmpty == false { self.navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in self?.remove(childCoordinator: rootCoordinator) @@ -77,6 +80,14 @@ final class FlowTemplateCoordinator: FlowTemplateCoordinatorProtocol { } } +// MARK: - UIAdaptivePresentationControllerDelegate +extension FlowTemplateCoordinator: UIAdaptivePresentationControllerDelegate { + + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + self.delegate?.flowTemplateCoordinatorDidDismissInteractively(self) + } +} + // MARK: - TemplateScreenCoordinatorDelegate extension FlowTemplateCoordinator: TemplateScreenCoordinatorDelegate { func templateScreenCoordinator(_ coordinator: TemplateScreenCoordinatorProtocol, didCompleteWithUserDisplayName userDisplayName: String?) { diff --git a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorBridgePresenter.swift b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorBridgePresenter.swift index 0ab003b94..cead36efc 100644 --- a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorBridgePresenter.swift +++ b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorBridgePresenter.swift @@ -18,6 +18,7 @@ import Foundation @objc protocol FlowTemplateCoordinatorBridgePresenterDelegate { func flowTemplateCoordinatorBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: FlowTemplateCoordinatorBridgePresenter) + func flowTemplateCoordinatorBridgePresenterDidDismissInteractively(_ coordinatorBridgePresenter: FlowTemplateCoordinatorBridgePresenter) } /// FlowTemplateCoordinatorBridgePresenter enables to start FlowTemplateCoordinator from a view controller. @@ -66,7 +67,6 @@ final class FlowTemplateCoordinatorBridgePresenter: NSObject { let flowTemplateCoordinator = FlowTemplateCoordinator(parameters: flowTemplateCoordinatorParameters) flowTemplateCoordinator.delegate = self let presentable = flowTemplateCoordinator.toPresentable() - presentable.presentationController?.delegate = self viewController.present(presentable, animated: animated, completion: nil) flowTemplateCoordinator.start() @@ -120,17 +120,12 @@ final class FlowTemplateCoordinatorBridgePresenter: NSObject { // MARK: - FlowTemplateCoordinatorDelegate extension FlowTemplateCoordinatorBridgePresenter: FlowTemplateCoordinatorDelegate { + func flowTemplateCoordinatorDidComplete(_ coordinator: FlowTemplateCoordinatorProtocol) { self.delegate?.flowTemplateCoordinatorBridgePresenterDelegateDidComplete(self) } -} - -// MARK: - UIAdaptivePresentationControllerDelegate - -extension FlowTemplateCoordinatorBridgePresenter: UIAdaptivePresentationControllerDelegate { - func flowTemplateCoordinatorDidComplete(_ presentationController: UIPresentationController) { - self.delegate?.flowTemplateCoordinatorBridgePresenterDelegateDidComplete(self) + func flowTemplateCoordinatorDidDismissInteractively(_ coordinator: FlowTemplateCoordinatorProtocol) { + self.delegate?.flowTemplateCoordinatorBridgePresenterDidDismissInteractively(self) } - } diff --git a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorProtocol.swift b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorProtocol.swift index eba9e522c..a31991f68 100644 --- a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorProtocol.swift +++ b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorProtocol.swift @@ -18,6 +18,9 @@ import Foundation protocol FlowTemplateCoordinatorDelegate: AnyObject { func flowTemplateCoordinatorDidComplete(_ coordinator: FlowTemplateCoordinatorProtocol) + + /// Called when the view has been dismissed by gesture when presented modally (not in full screen). + func flowTemplateCoordinatorDidDismissInteractively(_ coordinator: FlowTemplateCoordinatorProtocol) } /// `FlowTemplateCoordinatorProtocol` is a protocol describing a Coordinator that handle xxxxxxx navigation flow. From 189080d917fc52e0c42fc06a89f72de496855c2c Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 6 Oct 2021 17:20:34 +0200 Subject: [PATCH 084/276] SplitViewCoordinator: Add possibility to stack controllers on detail view. --- Riot/Modules/SplitView/SplitViewCoordinator.swift | 14 ++++++++++++-- Riot/Modules/SplitView/SplitViewPresentable.swift | 10 ++++++++-- Riot/Modules/TabBar/TabBarCoordinator.swift | 4 ++-- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/Riot/Modules/SplitView/SplitViewCoordinator.swift b/Riot/Modules/SplitView/SplitViewCoordinator.swift index 8a6cb473b..9ebe90c87 100644 --- a/Riot/Modules/SplitView/SplitViewCoordinator.swift +++ b/Riot/Modules/SplitView/SplitViewCoordinator.swift @@ -315,8 +315,8 @@ extension SplitViewCoordinator: SplitViewMasterPresentableDelegate { return self.detailNavigationRouter?.modules ?? [] } - func splitViewMasterPresentable(_ presentable: Presentable, wantsToDisplay detailPresentable: Presentable, popCompletion: (() -> Void)?) { - MXLog.debug("[SplitViewCoordinator] splitViewMasterPresentable: \(presentable) wantsToDisplay detailPresentable: \(detailPresentable)") + func splitViewMasterPresentable(_ presentable: Presentable, wantsToReplaceDetailWith detailPresentable: Presentable, popCompletion: (() -> Void)?) { + MXLog.debug("[SplitViewCoordinator] splitViewMasterPresentable: \(presentable) wantsToReplaceDetailWith detailPresentable: \(detailPresentable)") guard let detailNavigationController = self.detailNavigationController else { MXLog.debug("[SplitViewCoordinator] splitViewMasterPresentable: Failed to display because detailNavigationController is nil") @@ -340,4 +340,14 @@ extension SplitViewCoordinator: SplitViewMasterPresentableDelegate { // Set leftBarButtonItem with split view display mode button if there is no leftBarButtonItem defined detailController.vc_setupDisplayModeLeftBarButtonItemIfNeeded() } + + func splitViewMasterPresentable(_ presentable: Presentable, wantsToStack detailPresentable: Presentable, popCompletion: (() -> Void)?) { + + guard let detailNavigationRouter = self.detailNavigationRouter else { + MXLog.debug("Failed to stack \(detailPresentable) because detailNavigationRouter is nil") + return + } + + detailNavigationRouter.push(detailPresentable, animated: true, popCompletion: popCompletion) + } } diff --git a/Riot/Modules/SplitView/SplitViewPresentable.swift b/Riot/Modules/SplitView/SplitViewPresentable.swift index 9c989bf55..3521c76bd 100644 --- a/Riot/Modules/SplitView/SplitViewPresentable.swift +++ b/Riot/Modules/SplitView/SplitViewPresentable.swift @@ -17,15 +17,21 @@ import UIKit protocol SplitViewMasterPresentableDelegate: AnyObject { - func splitViewMasterPresentable(_ presentable: Presentable, wantsToDisplay detailPresentable: Presentable, popCompletion: (() -> Void)?) + /// Detail items from the split view var detailModules: [Presentable] { get } + + /// Replace split view detail with the given detailPresentable + func splitViewMasterPresentable(_ presentable: Presentable, wantsToReplaceDetailWith detailPresentable: Presentable, popCompletion: (() -> Void)?) + + /// Stack the detailPresentable on the existing split view detail stack + func splitViewMasterPresentable(_ presentable: Presentable, wantsToStack detailPresentable: Presentable, popCompletion: (() -> Void)?) } /// `SplitViewMasterPresentableDelegate` default implementation extension SplitViewMasterPresentableDelegate { func splitViewMasterPresentable(_ presentable: Presentable, wantsToDisplay detailPresentable: Presentable) { - splitViewMasterPresentable(presentable, wantsToDisplay: detailPresentable, popCompletion: nil) + splitViewMasterPresentable(presentable, wantsToReplaceDetailWith: detailPresentable, popCompletion: nil) } } diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index 176b0c3b4..399680d47 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -404,7 +404,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { // RoomCoordinator with the same room id and Matrix session is shown if let eventId = parameters.eventId { - // If there is an event id ask the RoomCoordinator to start with this one + // If there is an event id ask the RoomCoordinator to start with this one topRoomCoordinator.start(withEventId: eventId, completion: completion) } else { // If there is no event id defined do nothing @@ -427,7 +427,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { /// If the split view is collapsed (one column visible) it will push the Presentable on the primary navigation controller, otherwise it will show the Presentable as the secondary view of the split view. private func replaceSplitViewDetails(with presentable: Presentable, popCompletion: (() -> Void)? = nil) { - self.splitViewMasterPresentableDelegate?.splitViewMasterPresentable(self, wantsToDisplay: presentable, popCompletion: popCompletion) + self.splitViewMasterPresentableDelegate?.splitViewMasterPresentable(self, wantsToReplaceDetailWith: presentable, popCompletion: popCompletion) } // MARK: UserSessions management From c7050187efb364dc862ff18cffce3bebc3bf4011 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 6 Oct 2021 17:47:27 +0200 Subject: [PATCH 085/276] Update changes --- changelog.d/4734.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/4734.change diff --git a/changelog.d/4734.change b/changelog.d/4734.change new file mode 100644 index 000000000..23cd68343 --- /dev/null +++ b/changelog.d/4734.change @@ -0,0 +1 @@ +Navigation: Create RoomCoordinator. \ No newline at end of file From f2dd993891010b5f1b5cf42c95e8e123ded01b29 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 6 Oct 2021 17:36:28 +0100 Subject: [PATCH 086/276] Add final strings. Tweak header. --- Riot/Assets/en.lproj/Vector.strings | 4 +-- Riot/Generated/Strings.swift | 12 ++++---- ...erviceTermsModalScreenViewController.swift | 28 +++++++++++++------ 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 67d390007..8691d30a2 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -1042,8 +1042,8 @@ Tap the + to start adding people."; // Alert explaining what an identity server / integration manager is. "service_terms_modal_information_title_identity_server" = "Identity Server"; "service_terms_modal_information_title_integration_manager" = "Integration Manager"; -"service_terms_modal_information_description_identity_server" = "An identity server allows %@ to find users on Matrix by looking up their phone number or email address."; -"service_terms_modal_information_description_integration_manager" = "An integration manager allows %@ to ..."; +"service_terms_modal_information_description_identity_server" = "An identity server helps you find your contacts, by looking up their phone number or email address, to see if they already have an account."; +"service_terms_modal_information_description_integration_manager" = "An integration manager lets you add features from third parties."; "service_terms_modal_policy_checkbox_accessibility_hint" = "Check to accept %@"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index cf7f1b935..efa5d379c 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4051,13 +4051,13 @@ public class VectorL10n: NSObject { public static var serviceTermsModalFooter: String { return VectorL10n.tr("Vector", "service_terms_modal_footer") } - /// An identity server allows %@ to find users on Matrix by looking up their phone number or email address. - public static func serviceTermsModalInformationDescriptionIdentityServer(_ p1: String) -> String { - return VectorL10n.tr("Vector", "service_terms_modal_information_description_identity_server", p1) + /// An identity server helps you find your contacts, by looking up their phone number or email address, to see if they already have an account. + public static var serviceTermsModalInformationDescriptionIdentityServer: String { + return VectorL10n.tr("Vector", "service_terms_modal_information_description_identity_server") } - /// An integration manager allows %@ to ... - public static func serviceTermsModalInformationDescriptionIntegrationManager(_ p1: String) -> String { - return VectorL10n.tr("Vector", "service_terms_modal_information_description_integration_manager", p1) + /// An integration manager lets you add features from third parties. + public static var serviceTermsModalInformationDescriptionIntegrationManager: String { + return VectorL10n.tr("Vector", "service_terms_modal_information_description_integration_manager") } /// Identity Server public static var serviceTermsModalInformationTitleIdentityServer: String { diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift index ec5c271cb..a1bbb053b 100644 --- a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift @@ -160,16 +160,26 @@ final class ServiceTermsModalScreenViewController: UIViewController { } private func setupTableView() { - self.tableView.delegate = self - self.tableView.dataSource = self - self.tableView.separatorStyle = .none - self.tableView.alwaysBounceVertical = false - self.tableView.backgroundColor = .clear - self.tableView.register(TableViewCellWithCheckBoxAndLabel.nib(), forCellReuseIdentifier: TableViewCellWithCheckBoxAndLabel.defaultReuseIdentifier()) + guard let tableView = tableView else { return } + + tableView.delegate = self + tableView.dataSource = self + tableView.separatorStyle = .none + tableView.alwaysBounceVertical = false + tableView.backgroundColor = .clear + tableView.register(TableViewCellWithCheckBoxAndLabel.nib(), forCellReuseIdentifier: TableViewCellWithCheckBoxAndLabel.defaultReuseIdentifier()) tableHeaderView = ServiceTermsModalTableHeaderView.instantiate() tableHeaderView.delegate = self - self.tableView.tableHeaderView = tableHeaderView + tableView.tableHeaderView = tableHeaderView + + tableView.addConstraint(NSLayoutConstraint(item: tableView, + attribute: .width, + relatedBy: .equal, + toItem: tableHeaderView, + attribute: .width, + multiplier: 1, + constant: 10)) } private func render(viewState: ServiceTermsModalScreenViewState) { @@ -296,10 +306,10 @@ extension ServiceTermsModalScreenViewController: ServiceTermsModalTableHeaderVie if viewModel.serviceType == MXServiceTypeIdentityService { title = VectorL10n.serviceTermsModalInformationTitleIdentityServer - message = VectorL10n.serviceTermsModalInformationDescriptionIdentityServer(AppInfo.current.displayName) + message = VectorL10n.serviceTermsModalInformationDescriptionIdentityServer } else { title = VectorL10n.serviceTermsModalInformationTitleIntegrationManager - message = VectorL10n.serviceTermsModalInformationDescriptionIntegrationManager(AppInfo.current.displayName) + message = VectorL10n.serviceTermsModalInformationDescriptionIntegrationManager } let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) From f9ae4f8efe5c6980ab807538ead096f9c8048b4c Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Thu, 7 Oct 2021 15:37:20 +0200 Subject: [PATCH 087/276] [Spaces] M10.4.1 Home space data filtering #4570 - Added settings menu for Home space --- .../space_private_icon.imageset/Contents.json | 26 ++++++ .../space_private_icon.png | Bin 0 -> 1003 bytes .../space_private_icon@2x.png | Bin 0 -> 1448 bytes .../space_private_icon@3x.png | Bin 0 -> 1914 bytes Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Images.swift | 16 +++- Riot/Generated/Strings.swift | 4 + .../Recents/DataSources/RecentsDataSource.m | 2 +- .../SpaceDetailViewController.swift | 3 + .../Spaces/SpaceList/SpaceListViewCell.swift | 2 +- .../Spaces/SpaceList/SpaceListViewModel.swift | 3 +- .../Spaces/SpaceMenu/SpaceMenuCell.swift | 21 +++++ .../SpaceMenu/SpaceMenuListItemViewData.swift | 2 + .../SpaceMenu/SpaceMenuListViewCell.swift | 2 +- .../SpaceMenu/SpaceMenuSwitchViewCell.swift | 74 ++++++++++++++++++ .../SpaceMenu/SpaceMenuSwitchViewCell.xib | 64 +++++++++++++++ .../SpaceMenu/SpaceMenuViewController.swift | 26 ++++-- .../Spaces/SpaceMenu/SpaceMenuViewModel.swift | 28 +++++-- .../Spaces/SpaceMenu/SpaceMenuViewState.swift | 1 + 19 files changed, 259 insertions(+), 16 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon.png create mode 100644 Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon@2x.png create mode 100644 Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon@3x.png create mode 100644 Riot/Modules/Spaces/SpaceMenu/SpaceMenuCell.swift create mode 100644 Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.swift create mode 100644 Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.xib diff --git a/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/Contents.json new file mode 100644 index 000000000..6bb7945f7 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "space_private_icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "space_private_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "space_private_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon.png b/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..10bc95ab82ab6db1ea7d4fd44aa9486f9c2bce9b GIT binary patch literal 1003 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1|-8uW1a&kmUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lxRtf@J#ddWzYh$IT%VstT4fPE4;bsH1+JHo@{EISEfi{E8w==W>t3(ll+GC>+vK+}V5TAlYfnK%a zveAbJn;n;A+(UL?Am@0xIEHA5#s=>8Vh$9r-8_40iZHWa$Oger;zB$t82>hfm?_>D z(79{3wY9aCqpq1zalw+!Teq5bWSbfCmw)dwF8Af&yOa8Ej%E6}JFL?LSme}=90*H)`c@Cv6?!iRGXYnn)Q{T%qdK` zZ{25mmpWUW#=U#0K0JJt_vy;a#X0WbJ~NhlxXzrdnG>RZq{3;APQsb#H#*;3wDGHB zspez!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBDAAG{;hE;^%b*2hb1<+n z3NbJPS&Tr)z$nE4G7ZRL@M4sPvx68lplX;H7}_%#SfFa6fHVk90Ai3H2+h2J5n0+>{umUo3Q%e#RDspr3imfVamB1>jfNYSkzLEl1NlCV?QiN}Sf^&XRs)CuG zfu4bq9hZWFf=y9MnpKdC8&o@xXRDM^Qc_^0uU}qXu2*iXmtT~wZ)j<0sc&GUZ)Btk zRH0j3nOBlnp_^B%3^4>|j!SBBa#3bMNoIbY0?6FNr2NtnTO}osMQ{LdXGvxn!lt}p zsJDO~)CbAv8|oS8!_5Y2wE>A*`4?rT0&NDFZ)a!&R*518wZ}#uWI2*!AU*|)0=;U- zWup%dHajlKxQFZv49uFIE{-7{ym!OS`wKgY)aCDP&Go)mq!iOr_2%La<2Vk(c@{y* zk7CzL9Ozcd{-W}tarXZPwWl6Qj~Fd8coaWsn5(KSsbb39{nlx>bzOei_7ZPF4;#yq z8;n2S`@Yxw{c({+kKHP@Tu*J~Te;Ih_@-g=e=Ys#!690#QO`fGK64`9S~pw$Mqaqu z-8Q@T*DkW3w0D;lSbvkZJv=^cvB8`$pXr_#Z*s(?KHFN>{Y-j$`o0&^PUfDuWq)~H z;+}?yy^9jt|L_chp!3zPE6j(N+Sv1SzONKE&VSi4TgFVGP*;~V{hoddSMFB7vQtXF zE9^Et(hbm7d9ujsqiAl3Pe8$5tA823&slD~n||hspiS^)s`>2m&&({I&k2qz-fL!9 z9@_k9Rp*{F^)vpT*m=hHMb5Q3Artd#a+q>0l(cQ1vOhQE>YLnl%Rte};^#8{@YvZ; z%8XKb4=U!q+@ql-vZq$XtL>pd_M&+LGr4b;*=4lzuI&sm+G_W1Te84DwbMSw99yFS<<*%Z^^n(D_=TD@-90v-(+vJjLs5izUP~7typDRv3%-R(YH%2j|c{99lE0V zsAI0Bf%w(hy3{(Ms~@G#EZ(v1M0A9?<0P4+Q;T>~g#311_OE$=T`g_;?%#(FJUUXN z%m1YQ?+urt;~{sa|8j0wAauR{5{pM7>-Lt36WRj`=f$t=t$M!l#iB0_ZUb;_p?K%v>`!$~!Em&>n;@sM$EFv;+h4YOC(c#Ov#|fj7IF3>;}?k+bN+8WTA!W}?KJtsJI!R)HP2og5SqK@ z65phjPg8!`%~dRE$~!gX;foK)LsPiY)6e8LZC^J(Vb9TuHy=3WBshg9cdTl2=8^E5 zoKm^y>BKSh5CMV_vdcOsgKc23BF6*2U FngHx;9)bV> literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon@3x.png b/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..eaf48592a8316995ad9f9468f7eacd45410351e5 GIT binary patch literal 1914 zcmY*a2~<;88h!yph=a70vI$HG9408)Ku|&g1Oizgge^5tBm@W;H3X9&DN|O7Yyt{X ziB%vfi*W&hU>ju-L2#5Jic*we6Pbd5C?1rx&WkcQbKkk||L%Xk@B8n6&pYSk2KbZp zpk`120Q9I7QVt|#~?V>n$J;F4*B9J*Vts+$Th!UwJdJW@f~2$O`wE|0KjUm zdP2NUVoMO zr_pfOCyMwG0nVZaz`O)PF3cH)L1Ex-P#6q`7skZmf=E6aa&Sd}9}tTZaA8>ZGG1i;+2p@IB(5l0$V(9O1o1GnFDFWnC?>$+ z>O!02^Et)5*e{jhMH_8_4WiXMXe7XOL)f4Pl1c(l6s z{~YFXr=PN5t8P#{dh^=cpr#SyTfvWLl}aLpuyp5G^c-_^y(JCU=sWiHY~KTl<^lkYN1$Ezg8a+}`R zq~t$qZmmTyf8XmkOdxP_YLHWNKVSaP`r+w5X5Hm|eQ@tnnAbZ=x@ph`y?nX*@w&^j z=R+a`!mon)Fk#vS>%dqacB8KyrG6JBL~}UwifU#@(Ly_q=xhDJViEH(q||wr=J3PL zX(O?rXqVAJ^8yQ=LZkU$&eealv?$C~1&C5kl4TLI<8KCO28zGKby{&LiGP%LnZ7F+ z^LOeY-_sZv5tgDO*3MP_7;HaDU9{@#%W=B%8^yq-G~^cf`}VU z4eU>1PdwdzIn^n6d){b2?TBw#2mKRDhVl&JxSkMB$yJ{%K@zw|NE9;kS zxyq=VLED}qG{wF~`Wae6BX~Tos_NwiKOn%_blMA?%lmE=<5zrr|ovD4jI~vng zH6K5^AN2^r9X3|%naZ#7G-@5HCseL%bM&T^JNPI6l*h<+GQQYcX^g2eJy1zX@GV&$ z?>8SXsA+#gEE|#`uGFvsgoDOS53=azv1DB*+&M#O&cLmr1w!$|>0ujh?f$V-u4|!Q z{FeTT9;``okgPQct9``d*!gU4HZap*b=iWucJzvDXL*A%dnll>pjN60 zu{(CR<>s+nr;7a$TD{u60r~;AG<24vPnp)Paq+!@6AamJO?o0_d*JUZv+k1vPZjQO z58boDlw}gSkyICq-=HaPVX3ldWtLNb>+Y^Kv7fFtDrH7G94^V2JCI!ej6gk{ zl8YTbG=?lr$v*yYe&0Ji<_b$OmRP^HhB@YT_^(=TBP}7gJC!V0N?X4#Nz3f-Q$SO9ieu9OI;pA*;ukz#~5*TOZSz79VWLQ%PL!!SDQYBt+*r1MOXbEPZTP*nT_Vk zdv_d|EU6$HH<2gIrD*47t*|ABn-ebA&QgsVmjElf?VU*%+iAV$wb6H$G_hp?n#OC* zt7rqLXadQxXip3T(t Image { + let bundle = BundleToken.bundle + guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else { + fatalError("Unable to load image asset named \(name).") + } + return result + } + #endif } internal extension ImageAsset.Image { + @available(iOS 8.0, tvOS 9.0, watchOS 2.0, *) @available(macOS, deprecated, message: "This initializer is unsafe on macOS, please use the ImageAsset.image property") convenience init!(asset: ImageAsset) { diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index d82e45c6f..e53eed47b 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4855,6 +4855,10 @@ public class VectorL10n: NSObject { public static var spaceFeatureUnavailableTitle: String { return VectorL10n.tr("Vector", "space_feature_unavailable_title") } + /// Show all rooms + public static var spaceHomeShowAllRooms: String { + return VectorL10n.tr("Vector", "space_home_show_all_rooms") + } /// Ban from this space public static var spaceParticipantsActionBan: String { return VectorL10n.tr("Vector", "space_participants_action_ban") diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m index a219ab3c9..cdd7d014e 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -1610,7 +1610,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou } MXLogDebug(@"[RecentsDataSource] refreshRoomsSections: Done in %.0fms", [[NSDate date] timeIntervalSinceDate:startDate] * 1000); - MXLogDebug(@"[Spaces] refreshRoomsSections with %ld suggested room", suggestedRoomCellDataArray.count); + MXLogDebug(@"[RecentsDataSource] refreshRoomsSections with %ld suggested room", suggestedRoomCellDataArray.count); return [[RecentsDataSourceState alloc] initWithInvitesCellDataArray:invitesCellDataArray diff --git a/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewController.swift b/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewController.swift index 95f7e7b19..bca282949 100644 --- a/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewController.swift +++ b/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewController.swift @@ -231,6 +231,9 @@ class SpaceDetailViewController: UIViewController { let membersString = membersCount == 1 ? VectorL10n.roomTitleOneMember : VectorL10n.roomTitleMembers("\(membersCount)") self.spaceTypeLabel.text = "\(joinRuleString) · \(membersString)" + let joinRuleIcon = parameters.joinRule == .public ? Asset.Images.spaceTypeIcon : Asset.Images.spacePrivateIcon + self.spaceTypeIconView.image = joinRuleIcon.image + self.inviterIdLabel.text = parameters.inviterId if let inviterId = parameters.inviterId { self.inviterTitleLabel.text = "\(parameters.inviter?.displayname ?? inviterId) invited you" diff --git a/Riot/Modules/Spaces/SpaceList/SpaceListViewCell.swift b/Riot/Modules/Spaces/SpaceList/SpaceListViewCell.swift index d0ea314dd..eaf3a5e09 100644 --- a/Riot/Modules/Spaces/SpaceList/SpaceListViewCell.swift +++ b/Riot/Modules/Spaces/SpaceList/SpaceListViewCell.swift @@ -58,7 +58,7 @@ final class SpaceListViewCell: UITableViewCell, Themable, NibReusable { func fill(with viewData: SpaceListItemViewData) { self.avatarView.fill(with: viewData.avatarViewData) self.titleLabel.text = viewData.title - self.moreButton.isHidden = viewData.spaceId == SpaceListViewModel.Constants.homeSpaceId || viewData.isInvite + self.moreButton.isHidden = viewData.isInvite if viewData.isInvite { self.isBadgeAlert = true self.badgeLabel.isHidden = false diff --git a/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift b/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift index ed11d2df9..3f9924531 100644 --- a/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift +++ b/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift @@ -91,7 +91,8 @@ final class SpaceListViewModel: SpaceListViewModelType { case .moreAction(at: let indexPath, from: let sourceView): let section = self.sections[indexPath.section] switch section { - case .home: break + case .home: + self.coordinatorDelegate?.spaceListViewModel(self, didPressMoreForSpaceWithId: Constants.homeSpaceId, from: sourceView) case .spaces(let viewDataList): let spaceViewData = viewDataList[indexPath.row] self.coordinatorDelegate?.spaceListViewModel(self, didPressMoreForSpaceWithId: spaceViewData.spaceId, from: sourceView) diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuCell.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuCell.swift new file mode 100644 index 000000000..2447ae918 --- /dev/null +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuCell.swift @@ -0,0 +1,21 @@ +// +// 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 SpaceMenuCell: Themable { + func fill(with viewData: SpaceMenuListItemViewData) +} diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift index 6b7a67fa5..ca555e580 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift @@ -19,6 +19,7 @@ import Foundation /// Style of the `SpaceMenuListViewCell` enum SpaceMenuListItemStyle { case normal + case boolean case destructive } @@ -28,4 +29,5 @@ struct SpaceMenuListItemViewData { let style: SpaceMenuListItemStyle let title: String? let icon: UIImage? + var value: Any? } diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.swift index 952d89d4d..86bfe8805 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.swift @@ -17,7 +17,7 @@ import Foundation import Reusable -class SpaceMenuListViewCell: UITableViewCell, Themable, NibReusable { +class SpaceMenuListViewCell: UITableViewCell, SpaceMenuCell, NibReusable { // MARK: - Properties diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.swift new file mode 100644 index 000000000..6cd7a7a39 --- /dev/null +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.swift @@ -0,0 +1,74 @@ +// +// 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 SpaceMenuSwitchViewCell: UITableViewCell, SpaceMenuCell, NibReusable { + + // MARK: - Properties + + @IBOutlet private weak var titleLabel: UILabel! + @IBOutlet private weak var selectionView: UIView! + @IBOutlet private weak var switchView: UISwitch! + + // 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) { + super.setSelected(selected, animated: animated) + + UIView.animate(withDuration: animated ? 0.3 : 0.0) { + self.selectionView.alpha = selected ? 1.0 : 0.0 + } + } + + // MARK: - Public + + func fill(with viewData: SpaceMenuListItemViewData) { + self.titleLabel.text = viewData.title + self.switchView.isOn = (viewData.value as? Bool) ?? false + + guard let theme = self.theme else { + return + } + + if viewData.style == .destructive { + self.titleLabel.textColor = theme.colors.alert + } else { + self.titleLabel.textColor = theme.colors.primaryContent + } + } + + func update(theme: Theme) { + self.theme = theme + self.backgroundColor = theme.colors.background + self.titleLabel.textColor = theme.colors.primaryContent + self.titleLabel.font = theme.fonts.body + self.selectionView.backgroundColor = theme.colors.separator + } +} diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.xib b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.xib new file mode 100644 index 000000000..f652ee4b7 --- /dev/null +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.xib @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift index 8f9710e56..9cb954398 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift @@ -114,6 +114,17 @@ class SpaceMenuViewController: UIViewController { } private func setupViews() { + setupTableView() + + if self.spaceId == SpaceListViewModel.Constants.homeSpaceId { + let avatarViewData = AvatarViewData(matrixItemId: self.spaceId, displayName: nil, avatarUrl: nil, mediaManager: session.mediaManager, fallbackImage: .image(Asset.Images.spaceHomeIcon.image, .center)) + self.avatarView.fill(with: avatarViewData) + self.titleLabel.text = VectorL10n.titleHome + self.subtitleLabel.text = VectorL10n.settingsTitle + + return + } + guard let space = self.session.spaceService.getSpace(withId: self.spaceId), let summary = space.summary else { MXLog.error("[SpaceMenuViewController] setupViews: no space found") return @@ -130,7 +141,6 @@ class SpaceMenuViewController: UIViewController { self.closeButton.layer.masksToBounds = true self.closeButton.layer.cornerRadius = self.closeButton.bounds.height / 2 - setupTableView() } private func setupTableView() { @@ -139,6 +149,7 @@ class SpaceMenuViewController: UIViewController { self.tableView.estimatedRowHeight = Constants.estimatedRowHeight self.tableView.allowsSelection = true self.tableView.register(cellType: SpaceMenuListViewCell.self) + self.tableView.register(cellType: SpaceMenuSwitchViewCell.self) self.tableView.tableFooterView = UIView() } @@ -150,6 +161,8 @@ class SpaceMenuViewController: UIViewController { self.renderLoaded() case .leaveOptions(let displayName, let isAdmin): self.renderLeaveOptions(displayName: displayName, isAdmin: isAdmin) + case .updateItem(let indexPath): + self.tableView.reloadRows(at: [indexPath], with: .fade) case .error(let error): self.render(error: error) case .deselect: @@ -235,12 +248,15 @@ extension SpaceMenuViewController: UITableViewDataSource { } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(for: indexPath, cellType: SpaceMenuListViewCell.self) - let viewData = viewModel.menuItems[indexPath.row] - cell.update(theme: self.theme) - cell.fill(with: viewData) + let cell = viewData.style == .boolean ? tableView.dequeueReusableCell(for: indexPath, cellType: SpaceMenuSwitchViewCell.self) : + tableView.dequeueReusableCell(for: indexPath, cellType: SpaceMenuListViewCell.self) + + if let cell = cell as? SpaceMenuCell { + cell.update(theme: self.theme) + cell.fill(with: viewData) + } return cell } diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift index be6ad2514..cd3c99f9c 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift @@ -22,6 +22,7 @@ class SpaceMenuViewModel: SpaceMenuViewModelType { // MARK: - Enum enum ActionId: String { + case showAllRoomsInHome = "showAllRoomsInHome" case members = "members" case rooms = "rooms" case leave = "leave" @@ -32,12 +33,14 @@ class SpaceMenuViewModel: SpaceMenuViewModelType { weak var coordinatorDelegate: SpaceMenuModelViewModelCoordinatorDelegate? weak var viewDelegate: SpaceMenuViewModelViewDelegate? - var menuItems: [SpaceMenuListItemViewData] = [ - SpaceMenuListItemViewData(actionId: ActionId.members.rawValue, style: .normal, title: VectorL10n.roomDetailsPeople, icon: UIImage(named: "space_menu_members")), - SpaceMenuListItemViewData(actionId: ActionId.rooms.rawValue, style: .normal, title: VectorL10n.spacesExploreRooms, icon: UIImage(named: "space_menu_rooms")), - SpaceMenuListItemViewData(actionId: ActionId.leave.rawValue, style: .destructive, title: VectorL10n.leave, icon: UIImage(named: "space_menu_leave")) + private let spaceMenuItems: [SpaceMenuListItemViewData] = [ + SpaceMenuListItemViewData(actionId: ActionId.members.rawValue, style: .normal, title: VectorL10n.roomDetailsPeople, icon: Asset.Images.spaceMenuMembers.image, value: nil), + SpaceMenuListItemViewData(actionId: ActionId.rooms.rawValue, style: .normal, title: VectorL10n.spacesExploreRooms, icon: Asset.Images.spaceMenuRooms.image, value: nil), + SpaceMenuListItemViewData(actionId: ActionId.leave.rawValue, style: .destructive, title: VectorL10n.leave, icon: Asset.Images.spaceMenuLeave.image, value: nil) ] + var menuItems: [SpaceMenuListItemViewData] = [] + private let session: MXSession private let spaceId: String @@ -46,6 +49,14 @@ class SpaceMenuViewModel: SpaceMenuViewModelType { init(session: MXSession, spaceId: String) { self.session = session self.spaceId = spaceId + + if spaceId != SpaceListViewModel.Constants.homeSpaceId { + self.menuItems = spaceMenuItems + } else { + self.menuItems = [ + SpaceMenuListItemViewData(actionId: ActionId.showAllRoomsInHome.rawValue, style: .boolean, title: VectorL10n.spaceHomeShowAllRooms, icon: nil, value: MXKAppSettings.standard().isShowAllRoomsInHomeEnabled) + ] + } } // MARK: - Public @@ -55,7 +66,7 @@ class SpaceMenuViewModel: SpaceMenuViewModelType { case .dismiss: self.coordinatorDelegate?.spaceMenuViewModelDidDismiss(self) case .selectRow(at: let indexPath): - self.processAction(with: menuItems[indexPath.row].actionId) + self.processAction(with: menuItems[indexPath.row].actionId, at: indexPath) case .leaveSpaceAndKeepRooms: self.leaveSpaceAndKeepRooms() case .leaveSpaceAndLeaveRooms: @@ -65,9 +76,14 @@ class SpaceMenuViewModel: SpaceMenuViewModelType { // MARK: - Private - private func processAction(with actionStringId: String) { + private func processAction(with actionStringId: String, at indexPath: IndexPath) { let actionId = ActionId(rawValue: actionStringId) switch actionId { + case .showAllRoomsInHome: + MXKAppSettings.standard().isShowAllRoomsInHomeEnabled = !MXKAppSettings.standard().isShowAllRoomsInHomeEnabled + self.menuItems[indexPath.row].value = MXKAppSettings.standard().isShowAllRoomsInHomeEnabled + self.viewDelegate?.spaceMenuViewModel(self, didUpdateViewState: .deselect) + self.viewDelegate?.spaceMenuViewModel(self, didUpdateViewState: .updateItem(indexPath)) case .leave: self.leaveSpace() default: diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewState.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewState.swift index 9801ffaf5..0e37aad97 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewState.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewState.swift @@ -21,6 +21,7 @@ enum SpaceMenuViewState { case loading case loaded case deselect + case updateItem(_ indexPath: IndexPath) case leaveOptions(_ displayName: String, _ isAdmin: Bool) case error(Error) } From ce4f7e4d1b2b021ffe35118613df813d4d22ae06 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 10:55:27 +0200 Subject: [PATCH 088/276] Update Riot/Modules/Room/RoomCoordinator.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Modules/Room/RoomCoordinator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index a87a752e9..abb6e7491 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -98,7 +98,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { self.start(with: self.parameters.roomId, completion: completion) } - // Add `roomViewController` to the NavigationRouter, only if it has been explicity set as parameter + // Add `roomViewController` to the NavigationRouter, only if it has been explicitly set as parameter if let navigationRouter = self.parameters.navigationRouter { if navigationRouter.modules.isEmpty == false { navigationRouter.push(self.roomViewController, animated: true, popCompletion: nil) From 6a5d1509e7f66bcfb893b5a29fe0675f0c89ae64 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 10:56:49 +0200 Subject: [PATCH 089/276] Update Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift index 910014d05..9fd7b2d8b 100644 --- a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift @@ -104,9 +104,7 @@ final class RoomCoordinatorBridgePresenter: NSObject { coordinator.toPresentable().dismiss(animated: animated) { self.coordinator = nil - if let completion = completion { - completion() - } + completion?() } } From f56bb88e3f94600b9206e4cd243833c903f4f6b8 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 11:43:51 +0200 Subject: [PATCH 090/276] Update Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift index 9fd7b2d8b..5f108d755 100644 --- a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift @@ -131,7 +131,6 @@ extension RoomCoordinatorBridgePresenter: RoomCoordinatorDelegate { self.delegate?.roomCoordinatorBridgePresenter(self, didSelectRoomWithId: roomId) } - func roomCoordinatorDidLeaveRoom(_ coordinator: RoomCoordinatorProtocol) { self.delegate?.roomCoordinatorBridgePresenterDidLeaveRoom(self) } From eab32e30f6ccd3ad992876ee10d93e2a79ac9e3d Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 11:45:18 +0200 Subject: [PATCH 091/276] Update Riot/Modules/SplitView/SplitViewCoordinator.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Modules/SplitView/SplitViewCoordinator.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Modules/SplitView/SplitViewCoordinator.swift b/Riot/Modules/SplitView/SplitViewCoordinator.swift index 9ebe90c87..408b236ce 100644 --- a/Riot/Modules/SplitView/SplitViewCoordinator.swift +++ b/Riot/Modules/SplitView/SplitViewCoordinator.swift @@ -220,9 +220,9 @@ final class SplitViewCoordinator: NSObject, SplitViewCoordinatorType { } // In our split view configuration is possible to have nested navigation controller (see https://blog.malcolmhall.com/2017/01/27/default-behaviour-of-uisplitviewcontroller-collapsesecondaryviewcontroller/)). - // If the split view controller has one column visible, - // and if the primary navigation controller pop the detail navigation controller. - // In this case the detail navigation controller will be popped but not his content. It means completions will not be called. + // When the split view controller has one column visible with the detail navigation controller nested inside the primary, + // check to see whether the primary navigation controller is popping the detail navigation controller. + // In this case the detail navigation controller will be popped but not its content. It means completions will not be called. if navigationRouter === self.selectedNavigationRouter, let poppedNavigationController = poppedController as? UINavigationController, poppedNavigationController == self.detailNavigationController { From 3a8e2f39a7438d2c071e6abbdf8be0675c372fab Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 11:45:37 +0200 Subject: [PATCH 092/276] Update Riot/Modules/SplitView/SplitViewCoordinator.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Modules/SplitView/SplitViewCoordinator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/SplitView/SplitViewCoordinator.swift b/Riot/Modules/SplitView/SplitViewCoordinator.swift index 408b236ce..745fefefe 100644 --- a/Riot/Modules/SplitView/SplitViewCoordinator.swift +++ b/Riot/Modules/SplitView/SplitViewCoordinator.swift @@ -280,7 +280,7 @@ extension SplitViewCoordinator: UISplitViewControllerDelegate { /// Adjust the primary view controller and incorporate the secondary view controller into the collapsed interface if needed. /// Return false to let the split view controller try to incorporate the secondary view controller's content into the collapsed interface, /// or true to indicate that you do not want the split view controller to do anything with the secondary view controller. - /// Sample case: large iPhone goes from lanscape to portrait. + /// Sample case: large iPhone goes from landscape to portrait. func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool { // If the secondary view is the placeholder screen do not merge the secondary into the primary. From ede0578965af278c0fd0c3411a07a9db8fcefabc Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 11:46:53 +0200 Subject: [PATCH 093/276] Update Riot/Modules/SplitView/SplitViewCoordinator.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Modules/SplitView/SplitViewCoordinator.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Modules/SplitView/SplitViewCoordinator.swift b/Riot/Modules/SplitView/SplitViewCoordinator.swift index 745fefefe..58c9cac5d 100644 --- a/Riot/Modules/SplitView/SplitViewCoordinator.swift +++ b/Riot/Modules/SplitView/SplitViewCoordinator.swift @@ -284,6 +284,7 @@ extension SplitViewCoordinator: UISplitViewControllerDelegate { func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool { // If the secondary view is the placeholder screen do not merge the secondary into the primary. + // Note: In this case, the secondaryViewController will be automatically discarded. if self.isPlaceholderShown(from: secondaryViewController) { return true } From cbcf965aa5981c382b4bc4f52a13f1058d371bfb Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 12:02:06 +0200 Subject: [PATCH 094/276] Update Riot/Routers/NavigationRouter.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Routers/NavigationRouter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Routers/NavigationRouter.swift b/Riot/Routers/NavigationRouter.swift index b4883748c..38f133093 100755 --- a/Riot/Routers/NavigationRouter.swift +++ b/Riot/Routers/NavigationRouter.swift @@ -154,7 +154,7 @@ final class NavigationRouter: NSObject, NavigationRouterType { let controllers = self.navigationController.viewControllers if controllers.count > 1 { - let controllersToPop = controllers[1...controllers.count-1] + let controllersToPop = controllers[1.. Date: Fri, 8 Oct 2021 12:02:54 +0200 Subject: [PATCH 095/276] Update Riot/Routers/NavigationRouter.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Routers/NavigationRouter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Routers/NavigationRouter.swift b/Riot/Routers/NavigationRouter.swift index 38f133093..934acf42e 100755 --- a/Riot/Routers/NavigationRouter.swift +++ b/Riot/Routers/NavigationRouter.swift @@ -175,7 +175,7 @@ final class NavigationRouter: NSObject, NavigationRouterType { let controllersBeforePop = self.navigationController.viewControllers if let controllerIndex = controllersBeforePop.firstIndex(of: controller) { - let controllersToPop = controllersBeforePop[controllerIndex...controllersBeforePop.count-1] + let controllersToPop = controllersBeforePop[controllerIndex.. Date: Fri, 8 Oct 2021 12:08:40 +0200 Subject: [PATCH 096/276] Update Riot/Routers/NavigationRouter.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Routers/NavigationRouter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Routers/NavigationRouter.swift b/Riot/Routers/NavigationRouter.swift index 934acf42e..c1fea2104 100755 --- a/Riot/Routers/NavigationRouter.swift +++ b/Riot/Routers/NavigationRouter.swift @@ -182,7 +182,7 @@ final class NavigationRouter: NSObject, NavigationRouterType { } } - if let controllers = navigationController.popToViewController(module.toPresentable(), animated: animated) { + if let controllers = navigationController.popToViewController(controller, animated: animated) { controllers.forEach { self.didPopViewController($0) } From ccdeaf7ae0cf1e93c1548455a8d32eed107f4eca Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 12:20:19 +0200 Subject: [PATCH 097/276] Update Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- .../FlowCoordinatorTemplate/FlowTemplateCoordinator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift index dbbfa540d..f1c671318 100644 --- a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift +++ b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift @@ -17,7 +17,7 @@ import UIKit @objcMembers -final class FlowTemplateCoordinator: NSObject, FlowTemplateCoordinatorProtocol { +final class FlowTemplateCoordinator: NSObject, FlowTemplateCoordinatorProtocol { // MARK: - Properties From bcd5987167f2eb0310545e6fc8a5400da95e0ffe Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 12:23:37 +0200 Subject: [PATCH 098/276] Update Riot/Modules/TabBar/TabBarCoordinator.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Modules/TabBar/TabBarCoordinator.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index 399680d47..e128d227d 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -348,8 +348,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { coordinator.start() self.add(childCoordinator: coordinator) - self.replaceSplitViewDetails(with: coordinator) { - [weak self] in + self.replaceSplitViewDetails(with: coordinator) { [weak self] in self?.remove(childCoordinator: coordinator) } } From 4ba83c290aa83e5359d123d2083b270030b73bd0 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 13:28:26 +0200 Subject: [PATCH 099/276] Update Riot/Modules/SplitView/SplitViewCoordinator.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Modules/SplitView/SplitViewCoordinator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/SplitView/SplitViewCoordinator.swift b/Riot/Modules/SplitView/SplitViewCoordinator.swift index 58c9cac5d..fd79b0c26 100644 --- a/Riot/Modules/SplitView/SplitViewCoordinator.swift +++ b/Riot/Modules/SplitView/SplitViewCoordinator.swift @@ -329,7 +329,7 @@ extension SplitViewCoordinator: SplitViewMasterPresentableDelegate { // Reset the detail navigation controller with the given detail controller self.detailNavigationRouter?.setRootModule(detailPresentable, popCompletion: popCompletion) - // This will call first UISplitViewControllerDelegate method: `splitViewController(_:showDetail:sender:)`, if implemented, to give the opportunity to customise `UISplitViewController.showDetailViewController(:sender:)` behavior. + // This will call first UISplitViewControllerDelegate method: `splitViewController(_:showDetail:sender:)`, if implemented, to give the opportunity to customise `UISplitViewController.showDetailViewController(:sender:)` behaviour. // - If the split view controller is collpased (one column visible): // The `detailNavigationController` will be pushed on top of the primary navigation controller. // In fact if the primary root controller of a UISplitViewController is a UINavigationController, From 553c6ae5399f3ea04bc2b7349898b6f2c8f28de0 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 13:28:40 +0200 Subject: [PATCH 100/276] Update Riot/Modules/SplitView/SplitViewCoordinator.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Modules/SplitView/SplitViewCoordinator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/SplitView/SplitViewCoordinator.swift b/Riot/Modules/SplitView/SplitViewCoordinator.swift index fd79b0c26..4be39acd1 100644 --- a/Riot/Modules/SplitView/SplitViewCoordinator.swift +++ b/Riot/Modules/SplitView/SplitViewCoordinator.swift @@ -330,7 +330,7 @@ extension SplitViewCoordinator: SplitViewMasterPresentableDelegate { self.detailNavigationRouter?.setRootModule(detailPresentable, popCompletion: popCompletion) // This will call first UISplitViewControllerDelegate method: `splitViewController(_:showDetail:sender:)`, if implemented, to give the opportunity to customise `UISplitViewController.showDetailViewController(:sender:)` behaviour. - // - If the split view controller is collpased (one column visible): + // - If the split view controller is collapsed (one column visible): // The `detailNavigationController` will be pushed on top of the primary navigation controller. // In fact if the primary root controller of a UISplitViewController is a UINavigationController, // it's possible to have nested navigation controllers due to private property `_allowNestedNavigationControllers` set to true (https://blog.malcolmhall.com/2017/01/27/default-behaviour-of-uisplitviewcontroller-collapsesecondaryviewcontroller/). From b567db45c659a39a089c4d6d7b498b01b3754913 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 13:28:46 +0200 Subject: [PATCH 101/276] Update Riot/Modules/SplitView/SplitViewCoordinator.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Modules/SplitView/SplitViewCoordinator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/SplitView/SplitViewCoordinator.swift b/Riot/Modules/SplitView/SplitViewCoordinator.swift index 4be39acd1..b1c6df26b 100644 --- a/Riot/Modules/SplitView/SplitViewCoordinator.swift +++ b/Riot/Modules/SplitView/SplitViewCoordinator.swift @@ -334,7 +334,7 @@ extension SplitViewCoordinator: SplitViewMasterPresentableDelegate { // The `detailNavigationController` will be pushed on top of the primary navigation controller. // In fact if the primary root controller of a UISplitViewController is a UINavigationController, // it's possible to have nested navigation controllers due to private property `_allowNestedNavigationControllers` set to true (https://blog.malcolmhall.com/2017/01/27/default-behaviour-of-uisplitviewcontroller-collapsesecondaryviewcontroller/). - // - Else if the split view controller is not collpased (two column visible) + // - Else if the split view controller is not collapsed (two column visible) // It will set the `detailNavigationController` as the secondary view of the split view controller self.splitViewController.showDetailViewController(detailNavigationController, sender: nil) From 0dff280edfc48589330a1edbe9737c2930b143f6 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 8 Oct 2021 14:43:19 +0300 Subject: [PATCH 102/276] Adapt api changes --- .../Common/Recents/DataSources/RecentsDataSource.m | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m index e98b7e967..e1526e4e4 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -722,26 +722,26 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou counts = self.recentsListService.suggestedRoomListData.counts; } - if (counts.totalNotificationCount) + if (counts.numberOfNotifications) { UILabel *missedNotifAndUnreadBadgeLabel = [[UILabel alloc] init]; missedNotifAndUnreadBadgeLabel.textColor = ThemeService.shared.theme.baseTextPrimaryColor; missedNotifAndUnreadBadgeLabel.font = [UIFont boldSystemFontOfSize:14]; - if (counts.totalNotificationCount > 1000) + if (counts.numberOfNotifications > 1000) { - CGFloat value = counts.totalNotificationCount / 1000.0; + CGFloat value = counts.numberOfNotifications / 1000.0; missedNotifAndUnreadBadgeLabel.text = [VectorL10n largeBadgeValueKFormat:value]; } else { - missedNotifAndUnreadBadgeLabel.text = [NSString stringWithFormat:@"%tu", counts.totalNotificationCount]; + missedNotifAndUnreadBadgeLabel.text = [NSString stringWithFormat:@"%tu", counts.numberOfNotifications]; } [missedNotifAndUnreadBadgeLabel sizeToFit]; CGFloat bgViewWidth = missedNotifAndUnreadBadgeLabel.frame.size.width + 18; - BOOL highlight = counts.totalHighlightCount > 0; + BOOL highlight = counts.numberOfHighlights > 0; missedNotifAndUnreadBadgeBgView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, bgViewWidth, 20)]; [missedNotifAndUnreadBadgeBgView.layer setCornerRadius:10]; missedNotifAndUnreadBadgeBgView.backgroundColor = highlight ? ThemeService.shared.theme.noticeColor : ThemeService.shared.theme.noticeSecondaryColor; From 69578ba0b9adf36fe4f441ae246c0d8fefe5515e Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 13:46:33 +0200 Subject: [PATCH 103/276] TabBarCoordinator: Fix some typos. --- Riot/Modules/SplitView/SplitViewCoordinator.swift | 6 +++--- Riot/Modules/TabBar/TabBarCoordinator.swift | 11 +++++------ Riot/Routers/NavigationRouter.swift | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Riot/Modules/SplitView/SplitViewCoordinator.swift b/Riot/Modules/SplitView/SplitViewCoordinator.swift index b1c6df26b..58c9cac5d 100644 --- a/Riot/Modules/SplitView/SplitViewCoordinator.swift +++ b/Riot/Modules/SplitView/SplitViewCoordinator.swift @@ -329,12 +329,12 @@ extension SplitViewCoordinator: SplitViewMasterPresentableDelegate { // Reset the detail navigation controller with the given detail controller self.detailNavigationRouter?.setRootModule(detailPresentable, popCompletion: popCompletion) - // This will call first UISplitViewControllerDelegate method: `splitViewController(_:showDetail:sender:)`, if implemented, to give the opportunity to customise `UISplitViewController.showDetailViewController(:sender:)` behaviour. - // - If the split view controller is collapsed (one column visible): + // This will call first UISplitViewControllerDelegate method: `splitViewController(_:showDetail:sender:)`, if implemented, to give the opportunity to customise `UISplitViewController.showDetailViewController(:sender:)` behavior. + // - If the split view controller is collpased (one column visible): // The `detailNavigationController` will be pushed on top of the primary navigation controller. // In fact if the primary root controller of a UISplitViewController is a UINavigationController, // it's possible to have nested navigation controllers due to private property `_allowNestedNavigationControllers` set to true (https://blog.malcolmhall.com/2017/01/27/default-behaviour-of-uisplitviewcontroller-collapsesecondaryviewcontroller/). - // - Else if the split view controller is not collapsed (two column visible) + // - Else if the split view controller is not collpased (two column visible) // It will set the `detailNavigationController` as the secondary view of the split view controller self.splitViewController.showDetailViewController(detailNavigationController, sender: nil) diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index e128d227d..fad98dda4 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -95,14 +95,13 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { self.setupSideMenuGestures() } - self.registerUserSessionsServiceNotifications() self.registerUserSessionsServiceNotifications() self.registerSessionChange() if let homeViewController = homeViewControllerWrapperViewController { let versionCheckCoordinator = VersionCheckCoordinator(rootViewController: masterTabBarController, - bannerPresenter: homeViewController, - themeService: ThemeService.shared()) + bannerPresenter: homeViewController, + themeService: ThemeService.shared()) versionCheckCoordinator.start() add(childCoordinator: versionCheckCoordinator) } @@ -377,9 +376,9 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { private func showRoom(with roomId: String, eventId: String?, matrixSession: MXSession, completion: (() -> Void)? = nil) { - // 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 + // RoomCoordinator will be presented by the split view. + // Ass 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) self.showRoom(with: roomCoordinatorParameters, completion: completion) diff --git a/Riot/Routers/NavigationRouter.swift b/Riot/Routers/NavigationRouter.swift index c1fea2104..8d9761079 100755 --- a/Riot/Routers/NavigationRouter.swift +++ b/Riot/Routers/NavigationRouter.swift @@ -320,7 +320,7 @@ extension NavigationRouter: UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { - // TODO: Try to post `NavigationRouter.willPopViewController` notification here + // TODO: Try to post `NavigationRouter.willPopModule` notification here } func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { From 2e0646fa54ed98e4d574101b177e53c5af6cbae1 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 13:47:40 +0200 Subject: [PATCH 104/276] RoomCoordinatorProtocol: Update comment description. --- Riot/Modules/Room/RoomCoordinatorProtocol.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Room/RoomCoordinatorProtocol.swift b/Riot/Modules/Room/RoomCoordinatorProtocol.swift index afb7d6d31..1c30f02ec 100644 --- a/Riot/Modules/Room/RoomCoordinatorProtocol.swift +++ b/Riot/Modules/Room/RoomCoordinatorProtocol.swift @@ -25,7 +25,7 @@ protocol RoomCoordinatorDelegate: AnyObject { func roomCoordinatorDidDismissInteractively(_ coordinator: RoomCoordinatorProtocol) } -/// `RoomCoordinatorProtocol` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow. +/// `RoomCoordinatorProtocol` is a protocol describing a Coordinator that handle room navigation flow. protocol RoomCoordinatorProtocol: Coordinator, Presentable, RoomIdentifiable { var delegate: RoomCoordinatorDelegate? { get } From 481d84d1e4d384d2eddf1502deff944ba05be081 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 8 Oct 2021 12:50:39 +0100 Subject: [PATCH 105/276] Update Analytics from Steve's comments. --- .../Modal/ServiceTermsModalCoordinator.swift | 20 ++++++++++++++++++- ...TermsModalCoordinatorBridgePresenter.swift | 20 ++----------------- .../ServiceTermsModalCoordinatorType.swift | 1 + 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift index c478d944d..4b05e230c 100644 --- a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift +++ b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift @@ -19,7 +19,7 @@ import UIKit @objcMembers -final class ServiceTermsModalCoordinator: ServiceTermsModalCoordinatorType { +final class ServiceTermsModalCoordinator: NSObject, ServiceTermsModalCoordinatorType { // MARK: - Properties @@ -51,6 +51,8 @@ final class ServiceTermsModalCoordinator: ServiceTermsModalCoordinatorType { rootCoordinator.start() self.add(childCoordinator: rootCoordinator) + + self.toPresentable().presentationController?.delegate = self self.navigationRouter.setRootModule(rootCoordinator) } @@ -104,6 +106,10 @@ final class ServiceTermsModalCoordinator: ServiceTermsModalCoordinatorType { extension ServiceTermsModalCoordinator: ServiceTermsModalScreenCoordinatorDelegate { func serviceTermsModalScreenCoordinatorDidAccept(_ coordinator: ServiceTermsModalScreenCoordinatorType) { + if serviceTerms.serviceType == MXServiceTypeIdentityService { + Analytics.sharedInstance().trackValue(1, category: MXKAnalyticsCategory.contacts.rawValue, name: AnalyticsContactsIdentityServerAccepted) + } + self.delegate?.serviceTermsModalCoordinatorDidAccept(self) } @@ -113,9 +119,21 @@ extension ServiceTermsModalCoordinator: ServiceTermsModalScreenCoordinatorDelega func serviceTermsModalScreenCoordinatorDidDecline(_ coordinator: ServiceTermsModalScreenCoordinatorType) { if serviceTerms.serviceType == MXServiceTypeIdentityService { + Analytics.sharedInstance().trackValue(1, category: MXKAnalyticsCategory.contacts.rawValue, name: AnalyticsContactsIdentityServerAccepted) disableIdentityServer() } self.delegate?.serviceTermsModalCoordinatorDidDecline(self) } } + +// MARK: - UIAdaptivePresentationControllerDelegate +extension ServiceTermsModalCoordinator: UIAdaptivePresentationControllerDelegate { + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + if serviceTerms.serviceType == MXServiceTypeIdentityService { + Analytics.sharedInstance().trackValue(0, category: MXKAnalyticsCategory.contacts.rawValue, name: AnalyticsContactsIdentityServerAccepted) + } + + self.delegate?.serviceTermsModalCoordinatorDidDismissInteractively(self) + } +} diff --git a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorBridgePresenter.swift b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorBridgePresenter.swift index 8662a4d58..aac545e3e 100644 --- a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorBridgePresenter.swift +++ b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorBridgePresenter.swift @@ -68,7 +68,6 @@ final class ServiceTermsModalCoordinatorBridgePresenter: NSObject { let serviceTermsModalCoordinator = ServiceTermsModalCoordinator(session: self.session, baseUrl: self.baseUrl, serviceType: self.serviceType, accessToken: accessToken) serviceTermsModalCoordinator.delegate = self let presentable = serviceTermsModalCoordinator.toPresentable() - presentable.presentationController?.delegate = self viewController.present(presentable, animated: animated, completion: nil) serviceTermsModalCoordinator.start() @@ -98,28 +97,13 @@ extension ServiceTermsModalCoordinatorBridgePresenter: ServiceTermsModalCoordina func serviceTermsModalCoordinatorDidAccept(_ coordinator: ServiceTermsModalCoordinatorType) { self.delegate?.serviceTermsModalCoordinatorBridgePresenterDelegateDidAccept(self) - - if serviceType == MXServiceTypeIdentityService { - Analytics.sharedInstance().trackValue(1, category: kMXKAnalyticsContactsCategory, name: AnalyticsContactsIdentityServerAccepted) - } } func serviceTermsModalCoordinatorDidDecline(_ coordinator: ServiceTermsModalCoordinatorType) { self.delegate?.serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline(self, session: self.session) - - if serviceType == MXServiceTypeIdentityService { - Analytics.sharedInstance().trackValue(0, category: kMXKAnalyticsContactsCategory, name: AnalyticsContactsIdentityServerAccepted) - } } -} - -// MARK: - UIAdaptivePresentationControllerDelegate -extension ServiceTermsModalCoordinatorBridgePresenter: UIAdaptivePresentationControllerDelegate { - func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + + func serviceTermsModalCoordinatorDidDismissInteractively(_ coordinator: ServiceTermsModalCoordinatorType) { self.delegate?.serviceTermsModalCoordinatorBridgePresenterDelegateDidClose(self) - - if serviceType == MXServiceTypeIdentityService { - Analytics.sharedInstance().trackValue(0, category: kMXKAnalyticsContactsCategory, name: AnalyticsContactsIdentityServerAccepted) - } } } diff --git a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorType.swift b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorType.swift index ceb9699c8..fe9081c39 100644 --- a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorType.swift +++ b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorType.swift @@ -21,6 +21,7 @@ import Foundation protocol ServiceTermsModalCoordinatorDelegate: AnyObject { func serviceTermsModalCoordinatorDidAccept(_ coordinator: ServiceTermsModalCoordinatorType) func serviceTermsModalCoordinatorDidDecline(_ coordinator: ServiceTermsModalCoordinatorType) + func serviceTermsModalCoordinatorDidDismissInteractively(_ coordinator: ServiceTermsModalCoordinatorType) } /// `ServiceTermsModalCoordinatorType` is a protocol describing a Coordinator that handle keybackup setup navigation flow. From 8018c772a141ebe72ca921a5527a012391783543 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 14:34:42 +0200 Subject: [PATCH 106/276] GroupDetailsViewController: Update initializer. --- .../Communities/TabDetail/GroupDetailsViewController.h | 2 -- .../Communities/TabDetail/GroupDetailsViewController.m | 7 +------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.h b/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.h index 4bd684892..e228101d4 100644 --- a/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.h +++ b/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.h @@ -35,8 +35,6 @@ @discussion This is the designated initializer for programmatic instantiation. @return An initialized `GroupDetailsViewController` object if successful, `nil` otherwise. */ -+ (instancetype)groupDetailsViewController; - + (instancetype)instantiate; /** diff --git a/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m b/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m index 9c93bb246..c36120cc4 100644 --- a/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m +++ b/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m @@ -55,17 +55,12 @@ bundle:[NSBundle bundleForClass:self.class]]; } -+ (instancetype)groupDetailsViewController ++ (instancetype)instantiate { return [[[self class] alloc] initWithNibName:NSStringFromClass(self.class) bundle:[NSBundle bundleForClass:self.class]]; } -+ (instancetype)instantiate -{ - return [self groupDetailsViewController]; -} - #pragma mark - - (void)finalizeInit From c7ab8b7eedd1d6931a471bf62315df643e117f9c Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 15:08:58 +0200 Subject: [PATCH 107/276] ContactDetailsViewController: Update initializer. --- Riot/Modules/Communities/Home/GroupHomeViewController.m | 2 +- .../Communities/Members/GroupParticipantsViewController.m | 2 +- .../Contacts/Details/ContactDetailsViewController.h | 2 -- .../Contacts/Details/ContactDetailsViewController.m | 7 +------ 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/Riot/Modules/Communities/Home/GroupHomeViewController.m b/Riot/Modules/Communities/Home/GroupHomeViewController.m index 3f0d8ac5f..5fbfbf82e 100644 --- a/Riot/Modules/Communities/Home/GroupHomeViewController.m +++ b/Riot/Modules/Communities/Home/GroupHomeViewController.m @@ -807,7 +807,7 @@ contact = [[MXKContact alloc] initMatrixContactWithDisplayName:userId andMatrixID:userId]; } - ContactDetailsViewController *contactDetailsViewController = [ContactDetailsViewController contactDetailsViewController]; + ContactDetailsViewController *contactDetailsViewController = [ContactDetailsViewController instantiate]; contactDetailsViewController.enableVoipCall = NO; contactDetailsViewController.contact = contact; diff --git a/Riot/Modules/Communities/Members/GroupParticipantsViewController.m b/Riot/Modules/Communities/Members/GroupParticipantsViewController.m index 34aa57c48..f46a9cf72 100644 --- a/Riot/Modules/Communities/Members/GroupParticipantsViewController.m +++ b/Riot/Modules/Communities/Members/GroupParticipantsViewController.m @@ -964,7 +964,7 @@ if (contact) { - ContactDetailsViewController *contactDetailsViewController = [ContactDetailsViewController contactDetailsViewController]; + ContactDetailsViewController *contactDetailsViewController = [ContactDetailsViewController instantiate]; contactDetailsViewController.enableVoipCall = NO; contactDetailsViewController.contact = contact; diff --git a/Riot/Modules/Contacts/Details/ContactDetailsViewController.h b/Riot/Modules/Contacts/Details/ContactDetailsViewController.h index 1e5fff596..a41a8df84 100644 --- a/Riot/Modules/Contacts/Details/ContactDetailsViewController.h +++ b/Riot/Modules/Contacts/Details/ContactDetailsViewController.h @@ -69,8 +69,6 @@ typedef enum : NSUInteger @discussion This is the designated initializer for programmatic instantiation. @return An initialized `ContactDetailsViewController` object if successful, `nil` otherwise. */ -+ (instancetype)contactDetailsViewController; - + (instancetype)instantiate; @end diff --git a/Riot/Modules/Contacts/Details/ContactDetailsViewController.m b/Riot/Modules/Contacts/Details/ContactDetailsViewController.m index 04f251b39..e26c94ab1 100644 --- a/Riot/Modules/Contacts/Details/ContactDetailsViewController.m +++ b/Riot/Modules/Contacts/Details/ContactDetailsViewController.m @@ -98,17 +98,12 @@ bundle:[NSBundle bundleForClass:self.class]]; } -+ (instancetype)contactDetailsViewController ++ (instancetype)instantiate { return [[[self class] alloc] initWithNibName:NSStringFromClass(self.class) bundle:[NSBundle bundleForClass:self.class]]; } -+ (instancetype)instantiate -{ - return [self contactDetailsViewController]; -} - #pragma mark - - (void)finalizeInit From a299bcb51a721890ac6f89ad9dc46dfd419f8b6c Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 15:45:53 +0200 Subject: [PATCH 108/276] Array: Update convenient method signature. --- Riot/Categories/Array.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Categories/Array.swift b/Riot/Categories/Array.swift index 237397b65..88cf73e1f 100644 --- a/Riot/Categories/Array.swift +++ b/Riot/Categories/Array.swift @@ -20,7 +20,7 @@ extension Array where Element: Equatable { /// Remove first collection element that is equal to the given `object` /// Credits: https://stackoverflow.com/a/45008042 - mutating func vc_remove(object: Element) { + mutating func vc_removeFirstOccurrence(of object: Element) { guard let index = firstIndex(of: object) else { return } From f84a9dff5574630b9436617fad2f387904407001 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 8 Oct 2021 16:52:09 +0300 Subject: [PATCH 109/276] Remove old code piece --- .../Recents/DataSources/RecentsDataSource.m | 50 ------------------- 1 file changed, 50 deletions(-) diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m index e1526e4e4..2299783fa 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -1188,56 +1188,6 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou } #pragma mark - MXKDataSourceDelegate - -//if (recentsDataSourceMode == RecentsDataSourceModeHome) -//{ -// -// else if (recentCellDataStoring.isSuggestedRoom && recentCellDataStoring.spaceChildInfo.roomType != MXRoomTypeSpace) -// { -// MXRoomSummary *roomSummary = [mxSession roomSummaryWithRoomId:recentCellDataStoring.spaceChildInfo.childRoomId]; -// if (!roomSummary.isJoined) -// { -// [suggestedRoomCellDataArray addObject:recentCellDataStoring]; -// } -// } -// else -// { -// // Hide spaces from home (keep space invites) -// if (room.summary.roomType != MXRoomTypeSpace) -// { -// [conversationCellDataArray addObject:recentCellDataStoring]; -// } -// } -//} -// -//else if (recentsDataSourceMode == RecentsDataSourceModeRooms) -//{ -// if (recentCellDataStoring.isSuggestedRoom && recentCellDataStoring.spaceChildInfo.roomType != MXRoomTypeSpace) -// { -// MXRoomSummary *roomSummary = [mxSession roomSummaryWithRoomId:recentCellDataStoring.spaceChildInfo.childRoomId]; -// BOOL isJoined = roomSummary.membership == MXMembershipJoin || roomSummary.membershipTransitionState == MXMembershipTransitionStateJoined; -// if (!isJoined) -// { -// [suggestedRoomCellDataArray addObject:recentCellDataStoring]; -// } -// } -// // Consider only non direct rooms. -// else if (!room.isDirect) -// { -// // Keep only the invites, the favourites and the rooms without tag and room type different from space -// if (room.summary.membership == MXMembershipInvite) -// { -// if (room.summary.roomType != MXRoomTypeSpace && !MXSDKOptions.sharedInstance.autoAcceptRoomInvites) -// { -// [invitesCellDataArray addObject:recentCellDataStoring]; -// } -// } -// else if ((!room.accountData.tags.count || room.accountData.tags[kMXRoomTagFavourite]) && room.summary.roomType != MXRoomTypeSpace) -// { -// [conversationCellDataArray addObject:recentCellDataStoring]; -// } -// } -//} - (void)dataSource:(MXKDataSource*)dataSource didCellChange:(id)changes { From d144bb5bad9ba0c3ceb2ce1356afebec12641c7c Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 8 Oct 2021 16:52:56 +0300 Subject: [PATCH 110/276] Update Riot/Modules/Common/Recents/Model/DiscussionsCount.swift Co-authored-by: manuroe --- Riot/Modules/Common/Recents/Model/DiscussionsCount.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Common/Recents/Model/DiscussionsCount.swift b/Riot/Modules/Common/Recents/Model/DiscussionsCount.swift index 7d8e751ff..ee80cc52f 100644 --- a/Riot/Modules/Common/Recents/Model/DiscussionsCount.swift +++ b/Riot/Modules/Common/Recents/Model/DiscussionsCount.swift @@ -25,7 +25,7 @@ public class DiscussionsCount: NSObject { /// Number of highlighted rooms with mentions like notications public let numberOfHighlighted: Int - /// Number of rooms that has unsent messages in it + /// Number of rooms that have unsent messages in it public let numberOfUnsent: Int /// Flag indicating is there any unsent From 96b206eb997d7e5a7370f2fab23fab03058890e8 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 8 Oct 2021 16:57:54 +0300 Subject: [PATCH 111/276] Do not expose session from the protocol --- .../Common/Recents/Service/MatrixSDK/RecentsListService.swift | 2 +- .../Common/Recents/Service/RecentsListServiceProtocol.swift | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Riot/Modules/Common/Recents/Service/MatrixSDK/RecentsListService.swift b/Riot/Modules/Common/Recents/Service/MatrixSDK/RecentsListService.swift index 0a61470fe..1f48a3135 100644 --- a/Riot/Modules/Common/Recents/Service/MatrixSDK/RecentsListService.swift +++ b/Riot/Modules/Common/Recents/Service/MatrixSDK/RecentsListService.swift @@ -19,7 +19,7 @@ import Foundation @objcMembers public class RecentsListService: NSObject, RecentsListServiceProtocol { - public private(set) weak var session: MXSession? + private weak var session: MXSession? public private(set) var mode: RecentsDataSourceMode public private(set) var query: String? public private(set) var space: MXSpace? diff --git a/Riot/Modules/Common/Recents/Service/RecentsListServiceProtocol.swift b/Riot/Modules/Common/Recents/Service/RecentsListServiceProtocol.swift index b3ca9a90a..154cf2e21 100644 --- a/Riot/Modules/Common/Recents/Service/RecentsListServiceProtocol.swift +++ b/Riot/Modules/Common/Recents/Service/RecentsListServiceProtocol.swift @@ -21,9 +21,6 @@ public protocol RecentsListServiceProtocol { // MARK: - Properties - /// Session instance, Implementations encouraged to hold a weak reference - var session: MXSession? { get } - /// Current mode var mode: RecentsDataSourceMode { get } From 546c92ff4274ea994c8a84986776ea4d95049419 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 16:00:56 +0200 Subject: [PATCH 112/276] RoomCoordinator: Refactor room loading methods. --- Riot/Modules/Room/RoomCoordinator.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index abb6e7491..73720546c 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -93,9 +93,9 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { self.roomViewController.presentationController?.delegate = self if let eventId = self.selectedEventId { - self.start(with: self.parameters.roomId, and: eventId, completion: completion) + self.loadRoom(withId: self.parameters.roomId, and: eventId, completion: completion) } else { - self.start(with: self.parameters.roomId, completion: completion) + self.loadRoom(withId: self.parameters.roomId, completion: completion) } // Add `roomViewController` to the NavigationRouter, only if it has been explicitly set as parameter @@ -113,7 +113,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { self.selectedEventId = eventId if self.hasStartedOnce { - self.start(with: self.parameters.roomId, and: eventId, completion: completion) + self.loadRoom(withId: self.parameters.roomId, and: eventId, completion: completion) } else { self.start(withCompletion: completion) } @@ -125,7 +125,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { // MARK: - Private - private func start(with roomId: String, completion: (() -> Void)?) { + private func loadRoom(withId roomId: String, completion: (() -> Void)?) { // Present activity indicator when retrieving roomDataSource for given room ID self.activityIndicatorPresenter.presentActivityIndicator(on: roomViewController.view, animated: false) @@ -149,7 +149,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { }) } - private func start(with roomId: String, and eventId: String, completion: (() -> Void)?) { + private func loadRoom(withId roomId: String, and eventId: String, completion: (() -> Void)?) { // Present activity indicator when retrieving roomDataSource for given room ID self.activityIndicatorPresenter.presentActivityIndicator(on: roomViewController.view, animated: false) From 252ee400325bd24fb30674718e62183f5a4c88a2 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 16:06:23 +0200 Subject: [PATCH 113/276] Update MasterTabBarController. --- Riot/Modules/TabBar/MasterTabBarController.m | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m index 84c3098b6..aba1aef37 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -591,7 +591,7 @@ [self.masterTabBarDelegate masterTabBarController:self didSelectRoomWithId:roomId andEventId:eventId inMatrixSession:matrixSession completion:completion]; - [self refreshSelectedControllerIfNeeded]; + [self refreshSelectedControllerSelectedCellIfNeeded]; } - (void)showRoomPreview:(RoomPreviewData *)roomPreviewData @@ -604,7 +604,7 @@ [self.masterTabBarDelegate masterTabBarController:self didSelectRoomPreviewWithData:roomPreviewData]; - [self refreshSelectedControllerIfNeeded]; + [self refreshSelectedControllerSelectedCellIfNeeded]; } - (void)selectContact:(MXKContact*)contact @@ -615,7 +615,7 @@ [self.masterTabBarDelegate masterTabBarController:self didSelectContact:contact]; - [self refreshSelectedControllerIfNeeded]; + [self refreshSelectedControllerSelectedCellIfNeeded]; } - (void)selectGroup:(MXGroup*)group inMatrixSession:(MXSession*)matrixSession @@ -627,7 +627,7 @@ [self.masterTabBarDelegate masterTabBarController:self didSelectGroup:group inMatrixSession:matrixSession]; - [self refreshSelectedControllerIfNeeded]; + [self refreshSelectedControllerSelectedCellIfNeeded]; } - (void)releaseSelectedItem @@ -742,7 +742,7 @@ [super presentViewController:viewControllerToPresent animated:flag completion:completion]; } -- (void)refreshSelectedControllerIfNeeded +- (void)refreshSelectedControllerSelectedCellIfNeeded { if (self.splitViewController) { From 15a93fcc6ab8c9780d8bd6a0141ceb4f581e735c Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 16:25:14 +0200 Subject: [PATCH 114/276] NavigationRouter: Fix issues when setting the same presentable instances. --- Riot/Routers/NavigationRouter.swift | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Riot/Routers/NavigationRouter.swift b/Riot/Routers/NavigationRouter.swift index 8d9761079..075dd4e92 100755 --- a/Riot/Routers/NavigationRouter.swift +++ b/Riot/Routers/NavigationRouter.swift @@ -106,12 +106,15 @@ final class NavigationRouter: NSObject, NavigationRouterType { navigationController.setViewControllers([controller], animated: animated) navigationController.isNavigationBarHidden = hideNavigationBar - self.didPushViewController(controller) - // Pop old view controllers controllersToPop.forEach { self.didPopViewController($0) } + + // Add again controller to module association, in case same module instance is added back + self.addModule(module, for: controller) + + self.didPushViewController(controller) } func setModules(_ modules: [Presentable], hideNavigationBar: Bool, animated: Bool) { @@ -138,14 +141,19 @@ final class NavigationRouter: NSObject, NavigationRouterType { navigationController.setViewControllers(controllers, animated: animated) navigationController.isNavigationBarHidden = hideNavigationBar - controllers.forEach { - self.didPushViewController($0) - } - // Pop old view controllers controllersToPop.forEach { self.didPopViewController($0) } + + // Add again controller to module association, in case same modules instance are added back + modules.forEach { (presentable) in + self.addModule(presentable, for: presentable.toPresentable()) + } + + controllers.forEach { + self.didPushViewController($0) + } } func popToRootModule(animated: Bool) { From e50143addcfe7393d9d3117f259219f34dd5e2be Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 16:39:09 +0200 Subject: [PATCH 115/276] Update Riot/Modules/SplitView/SplitViewCoordinator.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Modules/SplitView/SplitViewCoordinator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/SplitView/SplitViewCoordinator.swift b/Riot/Modules/SplitView/SplitViewCoordinator.swift index 58c9cac5d..aa5e74b89 100644 --- a/Riot/Modules/SplitView/SplitViewCoordinator.swift +++ b/Riot/Modules/SplitView/SplitViewCoordinator.swift @@ -345,7 +345,7 @@ extension SplitViewCoordinator: SplitViewMasterPresentableDelegate { func splitViewMasterPresentable(_ presentable: Presentable, wantsToStack detailPresentable: Presentable, popCompletion: (() -> Void)?) { guard let detailNavigationRouter = self.detailNavigationRouter else { - MXLog.debug("Failed to stack \(detailPresentable) because detailNavigationRouter is nil") + MXLog.debug("[SplitViewCoordinator] Failed to stack \(detailPresentable) because detailNavigationRouter is nil") return } From 00fd7e26cec0f54c7f6e5d499f747e0320d7d178 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 16:39:49 +0200 Subject: [PATCH 116/276] Update Riot/Modules/SplitView/SplitViewCoordinator.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Modules/SplitView/SplitViewCoordinator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/SplitView/SplitViewCoordinator.swift b/Riot/Modules/SplitView/SplitViewCoordinator.swift index aa5e74b89..1fd948dd0 100644 --- a/Riot/Modules/SplitView/SplitViewCoordinator.swift +++ b/Riot/Modules/SplitView/SplitViewCoordinator.swift @@ -199,7 +199,7 @@ final class SplitViewCoordinator: NSObject, SplitViewCoordinatorType { } guard existingRoomCoordinatorWithSameRoomId == nil else { - MXLog.debug("Do not release RoomDataSource for room id \(roomId), another RoomCoordinator with same room id using it") + MXLog.debug("[SplitViewCoordinator] Do not release RoomDataSource for room id \(roomId), another RoomCoordinator with same room id using it") return } From ab613c38892916426eac5c593c774720d4e97ba3 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 17:03:45 +0200 Subject: [PATCH 117/276] Update Riot/Routers/NavigationRouterStore.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Routers/NavigationRouterStore.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Riot/Routers/NavigationRouterStore.swift b/Riot/Routers/NavigationRouterStore.swift index 6e8ab5344..c1b4e96c4 100644 --- a/Riot/Routers/NavigationRouterStore.swift +++ b/Riot/Routers/NavigationRouterStore.swift @@ -39,7 +39,8 @@ class NavigationRouterStore: NavigationRouterStoreProtocol { // MARK: - Public - func getOrCreateNavigationRouter(for navigationController: UINavigationController) -> NavigationRouterType { + /// Gets the existing navigation router for the supplied controller, creating a new one if it doesn't yet exist. + func navigationRouter(for navigationController: UINavigationController) -> NavigationRouterType { if let existingNavigationRouter = self.getNavigationRouter(for: navigationController) { return existingNavigationRouter From 004d73889806af909934b512c18a977f10b22a78 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 17:15:14 +0200 Subject: [PATCH 118/276] NavigationStore: Update public method signature. --- ...KeyBackupRecoverCoordinatorBridgePresenter.swift | 2 +- .../KeyVerificationCoordinatorBridgePresenter.swift | 2 +- Riot/Modules/Room/RoomCoordinator.swift | 2 +- .../Room/RoomCoordinatorBridgePresenter.swift | 2 +- .../RoomInfoCoordinatorBridgePresenter.swift | 2 +- ...yThreePidDetailsCoordinatorBridgePresenter.swift | 2 +- ...gsIdentityServerCoordinatorBridgePresenter.swift | 2 +- Riot/Routers/NavigationRouterStore.swift | 13 ++++++------- Riot/Routers/NavigationRouterStoreProtocol.swift | 5 +++-- .../NotificationSettingsBridgePresenter.swift | 2 +- .../FlowTemplateCoordinatorBridgePresenter.swift | 2 +- 11 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinatorBridgePresenter.swift b/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinatorBridgePresenter.swift index 3bc7d2c6f..a06e9befd 100644 --- a/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinatorBridgePresenter.swift +++ b/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinatorBridgePresenter.swift @@ -61,7 +61,7 @@ final class KeyBackupRecoverCoordinatorBridgePresenter: NSObject { MXLog.debug("[KeyBackupRecoverCoordinatorBridgePresenter] Push complete security from \(navigationController)") - let navigationRouter = NavigationRouterStore.shared.getOrCreateNavigationRouter(for: navigationController) + let navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController) let keyBackupSetupCoordinator = KeyBackupRecoverCoordinator(session: self.session, keyBackupVersion: keyBackupVersion, navigationRouter: navigationRouter) keyBackupSetupCoordinator.delegate = self diff --git a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinatorBridgePresenter.swift b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinatorBridgePresenter.swift index 8fb3fb0c8..5d8ead04c 100644 --- a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinatorBridgePresenter.swift +++ b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinatorBridgePresenter.swift @@ -101,7 +101,7 @@ final class KeyVerificationCoordinatorBridgePresenter: NSObject { MXLog.debug("[KeyVerificationCoordinatorBridgePresenter] Push complete security from \(navigationController)") - let navigationRouter = NavigationRouterStore.shared.getOrCreateNavigationRouter(for: navigationController) + let navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController) let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .completeSecurity(isNewSignIn), navigationRouter: navigationRouter) keyVerificationCoordinator.delegate = self diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index 73720546c..ce8f41ef7 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -47,7 +47,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { finalNavigationRouter = navigationRouter } else if let navigationRouterStore = self.parameters.navigationRouterStore, let currentNavigationController = self.roomViewController.navigationController { // If no navigationRouter has been provided, try to get the navigation router from the current RoomViewController navigation controller if exists - finalNavigationRouter = navigationRouterStore.getOrCreateNavigationRouter(for: currentNavigationController) + finalNavigationRouter = navigationRouterStore.navigationRouter(for: currentNavigationController) } return finalNavigationRouter diff --git a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift index 5f108d755..d6ec2aa4e 100644 --- a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift @@ -88,7 +88,7 @@ final class RoomCoordinatorBridgePresenter: NSObject { func push(from navigationController: UINavigationController, animated: Bool) { - let navigationRouter = NavigationRouterStore.shared.getOrCreateNavigationRouter(for: navigationController) + let navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController) let coordinator = self.createRoomCoordinator(with: navigationRouter) coordinator.delegate = self diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift index 6fd6721c7..ae71c988b 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift @@ -73,7 +73,7 @@ final class RoomInfoCoordinatorBridgePresenter: NSObject { } func push(from navigationController: UINavigationController, animated: Bool) { - let navigationRouter = NavigationRouterStore.shared.getOrCreateNavigationRouter(for: navigationController) + let navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController) let roomInfoCoordinator = RoomInfoCoordinator(parameters: self.coordinatorParameters, navigationRouter: navigationRouter) roomInfoCoordinator.delegate = self diff --git a/Riot/Modules/Settings/Discovery/ThreePidDetails/SettingsDiscoveryThreePidDetailsCoordinatorBridgePresenter.swift b/Riot/Modules/Settings/Discovery/ThreePidDetails/SettingsDiscoveryThreePidDetailsCoordinatorBridgePresenter.swift index 0f4a07774..7dfbd0aad 100644 --- a/Riot/Modules/Settings/Discovery/ThreePidDetails/SettingsDiscoveryThreePidDetailsCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Settings/Discovery/ThreePidDetails/SettingsDiscoveryThreePidDetailsCoordinatorBridgePresenter.swift @@ -45,7 +45,7 @@ final class SettingsDiscoveryThreePidDetailsCoordinatorBridgePresenter: NSObject func push(from navigationController: UINavigationController, animated: Bool, popCompletion: (() -> Void)?) { - let router = NavigationRouterStore.shared.getOrCreateNavigationRouter(for: navigationController) + let router = NavigationRouterStore.shared.navigationRouter(for: navigationController) let settingsDiscoveryThreePidDetailsCoordinator = SettingsDiscoveryThreePidDetailsCoordinator(session: self.session, threePid: self.threePid) diff --git a/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerCoordinatorBridgePresenter.swift b/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerCoordinatorBridgePresenter.swift index 24df77a97..c25e81b78 100644 --- a/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerCoordinatorBridgePresenter.swift @@ -50,7 +50,7 @@ final class SettingsIdentityServerCoordinatorBridgePresenter: NSObject { func push(from navigationController: UINavigationController, animated: Bool, popCompletion: (() -> Void)?) { - let router = NavigationRouterStore.shared.getOrCreateNavigationRouter(for: navigationController) + let router = NavigationRouterStore.shared.navigationRouter(for: navigationController) let settingsIdentityServerCoordinator = SettingsIdentityServerCoordinator(session: self.session) diff --git a/Riot/Routers/NavigationRouterStore.swift b/Riot/Routers/NavigationRouterStore.swift index c1b4e96c4..072798f55 100644 --- a/Riot/Routers/NavigationRouterStore.swift +++ b/Riot/Routers/NavigationRouterStore.swift @@ -39,10 +39,9 @@ class NavigationRouterStore: NavigationRouterStoreProtocol { // MARK: - Public - /// Gets the existing navigation router for the supplied controller, creating a new one if it doesn't yet exist. func navigationRouter(for navigationController: UINavigationController) -> NavigationRouterType { - if let existingNavigationRouter = self.getNavigationRouter(for: navigationController) { + if let existingNavigationRouter = self.findNavigationRouter(for: navigationController) { return existingNavigationRouter } @@ -50,12 +49,12 @@ class NavigationRouterStore: NavigationRouterStoreProtocol { return navigationRouter } - func getNavigationRouter(for navigationController: UINavigationController) -> NavigationRouterType? { + // MARK: - Private + + private func findNavigationRouter(for navigationController: UINavigationController) -> NavigationRouterType? { return self.navigationRouters[navigationController] } - // MARK: - Private - private func removeNavigationRouter(for navigationController: UINavigationController) { self.navigationRouters[navigationController] = nil } @@ -73,7 +72,7 @@ class NavigationRouterStore: NavigationRouterStoreProtocol { return } - if let existingNavigationRouter = self.getNavigationRouter(for: navigationController) { + if let existingNavigationRouter = self.findNavigationRouter(for: navigationController) { fatalError("\(existingNavigationRouter) is already tied to the same navigation controller as \(navigationRouter). We should have only one NavigationRouter per navigation controller") } else { // FIXME: WeakDictionary does not work with protocol @@ -89,7 +88,7 @@ class NavigationRouterStore: NavigationRouterStoreProtocol { return } - if let existingNavigationRouter = self.getNavigationRouter(for: navigationController), existingNavigationRouter !== navigationRouter { + if let existingNavigationRouter = self.findNavigationRouter(for: navigationController), existingNavigationRouter !== navigationRouter { fatalError("\(existingNavigationRouter) is already tied to the same navigation controller as \(navigationRouter). We should have only one NavigationRouter per navigation controller") } diff --git a/Riot/Routers/NavigationRouterStoreProtocol.swift b/Riot/Routers/NavigationRouterStoreProtocol.swift index 0c38bb755..570236232 100644 --- a/Riot/Routers/NavigationRouterStoreProtocol.swift +++ b/Riot/Routers/NavigationRouterStoreProtocol.swift @@ -18,6 +18,7 @@ import Foundation /// `NavigationRouterStoreProtocol` describes a structure that enables to get a NavigationRouter from a UINavigationController instance. protocol NavigationRouterStoreProtocol { - func getOrCreateNavigationRouter(for navigationController: UINavigationController) -> NavigationRouterType - func getNavigationRouter(for navigationController: UINavigationController) -> NavigationRouterType? + + /// Gets the existing navigation router for the supplied controller, creating a new one if it doesn't yet exist. + func navigationRouter(for navigationController: UINavigationController) -> NavigationRouterType } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsBridgePresenter.swift b/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsBridgePresenter.swift index 6d6c34db5..76d83ebcb 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsBridgePresenter.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsBridgePresenter.swift @@ -51,7 +51,7 @@ final class NotificationSettingsCoordinatorBridgePresenter: NSObject { func push(from navigationController: UINavigationController, animated: Bool, screen: NotificationSettingsScreen, popCompletion: (() -> Void)?) { - let router = NavigationRouterStore.shared.getOrCreateNavigationRouter(for: navigationController) + let router = NavigationRouterStore.shared.navigationRouter(for: navigationController) let notificationSettingsCoordinator = NotificationSettingsCoordinator(session: session, screen: screen) diff --git a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorBridgePresenter.swift b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorBridgePresenter.swift index cead36efc..5d2e199fd 100644 --- a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorBridgePresenter.swift +++ b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorBridgePresenter.swift @@ -76,7 +76,7 @@ final class FlowTemplateCoordinatorBridgePresenter: NSObject { func push(from navigationController: UINavigationController, animated: Bool) { - let navigationRouter = NavigationRouterStore.shared.getOrCreateNavigationRouter(for: navigationController) + let navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController) let flowTemplateCoordinatorParameters = FlowTemplateCoordinatorParameters(session: self.session, navigationRouter: navigationRouter) From e38fac848545e7b995a78b45664c69ed9b45359a Mon Sep 17 00:00:00 2001 From: sr093906 Date: Fri, 8 Oct 2021 13:42:37 +0000 Subject: [PATCH 119/276] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (1316 of 1316 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/zh_Hans/ --- Riot/Assets/zh_Hans.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index c36ccb9ec..d03d533fb 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -61,7 +61,7 @@ "auth_missing_phone" = "缺少电话号码"; "auth_missing_email_or_phone" = "缺少电子邮箱地址或电话号码"; "auth_password_dont_match" = "密码不匹配"; -"auth_username_in_use" = "用户名已被占用"; +"auth_username_in_use" = "用户名被占用"; "auth_forgot_password" = "忘记密码?"; "auth_use_server_options" = "使用自定义服务器选项(高级)"; "auth_email_validation_message" = "请检查我们发给您的电子邮件以继续注册"; From 52d934be6970a704449fa80abaf6862862efe8d2 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Sat, 9 Oct 2021 08:47:20 +0000 Subject: [PATCH 120/276] Translated using Weblate (Hungarian) Currently translated at 100.0% (1316 of 1316 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ --- Riot/Assets/hu.lproj/Vector.strings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index d18f8a407..7f3966bae 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -72,7 +72,7 @@ "auth_phone_in_use" = "Ez a telefonszám már használatban van"; "auth_untrusted_id_server" = "Az azonosító szerver megbízhatatlan"; "auth_password_dont_match" = "A jelszavak nem egyeznek meg"; -"auth_username_in_use" = "A felhasználói név használatban"; +"auth_username_in_use" = "A felhasználónév foglalt"; "auth_forgot_password" = "Elfelejtetted a jelszót?"; "auth_email_not_found" = "E-mail küldési hiba: Az e-mail cím nem található"; "auth_use_server_options" = "Egyedi szerver beállítások használata (haladó)"; @@ -863,7 +863,7 @@ "settings_calls_stun_server_fallback_description" = "Másodlagos hívást segítő szerver (%@) engedélyezése ha a matrix szervered nem ajánl fel másikat (az IP címed a hívás ideje alatt meg lesz osztva)."; "settings_devices_description" = "Az munkamenet nyilvános neve látható azoknál az embereknél akikkel beszélgetsz"; "settings_discovery_no_identity_server" = "Jelenleg nem használsz azonosítási szervert. Ahhoz, hogy az ismerősök megtalálhassanak adj hozzá egyet."; -"settings_discovery_terms_not_signed" = "Egyetértés az Azonosítási Szerver (%@) Felhasználási Feltételeivel, hogy e-mail címmel vagy telefonszámmal megtalálható lehess."; +"settings_discovery_terms_not_signed" = "Egyetértés az azonosítási szerver (%@) Felhasználási Feltételeivel, hogy e-mail címmel vagy telefonszámmal megtalálható lehess."; "settings_discovery_three_pids_management_information_part1" = "E-mail címek és telefonszámok beállítása amivel más felhasználók megtalálhatnak és meghívhatna szobákba. E-mail cím és telefonszám hozzáadása és törlése a listából itt: "; "settings_discovery_three_pids_management_information_part2" = "Felhasználói Beállítások"; "settings_discovery_three_pids_management_information_part3" = "."; @@ -880,7 +880,7 @@ "settings_identity_server_no_is" = "Azonosítási szerver nincs beállítva"; "settings_identity_server_no_is_description" = "Jelenleg nem használsz azonosítási szervert. Ahhoz, hogy megtalálhass másokat és az ismerősök megtalálhassanak adj hozzá egyet alább."; // Identity server settings -"identity_server_settings_title" = "Azonosítási Szerver"; +"identity_server_settings_title" = "Azonosítási szerver"; "identity_server_settings_description" = "Jelenleg ezt használod: %@, hogy megtalálj másokat és megtalálhassanak ismerősök."; "identity_server_settings_no_is_description" = "Jelenleg nem használsz azonosítási szervert. Ahhoz, hogy megtalálj másokat és megtalálhassanak ismerősök adj meg egyet alább."; "identity_server_settings_place_holder" = "Adj meg egy azonosítási szervet"; From 57338f28422dec7a4ac78cee0ba3f7065deba67d Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Fri, 8 Oct 2021 21:51:18 +0000 Subject: [PATCH 121/276] Translated using Weblate (Albanian) Currently translated at 99.1% (1305 of 1316 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/ --- Riot/Assets/sq.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index 0f1043ea5..f1b72a758 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -61,7 +61,7 @@ "auth_phone_in_use" = "Ky numër telefoni është tashmë në përdorim"; "auth_untrusted_id_server" = "Shërbyesi i identiteteve s’është i besuar"; "auth_password_dont_match" = "Fjalëkalimet s’përputhen"; -"auth_username_in_use" = "Emër përdoruesi në përdorim"; +"auth_username_in_use" = "Emër përdoruesi i përdorur"; "auth_forgot_password" = "Harruat fjalëkalimin?"; "auth_email_not_found" = "S’u arrit të dërgohej email: Kjo adresë email s’u gjet"; "auth_email_validation_message" = "Ju lutemi, që të vazhdojë regjistrimi, kontrolloni email-in tuaj"; From e197df7b8d70e9b1022a88b2bcf62b5afe1f8d0d Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Sat, 9 Oct 2021 01:31:29 +0000 Subject: [PATCH 122/276] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1316 of 1316 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ --- Riot/Assets/pt_BR.lproj/Vector.strings | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index a2d68de2b..77bfd450a 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -720,7 +720,7 @@ "settings_three_pids_management_information_part3" = "."; "settings_security" = "SEGURANÇA"; "settings_integrations_allow_button" = "Gerenciar integrações"; -"settings_integrations_allow_description" = "Use um Gerenciador de Integrações (%@) para gerenciar bots, bridges, widgets e pacotes de stickers.\n\nGerenciadores de Integrações recebem dados de configuração, e podem modificar widgets, enviar convites de sala e definir níveis de poder em seu nome."; +"settings_integrations_allow_description" = "Use um gerenciador de integrações (%@) para gerenciar bots, bridges, widgets e pacotes de stickers.\n\nGerenciadores de integrações recebem dados de configuração, e podem modificar widgets, enviar convites de sala e definir níveis de poder em seu nome."; "settings_add_3pid_password_title_email" = "Adicionar endereço de email"; "settings_discovery_three_pids_management_information_part2" = "Configurações de Usuária(o)"; "settings_discovery_three_pids_management_information_part3" = "."; @@ -779,7 +779,7 @@ "room_participants_action_security_status_warning" = "Aviso"; "settings_three_pids_management_information_part1" = "Gerencie quais endereços de email ou números de telefone você pode usar para fazer login ou recuperar sua conta aqui. Controle quem pode encontrar você em "; "settings_calls_stun_server_fallback_button" = "Permitir servidor fallback de assistência de chamadas"; -"settings_discovery_terms_not_signed" = "Concorde com os Termos de Serviço do Servidor de Identidade (%@) para permitir que você mesma(o) seja descobertável por endereço de email ou número de telefone."; +"settings_discovery_terms_not_signed" = "Concorde com os Termos de Serviço do servidor de identidade (%@) para permitir que você mesma(o) seja descobertável por endereço de email ou número de telefone."; "settings_discovery_three_pids_management_information_part1" = "Gerencie quais endereços de email ou números de telefone outras(os) usuárias(os) podem usar para descobrir você e usar para convidar você para salas. Adicione ou remova endereços de email ou números de telefone desta lista em "; "settings_discovery_three_pid_details_title_email" = "Gerenciar email"; "settings_discovery_three_pid_details_information_email" = "Gerencie preferências para este endereço de email, que outras(os) usuárias(os) podem usar para descobrir você e usar para convidar você para salas. Adicione ou remova endereços de email em Contas."; @@ -794,7 +794,7 @@ "identity_server_settings_disconnect_info" = "Desconectar-se de seu servidor de identidade vai significar que você não vai ser descobertável por outras(os) usuárias(os) e ser capaz de convidar outras(os) por email ou telefone."; "identity_server_settings_alert_disconnect_still_sharing_3pid" = "Você ainda está compartilhando seus dados pessoais no servidor de identidade %@.\n\nNós recomendamos que você remova seus endereços de email e números de telefone do servidor de identidade antes de se desconectar."; "call_no_stun_server_error_title" = "Chamada falhou devido a servidor malconfigurado"; -"widget_integration_manager_disabled" = "Você precisa habilitar Gerenciador de Integrações em configurações"; +"widget_integration_manager_disabled" = "Você precisa habilitar gerenciador de integrações em configurações"; "service_terms_modal_description_for_identity_server_1" = "Encontrar outras(os) por telefone ou email"; "service_terms_modal_description_for_identity_server_2" = "Ser encontrada(o) por telefone ou email"; "device_verification_self_verify_wait_additional_information" = "Isto funciona com %@ e outros clientes Matrix capazes de assinatura cruzada."; @@ -885,7 +885,7 @@ "manage_session_not_trusted" = "Não confiada"; "manage_session_sign_out" = "Fazer signout desta sessão"; // Identity server settings -"identity_server_settings_title" = "Servidor de Identidade"; +"identity_server_settings_title" = "Servidor de identidade"; "identity_server_settings_description" = "Vvocê está atualmente usando %@ para descobrir e ser descobertável por contatos existentes que você conhece."; "identity_server_settings_no_is_description" = "Você não está atualmente usando um servidor de identidade. Para descobrir e ser descobertável por contatos existentes, adicione um acima."; "identity_server_settings_place_holder" = "Entrar um servidor de identidade"; From 5b00c5ee3edabdbd7714ccf3629ce46d0806cf10 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Mon, 11 Oct 2021 09:52:21 +0200 Subject: [PATCH 123/276] [Spaces] M10.4.1 Home space data filtering #4570 - Improved visual feedback when switching home settings --- .../SpaceMenu/SpaceMenuListItemViewData.swift | 23 +++++++++++++++++-- .../SpaceMenu/SpaceMenuSwitchViewCell.swift | 9 ++++++++ .../SpaceMenu/SpaceMenuViewController.swift | 2 -- .../Spaces/SpaceMenu/SpaceMenuViewModel.swift | 1 - .../Spaces/SpaceMenu/SpaceMenuViewState.swift | 1 - 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift index ca555e580..18a1f29bd 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift @@ -23,11 +23,30 @@ enum SpaceMenuListItemStyle { case destructive } +/// `SpaceMenuListItemViewDataDelegate` allows the table view cell to update its view accordingly with it's related data change +protocol SpaceMenuListItemViewDataDelegate: AnyObject { + func spaceMenuItemValueDidChange(_ item: SpaceMenuListItemViewData) +} + /// `SpaceMenuListViewCell` view data -struct SpaceMenuListItemViewData { +class SpaceMenuListItemViewData { let actionId: String let style: SpaceMenuListItemStyle let title: String? let icon: UIImage? - var value: Any? + + var value: Any? { + didSet { + delegate?.spaceMenuItemValueDidChange(self) + } + } + weak var delegate: SpaceMenuListItemViewDataDelegate? + + init(actionId: String, style: SpaceMenuListItemStyle, title: String?, icon: UIImage?, value: Any?) { + self.actionId = actionId + self.style = style + self.title = title + self.icon = icon + self.value = value + } } diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.swift index 6cd7a7a39..3887f668f 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.swift @@ -62,6 +62,8 @@ class SpaceMenuSwitchViewCell: UITableViewCell, SpaceMenuCell, NibReusable { } else { self.titleLabel.textColor = theme.colors.primaryContent } + + viewData.delegate = self } func update(theme: Theme) { @@ -72,3 +74,10 @@ class SpaceMenuSwitchViewCell: UITableViewCell, SpaceMenuCell, NibReusable { self.selectionView.backgroundColor = theme.colors.separator } } + +// MARK: - SpaceMenuListItemViewDataDelegate +extension SpaceMenuSwitchViewCell: SpaceMenuListItemViewDataDelegate { + func spaceMenuItemValueDidChange(_ item: SpaceMenuListItemViewData) { + self.switchView.setOn((item.value as? Bool) ?? false, animated: true) + } +} diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift index 9cb954398..83ab85bdf 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift @@ -161,8 +161,6 @@ class SpaceMenuViewController: UIViewController { self.renderLoaded() case .leaveOptions(let displayName, let isAdmin): self.renderLeaveOptions(displayName: displayName, isAdmin: isAdmin) - case .updateItem(let indexPath): - self.tableView.reloadRows(at: [indexPath], with: .fade) case .error(let error): self.render(error: error) case .deselect: diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift index cd3c99f9c..4d14eed4d 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift @@ -83,7 +83,6 @@ class SpaceMenuViewModel: SpaceMenuViewModelType { MXKAppSettings.standard().isShowAllRoomsInHomeEnabled = !MXKAppSettings.standard().isShowAllRoomsInHomeEnabled self.menuItems[indexPath.row].value = MXKAppSettings.standard().isShowAllRoomsInHomeEnabled self.viewDelegate?.spaceMenuViewModel(self, didUpdateViewState: .deselect) - self.viewDelegate?.spaceMenuViewModel(self, didUpdateViewState: .updateItem(indexPath)) case .leave: self.leaveSpace() default: diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewState.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewState.swift index 0e37aad97..9801ffaf5 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewState.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewState.swift @@ -21,7 +21,6 @@ enum SpaceMenuViewState { case loading case loaded case deselect - case updateItem(_ indexPath: IndexPath) case leaveOptions(_ displayName: String, _ isAdmin: Bool) case error(Error) } From cc576861c0fe312568b4ff89f60af76f882e41ab Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Mon, 11 Oct 2021 10:29:27 +0200 Subject: [PATCH 124/276] Added changelogs --- changelog.d/4570.feature | 1 + changelog.d/4886.bugfix | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog.d/4570.feature create mode 100644 changelog.d/4886.bugfix diff --git a/changelog.d/4570.feature b/changelog.d/4570.feature new file mode 100644 index 000000000..e1a38944f --- /dev/null +++ b/changelog.d/4570.feature @@ -0,0 +1 @@ +M10.4.1 Home space data filtering \ No newline at end of file diff --git a/changelog.d/4886.bugfix b/changelog.d/4886.bugfix new file mode 100644 index 000000000..ad58b6c36 --- /dev/null +++ b/changelog.d/4886.bugfix @@ -0,0 +1 @@ +Fixed private space invite should use lock icon instead of planet \ No newline at end of file From 89583010bb7fc2b1179423c747328929c75371d4 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 11 Oct 2021 11:10:26 +0200 Subject: [PATCH 125/276] Update Riot/Routers/NavigationRouterStoreProtocol.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Routers/NavigationRouterStoreProtocol.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Routers/NavigationRouterStoreProtocol.swift b/Riot/Routers/NavigationRouterStoreProtocol.swift index 570236232..e13a24f19 100644 --- a/Riot/Routers/NavigationRouterStoreProtocol.swift +++ b/Riot/Routers/NavigationRouterStoreProtocol.swift @@ -20,5 +20,6 @@ import Foundation protocol NavigationRouterStoreProtocol { /// Gets the existing navigation router for the supplied controller, creating a new one if it doesn't yet exist. + /// Note: The store only holds a weak reference to the returned router. It is the caller's responsibility to retain it. func navigationRouter(for navigationController: UINavigationController) -> NavigationRouterType } From f409f7ab9d75c21f21f350c58172713d58871d86 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 11 Oct 2021 11:11:13 +0200 Subject: [PATCH 126/276] Update Riot/Modules/TabBar/TabBarCoordinator.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Modules/TabBar/TabBarCoordinator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index fad98dda4..8b1d3735a 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -377,7 +377,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { private func showRoom(with roomId: String, eventId: String?, matrixSession: MXSession, completion: (() -> Void)? = nil) { // RoomCoordinator will be presented by the split view. - // Ass we don't know which navigation controller instance will be used, + // 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) From 72c455b0b412fb9e13796ee18451bc6ed050640a Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 11 Oct 2021 12:18:10 +0100 Subject: [PATCH 127/276] Add FramePreferenceKey for use in ViewFrameReader. --- .../ViewFrameReader/FramePreferenceKey.swift | 27 +++++++++++++++++++ .../ViewFrameReader/ViewFrameReader.swift | 12 +++++---- changelog.d/4974.change | 1 + 3 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 RiotSwiftUI/Modules/Common/ViewFrameReader/FramePreferenceKey.swift create mode 100644 changelog.d/4974.change diff --git a/RiotSwiftUI/Modules/Common/ViewFrameReader/FramePreferenceKey.swift b/RiotSwiftUI/Modules/Common/ViewFrameReader/FramePreferenceKey.swift new file mode 100644 index 000000000..9da914bd2 --- /dev/null +++ b/RiotSwiftUI/Modules/Common/ViewFrameReader/FramePreferenceKey.swift @@ -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 SwiftUI + +/// A SwiftUI `PreferenceKey` for `CGRect` values such as a view's frame. +@available(iOS 14.0, *) +struct FramePreferenceKey: PreferenceKey { + static var defaultValue: CGRect = .zero + + static func reduce(value: inout CGRect, nextValue: () -> CGRect) { + value = nextValue() + } +} diff --git a/RiotSwiftUI/Modules/Common/ViewFrameReader/ViewFrameReader.swift b/RiotSwiftUI/Modules/Common/ViewFrameReader/ViewFrameReader.swift index 74318a0e1..5c5671584 100644 --- a/RiotSwiftUI/Modules/Common/ViewFrameReader/ViewFrameReader.swift +++ b/RiotSwiftUI/Modules/Common/ViewFrameReader/ViewFrameReader.swift @@ -31,11 +31,13 @@ struct ViewFrameReader: View { @Binding var frame: CGRect var body: some View { - GeometryReader { geo -> Color in - DispatchQueue.main.async { - frame = geo.frame(in: .local) - } - return .clear + GeometryReader { geometry in + Color.clear + .preference(key: FramePreferenceKey.self, + value: geometry.frame(in: .local)) + } + .onPreferenceChange(FramePreferenceKey.self) { + frame = $0 } } } diff --git a/changelog.d/4974.change b/changelog.d/4974.change new file mode 100644 index 000000000..8c4588c4f --- /dev/null +++ b/changelog.d/4974.change @@ -0,0 +1 @@ +SwiftUI: Add FramePreferenceKey for use in ViewFrameReader. \ No newline at end of file From 3baceed6bdf425df73162fa42340ccb99ca419f6 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 11 Oct 2021 16:51:12 +0200 Subject: [PATCH 128/276] NavigationRouter: Update controllers pop notification order sending. --- Riot/Routers/NavigationRouter.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Riot/Routers/NavigationRouter.swift b/Riot/Routers/NavigationRouter.swift index 075dd4e92..880dbd9ee 100755 --- a/Riot/Routers/NavigationRouter.swift +++ b/Riot/Routers/NavigationRouter.swift @@ -91,7 +91,7 @@ final class NavigationRouter: NSObject, NavigationRouterType { self.addModule(module, for: controller) - let controllersToPop = self.navigationController.viewControllers + let controllersToPop = self.navigationController.viewControllers.reversed() controllersToPop.forEach { self.willPopViewController($0) @@ -127,7 +127,7 @@ final class NavigationRouter: NSObject, NavigationRouterType { return controller } - let controllersToPop = self.navigationController.viewControllers + let controllersToPop = self.navigationController.viewControllers.reversed() controllersToPop.forEach { self.willPopViewController($0) @@ -164,13 +164,13 @@ final class NavigationRouter: NSObject, NavigationRouterType { if controllers.count > 1 { let controllersToPop = controllers[1.. Date: Mon, 11 Oct 2021 16:06:15 +0100 Subject: [PATCH 129/276] Fix generated avatar colours not matching Element Web. --- Riot/Categories/MXKImageView.swift | 4 +-- Riot/Categories/MXRoomSummary+Riot.m | 32 ++++++------------- .../Recents/Views/RecentTableViewCell.m | 10 ++++-- .../Home/Views/RoomCollectionViewCell.m | 10 ++++-- .../RiotShareExtension-Bridging-Header.h | 1 + RiotShareExtension/target.yml | 1 + changelog.d/4978.bugfix | 1 + 7 files changed, 31 insertions(+), 28 deletions(-) create mode 100644 changelog.d/4978.bugfix diff --git a/Riot/Categories/MXKImageView.swift b/Riot/Categories/MXKImageView.swift index 5f32960ed..2f324884a 100644 --- a/Riot/Categories/MXKImageView.swift +++ b/Riot/Categories/MXKImageView.swift @@ -17,9 +17,9 @@ import Foundation extension MXKImageView { - @objc func vc_setRoomAvatarImage(with url: String?, displayName: String, mediaManager: MXMediaManager) { + @objc func vc_setRoomAvatarImage(with url: String?, roomId: String, displayName: String, mediaManager: MXMediaManager) { // Use the display name to prepare the default avatar image. - let avatarImage = AvatarGenerator.generateAvatar(forText: displayName) + let avatarImage = AvatarGenerator.generateAvatar(forMatrixItem: roomId, withDisplayName: displayName) if let avatarUrl = url { self.enableInMemoryCache = true diff --git a/Riot/Categories/MXRoomSummary+Riot.m b/Riot/Categories/MXRoomSummary+Riot.m index 961a51a6e..7a83d1d40 100644 --- a/Riot/Categories/MXRoomSummary+Riot.m +++ b/Riot/Categories/MXRoomSummary+Riot.m @@ -19,32 +19,20 @@ #import "AvatarGenerator.h" +#ifdef IS_SHARE_EXTENSION +#import "RiotShareExtension-Swift.h" +#else +#import "Riot-Swift.h" +#endif + @implementation MXRoomSummary (Riot) - (void)setRoomAvatarImageIn:(MXKImageView*)mxkImageView { - // Use the room display name to prepare the default avatar image. - NSString *avatarDisplayName = self.displayname; - UIImage* avatarImage = [AvatarGenerator generateAvatarForMatrixItem:self.roomId withDisplayName:avatarDisplayName]; - - if (self.avatar) - { - mxkImageView.enableInMemoryCache = YES; - - [mxkImageView setImageURI:self.avatar - withType:nil - andImageOrientation:UIImageOrientationUp - toFitViewSize:mxkImageView.frame.size - withMethod:MXThumbnailingMethodCrop - previewImage:avatarImage - mediaManager:self.mxSession.mediaManager]; - } - else - { - mxkImageView.image = avatarImage; - } - - mxkImageView.contentMode = UIViewContentModeScaleAspectFill; + [mxkImageView vc_setRoomAvatarImageWith:self.avatar + roomId:self.roomId + displayName:self.displayname + mediaManager:self.mxSession.mediaManager]; } - (RoomEncryptionTrustLevel)roomEncryptionTrustLevel diff --git a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m index cf6077adf..a38edf5c1 100644 --- a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m +++ b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m @@ -126,11 +126,17 @@ if (roomCellData.spaceChildInfo) { - [self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.spaceChildInfo.avatarUrl displayName:roomCellData.spaceChildInfo.displayName mediaManager:roomCellData.recentsDataSource.mxSession.mediaManager]; + [self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.spaceChildInfo.avatarUrl + roomId:roomCellData.spaceChildInfo.childRoomId + displayName:roomCellData.spaceChildInfo.displayName + mediaManager:roomCellData.recentsDataSource.mxSession.mediaManager]; } else { - [self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.roomSummary.avatar displayName:roomCellData.roomSummary.displayname mediaManager:roomCellData.roomSummary.mxSession.mediaManager]; + [self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.roomSummary.avatar + roomId:roomCellData.roomSummary.roomId + displayName:roomCellData.roomSummary.displayname + mediaManager:roomCellData.roomSummary.mxSession.mediaManager]; } } else diff --git a/Riot/Modules/Home/Views/RoomCollectionViewCell.m b/Riot/Modules/Home/Views/RoomCollectionViewCell.m index f8986da55..26fba7ec0 100644 --- a/Riot/Modules/Home/Views/RoomCollectionViewCell.m +++ b/Riot/Modules/Home/Views/RoomCollectionViewCell.m @@ -132,11 +132,17 @@ if (roomCellData.roomSummary) { - [self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.roomSummary.avatar displayName:roomCellData.roomSummary.displayname mediaManager:roomCellData.roomSummary.mxSession.mediaManager]; + [self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.roomSummary.avatar + roomId:roomCellData.roomSummary.roomId + displayName:roomCellData.roomSummary.displayname + mediaManager:roomCellData.roomSummary.mxSession.mediaManager]; } else { - [self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.spaceChildInfo.avatarUrl displayName:roomCellData.spaceChildInfo.displayName mediaManager:roomCellData.recentsDataSource.mxSession.mediaManager]; + [self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.spaceChildInfo.avatarUrl + roomId:roomCellData.spaceChildInfo.childRoomId + displayName:roomCellData.spaceChildInfo.displayName + mediaManager:roomCellData.recentsDataSource.mxSession.mediaManager]; } } } diff --git a/RiotShareExtension/SupportingFiles/RiotShareExtension-Bridging-Header.h b/RiotShareExtension/SupportingFiles/RiotShareExtension-Bridging-Header.h index 7047b9786..d3bf536b2 100644 --- a/RiotShareExtension/SupportingFiles/RiotShareExtension-Bridging-Header.h +++ b/RiotShareExtension/SupportingFiles/RiotShareExtension-Bridging-Header.h @@ -3,3 +3,4 @@ // #import "ThemeService.h" +#import "AvatarGenerator.h" diff --git a/RiotShareExtension/target.yml b/RiotShareExtension/target.yml index 2fb6d6fe9..9e61389f1 100644 --- a/RiotShareExtension/target.yml +++ b/RiotShareExtension/target.yml @@ -43,6 +43,7 @@ targets: - path: ../Riot/Utils/AvatarGenerator.m - path: ../Config/BuildSettings.swift - path: ../Riot/Categories/Character.swift + - path: ../Riot/Categories/MXKImageView.swift - path: ../Riot/Categories/MXRoom+Riot.m - path: ../Config/Configurable.swift - path: ../Config/CommonConfiguration.swift diff --git a/changelog.d/4978.bugfix b/changelog.d/4978.bugfix new file mode 100644 index 000000000..36dcc382c --- /dev/null +++ b/changelog.d/4978.bugfix @@ -0,0 +1 @@ +Room Lists: Fix generated avatar colours not matching Element Web. \ No newline at end of file From 6c11005319a0dc9c188d83ceb00f2c1cf5636c27 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 12 Oct 2021 11:17:20 +0100 Subject: [PATCH 130/276] Move call to validateSyncLocalContactsState into MatrixKit. --- Riot/Modules/Application/LegacyAppDelegate.m | 7 ------- changelog.d/4989.bugfix | 1 + 2 files changed, 1 insertion(+), 7 deletions(-) create mode 100644 changelog.d/4989.bugfix diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 2052c6a6a..c86695012 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -1843,13 +1843,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [self checkLocalPrivateKeysInSession:mxSession]; [self.pushNotificationService checkPushKitPushersInSession:mxSession]; - - // Validate the availability of local contact sync for any changes to the - // authorization of contacts access that may have occurred since the last launch. - if (BuildSettings.allowLocalContactsAccess) - { - [MXKContactManager.sharedManager validateSyncLocalContactsState]; - } } else if (mxSession.state == MXSessionStateClosed) { diff --git a/changelog.d/4989.bugfix b/changelog.d/4989.bugfix new file mode 100644 index 000000000..bc127643a --- /dev/null +++ b/changelog.d/4989.bugfix @@ -0,0 +1 @@ +Contacts Sync: Move call to validateSyncLocalContactsState into MatrixKit. \ No newline at end of file From 4979d5b29ede39cae1963fda77f4197eda5ab2bc Mon Sep 17 00:00:00 2001 From: sr093906 Date: Mon, 11 Oct 2021 09:41:28 +0000 Subject: [PATCH 131/276] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (6 of 6 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/zh_Hans/ --- Riot/Assets/zh_Hans.lproj/InfoPlist.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/zh_Hans.lproj/InfoPlist.strings b/Riot/Assets/zh_Hans.lproj/InfoPlist.strings index b4daf9c55..8133fc8ee 100644 --- a/Riot/Assets/zh_Hans.lproj/InfoPlist.strings +++ b/Riot/Assets/zh_Hans.lproj/InfoPlist.strings @@ -2,6 +2,6 @@ "NSCameraUsageDescription" = "摄像头权限用于拍摄照片、录制视频或进行视频聊天。"; "NSPhotoLibraryUsageDescription" = "照片库访问权限用于发送图片与视频。"; "NSMicrophoneUsageDescription" = "Element 需要访问您的麦克风才能拨打和接听电话、拍摄视频和录制语音消息。"; -"NSContactsUsageDescription" = "为了发现已在使用 Matrix 的联系人,Element 可以把你地址簿里的邮箱地址和电话号码发送到你所选择的 Matrix 身份认证服务器。如果支持的话,个人数据在发送前会被哈希处理——请检查你的身份认证服务器的隐私政策以获取详细信息。"; +"NSContactsUsageDescription" = "Element 将显示您的联系人,以便您可以邀请他们聊天。"; "NSCalendarsUsageDescription" = "在此应用中查看你计划的会议。"; "NSFaceIDUsageDescription" = "Face ID 权限用于访问您的应用。"; From a9a3f7bd05cf4db034f518c27babcb90618aa33e Mon Sep 17 00:00:00 2001 From: Szimszon Date: Mon, 11 Oct 2021 13:23:23 +0000 Subject: [PATCH 132/276] Translated using Weblate (Hungarian) Currently translated at 100.0% (6 of 6 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/hu/ --- Riot/Assets/hu.lproj/InfoPlist.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/hu.lproj/InfoPlist.strings b/Riot/Assets/hu.lproj/InfoPlist.strings index f874c48d5..0edf4d429 100644 --- a/Riot/Assets/hu.lproj/InfoPlist.strings +++ b/Riot/Assets/hu.lproj/InfoPlist.strings @@ -2,6 +2,6 @@ "NSCameraUsageDescription" = "A kamera fényképek, videók készítéséhez és videóhívásokhoz lesz használva."; "NSPhotoLibraryUsageDescription" = "A fénykép galéria fényképek és videók küldéséhez lesz használva."; "NSMicrophoneUsageDescription" = "A hívás indításához és fogadásához, videó és hangüzenet felvételéhez az Elementnek hozzáférési engedélyre van szüksége a mikrofonhoz."; -"NSContactsUsageDescription" = "Az olyan ismerősök felderítéséhez akik már használják a Matrixot, Elementet el tudja küldeni a címjegyzékben található e-mail címeket és telefonszámokat az általad választott Matrix azonosítási szervernek. Ahol lehetséges a személyes adatok hash-elve lesznek - kérlek ellenőrizd az azonosítási szervered adatvédelmi szabályait."; +"NSContactsUsageDescription" = "Element megmutatja a névjegyzéket, hogy beszélgetésbe meghívhasd őket."; "NSCalendarsUsageDescription" = "Nézd meg a találkozóidat az alkalmazásban."; "NSFaceIDUsageDescription" = "Arc felismerés használata az alkalmazás eléréséhez."; From 6d9af0a9131d540f4640bc790e682a880dcc44f7 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Mon, 11 Oct 2021 10:12:10 +0000 Subject: [PATCH 133/276] Translated using Weblate (Albanian) Currently translated at 100.0% (6 of 6 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/sq/ --- Riot/Assets/sq.lproj/InfoPlist.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/sq.lproj/InfoPlist.strings b/Riot/Assets/sq.lproj/InfoPlist.strings index f5a1bd783..ee2f2697a 100644 --- a/Riot/Assets/sq.lproj/InfoPlist.strings +++ b/Riot/Assets/sq.lproj/InfoPlist.strings @@ -2,6 +2,6 @@ "NSCameraUsageDescription" = "Kamera përdoret për të bërë foto dhe regjistruar video, dhe për të bërë thirrje video."; "NSPhotoLibraryUsageDescription" = "Fototeka përdoret për të dërguar foto dhe video."; "NSMicrophoneUsageDescription" = "Element-it i duhet të përdorë mikrofonin tuaj për të bërë dhe marrë thirrje, për të regjistruar video, dhe për të regjistruar mesazhe zanorë."; -"NSContactsUsageDescription" = "Për zbulim kontaktesh që përdorin tashmë Matrix-in, Element-i mund të dërgojë adresa email dhe numra telefonash nga libri juaj i adresave te shërbyesi juaj i zgjedhur Matrix i identiteteve. Kur kjo mbulohet, të dhënat personale fshehtëzohen, para se të dërgohen - ju lutemi, për më tepër hollësi, shihni rregulla privatësie të shërbyesit tuaj të identiteteve."; +"NSContactsUsageDescription" = "Element-i do të shfaqë kontaktet tuaja, që kështu të mund t’i ftoni për të biseduar."; "NSCalendarsUsageDescription" = "Shihini te aplikacioni takimet tuaja të planifikuara."; "NSFaceIDUsageDescription" = "Face ID përdoret që të hyni në aplikacionin tuaj."; From 6a4b4ed766b894a9eb7bb178f6c7f928139f2073 Mon Sep 17 00:00:00 2001 From: random Date: Tue, 12 Oct 2021 10:21:29 +0000 Subject: [PATCH 134/276] Translated using Weblate (Italian) Currently translated at 100.0% (6 of 6 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/it/ --- Riot/Assets/it.lproj/InfoPlist.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/it.lproj/InfoPlist.strings b/Riot/Assets/it.lproj/InfoPlist.strings index 8e49c5075..6382ef48e 100644 --- a/Riot/Assets/it.lproj/InfoPlist.strings +++ b/Riot/Assets/it.lproj/InfoPlist.strings @@ -2,6 +2,6 @@ "NSCameraUsageDescription" = "La fotocamera viene utilizzata per scattare fotografie, registrare video ed eseguire videochiamate."; "NSPhotoLibraryUsageDescription" = "La libreria fotografica viene utilizzata per inviare foto e video."; "NSMicrophoneUsageDescription" = "Element ha bisogno di accedere al microfono per effettuare e ricevere chiamate, registrare video e messaggi vocali."; -"NSContactsUsageDescription" = "Per scoprire i contatti che già usano Matrix, Element può inviare gli indirizzi email e i numeri di telefono della tua rubrica al server identità che hai scelto. Se supportato, viene fatto un hash dei dati personali prima dell'invio - controlla la politica sulla privacy del tuo server di identità per maggiori informazioni."; +"NSContactsUsageDescription" = "Element mostrerà i tuoi contatti così da poterli invitare in chat."; "NSCalendarsUsageDescription" = "Vedi le tue riunioni programmate nell'app."; "NSFaceIDUsageDescription" = "Face ID viene usato per accedere all'app."; From 6ecfb8d84351c416e7c328683e320769b5b5c4be Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Mon, 11 Oct 2021 10:40:39 +0000 Subject: [PATCH 135/276] Translated using Weblate (Ukrainian) Currently translated at 100.0% (6 of 6 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/uk/ --- Riot/Assets/uk.lproj/InfoPlist.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/uk.lproj/InfoPlist.strings b/Riot/Assets/uk.lproj/InfoPlist.strings index 6973b0ae6..1923cbcbc 100644 --- a/Riot/Assets/uk.lproj/InfoPlist.strings +++ b/Riot/Assets/uk.lproj/InfoPlist.strings @@ -2,6 +2,6 @@ "NSCameraUsageDescription" = "Камера використовується для знімків фото і відео, а також для відео-викликів."; "NSPhotoLibraryUsageDescription" = "Фотографії використовуються для надсилання фото і відео."; "NSMicrophoneUsageDescription" = "Element потребує доступу до вашого мікрофона, щоб здійснювати та отримувати виклики, знімати відео та записувати голосові повідомлення."; -"NSContactsUsageDescription" = "Щоб показати, які з ваших контактів вже використовують Matrix, Element може надіслати адреси електронної пошти і номери телефонів з вашої адресної книги до вашого ідентифікаційного сервера Matrix. При наявності підтримки, перед надсиланням створюється хеш особистих даних. Для докладних відомостей ознайомтеся з політикою приватності свого ідентифікаційного сервера."; +"NSContactsUsageDescription" = "Element покаже ваші контакти, щоб ви могли запросити їх до бесіди."; "NSCalendarsUsageDescription" = "Переглядайте свої заплановані зустрічі в додатку."; "NSFaceIDUsageDescription" = "Face ID використовується для доступу до вашого додатку."; From 5af11616e84d3ef5128faa71cc654ccd487b9c95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Mon, 11 Oct 2021 20:16:42 +0000 Subject: [PATCH 136/276] Translated using Weblate (Estonian) Currently translated at 100.0% (6 of 6 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/et/ --- Riot/Assets/et.lproj/InfoPlist.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/et.lproj/InfoPlist.strings b/Riot/Assets/et.lproj/InfoPlist.strings index 238ddd486..25a5d9479 100644 --- a/Riot/Assets/et.lproj/InfoPlist.strings +++ b/Riot/Assets/et.lproj/InfoPlist.strings @@ -3,5 +3,5 @@ "NSPhotoLibraryUsageDescription" = "Fotogaleriid kasutatakse fotode ja videote saatmiseks teistele kasutajatele."; "NSMicrophoneUsageDescription" = "Kõnede tegemiseks, videote ja häälsõnumite salvestamiseks vajab Element ligipääsu sinu seadme mikrofonile."; "NSCalendarsUsageDescription" = "Vaata päevakavasse lisatud koosolekuid vastvast rakendusest."; -"NSContactsUsageDescription" = "Selleks, et leida Matrixi võrgu kasutajaid, võib Element saata sinu aadressiraamatus leiduvad e-posti aadressid ja telefoninumbrid sinu valitud Matrixi isikutuvastusserverile. Kui server seda toetab, siis andmed muudetakse enne saatmist räsideks - täpsema teabe leiad oma isikutuvastusserveri privaatsuspoliitikast."; +"NSContactsUsageDescription" = "Element näitab sulle tuttavaid, kellega saad alustada vestlust."; "NSFaceIDUsageDescription" = "Ligipääsuks sinu rakendusele on kasutusel Face ID."; From ec8790eb823632c7245584ba2fb10c7a06faf916 Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Mon, 11 Oct 2021 18:59:35 +0000 Subject: [PATCH 137/276] Translated using Weblate (Swedish) Currently translated at 100.0% (6 of 6 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/sv/ --- Riot/Assets/sv.lproj/InfoPlist.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/sv.lproj/InfoPlist.strings b/Riot/Assets/sv.lproj/InfoPlist.strings index d00b2d989..45ccee26b 100644 --- a/Riot/Assets/sv.lproj/InfoPlist.strings +++ b/Riot/Assets/sv.lproj/InfoPlist.strings @@ -3,5 +3,5 @@ // Permissions usage explanations "NSCameraUsageDescription" = "Kameran används för att ta bilder och videor, och ringa videosamtal."; "NSMicrophoneUsageDescription" = "Element behöver åtkomst till din mikrofon för att kunna ringa och ta emot samtal samt spela in video och röstmeddelanden."; -"NSContactsUsageDescription" = "För att upptäcka kontakter som redan använder Matrix kan Element skicka e-postadresser och telefonnummer i din adressbok till din valda Matrix-identitetsserver. Där det stöds hashas personuppgifter innan de skickas - kontrollera din identitetsservers integritetspolicy för mer information."; +"NSContactsUsageDescription" = "Element kommer att visa dina kontakter så du kan bjuda in dem att chatta."; "NSFaceIDUsageDescription" = "Face ID används för att komma åt appen."; From 712434ea0ffb6d35743ac0f0fa73e9d776e86489 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 11 Oct 2021 10:33:17 +0300 Subject: [PATCH 138/276] Fixes #4976 - Replaced HPGrowingTextView with GrowingTextView. Reduced inheritance chain. --- Podfile | 2 +- .../InputToolbar/KeyboardGrowingTextView.m | 41 -------- .../RoomInputToolbarTextView.swift | 32 +++++++ .../Views/InputToolbar/RoomInputToolbarView.h | 2 +- .../Views/InputToolbar/RoomInputToolbarView.m | 96 ++++++++++++------- .../InputToolbar/RoomInputToolbarView.xib | 11 ++- Riot/SupportingFiles/Riot-Bridging-Header.h | 1 + changelog.d/4976.misc | 1 + 8 files changed, 103 insertions(+), 83 deletions(-) delete mode 100644 Riot/Modules/Room/Views/InputToolbar/KeyboardGrowingTextView.m create mode 100644 Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarTextView.swift create mode 100644 changelog.d/4976.misc diff --git a/Podfile b/Podfile index 0cb651d7a..a91634b28 100644 --- a/Podfile +++ b/Podfile @@ -56,7 +56,6 @@ abstract_target 'RiotPods' do # Remove warnings from "bad" pods pod 'OLMKit', :inhibit_warnings => true pod 'zxcvbn-ios', :inhibit_warnings => true - pod 'HPGrowingTextView', :inhibit_warnings => true # Tools pod 'SwiftGen', '~> 6.3' @@ -74,6 +73,7 @@ abstract_target 'RiotPods' do pod 'SideMenu', '~> 6.5' pod 'DSWaveformImage', '~> 6.1.1' pod 'ffmpeg-kit-ios-audio', '~> 4.5' + pod 'GrowingTextView', '~> 0.7.2' pod 'FLEX', '~> 4.5.0', :configurations => ['Debug'] diff --git a/Riot/Modules/Room/Views/InputToolbar/KeyboardGrowingTextView.m b/Riot/Modules/Room/Views/InputToolbar/KeyboardGrowingTextView.m deleted file mode 100644 index 9fe8d69f1..000000000 --- a/Riot/Modules/Room/Views/InputToolbar/KeyboardGrowingTextView.m +++ /dev/null @@ -1,41 +0,0 @@ -/* - Copyright 2015 OpenMarket Ltd - Copyright 2017 Vector Creations 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 -#import -#import "RoomInputToolbarView.h" - -@interface KeyboardGrowingTextView: HPGrowingTextView -- (NSArray *)keyCommands; -@end - -@implementation KeyboardGrowingTextView - -- (NSArray *)keyCommands { - return @[ - [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:0 action:@selector(keyCommandSelector:)] - ]; -} - -- (void)keyCommandSelector:(UIKeyCommand *)sender { - if ([sender.input isEqualToString:@"\r"] && [self.delegate isKindOfClass: RoomInputToolbarView.class]){ - RoomInputToolbarView *ritv = (RoomInputToolbarView *)self.delegate; - [ritv onTouchUpInside:ritv.rightInputToolbarButton]; // touch the Send button. - } -} - -@end diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarTextView.swift b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarTextView.swift new file mode 100644 index 000000000..ad769b52e --- /dev/null +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarTextView.swift @@ -0,0 +1,32 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import GrowingTextView + +class RoomInputToolbarTextView: GrowingTextView { + + override var keyCommands: [UIKeyCommand]? { + return [UIKeyCommand(input: "\r", modifierFlags: [], action: #selector(keyCommandSelector(_:)))] + } + + @objc private func keyCommandSelector(_ keyCommand: UIKeyCommand) { + guard keyCommand.input == "\r", let delegate = (self.delegate as? RoomInputToolbarView) else { + return + } + + delegate.onTouchUp(inside: delegate.rightInputToolbarButton) + } +} diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h index 430daa2bf..8e5057db0 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h @@ -46,7 +46,7 @@ typedef enum : NSUInteger `RoomInputToolbarView` instance is a view used to handle all kinds of available inputs for a room (message composer, attachments selection...). */ -@interface RoomInputToolbarView : MXKRoomInputToolbarViewWithHPGrowingText +@interface RoomInputToolbarView : MXKRoomInputToolbarView /** The delegate notified when inputs are ready. diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index a0ab6b833..5c8fab5ec 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -27,6 +27,8 @@ #import "WidgetManager.h" #import "IntegrationManagerViewController.h" +@import GrowingTextView; + const double kContextBarHeight = 24; const NSTimeInterval kSendModeAnimationDuration = .15; const NSTimeInterval kActionMenuAttachButtonAnimationDuration = .4; @@ -36,12 +38,15 @@ const NSTimeInterval kActionMenuContentAlphaAnimationDuration = .2; const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; const CGFloat kComposerContainerTrailingPadding = 12; -@interface RoomInputToolbarView() +@interface RoomInputToolbarView() { // The intermediate action sheet UIAlertController *actionSheet; } +@property (nonatomic, weak) IBOutlet RoomInputToolbarTextView *textView; +@property (nonatomic, assign) CGFloat expandedMainToolbarHeight; + @end @implementation RoomInputToolbarView @@ -102,21 +107,21 @@ const CGFloat kComposerContainerTrailingPadding = 12; self.backgroundColor = [UIColor clearColor]; // Custom the growingTextView display - growingTextView.layer.cornerRadius = 0; - growingTextView.layer.borderWidth = 0; - growingTextView.backgroundColor = [UIColor clearColor]; + self.textView.layer.cornerRadius = 0; + self.textView.layer.borderWidth = 0; + self.textView.backgroundColor = [UIColor clearColor]; + + self.textView.font = [UIFont systemFontOfSize:15]; + self.textView.textColor = ThemeService.shared.theme.textPrimaryColor; + self.textView.tintColor = ThemeService.shared.theme.tintColor; + self.textView.placeholderColor = ThemeService.shared.theme.textTertiaryColor; + self.textView.showsVerticalScrollIndicator = NO; - growingTextView.font = [UIFont systemFontOfSize:15]; - growingTextView.textColor = ThemeService.shared.theme.textPrimaryColor; - growingTextView.tintColor = ThemeService.shared.theme.tintColor; - growingTextView.placeholderColor = ThemeService.shared.theme.textTertiaryColor; - growingTextView.internalTextView.showsVerticalScrollIndicator = NO; - - growingTextView.internalTextView.keyboardAppearance = ThemeService.shared.theme.keyboardAppearance; - if (growingTextView.isFirstResponder) + self.textView.keyboardAppearance = ThemeService.shared.theme.keyboardAppearance; + if (self.textView.isFirstResponder) { - [growingTextView resignFirstResponder]; - [growingTextView becomeFirstResponder]; + [self.textView resignFirstResponder]; + [self.textView becomeFirstResponder]; } self.attachMediaButton.accessibilityLabel = [VectorL10n roomAccessibilityUpload]; @@ -148,8 +153,15 @@ const CGFloat kComposerContainerTrailingPadding = 12; - (void)setTextMessage:(NSString *)textMessage { - [self updateUIWithTextMessage:textMessage animated:YES]; [super setTextMessage:textMessage]; + + self.textView.text = textMessage; + [self updateUIWithTextMessage:textMessage animated:YES]; +} + +- (NSString *)textMessage +{ + return self.textView.text; } - (void)setIsEncryptionEnabled:(BOOL)isEncryptionEnabled @@ -184,7 +196,7 @@ const CGFloat kComposerContainerTrailingPadding = 12; self.inputContextViewHeightConstraint.constant = kContextBarHeight; updatedHeight += kContextBarHeight; - self->growingTextView.maxHeight -= kContextBarHeight; + self.textView.maxHeight -= kContextBarHeight; break; case RoomInputToolbarViewSendModeEdit: buttonImage = [UIImage imageNamed:@"save_icon"]; @@ -193,7 +205,7 @@ const CGFloat kComposerContainerTrailingPadding = 12; self.inputContextViewHeightConstraint.constant = kContextBarHeight; updatedHeight += kContextBarHeight; - self->growingTextView.maxHeight -= kContextBarHeight; + self.textView.maxHeight -= kContextBarHeight; break; default: buttonImage = [UIImage imageNamed:@"send_icon"]; @@ -201,7 +213,7 @@ const CGFloat kComposerContainerTrailingPadding = 12; if (previousMode != _sendMode) { updatedHeight -= kContextBarHeight; - self->growingTextView.maxHeight += kContextBarHeight; + self.textView.maxHeight += kContextBarHeight; } self.inputContextViewHeightConstraint.constant = 0; break; @@ -211,7 +223,7 @@ const CGFloat kComposerContainerTrailingPadding = 12; if (self.maxHeight && updatedHeight > self.maxHeight) { - growingTextView.maxHeight -= updatedHeight - self.maxHeight; + self.textView.maxHeight -= updatedHeight - self.maxHeight; updatedHeight = self.maxHeight; } @@ -290,6 +302,12 @@ const CGFloat kComposerContainerTrailingPadding = 12; self.placeholder = placeholder; } +- (void)setPlaceholder:(NSString *)inPlaceholder +{ + [super setPlaceholder:inPlaceholder]; + self.textView.placeholder = inPlaceholder; +} + #pragma mark - Actions - (IBAction)cancelAction:(id)sender @@ -300,17 +318,17 @@ const CGFloat kComposerContainerTrailingPadding = 12; } } -#pragma mark - HPGrowingTextView delegate +#pragma mark - GrowingTextViewDelegate -- (BOOL)growingTextView:(HPGrowingTextView *)growingTextView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { - NSString *newText = [growingTextView.text stringByReplacingCharactersInRange:range withString:text]; + NSString *newText = [textView.text stringByReplacingCharactersInRange:range withString:text]; [self updateUIWithTextMessage:newText animated:YES]; - + return YES; } -- (void)growingTextViewDidChange:(HPGrowingTextView *)hpGrowingTextView +- (void)textViewDidChange:(UITextView *)textView { // Clean the carriage return added on return press if ([self.textMessage isEqualToString:@"\n"]) @@ -318,17 +336,20 @@ const CGFloat kComposerContainerTrailingPadding = 12; self.textMessage = nil; } - [super growingTextViewDidChange:hpGrowingTextView]; + if ([self.delegate respondsToSelector:@selector(roomInputToolbarView:isTyping:)]) + { + [self.delegate roomInputToolbarView:self isTyping:(self.textMessage.length > 0 ? YES : NO)]; + } } -- (void)growingTextView:(HPGrowingTextView *)hpGrowingTextView willChangeHeight:(float)height +- (void)textViewDidChangeHeight:(GrowingTextView *)textView height:(CGFloat)height { // Update height of the main toolbar (message composer) CGFloat updatedHeight = height + (self.messageComposerContainerTopConstraint.constant + self.messageComposerContainerBottomConstraint.constant) + self.inputContextViewHeightConstraint.constant; - + if (self.maxHeight && updatedHeight > self.maxHeight) { - hpGrowingTextView.maxHeight -= updatedHeight - self.maxHeight; + textView.maxHeight -= updatedHeight - self.maxHeight; updatedHeight = self.maxHeight; } @@ -336,9 +357,9 @@ const CGFloat kComposerContainerTrailingPadding = 12; { updatedHeight = self.mainToolbarMinHeightConstraint.constant; } - + self.mainToolbarHeightConstraint.constant = updatedHeight; - + // Update toolbar superview if ([self.delegate respondsToSelector:@selector(roomInputToolbarView:heightDidChanged:completion:)]) { @@ -377,12 +398,12 @@ const CGFloat kComposerContainerTrailingPadding = 12; { _actionMenuOpened = actionMenuOpened; - if (self->growingTextView.internalTextView.selectedRange.length > 0) + if (self.textView.selectedRange.length > 0) { - NSRange range = self->growingTextView.internalTextView.selectedRange; + NSRange range = self.textView.selectedRange; range.location = range.location + range.length; range.length = 0; - self->growingTextView.internalTextView.selectedRange = range; + self.textView.selectedRange = range; } if (_actionMenuOpened) { @@ -402,18 +423,19 @@ const CGFloat kComposerContainerTrailingPadding = 12; [UIView animateWithDuration:kActionMenuContentAlphaAnimationDuration delay:_actionMenuOpened ? 0 : .1 options:UIViewAnimationOptionCurveEaseIn animations:^{ self->messageComposerContainer.alpha = actionMenuOpened ? 0 : 1; - self.rightInputToolbarButton.alpha = self->growingTextView.text.length == 0 || actionMenuOpened ? 0 : 1; - self.voiceMessageToolbarView.alpha = self->growingTextView.text.length > 0 || actionMenuOpened ? 0 : 1; + self.rightInputToolbarButton.alpha = self.textView.text.length == 0 || actionMenuOpened ? 0 : 1; + self.voiceMessageToolbarView.alpha = self.textView.text.length > 0 || actionMenuOpened ? 0 : 1; } completion:nil]; [UIView animateWithDuration:kActionMenuComposerHeightAnimationDuration animations:^{ if (actionMenuOpened) { + self.expandedMainToolbarHeight = self.mainToolbarHeightConstraint.constant; self.mainToolbarHeightConstraint.constant = self.mainToolbarMinHeightConstraint.constant; } else { - [self->growingTextView refreshHeight]; + self.mainToolbarHeightConstraint.constant = self.expandedMainToolbarHeight; } [self layoutIfNeeded]; [self.delegate roomInputToolbarView:self heightDidChanged:self.mainToolbarHeightConstraint.constant completion:nil]; @@ -438,6 +460,8 @@ const CGFloat kComposerContainerTrailingPadding = 12; [UIView animateWithDuration:(animated ? 0.15f : 0.0f) animations:^{ self.rightInputToolbarButton.alpha = textMessage.length ? 1.0f : 0.0f; + self.rightInputToolbarButton.enabled = textMessage.length; + self.voiceMessageToolbarView.alpha = textMessage.length ? 0.0f : 1.0; }]; } diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib index f0f0c35b3..ff1cee7be 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib @@ -1,9 +1,9 @@ - + - + @@ -77,10 +77,13 @@ - + + + + @@ -138,7 +141,6 @@ - @@ -152,6 +154,7 @@ + diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index d6abf2904..2e83beb81 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -43,3 +43,4 @@ #import "RoomViewController.h" #import "ContactDetailsViewController.h" #import "GroupDetailsViewController.h" +#import "RoomInputToolbarView.h" diff --git a/changelog.d/4976.misc b/changelog.d/4976.misc new file mode 100644 index 000000000..882070b8d --- /dev/null +++ b/changelog.d/4976.misc @@ -0,0 +1 @@ +Replaced deprecated HPGrowingTextView with GrowingTextView. \ No newline at end of file From bb6fde3a08ecba036327c9fd4d9c83ceb616f261 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 12 Oct 2021 14:29:16 +0300 Subject: [PATCH 139/276] Add docs and rename block parameter --- Riot/Categories/NSArray+Element.h | 18 +++++++++++++++--- Riot/Categories/NSArray+Element.m | 12 ++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/Riot/Categories/NSArray+Element.h b/Riot/Categories/NSArray+Element.h index f8ef1a857..a71f8f714 100644 --- a/Riot/Categories/NSArray+Element.h +++ b/Riot/Categories/NSArray+Element.h @@ -20,11 +20,23 @@ NS_ASSUME_NONNULL_BEGIN @interface NSArray (Element) -- (NSArray *)vc_map:(id (^)(id obj))block; +/// Returns an array containing the results of mapping the given closure over the array's elements. +/// @param transform A mapping closure. `transform` accepts an element of this array as its parameter +/// and returns a transformed value of the same or of a different type. +/// @return An array containing the transformed elements of this array. +- (NSArray *)vc_map:(id (^)(id obj))transform; -- (NSArray *)vc_compactMap:(id _Nullable (^)(id obj))block; +/// Returns an array containing the non-nil results of mapping the given closure over the array's elements. +/// @param transform A mapping closure. `transform` accepts an element of this array as its parameter +/// and returns a nullable transformed value of the same or of a different type. +/// @return An array of the non-nil results of calling `transform` with each element of the array. +- (NSArray *)vc_compactMap:(id _Nullable (^)(id obj))transform; -- (NSArray *)vc_flatMap:(NSArray* (^)(id obj))block; +/// Returns an array containing the concatenated results of mapping the given closure over the array's elements. +/// @param transform A mapping closure. `transform` accepts an element of this array as its parameter +/// and returns an array.. +/// @return The resulting flattened array. +- (NSArray *)vc_flatMap:(NSArray* (^)(id obj))transform; @end diff --git a/Riot/Categories/NSArray+Element.m b/Riot/Categories/NSArray+Element.m index 35427c9df..c5b37d01d 100644 --- a/Riot/Categories/NSArray+Element.m +++ b/Riot/Categories/NSArray+Element.m @@ -18,22 +18,22 @@ @implementation NSArray (Element) -- (NSArray *)vc_map:(id (^)(id obj))block +- (NSArray *)vc_map:(id (^)(id obj))transform { NSMutableArray *result = [NSMutableArray arrayWithCapacity:self.count]; [self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { - [result addObject:block(obj)]; + [result addObject:transform(obj)]; }]; return result; } -- (NSArray *)vc_compactMap:(id _Nullable (^)(id obj))block +- (NSArray *)vc_compactMap:(id _Nullable (^)(id obj))transform { NSMutableArray *result = [NSMutableArray arrayWithCapacity:self.count]; [self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { - id mappedObject = block(obj); + id mappedObject = transform(obj); if (mappedObject) { [result addObject:mappedObject]; @@ -42,12 +42,12 @@ return result; } -- (NSArray *)vc_flatMap:(NSArray* (^)(id obj))block +- (NSArray *)vc_flatMap:(NSArray* (^)(id obj))transform { NSMutableArray *result = [NSMutableArray arrayWithCapacity:self.count]; [self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { - [result addObjectsFromArray:block(obj)]; + [result addObjectsFromArray:transform(obj)]; }]; return result; } From 582abd8653f1731f57faccf33cc0d989e9146e70 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 12 Oct 2021 14:29:40 +0300 Subject: [PATCH 140/276] Add unit tests for NSArray mapping --- Riot/SupportingFiles/Riot-Bridging-Header.h | 1 + RiotTests/NSArray+Element.swift | 95 +++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 RiotTests/NSArray+Element.swift diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index d6abf2904..3659fe244 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -43,3 +43,4 @@ #import "RoomViewController.h" #import "ContactDetailsViewController.h" #import "GroupDetailsViewController.h" +#import "NSArray+Element.h" diff --git a/RiotTests/NSArray+Element.swift b/RiotTests/NSArray+Element.swift new file mode 100644 index 000000000..4bcab0354 --- /dev/null +++ b/RiotTests/NSArray+Element.swift @@ -0,0 +1,95 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class NSArray_Element: XCTestCase { + + var array: NSArray = NSArray() // will contain strings + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + array = NSArray(array: ["str 1", "string 2", "test string 3"]) + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testMap() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + let mapped = array.vc_map { obj in + guard let string = obj as? String else { + XCTFail("Failed to setup test conditions") + return -1 + } + return string.count + } + + XCTAssertEqual(array.count, mapped.count, "Every element in the array must be mapped") + XCTAssertTrue(mapped.allSatisfy({ $0 is Int }), "Every element must be an Int") + XCTAssertTrue(mapped.allSatisfy({ ($0 as? Int ?? 0) > 0 }), "Every element must be an Int, greater than zero") + } + + func testCompactMap() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + let nilMapped: [Any] = array.vc_compactMap { obj in + return nil + } + XCTAssertTrue(nilMapped.isEmpty, "Mapped array must be empty") + + let onlyOneNilMapped: [Any] = array.vc_compactMap { obj in + guard let string = obj as? String else { + XCTFail("Failed to setup test conditions") + return -1 + } + if string.hasSuffix("2") { + return nil + } + return string.count + } + + XCTAssertEqual(array.count - 1, onlyOneNilMapped.count, "Every element in the array must be mapped, except 'string 2'") + XCTAssertTrue(onlyOneNilMapped.allSatisfy({ $0 is Int }), "Every element must be an Int") + XCTAssertTrue(onlyOneNilMapped.allSatisfy({ ($0 as? Int ?? 0) > 0 }), "Every element must be an Int, greater than zero") + } + + func testFlatMap() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + let emptyMapped: [Any] = array.vc_flatMap { obj in + return [] + } + XCTAssertTrue(emptyMapped.isEmpty, "Mapped array must be empty") + + let twiceMapped: [Any] = array.vc_flatMap { obj in + guard let string = obj as? String else { + XCTFail("Failed to setup test conditions") + return [] + } + return [string.count, string.count * 2] + } + XCTAssertEqual(array.count * 2, twiceMapped.count, "Mapped array must have twice time elements than the 'array'") + + let constantMapped: [Any] = array.vc_flatMap { obj in + return [1, 2] + } + XCTAssertEqual(array.count * 2, constantMapped.count, "Constantly mapped array must still have twice time elements than the 'array'") + } + +} From 3a2b5cf11661f35b365d04899505b9f6d0964837 Mon Sep 17 00:00:00 2001 From: manuroe Date: Tue, 12 Oct 2021 13:41:53 +0200 Subject: [PATCH 141/276] Prepare for new sprint --- Config/AppVersion.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 5e3a09e8f..79d20fa80 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.6.4 -CURRENT_PROJECT_VERSION = 1.6.4 +MARKETING_VERSION = 1.6.5 +CURRENT_PROJECT_VERSION = 1.6.5 From ac94a4978bc0c223a4836970c90303bac118d136 Mon Sep 17 00:00:00 2001 From: manuroe Date: Tue, 12 Oct 2021 13:45:01 +0200 Subject: [PATCH 142/276] Prepare for new sprint --- Config/AppVersion.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 5e3a09e8f..79d20fa80 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.6.4 -CURRENT_PROJECT_VERSION = 1.6.4 +MARKETING_VERSION = 1.6.5 +CURRENT_PROJECT_VERSION = 1.6.5 From 8d426188589208943849a8495cac48c008bf191c Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 12 Oct 2021 19:06:56 +0200 Subject: [PATCH 143/276] BuildSettings: Add property to allow split view detail view stacking. --- Config/BuildSettings.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 321fb8b30..bb1b25a0a 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -281,6 +281,9 @@ final class BuildSettings: NSObject { static let roomScreenAllowStickerAction: Bool = true static let roomScreenAllowFilesAction: Bool = true + /// Allow split view detail view stacking + static let allowSplitViewDetailsScreenStacking: Bool = true + // MARK: - Room Contextual Menu static let roomContextualMenuShowMoreOptionForMessages: Bool = true From 135058846501fd1ac9b13579d64f4c6f2686c297 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 12 Oct 2021 19:07:44 +0200 Subject: [PATCH 144/276] Add UniversalLinkParameters. --- .../DeepLink/UniversalLinkParameters.swift | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 Riot/Modules/DeepLink/UniversalLinkParameters.swift diff --git a/Riot/Modules/DeepLink/UniversalLinkParameters.swift b/Riot/Modules/DeepLink/UniversalLinkParameters.swift new file mode 100644 index 000000000..baf2dbe94 --- /dev/null +++ b/Riot/Modules/DeepLink/UniversalLinkParameters.swift @@ -0,0 +1,59 @@ +// +// 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 + +/// Parameters describing a universal link +@objcMembers +class UniversalLinkParameters: NSObject { + + // MARK: - Properties + + /// The unprocessed the universal link URL + let universalLinkURL: URL? + + /// The fragment part of the universal link + let fragment: String + + /// Indicates if the view to open after the link should replace or be stacked on visible views + let stackAboveVisibleViewsOnRedirect: Bool + + // MARK: - Setup + + init(fragment: String, + universalLinkURL: URL, + stackAboveVisibleViewsOnRedirect: Bool) { + self.fragment = fragment + self.universalLinkURL = universalLinkURL + self.stackAboveVisibleViewsOnRedirect = stackAboveVisibleViewsOnRedirect + + super.init() + } + + init?(universalLinkURL: URL, + stackAboveVisibleViewsOnRedirect: Bool) { + + guard let fixedURL = Tools.fixURL(withSeveralHashKeys: universalLinkURL), let fragment = fixedURL.fragment else { + return nil + } + + self.fragment = fragment + self.universalLinkURL = universalLinkURL + self.stackAboveVisibleViewsOnRedirect = stackAboveVisibleViewsOnRedirect + + super.init() + } +} From d3dbee65e3fe38a2ccfa6c499cb17a66fe4e8ac6 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 12 Oct 2021 19:09:18 +0200 Subject: [PATCH 145/276] Add RoomPresentationParameters and RoomPreviewPresentationParameters to display a room or room preview with identifiers and presentation configuration. --- .../RoomPresentationParameters.swift | 56 +++++++++++++++++++ .../RoomPreviewPresentationParameters.swift | 40 +++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 Riot/Modules/Application/RoomPresentationParameters.swift create mode 100644 Riot/Modules/Application/RoomPreviewPresentationParameters.swift diff --git a/Riot/Modules/Application/RoomPresentationParameters.swift b/Riot/Modules/Application/RoomPresentationParameters.swift new file mode 100644 index 000000000..46019f222 --- /dev/null +++ b/Riot/Modules/Application/RoomPresentationParameters.swift @@ -0,0 +1,56 @@ +// +// 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 + +/// Presentation parameters to display a room with a provided identifier in a specific matrix session. +@objcMembers +class RoomPresentationParameters: NSObject { + + // MARK: - Properties + + /// The room identifier + let roomId: String + + /// If not nil, the room will be opened on this event. + let eventId: String? + + /// The Matrix session in which the room should be available. + let mxSession: MXSession + + /// Indicate to pop to home and restore initial view hierarchy + let restoreInitialDisplay: Bool + + /// Indicate to stack above visible views + /// If this variable is set to true `restoreInitialDisplay` should be set to false to have effect + let stackAboveVisibleViews: Bool + + // MARK: - Setup + + init(roomId: String, + eventId: String?, + mxSession: MXSession, + restoreInitialDisplay: Bool, + stackAboveVisibleViews: Bool) { + self.roomId = roomId + self.eventId = eventId + self.mxSession = mxSession + self.restoreInitialDisplay = restoreInitialDisplay + self.stackAboveVisibleViews = stackAboveVisibleViews + + super.init() + } +} diff --git a/Riot/Modules/Application/RoomPreviewPresentationParameters.swift b/Riot/Modules/Application/RoomPreviewPresentationParameters.swift new file mode 100644 index 000000000..b84b25c0d --- /dev/null +++ b/Riot/Modules/Application/RoomPreviewPresentationParameters.swift @@ -0,0 +1,40 @@ +// +// 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 + +/// Presentation parameters to display a preview of a room that is unknown for the user. +/// This room can come from an email invitation link or a simple link to a room. +@objcMembers +class RoomPreviewPresentationParameters: RoomPresentationParameters { + + // MARK: - Properties + + /// The data for the room preview + let previewData: RoomPreviewData + + // MARK: - Setup + + init(previewData: RoomPreviewData, restoreInitialDisplay: Bool, stackAboveVisibleViews: Bool) { + self.previewData = previewData + + super.init(roomId: previewData.roomId, + eventId: previewData.eventId, + mxSession: previewData.mxSession, + restoreInitialDisplay: restoreInitialDisplay, + stackAboveVisibleViews: stackAboveVisibleViews) + } +} From 5de0306e17c173182ca0b18023eb135e10801f62 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 12 Oct 2021 19:10:30 +0200 Subject: [PATCH 146/276] MasterTabBarController: Use RoomPresentationParameters and RoomPreviewPresentationParameters input parameters and selecting a room. --- Riot/Modules/TabBar/MasterTabBarController.h | 43 +++++++------------- Riot/Modules/TabBar/MasterTabBarController.m | 21 ++++------ 2 files changed, 23 insertions(+), 41 deletions(-) diff --git a/Riot/Modules/TabBar/MasterTabBarController.h b/Riot/Modules/TabBar/MasterTabBarController.h index 92d064685..b9a40a00a 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.h +++ b/Riot/Modules/TabBar/MasterTabBarController.h @@ -42,7 +42,8 @@ typedef NS_ENUM(NSUInteger, MasterTabBarIndex) { }; @protocol MasterTabBarControllerDelegate; - +@class RoomPresentationParameters; +@class RoomPreviewPresentationParameters; @interface MasterTabBarController : UITabBarController @@ -79,33 +80,16 @@ typedef NS_ENUM(NSUInteger, MasterTabBarIndex) { */ - (void)showAuthenticationScreenAfterSoftLogout:(MXCredentials*)softLogoutCredentials; -/** - Open the room with the provided identifier in a specific matrix session. - - @param roomId the room identifier. - @param eventId if not nil, the room will be opened on this event. - @param mxSession the matrix session in which the room should be available. - */ -- (void)selectRoomWithId:(NSString*)roomId andEventId:(NSString*)eventId inMatrixSession:(MXSession*)mxSession; +/// Open the room with the provided identifier in a specific matrix session. +/// @param parameters the presentation parameters that contains room information plus display information. +/// @param completion the block to execute at the end of the operation. +- (void)selectRoomWithParameters:(RoomPresentationParameters*)parameters completion:(void (^)(void))completion; -/** - Open the room with the provided identifier in a specific matrix session. - - @param roomId the room identifier. - @param eventId if not nil, the room will be opened on this event. - @param matrixSession the matrix session in which the room should be available. - @param completion the block to execute at the end of the operation. - */ -- (void)selectRoomWithId:(NSString*)roomId andEventId:(NSString*)eventId inMatrixSession:(MXSession*)matrixSession completion:(void (^)(void))completion; - -/** - Open the RoomViewController to display the preview of a room that is unknown for the user. - - This room can come from an email invitation link or a simple link to a room. - - @param roomPreviewData the data for the room preview. - */ -- (void)showRoomPreview:(RoomPreviewData*)roomPreviewData; +/// Open the RoomViewController to display the preview of a room that is unknown for the user. +/// This room can come from an email invitation link or a simple link to a room. +/// @param parameters the presentation parameters that contains room preview information plus display information. +/// @param completion the block to execute at the end of the operation. +- (void)selectRoomPreviewWithParameters:(RoomPreviewPresentationParameters*)parameters completion:(void (^)(void))completion; /** Open a ContactDetailsViewController to display the information of the provided contact. @@ -202,8 +186,9 @@ typedef NS_ENUM(NSUInteger, MasterTabBarIndex) { - (void)masterTabBarControllerDidCompleteAuthentication:(MasterTabBarController *)masterTabBarController; - (void)masterTabBarController:(MasterTabBarController*)masterTabBarController needsSideMenuIconWithNotification:(BOOL)displayNotification; -- (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectRoomWithId:(NSString*)roomId andEventId:(NSString*)eventId inMatrixSession:(MXSession*)matrixSession completion:(void (^)(void))completion; -- (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectRoomPreviewWithData:(RoomPreviewData*)roomPreviewData; +- (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectRoomWithParameters:(RoomPresentationParameters*)roomPresentationParameters completion:(void (^)(void))completion; +- (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectRoomPreviewWithParameters:(RoomPreviewPresentationParameters*)roomPreviewPresentationParameters completion:(void (^)(void))completion; + - (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectContact:(MXKContact*)contact; - (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectGroup:(MXGroup*)group inMatrixSession:(MXSession*)matrixSession; diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m index f5ceb2895..ee917f8e4 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -584,33 +584,30 @@ } } -- (void)selectRoomWithId:(NSString*)roomId andEventId:(NSString*)eventId inMatrixSession:(MXSession*)matrixSession -{ - [self selectRoomWithId:roomId andEventId:eventId inMatrixSession:matrixSession completion:nil]; -} - -- (void)selectRoomWithId:(NSString*)roomId andEventId:(NSString*)eventId inMatrixSession:(MXSession*)matrixSession completion:(void (^)(void))completion +- (void)selectRoomWithParameters:(RoomPresentationParameters*)paramaters completion:(void (^)(void))completion { [self releaseSelectedItem]; - _selectedRoomId = roomId; - _selectedEventId = eventId; - _selectedRoomSession = matrixSession; + _selectedRoomId = paramaters.roomId; + _selectedEventId = paramaters.eventId; + _selectedRoomSession = paramaters.mxSession; - [self.masterTabBarDelegate masterTabBarController:self didSelectRoomWithId:roomId andEventId:eventId inMatrixSession:matrixSession completion:completion]; + [self.masterTabBarDelegate masterTabBarController:self didSelectRoomWithParameters:paramaters completion:completion]; [self refreshSelectedControllerSelectedCellIfNeeded]; } -- (void)showRoomPreview:(RoomPreviewData *)roomPreviewData +- (void)selectRoomPreviewWithParameters:(RoomPreviewPresentationParameters*)parameters completion:(void (^)(void))completion { [self releaseSelectedItem]; + RoomPreviewData *roomPreviewData = parameters.previewData; + _selectedRoomPreviewData = roomPreviewData; _selectedRoomId = roomPreviewData.roomId; _selectedRoomSession = roomPreviewData.mxSession; - [self.masterTabBarDelegate masterTabBarController:self didSelectRoomPreviewWithData:roomPreviewData]; + [self.masterTabBarDelegate masterTabBarController:self didSelectRoomPreviewWithParameters:parameters completion:completion]; [self refreshSelectedControllerSelectedCellIfNeeded]; } From 9d16b7f93029fe7826483afdaeca223f3f87e087 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 12 Oct 2021 19:12:08 +0200 Subject: [PATCH 147/276] RoomVC: Update universal link management with UniversalLinkParameters. --- .../Room/RoomViewController+Spaces.swift | 22 +++++++++-------- Riot/Modules/Room/RoomViewController.h | 19 ++++----------- Riot/Modules/Room/RoomViewController.m | 24 +++++++++---------- 3 files changed, 28 insertions(+), 37 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController+Spaces.swift b/Riot/Modules/Room/RoomViewController+Spaces.swift index f85197cd8..dfde5b11b 100644 --- a/Riot/Modules/Room/RoomViewController+Spaces.swift +++ b/Riot/Modules/Room/RoomViewController+Spaces.swift @@ -18,8 +18,10 @@ import Foundation /// this extension is temprorary and implements navigation to the Space bootom sheet. This should be moved to an universal link flow coordinator extension RoomViewController { - @objc func handleSpaceUniversalLink(with url: URL) { - let url = Tools.fixURL(withSeveralHashKeys: url) + + @objc func handleSpaceUniversalLink(with universalLinkParameters: UniversalLinkParameters) -> Bool { + + let url = universalLinkParameters.universalLinkURL var pathParamsObjc: NSArray? var queryParamsObjc: NSMutableDictionary? @@ -28,7 +30,7 @@ extension RoomViewController { // Sanity check guard let pathParams = pathParamsObjc as? [String], pathParams.count > 0 else { MXLog.error("[RoomViewController] Universal link: Error: No path parameters") - return + return false } var roomIdOrAliasParam: String? @@ -67,8 +69,7 @@ extension RoomViewController { } guard let roomIdOrAlias = roomIdOrAliasParam else { - AppDelegate.theDelegate().handleUniversalLinkURL(url) - return + return AppDelegate.theDelegate().handleUniversalLink(with: universalLinkParameters) } self.startActivityIndicator() @@ -94,14 +95,16 @@ extension RoomViewController { return } - self.requestSummaryAndShowSpaceDetail(forRoomWithId: roomId, via: viaServers, from: url) + self.requestSummaryAndShowSpaceDetail(forRoomWithId: roomId, via: viaServers, from: universalLinkParameters) } } else { - self.requestSummaryAndShowSpaceDetail(forRoomWithId: roomIdOrAlias, via: viaServers, from: url) + self.requestSummaryAndShowSpaceDetail(forRoomWithId: roomIdOrAlias, via: viaServers, from: universalLinkParameters) } + + return true } - private func requestSummaryAndShowSpaceDetail(forRoomWithId roomId: String, via: [String], from url: URL?) { + private func requestSummaryAndShowSpaceDetail(forRoomWithId roomId: String, via: [String], from universalLinkParameters: UniversalLinkParameters) { if self.mainSession.spaceService.getSpace(withId: roomId) != nil { self.stopActivityIndicator() self.showSpaceDetail(withId: roomId) @@ -116,12 +119,11 @@ extension RoomViewController { self.stopActivityIndicator() guard let publicRoom = response.value, publicRoom.roomTypeString == MXRoomTypeString.space.rawValue else { - AppDelegate.theDelegate().handleUniversalLinkURL(url) + AppDelegate.theDelegate().handleUniversalLink(with: universalLinkParameters) return } self.showSpaceDetail(with: publicRoom) } } - } diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index 7fa73286a..99598f5df 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -28,6 +28,7 @@ #import "UIViewController+RiotSearch.h" @class BadgeLabel; +@class UniversalLinkParameters; @protocol RoomViewControllerDelegate; NS_ASSUME_NONNULL_BEGIN @@ -170,26 +171,14 @@ extern NSNotificationName const RoomGroupCallTileTappedNotification; - (void)roomViewControllerPreviewDidTapCancel:(RoomViewController *)roomViewController; /** - Handle the fragment of a universal link. + Process universal link. @param roomViewController the `RoomViewController` instance. - @param fragment the fragment part of the universal link. - @param universalLinkURL the unprocessed the universal link URL (optional). - @return true to indicate that the fragment has been handled, or false when the fragment is not supported. - */ -- (BOOL)roomViewController:(RoomViewController *)roomViewController -handleUniversalLinkFragment:(NSString*)fragment - fromURL:(nullable NSURL*)universalLinkURL; - -/** - Process universal link. - - @param roomViewController the `RoomViewController` instance. - @param universalLinkURL the universal link URL. + @param parameters the universal link parameters. @return YES in case of processing success. */ - (BOOL)roomViewController:(RoomViewController *)roomViewController - handleUniversalLinkURL:(NSURL*)universalLinkURL; +handleUniversalLinkWithParameters:(UniversalLinkParameters*)parameters; @end diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 075a3f57f..aa056250b 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -2189,26 +2189,26 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; - (BOOL)handleUniversalLinkURL:(NSURL*)universalLinkURL { - if (self.delegate) - { - return [self.delegate roomViewController:self handleUniversalLinkURL:universalLinkURL]; - } - else - { - [self handleSpaceUniversalLinkWith:universalLinkURL]; - return YES; - } + UniversalLinkParameters *parameters = [[UniversalLinkParameters alloc] initWithUniversalLinkURL:universalLinkURL stackAboveVisibleViewsOnRedirect:BuildSettings.allowSplitViewDetailsScreenStacking]; + return [self handleUniversalLinkWithParameters:parameters]; } - + - (BOOL)handleUniversalLinkFragment:(NSString*)fragment fromURL:(NSURL*)universalLinkURL +{ + UniversalLinkParameters *parameters = [[UniversalLinkParameters alloc] initWithFragment:fragment + universalLinkURL:universalLinkURL stackAboveVisibleViewsOnRedirect:BuildSettings.allowSplitViewDetailsScreenStacking]; + return [self handleUniversalLinkWithParameters:parameters]; +} + +- (BOOL)handleUniversalLinkWithParameters:(UniversalLinkParameters*)parameters { if (self.delegate) { - return [self.delegate roomViewController:self handleUniversalLinkFragment:fragment fromURL:universalLinkURL]; + return [self.delegate roomViewController:self handleUniversalLinkWithParameters:parameters]; } else { - return [[AppDelegate theDelegate] handleUniversalLinkFragment:fragment fromURL:universalLinkURL]; + return [self handleSpaceUniversalLinkWith:parameters]; } } From 9bfce4257242a90b016f62712e7b74ac6018df25 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 12 Oct 2021 19:15:35 +0200 Subject: [PATCH 148/276] TabBarCoordinator: Update RoomViewControllerDelegate conformance. Handle RoomPresentationParameters and RoomPreviewPresentationParameters. --- Riot/Modules/TabBar/TabBarCoordinator.swift | 59 +++++++++++++++++---- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index 8b1d3735a..89c6d6b85 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -365,7 +365,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { } } - private func showRoom(with roomId: String) { + private func showRoom(withId roomId: String) { guard let matrixSession = self.parameters.userSessionsService.mainUserSession?.matrixSession else { return @@ -374,6 +374,18 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { self.showRoom(with: roomId, eventId: nil, matrixSession: matrixSession) } + private func showRoom(withPresentationParameters roomPresentationParameters: RoomPresentationParameters, completion: (() -> Void)?) { + + let roomCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared, + session: roomPresentationParameters.mxSession, + roomId: roomPresentationParameters.roomId, + eventId: roomPresentationParameters.eventId) + + self.showRoom(with: roomCoordinatorParameters, + stackOnSplitViewDetail: roomPresentationParameters.stackAboveVisibleViews, + completion: completion) + } + private func showRoom(with roomId: String, eventId: String?, matrixSession: MXSession, completion: (() -> Void)? = nil) { // RoomCoordinator will be presented by the split view. @@ -394,7 +406,19 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { self.showRoom(with: roomCoordinatorParameters) } - private func showRoom(with parameters: RoomCoordinatorParameters, completion: (() -> Void)? = nil) { + private func showRoomPreview(withPresentationParameters roomPreviewPresentationParameters: RoomPreviewPresentationParameters, completion: (() -> Void)?) { + + let roomCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared, + previewData: roomPreviewPresentationParameters.previewData) + + self.showRoom(with: roomCoordinatorParameters, + stackOnSplitViewDetail: roomPreviewPresentationParameters.stackAboveVisibleViews, + completion: completion) + } + + private func showRoom(with parameters: RoomCoordinatorParameters, + stackOnSplitViewDetail: Bool = false, + completion: (() -> Void)? = nil) { if let topRoomCoordinator = self.splitViewMasterPresentableDelegate?.detailModules.last as? RoomCoordinatorProtocol, parameters.roomId == topRoomCoordinator.roomId && parameters.session == topRoomCoordinator.mxSession { @@ -415,9 +439,8 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { coordinator.delegate = self coordinator.start(withCompletion: completion) self.add(childCoordinator: coordinator) - - self.replaceSplitViewDetails(with: coordinator) { - [weak self] in + + self.showSplitViewDetails(with: coordinator, stackOnSplitViewDetail: stackOnSplitViewDetail) { [weak self] in // NOTE: The RoomDataSource releasing is handled in SplitViewCoordinator self?.remove(childCoordinator: coordinator) } @@ -428,6 +451,20 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { self.splitViewMasterPresentableDelegate?.splitViewMasterPresentable(self, wantsToReplaceDetailWith: presentable, popCompletion: popCompletion) } + /// If the split view is collapsed (one column visible) it will push the Presentable on the primary navigation controller, otherwise it will show the Presentable as the secondary view of the split view on top of existing views. + private func stackSplitViewDetails(with presentable: Presentable, popCompletion: (() -> Void)? = nil) { + self.splitViewMasterPresentableDelegate?.splitViewMasterPresentable(self, wantsToStack: presentable, popCompletion: popCompletion) + } + + private func showSplitViewDetails(with presentable: Presentable, stackOnSplitViewDetail: Bool, popCompletion: (() -> Void)? = nil) { + + if stackOnSplitViewDetail { + self.stackSplitViewDetails(with: presentable, popCompletion: popCompletion) + } else { + self.replaceSplitViewDetails(with: presentable, popCompletion: popCompletion) + } + } + // MARK: UserSessions management private func registerUserSessionsServiceNotifications() { @@ -485,9 +522,13 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { // MARK: - MasterTabBarControllerDelegate extension TabBarCoordinator: MasterTabBarControllerDelegate { - - func masterTabBarController(_ masterTabBarController: MasterTabBarController!, didSelectRoomPreviewWith roomPreviewData: RoomPreviewData!) { - self.showRoomPreview(with: roomPreviewData) + + func masterTabBarController(_ masterTabBarController: MasterTabBarController!, didSelectRoomWith roomPresentationParameters: RoomPresentationParameters!, completion: (() -> Void)!) { + self.showRoom(withPresentationParameters: roomPresentationParameters, completion: completion) + } + + func masterTabBarController(_ masterTabBarController: MasterTabBarController!, didSelectRoomPreviewWith roomPreviewPresentationParameters: RoomPreviewPresentationParameters!, completion: (() -> Void)!) { + self.showRoomPreview(withPresentationParameters: roomPreviewPresentationParameters, completion: completion) } func masterTabBarController(_ masterTabBarController: MasterTabBarController!, didSelect contact: MXKContact!) { @@ -533,7 +574,7 @@ extension TabBarCoordinator: RoomCoordinatorDelegate { } func roomCoordinator(_ coordinator: RoomCoordinatorProtocol, didSelectRoomWithId roomId: String) { - self.showRoom(with: roomId) + self.showRoom(withId: roomId) } } From 0f46c99538c3cc455815b6e136cf26000f3dad2f Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 12 Oct 2021 19:20:39 +0200 Subject: [PATCH 149/276] LegacyAppDelegate: Update universal link management with UniversalLinkParameters. And handle show room with RoomPresentationParameters and room preview with RoomPreviewPresentationParameters. --- Riot/Modules/Application/LegacyAppDelegate.h | 24 +++++- Riot/Modules/Application/LegacyAppDelegate.m | 83 +++++++++++++++----- 2 files changed, 86 insertions(+), 21 deletions(-) diff --git a/Riot/Modules/Application/LegacyAppDelegate.h b/Riot/Modules/Application/LegacyAppDelegate.h index ddf33c28b..cd4526a94 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.h +++ b/Riot/Modules/Application/LegacyAppDelegate.h @@ -31,6 +31,9 @@ @protocol LegacyAppDelegateDelegate; @class CallBar; @class CallPresenter; +@class RoomPresentationParameters; +@class RoomPreviewPresentationParameters; +@class UniversalLinkParameters; #pragma mark - Notifications /** @@ -204,15 +207,24 @@ UINavigationControllerDelegate #pragma mark - Matrix Room handling // Show a room and jump to the given event if event id is not nil otherwise go to last messages. -- (void)showRoom:(NSString*)roomId andEventId:(NSString*)eventId withMatrixSession:(MXSession*)mxSession restoreInitialDisplay:(BOOL)restoreInitialDisplay completion:(void (^)(void))completion; +- (void)showRoomWithParameters:(RoomPresentationParameters*)parameters completion:(void (^)(void))completion; -- (void)showRoom:(NSString*)roomId andEventId:(NSString*)eventId withMatrixSession:(MXSession*)mxSession restoreInitialDisplay:(BOOL)restoreInitialDisplay; +- (void)showRoomWithParameters:(RoomPresentationParameters*)parameters; +// Restore display and show the room - (void)showRoom:(NSString*)roomId andEventId:(NSString*)eventId withMatrixSession:(MXSession*)mxSession; // Creates a new direct chat with the provided user id - (void)createDirectChatWithUserId:(NSString*)userId completion:(void (^)(void))completion; +// Show room preview +- (void)showRoomPreviewWithParameters:(RoomPreviewPresentationParameters*)parameters completion:(void (^)(void))completion; + +- (void)showRoomPreviewWithParameters:(RoomPreviewPresentationParameters*)parameters; + +// Restore display and show the room preview +- (void)showRoomPreview:(RoomPreviewData*)roomPreviewData; + // Reopen an existing direct room with this userId or creates a new one (if it doesn't exist) - (void)startDirectChatWithUserId:(NSString*)userId completion:(void (^)(void))completion; @@ -241,6 +253,14 @@ UINavigationControllerDelegate */ - (BOOL)handleUniversalLinkURL:(NSURL*)universalLinkURL; +/** + Process universal link. + + @param parameters the universal link parameters. + @return YES in case of processing success. + */ +- (BOOL)handleUniversalLinkWithParameters:(UniversalLinkParameters*)parameters; + /** Extract params from the URL fragment part (after '#') of a vector.im Universal link: diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index c86695012..e470cef05 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -1236,8 +1236,21 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni return [self handleUniversalLinkFragment:fragment fromURL:nil]; } + - (BOOL)handleUniversalLinkFragment:(NSString*)fragment fromURL:(NSURL*)universalLinkURL + { + UniversalLinkParameters *parameters = [[UniversalLinkParameters alloc] initWithFragment:fragment universalLinkURL:universalLinkURL stackAboveVisibleViewsOnRedirect:NO]; + + return [self handleUniversalLinkWithParameters:parameters]; +} + +- (BOOL)handleUniversalLinkWithParameters:(UniversalLinkParameters*)parameters +{ + NSString *fragment = parameters.fragment; + NSURL *universalLinkURL = parameters.universalLinkURL; + BOOL stackAboveVisibleViewsOnRedirect = parameters.stackAboveVisibleViewsOnRedirect; + BOOL continueUserActivity = NO; MXKAccountManager *accountManager = [MXKAccountManager sharedManager]; @@ -1347,7 +1360,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni else { // Open the room page - [self showRoom:roomId andEventId:eventId withMatrixSession:account.mxSession]; + RoomPresentationParameters *roomPresentationParameters = [[RoomPresentationParameters alloc] initWithRoomId:roomId eventId:eventId mxSession:account.mxSession restoreInitialDisplay:!stackAboveVisibleViewsOnRedirect stackAboveVisibleViews:stackAboveVisibleViewsOnRedirect]; + + [self showRoomWithParameters:roomPresentationParameters]; } continueUserActivity = YES; @@ -1398,7 +1413,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni { universalLinkFragmentPendingRoomAlias = @{roomId: roomIdOrAlias}; - [self handleUniversalLinkFragment:newUniversalLinkFragment fromURL:universalLinkURL]; + UniversalLinkParameters *newParameters = [[UniversalLinkParameters alloc] initWithFragment:newUniversalLinkFragment universalLinkURL:universalLinkURL stackAboveVisibleViewsOnRedirect:stackAboveVisibleViewsOnRedirect]; + + [self handleUniversalLinkWithParameters:newParameters]; } else { @@ -1436,7 +1453,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni if (notif.object == account.mxSession && account.mxSession.state == MXSessionStateRunning) { MXLogDebug(@"[AppDelegate] Universal link: The session is running. Retry the link"); - [self handleUniversalLinkFragment:fragment fromURL:universalLinkURL]; + [self handleUniversalLinkWithParameters:parameters]; } } }]; @@ -1494,7 +1511,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni if ([universalLinkFragmentPending isEqualToString:fragment]) { MXLogDebug(@"[AppDelegate] Universal link: The user is now logged in. Retry the link"); - [self handleUniversalLinkFragment:fragment fromURL:universalLinkURL]; + [self handleUniversalLinkWithParameters:parameters]; } }]; } @@ -1559,7 +1576,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni if ([universalLinkFragmentPending isEqualToString:fragment]) { MXLogDebug(@"[AppDelegate] Universal link: The user is now logged in. Retry the link"); - [self handleUniversalLinkFragment:fragment fromURL:universalLinkURL]; + [self handleUniversalLinkWithParameters:parameters]; } }]; } @@ -2782,8 +2799,17 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } } -- (void)showRoom:(NSString*)roomId andEventId:(NSString*)eventId withMatrixSession:(MXSession*)mxSession restoreInitialDisplay:(BOOL)restoreInitialDisplay completion:(void (^)(void))completion +- (void)showRoomWithParameters:(RoomPresentationParameters*)parameters { + [self showRoomWithParameters:parameters completion:nil]; +} + +- (void)showRoomWithParameters:(RoomPresentationParameters*)parameters completion:(void (^)(void))completion +{ + NSString *roomId = parameters.roomId; + MXSession *mxSession = parameters.mxSession; + BOOL restoreInitialDisplay = parameters.restoreInitialDisplay; + if (roomId && mxSession) { MXRoom *room = [mxSession roomWithRoomId:roomId]; @@ -2805,8 +2831,8 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni void (^selectRoom)(void) = ^() { // Select room to display its details (dispatch this action in order to let TabBarController end its refresh) - [self.masterTabBarController selectRoomWithId:roomId andEventId:eventId inMatrixSession:mxSession completion:^{ - + + [self.masterTabBarController selectRoomWithParameters:parameters completion:^{ // Remove delivered notifications for this room [self.pushNotificationService removeDeliveredNotificationsWithRoomId:roomId completion:nil]; @@ -2829,23 +2855,42 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } } -- (void)showRoom:(NSString*)roomId andEventId:(NSString*)eventId withMatrixSession:(MXSession*)mxSession restoreInitialDisplay:(BOOL)restoreInitialDisplay -{ - [self showRoom:roomId andEventId:eventId withMatrixSession:mxSession restoreInitialDisplay:restoreInitialDisplay completion:nil]; -} - - (void)showRoom:(NSString*)roomId andEventId:(NSString*)eventId withMatrixSession:(MXSession*)mxSession { - [self showRoom:roomId andEventId:eventId withMatrixSession:mxSession restoreInitialDisplay:YES completion:nil]; + RoomPresentationParameters *parameters = [[RoomPresentationParameters alloc] initWithRoomId:roomId + eventId:eventId mxSession:mxSession restoreInitialDisplay:YES stackAboveVisibleViews:NO]; + + [self showRoomWithParameters:parameters]; +} + +- (void)showRoomPreviewWithParameters:(RoomPreviewPresentationParameters*)parameters completion:(void (^)(void))completion +{ + void (^showRoomPreview)(void) = ^() { + [self.masterTabBarController selectRoomPreviewWithParameters:parameters completion:completion]; + }; + + if (parameters.restoreInitialDisplay) + { + [self restoreInitialDisplay:^{ + showRoomPreview(); + }]; + } + else + { + showRoomPreview(); + } +} + +- (void)showRoomPreviewWithParameters:(RoomPreviewPresentationParameters*)parameters +{ + [self showRoomPreviewWithParameters:parameters completion:nil]; } - (void)showRoomPreview:(RoomPreviewData*)roomPreviewData { - [self restoreInitialDisplay:^{ - - [_masterTabBarController showRoomPreview:roomPreviewData]; - - }]; + RoomPreviewPresentationParameters *parameters = [[RoomPreviewPresentationParameters alloc] initWithPreviewData:roomPreviewData restoreInitialDisplay:YES stackAboveVisibleViews:NO]; + + [self showRoomPreviewWithParameters:parameters]; } - (void)setVisibleRoomId:(NSString *)roomId From e8d1777919af14c4b5d437d49f9515d5d2fed16f Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 12 Oct 2021 19:21:19 +0200 Subject: [PATCH 150/276] RoomCoordinator: Update RoomViewControllerDelegate conformance. --- Riot/Modules/Room/RoomCoordinator.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index ce8f41ef7..dd3ca7d2f 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -231,12 +231,8 @@ extension RoomCoordinator: RoomViewControllerDelegate { func roomViewController(_ roomViewController: RoomViewController, showCompleteSecurityFor session: MXSession) { AppDelegate.theDelegate().presentCompleteSecurity(for: session) } - - func roomViewController(_ roomViewController: RoomViewController, handleUniversalLinkFragment fragment: String, from universalLinkURL: URL?) -> Bool { - return AppDelegate.theDelegate().handleUniversalLinkFragment(fragment, from: universalLinkURL) - } - func roomViewController(_ roomViewController: RoomViewController, handleUniversalLinkURL universalLinkURL: URL) -> Bool { - return AppDelegate.theDelegate().handleUniversalLinkURL(universalLinkURL) + func roomViewController(_ roomViewController: RoomViewController, handleUniversalLinkWith parameters: UniversalLinkParameters) -> Bool { + return AppDelegate.theDelegate().handleUniversalLink(with: parameters) } } From f8e97f03b31b1d73d65c98545aa49c9e37972d57 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 12 Oct 2021 19:22:46 +0200 Subject: [PATCH 151/276] Update room display management where needed. --- .../Common/Recents/RecentsViewController.m | 45 +++++++++++++------ .../Files/HomeFilesSearchViewController.m | 17 +++++-- .../HomeMessagesSearchViewController.m | 17 +++++-- .../Rooms/DirectoryViewController.m | 17 +++++-- 4 files changed, 72 insertions(+), 24 deletions(-) diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index d15bbdb0a..e5d5b6339 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -856,16 +856,29 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro } } -- (void)dispayRoomWithRoomId:(NSString*)roomId inMatrixSession:(MXSession*)matrixSession +- (void)showRoomWithRoomId:(NSString*)roomId inMatrixSession:(MXSession*)matrixSession { // Avoid multiple openings of rooms self.userInteractionEnabled = NO; + + // Do not stack views when showing room + RoomPresentationParameters *parameters = [[RoomPresentationParameters alloc] initWithRoomId:roomId eventId:nil + mxSession:matrixSession restoreInitialDisplay:NO + stackAboveVisibleViews:NO]; - [[AppDelegate theDelegate] showRoom:roomId andEventId:nil withMatrixSession:matrixSession restoreInitialDisplay:NO completion:^{ + [[AppDelegate theDelegate] showRoomWithParameters:parameters completion:^{ self.userInteractionEnabled = YES; }]; } +- (void)showRoomPreviewWithData:(RoomPreviewData*)roomPreviewData +{ + RoomPreviewPresentationParameters *parameters = [[RoomPreviewPresentationParameters alloc] initWithPreviewData:roomPreviewData + restoreInitialDisplay:NO stackAboveVisibleViews:NO]; + + [[AppDelegate theDelegate] showRoomPreviewWithParameters:parameters]; +} + // Disable UI interactions in this screen while we are going to open another screen. // Interactions on reset on viewWillAppear. - (void)setUserInteractionEnabled:(BOOL)userInteractionEnabled @@ -939,7 +952,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro } // Display the room preview - [self dispayRoomWithRoomId:invitedRoom.roomId inMatrixSession:invitedRoom.mxSession]; + [self showRoomWithRoomId:invitedRoom.roomId inMatrixSession:invitedRoom.mxSession]; } else if ([actionIdentifier isEqualToString:kInviteRecentTableViewCellAcceptButtonPressed]) { @@ -1462,7 +1475,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro else if ([self canShowRoomPreviewFor:invitedRoom]) { // Display the room preview - [self dispayRoomWithRoomId:invitedRoom.roomId inMatrixSession:invitedRoom.mxSession]; + [self showRoomWithRoomId:invitedRoom.roomId inMatrixSession:invitedRoom.mxSession]; } else { @@ -1982,7 +1995,8 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro if ([self.recentsDataSource.publicRoomsDirectoryDataSource.mxSession roomWithRoomId:publicRoom.roomId]) { // Open the public room - [[AppDelegate theDelegate] showRoom:publicRoom.roomId andEventId:nil withMatrixSession:self.recentsDataSource.publicRoomsDirectoryDataSource.mxSession restoreInitialDisplay:NO]; + [self showRoomWithRoomId:publicRoom.roomId + inMatrixSession:self.recentsDataSource.publicRoomsDirectoryDataSource.mxSession]; } else { @@ -1996,14 +2010,15 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro // Try to get more information about the room before opening its preview [roomPreviewData peekInRoom:^(BOOL succeeded) { [self stopActivityIndicator]; - - [[AppDelegate theDelegate].masterTabBarController showRoomPreview:roomPreviewData]; + + [self showRoomPreviewWithData:roomPreviewData]; }]; } else { RoomPreviewData *roomPreviewData = [[RoomPreviewData alloc] initWithPublicRoom:publicRoom andSession:self.recentsDataSource.publicRoomsDirectoryDataSource.mxSession]; - [[AppDelegate theDelegate].masterTabBarController showRoomPreview:roomPreviewData]; + + [self showRoomPreviewWithData:roomPreviewData]; } } } @@ -2069,7 +2084,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro - (void)recentListViewController:(MXKRecentListViewController *)recentListViewController didSelectRoom:(NSString *)roomId inMatrixSession:(MXSession *)matrixSession { - [self dispayRoomWithRoomId:roomId inMatrixSession:matrixSession]; + [self showRoomWithRoomId:roomId inMatrixSession:matrixSession]; } - (void)recentListViewController:(MXKRecentListViewController *)recentListViewController didSelectSuggestedRoom:(MXSpaceChildInfo *)childInfo @@ -2079,8 +2094,8 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro MXWeakify(self); [previewData peekInRoom:^(BOOL succeeded) { MXStrongifyAndReturnIfNil(self); - [self stopActivityIndicator]; - [[AppDelegate theDelegate].masterTabBarController showRoomPreview:previewData]; + [self stopActivityIndicator]; + [self showRoomPreviewWithData:previewData]; }]; } @@ -2123,7 +2138,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro - (void)createRoomCoordinatorBridgePresenterDelegate:(CreateRoomCoordinatorBridgePresenter *)coordinatorBridgePresenter didCreateNewRoom:(MXRoom *)room { [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ - [[AppDelegate theDelegate] showRoom:room.roomId andEventId:nil withMatrixSession:self.mainSession restoreInitialDisplay:NO]; + [self showRoomWithRoomId:room.roomId inMatrixSession:self.mainSession]; }]; coordinatorBridgePresenter = nil; } @@ -2252,7 +2267,8 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro { // Room is known show it directly [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ - [[AppDelegate theDelegate] showRoom:room.roomId andEventId:nil withMatrixSession:self.mainSession restoreInitialDisplay:NO]; + [self showRoomWithRoomId:room.roomId + inMatrixSession:self.mainSession]; }]; coordinatorBridgePresenter = nil; } @@ -2280,7 +2296,8 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro if (succeeded) { [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ - [[AppDelegate theDelegate].masterTabBarController showRoomPreview:roomPreviewData]; + + [self showRoomPreviewWithData:roomPreviewData]; }]; self.roomsDirectoryCoordinatorBridgePresenter = nil; } else { diff --git a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m index 958e2cb24..84dd46897 100644 --- a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m @@ -139,6 +139,17 @@ } } +- (void)showRoomWithId:(NSString*)roomId + andEventId:(NSString*)eventId + inMatrixSession:(MXSession*)session +{ + RoomPresentationParameters *parameters = [[RoomPresentationParameters alloc] initWithRoomId:roomId + eventId:eventId mxSession:session + restoreInitialDisplay:NO stackAboveVisibleViews:NO]; + + [[AppDelegate theDelegate] showRoomWithParameters:parameters]; +} + #pragma mark - MXKDataSourceDelegate - (Class)cellViewClassForCellData:(MXKCellData*)cellData @@ -188,9 +199,9 @@ [tableView deselectRowAtIndexPath:indexPath animated:YES]; // Make the master tabBar view controller open the RoomViewController - [[AppDelegate theDelegate].masterTabBarController selectRoomWithId:cellData.roomId - andEventId:_selectedEvent.eventId - inMatrixSession:self.mainSession]; + [self showRoomWithId:cellData.roomId + andEventId:_selectedEvent.eventId + inMatrixSession:self.mainSession]; // Reset the selected event. HomeViewController got it when here _selectedEvent = nil; diff --git a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m index 0dbc1cfcb..6c905f6fb 100644 --- a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m @@ -146,6 +146,17 @@ } } +- (void)showRoomWithId:(NSString*)roomId + andEventId:(NSString*)eventId + inMatrixSession:(MXSession*)session +{ + RoomPresentationParameters *parameters = [[RoomPresentationParameters alloc] initWithRoomId:roomId + eventId:eventId mxSession:session + restoreInitialDisplay:NO stackAboveVisibleViews:NO]; + + [[AppDelegate theDelegate] showRoomWithParameters:parameters]; +} + #pragma mark - MXKDataSourceDelegate - (Class)cellViewClassForCellData:(MXKCellData*)cellData @@ -239,9 +250,9 @@ [tableView deselectRowAtIndexPath:indexPath animated:YES]; // Make the master tabBar view controller open the RoomViewController - [[AppDelegate theDelegate].masterTabBarController selectRoomWithId:cellData.roomId - andEventId:_selectedEvent.eventId - inMatrixSession:cellData.mxSession]; + [self showRoomWithId:cellData.roomId + andEventId:_selectedEvent.eventId + inMatrixSession:cellData.mxSession]; // Reset the selected event. HomeViewController got it when here _selectedEvent = nil; diff --git a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m index 502bb3754..c7c04221d 100644 --- a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m +++ b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m @@ -209,13 +209,13 @@ [self stopActivityIndicator]; - [[AppDelegate theDelegate].masterTabBarController showRoomPreview:roomPreviewData]; + [self showRoomPreviewWithData:roomPreviewData]; }]; } else { RoomPreviewData *roomPreviewData = [[RoomPreviewData alloc] initWithPublicRoom:publicRoom andSession:dataSource.mxSession]; - [[AppDelegate theDelegate].masterTabBarController showRoomPreview:roomPreviewData]; + [self showRoomPreviewWithData:roomPreviewData]; } } @@ -233,8 +233,17 @@ #pragma mark - Private methods - (void)openRoomWithId:(NSString*)roomId inMatrixSession:(MXSession*)mxSession -{ - [[AppDelegate theDelegate] showRoom:roomId andEventId:nil withMatrixSession:mxSession restoreInitialDisplay:NO]; +{ + RoomPresentationParameters *parameters = [[RoomPresentationParameters alloc] initWithRoomId:roomId eventId:nil mxSession:mxSession + restoreInitialDisplay:NO stackAboveVisibleViews:NO]; + [[AppDelegate theDelegate] showRoomWithParameters:parameters]; +} + +- (void)showRoomPreviewWithData:(RoomPreviewData*)roomPreviewData +{ + RoomPreviewPresentationParameters *parameters = [[RoomPreviewPresentationParameters alloc] initWithPreviewData:roomPreviewData restoreInitialDisplay:NO + stackAboveVisibleViews:NO]; + [[AppDelegate theDelegate] showRoomPreviewWithParameters:parameters]; } - (void)refreshCurrentSelectedCell:(BOOL)forceVisible From 23f81ade18f2c488a986c9b24246b2cc02f3d745 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 12 Oct 2021 21:25:15 +0300 Subject: [PATCH 152/276] Refactor cell data arrays --- .../Recents/DataSources/RecentsDataSource.m | 77 ++++++++----------- 1 file changed, 33 insertions(+), 44 deletions(-) diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m index 2299783fa..e73396ac5 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -116,43 +116,35 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou #pragma mark - Properties -- (NSArray> *)invitesCellDataArray +- (NSArray> *)invitesCellDataArray { - return [self mapRoomSummaries:self.recentsListService.invitedRoomListData.rooms]; + return self.recentsListService.invitedRoomListData.rooms; } -- (NSArray> *)favoriteCellDataArray +- (NSArray> *)favoriteCellDataArray { - return [self mapRoomSummaries:self.recentsListService.favoritedRoomListData.rooms]; + return self.recentsListService.favoritedRoomListData.rooms; } -- (NSArray> *)peopleCellDataArray +- (NSArray> *)peopleCellDataArray { - return [self mapRoomSummaries:self.recentsListService.peopleRoomListData.rooms]; + return self.recentsListService.peopleRoomListData.rooms; } -- (NSArray> *)conversationCellDataArray +- (NSArray> *)conversationCellDataArray { - return [self mapRoomSummaries:self.recentsListService.conversationRoomListData.rooms]; + return self.recentsListService.conversationRoomListData.rooms; } -- (NSArray> *)lowPriorityCellDataArray +- (NSArray> *)lowPriorityCellDataArray { - return [self mapRoomSummaries:self.recentsListService.lowPriorityRoomListData.rooms]; + return self.recentsListService.lowPriorityRoomListData.rooms; } -- (NSArray> *)serverNoticeCellDataArray +- (NSArray> *)serverNoticeCellDataArray { - return [self mapRoomSummaries:self.recentsListService.serverNoticeRoomListData.rooms]; + return self.recentsListService.serverNoticeRoomListData.rooms; } -- (NSArray> *)suggestedRoomCellDataArray +- (NSArray> *)suggestedRoomCellDataArray { - return [self mapRoomSummaries:self.recentsListService.suggestedRoomListData.rooms]; + return self.recentsListService.suggestedRoomListData.rooms; } -- (NSArray> *)mapRoomSummaries:(NSArray> *)summaries - { - return [summaries vc_map:^id _Nonnull(id _Nonnull summary) { - return [[MXKRecentCellData alloc] initWithRoomSummary:summary - dataSource:self]; - }]; - } - - (NSInteger)totalVisibleItemCount { return self.recentsListService.totalVisibleItemCount; @@ -945,7 +937,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou - (id)cellDataAtIndexPath:(NSIndexPath *)indexPath { - id cellData = nil; + id summary = nil; NSUInteger cellDataIndex = indexPath.row; NSInteger tableSection = indexPath.section; @@ -963,53 +955,57 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou { if (cellDataIndex < self.favoriteCellDataArray.count) { - cellData = self.favoriteCellDataArray[cellDataIndex]; + summary = self.favoriteCellDataArray[cellDataIndex]; } } else if (tableSection == peopleSection) { if (cellDataIndex < self.peopleCellDataArray.count) { - cellData = self.peopleCellDataArray[cellDataIndex]; + summary = self.peopleCellDataArray[cellDataIndex]; } } else if (tableSection== conversationSection) { if (cellDataIndex < self.conversationCellDataArray.count) { - cellData = self.conversationCellDataArray[cellDataIndex]; + summary = self.conversationCellDataArray[cellDataIndex]; } } else if (tableSection == lowPrioritySection) { if (cellDataIndex < self.lowPriorityCellDataArray.count) { - cellData = self.lowPriorityCellDataArray[cellDataIndex]; + summary = self.lowPriorityCellDataArray[cellDataIndex]; } } else if (tableSection == serverNoticeSection) { if (cellDataIndex < self.serverNoticeCellDataArray.count) { - cellData = self.serverNoticeCellDataArray[cellDataIndex]; + summary = self.serverNoticeCellDataArray[cellDataIndex]; } } else if (tableSection == invitesSection) { if (cellDataIndex < self.invitesCellDataArray.count) { - cellData = self.invitesCellDataArray[cellDataIndex]; + summary = self.invitesCellDataArray[cellDataIndex]; } } else if (tableSection == suggestedRoomsSection) { if (cellDataIndex < self.suggestedRoomCellDataArray.count) { - cellData = self.suggestedRoomCellDataArray[cellDataIndex]; + summary = self.suggestedRoomCellDataArray[cellDataIndex]; } } - return cellData; + if (summary) + { + return [[MXKRecentCellData alloc] initWithRoomSummary:summary dataSource:self]; + } + return nil; } - (CGFloat)cellHeightAtIndexPath:(NSIndexPath *)indexPath @@ -1056,22 +1052,15 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou #pragma mark - -- (NSInteger)cellIndexPosWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)matrixSession within:(NSArray*)cellDataArray +- (NSInteger)cellIndexPosWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)matrixSession within:(NSArray> *)summaries { - if (roomId && matrixSession && cellDataArray.count) + if (!roomId || !matrixSession || !summaries.count || self.mxSession != matrixSession) { - for (int index = 0; index < cellDataArray.count; index++) - { - id cellData = cellDataArray[index]; - - if ([roomId isEqualToString:cellData.roomIdentifier] && cellData.mxSession == matrixSession) - { - return index; - } - } + return NSNotFound; } - - return NSNotFound; + return [summaries indexOfObjectPassingTest:^BOOL(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + return [obj.roomId isEqualToString:roomId]; + }]; } - (NSIndexPath*)cellIndexPathWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)matrixSession From ec8bc1ea79fe2e1244f268b8ab5a8923a59e3da4 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 12 Oct 2021 22:42:16 +0300 Subject: [PATCH 153/276] Improve mocking performance --- .../Service/Mock/MockRecentsListService.swift | 74 +++++++++++++++---- 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/Riot/Modules/Common/Recents/Service/Mock/MockRecentsListService.swift b/Riot/Modules/Common/Recents/Service/Mock/MockRecentsListService.swift index c8c37191f..6be38a676 100644 --- a/Riot/Modules/Common/Recents/Service/Mock/MockRecentsListService.swift +++ b/Riot/Modules/Common/Recents/Service/Mock/MockRecentsListService.swift @@ -18,7 +18,15 @@ import Foundation @objcMembers public class MockRecentsListService: NSObject, RecentsListServiceProtocol { - private var rooms: [MockRoomSummary] = [] + + private var rooms: [MockRoomSummary] + + private var _invitedRoomListData: MXRoomListData? + private var _favoritedRoomListData: MXRoomListData? + private var _peopleRoomListData: MXRoomListData? + private var _conversationRoomListData: MXRoomListData? + private var _lowPriorityRoomListData: MXRoomListData? + private var _serverNoticeRoomListData: MXRoomListData? // swiftlint:disable weak_delegate private let multicastDelegate: MXMulticastDelegate = MXMulticastDelegate() @@ -26,6 +34,45 @@ public class MockRecentsListService: NSObject, RecentsListServiceProtocol { public init(withRooms rooms: [MockRoomSummary]) { self.rooms = rooms + + var invited: [MockRoomSummary] = [] + var favorited: [MockRoomSummary] = [] + var people: [MockRoomSummary] = [] + var conversation: [MockRoomSummary] = [] + var lowPriority: [MockRoomSummary] = [] + var serverNotice: [MockRoomSummary] = [] + + rooms.forEach { summary in + if summary.isTyped(.invited) { + invited.append(summary) + } + if summary.isTyped(.favorited) { + favorited.append(summary) + } + if summary.isTyped(.direct) { + people.append( summary) + } + if !summary.isTyped([.direct, + .invited, + .favorited, + .lowPriority, + .serverNotice]) { + conversation.append(summary) + } + if summary.isTyped(.lowPriority) { + lowPriority.append(summary) + } + if summary.isTyped(.serverNotice) { + serverNotice.append(summary) + } + } + _invitedRoomListData = MockRoomListData(withRooms: invited) + _favoritedRoomListData = MockRoomListData(withRooms: favorited) + _peopleRoomListData = MockRoomListData(withRooms: people) + _conversationRoomListData = MockRoomListData(withRooms: conversation) + _lowPriorityRoomListData = MockRoomListData(withRooms: lowPriority) + _serverNoticeRoomListData = MockRoomListData(withRooms: serverNotice) + super.init() } @@ -72,38 +119,32 @@ public class MockRecentsListService: NSObject, RecentsListServiceProtocol { public var invitedRoomListData: MXRoomListData? { guard mode == .home else { return nil } - return MockRoomListData(withRooms: rooms.filter({ $0.isTyped(.invited) })) + return _invitedRoomListData } public var favoritedRoomListData: MXRoomListData? { guard mode == .home || mode == .favourites else { return nil } - return MockRoomListData(withRooms: rooms.filter({ $0.isTyped(.favorited) })) + return _favoritedRoomListData } public var peopleRoomListData: MXRoomListData? { guard mode == .home || mode == .people else { return nil } - return MockRoomListData(withRooms: rooms.filter({ $0.isTyped(.direct) })) + return _peopleRoomListData } public var conversationRoomListData: MXRoomListData? { guard mode == .home || mode == .rooms else { return nil } - let mockRooms = rooms.filter({ !$0.isTyped([.direct, - .invited, - .favorited, - .lowPriority, - .serverNotice]) - }) - return MockRoomListData(withRooms: mockRooms) + return _conversationRoomListData } public var lowPriorityRoomListData: MXRoomListData? { guard mode == .home else { return nil } - return MockRoomListData(withRooms: rooms.filter({ $0.isTyped(.lowPriority) })) + return _lowPriorityRoomListData } public var serverNoticeRoomListData: MXRoomListData? { guard mode == .home else { return nil } - return MockRoomListData(withRooms: rooms.filter({ $0.isTyped(.serverNotice) })) + return _serverNoticeRoomListData } public var suggestedRoomListData: MXRoomListData? @@ -147,7 +188,12 @@ public class MockRecentsListService: NSObject, RecentsListServiceProtocol { } public func stop() { - rooms.removeAll() + _invitedRoomListData = nil + _favoritedRoomListData = nil + _peopleRoomListData = nil + _conversationRoomListData = nil + _lowPriorityRoomListData = nil + _serverNoticeRoomListData = nil removeAllDelegates() } From 87d55b19475d6b51e4450dbe7d5bb294042a7ef8 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 12 Oct 2021 22:56:52 +0300 Subject: [PATCH 154/276] Improve tab switching performance --- Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h index e571a7850..585453317 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h @@ -26,10 +26,11 @@ /** List the different modes used to prepare the recents data source. Each mode corresponds to an application tab: Home, Favourites, People and Rooms. + Used as the tag of UITableView, starting from 1 in order to avoid collision with default tag of UIView. */ typedef NS_ENUM(NSInteger, RecentsDataSourceMode) { - RecentsDataSourceModeHome, + RecentsDataSourceModeHome = 1, RecentsDataSourceModeFavourites, RecentsDataSourceModePeople, RecentsDataSourceModeRooms From 1ca9bca7f509ca0c571477877ccdba9d02e0b5e1 Mon Sep 17 00:00:00 2001 From: Linerly Date: Wed, 13 Oct 2021 05:06:41 +0000 Subject: [PATCH 155/276] Added translation using Weblate (Indonesian) --- Riot/Assets/id.lproj/InfoPlist.strings | 1 + 1 file changed, 1 insertion(+) create mode 100644 Riot/Assets/id.lproj/InfoPlist.strings diff --git a/Riot/Assets/id.lproj/InfoPlist.strings b/Riot/Assets/id.lproj/InfoPlist.strings new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/Riot/Assets/id.lproj/InfoPlist.strings @@ -0,0 +1 @@ + From fe9243fc663bb6c98e542804670d435c1be49ee3 Mon Sep 17 00:00:00 2001 From: Linerly Date: Wed, 13 Oct 2021 05:10:09 +0000 Subject: [PATCH 156/276] Added translation using Weblate (Indonesian) --- Riot/Assets/id.lproj/Localizable.strings | 1 + 1 file changed, 1 insertion(+) create mode 100644 Riot/Assets/id.lproj/Localizable.strings diff --git a/Riot/Assets/id.lproj/Localizable.strings b/Riot/Assets/id.lproj/Localizable.strings new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/Riot/Assets/id.lproj/Localizable.strings @@ -0,0 +1 @@ + From 27c9188011b4109f930aff8541721f4972cb0860 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 28 Sep 2021 14:28:28 +0300 Subject: [PATCH 157/276] vector-im/element-ios/issues/1098 - Fixed typo in template file name. --- ...aters.swift => TemplateUserProfileCoordinatorParameters.swift} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/{TemplateUserProfileCoordinatorParamaters.swift => TemplateUserProfileCoordinatorParameters.swift} (100%) diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinatorParamaters.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinatorParameters.swift similarity index 100% rename from RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinatorParamaters.swift rename to RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinatorParameters.swift From adc20a27b558f41d395b8612b5e824142ca8f686 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 29 Sep 2021 11:55:05 +0300 Subject: [PATCH 158/276] vector-im/element-ios/issues/1098 - Fixed indentation in templates. Added UnitTests to the main RiotSwiftUI target. --- .../Coordinator/TemplateUserProfileCoordinator.swift | 2 +- .../TemplateUserProfileCoordinatorParameters.swift | 2 +- RiotSwiftUI/target.yml | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift index 6707bd839..cdd0e48cb 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift @@ -55,7 +55,7 @@ final class TemplateUserProfileCoordinator: Coordinator { switch result { case .cancel, .done: self.completion?() - break + break } } } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinatorParameters.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinatorParameters.swift index 17be5c41e..7f162ce38 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinatorParameters.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinatorParameters.swift @@ -17,5 +17,5 @@ import Foundation struct TemplateUserProfileCoordinatorParameters { - let session: MXSession + let session: MXSession } diff --git a/RiotSwiftUI/target.yml b/RiotSwiftUI/target.yml index 5200f73ca..ecbf8a91c 100644 --- a/RiotSwiftUI/target.yml +++ b/RiotSwiftUI/target.yml @@ -10,6 +10,7 @@ schemes: targets: RiotSwiftUI: - running + - testing - profiling - analyzing - archiving @@ -18,6 +19,11 @@ schemes: run: config: Debug disableMainThreadChecker: true + test: + config: Debug + disableMainThreadChecker: true + targets: + - RiotSwiftUnitTests targets: RiotSwiftUI: From 97a16806b12c98c5d86b022a2f5cf01e925c37a5 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 30 Sep 2021 09:37:59 +0300 Subject: [PATCH 159/276] #1098 - Generated UserSuggestion from template, got initial UI working and automatically updating. --- Riot/Modules/Room/RoomViewController.m | 5 + .../Views/InputToolbar/RoomInputToolbarView.m | 6 +- .../Modules/Common/Mock/MockAppScreens.swift | 3 +- .../Common/Util/RoundedCornerShape.swift | 30 ++++++ .../ViewModel/StateStoreViewModel.swift | 1 - .../UserSuggestionCoordinator.swift | 73 +++++++++++++++ .../UserSuggestionCoordinatorParameters.swift | 23 +++++ .../Model/UserSuggestionStateAction.swift | 24 +++++ .../Model/UserSuggestionViewAction.swift | 24 +++++ .../Model/UserSuggestionViewModelResult.swift | 24 +++++ .../Model/UserSuggestionViewState.swift | 32 +++++++ .../MatrixSDK/UserSuggestionService.swift | 42 +++++++++ .../Mock/MockUserSuggestionScreenState.swift | 55 +++++++++++ .../Mock/MockUserSuggestionService.swift | 59 ++++++++++++ .../UserSuggestionServiceProtocol.swift | 47 ++++++++++ .../Test/UI/UserSuggestionUITests.swift | 47 ++++++++++ .../Unit/UserSuggestionViewModelTests.swift | 42 +++++++++ .../View/UserSuggestionList.swift | 92 +++++++++++++++++++ .../View/UserSuggestionListItem.swift | 63 +++++++++++++ .../View/UserSuggestionListWithInput.swift | 59 ++++++++++++ .../ViewModel/UserSuggestionViewModel.swift | 87 ++++++++++++++++++ .../UserSuggestionViewModelProtocol.swift | 29 ++++++ .../MockTemplateUserProfileScreenState.swift | 1 - 23 files changed, 864 insertions(+), 4 deletions(-) create mode 100644 RiotSwiftUI/Modules/Common/Util/RoundedCornerShape.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionStateAction.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewAction.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Service/MatrixSDK/UserSuggestionService.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionService.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionViewModelTests.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModelProtocol.swift diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 075a3f57f..bf44086fd 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -4200,6 +4200,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { [self cancelEventSelection]; } + +- (void)roomInputToolbarViewDidRequestUserSuggestions:(MXKRoomInputToolbarView *)toolbarView +{ + +} #pragma mark - MXKRoomMemberDetailsViewControllerDelegate diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index 5c8fab5ec..3e72f3eb9 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -324,7 +324,11 @@ const CGFloat kComposerContainerTrailingPadding = 12; { NSString *newText = [textView.text stringByReplacingCharactersInRange:range withString:text]; [self updateUIWithTextMessage:newText animated:YES]; - + + if ([text isEqualToString:@"@"]) { + [self.delegate roomInputToolbarViewDidRequestUserSuggestions:self]; + } + return YES; } diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index 38022e7b8..1195cd4ff 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -22,7 +22,8 @@ enum MockAppScreens { static let appScreens: [MockScreenState.Type] = [ MockTemplateUserProfileScreenState.self, MockTemplateRoomListScreenState.self, - MockTemplateRoomChatScreenState.self + MockTemplateRoomChatScreenState.self, + MockUserSuggestionScreenState.self ] } diff --git a/RiotSwiftUI/Modules/Common/Util/RoundedCornerShape.swift b/RiotSwiftUI/Modules/Common/Util/RoundedCornerShape.swift new file mode 100644 index 000000000..69a3cf881 --- /dev/null +++ b/RiotSwiftUI/Modules/Common/Util/RoundedCornerShape.swift @@ -0,0 +1,30 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +@available(iOS 14.0, *) +struct RoundedCornerShape: Shape { + + let radius: CGFloat + let corners: UIRectCorner + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) + return Path(path.cgPath) + } +} diff --git a/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift b/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift index 7ba92ef30..915859165 100644 --- a/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift +++ b/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift @@ -40,7 +40,6 @@ class ViewModelContext: ObservableObject { // MARK: Private - private var cancellables = Set() fileprivate let viewActions: PassthroughSubject // MARK: Public diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift new file mode 100644 index 000000000..cca76b3cd --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -0,0 +1,73 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +/* + 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 +import SwiftUI + +final class UserSuggestionCoordinator: Coordinator { + + // MARK: - Properties + + // MARK: Private + + private let parameters: UserSuggestionCoordinatorParameters + private let userSuggestionHostingController: UIViewController + + private var userSuggestionService: UserSuggestionServiceProtocol + private var userSuggestionViewModel: UserSuggestionViewModelProtocol + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: (() -> Void)? + + // MARK: - Setup + + @available(iOS 14.0, *) + init(parameters: UserSuggestionCoordinatorParameters) { + self.parameters = parameters + + userSuggestionService = UserSuggestionService(session: parameters.room) + userSuggestionViewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: userSuggestionService) + + let view = UserSuggestionList(viewModel: viewModel.context) + .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + + userSuggestionHostingController = VectorHostingController(rootView: view) + } + + // MARK: - Public + func start() { + MXLog.debug("[UserSuggestionCoordinator] did start.") + userSuggestionViewModel.completion = { [weak self] result in + MXLog.debug("[UserSuggestionCoordinator] UserSuggestionViewModel did complete with result: \(result).") + guard let self = self else { return } + switch result { + case .cancel, .done: + self.completion?() + break + } + } + } + + func toPresentable() -> UIViewController { + return self.userSuggestionHostingController + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift new file mode 100644 index 000000000..c0c15053a --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift @@ -0,0 +1,23 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct UserSuggestionCoordinatorParameters { + let room: MXRoom +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionStateAction.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionStateAction.swift new file mode 100644 index 000000000..153848943 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionStateAction.swift @@ -0,0 +1,24 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// 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 + +@available(iOS 14.0, *) +enum UserSuggestionStateAction { + case updateWithItems([UserSuggestionItemProtocol]) +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewAction.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewAction.swift new file mode 100644 index 000000000..6352ed5ae --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewAction.swift @@ -0,0 +1,24 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// 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 + +@available(iOS 14, *) +enum UserSuggestionViewAction { + case selectedItem(UserSuggestionViewStateItem) +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift new file mode 100644 index 000000000..fc25fdcb6 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift @@ -0,0 +1,24 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// 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 + +@available(iOS 14, *) +enum UserSuggestionViewModelResult { + case selectedItem(UserSuggestionItemProtocol) +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift new file mode 100644 index 000000000..038852299 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift @@ -0,0 +1,32 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine + +@available(iOS 14.0, *) +struct UserSuggestionViewStateItem: BindableState, Identifiable { + let id: String + let avatar: AvatarInputProtocol? + let displayName: String? +} + +@available(iOS 14.0, *) +struct UserSuggestionViewState: BindableState { + var items: [UserSuggestionViewStateItem] +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/MatrixSDK/UserSuggestionService.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/MatrixSDK/UserSuggestionService.swift new file mode 100644 index 000000000..e95537483 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/MatrixSDK/UserSuggestionService.swift @@ -0,0 +1,42 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine + +@available(iOS 14.0, *) +class UserSuggestionService: UserSuggestionServiceProtocol { + + // MARK: - Properties + + // MARK: Private + + private let room: MXRoom + + // MARK: Public + + // MARK: - Setup + + init(room: MXRoom) { + self.room = room + } + + func processPartialUserName(_ userName: String) { + + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift new file mode 100644 index 000000000..8c9d3b80d --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift @@ -0,0 +1,55 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +@available(iOS 14.0, *) +enum MockUserSuggestionScreenState: MockScreenState, CaseIterable { + case multipleResults + case oneResult + case empty + + var screenType: Any.Type { + MockUserSuggestionScreenState.self + } + + var screenView: ([Any], AnyView) { + let service: MockUserSuggestionService + switch self { + case .empty: + service = MockUserSuggestionService(userCount: 0) + case .oneResult: + service = MockUserSuggestionService(userCount: 1) + case .multipleResults: + service = MockUserSuggestionService(userCount: 10) + } + + let listViewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: service) + + let viewModel = UserSuggestionListWithInputViewModel(listViewModel: listViewModel) { partialUserName in + service.processPartialUserName(partialUserName) + } + + return ( + [service, listViewModel], + AnyView(UserSuggestionListWithInput(viewModel: viewModel) + .addDependency(MockAvatarService.example)) + ) + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionService.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionService.swift new file mode 100644 index 000000000..8460646d5 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionService.swift @@ -0,0 +1,59 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine + +@available(iOS 14.0, *) +struct MockUserSuggestionServiceItem: UserSuggestionItemProtocol { + let userId: String + let displayName: String? + let avatarUrl: String? +} + +@available(iOS 14.0, *) +class MockUserSuggestionService: UserSuggestionServiceProtocol { + private var suggestionItems: [UserSuggestionItemProtocol] = [] + + var items: CurrentValueSubject<[UserSuggestionItemProtocol], Never> + + init(userCount: UInt) { + items = CurrentValueSubject([]) + generateUsersWithCount(userCount) + items.send(suggestionItems) + } + + func processPartialUserName(_ userName: String) { + guard userName.count > 0 else { + items.send(suggestionItems) + return + } + + items.send(suggestionItems.filter({ userSuggestion in + return (userSuggestion.displayName?.lowercased().range(of: userName.lowercased()) != .none) + })) + } + + private func generateUsersWithCount(_ count: UInt) { + suggestionItems.removeAll() + for _ in 0.. { get } + + func processPartialUserName(_ userName: String) +} + +// MARK: Avatarable + +@available(iOS 14.0, *) +extension UserSuggestionItemProtocol { + var mxContentUri: String? { + avatarUrl + } + var matrixItemId: String { + userId + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift new file mode 100644 index 000000000..f3f984791 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift @@ -0,0 +1,47 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import RiotSwiftUI + +@available(iOS 14.0, *) +class UserSuggestionUITests: MockScreenTest { + + override class var screenType: MockScreenState.Type { + return MockUserSuggestionScreenState.self + } + + override class func createTest() -> MockScreenTest { + return UserSuggestionUITests(selector: #selector(verifyUserSuggestionScreen)) + } + + func verifyUserSuggestionScreen() throws { + guard let screenState = screenState as? MockUserSuggestionScreenState else { fatalError("no screen") } + switch screenState { + case .longDisplayName(let name): + verifyUserSuggestionLongName(name: name) + } + } + + func verifyUserSuggestionLongName(name: String) { + let displayNameText = app.staticTexts["displayNameText"] + XCTAssert(displayNameText.exists) + XCTAssertEqual(displayNameText.label, name) + } + +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionViewModelTests.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionViewModelTests.swift new file mode 100644 index 000000000..7e9060bf0 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionViewModelTests.swift @@ -0,0 +1,42 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Combine + +@testable import RiotSwiftUI + +@available(iOS 14.0, *) +class UserSuggestionViewModelTests: XCTestCase { + private enum Constants { + static let displayName = "Alice" + } + var service: MockUserSuggestionService! + var viewModel: UserSuggestionViewModelProtocol! + var context: UserSuggestionViewModelType.Context! + var cancellables = Set() + override func setUpWithError() throws { + service = MockUserSuggestionService(userCount: 10) + viewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: service) + context = viewModel.context + } + + func testInitialState() { +// XCTAssertEqual(context.viewState.displayName, Constants.displayName) + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift new file mode 100644 index 000000000..ddf8dcb31 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift @@ -0,0 +1,92 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@available(iOS 14.0, *) +struct UserSuggestionList: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + @ObservedObject var viewModel: UserSuggestionViewModel.Context + let rowHeight: CGFloat = 60.0 + let maxHeight: CGFloat = 300.0 + + var body: some View { + BackgroundView { + ScrollViewReader { scrollViewReader in + List(viewModel.viewState.items) { item in + UserSuggestionListItem( + avatar: item.avatar, + displayName: item.displayName, + userId: item.id + ) + .padding([.top, .bottom], 4.0) + .onTapGesture { + viewModel.send(viewAction: .selectedItem(item)) + } + } + .environment(\.defaultMinListRowHeight, rowHeight) + .frame(height: min(maxHeight, rowHeight * CGFloat(viewModel.viewState.items.count))) + .onAppear(perform: { + guard let lastItemId = viewModel.viewState.items.last?.id else { + return + } + + scrollViewReader.scrollTo(lastItemId) + }) + } + } + } +} + +@available(iOS 14.0, *) +private struct BackgroundView: View { + + var content: () -> Content + + @Environment(\.theme) private var theme: ThemeSwiftUI + + init(@ViewBuilder content: @escaping () -> Content) { + self.content = content + } + + var body: some View { + VStack(content: content) + .background(theme.colors.background) + .clipShape(RoundedCornerShape(radius: 20.0, corners: [.topLeft, .topRight])) + .shadow(color: .black.opacity(0.20), radius: 20.0, x: 0.0, y: 3.0) + .edgesIgnoringSafeArea(.all) + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct UserSuggestion_Previews: PreviewProvider { + static let stateRenderer = MockUserSuggestionScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup() + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift new file mode 100644 index 000000000..927155a73 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift @@ -0,0 +1,63 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@available(iOS 14.0, *) +struct UserSuggestionListItem: View { + + // MARK: - Properties + + // MARK: Private + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + let avatar: AvatarInputProtocol? + let displayName: String? + let userId: String + + var body: some View { + HStack { + if let avatar = avatar { + AvatarImage(avatarData: avatar, size: .medium) + } + VStack(alignment:.leading) { + Text(displayName ?? "") + .font(theme.fonts.body) + .foregroundColor(theme.colors.primaryContent) + .accessibility(identifier: "displayNameText") + .lineLimit(1) + Text(userId) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.tertiaryContent) + .accessibility(identifier: "userIdText") + .lineLimit(1) + } + } + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct UserSuggestionHeader_Previews: PreviewProvider { + static var previews: some View { + UserSuggestionListItem(avatar: MockAvatarInput.example, displayName: "Alice", userId: "@alice:matrix.org") + .addDependency(MockAvatarService.example) + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift new file mode 100644 index 000000000..4d34f8f34 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift @@ -0,0 +1,59 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@available(iOS 14.0, *) +struct UserSuggestionListWithInputViewModel { + let listViewModel: UserSuggestionViewModelProtocol + let callback: (String)->() +} + +@available(iOS 14.0, *) +struct UserSuggestionListWithInput: View { + + // MARK: - Properties + + // MARK: Private + + // MARK: Public + + var viewModel: UserSuggestionListWithInputViewModel + @State private var inputText: String = "" + + var body: some View { + VStack(spacing: 0.0) { + UserSuggestionList(viewModel: viewModel.listViewModel.context) + TextField("Search for user", text: $inputText) + .background(Color.white) + .onChange(of: inputText, perform: { value in + viewModel.callback(value) + }) + .border(Color.black) + .padding([.leading, .trailing]) + } + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct UserSuggestionListWithInput_Previews: PreviewProvider { + static let stateRenderer = MockUserSuggestionScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup() + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift new file mode 100644 index 000000000..48ac36b81 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift @@ -0,0 +1,87 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import Combine + +@available(iOS 14, *) +typealias UserSuggestionViewModelType = StateStoreViewModel +@available(iOS 14, *) +class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewModelProtocol { + + // MARK: - Properties + + // MARK: Private + + private let userSuggestionService: UserSuggestionServiceProtocol + + // MARK: Public + + var completion: ((UserSuggestionViewModelResult) -> Void)? + + // MARK: - Setup + + static func makeUserSuggestionViewModel(userSuggestionService: UserSuggestionServiceProtocol) -> UserSuggestionViewModelProtocol { + return UserSuggestionViewModel(userSuggestionService: userSuggestionService) + } + + deinit { + print("well shit") + } + + private init(userSuggestionService: UserSuggestionServiceProtocol) { + self.userSuggestionService = userSuggestionService + super.init(initialViewState: Self.defaultState(userSuggestionService: userSuggestionService)) + setupItemsObserving() + } + + private func setupItemsObserving() { + let updatePublisher = userSuggestionService.items + .map(UserSuggestionStateAction.updateWithItems) + .eraseToAnyPublisher() + dispatch(actionPublisher: updatePublisher) + } + + private static func defaultState(userSuggestionService: UserSuggestionServiceProtocol) -> UserSuggestionViewState { + let viewStateItems = userSuggestionService.items.value.map { suggestionItem in + return UserSuggestionViewStateItem(id: suggestionItem.userId, avatar: suggestionItem, displayName: suggestionItem.displayName) + } + + return UserSuggestionViewState(items: viewStateItems) + } + + // MARK: - Public + + override func process(viewAction: UserSuggestionViewAction) { + switch viewAction { + case .selectedItem(_): + break + } + } + + override class func reducer(state: inout UserSuggestionViewState, action: UserSuggestionStateAction) { + switch action { + case .updateWithItems(let items): + state.items = items.map({ item in + UserSuggestionViewStateItem(id: item.userId, avatar: item, displayName: item.displayName) + }) + } + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModelProtocol.swift new file mode 100644 index 000000000..10207210c --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModelProtocol.swift @@ -0,0 +1,29 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// 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 + +@available(iOS 14, *) +protocol UserSuggestionViewModelProtocol { + + static func makeUserSuggestionViewModel(userSuggestionService: UserSuggestionServiceProtocol) -> UserSuggestionViewModelProtocol + + var context: UserSuggestionViewModelType.Context { get } + + var completion: ((UserSuggestionViewModelResult) -> Void)? { get set } +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileScreenState.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileScreenState.swift index ae0dbd208..f9e25cadc 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileScreenState.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileScreenState.swift @@ -17,7 +17,6 @@ import Foundation import SwiftUI - /// Using an enum for the screen allows you define the different state cases with /// the relevant associated data for each case. @available(iOS 14.0, *) From e63dbed95cebfd76816dc1611698c8214fcc6c0d Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 4 Oct 2021 11:37:37 +0300 Subject: [PATCH 160/276] #1098 - Added user suggestions to the main app timeline. --- Riot/Modules/Room/RoomViewController.m | 25 ++++++++++ Riot/Modules/Room/RoomViewController.xib | 24 +++++++--- .../UserSuggestionCoordinator.swift | 24 ++++------ .../UserSuggestionCoordinatorBridge.swift | 48 +++++++++++++++++++ .../UserSuggestionCoordinatorParameters.swift | 1 + .../MatrixSDK/UserSuggestionService.swift | 23 +++++++++ .../Mock/MockUserSuggestionScreenState.swift | 6 --- .../View/UserSuggestionList.swift | 5 +- 8 files changed, 129 insertions(+), 27 deletions(-) create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index bf44086fd..60689bc21 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -249,6 +249,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @property (nonatomic, strong) VoiceMessageController *voiceMessageController; @property (nonatomic, strong) SpaceDetailPresenter *spaceDetailPresenter; +@property (nonatomic, strong) UserSuggestionCoordinatorBridge *userSuggestionCoordinator; +@property (nonatomic, weak) IBOutlet UIView *userSuggestionContainerView; + @end @implementation RoomViewController @@ -452,12 +455,31 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self userInterfaceThemeDidChange]; }]; + [self userInterfaceThemeDidChange]; // Observe URL preview updates. [self registerURLPreviewNotifications]; [self setupActions]; + + [self setupUserSuggestionView]; +} + +- (void)setupUserSuggestionView +{ + UIViewController *suggestionsViewController = self.userSuggestionCoordinator.toPresentable; + [suggestionsViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO]; + + [self addChildViewController:suggestionsViewController]; + [self.userSuggestionContainerView addSubview:suggestionsViewController.view]; + + [NSLayoutConstraint activateConstraints:@[[suggestionsViewController.view.topAnchor constraintEqualToAnchor:self.userSuggestionContainerView.topAnchor], + [suggestionsViewController.view.leadingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.leadingAnchor], + [suggestionsViewController.view.trailingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.trailingAnchor], + [suggestionsViewController.view.bottomAnchor constraintEqualToAnchor:self.userSuggestionContainerView.bottomAnchor],]]; + + [suggestionsViewController didMoveToParentViewController:self]; } - (void)userInterfaceThemeDidChange @@ -1019,6 +1041,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [VoiceMessageMediaServiceProvider.sharedProvider setCurrentRoomSummary:dataSource.room.summary]; _voiceMessageController.roomId = dataSource.roomId; + + _userSuggestionCoordinator = [[UserSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager + room:dataSource.room]; } - (void)onRoomDataSourceReady diff --git a/Riot/Modules/Room/RoomViewController.xib b/Riot/Modules/Room/RoomViewController.xib index 5b1a0a909..1c8f83913 100644 --- a/Riot/Modules/Room/RoomViewController.xib +++ b/Riot/Modules/Room/RoomViewController.xib @@ -1,9 +1,9 @@ - + - + @@ -32,6 +32,7 @@ + @@ -136,14 +137,14 @@ - + - + diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift index cca76b3cd..83c3ac161 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -20,6 +20,7 @@ import Foundation import UIKit import SwiftUI +@available(iOS 14.0, *) final class UserSuggestionCoordinator: Coordinator { // MARK: - Properties @@ -44,27 +45,22 @@ final class UserSuggestionCoordinator: Coordinator { init(parameters: UserSuggestionCoordinatorParameters) { self.parameters = parameters - userSuggestionService = UserSuggestionService(session: parameters.room) + userSuggestionService = UserSuggestionService(room: parameters.room) userSuggestionViewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: userSuggestionService) - let view = UserSuggestionList(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + let view = UserSuggestionList(viewModel: userSuggestionViewModel.context) + .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) - userSuggestionHostingController = VectorHostingController(rootView: view) + userSuggestionHostingController = UIHostingController(rootView: view) + } + + func processPartialUserName(_ userName: String) { + userSuggestionService.processPartialUserName(userName) } // MARK: - Public func start() { - MXLog.debug("[UserSuggestionCoordinator] did start.") - userSuggestionViewModel.completion = { [weak self] result in - MXLog.debug("[UserSuggestionCoordinator] UserSuggestionViewModel did complete with result: \(result).") - guard let self = self else { return } - switch result { - case .cancel, .done: - self.completion?() - break - } - } + } func toPresentable() -> UIViewController { diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift new file mode 100644 index 000000000..bfa9b697e --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift @@ -0,0 +1,48 @@ +// +// 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 + +@objcMembers +final class UserSuggestionCoordinatorBridge: NSObject { + + private var _userSuggestionCoordinator: Any? = nil + @available(iOS 14.0, *) + fileprivate var userSuggestionCoordinator: UserSuggestionCoordinator { + return _userSuggestionCoordinator as! UserSuggestionCoordinator + } + + init(mediaManager: MXMediaManager, room: MXRoom) { + let parameters = UserSuggestionCoordinatorParameters(mediaManager: mediaManager, room: room) + if #available(iOS 14.0, *) { + self._userSuggestionCoordinator = UserSuggestionCoordinator(parameters: parameters) + } + } + + func processPartialUserName(_ userName: String) { + if #available(iOS 14.0, *) { + return self.userSuggestionCoordinator.processPartialUserName(userName) + } + } + + func toPresentable() -> UIViewController? { + if #available(iOS 14.0, *) { + return self.userSuggestionCoordinator.toPresentable() + } + + return nil + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift index c0c15053a..dbad53c94 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift @@ -19,5 +19,6 @@ import Foundation struct UserSuggestionCoordinatorParameters { + let mediaManager: MXMediaManager let room: MXRoom } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/MatrixSDK/UserSuggestionService.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/MatrixSDK/UserSuggestionService.swift index e95537483..29f37ee0e 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/MatrixSDK/UserSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/MatrixSDK/UserSuggestionService.swift @@ -28,15 +28,38 @@ class UserSuggestionService: UserSuggestionServiceProtocol { private let room: MXRoom + private var suggestionItems: [UserSuggestionItemProtocol] = [] + // MARK: Public + var items: CurrentValueSubject<[UserSuggestionItemProtocol], Never> + // MARK: - Setup init(room: MXRoom) { self.room = room + self.items = CurrentValueSubject([]) + + generateUsersWithCount(10) + items.send(suggestionItems) } func processPartialUserName(_ userName: String) { + guard userName.count > 0 else { + items.send(suggestionItems) + return + } + items.send(suggestionItems.filter({ userSuggestion in + return (userSuggestion.displayName?.lowercased().range(of: userName.lowercased()) != .none) + })) + } + + private func generateUsersWithCount(_ count: UInt) { + suggestionItems.removeAll() + for _ in 0..: View { var content: () -> Content @Environment(\.theme) private var theme: ThemeSwiftUI + private let shadowRadius: CGFloat = 20.0 init(@ViewBuilder content: @escaping () -> Content) { self.content = content @@ -75,8 +77,9 @@ private struct BackgroundView: View { var body: some View { VStack(content: content) .background(theme.colors.background) - .clipShape(RoundedCornerShape(radius: 20.0, corners: [.topLeft, .topRight])) + .clipShape(RoundedCornerShape(radius: shadowRadius, corners: [.topLeft, .topRight])) .shadow(color: .black.opacity(0.20), radius: 20.0, x: 0.0, y: 3.0) + .mask(Rectangle().padding(.init(top: -(shadowRadius * 2), leading: 0.0, bottom: 0.0, trailing: 0.0))) .edgesIgnoringSafeArea(.all) } } From 9724409fc080a13ca097ea045a53bcb70b01baec Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 5 Oct 2021 14:30:31 +0300 Subject: [PATCH 161/276] #1098 - Working real user suggestion inside the main application. --- Riot/Generated/Images.swift | 15 +++- Riot/Modules/Room/RoomViewController.m | 25 ++++++- .../Views/InputToolbar/RoomInputToolbarView.m | 6 +- .../UserSuggestionCoordinator.swift | 35 ++++++++- .../UserSuggestionCoordinatorBridge.swift | 33 +++++++- .../Model/UserSuggestionViewModelResult.swift | 2 +- .../MatrixSDK/UserSuggestionService.swift | 75 +++++++++++++++---- .../Mock/MockUserSuggestionScreenState.swift | 4 +- .../Mock/MockUserSuggestionService.swift | 23 +++++- .../UserSuggestionServiceProtocol.swift | 2 +- .../View/UserSuggestionList.swift | 20 ++--- .../ViewModel/UserSuggestionViewModel.swift | 8 +- 12 files changed, 191 insertions(+), 57 deletions(-) diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 78b460c56..c681c2fbe 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -225,6 +225,7 @@ internal struct ImageAsset { internal typealias Image = UIImage #endif + @available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *) internal var image: Image { let bundle = BundleToken.bundle #if os(iOS) || os(tvOS) @@ -236,13 +237,25 @@ internal struct ImageAsset { let image = Image(named: name) #endif guard let result = image else { - fatalError("Unable to load image named \(name).") + fatalError("Unable to load image asset named \(name).") } return result } + + #if os(iOS) || os(tvOS) + @available(iOS 8.0, tvOS 9.0, *) + internal func image(compatibleWith traitCollection: UITraitCollection) -> Image { + let bundle = BundleToken.bundle + guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else { + fatalError("Unable to load image asset named \(name).") + } + return result + } + #endif } internal extension ImageAsset.Image { + @available(iOS 8.0, tvOS 9.0, watchOS 2.0, *) @available(macOS, deprecated, message: "This initializer is unsafe on macOS, please use the ImageAsset.image property") convenience init!(asset: ImageAsset) { diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 60689bc21..0bb714f65 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -137,7 +137,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @interface RoomViewController () + RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, UserSuggestionCoordinatorBridgeDelegate> { // The preview header @@ -1044,6 +1044,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; _userSuggestionCoordinator = [[UserSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager room:dataSource.room]; + _userSuggestionCoordinator.delegate = self; } - (void)onRoomDataSourceReady @@ -4226,9 +4227,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self cancelEventSelection]; } -- (void)roomInputToolbarViewDidRequestUserSuggestions:(MXKRoomInputToolbarView *)toolbarView +- (void)roomInputToolbarViewDidChangeTextMessage:(MXKRoomInputToolbarView *)toolbarView { - + [self.userSuggestionCoordinator processTextMessage:toolbarView.textMessage]; } #pragma mark - MXKRoomMemberDetailsViewControllerDelegate @@ -6547,4 +6548,22 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [[LegacyAppDelegate theDelegate] openSpaceWithId:spaceId]; } +#pragma mark - UserSuggestionCoordinatorBridgeDelegate + +- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator + didRequestMentionForMember:(MXRoomMember *)member + textTrigger:(NSString *)textTrigger +{ + if (textTrigger.length) { + NSString *textMessage = [self.inputToolbarView textMessage]; + textMessage = [textMessage stringByReplacingOccurrencesOfString:textTrigger + withString:@"" + options:NSBackwardsSearch | NSAnchoredSearch + range:NSMakeRange(0, textMessage.length)]; + [self.inputToolbarView setTextMessage:textMessage]; + } + + [self mention:member]; +} + @end diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index 3e72f3eb9..758782f14 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -325,10 +325,6 @@ const CGFloat kComposerContainerTrailingPadding = 12; NSString *newText = [textView.text stringByReplacingCharactersInRange:range withString:text]; [self updateUIWithTextMessage:newText animated:YES]; - if ([text isEqualToString:@"@"]) { - [self.delegate roomInputToolbarViewDidRequestUserSuggestions:self]; - } - return YES; } @@ -344,6 +340,8 @@ const CGFloat kComposerContainerTrailingPadding = 12; { [self.delegate roomInputToolbarView:self isTyping:(self.textMessage.length > 0 ? YES : NO)]; } + + [self.delegate roomInputToolbarViewDidChangeTextMessage:self]; } - (void)textViewDidChangeHeight:(GrowingTextView *)textView height:(CGFloat)height diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift index 83c3ac161..5115424b4 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -20,6 +20,13 @@ import Foundation import UIKit import SwiftUI +@available(iOS 14.0, *) +protocol UserSuggestionCoordinatorDelegate: AnyObject { + func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, + didRequestMentionForMember member: MXRoomMember, + textTrigger: String?) +} + @available(iOS 14.0, *) final class UserSuggestionCoordinator: Coordinator { @@ -30,7 +37,7 @@ final class UserSuggestionCoordinator: Coordinator { private let parameters: UserSuggestionCoordinatorParameters private let userSuggestionHostingController: UIViewController - private var userSuggestionService: UserSuggestionServiceProtocol + private var userSuggestionService: UserSuggestionService private var userSuggestionViewModel: UserSuggestionViewModelProtocol // MARK: Public @@ -39,6 +46,8 @@ final class UserSuggestionCoordinator: Coordinator { var childCoordinators: [Coordinator] = [] var completion: (() -> Void)? + weak var delegate: UserSuggestionCoordinatorDelegate? + // MARK: - Setup @available(iOS 14.0, *) @@ -47,15 +56,33 @@ final class UserSuggestionCoordinator: Coordinator { userSuggestionService = UserSuggestionService(room: parameters.room) userSuggestionViewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: userSuggestionService) - + let view = UserSuggestionList(viewModel: userSuggestionViewModel.context) .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) userSuggestionHostingController = UIHostingController(rootView: view) + + userSuggestionViewModel.completion = { [weak self] result in + guard let self = self else { + return + } + + switch result { + case .selectedItemWithIdentifier(let identifier): + guard let member = self.userSuggestionService.roomMemberForIdentifier(identifier) else { + return + } + + self.delegate?.userSuggestionCoordinator(self, + didRequestMentionForMember: member, + textTrigger: self.userSuggestionService.currentTextTrigger) + break + } + } } - func processPartialUserName(_ userName: String) { - userSuggestionService.processPartialUserName(userName) + func processTextMessage(_ textMessage: String) { + userSuggestionService.processTextMessage(textMessage) } // MARK: - Public diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift index bfa9b697e..a92ea2df4 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift @@ -16,6 +16,13 @@ import Foundation +@objc +protocol UserSuggestionCoordinatorBridgeDelegate: AnyObject { + func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, + didRequestMentionForMember member: MXRoomMember, + textTrigger: String?) +} + @objcMembers final class UserSuggestionCoordinatorBridge: NSObject { @@ -25,16 +32,25 @@ final class UserSuggestionCoordinatorBridge: NSObject { return _userSuggestionCoordinator as! UserSuggestionCoordinator } + weak var delegate: UserSuggestionCoordinatorBridgeDelegate? + init(mediaManager: MXMediaManager, room: MXRoom) { let parameters = UserSuggestionCoordinatorParameters(mediaManager: mediaManager, room: room) if #available(iOS 14.0, *) { - self._userSuggestionCoordinator = UserSuggestionCoordinator(parameters: parameters) + let userSuggestionCoordinator = UserSuggestionCoordinator(parameters: parameters) + self._userSuggestionCoordinator = userSuggestionCoordinator + } + + super.init() + + if #available(iOS 14.0, *) { + userSuggestionCoordinator.delegate = self } } - func processPartialUserName(_ userName: String) { + func processTextMessage(_ textMessage: String) { if #available(iOS 14.0, *) { - return self.userSuggestionCoordinator.processPartialUserName(userName) + return self.userSuggestionCoordinator.processTextMessage(textMessage) } } @@ -46,3 +62,14 @@ final class UserSuggestionCoordinatorBridge: NSObject { return nil } } + +@available(iOS 14.0, *) +extension UserSuggestionCoordinatorBridge: UserSuggestionCoordinatorDelegate { + func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, + didRequestMentionForMember member: MXRoomMember, + textTrigger: String?) { + delegate?.userSuggestionCoordinatorBridge(self, + didRequestMentionForMember: member, + textTrigger: textTrigger) + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift index fc25fdcb6..0336e5be0 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift @@ -20,5 +20,5 @@ import Foundation @available(iOS 14, *) enum UserSuggestionViewModelResult { - case selectedItem(UserSuggestionItemProtocol) + case selectedItemWithIdentifier(String) } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/MatrixSDK/UserSuggestionService.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/MatrixSDK/UserSuggestionService.swift index 29f37ee0e..abb143a5c 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/MatrixSDK/UserSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/MatrixSDK/UserSuggestionService.swift @@ -19,6 +19,13 @@ import Foundation import Combine +@available(iOS 14.0, *) +struct UserSuggestionServiceItem: UserSuggestionItemProtocol { + let userId: String + let displayName: String? + let avatarUrl: String? +} + @available(iOS 14.0, *) class UserSuggestionService: UserSuggestionServiceProtocol { @@ -29,10 +36,12 @@ class UserSuggestionService: UserSuggestionServiceProtocol { private let room: MXRoom private var suggestionItems: [UserSuggestionItemProtocol] = [] + private var roomJoinedMembers: [MXRoomMember] = [] // MARK: Public var items: CurrentValueSubject<[UserSuggestionItemProtocol], Never> + var currentTextTrigger: String? // MARK: - Setup @@ -40,26 +49,64 @@ class UserSuggestionService: UserSuggestionServiceProtocol { self.room = room self.items = CurrentValueSubject([]) - generateUsersWithCount(10) - items.send(suggestionItems) + self.room.members { [weak self] members in + guard let self = self, let joinedMembers = members?.joinedMembers else { + return + } + + self.roomJoinedMembers = joinedMembers + + self.suggestionItems = joinedMembers.map { member in + UserSuggestionServiceItem(userId: member.userId, displayName: member.displayname, avatarUrl: member.avatarUrl) + } + } lazyLoadedMembers: { [weak self] lazyMembers in + guard let self = self, let joinedMembers = lazyMembers?.joinedMembers else { + return + } + + self.roomJoinedMembers = joinedMembers + + self.suggestionItems = joinedMembers.map { member in + UserSuggestionServiceItem(userId: member.userId, displayName: member.displayname, avatarUrl: member.avatarUrl) + } + } failure: { error in + MXLog.error("[UserSuggestionService] Failed loading room with error: \(String(describing: error))") + } } - func processPartialUserName(_ userName: String) { - guard userName.count > 0 else { - items.send(suggestionItems) + func roomMemberForIdentifier(_ identifier: String) -> MXRoomMember? { + return roomJoinedMembers.filter { $0.userId == identifier }.first + } + + // MARK: - UserSuggestionServiceProtocol + + func processTextMessage(_ textMessage: String) { + items.send([]) + currentTextTrigger = nil + + guard textMessage.count > 0 else { return } + let components = textMessage.components(separatedBy: .whitespaces) + + guard let lastComponent = components.last else { + return + } + + guard lastComponent.hasPrefix("@") else { + return + } + + currentTextTrigger = lastComponent + + var partialName = lastComponent + partialName.removeFirst() + items.send(suggestionItems.filter({ userSuggestion in - return (userSuggestion.displayName?.lowercased().range(of: userName.lowercased()) != .none) + let containedInUsername = userSuggestion.userId.lowercased().range(of: partialName.lowercased()) != .none + let containedInDisplayName = (userSuggestion.displayName ?? "").lowercased().range(of: partialName.lowercased()) != .none + return (containedInUsername || containedInDisplayName) })) } - - private func generateUsersWithCount(_ count: UInt) { - suggestionItems.removeAll() - for _ in 0.. 0 else { - items.send(suggestionItems) + func processTextMessage(_ textMessage: String) { + + items.send([]) + + guard textMessage.count > 0 else { return } + let components = textMessage.components(separatedBy: .whitespaces) + + guard let lastComponent = components.last else { + return + } + + guard lastComponent.hasPrefix("@") else { + return + } + + var partialName = lastComponent + partialName.removeFirst() + items.send(suggestionItems.filter({ userSuggestion in - return (userSuggestion.displayName?.lowercased().range(of: userName.lowercased()) != .none) + return (userSuggestion.displayName?.lowercased().range(of: partialName.lowercased()) != .none) })) } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift index 5f5df77ef..438cd9ff4 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift @@ -31,7 +31,7 @@ protocol UserSuggestionServiceProtocol { var items: CurrentValueSubject<[UserSuggestionItemProtocol], Never> { get } - func processPartialUserName(_ userName: String) + func processTextMessage(_ textMessage: String) } // MARK: Avatarable diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift index 70b2d2f36..9c2ec2e1b 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift @@ -36,28 +36,20 @@ struct UserSuggestionList: View { var body: some View { BackgroundView { - ScrollViewReader { scrollViewReader in - List(viewModel.viewState.items) { item in + List(viewModel.viewState.items) { item in + Button { + viewModel.send(viewAction: .selectedItem(item)) + } label: { UserSuggestionListItem( avatar: item.avatar, displayName: item.displayName, userId: item.id ) .padding([.top, .bottom], 4.0) - .onTapGesture { - viewModel.send(viewAction: .selectedItem(item)) - } } - .environment(\.defaultMinListRowHeight, rowHeight) - .frame(height: min(maxHeight, rowHeight * CGFloat(viewModel.viewState.items.count))) - .onAppear(perform: { - guard let lastItemId = viewModel.viewState.items.last?.id else { - return - } - - scrollViewReader.scrollTo(lastItemId) - }) } + .environment(\.defaultMinListRowHeight, rowHeight) + .frame(height: min(maxHeight, rowHeight * CGFloat(viewModel.viewState.items.count))) } } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift index 48ac36b81..3d7dca491 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift @@ -42,10 +42,6 @@ class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewMo return UserSuggestionViewModel(userSuggestionService: userSuggestionService) } - deinit { - print("well shit") - } - private init(userSuggestionService: UserSuggestionServiceProtocol) { self.userSuggestionService = userSuggestionService super.init(initialViewState: Self.defaultState(userSuggestionService: userSuggestionService)) @@ -71,8 +67,8 @@ class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewMo override func process(viewAction: UserSuggestionViewAction) { switch viewAction { - case .selectedItem(_): - break + case .selectedItem(let item): + completion?(.selectedItemWithIdentifier(item.id)) } } From af4b5dc1aa39031433fcd047f29dedc5003821c1 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 6 Oct 2021 14:42:48 +0300 Subject: [PATCH 162/276] #1098 - Stopped relying on the SDK in the suggestionService and added unit and ui tests. --- .../UserSuggestionCoordinator.swift | 53 ++++++-- .../UserSuggestionCoordinatorBridge.swift | 12 +- .../MatrixSDK/UserSuggestionService.swift | 112 ----------------- .../Mock/MockUserSuggestionScreenState.swift | 21 +++- .../Mock/MockUserSuggestionService.swift | 74 ----------- .../Service/UserSuggestionService.swift | 107 ++++++++++++++++ .../UserSuggestionServiceProtocol.swift | 2 + .../Test/UI/UserSuggestionUITests.swift | 17 +-- .../Unit/UserSuggestionServiceTests.swift | 119 ++++++++++++++++++ .../Unit/UserSuggestionViewModelTests.swift | 42 ------- .../View/UserSuggestionListWithInput.swift | 3 + 11 files changed, 294 insertions(+), 268 deletions(-) delete mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Service/MatrixSDK/UserSuggestionService.swift delete mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionService.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift delete mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionViewModelTests.swift diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift index 5115424b4..fae1418d2 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -22,9 +22,7 @@ import SwiftUI @available(iOS 14.0, *) protocol UserSuggestionCoordinatorDelegate: AnyObject { - func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, - didRequestMentionForMember member: MXRoomMember, - textTrigger: String?) + func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) } @available(iOS 14.0, *) @@ -35,10 +33,12 @@ final class UserSuggestionCoordinator: Coordinator { // MARK: Private private let parameters: UserSuggestionCoordinatorParameters - private let userSuggestionHostingController: UIViewController - private var userSuggestionService: UserSuggestionService - private var userSuggestionViewModel: UserSuggestionViewModelProtocol + private var userSuggestionHostingController: UIViewController! + private var userSuggestionService: UserSuggestionServiceProtocol! + private var userSuggestionViewModel: UserSuggestionViewModelProtocol! + + private var roomMembers: [MXRoomMember] = [] // MARK: Public @@ -54,13 +54,13 @@ final class UserSuggestionCoordinator: Coordinator { init(parameters: UserSuggestionCoordinatorParameters) { self.parameters = parameters - userSuggestionService = UserSuggestionService(room: parameters.room) + userSuggestionService = UserSuggestionService(roomMembersProvider: self) userSuggestionViewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: userSuggestionService) let view = UserSuggestionList(viewModel: userSuggestionViewModel.context) .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) - userSuggestionHostingController = UIHostingController(rootView: view) + userSuggestionHostingController = VectorHostingController(rootView: view) userSuggestionViewModel.completion = { [weak self] result in guard let self = self else { @@ -69,14 +69,11 @@ final class UserSuggestionCoordinator: Coordinator { switch result { case .selectedItemWithIdentifier(let identifier): - guard let member = self.userSuggestionService.roomMemberForIdentifier(identifier) else { + guard let member = self.roomMembers.filter({ $0.userId == identifier }).first else { return } - self.delegate?.userSuggestionCoordinator(self, - didRequestMentionForMember: member, - textTrigger: self.userSuggestionService.currentTextTrigger) - break + self.delegate?.userSuggestionCoordinator(self, didRequestMentionForMember: member, textTrigger: self.userSuggestionService.currentTextTrigger) } } } @@ -94,3 +91,33 @@ final class UserSuggestionCoordinator: Coordinator { return self.userSuggestionHostingController } } + +@available(iOS 14.0, *) +extension UserSuggestionCoordinator: RoomMembersProviderProtocol { + func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void) { + guard roomMembers.count == 0 else { + members(roomMembersToProviderMembers(roomMembers)) + return + } + + parameters.room.members({ [weak self] roomMembers in + guard let self = self, let joinedMembers = roomMembers?.joinedMembers else { + return + } + self.roomMembers = joinedMembers + members(self.roomMembersToProviderMembers(joinedMembers)) + }, lazyLoadedMembers: { [weak self] lazyRoomMembers in + guard let self = self, let joinedMembers = lazyRoomMembers?.joinedMembers else { + return + } + self.roomMembers = joinedMembers + members(self.roomMembersToProviderMembers(joinedMembers)) + }, failure: { error in + MXLog.error("[UserSuggestionCoordinator] Failed loading room with error: \(String(describing: error))") + }) + } + + private func roomMembersToProviderMembers(_ roomMembers: [MXRoomMember]) -> [RoomMembersProviderMember] { + roomMembers.map { RoomMembersProviderMember(identifier: $0.userId, displayName: $0.displayname ?? "", avatarURL: $0.avatarUrl ?? "") } + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift index a92ea2df4..0760670a3 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift @@ -18,9 +18,7 @@ import Foundation @objc protocol UserSuggestionCoordinatorBridgeDelegate: AnyObject { - func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, - didRequestMentionForMember member: MXRoomMember, - textTrigger: String?) + func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) } @objcMembers @@ -65,11 +63,7 @@ final class UserSuggestionCoordinatorBridge: NSObject { @available(iOS 14.0, *) extension UserSuggestionCoordinatorBridge: UserSuggestionCoordinatorDelegate { - func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, - didRequestMentionForMember member: MXRoomMember, - textTrigger: String?) { - delegate?.userSuggestionCoordinatorBridge(self, - didRequestMentionForMember: member, - textTrigger: textTrigger) + func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) { + delegate?.userSuggestionCoordinatorBridge(self, didRequestMentionForMember: member, textTrigger: textTrigger) } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/MatrixSDK/UserSuggestionService.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/MatrixSDK/UserSuggestionService.swift deleted file mode 100644 index abb143a5c..000000000 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/MatrixSDK/UserSuggestionService.swift +++ /dev/null @@ -1,112 +0,0 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Combine - -@available(iOS 14.0, *) -struct UserSuggestionServiceItem: UserSuggestionItemProtocol { - let userId: String - let displayName: String? - let avatarUrl: String? -} - -@available(iOS 14.0, *) -class UserSuggestionService: UserSuggestionServiceProtocol { - - // MARK: - Properties - - // MARK: Private - - private let room: MXRoom - - private var suggestionItems: [UserSuggestionItemProtocol] = [] - private var roomJoinedMembers: [MXRoomMember] = [] - - // MARK: Public - - var items: CurrentValueSubject<[UserSuggestionItemProtocol], Never> - var currentTextTrigger: String? - - // MARK: - Setup - - init(room: MXRoom) { - self.room = room - self.items = CurrentValueSubject([]) - - self.room.members { [weak self] members in - guard let self = self, let joinedMembers = members?.joinedMembers else { - return - } - - self.roomJoinedMembers = joinedMembers - - self.suggestionItems = joinedMembers.map { member in - UserSuggestionServiceItem(userId: member.userId, displayName: member.displayname, avatarUrl: member.avatarUrl) - } - } lazyLoadedMembers: { [weak self] lazyMembers in - guard let self = self, let joinedMembers = lazyMembers?.joinedMembers else { - return - } - - self.roomJoinedMembers = joinedMembers - - self.suggestionItems = joinedMembers.map { member in - UserSuggestionServiceItem(userId: member.userId, displayName: member.displayname, avatarUrl: member.avatarUrl) - } - } failure: { error in - MXLog.error("[UserSuggestionService] Failed loading room with error: \(String(describing: error))") - } - } - - func roomMemberForIdentifier(_ identifier: String) -> MXRoomMember? { - return roomJoinedMembers.filter { $0.userId == identifier }.first - } - - // MARK: - UserSuggestionServiceProtocol - - func processTextMessage(_ textMessage: String) { - items.send([]) - currentTextTrigger = nil - - guard textMessage.count > 0 else { - return - } - - let components = textMessage.components(separatedBy: .whitespaces) - - guard let lastComponent = components.last else { - return - } - - guard lastComponent.hasPrefix("@") else { - return - } - - currentTextTrigger = lastComponent - - var partialName = lastComponent - partialName.removeFirst() - - items.send(suggestionItems.filter({ userSuggestion in - let containedInUsername = userSuggestion.userId.lowercased().range(of: partialName.lowercased()) != .none - let containedInDisplayName = (userSuggestion.displayName ?? "").lowercased().range(of: partialName.lowercased()) != .none - return (containedInUsername || containedInDisplayName) - })) - } -} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift index fce3d3acf..d10fe713b 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift @@ -28,12 +28,7 @@ enum MockUserSuggestionScreenState: MockScreenState, CaseIterable { } var screenView: ([Any], AnyView) { - let service: MockUserSuggestionService - switch self { - case .multipleResults: - service = MockUserSuggestionService(userCount: 10) - } - + let service = UserSuggestionService(roomMembersProvider: self) let listViewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: service) let viewModel = UserSuggestionListWithInputViewModel(listViewModel: listViewModel) { textMessage in @@ -47,3 +42,17 @@ enum MockUserSuggestionScreenState: MockScreenState, CaseIterable { ) } } + +@available(iOS 14.0, *) +extension MockUserSuggestionScreenState: RoomMembersProviderProtocol { + func fetchMembers(_ members: ([RoomMembersProviderMember]) -> Void) { + members(generateUsersWithCount(10)) + } + + private func generateUsersWithCount(_ count: UInt) -> [RoomMembersProviderMember] { + return (0.. - - init(userCount: UInt) { - items = CurrentValueSubject([]) - generateUsersWithCount(userCount) - items.send(suggestionItems) - } - - func processTextMessage(_ textMessage: String) { - - items.send([]) - - guard textMessage.count > 0 else { - return - } - - let components = textMessage.components(separatedBy: .whitespaces) - - guard let lastComponent = components.last else { - return - } - - guard lastComponent.hasPrefix("@") else { - return - } - - var partialName = lastComponent - partialName.removeFirst() - - items.send(suggestionItems.filter({ userSuggestion in - return (userSuggestion.displayName?.lowercased().range(of: partialName.lowercased()) != .none) - })) - } - - private func generateUsersWithCount(_ count: UInt) { - suggestionItems.removeAll() - for _ in 0.. Void) +} + +@available(iOS 14.0, *) +struct UserSuggestionServiceItem: UserSuggestionItemProtocol { + let userId: String + let displayName: String? + let avatarUrl: String? +} + +@available(iOS 14.0, *) +class UserSuggestionService: UserSuggestionServiceProtocol { + + // MARK: - Properties + + // MARK: Private + + private let roomMembersProvider: RoomMembersProviderProtocol + + private var suggestionItems: [UserSuggestionItemProtocol] = [] + + // MARK: Public + + var items: CurrentValueSubject<[UserSuggestionItemProtocol], Never> + var currentTextTrigger: String? + + // MARK: - Setup + + init(roomMembersProvider: RoomMembersProviderProtocol) { + self.roomMembersProvider = roomMembersProvider + self.items = CurrentValueSubject([]) + } + + // MARK: - UserSuggestionServiceProtocol + + func processTextMessage(_ textMessage: String) { + roomMembersProvider.fetchMembers { [weak self] members in + guard let self = self else { + return + } + + self.suggestionItems = members.map { member in + UserSuggestionServiceItem(userId: member.identifier, displayName: member.displayName, avatarUrl: member.avatarURL) + } + + self.items.send([]) + self.currentTextTrigger = nil + + guard textMessage.count > 0 else { + return + } + + let components = textMessage.components(separatedBy: .whitespaces) + + guard let lastComponent = components.last else { + return + } + + // Partial username should start with one and only one "@" character + guard lastComponent.prefix(while: { character in character == "@" }).count == 1 else { + return + } + + self.currentTextTrigger = lastComponent + + var partialName = lastComponent + partialName.removeFirst() + + self.items.send(self.suggestionItems.filter({ userSuggestion in + let containedInUsername = userSuggestion.userId.lowercased().range(of: partialName.lowercased()) != .none + let containedInDisplayName = (userSuggestion.displayName ?? "").lowercased().range(of: partialName.lowercased()) != .none + + return (containedInUsername || containedInDisplayName) + })) + } + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift index 438cd9ff4..8eb69854f 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift @@ -31,6 +31,8 @@ protocol UserSuggestionServiceProtocol { var items: CurrentValueSubject<[UserSuggestionItemProtocol], Never> { get } + var currentTextTrigger: String? { get } + func processTextMessage(_ textMessage: String) } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift index f3f984791..c0fa3c926 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift @@ -31,17 +31,10 @@ class UserSuggestionUITests: MockScreenTest { } func verifyUserSuggestionScreen() throws { - guard let screenState = screenState as? MockUserSuggestionScreenState else { fatalError("no screen") } - switch screenState { - case .longDisplayName(let name): - verifyUserSuggestionLongName(name: name) - } + XCTAssert(app.tables.firstMatch.exists) + + let firstButton = app.tables.firstMatch.buttons.firstMatch + _ = firstButton.waitForExistence(timeout: 10) + XCTAssert(firstButton.identifier == "displayNameText-userIdText") } - - func verifyUserSuggestionLongName(name: String) { - let displayNameText = app.staticTexts["displayNameText"] - XCTAssert(displayNameText.exists) - XCTAssertEqual(displayNameText.label, name) - } - } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift new file mode 100644 index 000000000..592155079 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift @@ -0,0 +1,119 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Combine + +@testable import RiotSwiftUI + +@available(iOS 14.0, *) +class UserSuggestionServiceTests: XCTestCase { + + var service: UserSuggestionService? + + override func setUp() { + service = UserSuggestionService(roomMembersProvider: self) + } + + func testAlice() { + service?.processTextMessage("@Al") + assert(service?.items.value.first?.displayName == "Alice") + + service?.processTextMessage("@al") + assert(service?.items.value.first?.displayName == "Alice") + + service?.processTextMessage("@ice") + assert(service?.items.value.first?.displayName == "Alice") + + service?.processTextMessage("@Alice") + assert(service?.items.value.first?.displayName == "Alice") + + service?.processTextMessage("@alice:matrix.org") + assert(service?.items.value.first?.displayName == "Alice") + } + + func testBob() { + service?.processTextMessage("@ob") + assert(service?.items.value.first?.displayName == "Bob") + + service?.processTextMessage("@ob:") + assert(service?.items.value.first?.displayName == "Bob") + + service?.processTextMessage("@b:matrix") + assert(service?.items.value.first?.displayName == "Bob") + } + + func testBoth() { + service?.processTextMessage("@:matrix") + assert(service?.items.value.first?.displayName == "Alice") + assert(service?.items.value.last?.displayName == "Bob") + + service?.processTextMessage("@.org") + assert(service?.items.value.first?.displayName == "Alice") + assert(service?.items.value.last?.displayName == "Bob") + } + + func testEmptyResult() { + service?.processTextMessage("Lorem ipsum idolor") + assert(service?.items.value.count == 0) + + service?.processTextMessage("@") + assert(service?.items.value.count == 0) + + service?.processTextMessage("@@") + assert(service?.items.value.count == 0) + } + + func testStuff() { + service?.processTextMessage("@@") + assert(service?.items.value.count == 0) + } + + func testWhitespaces() { + service?.processTextMessage("") + assert(service?.items.value.count == 0) + + service?.processTextMessage(" ") + assert(service?.items.value.count == 0) + + service?.processTextMessage("\n") + assert(service?.items.value.count == 0) + + service?.processTextMessage(" \n ") + assert(service?.items.value.count == 0) + + service?.processTextMessage("@A ") + assert(service?.items.value.count == 0) + + service?.processTextMessage(" @A ") + assert(service?.items.value.count == 0) + } +} + +@available(iOS 14.0, *) +extension UserSuggestionServiceTests: RoomMembersProviderProtocol { + func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void) { + + let users = [("Alice", "@alice:matrix.org"), + ("Bob", "@bob:matrix.org")] + + members(users.map({ user in + RoomMembersProviderMember(identifier: user.1, displayName: user.0, avatarURL: "") + })) + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionViewModelTests.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionViewModelTests.swift deleted file mode 100644 index 7e9060bf0..000000000 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionViewModelTests.swift +++ /dev/null @@ -1,42 +0,0 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest -import Combine - -@testable import RiotSwiftUI - -@available(iOS 14.0, *) -class UserSuggestionViewModelTests: XCTestCase { - private enum Constants { - static let displayName = "Alice" - } - var service: MockUserSuggestionService! - var viewModel: UserSuggestionViewModelProtocol! - var context: UserSuggestionViewModelType.Context! - var cancellables = Set() - override func setUpWithError() throws { - service = MockUserSuggestionService(userCount: 10) - viewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: service) - context = viewModel.context - } - - func testInitialState() { -// XCTAssertEqual(context.viewState.displayName, Constants.displayName) - } -} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift index 4d34f8f34..f2ef43e3c 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift @@ -44,6 +44,9 @@ struct UserSuggestionListWithInput: View { }) .border(Color.black) .padding([.leading, .trailing]) + .onAppear(perform: { + inputText = "@-" // Make the list show all available mock results + }) } } } From d30cab44ad575b1080fae290f1db992bc32ed2ab Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 6 Oct 2021 14:43:21 +0300 Subject: [PATCH 163/276] #1098 - Fixed iOS 15 issues. --- .../Modules/Common/SwiftUI/VectorHostingController.swift | 9 +++++++++ .../Room/UserSuggestion/View/UserSuggestionList.swift | 1 + 2 files changed, 10 insertions(+) diff --git a/Riot/Modules/Common/SwiftUI/VectorHostingController.swift b/Riot/Modules/Common/SwiftUI/VectorHostingController.swift index 504504312..854bcae07 100644 --- a/Riot/Modules/Common/SwiftUI/VectorHostingController.swift +++ b/Riot/Modules/Common/SwiftUI/VectorHostingController.swift @@ -45,6 +45,15 @@ class VectorHostingController: UIHostingController { self.update(theme: self.theme) } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + // Fixes weird iOS 15 bug where the view no longer grows its enclosing host + if #available(iOS 15.0, *) { + self.view.invalidateIntrinsicContentSize() + } + } + private func registerThemeServiceDidChangeThemeNotification() { NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil) } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift index 9c2ec2e1b..2653caabc 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift @@ -48,6 +48,7 @@ struct UserSuggestionList: View { .padding([.top, .bottom], 4.0) } } + .listStyle(PlainListStyle()) .environment(\.defaultMinListRowHeight, rowHeight) .frame(height: min(maxHeight, rowHeight * CGFloat(viewModel.viewState.items.count))) } From b847528d815095140cb223d3e6bc4caaeab26bfb Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 6 Oct 2021 15:51:41 +0300 Subject: [PATCH 164/276] #1098 - Moved new MXKRoomInputToolbarDelegate method to Element. --- .../Modules/Room/Views/InputToolbar/RoomInputToolbarView.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h index 8e5057db0..4a8ccb1ff 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h @@ -40,6 +40,13 @@ typedef enum : NSUInteger */ - (void)roomInputToolbarViewDidTapCancel:(MXKRoomInputToolbarView*)toolbarView; +/** + Tells the delegate that a user mention has been triggered + + @param toolbarView the room input toolbar view + */ +- (void)roomInputToolbarViewDidChangeTextMessage:(MXKRoomInputToolbarView*)toolbarView; + @end /** From 62f9586a7a40e147b06b089ed33d797b5a38dac6 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 6 Oct 2021 16:12:21 +0300 Subject: [PATCH 165/276] #1098 - Removed vectorHostingController background color. --- Riot/Modules/Common/SwiftUI/VectorHostingController.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/Common/SwiftUI/VectorHostingController.swift b/Riot/Modules/Common/SwiftUI/VectorHostingController.swift index 854bcae07..3e9ad98b4 100644 --- a/Riot/Modules/Common/SwiftUI/VectorHostingController.swift +++ b/Riot/Modules/Common/SwiftUI/VectorHostingController.swift @@ -41,6 +41,9 @@ class VectorHostingController: UIHostingController { override func viewDidLoad() { super.viewDidLoad() + + self.view.backgroundColor = .clear + self.registerThemeServiceDidChangeThemeNotification() self.update(theme: self.theme) } @@ -63,8 +66,6 @@ class VectorHostingController: UIHostingController { } private func update(theme: Theme) { - self.view.backgroundColor = theme.headerBackgroundColor - if let navigationBar = self.navigationController?.navigationBar { theme.applyStyle(onNavigationBar: navigationBar) } From 261524c99c88fa216ca1224962feb406662d8b94 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 7 Oct 2021 09:23:32 +0300 Subject: [PATCH 166/276] #1098 - Avatar view model: stop requesting avatars for empty URIs. --- .../Modules/Common/Avatar/ViewModel/AvatarViewModel.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift index 6808f0dd6..1a5f8f032 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift @@ -47,7 +47,10 @@ class AvatarViewModel: InjectableObject, ObservableObject { stableColorIndex(matrixItemId: matrixItemId, colorCount: colorCount) ) - guard let mxContentUri = mxContentUri else { return } + guard let mxContentUri = mxContentUri, mxContentUri.count > 0 else { + return + } + avatarService.avatarImage(mxContentUri: mxContentUri, avatarSize: avatarSize) .sink { completion in guard case let .failure(error) = completion else { return } From c5e9d115003985fb7a358711a1a3376690bc13ff Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 7 Oct 2021 10:34:17 +0300 Subject: [PATCH 167/276] #1098 - Fix performance issues on item list change. Rebuild the whole list instead of comparing changed items. --- .../Modules/Room/UserSuggestion/View/UserSuggestionList.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift index 2653caabc..8c667fb5d 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift @@ -51,6 +51,7 @@ struct UserSuggestionList: View { .listStyle(PlainListStyle()) .environment(\.defaultMinListRowHeight, rowHeight) .frame(height: min(maxHeight, rowHeight * CGFloat(viewModel.viewState.items.count))) + .id(UUID()) // Rebuild the whole list on item changes. Fixes performance issues. } } } From 432af4b87985f36fdaec254d51f9eaec837766f4 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 7 Oct 2021 12:21:40 +0300 Subject: [PATCH 168/276] #1098 - Various changes following code review. --- .../Room/Views/InputToolbar/RoomInputToolbarView.h | 2 +- .../Model/UserSuggestionViewState.swift | 2 +- .../Service/UserSuggestionService.swift | 6 +++--- .../Test/Unit/UserSuggestionServiceTests.swift | 3 +++ .../UserSuggestion/View/UserSuggestionList.swift | 3 ++- .../UserSuggestion/View/UserSuggestionListItem.swift | 2 +- .../View/UserSuggestionListWithInput.swift | 12 +++++------- .../Coordinator/TemplateUserProfileCoordinator.swift | 1 - 8 files changed, 16 insertions(+), 15 deletions(-) diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h index 4a8ccb1ff..3099fd50f 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h @@ -41,7 +41,7 @@ typedef enum : NSUInteger - (void)roomInputToolbarViewDidTapCancel:(MXKRoomInputToolbarView*)toolbarView; /** - Tells the delegate that a user mention has been triggered + Inform the delegate that the text message has changed. @param toolbarView the room input toolbar view */ diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift index 038852299..b47e436c4 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift @@ -20,7 +20,7 @@ import Foundation import Combine @available(iOS 14.0, *) -struct UserSuggestionViewStateItem: BindableState, Identifiable { +struct UserSuggestionViewStateItem: Identifiable { let id: String let avatar: AvatarInputProtocol? let displayName: String? diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift index 3f7ae9582..ee25a101a 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift @@ -87,7 +87,7 @@ class UserSuggestionService: UserSuggestionServiceProtocol { } // Partial username should start with one and only one "@" character - guard lastComponent.prefix(while: { character in character == "@" }).count == 1 else { + guard lastComponent.prefix(while: { $0 == "@" }).count == 1 else { return } @@ -97,8 +97,8 @@ class UserSuggestionService: UserSuggestionServiceProtocol { partialName.removeFirst() self.items.send(self.suggestionItems.filter({ userSuggestion in - let containedInUsername = userSuggestion.userId.lowercased().range(of: partialName.lowercased()) != .none - let containedInDisplayName = (userSuggestion.displayName ?? "").lowercased().range(of: partialName.lowercased()) != .none + let containedInUsername = userSuggestion.userId.lowercased().contains(partialName.lowercased()) + let containedInDisplayName = (userSuggestion.displayName ?? "").lowercased().contains(partialName.lowercased()) return (containedInUsername || containedInDisplayName) })) diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift index 592155079..1c34f7eb3 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift @@ -77,6 +77,9 @@ class UserSuggestionServiceTests: XCTestCase { service?.processTextMessage("@@") assert(service?.items.value.count == 0) + + service?.processTextMessage("alice@matrix.org") + assert(service?.items.value.count == 0) } func testStuff() { diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift index 8c667fb5d..5693615ec 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift @@ -51,6 +51,7 @@ struct UserSuggestionList: View { .listStyle(PlainListStyle()) .environment(\.defaultMinListRowHeight, rowHeight) .frame(height: min(maxHeight, rowHeight * CGFloat(viewModel.viewState.items.count))) +// .frame(maxHeight: maxHeight) .id(UUID()) // Rebuild the whole list on item changes. Fixes performance issues. } } @@ -69,7 +70,7 @@ private struct BackgroundView: View { } var body: some View { - VStack(content: content) + content() .background(theme.colors.background) .clipShape(RoundedCornerShape(radius: shadowRadius, corners: [.topLeft, .topRight])) .shadow(color: .black.opacity(0.20), radius: 20.0, x: 0.0, y: 3.0) diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift index 927155a73..dcfa53fa8 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift @@ -36,7 +36,7 @@ struct UserSuggestionListItem: View { if let avatar = avatar { AvatarImage(avatarData: avatar, size: .medium) } - VStack(alignment:.leading) { + VStack(alignment: .leading) { Text(displayName ?? "") .font(theme.fonts.body) .foregroundColor(theme.colors.primaryContent) diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift index f2ef43e3c..40573b75c 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift @@ -33,20 +33,18 @@ struct UserSuggestionListWithInput: View { var viewModel: UserSuggestionListWithInputViewModel @State private var inputText: String = "" - + var body: some View { VStack(spacing: 0.0) { UserSuggestionList(viewModel: viewModel.listViewModel.context) TextField("Search for user", text: $inputText) .background(Color.white) - .onChange(of: inputText, perform: { value in - viewModel.callback(value) - }) - .border(Color.black) + .onChange(of: inputText, perform:viewModel.callback) + .textFieldStyle(RoundedBorderTextFieldStyle()) .padding([.leading, .trailing]) - .onAppear(perform: { + .onAppear { inputText = "@-" // Make the list show all available mock results - }) + } } } } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift index cdd0e48cb..02874dee9 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift @@ -55,7 +55,6 @@ final class TemplateUserProfileCoordinator: Coordinator { switch result { case .cancel, .done: self.completion?() - break } } } From 811bda3aaf6c47015cc5a1e6d92a5562f07d2ab6 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 7 Oct 2021 12:59:58 +0300 Subject: [PATCH 169/276] #1098 - Add debouncer to suggestion service text processing. Stop returning cached member results from the coordinator, rely on the room directly for up to date results. --- .../UserSuggestionCoordinator.swift | 7 +- .../Mock/MockUserSuggestionScreenState.swift | 2 +- .../Service/UserSuggestionService.swift | 77 +++++++++++-------- .../UserSuggestionServiceProtocol.swift | 2 +- 4 files changed, 50 insertions(+), 38 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift index fae1418d2..bc50aae42 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -95,11 +95,6 @@ final class UserSuggestionCoordinator: Coordinator { @available(iOS 14.0, *) extension UserSuggestionCoordinator: RoomMembersProviderProtocol { func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void) { - guard roomMembers.count == 0 else { - members(roomMembersToProviderMembers(roomMembers)) - return - } - parameters.room.members({ [weak self] roomMembers in guard let self = self, let joinedMembers = roomMembers?.joinedMembers else { return @@ -118,6 +113,6 @@ extension UserSuggestionCoordinator: RoomMembersProviderProtocol { } private func roomMembersToProviderMembers(_ roomMembers: [MXRoomMember]) -> [RoomMembersProviderMember] { - roomMembers.map { RoomMembersProviderMember(identifier: $0.userId, displayName: $0.displayname ?? "", avatarURL: $0.avatarUrl ?? "") } + roomMembers.map { RoomMembersProviderMember(userId: $0.userId, displayName: $0.displayname ?? "", avatarUrl: $0.avatarUrl ?? "") } } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift index d10fe713b..6c1796d45 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift @@ -52,7 +52,7 @@ extension MockUserSuggestionScreenState: RoomMembersProviderProtocol { private func generateUsersWithCount(_ count: UInt) -> [RoomMembersProviderMember] { return (0..(nil) + private var cancellables = Set() // MARK: Public - var items: CurrentValueSubject<[UserSuggestionItemProtocol], Never> - var currentTextTrigger: String? + var items = CurrentValueSubject<[UserSuggestionItemProtocol], Never>([]) + + var currentTextTrigger: String? { + currentTextTriggerSubject.value + } // MARK: - Setup init(roomMembersProvider: RoomMembersProviderProtocol) { self.roomMembersProvider = roomMembersProvider - self.items = CurrentValueSubject([]) + + currentTextTriggerSubject + .removeDuplicates() + .debounce(for: 0.5, scheduler: RunLoop.main) + .sink { self.fetchAndFilterMembersForTextTrigger($0) } + .store(in: &cancellables) } // MARK: - UserSuggestionServiceProtocol - func processTextMessage(_ textMessage: String) { + func processTextMessage(_ textMessage: String?) { + self.items.send([]) + self.currentTextTriggerSubject.send(nil) + + guard let textMessage = textMessage, textMessage.count > 0 else { + return + } + + let components = textMessage.components(separatedBy: .whitespaces) + + guard let lastComponent = components.last else { + return + } + + // Partial username should start with one and only one "@" character + guard lastComponent.prefix(while: { $0 == "@" }).count == 1 else { + return + } + + self.currentTextTriggerSubject.send(lastComponent) + } + + // MARK: - Private + + private func fetchAndFilterMembersForTextTrigger(_ textTrigger: String?) { + guard var partialName = textTrigger else { + return + } + + partialName.removeFirst() // remove the '@' prefix + roomMembersProvider.fetchMembers { [weak self] members in guard let self = self else { return } self.suggestionItems = members.map { member in - UserSuggestionServiceItem(userId: member.identifier, displayName: member.displayName, avatarUrl: member.avatarURL) + UserSuggestionServiceItem(userId: member.userId, displayName: member.displayName, avatarUrl: member.avatarUrl) } - self.items.send([]) - self.currentTextTrigger = nil - - guard textMessage.count > 0 else { - return - } - - let components = textMessage.components(separatedBy: .whitespaces) - - guard let lastComponent = components.last else { - return - } - - // Partial username should start with one and only one "@" character - guard lastComponent.prefix(while: { $0 == "@" }).count == 1 else { - return - } - - self.currentTextTrigger = lastComponent - - var partialName = lastComponent - partialName.removeFirst() - self.items.send(self.suggestionItems.filter({ userSuggestion in let containedInUsername = userSuggestion.userId.lowercased().contains(partialName.lowercased()) let containedInDisplayName = (userSuggestion.displayName ?? "").lowercased().contains(partialName.lowercased()) diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift index 8eb69854f..42faeb6c7 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift @@ -33,7 +33,7 @@ protocol UserSuggestionServiceProtocol { var currentTextTrigger: String? { get } - func processTextMessage(_ textMessage: String) + func processTextMessage(_ textMessage: String?) } // MARK: Avatarable From 5e5d1cf118d9a79eec46653b978a305fcb9d86cb Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 11 Oct 2021 13:17:59 +0300 Subject: [PATCH 170/276] #1098 - Various small tweaks. --- .../Mock/MockUserSuggestionScreenState.swift | 8 ++++++- .../Service/UserSuggestionService.swift | 22 ++++++------------- .../View/UserSuggestionList.swift | 15 +++++++------ 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift index 6c1796d45..7897c608d 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift @@ -23,6 +23,8 @@ import SwiftUI enum MockUserSuggestionScreenState: MockScreenState, CaseIterable { case multipleResults + static private var members: [RoomMembersProviderMember]! + var screenType: Any.Type { MockUserSuggestionScreenState.self } @@ -46,7 +48,11 @@ enum MockUserSuggestionScreenState: MockScreenState, CaseIterable { @available(iOS 14.0, *) extension MockUserSuggestionScreenState: RoomMembersProviderProtocol { func fetchMembers(_ members: ([RoomMembersProviderMember]) -> Void) { - members(generateUsersWithCount(10)) + if Self.members == nil { + Self.members = generateUsersWithCount(10) + } + + members(Self.members) } private func generateUsersWithCount(_ count: UInt) -> [RoomMembersProviderMember] { diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift index e4f95c98e..fac8265ba 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift @@ -74,21 +74,13 @@ class UserSuggestionService: UserSuggestionServiceProtocol { // MARK: - UserSuggestionServiceProtocol func processTextMessage(_ textMessage: String?) { - self.items.send([]) - self.currentTextTriggerSubject.send(nil) - - guard let textMessage = textMessage, textMessage.count > 0 else { - return - } - - let components = textMessage.components(separatedBy: .whitespaces) - - guard let lastComponent = components.last else { - return - } - - // Partial username should start with one and only one "@" character - guard lastComponent.prefix(while: { $0 == "@" }).count == 1 else { + guard let textMessage = textMessage, + textMessage.count > 0, + let lastComponent = textMessage.components(separatedBy: .whitespaces).last, + lastComponent.prefix(while: { $0 == "@" }).count == 1 // Partial username should start with one and only one "@" character + else { + self.items.send([]) + self.currentTextTriggerSubject.send(nil) return } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift index 5693615ec..2e1d3c331 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift @@ -20,6 +20,11 @@ import SwiftUI @available(iOS 14.0, *) struct UserSuggestionList: View { + private struct Constants { + static let rowHeight: CGFloat = 60.0 + static let maxHeight: CGFloat = 300.0 + static let listItemPadding: CGFloat = 4.0 + } // MARK: - Properties @@ -30,9 +35,6 @@ struct UserSuggestionList: View { // MARK: Public @ObservedObject var viewModel: UserSuggestionViewModel.Context - // FIXME: This should be dynamic - let rowHeight: CGFloat = 60.0 - let maxHeight: CGFloat = 300.0 var body: some View { BackgroundView { @@ -45,13 +47,12 @@ struct UserSuggestionList: View { displayName: item.displayName, userId: item.id ) - .padding([.top, .bottom], 4.0) + .padding([.top, .bottom], Constants.listItemPadding) } } .listStyle(PlainListStyle()) - .environment(\.defaultMinListRowHeight, rowHeight) - .frame(height: min(maxHeight, rowHeight * CGFloat(viewModel.viewState.items.count))) -// .frame(maxHeight: maxHeight) + .environment(\.defaultMinListRowHeight, Constants.rowHeight) + .frame(height: min(Constants.maxHeight, Constants.rowHeight * CGFloat(viewModel.viewState.items.count))) .id(UUID()) // Rebuild the whole list on item changes. Fixes performance issues. } } From e54c888d93a183f882a6a259b78d43b3b8ef8acb Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 11 Oct 2021 16:56:34 +0300 Subject: [PATCH 171/276] #1098 - Switched to dynamically calculated row heights. --- .../Service/UserSuggestionService.swift | 2 +- .../View/UserSuggestionList.swift | 52 +++++++++++++------ 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift index fac8265ba..ad9de7e56 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift @@ -65,8 +65,8 @@ class UserSuggestionService: UserSuggestionServiceProtocol { self.roomMembersProvider = roomMembersProvider currentTextTriggerSubject - .removeDuplicates() .debounce(for: 0.5, scheduler: RunLoop.main) + .removeDuplicates() .sink { self.fetchAndFilterMembersForTextTrigger($0) } .store(in: &cancellables) } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift index 2e1d3c331..4dcbf0f28 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift @@ -21,9 +21,10 @@ import SwiftUI @available(iOS 14.0, *) struct UserSuggestionList: View { private struct Constants { - static let rowHeight: CGFloat = 60.0 - static let maxHeight: CGFloat = 300.0 static let listItemPadding: CGFloat = 4.0 + static let lineSpacing: CGFloat = 10.0 + static let maxHeight: CGFloat = 300.0 + static let maxVisibleRows = 4 } // MARK: - Properties @@ -31,31 +32,48 @@ struct UserSuggestionList: View { // MARK: Private @Environment(\.theme) private var theme: ThemeSwiftUI + @State private var prototypeListItemFrame: CGRect = .zero // MARK: Public @ObservedObject var viewModel: UserSuggestionViewModel.Context var body: some View { - BackgroundView { - List(viewModel.viewState.items) { item in - Button { - viewModel.send(viewAction: .selectedItem(item)) - } label: { - UserSuggestionListItem( - avatar: item.avatar, - displayName: item.displayName, - userId: item.id - ) - .padding([.top, .bottom], Constants.listItemPadding) + if viewModel.viewState.items.isEmpty { + EmptyView() + } else { + ZStack { + UserSuggestionListItem(avatar: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: "Prototype"), + displayName: "Prototype", + userId: "Prototype") + .background(ViewFrameReader(frame: $prototypeListItemFrame)) + .hidden() + BackgroundView { + List(viewModel.viewState.items) { item in + Button { + viewModel.send(viewAction: .selectedItem(item)) + } label: { + UserSuggestionListItem( + avatar: item.avatar, + displayName: item.displayName, + userId: item.id + ) + .padding([.top, .bottom], Constants.listItemPadding) + } + } + .listStyle(PlainListStyle()) + .frame(height: min(Constants.maxHeight, + min(contentHeightForRowCount(Constants.maxVisibleRows), + contentHeightForRowCount(viewModel.viewState.items.count)))) + .id(UUID()) // Rebuild the whole list on item changes. Fixes performance issues. } } - .listStyle(PlainListStyle()) - .environment(\.defaultMinListRowHeight, Constants.rowHeight) - .frame(height: min(Constants.maxHeight, Constants.rowHeight * CGFloat(viewModel.viewState.items.count))) - .id(UUID()) // Rebuild the whole list on item changes. Fixes performance issues. } } + + private func contentHeightForRowCount(_ count: Int) -> CGFloat { + (prototypeListItemFrame.height + (Constants.listItemPadding * 2) + Constants.lineSpacing) * CGFloat(count) + } } @available(iOS 14.0, *) From fddeb5e0ee3f7784bd0a6a5d1f5c49e36841cdbe Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 13 Oct 2021 09:18:44 +0300 Subject: [PATCH 172/276] #1098 - Added extra top padding on first list item. Fixed various merge problems. --- Riot/Modules/Room/RoomViewController.m | 41 +++++++++++-------- .../Views/InputToolbar/RoomInputToolbarView.m | 1 + .../View/UserSuggestionList.swift | 6 ++- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 0bb714f65..d32da8258 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -462,24 +462,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self registerURLPreviewNotifications]; [self setupActions]; - - [self setupUserSuggestionView]; -} - -- (void)setupUserSuggestionView -{ - UIViewController *suggestionsViewController = self.userSuggestionCoordinator.toPresentable; - [suggestionsViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO]; - - [self addChildViewController:suggestionsViewController]; - [self.userSuggestionContainerView addSubview:suggestionsViewController.view]; - - [NSLayoutConstraint activateConstraints:@[[suggestionsViewController.view.topAnchor constraintEqualToAnchor:self.userSuggestionContainerView.topAnchor], - [suggestionsViewController.view.leadingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.leadingAnchor], - [suggestionsViewController.view.trailingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.trailingAnchor], - [suggestionsViewController.view.bottomAnchor constraintEqualToAnchor:self.userSuggestionContainerView.bottomAnchor],]]; - - [suggestionsViewController didMoveToParentViewController:self]; } - (void)userInterfaceThemeDidChange @@ -1045,6 +1027,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; _userSuggestionCoordinator = [[UserSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager room:dataSource.room]; _userSuggestionCoordinator.delegate = self; + + [self setupUserSuggestionView]; } - (void)onRoomDataSourceReady @@ -2238,6 +2222,27 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } } +- (void)setupUserSuggestionView +{ + if(!self.isViewLoaded) { + MXLogError(@"Failed setting up user suggestions. View not loaded."); + return; + } + + UIViewController *suggestionsViewController = self.userSuggestionCoordinator.toPresentable; + [suggestionsViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO]; + + [self addChildViewController:suggestionsViewController]; + [self.userSuggestionContainerView addSubview:suggestionsViewController.view]; + + [NSLayoutConstraint activateConstraints:@[[suggestionsViewController.view.topAnchor constraintEqualToAnchor:self.userSuggestionContainerView.topAnchor], + [suggestionsViewController.view.leadingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.leadingAnchor], + [suggestionsViewController.view.trailingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.trailingAnchor], + [suggestionsViewController.view.bottomAnchor constraintEqualToAnchor:self.userSuggestionContainerView.bottomAnchor],]]; + + [suggestionsViewController didMoveToParentViewController:self]; +} + #pragma mark - Jitsi - (void)showJitsiCallWithWidget:(Widget*)widget diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index 758782f14..a8c9f57e7 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -157,6 +157,7 @@ const CGFloat kComposerContainerTrailingPadding = 12; self.textView.text = textMessage; [self updateUIWithTextMessage:textMessage animated:YES]; + [self textViewDidChange:self.textView]; } - (NSString *)textMessage diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift index 4dcbf0f28..a8c400a1f 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift @@ -21,6 +21,7 @@ import SwiftUI @available(iOS 14.0, *) struct UserSuggestionList: View { private struct Constants { + static let topPadding: CGFloat = 8.0 static let listItemPadding: CGFloat = 4.0 static let lineSpacing: CGFloat = 10.0 static let maxHeight: CGFloat = 300.0 @@ -58,7 +59,8 @@ struct UserSuggestionList: View { displayName: item.displayName, userId: item.id ) - .padding([.top, .bottom], Constants.listItemPadding) + .padding(.bottom, Constants.listItemPadding) + .padding(.top, (viewModel.viewState.items.first?.id == item.id ? Constants.listItemPadding + Constants.topPadding : Constants.listItemPadding)) } } .listStyle(PlainListStyle()) @@ -72,7 +74,7 @@ struct UserSuggestionList: View { } private func contentHeightForRowCount(_ count: Int) -> CGFloat { - (prototypeListItemFrame.height + (Constants.listItemPadding * 2) + Constants.lineSpacing) * CGFloat(count) + (prototypeListItemFrame.height + (Constants.listItemPadding * 2) + Constants.lineSpacing) * CGFloat(count) + Constants.topPadding } } From 1ad5bbf0e230cbc7f154aed2dbec6cb327f75288 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Wed, 13 Oct 2021 10:40:50 +0100 Subject: [PATCH 173/276] Update Xcode to 12.5 and use macOS 11. Fixes CI failing when using Swift 5.4 features. --- .github/workflows/ci-build.yml | 2 +- .github/workflows/ci-tests.yml | 2 +- changelog.d/pr-4998.build | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/pr-4998.build diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index d1cd1b3a9..c86de1d80 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -16,7 +16,7 @@ env: jobs: build: name: Build - runs-on: macos-latest + runs-on: macos-11 steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index eac0c885d..65846be15 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -16,7 +16,7 @@ env: jobs: tests: name: Tests - runs-on: macos-latest + runs-on: macos-11 steps: - uses: actions/checkout@v2 diff --git a/changelog.d/pr-4998.build b/changelog.d/pr-4998.build new file mode 100644 index 000000000..6b36ac81e --- /dev/null +++ b/changelog.d/pr-4998.build @@ -0,0 +1 @@ +Build: Update to Xcode 12.5 in the Fastfile and macOS 11 in the GitHub actions. \ No newline at end of file From 7cfe5590e17164bffdd9244509cf4e8b0d5a8b17 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 13 Oct 2021 12:51:15 +0100 Subject: [PATCH 174/276] Stop requesting URL previews if the feature has been disabled on the homeserver. --- .../URLPreviews/URLPreviewService.swift | 23 ++++++++++++++++++- .../Room/CellData/RoomBubbleCellData.m | 2 ++ changelog.d/5002.change | 1 + 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 changelog.d/5002.change diff --git a/Riot/Managers/URLPreviews/URLPreviewService.swift b/Riot/Managers/URLPreviews/URLPreviewService.swift index e731c27db..5e39058d0 100644 --- a/Riot/Managers/URLPreviews/URLPreviewService.swift +++ b/Riot/Managers/URLPreviews/URLPreviewService.swift @@ -15,6 +15,7 @@ // import Foundation +import AFNetworking enum URLPreviewServiceError: Error { case missingResponse @@ -74,7 +75,10 @@ class URLPreviewService: NSObject { success(previewData) } - }, failure: failure) + }, failure: { error in + self.checkForDisabledAPI(in: error) + failure(error) + }) } /// Removes any cached preview data that has expired. @@ -156,4 +160,21 @@ class URLPreviewService: NSObject { return components?.url ?? url } + + /// Checks an error returned from `MXRestClient` to see whether the previews API + /// has been disabled on the homeserver. If this is true, link detection will be disabled + /// to prevent further requests being made and stop any previews loaders being presented. + private func checkForDisabledAPI(in error: Error?) { + // The error will contain a 404 and no matrix error code. + guard + let error = error as NSError?, + error.userInfo[kMXErrorCodeKey] == nil, + let response = error.userInfo[AFNetworkingOperationFailingURLResponseErrorKey] as? HTTPURLResponse + else { return } + + if response.statusCode == 404 { + MXLog.debug("[URLPreviewService] Disabling link detection as homeserver does not support URL previews.") + MXKAppSettings.standard().enableBubbleComponentLinkDetection = false + } + } } diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index ba19a08fe..a71e6d72e 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -1117,6 +1117,8 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat }); } failure:^(NSError * _Nullable error) { + MXStrongifyAndReturnIfNil(self); + MXLogDebug(@"[RoomBubbleCellData] Failed to get url preview") // Remove the loading URLPreviewView, indicate that the layout needs refreshing and send a notification for refresh diff --git a/changelog.d/5002.change b/changelog.d/5002.change new file mode 100644 index 000000000..dee394802 --- /dev/null +++ b/changelog.d/5002.change @@ -0,0 +1 @@ +URL Previews: Stop requesting URL previews if the feature has been disabled on the homeserver. From d9e253c904d30a749e1cafa70c751a2ef6b119af Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 12 Oct 2021 17:18:29 +0100 Subject: [PATCH 175/276] Add tags to URLPreviewView and BubbleReactionsView so their highlight behaviour matches their bubble component. Fix bug setting text alpha. --- Riot/Modules/Room/DataSources/RoomDataSource.m | 2 ++ Riot/Utils/Tools.m | 2 +- changelog.d/4992.bugfix | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelog.d/4992.bugfix diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 7f94a8c2f..9817f503d 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -377,6 +377,7 @@ const CGFloat kTypingCellHeight = 24; urlPreviewView = [URLPreviewView instantiate]; urlPreviewView.preview = component.urlPreviewData; urlPreviewView.delegate = self; + urlPreviewView.tag = index; [temporaryViews addObject:urlPreviewView]; @@ -416,6 +417,7 @@ const CGFloat kTypingCellHeight = 24; reactionsView = [BubbleReactionsView new]; reactionsView.viewModel = bubbleReactionsViewModel; + reactionsView.tag = index; [reactionsView updateWithTheme:ThemeService.shared.theme]; bubbleReactionsViewModel.viewModelDelegate = self; diff --git a/Riot/Utils/Tools.m b/Riot/Utils/Tools.m index aefdea642..66fe38d0c 100644 --- a/Riot/Utils/Tools.m +++ b/Riot/Utils/Tools.m @@ -126,7 +126,7 @@ if (attrs[NSForegroundColorAttributeName]) { UIColor *color = attrs[NSForegroundColorAttributeName]; - color = [color colorWithAlphaComponent:0.2]; + color = [color colorWithAlphaComponent:alpha]; NSMutableDictionary *newAttrs = [NSMutableDictionary dictionaryWithDictionary:attrs]; newAttrs[NSForegroundColorAttributeName] = color; diff --git a/changelog.d/4992.bugfix b/changelog.d/4992.bugfix new file mode 100644 index 000000000..2a89ca027 --- /dev/null +++ b/changelog.d/4992.bugfix @@ -0,0 +1 @@ +Timeline: Selecting a message now correctly selects any reactions and URL previews too. \ No newline at end of file From 5da9f4646c9217d471c7d4f09ad1b05b16ee634e Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 13 Oct 2021 14:51:50 +0100 Subject: [PATCH 176/276] Use MXError and MXHTTPOperation where possible. --- Riot/Managers/URLPreviews/URLPreviewService.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Riot/Managers/URLPreviews/URLPreviewService.swift b/Riot/Managers/URLPreviews/URLPreviewService.swift index 5e39058d0..2f8d31791 100644 --- a/Riot/Managers/URLPreviews/URLPreviewService.swift +++ b/Riot/Managers/URLPreviews/URLPreviewService.swift @@ -165,11 +165,10 @@ class URLPreviewService: NSObject { /// has been disabled on the homeserver. If this is true, link detection will be disabled /// to prevent further requests being made and stop any previews loaders being presented. private func checkForDisabledAPI(in error: Error?) { - // The error will contain a 404 and no matrix error code. + // The error we're looking for is a generic 404 and not a matrix error. guard - let error = error as NSError?, - error.userInfo[kMXErrorCodeKey] == nil, - let response = error.userInfo[AFNetworkingOperationFailingURLResponseErrorKey] as? HTTPURLResponse + !MXError.isMXError(error), + let response = MXHTTPOperation.urlResponse(fromError: error) else { return } if response.statusCode == 404 { From 91fd7ddb64eee9be743023a91702075761138862 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 13 Oct 2021 16:01:19 +0100 Subject: [PATCH 177/276] Add observer to re-enable link detection when signing out of an account. --- .../Managers/URLPreviews/URLPreviewService.swift | 16 ++++++++++++++++ Riot/Modules/Application/AppCoordinator.swift | 2 ++ 2 files changed, 18 insertions(+) diff --git a/Riot/Managers/URLPreviews/URLPreviewService.swift b/Riot/Managers/URLPreviews/URLPreviewService.swift index 2f8d31791..3eb594e4e 100644 --- a/Riot/Managers/URLPreviews/URLPreviewService.swift +++ b/Riot/Managers/URLPreviews/URLPreviewService.swift @@ -33,6 +33,22 @@ class URLPreviewService: NSObject { /// A persistent store backed by Core Data to reduce network requests private let store = URLPreviewStore() + /// The observer that re-enables link detection on sign out, + private let resetLinkDetectionObserver: Any + + // MARK: - Setup + + override init() { + resetLinkDetectionObserver = NotificationCenter.default.addObserver(forName: .mxkAccountManagerDidRemoveAccount, object: nil, queue: .main) { _ in + MXKAppSettings.standard().enableBubbleComponentLinkDetection = true + } + + super.init() + } + + deinit { + NotificationCenter.default.removeObserver(resetLinkDetectionObserver) + } // MARK: - Public diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift index 32121a0f6..bed586ef9 100755 --- a/Riot/Modules/Application/AppCoordinator.swift +++ b/Riot/Modules/Application/AppCoordinator.swift @@ -82,6 +82,8 @@ final class AppCoordinator: NSObject, AppCoordinatorType { // Setup navigation router store _ = NavigationRouterStore.shared + // Setup URL preview service observers + _ = URLPreviewService.shared if BuildSettings.enableSideMenu { self.addSideMenu() From c8d8d079fb586670b31f9b620719d490f8cb97c3 Mon Sep 17 00:00:00 2001 From: Linerly Date: Wed, 13 Oct 2021 05:09:56 +0000 Subject: [PATCH 178/276] Translated using Weblate (Indonesian) Currently translated at 100.0% (6 of 6 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/id/ --- Riot/Assets/id.lproj/InfoPlist.strings | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Riot/Assets/id.lproj/InfoPlist.strings b/Riot/Assets/id.lproj/InfoPlist.strings index 8b1378917..e74ebc244 100644 --- a/Riot/Assets/id.lproj/InfoPlist.strings +++ b/Riot/Assets/id.lproj/InfoPlist.strings @@ -1 +1,9 @@ + +"NSContactsUsageDescription" = "Element akan menampilkan kontak Anda supaya Anda bisa mengundang mereka ke obrolan."; +// Permissions usage explanations +"NSCameraUsageDescription" = "Kamera digunakan untuk mengambil foto dan video, dan melakukan panggilan video."; +"NSFaceIDUsageDescription" = "Face ID digunakan untuk mengakses aplikasi Anda."; +"NSCalendarsUsageDescription" = "Lihat pertemuan yang sudah dijadwalkan di aplikasi."; +"NSMicrophoneUsageDescription" = "Element membutuhkan akses ke mikrofon Anda untuk melakukan dan menerima panggilan, mengambil video, dan merekam pesan suara."; +"NSPhotoLibraryUsageDescription" = "Galeri digunakan untuk mengirim foto dan video."; From ee2ed8a42d1555a8a1e5986218766ca912677e2d Mon Sep 17 00:00:00 2001 From: Linerly Date: Wed, 13 Oct 2021 05:19:34 +0000 Subject: [PATCH 179/276] Translated using Weblate (Indonesian) Currently translated at 100.0% (48 of 48 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/id/ --- Riot/Assets/id.lproj/Localizable.strings | 166 +++++++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/Riot/Assets/id.lproj/Localizable.strings b/Riot/Assets/id.lproj/Localizable.strings index 8b1378917..679a5cbb8 100644 --- a/Riot/Assets/id.lproj/Localizable.strings +++ b/Riot/Assets/id.lproj/Localizable.strings @@ -1 +1,167 @@ + + +/** Key verification **/ + +"KEY_VERIFICATION_REQUEST_FROM_USER" = "%@ ingin memverifikasi"; + +/* Group call from user, CallKit caller name */ +"GROUP_CALL_FROM_USER" = "%@ (Panggilan grup)"; + +/* A user added a Jitsi call to a room */ +"GROUP_CALL_STARTED" = "Panggilan grup dimulai"; + +/* Incoming named video conference invite from a specific person */ +"VIDEO_CONF_NAMED_FROM_USER" = "Panggilan video grup dari %@: '%@'"; + +/* Incoming named voice conference invite from a specific person */ +"VOICE_CONF_NAMED_FROM_USER" = "Panggilan grup dari %@: '%@'"; + +/* Incoming unnamed video conference invite from a specific person */ +"VIDEO_CONF_FROM_USER" = "Panggilan video grup dari %@"; + +/* Incoming unnamed voice conference invite from a specific person */ +"VOICE_CONF_FROM_USER" = "Panggilan grup dari %@"; + +/* Incoming one-to-one video call */ +"VIDEO_CALL_FROM_USER" = "Panggilan video dari %@"; + +/** Calls **/ + +/* Incoming one-to-one voice call */ +"VOICE_CALL_FROM_USER" = "Panggilan dari %@"; + +/* A user's membership has updated in an unknown way */ +"USER_MEMBERSHIP_UPDATED" = "%@ memperbarui profilnya"; + +/* A user has change their avatar */ +"USER_UPDATED_AVATAR" = "%@ mengubah avatarnya"; + +/* A user has change their name to a new name which we don't know */ +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ mengubah namanya"; + +/** Membership Updates **/ + +/* A user has change their name to a new name */ +"USER_UPDATED_DISPLAYNAME" = "%@ mengubah namanya ke %@"; + +/* A user has invited you to a named room */ +"USER_INVITE_TO_NAMED_ROOM" = "%@ mengundang Anda ke %@"; + +/* A user has invited you to an (unamed) group chat */ +"USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@ mengundang Anda ke obrolan grup"; + +/** Invites **/ + +/* A user has invited you to a chat */ +"USER_INVITE_TO_CHAT" = "%@ mengundang Anda untuk mengobrol"; + +/* A user has reacted to a message, but the reaction content is unknown */ +"GENERIC_REACTION_FROM_USER" = "%@ mengirim sebuah reaksi"; + +/** Reactions **/ + +/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ +"REACTION_FROM_USER" = "%@ mereaksi %@"; + +/* Look, stuff's happened, alright? Just open the app. */ +"MSGS_IN_TWO_PLUS_ROOMS" = "%@ pesan baru di %@, %@ dan %@ lainnya"; + +/* Multiple messages in two rooms */ +"MSGS_IN_TWO_ROOMS" = "%@ pesan baru di %@ dan %@"; + +/* Multiple unread messages from two plus people (ie. for 4+ people: 'others' replaces the third person) */ +"MSGS_FROM_TWO_PLUS_USERS" = "%@ pesan baru dari %@, %@ dan %@ lainnya"; + +/* Multiple unread messages from three people */ +"MSGS_FROM_THREE_USERS" = "%@ pesan baru dari %@, %@ dan %@"; + +/* Multiple unread messages from two people */ +"MSGS_FROM_TWO_USERS" = "%@ pesan baru dari %@ dan %@"; + +/* Multiple unread messages from a specific person, not referencing a room */ +"MSGS_FROM_USER" = "%@ pesan baru di %@"; + +/** Coalesced messages **/ + +/* Multiple unread messages in a room */ +"UNREAD_IN_ROOM" = "%@ pesan baru di %@"; + +/* New message with hidden content due to PIN enabled */ +"MESSAGE_PROTECTED" = "Pesan Baru"; + +/* New message indicator on a room */ +"MESSAGE_IN_X" = "Pesan di %@"; + +/* New message indicator from a DM */ +"MESSAGE_FROM_X" = "Pesan dari %@"; + +/** Notification messages **/ + +/* New message indicator on unknown room */ +"MESSAGE" = "Pesan"; + +/* Sticker from a specific person, not referencing a room. */ +"STICKER_FROM_USER" = "%@ mengirim sebuah stiker"; + +/** Single, end-to-end encrypted messages (ie. we don't know what they say) */ + +/* New message from a specific person, not referencing a room */ +"MSG_FROM_USER" = "%@ mengirim sebuah pesan"; + +/* New image message from a specific person in a named room. */ +"IMAGE_FROM_USER_IN_ROOM" = "%@ mengirim gambar %@ di %@"; + +/** Media Messages **/ + +/* New image message from a specific person, not referencing a room. */ +"PICTURE_FROM_USER" = "%@ mengirim sebuah gambar"; + +/* New video message from a specific person, not referencing a room. */ +"VIDEO_FROM_USER" = "%@ mengirim sebuah video"; + +/* New audio message from a specific person, not referencing a room. */ +"AUDIO_FROM_USER" = "%@ mengirim sebuah file audio %@"; + +/* New voice message from a specific person, not referencing a room. */ +"VOICE_MESSAGE_FROM_USER" = "%@ mengirim sebuah pesan suara"; + +/* New file message from a specific person, not referencing a room. */ +"FILE_FROM_USER" = "%@ mengirim sebuah file %@"; + +/* A single unread message in a room */ +"SINGLE_UNREAD_IN_ROOM" = "Anda menerima sebuah pesan di %@"; + +/* A single unread message */ +"SINGLE_UNREAD" = "Anda menerima sebuah pesan"; + +/* New action message from a specific person in a named room. */ +"ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; + +/* New message from a specific person in a named room. Content included. */ +"MSG_FROM_USER_IN_ROOM_WITH_CONTENT" = "%@ di %@: %@"; + +/* New action message from a specific person, not referencing a room. */ +"ACTION_FROM_USER" = "* %@ %@"; + +/** Single, unencrypted messages (where we can include the content */ + +/* New message from a specific person, not referencing a room. Content included. */ +"MSG_FROM_USER_WITH_CONTENT" = "%@: %@"; + +/* New message from a specific person in a named room */ +"MSG_FROM_USER_IN_ROOM" = "%@ mengirim di %@"; + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ membalas di %@"; + +/* New message reply from a specific person, not referencing a room. */ +"REPLY_FROM_USER_TITLE" = "%@ membalas"; + +/** Titles **/ + +/* Message title for a specific person in a named room */ +"MSG_FROM_USER_IN_ROOM_TITLE" = "%@ di %@"; +/** General **/ + +"NOTIFICATION" = "Notifikasi"; From b059b08d1007f075e11f19f2b833f258c695a914 Mon Sep 17 00:00:00 2001 From: Leonidas Shear Date: Tue, 12 Oct 2021 20:48:32 +0000 Subject: [PATCH 180/276] Translated using Weblate (Russian) Currently translated at 100.0% (6 of 6 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/ru/ --- Riot/Assets/ru.lproj/InfoPlist.strings | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Riot/Assets/ru.lproj/InfoPlist.strings b/Riot/Assets/ru.lproj/InfoPlist.strings index 5edbd7a0c..9608ff9a5 100644 --- a/Riot/Assets/ru.lproj/InfoPlist.strings +++ b/Riot/Assets/ru.lproj/InfoPlist.strings @@ -1,7 +1,7 @@ // Permissions usage explanations -"NSCameraUsageDescription" = "Камера используется для съемки фотографий и видеороликов, а также для видеозвонков."; -"NSPhotoLibraryUsageDescription" = "Галерея используется для отправки фотографий и видео."; +"NSCameraUsageDescription" = "Камера используется для съемки фото и видео, совершения видеозвонков."; +"NSPhotoLibraryUsageDescription" = "Галерея используется для отправки фото и видео."; "NSMicrophoneUsageDescription" = "Element необходим доступ к вашему микрофону, чтобы совершать и принимать звонки, снимать видео и записывать голосовые сообщения."; -"NSContactsUsageDescription" = "Чтобы обнаружить контакты, уже использующие Matrix, Element может отправлять адреса электронной почты и номера телефонов из адресной книги на выбранный вами сервер идентификации Matrix. Если поддерживается, то личные данные перед отправкой хэшируются - пожалуйста, ознакомьтесь с политикой конфиденциальности вашего сервера идентификации для получения более подробной информации."; -"NSCalendarsUsageDescription" = "Ознакомьтесь со своими запланированными встречами в приложении."; +"NSContactsUsageDescription" = "Element покажет ваши контакты, чтобы вы могли пригласить их в чат."; +"NSCalendarsUsageDescription" = "Просматривайте запланированные встречи в приложении."; "NSFaceIDUsageDescription" = "Face ID используется для доступа к вашему приложению."; From 6ce9a32ce34f5c3381bb0867fb992648be1ad353 Mon Sep 17 00:00:00 2001 From: manuroe Date: Thu, 14 Oct 2021 09:12:33 +0200 Subject: [PATCH 181/276] Prepare for new sprint --- Config/AppVersion.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 79d20fa80..6dab241e0 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.6.5 -CURRENT_PROJECT_VERSION = 1.6.5 +MARKETING_VERSION = 1.6.6 +CURRENT_PROJECT_VERSION = 1.6.6 From 0750fa55446b015f19e496c033ff59c8aafcb163 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 14 Oct 2021 12:02:46 +0300 Subject: [PATCH 182/276] Make properties optional --- Riot/Model/WellKnown/VectorWellKnown.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Model/WellKnown/VectorWellKnown.swift b/Riot/Model/WellKnown/VectorWellKnown.swift index 2a44c0c19..dd548d6b2 100644 --- a/Riot/Model/WellKnown/VectorWellKnown.swift +++ b/Riot/Model/WellKnown/VectorWellKnown.swift @@ -45,7 +45,7 @@ extension VectorWellKnown: Decodable { struct VectorWellKnownEncryptionConfiguration: Decodable { /// Indicate if E2EE is enabled by default - let isE2EEByDefaultEnabled: Bool + let isE2EEByDefaultEnabled: Bool? enum CodingKeys: String, CodingKey { case isE2EEByDefaultEnabled = "default" @@ -56,5 +56,5 @@ struct VectorWellKnownEncryptionConfiguration: Decodable { struct VectorWellKnownJitsiConfiguration: Decodable { /// Default Jitsi server - let preferredDomain: String + let preferredDomain: String? } From 4b8aae33ad86516f5dc3d4f2394643bcd65299dc Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 14 Oct 2021 12:03:27 +0300 Subject: [PATCH 183/276] Adapt to optional wellknown properties --- .../HomeserverConfigurationBuilder.swift | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift b/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift index f3cf06e13..a0eab4c13 100644 --- a/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift +++ b/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift @@ -41,20 +41,16 @@ final class HomeserverConfigurationBuilder: NSObject { } // Encryption configuration - if let vectorWellKnownEncryptionConfig = vectorWellKnownEncryptionConfiguration { - isE2EEByDefaultEnabled = vectorWellKnownEncryptionConfig.isE2EEByDefaultEnabled - } else { - // Enable E2EE by default when there is no value - isE2EEByDefaultEnabled = true - } + // Enable E2EE by default when there is no value + isE2EEByDefaultEnabled = vectorWellKnownEncryptionConfiguration?.isE2EEByDefaultEnabled ?? true // Jitsi configuration let jitsiServerURL: URL let hardcodedJitsiServerURL: URL = BuildSettings.jitsiServerUrl - if let vectorWellKnownJitsiConfig = vectorWellKnownJitsiConfiguration { - jitsiPreferredDomain = vectorWellKnownJitsiConfig.preferredDomain - jitsiServerURL = self.jitsiServerURL(from: jitsiPreferredDomain) ?? hardcodedJitsiServerURL + if let preferredDomain = vectorWellKnownJitsiConfiguration?.preferredDomain { + jitsiPreferredDomain = preferredDomain + jitsiServerURL = self.jitsiServerURL(from: preferredDomain) ?? hardcodedJitsiServerURL } else { guard let hardcodedJitsiDomain = hardcodedJitsiServerURL.host else { fatalError("[HomeserverConfigurationBuilder] Fail to get Jitsi domain from hardcoded Jitsi URL") From 1e720f80fb8d51d61c63b9e157015c2317646c3a Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 14 Oct 2021 12:04:23 +0300 Subject: [PATCH 184/276] Add changelog --- changelog.d/5008.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5008.change diff --git a/changelog.d/5008.change b/changelog.d/5008.change new file mode 100644 index 000000000..f400bb2af --- /dev/null +++ b/changelog.d/5008.change @@ -0,0 +1 @@ +VectorWellKnown: Make all properties optional. From e8f64cfdb42645aea0e1df5ff1fc3b5d34d849bf Mon Sep 17 00:00:00 2001 From: Ekaterina Gerasimova Date: Thu, 14 Oct 2021 13:52:38 +0100 Subject: [PATCH 185/276] Add issue triage automation Fixes #5012 Move new issues into incoming column and move X-Needs-Info into Need info column on the vector-im/element-android/projects/4 board Signed-off-by: Ekaterina Gerasimova --- .github/workflows/triage-incoming.yml | 15 +++++++++++++++ .github/workflows/triage-needs-info.yml | 16 ++++++++++++++++ changelog.d/5012.misc | 1 + 3 files changed, 32 insertions(+) create mode 100644 .github/workflows/triage-incoming.yml create mode 100644 .github/workflows/triage-needs-info.yml create mode 100644 changelog.d/5012.misc diff --git a/.github/workflows/triage-incoming.yml b/.github/workflows/triage-incoming.yml new file mode 100644 index 000000000..40d550741 --- /dev/null +++ b/.github/workflows/triage-incoming.yml @@ -0,0 +1,15 @@ +name: Move new issues onto Issue triage board + +on: + issues: + types: [opened] + +jobs: + automate-project-columns: + runs-on: ubuntu-latest + steps: + - uses: alex-page/github-project-automation-plus@v0.8.1 + with: + project: Issue triage + column: Incoming + repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/.github/workflows/triage-needs-info.yml b/.github/workflows/triage-needs-info.yml new file mode 100644 index 000000000..4a4a6a7c0 --- /dev/null +++ b/.github/workflows/triage-needs-info.yml @@ -0,0 +1,16 @@ +name: Move X-Needs-Info into Need info column in the Issue triage board + +on: + issues: + types: [labeled] + +jobs: + Move_Labeled_Issue_On_Project_Board: + runs-on: ubuntu-latest + steps: + - uses: konradpabjan/move-labeled-or-milestoned-issue@v2.0 + with: + action-token: ${{ secrets.GITHUB_TOKEN }} + project-url: "https://github.com/vector-im/element-ios/projects/12" + column-name: "Need info" + label-name: "X-Needs-Info" diff --git a/changelog.d/5012.misc b/changelog.d/5012.misc new file mode 100644 index 000000000..fa300a8a4 --- /dev/null +++ b/changelog.d/5012.misc @@ -0,0 +1 @@ +Move new issues into incoming column and move X-Needs-Info into Need info column on the issue triage board From c882045a8c39829eaef840aa1414e358d3044388 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 14 Oct 2021 13:07:18 +0100 Subject: [PATCH 186/276] Re-enable link detection in `clearStore` instead of via NotificationCenter. --- .../URLPreviews/URLPreviewService.swift | 20 +++---------------- Riot/Modules/Application/AppCoordinator.swift | 2 -- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/Riot/Managers/URLPreviews/URLPreviewService.swift b/Riot/Managers/URLPreviews/URLPreviewService.swift index 3eb594e4e..f45988b0b 100644 --- a/Riot/Managers/URLPreviews/URLPreviewService.swift +++ b/Riot/Managers/URLPreviews/URLPreviewService.swift @@ -33,22 +33,6 @@ class URLPreviewService: NSObject { /// A persistent store backed by Core Data to reduce network requests private let store = URLPreviewStore() - /// The observer that re-enables link detection on sign out, - private let resetLinkDetectionObserver: Any - - // MARK: - Setup - - override init() { - resetLinkDetectionObserver = NotificationCenter.default.addObserver(forName: .mxkAccountManagerDidRemoveAccount, object: nil, queue: .main) { _ in - MXKAppSettings.standard().enableBubbleComponentLinkDetection = true - } - - super.init() - } - - deinit { - NotificationCenter.default.removeObserver(resetLinkDetectionObserver) - } // MARK: - Public @@ -102,9 +86,11 @@ class URLPreviewService: NSObject { store.removeExpiredItems() } - /// Deletes all cached preview data and closed previews from the store. + /// Deletes all cached preview data and closed previews from the store, + /// re-enabling URL previews if they have been disabled by `checkForDisabledAPI`. func clearStore() { store.deleteAll() + MXKAppSettings.standard().enableBubbleComponentLinkDetection = true } /// Store the `eventId` and `roomId` of a closed preview. diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift index bed586ef9..32121a0f6 100755 --- a/Riot/Modules/Application/AppCoordinator.swift +++ b/Riot/Modules/Application/AppCoordinator.swift @@ -82,8 +82,6 @@ final class AppCoordinator: NSObject, AppCoordinatorType { // Setup navigation router store _ = NavigationRouterStore.shared - // Setup URL preview service observers - _ = URLPreviewService.shared if BuildSettings.enableSideMenu { self.addSideMenu() From 50d31ce2b3381d17799c06f84c30887f9cb9f43e Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Mon, 11 Oct 2021 10:39:47 +0000 Subject: [PATCH 187/276] Translated using Weblate (Ukrainian) Currently translated at 54.8% (729 of 1328 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index d91f1d708..a573557ce 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -139,7 +139,7 @@ "client_web_name" = "Element Web"; "client_ios_name" = "Element iOS"; "auth_login_single_sign_on" = "Увійти"; -"room_creation_invite_another_user" = "Пошук / запрошення за ID користувача, іменем або е-поштою"; +"room_creation_invite_another_user" = "ID користувача, ім'я або е-пошта"; "room_creation_error_invite_user_by_email_without_identity_server" = "Сервер ідентифікації не налаштовано, тому ви не можете додати учасника з е-поштою."; // Room recents "room_recents_directory_section" = "КАТАЛОГ КІМНАТ"; @@ -381,7 +381,7 @@ "secrets_recovery_with_passphrase_lost_passphrase_action_part3" = "."; "key_backup_recover_from_passphrase_lost_passphrase_action_part3" = "."; "settings_discovery_three_pids_management_information_part3" = "."; -"settings_contacts" = "ЛОКАЛЬНІ КОНТАКТИ"; +"settings_contacts" = "КОНТАКТИ ПРИСТРОЮ"; "settings_ignored_users" = "НЕХТУВАНІ КОРИСТУВАЧІ"; "settings_user_interface" = "КОРИСТУВАЦЬКИЙ ІНТЕРФЕЙС"; "settings_integrations" = "ІНТЕГРАЦІЇ"; From 59d3db751c06b6ce14fa4e0db26d412468d987cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Mon, 11 Oct 2021 22:10:03 +0000 Subject: [PATCH 188/276] Translated using Weblate (Estonian) Currently translated at 100.0% (1328 of 1328 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 38 +++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index da076b099..e33a6047c 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -691,10 +691,10 @@ "user_verification_sessions_list_user_trust_level_unknown_title" = "Teadmata olek"; "user_verification_sessions_list_information" = "Sõnumid selle kasutajaga selles jututoas on läbivalt krüptitud ning kolmandad osapooled ei saa neid lugeda."; "user_verification_sessions_list_table_title" = "Sessioonid"; -"user_verification_sessions_list_session_trusted" = "Usaldusväärsed"; -"user_verification_sessions_list_session_untrusted" = "Ei ole usaldusväärsed"; -"user_verification_session_details_trusted_title" = "Usaldusväärsed"; -"user_verification_session_details_untrusted_title" = "Ei ole usaldusväärsed"; +"user_verification_sessions_list_session_trusted" = "Usaldusväärne"; +"user_verification_sessions_list_session_untrusted" = "Ei ole usaldusväärne"; +"user_verification_session_details_trusted_title" = "Usaldusväärne"; +"user_verification_session_details_untrusted_title" = "Ei ole usaldusväärne"; // Chat creation "room_creation_title" = "Uus vestlus"; "room_creation_account" = "Kasutajakonto"; @@ -710,7 +710,7 @@ "room_creation_keep_private" = "Jäta privaatseks"; "room_creation_make_private" = "Muuda privaatseks"; "room_creation_wait_for_creation" = "Jututuba on just loomisel. Palun oota üks hetk."; -"room_creation_invite_another_user" = "Otsi või kutsu uut kasutajat tema kasutajatunnuse, nime või e-posti aadressi alusel"; +"room_creation_invite_another_user" = "Kasutajatunnus, nimi või e-posti aadress"; "room_creation_error_invite_user_by_email_without_identity_server" = "Ühtegi isikutuvastusserverit pole seadistatud ning sul ei ole võimalik lisada kasutajaid e-posti aadressi alusel."; "room_recents_low_priority_section" = "VÄHETÄHTIS"; "room_recents_server_notice_section" = "SÜSTEEMSED TEATED"; @@ -757,7 +757,7 @@ "settings_integrations" = "LÕIMINGUD"; "settings_user_interface" = "KASUTAJALIIDES"; "settings_ignored_users" = "EIRATUD KASUTAJAD"; -"settings_contacts" = "KONTAKTID SIIN SEADMES"; +"settings_contacts" = "KONTAKTID SEADMES"; "settings_advanced" = "KEERUKAMAD SEADISTUSED"; "settings_other" = "MUUD SEADISTUSED"; "settings_labs" = "KATSED"; @@ -916,7 +916,7 @@ "user_verification_start_information_part2" = " võrreldes selleks üheks korraks loodud koodi mõlemas seadmes."; "user_verification_start_waiting_partner" = "Ootan vastust kasutajalt %@…"; "user_verification_start_additional_information" = "Turvalisuse mõttes on oluline, et teed seda nii, et kas olete üheskoos või kasutate suhtluskanalit, mida mõlemad usaldate."; -"user_verification_sessions_list_user_trust_level_trusted_title" = "Usaldusväärsed"; +"user_verification_sessions_list_user_trust_level_trusted_title" = "Usaldusväärne"; "user_verification_session_details_information_trusted_current_user" = "Kuna sina oled selle sessiooni verifitseerinud, siis see sessioon on krüptitud sõnumite saatmiseks usaldusväärne:"; "user_verification_session_details_information_trusted_other_user_part1" = "See sessioon on krüptitud sõnumite saatmiseks usaldusväärne, sest "; "user_verification_session_details_information_trusted_other_user_part2" = " verifitseeris selle:"; @@ -1443,3 +1443,27 @@ "room_recents_suggested_rooms_section" = "SOOVITATUD JUTUTOAD"; "done" = "Valmis"; "open" = "Ava"; +"settings_contacts_enable_sync_description" = "Sellega võimaldad oma isikutuvastusserveril lubada sind leida."; + +// Service terms +"service_terms_modal_title_message" = "Jätkamiseks palun nõustu kasutustingimustega"; +"settings_phone_contacts" = "KONTAKTID NUTISEADMES"; +"settings_contacts_enable_sync" = "Otsi kontakte"; +"service_terms_modal_information_description_integration_manager" = "Lõiminguhalduri alusel saad lisada ja kasutada kolmandate osapoolte loodud lisavõimalusi."; +"service_terms_modal_information_description_identity_server" = "Isikutuvastusserver võimaldab sul telefoninumbri või e-posti aadressi alusel leida Matrix'i kasutajaid."; +"service_terms_modal_information_title_integration_manager" = "Lõiminguhaldur"; + +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "Isikutuvastusserver"; +"service_terms_modal_description_integration_manager" = "Sellega tekib sul võimalus kasutada roboteid, sõnumisildu, vidinaid ja kleepsupakke."; +"service_terms_modal_description_identity_server" = "Sellega annad teistele inimestele võimaluse sind leida, kui nende aadressiraamatus on sinu telefoninumber või e-posti aadress."; +"service_terms_modal_table_header_integration_manager" = "LÕIMINGUHALDURI TINGIMUSED"; +"service_terms_modal_table_header_identity_server" = "ISIKUTUVASTUSSERVERI TINGIMUSED"; +"service_terms_modal_footer" = "Selle võimaluse saad alati seadistustest välja lülitada."; +"find_your_contacts_identity_service_error" = "Ei õnnestu leida isikutuvastusserverit."; +"find_your_contacts_footer" = "Selle võimaluse saad alati seadistustest välja lülitada."; +"find_your_contacts_message" = "Las %@ näitab sulle kontakte ja nii saad tuttavatega kiiresti vestlema asuda."; +"find_your_contacts_button_title" = "Otsi kontakte"; +"find_your_contacts_title" = "Alusta kontaktide looendist"; +"contacts_address_book_permission_denied_alert_message" = "Palun luba seadistustest aadressiraamatu lugemine."; +"contacts_address_book_permission_denied_alert_title" = "Kontaktid pole kasutusel"; From 5a098ff16784bf2657ea8fc8db9a82767669ad0a Mon Sep 17 00:00:00 2001 From: random Date: Tue, 12 Oct 2021 10:20:01 +0000 Subject: [PATCH 189/276] Translated using Weblate (Italian) Currently translated at 100.0% (1328 of 1328 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ --- Riot/Assets/it.lproj/Vector.strings | 30 ++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index 70d0cad7a..aa161e9da 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -109,7 +109,7 @@ "room_creation_keep_private" = "Mantieni privata"; "room_creation_make_private" = "Rendi privata"; "room_creation_wait_for_creation" = "Una stanza è già in fase di creazione. Per favore attendi."; -"room_creation_invite_another_user" = "Cerca / invita per ID utente, nome o email"; +"room_creation_invite_another_user" = "ID utente, nome o email"; // Room recents "room_recents_directory_section" = "ELENCO STANZE"; "room_recents_favourites_section" = "PREFERITI"; @@ -295,7 +295,7 @@ "settings_calls_settings" = "CHIAMATE"; "settings_user_interface" = "INTERFACCIA UTENTE"; "settings_ignored_users" = "UTENTI IGNORATI"; -"settings_contacts" = "CONTATTI LOCALI"; +"settings_contacts" = "CONTATTI DEL DISPOSITIVO"; "settings_advanced" = "AVANZATE"; "settings_other" = "ALTRO"; "settings_labs" = "LABORATORIO"; @@ -892,7 +892,7 @@ "widget_menu_remove" = "Rimuovi per tutti"; "settings_integrations" = "INTEGRAZIONI"; "settings_integrations_allow_button" = "Gestisci le integrazioni"; -"settings_integrations_allow_description" = "Usa un Integration Manager (%@) per gestire bot, bridge, widget e pacchetti di sticker.\n\nGli Integration Manager possono ricevere dati di configurazione, modificare widget, mandare inviti alle stanze e modificare permessi a tuo nome."; +"settings_integrations_allow_description" = "Usa un gestore di integrazioni (%@) per gestire bot, bridge, widget e pacchetti di sticker.\n\nI gestori di integrazioni possono ricevere dati di configurazione, modificare widget, mandare inviti alle stanze e modificare permessi a tuo nome."; "widget_integration_manager_disabled" = "Devi attivare il gestore di integrazioni nelle impostazioni"; // Room widget permissions "room_widget_permission_title" = "Carica widget"; @@ -1477,3 +1477,27 @@ "room_recents_suggested_rooms_section" = "STANZE CONSIGLIATE"; "done" = "Fatto"; "open" = "Apri"; +"service_terms_modal_information_description_integration_manager" = "Un gestore di integrazioni ti permette di aggiungere funzioni da terze parti."; +"service_terms_modal_information_description_identity_server" = "Un server d'identità ti aiuta a trovare i tuoi contatti, cercando il loro numero di telefono o l'indirizzo email, per vedere se hanno già un account."; +"service_terms_modal_information_title_integration_manager" = "Gestore di integrazioni"; + +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "Server d'identità"; +"service_terms_modal_description_integration_manager" = "Ciò ti permetterà di usare bot, bridge, widget e pacchetti di sticker."; +"service_terms_modal_description_identity_server" = "Ciò aiuterà qualcuno a trovarti se ha il tuo numero di telefono o l'email salvati nei suoi contatti."; +"service_terms_modal_table_header_integration_manager" = "TERMINI DEL GESTORE DI INTEGRAZIONI"; +"service_terms_modal_table_header_identity_server" = "TERMINI DEL SERVER D'IDENTITÀ"; +"service_terms_modal_footer" = "Può essere disattivato in qualsiasi momento nelle impostazioni."; + +// Service terms +"service_terms_modal_title_message" = "Per continuare, accetta i termini e condizioni sottostanti"; +"settings_contacts_enable_sync_description" = "Verrà usato il tuo server d'identità per connetterti ai tuoi contatti e per aiutarli a trovarti."; +"settings_contacts_enable_sync" = "Trova i tuoi contatti"; +"settings_phone_contacts" = "CONTATTI DEL TELEFONO"; +"find_your_contacts_identity_service_error" = "Impossibile connettersi al server d'identità."; +"find_your_contacts_footer" = "Può essere disattivato in qualsiasi momento dalle impostazioni."; +"find_your_contacts_button_title" = "Trova i tuoi contatti"; +"find_your_contacts_message" = "Lascia che %@ mostri i tuoi contatti per poter iniziare subito a chattare con chi conosci già."; +"find_your_contacts_title" = "Inizia elencando i tuoi contatti"; +"contacts_address_book_permission_denied_alert_message" = "Per attivare i contatti, vai nelle impostazioni del tuo dispositivo."; +"contacts_address_book_permission_denied_alert_title" = "Contatti disattivati"; From 52bbae2c77a599af64c68d8aba5020c037707d8c Mon Sep 17 00:00:00 2001 From: sr093906 Date: Mon, 11 Oct 2021 10:45:33 +0000 Subject: [PATCH 190/276] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (1328 of 1328 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/zh_Hans/ --- Riot/Assets/zh_Hans.lproj/Vector.strings | 28 ++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index d03d533fb..12ee6b01a 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -93,7 +93,7 @@ "room_creation_keep_private" = "保持私密"; "room_creation_make_private" = "使其变成私密"; "room_creation_wait_for_creation" = "聊天室已创建。请稍候。"; -"room_creation_invite_another_user" = "通过用户 ID、名称或电子邮件进行搜索/邀请"; +"room_creation_invite_another_user" = "用户 ID、名称或电子邮件"; // Room recents "room_recents_directory_section" = "聊天室目录"; "room_recents_favourites_section" = "收藏夹"; @@ -245,7 +245,7 @@ "settings_user_settings" = "用户设置"; "settings_notifications_settings" = "通知设置"; "settings_ignored_users" = "已忽略用户"; -"settings_contacts" = "本地联系人"; +"settings_contacts" = "设备联系人"; "settings_advanced" = "高级"; "settings_other" = "其他"; "settings_labs" = "实验室"; @@ -1521,3 +1521,27 @@ "room_recents_suggested_rooms_section" = "建议的聊天室"; "done" = "完成"; "open" = "打开"; +"service_terms_modal_information_description_integration_manager" = "集成管理器允许您添加来自第三方的功能。"; +"service_terms_modal_information_description_identity_server" = "身份服务器通过查找电话号码或电子邮件地址帮助您找到联系人,看看他们是否已经有一个帐户。"; +"service_terms_modal_information_title_integration_manager" = "集成管理器"; + +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "身份服务器"; +"service_terms_modal_description_integration_manager" = "这将允许您使用机器人、桥、小部件和贴纸包。"; +"service_terms_modal_description_identity_server" = "如果有人在电话联系人中保存了你的电话号码或电子邮件,就可以找到你。"; +"service_terms_modal_table_header_integration_manager" = "集成管理器条款"; +"service_terms_modal_table_header_identity_server" = "身份服务器条款"; +"service_terms_modal_footer" = "你可以随时在设置中禁用它。"; + +// Service terms +"service_terms_modal_title_message" = "如需继续,请接受以下条款和条件"; +"settings_contacts_enable_sync_description" = "这将使用您的身份服务器连接您和您的联系人,并帮助他们找到您。"; +"settings_contacts_enable_sync" = "寻找联系人"; +"settings_phone_contacts" = "电话联系人"; +"find_your_contacts_identity_service_error" = "无法连接到身份服务器。"; +"find_your_contacts_footer" = "你可以随时从设置中禁用它。"; +"find_your_contacts_button_title" = "寻找联系人"; +"find_your_contacts_message" = "让 %@ 显示你的联系人,以便你可以快速开始和你最了解的人聊天。"; +"find_your_contacts_title" = "从列出你的联系人开始"; +"contacts_address_book_permission_denied_alert_message" = "要启用联系人,请转到设备设置。"; +"contacts_address_book_permission_denied_alert_title" = "联系人被禁用"; From f0a3867ae706b6a10c99741be3b2d9e8e05f2d6b Mon Sep 17 00:00:00 2001 From: Leonidas Shear Date: Tue, 12 Oct 2021 20:56:04 +0000 Subject: [PATCH 191/276] Translated using Weblate (Russian) Currently translated at 100.0% (1328 of 1328 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ru/ --- Riot/Assets/ru.lproj/Vector.strings | 38 +++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/Riot/Assets/ru.lproj/Vector.strings b/Riot/Assets/ru.lproj/Vector.strings index e46438442..ad004e9e0 100644 --- a/Riot/Assets/ru.lproj/Vector.strings +++ b/Riot/Assets/ru.lproj/Vector.strings @@ -95,7 +95,7 @@ "room_creation_keep_private" = "Оставить приватным"; "room_creation_make_private" = "Сделать приватным"; "room_creation_wait_for_creation" = "Комната уже создана. Подождите."; -"room_creation_invite_another_user" = "Поиск / приглашение по идентификатору пользователя, имени или адресу электронной почты"; +"room_creation_invite_another_user" = "Идентификатор пользователя, имя или электронная почта"; // Room recents "room_recents_directory_section" = "КАТАЛОГ КОМНАТ"; "room_recents_favourites_section" = "ИЗБРАННЫЕ"; @@ -237,7 +237,7 @@ "settings_notifications_settings" = "НАСТРОЙКИ УВЕДОМЛЕНИЙ"; "settings_user_interface" = "ПОЛЬЗОВАТЕЛЬСКИЙ ИНТЕРФЕЙС"; "settings_ignored_users" = "ИГНОРИРУЕМЫЕ ПОЛЬЗОВАТЕЛИ"; -"settings_contacts" = "ЛОКАЛЬНЫЕ КОНТАКТЫ"; +"settings_contacts" = "КОНТАКТЫ УСТРОЙСТВА"; "settings_advanced" = "ДОПОЛНИТЕЛЬНО"; "settings_other" = "ДРУГИЕ"; "settings_labs" = "ЛАБОРАТОРИЯ"; @@ -1420,20 +1420,20 @@ "event_formatter_call_has_ended_with_time" = "Вызов закончен • %@"; "settings_notifications" = "УВЕДОМЛЕНИЯ"; "version_check_modal_action_title_deprecated" = "Узнайте, как"; -"version_check_modal_subtitle_deprecated" = "Мы работали над улучшением Element для более быстрой и совершенной работы. К сожалению, ваша текущая версия iOS не совместима с некоторыми из этих исправлений и больше не будет поддерживаться.\nМы советуем вам обновить свою операционную систему, чтобы использовать Element в полной мере."; +"version_check_modal_subtitle_deprecated" = "Мы работали над улучшением %@ для более быстрой и совершенной работы. К сожалению, ваша текущая версия iOS не совместима с некоторыми из этих исправлений и больше не будет поддерживаться.\nМы советуем вам обновить свою операционную систему, чтобы использовать %@ в полной мере."; "version_check_modal_title_deprecated" = "Мы больше не поддерживаем iOS %@"; "version_check_modal_action_title_supported" = "Понятно"; -"version_check_modal_subtitle_supported" = "Мы работали над улучшением Element для более быстрой и совершенной работы. К сожалению, ваша текущая версия iOS не совместима с некоторыми из этих исправлений и больше не будет поддерживаться.\nМы советуем вам обновить свою операционную систему, чтобы использовать Element в полной мере."; +"version_check_modal_subtitle_supported" = "Мы работали над улучшением %@ для более быстрой и совершенной работы. К сожалению, ваша текущая версия iOS не совместима с некоторыми из этих исправлений и больше не будет поддерживаться.\nМы советуем вам обновить свою операционную систему, чтобы использовать %@ в полной мере."; "version_check_modal_title_supported" = "Мы прекращаем поддержку iOS %@"; -"version_check_banner_subtitle_deprecated" = "Мы больше не поддерживаем Element на iOS %@. Чтобы продолжать использовать Element в полной мере, мы советуем вам обновить версию iOS."; +"version_check_banner_subtitle_deprecated" = "Мы больше не поддерживаем %@ на iOS %@. Чтобы продолжать использовать %@ в полной мере, мы советуем вам обновить версию iOS."; "version_check_banner_title_deprecated" = "Мы больше не поддерживаем iOS %@"; -"version_check_banner_subtitle_supported" = "В ближайшее время мы прекращаем поддержку Element на iOS %@. Чтобы продолжать использовать Element в полной мере, мы советуем вам обновить вашу версию iOS."; +"version_check_banner_subtitle_supported" = "В ближайшее время мы прекращаем поддержку %@ на iOS %@. Чтобы продолжать использовать %@ в полной мере, мы советуем вам обновить версию iOS."; // Mark: - Version check "version_check_banner_title_supported" = "Мы прекращаем поддержку iOS %@"; "settings_show_url_previews_description" = "Предварительный просмотр будет осуществляться только в незашифрованных комнатах."; -"settings_show_url_previews" = "Показывать встроенные предварительные просмотры URL-адресов"; +"settings_show_url_previews" = "Предварительный просмотр веб-сайта"; "settings_mentions_and_keywords_encryption_notice" = "Вы не будете получать уведомления об упоминаниях и ключевых словах в зашифрованных комнатах на мобильных устройствах."; "settings_new_keyword" = "Добавить новое ключевое слово"; "settings_your_keywords" = "Ваши ключевые слова"; @@ -1487,3 +1487,27 @@ "settings_links" = "ССЫЛКИ"; "done" = "Готово"; "open" = "Открыть"; +"service_terms_modal_information_description_integration_manager" = "Менеджер интеграции позволяет добавлять функции от сторонних производителей."; +"service_terms_modal_information_description_identity_server" = "Сервер идентификации помогает найти контакты, просматривая номер телефона или адрес электронной почты, чтобы узнать, есть ли у них учетная запись."; +"service_terms_modal_information_title_integration_manager" = "Менеджер интеграции"; + +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "Сервер идентификации"; +"service_terms_modal_description_integration_manager" = "Это позволит вам использовать ботов, мосты, виджеты и пакеты стикеров."; +"service_terms_modal_description_identity_server" = "Это позволит найти вас, если у них есть ваш номер телефона или электронная почта, сохраненные в контактах на его телефоне."; +"service_terms_modal_table_header_integration_manager" = "УСЛОВИЯ МЕНЕДЖЕРА ИНТЕГРАЦИИ"; +"service_terms_modal_table_header_identity_server" = "УСЛОВИЯ СЕРВЕРА ИДЕНТИФИКАЦИИ"; +"service_terms_modal_footer" = "Это можно отключить в любое время в настройках."; + +// Service terms +"service_terms_modal_title_message" = "Чтобы продолжить, примите приведенные ниже положения и условия"; +"settings_contacts_enable_sync_description" = "Это позволит использовать ваш сервер идентификации для связи с вашими контактами и поможет им найти вас."; +"settings_contacts_enable_sync" = "Поиск контактов"; +"settings_phone_contacts" = "КОНТАКТЫ ТЕЛЕФОНА"; +"find_your_contacts_identity_service_error" = "Невозможно подключиться к серверу идентификации."; +"find_your_contacts_footer" = "Это можно отключить в настройках в любое время."; +"find_your_contacts_button_title" = "Поиск контактов"; +"find_your_contacts_message" = "Пусть %@ покажет ваши контакты, так вы сразу начнёте общаться с теми, кого вы хорошо знаете."; +"find_your_contacts_title" = "Начните с составления списка контактов"; +"contacts_address_book_permission_denied_alert_message" = "Для включения контактов, перейдите в настройки устройства."; +"contacts_address_book_permission_denied_alert_title" = "Контакты отключены"; From af80adb7ed3aec32ea0723e49d3a428955484670 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Wed, 13 Oct 2021 09:20:01 +0000 Subject: [PATCH 192/276] Translated using Weblate (Hungarian) Currently translated at 100.0% (1328 of 1328 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ --- Riot/Assets/hu.lproj/Vector.strings | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index 7f3966bae..ed2f5b3a5 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -105,7 +105,7 @@ "room_creation_keep_private" = "Maradjon zárt"; "room_creation_make_private" = "Legyen nyilvános"; "room_creation_wait_for_creation" = "A szoba elkészítése folyamatban van. Kérlek várj."; -"room_creation_invite_another_user" = "Keresés / meghívás felhasználói azonosítás, név vagy e-mail cím alapján"; +"room_creation_invite_another_user" = "Felhasználói azonosító, név vagy e-mail"; // Room recents "room_recents_directory_section" = "SZOBA KÖNYVTÁR"; "room_recents_favourites_section" = "KEDVENCEK"; @@ -281,7 +281,7 @@ "settings_calls_settings" = "HÍVÁSOK"; "settings_user_interface" = "FELHASZNÁLÓI FELÜLET"; "settings_ignored_users" = "FIGYELMEN KÍVÜL HAGYOTT FELHASZNÁLÓK"; -"settings_contacts" = "HELYI KAPCSOLATOK"; +"settings_contacts" = "ESZKÖZ NÉVJEGYZÉK"; "settings_advanced" = "HALADÓ"; "settings_other" = "MÁS"; "settings_labs" = "LABOR"; @@ -1506,3 +1506,27 @@ "room_recents_suggested_rooms_section" = "JAVASOLT SZOBÁK"; "done" = "Kész"; "open" = "Megnyitás"; +"find_your_contacts_title" = "Kezdjük a kapcsolataid felsorolásával"; +"service_terms_modal_information_description_integration_manager" = "Az integrációs menedzser lehetővé teszi harmadik féltől származó lehetőségek használatát."; +"service_terms_modal_information_description_identity_server" = "Az azonosítási szerver lehetővé teszi, hogy megtaláld a kapcsolataidat azzal, hogy megnézi regisztráltak-e már az ő telefonszámaival vagy e-mail címeivel."; +"service_terms_modal_information_title_integration_manager" = "Integrációs Menedzser"; + +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "Azonosító szerver"; +"service_terms_modal_description_integration_manager" = "Ez lehetővé teszi, hogy botokat, hidakat, kisalkalmazásokat vagy matricákat használj."; +"service_terms_modal_description_identity_server" = "Ez lehetővé teszi másnak, akinek a telefonjába el van mentve a telefonszámod vagy e-mail címed, hogy megtaláljon."; +"service_terms_modal_table_header_integration_manager" = "INTEGRÁCIÓS MENEDZSER FELTÉTELEK"; +"service_terms_modal_table_header_identity_server" = "AZONOSÍTÁSI SZERVER FELTÉTELEK"; +"settings_contacts_enable_sync" = "Kapcsolatok megkeresése"; +"find_your_contacts_button_title" = "Kapcsolatok megkeresése"; +"find_your_contacts_message" = "Engedd meg, hogy %@ megjelenítse a névjegyzéket, hogy gyorsan elkezdhess beszélgetni azokkal akiket a legjobban ismersz."; +"service_terms_modal_footer" = "Bármikor letiltható a beállításokban."; + +// Service terms +"service_terms_modal_title_message" = "A folytatáshoz fogadd el a felhasználási feltételeket alább"; +"settings_contacts_enable_sync_description" = "Az azonosítási szerveredet fogja használni, hogy összekössön az ismerőseiddel és megtalálhassanak."; +"settings_phone_contacts" = "TELEFON NÉVJEGYZÉK"; +"find_your_contacts_identity_service_error" = "Az azonosítási szerverhez nem sikerült csatlakozni."; +"find_your_contacts_footer" = "Bármikor letiltható a beállításokban."; +"contacts_address_book_permission_denied_alert_message" = "A névjegyzék engedélyezéséhez lépj be az eszköz beállításokba."; +"contacts_address_book_permission_denied_alert_title" = "Névjegyzék letiltva"; From c03bfeb62f0f39adf70c5fb4eaf7fea24272e1d7 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Mon, 11 Oct 2021 10:23:26 +0000 Subject: [PATCH 193/276] Translated using Weblate (Albanian) Currently translated at 99.6% (1323 of 1328 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/ --- Riot/Assets/sq.lproj/Vector.strings | 38 ++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index f1b72a758..221b56ec4 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -220,7 +220,7 @@ "settings_calls_settings" = "THIRRJE"; "settings_user_interface" = "NDËRFAQE PËRDORUESI"; "settings_ignored_users" = "PËRDORUES TË SHPËRFILLUR"; -"settings_contacts" = "KONTAKTE VENDORE"; +"settings_contacts" = "KONTAKTE PAJISJEJE"; "settings_advanced" = "TË MËTEJSHME"; "settings_other" = "TË TJERA"; "settings_devices" = "SESIONE"; @@ -397,7 +397,7 @@ "auth_use_server_options" = "Përdor mundësi vetjake shërbyesi (e përparuar)"; "auth_add_email_and_phone_warning" = "Regjistrimi me email dhe me numër telefoni njëherazi nuk mbulohet ende, deri sa të ketë API. Do të merret parasysh vetëm numri i telefonit. Email-in tuaj mund ta shtoni te profili juaj, te rregullimet."; "room_creation_appearance_picture" = "Foto fjalosjeje (në daçi)"; -"room_creation_invite_another_user" = "Kërkoni / ftoni sipas ID-je Përdoruesi, Emri ose email-i"; +"room_creation_invite_another_user" = "ID Përdoruesi, emër ose email"; "room_recents_favourites_section" = "TË PARAPALQYERA"; "room_recents_server_notice_section" = "SINJALIZIME SISTEMI"; "room_recents_join_room_title" = "Hyni në një dhomë"; @@ -854,7 +854,7 @@ "settings_add_3pid_invalid_password_message" = "Kredenciale të pavlefshme"; "settings_devices_description" = "Emri publik i një sesioni është i dukshëm për persona me të cilët komunikoni"; "settings_discovery_no_identity_server" = "S’po përdorni ndonjë shërbyes identitetesh. Që të jeni i zbulueshëm nga kontakte ekzistuese që njihni, shtoni një të tillë."; -"settings_discovery_terms_not_signed" = "Pajtohuni me Kushtet e Shërbimit të Shërbyesit të Identiteteve që t’i lejoni vetes të jeni i zbulueshëm përmes adrese email ose numri telefoni."; +"settings_discovery_terms_not_signed" = "Pajtohuni me Kushtet e Shërbimit të shërbyesit të identiteteve (%@), që t’i lejoni vetes të jeni i zbulueshëm përmes adrese email ose numri telefoni."; "settings_discovery_three_pids_management_information_part1" = "Administroni cilat adresa email ose numra telefonash mund të përdorin përdoruesit e tjerë për t’ju zbuluar dhe ftuar në dhoma. Shtoni ose hiqni prej kësaj liste adresa email ose numra telefonash "; "settings_discovery_three_pids_management_information_part2" = "Rregullime Përdoruesi"; "settings_discovery_three_pids_management_information_part3" = "."; @@ -871,7 +871,7 @@ "settings_identity_server_no_is" = "S’ka të formësuar shërbyes identitetesh"; "settings_identity_server_no_is_description" = "S’po përdorni ndonjë shërbyes identitetesh. Që të zbuloni dhe të jeni i zbulueshëm nga kontakte ekzistuese që njihni, shtoni një më sipër."; // Identity server settings -"identity_server_settings_title" = "Shërbyes Identitetesh"; +"identity_server_settings_title" = "Shërbyes identitetesh"; "identity_server_settings_description" = "Po përdorni %@ që të zbuloni dhe të jeni i zbulueshëm nga kontakte ekzistuese që dini."; "identity_server_settings_no_is_description" = "S’po përdorni ndonjë shërbyes identitetesh. Që të zbuloni dhe të jeni i zbulueshëm nga kontakte ekzistuese, shtoni një më sipër."; "identity_server_settings_place_holder" = "Jepni një shërbyes identitetesh"; @@ -909,12 +909,12 @@ "accessibility_checkbox_label" = "kutizë"; "settings_integrations" = "INTEGRIME"; "settings_integrations_allow_button" = "Administroni integrime"; -"settings_integrations_allow_description" = "Përdorni një Përgjegjës Integrimesh (%@) që të administroni robotë, ura, widget-e dhe paketa ngjitësish.\n\nPërgjegjësit e Integrimeve marrin të dhëna formësimi dhe mund të ndryshojnë widget-e, të dërgojnë ftesa për në dhoma dhe të caktojnë shkallë pushteti në emrin tuaj."; +"settings_integrations_allow_description" = "Përdorni një përgjegjës integrimesh (%@) që të administroni robotë, ura, widget-e dhe paketa ngjitësish.\n\nPërgjegjësit e integrimeve marrin të dhëna formësimi dhe mund të ndryshojnë widget-e, të dërgojnë ftesa për në dhoma dhe të caktojnë shkallë pushteti në emrin tuaj."; "widget_menu_refresh" = "Rifreskoje"; "widget_menu_open_outside" = "Hape në shfletues"; "widget_menu_revoke_permission" = "Shfuqizo hyrje për mua"; "widget_menu_remove" = "Hiqe për këdo"; -"widget_integration_manager_disabled" = "Lypset të aktivizoni Përgjegjës Integrimesh te rregullimet"; +"widget_integration_manager_disabled" = "Lypset të aktivizoni përgjegjës integrimesh te rregullimet"; "widget_picker_manage_integrations" = "Administroni integrime…"; // Room widget permissions "room_widget_permission_title" = "Ngarko Widget"; @@ -1492,3 +1492,29 @@ "settings_links" = "LIDHJE"; "room_recents_suggested_rooms_section" = "DHOMA TË SUGJERUARA"; "done" = "U bë"; +"spaces_home_space_title" = "Kreu"; +"service_terms_modal_information_description_integration_manager" = "Një përgjegjës integrimesh ju lejon të shtoni veçori prej palësh të treta."; +"service_terms_modal_information_description_identity_server" = "Një shërbyes identitetesh ju ndihmon të gjeni kontaktet tuaja, duke kërkuar numrat e telefonave ose adresat email të tyre, për të parë nëse kanë tashmë një llogari."; +"service_terms_modal_information_title_integration_manager" = "Përgjegjës Integrimesh"; + +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "Shërbyes Identitetesh"; +"service_terms_modal_description_integration_manager" = "Kjo do t’ju lejojë të përdorni robotë, ura, widget-e dhe paketa ngjitësish."; +"service_terms_modal_description_identity_server" = "Kjo do t’i lejojë dikujt t’ju gjejë, nëse ka ruajtur në kontaktet e telefonit të vet numrin tuaj të telefonit ose email-in tuaj."; +"service_terms_modal_table_header_integration_manager" = "KUSHTE PËRGJEGJËSI INTEGRIMESH"; +"service_terms_modal_table_header_identity_server" = "KUSHTE SHËRBYESI IDENTITETESH"; +"service_terms_modal_footer" = "Kjo mund të çaktivizohet kurdo që nga rregullimet."; + +// Service terms +"service_terms_modal_title_message" = "Që të vazhdohet, pranoni termat dhe kushtet më poshtë"; +"security_settings_secure_backup_reset" = "Riktheji te parazgjedhjet"; +"settings_contacts_enable_sync_description" = "Kjo do të përdorë shërbyesin tuaj të identiteteve për t’ju lidhur me kontaktet tuaja dhe për t’i ndihmuar ata t’ju gjejnë."; +"settings_contacts_enable_sync" = "Gjeni kontaktet tuaja"; +"settings_phone_contacts" = "KONTAKTE TELEFONI"; +"find_your_contacts_identity_service_error" = "S’arrihet të lidhet me shërbyesin e identiteteve."; +"find_your_contacts_footer" = "Kjo mund të çaktivizohet kurdo që nga rregullimet."; +"find_your_contacts_button_title" = "Gjeni kontakte tuajt"; +"find_your_contacts_message" = "Lejojeni %@ të shfaqë kontaktet tuaja, që kështu të mund të filloni shpejt e shpejt të bisedoni me ata që ju njohin më mirë."; +"find_your_contacts_title" = "Fillojani duke shfaqur kontaktet tuaja"; +"contacts_address_book_permission_denied_alert_message" = "Që të aktivizoni kontakte, kaloni te rregullimet e pajisjes tua."; +"contacts_address_book_permission_denied_alert_title" = "Kontaktet u çaktivizuan"; From 2d61cc9669a844e0749b6bb4df73c0aaac68300d Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Mon, 11 Oct 2021 10:27:09 +0000 Subject: [PATCH 194/276] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1328 of 1328 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ --- Riot/Assets/pt_BR.lproj/Vector.strings | 28 ++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index 77bfd450a..1039b5013 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -100,7 +100,7 @@ "room_creation_keep_private" = "Manter privado"; "room_creation_make_private" = "Fazer privado"; "room_creation_wait_for_creation" = "Uma sala já está sendo criada. Por favor espere."; -"room_creation_invite_another_user" = "Buscar / convidar por ID de usuária(o), Nome ou email"; +"room_creation_invite_another_user" = "ID de usuária(o), nome ou email"; "room_recents_favourites_section" = "FAVORITOS"; "room_recents_people_section" = "PESSOAS"; "room_recents_conversations_section" = "SALAS"; @@ -288,7 +288,7 @@ "settings_calls_settings" = "CHAMADAS"; "settings_user_interface" = "INTERFACE DE USUÁRIA(O)"; "settings_ignored_users" = "USUÁRIAS(OS) IGNORADAS(OS)"; -"settings_contacts" = "CONTATOS LOCAIS"; +"settings_contacts" = "CONTATOS DE DISPOSITIVO"; "settings_advanced" = "AVANÇADAS"; "settings_other" = "OUTRAS"; "settings_labs" = "LABS"; @@ -1474,3 +1474,27 @@ "room_recents_suggested_rooms_section" = "SALAS SUGERIDAS"; "done" = "Feito"; "open" = "Abrir"; +"service_terms_modal_information_description_integration_manager" = "Um gerenciador de integrações deixa você adicionar funcionalidades de terceiros."; +"service_terms_modal_information_description_identity_server" = "Um servidor de identidade ajuda você a encontrar seus contatos, ao buscar o número de telefone ou endereço de email deles, para ver se eles já têm uma conta."; +"service_terms_modal_information_title_integration_manager" = "Gerenciador de Integrações"; + +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "Servidor de Identidade"; +"service_terms_modal_description_integration_manager" = "Isto vai permitir você usar bots, bridges, widgets e pacotes de stickers."; +"service_terms_modal_description_identity_server" = "Isto vai permitir alguém encontrar você se ela/ele tem seu número de telefone ou email salvo nos contatos de telefone dela/dele."; +"service_terms_modal_table_header_integration_manager" = "TERMOS DE GERENCIADOR DE INTEGRAÇÕES"; +"service_terms_modal_table_header_identity_server" = "TERMOS DE SERVIDOR DE IDENTIDADE"; +"service_terms_modal_footer" = "Isto pode ser desabilitado a qualquer hora em configurações."; + +// Service terms +"service_terms_modal_title_message" = "Para continuar, aceite os termos e condições abaixo"; +"settings_contacts_enable_sync_description" = "Isto vai usar seu servidor de identidade para conectar você com seus contatos, e ajudá-los a encontrar você."; +"settings_contacts_enable_sync" = "Encontre seus contatos"; +"settings_phone_contacts" = "CONTATOS DE TELEFONE"; +"find_your_contacts_identity_service_error" = "Incapaz de se conectar ao servidor de identidade."; +"find_your_contacts_footer" = "Isto pode ser desabilitado a qualquer hora a partir de configurações."; +"find_your_contacts_button_title" = "Encontre seus contatos"; +"find_your_contacts_message" = "Deixe %@ mostrar seus contatos para que você possa rapidamente começar a fazer chat com aquelas(es) que você conhece melhor."; +"find_your_contacts_title" = "Comece por listar seus contatos"; +"contacts_address_book_permission_denied_alert_message" = "Para habilitar contatos, vá para as configurações de seu dispositivo."; +"contacts_address_book_permission_denied_alert_title" = "Contatos desabilitados"; From a260ad294e04d73384dd20779396f781877b2638 Mon Sep 17 00:00:00 2001 From: Linerly Date: Thu, 14 Oct 2021 01:50:00 +0000 Subject: [PATCH 195/276] Translated using Weblate (Indonesian) Currently translated at 7.0% (94 of 1328 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index f61661288..b5f872e82 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -35,7 +35,7 @@ "close" = "Tutup"; "sending" = "Mengirim"; "send_to" = "Kirim ke %@"; -"rename" = "Ubah nama"; +"rename" = "Ubah Nama"; "later" = "Nanti"; "active_call_details" = "Panggilan Aktif (%@)"; "active_call" = "Panggilan Aktif"; @@ -47,21 +47,21 @@ "decline" = "Tolak"; "join" = "Bergabung"; "save" = "Simpan"; -"cancel" = "Batal"; +"cancel" = "Batalkan"; "off" = "Nonaktif"; "on" = "Aktif"; -"retry" = "Coba lagi"; +"retry" = "Coba Lagi"; "invite" = "Undang"; "remove" = "Hapus"; "leave" = "Tinggalkan"; "start" = "Mulai"; "create" = "Buat"; -"continue" = "Lanjut"; +"continue" = "Lanjutkan"; "back" = "Kembali"; "next" = "Selanjutnya"; // Actions -"view" = "Lihat"; +"view" = "Tampilkan"; "warning" = "Peringatan"; "title_groups" = "Komunitas-komunitas"; "title_rooms" = "Ruangan-ruangan"; @@ -75,7 +75,7 @@ "auth_email_not_found" = "Gagal mengirim surel: Alamat email ini tidak ditemukan"; "auth_forgot_password_error_no_configured_identity_server" = "Tidak ada server identitas yang dikonfigurasikan: tambahkan satu untuk mengatur ulang kata sandi Anda."; "auth_forgot_password" = "Lupa kata sandi?"; -"auth_username_in_use" = "Nama pengguna sudah dipakai"; +"auth_username_in_use" = "Nama pengguna telah dipakai"; "auth_password_dont_match" = "Kata sandi tidak cocok"; "auth_untrusted_id_server" = "Server identitas tidak dipercaya"; "auth_phone_is_required" = "Tidak ada server identitas yang dikonfigurasi sehingga Anda tidak dapat menambahkan nomor telepon untuk mengatur ulang kata sandi Anda di masa depan."; From 45566f872d8df6e73d1f671c2fb8d3cab4ef72d0 Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Fri, 15 Oct 2021 23:41:22 +0000 Subject: [PATCH 196/276] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (48 of 48 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/pt_BR/ --- Riot/Assets/pt_BR.lproj/Localizable.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/pt_BR.lproj/Localizable.strings b/Riot/Assets/pt_BR.lproj/Localizable.strings index aed9c682c..165ee183e 100644 --- a/Riot/Assets/pt_BR.lproj/Localizable.strings +++ b/Riot/Assets/pt_BR.lproj/Localizable.strings @@ -33,7 +33,7 @@ /* Look, stuff's happened, alright? Just open the app. */ "MSGS_IN_TWO_PLUS_ROOMS" = "%@ novas mensagens em %@, %@ e outras"; /* A user has invited you to a chat */ -"USER_INVITE_TO_CHAT" = "%@ tem conviado você para fazer chat"; +"USER_INVITE_TO_CHAT" = "%@ tem conviado você para conversar"; /* A user has invited you to an (unamed) group chat */ "USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@ tem convidado você para um chat de grupo"; /* A user has invited you to a named room */ From ea8ba12ef31aa77af450109637deae8c5da9b758 Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Fri, 15 Oct 2021 23:38:56 +0000 Subject: [PATCH 197/276] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (6 of 6 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/pt_BR/ --- Riot/Assets/pt_BR.lproj/InfoPlist.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/pt_BR.lproj/InfoPlist.strings b/Riot/Assets/pt_BR.lproj/InfoPlist.strings index 6449e513c..bb44cdb98 100644 --- a/Riot/Assets/pt_BR.lproj/InfoPlist.strings +++ b/Riot/Assets/pt_BR.lproj/InfoPlist.strings @@ -2,6 +2,6 @@ "NSCameraUsageDescription" = "A câmera é usada para tirar fotos e vídeos, fazer chamadas de vídeo."; "NSPhotoLibraryUsageDescription" = "A biblioteca de fotos é usada para enviar fotos e vídeos."; "NSMicrophoneUsageDescription" = "Element precisa acessar seu microfone para fazer e receber chamadas, tirar vídeos, e gravar mensagens de voz."; -"NSContactsUsageDescription" = "Para descobrir contatos já usando Matrix, Element pode enviar endereços de email e números de telefone em seu livro de endereços para seu servidor de identidade Matrix escolhido. Onde suportado, dados pessoais são hashados antes do envio - por favor cheque a política de privacidade de seu servidor de identidade para mais detalhes."; +"NSContactsUsageDescription" = "Element vai mostrar seus contatos para que você possa convidá-los a conversar."; "NSCalendarsUsageDescription" = "Ver suas reuniões agendadas no app."; "NSFaceIDUsageDescription" = "Face ID é usada para acessar seu app."; From e2959406e3d15a5c89d614c7792dd7e675407950 Mon Sep 17 00:00:00 2001 From: artevaeckt Date: Fri, 15 Oct 2021 12:46:51 +0000 Subject: [PATCH 198/276] Translated using Weblate (German) Currently translated at 97.9% (1301 of 1328 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 244995f29..7dd14568c 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -1483,3 +1483,4 @@ "space_tag" = "Space"; "open" = "Öffnen"; "settings_links" = "LINKS"; +"find_your_contacts_footer" = "Dies kann jederzeit in den Einstellungen deaktiviert werden."; From 36ecef0b345723891cd7acdc762e861e1f0117aa Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Fri, 15 Oct 2021 23:41:07 +0000 Subject: [PATCH 199/276] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1328 of 1328 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ --- Riot/Assets/pt_BR.lproj/Vector.strings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index 1039b5013..77ab52883 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -556,7 +556,7 @@ "rerequest_keys_alert_message" = "Por favor lance %@ num outro dispositivo que possa decriptar a mensagem para que ele possa enviar as chaves para esta sessão."; // String for App Store "store_short_description" = "Chat/VoIP descentralizado e seguro"; -"store_full_description" = "Element is um novo tipo de mensageiro e app de colaboração que:\n\n1. Põe você em controle para preservar sua privacidade\n2. Permite você se comunicar com qualquer pessoa na rede Matrix, e até além ao integrar-se com apps como Slack\n3. Protege você de publicidade, datamining, backdoors e jardins murados\n4. Assegura você através de encriptação ponta-a-pontam com assinatura cruzada para verificar ouras(os)\n\nElement é completamente diferente de outros apps de mensageria e colaboração porque ele é descentralizado e open source.\n\nElement permite você auto-hospedar - ou escolher um host - para que você tenha privacidade, propriedade e controle de seus dados e conversas. Ele dá a você acesso a uma rede aberta; então você não está simplesmente preso falando com outras(os) usuárias(os) Element somente. E ele é muito seguro.\n\nElement é capaz de fazer tudo isso porque ele opera em Matrix - o padrão para comunicação aberta e descentralizada.\n\nElement põe você em controle ao permitir você escolher quem hospeda suas conversas. De seu app Element, você pode escolher hospedar de diferentes maneiras:\n\n1. Pegar uma conta grátis no servidor público matrix.org\n2. Auto-hospedar sua conta ao rodar um servidor em seu próprio hardware\n3. Registrar-se para uma conta num servidor personalizado ao simplesmente assinar a plataforma de hospedagem Element Matrix Services\n\nPorquê escolher Element?\n\nTENHA POSSE DE SEUS DADOS: Você decide onde manter seus dados e mensagens. Você os possui e controla, não alguma MEGACORP que mina seus dados ou dá acesso a terceiros.\n\nMENSAGERIA E COLABORAÇÃO ABERTOS: Você pode fazer chat com qualquer outra pessoa na rede Matrix, caso ela esteja usando Element ou um outro app Matrix, e mesmo se ela estiver usando um sistema de mensageria diferente, do tipo de Slack, IRC ou XMPP.\n\nSUPER-SEGURO: Encriptação ponta-a-ponta real (somente aquelas/es na conversa podem decriptar mensagens), e assinatura cruzada para verificar os dispositivos de participantes de conversa.\n\nCOMUNICAÇÃO COMPLETA: Mensageria, chamadas de voz e vídeo, compartilhamento de arquivo, compartilhamento de tela e um monte de integrações, bots e widgets. Construa salas, comunidades, fique em contato e tenha as coisas feitas.\n\nEM TODO LUGAR ONDE VOCÊ ESTEJA: Fique em contato onde quer que você esteja com histórico de mensagem sincronizado por todos os dispositivos e na web em https://element.io/app."; +"store_full_description" = "Element is um novo tipo de mensageiro e app de colaboração que:\n\n1. Põe você em controle para preservar sua privacidade\n2. Permite você se comunicar com qualquer pessoa na rede Matrix, e até além ao integrar-se com apps como Slack\n3. Protege você de publicidade, datamining, backdoors e jardins murados\n4. Assegura você através de encriptação ponta-a-pontam com assinatura cruzada para verificar ouras(os)\n\nElement é completamente diferente de outros apps de mensageria e colaboração porque ele é descentralizado e open source.\n\nElement permite você auto-hospedar - ou escolher um host - para que você tenha privacidade, propriedade e controle de seus dados e conversas. Ele dá a você acesso a uma rede aberta; então você não está simplesmente preso falando com outras(os) usuárias(os) Element somente. E ele é muito seguro.\n\nElement é capaz de fazer tudo isso porque ele opera em Matrix - o padrão para comunicação aberta e descentralizada.\n\nElement põe você em controle ao permitir você escolher quem hospeda suas conversas. De seu app Element, você pode escolher hospedar de diferentes maneiras:\n\n1. Pegar uma conta grátis no servidor público matrix.org\n2. Auto-hospedar sua conta ao rodar um servidor em seu próprio hardware\n3. Registrar-se para uma conta num servidor personalizado ao simplesmente assinar a plataforma de hospedagem Element Matrix Services\n\nPorquê escolher Element?\n\nTENHA POSSE DE SEUS DADOS: Você decide onde manter seus dados e mensagens. Você os possui e controla, não alguma MEGACORP que mina seus dados ou dá acesso a terceiros.\n\nMENSAGERIA E COLABORAÇÃO ABERTOS: Você pode conversar com qualquer outra pessoa na rede Matrix, caso ela esteja usando Element ou um outro app Matrix, e mesmo se ela estiver usando um sistema de mensageria diferente, do tipo de Slack, IRC ou XMPP.\n\nSUPER-SEGURO: Encriptação ponta-a-ponta real (somente aquelas/es na conversa podem decriptar mensagens), e assinatura cruzada para verificar os dispositivos de participantes de conversa.\n\nCOMUNICAÇÃO COMPLETA: Mensageria, chamadas de voz e vídeo, compartilhamento de arquivo, compartilhamento de tela e um monte de integrações, bots e widgets. Construa salas, comunidades, fique em contato e tenha as coisas feitas.\n\nEM TODO LUGAR ONDE VOCÊ ESTEJA: Fique em contato onde quer que você esteja com histórico de mensagem sincronizado por todos os dispositivos e na web em https://element.io/app."; "auth_login_single_sign_on" = "Fazer Sign In"; "auth_autodiscover_invalid_response" = "Resposta de descoberta de servidorcasa inválida"; "room_message_unable_open_link_error_message" = "Incapaz de abrir o link."; @@ -1494,7 +1494,7 @@ "find_your_contacts_identity_service_error" = "Incapaz de se conectar ao servidor de identidade."; "find_your_contacts_footer" = "Isto pode ser desabilitado a qualquer hora a partir de configurações."; "find_your_contacts_button_title" = "Encontre seus contatos"; -"find_your_contacts_message" = "Deixe %@ mostrar seus contatos para que você possa rapidamente começar a fazer chat com aquelas(es) que você conhece melhor."; +"find_your_contacts_message" = "Deixe %@ mostrar seus contatos para que você possa rapidamente começar a conversar com aqueles que você conhece melhor."; "find_your_contacts_title" = "Comece por listar seus contatos"; "contacts_address_book_permission_denied_alert_message" = "Para habilitar contatos, vá para as configurações de seu dispositivo."; "contacts_address_book_permission_denied_alert_title" = "Contatos desabilitados"; From 0a724d439c2de6ff9d325b91e207479abf46af93 Mon Sep 17 00:00:00 2001 From: Linerly Date: Sat, 16 Oct 2021 12:51:20 +0000 Subject: [PATCH 200/276] Translated using Weblate (Indonesian) Currently translated at 7.1% (95 of 1328 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index b5f872e82..6ee262df3 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -102,6 +102,10 @@ "joined" = "Bergabung"; "collapse" = "tutup"; "store_promotional_text" = "Aplikasi perpesanan dan kolaborasi yang menjaga privasi, pada jaringan terbuka. Terdesentralisasi untuk Anda kendali. Tidak ada penambangan data, tidak ada backdoor dan tidak ada akses pihak ketiga."; -"store_full_description" = "Element adalah aplikasi messenger dan kolaborasi tipe baru yang:\n\n1. Menempatkan Anda dalam kendali untuk mempertahankan privasi Anda\n2. Memungkinkan Anda berkomunikasi dengan siapa pun di jaringan Matrix, dan bahkan di luar dengan mengintegrasikan dengan aplikasi seperti Slack\n3. Melindungi Anda dari iklan, menambangan data, backdoor, dan taman berdinding\n4. Mengamankan Anda melalui enkripsi ujung-ke-ujung, dengan penandatanganan-silang untuk memverifikasi orang lain\n\nElement benar-benar berbeda dari aplikasi perpesanan dan kolaborasi lain karena terdesentralisasi dan sumber terbuka.\n\nElement memungkinkan Anda host sendiri - atau memilih host - sehingga Anda memiliki privasi, kepemilikan, dan kontrol data dan percakapan Anda. Ini memberi Anda akses ke jaringan terbuka; jadi Anda tidak hanya terjebak berbicara dengan pengguna Element. Itu sangat aman.\n\nElement dapat melakukan semua ini karena beroperasi pada Matrix - standar untuk komunikasi terdesentralisasi terbuka.\n\nElement menempatkan Anda dalam kendali dengan membiarkan Anda memilih siapa yang meng-host percakapan Anda. Dari aplikasi Element, Anda dapat memilih untuk meng-host dengan cara yang berbeda:\n\n1. Dapatkan akun gratis pada server publik matrix.org\n2. Host sendiri akun Anda dengan menjalankan server pada perangkat keras Anda sendiri\n3. Mendaftar untuk akun di server khusus dengan hanya berlangganan platform hosting Element Matrix Services\n\nMengapa memilih Element?\n\nMILIKI DATA ANDA: Anda memutuskan di mana harus menyimpan data dan pesan Anda. Anda memilikinya dan mengendalikannya, bukan perusahaan besar yang menambang data Anda atau memberikan akses ke pihak ketiga.\n\nPESAN DAN KOLABORASI TERBUKA: Anda dapat mengobrol dengan orang lain di jaringan Matrix, jika mereka menggunakan Element atau aplikasi Matrix lain, dan bahkan jika mereka menggunakan sistem perpesanan seperti Slack, IRC atau XMPP.\n\nSUPER-AMAN: Enkripsi ujung-ke-ujung (hanya mereka yang dalam percakapan dapat mendekripsi pesan), dan penandatanganan-silang untuk memverifikasi perangkat peserta percakapan.\n\nKOMUNIKASI LENGKAP: Pesan, panggilan suara dan video, berbagi file, berbagi layar dan banyak integrasi, bot dan widget. Buat ruangan, komunitas, tetap terhubung dan selesaikan hal-hal.\n\nDI MANA PUN ANDA BERADA: Tetap berkomunikasi di mana pun Anda berada dengan riwayat pesan yang sepenuhnya disinkronkan di semua perangkat Anda dan di web di https://element.io/app."; +"store_full_description" = "Element adalah aplikasi messenger dan kolaborasi tipe baru yang:\n\n1. Menempatkan Anda dalam kendali untuk mempertahankan privasi Anda\n2. Memungkinkan Anda berkomunikasi dengan siapa pun di jaringan Matrix, dan bahkan di luar dengan mengintegrasikan dengan aplikasi seperti Slack\n3. Melindungi Anda dari iklan, menambangan data, backdoor, dan taman berdinding\n4. Mengamankan Anda melalui enkripsi ujung-ke-ujung, dengan penandatanganan-silang untuk memverifikasi orang lain\n\nElement benar-benar berbeda dari aplikasi perpesanan dan kolaborasi lain karena terdesentralisasi dan sumber terbuka.\n\nElement memungkinkan Anda host sendiri - atau memilih host - sehingga Anda memiliki privasi, kepemilikan, dan kontrol data dan percakapan Anda. Ini memberi Anda akses ke jaringan terbuka; jadi Anda tidak hanya terjebak berbicara dengan pengguna Element. Itu sangat aman.\n\nElement dapat melakukan semua ini karena beroperasi pada Matrix - standar untuk komunikasi terdesentralisasi terbuka.\n\nElement menempatkan Anda dalam kendali dengan membiarkan Anda memilih siapa yang meng-host percakapan Anda. Dari aplikasi Element, Anda dapat memilih untuk meng-host dengan cara yang berbeda:\n\n1. Dapatkan akun gratis pada server publik matrix.org\n2. Host sendiri akun Anda dengan menjalankan server pada perangkat keras Anda sendiri\n3. Mendaftar untuk akun di server khusus dengan hanya berlangganan platform hosting Element Matrix Services\n\nMengapa memilih Element?\n\nMILIKI DATA ANDA: Anda memutuskan di mana harus menyimpan data dan pesan Anda. Anda memilikinya dan mengendalikannya, bukan perusahaan besar yang menambang data Anda atau memberikan akses ke pihak ketiga.\n\nPESAN DAN KOLABORASI TERBUKA: Anda dapat mengobrol dengan orang lain di jaringan Matrix, jika mereka menggunakan Element atau aplikasi Matrix lain, dan bahkan jika mereka menggunakan sistem perpesanan seperti Slack, IRC atau XMPP.\n\nSUPER-AMAN: Enkripsi ujung-ke-ujung (hanya mereka yang dalam percakapan dapat mendekripsi pesan), dan penandatanganan-silang untuk memverifikasi perangkat peserta percakapan.\n\nKOMUNIKASI LENGKAP: Pesan, panggilan suara dan video, berbagi file, berbagi layar dan banyak integrasi, bot dan widget. Buat ruangan, komunitas, tetap terhubung dan selesaikan hal-hal.\n\nDI MANA PUN ANDA BERADA: Tetap berkomunikasi di mana pun Anda berada dengan riwayat pesan yang sepenuhnya disinkronkan di semua perangkat Anda dan di web di https://app.element.io/."; // String for App Store "store_short_description" = "Obrolan/VoIP terdesentralisasi aman"; + + +// Room Details +"room_details_title" = "Detail Ruangan"; From 5ae64a8b9bd8d6002d45fa3469bab1a348f7ceed Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Mon, 18 Oct 2021 15:01:11 +0200 Subject: [PATCH 201/276] [Spaces] M10.4.1 Home space data filtering #4570 - update after review --- .../Spaces/SpaceMenu/SpaceMenuCell.swift | 2 +- .../SpaceMenu/SpaceMenuListItemViewData.swift | 14 +++++++-- .../SpaceMenu/SpaceMenuListViewCell.swift | 2 +- .../Spaces/SpaceMenu/SpaceMenuPresenter.swift | 11 ++++--- .../SpaceMenu/SpaceMenuSwitchViewCell.swift | 12 +------- .../SpaceMenu/SpaceMenuViewController.swift | 2 +- .../Spaces/SpaceMenu/SpaceMenuViewModel.swift | 30 +++++++------------ .../SpaceMenu/SpaceMenuViewModelType.swift | 2 +- 8 files changed, 31 insertions(+), 44 deletions(-) diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuCell.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuCell.swift index 2447ae918..d7a3d1fe0 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuCell.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuCell.swift @@ -17,5 +17,5 @@ import Foundation protocol SpaceMenuCell: Themable { - func fill(with viewData: SpaceMenuListItemViewData) + func update(with viewData: SpaceMenuListItemViewData) } diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift index 18a1f29bd..bae8bf13f 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift @@ -16,6 +16,14 @@ import Foundation +/// Possible action ID related to a `SpaceMenuListViewCell` view data +enum SpaceMenuListItemActionId { + case showAllRoomsInHomeSpace + case exploreSpaceMembers + case exploreSpaceRooms + case leaveSpace +} + /// Style of the `SpaceMenuListViewCell` enum SpaceMenuListItemStyle { case normal @@ -30,11 +38,11 @@ protocol SpaceMenuListItemViewDataDelegate: AnyObject { /// `SpaceMenuListViewCell` view data class SpaceMenuListItemViewData { - let actionId: String + let actionId: SpaceMenuListItemActionId let style: SpaceMenuListItemStyle let title: String? let icon: UIImage? - + /// Any value related to the type of data (e.g. `Bool` for `boolean` style, `nil` for `normal` and `destructive` style) var value: Any? { didSet { delegate?.spaceMenuItemValueDidChange(self) @@ -42,7 +50,7 @@ class SpaceMenuListItemViewData { } weak var delegate: SpaceMenuListItemViewDataDelegate? - init(actionId: String, style: SpaceMenuListItemStyle, title: String?, icon: UIImage?, value: Any?) { + init(actionId: SpaceMenuListItemActionId, style: SpaceMenuListItemStyle, title: String?, icon: UIImage?, value: Any?) { self.actionId = actionId self.style = style self.title = title diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.swift index 86bfe8805..bc8243a9a 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.swift @@ -49,7 +49,7 @@ class SpaceMenuListViewCell: UITableViewCell, SpaceMenuCell, NibReusable { // MARK: - Public - func fill(with viewData: SpaceMenuListItemViewData) { + func update(with viewData: SpaceMenuListItemViewData) { self.iconView.image = viewData.icon self.titleLabel.text = viewData.title diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift index 697c65b43..dfc755675 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift @@ -101,13 +101,12 @@ extension SpaceMenuPresenter: SpaceMenuModelViewModelCoordinatorDelegate { self.dismiss(animated: true, completion: nil) } - func spaceMenuViewModel(_ viewModel: SpaceMenuViewModelType, didSelectItemWithId itemId: String) { - let actionId = SpaceMenuViewModel.ActionId(rawValue: itemId) - switch actionId { - case .leave: break - case .members: + func spaceMenuViewModel(_ viewModel: SpaceMenuViewModelType, didSelectItemWithId itemId: SpaceMenuListItemActionId) { + switch itemId { + case .leaveSpace: break + case .exploreSpaceMembers: self.delegate?.spaceMenuPresenter(self, didCompleteWith: .exploreMembers, forSpaceWithId: self.spaceId, with: self.session) - case .rooms: + case .exploreSpaceRooms: self.delegate?.spaceMenuPresenter(self, didCompleteWith: .exploreRooms, forSpaceWithId: self.spaceId, with: self.session) default: MXLog.error("[SpaceMenuPresenter] spaceListViewModel didSelectItemWithId: invalid itemId \(itemId)") diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.swift index 3887f668f..2d50e9df1 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.swift @@ -49,20 +49,10 @@ class SpaceMenuSwitchViewCell: UITableViewCell, SpaceMenuCell, NibReusable { // MARK: - Public - func fill(with viewData: SpaceMenuListItemViewData) { + func update(with viewData: SpaceMenuListItemViewData) { self.titleLabel.text = viewData.title self.switchView.isOn = (viewData.value as? Bool) ?? false - guard let theme = self.theme else { - return - } - - if viewData.style == .destructive { - self.titleLabel.textColor = theme.colors.alert - } else { - self.titleLabel.textColor = theme.colors.primaryContent - } - viewData.delegate = self } diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift index 83ab85bdf..32bc9a76f 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift @@ -253,7 +253,7 @@ extension SpaceMenuViewController: UITableViewDataSource { if let cell = cell as? SpaceMenuCell { cell.update(theme: self.theme) - cell.fill(with: viewData) + cell.update(with: viewData) } return cell diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift index 4d14eed4d..1150ff155 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift @@ -19,24 +19,15 @@ import Foundation /// View model used by `SpaceMenuViewController` class SpaceMenuViewModel: SpaceMenuViewModelType { - // MARK: - Enum - - enum ActionId: String { - case showAllRoomsInHome = "showAllRoomsInHome" - case members = "members" - case rooms = "rooms" - case leave = "leave" - } - // MARK: - Properties weak var coordinatorDelegate: SpaceMenuModelViewModelCoordinatorDelegate? weak var viewDelegate: SpaceMenuViewModelViewDelegate? private let spaceMenuItems: [SpaceMenuListItemViewData] = [ - SpaceMenuListItemViewData(actionId: ActionId.members.rawValue, style: .normal, title: VectorL10n.roomDetailsPeople, icon: Asset.Images.spaceMenuMembers.image, value: nil), - SpaceMenuListItemViewData(actionId: ActionId.rooms.rawValue, style: .normal, title: VectorL10n.spacesExploreRooms, icon: Asset.Images.spaceMenuRooms.image, value: nil), - SpaceMenuListItemViewData(actionId: ActionId.leave.rawValue, style: .destructive, title: VectorL10n.leave, icon: Asset.Images.spaceMenuLeave.image, value: nil) + SpaceMenuListItemViewData(actionId: .exploreSpaceMembers, style: .normal, title: VectorL10n.roomDetailsPeople, icon: Asset.Images.spaceMenuMembers.image, value: nil), + SpaceMenuListItemViewData(actionId: .exploreSpaceRooms, style: .normal, title: VectorL10n.spacesExploreRooms, icon: Asset.Images.spaceMenuRooms.image, value: nil), + SpaceMenuListItemViewData(actionId: .leaveSpace, style: .destructive, title: VectorL10n.leave, icon: Asset.Images.spaceMenuLeave.image, value: nil) ] var menuItems: [SpaceMenuListItemViewData] = [] @@ -54,7 +45,7 @@ class SpaceMenuViewModel: SpaceMenuViewModelType { self.menuItems = spaceMenuItems } else { self.menuItems = [ - SpaceMenuListItemViewData(actionId: ActionId.showAllRoomsInHome.rawValue, style: .boolean, title: VectorL10n.spaceHomeShowAllRooms, icon: nil, value: MXKAppSettings.standard().isShowAllRoomsInHomeEnabled) + SpaceMenuListItemViewData(actionId: .showAllRoomsInHomeSpace, style: .boolean, title: VectorL10n.spaceHomeShowAllRooms, icon: nil, value: MXKAppSettings.standard().showAllRoomsInHomeSpace) ] } } @@ -76,17 +67,16 @@ class SpaceMenuViewModel: SpaceMenuViewModelType { // MARK: - Private - private func processAction(with actionStringId: String, at indexPath: IndexPath) { - let actionId = ActionId(rawValue: actionStringId) + private func processAction(with actionId: SpaceMenuListItemActionId, at indexPath: IndexPath) { switch actionId { - case .showAllRoomsInHome: - MXKAppSettings.standard().isShowAllRoomsInHomeEnabled = !MXKAppSettings.standard().isShowAllRoomsInHomeEnabled - self.menuItems[indexPath.row].value = MXKAppSettings.standard().isShowAllRoomsInHomeEnabled + case .showAllRoomsInHomeSpace: + MXKAppSettings.standard().showAllRoomsInHomeSpace = !MXKAppSettings.standard().showAllRoomsInHomeSpace + self.menuItems[indexPath.row].value = MXKAppSettings.standard().showAllRoomsInHomeSpace self.viewDelegate?.spaceMenuViewModel(self, didUpdateViewState: .deselect) - case .leave: + case .leaveSpace: self.leaveSpace() default: - self.coordinatorDelegate?.spaceMenuViewModel(self, didSelectItemWithId: actionStringId) + self.coordinatorDelegate?.spaceMenuViewModel(self, didSelectItemWithId: actionId) } } diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModelType.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModelType.swift index edaef39b3..dd55ebb26 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModelType.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModelType.swift @@ -22,7 +22,7 @@ protocol SpaceMenuViewModelViewDelegate: AnyObject { protocol SpaceMenuModelViewModelCoordinatorDelegate: AnyObject { func spaceMenuViewModelDidDismiss(_ viewModel: SpaceMenuViewModelType) - func spaceMenuViewModel(_ viewModel: SpaceMenuViewModelType, didSelectItemWithId itemId: String) + func spaceMenuViewModel(_ viewModel: SpaceMenuViewModelType, didSelectItemWithId itemId: SpaceMenuListItemActionId) } /// Protocol describing the view model used by `SpaceMenuViewController` From 58814cccb75a32a910f110b44c228b335e410282 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 14 Oct 2021 12:05:28 +0300 Subject: [PATCH 202/276] vector-im/element-ios/issues/5009 - Refactored share extension and started using the shared code directly in the main application. --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 + Riot/Managers/AppInfo/BuildInfo.m | 4 + Riot/Modules/Room/RoomViewController.m | 69 +++- Riot/target.yml | 2 + .../Managers/ShareExtensionManager.h | 105 +---- .../Managers/ShareExtensionManager.m | 391 ++++++++++-------- .../Modules/Fallback/FallbackViewController.h | 4 +- .../Modules/Fallback/FallbackViewController.m | 6 - ...r.h => ShareExtensionRootViewController.h} | 4 +- .../Main/ShareExtensionRootViewController.m | 92 +++++ .../Main/SharePresentingViewController.m | 79 ---- .../Share/DataSources/ShareDataSource.h | 4 +- .../Share/DataSources/ShareDataSource.m | 17 +- .../Share/Listing/RoomsListViewController.h | 4 +- .../Share/Listing/RoomsListViewController.m | 111 +---- .../Listing/Views/RecentRoomTableViewCell.m | 5 + .../Modules/Share/ShareViewController.h | 44 +- .../Modules/Share/ShareViewController.m | 195 +++++---- .../Modules/Share/ShareViewController.xib | 65 +-- RiotShareExtension/SupportingFiles/Info.plist | 2 +- .../RiotShareExtension-Bridging-Header.h | 1 + RiotShareExtension/target.yml | 2 + 23 files changed, 607 insertions(+), 604 deletions(-) rename RiotShareExtension/Modules/Main/{SharePresentingViewController.h => ShareExtensionRootViewController.h} (88%) create mode 100644 RiotShareExtension/Modules/Main/ShareExtensionRootViewController.m delete mode 100644 RiotShareExtension/Modules/Main/SharePresentingViewController.m diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 63b974866..4dd0b28ed 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -360,6 +360,7 @@ Tap the + to start adding people."; "room_event_action_redact" = "Remove"; "room_event_action_more" = "More"; "room_event_action_share" = "Share"; +"room_event_action_forward" = "Forward"; "room_event_action_permalink" = "Permalink"; "room_event_action_view_source" = "View Source"; "room_event_action_view_decrypted_source" = "View Decrypted Source"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index f23c03d2a..2b8768a86 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2887,6 +2887,10 @@ public class VectorL10n: NSObject { public static var roomEventActionEdit: String { return VectorL10n.tr("Vector", "room_event_action_edit") } + /// Forward + public static var roomEventActionForward: String { + return VectorL10n.tr("Vector", "room_event_action_forward") + } /// Reason for kicking this user public static var roomEventActionKickPromptReason: String { return VectorL10n.tr("Vector", "room_event_action_kick_prompt_reason") diff --git a/Riot/Managers/AppInfo/BuildInfo.m b/Riot/Managers/AppInfo/BuildInfo.m index 0853efe94..a1dbffe14 100644 --- a/Riot/Managers/AppInfo/BuildInfo.m +++ b/Riot/Managers/AppInfo/BuildInfo.m @@ -16,7 +16,11 @@ #import "BuildInfo.h" +#ifdef IS_SHARE_EXTENSION +#import "RiotShareExtension-Swift.h" +#else #import "Riot-Swift.h" +#endif #define MAKE_STRING(x) #x #define MAKE_NS_STRING(x) @MAKE_STRING(x) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index d32da8258..7158ae820 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -16,6 +16,8 @@ limitations under the License. */ +@import MobileCoreServices; + #import "RoomViewController.h" #import "RoomDataSource.h" @@ -106,6 +108,7 @@ #import "AvatarGenerator.h" #import "Tools.h" #import "WidgetManager.h" +#import "ShareExtensionManager.h" #import "GBDeviceInfo_iOS.h" @@ -249,6 +252,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @property (nonatomic, strong) VoiceMessageController *voiceMessageController; @property (nonatomic, strong) SpaceDetailPresenter *spaceDetailPresenter; +@property (nonatomic, strong) ShareExtensionManager *shareExtensionManager; + @property (nonatomic, strong) UserSuggestionCoordinatorBridge *userSuggestionCoordinator; @property (nonatomic, weak) IBOutlet UIView *userSuggestionContainerView; @@ -3190,6 +3195,25 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]]; } + [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + NSExtensionItem *item = [[NSExtensionItem alloc] init]; + item.attachments = @[[[NSItemProvider alloc] initWithItem:selectedComponent.textMessage typeIdentifier:(__bridge NSString *)kUTTypeText]]; + + self.shareExtensionManager = [[ShareExtensionManager alloc] initWithShareExtensionContext:nil + extensionItems:@[item]]; + + MXWeakify(self); + [self.shareExtensionManager setCompletionCallback:^(ShareExtensionManagerResult result) { + MXStrongifyAndReturnIfNil(self); + [attachment onShareEnded]; + [self dismissViewControllerAnimated:YES completion:nil]; + }]; + + [self presentViewController:self.shareExtensionManager.mainViewController animated:YES completion:nil]; + }]]; + if (!isJitsiCallEvent) { [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionQuote] @@ -3340,7 +3364,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self cancelEventSelection]; + [self startActivityIndicator]; + [attachment prepareShare:^(NSURL *fileURL) { + [self stopActivityIndicator]; __strong __typeof(weakSelf)self = weakSelf; self->documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:fileURL]; @@ -3355,10 +3382,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } } failure:^(NSError *error) { - - //Alert user [self showError:error]; - + [self stopActivityIndicator]; }]; // Start animation in case of download during attachment preparing @@ -3368,6 +3393,44 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]]; } } + + if (attachment.type == MXKAttachmentTypeFile || + attachment.type == MXKAttachmentTypeImage || + attachment.type == MXKAttachmentTypeVideo) { + + [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + + NSDictionary *attachmentTypeToIdentifier = @{@(MXKAttachmentTypeFile): (__bridge NSString *)kUTTypeFileURL, + @(MXKAttachmentTypeImage): (__bridge NSString *)kUTTypeImage, + @(MXKAttachmentTypeVideo): (__bridge NSString *)kUTTypeVideo}; + + [self startActivityIndicator]; + + [attachment prepareShare:^(NSURL *fileURL) { + [self stopActivityIndicator]; + + NSExtensionItem *item = [[NSExtensionItem alloc] init]; + item.attachments = @[[[NSItemProvider alloc] initWithItem:fileURL typeIdentifier:attachmentTypeToIdentifier[@(attachment.type)]]]; + + self.shareExtensionManager = [[ShareExtensionManager alloc] initWithShareExtensionContext:nil + extensionItems:@[item]]; + + MXWeakify(self); + [self.shareExtensionManager setCompletionCallback:^(ShareExtensionManagerResult result) { + MXStrongifyAndReturnIfNil(self); + [attachment onShareEnded]; + [self dismissViewControllerAnimated:YES completion:nil]; + }]; + + [self presentViewController:self.shareExtensionManager.mainViewController animated:YES completion:nil]; + } failure:^(NSError *error) { + [self showError:error]; + [self stopActivityIndicator]; + }]; + }]]; + } } // Check status of the selected event diff --git a/Riot/target.yml b/Riot/target.yml index 9428387f6..a4a61ac4c 100644 --- a/Riot/target.yml +++ b/Riot/target.yml @@ -66,6 +66,8 @@ targets: excludes: - "Modules/Room/EmojiPicker/Data/EmojiMart/EmojiJSONStore.swift" - "**/*.strings" # Exclude all strings files + - path: ../RiotShareExtension/Managers + - path: ../RiotShareExtension/Modules # Add separately localizable files # Once a language has enough translations (>80%), it must be declared here diff --git a/RiotShareExtension/Managers/ShareExtensionManager.h b/RiotShareExtension/Managers/ShareExtensionManager.h index 042d55fb0..e8dc215e6 100644 --- a/RiotShareExtension/Managers/ShareExtensionManager.h +++ b/RiotShareExtension/Managers/ShareExtensionManager.h @@ -14,111 +14,24 @@ limitations under the License. */ -#import #import @class ShareExtensionManager; -@class SharePresentingViewController; -@protocol Configurable; -/** - Posted when the matrix user account and his data has been checked and updated. - The notification object is the MXKAccount instance. - */ -extern NSString *const kShareExtensionManagerDidUpdateAccountDataNotification; - - -/** - The protocol for the manager's delegate - */ -@protocol ShareExtensionManagerDelegate - -@required - -/** - Called when an image is going to be shared to show a compression prompt - @param extensionManager the ShareExtensionManager object that called the method - @param compressionPrompt the prompt that was prepared for the image which is going to be shared - */ -- (void)shareExtensionManager:(ShareExtensionManager *)extensionManager showImageCompressionPrompt:(UIAlertController *)compressionPrompt; - -@optional - -/** - Called when the manager starts sending the content to a room - @param extensionManager the ShareExtensionManager object that called the method - @param room the room where content will be sent - */ -- (void)shareExtensionManager:(ShareExtensionManager *)extensionManager didStartSendingContentToRoom:(MXRoom *)room; - -/** - Called when the progress of the uploading media changes - @param extensionManager the ShareExtensionManager object that called the method - @param progress the current progress - */ -- (void)shareExtensionManager:(ShareExtensionManager *)extensionManager mediaUploadProgress:(CGFloat)progress; - -@end - - -/** - A class used to share content from the extension - */ +typedef NS_ENUM(NSUInteger, ShareExtensionManagerResult) { + ShareExtensionManagerResultFinished, + ShareExtensionManagerResultCancelled, + ShareExtensionManagerResultFailed +}; @interface ShareExtensionManager : NSObject -/** - The share extension context that represents a user's sharing request, also stores the content to be shared - */ -@property (nonatomic) NSExtensionContext *shareExtensionContext; +@property (nonatomic, copy) void (^completionCallback)(ShareExtensionManagerResult); -/** - The share app extension’s primary view controller. - */ -@property (nonatomic) SharePresentingViewController *primaryViewController; +- (instancetype)initWithShareExtensionContext:(NSExtensionContext *)shareExtensionContext + extensionItems:(NSArray *)extensionItems; -/** - The current user account - */ -@property (nonatomic, readonly) MXKAccount *userAccount; - -/** - The shared file store - */ -@property (nonatomic, readonly) MXFileStore *fileStore; - -/** - A delegate used to notify about needed UI changes when sharing - */ -@property (nonatomic, weak) id delegate; - -// Build Settings -@property (nonatomic, readonly) id configuration; - -/** - The singleton instance - */ -+ (instancetype)sharedManager; - -/** - Send the content that the user has chosen to a room - @param room the room to send the content to - @param failureBlock the code to be executed when sharing has failed for whatever reason - note: there is no "successBlock" parameter because when the sharing succeeds, the extension needs to close itself - */ -- (void)sendContentToRoom:(MXRoom *)room failureBlock:(void(^)(NSError *error))failureBlock; - -/** - Checks if there is an image in the user chosen content - @return YES if there is, NO otherwise - */ -- (BOOL)hasImageTypeContent; - -/** - Terminate the extension and return to the app that started it - @param canceled YES if the user chose to cancel the sharing, NO otherwise - */ -- (void)terminateExtensionCanceled:(BOOL)canceled; +- (UIViewController *)mainViewController; @end diff --git a/RiotShareExtension/Managers/ShareExtensionManager.m b/RiotShareExtension/Managers/ShareExtensionManager.m index ea58a7d34..6d2ab3cea 100644 --- a/RiotShareExtension/Managers/ShareExtensionManager.m +++ b/RiotShareExtension/Managers/ShareExtensionManager.m @@ -15,15 +15,21 @@ */ #import "ShareExtensionManager.h" -#import "SharePresentingViewController.h" +#import "ShareViewController.h" +#import "ShareDataSource.h" + #import + @import MobileCoreServices; #import "objc/runtime.h" #include #import -#import "RiotShareExtension-Swift.h" -NSString *const kShareExtensionManagerDidUpdateAccountDataNotification = @"kShareExtensionManagerDidUpdateAccountDataNotification"; +#ifdef IS_SHARE_EXTENSION +#import "RiotShareExtension-Swift.h" +#else +#import "Riot-Swift.h" +#endif static const CGFloat kLargeImageSizeMaxDimension = 2048.0; @@ -35,48 +41,45 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) ImageCompressionModeLarge }; +@interface ShareExtensionManager () -@interface ShareExtensionManager () +@property (nonatomic, strong, readonly) NSExtensionContext *shareExtensionContext; +@property (nonatomic, strong, readonly) NSArray *extensionItems; -@property (nonatomic, readwrite) MXKAccount *userAccount; +@property (nonatomic, strong, readonly) NSMutableArray *pendingImages; +@property (nonatomic, strong, readonly) NSMutableDictionary *imageUploadProgresses; +@property (nonatomic, strong, readonly) id configuration; +@property (nonatomic, strong, readonly) ShareViewController *shareViewController; -@property (nonatomic) NSMutableArray *pendingImages; -@property (nonatomic) NSMutableDictionary *imageUploadProgresses; -@property (nonatomic) ImageCompressionMode imageCompressionMode; -@property (nonatomic) CGFloat actualLargeSize; +@property (nonatomic, strong) MXKAccount *userAccount; +@property (nonatomic, strong) MXFileStore *fileStore; + +@property (nonatomic, assign) ImageCompressionMode imageCompressionMode; +@property (nonatomic, assign) CGFloat actualLargeSize; @end @implementation ShareExtensionManager -#pragma mark - Lifecycle - -+ (instancetype)sharedManager +- (instancetype)initWithShareExtensionContext:(NSExtensionContext *)shareExtensionContext + extensionItems:(NSArray *)extensionItems { - static ShareExtensionManager *sharedInstance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ + if (self = [super init]) { - sharedInstance = [[self alloc] init]; + _shareExtensionContext = shareExtensionContext; + _extensionItems = extensionItems; - sharedInstance.pendingImages = [NSMutableArray array]; - sharedInstance.imageUploadProgresses = [NSMutableDictionary dictionary]; + _pendingImages = [NSMutableArray array]; + _imageUploadProgresses = [NSMutableDictionary dictionary]; - [[NSNotificationCenter defaultCenter] addObserver:sharedInstance selector:@selector(onMediaLoaderStateDidChange:) name:kMXMediaLoaderStateDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaLoaderStateDidChange:) name:kMXMediaLoaderStateDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(checkUserAccount) name:kMXKAccountManagerDidRemoveAccountNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(checkUserAccount) name:NSExtensionHostWillEnterForegroundNotification object:nil]; + [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; - // Add observer to handle logout - [[NSNotificationCenter defaultCenter] addObserver:sharedInstance selector:@selector(checkUserAccount) name:kMXKAccountManagerDidRemoveAccountNotification object:nil]; - - // Add observer on the Extension host - [[NSNotificationCenter defaultCenter] addObserver:sharedInstance selector:@selector(checkUserAccount) name:NSExtensionHostWillEnterForegroundNotification object:nil]; - - // Add observer to handle memory warning - [NSNotificationCenter.defaultCenter addObserver:sharedInstance selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; - - // Set static application settings - sharedInstance->_configuration = [CommonConfiguration new]; - [sharedInstance.configuration setupSettings]; + _configuration = [CommonConfiguration new]; + [_configuration setupSettings]; // NSLog -> console.log file when not debugging the app MXLogConfiguration *configuration = [[MXLogConfiguration alloc] init]; @@ -91,68 +94,74 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) } [MXLog configure:configuration]; - }); - return sharedInstance; -} - -- (void)checkUserAccount -{ - // Force account manager to reload account from the local storage. - [[MXKAccountManager sharedManager] forceReloadAccounts]; - - if (self.userAccount) - { - // Check whether the used account is still the first active one - MXKAccount *firstAccount = [MXKAccountManager sharedManager].activeAccounts.firstObject; - // Compare the access token - if (!firstAccount || ![self.userAccount.mxCredentials.accessToken isEqualToString:firstAccount.mxCredentials.accessToken]) - { - // Remove this account - self.userAccount = nil; - } + _shareViewController = [[ShareViewController alloc] initWithType:ShareViewControllerTypeSend + currentState:ShareViewControllerAccountStateNotConfigured]; + [_shareViewController setDelegate:self]; + + // Set up runtime language on each context update. + NSUserDefaults *sharedUserDefaults = [MXKAppSettings standardAppSettings].sharedUserDefaults; + NSString *language = [sharedUserDefaults objectForKey:@"appLanguage"]; + [NSBundle mxk_setLanguage:language]; + [NSBundle mxk_setFallbackLanguage:@"en"]; + + // Check the current matrix user. + [self checkUserAccount]; } - if (!self.userAccount) - { - // We consider the first enabled account. - // TODO: Handle multiple accounts - self.userAccount = [MXKAccountManager sharedManager].activeAccounts.firstObject; - } - - // Reset the file store to reload the room data. - if (_fileStore) - { - [_fileStore close]; - _fileStore = nil; - } - - if (self.userAccount) - { - _fileStore = [[MXFileStore alloc] initWithCredentials:self.userAccount.mxCredentials]; - } - - // Post notification - [[NSNotificationCenter defaultCenter] postNotificationName:kShareExtensionManagerDidUpdateAccountDataNotification object:self.userAccount userInfo:nil]; + return self; } #pragma mark - Public -- (void)setShareExtensionContext:(NSExtensionContext *)shareExtensionContext +- (UIViewController *)mainViewController { - _shareExtensionContext = shareExtensionContext; - - // Set up runtime language on each context update. - NSUserDefaults *sharedUserDefaults = [MXKAppSettings standardAppSettings].sharedUserDefaults; - NSString *language = [sharedUserDefaults objectForKey:@"appLanguage"]; - [NSBundle mxk_setLanguage:language]; - [NSBundle mxk_setFallbackLanguage:@"en"]; - - // Check the current matrix user. - [self checkUserAccount]; + return self.shareViewController; } -- (void)sendContentToRoom:(MXRoom *)room failureBlock:(void(^)(NSError *error))failureBlock +#pragma mark - ShareViewControllerDelegate + +- (void)shareViewControllerDidRequestShare:(ShareViewController *)shareViewController + forRoomIdentifier:(NSString *)roomIdentifier +{ + MXSession *session = [[MXSession alloc] initWithMatrixRestClient:[[MXRestClient alloc] initWithCredentials:self.userAccount.mxCredentials andOnUnrecognizedCertificateBlock:nil]]; + [MXFileStore setPreloadOptions:0]; + + MXWeakify(session); + [session setStore:self.fileStore success:^{ + MXStrongifyAndReturnIfNil(session); + + MXRoom *selectedRoom = [MXRoom loadRoomFromStore:self.fileStore withRoomId:roomIdentifier matrixSession:session]; + + // Do not warn for unknown devices. We have cross-signing now + session.crypto.warnOnUnknowDevices = NO; + + [self _sendContentToRoom:selectedRoom failureBlock:^(NSError* error) { + NSString *title = [VectorL10n roomEventFailedToSend]; + if ([error.domain isEqualToString:MXEncryptingErrorDomain]) + { + title = [VectorL10n shareExtensionFailedToEncrypt]; + } + + [self _showFailureAlert:title]; + }]; + + } failure:^(NSError *error) { + MXLogError(@"[ShareExtensionManager] Failed preparign matrix session"); + }]; +} + +- (void)shareViewControllerDidRequestDismissal:(ShareViewController *)shareViewController +{ + if (self.completionCallback) + { + self.completionCallback(ShareExtensionManagerResultCancelled); + } +} + +#pragma mark - Private + +- (void)_sendContentToRoom:(MXRoom *)room failureBlock:(void(^)(NSError *error))failureBlock { [self resetPendingData]; @@ -190,7 +199,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) __weak typeof(self) weakSelf = self; - for (NSExtensionItem *item in self.shareExtensionContext.inputItems) + for (NSExtensionItem *item in self.extensionItems) { for (NSItemProvider *itemProvider in item.attachments) { @@ -198,7 +207,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) { dispatch_group_enter(requestsGroup); - [itemProvider loadItemForTypeIdentifier:UTTypeFileUrl options:nil completionHandler:^(NSURL *fileUrl, NSError * _Null_unspecified error) { + [itemProvider loadItemForTypeIdentifier:UTTypeFileUrl options:nil completionHandler:^(NSURL *fileUrl, NSError *error) { // Switch back on the main thread to handle correctly the UI change dispatch_async(dispatch_get_main_queue(), ^{ @@ -269,7 +278,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) itemProvider.isLoaded = NO; - [itemProvider loadItemForTypeIdentifier:UTTypeImage options:nil completionHandler:^(id _Nullable itemProviderItem, NSError * _Null_unspecified error) + [itemProvider loadItemForTypeIdentifier:UTTypeImage options:nil completionHandler:^(id itemProviderItem, NSError *error) { if (weakSelf) { @@ -340,7 +349,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) if (compressionPrompt) { - [self.delegate shareExtensionManager:self showImageCompressionPrompt:compressionPrompt]; + [self presentCompressionPrompt:compressionPrompt]; } } else @@ -356,44 +365,44 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) dispatch_group_enter(requestsGroup); [itemProvider loadItemForTypeIdentifier:UTTypeVideo options:nil completionHandler:^(NSURL *videoLocalUrl, NSError * _Null_unspecified error) { - - // Switch back on the main thread to handle correctly the UI change - dispatch_async(dispatch_get_main_queue(), ^{ - - if (weakSelf) - { - typeof(self) self = weakSelf; - [self sendVideo:videoLocalUrl - toRoom:room - successBlock:^{ - requestSuccess(item); - } failureBlock:requestFailure]; - } - - }); - }]; + // Switch back on the main thread to handle correctly the UI change + dispatch_async(dispatch_get_main_queue(), ^{ + + if (weakSelf) + { + typeof(self) self = weakSelf; + [self sendVideo:videoLocalUrl + toRoom:room + successBlock:^{ + requestSuccess(item); + } failureBlock:requestFailure]; + } + + }); + + }]; } else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeMovie]) { dispatch_group_enter(requestsGroup); [itemProvider loadItemForTypeIdentifier:UTTypeMovie options:nil completionHandler:^(NSURL *videoLocalUrl, NSError * _Null_unspecified error) { - - // Switch back on the main thread to handle correctly the UI change - dispatch_async(dispatch_get_main_queue(), ^{ - - if (weakSelf) - { - typeof(self) self = weakSelf; - [self sendVideo:videoLocalUrl - toRoom:room - successBlock:^{ - requestSuccess(item); - } failureBlock:requestFailure]; - } - - }); + + // Switch back on the main thread to handle correctly the UI change + dispatch_async(dispatch_get_main_queue(), ^{ + + if (weakSelf) + { + typeof(self) self = weakSelf; + [self sendVideo:videoLocalUrl + toRoom:room + successBlock:^{ + requestSuccess(item); + } failureBlock:requestFailure]; + } + + }); }]; } @@ -412,65 +421,93 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) } else { - [self completeRequestReturningItems:returningExtensionItems completionHandler:nil]; + if (self.completionCallback) + { + self.completionCallback(ShareExtensionManagerResultFinished); + } } }); } -- (BOOL)hasImageTypeContent +- (void)_showFailureAlert:(NSString *)title { - for (NSExtensionItem *item in self.shareExtensionContext.inputItems) - { - for (NSItemProvider *itemProvider in item.attachments) + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:nil preferredStyle:UIAlertControllerStyleAlert]; + + MXWeakify(self); + UIAlertAction *okAction = [UIAlertAction actionWithTitle:[MatrixKitL10n ok] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + MXStrongifyAndReturnIfNil(self); + + if (self.completionCallback) { - if ([itemProvider hasItemConformingToTypeIdentifier:(__bridge NSString *)kUTTypeImage]) - { - return YES; - } + self.completionCallback(ShareExtensionManagerResultFailed); + } + }]; + + [alertController addAction:okAction]; + + [self.mainViewController presentViewController:alertController animated:YES completion:nil]; +} + +- (void)checkUserAccount +{ + // Force account manager to reload account from the local storage. + [[MXKAccountManager sharedManager] forceReloadAccounts]; + + if (self.userAccount) + { + // Check whether the used account is still the first active one + MXKAccount *firstAccount = [MXKAccountManager sharedManager].activeAccounts.firstObject; + + // Compare the access token + if (!firstAccount || ![self.userAccount.mxCredentials.accessToken isEqualToString:firstAccount.mxCredentials.accessToken]) + { + // Remove this account + self.userAccount = nil; } } - return NO; -} - -- (void)terminateExtensionCanceled:(BOOL)canceled -{ - if (canceled) + + if (!self.userAccount) { - [self.shareExtensionContext cancelRequestWithError:[NSError errorWithDomain:@"MXUserCancelErrorDomain" code:4201 userInfo:nil]]; - } - else - { - [self.shareExtensionContext cancelRequestWithError:[NSError errorWithDomain:@"MXFailureErrorDomain" code:500 userInfo:nil]]; + // We consider the first enabled account. + // TODO: Handle multiple accounts + self.userAccount = [MXKAccountManager sharedManager].activeAccounts.firstObject; } - [self.primaryViewController destroy]; - self.primaryViewController = nil; + // Reset the file store to reload the room data. + if (_fileStore) + { + [_fileStore close]; + _fileStore = nil; + } - // FIXME: Share extension memory usage increase when launched several times and then crash due to some memory leaks. - // For now, we force the share extension to exit and free memory. - [NSException raise:@"Kill the app extension" format:@"Free memory used by share extension"]; + if (self.userAccount) + { + _fileStore = [[MXFileStore alloc] initWithCredentials:self.userAccount.mxCredentials]; + + ShareDataSource *roomDataSource = [[ShareDataSource alloc] initWithMode:DataSourceModeRooms + fileStore:_fileStore + credentials:self.userAccount.mxCredentials]; + + ShareDataSource *peopleDataSource = [[ShareDataSource alloc] initWithMode:DataSourceModePeople + fileStore:_fileStore + credentials:self.userAccount.mxCredentials]; + + [self.shareViewController configureWithState:ShareViewControllerAccountStateConfigured + roomDataSource:roomDataSource + peopleDataSource:peopleDataSource]; + } else { + [self.shareViewController configureWithState:ShareViewControllerAccountStateNotConfigured + roomDataSource:nil + peopleDataSource:nil]; + } } -#pragma mark - Private - - (void)resetPendingData { [self.pendingImages removeAllObjects]; [self.imageUploadProgresses removeAllObjects]; } -- (void)completeRequestReturningItems:(nullable NSArray *)items completionHandler:(void(^ __nullable)(BOOL expired))completionHandler; -{ - [self.shareExtensionContext completeRequestReturningItems:items completionHandler:completionHandler]; - - [self.primaryViewController destroy]; - self.primaryViewController = nil; - - // FIXME: Share extension memory usage increase when launched several times and then crash due to some memory leaks. - // For now, we force the share extension to exit and free memory. - [NSException raise:@"Kill the app extension" format:@"Free memory used by share extension"]; -} - - (BOOL)isAPendingImageNotOrientedUp { BOOL isAPendingImageNotOrientedUp = NO; @@ -659,15 +696,12 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) - (void)didStartSendingToRoom:(MXRoom *)room { - if ([self.delegate respondsToSelector:@selector(shareExtensionManager:didStartSendingContentToRoom:)]) - { - [self.delegate shareExtensionManager:self didStartSendingContentToRoom:room]; - } + [self.shareViewController showProgressIndicator]; } - (BOOL)areAttachmentsFullyLoaded { - for (NSExtensionItem *item in self.shareExtensionContext.inputItems) + for (NSExtensionItem *item in self.extensionItems) { for (NSItemProvider *itemProvider in item.attachments) { @@ -682,7 +716,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) - (BOOL)areAllAttachmentsImages { - for (NSExtensionItem *item in self.shareExtensionContext.inputItems) + for (NSExtensionItem *item in self.extensionItems) { for (NSItemProvider *itemProvider in item.attachments) { @@ -868,6 +902,14 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) } } +- (void)presentCompressionPrompt:(UIAlertController *)compressionPrompt +{ + dispatch_async(dispatch_get_main_queue(), ^{ + [compressionPrompt popoverPresentationController].sourceView = self.mainViewController.view; + [compressionPrompt popoverPresentationController].sourceRect = self.mainViewController.view.frame; + [self.mainViewController presentViewController:compressionPrompt animated:YES completion:nil]; + }); +} #pragma mark - Notifications @@ -879,18 +921,16 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) case MXMediaLoaderStateUploadInProgress: { self.imageUploadProgresses[loader.uploadId] = (NSNumber *)loader.statisticsDict[kMXMediaLoaderProgressValueKey]; - if ([self.delegate respondsToSelector:@selector(shareExtensionManager:mediaUploadProgress:)]) + + const NSInteger totalImagesCount = self.pendingImages.count; + CGFloat totalProgress = 0.0; + + for (NSNumber *progress in self.imageUploadProgresses.allValues) { - const NSInteger totalImagesCount = self.pendingImages.count; - CGFloat totalProgress = 0.0; - - for (NSNumber *progress in self.imageUploadProgresses.allValues) - { - totalProgress += progress.floatValue/totalImagesCount; - } - - [self.delegate shareExtensionManager:self mediaUploadProgress:totalProgress]; + totalProgress += progress.floatValue/totalImagesCount; } + + [self.shareViewController setProgress:totalProgress]; break; } default: @@ -1161,7 +1201,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) MXWeakify(self); // Ignore showMediaCompressionPrompt setting due to memory constraints when encrypting large videos. - UIAlertController *compressionPrompt = [MXKTools videoConversionPromptForVideoAsset:videoAsset withCompletion:^(NSString * _Nullable presetName) { + UIAlertController *compressionPrompt = [MXKTools videoConversionPromptForVideoAsset:videoAsset withCompletion:^(NSString *presetName) { MXStrongifyAndReturnIfNil(self); // If the preset name is nil, the user cancelled. @@ -1207,10 +1247,9 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) }]; }]; - [self.delegate shareExtensionManager:self showImageCompressionPrompt:compressionPrompt]; + [self presentCompressionPrompt:compressionPrompt]; } - @end diff --git a/RiotShareExtension/Modules/Fallback/FallbackViewController.h b/RiotShareExtension/Modules/Fallback/FallbackViewController.h index ddee2a064..c4f26c2ef 100644 --- a/RiotShareExtension/Modules/Fallback/FallbackViewController.h +++ b/RiotShareExtension/Modules/Fallback/FallbackViewController.h @@ -14,8 +14,8 @@ limitations under the License. */ -#import +@import UIKit; -@interface FallbackViewController : MXKViewController +@interface FallbackViewController : UIViewController @end diff --git a/RiotShareExtension/Modules/Fallback/FallbackViewController.m b/RiotShareExtension/Modules/Fallback/FallbackViewController.m index 74f81b4a0..57412f705 100644 --- a/RiotShareExtension/Modules/Fallback/FallbackViewController.m +++ b/RiotShareExtension/Modules/Fallback/FallbackViewController.m @@ -42,10 +42,4 @@ self.logoImageView.tintColor = ThemeService.shared.theme.tintColor; } -- (void)didReceiveMemoryWarning -{ - [super didReceiveMemoryWarning]; - // Dispose of any resources that can be recreated. -} - @end diff --git a/RiotShareExtension/Modules/Main/SharePresentingViewController.h b/RiotShareExtension/Modules/Main/ShareExtensionRootViewController.h similarity index 88% rename from RiotShareExtension/Modules/Main/SharePresentingViewController.h rename to RiotShareExtension/Modules/Main/ShareExtensionRootViewController.h index c9eff12d0..e0868b6ad 100644 --- a/RiotShareExtension/Modules/Main/SharePresentingViewController.h +++ b/RiotShareExtension/Modules/Main/ShareExtensionRootViewController.h @@ -16,8 +16,6 @@ #import -@interface SharePresentingViewController : UIViewController - -- (void)destroy; +@interface ShareExtensionRootViewController : UIViewController @end diff --git a/RiotShareExtension/Modules/Main/ShareExtensionRootViewController.m b/RiotShareExtension/Modules/Main/ShareExtensionRootViewController.m new file mode 100644 index 000000000..ad5178c41 --- /dev/null +++ b/RiotShareExtension/Modules/Main/ShareExtensionRootViewController.m @@ -0,0 +1,92 @@ +/* + Copyright 2017 Aram Sargsyan + + 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 "ShareExtensionRootViewController.h" +#import "ShareViewController.h" +#import "ShareExtensionManager.h" +#import "ThemeService.h" + +#ifdef IS_SHARE_EXTENSION +#import "RiotShareExtension-Swift.h" +#else +#import "Riot-Swift.h" +#endif + +@interface ShareExtensionRootViewController () + +@property (nonatomic, strong, readonly) ShareExtensionManager *shareExtensionManager; + +@end + +@implementation ShareExtensionRootViewController + +- (instancetype)init +{ + if(self = [super init]) { + + [ThemeService.shared setThemeId:RiotSettings.shared.userInterfaceTheme]; + + _shareExtensionManager = [[ShareExtensionManager alloc] initWithShareExtensionContext:self.extensionContext + extensionItems:self.extensionContext.inputItems]; + + MXWeakify(self); + [_shareExtensionManager setCompletionCallback:^(ShareExtensionManagerResult result) { + MXStrongifyAndReturnIfNil(self); + + switch (result) + { + case ShareExtensionManagerResultFinished: + [self.extensionContext completeRequestReturningItems:nil completionHandler:nil]; + [self _dismiss]; + break; + case ShareExtensionManagerResultCancelled: + [self.extensionContext cancelRequestWithError:[NSError errorWithDomain:@"MXUserCancelErrorDomain" code:4201 userInfo:nil]]; + [self _dismiss]; + break; + case ShareExtensionManagerResultFailed: + [self.extensionContext cancelRequestWithError:[NSError errorWithDomain:@"MXFailureErrorDomain" code:500 userInfo:nil]]; + [self _dismiss]; + break; + default: + break; + } + }]; + } + + return self; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + [self presentViewController:self.shareExtensionManager.mainViewController animated:YES completion:nil]; +} + +#pragma mark - Private + +- (void)_dismiss +{ + [self dismissViewControllerAnimated:true completion:^{ + [self.presentingViewController dismissViewControllerAnimated:false completion:nil]; + +// // FIXME: Share extension memory usage increase when launched several times and then crash due to some memory leaks. +// // For now, we force the share extension to exit and free memory. +// [NSException raise:@"Kill the app extension" format:@"Free memory used by share extension"]; + }]; +} + +@end diff --git a/RiotShareExtension/Modules/Main/SharePresentingViewController.m b/RiotShareExtension/Modules/Main/SharePresentingViewController.m deleted file mode 100644 index ef57eba3c..000000000 --- a/RiotShareExtension/Modules/Main/SharePresentingViewController.m +++ /dev/null @@ -1,79 +0,0 @@ -/* - Copyright 2017 Aram Sargsyan - - 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 "SharePresentingViewController.h" -#import "ShareViewController.h" -#import "ShareExtensionManager.h" -#import "ThemeService.h" - -#ifdef IS_SHARE_EXTENSION -#import "RiotShareExtension-Swift.h" -#else -#import "Riot-Swift.h" -#endif - -@interface SharePresentingViewController () - -@property (nonatomic) ShareViewController *shareViewController; - -@end - -@implementation SharePresentingViewController - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - ShareExtensionManager *sharedManager = [ShareExtensionManager sharedManager]; - - sharedManager.primaryViewController = self; - sharedManager.shareExtensionContext = self.extensionContext; - - // Set up current theme - ThemeService.shared.themeId = RiotSettings.shared.userInterfaceTheme; - - [self presentShareViewController]; -} - -- (void)destroy -{ - if (self.shareViewController) - { - [self.shareViewController destroy]; - self.shareViewController = nil; - } -} - -- (void)presentShareViewController -{ - self.shareViewController = [[ShareViewController alloc] init]; - - self.shareViewController.providesPresentationContextTransitionStyle = YES; - self.shareViewController.definesPresentationContext = YES; - self.shareViewController.modalPresentationStyle = UIModalPresentationOverFullScreen; - self.shareViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; - - [self presentViewController:self.shareViewController animated:YES completion:nil]; -} - -- (void)didReceiveMemoryWarning -{ - [super didReceiveMemoryWarning]; - // Dispose of any resources that can be recreated. -} - - -@end diff --git a/RiotShareExtension/Modules/Share/DataSources/ShareDataSource.h b/RiotShareExtension/Modules/Share/DataSources/ShareDataSource.h index 2a50bfdee..6d62631da 100644 --- a/RiotShareExtension/Modules/Share/DataSources/ShareDataSource.h +++ b/RiotShareExtension/Modules/Share/DataSources/ShareDataSource.h @@ -25,7 +25,9 @@ typedef NS_ENUM(NSInteger, ShareDataSourceMode) @interface ShareDataSource : MXKRecentsDataSource -- (instancetype)initWithMode:(ShareDataSourceMode)dataSourceMode; +- (instancetype)initWithMode:(ShareDataSourceMode)dataSourceMode + fileStore:(MXFileStore *)fileStore + credentials:(MXCredentials *)credentials; /** Returns the cell data at the index path diff --git a/RiotShareExtension/Modules/Share/DataSources/ShareDataSource.m b/RiotShareExtension/Modules/Share/DataSources/ShareDataSource.m index e841769ea..802425ebc 100644 --- a/RiotShareExtension/Modules/Share/DataSources/ShareDataSource.m +++ b/RiotShareExtension/Modules/Share/DataSources/ShareDataSource.m @@ -20,7 +20,9 @@ @interface ShareDataSource () -@property (nonatomic, readwrite) ShareDataSourceMode dataSourceMode; +@property (nonatomic, assign, readonly) ShareDataSourceMode dataSourceMode; +@property (nonatomic, strong, readonly) MXFileStore *fileStore; +@property (nonatomic, strong, readonly) MXCredentials *credentials; @property NSArray *recentCellDatas; @property NSMutableArray *visibleRoomCellDatas; @@ -30,11 +32,14 @@ @implementation ShareDataSource - (instancetype)initWithMode:(ShareDataSourceMode)dataSourceMode + fileStore:(MXFileStore *)fileStore + credentials:(MXCredentials *)credentials { - self = [super init]; - if (self) + if (self = [super init]) { - self.dataSourceMode = dataSourceMode; + _dataSourceMode = dataSourceMode; + _fileStore = fileStore; + _credentials = credentials; [self loadCellData]; } @@ -53,12 +58,12 @@ - (void)loadCellData { - [[ShareExtensionManager sharedManager].fileStore asyncRoomsSummaries:^(NSArray * _Nonnull roomsSummaries) { + [self.fileStore asyncRoomsSummaries:^(NSArray *roomsSummaries) { NSMutableArray *cellData = [NSMutableArray array]; // Add a fake matrix session to each room summary to provide it a REST client (used to handle correctly the room avatar). - MXSession *session = [[MXSession alloc] initWithMatrixRestClient:[[MXRestClient alloc] initWithCredentials:[ShareExtensionManager sharedManager].userAccount.mxCredentials andOnUnrecognizedCertificateBlock:nil]]; + MXSession *session = [[MXSession alloc] initWithMatrixRestClient:[[MXRestClient alloc] initWithCredentials:self.credentials andOnUnrecognizedCertificateBlock:nil]]; for (MXRoomSummary *roomSummary in roomsSummaries) { diff --git a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.h b/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.h index b8a50f7e4..bee26845b 100644 --- a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.h +++ b/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.h @@ -18,8 +18,8 @@ #import "MXRoom+Riot.h" #import "ShareDataSource.h" +@class RoomsListViewController; + @interface RoomsListViewController : MXKRecentListViewController -@property (copy) void (^failureBlock)(void); - @end diff --git a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m b/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m index 3f6e67b2e..7a9e198f0 100644 --- a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m +++ b/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m @@ -28,9 +28,7 @@ #import "Riot-Swift.h" #endif -@interface RoomsListViewController () - -@property (nonatomic) MXKPieChartHUD *hudView; +@interface RoomsListViewController () // The fake search bar displayed at the top of the recents table. We switch on the actual search bar (self.recentsSearchBar) // when the user selects it. @@ -136,76 +134,6 @@ return; } -#pragma mark - Private - -- (void)showShareAlertForRoomPath:(NSIndexPath *)indexPath -{ - MXKRecentCellData *recentCellData = [self.dataSource cellDataAtIndexPath:indexPath]; - NSString *roomName = recentCellData.roomSummary.displayname; - if (!roomName.length) - { - roomName = [MatrixKitL10n roomDisplaynameEmptyRoom]; - } - - UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[VectorL10n sendTo:roomName] message:nil preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:[MatrixKitL10n cancel] style:UIAlertActionStyleCancel handler:nil]; - [alertController addAction:cancelAction]; - - UIAlertAction *sendAction = [UIAlertAction actionWithTitle:[MatrixKitL10n send] style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { - - // The selected room is instanciated here - MXSession *session = [[MXSession alloc] initWithMatrixRestClient:[[MXRestClient alloc] initWithCredentials:[ShareExtensionManager sharedManager].userAccount.mxCredentials andOnUnrecognizedCertificateBlock:nil]]; - - [MXFileStore setPreloadOptions:0]; - - MXWeakify(session); - [session setStore:[ShareExtensionManager sharedManager].fileStore success:^{ - MXStrongifyAndReturnIfNil(session); - - MXRoom *selectedRoom = [MXRoom loadRoomFromStore:[ShareExtensionManager sharedManager].fileStore withRoomId:recentCellData.roomSummary.roomId matrixSession:session]; - - // Do not warn for unknown devices. We have cross-signing now - session.crypto.warnOnUnknowDevices = NO; - - [ShareExtensionManager sharedManager].delegate = self; - - [[ShareExtensionManager sharedManager] sendContentToRoom:selectedRoom failureBlock:^(NSError* error) { - - NSString *title; - if ([error.domain isEqualToString:MXEncryptingErrorDomain]) - { - title = [VectorL10n shareExtensionFailedToEncrypt]; - } - - [self showFailureAlert:title]; - }]; - - } failure:^(NSError *error) { - - MXLogDebug(@"[RoomsListViewController] failed to prepare matrix session]"); - - }]; - }]; - - [alertController addAction:sendAction]; - - [self presentViewController:alertController animated:YES completion:nil]; -} - -- (void)showFailureAlert:(NSString *)title -{ - UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title.length ? title : [VectorL10n roomEventFailedToSend] message:nil preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction *okAction = [UIAlertAction actionWithTitle:[MatrixKitL10n ok] style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { - if (self.failureBlock) - { - self.failureBlock(); - } - }]; - [alertController addAction:okAction]; - [self presentViewController:alertController animated:YES completion:nil]; -} - #pragma mark - UITableViewDelegate - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath @@ -213,13 +141,6 @@ return [RecentRoomTableViewCell cellHeight]; } -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - [tableView deselectRowAtIndexPath:indexPath animated:YES]; - - [self showShareAlertForRoomPath:indexPath]; -} - #pragma mark - MXKDataSourceDelegate - (Class)cellViewClassForCellData:(MXKCellData*)cellData @@ -304,34 +225,4 @@ } } -#pragma mark - ShareExtensionManagerDelegate - -- (void)shareExtensionManager:(ShareExtensionManager *)extensionManager showImageCompressionPrompt:(UIAlertController *)compressionPrompt -{ - dispatch_async(dispatch_get_main_queue(), ^{ - [compressionPrompt popoverPresentationController].sourceView = self.view; - [compressionPrompt popoverPresentationController].sourceRect = self.view.frame; - [self presentViewController:compressionPrompt animated:YES completion:nil]; - }); -} - -- (void)shareExtensionManager:(ShareExtensionManager *)extensionManager didStartSendingContentToRoom:(MXRoom *)room -{ - dispatch_async(dispatch_get_main_queue(), ^{ - if (!self.hudView) - { - self.parentViewController.view.userInteractionEnabled = NO; - self.hudView = [MXKPieChartHUD showLoadingHudOnView:self.view WithMessage:[VectorL10n sending]]; - [self.hudView setProgress:0.0]; - } - }); -} - -- (void)shareExtensionManager:(ShareExtensionManager *)extensionManager mediaUploadProgress:(CGFloat)progress -{ - dispatch_async(dispatch_get_main_queue(), ^{ - [self.hudView setProgress:progress]; - }); -} - @end diff --git a/RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.m b/RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.m index 90550a99e..5abb551d9 100644 --- a/RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.m +++ b/RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.m @@ -18,7 +18,12 @@ #import "MXRoomSummary+Riot.h" #import "ThemeService.h" + +#ifdef IS_SHARE_EXTENSION #import "RiotShareExtension-Swift.h" +#else +#import "Riot-Swift.h" +#endif @interface RecentRoomTableViewCell () diff --git a/RiotShareExtension/Modules/Share/ShareViewController.h b/RiotShareExtension/Modules/Share/ShareViewController.h index 74e7af78a..edf5ce348 100644 --- a/RiotShareExtension/Modules/Share/ShareViewController.h +++ b/RiotShareExtension/Modules/Share/ShareViewController.h @@ -14,9 +14,47 @@ limitations under the License. */ -#import -#import +@import UIKit; -@interface ShareViewController : MXKViewController +@class ShareViewController; +@class ShareDataSource; + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, ShareViewControllerType) { + ShareViewControllerTypeSend, + ShareViewControllerTypeForward +}; + +typedef NS_ENUM(NSUInteger, ShareViewControllerAccountState) { + ShareViewControllerAccountStateConfigured, + ShareViewControllerAccountStateNotConfigured +}; + +@protocol ShareViewControllerDelegate + +- (void)shareViewControllerDidRequestShare:(ShareViewController *)shareViewController + forRoomIdentifier:(NSString *)roomIdentifier; + +- (void)shareViewControllerDidRequestDismissal:(ShareViewController *)shareViewController; @end + +@interface ShareViewController : UIViewController + +@property (nonatomic, weak, nullable) id delegate; + +- (instancetype)initWithType:(ShareViewControllerType)type + currentState:(ShareViewControllerAccountState)state; + +- (void)configureWithState:(ShareViewControllerAccountState)state + roomDataSource:(nullable ShareDataSource *)roomDataSource + peopleDataSource:(nullable ShareDataSource *)peopleDataSource; + +- (void)showProgressIndicator; + +- (void)setProgress:(CGFloat)progress; + +@end + +NS_ASSUME_NONNULL_END diff --git a/RiotShareExtension/Modules/Share/ShareViewController.m b/RiotShareExtension/Modules/Share/ShareViewController.m index af4796e72..21cce2d00 100644 --- a/RiotShareExtension/Modules/Share/ShareViewController.m +++ b/RiotShareExtension/Modules/Share/ShareViewController.m @@ -22,161 +22,188 @@ #import "ShareExtensionManager.h" #import "ThemeService.h" + +#ifdef IS_SHARE_EXTENSION #import "RiotShareExtension-Swift.h" +#else +#import "Riot-Swift.h" +#endif +@interface ShareViewController () -@interface ShareViewController () +@property (nonatomic, assign, readonly) ShareViewControllerType type; -@property (weak, nonatomic) IBOutlet UIView *masterContainerView; -@property (weak, nonatomic) IBOutlet UILabel *titleLabel; -@property (weak, nonatomic) IBOutlet UIView *contentView; +@property (nonatomic, assign) ShareViewControllerAccountState state; +@property (nonatomic, strong) ShareDataSource *roomDataSource; +@property (nonatomic, strong) ShareDataSource *peopleDataSource; -@property (nonatomic) SegmentedViewController *segmentedViewController; +@property (nonatomic, weak) IBOutlet UIView *masterContainerView; +@property (nonatomic, weak) IBOutlet UIButton *cancelButton; +@property (nonatomic, weak) IBOutlet UILabel *titleLabel; +@property (nonatomic, weak) IBOutlet UIButton *shareButton; +@property (nonatomic, weak) IBOutlet UIView *contentView; -@property (nonatomic) id shareExtensionManagerDidUpdateAccountDataObserver; +@property (nonatomic, strong) SegmentedViewController *segmentedViewController; +@property (nonatomic, strong) MXKPieChartHUD *hudView; @end @implementation ShareViewController -#pragma mark - Lifecycle +- (instancetype)initWithType:(ShareViewControllerType)type + currentState:(ShareViewControllerAccountState)state +{ + if (self = [super init]) + { + _type = type; + _state = state; + } + + return self; +} - (void)viewDidLoad { [super viewDidLoad]; - self.view.tintColor = ThemeService.shared.theme.tintColor; - self.titleLabel.textColor = ThemeService.shared.theme.textPrimaryColor; - self.masterContainerView.backgroundColor = ThemeService.shared.theme.baseColor; + [self.masterContainerView setBackgroundColor:ThemeService.shared.theme.baseColor]; + [self.masterContainerView.layer setCornerRadius:7.0]; - self.shareExtensionManagerDidUpdateAccountDataObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kShareExtensionManagerDidUpdateAccountDataNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - - [self configureViews]; + [self.titleLabel setTextColor:ThemeService.shared.theme.textPrimaryColor]; - }]; + [self.cancelButton setTintColor:ThemeService.shared.theme.tintColor]; + [self.cancelButton setTitle:[VectorL10n cancel] forState:UIControlStateNormal]; + + [self.shareButton setTintColor:ThemeService.shared.theme.tintColor]; + + [self configureWithState:self.state roomDataSource:self.roomDataSource peopleDataSource:self.peopleDataSource]; +} + +- (void)configureWithState:(ShareViewControllerAccountState)state + roomDataSource:(ShareDataSource *)roomDataSource + peopleDataSource:(ShareDataSource *)peopleDataSource +{ + self.state = state; + self.roomDataSource = roomDataSource; + self.peopleDataSource = peopleDataSource; + + if (!self.isViewLoaded) { + return; + } [self configureViews]; } -- (void)destroy +#pragma mark - MXKRecentListViewControllerDelegate + +- (void)recentListViewController:(MXKRecentListViewController *)recentListViewController + didSelectRoom:(NSString *)roomId + inMatrixSession:(MXSession *)mxSession { - [super destroy]; - - if (self.shareExtensionManagerDidUpdateAccountDataObserver) - { - [[NSNotificationCenter defaultCenter] removeObserver:self.shareExtensionManagerDidUpdateAccountDataObserver]; - self.shareExtensionManagerDidUpdateAccountDataObserver = nil; - } - - [self resetContentView]; + [self.delegate shareViewControllerDidRequestShare:self forRoomIdentifier:roomId]; } -- (void)resetContentView +- (void)recentListViewController:(MXKRecentListViewController *)recentListViewController + didSelectSuggestedRoom:(MXSpaceChildInfo *)childInfo { - // Empty the content view - NSArray *subviews = self.contentView.subviews; - for (UIView *subview in subviews) - { - [subview removeFromSuperview]; - } - - // Release the current segmented view controller if any - if (self.segmentedViewController) - { - [self.segmentedViewController removeFromParentViewController]; - - // Release correctly all the existing data source and view controllers. - [self.segmentedViewController destroy]; - self.segmentedViewController = nil; - } + [self.delegate shareViewControllerDidRequestShare:self forRoomIdentifier:childInfo.childRoomId]; +} + +#pragma mark - ShareExtensionManagerDelegate + +- (void)showProgressIndicator +{ + dispatch_async(dispatch_get_main_queue(), ^{ + if (!self.hudView) + { + self.parentViewController.view.userInteractionEnabled = NO; + self.hudView = [MXKPieChartHUD showLoadingHudOnView:self.view WithMessage:[VectorL10n sending]]; + [self.hudView setProgress:0.0]; + } + }); +} + +- (void)setProgress:(CGFloat)progress +{ + [self.hudView setProgress:progress]; } #pragma mark - Private - (void)configureViews { - self.masterContainerView.layer.cornerRadius = 7; - [self resetContentView]; - if ([ShareExtensionManager sharedManager].userAccount) + if (self.state == ShareViewControllerAccountStateConfigured) { self.titleLabel.text = [VectorL10n sendTo:@""]; + [self.shareButton setTitle:[VectorL10n roomEventActionForward] forState:UIControlStateNormal]; + [self configureSegmentedViewController]; } else { - NSDictionary *infoDictionary = [NSBundle mainBundle].infoDictionary; - NSString *bundleDisplayName = infoDictionary[@"CFBundleDisplayName"]; - self.titleLabel.text = bundleDisplayName; + self.titleLabel.text = [AppInfo.current displayName]; [self configureFallbackViewController]; } } - (void)configureSegmentedViewController { - self.segmentedViewController = [SegmentedViewController segmentedViewController]; - - NSArray *titles = @[[VectorL10n titleRooms], [VectorL10n titlePeople]]; - - void (^failureBlock)(void) = ^void() { - [self dismissViewControllerAnimated:YES completion:^{ - [[ShareExtensionManager sharedManager] terminateExtensionCanceled:NO]; - }]; - }; - - ShareDataSource *roomsDataSource = [[ShareDataSource alloc] initWithMode:DataSourceModeRooms]; RoomsListViewController *roomsViewController = [RoomsListViewController recentListViewController]; - roomsViewController.failureBlock = failureBlock; - [roomsViewController displayList:roomsDataSource]; + [roomsViewController displayList:self.roomDataSource]; + [roomsViewController setDelegate:self]; - ShareDataSource *peopleDataSource = [[ShareDataSource alloc] initWithMode:DataSourceModePeople]; RoomsListViewController *peopleViewController = [RoomsListViewController recentListViewController]; - peopleViewController.failureBlock = failureBlock; - [peopleViewController displayList:peopleDataSource]; + [peopleViewController setDelegate:self]; + [peopleViewController displayList:self.peopleDataSource]; - [self.segmentedViewController initWithTitles:titles viewControllers:@[roomsViewController, peopleViewController] defaultSelected:0]; + self.segmentedViewController = [SegmentedViewController segmentedViewController]; + [self.segmentedViewController initWithTitles:@[[VectorL10n titleRooms], [VectorL10n titlePeople]] + viewControllers:@[roomsViewController, peopleViewController] defaultSelected:0]; [self addChildViewController:self.segmentedViewController]; - [self.contentView addSubview:self.segmentedViewController.view]; + [self.contentView vc_addSubViewMatchingParent:self.segmentedViewController.view]; [self.segmentedViewController didMoveToParentViewController:self]; - - [self autoPinSubviewEdges:self.segmentedViewController.view toSuperviewEdges:self.contentView]; } - (void)configureFallbackViewController { FallbackViewController *fallbackVC = [FallbackViewController new]; [self addChildViewController:fallbackVC]; - [self.contentView addSubview:fallbackVC.view]; + [self.contentView vc_addSubViewMatchingParent:fallbackVC.view]; [fallbackVC didMoveToParentViewController:self]; - - [self autoPinSubviewEdges:fallbackVC.view toSuperviewEdges:self.contentView]; } -- (void)autoPinSubviewEdges:(UIView *)subview toSuperviewEdges:(UIView *)superview +- (void)resetContentView { - subview.translatesAutoresizingMaskIntoConstraints = NO; - NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeWidth multiplier:1 constant:0]; - widthConstraint.active = YES; - NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeHeight multiplier:1 constant:0]; - heightConstraint.active = YES; - NSLayoutConstraint *centerXConstraint = [NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]; - centerXConstraint.active = YES; - NSLayoutConstraint *centerYConstraint = [NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]; - centerYConstraint.active = YES; + NSArray *subviews = self.contentView.subviews; + for (UIView *subview in subviews) + { + [subview removeFromSuperview]; + } + + if (self.segmentedViewController) + { + [self.segmentedViewController removeFromParentViewController]; + + [self.segmentedViewController destroy]; + self.segmentedViewController = nil; + } } #pragma mark - Actions -- (IBAction)close:(UIButton *)sender +- (IBAction)onCancelButtonTap:(UIButton *)sender { - [self dismissViewControllerAnimated:YES completion:^{ - [[ShareExtensionManager sharedManager] terminateExtensionCanceled:YES]; - }]; + [self.delegate shareViewControllerDidRequestDismissal:self]; } +- (IBAction)onShareButtonTap:(UIButton *)sender +{ + +} @end diff --git a/RiotShareExtension/Modules/Share/ShareViewController.xib b/RiotShareExtension/Modules/Share/ShareViewController.xib index 04a1316c8..1718d4501 100644 --- a/RiotShareExtension/Modules/Share/ShareViewController.xib +++ b/RiotShareExtension/Modules/Share/ShareViewController.xib @@ -9,8 +9,10 @@ + + @@ -21,40 +23,50 @@ - + - + - + - + + + + - - + + + + - + @@ -64,9 +76,7 @@ - - @@ -74,21 +84,12 @@ - - - - - - - - - - + + + + - - - diff --git a/RiotShareExtension/SupportingFiles/Info.plist b/RiotShareExtension/SupportingFiles/Info.plist index f0a876166..fce6e4c3f 100644 --- a/RiotShareExtension/SupportingFiles/Info.plist +++ b/RiotShareExtension/SupportingFiles/Info.plist @@ -43,7 +43,7 @@ NSExtensionPointIdentifier com.apple.share-services NSExtensionPrincipalClass - SharePresentingViewController + ShareExtensionRootViewController diff --git a/RiotShareExtension/SupportingFiles/RiotShareExtension-Bridging-Header.h b/RiotShareExtension/SupportingFiles/RiotShareExtension-Bridging-Header.h index d3bf536b2..9d1bfc7d9 100644 --- a/RiotShareExtension/SupportingFiles/RiotShareExtension-Bridging-Header.h +++ b/RiotShareExtension/SupportingFiles/RiotShareExtension-Bridging-Header.h @@ -4,3 +4,4 @@ #import "ThemeService.h" #import "AvatarGenerator.h" +#import "BuildInfo.h" diff --git a/RiotShareExtension/target.yml b/RiotShareExtension/target.yml index 9e61389f1..81e952ff5 100644 --- a/RiotShareExtension/target.yml +++ b/RiotShareExtension/target.yml @@ -52,9 +52,11 @@ targets: - path: ../Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift - path: ../Riot/Managers/KeyValueStorage - path: ../Riot/Managers/Settings/RiotSettings.swift + - path: ../Riot/Managers/AppInfo/ - path: ../Riot/Categories/UIColor.swift - path: ../Riot/Categories/UISearchBar.swift - path: ../Riot/Categories/String.swift + - path: ../Riot/Categories/UIView.swift - path: ../Riot/Modules/Common/Recents/CellData/RecentCellData.m - path: ../Riot/PropertyWrappers/UserDefaultsBackedPropertyWrapper.swift - path: ../Riot/Generated/Strings.swift From 64df0265bc5ca00d9ec30738775f1981108dd864 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 14 Oct 2021 12:57:32 +0300 Subject: [PATCH 203/276] vector-im/element-ios/issues/5009 - Moved files around, cleaned up imports and logs. --- Riot/Modules/Room/RoomViewController.m | 20 +++--- Riot/target.yml | 3 +- .../ShareExtensionRootViewController.h | 0 .../ShareExtensionRootViewController.m | 23 ++++--- .../FallbackViewController.h | 0 .../FallbackViewController.m | 0 .../FallbackViewController.xib | 0 .../RecentRoomTableViewCell.h | 0 .../RecentRoomTableViewCell.m | 0 .../RecentRoomTableViewCell.xib | 0 .../RoomsListViewController.h | 0 .../RoomsListViewController.m | 5 +- .../RoomsListViewController.xib | 0 .../DataSources => Shared}/ShareDataSource.h | 0 .../DataSources => Shared}/ShareDataSource.m | 1 - .../ShareManager.h} | 16 +++-- .../ShareManager.m} | 61 +++++++++---------- .../Share => Shared}/ShareViewController.h | 0 .../Share => Shared}/ShareViewController.m | 35 +++++------ .../Share => Shared}/ShareViewController.xib | 0 20 files changed, 76 insertions(+), 88 deletions(-) rename RiotShareExtension/{Modules/Main => }/ShareExtensionRootViewController.h (100%) rename RiotShareExtension/{Modules/Main => }/ShareExtensionRootViewController.m (70%) rename RiotShareExtension/{Modules/Fallback => Shared}/FallbackViewController.h (100%) rename RiotShareExtension/{Modules/Fallback => Shared}/FallbackViewController.m (100%) rename RiotShareExtension/{Modules/Fallback => Shared}/FallbackViewController.xib (100%) rename RiotShareExtension/{Modules/Share/Listing/Views => Shared}/RecentRoomTableViewCell.h (100%) rename RiotShareExtension/{Modules/Share/Listing/Views => Shared}/RecentRoomTableViewCell.m (100%) rename RiotShareExtension/{Modules/Share/Listing/Views => Shared}/RecentRoomTableViewCell.xib (100%) rename RiotShareExtension/{Modules/Share/Listing => Shared}/RoomsListViewController.h (100%) rename RiotShareExtension/{Modules/Share/Listing => Shared}/RoomsListViewController.m (98%) rename RiotShareExtension/{Modules/Share/Listing => Shared}/RoomsListViewController.xib (100%) rename RiotShareExtension/{Modules/Share/DataSources => Shared}/ShareDataSource.h (100%) rename RiotShareExtension/{Modules/Share/DataSources => Shared}/ShareDataSource.m (99%) rename RiotShareExtension/{Managers/ShareExtensionManager.h => Shared/ShareManager.h} (69%) rename RiotShareExtension/{Managers/ShareExtensionManager.m => Shared/ShareManager.m} (95%) rename RiotShareExtension/{Modules/Share => Shared}/ShareViewController.h (100%) rename RiotShareExtension/{Modules/Share => Shared}/ShareViewController.m (93%) rename RiotShareExtension/{Modules/Share => Shared}/ShareViewController.xib (100%) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 7158ae820..bd7266eb9 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -108,7 +108,7 @@ #import "AvatarGenerator.h" #import "Tools.h" #import "WidgetManager.h" -#import "ShareExtensionManager.h" +#import "ShareManager.h" #import "GBDeviceInfo_iOS.h" @@ -252,7 +252,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @property (nonatomic, strong) VoiceMessageController *voiceMessageController; @property (nonatomic, strong) SpaceDetailPresenter *spaceDetailPresenter; -@property (nonatomic, strong) ShareExtensionManager *shareExtensionManager; +@property (nonatomic, strong) ShareManager *shareManager; @property (nonatomic, strong) UserSuggestionCoordinatorBridge *userSuggestionCoordinator; @property (nonatomic, weak) IBOutlet UIView *userSuggestionContainerView; @@ -3201,17 +3201,17 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; NSExtensionItem *item = [[NSExtensionItem alloc] init]; item.attachments = @[[[NSItemProvider alloc] initWithItem:selectedComponent.textMessage typeIdentifier:(__bridge NSString *)kUTTypeText]]; - self.shareExtensionManager = [[ShareExtensionManager alloc] initWithShareExtensionContext:nil - extensionItems:@[item]]; + self.shareManager = [[ShareManager alloc] initWithShareExtensionContext:nil + extensionItems:@[item]]; MXWeakify(self); - [self.shareExtensionManager setCompletionCallback:^(ShareExtensionManagerResult result) { + [self.shareManager setCompletionCallback:^(ShareManagerResult result) { MXStrongifyAndReturnIfNil(self); [attachment onShareEnded]; [self dismissViewControllerAnimated:YES completion:nil]; }]; - [self presentViewController:self.shareExtensionManager.mainViewController animated:YES completion:nil]; + [self presentViewController:self.shareManager.mainViewController animated:YES completion:nil]; }]]; if (!isJitsiCallEvent) @@ -3414,17 +3414,17 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; NSExtensionItem *item = [[NSExtensionItem alloc] init]; item.attachments = @[[[NSItemProvider alloc] initWithItem:fileURL typeIdentifier:attachmentTypeToIdentifier[@(attachment.type)]]]; - self.shareExtensionManager = [[ShareExtensionManager alloc] initWithShareExtensionContext:nil - extensionItems:@[item]]; + self.shareManager = [[ShareManager alloc] initWithShareExtensionContext:nil + extensionItems:@[item]]; MXWeakify(self); - [self.shareExtensionManager setCompletionCallback:^(ShareExtensionManagerResult result) { + [self.shareManager setCompletionCallback:^(ShareManagerResult result) { MXStrongifyAndReturnIfNil(self); [attachment onShareEnded]; [self dismissViewControllerAnimated:YES completion:nil]; }]; - [self presentViewController:self.shareExtensionManager.mainViewController animated:YES completion:nil]; + [self presentViewController:self.shareManager.mainViewController animated:YES completion:nil]; } failure:^(NSError *error) { [self showError:error]; [self stopActivityIndicator]; diff --git a/Riot/target.yml b/Riot/target.yml index a4a61ac4c..0e9f722da 100644 --- a/Riot/target.yml +++ b/Riot/target.yml @@ -66,8 +66,7 @@ targets: excludes: - "Modules/Room/EmojiPicker/Data/EmojiMart/EmojiJSONStore.swift" - "**/*.strings" # Exclude all strings files - - path: ../RiotShareExtension/Managers - - path: ../RiotShareExtension/Modules + - path: ../RiotShareExtension/Shared # Add separately localizable files # Once a language has enough translations (>80%), it must be declared here diff --git a/RiotShareExtension/Modules/Main/ShareExtensionRootViewController.h b/RiotShareExtension/ShareExtensionRootViewController.h similarity index 100% rename from RiotShareExtension/Modules/Main/ShareExtensionRootViewController.h rename to RiotShareExtension/ShareExtensionRootViewController.h diff --git a/RiotShareExtension/Modules/Main/ShareExtensionRootViewController.m b/RiotShareExtension/ShareExtensionRootViewController.m similarity index 70% rename from RiotShareExtension/Modules/Main/ShareExtensionRootViewController.m rename to RiotShareExtension/ShareExtensionRootViewController.m index ad5178c41..bd5c5ad6b 100644 --- a/RiotShareExtension/Modules/Main/ShareExtensionRootViewController.m +++ b/RiotShareExtension/ShareExtensionRootViewController.m @@ -15,8 +15,7 @@ */ #import "ShareExtensionRootViewController.h" -#import "ShareViewController.h" -#import "ShareExtensionManager.h" +#import "ShareManager.h" #import "ThemeService.h" #ifdef IS_SHARE_EXTENSION @@ -27,7 +26,7 @@ @interface ShareExtensionRootViewController () -@property (nonatomic, strong, readonly) ShareExtensionManager *shareExtensionManager; +@property (nonatomic, strong, readonly) ShareManager *shareManager; @end @@ -39,24 +38,24 @@ [ThemeService.shared setThemeId:RiotSettings.shared.userInterfaceTheme]; - _shareExtensionManager = [[ShareExtensionManager alloc] initWithShareExtensionContext:self.extensionContext + _shareManager = [[ShareManager alloc] initWithShareExtensionContext:self.extensionContext extensionItems:self.extensionContext.inputItems]; MXWeakify(self); - [_shareExtensionManager setCompletionCallback:^(ShareExtensionManagerResult result) { + [_shareManager setCompletionCallback:^(ShareManagerResult result) { MXStrongifyAndReturnIfNil(self); switch (result) { - case ShareExtensionManagerResultFinished: + case ShareManagerResultFinished: [self.extensionContext completeRequestReturningItems:nil completionHandler:nil]; [self _dismiss]; break; - case ShareExtensionManagerResultCancelled: + case ShareManagerResultCancelled: [self.extensionContext cancelRequestWithError:[NSError errorWithDomain:@"MXUserCancelErrorDomain" code:4201 userInfo:nil]]; [self _dismiss]; break; - case ShareExtensionManagerResultFailed: + case ShareManagerResultFailed: [self.extensionContext cancelRequestWithError:[NSError errorWithDomain:@"MXFailureErrorDomain" code:500 userInfo:nil]]; [self _dismiss]; break; @@ -73,7 +72,7 @@ { [super viewWillAppear:animated]; - [self presentViewController:self.shareExtensionManager.mainViewController animated:YES completion:nil]; + [self presentViewController:self.shareManager.mainViewController animated:YES completion:nil]; } #pragma mark - Private @@ -83,9 +82,9 @@ [self dismissViewControllerAnimated:true completion:^{ [self.presentingViewController dismissViewControllerAnimated:false completion:nil]; -// // FIXME: Share extension memory usage increase when launched several times and then crash due to some memory leaks. -// // For now, we force the share extension to exit and free memory. -// [NSException raise:@"Kill the app extension" format:@"Free memory used by share extension"]; + // FIXME: Share extension memory usage increase when launched several times and then crash due to some memory leaks. + // For now, we force the share extension to exit and free memory. + [NSException raise:@"Kill the app extension" format:@"Free memory used by share extension"]; }]; } diff --git a/RiotShareExtension/Modules/Fallback/FallbackViewController.h b/RiotShareExtension/Shared/FallbackViewController.h similarity index 100% rename from RiotShareExtension/Modules/Fallback/FallbackViewController.h rename to RiotShareExtension/Shared/FallbackViewController.h diff --git a/RiotShareExtension/Modules/Fallback/FallbackViewController.m b/RiotShareExtension/Shared/FallbackViewController.m similarity index 100% rename from RiotShareExtension/Modules/Fallback/FallbackViewController.m rename to RiotShareExtension/Shared/FallbackViewController.m diff --git a/RiotShareExtension/Modules/Fallback/FallbackViewController.xib b/RiotShareExtension/Shared/FallbackViewController.xib similarity index 100% rename from RiotShareExtension/Modules/Fallback/FallbackViewController.xib rename to RiotShareExtension/Shared/FallbackViewController.xib diff --git a/RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.h b/RiotShareExtension/Shared/RecentRoomTableViewCell.h similarity index 100% rename from RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.h rename to RiotShareExtension/Shared/RecentRoomTableViewCell.h diff --git a/RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.m b/RiotShareExtension/Shared/RecentRoomTableViewCell.m similarity index 100% rename from RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.m rename to RiotShareExtension/Shared/RecentRoomTableViewCell.m diff --git a/RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.xib b/RiotShareExtension/Shared/RecentRoomTableViewCell.xib similarity index 100% rename from RiotShareExtension/Modules/Share/Listing/Views/RecentRoomTableViewCell.xib rename to RiotShareExtension/Shared/RecentRoomTableViewCell.xib diff --git a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.h b/RiotShareExtension/Shared/RoomsListViewController.h similarity index 100% rename from RiotShareExtension/Modules/Share/Listing/RoomsListViewController.h rename to RiotShareExtension/Shared/RoomsListViewController.h diff --git a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m b/RiotShareExtension/Shared/RoomsListViewController.m similarity index 98% rename from RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m rename to RiotShareExtension/Shared/RoomsListViewController.m index 7a9e198f0..8f387a4d4 100644 --- a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m +++ b/RiotShareExtension/Shared/RoomsListViewController.m @@ -14,13 +14,12 @@ limitations under the License. */ +#import + #import "RoomsListViewController.h" #import "RecentRoomTableViewCell.h" -#import "NSBundle+MatrixKit.h" -#import "ShareExtensionManager.h" #import "RecentCellData.h" #import "ThemeService.h" -#import #ifdef IS_SHARE_EXTENSION #import "RiotShareExtension-Swift.h" diff --git a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.xib b/RiotShareExtension/Shared/RoomsListViewController.xib similarity index 100% rename from RiotShareExtension/Modules/Share/Listing/RoomsListViewController.xib rename to RiotShareExtension/Shared/RoomsListViewController.xib diff --git a/RiotShareExtension/Modules/Share/DataSources/ShareDataSource.h b/RiotShareExtension/Shared/ShareDataSource.h similarity index 100% rename from RiotShareExtension/Modules/Share/DataSources/ShareDataSource.h rename to RiotShareExtension/Shared/ShareDataSource.h diff --git a/RiotShareExtension/Modules/Share/DataSources/ShareDataSource.m b/RiotShareExtension/Shared/ShareDataSource.m similarity index 99% rename from RiotShareExtension/Modules/Share/DataSources/ShareDataSource.m rename to RiotShareExtension/Shared/ShareDataSource.m index 802425ebc..b88a846a4 100644 --- a/RiotShareExtension/Modules/Share/DataSources/ShareDataSource.m +++ b/RiotShareExtension/Shared/ShareDataSource.m @@ -15,7 +15,6 @@ */ #import "ShareDataSource.h" -#import "ShareExtensionManager.h" #import "RecentRoomTableViewCell.h" @interface ShareDataSource () diff --git a/RiotShareExtension/Managers/ShareExtensionManager.h b/RiotShareExtension/Shared/ShareManager.h similarity index 69% rename from RiotShareExtension/Managers/ShareExtensionManager.h rename to RiotShareExtension/Shared/ShareManager.h index e8dc215e6..04b49bf48 100644 --- a/RiotShareExtension/Managers/ShareExtensionManager.h +++ b/RiotShareExtension/Shared/ShareManager.h @@ -16,17 +16,15 @@ #import -@class ShareExtensionManager; - -typedef NS_ENUM(NSUInteger, ShareExtensionManagerResult) { - ShareExtensionManagerResultFinished, - ShareExtensionManagerResultCancelled, - ShareExtensionManagerResultFailed +typedef NS_ENUM(NSUInteger, ShareManagerResult) { + ShareManagerResultFinished, + ShareManagerResultCancelled, + ShareManagerResultFailed }; -@interface ShareExtensionManager : NSObject +@interface ShareManager : NSObject -@property (nonatomic, copy) void (^completionCallback)(ShareExtensionManagerResult); +@property (nonatomic, copy) void (^completionCallback)(ShareManagerResult); - (instancetype)initWithShareExtensionContext:(NSExtensionContext *)shareExtensionContext extensionItems:(NSArray *)extensionItems; @@ -36,7 +34,7 @@ typedef NS_ENUM(NSUInteger, ShareExtensionManagerResult) { @end -@interface NSItemProvider (ShareExtensionManager) +@interface NSItemProvider (ShareManager) @property BOOL isLoaded; diff --git a/RiotShareExtension/Managers/ShareExtensionManager.m b/RiotShareExtension/Shared/ShareManager.m similarity index 95% rename from RiotShareExtension/Managers/ShareExtensionManager.m rename to RiotShareExtension/Shared/ShareManager.m index 6d2ab3cea..2aec891f4 100644 --- a/RiotShareExtension/Managers/ShareExtensionManager.m +++ b/RiotShareExtension/Shared/ShareManager.m @@ -14,16 +14,16 @@ limitations under the License. */ -#import "ShareExtensionManager.h" -#import "ShareViewController.h" -#import "ShareDataSource.h" +@import MobileCoreServices; + +#import "objc/runtime.h" +#import #import -@import MobileCoreServices; -#import "objc/runtime.h" -#include -#import +#import "ShareManager.h" +#import "ShareViewController.h" +#import "ShareDataSource.h" #ifdef IS_SHARE_EXTENSION #import "RiotShareExtension-Swift.h" @@ -41,7 +41,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) ImageCompressionModeLarge }; -@interface ShareExtensionManager () +@interface ShareManager () @property (nonatomic, strong, readonly) NSExtensionContext *shareExtensionContext; @property (nonatomic, strong, readonly) NSArray *extensionItems; @@ -60,7 +60,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) @end -@implementation ShareExtensionManager +@implementation ShareManager - (instancetype)initWithShareExtensionContext:(NSExtensionContext *)shareExtensionContext extensionItems:(NSArray *)extensionItems @@ -147,7 +147,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) }]; } failure:^(NSError *error) { - MXLogError(@"[ShareExtensionManager] Failed preparign matrix session"); + MXLogError(@"[ShareManager] Failed preparign matrix session"); }]; } @@ -155,7 +155,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) { if (self.completionCallback) { - self.completionCallback(ShareExtensionManagerResultCancelled); + self.completionCallback(ShareManagerResultCancelled); } } @@ -328,7 +328,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) } else { - MXLogDebug(@"[ShareExtensionManager] sendContentToRoom: failed to loadItemForTypeIdentifier. Error: %@", error); + MXLogError(@"[ShareManager] sendContentToRoom: failed to loadItemForTypeIdentifier. Error: %@", error); dispatch_group_leave(requestsGroup); } @@ -423,7 +423,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) { if (self.completionCallback) { - self.completionCallback(ShareExtensionManagerResultFinished); + self.completionCallback(ShareManagerResultFinished); } } }); @@ -439,7 +439,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) if (self.completionCallback) { - self.completionCallback(ShareExtensionManagerResultFailed); + self.completionCallback(ShareManagerResultFailed); } }]; @@ -683,7 +683,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) self.imageCompressionMode = ImageCompressionModeNone; } - MXLogDebug(@"[ShareExtensionManager] Send %lu image(s) without compression prompt using compression mode: %ld", (unsigned long)self.pendingImages.count, (long)self.imageCompressionMode); + MXLogDebug(@"[ShareManager] Send %lu image(s) without compression prompt using compression mode: %ld", (unsigned long)self.pendingImages.count, (long)self.imageCompressionMode); if (shareBlock) { @@ -874,8 +874,8 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) NSUInteger imageWidth = compressionSize.imageSize.width; NSUInteger imageHeight = compressionSize.imageSize.height; - MXLogDebug(@"[ShareExtensionManager] User choose image compression with output size %lu x %lu (output file size: %@)", (unsigned long)imageWidth, (unsigned long)imageHeight, fileSize); - MXLogDebug(@"[ShareExtensionManager] Number of images to send: %lu", (unsigned long)self.pendingImages.count); + MXLogDebug(@"[ShareManager] User choose image compression with output size %lu x %lu (output file size: %@)", (unsigned long)imageWidth, (unsigned long)imageHeight, fileSize); + MXLogDebug(@"[ShareManager] Number of images to send: %lu", (unsigned long)self.pendingImages.count); } // Log memory usage. @@ -894,11 +894,11 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) if (kerr == KERN_SUCCESS) { - MXLogDebug(@"[ShareExtensionManager] Memory in use (in MB): %f", memoryUsedInMegabytes); + MXLogDebug(@"[ShareManager] Memory in use (in MB): %f", memoryUsedInMegabytes); } else { - MXLogDebug(@"[ShareExtensionManager] Error with task_info(): %s", mach_error_string(kerr)); + MXLogDebug(@"[ShareManager] Error with task_info(): %s", mach_error_string(kerr)); } } @@ -940,7 +940,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) - (void)didReceiveMemoryWarning:(NSNotification*)notification { - MXLogDebug(@"[ShareExtensionManager] Did receive memory warning"); + MXLogDebug(@"[ShareManager] Did receive memory warning"); [self logMemoryUsage]; } @@ -951,7 +951,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) [self didStartSendingToRoom:room]; if (!text) { - MXLogDebug(@"[ShareExtensionManager] loadItemForTypeIdentifier: failed."); + MXLogError(@"[ShareManager] loadItemForTypeIdentifier: failed."); if (failureBlock) { failureBlock(nil); @@ -965,7 +965,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) successBlock(); } } failure:^(NSError *error) { - MXLogDebug(@"[ShareExtensionManager] sendTextMessage failed."); + MXLogError(@"[ShareManager] sendTextMessage failed with error %@", error); if (failureBlock) { failureBlock(error); @@ -978,7 +978,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) [self didStartSendingToRoom:room]; if (!fileUrl) { - MXLogDebug(@"[ShareExtensionManager] loadItemForTypeIdentifier: failed."); + MXLogError(@"[ShareManager] loadItemForTypeIdentifier: failed."); if (failureBlock) { failureBlock(nil); @@ -997,7 +997,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) successBlock(); } } failure:^(NSError *error) { - MXLogDebug(@"[ShareExtensionManager] sendFile failed."); + MXLogError(@"[ShareManager] sendFile failed with error %@", error); if (failureBlock) { failureBlock(error); @@ -1035,7 +1035,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) // Sanity check if (!mimeType) { - MXLogDebug(@"[ShareExtensionManager] sendImage failed. Cannot determine MIME type of %@", itemProvider); + MXLogError(@"[ShareManager] sendImage failed. Cannot determine MIME type of %@", itemProvider); if (failureBlock) { failureBlock(nil); @@ -1121,8 +1121,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) successBlock(); } } failure:^(NSError *error) { - - MXLogDebug(@"[ShareExtensionManager] sendImage failed."); + MXLogError(@"[ShareManager] sendImage failed with error %@", error); if (failureBlock) { failureBlock(error); @@ -1135,7 +1134,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) { if (imageDatas.count == 0 || imageDatas.count != itemProviders.count) { - MXLogDebug(@"[ShareExtensionManager] sendImages: no images to send."); + MXLogError(@"[ShareManager] sendImages: no images to send."); if (failureBlock) { @@ -1216,7 +1215,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) [self didStartSendingToRoom:room]; if (!videoLocalUrl) { - MXLogDebug(@"[ShareExtensionManager] loadItemForTypeIdentifier: failed."); + MXLogError(@"[ShareManager] loadItemForTypeIdentifier: failed."); if (failureBlock) { failureBlock(nil); @@ -1239,7 +1238,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) successBlock(); } } failure:^(NSError *error) { - MXLogDebug(@"[ShareExtensionManager] sendVideo failed."); + MXLogError(@"[ShareManager] Failed sending video with error %@", error); if (failureBlock) { failureBlock(error); @@ -1253,7 +1252,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) @end -@implementation NSItemProvider (ShareExtensionManager) +@implementation NSItemProvider (ShareManager) - (void)setIsLoaded:(BOOL)isLoaded { diff --git a/RiotShareExtension/Modules/Share/ShareViewController.h b/RiotShareExtension/Shared/ShareViewController.h similarity index 100% rename from RiotShareExtension/Modules/Share/ShareViewController.h rename to RiotShareExtension/Shared/ShareViewController.h diff --git a/RiotShareExtension/Modules/Share/ShareViewController.m b/RiotShareExtension/Shared/ShareViewController.m similarity index 93% rename from RiotShareExtension/Modules/Share/ShareViewController.m rename to RiotShareExtension/Shared/ShareViewController.m index 21cce2d00..148069665 100644 --- a/RiotShareExtension/Modules/Share/ShareViewController.m +++ b/RiotShareExtension/Shared/ShareViewController.m @@ -19,7 +19,6 @@ #import "RoomsListViewController.h" #import "FallbackViewController.h" #import "ShareDataSource.h" -#import "ShareExtensionManager.h" #import "ThemeService.h" @@ -96,6 +95,21 @@ [self configureViews]; } +- (void)showProgressIndicator +{ + if (!self.hudView) + { + self.parentViewController.view.userInteractionEnabled = NO; + self.hudView = [MXKPieChartHUD showLoadingHudOnView:self.view WithMessage:[VectorL10n sending]]; + [self.hudView setProgress:0.0]; + } +} + +- (void)setProgress:(CGFloat)progress +{ + [self.hudView setProgress:progress]; +} + #pragma mark - MXKRecentListViewControllerDelegate - (void)recentListViewController:(MXKRecentListViewController *)recentListViewController @@ -111,25 +125,6 @@ [self.delegate shareViewControllerDidRequestShare:self forRoomIdentifier:childInfo.childRoomId]; } -#pragma mark - ShareExtensionManagerDelegate - -- (void)showProgressIndicator -{ - dispatch_async(dispatch_get_main_queue(), ^{ - if (!self.hudView) - { - self.parentViewController.view.userInteractionEnabled = NO; - self.hudView = [MXKPieChartHUD showLoadingHudOnView:self.view WithMessage:[VectorL10n sending]]; - [self.hudView setProgress:0.0]; - } - }); -} - -- (void)setProgress:(CGFloat)progress -{ - [self.hudView setProgress:progress]; -} - #pragma mark - Private - (void)configureViews diff --git a/RiotShareExtension/Modules/Share/ShareViewController.xib b/RiotShareExtension/Shared/ShareViewController.xib similarity index 100% rename from RiotShareExtension/Modules/Share/ShareViewController.xib rename to RiotShareExtension/Shared/ShareViewController.xib From 9fffe41fdd8a9edf58ab603accd6de7c726b187d Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 14 Oct 2021 14:35:36 +0300 Subject: [PATCH 204/276] vector-im/element-ios/issues/5009 - Fixed share extension setup and cleaned up code. --- Riot/Modules/Room/RoomViewController.m | 6 +- .../ShareExtensionRootViewController.m | 65 +- RiotShareExtension/Shared/ShareManager.h | 7 +- RiotShareExtension/Shared/ShareManager.m | 621 ++++++------------ 4 files changed, 247 insertions(+), 452 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index bd7266eb9..928b44a88 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -3201,8 +3201,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; NSExtensionItem *item = [[NSExtensionItem alloc] init]; item.attachments = @[[[NSItemProvider alloc] initWithItem:selectedComponent.textMessage typeIdentifier:(__bridge NSString *)kUTTypeText]]; - self.shareManager = [[ShareManager alloc] initWithShareExtensionContext:nil - extensionItems:@[item]]; + self.shareManager = [[ShareManager alloc] initWithItems:@[item]]; MXWeakify(self); [self.shareManager setCompletionCallback:^(ShareManagerResult result) { @@ -3414,8 +3413,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; NSExtensionItem *item = [[NSExtensionItem alloc] init]; item.attachments = @[[[NSItemProvider alloc] initWithItem:fileURL typeIdentifier:attachmentTypeToIdentifier[@(attachment.type)]]]; - self.shareManager = [[ShareManager alloc] initWithShareExtensionContext:nil - extensionItems:@[item]]; + self.shareManager = [[ShareManager alloc] initWithItems:@[item]]; MXWeakify(self); [self.shareManager setCompletionCallback:^(ShareManagerResult result) { diff --git a/RiotShareExtension/ShareExtensionRootViewController.m b/RiotShareExtension/ShareExtensionRootViewController.m index bd5c5ad6b..b523d0fb6 100644 --- a/RiotShareExtension/ShareExtensionRootViewController.m +++ b/RiotShareExtension/ShareExtensionRootViewController.m @@ -32,45 +32,36 @@ @implementation ShareExtensionRootViewController -- (instancetype)init +- (void)viewDidLoad { - if(self = [super init]) { - - [ThemeService.shared setThemeId:RiotSettings.shared.userInterfaceTheme]; - - _shareManager = [[ShareManager alloc] initWithShareExtensionContext:self.extensionContext - extensionItems:self.extensionContext.inputItems]; - - MXWeakify(self); - [_shareManager setCompletionCallback:^(ShareManagerResult result) { - MXStrongifyAndReturnIfNil(self); - - switch (result) - { - case ShareManagerResultFinished: - [self.extensionContext completeRequestReturningItems:nil completionHandler:nil]; - [self _dismiss]; - break; - case ShareManagerResultCancelled: - [self.extensionContext cancelRequestWithError:[NSError errorWithDomain:@"MXUserCancelErrorDomain" code:4201 userInfo:nil]]; - [self _dismiss]; - break; - case ShareManagerResultFailed: - [self.extensionContext cancelRequestWithError:[NSError errorWithDomain:@"MXFailureErrorDomain" code:500 userInfo:nil]]; - [self _dismiss]; - break; - default: - break; - } - }]; - } + [super viewDidLoad]; - return self; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; + [ThemeService.shared setThemeId:RiotSettings.shared.userInterfaceTheme]; + + _shareManager = [[ShareManager alloc] initWithItems:self.extensionContext.inputItems]; + + MXWeakify(self); + [_shareManager setCompletionCallback:^(ShareManagerResult result) { + MXStrongifyAndReturnIfNil(self); + + switch (result) + { + case ShareManagerResultFinished: + [self.extensionContext completeRequestReturningItems:nil completionHandler:nil]; + [self _dismiss]; + break; + case ShareManagerResultCancelled: + [self.extensionContext cancelRequestWithError:[NSError errorWithDomain:@"MXUserCancelErrorDomain" code:4201 userInfo:nil]]; + [self _dismiss]; + break; + case ShareManagerResultFailed: + [self.extensionContext cancelRequestWithError:[NSError errorWithDomain:@"MXFailureErrorDomain" code:500 userInfo:nil]]; + [self _dismiss]; + break; + default: + break; + } + }]; [self presentViewController:self.shareManager.mainViewController animated:YES completion:nil]; } diff --git a/RiotShareExtension/Shared/ShareManager.h b/RiotShareExtension/Shared/ShareManager.h index 04b49bf48..b81233883 100644 --- a/RiotShareExtension/Shared/ShareManager.h +++ b/RiotShareExtension/Shared/ShareManager.h @@ -16,6 +16,8 @@ #import +NS_ASSUME_NONNULL_BEGIN + typedef NS_ENUM(NSUInteger, ShareManagerResult) { ShareManagerResultFinished, ShareManagerResultCancelled, @@ -26,8 +28,7 @@ typedef NS_ENUM(NSUInteger, ShareManagerResult) { @property (nonatomic, copy) void (^completionCallback)(ShareManagerResult); -- (instancetype)initWithShareExtensionContext:(NSExtensionContext *)shareExtensionContext - extensionItems:(NSArray *)extensionItems; +- (instancetype)initWithItems:(NSArray *)items; - (UIViewController *)mainViewController; @@ -39,3 +40,5 @@ typedef NS_ENUM(NSUInteger, ShareManagerResult) { @property BOOL isLoaded; @end + +NS_ASSUME_NONNULL_END diff --git a/RiotShareExtension/Shared/ShareManager.m b/RiotShareExtension/Shared/ShareManager.m index 2aec891f4..c34b841d1 100644 --- a/RiotShareExtension/Shared/ShareManager.m +++ b/RiotShareExtension/Shared/ShareManager.m @@ -43,13 +43,12 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) @interface ShareManager () -@property (nonatomic, strong, readonly) NSExtensionContext *shareExtensionContext; @property (nonatomic, strong, readonly) NSArray *extensionItems; +@property (nonatomic, strong, readonly) ShareViewController *shareViewController; @property (nonatomic, strong, readonly) NSMutableArray *pendingImages; @property (nonatomic, strong, readonly) NSMutableDictionary *imageUploadProgresses; @property (nonatomic, strong, readonly) id configuration; -@property (nonatomic, strong, readonly) ShareViewController *shareViewController; @property (nonatomic, strong) MXKAccount *userAccount; @property (nonatomic, strong) MXFileStore *fileStore; @@ -62,13 +61,11 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) @implementation ShareManager -- (instancetype)initWithShareExtensionContext:(NSExtensionContext *)shareExtensionContext - extensionItems:(NSArray *)extensionItems +- (instancetype)initWithItems:(NSArray *)items { if (self = [super init]) { - _shareExtensionContext = shareExtensionContext; - _extensionItems = extensionItems; + _extensionItems = items; _pendingImages = [NSMutableArray array]; _imageUploadProgresses = [NSMutableDictionary dictionary]; @@ -78,7 +75,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(checkUserAccount) name:NSExtensionHostWillEnterForegroundNotification object:nil]; [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; - _configuration = [CommonConfiguration new]; + _configuration = [[CommonConfiguration alloc] init]; [_configuration setupSettings]; // NSLog -> console.log file when not debugging the app @@ -131,21 +128,12 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) [session setStore:self.fileStore success:^{ MXStrongifyAndReturnIfNil(session); + session.crypto.warnOnUnknowDevices = NO; // Do not warn for unknown devices. We have cross-signing now + MXRoom *selectedRoom = [MXRoom loadRoomFromStore:self.fileStore withRoomId:roomIdentifier matrixSession:session]; - - // Do not warn for unknown devices. We have cross-signing now - session.crypto.warnOnUnknowDevices = NO; - - [self _sendContentToRoom:selectedRoom failureBlock:^(NSError* error) { - NSString *title = [VectorL10n roomEventFailedToSend]; - if ([error.domain isEqualToString:MXEncryptingErrorDomain]) - { - title = [VectorL10n shareExtensionFailedToEncrypt]; - } - - [self _showFailureAlert:title]; + [self sendContentToRoom:selectedRoom success:nil failure:^(NSError *error){ + [self showFailureAlert:[VectorL10n roomEventFailedToSend]]; }]; - } failure:^(NSError *error) { MXLogError(@"[ShareManager] Failed preparign matrix session"); }]; @@ -153,15 +141,12 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) - (void)shareViewControllerDidRequestDismissal:(ShareViewController *)shareViewController { - if (self.completionCallback) - { - self.completionCallback(ShareManagerResultCancelled); - } + self.completionCallback(ShareManagerResultCancelled); } #pragma mark - Private -- (void)_sendContentToRoom:(MXRoom *)room failureBlock:(void(^)(NSError *error))failureBlock +- (void)sendContentToRoom:(MXRoom *)room success:(void(^)(void))success failure:(void(^)(NSError *))failure { [self resetPendingData]; @@ -176,17 +161,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) NSMutableArray *pendingImagesItemProviders = [NSMutableArray new]; // Used to keep NSItemProvider associated to pending images (used only when all items are images). __block NSError *firstRequestError = nil; - __block NSMutableArray *returningExtensionItems = [NSMutableArray new]; - dispatch_group_t requestsGroup = dispatch_group_create(); - - void (^requestSuccess)(NSExtensionItem*) = ^(NSExtensionItem *extensionItem) { - if (extensionItem && ![returningExtensionItems containsObject:extensionItem]) - { - [returningExtensionItems addObject:extensionItem]; - } - - dispatch_group_leave(requestsGroup); - }; + dispatch_group_t dispatchGroup = dispatch_group_create(); void (^requestFailure)(NSError*) = ^(NSError *requestError) { if (requestError && !firstRequestError) @@ -194,242 +169,171 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) firstRequestError = requestError; } - dispatch_group_leave(requestsGroup); + dispatch_group_leave(dispatchGroup); }; - __weak typeof(self) weakSelf = self; - + MXWeakify(self); for (NSExtensionItem *item in self.extensionItems) { for (NSItemProvider *itemProvider in item.attachments) { if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeFileUrl]) { - dispatch_group_enter(requestsGroup); - + dispatch_group_enter(dispatchGroup); [itemProvider loadItemForTypeIdentifier:UTTypeFileUrl options:nil completionHandler:^(NSURL *fileUrl, NSError *error) { - - // Switch back on the main thread to handle correctly the UI change dispatch_async(dispatch_get_main_queue(), ^{ - - if (weakSelf) - { - typeof(self) self = weakSelf; - [self sendFileWithUrl:fileUrl - toRoom:room - successBlock:^{ - requestSuccess(item); - } failureBlock:requestFailure]; - } - + MXStrongifyAndReturnIfNil(self); + [self sendFileWithUrl:fileUrl toRoom:room successBlock:^{ + dispatch_group_leave(dispatchGroup); + } failureBlock:requestFailure]; }); }]; } else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeText]) { - dispatch_group_enter(requestsGroup); - - [itemProvider loadItemForTypeIdentifier:UTTypeText options:nil completionHandler:^(NSString *text, NSError * _Null_unspecified error) { - - // Switch back on the main thread to handle correctly the UI change + dispatch_group_enter(dispatchGroup); + [itemProvider loadItemForTypeIdentifier:UTTypeText options:nil completionHandler:^(NSString *text, NSError *error) { dispatch_async(dispatch_get_main_queue(), ^{ - - if (weakSelf) - { - typeof(self) self = weakSelf; - [self sendText:text - toRoom:room - successBlock:^{ - requestSuccess(item); - } failureBlock:requestFailure]; - } - + MXStrongifyAndReturnIfNil(self); + [self sendText:text toRoom:room successBlock:^{ + dispatch_group_leave(dispatchGroup); + } failureBlock:requestFailure]; }); }]; } else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeURL]) { - dispatch_group_enter(requestsGroup); - - [itemProvider loadItemForTypeIdentifier:UTTypeURL options:nil completionHandler:^(NSURL *url, NSError * _Null_unspecified error) { - - // Switch back on the main thread to handle correctly the UI change + dispatch_group_enter(dispatchGroup); + [itemProvider loadItemForTypeIdentifier:UTTypeURL options:nil completionHandler:^(NSURL *url, NSError *error) { dispatch_async(dispatch_get_main_queue(), ^{ - - if (weakSelf) - { - typeof(self) self = weakSelf; - [self sendText:url.absoluteString - toRoom:room - successBlock:^{ - requestSuccess(item); - } failureBlock:requestFailure]; - } - + MXStrongifyAndReturnIfNil(self); + [self sendText:url.absoluteString toRoom:room successBlock:^{ + dispatch_group_leave(dispatchGroup); + } failureBlock:requestFailure]; }); - }]; } else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeImage]) { - dispatch_group_enter(requestsGroup); - itemProvider.isLoaded = NO; - [itemProvider loadItemForTypeIdentifier:UTTypeImage options:nil completionHandler:^(id itemProviderItem, NSError *error) - { - if (weakSelf) - { - typeof(self) self = weakSelf; - itemProvider.isLoaded = YES; - - NSData *imageData; - - if ([(NSObject *)itemProviderItem isKindOfClass:[NSData class]]) - { - imageData = (NSData*)itemProviderItem; - } - else if ([(NSObject *)itemProviderItem isKindOfClass:[NSURL class]]) - { - NSURL *imageURL = (NSURL*)itemProviderItem; - imageData = [NSData dataWithContentsOfURL:imageURL]; - } - else if ([(NSObject *)itemProviderItem isKindOfClass:[UIImage class]]) - { - // An application can share directly an UIImage. - // The most common case is screenshot sharing without saving to file. - // As screenshot using PNG format when they are saved to file we also use PNG format when saving UIImage to NSData. - UIImage *image = (UIImage*)itemProviderItem; - imageData = UIImagePNGRepresentation(image); - } - - if (imageData) - { - if (areAllAttachmentsImages) - { - [self.pendingImages addObject:imageData]; - [pendingImagesItemProviders addObject:itemProvider]; - } - else - { - CGSize imageSize = [self imageSizeFromImageData:imageData]; - self.imageCompressionMode = ImageCompressionModeNone; - self.actualLargeSize = MAX(imageSize.width, imageSize.height); - - [self sendImageData:imageData - withProvider:itemProvider - toRoom:room - successBlock:^{ - requestSuccess(item); - } failureBlock:requestFailure]; - } - } - else - { - MXLogError(@"[ShareManager] sendContentToRoom: failed to loadItemForTypeIdentifier. Error: %@", error); - dispatch_group_leave(requestsGroup); - } - - // Only prompt for image resize if all items are images - // Ignore showMediaCompressionPrompt setting due to memory constraints with full size images. - if (areAllAttachmentsImages) - { - if ([self areAttachmentsFullyLoaded]) - { - UIAlertController *compressionPrompt = [self compressionPromptForPendingImagesWithShareBlock:^{ - [self sendImageDatas:self.pendingImages - withProviders:pendingImagesItemProviders - toRoom:room - successBlock:^{ - requestSuccess(item); - } failureBlock:requestFailure]; - }]; - - if (compressionPrompt) - { - [self presentCompressionPrompt:compressionPrompt]; - } - } - else - { - dispatch_group_leave(requestsGroup); - } - } - } - }]; + dispatch_group_enter(dispatchGroup); + [itemProvider loadItemForTypeIdentifier:UTTypeImage options:nil completionHandler:^(id itemProviderItem, NSError *error) { + MXStrongifyAndReturnIfNil(self); + + itemProvider.isLoaded = YES; + + NSData *imageData; + if ([(NSObject *)itemProviderItem isKindOfClass:[NSData class]]) + { + imageData = (NSData*)itemProviderItem; + } + else if ([(NSObject *)itemProviderItem isKindOfClass:[NSURL class]]) + { + NSURL *imageURL = (NSURL*)itemProviderItem; + imageData = [NSData dataWithContentsOfURL:imageURL]; + } + else if ([(NSObject *)itemProviderItem isKindOfClass:[UIImage class]]) + { + // An application can share directly an UIImage. + // The most common case is screenshot sharing without saving to file. + // As screenshot using PNG format when they are saved to file we also use PNG format when saving UIImage to NSData. + UIImage *image = (UIImage*)itemProviderItem; + imageData = UIImagePNGRepresentation(image); + } + + if (imageData) + { + if (areAllAttachmentsImages) + { + [self.pendingImages addObject:imageData]; + [pendingImagesItemProviders addObject:itemProvider]; + } + else + { + CGSize imageSize = [self imageSizeFromImageData:imageData]; + self.imageCompressionMode = ImageCompressionModeNone; + self.actualLargeSize = MAX(imageSize.width, imageSize.height); + + [self sendImageData:imageData withProvider:itemProvider toRoom:room successBlock:^{ + dispatch_group_leave(dispatchGroup); + } failureBlock:requestFailure]; + } + } + else + { + MXLogError(@"[ShareManager] sendContentToRoom: failed to loadItemForTypeIdentifier. Error: %@", error); + dispatch_group_leave(dispatchGroup); + } + + // Only prompt for image resize if all items are images + // Ignore showMediaCompressionPrompt setting due to memory constraints with full size images. + if (areAllAttachmentsImages) + { + if ([self areAttachmentsFullyLoaded]) + { + UIAlertController *compressionPrompt = [self compressionPromptForPendingImagesWithShareBlock:^{ + [self sendImageDatas:self.pendingImages withProviders:pendingImagesItemProviders toRoom:room successBlock:^{ + dispatch_group_leave(dispatchGroup); + } failureBlock:requestFailure]; + }]; + + if (compressionPrompt) + { + [self presentCompressionPrompt:compressionPrompt]; + } + } + else + { + dispatch_group_leave(dispatchGroup); + } + } + }]; } else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeVideo]) { - dispatch_group_enter(requestsGroup); - - [itemProvider loadItemForTypeIdentifier:UTTypeVideo options:nil completionHandler:^(NSURL *videoLocalUrl, NSError * _Null_unspecified error) { - - // Switch back on the main thread to handle correctly the UI change + dispatch_group_enter(dispatchGroup); + [itemProvider loadItemForTypeIdentifier:UTTypeVideo options:nil completionHandler:^(NSURL *videoLocalUrl, NSError *error) { dispatch_async(dispatch_get_main_queue(), ^{ - - if (weakSelf) - { - typeof(self) self = weakSelf; - [self sendVideo:videoLocalUrl - toRoom:room - successBlock:^{ - requestSuccess(item); - } failureBlock:requestFailure]; - } - + MXStrongifyAndReturnIfNil(self); + [self sendVideo:videoLocalUrl toRoom:room successBlock:^{ + dispatch_group_leave(dispatchGroup); + } failureBlock:requestFailure]; }); - }]; } else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeMovie]) { - dispatch_group_enter(requestsGroup); - - [itemProvider loadItemForTypeIdentifier:UTTypeMovie options:nil completionHandler:^(NSURL *videoLocalUrl, NSError * _Null_unspecified error) { - - // Switch back on the main thread to handle correctly the UI change + dispatch_group_enter(dispatchGroup); + [itemProvider loadItemForTypeIdentifier:UTTypeMovie options:nil completionHandler:^(NSURL *videoLocalUrl, NSError *error) { dispatch_async(dispatch_get_main_queue(), ^{ - - if (weakSelf) - { - typeof(self) self = weakSelf; - [self sendVideo:videoLocalUrl - toRoom:room - successBlock:^{ - requestSuccess(item); - } failureBlock:requestFailure]; - } - + MXStrongifyAndReturnIfNil(self); + [self sendVideo:videoLocalUrl toRoom:room successBlock:^{ + dispatch_group_leave(dispatchGroup); + } failureBlock:requestFailure]; }); - - }]; + }]; } } } - dispatch_group_notify(requestsGroup, dispatch_get_main_queue(), ^{ + dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{ [self resetPendingData]; if (firstRequestError) { - if (failureBlock) - { - failureBlock(firstRequestError); - } + failure(firstRequestError); } else { - if (self.completionCallback) - { - self.completionCallback(ShareManagerResultFinished); - } + self.completionCallback(ShareManagerResultFinished); } }); } -- (void)_showFailureAlert:(NSString *)title +- (void)showFailureAlert:(NSString *)title { UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:nil preferredStyle:UIAlertControllerStyleAlert]; @@ -534,145 +438,14 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) return nil; } - UIAlertController *compressionPrompt; BOOL isAPendingImageNotOrientedUp = [self isAPendingImageNotOrientedUp]; NSData *firstImageData = self.pendingImages.firstObject; UIImage *firstImage = [UIImage imageWithData:firstImageData]; - // Get available sizes for this image MXKImageCompressionSizes compressionSizes = [MXKTools availableCompressionSizesForImage:firstImage originalFileSize:firstImageData.length]; - // Apply the compression mode - if (compressionSizes.small.fileSize || compressionSizes.medium.fileSize || compressionSizes.large.fileSize) - { - __weak typeof(self) weakSelf = self; - - compressionPrompt = [UIAlertController alertControllerWithTitle:[MatrixKitL10n attachmentSizePromptTitle] - message:[MatrixKitL10n attachmentSizePromptMessage] - preferredStyle:UIAlertControllerStyleActionSheet]; - - if (compressionSizes.small.fileSize) - { - NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.small.fileSize]; - - NSString *title = [MatrixKitL10n attachmentSmall:fileSizeString]; - - [compressionPrompt addAction:[UIAlertAction actionWithTitle:title - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - - // Send the small image - self.imageCompressionMode = ImageCompressionModeSmall; - - [self logCompressionSizeChoice:compressionSizes.large]; - - if (shareBlock) - { - shareBlock(); - } - } - - }]]; - } - - if (compressionSizes.medium.fileSize) - { - NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.medium.fileSize]; - - NSString *title = [MatrixKitL10n attachmentMedium:fileSizeString]; - - [compressionPrompt addAction:[UIAlertAction actionWithTitle:title - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - - // Send the medium image - self.imageCompressionMode = ImageCompressionModeMedium; - - [self logCompressionSizeChoice:compressionSizes.large]; - - if (shareBlock) - { - shareBlock(); - } - } - - }]]; - } - - // Do not offer the possibility to resize an image with a dimension above kLargeImageSizeMaxDimension, to prevent the risk of memory limit exception. - // TODO: Remove this condition when issue https://github.com/vector-im/riot-ios/issues/2341 will be fixed. - if (compressionSizes.large.fileSize && (MAX(compressionSizes.large.imageSize.width, compressionSizes.large.imageSize.height) <= kLargeImageSizeMaxDimension)) - { - NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.large.fileSize]; - - NSString *title = [MatrixKitL10n attachmentLarge:fileSizeString]; - - [compressionPrompt addAction:[UIAlertAction actionWithTitle:title - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - - // Send the large image - self.imageCompressionMode = ImageCompressionModeLarge; - self.actualLargeSize = compressionSizes.actualLargeSize; - - [self logCompressionSizeChoice:compressionSizes.large]; - - if (shareBlock) - { - shareBlock(); - } - } - - }]]; - } - - // To limit memory consumption, we suggest the original resolution only if the image orientation is up, or if the image size is moderate - if (!isAPendingImageNotOrientedUp || !compressionSizes.large.fileSize) - { - NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.original.fileSize]; - - NSString *title = [MatrixKitL10n attachmentOriginal:fileSizeString]; - - [compressionPrompt addAction:[UIAlertAction actionWithTitle:title - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - - self.imageCompressionMode = ImageCompressionModeNone; - - [self logCompressionSizeChoice:compressionSizes.large]; - if (shareBlock) - { - shareBlock(); - } - } - - }]]; - } - - [compressionPrompt addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n cancel] - style:UIAlertActionStyleCancel - handler:nil]]; - - - } - else + if (compressionSizes.small.fileSize == 0 && compressionSizes.medium.fileSize == 0 && compressionSizes.large.fileSize == 0) { if (isAPendingImageNotOrientedUp && self.pendingImages.count > 1) { @@ -685,12 +458,86 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) MXLogDebug(@"[ShareManager] Send %lu image(s) without compression prompt using compression mode: %ld", (unsigned long)self.pendingImages.count, (long)self.imageCompressionMode); - if (shareBlock) - { - shareBlock(); - } + shareBlock(); + + return nil; } + UIAlertController *compressionPrompt = [UIAlertController alertControllerWithTitle:[MatrixKitL10n attachmentSizePromptTitle] + message:[MatrixKitL10n attachmentSizePromptMessage] + preferredStyle:UIAlertControllerStyleActionSheet]; + + if (compressionSizes.small.fileSize) + { + NSString *title = [MatrixKitL10n attachmentSmall:[MXTools fileSizeToString:compressionSizes.small.fileSize]]; + + MXWeakify(self); + [compressionPrompt addAction:[UIAlertAction actionWithTitle:title style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + + self.imageCompressionMode = ImageCompressionModeSmall; + [self logCompressionSizeChoice:compressionSizes.large]; + + shareBlock(); + }]]; + } + + if (compressionSizes.medium.fileSize) + { + NSString *title = [MatrixKitL10n attachmentMedium:[MXTools fileSizeToString:compressionSizes.medium.fileSize]]; + + MXWeakify(self); + [compressionPrompt addAction:[UIAlertAction actionWithTitle:title style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + + self.imageCompressionMode = ImageCompressionModeMedium; + [self logCompressionSizeChoice:compressionSizes.large]; + + shareBlock(); + }]]; + } + + // Do not offer the possibility to resize an image with a dimension above kLargeImageSizeMaxDimension, to prevent the risk of memory limit exception. + // TODO: Remove this condition when issue https://github.com/vector-im/riot-ios/issues/2341 will be fixed. + if (compressionSizes.large.fileSize && (MAX(compressionSizes.large.imageSize.width, compressionSizes.large.imageSize.height) <= kLargeImageSizeMaxDimension)) + { + NSString *title = [MatrixKitL10n attachmentLarge:[MXTools fileSizeToString:compressionSizes.large.fileSize]]; + + MXWeakify(self); + [compressionPrompt addAction:[UIAlertAction actionWithTitle:title style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + + self.imageCompressionMode = ImageCompressionModeLarge; + self.actualLargeSize = compressionSizes.actualLargeSize; + + [self logCompressionSizeChoice:compressionSizes.large]; + + shareBlock(); + }]]; + } + + // To limit memory consumption, we suggest the original resolution only if the image orientation is up, or if the image size is moderate + if (!isAPendingImageNotOrientedUp || !compressionSizes.large.fileSize) + { + NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.original.fileSize]; + + NSString *title = [MatrixKitL10n attachmentOriginal:fileSizeString]; + + MXWeakify(self); + [compressionPrompt addAction:[UIAlertAction actionWithTitle:title style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + + self.imageCompressionMode = ImageCompressionModeNone; + [self logCompressionSizeChoice:compressionSizes.large]; + + shareBlock(); + }]]; + } + + [compressionPrompt addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n cancel] + style:UIAlertActionStyleCancel + handler:nil]]; + return compressionPrompt; } @@ -952,24 +799,15 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) if (!text) { MXLogError(@"[ShareManager] loadItemForTypeIdentifier: failed."); - if (failureBlock) - { - failureBlock(nil); - } + failureBlock(nil); return; } [room sendTextMessage:text success:^(NSString *eventId) { - if (successBlock) - { - successBlock(); - } + successBlock(); } failure:^(NSError *error) { MXLogError(@"[ShareManager] sendTextMessage failed with error %@", error); - if (failureBlock) - { - failureBlock(error); - } + failureBlock(error); }]; } @@ -979,10 +817,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) if (!fileUrl) { MXLogError(@"[ShareManager] loadItemForTypeIdentifier: failed."); - if (failureBlock) - { - failureBlock(nil); - } + failureBlock(nil); return; } @@ -992,16 +827,10 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) CFRelease(uti); [room sendFile:fileUrl mimeType:mimeType localEcho:nil success:^(NSString *eventId) { - if (successBlock) - { - successBlock(); - } + successBlock(); } failure:^(NSError *error) { MXLogError(@"[ShareManager] sendFile failed with error %@", error); - if (failureBlock) - { - failureBlock(error); - } + failureBlock(error); } keepActualFilename:YES]; } @@ -1116,17 +945,10 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) } [room sendImage:finalImageData withImageSize:imageSize mimeType:mimeType andThumbnail:thumbnail localEcho:nil success:^(NSString *eventId) { - if (successBlock) - { - successBlock(); - } + successBlock(); } failure:^(NSError *error) { MXLogError(@"[ShareManager] sendImage failed with error %@", error); - if (failureBlock) - { - failureBlock(error); - } - + failureBlock(error); }]; } @@ -1135,11 +957,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) if (imageDatas.count == 0 || imageDatas.count != itemProviders.count) { MXLogError(@"[ShareManager] sendImages: no images to send."); - - if (failureBlock) - { - failureBlock(nil); - } + failureBlock(nil); return; } @@ -1178,17 +996,11 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) if (firstRequestError) { - if (failureBlock) - { - failureBlock(firstRequestError); - } + failureBlock(firstRequestError); } else { - if (successBlock) - { - successBlock(); - } + successBlock(); } }); } @@ -1216,10 +1028,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) if (!videoLocalUrl) { MXLogError(@"[ShareManager] loadItemForTypeIdentifier: failed."); - if (failureBlock) - { - failureBlock(nil); - } + failureBlock(nil); return; } @@ -1233,16 +1042,10 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) CFRelease(imageRef); [room sendVideoAsset:videoAsset withThumbnail:videoThumbnail localEcho:nil success:^(NSString *eventId) { - if (successBlock) - { - successBlock(); - } + successBlock(); } failure:^(NSError *error) { MXLogError(@"[ShareManager] Failed sending video with error %@", error); - if (failureBlock) - { - failureBlock(error); - } + failureBlock(error); }]; }]; From 4329040d3684a5c444d94c415910328c43e9d427 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 15 Oct 2021 13:09:11 +0300 Subject: [PATCH 205/276] vector-im/element-ios/issues/5009 - Refactored away the NSExtensionContext dependency from the ShareManager. Introduced different ShareItemProviders for the share extension and the main application. Improved item loading error handling. --- Riot/Modules/Room/RoomViewController.m | 20 +- .../ShareItemProviderProtocol.swift | 35 ++ .../SimpleShareItemProvider.swift | 95 ++++ RiotShareExtension/Shared/ShareManager.h | 4 +- RiotShareExtension/Shared/ShareManager.m | 502 +++++++++--------- .../{ => View}/FallbackViewController.h | 0 .../{ => View}/FallbackViewController.m | 0 .../{ => View}/FallbackViewController.xib | 0 .../{ => View}/RecentRoomTableViewCell.h | 0 .../{ => View}/RecentRoomTableViewCell.m | 0 .../{ => View}/RecentRoomTableViewCell.xib | 0 .../{ => View}/RoomsListViewController.h | 0 .../{ => View}/RoomsListViewController.m | 0 .../{ => View}/RoomsListViewController.xib | 0 .../Shared/{ => View}/ShareViewController.h | 0 .../Shared/{ => View}/ShareViewController.m | 0 .../Shared/{ => View}/ShareViewController.xib | 5 +- .../ShareExtensionRootViewController.h | 0 .../ShareExtensionRootViewController.m | 3 +- .../ShareExtensionShareItemProvider.swift | 134 +++++ changelog.d/5009.feature | 1 + 21 files changed, 525 insertions(+), 274 deletions(-) create mode 100644 RiotShareExtension/Shared/ShareItemProvider/ShareItemProviderProtocol.swift create mode 100644 RiotShareExtension/Shared/ShareItemProvider/SimpleShareItemProvider.swift rename RiotShareExtension/Shared/{ => View}/FallbackViewController.h (100%) rename RiotShareExtension/Shared/{ => View}/FallbackViewController.m (100%) rename RiotShareExtension/Shared/{ => View}/FallbackViewController.xib (100%) rename RiotShareExtension/Shared/{ => View}/RecentRoomTableViewCell.h (100%) rename RiotShareExtension/Shared/{ => View}/RecentRoomTableViewCell.m (100%) rename RiotShareExtension/Shared/{ => View}/RecentRoomTableViewCell.xib (100%) rename RiotShareExtension/Shared/{ => View}/RoomsListViewController.h (100%) rename RiotShareExtension/Shared/{ => View}/RoomsListViewController.m (100%) rename RiotShareExtension/Shared/{ => View}/RoomsListViewController.xib (100%) rename RiotShareExtension/Shared/{ => View}/ShareViewController.h (100%) rename RiotShareExtension/Shared/{ => View}/ShareViewController.m (100%) rename RiotShareExtension/Shared/{ => View}/ShareViewController.xib (97%) rename RiotShareExtension/{ => Sources}/ShareExtensionRootViewController.h (100%) rename RiotShareExtension/{ => Sources}/ShareExtensionRootViewController.m (92%) create mode 100644 RiotShareExtension/Sources/ShareExtensionShareItemProvider.swift create mode 100644 changelog.d/5009.feature diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 928b44a88..96b2a2065 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -3198,16 +3198,14 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { - NSExtensionItem *item = [[NSExtensionItem alloc] init]; - item.attachments = @[[[NSItemProvider alloc] initWithItem:selectedComponent.textMessage typeIdentifier:(__bridge NSString *)kUTTypeText]]; - - self.shareManager = [[ShareManager alloc] initWithItems:@[item]]; + self.shareManager = [[ShareManager alloc] initWithShareItemProvider:[[SimpleShareItemProvider alloc] initWithTextMessage:selectedComponent.textMessage]]; MXWeakify(self); [self.shareManager setCompletionCallback:^(ShareManagerResult result) { MXStrongifyAndReturnIfNil(self); [attachment onShareEnded]; [self dismissViewControllerAnimated:YES completion:nil]; + self.shareManager = nil; }]; [self presentViewController:self.shareManager.mainViewController animated:YES completion:nil]; @@ -3395,31 +3393,25 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; if (attachment.type == MXKAttachmentTypeFile || attachment.type == MXKAttachmentTypeImage || - attachment.type == MXKAttachmentTypeVideo) { + attachment.type == MXKAttachmentTypeVideo || + attachment.type == MXKAttachmentTypeVoiceMessage) { [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { - - NSDictionary *attachmentTypeToIdentifier = @{@(MXKAttachmentTypeFile): (__bridge NSString *)kUTTypeFileURL, - @(MXKAttachmentTypeImage): (__bridge NSString *)kUTTypeImage, - @(MXKAttachmentTypeVideo): (__bridge NSString *)kUTTypeVideo}; - [self startActivityIndicator]; [attachment prepareShare:^(NSURL *fileURL) { [self stopActivityIndicator]; - NSExtensionItem *item = [[NSExtensionItem alloc] init]; - item.attachments = @[[[NSItemProvider alloc] initWithItem:fileURL typeIdentifier:attachmentTypeToIdentifier[@(attachment.type)]]]; - - self.shareManager = [[ShareManager alloc] initWithItems:@[item]]; + self.shareManager = [[ShareManager alloc] initWithShareItemProvider:[[SimpleShareItemProvider alloc] initWithAttachment:attachment]]; MXWeakify(self); [self.shareManager setCompletionCallback:^(ShareManagerResult result) { MXStrongifyAndReturnIfNil(self); [attachment onShareEnded]; [self dismissViewControllerAnimated:YES completion:nil]; + self.shareManager = nil; }]; [self presentViewController:self.shareManager.mainViewController animated:YES completion:nil]; diff --git a/RiotShareExtension/Shared/ShareItemProvider/ShareItemProviderProtocol.swift b/RiotShareExtension/Shared/ShareItemProvider/ShareItemProviderProtocol.swift new file mode 100644 index 000000000..417fea577 --- /dev/null +++ b/RiotShareExtension/Shared/ShareItemProvider/ShareItemProviderProtocol.swift @@ -0,0 +1,35 @@ +// +// 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 public enum ShareItemType: UInt { + case fileURL, text, URL, image, video, movie, voiceMessage, unknown +} + +@objc public protocol ShareItemProtocol { + var type: ShareItemType { get } +} + +@objc public protocol ShareItemProviderProtocol { + var items: [ShareItemProtocol] { get } + + func areAllItemsImages() -> Bool + + func areAllItemsLoaded() -> Bool + + func loadItem(_ item: ShareItemProtocol, completion: @escaping (Any?, Error?) -> Void) +} diff --git a/RiotShareExtension/Shared/ShareItemProvider/SimpleShareItemProvider.swift b/RiotShareExtension/Shared/ShareItemProvider/SimpleShareItemProvider.swift new file mode 100644 index 000000000..261c160fe --- /dev/null +++ b/RiotShareExtension/Shared/ShareItemProvider/SimpleShareItemProvider.swift @@ -0,0 +1,95 @@ +// +// 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 + +private class SimpleShareItem: ShareItemProtocol { + let attachment: MXKAttachment? + let textMessage: String? + + init(withAttachment attachment: MXKAttachment) { + self.attachment = attachment + self.textMessage = nil + } + + init(withTextMessage textMessage: String) { + self.attachment = nil + self.textMessage = textMessage + } + + var type: ShareItemType { + guard textMessage == nil else { + return .text + } + + guard let attachment = attachment else { + return .unknown + } + + if attachment.type == MXKAttachmentTypeImage { + return .image + } else if attachment.type == MXKAttachmentTypeVideo { + return .video + } else if attachment.type == MXKAttachmentTypeFile { + return .fileURL + } else if attachment.type == MXKAttachmentTypeVoiceMessage { + return .voiceMessage + } else { + return .unknown + } + } +} + +@objc class SimpleShareItemProvider: NSObject, ShareItemProviderProtocol { + + private let attachment: MXKAttachment? + private let textMessage: String? + + let items: [ShareItemProtocol] + + @objc public init(withAttachment attachment: MXKAttachment) { + self.attachment = attachment + self.items = [SimpleShareItem(withAttachment: attachment)]; + self.textMessage = nil + } + + @objc public init(withTextMessage textMessage: String) { + self.textMessage = textMessage + self.items = [SimpleShareItem(withTextMessage: textMessage)]; + self.attachment = nil + } + + func loadItem(_ item: ShareItemProtocol, completion: @escaping (Any?, Error?) -> Void) { + if let textMessage = self.textMessage { + completion(textMessage, nil) + return + } + + attachment?.prepareShare({ url in + completion(url, nil) + }, failure: { error in + completion(nil, error) + }) + } + + func areAllItemsLoaded() -> Bool { + return true + } + + func areAllItemsImages() -> Bool { + return (attachment != nil && attachment?.type == MXKAttachmentTypeImage) + } +} diff --git a/RiotShareExtension/Shared/ShareManager.h b/RiotShareExtension/Shared/ShareManager.h index b81233883..965653d00 100644 --- a/RiotShareExtension/Shared/ShareManager.h +++ b/RiotShareExtension/Shared/ShareManager.h @@ -16,6 +16,8 @@ #import +@protocol ShareItemProviderProtocol; + NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, ShareManagerResult) { @@ -28,7 +30,7 @@ typedef NS_ENUM(NSUInteger, ShareManagerResult) { @property (nonatomic, copy) void (^completionCallback)(ShareManagerResult); -- (instancetype)initWithItems:(NSArray *)items; +- (instancetype)initWithShareItemProvider:(id)shareItemProvider; - (UIViewController *)mainViewController; diff --git a/RiotShareExtension/Shared/ShareManager.m b/RiotShareExtension/Shared/ShareManager.m index c34b841d1..c5ebeba6b 100644 --- a/RiotShareExtension/Shared/ShareManager.m +++ b/RiotShareExtension/Shared/ShareManager.m @@ -16,7 +16,6 @@ @import MobileCoreServices; -#import "objc/runtime.h" #import #import @@ -43,11 +42,11 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) @interface ShareManager () -@property (nonatomic, strong, readonly) NSArray *extensionItems; +@property (nonatomic, strong, readonly) id shareItemProvider; @property (nonatomic, strong, readonly) ShareViewController *shareViewController; -@property (nonatomic, strong, readonly) NSMutableArray *pendingImages; -@property (nonatomic, strong, readonly) NSMutableDictionary *imageUploadProgresses; +@property (nonatomic, strong, readonly) NSMutableArray *pendingImages; +@property (nonatomic, strong, readonly) NSMutableDictionary *imageUploadProgresses; @property (nonatomic, strong, readonly) id configuration; @property (nonatomic, strong) MXKAccount *userAccount; @@ -61,11 +60,11 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) @implementation ShareManager -- (instancetype)initWithItems:(NSArray *)items +- (instancetype)initWithShareItemProvider:(id)shareItemProvider { - if (self = [super init]) { - - _extensionItems = items; + if (self = [super init]) + { + _shareItemProvider = shareItemProvider; _pendingImages = [NSMutableArray array]; _imageUploadProgresses = [NSMutableDictionary dictionary]; @@ -150,16 +149,8 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) { [self resetPendingData]; - NSString *UTTypeText = (__bridge NSString *)kUTTypeText; - NSString *UTTypeURL = (__bridge NSString *)kUTTypeURL; - NSString *UTTypeImage = (__bridge NSString *)kUTTypeImage; - NSString *UTTypeVideo = (__bridge NSString *)kUTTypeVideo; - NSString *UTTypeFileUrl = (__bridge NSString *)kUTTypeFileURL; - NSString *UTTypeMovie = (__bridge NSString *)kUTTypeMovie; + NSMutableArray > *pendingImagesItemProviders = [NSMutableArray array]; // Used to keep the items associated to pending images (used only when all items are images). - BOOL areAllAttachmentsImages = [self areAllAttachmentsImages]; - NSMutableArray *pendingImagesItemProviders = [NSMutableArray new]; // Used to keep NSItemProvider associated to pending images (used only when all items are images). - __block NSError *firstRequestError = nil; dispatch_group_t dispatchGroup = dispatch_group_create(); @@ -173,149 +164,198 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) }; MXWeakify(self); - for (NSExtensionItem *item in self.extensionItems) + for (id item in self.shareItemProvider.items) { - for (NSItemProvider *itemProvider in item.attachments) - { - if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeFileUrl]) - { - dispatch_group_enter(dispatchGroup); - [itemProvider loadItemForTypeIdentifier:UTTypeFileUrl options:nil completionHandler:^(NSURL *fileUrl, NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - MXStrongifyAndReturnIfNil(self); - [self sendFileWithUrl:fileUrl toRoom:room successBlock:^{ - dispatch_group_leave(dispatchGroup); - } failureBlock:requestFailure]; - }); - - }]; - } - else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeText]) - { - dispatch_group_enter(dispatchGroup); - [itemProvider loadItemForTypeIdentifier:UTTypeText options:nil completionHandler:^(NSString *text, NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - MXStrongifyAndReturnIfNil(self); - [self sendText:text toRoom:room successBlock:^{ - dispatch_group_leave(dispatchGroup); - } failureBlock:requestFailure]; - }); - - }]; - } - else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeURL]) - { - dispatch_group_enter(dispatchGroup); - [itemProvider loadItemForTypeIdentifier:UTTypeURL options:nil completionHandler:^(NSURL *url, NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - MXStrongifyAndReturnIfNil(self); - [self sendText:url.absoluteString toRoom:room successBlock:^{ - dispatch_group_leave(dispatchGroup); - } failureBlock:requestFailure]; - }); - }]; - } - else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeImage]) - { - itemProvider.isLoaded = NO; + if (item.type == ShareItemTypeFileURL) { + dispatch_group_enter(dispatchGroup); + [self.shareItemProvider loadItem:item completion:^(NSURL *url, NSError *error) { + if (error) { + requestFailure(error); + dispatch_group_leave(dispatchGroup); + return; + } - dispatch_group_enter(dispatchGroup); - [itemProvider loadItemForTypeIdentifier:UTTypeImage options:nil completionHandler:^(id itemProviderItem, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ MXStrongifyAndReturnIfNil(self); - - itemProvider.isLoaded = YES; - - NSData *imageData; - if ([(NSObject *)itemProviderItem isKindOfClass:[NSData class]]) + [self sendFileWithUrl:url toRoom:room success:^{ + dispatch_group_leave(dispatchGroup); + } failure:requestFailure]; + }); + }]; + } + + if (item.type == ShareItemTypeText) { + dispatch_group_enter(dispatchGroup); + [self.shareItemProvider loadItem:item completion:^(NSString *text, NSError *error) { + if (error) { + requestFailure(error); + dispatch_group_leave(dispatchGroup); + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + MXStrongifyAndReturnIfNil(self); + [self sendText:text toRoom:room success:^{ + dispatch_group_leave(dispatchGroup); + } failure:requestFailure]; + }); + }]; + } + + if (item.type == ShareItemTypeURL) + { + dispatch_group_enter(dispatchGroup); + [self.shareItemProvider loadItem:item completion:^(NSURL *url, NSError *error) { + if (error) { + requestFailure(error); + dispatch_group_leave(dispatchGroup); + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + MXStrongifyAndReturnIfNil(self); + [self sendText:url.absoluteString toRoom:room success:^{ + dispatch_group_leave(dispatchGroup); + } failure:requestFailure]; + }); + }]; + } + + if (item.type == ShareItemTypeImage) + { + dispatch_group_enter(dispatchGroup); + [self.shareItemProvider loadItem:item completion:^(id itemProviderItem, NSError *error) { + if (error) { + requestFailure(error); + dispatch_group_leave(dispatchGroup); + return; + } + + NSData *imageData; + if ([(NSObject *)itemProviderItem isKindOfClass:[NSData class]]) + { + imageData = (NSData*)itemProviderItem; + } + else if ([(NSObject *)itemProviderItem isKindOfClass:[NSURL class]]) + { + NSURL *imageURL = (NSURL*)itemProviderItem; + imageData = [NSData dataWithContentsOfURL:imageURL]; + } + else if ([(NSObject *)itemProviderItem isKindOfClass:[UIImage class]]) + { + // An application can share directly an UIImage. + // The most common case is screenshot sharing without saving to file. + // As screenshot using PNG format when they are saved to file we also use PNG format when saving UIImage to NSData. + UIImage *image = (UIImage*)itemProviderItem; + imageData = UIImagePNGRepresentation(image); + } + + MXStrongifyAndReturnIfNil(self); + + if (imageData) + { + if ([self.shareItemProvider areAllItemsImages]) { - imageData = (NSData*)itemProviderItem; + [self.pendingImages addObject:imageData]; + [pendingImagesItemProviders addObject:item]; } - else if ([(NSObject *)itemProviderItem isKindOfClass:[NSURL class]]) + else { - NSURL *imageURL = (NSURL*)itemProviderItem; - imageData = [NSData dataWithContentsOfURL:imageURL]; + CGSize imageSize = [self imageSizeFromImageData:imageData]; + self.imageCompressionMode = ImageCompressionModeNone; + self.actualLargeSize = MAX(imageSize.width, imageSize.height); + + [self sendImageData:imageData withItem:item toRoom:room success:^{ + dispatch_group_leave(dispatchGroup); + } failure:requestFailure]; } - else if ([(NSObject *)itemProviderItem isKindOfClass:[UIImage class]]) + } + else + { + MXLogError(@"[ShareManager] sendContentToRoom: failed to loadItemForTypeIdentifier. Error: %@", error); + dispatch_group_leave(dispatchGroup); + } + + // Only prompt for image resize if all items are images + // Ignore showMediaCompressionPrompt setting due to memory constraints with full size images. + if ([self.shareItemProvider areAllItemsImages]) + { + if ([self.shareItemProvider areAllItemsLoaded]) { - // An application can share directly an UIImage. - // The most common case is screenshot sharing without saving to file. - // As screenshot using PNG format when they are saved to file we also use PNG format when saving UIImage to NSData. - UIImage *image = (UIImage*)itemProviderItem; - imageData = UIImagePNGRepresentation(image); - } - - if (imageData) - { - if (areAllAttachmentsImages) - { - [self.pendingImages addObject:imageData]; - [pendingImagesItemProviders addObject:itemProvider]; - } - else - { - CGSize imageSize = [self imageSizeFromImageData:imageData]; - self.imageCompressionMode = ImageCompressionModeNone; - self.actualLargeSize = MAX(imageSize.width, imageSize.height); - - [self sendImageData:imageData withProvider:itemProvider toRoom:room successBlock:^{ + UIAlertController *compressionPrompt = [self compressionPromptForPendingImagesWithShareBlock:^{ + [self sendImageDatas:self.pendingImages.copy withItems:pendingImagesItemProviders toRoom:room success:^{ dispatch_group_leave(dispatchGroup); - } failureBlock:requestFailure]; + } failure:requestFailure]; + }]; + + if (compressionPrompt) + { + [self presentCompressionPrompt:compressionPrompt]; } } else { - MXLogError(@"[ShareManager] sendContentToRoom: failed to loadItemForTypeIdentifier. Error: %@", error); dispatch_group_leave(dispatchGroup); } - - // Only prompt for image resize if all items are images - // Ignore showMediaCompressionPrompt setting due to memory constraints with full size images. - if (areAllAttachmentsImages) - { - if ([self areAttachmentsFullyLoaded]) - { - UIAlertController *compressionPrompt = [self compressionPromptForPendingImagesWithShareBlock:^{ - [self sendImageDatas:self.pendingImages withProviders:pendingImagesItemProviders toRoom:room successBlock:^{ - dispatch_group_leave(dispatchGroup); - } failureBlock:requestFailure]; - }]; - - if (compressionPrompt) - { - [self presentCompressionPrompt:compressionPrompt]; - } - } - else - { - dispatch_group_leave(dispatchGroup); - } - } - }]; - } - else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeVideo]) - { - dispatch_group_enter(dispatchGroup); - [itemProvider loadItemForTypeIdentifier:UTTypeVideo options:nil completionHandler:^(NSURL *videoLocalUrl, NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - MXStrongifyAndReturnIfNil(self); - [self sendVideo:videoLocalUrl toRoom:room successBlock:^{ - dispatch_group_leave(dispatchGroup); - } failureBlock:requestFailure]; - }); - }]; - } - else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeMovie]) - { - dispatch_group_enter(dispatchGroup); - [itemProvider loadItemForTypeIdentifier:UTTypeMovie options:nil completionHandler:^(NSURL *videoLocalUrl, NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - MXStrongifyAndReturnIfNil(self); - [self sendVideo:videoLocalUrl toRoom:room successBlock:^{ - dispatch_group_leave(dispatchGroup); - } failureBlock:requestFailure]; - }); - }]; - } + } + }]; + } + + if (item.type == ShareItemTypeVideo) + { + dispatch_group_enter(dispatchGroup); + [self.shareItemProvider loadItem:item completion:^(NSURL *videoLocalUrl, NSError *error) { + if (error) { + requestFailure(error); + dispatch_group_leave(dispatchGroup); + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + MXStrongifyAndReturnIfNil(self); + [self sendVideo:videoLocalUrl toRoom:room success:^{ + dispatch_group_leave(dispatchGroup); + } failure:requestFailure]; + }); + }]; + } + + if (item.type == ShareItemTypeMovie) + { + dispatch_group_enter(dispatchGroup); + [self.shareItemProvider loadItem:item completion:^(NSURL *videoLocalUrl, NSError *error) { + if (error) { + requestFailure(error); + dispatch_group_leave(dispatchGroup); + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + MXStrongifyAndReturnIfNil(self); + [self sendVideo:videoLocalUrl toRoom:room success:^{ + dispatch_group_leave(dispatchGroup); + } failure:requestFailure]; + }); + }]; + } + + if (item.type == ShareItemTypeVoiceMessage) + { + dispatch_group_enter(dispatchGroup); + [self.shareItemProvider loadItem:item completion:^(NSURL *fileURL, NSError *error) { + if (error) { + requestFailure(error); + dispatch_group_leave(dispatchGroup); + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + MXStrongifyAndReturnIfNil(self); + [self sendVoiceMessage:fileURL toRoom:room success:^{ + dispatch_group_leave(dispatchGroup); + } failure:requestFailure]; + }); + }]; } } @@ -546,59 +586,6 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) [self.shareViewController showProgressIndicator]; } -- (BOOL)areAttachmentsFullyLoaded -{ - for (NSExtensionItem *item in self.extensionItems) - { - for (NSItemProvider *itemProvider in item.attachments) - { - if (itemProvider.isLoaded == NO) - { - return NO; - } - } - } - return YES; -} - -- (BOOL)areAllAttachmentsImages -{ - for (NSExtensionItem *item in self.extensionItems) - { - for (NSItemProvider *itemProvider in item.attachments) - { - if (![itemProvider hasItemConformingToTypeIdentifier:(__bridge NSString *)kUTTypeImage]) - { - return NO; - } - } - } - return YES; -} - -- (NSString*)utiFromImageTypeItemProvider:(NSItemProvider*)itemProvider -{ - NSString *uti; - - NSString *utiPNG = (__bridge NSString *)kUTTypePNG; - NSString *utiJPEG = (__bridge NSString *)kUTTypeJPEG; - - if ([itemProvider hasItemConformingToTypeIdentifier:utiPNG]) - { - uti = utiPNG; - } - else if ([itemProvider hasItemConformingToTypeIdentifier:utiJPEG]) - { - uti = utiJPEG; - } - else - { - uti = itemProvider.registeredTypeIdentifiers.firstObject; - } - - return uti; -} - - (NSString*)utiFromImageData:(NSData*)imageData { CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)imageData, NULL); @@ -793,31 +780,36 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) #pragma mark - Sharing -- (void)sendText:(NSString *)text toRoom:(MXRoom *)room successBlock:(dispatch_block_t)successBlock failureBlock:(void(^)(NSError *error))failureBlock +- (void)sendText:(NSString *)text + toRoom:(MXRoom *)room + success:(dispatch_block_t)success + failure:(void(^)(NSError *error))failure { [self didStartSendingToRoom:room]; if (!text) { - MXLogError(@"[ShareManager] loadItemForTypeIdentifier: failed."); - failureBlock(nil); + MXLogError(@"[ShareManager] Invalid text."); + failure(nil); return; } [room sendTextMessage:text success:^(NSString *eventId) { - successBlock(); + success(); } failure:^(NSError *error) { MXLogError(@"[ShareManager] sendTextMessage failed with error %@", error); - failureBlock(error); + failure(error); }]; } -- (void)sendFileWithUrl:(NSURL *)fileUrl toRoom:(MXRoom *)room successBlock:(dispatch_block_t)successBlock failureBlock:(void(^)(NSError *error))failureBlock +- (void)sendFileWithUrl:(NSURL *)fileUrl toRoom:(MXRoom *)room + success:(dispatch_block_t)success + failure:(void(^)(NSError *error))failure { [self didStartSendingToRoom:room]; if (!fileUrl) { - MXLogError(@"[ShareManager] loadItemForTypeIdentifier: failed."); - failureBlock(nil); + MXLogError(@"[ShareManager] Invalid file url."); + failure(nil); return; } @@ -827,47 +819,39 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) CFRelease(uti); [room sendFile:fileUrl mimeType:mimeType localEcho:nil success:^(NSString *eventId) { - successBlock(); + success(); } failure:^(NSError *error) { MXLogError(@"[ShareManager] sendFile failed with error %@", error); - failureBlock(error); + failure(error); } keepActualFilename:YES]; } -- (void)sendImageData:(NSData *)imageData withProvider:(NSItemProvider*)itemProvider toRoom:(MXRoom *)room successBlock:(dispatch_block_t)successBlock failureBlock:(void(^)(NSError *error))failureBlock +- (void)sendImageData:(NSData *)imageData + withItem:(id)item + toRoom:(MXRoom *)room + success:(dispatch_block_t)success + failure:(void(^)(NSError *error))failure { [self didStartSendingToRoom:room]; NSString *imageUTI; NSString *mimeType; - // Try to get UTI plus mime type from NSItemProvider - imageUTI = [self utiFromImageTypeItemProvider:itemProvider]; - - if (imageUTI) - { - mimeType = [self mimeTypeFromUTI:imageUTI]; - } - if (!mimeType) { - // Try to get UTI plus mime type from image data - imageUTI = [self utiFromImageData:imageData]; - if (imageUTI) { mimeType = [self mimeTypeFromUTI:imageUTI]; } } - // Sanity check if (!mimeType) { - MXLogError(@"[ShareManager] sendImage failed. Cannot determine MIME type of %@", itemProvider); - if (failureBlock) + MXLogError(@"[ShareManager] sendImage failed. Cannot determine MIME type of %@", item); + if (failure) { - failureBlock(nil); + failure(nil); } return; } @@ -945,19 +929,22 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) } [room sendImage:finalImageData withImageSize:imageSize mimeType:mimeType andThumbnail:thumbnail localEcho:nil success:^(NSString *eventId) { - successBlock(); + success(); } failure:^(NSError *error) { MXLogError(@"[ShareManager] sendImage failed with error %@", error); - failureBlock(error); + failure(error); }]; } -- (void)sendImageDatas:(NSMutableArray *)imageDatas withProviders:(NSArray*)itemProviders toRoom:(MXRoom *)room successBlock:(dispatch_block_t)successBlock failureBlock:(void(^)(NSError *error))failureBlock +- (void)sendImageDatas:(NSArray> *)imageDatas + withItems:(NSArray> *)items toRoom:(MXRoom *)room + success:(dispatch_block_t)success + failure:(void(^)(NSError *error))failure { - if (imageDatas.count == 0 || imageDatas.count != itemProviders.count) + if (imageDatas.count == 0 || imageDatas.count != items.count) { MXLogError(@"[ShareManager] sendImages: no images to send."); - failureBlock(nil); + failure(nil); return; } @@ -973,13 +960,9 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) @autoreleasepool { dispatch_group_enter(requestsGroup); - - NSItemProvider *itemProvider = itemProviders[index]; - - [self sendImageData:imageData withProvider:itemProvider toRoom:room successBlock:^{ + [self sendImageData:imageData withItem:items[index] toRoom:room success:^{ dispatch_group_leave(requestsGroup); - } failureBlock:^(NSError *error) { - + } failure:^(NSError *error) { if (error && !firstRequestError) { firstRequestError = error; @@ -996,16 +979,19 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) if (firstRequestError) { - failureBlock(firstRequestError); + failure(firstRequestError); } else { - successBlock(); + success(); } }); } -- (void)sendVideo:(NSURL *)videoLocalUrl toRoom:(MXRoom *)room successBlock:(dispatch_block_t)successBlock failureBlock:(void(^)(NSError *error))failureBlock +- (void)sendVideo:(NSURL *)videoLocalUrl + toRoom:(MXRoom *)room + success:(dispatch_block_t)success + failure:(void(^)(NSError *error))failure { AVURLAsset *videoAsset = [[AVURLAsset alloc] initWithURL:videoLocalUrl options:nil]; @@ -1027,8 +1013,8 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) [self didStartSendingToRoom:room]; if (!videoLocalUrl) { - MXLogError(@"[ShareManager] loadItemForTypeIdentifier: failed."); - failureBlock(nil); + MXLogError(@"[ShareManager] Invalid video file url."); + failure(nil); return; } @@ -1042,31 +1028,35 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) CFRelease(imageRef); [room sendVideoAsset:videoAsset withThumbnail:videoThumbnail localEcho:nil success:^(NSString *eventId) { - successBlock(); + success(); } failure:^(NSError *error) { MXLogError(@"[ShareManager] Failed sending video with error %@", error); - failureBlock(error); + failure(error); }]; }]; [self presentCompressionPrompt:compressionPrompt]; } -@end - - -@implementation NSItemProvider (ShareManager) - -- (void)setIsLoaded:(BOOL)isLoaded +- (void)sendVoiceMessage:(NSURL *)fileUrl + toRoom:(MXRoom *)room + success:(dispatch_block_t)success + failure:(void(^)(NSError *error))failure { - NSNumber *number = @(isLoaded); - objc_setAssociatedObject(self, @selector(isLoaded), number, OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (BOOL)isLoaded -{ - NSNumber *number = objc_getAssociatedObject(self, @selector(isLoaded)); - return number.boolValue; + [self didStartSendingToRoom:room]; + if (!fileUrl) + { + MXLogError(@"[ShareManager] Invalid voice message file url."); + failure(nil); + return; + } + + [room sendVoiceMessage:fileUrl mimeType:nil duration:0.0 samples:nil localEcho:nil success:^(NSString *eventId) { + success(); + } failure:^(NSError *error) { + MXLogError(@"[ShareManager] sendVoiceMessage failed with error %@", error); + failure(error); + } keepActualFilename:YES]; } @end diff --git a/RiotShareExtension/Shared/FallbackViewController.h b/RiotShareExtension/Shared/View/FallbackViewController.h similarity index 100% rename from RiotShareExtension/Shared/FallbackViewController.h rename to RiotShareExtension/Shared/View/FallbackViewController.h diff --git a/RiotShareExtension/Shared/FallbackViewController.m b/RiotShareExtension/Shared/View/FallbackViewController.m similarity index 100% rename from RiotShareExtension/Shared/FallbackViewController.m rename to RiotShareExtension/Shared/View/FallbackViewController.m diff --git a/RiotShareExtension/Shared/FallbackViewController.xib b/RiotShareExtension/Shared/View/FallbackViewController.xib similarity index 100% rename from RiotShareExtension/Shared/FallbackViewController.xib rename to RiotShareExtension/Shared/View/FallbackViewController.xib diff --git a/RiotShareExtension/Shared/RecentRoomTableViewCell.h b/RiotShareExtension/Shared/View/RecentRoomTableViewCell.h similarity index 100% rename from RiotShareExtension/Shared/RecentRoomTableViewCell.h rename to RiotShareExtension/Shared/View/RecentRoomTableViewCell.h diff --git a/RiotShareExtension/Shared/RecentRoomTableViewCell.m b/RiotShareExtension/Shared/View/RecentRoomTableViewCell.m similarity index 100% rename from RiotShareExtension/Shared/RecentRoomTableViewCell.m rename to RiotShareExtension/Shared/View/RecentRoomTableViewCell.m diff --git a/RiotShareExtension/Shared/RecentRoomTableViewCell.xib b/RiotShareExtension/Shared/View/RecentRoomTableViewCell.xib similarity index 100% rename from RiotShareExtension/Shared/RecentRoomTableViewCell.xib rename to RiotShareExtension/Shared/View/RecentRoomTableViewCell.xib diff --git a/RiotShareExtension/Shared/RoomsListViewController.h b/RiotShareExtension/Shared/View/RoomsListViewController.h similarity index 100% rename from RiotShareExtension/Shared/RoomsListViewController.h rename to RiotShareExtension/Shared/View/RoomsListViewController.h diff --git a/RiotShareExtension/Shared/RoomsListViewController.m b/RiotShareExtension/Shared/View/RoomsListViewController.m similarity index 100% rename from RiotShareExtension/Shared/RoomsListViewController.m rename to RiotShareExtension/Shared/View/RoomsListViewController.m diff --git a/RiotShareExtension/Shared/RoomsListViewController.xib b/RiotShareExtension/Shared/View/RoomsListViewController.xib similarity index 100% rename from RiotShareExtension/Shared/RoomsListViewController.xib rename to RiotShareExtension/Shared/View/RoomsListViewController.xib diff --git a/RiotShareExtension/Shared/ShareViewController.h b/RiotShareExtension/Shared/View/ShareViewController.h similarity index 100% rename from RiotShareExtension/Shared/ShareViewController.h rename to RiotShareExtension/Shared/View/ShareViewController.h diff --git a/RiotShareExtension/Shared/ShareViewController.m b/RiotShareExtension/Shared/View/ShareViewController.m similarity index 100% rename from RiotShareExtension/Shared/ShareViewController.m rename to RiotShareExtension/Shared/View/ShareViewController.m diff --git a/RiotShareExtension/Shared/ShareViewController.xib b/RiotShareExtension/Shared/View/ShareViewController.xib similarity index 97% rename from RiotShareExtension/Shared/ShareViewController.xib rename to RiotShareExtension/Shared/View/ShareViewController.xib index 1718d4501..c6eaf5feb 100644 --- a/RiotShareExtension/Shared/ShareViewController.xib +++ b/RiotShareExtension/Shared/View/ShareViewController.xib @@ -1,9 +1,9 @@ - + - + @@ -55,6 +55,7 @@ + diff --git a/RiotShareExtension/ShareExtensionRootViewController.h b/RiotShareExtension/Sources/ShareExtensionRootViewController.h similarity index 100% rename from RiotShareExtension/ShareExtensionRootViewController.h rename to RiotShareExtension/Sources/ShareExtensionRootViewController.h diff --git a/RiotShareExtension/ShareExtensionRootViewController.m b/RiotShareExtension/Sources/ShareExtensionRootViewController.m similarity index 92% rename from RiotShareExtension/ShareExtensionRootViewController.m rename to RiotShareExtension/Sources/ShareExtensionRootViewController.m index b523d0fb6..c2be442a0 100644 --- a/RiotShareExtension/ShareExtensionRootViewController.m +++ b/RiotShareExtension/Sources/ShareExtensionRootViewController.m @@ -38,7 +38,8 @@ [ThemeService.shared setThemeId:RiotSettings.shared.userInterfaceTheme]; - _shareManager = [[ShareManager alloc] initWithItems:self.extensionContext.inputItems]; + ShareExtensionShareItemProvider *provider = [[ShareExtensionShareItemProvider alloc] initWithExtensionContext:self.extensionContext]; + _shareManager = [[ShareManager alloc] initWithShareItemProvider:provider]; MXWeakify(self); [_shareManager setCompletionCallback:^(ShareManagerResult result) { diff --git a/RiotShareExtension/Sources/ShareExtensionShareItemProvider.swift b/RiotShareExtension/Sources/ShareExtensionShareItemProvider.swift new file mode 100644 index 000000000..e9ec4b6f2 --- /dev/null +++ b/RiotShareExtension/Sources/ShareExtensionShareItemProvider.swift @@ -0,0 +1,134 @@ +// +// 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 MobileCoreServices + +let UTTypeText = kUTTypeText as String +let UTTypeURL = kUTTypeURL as String +let UTTypeFileUrl = kUTTypeFileURL as String +let UTTypeImage = kUTTypeImage as String +let UTTypeVideo = kUTTypeVideo as String +let UTTypeMovie = kUTTypeMovie as String + +private class ShareExtensionItem: ShareItemProtocol { + let itemProvider: NSItemProvider + + var loaded = false + + init(itemProvider: NSItemProvider) { + self.itemProvider = itemProvider + } + + var type: ShareItemType { + if itemProvider.hasItemConformingToTypeIdentifier(UTTypeText) { + return .text + } else if itemProvider.hasItemConformingToTypeIdentifier(UTTypeURL) { + return .URL + } else if itemProvider.hasItemConformingToTypeIdentifier(UTTypeFileUrl) { + return .fileURL + } else if itemProvider.hasItemConformingToTypeIdentifier(UTTypeImage) { + return .image + } else if itemProvider.hasItemConformingToTypeIdentifier(UTTypeVideo) { + return .video + } else if itemProvider.hasItemConformingToTypeIdentifier(UTTypeMovie) { + return .movie + } + + return .unknown + } +} + +@objcMembers +class ShareExtensionShareItemProvider: NSObject, ShareItemProviderProtocol { + + public let items: [ShareItemProtocol] + + public init(extensionContext: NSExtensionContext) { + + var items: [ShareItemProtocol] = [] + for case let extensionItem as NSExtensionItem in extensionContext.inputItems { + guard let attachments = extensionItem.attachments else { + continue; + } + + for itemProvider in attachments { + items.append(ShareExtensionItem(itemProvider: itemProvider)) + } + } + self.items = items + } + + func areAllItemsLoaded() -> Bool { + for case let item as ShareExtensionItem in self.items { + if !item.loaded { + return false + } + } + + return true + } + + func areAllItemsImages() -> Bool { + for case let item as ShareExtensionItem in self.items { + if item.type != .image { + return false + } + } + + return true + } + + func loadItem(_ item: ShareItemProtocol, completion: @escaping (Any?, Error?) -> Void) { + guard let shareExtensionItem = item as? ShareExtensionItem else { + fatalError("[ShareExtensionShareItemProvider] Unexpected item type.") + } + + let typeIdentifier = typeIdentifierForType(item.type) + + shareExtensionItem.loaded = false + shareExtensionItem.itemProvider.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { result, error in + if error == nil { + shareExtensionItem.loaded = true + } + + completion(result, error) + } + } + + // MARK: - Private + + private func typeIdentifierForType(_ type: ShareItemType) -> String { + switch type { + case .text: + return UTTypeText + case .URL: + return UTTypeURL + case .fileURL: + return UTTypeFileUrl + case .image: + return UTTypeImage + case .video: + return UTTypeVideo + case .movie: + return UTTypeMovie + case .voiceMessage: + return UTTypeFileUrl + default: + return "" + } + } +} diff --git a/changelog.d/5009.feature b/changelog.d/5009.feature new file mode 100644 index 000000000..55e904a3b --- /dev/null +++ b/changelog.d/5009.feature @@ -0,0 +1 @@ +Implemented message forwarding from within the main application. Updated the share extension designs. \ No newline at end of file From 5db0f382703f315f9c0e8e159451ab313300b902 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 18 Oct 2021 16:30:32 +0300 Subject: [PATCH 206/276] vector-im/element-ios/issues/5009 - Implemented multi-room forwarding and added various tweaks following code review. --- .../SharedImages.xcassets/Contents.json | 6 +- .../Contents.json | 23 + .../radio-button-default.png | Bin 0 -> 615 bytes .../radio-button-default@2x.png | Bin 0 -> 1153 bytes .../radio-button-default@3x.png | Bin 0 -> 1705 bytes .../Contents.json | 23 + .../radio-button-selected.png | Bin 0 -> 729 bytes .../radio-button-selected@2x.png | Bin 0 -> 1348 bytes .../radio-button-selected@3x.png | Bin 0 -> 2072 bytes Riot/Generated/Images.swift | 2 + Riot/Modules/Room/RoomViewController.m | 62 +- RiotShareExtension/Shared/ShareDataSource.h | 30 +- RiotShareExtension/Shared/ShareDataSource.m | 39 +- .../SimpleShareItemProvider.swift | 20 +- RiotShareExtension/Shared/ShareManager.h | 14 +- RiotShareExtension/Shared/ShareManager.m | 664 +++++++++--------- .../Shared/View/RecentRoomTableViewCell.h | 2 + .../Shared/View/RecentRoomTableViewCell.m | 14 + .../Shared/View/RecentRoomTableViewCell.xib | 31 +- .../Shared/View/RoomsListViewController.m | 17 + .../Shared/View/ShareViewController.h | 7 +- .../Shared/View/ShareViewController.m | 99 ++- .../Shared/View/ShareViewController.xib | 2 +- .../ShareExtensionRootViewController.m | 11 +- .../ShareExtensionShareItemProvider.swift | 38 +- 25 files changed, 620 insertions(+), 484 deletions(-) create mode 100644 Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/Contents.json create mode 100644 Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default.png create mode 100644 Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default@2x.png create mode 100644 Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default@3x.png create mode 100644 Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/Contents.json create mode 100644 Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected.png create mode 100644 Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected@2x.png create mode 100644 Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected@3x.png diff --git a/Riot/Assets/SharedImages.xcassets/Contents.json b/Riot/Assets/SharedImages.xcassets/Contents.json index da4a164c9..73c00596a 100644 --- a/Riot/Assets/SharedImages.xcassets/Contents.json +++ b/Riot/Assets/SharedImages.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/Contents.json b/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/Contents.json new file mode 100644 index 000000000..35812152a --- /dev/null +++ b/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "radio-button-default.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "radio-button-default@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "radio-button-default@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default.png b/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default.png new file mode 100644 index 0000000000000000000000000000000000000000..84e419079478efbbd5b03ef22c5b6c750ec45e79 GIT binary patch literal 615 zcmV-t0+{`YP)p&1avqt&2U+LibR&Zl+xe`GLTtQ3+PywU@k_tix&;g`^MM6ms4<&S9I^Y}R9d9{b zK%EKcQpASQUM8d#4c-ifr*#bxy(_7b6JBTJCKQn zP0NNa2Bsa>x6ZHvx_^J7pgJyFQmx(XpuJQ;_Z|-=8;K>$aOeLB1)zI(HASAbVby>S z!ur3f?A=U>EVln}2g%R&*mhCtVjqw*Hz1Qgf} z+5>H)BoZXulr1k{!{6z&!llW9HeBr$(cioPu{07sQ~P;@+d-U$eqP{JSSiq^*JYCK<?p-hInDV`jh3@1+5=9&tATqJFAHJSOt;^8U+O9hf*1T)yaUqy)<{&lLtOv>002ovPDHLkV1lm0 B5dr`J literal 0 HcmV?d00001 diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default@2x.png b/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..7e6083bc319d633e3955789d9d89f19b8123b096 GIT binary patch literal 1153 zcmV-{1b+L8P)iLCFA}0fvWHP*_3J70|ALu!6!02rDREfddRM!(##~sJnxFbxMk* zUKs{z0X81C7NI? zV30v5#VrKt-pWE91)whizTcRcc!ez?#Fr;yvm;;(_3%$?$MY5g^oX7aD8rp8Y)g>g z1%QaudL@#ZfPZ$i!6n=8VaB^55E47y@?0%Xqg(`z&l=x4%z9HY97=&BAiTl!WZ;#{ zP4&WWG-mMouM0S{U~-*T^u zS2)8q{dJ8lX-Tav&VA0(PbLB>#y5o&d*1Rw6@9=zy{yu#Z|mh*NpvM}a^Bb`L#tY= zK!|YDTmEo^0^pxFTrPzV?Q1ab-PNV?*G_Fa5{N(ZzSbfTXpzI{-St_=N+BU2H@&6# z!?bp(1jtpod`jzZK*m3fX(!PXcl@2_+4mPu>x@r@RT#4|IO7$(#WK0laFhPF8ylTD zX>F_o)a=uAe`+tIeXuh~;~$~+m2wX4Wy*D{!_!vZN+6#7XVB8=*W2VlJXVk>EZtkr zpOhuU*4m$hK<0y`CYf z<#ot~{zw!$6sBw7W&ul7dWdaC1j*{!^SWNevIqFvorUQOuC8Z z)?|QD!5rIgr8X~=sFZdB4g;34GZFZ&U?Y%Vopu5a1D3Ik)zFmyN-C2Y5sa}}pjR;o zNO`N37`}ISa1vuvjPy&dVGs~Z*4v*Mch)c>aLS*PCDL%|<`)Q&sKg{TMz;^5sed3CU&(F zSE&aW5|oetEgO_*Swng&fhZD`(ul|X(~E(7B$W;7DhNfywX|$7{UmNVRcL#GGv~YS zpSu_k+JuB>(ArO%eT7s4aXhXIOEf^q9|Q@j3WA4huv0GHF&0vKjJ}jG0201`F>5tm z2?<@Y!LGraw>(eDv^>y?6^WnmX6p;84_y(T<#`BTEr3ZOEx`GneNV{Jk@_QCN=zvS5-$C^cn-#zGCCCHjTG#Nw3?fuu35(zZF6Btggid{gJ?F4ZG?-7)$ zJb2oZt^d3S6;O#lAC6956X>l1 zQ$~caPbr*&q6F}tAuKPCqTaU%6(1jm9JNA;cBtqHyAemb8!Y6{D2RfBd^|pJ<~JPT_w};sH6@M^t@RRp zu6mLW|b2ke%^Mu96#d*KjS`l;w=#Sd#Jj0 zGnVxxT7S3=b3lCf<&8poVHqB=wf*S39(lW%>ZaRZ5l7r=Uc?ci^#@xHcgwPU@We}? zEdK!9G!F8DOn239FlVIa*)t`kG1&^qei1zI5-7lqb#c=$j_L~Ivmh>{Dc!J*%Vrup z$Q4MGwneRM-C3)@CQ}xPq**RzKz@^t73B(~6q=(FyOtz@Fz<}BXEa*4j}1=_ncafqXvRe5Ud`>Zs^X#sWp*>$ zZv~e?3i_s(wWd>ODZCFmFb)JzTp%?qaoz-J0Ih|LmXD~cbuFtvJn0AHKnksMuW!gn zt>IO|;uge91Xn^L%<*k;Pqr2kg}m7%ae zd3%@t8ZLnn`ERdSwf91O5^R>Y^R#@aeB&;wTj>CA&tNDs`DaI<~+P}Wm)10sCq zn&nOu&pUgTk*w_mT7vWxfX%(sc;K3wdp7TAbt!bleOrj7tFAr)gFsxTKHy_&=W>y| z)kxFS*5}s>E9xwRG%2e}PZUYk{i z6>)S3-p}KLec@WMZJbEI1P{Cfk`n<(J4sliounx)o+&hq5yGCY)q9saF4{?q3$nD6 zlxc6Pi)ovnCF7TN9Ts~Qzx-7T9~N|{9k>KXgXrq8u2aVxQ$SsJ}6xek@rylo)iFCL~E zeh1P+Ijy}KSu(ej!vU#(YYel?znT}{0(a4>YHF|ULY?>Ro7&0Jks{snE~>qfDaR_5 zZYx_)bU4{alMcm&l@u0lr|NYAMmy zDa<9c3Uo>4dRq4pX>(Sq%0$t40N<&-Cfbl25$G!M2}h{EC#%xphtp#ij!YQhwZ)y} zIGAX}^^{?D`B$Rd_nl!Idb1ZSSg>Hhg8ATI6MEZVK&@sK00000NkvXXu0mjfI}#^A literal 0 HcmV?d00001 diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/Contents.json b/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/Contents.json new file mode 100644 index 000000000..a69d70fe6 --- /dev/null +++ b/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "radio-button-selected.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "radio-button-selected@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "radio-button-selected@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected.png b/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected.png new file mode 100644 index 0000000000000000000000000000000000000000..6a744d6bed5a4753d6cdfde938f85ea02d685d63 GIT binary patch literal 729 zcmV;~0w(>5P)>k%Ly)OFd9QChHvOJpg&f9sjGX}Wtz-^08;5kcp)Bz1`M644*&1^!rBBGGZ$*g(w zY8`V__12!EXd^tOK>#rrKPRGAQ7OkO|J-H!9e8rpVER7?w0nyj2?wL%&lGbJtQlF=O(zo*sHqFo4iMeImS{iI*C6mGNEVa=&3m3LJJvmJL*R8%Mb0?Joqu8eI zb?ewmzN-eOZ<0P*CC#96aoXWb&e@E3ptai9 zWE14%N~^$x{!g-81_>U2AA8(*$ruQwyEQl9-1dmvw0*tTA%u3)D-t{_*;WF9i~uRx zPCp#dz1hLBzitr=Hro-d_?Jnwq}!!uh>)|vNlDH8$n|z<$a{)^ahVuYI-wB^00000 LNkvXXu0mjf1<*je literal 0 HcmV?d00001 diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected@2x.png b/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..67c3bbd64f75d22ed14d368c2a93d7f84aecb799 GIT binary patch literal 1348 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 -typedef NS_ENUM(NSInteger, ShareDataSourceMode) -{ - DataSourceModePeople, - DataSourceModeRooms -}; +@class ShareDataSource; +@protocol ShareDataSourceDelegate + +- (void)shareDataSourceDidChangeSelectedRoomIdentifiers:(ShareDataSource *)shareDataSource; + +@end @interface ShareDataSource : MXKRecentsDataSource -- (instancetype)initWithMode:(ShareDataSourceMode)dataSourceMode - fileStore:(MXFileStore *)fileStore - credentials:(MXCredentials *)credentials; +@property (nonatomic, weak) id shareDelegate; -/** - Returns the cell data at the index path - - @param indexPath the index of the cell - @return the MXKRecentCellData instance if it exists - */ -- (MXKRecentCellData *)cellDataAtIndexPath:(NSIndexPath *)indexPath; +@property (nonatomic, strong, readonly) NSSet *selectedRoomIdentifiers; + +- (instancetype)initWithFileStore:(MXFileStore *)fileStore + credentials:(MXCredentials *)credentials; + +- (void)selectRoomWithIdentifier:(NSString *)roomIdentifier animated:(BOOL)animated; + +- (void)deselectRoomWithIdentifier:(NSString *)roomIdentifier animated:(BOOL)animated; @end diff --git a/RiotShareExtension/Shared/ShareDataSource.m b/RiotShareExtension/Shared/ShareDataSource.m index b88a846a4..4bee2a44a 100644 --- a/RiotShareExtension/Shared/ShareDataSource.m +++ b/RiotShareExtension/Shared/ShareDataSource.m @@ -19,27 +19,28 @@ @interface ShareDataSource () -@property (nonatomic, assign, readonly) ShareDataSourceMode dataSourceMode; @property (nonatomic, strong, readonly) MXFileStore *fileStore; @property (nonatomic, strong, readonly) MXCredentials *credentials; @property NSArray *recentCellDatas; @property NSMutableArray *visibleRoomCellDatas; +@property (nonatomic, strong) NSMutableSet *internalSelectedRoomIdentifiers; + @end @implementation ShareDataSource -- (instancetype)initWithMode:(ShareDataSourceMode)dataSourceMode - fileStore:(MXFileStore *)fileStore - credentials:(MXCredentials *)credentials +- (instancetype)initWithFileStore:(MXFileStore *)fileStore + credentials:(MXCredentials *)credentials { if (self = [super init]) { - _dataSourceMode = dataSourceMode; _fileStore = fileStore; _credentials = credentials; + _internalSelectedRoomIdentifiers = [NSMutableSet set]; + [self loadCellData]; } return self; @@ -53,6 +54,25 @@ _visibleRoomCellDatas = nil; } +- (NSSet *)selectedRoomIdentifiers +{ + return self.internalSelectedRoomIdentifiers.copy; +} + +- (void)selectRoomWithIdentifier:(NSString *)roomIdentifier animated:(BOOL)animated +{ + [self.internalSelectedRoomIdentifiers addObject:roomIdentifier]; + + [self.shareDelegate shareDataSourceDidChangeSelectedRoomIdentifiers:self]; +} + +- (void)deselectRoomWithIdentifier:(NSString *)roomIdentifier animated:(BOOL)animated +{ + [self.internalSelectedRoomIdentifiers removeObject:roomIdentifier]; + + [self.shareDelegate shareDataSourceDidChangeSelectedRoomIdentifiers:self]; +} + #pragma mark - Private - (void)loadCellData @@ -66,7 +86,7 @@ for (MXRoomSummary *roomSummary in roomsSummaries) { - if (!roomSummary.hiddenFromUser && ((self.dataSourceMode == DataSourceModeRooms) ^ roomSummary.isDirect)) + if (!roomSummary.hiddenFromUser) { [roomSummary setMatrixSession:session]; @@ -137,6 +157,7 @@ { self.visibleRoomCellDatas = nil; } + [self.delegate dataSource:self didCellChange:nil]; } @@ -160,7 +181,11 @@ { RecentRoomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[RecentRoomTableViewCell defaultReuseIdentifier]]; - [cell render:[self cellDataAtIndexPath:indexPath]]; + MXKRecentCellData *data = [self cellDataAtIndexPath:indexPath]; + + [cell render:data]; + + [cell setCustomSelected:[self.selectedRoomIdentifiers containsObject:data.roomSummary.roomId] animated:NO]; return cell; } diff --git a/RiotShareExtension/Shared/ShareItemProvider/SimpleShareItemProvider.swift b/RiotShareExtension/Shared/ShareItemProvider/SimpleShareItemProvider.swift index 261c160fe..e08435aec 100644 --- a/RiotShareExtension/Shared/ShareItemProvider/SimpleShareItemProvider.swift +++ b/RiotShareExtension/Shared/ShareItemProvider/SimpleShareItemProvider.swift @@ -60,6 +60,12 @@ private class SimpleShareItem: ShareItemProtocol { let items: [ShareItemProtocol] + private override init() { + attachment = nil + textMessage = nil + self.items = [] + } + @objc public init(withAttachment attachment: MXKAttachment) { self.attachment = attachment self.items = [SimpleShareItem(withAttachment: attachment)]; @@ -78,10 +84,18 @@ private class SimpleShareItem: ShareItemProtocol { return } - attachment?.prepareShare({ url in - completion(url, nil) + guard let attachment = attachment else { + fatalError("[SimpleShareItemProvider] Invalid item provider state.") + } + + attachment.prepareShare({ url in + DispatchQueue.main.async { + completion(url, nil) + } }, failure: { error in - completion(nil, error) + DispatchQueue.main.async { + completion(nil, error) + } }) } diff --git a/RiotShareExtension/Shared/ShareManager.h b/RiotShareExtension/Shared/ShareManager.h index 965653d00..bb7245040 100644 --- a/RiotShareExtension/Shared/ShareManager.h +++ b/RiotShareExtension/Shared/ShareManager.h @@ -20,6 +20,11 @@ NS_ASSUME_NONNULL_BEGIN +typedef NS_ENUM(NSUInteger, ShareManagerType) { + ShareManagerTypeSend, + ShareManagerTypeForward, +}; + typedef NS_ENUM(NSUInteger, ShareManagerResult) { ShareManagerResultFinished, ShareManagerResultCancelled, @@ -30,17 +35,12 @@ typedef NS_ENUM(NSUInteger, ShareManagerResult) { @property (nonatomic, copy) void (^completionCallback)(ShareManagerResult); -- (instancetype)initWithShareItemProvider:(id)shareItemProvider; +- (instancetype)initWithShareItemProvider:(id)shareItemProvider + type:(ShareManagerType)type; - (UIViewController *)mainViewController; @end -@interface NSItemProvider (ShareManager) - -@property BOOL isLoaded; - -@end - NS_ASSUME_NONNULL_END diff --git a/RiotShareExtension/Shared/ShareManager.m b/RiotShareExtension/Shared/ShareManager.m index c5ebeba6b..5ca23a2b1 100644 --- a/RiotShareExtension/Shared/ShareManager.m +++ b/RiotShareExtension/Shared/ShareManager.m @@ -31,6 +31,7 @@ #endif static const CGFloat kLargeImageSizeMaxDimension = 2048.0; +static const CGSize kThumbnailSize = {800.0, 600.0}; typedef NS_ENUM(NSInteger, ImageCompressionMode) { @@ -61,6 +62,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) @implementation ShareManager - (instancetype)initWithShareItemProvider:(id)shareItemProvider + type:(ShareManagerType)type { if (self = [super init]) { @@ -91,7 +93,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) [MXLog configure:configuration]; - _shareViewController = [[ShareViewController alloc] initWithType:ShareViewControllerTypeSend + _shareViewController = [[ShareViewController alloc] initWithType:(type == ShareManagerTypeForward ? ShareViewControllerTypeForward : ShareViewControllerTypeSend) currentState:ShareViewControllerAccountStateNotConfigured]; [_shareViewController setDelegate:self]; @@ -101,7 +103,6 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) [NSBundle mxk_setLanguage:language]; [NSBundle mxk_setFallbackLanguage:@"en"]; - // Check the current matrix user. [self checkUserAccount]; } @@ -117,8 +118,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) #pragma mark - ShareViewControllerDelegate -- (void)shareViewControllerDidRequestShare:(ShareViewController *)shareViewController - forRoomIdentifier:(NSString *)roomIdentifier +- (void)shareViewController:(ShareViewController *)shareViewController didRequestShareForRoomIdentifiers:(NSSet *)roomIdentifiers { MXSession *session = [[MXSession alloc] initWithMatrixRestClient:[[MXRestClient alloc] initWithCredentials:self.userAccount.mxCredentials andOnUnrecognizedCertificateBlock:nil]]; [MXFileStore setPreloadOptions:0]; @@ -129,12 +129,22 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) session.crypto.warnOnUnknowDevices = NO; // Do not warn for unknown devices. We have cross-signing now - MXRoom *selectedRoom = [MXRoom loadRoomFromStore:self.fileStore withRoomId:roomIdentifier matrixSession:session]; - [self sendContentToRoom:selectedRoom success:nil failure:^(NSError *error){ + NSMutableArray *rooms = [NSMutableArray array]; + for (NSString *roomIdentifier in roomIdentifiers) { + MXRoom *room = [MXRoom loadRoomFromStore:self.fileStore withRoomId:roomIdentifier matrixSession:session]; + if (room) { + [rooms addObject:room]; + } + } + + [self sendContentToRooms:rooms success:^{ + self.completionCallback(ShareManagerResultFinished); + } failure:^(NSError *error){ [self showFailureAlert:[VectorL10n roomEventFailedToSend]]; }]; + } failure:^(NSError *error) { - MXLogError(@"[ShareManager] Failed preparign matrix session"); + MXLogError(@"[ShareManager] Failed preparing matrix session"); }]; } @@ -145,16 +155,37 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) #pragma mark - Private -- (void)sendContentToRoom:(MXRoom *)room success:(void(^)(void))success failure:(void(^)(NSError *))failure +- (void)showFailureAlert:(NSString *)title +{ + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:nil preferredStyle:UIAlertControllerStyleAlert]; + + MXWeakify(self); + UIAlertAction *okAction = [UIAlertAction actionWithTitle:[MatrixKitL10n ok] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + MXStrongifyAndReturnIfNil(self); + + if (self.completionCallback) + { + self.completionCallback(ShareManagerResultFailed); + } + }]; + + [alertController addAction:okAction]; + + [self.mainViewController presentViewController:alertController animated:YES completion:nil]; +} + +- (void)sendContentToRooms:(NSArray *)rooms success:(void(^)(void))success failure:(void(^)(NSError *))failure { [self resetPendingData]; - NSMutableArray > *pendingImagesItemProviders = [NSMutableArray array]; // Used to keep the items associated to pending images (used only when all items are images). - __block NSError *firstRequestError = nil; dispatch_group_t dispatchGroup = dispatch_group_create(); - void (^requestFailure)(NSError*) = ^(NSError *requestError) { + void (^requestSuccess)(void) = ^() { + dispatch_group_leave(dispatchGroup); + }; + + void (^requestFailure)(NSError *) = ^(NSError *requestError) { if (requestError && !firstRequestError) { firstRequestError = requestError; @@ -166,68 +197,93 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) MXWeakify(self); for (id item in self.shareItemProvider.items) { + if (item.type == ShareItemTypeText || item.type == ShareItemTypeURL) { + dispatch_group_enter(dispatchGroup); + [self.shareItemProvider loadItem:item completion:^(id item, NSError *error) { + MXStrongifyAndReturnIfNil(self); + + if (error) + { + requestFailure(error); + return; + } + + NSString *text = nil; + if([item isKindOfClass:[NSString class]]) + { + text = item; + } + else if([item isKindOfClass:[NSURL class]]) + { + text = [(NSURL *)item absoluteString]; + } + + if(text.length == 0) + { + requestFailure(nil); + return; + } + + [self sendText:text toRooms:rooms success:requestSuccess failure:requestFailure]; + }]; + } + if (item.type == ShareItemTypeFileURL) { dispatch_group_enter(dispatchGroup); [self.shareItemProvider loadItem:item completion:^(NSURL *url, NSError *error) { - if (error) { + MXStrongifyAndReturnIfNil(self); + + if (error) + { requestFailure(error); - dispatch_group_leave(dispatchGroup); return; } - dispatch_async(dispatch_get_main_queue(), ^{ - MXStrongifyAndReturnIfNil(self); - [self sendFileWithUrl:url toRoom:room success:^{ - dispatch_group_leave(dispatchGroup); - } failure:requestFailure]; - }); + [self sendFileWithUrl:url toRooms:rooms success:requestSuccess failure:requestFailure]; }]; } - if (item.type == ShareItemTypeText) { - dispatch_group_enter(dispatchGroup); - [self.shareItemProvider loadItem:item completion:^(NSString *text, NSError *error) { - if (error) { - requestFailure(error); - dispatch_group_leave(dispatchGroup); - return; - } - - dispatch_async(dispatch_get_main_queue(), ^{ - MXStrongifyAndReturnIfNil(self); - [self sendText:text toRoom:room success:^{ - dispatch_group_leave(dispatchGroup); - } failure:requestFailure]; - }); - }]; - } - - if (item.type == ShareItemTypeURL) + if (item.type == ShareItemTypeVideo || item.type == ShareItemTypeMovie) { dispatch_group_enter(dispatchGroup); - [self.shareItemProvider loadItem:item completion:^(NSURL *url, NSError *error) { - if (error) { + [self.shareItemProvider loadItem:item completion:^(NSURL *videoLocalUrl, NSError *error) { + MXStrongifyAndReturnIfNil(self); + + if (error) + { requestFailure(error); - dispatch_group_leave(dispatchGroup); return; } - dispatch_async(dispatch_get_main_queue(), ^{ - MXStrongifyAndReturnIfNil(self); - [self sendText:url.absoluteString toRoom:room success:^{ - dispatch_group_leave(dispatchGroup); - } failure:requestFailure]; - }); + [self sendVideo:videoLocalUrl toRooms:rooms success:requestSuccess failure:requestFailure]; }]; } + if (item.type == ShareItemTypeVoiceMessage) + { + dispatch_group_enter(dispatchGroup); + [self.shareItemProvider loadItem:item completion:^(NSURL *fileURL, NSError *error) { + MXStrongifyAndReturnIfNil(self); + + if (error) + { + requestFailure(error); + return; + } + + [self sendVoiceMessage:fileURL toRooms:rooms success:requestSuccess failure:requestFailure]; + }]; + } + if (item.type == ShareItemTypeImage) { dispatch_group_enter(dispatchGroup); [self.shareItemProvider loadItem:item completion:^(id itemProviderItem, NSError *error) { - if (error) { + MXStrongifyAndReturnIfNil(self); + + if (error) + { requestFailure(error); - dispatch_group_leave(dispatchGroup); return; } @@ -250,30 +306,23 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) imageData = UIImagePNGRepresentation(image); } - MXStrongifyAndReturnIfNil(self); - - if (imageData) + if (!imageData) { - if ([self.shareItemProvider areAllItemsImages]) - { - [self.pendingImages addObject:imageData]; - [pendingImagesItemProviders addObject:item]; - } - else - { - CGSize imageSize = [self imageSizeFromImageData:imageData]; - self.imageCompressionMode = ImageCompressionModeNone; - self.actualLargeSize = MAX(imageSize.width, imageSize.height); - - [self sendImageData:imageData withItem:item toRoom:room success:^{ - dispatch_group_leave(dispatchGroup); - } failure:requestFailure]; - } + requestFailure(error); + return; + } + + if ([self.shareItemProvider areAllItemsImages]) + { + [self.pendingImages addObject:imageData]; } else { - MXLogError(@"[ShareManager] sendContentToRoom: failed to loadItemForTypeIdentifier. Error: %@", error); - dispatch_group_leave(dispatchGroup); + CGSize imageSize = [self imageSizeFromImageData:imageData]; + self.imageCompressionMode = ImageCompressionModeNone; + self.actualLargeSize = MAX(imageSize.width, imageSize.height); + + [self sendImageData:imageData toRooms:rooms success:requestSuccess failure:requestFailure]; } // Only prompt for image resize if all items are images @@ -283,9 +332,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) if ([self.shareItemProvider areAllItemsLoaded]) { UIAlertController *compressionPrompt = [self compressionPromptForPendingImagesWithShareBlock:^{ - [self sendImageDatas:self.pendingImages.copy withItems:pendingImagesItemProviders toRoom:room success:^{ - dispatch_group_leave(dispatchGroup); - } failure:requestFailure]; + [self sendImageDatas:self.pendingImages.copy toRooms:rooms success:requestSuccess failure:requestFailure]; }]; if (compressionPrompt) @@ -300,63 +347,6 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) } }]; } - - if (item.type == ShareItemTypeVideo) - { - dispatch_group_enter(dispatchGroup); - [self.shareItemProvider loadItem:item completion:^(NSURL *videoLocalUrl, NSError *error) { - if (error) { - requestFailure(error); - dispatch_group_leave(dispatchGroup); - return; - } - - dispatch_async(dispatch_get_main_queue(), ^{ - MXStrongifyAndReturnIfNil(self); - [self sendVideo:videoLocalUrl toRoom:room success:^{ - dispatch_group_leave(dispatchGroup); - } failure:requestFailure]; - }); - }]; - } - - if (item.type == ShareItemTypeMovie) - { - dispatch_group_enter(dispatchGroup); - [self.shareItemProvider loadItem:item completion:^(NSURL *videoLocalUrl, NSError *error) { - if (error) { - requestFailure(error); - dispatch_group_leave(dispatchGroup); - return; - } - - dispatch_async(dispatch_get_main_queue(), ^{ - MXStrongifyAndReturnIfNil(self); - [self sendVideo:videoLocalUrl toRoom:room success:^{ - dispatch_group_leave(dispatchGroup); - } failure:requestFailure]; - }); - }]; - } - - if (item.type == ShareItemTypeVoiceMessage) - { - dispatch_group_enter(dispatchGroup); - [self.shareItemProvider loadItem:item completion:^(NSURL *fileURL, NSError *error) { - if (error) { - requestFailure(error); - dispatch_group_leave(dispatchGroup); - return; - } - - dispatch_async(dispatch_get_main_queue(), ^{ - MXStrongifyAndReturnIfNil(self); - [self sendVoiceMessage:fileURL toRoom:room success:^{ - dispatch_group_leave(dispatchGroup); - } failure:requestFailure]; - }); - }]; - } } dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{ @@ -368,30 +358,11 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) } else { - self.completionCallback(ShareManagerResultFinished); + success(); } }); } -- (void)showFailureAlert:(NSString *)title -{ - UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:nil preferredStyle:UIAlertControllerStyleAlert]; - - MXWeakify(self); - UIAlertAction *okAction = [UIAlertAction actionWithTitle:[MatrixKitL10n ok] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { - MXStrongifyAndReturnIfNil(self); - - if (self.completionCallback) - { - self.completionCallback(ShareManagerResultFailed); - } - }]; - - [alertController addAction:okAction]; - - [self.mainViewController presentViewController:alertController animated:YES completion:nil]; -} - - (void)checkUserAccount { // Force account manager to reload account from the local storage. @@ -428,21 +399,14 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) { _fileStore = [[MXFileStore alloc] initWithCredentials:self.userAccount.mxCredentials]; - ShareDataSource *roomDataSource = [[ShareDataSource alloc] initWithMode:DataSourceModeRooms - fileStore:_fileStore - credentials:self.userAccount.mxCredentials]; - - ShareDataSource *peopleDataSource = [[ShareDataSource alloc] initWithMode:DataSourceModePeople - fileStore:_fileStore - credentials:self.userAccount.mxCredentials]; + ShareDataSource *roomDataSource = [[ShareDataSource alloc] initWithFileStore:_fileStore + credentials:self.userAccount.mxCredentials]; [self.shareViewController configureWithState:ShareViewControllerAccountStateConfigured - roomDataSource:roomDataSource - peopleDataSource:peopleDataSource]; + roomDataSource:roomDataSource]; } else { [self.shareViewController configureWithState:ShareViewControllerAccountStateNotConfigured - roomDataSource:nil - peopleDataSource:nil]; + roomDataSource:nil]; } } @@ -581,7 +545,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) return compressionPrompt; } -- (void)didStartSendingToRoom:(MXRoom *)room +- (void)didStartSending { [self.shareViewController showProgressIndicator]; } @@ -738,11 +702,9 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) - (void)presentCompressionPrompt:(UIAlertController *)compressionPrompt { - dispatch_async(dispatch_get_main_queue(), ^{ - [compressionPrompt popoverPresentationController].sourceView = self.mainViewController.view; - [compressionPrompt popoverPresentationController].sourceRect = self.mainViewController.view.frame; - [self.mainViewController presentViewController:compressionPrompt animated:YES completion:nil]; - }); + [compressionPrompt popoverPresentationController].sourceView = self.mainViewController.view; + [compressionPrompt popoverPresentationController].sourceRect = self.mainViewController.view.frame; + [self.mainViewController presentViewController:compressionPrompt animated:YES completion:nil]; } #pragma mark - Notifications @@ -781,11 +743,11 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) #pragma mark - Sharing - (void)sendText:(NSString *)text - toRoom:(MXRoom *)room + toRooms:(NSArray *)rooms success:(dispatch_block_t)success failure:(void(^)(NSError *error))failure { - [self didStartSendingToRoom:room]; + [self didStartSending]; if (!text) { MXLogError(@"[ShareManager] Invalid text."); @@ -793,19 +755,34 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) return; } - [room sendTextMessage:text success:^(NSString *eventId) { - success(); - } failure:^(NSError *error) { - MXLogError(@"[ShareManager] sendTextMessage failed with error %@", error); - failure(error); - }]; + __block NSError *error = nil; + dispatch_group_t dispatchGroup = dispatch_group_create(); + for (MXRoom *room in rooms) { + dispatch_group_enter(dispatchGroup); + [room sendTextMessage:text success:^(NSString *eventId) { + dispatch_group_leave(dispatchGroup); + } failure:^(NSError *innerError) { + MXLogError(@"[ShareManager] sendTextMessage failed with error %@", error); + error = innerError; + dispatch_group_leave(dispatchGroup); + }]; + } + + dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{ + if(error) { + failure(error); + } else { + success(); + } + }); } -- (void)sendFileWithUrl:(NSURL *)fileUrl toRoom:(MXRoom *)room - success:(dispatch_block_t)success - failure:(void(^)(NSError *error))failure +- (void)sendFileWithUrl:(NSURL *)fileUrl + toRooms:(NSArray *)rooms + success:(dispatch_block_t)success + failure:(void(^)(NSError *error))failure { - [self didStartSendingToRoom:room]; + [self didStartSending]; if (!fileUrl) { MXLogError(@"[ShareManager] Invalid file url."); @@ -818,21 +795,185 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) mimeType = [self mimeTypeFromUTI:(__bridge NSString *)uti]; CFRelease(uti); - [room sendFile:fileUrl mimeType:mimeType localEcho:nil success:^(NSString *eventId) { - success(); - } failure:^(NSError *error) { - MXLogError(@"[ShareManager] sendFile failed with error %@", error); - failure(error); - } keepActualFilename:YES]; + __block NSError *error = nil; + dispatch_group_t dispatchGroup = dispatch_group_create(); + for (MXRoom *room in rooms) { + dispatch_group_enter(dispatchGroup); + [room sendFile:fileUrl mimeType:mimeType localEcho:nil success:^(NSString *eventId) { + dispatch_group_leave(dispatchGroup); + } failure:^(NSError *innerError) { + MXLogError(@"[ShareManager] sendFile failed with error %@", innerError); + error = innerError; + dispatch_group_leave(dispatchGroup); + } keepActualFilename:YES]; + } + + dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{ + if(error) { + failure(error); + } else { + success(); + } + }); +} + +- (void)sendVideo:(NSURL *)videoLocalUrl + toRooms:(NSArray *)rooms + success:(dispatch_block_t)success + failure:(void(^)(NSError *error))failure +{ + AVURLAsset *videoAsset = [[AVURLAsset alloc] initWithURL:videoLocalUrl options:nil]; + + MXWeakify(self); + + // Ignore showMediaCompressionPrompt setting due to memory constraints when encrypting large videos. + UIAlertController *compressionPrompt = [MXKTools videoConversionPromptForVideoAsset:videoAsset withCompletion:^(NSString *presetName) { + MXStrongifyAndReturnIfNil(self); + + // If the preset name is nil, the user cancelled. + if (!presetName) + { + return; + } + + // Set the chosen video conversion preset. + [MXSDKOptions sharedInstance].videoConversionPresetName = presetName; + + [self didStartSending]; + if (!videoLocalUrl) + { + MXLogError(@"[ShareManager] Invalid video file url."); + failure(nil); + return; + } + + // Retrieve the video frame at 1 sec to define the video thumbnail + AVAssetImageGenerator *assetImageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:videoAsset]; + assetImageGenerator.appliesPreferredTrackTransform = YES; + CMTime time = CMTimeMake(1, 1); + CGImageRef imageRef = [assetImageGenerator copyCGImageAtTime:time actualTime:NULL error:nil]; + // Finalize video attachment + UIImage *videoThumbnail = [[UIImage alloc] initWithCGImage:imageRef]; + CFRelease(imageRef); + + __block NSError *error = nil; + dispatch_group_t dispatchGroup = dispatch_group_create(); + for (MXRoom *room in rooms) { + dispatch_group_enter(dispatchGroup); + [room sendVideoAsset:videoAsset withThumbnail:videoThumbnail localEcho:nil success:^(NSString *eventId) { + dispatch_group_leave(dispatchGroup); + } failure:^(NSError *innerError) { + MXLogError(@"[ShareManager] Failed sending video with error %@", innerError); + error = innerError; + dispatch_group_leave(dispatchGroup); + }]; + } + + dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{ + if(error) { + failure(error); + } else { + success(); + } + }); + }]; + + [self presentCompressionPrompt:compressionPrompt]; +} + +- (void)sendVoiceMessage:(NSURL *)fileUrl + toRooms:(NSArray *)rooms + success:(dispatch_block_t)success + failure:(void(^)(NSError *error))failure +{ + [self didStartSending]; + if (!fileUrl) + { + MXLogError(@"[ShareManager] Invalid voice message file url."); + failure(nil); + return; + } + + __block NSError *error = nil; + dispatch_group_t dispatchGroup = dispatch_group_create(); + for (MXRoom *room in rooms) { + dispatch_group_enter(dispatchGroup); + [room sendVoiceMessage:fileUrl mimeType:nil duration:0.0 samples:nil localEcho:nil success:^(NSString *eventId) { + dispatch_group_leave(dispatchGroup); + } failure:^(NSError *innerError) { + MXLogError(@"[ShareManager] sendVoiceMessage failed with error %@", error); + error = innerError; + dispatch_group_leave(dispatchGroup); + } keepActualFilename:YES]; + } + + dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{ + if(error) { + failure(error); + } else { + success(); + } + }); +} + +- (void)sendImageDatas:(NSArray> *)imageDatas + toRooms:(NSArray *)rooms + success:(dispatch_block_t)success + failure:(void(^)(NSError *error))failure +{ + if (imageDatas.count == 0) + { + MXLogError(@"[ShareManager] sendImages: no images to send."); + failure(nil); + return; + } + + [self didStartSending]; + + dispatch_group_t requestsGroup = dispatch_group_create(); + __block NSError *firstRequestError; + + NSUInteger index = 0; + + for (NSData *imageData in imageDatas) + { + @autoreleasepool + { + dispatch_group_enter(requestsGroup); + [self sendImageData:imageData toRooms:rooms success:^{ + dispatch_group_leave(requestsGroup); + } failure:^(NSError *error) { + if (error && !firstRequestError) + { + firstRequestError = error; + } + + dispatch_group_leave(requestsGroup); + }]; + } + + index++; + } + + dispatch_group_notify(requestsGroup, dispatch_get_main_queue(), ^{ + + if (firstRequestError) + { + failure(firstRequestError); + } + else + { + success(); + } + }); } - (void)sendImageData:(NSData *)imageData - withItem:(id)item - toRoom:(MXRoom *)room + toRooms:(NSArray *)rooms success:(dispatch_block_t)success failure:(void(^)(NSError *error))failure { - [self didStartSendingToRoom:room]; + [self didStartSending]; NSString *imageUTI; NSString *mimeType; @@ -848,7 +989,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) if (!mimeType) { - MXLogError(@"[ShareManager] sendImage failed. Cannot determine MIME type of %@", item); + MXLogError(@"[ShareManager] sendImage failed. Cannot determine MIME type ."); if (failure) { failure(nil); @@ -921,142 +1062,33 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) imageSize = [self imageSizeFromImageData:imageData]; } - UIImage *thumbnail = nil; - // Thumbnail is useful only in case of encrypted room - if (room.summary.isEncrypted) - { - thumbnail = [MXKTools resizeImageWithData:imageData toFitInSize:CGSizeMake(800, 600)]; - } - - [room sendImage:finalImageData withImageSize:imageSize mimeType:mimeType andThumbnail:thumbnail localEcho:nil success:^(NSString *eventId) { - success(); - } failure:^(NSError *error) { - MXLogError(@"[ShareManager] sendImage failed with error %@", error); - failure(error); - }]; -} - -- (void)sendImageDatas:(NSArray> *)imageDatas - withItems:(NSArray> *)items toRoom:(MXRoom *)room - success:(dispatch_block_t)success - failure:(void(^)(NSError *error))failure -{ - if (imageDatas.count == 0 || imageDatas.count != items.count) - { - MXLogError(@"[ShareManager] sendImages: no images to send."); - failure(nil); - return; - } - - [self didStartSendingToRoom:room]; - - dispatch_group_t requestsGroup = dispatch_group_create(); - __block NSError *firstRequestError; - - NSUInteger index = 0; - - for (NSData *imageData in imageDatas) - { - @autoreleasepool - { - dispatch_group_enter(requestsGroup); - [self sendImageData:imageData withItem:items[index] toRoom:room success:^{ - dispatch_group_leave(requestsGroup); - } failure:^(NSError *error) { - if (error && !firstRequestError) - { - firstRequestError = error; - } + __block NSError *error = nil; + dispatch_group_t dispatchGroup = dispatch_group_create(); + for (MXRoom *room in rooms) { - dispatch_group_leave(requestsGroup); - }]; + UIImage *thumbnail = nil; + if (room.summary.isEncrypted) // Thumbnail is useful only in case of encrypted room + { + thumbnail = [MXKTools resizeImageWithData:imageData toFitInSize:kThumbnailSize]; } - index++; + dispatch_group_enter(dispatchGroup); + [room sendImage:finalImageData withImageSize:imageSize mimeType:mimeType andThumbnail:thumbnail localEcho:nil success:^(NSString *eventId) { + dispatch_group_leave(dispatchGroup); + } failure:^(NSError *innerError) { + MXLogError(@"[ShareManager] sendImage failed with error %@", error); + error = innerError; + dispatch_group_leave(dispatchGroup); + }]; } - dispatch_group_notify(requestsGroup, dispatch_get_main_queue(), ^{ - - if (firstRequestError) - { - failure(firstRequestError); - } - else - { + dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{ + if(error) { + failure(error); + } else { success(); } }); } -- (void)sendVideo:(NSURL *)videoLocalUrl - toRoom:(MXRoom *)room - success:(dispatch_block_t)success - failure:(void(^)(NSError *error))failure -{ - AVURLAsset *videoAsset = [[AVURLAsset alloc] initWithURL:videoLocalUrl options:nil]; - - MXWeakify(self); - - // Ignore showMediaCompressionPrompt setting due to memory constraints when encrypting large videos. - UIAlertController *compressionPrompt = [MXKTools videoConversionPromptForVideoAsset:videoAsset withCompletion:^(NSString *presetName) { - MXStrongifyAndReturnIfNil(self); - - // If the preset name is nil, the user cancelled. - if (!presetName) - { - return; - } - - // Set the chosen video conversion preset. - [MXSDKOptions sharedInstance].videoConversionPresetName = presetName; - - [self didStartSendingToRoom:room]; - if (!videoLocalUrl) - { - MXLogError(@"[ShareManager] Invalid video file url."); - failure(nil); - return; - } - - // Retrieve the video frame at 1 sec to define the video thumbnail - AVAssetImageGenerator *assetImageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:videoAsset]; - assetImageGenerator.appliesPreferredTrackTransform = YES; - CMTime time = CMTimeMake(1, 1); - CGImageRef imageRef = [assetImageGenerator copyCGImageAtTime:time actualTime:NULL error:nil]; - // Finalize video attachment - UIImage *videoThumbnail = [[UIImage alloc] initWithCGImage:imageRef]; - CFRelease(imageRef); - - [room sendVideoAsset:videoAsset withThumbnail:videoThumbnail localEcho:nil success:^(NSString *eventId) { - success(); - } failure:^(NSError *error) { - MXLogError(@"[ShareManager] Failed sending video with error %@", error); - failure(error); - }]; - }]; - - [self presentCompressionPrompt:compressionPrompt]; -} - -- (void)sendVoiceMessage:(NSURL *)fileUrl - toRoom:(MXRoom *)room - success:(dispatch_block_t)success - failure:(void(^)(NSError *error))failure -{ - [self didStartSendingToRoom:room]; - if (!fileUrl) - { - MXLogError(@"[ShareManager] Invalid voice message file url."); - failure(nil); - return; - } - - [room sendVoiceMessage:fileUrl mimeType:nil duration:0.0 samples:nil localEcho:nil success:^(NSString *eventId) { - success(); - } failure:^(NSError *error) { - MXLogError(@"[ShareManager] sendVoiceMessage failed with error %@", error); - failure(error); - } keepActualFilename:YES]; -} - @end diff --git a/RiotShareExtension/Shared/View/RecentRoomTableViewCell.h b/RiotShareExtension/Shared/View/RecentRoomTableViewCell.h index 959ca786e..63eb213f7 100644 --- a/RiotShareExtension/Shared/View/RecentRoomTableViewCell.h +++ b/RiotShareExtension/Shared/View/RecentRoomTableViewCell.h @@ -20,4 +20,6 @@ + (CGFloat)cellHeight; +- (void)setCustomSelected:(BOOL)selected animated:(BOOL)animated; + @end diff --git a/RiotShareExtension/Shared/View/RecentRoomTableViewCell.m b/RiotShareExtension/Shared/View/RecentRoomTableViewCell.m index 5abb551d9..4dec0815a 100644 --- a/RiotShareExtension/Shared/View/RecentRoomTableViewCell.m +++ b/RiotShareExtension/Shared/View/RecentRoomTableViewCell.m @@ -30,6 +30,7 @@ @property (weak, nonatomic) IBOutlet MXKImageView *avatarImageView; @property (weak, nonatomic) IBOutlet UILabel *roomTitleLabel; @property (weak, nonatomic) IBOutlet UIImageView *encryptedRoomIcon; +@property (weak, nonatomic) IBOutlet UIButton *selectionButton; @end @@ -56,6 +57,12 @@ self.roomTitleLabel.textColor = ThemeService.shared.theme.textPrimaryColor; self.contentView.backgroundColor = ThemeService.shared.theme.backgroundColor; + + [self.selectionButton setImage:[UIImage imageNamed:@"radio-button-default"] forState:UIControlStateNormal]; + [self.selectionButton setImage:[UIImage imageNamed:@"radio-button-selected"] forState:UIControlStateSelected]; + + [self.selectionButton setTitle:@"" forState:UIControlStateNormal]; + [self.selectionButton setTitle:@"" forState:UIControlStateSelected]; } - (void)layoutSubviews @@ -92,4 +99,11 @@ return 74; } +- (void)setCustomSelected:(BOOL)selected animated:(BOOL)animated +{ + [UIView animateWithDuration:(animated ? 0.25f : 0.0f) animations:^{ + [self.selectionButton setSelected:selected]; + }]; +} + @end diff --git a/RiotShareExtension/Shared/View/RecentRoomTableViewCell.xib b/RiotShareExtension/Shared/View/RecentRoomTableViewCell.xib index 80e07581e..fe39d2a67 100644 --- a/RiotShareExtension/Shared/View/RecentRoomTableViewCell.xib +++ b/RiotShareExtension/Shared/View/RecentRoomTableViewCell.xib @@ -1,9 +1,9 @@ - + - + @@ -25,12 +25,9 @@ - + diff --git a/RiotShareExtension/Shared/View/RoomsListViewController.m b/RiotShareExtension/Shared/View/RoomsListViewController.m index 8f387a4d4..414e0364a 100644 --- a/RiotShareExtension/Shared/View/RoomsListViewController.m +++ b/RiotShareExtension/Shared/View/RoomsListViewController.m @@ -18,6 +18,7 @@ #import "RoomsListViewController.h" #import "RecentRoomTableViewCell.h" +#import "ShareDataSource.h" #import "RecentCellData.h" #import "ThemeService.h" @@ -140,6 +141,22 @@ return [RecentRoomTableViewCell cellHeight]; } +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + [tableView deselectRowAtIndexPath:indexPath animated:YES]; + + NSString *roomIdentifier = [self.dataSource cellDataAtIndexPath:indexPath].roomSummary.roomId; + + ShareDataSource *dataSource = (ShareDataSource *)self.dataSource; + if ([dataSource.selectedRoomIdentifiers containsObject:roomIdentifier]) { + [dataSource deselectRoomWithIdentifier:roomIdentifier animated:YES]; + } else { + [dataSource selectRoomWithIdentifier:roomIdentifier animated:YES]; + } + + [self.recentsTableView reloadData]; +} + #pragma mark - MXKDataSourceDelegate - (Class)cellViewClassForCellData:(MXKCellData*)cellData diff --git a/RiotShareExtension/Shared/View/ShareViewController.h b/RiotShareExtension/Shared/View/ShareViewController.h index edf5ce348..d45a66049 100644 --- a/RiotShareExtension/Shared/View/ShareViewController.h +++ b/RiotShareExtension/Shared/View/ShareViewController.h @@ -33,9 +33,7 @@ typedef NS_ENUM(NSUInteger, ShareViewControllerAccountState) { @protocol ShareViewControllerDelegate -- (void)shareViewControllerDidRequestShare:(ShareViewController *)shareViewController - forRoomIdentifier:(NSString *)roomIdentifier; - +- (void)shareViewController:(ShareViewController *)shareViewController didRequestShareForRoomIdentifiers:(NSSet *)roomIdentifiers; - (void)shareViewControllerDidRequestDismissal:(ShareViewController *)shareViewController; @end @@ -48,8 +46,7 @@ typedef NS_ENUM(NSUInteger, ShareViewControllerAccountState) { currentState:(ShareViewControllerAccountState)state; - (void)configureWithState:(ShareViewControllerAccountState)state - roomDataSource:(nullable ShareDataSource *)roomDataSource - peopleDataSource:(nullable ShareDataSource *)peopleDataSource; + roomDataSource:(nullable ShareDataSource *)roomDataSource; - (void)showProgressIndicator; diff --git a/RiotShareExtension/Shared/View/ShareViewController.m b/RiotShareExtension/Shared/View/ShareViewController.m index 148069665..cfe4f2a87 100644 --- a/RiotShareExtension/Shared/View/ShareViewController.m +++ b/RiotShareExtension/Shared/View/ShareViewController.m @@ -15,10 +15,9 @@ */ #import "ShareViewController.h" -#import "SegmentedViewController.h" +#import "ShareDataSource.h" #import "RoomsListViewController.h" #import "FallbackViewController.h" -#import "ShareDataSource.h" #import "ThemeService.h" @@ -28,13 +27,16 @@ #import "Riot-Swift.h" #endif -@interface ShareViewController () +@interface ShareViewController () @property (nonatomic, assign, readonly) ShareViewControllerType type; @property (nonatomic, assign) ShareViewControllerAccountState state; + +@property (nonatomic, strong) RoomsListViewController *roomListViewController; @property (nonatomic, strong) ShareDataSource *roomDataSource; -@property (nonatomic, strong) ShareDataSource *peopleDataSource; + +@property (nonatomic, strong) FallbackViewController *fallbackViewController; @property (nonatomic, weak) IBOutlet UIView *masterContainerView; @property (nonatomic, weak) IBOutlet UIButton *cancelButton; @@ -42,8 +44,6 @@ @property (nonatomic, weak) IBOutlet UIButton *shareButton; @property (nonatomic, weak) IBOutlet UIView *contentView; -@property (nonatomic, strong) SegmentedViewController *segmentedViewController; - @property (nonatomic, strong) MXKPieChartHUD *hudView; @end @@ -76,17 +76,17 @@ [self.cancelButton setTitle:[VectorL10n cancel] forState:UIControlStateNormal]; [self.shareButton setTintColor:ThemeService.shared.theme.tintColor]; + [self.shareButton setEnabled:NO]; - [self configureWithState:self.state roomDataSource:self.roomDataSource peopleDataSource:self.peopleDataSource]; + [self configureWithState:self.state roomDataSource:self.roomDataSource]; } - (void)configureWithState:(ShareViewControllerAccountState)state roomDataSource:(ShareDataSource *)roomDataSource - peopleDataSource:(ShareDataSource *)peopleDataSource { self.state = state; self.roomDataSource = roomDataSource; - self.peopleDataSource = peopleDataSource; + self.roomDataSource.shareDelegate = self; if (!self.isViewLoaded) { return; @@ -110,19 +110,11 @@ [self.hudView setProgress:progress]; } -#pragma mark - MXKRecentListViewControllerDelegate +#pragma mark - ShareDataSourceDelegate -- (void)recentListViewController:(MXKRecentListViewController *)recentListViewController - didSelectRoom:(NSString *)roomId - inMatrixSession:(MXSession *)mxSession +- (void)shareDataSourceDidChangeSelectedRoomIdentifiers:(ShareDataSource *)shareDataSource { - [self.delegate shareViewControllerDidRequestShare:self forRoomIdentifier:roomId]; -} - -- (void)recentListViewController:(MXKRecentListViewController *)recentListViewController - didSelectSuggestedRoom:(MXSpaceChildInfo *)childInfo -{ - [self.delegate shareViewControllerDidRequestShare:self forRoomIdentifier:childInfo.childRoomId]; + self.shareButton.enabled = (shareDataSource.selectedRoomIdentifiers.count > 0); } #pragma mark - Private @@ -133,60 +125,53 @@ if (self.state == ShareViewControllerAccountStateConfigured) { - self.titleLabel.text = [VectorL10n sendTo:@""]; - [self.shareButton setTitle:[VectorL10n roomEventActionForward] forState:UIControlStateNormal]; - [self configureSegmentedViewController]; + [self.shareButton setHidden:NO]; + + if (self.type == ShareViewControllerTypeSend) { + [self.titleLabel setText:[VectorL10n sendTo:@""]]; + [self.shareButton setTitle:[VectorL10n sendTo:@""] forState:UIControlStateNormal]; + } else { + [self.titleLabel setText:[VectorL10n roomEventActionForward]]; + [self.shareButton setTitle:[VectorL10n roomEventActionForward] forState:UIControlStateNormal]; + } } else { - self.titleLabel.text = [AppInfo.current displayName]; [self configureFallbackViewController]; + [self.shareButton setHidden:NO]; + + self.titleLabel.text = [AppInfo.current displayName]; } } - (void)configureSegmentedViewController { - RoomsListViewController *roomsViewController = [RoomsListViewController recentListViewController]; - [roomsViewController displayList:self.roomDataSource]; - [roomsViewController setDelegate:self]; - - RoomsListViewController *peopleViewController = [RoomsListViewController recentListViewController]; - [peopleViewController setDelegate:self]; - [peopleViewController displayList:self.peopleDataSource]; - - self.segmentedViewController = [SegmentedViewController segmentedViewController]; - [self.segmentedViewController initWithTitles:@[[VectorL10n titleRooms], [VectorL10n titlePeople]] - viewControllers:@[roomsViewController, peopleViewController] defaultSelected:0]; - - [self addChildViewController:self.segmentedViewController]; - [self.contentView vc_addSubViewMatchingParent:self.segmentedViewController.view]; - [self.segmentedViewController didMoveToParentViewController:self]; + self.roomListViewController = [RoomsListViewController recentListViewController]; + [self.roomListViewController displayList:self.roomDataSource]; + + [self addChildViewController:self.roomListViewController]; + [self.contentView vc_addSubViewMatchingParent:self.roomListViewController.view]; + [self.roomListViewController didMoveToParentViewController:self]; } - (void)configureFallbackViewController { - FallbackViewController *fallbackVC = [FallbackViewController new]; - [self addChildViewController:fallbackVC]; - [self.contentView vc_addSubViewMatchingParent:fallbackVC.view]; - [fallbackVC didMoveToParentViewController:self]; + self.fallbackViewController = [FallbackViewController new]; + [self addChildViewController:self.fallbackViewController]; + [self.contentView vc_addSubViewMatchingParent:self.fallbackViewController.view]; + [self.fallbackViewController didMoveToParentViewController:self]; } - (void)resetContentView { - NSArray *subviews = self.contentView.subviews; - for (UIView *subview in subviews) - { - [subview removeFromSuperview]; - } + [self.roomListViewController willMoveToParentViewController:nil]; + [self.roomListViewController.view removeFromSuperview]; + [self.roomListViewController removeFromParentViewController]; - if (self.segmentedViewController) - { - [self.segmentedViewController removeFromParentViewController]; - - [self.segmentedViewController destroy]; - self.segmentedViewController = nil; - } + [self.fallbackViewController willMoveToParentViewController:nil]; + [self.fallbackViewController.view removeFromSuperview]; + [self.fallbackViewController removeFromParentViewController]; } #pragma mark - Actions @@ -198,7 +183,11 @@ - (IBAction)onShareButtonTap:(UIButton *)sender { + if (self.roomDataSource.selectedRoomIdentifiers.count == 0) { + return; + } + [self.delegate shareViewController:self didRequestShareForRoomIdentifiers:self.roomDataSource.selectedRoomIdentifiers]; } @end diff --git a/RiotShareExtension/Shared/View/ShareViewController.xib b/RiotShareExtension/Shared/View/ShareViewController.xib index c6eaf5feb..ece4f812b 100644 --- a/RiotShareExtension/Shared/View/ShareViewController.xib +++ b/RiotShareExtension/Shared/View/ShareViewController.xib @@ -42,7 +42,7 @@ -