diff --git a/CHANGES.rst b/CHANGES.rst index d6d2cd767..e62d281e9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,12 +5,14 @@ Changes to be released in next version * 🙌 Improvements - * + * Room Norification Settings: Ability to change between "All Messages", "Mentions and Keywords" and "None". Not yet exposed in Element UI. (#4458). 🐛 Bugfix * VoIP: Do not present ended calls. * More fixes to Main.storyboard layout on iPhone 12 Pro Max (#4527) * Fix crash on Apple Silicon Macs. + * Media Picker: Generate video thumbnails with the correct orientation (#4515). + * Directory List (pop-up one): Fix duplicate rooms being shown (#4537). * Use different title for scan button for self verification (#4525) ⚠️ API Changes diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 3cfcf65dc..0d5ae6dae 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -295,6 +295,7 @@ final class BuildSettings: NSObject { static let roomSettingsScreenShowFlairSettings: Bool = true static let roomSettingsScreenShowAdvancedSettings: Bool = true static let roomSettingsScreenAdvancedShowEncryptToVerifiedOption: Bool = true + static let roomSettingsScreenShowNotificationsV2: Bool = false // MARK: - Room Member Screen diff --git a/Riot/Assets/Images.xcassets/Home/RoomContextualMenu/room_action_notification_muted.imageset/Contents.json b/Riot/Assets/Images.xcassets/Home/RoomContextualMenu/room_action_notification_muted.imageset/Contents.json new file mode 100644 index 000000000..f60c4497c --- /dev/null +++ b/Riot/Assets/Images.xcassets/Home/RoomContextualMenu/room_action_notification_muted.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "room_action_notification_muted.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "room_action_notification_muted@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "room_action_notification_muted@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Home/RoomContextualMenu/room_action_notification_muted.imageset/room_action_notification_muted.png b/Riot/Assets/Images.xcassets/Home/RoomContextualMenu/room_action_notification_muted.imageset/room_action_notification_muted.png new file mode 100644 index 000000000..02ca4c76f Binary files /dev/null and b/Riot/Assets/Images.xcassets/Home/RoomContextualMenu/room_action_notification_muted.imageset/room_action_notification_muted.png differ diff --git a/Riot/Assets/Images.xcassets/Home/RoomContextualMenu/room_action_notification_muted.imageset/room_action_notification_muted@2x.png b/Riot/Assets/Images.xcassets/Home/RoomContextualMenu/room_action_notification_muted.imageset/room_action_notification_muted@2x.png new file mode 100644 index 000000000..c264ba430 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Home/RoomContextualMenu/room_action_notification_muted.imageset/room_action_notification_muted@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Home/RoomContextualMenu/room_action_notification_muted.imageset/room_action_notification_muted@3x.png b/Riot/Assets/Images.xcassets/Home/RoomContextualMenu/room_action_notification_muted.imageset/room_action_notification_muted@3x.png new file mode 100644 index 000000000..b9738ab8b Binary files /dev/null and b/Riot/Assets/Images.xcassets/Home/RoomContextualMenu/room_action_notification_muted.imageset/room_action_notification_muted@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/notifications.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/notifications.imageset/Contents.json new file mode 100644 index 000000000..66a053751 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/notifications.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "notifications.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "notifications@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "notifications@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/notifications.imageset/notifications.png b/Riot/Assets/Images.xcassets/Room/notifications.imageset/notifications.png new file mode 100644 index 000000000..6b100741d Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/notifications.imageset/notifications.png differ diff --git a/Riot/Assets/Images.xcassets/Room/notifications.imageset/notifications@2x.png b/Riot/Assets/Images.xcassets/Room/notifications.imageset/notifications@2x.png new file mode 100644 index 000000000..98d86c152 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/notifications.imageset/notifications@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/notifications.imageset/notifications@3x.png b/Riot/Assets/Images.xcassets/Room/notifications.imageset/notifications@3x.png new file mode 100644 index 000000000..ddc1a6e7a Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/notifications.imageset/notifications@3x.png differ diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index a0929d888..4954f8c88 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -710,6 +710,7 @@ Tap the + to start adding people."; "room_details_topic" = "Topic"; "room_details_favourite_tag" = "Favourite"; "room_details_low_priority_tag" = "Low priority"; +"room_details_notifs" = "Notifications"; "room_details_mute_notifs" = "Mute notifications"; "room_details_direct_chat" = "Direct Chat"; "room_details_access_section"="Who can access this room?"; @@ -772,6 +773,17 @@ Tap the + to start adding people."; "room_details_copy_room_address" = "Copy Room Address"; "room_details_copy_room_url" = "Copy Room URL"; +// Room Notification Settings +"room_notifs_settings_notify_me_for" = "Notify me for"; +"room_notifs_settings_all_messages" = "All Messages"; +"room_notifs_settings_mentions_and_keywords" = "Mentions and Keywords only"; +"room_notifs_settings_none" = "None"; +"room_notifs_settings_done_action" = "Done"; +"room_notifs_settings_cancel_action" = "Cancel"; +"room_notifs_settings_manage_notifications" = "You can manage notifications in %@"; +"room_notifs_settings_account_settings" = "Account settings"; +"room_notifs_settings_encrypted_room_notice" = "Please note that mentions & keyword notifications are not available in encrypted rooms on mobile."; + // Group Details "group_details_title" = "Community Details"; "group_details_home" = "Home"; diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index afc57884a..2cec2c63b 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -85,6 +85,7 @@ internal enum Asset { internal static let roomActionFavourite = ImageAsset(name: "room_action_favourite") internal static let roomActionLeave = ImageAsset(name: "room_action_leave") internal static let roomActionNotification = ImageAsset(name: "room_action_notification") + internal static let roomActionNotificationMuted = ImageAsset(name: "room_action_notification_muted") internal static let roomActionPriorityHigh = ImageAsset(name: "room_action_priority_high") internal static let roomActionPriorityLow = ImageAsset(name: "room_action_priority_low") internal static let homeEmptyScreenArtwork = ImageAsset(name: "home_empty_screen_artwork") @@ -144,6 +145,7 @@ internal enum Asset { internal static let membersListIcon = ImageAsset(name: "members_list_icon") internal static let modIcon = ImageAsset(name: "mod_icon") internal static let moreReactions = ImageAsset(name: "more_reactions") + internal static let notifications = ImageAsset(name: "notifications") internal static let scrollup = ImageAsset(name: "scrollup") internal static let roomsEmptyScreenArtwork = ImageAsset(name: "rooms_empty_screen_artwork") internal static let roomsEmptyScreenArtworkDark = ImageAsset(name: "rooms_empty_screen_artwork_dark") diff --git a/Riot/Generated/Storyboards.swift b/Riot/Generated/Storyboards.swift index c66d40f6a..0b77710ce 100644 --- a/Riot/Generated/Storyboards.swift +++ b/Riot/Generated/Storyboards.swift @@ -172,6 +172,11 @@ internal enum StoryboardScene { internal static let initialScene = InitialSceneType(storyboard: RoomInfoListViewController.self) } + internal enum RoomNotificationSettingsViewController: StoryboardType { + internal static let storyboardName = "RoomNotificationSettingsViewController" + + internal static let initialScene = InitialSceneType(storyboard: RoomNotificationSettingsViewController.self) + } internal enum SecretsRecoveryWithKeyViewController: StoryboardType { internal static let storyboardName = "SecretsRecoveryWithKeyViewController" diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 34091e1d9..3168d5ad4 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2714,6 +2714,10 @@ internal enum VectorL10n { internal static var roomDetailsNoLocalAddressesForDm: String { return VectorL10n.tr("Vector", "room_details_no_local_addresses_for_dm") } + /// Notifications + internal static var roomDetailsNotifs: String { + return VectorL10n.tr("Vector", "room_details_notifs") + } /// Members internal static var roomDetailsPeople: String { return VectorL10n.tr("Vector", "room_details_people") @@ -3022,6 +3026,42 @@ internal enum VectorL10n { internal static var roomNoPrivilegesToCreateGroupCall: String { return VectorL10n.tr("Vector", "room_no_privileges_to_create_group_call") } + /// Account settings + internal static var roomNotifsSettingsAccountSettings: String { + return VectorL10n.tr("Vector", "room_notifs_settings_account_settings") + } + /// All Messages + internal static var roomNotifsSettingsAllMessages: String { + return VectorL10n.tr("Vector", "room_notifs_settings_all_messages") + } + /// Cancel + internal static var roomNotifsSettingsCancelAction: String { + return VectorL10n.tr("Vector", "room_notifs_settings_cancel_action") + } + /// Done + internal static var roomNotifsSettingsDoneAction: String { + return VectorL10n.tr("Vector", "room_notifs_settings_done_action") + } + /// Please note that mentions & keyword notifications are not available in encrypted rooms on mobile. + internal static var roomNotifsSettingsEncryptedRoomNotice: String { + return VectorL10n.tr("Vector", "room_notifs_settings_encrypted_room_notice") + } + /// You can manage notifications in %@ + internal static func roomNotifsSettingsManageNotifications(_ p1: String) -> String { + return VectorL10n.tr("Vector", "room_notifs_settings_manage_notifications", p1) + } + /// Mentions and Keywords only + internal static var roomNotifsSettingsMentionsAndKeywords: String { + return VectorL10n.tr("Vector", "room_notifs_settings_mentions_and_keywords") + } + /// None + internal static var roomNotifsSettingsNone: String { + return VectorL10n.tr("Vector", "room_notifs_settings_none") + } + /// Notify me for + internal static var roomNotifsSettingsNotifyMeFor: String { + return VectorL10n.tr("Vector", "room_notifs_settings_notify_me_for") + } /// Connectivity to the server has been lost. internal static var roomOfflineNotification: String { return VectorL10n.tr("Vector", "room_offline_notification") diff --git a/Riot/Modules/Common/Recents/RecentsViewController.h b/Riot/Modules/Common/Recents/RecentsViewController.h index b2b640d80..d3968cfd6 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.h +++ b/Riot/Modules/Common/Recents/RecentsViewController.h @@ -161,10 +161,15 @@ - (void)makeDirectEditedRoom:(BOOL)isDirect; /** - Enable/disable the notifications for the selected room. - */ +Enable/disable the notifications for the selected room. +*/ - (void)muteEditedRoomNotifications:(BOOL)mute; +/** + Edit notification settings for the selected room. + */ +- (void)changeEditedRoomNotificationSettings; + /** Show room directory. */ diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index 6ac8c618b..e76a83cab 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -34,7 +34,7 @@ #import "Riot-Swift.h" -@interface RecentsViewController () +@interface RecentsViewController () { // Tell whether a recents refresh is pending (suspended during editing mode). BOOL isRefreshPending; @@ -74,6 +74,8 @@ @property (nonatomic, strong) CustomSizedPresentationController *customSizedPresentationController; +@property (nonatomic, strong) RoomNotificationSettingsCoordinatorBridgePresenter *roomNotificationSettingsCoordinatorBridgePresenter; + @end @implementation RecentsViewController @@ -1031,12 +1033,31 @@ UIContextualAction *muteAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive title:title handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) { - [self muteEditedRoomNotifications:!isMuted]; + + if ([BuildSettings roomSettingsScreenShowNotificationsV2]) + { + [self changeEditedRoomNotificationSettings]; + } + else + { + [self muteEditedRoomNotifications:!isMuted]; + } + + completionHandler(YES); }]; muteAction.backgroundColor = actionBackgroundColor; - UIImage *notificationImage = [UIImage imageNamed:@"room_action_notification"]; + UIImage *notificationImage; + if([BuildSettings roomSettingsScreenShowNotificationsV2]) + { + notificationImage = isMuted ? [UIImage imageNamed:@"room_action_notification_muted"] : [UIImage imageNamed:@"room_action_notification"]; + } + else + { + notificationImage = [UIImage imageNamed:@"room_action_notification"]; + } + notificationImage = [notificationImage vc_tintedImageUsingColor:isMuted ? unselectedColor : selectedColor]; muteAction.image = [notificationImage vc_notRenderedImage]; @@ -1298,6 +1319,23 @@ } } +- (void)changeEditedRoomNotificationSettings +{ + if (editedRoomId) + { + // Check whether the user didn't leave the room + MXRoom *room = [self.mainSession roomWithRoomId:editedRoomId]; + if (room) + { + // navigate + self.roomNotificationSettingsCoordinatorBridgePresenter = [[RoomNotificationSettingsCoordinatorBridgePresenter alloc] initWithRoom:room]; + self.roomNotificationSettingsCoordinatorBridgePresenter.delegate = self; + [self.roomNotificationSettingsCoordinatorBridgePresenter presentFrom:self animated:YES]; + } + [self cancelEditionMode:isRefreshPending]; + } +} + - (void)muteEditedRoomNotifications:(BOOL)mute { if (editedRoomId) @@ -1307,27 +1345,27 @@ if (room) { [self startActivityIndicator]; - + if (mute) { [room mentionsOnly:^{ - + [self stopActivityIndicator]; - + // Leave editing mode - [self cancelEditionMode:isRefreshPending]; - + [self cancelEditionMode:self->isRefreshPending]; + }]; } else { [room allMessages:^{ - + [self stopActivityIndicator]; - + // Leave editing mode - [self cancelEditionMode:isRefreshPending]; - + [self cancelEditionMode:self->isRefreshPending]; + }]; } } @@ -2219,4 +2257,11 @@ } } +#pragma mark - RoomNotificationSettingsCoordinatorBridgePresenterDelegate +-(void)roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete:(RoomNotificationSettingsCoordinatorBridgePresenter *)coordinatorBridgePresenter +{ + [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; + self.roomNotificationSettingsCoordinatorBridgePresenter = nil; +} + @end diff --git a/Riot/Modules/Common/SectionHeaders/TitleHeaderView.swift b/Riot/Modules/Common/SectionHeaders/TitleHeaderView.swift new file mode 100644 index 000000000..a26d99820 --- /dev/null +++ b/Riot/Modules/Common/SectionHeaders/TitleHeaderView.swift @@ -0,0 +1,39 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Reusable + +class TitleHeaderView: UITableViewHeaderFooterView { + + @IBOutlet weak var label: UILabel! + + func update(title: String) { + label.text = title.uppercased() + } + +} + + +extension TitleHeaderView: NibReusable {} +extension TitleHeaderView: Themable { + + func update(theme: Theme) { + contentView.backgroundColor = theme.headerBackgroundColor + label.textColor = theme.headerTextSecondaryColor + label.font = theme.fonts.body + } +} diff --git a/Riot/Modules/Common/SectionHeaders/TitleHeaderView.xib b/Riot/Modules/Common/SectionHeaders/TitleHeaderView.xib new file mode 100644 index 000000000..c49ab7fff --- /dev/null +++ b/Riot/Modules/Common/SectionHeaders/TitleHeaderView.xib @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/GlobalSearch/Views/DirectoryRecentTableViewCell.m b/Riot/Modules/GlobalSearch/Views/DirectoryRecentTableViewCell.m index 63cdbfbce..26c103fb2 100644 --- a/Riot/Modules/GlobalSearch/Views/DirectoryRecentTableViewCell.m +++ b/Riot/Modules/GlobalSearch/Views/DirectoryRecentTableViewCell.m @@ -54,20 +54,20 @@ self.titleLabel.text = NSLocalizedStringFromTable(@"directory_search_results_title", @"Vector", nil); // Do we need to display like ">20 results found" or "18 results found"? - NSString *descriptionLabel = (publicRoomsDirectoryDataSource.moreThanRoomsCount && publicRoomsDirectoryDataSource.roomsCount > 0) ? NSLocalizedStringFromTable(@"directory_search_results_more_than", @"Vector", nil) : NSLocalizedStringFromTable(@"directory_search_results", @"Vector", nil); + NSString *descriptionLabel = (publicRoomsDirectoryDataSource.searchResultsCountIsLimited && publicRoomsDirectoryDataSource.searchResultsCount > 0) ? NSLocalizedStringFromTable(@"directory_search_results_more_than", @"Vector", nil) : NSLocalizedStringFromTable(@"directory_search_results", @"Vector", nil); self.descriptionLabel.text = [NSString stringWithFormat:descriptionLabel, - publicRoomsDirectoryDataSource.roomsCount, + publicRoomsDirectoryDataSource.searchResultsCount, publicRoomsDirectoryDataSource.searchPattern]; } else { self.titleLabel.text = NSLocalizedStringFromTable(@"directory_cell_title", @"Vector", nil); self.descriptionLabel.text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"directory_cell_description", @"Vector", nil), - publicRoomsDirectoryDataSource.roomsCount]; + publicRoomsDirectoryDataSource.searchResultsCount]; } - if (publicRoomsDirectoryDataSource.roomsCount) + if (publicRoomsDirectoryDataSource.searchResultsCount) { self.userInteractionEnabled = YES; self.chevronImageView.hidden = NO; diff --git a/Riot/Modules/Home/HomeViewController.m b/Riot/Modules/Home/HomeViewController.m index d8d5382fc..74fef7d37 100644 --- a/Riot/Modules/Home/HomeViewController.m +++ b/Riot/Modules/Home/HomeViewController.m @@ -347,7 +347,16 @@ tableViewCell.notificationsButton.tag = room.isMute || room.isMentionsOnly; [tableViewCell.notificationsButton addTarget:self action:@selector(onNotificationsButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; - tableViewCell.notificationsImageView.image = [UIImage imageNamed:@"room_action_notification"]; + + if ([BuildSettings roomSettingsScreenShowNotificationsV2]) + { + tableViewCell.notificationsImageView.image = tableViewCell.notificationsButton.tag ? [UIImage imageNamed:@"room_action_notification_muted"] : [UIImage imageNamed:@"room_action_notification"]; + } + else + { + tableViewCell.notificationsImageView.image = [UIImage imageNamed:@"room_action_notification"]; + } + tableViewCell.notificationsImageView.tintColor = tableViewCell.notificationsButton.tag ? unselectedColor : selectedColor; // Get the room tag (use only the first one). @@ -663,8 +672,15 @@ MXRoom *room = [self.mainSession roomWithRoomId:editedRoomId]; if (room) { - UIButton *button = (UIButton*)sender; - [self muteEditedRoomNotifications:!button.tag]; + if ([BuildSettings roomSettingsScreenShowNotificationsV2]) + { + [self changeEditedRoomNotificationSettings]; + } + else + { + UIButton *button = (UIButton*)sender; + [self muteEditedRoomNotifications:!button.tag]; + } } } } diff --git a/Riot/Modules/MediaPicker/MediaPickerViewController.m b/Riot/Modules/MediaPicker/MediaPickerViewController.m index 6b7725a11..d08e6ab17 100644 --- a/Riot/Modules/MediaPicker/MediaPickerViewController.m +++ b/Riot/Modules/MediaPicker/MediaPickerViewController.m @@ -736,6 +736,7 @@ // create a thumbnail for the first frame AVAsset *asset = [AVAsset assetWithURL:selectedVideoURL]; AVAssetImageGenerator *generator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset]; + generator.appliesPreferredTrackTransform = YES; CGImageRef thumbnailRef = [generator copyCGImageAtTime:kCMTimeZero actualTime:nil error:nil]; // set thumbnail on validationView diff --git a/Riot/Modules/PublicRoomList/DataSources/PublicRoomsDirectoryDataSource.h b/Riot/Modules/PublicRoomList/DataSources/PublicRoomsDirectoryDataSource.h index 752cce049..750daac05 100644 --- a/Riot/Modules/PublicRoomList/DataSources/PublicRoomsDirectoryDataSource.h +++ b/Riot/Modules/PublicRoomList/DataSources/PublicRoomsDirectoryDataSource.h @@ -62,11 +62,16 @@ @property (nonatomic, readonly) NSString *directoryServerDisplayname; /** - The number of public rooms matching `searchPattern`. - It is accurate only if 'moreThanRoomsCount' is NO. + The number of public rooms that have been fetched so far. */ @property (nonatomic, readonly) NSUInteger roomsCount; +/** + The total number of public rooms matching `searchPattern`. + It is accurate only if 'searchResultsCountIsLimited' is NO. + */ +@property (nonatomic, readonly) NSUInteger searchResultsCount; + /** In case of search with a lot of matching public rooms, we cannot return an accurate value except by paginating the full list of rooms, which is not expected. @@ -74,7 +79,7 @@ This flag indicates that we know that there is more matching rooms than we got so far. */ -@property (nonatomic, readonly) BOOL moreThanRoomsCount; +@property (nonatomic, readonly) BOOL searchResultsCountIsLimited; /** The maximum number of public rooms to retrieve during a pagination. diff --git a/Riot/Modules/PublicRoomList/DataSources/PublicRoomsDirectoryDataSource.m b/Riot/Modules/PublicRoomList/DataSources/PublicRoomsDirectoryDataSource.m index e69035aa0..fa8ac0ab0 100644 --- a/Riot/Modules/PublicRoomList/DataSources/PublicRoomsDirectoryDataSource.m +++ b/Riot/Modules/PublicRoomList/DataSources/PublicRoomsDirectoryDataSource.m @@ -165,6 +165,11 @@ static NSString *const kNSFWKeyword = @"nsfw"; } } +- (NSUInteger)roomsCount +{ + return rooms.count; +} + - (NSIndexPath*)cellIndexPathWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)matrixSession { NSIndexPath *indexPath = nil; @@ -217,8 +222,8 @@ static NSString *const kNSFWKeyword = @"nsfw"; // Reset all pagination vars [rooms removeAllObjects]; nextBatch = nil; - _roomsCount = 0; - _moreThanRoomsCount = NO; + _searchResultsCount = 0; + _searchResultsCountIsLimited = NO; _hasReachedPaginationEnd = NO; } @@ -264,14 +269,14 @@ static NSString *const kNSFWKeyword = @"nsfw"; if (!self->_searchPattern) { // When there is no search, we can use totalRoomCountEstimate returned by the server - self->_roomsCount = publicRoomsResponse.totalRoomCountEstimate; - self->_moreThanRoomsCount = NO; + self->_searchResultsCount = publicRoomsResponse.totalRoomCountEstimate; + self->_searchResultsCountIsLimited = NO; } else { // Else we can only display something like ">20 matching rooms" - self->_roomsCount = self->rooms.count; - self->_moreThanRoomsCount = publicRoomsResponse.nextBatch ? YES : NO; + self->_searchResultsCount = self->rooms.count; + self->_searchResultsCountIsLimited = publicRoomsResponse.nextBatch ? YES : NO; } // Detect pagination end diff --git a/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsAvatarView.swift b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsAvatarView.swift new file mode 100644 index 000000000..c9bf9af89 --- /dev/null +++ b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsAvatarView.swift @@ -0,0 +1,51 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Reusable + +class RoomNotificationSettingsAvatarView: UIView { + + @IBOutlet weak var avatarView: MXKImageView! + @IBOutlet weak var nameLabel: UILabel! + + func configure(viewData: AvatarViewDataProtocol) { + let avatarImage = AvatarGenerator.generateAvatar(forMatrixItem: viewData.matrixItemId, withDisplayName: viewData.displayName) + + if let avatarUrl = viewData.avatarUrl { + avatarView.enableInMemoryCache = true + + avatarView.setImageURI(avatarUrl, + withType: nil, + andImageOrientation: .up, + toFitViewSize: avatarView.frame.size, + with: MXThumbnailingMethodCrop, + previewImage: avatarImage, + mediaManager: viewData.mediaManager) + } else { + avatarView.image = avatarImage + } + nameLabel.text = viewData.displayName + } +} + +extension RoomNotificationSettingsAvatarView: NibLoadable { } +extension RoomNotificationSettingsAvatarView: Themable { + func update(theme: Theme) { + nameLabel?.font = theme.fonts.title3SB + nameLabel?.textColor = theme.textPrimaryColor + } +} diff --git a/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsAvatarView.xib b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsAvatarView.xib new file mode 100644 index 000000000..b9c21f892 --- /dev/null +++ b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsAvatarView.xib @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsBridgePresenter.swift b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsBridgePresenter.swift new file mode 100644 index 000000000..0be2f18a1 --- /dev/null +++ b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsBridgePresenter.swift @@ -0,0 +1,100 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +import Foundation + +@objc protocol RoomNotificationSettingsCoordinatorBridgePresenterDelegate { + func roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: RoomNotificationSettingsCoordinatorBridgePresenter) +} + +/// RoomNotificationSettingsCoordinatorBridgePresenter enables to start RoomNotificationSettingsCoordinator from a view controller. +/// This bridge is used while waiting for global usage of coordinator pattern. +/// It breaks the Coordinator abstraction and it has been introduced for Objective-C compatibility (mainly for integration in legacy view controllers). +/// Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. +@objcMembers +final class RoomNotificationSettingsCoordinatorBridgePresenter: NSObject { + + // MARK: - Properties + + // MARK: Private + + private let room: MXRoom + private var coordinator: RoomNotificationSettingsCoordinator? + + // MARK: Public + + weak var delegate: RoomNotificationSettingsCoordinatorBridgePresenterDelegate? + + // MARK: - Setup + + init(room: MXRoom) { + self.room = room + super.init() + } + + // MARK: - Public + + // NOTE: Default value feature is not compatible with Objective-C. + // func present(from viewController: UIViewController, animated: Bool) { + // self.present(from: viewController, animated: animated) + // } + + func present(from viewController: UIViewController, animated: Bool) { + let roomNotificationSettingsCoordinator = RoomNotificationSettingsCoordinator(room: room) + roomNotificationSettingsCoordinator.delegate = self + let presentable = roomNotificationSettingsCoordinator.toPresentable() + let navigationController = RiotNavigationController(rootViewController: presentable) + navigationController.modalPresentationStyle = .formSheet + presentable.presentationController?.delegate = self + viewController.present(navigationController, animated: animated, completion: nil) + roomNotificationSettingsCoordinator.start() + + self.coordinator = roomNotificationSettingsCoordinator + } + + func dismiss(animated: Bool, completion: (() -> Void)?) { + guard let coordinator = self.coordinator else { + return + } + coordinator.toPresentable().dismiss(animated: animated) { + self.coordinator = nil + + if let completion = completion { + completion() + } + } + } +} + +// MARK: - RoomNotificationSettingsCoordinatorDelegate +extension RoomNotificationSettingsCoordinatorBridgePresenter: RoomNotificationSettingsCoordinatorDelegate { + func roomNotificationSettingsCoordinatorDidCancel(_ coordinator: RoomNotificationSettingsCoordinatorType) { + self.delegate?.roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self) + } + + func roomNotificationSettingsCoordinatorDidComplete(_ coordinator: RoomNotificationSettingsCoordinatorType) { + self.delegate?.roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self) + } +} + +// MARK: - UIAdaptivePresentationControllerDelegate + +extension RoomNotificationSettingsCoordinatorBridgePresenter: UIAdaptivePresentationControllerDelegate { + + func roomNotificationSettingsCoordinatorDidComplete(_ presentationController: UIPresentationController) { + self.delegate?.roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self) + } + +} diff --git a/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsCellViewData.swift b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsCellViewData.swift new file mode 100644 index 000000000..2b50835d1 --- /dev/null +++ b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsCellViewData.swift @@ -0,0 +1,22 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct RoomNotificationSettingsCellViewData { + let notificicationState: RoomNotificationState + let selected: Bool +} diff --git a/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsCoordinator.swift b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsCoordinator.swift new file mode 100644 index 000000000..2d8e48cee --- /dev/null +++ b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsCoordinator.swift @@ -0,0 +1,75 @@ +// File created from ScreenTemplate +// $ createScreen.sh Room/NotificationSettings RoomNotificationSettings +/* + Copyright 2021 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation +import UIKit + +final class RoomNotificationSettingsCoordinator: RoomNotificationSettingsCoordinatorType { + + // MARK: - Properties + + // MARK: Private + private var roomNotificationSettingsViewModel: RoomNotificationSettingsViewModelType + private let roomNotificationSettingsViewController: RoomNotificationSettingsViewController + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + + weak var delegate: RoomNotificationSettingsCoordinatorDelegate? + + // MARK: - Setup + + init(room: MXRoom, showAvatar: Bool = true) { + let repository = RoomNotificationSettingsService(room: room) + + let avatarData = showAvatar ? RoomAvatarViewData( + roomId: room.roomId, + displayName: room.summary.displayname, + avatarUrl: room.summary.avatar, + mediaManager: room.mxSession.mediaManager + ) : nil + let roomNotificationSettingsViewModel = RoomNotificationSettingsViewModel(roomNotificationService: repository, roomEncrypted: room.summary.isEncrypted, avatarViewData: avatarData) + let roomNotificationSettingsViewController = RoomNotificationSettingsViewController.instantiate(with: roomNotificationSettingsViewModel) + self.roomNotificationSettingsViewModel = roomNotificationSettingsViewModel + self.roomNotificationSettingsViewController = roomNotificationSettingsViewController + } + + // MARK: - Public methods + + func start() { + self.roomNotificationSettingsViewModel.coordinatorDelegate = self + } + + func toPresentable() -> UIViewController { + return self.roomNotificationSettingsViewController + } +} + +// MARK: - RoomNotificationSettingsViewModelCoordinatorDelegate +extension RoomNotificationSettingsCoordinator: RoomNotificationSettingsViewModelCoordinatorDelegate { + + func roomNotificationSettingsViewModelDidComplete(_ viewModel: RoomNotificationSettingsViewModelType) { + self.delegate?.roomNotificationSettingsCoordinatorDidComplete(self) + } + + func roomNotificationSettingsViewModelDidCancel(_ viewModel: RoomNotificationSettingsViewModelType) { + self.delegate?.roomNotificationSettingsCoordinatorDidCancel(self) + } +} diff --git a/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsCoordinatorType.swift b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsCoordinatorType.swift new file mode 100644 index 000000000..ac0c6288b --- /dev/null +++ b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsCoordinatorType.swift @@ -0,0 +1,29 @@ +// File created from ScreenTemplate +// $ createScreen.sh Room/NotificationSettings RoomNotificationSettings +/* + Copyright 2021 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +protocol RoomNotificationSettingsCoordinatorDelegate: AnyObject { + func roomNotificationSettingsCoordinatorDidComplete(_ coordinator: RoomNotificationSettingsCoordinatorType) + func roomNotificationSettingsCoordinatorDidCancel(_ coordinator: RoomNotificationSettingsCoordinatorType) +} + +/// `RoomNotificationSettingsCoordinatorType` is a protocol describing a Coordinator that handles changes to the room navigation settings navigation flow. +protocol RoomNotificationSettingsCoordinatorType: Coordinator, Presentable { + var delegate: RoomNotificationSettingsCoordinatorDelegate? { get } +} diff --git a/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsFooter.swift b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsFooter.swift new file mode 100644 index 000000000..db5285192 --- /dev/null +++ b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsFooter.swift @@ -0,0 +1,60 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit +import Reusable + +class RoomNotificationSettingsFooter: UITableViewHeaderFooterView { + + struct State { + let showEncryptedNotice: Bool + let showAccountLink: Bool + } + + @IBOutlet weak var label: UILabel! + + func update(footerState: State) { + + // Don't include link until global settings in place +// let paragraphStyle = NSMutableParagraphStyle() +// paragraphStyle.lineHeightMultiple = 1.16 +// let paragraphAttributes: [NSAttributedString.Key: Any] = [ +// NSAttributedString.Key.kern: -0.08, +// NSAttributedString.Key.paragraphStyle: paragraphStyle, +// NSAttributedString.Key.font: UIFont.systemFont(ofSize: 13.0) +// ] +// let linkStr = VectorL10n.roomNotifsSettingsAccountSettings +// let formatStr = VectorL10n.roomNotifsSettingsManageNotifications(linkStr) +// +// let formattedStr = String(format: formatStr, arguments: [linkStr]) +// let footer0 = NSMutableAttributedString(string: formattedStr, attributes: paragraphAttributes) +// let linkRange = (footer0.string as NSString).range(of: linkStr) +// footer0.addAttribute(NSAttributedString.Key.link, value: Constants.linkToAccountSettings, range: linkRange) + + label.text = footerState.showEncryptedNotice ? VectorL10n.roomNotifsSettingsEncryptedRoomNotice : nil + + } +} + + +extension RoomNotificationSettingsFooter: NibReusable {} +extension RoomNotificationSettingsFooter: Themable { + + func update(theme: Theme) { + contentView.backgroundColor = theme.headerBackgroundColor + label.textColor = theme.headerTextSecondaryColor + } +} diff --git a/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsFooter.xib b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsFooter.xib new file mode 100644 index 000000000..48895dca9 --- /dev/null +++ b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsFooter.xib @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsSerivceType.swift b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsSerivceType.swift new file mode 100644 index 000000000..22961ad03 --- /dev/null +++ b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsSerivceType.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 Foundation + +typealias UpdateRoomNotificationStateCompletion = () -> Void +typealias RoomNotificationStateCallback = (RoomNotificationState) -> Void + +protocol RoomNotificationSettingsServiceType { + + func observeNotificationState(listener: @escaping RoomNotificationStateCallback) + func update(state: RoomNotificationState, completion: @escaping UpdateRoomNotificationStateCompletion) + var notificationState: RoomNotificationState { get } +} diff --git a/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsViewAction.swift b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsViewAction.swift new file mode 100644 index 000000000..5d42f64fa --- /dev/null +++ b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsViewAction.swift @@ -0,0 +1,26 @@ +// File created from ScreenTemplate +// $ createScreen.sh Room/NotificationSettings RoomNotificationSettings +/* + Copyright 2021 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation +/// RoomNotificationSettingsViewController view actions exposed to view model +enum RoomNotificationSettingsViewAction { + case load + case selectNotificationState(RoomNotificationState) + case save + case cancel +} diff --git a/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsViewController.storyboard b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsViewController.storyboard new file mode 100644 index 000000000..8d49ce878 --- /dev/null +++ b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsViewController.storyboard @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsViewController.swift b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsViewController.swift new file mode 100644 index 000000000..3e2adf89e --- /dev/null +++ b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsViewController.swift @@ -0,0 +1,233 @@ +// File created from ScreenTemplate +// $ createScreen.sh Room/NotificationSettings RoomNotificationSettings +/* + Copyright 2021 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit + +final class RoomNotificationSettingsViewController: UIViewController { + + // MARK: - Properties + + private enum Constants { + static let linkToAccountSettings = "linkToAccountSettings" + } + + // MARK: Outlets + + @IBOutlet private weak var mainTableView: UITableView! + + // MARK: Private + + private var viewModel: RoomNotificationSettingsViewModelType! + private var theme: Theme! + private var errorPresenter: MXKErrorPresentation! + private var activityPresenter: ActivityIndicatorPresenter! + private lazy var avatarView: RoomNotificationSettingsAvatarView = { + RoomNotificationSettingsAvatarView.loadFromNib() + }() + + private struct Row { + var cellViewData: RoomNotificationSettingsCellViewData + var action: (() -> Void)? + } + + private struct Section { + var title: String + var rows: [Row] + var footerState: RoomNotificationSettingsFooter.State + } + + private var sections: [Section] = [] { + didSet { + mainTableView.reloadData() + } + } + + private var viewState: RoomNotificationSettingsViewStateType! + + // MARK: - Setup + + class func instantiate(with viewModel: RoomNotificationSettingsViewModelType) -> RoomNotificationSettingsViewController { + let viewController = StoryboardScene.RoomNotificationSettingsViewController.initialScene.instantiate() + viewController.viewModel = viewModel + viewController.theme = ThemeService.shared().theme + return viewController + } + + // MARK: - Life cycle + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + + setupViews() + activityPresenter = ActivityIndicatorPresenter() + errorPresenter = MXKErrorAlertPresentation() + + registerThemeServiceDidChangeThemeNotification() + update(theme: theme) + + viewModel.viewDelegate = self + viewModel.process(viewAction: .load) + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return theme.statusBarStyle + } + + // MARK: - Private + + private func update(theme: Theme) { + self.theme = theme + + view.backgroundColor = theme.headerBackgroundColor + mainTableView.backgroundColor = theme.headerBackgroundColor + + if let navigationBar = navigationController?.navigationBar { + theme.applyStyle(onNavigationBar: navigationBar) + } + mainTableView.reloadData() + } + + private func registerThemeServiceDidChangeThemeNotification() { + NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil) + } + + @objc private func themeDidChange() { + update(theme: ThemeService.shared().theme) + } + + private func setupViews() { + + self.title = VectorL10n.roomDetailsNotifs + let doneBarButtonItem = MXKBarButtonItem(title: VectorL10n.roomNotifsSettingsDoneAction, style: .plain) { [weak self] in + self?.viewModel.process(viewAction: .save) + } + + let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.roomNotifsSettingsCancelAction, style: .plain) { [weak self] in + self?.viewModel.process(viewAction: .cancel) + } + + if navigationController?.navigationBar.backItem == nil { + navigationItem.leftBarButtonItem = cancelBarButtonItem + } + navigationItem.rightBarButtonItem = doneBarButtonItem + mainTableView.register(cellType: RoomNotificationSettingsCell.self) + mainTableView.register(headerFooterViewType: RoomNotificationSettingsFooter.self) + mainTableView.register(headerFooterViewType: TitleHeaderView.self) + mainTableView.sectionFooterHeight = UITableView.automaticDimension + mainTableView.sectionHeaderHeight = UITableView.automaticDimension + mainTableView.estimatedSectionFooterHeight = 50 + mainTableView.estimatedSectionHeaderHeight = 30 + } + + private func render(viewState: RoomNotificationSettingsViewStateType) { + + if viewState.saving { + activityPresenter.presentActivityIndicator(on: view, animated: true) + } else { + activityPresenter.removeCurrentActivityIndicator(animated: true) + } + self.viewState = viewState + if let avatarData = viewState.avatarData { + mainTableView.tableHeaderView = avatarView + avatarView.configure(viewData: avatarData) + avatarView.update(theme: theme) + } + updateSections() + } + + private func updateSections() { + let rows = viewState.notificationOptions.map({ (setting) -> Row in + let cellViewData = RoomNotificationSettingsCellViewData(notificicationState: setting, selected: viewState.notificationState == setting) + return Row(cellViewData: cellViewData, + action: { + self.viewModel.process(viewAction: .selectNotificationState(setting)) + }) + }) + let footerState = RoomNotificationSettingsFooter.State(showEncryptedNotice: viewState.roomEncrypted, showAccountLink: false) + let section0 = Section(title: VectorL10n.roomNotifsSettingsNotifyMeFor, rows: rows, footerState: footerState) + sections = [ + section0 + ] + } +} + +// MARK: - UITableViewDataSource +extension RoomNotificationSettingsViewController: UITableViewDataSource { + + func numberOfSections(in tableView: UITableView) -> Int { + return sections.count + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return sections[section].rows.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let row = sections[indexPath.section].rows[indexPath.row] + let cell: RoomNotificationSettingsCell = tableView.dequeueReusableCell(for: indexPath) + cell.update(state: row.cellViewData) + cell.update(theme: theme) + return cell + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return UITableView.automaticDimension + } + + func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + return UITableView.automaticDimension + } + +} + +// MARK: - UITableViewDelegate +extension RoomNotificationSettingsViewController: UITableViewDelegate { + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + guard let headerView: TitleHeaderView = tableView.dequeueReusableHeaderFooterView() else { return nil } + headerView.update(title: sections[section].title) + headerView.update(theme: theme) + return headerView + } + + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + guard let footerView: RoomNotificationSettingsFooter = tableView.dequeueReusableHeaderFooterView() else { return nil } + let footerState = sections[section].footerState + footerView.update(footerState: footerState) + footerView.update(theme: theme) + return footerView + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + let row = sections[indexPath.section].rows[indexPath.row] + row.action?() + } + +} + +// MARK: - RoomNotificationSettingsViewModelViewDelegate +extension RoomNotificationSettingsViewController: RoomNotificationSettingsViewModelViewDelegate { + + func roomNotificationSettingsViewModel(_ viewModel: RoomNotificationSettingsViewModelType, didUpdateViewState viewSate: RoomNotificationSettingsViewStateType) { + render(viewState: viewSate) + } +} diff --git a/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsViewModel.swift b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsViewModel.swift new file mode 100644 index 000000000..e9f5729df --- /dev/null +++ b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsViewModel.swift @@ -0,0 +1,89 @@ +// File created from ScreenTemplate +// $ createScreen.sh Room/NotificationSettings RoomNotificationSettings +/* + Copyright 2021 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +final class RoomNotificationSettingsViewModel: RoomNotificationSettingsViewModelType { + + // MARK: - Properties + + // MARK: Private + + private let roomNotificationService: RoomNotificationSettingsServiceType + private var state: RoomNotificationSettingsViewState { + willSet { + update(viewState: newValue) + } + } + // MARK: Public + + weak var viewDelegate: RoomNotificationSettingsViewModelViewDelegate? + + weak var coordinatorDelegate: RoomNotificationSettingsViewModelCoordinatorDelegate? + + // MARK: - Setup + + init(roomNotificationService: RoomNotificationSettingsServiceType, roomEncrypted: Bool, avatarViewData: AvatarViewDataProtocol?) { + self.roomNotificationService = roomNotificationService + + let notificationState = Self.mapNotificationStateOnRead(encrypted: roomEncrypted, state: roomNotificationService.notificationState) + self.state = RoomNotificationSettingsViewState(roomEncrypted: roomEncrypted, saving: false, notificationState: notificationState, avatarData: avatarViewData) + self.roomNotificationService.observeNotificationState { [weak self] state in + guard let self = self else { return } + + self.state.notificationState = Self.mapNotificationStateOnRead(encrypted: roomEncrypted, state: state) + } + } + + // MARK: - Public + + func process(viewAction: RoomNotificationSettingsViewAction) { + switch viewAction { + case .load: + update(viewState: self.state) + case .selectNotificationState(let state): + self.state.notificationState = state + case .save: + self.state.saving = true + roomNotificationService.update(state: state.notificationState) { [weak self] in + guard let self = self else { return } + self.state.saving = false + self.coordinatorDelegate?.roomNotificationSettingsViewModelDidComplete(self) + } + case .cancel: + coordinatorDelegate?.roomNotificationSettingsViewModelDidCancel(self) + } + } + + // MARK: - Private + + private static func mapNotificationStateOnRead(encrypted: Bool, state: RoomNotificationState) -> RoomNotificationState { + if encrypted, case .mentionsAndKeywordsOnly = state { + // Notifications not supported on encrypted rooms, map mentionsOnly to mute on read + return .mute + } else { + return state + } + } + + private func update(viewState: RoomNotificationSettingsViewStateType) { + self.viewDelegate?.roomNotificationSettingsViewModel(self, didUpdateViewState: viewState) + } + + +} diff --git a/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsViewModelType.swift b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsViewModelType.swift new file mode 100644 index 000000000..e2fe135d4 --- /dev/null +++ b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsViewModelType.swift @@ -0,0 +1,37 @@ +// File created from ScreenTemplate +// $ createScreen.sh Room/NotificationSettings RoomNotificationSettings +/* + Copyright 2021 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +protocol RoomNotificationSettingsViewModelViewDelegate: AnyObject { + func roomNotificationSettingsViewModel(_ viewModel: RoomNotificationSettingsViewModelType, didUpdateViewState viewState: RoomNotificationSettingsViewStateType) +} + +protocol RoomNotificationSettingsViewModelCoordinatorDelegate: AnyObject { + func roomNotificationSettingsViewModelDidComplete(_ viewModel: RoomNotificationSettingsViewModelType) + func roomNotificationSettingsViewModelDidCancel(_ viewModel: RoomNotificationSettingsViewModelType) +} + +/// Protocol describing the view model used by `RoomNotificationSettingsViewController` +protocol RoomNotificationSettingsViewModelType { + + var viewDelegate: RoomNotificationSettingsViewModelViewDelegate? { get set } + var coordinatorDelegate: RoomNotificationSettingsViewModelCoordinatorDelegate? { get set } + + func process(viewAction: RoomNotificationSettingsViewAction) +} diff --git a/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsViewStateType.swift b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsViewStateType.swift new file mode 100644 index 000000000..56de53b13 --- /dev/null +++ b/Riot/Modules/Room/NotificationSettings/RoomNotificationSettingsViewStateType.swift @@ -0,0 +1,42 @@ +// File created from ScreenTemplate +// $ createScreen.sh Room/NotificationSettings RoomNotificationSettings +/* + Copyright 2021 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +/// RoomNotificationSettingsViewController view state +struct RoomNotificationSettingsViewState: RoomNotificationSettingsViewStateType { + let roomEncrypted: Bool + var saving: Bool + var notificationState: RoomNotificationState + var notificationOptions: [RoomNotificationState] { + if roomEncrypted { + return [.all, .mute] + } else { + return RoomNotificationState.allCases + } + } + let avatarData: AvatarViewDataProtocol? +} + +protocol RoomNotificationSettingsViewStateType { + var saving: Bool { get } + var roomEncrypted: Bool { get } + var notificationOptions: [RoomNotificationState] { get } + var notificationState: RoomNotificationState { get } + var avatarData: AvatarViewDataProtocol? { get } +} diff --git a/Riot/Modules/Room/NotificationSettings/RoomNotificationState.swift b/Riot/Modules/Room/NotificationSettings/RoomNotificationState.swift new file mode 100644 index 000000000..054993117 --- /dev/null +++ b/Riot/Modules/Room/NotificationSettings/RoomNotificationState.swift @@ -0,0 +1,23 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +enum RoomNotificationState: CaseIterable { + case all + case mentionsAndKeywordsOnly + case mute +} diff --git a/Riot/Modules/Room/NotificationSettings/RoomNotificationsSettingsCell.swift b/Riot/Modules/Room/NotificationSettings/RoomNotificationsSettingsCell.swift new file mode 100644 index 000000000..343c7bba4 --- /dev/null +++ b/Riot/Modules/Room/NotificationSettings/RoomNotificationsSettingsCell.swift @@ -0,0 +1,56 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +import UIKit +import Reusable + +class RoomNotificationSettingsCell: UITableViewCell { + + func update(state: RoomNotificationSettingsCellViewData) { + textLabel?.text = state.notificicationState.title + if state.selected { + accessoryView = UIImageView(image: Asset.Images.checkmark.image) + } else { + accessoryView = nil + } + } +} + +extension RoomNotificationSettingsCell: Reusable {} + +extension RoomNotificationSettingsCell: Themable { + func update(theme: Theme) { + textLabel?.font = theme.fonts.body + textLabel?.textColor = theme.textPrimaryColor + backgroundColor = theme.backgroundColor + contentView.backgroundColor = .clear + tintColor = theme.tintColor + selectedBackgroundView = UIView() + selectedBackgroundView?.backgroundColor = theme.selectedBackgroundColor + } +} + +fileprivate extension RoomNotificationState { + var title: String { + switch self { + case .all: + return VectorL10n.roomNotifsSettingsAllMessages + case .mentionsAndKeywordsOnly: + return VectorL10n.roomNotifsSettingsMentionsAndKeywords + case .mute: + return VectorL10n.roomNotifsSettingsNone + } + } +} diff --git a/Riot/Modules/Room/NotificationSettings/RoomNotificationsSettingsService.swift b/Riot/Modules/Room/NotificationSettings/RoomNotificationsSettingsService.swift new file mode 100644 index 000000000..1bd0e4b6b --- /dev/null +++ b/Riot/Modules/Room/NotificationSettings/RoomNotificationsSettingsService.swift @@ -0,0 +1,325 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +final class RoomNotificationSettingsService: RoomNotificationSettingsServiceType { + + typealias Completion = () -> Void + + // MARK: - Properties + + // MARK: Private + + private let room: MXRoom + + private var notificationCenterDidUpdateObserver: NSObjectProtocol? + private var notificationCenterDidFailObserver: NSObjectProtocol? + + private var observers: [ObjectIdentifier] = [] + + // MARK: Public + + var notificationState: RoomNotificationState { + room.notificationState + } + + // MARK: - Setup + + init(room: MXRoom) { + self.room = room + } + + deinit { + observers.forEach(NotificationCenter.default.removeObserver) + } + + // MARK: - Public + + func observeNotificationState(listener: @escaping RoomNotificationStateCallback) { + + let observer = NotificationCenter.default.addObserver( + forName: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules), + object: nil, + queue: OperationQueue.main) { [weak self] _ in + guard let self = self else { return } + listener(self.room.notificationState) + } + observers += [ObjectIdentifier(observer)] + } + + func update(state: RoomNotificationState, completion: @escaping Completion) { + switch state { + case .all: + allMessages(completion: completion) + case .mentionsAndKeywordsOnly: + mentionsOnly(completion: completion) + case .mute: + mute(completion: completion) + } + } + + // MARK: - Private + + private func mute(completion: @escaping Completion) { + guard !room.isMuted else { + completion() + return + } + + if let rule = room.roomPushRule { + removePushRule(rule: rule) { + self.mute(completion: completion) + } + return + } + + guard let rule = room.overridePushRule else { + self.addPushRuleToMute(completion: completion) + return + } + + guard notificationCenterDidUpdateObserver == nil else { + MXLog.debug("[RoomNotificationSettingsService] Request in progress: ignore push rule update") + completion() + return + } + + // if the user defined one, use it + if rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify) { + enablePushRule(rule: rule, completion: completion) + } else { + removePushRule(rule: rule) { + self.addPushRuleToMute(completion: completion) + } + } + } + + private func mentionsOnly(completion: @escaping Completion) { + guard !room.isMentionsOnly else { + completion() + return + } + + if let rule = room.overridePushRule, room.isMuted { + removePushRule(rule: rule) { + self.mentionsOnly(completion: completion) + } + return + } + + guard let rule = room.roomPushRule else { + addPushRuleToMentionOnly(completion: completion) + return + } + + guard notificationCenterDidUpdateObserver == nil else { + MXLog.debug("[MXRoom+Riot] Request in progress: ignore push rule update") + completion() + return + } + + // if the user defined one, use it + if rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify) { + enablePushRule(rule: rule, completion: completion) + } else { + removePushRule(rule: rule) { + self.addPushRuleToMentionOnly(completion: completion) + } + } + + } + + private func allMessages(completion: @escaping Completion) { + if !room.isMentionsOnly && !room.isMuted { + completion() + return + } + + if let rule = room.overridePushRule, room.isMuted { + removePushRule(rule: rule) { + self.allMessages(completion: completion) + } + return + } + + if let rule = room.roomPushRule, room.isMentionsOnly { + removePushRule(rule: rule, completion: completion) + } + } + + private func addPushRuleToMentionOnly(completion: @escaping Completion) { + handleUpdateCallback(completion) { [weak self] in + guard let self = self else { return true } + return self.room.roomPushRule != nil + } + handleFailureCallback(completion) + + room.mxSession.notificationCenter.addRoomRule( + room.roomId, + notify: false, + sound: false, + highlight: false) + } + + private func addPushRuleToMute(completion: @escaping Completion) { + guard let roomId = room.roomId else { + return + } + handleUpdateCallback(completion) { [weak self] in + guard let self = self else { return true } + return self.room.overridePushRule != nil + } + handleFailureCallback(completion) + + room.mxSession.notificationCenter.addOverrideRule( + withId: roomId, + conditions: [["kind": "event_match", "key": "room_id", "pattern": roomId]], + notify: false, + sound: false, + highlight: false + ) + } + + private func removePushRule(rule: MXPushRule, completion: @escaping Completion) { + handleUpdateCallback(completion) { [weak self] in + guard let self = self else { return true } + return self.room.mxSession.notificationCenter.rule(byId: rule.ruleId) == nil + } + handleFailureCallback(completion) + + room.mxSession.notificationCenter.removeRule(rule) + } + + private func enablePushRule(rule: MXPushRule, completion: @escaping Completion) { + handleUpdateCallback(completion) { + // No way to check whether this notification concerns the push rule. Consider the change is applied. + return true + } + handleFailureCallback(completion) + + room.mxSession.notificationCenter.enableRule(rule, isEnabled: true) + } + + private func handleUpdateCallback(_ completion: @escaping Completion, releaseCheck: @escaping () -> Bool) { + notificationCenterDidUpdateObserver = NotificationCenter.default.addObserver( + forName: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules), + object: nil, + queue: OperationQueue.main) { [weak self] _ in + guard let self = self else { return } + if releaseCheck() { + self.removeObservers() + completion() + } + } + } + + private func handleFailureCallback(_ completion: @escaping Completion) { + notificationCenterDidFailObserver = NotificationCenter.default.addObserver( + forName: NSNotification.Name(rawValue: kMXNotificationCenterDidFailRulesUpdate), + object: nil, + queue: OperationQueue.main) { [weak self] _ in + guard let self = self else { return } + self.removeObservers() + completion() + } + } + + func removeObservers() { + if let observer = self.notificationCenterDidUpdateObserver { + NotificationCenter.default.removeObserver(observer) + self.notificationCenterDidUpdateObserver = nil + } + + if let observer = self.notificationCenterDidFailObserver { + NotificationCenter.default.removeObserver(observer) + self.notificationCenterDidFailObserver = nil + } + } +} + +// We could move these to their own file and make available in global namespace or move to sdk but they are only used here at the moment +fileprivate extension MXRoom { + + typealias Completion = () -> Void + func getRoomRule(from rules: [Any]) -> MXPushRule? { + guard let pushRules = rules as? [MXPushRule] else { + return nil + } + + return pushRules.first(where: { self.roomId == $0.ruleId }) + } + + var overridePushRule: MXPushRule? { + getRoomRule(from: mxSession.notificationCenter.rules.global.override) + } + + var roomPushRule: MXPushRule? { + getRoomRule(from: mxSession.notificationCenter.rules.global.room) + } + + var notificationState: RoomNotificationState { + + if isMuted { + return .mute + } + if isMentionsOnly { + return .mentionsAndKeywordsOnly + } + return .all + } + + var isMuted: Bool { + // Check whether an override rule has been defined with the roomm id as rule id. + // This kind of rule is created to mute the room + guard let rule = self.overridePushRule, + rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify), + rule.conditionIsEnabled(kind: .eventMatch, for: roomId) else { + return false + } + return rule.enabled + } + + var isMentionsOnly: Bool { + // Check push rules at room level + guard let rule = roomPushRule else { return false } + return rule.enabled && rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify) + } + +} + +fileprivate extension MXPushRule { + func actionsContains(actionType: MXPushRuleActionType) -> Bool { + guard let actions = actions as? [MXPushRuleAction] else { + return false + } + return actions.contains(where: { $0.actionType == actionType }) + } + + func conditionIsEnabled(kind: MXPushRuleConditionType, for roomId: String) -> Bool { + guard let conditions = conditions as? [MXPushRuleCondition] else { + return false + } + let ruleContainsCondition = conditions.contains { condition in + guard case kind = MXPushRuleConditionType(identifier: condition.kind), + let key = condition.parameters["key"] as? String, + let pattern = condition.parameters["pattern"] as? String + else { return false } + return key == "room_id" && pattern == roomId + } + return ruleContainsCondition && enabled + } +} diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift index f1efb7577..495d3468b 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift @@ -137,6 +137,12 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { return coordinator } + private func createRoomNotificationSettingsCoordinator() -> RoomNotificationSettingsCoordinator { + let coordinator = RoomNotificationSettingsCoordinator(room: room, showAvatar: false) + coordinator.delegate = self + return coordinator + } + private func showRoomDetails(with target: RoomInfoListTarget, animated: Bool) { switch target { case .integrations: @@ -152,8 +158,16 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { self.navigationRouter.push(search, animated: animated, popCompletion: nil) } }) + case .notifications: + let coordinator = createRoomNotificationSettingsCoordinator() + coordinator.start() + self.add(childCoordinator: coordinator) + self.navigationRouter.push(coordinator, animated: true, popCompletion: nil) default: - segmentedViewController.selectedIndex = target.tabIndex + guard let tabIndex = target.tabIndex else { + fatalError("No settings tab index for this target.") + } + segmentedViewController.selectedIndex = tabIndex if case .settings(let roomSettingsField) = target { roomSettingsViewController?.selectedRoomSettingsField = roomSettingsField @@ -184,3 +198,14 @@ extension RoomInfoCoordinator: RoomParticipantsViewControllerDelegate { } } + +extension RoomInfoCoordinator: RoomNotificationSettingsCoordinatorDelegate { + func roomNotificationSettingsCoordinatorDidComplete(_ coordinator: RoomNotificationSettingsCoordinatorType) { + self.navigationRouter.popModule(animated: true) + } + + func roomNotificationSettingsCoordinatorDidCancel(_ coordinator: RoomNotificationSettingsCoordinatorType) { + + } + +} diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewAction.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewAction.swift index 945d2844a..c6e0c8887 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewAction.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewAction.swift @@ -24,25 +24,21 @@ enum RoomInfoListTarget: Equatable { case uploads case integrations case search - - var tabIndex: UInt { - let tabIndex: UInt - + case notifications + + var tabIndex: UInt? { switch self { case .members: - tabIndex = 0 + return 0 case .uploads: - tabIndex = 1 + return 1 case .settings: - tabIndex = 2 - case .integrations: - tabIndex = 3 - case .search: - tabIndex = 4 + return 2 + default: + return nil } - - return tabIndex } + } /// RoomInfoListViewController view actions exposed to view model diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift index bfc3396bf..ddc14d605 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift @@ -150,6 +150,9 @@ final class RoomInfoListViewController: UIViewController { let rowSettings = Row(type: .default, icon: Asset.Images.settingsIcon.image, text: VectorL10n.roomDetailsSettings, accessoryType: .disclosureIndicator) { self.viewModel.process(viewAction: .navigate(target: .settings())) } + let roomNotifications = Row(type: .default, icon: Asset.Images.notifications.image, text: VectorL10n.roomDetailsNotifs, accessoryType: .disclosureIndicator) { + self.viewModel.process(viewAction: .navigate(target: .notifications)) + } let text = viewData.numberOfMembers == 1 ? VectorL10n.roomInfoListOneMember : VectorL10n.roomInfoListSeveralMembers(String(viewData.numberOfMembers)) let rowMembers = Row(type: .default, icon: Asset.Images.userIcon.image, text: text, accessoryType: .disclosureIndicator) { self.viewModel.process(viewAction: .navigate(target: .members)) @@ -165,8 +168,11 @@ final class RoomInfoListViewController: UIViewController { } var rows = [rowSettings] - if (RiotSettings.shared.roomInfoScreenShowIntegrations) - { + + if BuildSettings.roomSettingsScreenShowNotificationsV2 { + rows.append(roomNotifications) + } + if RiotSettings.shared.roomInfoScreenShowIntegrations { rows.append(rowIntegrations) } rows.append(rowMembers) diff --git a/Riot/Modules/Room/Settings/RoomSettingsViewController.m b/Riot/Modules/Room/Settings/RoomSettingsViewController.m index 634468fe8..08d18f655 100644 --- a/Riot/Modules/Room/Settings/RoomSettingsViewController.m +++ b/Riot/Modules/Room/Settings/RoomSettingsViewController.m @@ -528,7 +528,10 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti { [sectionMain addRowWithTag:ROOM_SETTINGS_MAIN_SECTION_ROW_DIRECT_CHAT]; } - [sectionMain addRowWithTag:ROOM_SETTINGS_MAIN_SECTION_ROW_MUTE_NOTIFICATIONS]; + if (!BuildSettings.roomSettingsScreenShowNotificationsV2) + { + [sectionMain addRowWithTag:ROOM_SETTINGS_MAIN_SECTION_ROW_MUTE_NOTIFICATIONS]; + } [sectionMain addRowWithTag:ROOM_SETTINGS_MAIN_SECTION_ROW_LEAVE]; [tmpSections addObject:sectionMain]; diff --git a/RiotTests/RoomNotificationSettingsViewModelTests.swift b/RiotTests/RoomNotificationSettingsViewModelTests.swift new file mode 100644 index 000000000..b10dbd63b --- /dev/null +++ b/RiotTests/RoomNotificationSettingsViewModelTests.swift @@ -0,0 +1,146 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import Riot + + +class MockRoomNotificationSettingsService: RoomNotificationSettingsServiceType { + + var listener: RoomNotificationStateCallback? + var notificationState: RoomNotificationState + + init(initialState: RoomNotificationState) { + notificationState = initialState + } + + func observeNotificationState(listener: @escaping RoomNotificationStateCallback) { + self.listener = listener + } + + func update(state: RoomNotificationState, completion: @escaping UpdateRoomNotificationStateCompletion) { + self.notificationState = state + completion() + listener?(state) + } +} + +class MockRoomNotificationSettingsView: RoomNotificationSettingsViewModelViewDelegate { + + var viewState: RoomNotificationSettingsViewStateType? + + func roomNotificationSettingsViewModel(_ viewModel: RoomNotificationSettingsViewModelType, didUpdateViewState viewState: RoomNotificationSettingsViewStateType) { + self.viewState = viewState + } +} + +class MockRoomNotificationSettingsCoordinator: RoomNotificationSettingsViewModelCoordinatorDelegate { + + var didComplete = false + var didCancel = false + func roomNotificationSettingsViewModelDidComplete(_ viewModel: RoomNotificationSettingsViewModelType) { + didComplete = true + } + + func roomNotificationSettingsViewModelDidCancel(_ viewModel: RoomNotificationSettingsViewModelType) { + didCancel = true + } +} + +class RoomNotificationSettingsViewModelTests: XCTestCase { + + enum Constants{ + static let roomDisplayName: String = "Test Room Name" + static let roomId: String = "1" + static let avatarUrl: String = "http://test.url.com" + static let avatarData = RoomAvatarViewData(roomId: "1", displayName: roomDisplayName, avatarUrl: avatarUrl, mediaManager: MXMediaManager()) + } + + var coordinator: MockRoomNotificationSettingsCoordinator! + var service: MockRoomNotificationSettingsService! + var view: MockRoomNotificationSettingsView! + var viewModel: RoomNotificationSettingsViewModel! + + override func setUpWithError() throws { + service = MockRoomNotificationSettingsService(initialState: .all) + view = MockRoomNotificationSettingsView() + coordinator = MockRoomNotificationSettingsCoordinator() + } + + func setupViewModel(roomEncrypted: Bool, showAvatar: Bool) { + let avatarData = showAvatar ? Constants.avatarData : nil + let viewModel = RoomNotificationSettingsViewModel(roomNotificationService: service, roomEncrypted: roomEncrypted, avatarViewData: avatarData) + viewModel.viewDelegate = view + viewModel.coordinatorDelegate = coordinator + self.viewModel = viewModel + } + + func testUnloaded() throws { + setupViewModel(roomEncrypted: true, showAvatar: false) + XCTAssertNil(view.viewState) + } + + func testUnencryptedOptions() throws { + setupViewModel(roomEncrypted: false, showAvatar: false) + viewModel.process(viewAction: .load) + XCTAssertNotNil(view.viewState) + XCTAssertTrue(view.viewState!.notificationOptions.count == 3) + } + + func testEncryptedOptions() throws { + setupViewModel(roomEncrypted: true, showAvatar: false) + viewModel.process(viewAction: .load) + XCTAssertNotNil(view.viewState) + XCTAssertTrue(view.viewState!.notificationOptions.count == 2) + } + + func testAvatar() throws { + setupViewModel(roomEncrypted: true, showAvatar: true) + viewModel.process(viewAction: .load) + XCTAssertNotNil(view.viewState?.avatarData) + XCTAssertEqual(view.viewState!.avatarData!.avatarUrl, Constants.avatarUrl) + } + + func testSelectionUpdateAndSave() throws { + setupViewModel(roomEncrypted: false, showAvatar: false) + viewModel.process(viewAction: .load) + XCTAssertNotNil(view.viewState) + XCTAssertTrue(view.viewState!.notificationState == .all) + viewModel.process(viewAction: .selectNotificationState(.mentionsAndKeywordsOnly)) + XCTAssertTrue(view.viewState!.notificationState == .mentionsAndKeywordsOnly) + viewModel.process(viewAction: .save) + XCTAssertTrue(service.notificationState == .mentionsAndKeywordsOnly) + XCTAssertTrue(coordinator.didComplete) + } + + func testCancel() throws { + setupViewModel(roomEncrypted: false, showAvatar: false) + viewModel.process(viewAction: .load) + XCTAssertNotNil(view.viewState) + viewModel.process(viewAction: .cancel) + XCTAssertTrue(coordinator.didCancel) + } + + func testMentionsOnlyNotAvaileOnEncryptedRoom() throws { + service = MockRoomNotificationSettingsService(initialState: .mentionsAndKeywordsOnly) + setupViewModel(roomEncrypted: true, showAvatar: false) + + viewModel.process(viewAction: .load) + XCTAssertNotNil(view.viewState) + XCTAssertTrue(view.viewState!.notificationState == .mute) + } + +} diff --git a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift index a211ab040..2f51aa2c2 100644 --- a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift +++ b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift @@ -1,5 +1,5 @@ /* - Copyright 2020 New Vector Ltd + 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. diff --git a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorBridgePresenter.swift b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorBridgePresenter.swift index bb7f824e4..25024f94d 100644 --- a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorBridgePresenter.swift +++ b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorBridgePresenter.swift @@ -1,5 +1,5 @@ /* - Copyright 2020 New Vector Ltd + 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. diff --git a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorType.swift b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorType.swift index 4fa5cc5dc..057328e71 100644 --- a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorType.swift +++ b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorType.swift @@ -1,5 +1,5 @@ /* - Copyright 2020 New Vector Ltd + 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. diff --git a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinator.swift b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinator.swift index 78ebc30f8..276fff2a8 100644 --- a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinator.swift +++ b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinator.swift @@ -1,5 +1,5 @@ /* - Copyright 2020 New Vector Ltd + 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. diff --git a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinatorType.swift b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinatorType.swift index 745997b9c..8b390ac60 100644 --- a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinatorType.swift +++ b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinatorType.swift @@ -1,5 +1,5 @@ /* - Copyright 2020 New Vector Ltd + 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. diff --git a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewAction.swift b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewAction.swift index 0bfc0f7f2..fc25efd98 100644 --- a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewAction.swift +++ b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewAction.swift @@ -1,5 +1,5 @@ /* - Copyright 2020 New Vector Ltd + 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. diff --git a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewController.swift b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewController.swift index f3a30d180..78175a877 100644 --- a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewController.swift +++ b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewController.swift @@ -1,5 +1,5 @@ /* - Copyright 2020 New Vector Ltd + 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. diff --git a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModel.swift b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModel.swift index 05c07ba58..4c386a8f7 100644 --- a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModel.swift +++ b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModel.swift @@ -1,5 +1,5 @@ /* - Copyright 2020 New Vector Ltd + 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. diff --git a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModelType.swift b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModelType.swift index fd03254da..19e3af58c 100644 --- a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModelType.swift +++ b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModelType.swift @@ -1,5 +1,5 @@ /* - Copyright 2020 New Vector Ltd + 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. diff --git a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewState.swift b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewState.swift index cc347baaa..4cebae662 100644 --- a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewState.swift +++ b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewState.swift @@ -1,5 +1,5 @@ /* - Copyright 2020 New Vector Ltd + 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. diff --git a/Tools/Templates/buildable/SimpleScreenTemplate/SimpleScreenTemplateViewController.swift b/Tools/Templates/buildable/SimpleScreenTemplate/SimpleScreenTemplateViewController.swift index 2955714c9..b4c86a60b 100644 --- a/Tools/Templates/buildable/SimpleScreenTemplate/SimpleScreenTemplateViewController.swift +++ b/Tools/Templates/buildable/SimpleScreenTemplate/SimpleScreenTemplateViewController.swift @@ -1,5 +1,5 @@ /* - Copyright 2020 New Vector Ltd + 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.