From ccdb571821c18bf81747c9c825199d7cd83af127 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 23 Jul 2021 16:34:43 +0100 Subject: [PATCH 001/100] 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/100] 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/100] 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/100] 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/100] 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/100] 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/100] 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/100] 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/100] 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/100] 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/100] 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/100] 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/100] 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/100] 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/100] 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/100] 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/100] 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/100] 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/100] 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/100] 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/100] 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/100] 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/100] 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/100] 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 5d3c3cf5d71a2943e3ea93e21c75d3b3c4946b73 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 29 Sep 2021 17:40:37 +0100 Subject: [PATCH 032/100] 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 634424d288b7bd11bfb921c964a14834d6ac6990 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 30 Sep 2021 14:14:15 +0100 Subject: [PATCH 033/100] 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 061/100] 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 062/100] 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 063/100] 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 064/100] 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 ce4f7e4d1b2b021ffe35118613df813d4d22ae06 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 10:55:27 +0200 Subject: [PATCH 065/100] 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 066/100] 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 067/100] 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 068/100] 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 069/100] 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 070/100] 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 071/100] 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 072/100] 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 073/100] 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 074/100] 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 075/100] 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 076/100] 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 077/100] 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 078/100] 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 69578ba0b9adf36fe4f441ae246c0d8fefe5515e Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 13:46:33 +0200 Subject: [PATCH 079/100] 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 080/100] 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 081/100] 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 082/100] 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 083/100] 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 084/100] 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 546c92ff4274ea994c8a84986776ea4d95049419 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 16:00:56 +0200 Subject: [PATCH 085/100] 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 086/100] 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 087/100] 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 088/100] 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 089/100] 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 090/100] 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 2d59a6be1c3589fb7c01e0b4fd0d1f962703d7ca Mon Sep 17 00:00:00 2001 From: manuroe Date: Fri, 8 Oct 2021 17:04:44 +0200 Subject: [PATCH 091/100] changelog.d: Upgrade MatrixKit version ([v0.16.5](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.16.5)). --- Podfile | 2 +- changelog.d/x-nolink-0.change | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/x-nolink-0.change diff --git a/Podfile b/Podfile index b06b5f92b..49dec32b5 100644 --- a/Podfile +++ b/Podfile @@ -13,7 +13,7 @@ use_frameworks! # - `{ {kit spec hash} => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for each repo. Used by Fastfile during CI # # Warning: our internal tooling depends on the name of this variable name, so be sure not to change it -$matrixKitVersion = '= 0.16.4' +$matrixKitVersion = '= 0.16.5' # $matrixKitVersion = :local # $matrixKitVersion = {'develop' => 'develop'} diff --git a/changelog.d/x-nolink-0.change b/changelog.d/x-nolink-0.change new file mode 100644 index 000000000..a9359bea0 --- /dev/null +++ b/changelog.d/x-nolink-0.change @@ -0,0 +1 @@ +Upgrade MatrixKit version ([v0.16.5](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.16.5)). \ No newline at end of file From 7b6534941079660efaf4311d11a939207834c5b0 Mon Sep 17 00:00:00 2001 From: manuroe Date: Fri, 8 Oct 2021 17:04:44 +0200 Subject: [PATCH 092/100] version++ --- CHANGES.md | 28 ++++++++++++++++++++++++++++ changelog.d/4559.bugfix | 1 - changelog.d/4896.change | 1 - changelog.d/4898.bugfix | 1 - changelog.d/4899.change | 1 - changelog.d/4920.doc | 1 - changelog.d/4935.change | 1 - changelog.d/4937.build | 1 - changelog.d/4939.change | 1 - changelog.d/4951.build | 1 - changelog.d/4955.change | 1 - changelog.d/4970.bugfix | 1 - changelog.d/pr-4193.bugfix | 1 - changelog.d/x-nolink-0.change | 1 - 14 files changed, 28 insertions(+), 13 deletions(-) delete mode 100644 changelog.d/4559.bugfix delete mode 100644 changelog.d/4896.change delete mode 100644 changelog.d/4898.bugfix delete mode 100644 changelog.d/4899.change delete mode 100644 changelog.d/4920.doc delete mode 100644 changelog.d/4935.change delete mode 100644 changelog.d/4937.build delete mode 100644 changelog.d/4939.change delete mode 100644 changelog.d/4951.build delete mode 100644 changelog.d/4955.change delete mode 100644 changelog.d/4970.bugfix delete mode 100644 changelog.d/pr-4193.bugfix delete mode 100644 changelog.d/x-nolink-0.change diff --git a/CHANGES.md b/CHANGES.md index 7ace58680..ecde7c1af 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,31 @@ +## Changes in 1.6.2 (2021-10-08) + +🙌 Improvements + +- Upgrade MatrixKit version ([v0.16.5](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.16.5)). +- URL Previews: Use attributed string whitespace for cell heights and stop breaking up the bubble data. ([#4896](https://github.com/vector-im/element-ios/issues/4896)) +- Replaced localizable strings with generated ones throughout the code. Fixed various translation issues. ([#4899](https://github.com/vector-im/element-ios/issues/4899)) +- Voice Message scrubbing should require a slightly longer press, to avoid accidental scrubbing when scrolling the timeline ([#4935](https://github.com/vector-im/element-ios/issues/4935)) +- Pods: Update ffmpeg-kit-ios-audio, FLEX, FlowCommoniOS, Reusable and SwiftLint. ([#4939](https://github.com/vector-im/element-ios/issues/4939)) +- Service Terms: Track an analytics value on accept/decline of an identity server. ([#4955](https://github.com/vector-im/element-ios/issues/4955)) + +🐛 Bugfixes + +- RecentsDataSource: Memory leak in [RecentsDataSource dataSource:didStateChange:]. ([#4193](https://github.com/vector-im/element-ios/pull/4193)) +- i18n: Standardise casing of identity server and integration manager. ([#4559](https://github.com/vector-im/element-ios/issues/4559)) +- MasterTabBarController: Listen to `MXSpaceNotificationCounter` to update the notification badge ([#4898](https://github.com/vector-im/element-ios/issues/4898)) +- Fixed unintentional voice message drafts on automatically cancelled recordings (under 1 second) ([#4970](https://github.com/vector-im/element-ios/issues/4970)) + +🧱 Build + +- Element Alpha: Build on macOS 11 to fix iOS 15 installation error. ([#4937](https://github.com/vector-im/element-ios/issues/4937)) +- Bundler: Update CocoaPods and fastlane and xcode-install. ([#4951](https://github.com/vector-im/element-ios/issues/4951)) + +📄 Documentation + +- Update PR template with a checkbox for accessibility and self review. ([#4920](https://github.com/vector-im/element-ios/issues/4920)) + + ## Changes in 1.6.1 (2021-09-30) 🙌 Improvements diff --git a/changelog.d/4559.bugfix b/changelog.d/4559.bugfix deleted file mode 100644 index 1e0688197..000000000 --- a/changelog.d/4559.bugfix +++ /dev/null @@ -1 +0,0 @@ -i18n: Standardise casing of identity server and integration manager. \ No newline at end of file diff --git a/changelog.d/4896.change b/changelog.d/4896.change deleted file mode 100644 index 00f6d5b4e..000000000 --- a/changelog.d/4896.change +++ /dev/null @@ -1 +0,0 @@ -URL Previews: Use attributed string whitespace for cell heights and stop breaking up the bubble data. \ No newline at end of file diff --git a/changelog.d/4898.bugfix b/changelog.d/4898.bugfix deleted file mode 100644 index 0f4c5faf1..000000000 --- a/changelog.d/4898.bugfix +++ /dev/null @@ -1 +0,0 @@ -MasterTabBarController: Listen to `MXSpaceNotificationCounter` to update the notification badge \ No newline at end of file diff --git a/changelog.d/4899.change b/changelog.d/4899.change deleted file mode 100644 index b53120223..000000000 --- a/changelog.d/4899.change +++ /dev/null @@ -1 +0,0 @@ -Replaced localizable strings with generated ones throughout the code. Fixed various translation issues. \ No newline at end of file diff --git a/changelog.d/4920.doc b/changelog.d/4920.doc deleted file mode 100644 index 746bc48aa..000000000 --- a/changelog.d/4920.doc +++ /dev/null @@ -1 +0,0 @@ -Update PR template with a checkbox for accessibility and self review. \ No newline at end of file diff --git a/changelog.d/4935.change b/changelog.d/4935.change deleted file mode 100644 index 93c0dcc9c..000000000 --- a/changelog.d/4935.change +++ /dev/null @@ -1 +0,0 @@ -Voice Message scrubbing should require a slightly longer press, to avoid accidental scrubbing when scrolling the timeline \ No newline at end of file diff --git a/changelog.d/4937.build b/changelog.d/4937.build deleted file mode 100644 index 2e67b1197..000000000 --- a/changelog.d/4937.build +++ /dev/null @@ -1 +0,0 @@ -Element Alpha: Build on macOS 11 to fix iOS 15 installation error. diff --git a/changelog.d/4939.change b/changelog.d/4939.change deleted file mode 100644 index aed2767eb..000000000 --- a/changelog.d/4939.change +++ /dev/null @@ -1 +0,0 @@ -Pods: Update ffmpeg-kit-ios-audio, FLEX, FlowCommoniOS, Reusable and SwiftLint. \ No newline at end of file diff --git a/changelog.d/4951.build b/changelog.d/4951.build deleted file mode 100644 index 6e7d3ab86..000000000 --- a/changelog.d/4951.build +++ /dev/null @@ -1 +0,0 @@ -Bundler: Update CocoaPods and fastlane and xcode-install. \ No newline at end of file diff --git a/changelog.d/4955.change b/changelog.d/4955.change deleted file mode 100644 index ef63ba876..000000000 --- a/changelog.d/4955.change +++ /dev/null @@ -1 +0,0 @@ -Service Terms: Track an analytics value on accept/decline of an identity server. \ No newline at end of file diff --git a/changelog.d/4970.bugfix b/changelog.d/4970.bugfix deleted file mode 100644 index 365cf4aee..000000000 --- a/changelog.d/4970.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed unintentional voice message drafts on automatically cancelled recordings (under 1 second) \ No newline at end of file diff --git a/changelog.d/pr-4193.bugfix b/changelog.d/pr-4193.bugfix deleted file mode 100644 index 10e269cb8..000000000 --- a/changelog.d/pr-4193.bugfix +++ /dev/null @@ -1 +0,0 @@ -RecentsDataSource: Memory leak in [RecentsDataSource dataSource:didStateChange:]. \ No newline at end of file diff --git a/changelog.d/x-nolink-0.change b/changelog.d/x-nolink-0.change deleted file mode 100644 index a9359bea0..000000000 --- a/changelog.d/x-nolink-0.change +++ /dev/null @@ -1 +0,0 @@ -Upgrade MatrixKit version ([v0.16.5](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.16.5)). \ No newline at end of file From 004d73889806af909934b512c18a977f10b22a78 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 8 Oct 2021 17:15:14 +0200 Subject: [PATCH 093/100] 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 cebb38bb4126ce420ca2bb10ba3c13c8d68c5f8c Mon Sep 17 00:00:00 2001 From: manuroe Date: Fri, 8 Oct 2021 17:51:15 +0200 Subject: [PATCH 094/100] finish version++ --- Podfile.lock | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index 27e8a2ced..4a7bfb316 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -45,7 +45,7 @@ PODS: - GBDeviceInfo/Core (6.6.0) - GZIP (1.3.0) - HPGrowingTextView (1.1) - - JitsiMeetSDK (3.5.0) + - JitsiMeetSDK (3.10.2) - KeychainAccess (4.2.2) - KituraContracts (1.2.1): - LoggerAPI (~> 1.7) @@ -58,30 +58,30 @@ PODS: - MatomoTracker (7.4.1): - MatomoTracker/Core (= 7.4.1) - MatomoTracker/Core (7.4.1) - - MatrixKit (0.16.4): + - MatrixKit (0.16.5): - Down (~> 0.11.0) - DTCoreText (~> 1.6.25) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixKit/Core (= 0.16.4) - - MatrixSDK (= 0.20.4) - - MatrixKit/Core (0.16.4): + - MatrixKit/Core (= 0.16.5) + - MatrixSDK (= 0.20.5) + - MatrixKit/Core (0.16.5): - Down (~> 0.11.0) - DTCoreText (~> 1.6.25) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.20.4) - - MatrixSDK (0.20.4): - - MatrixSDK/Core (= 0.20.4) - - MatrixSDK/Core (0.20.4): + - MatrixSDK (= 0.20.5) + - MatrixSDK (0.20.5): + - MatrixSDK/Core (= 0.20.5) + - MatrixSDK/Core (0.20.5): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) - OLMKit (~> 3.2.5) - - Realm (= 10.7.6) + - Realm (= 10.16.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/JingleCallStack (0.20.4): - - JitsiMeetSDK (= 3.5.0) + - MatrixSDK/JingleCallStack (0.20.5): + - JitsiMeetSDK (= 3.10.2) - MatrixSDK/Core - OLMKit (3.2.5): - OLMKit/olmc (= 3.2.5) @@ -89,9 +89,9 @@ PODS: - OLMKit/olmc (3.2.5) - OLMKit/olmcpp (3.2.5) - ReadMoreTextView (3.0.1) - - Realm (10.7.6): - - Realm/Headers (= 10.7.6) - - Realm/Headers (10.7.6) + - Realm (10.16.0): + - Realm/Headers (= 10.16.0) + - Realm/Headers (10.16.0) - Reusable (4.1.2): - Reusable/Storyboard (= 4.1.2) - Reusable/View (= 4.1.2) @@ -124,7 +124,7 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - MatomoTracker (~> 7.4.1) - - MatrixKit (= 0.16.4) + - MatrixKit (= 0.16.5) - MatrixSDK - MatrixSDK/JingleCallStack - OLMKit @@ -195,7 +195,7 @@ SPEC CHECKSUMS: GBDeviceInfo: ed0db16230d2fa280e1cbb39a5a7f60f6946aaec GZIP: 416858efbe66b41b206895ac6dfd5493200d95b3 HPGrowingTextView: 88a716d97fb853bcb08a4a08e4727da17efc9b19 - JitsiMeetSDK: ef6ebbad2237c0e3ea6ff61fea78745f9543b238 + JitsiMeetSDK: 2f118fa770f23e518f3560fc224fae3ac7062223 KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51 KituraContracts: e845e60dc8627ad0a76fa55ef20a45451d8f830b KTCenterFlowLayout: 6e02b50ab2bd865025ae82fe266ed13b6d9eaf97 @@ -204,11 +204,11 @@ SPEC CHECKSUMS: LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b MatomoTracker: 24a846c9d3aa76933183fe9d47fd62c9efa863fb - MatrixKit: e2836912d2dc2072a276526e92679d094b2bd6b5 - MatrixSDK: 317928f6ef7bbffebbf7dbf9ca9dad4920695f1e + MatrixKit: a37efb94bb7c53b5dc912f0fd35971861b6c28bf + MatrixSDK: 417fac309f510b5f8ac121ba8abe3b897953e1ce OLMKit: 9fb4799c4a044dd2c06bda31ec31a12191ad30b5 ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d - Realm: ed860452717c8db8f4bf832b6807f7f2ce708839 + Realm: b6027801398f3743fc222f096faa85281b506e6c Reusable: 6bae6a5e8aa793c9c441db0213c863a64bce9136 SideMenu: f583187d21c5b1dd04c72002be544b555a2627a2 SwiftBase32: 9399c25a80666dc66b51e10076bf591e3bbb8f17 @@ -219,6 +219,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 367d7a514c7d8ea3cc4b0adb5878c8a192c7c2c8 +PODFILE CHECKSUM: e189a08f2a6f081d6eb0f57aaa898833f27a9adb -COCOAPODS: 1.10.1 +COCOAPODS: 1.11.2 From 5119aad49140efb1d9cc46828967403a39bccab3 Mon Sep 17 00:00:00 2001 From: manuroe Date: Fri, 8 Oct 2021 17:51:22 +0200 Subject: [PATCH 095/100] 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 2f0eca71b..90034d66f 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.6.2 -CURRENT_PROJECT_VERSION = 1.6.2 +MARKETING_VERSION = 1.6.3 +CURRENT_PROJECT_VERSION = 1.6.3 From 89583010bb7fc2b1179423c747328929c75371d4 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 11 Oct 2021 11:10:26 +0200 Subject: [PATCH 096/100] 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 097/100] 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 098/100] 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 099/100] 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 100/100] 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