From b1ea23c92548563da67e36e4cbd6f6941c712304 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Tue, 22 Aug 2023 14:33:39 +0200 Subject: [PATCH] Display the list of rooms with user-defined notification settings (#1535) --- ElementX.xcodeproj/project.pbxproj | 16 +++ .../en.lproj/Localizable.strings | 6 +- .../en.lproj/Localizable.stringsdict | 16 --- .../en.lproj/Untranslated.strings | 1 + .../RoomFlowCoordinator.swift | 1 + ElementX/Sources/Generated/Strings.swift | 21 ++-- .../Mocks/Generated/GeneratedMocks.swift | 46 +++++++ .../Mocks/NotificationSettingsProxyMock.swift | 2 + .../Other/AccessibilityIdentifiers.swift | 8 ++ ElementX/Sources/Other/AvatarSize.swift | 3 + .../RoomDetailsScreenCoordinator.swift | 3 +- ...otificationSettingsScreenCoordinator.swift | 14 ++- ...RoomNotificationSettingsScreenModels.swift | 15 ++- ...mNotificationSettingsScreenViewModel.swift | 23 +++- ...otificationSettingsCustomSectionView.swift | 57 +++++++++ .../View/RoomNotificationSettingsScreen.swift | 35 ++---- ...otificationSettingsUserDefinedScreen.swift | 65 ++++++++++ ...icationSettingsEditScreenCoordinator.swift | 29 ++++- ...NotificationSettingsEditScreenModels.swift | 57 ++++++++- ...ificationSettingsEditScreenViewModel.swift | 112 +++++++++++++++--- .../View/NotificationSettingsEditScreen.swift | 63 ++++++---- ...tificationSettingsEditScreenRoomCell.swift | 90 ++++++++++++++ ...otificationSettingsScreenCoordinator.swift | 14 ++- .../NotificationSettingsScreenModels.swift | 2 +- .../NotificationSettingsScreenViewModel.swift | 8 +- .../View/NotificationSettingsScreen.swift | 5 +- .../SettingsScreenCoordinator.swift | 1 + .../NotificationSettingsChatType.swift | 23 ++++ .../NotificationSettingsProxy.swift | 9 ++ .../NotificationSettingsProxyProtocol.swift | 2 + .../UITests/UITestsAppCoordinator.swift | 11 +- ...tionSettingsEditScreenViewModelTests.swift | 35 +++++- ...ficationSettingsScreenViewModelTests.swift | 7 +- ...ficationSettingsScreenViewModelTests.swift | 89 ++++++++++++-- 34 files changed, 760 insertions(+), 129 deletions(-) create mode 100644 ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsCustomSectionView.swift create mode 100644 ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsUserDefinedScreen.swift create mode 100644 ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreenRoomCell.swift create mode 100644 ElementX/Sources/Services/NotificationSettings/NotificationSettingsChatType.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index bb39011ff..2cb435857 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C070FD43DC6BF4E50217965A /* LocalizationTests.swift */; }; + 0180C44B997EDA8D21F883AC /* RoomNotificationSettingsCustomSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B746EFA112532A7B701FB914 /* RoomNotificationSettingsCustomSectionView.swift */; }; 020C530986D7B97631877FEF /* TimelineItemMacContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A4AD793D50748F8997E5B15 /* TimelineItemMacContextMenu.swift */; }; 020F7E70167FB2833266F2F0 /* AnalyticsSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39D7F513A36C9C1951DB44C /* AnalyticsSettingsScreen.swift */; }; 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 218AB05B4E3889731959C5F1 /* EventBasedTimelineItemProtocol.swift */; }; @@ -48,6 +49,7 @@ 0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */; }; 0F9E38A75337D0146652ACAB /* BackgroundTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DFCAA239095A116976E32C4 /* BackgroundTaskTests.swift */; }; 1146E9EDCF8344F7D6E0D553 /* MockCoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0376C429FAB1687C3D905F3E /* MockCoder.swift */; }; + 119AE9A3FC6E0606C1146528 /* NotificationSettingsEditScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97F8963B14EB0AF3940DDBF /* NotificationSettingsEditScreenRoomCell.swift */; }; 126EE01D8BEAEF26105D83C5 /* RoomDetailsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A5FEF17ED7E6176D922D4F /* RoomDetailsScreen.swift */; }; 12C867E85E6D12EEDFD0B127 /* CustomStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C4762F8D6112E43117DB2F /* CustomStringConvertible.swift */; }; 12CCA59536EDD99A3272CF77 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC3F82523D6F48B926D6AF68 /* AppSettings.swift */; }; @@ -201,6 +203,7 @@ 46BA7F4B4D3A7164DED44B88 /* FullscreenDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565F1B2B300597C616B37888 /* FullscreenDialog.swift */; }; 47305C0911C9E1AA774A4000 /* TemplateScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */; }; 4799A852132F1744E2825994 /* CreateRoomViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340179A0FC1AD4AEDA7FC134 /* CreateRoomViewModelProtocol.swift */; }; + 491D62ACD19E6F134B1766AF /* RoomNotificationSettingsUserDefinedScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3203C6566DC17B7AECC1B7FD /* RoomNotificationSettingsUserDefinedScreen.swift */; }; 492274DA6691EE985C2FCCAA /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 67E7A6F388D3BF85767609D9 /* Sentry */; }; 496CC9D59ACFAB84FD9B3B5F /* AnalyticsPromptScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840E86A67DB2C92C09771EAD /* AnalyticsPromptScreenModels.swift */; }; 49814A48470F347426513B07 /* TimelineReadReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1877038D1AD3D5A029F8AE2C /* TimelineReadReceiptsView.swift */; }; @@ -577,6 +580,7 @@ BDED6DA7AD1E76018C424143 /* LegalInformationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C34667458773B02AB5FB0B2 /* LegalInformationScreenViewModel.swift */; }; BEA646DF302711A753F0D420 /* MapTilerStyleBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225EFCA26877E75CDFE7F48D /* MapTilerStyleBuilderProtocol.swift */; }; BF675964C9159F718589C36A /* AnalyticsSettingsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16037EE9E9A52AF37B7818E3 /* AnalyticsSettingsScreenUITests.swift */; }; + C0090506A52A1991BAF4BA68 /* NotificationSettingsChatType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07579F9C29001E40715F3014 /* NotificationSettingsChatType.swift */; }; C051475DFF4C8EBDDF4DC8E4 /* StartChatScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B99E13633862847D8B7E2815 /* StartChatScreenModels.swift */; }; C08AAE7563E0722C9383F51C /* RoomMembersListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */; }; C11939FDC40716C4387275A4 /* NotificationSettingsEditScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8544F7058D31DBEB8DBFF0F5 /* NotificationSettingsEditScreenViewModelTests.swift */; }; @@ -845,6 +849,7 @@ 0685156EB62D7E243F097CFC /* ServerSelectionScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenViewModelProtocol.swift; sourceTree = ""; }; 06B098A612DCB5A7358EECD5 /* DeveloperOptionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenModels.swift; sourceTree = ""; }; 06FAE373A7F20780BA84B59C /* MessageForwardingScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenCoordinator.swift; sourceTree = ""; }; + 07579F9C29001E40715F3014 /* NotificationSettingsChatType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsChatType.swift; sourceTree = ""; }; 077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorStateMachine.swift; sourceTree = ""; }; 07E65E613F057697A1A0BC03 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; }; 086B997409328F091EBA43CE /* RoomScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenUITests.swift; sourceTree = ""; }; @@ -966,6 +971,7 @@ 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceipt.swift; sourceTree = ""; }; 31B35311C7FED04B0E1B80C2 /* RoomMemberDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetails.swift; sourceTree = ""; }; 31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModel.swift; sourceTree = ""; }; + 3203C6566DC17B7AECC1B7FD /* RoomNotificationSettingsUserDefinedScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsUserDefinedScreen.swift; sourceTree = ""; }; 32B5E17028C02DFA7DDA3931 /* RoomMemberProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyProtocol.swift; sourceTree = ""; }; 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutViewModelTests.swift; sourceTree = ""; }; 33649299575BADC34924ABC6 /* InvitesScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenCoordinator.swift; sourceTree = ""; }; @@ -1330,6 +1336,7 @@ B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineView.swift; sourceTree = ""; }; B697816AF93DA06EC58C5D70 /* WaitlistScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenViewModelProtocol.swift; sourceTree = ""; }; B6E89E530A8E92EC44301CA1 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; + B746EFA112532A7B701FB914 /* RoomNotificationSettingsCustomSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsCustomSectionView.swift; sourceTree = ""; }; B7AE92E7BFF71797BDE1D261 /* MapTilerStyleBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStyleBuilder.swift; sourceTree = ""; }; B7F0192CE2F891141A25B49F /* UITestsSignalling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsSignalling.swift; sourceTree = ""; }; B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenCoordinator.swift; sourceTree = ""; }; @@ -1391,6 +1398,7 @@ C843CF833BF6485B64AC87E1 /* AppRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouter.swift; sourceTree = ""; }; C8F2A7A4E3F5060F52ACFFB0 /* RedactedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineView.swift; sourceTree = ""; }; C936FDD017808FE416742D64 /* PollRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollRoomTimelineView.swift; sourceTree = ""; }; + C97F8963B14EB0AF3940DDBF /* NotificationSettingsEditScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenRoomCell.swift; sourceTree = ""; }; C99FDEEB71173C4C6FA2734C /* UserSessionFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinator.swift; sourceTree = ""; }; CA28F29C9F93E93CC3C2C715 /* NavigationRootCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRootCoordinator.swift; sourceTree = ""; }; CA29952595B804DA221A0C1D /* ComposerToolbarViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModelTests.swift; sourceTree = ""; }; @@ -1758,6 +1766,7 @@ 114DC16B28140F885FD833E2 /* NotificationSettings */ = { isa = PBXGroup; children = ( + 07579F9C29001E40715F3014 /* NotificationSettingsChatType.swift */, E6F5D66F158A6662F953733E /* NotificationSettingsProxy.swift */, 4FDD775CFD72DD2D3C8A8390 /* NotificationSettingsProxyProtocol.swift */, C1511766C534367700C8DD75 /* RoomNotificationModeProxy.swift */, @@ -2753,6 +2762,7 @@ isa = PBXGroup; children = ( FBB0328F2887BF0A65BC5D49 /* NotificationSettingsEditScreen.swift */, + C97F8963B14EB0AF3940DDBF /* NotificationSettingsEditScreenRoomCell.swift */, ); path = View; sourceTree = ""; @@ -3511,7 +3521,9 @@ DDF77194AB6E167891D0A8F3 /* View */ = { isa = PBXGroup; children = ( + B746EFA112532A7B701FB914 /* RoomNotificationSettingsCustomSectionView.swift */, 8896CDD20CA2D87EA3B848A1 /* RoomNotificationSettingsScreen.swift */, + 3203C6566DC17B7AECC1B7FD /* RoomNotificationSettingsUserDefinedScreen.swift */, ); path = View; sourceTree = ""; @@ -4567,9 +4579,11 @@ 652ACCF104A8CEF30788963C /* NotificationManager.swift in Sources */, 06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */, C4C84901ABAC9B17564AB7EB /* NotificationName.swift in Sources */, + C0090506A52A1991BAF4BA68 /* NotificationSettingsChatType.swift in Sources */, AA93B3F9B5DD097DEF79F981 /* NotificationSettingsEditScreen.swift in Sources */, 53A59720F4729D9BBFFB7CAB /* NotificationSettingsEditScreenCoordinator.swift in Sources */, 4EA1CE0E88EA68E862FF0EA2 /* NotificationSettingsEditScreenModels.swift in Sources */, + 119AE9A3FC6E0606C1146528 /* NotificationSettingsEditScreenRoomCell.swift in Sources */, D5FE90A6AF5FD5AE91BD37C7 /* NotificationSettingsEditScreenViewModel.swift in Sources */, 2F623DA1122140A987B34D08 /* NotificationSettingsEditScreenViewModelProtocol.swift in Sources */, B93FA0DA1504B301CAEE141B /* NotificationSettingsProxy.swift in Sources */, @@ -4647,6 +4661,7 @@ F3E2D3F7ACDED65A4E5CD8DE /* RoomMembersListScreenViewModel.swift in Sources */, C4078364FD9FA00EA9D00A15 /* RoomMembersListScreenViewModelProtocol.swift in Sources */, C9F5B48D15B9BCAE1F8D564E /* RoomNotificationModeProxy.swift in Sources */, + 0180C44B997EDA8D21F883AC /* RoomNotificationSettingsCustomSectionView.swift in Sources */, CE6F237360875D3D573FD0B2 /* RoomNotificationSettingsProxy.swift in Sources */, 8544657DEEE717ED2E22E382 /* RoomNotificationSettingsProxyMock.swift in Sources */, EF5009AC03212227131C8AF2 /* RoomNotificationSettingsProxyProtocol.swift in Sources */, @@ -4655,6 +4670,7 @@ 6E63704717F17593A475D152 /* RoomNotificationSettingsScreenModels.swift in Sources */, E9347F56CF0683208F4D9249 /* RoomNotificationSettingsScreenViewModel.swift in Sources */, BA4C9049BC96DED3A2F3B82E /* RoomNotificationSettingsScreenViewModelProtocol.swift in Sources */, + 491D62ACD19E6F134B1766AF /* RoomNotificationSettingsUserDefinedScreen.swift in Sources */, 4FC1EFE4968A259CBBACFAFB /* RoomProxy.swift in Sources */, BD203FC6A7AE7637EA003643 /* RoomProxyMock.swift in Sources */, FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 55227408d..af12d729b 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -1,5 +1,4 @@ "Notification" = "Notification"; -"Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings._web" = "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings."; "a11y_hide_password" = "Hide password"; "a11y_send_files" = "Send files"; "a11y_show_password" = "Show password"; @@ -266,7 +265,10 @@ "screen_notification_settings_additional_settings_section_title" = "Additional settings"; "screen_notification_settings_calls_label" = "Audio and video calls"; "screen_notification_settings_direct_chats" = "Direct chats"; +"screen_notification_settings_edit_custom_settings_section_title" = "Custom setting per chat"; "screen_notification_settings_edit_failed_updating_default_mode" = "An error occurred while updating the notification setting."; +"screen_notification_settings_edit_mode_all_messages" = "All messages"; +"screen_notification_settings_edit_mode_mentions_and_keywords" = "Mentions and Keywords only"; "screen_notification_settings_edit_screen_direct_section_header" = "On direct chats, notify me for"; "screen_notification_settings_edit_screen_group_section_header" = "On group chats, notify me for"; "screen_notification_settings_enable_notifications" = "Enable notifications on this device"; @@ -333,11 +335,13 @@ "screen_room_notification_settings_default_setting_footnote" = "You can change it in your %1$@."; "screen_room_notification_settings_default_setting_footnote_content_link" = "global settings"; "screen_room_notification_settings_default_setting_title" = "Default setting"; +"screen_room_notification_settings_edit_remove_setting" = "Remove custom setting"; "screen_room_notification_settings_error_loading_settings" = "An error occurred while loading notification settings."; "screen_room_notification_settings_error_restoring_default" = "Failed restoring the default mode, please try again."; "screen_room_notification_settings_error_setting_mode" = "Failed setting the mode, please try again."; "screen_room_notification_settings_mode_all_messages" = "All messages"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and Keywords only"; +"screen_room_notification_settings_room_custom_settings_title" = "In this room, notify me for"; "screen_room_reactions_show_less" = "Show less"; "screen_room_reactions_show_more" = "Show more"; "screen_room_retry_send_menu_send_again_action" = "Send again"; diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/en.lproj/Localizable.stringsdict index 0a4b84e08..0cc5e5aa7 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.stringsdict @@ -2,22 +2,6 @@ - %(count)s rooms_web - - NSStringLocalizedFormatKey - %#@COUNT@ - COUNT - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - d - one - %(count)s room - other - %(count)s rooms - - common_member_count NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings index 2b31da559..468acd206 100644 --- a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings +++ b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings @@ -14,3 +14,4 @@ "soft_logout_clear_data_submit" = "Clear all data"; "soft_logout_clear_data_dialog_title" = "Clear data"; "soft_logout_clear_data_dialog_content" = "Clear all data currently stored on this device?\nSign in again to access your account data and messages."; + diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 186a1593b..3d7e1e103 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -644,6 +644,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { private func presentNotificationSettingsScreen(animated: Bool) async { let navigationCoordinator = NavigationStackCoordinator() let parameters = await NotificationSettingsScreenCoordinatorParameters(navigationStackCoordinator: navigationCoordinator, + userSession: userSession, userNotificationCenter: UNUserNotificationCenter.current(), notificationSettings: userSession.clientProxy.notificationSettings(), isModallyPresented: true) diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index b2591f998..03fb39841 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -10,10 +10,6 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces public enum L10n { - /// Plural format key: "%#@COUNT@" - public static func countSRoomsWeb(_ p1: Int) -> String { - return L10n.tr("Localizable", "%(count)s rooms_web", p1) - } /// Hide password public static var a11yHidePassword: String { return L10n.tr("Localizable", "a11y_hide_password") } /// Send files @@ -692,8 +688,14 @@ public enum L10n { public static var screenNotificationSettingsCallsLabel: String { return L10n.tr("Localizable", "screen_notification_settings_calls_label") } /// Direct chats public static var screenNotificationSettingsDirectChats: String { return L10n.tr("Localizable", "screen_notification_settings_direct_chats") } + /// Custom setting per chat + public static var screenNotificationSettingsEditCustomSettingsSectionTitle: String { return L10n.tr("Localizable", "screen_notification_settings_edit_custom_settings_section_title") } /// An error occurred while updating the notification setting. public static var screenNotificationSettingsEditFailedUpdatingDefaultMode: String { return L10n.tr("Localizable", "screen_notification_settings_edit_failed_updating_default_mode") } + /// All messages + public static var screenNotificationSettingsEditModeAllMessages: String { return L10n.tr("Localizable", "screen_notification_settings_edit_mode_all_messages") } + /// Mentions and Keywords only + public static var screenNotificationSettingsEditModeMentionsAndKeywords: String { return L10n.tr("Localizable", "screen_notification_settings_edit_mode_mentions_and_keywords") } /// On direct chats, notify me for public static var screenNotificationSettingsEditScreenDirectSectionHeader: String { return L10n.tr("Localizable", "screen_notification_settings_edit_screen_direct_section_header") } /// On group chats, notify me for @@ -848,6 +850,8 @@ public enum L10n { public static var screenRoomNotificationSettingsDefaultSettingFootnoteContentLink: String { return L10n.tr("Localizable", "screen_room_notification_settings_default_setting_footnote_content_link") } /// Default setting public static var screenRoomNotificationSettingsDefaultSettingTitle: String { return L10n.tr("Localizable", "screen_room_notification_settings_default_setting_title") } + /// Remove custom setting + public static var screenRoomNotificationSettingsEditRemoveSetting: String { return L10n.tr("Localizable", "screen_room_notification_settings_edit_remove_setting") } /// An error occurred while loading notification settings. public static var screenRoomNotificationSettingsErrorLoadingSettings: String { return L10n.tr("Localizable", "screen_room_notification_settings_error_loading_settings") } /// Failed restoring the default mode, please try again. @@ -858,6 +862,8 @@ public enum L10n { public static var screenRoomNotificationSettingsModeAllMessages: String { return L10n.tr("Localizable", "screen_room_notification_settings_mode_all_messages") } /// Mentions and Keywords only public static var screenRoomNotificationSettingsModeMentionsAndKeywords: String { return L10n.tr("Localizable", "screen_room_notification_settings_mode_mentions_and_keywords") } + /// In this room, notify me for + public static var screenRoomNotificationSettingsRoomCustomSettingsTitle: String { return L10n.tr("Localizable", "screen_room_notification_settings_room_custom_settings_title") } /// Show less public static var screenRoomReactionsShowLess: String { return L10n.tr("Localizable", "screen_room_reactions_show_less") } /// Show more @@ -1194,13 +1200,6 @@ public enum L10n { public static var testLanguageIdentifier: String { return L10n.tr("Localizable", "test_language_identifier") } /// en public static var testUntranslatedDefaultLanguageIdentifier: String { return L10n.tr("Localizable", "test_untranslated_default_language_identifier") } - - public enum UseAnIdentityServerToInviteByEmail { - public enum DefaultUseTheDefaultDefaultIdentityServerNameSDefaultOrManageInSettingsSettingsSettings { - /// Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings. - public static var web: String { return L10n.tr("Localizable", "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings._web") } - } - } } // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 0b852fa11..f281b18c9 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -365,6 +365,31 @@ class NotificationSettingsProxyMock: NotificationSettingsProxyProtocol { setNotificationModeRoomIdModeReceivedInvocations.append((roomId: roomId, mode: mode)) try await setNotificationModeRoomIdModeClosure?(roomId, mode) } + //MARK: - getUserDefinedRoomNotificationMode + + var getUserDefinedRoomNotificationModeRoomIdThrowableError: Error? + var getUserDefinedRoomNotificationModeRoomIdCallsCount = 0 + var getUserDefinedRoomNotificationModeRoomIdCalled: Bool { + return getUserDefinedRoomNotificationModeRoomIdCallsCount > 0 + } + var getUserDefinedRoomNotificationModeRoomIdReceivedRoomId: String? + var getUserDefinedRoomNotificationModeRoomIdReceivedInvocations: [String] = [] + var getUserDefinedRoomNotificationModeRoomIdReturnValue: RoomNotificationModeProxy? + var getUserDefinedRoomNotificationModeRoomIdClosure: ((String) async throws -> RoomNotificationModeProxy?)? + + func getUserDefinedRoomNotificationMode(roomId: String) async throws -> RoomNotificationModeProxy? { + if let error = getUserDefinedRoomNotificationModeRoomIdThrowableError { + throw error + } + getUserDefinedRoomNotificationModeRoomIdCallsCount += 1 + getUserDefinedRoomNotificationModeRoomIdReceivedRoomId = roomId + getUserDefinedRoomNotificationModeRoomIdReceivedInvocations.append(roomId) + if let getUserDefinedRoomNotificationModeRoomIdClosure = getUserDefinedRoomNotificationModeRoomIdClosure { + return try await getUserDefinedRoomNotificationModeRoomIdClosure(roomId) + } else { + return getUserDefinedRoomNotificationModeRoomIdReturnValue + } + } //MARK: - getDefaultRoomNotificationMode var getDefaultRoomNotificationModeIsEncryptedIsOneToOneCallsCount = 0 @@ -586,6 +611,27 @@ class NotificationSettingsProxyMock: NotificationSettingsProxyProtocol { setCallEnabledEnabledReceivedInvocations.append(enabled) try await setCallEnabledEnabledClosure?(enabled) } + //MARK: - getRoomsWithUserDefinedRules + + var getRoomsWithUserDefinedRulesThrowableError: Error? + var getRoomsWithUserDefinedRulesCallsCount = 0 + var getRoomsWithUserDefinedRulesCalled: Bool { + return getRoomsWithUserDefinedRulesCallsCount > 0 + } + var getRoomsWithUserDefinedRulesReturnValue: [String]! + var getRoomsWithUserDefinedRulesClosure: (() async throws -> [String])? + + func getRoomsWithUserDefinedRules() async throws -> [String] { + if let error = getRoomsWithUserDefinedRulesThrowableError { + throw error + } + getRoomsWithUserDefinedRulesCallsCount += 1 + if let getRoomsWithUserDefinedRulesClosure = getRoomsWithUserDefinedRulesClosure { + return try await getRoomsWithUserDefinedRulesClosure() + } else { + return getRoomsWithUserDefinedRulesReturnValue + } + } } class RoomMemberProxyMock: RoomMemberProxyProtocol { var userID: String { diff --git a/ElementX/Sources/Mocks/NotificationSettingsProxyMock.swift b/ElementX/Sources/Mocks/NotificationSettingsProxyMock.swift index 1ff727dc4..c5abaa609 100644 --- a/ElementX/Sources/Mocks/NotificationSettingsProxyMock.swift +++ b/ElementX/Sources/Mocks/NotificationSettingsProxyMock.swift @@ -36,6 +36,8 @@ extension NotificationSettingsProxyMock { callbacks = configuration.callback getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = configuration.roomMode getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = configuration.defaultRoomMode + getUserDefinedRoomNotificationModeRoomIdReturnValue = configuration.roomMode.isDefault ? nil : configuration.roomMode.mode + getRoomsWithUserDefinedRulesReturnValue = [] setNotificationModeRoomIdModeClosure = { [weak self] _, mode in guard let self else { return } diff --git a/ElementX/Sources/Other/AccessibilityIdentifiers.swift b/ElementX/Sources/Other/AccessibilityIdentifiers.swift index 5d227188b..614b4e889 100644 --- a/ElementX/Sources/Other/AccessibilityIdentifiers.swift +++ b/ElementX/Sources/Other/AccessibilityIdentifiers.swift @@ -36,6 +36,7 @@ struct A11yIdentifiers { static let invitesScreen = InvitesScreen() static let welcomeScreen = WelcomeScreen() static let migrationScreen = MigrationScreen() + static let notificationSettingsEditScreen = NotificationSettingsEditScreen() struct AnalyticsPromptScreen { let title = "analytics_prompt-title" @@ -161,4 +162,11 @@ struct A11yIdentifiers { struct MigrationScreen { let message = "migration_screen-message" } + + struct NotificationSettingsEditScreen { + let roomNamePrefix = "notification_settings_edit_screen-room_name" + func roomName(_ name: String) -> String { + "\(roomNamePrefix):\(name)" + } + } } diff --git a/ElementX/Sources/Other/AvatarSize.swift b/ElementX/Sources/Other/AvatarSize.swift index e747b9a5c..faeaff276 100644 --- a/ElementX/Sources/Other/AvatarSize.swift +++ b/ElementX/Sources/Other/AvatarSize.swift @@ -78,9 +78,12 @@ enum RoomAvatarSizeOnScreen { case home case messageForwarding case details + case notificationSettings var value: CGFloat { switch self { + case .notificationSettings: + return 30 case .timeline: return 32 case .messageForwarding: diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift index bd1928e75..8312f5041 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift @@ -200,7 +200,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { private func presentNotificationSettings() { let roomNotificationSettingsParameters = RoomNotificationSettingsScreenCoordinatorParameters(navigationStackCoordinator: parameters.navigationStackCoordinator, notificationSettingsProxy: parameters.notificationSettings, - roomProxy: parameters.roomProxy) + roomProxy: parameters.roomProxy, + displayAsUserDefinedRoomSettings: false) let roomNotificationSettingsCoordinator = RoomNotificationSettingsScreenCoordinator(parameters: roomNotificationSettingsParameters) roomNotificationSettingsCoordinator.actions.sink { [weak self] actions in switch actions { diff --git a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenCoordinator.swift index bb8ebb0a6..d12878f23 100644 --- a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenCoordinator.swift @@ -21,6 +21,7 @@ struct RoomNotificationSettingsScreenCoordinatorParameters { weak var navigationStackCoordinator: NavigationStackCoordinator? let notificationSettingsProxy: NotificationSettingsProxyProtocol let roomProxy: RoomProxyProtocol + let displayAsUserDefinedRoomSettings: Bool } enum RoomNotificationSettingsScreenCoordinatorAction { @@ -45,7 +46,8 @@ final class RoomNotificationSettingsScreenCoordinator: CoordinatorProtocol { init(parameters: RoomNotificationSettingsScreenCoordinatorParameters) { self.parameters = parameters viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: parameters.notificationSettingsProxy, - roomProxy: parameters.roomProxy) + roomProxy: parameters.roomProxy, + displayAsUserDefinedRoomSettings: parameters.displayAsUserDefinedRoomSettings) } func start() { @@ -53,11 +55,17 @@ final class RoomNotificationSettingsScreenCoordinator: CoordinatorProtocol { switch action { case .openGlobalSettings: self?.actionsSubject.send(.presentNotificationSettingsScreen) + case .dismiss: + self?.parameters.navigationStackCoordinator?.pop(animated: true) } }.store(in: &cancellables) } - + func toPresentable() -> AnyView { - AnyView(RoomNotificationSettingsScreen(context: viewModel.context)) + if parameters.displayAsUserDefinedRoomSettings { + return AnyView(RoomNotificationSettingsUserDefinedScreen(context: viewModel.context)) + } else { + return AnyView(RoomNotificationSettingsScreen(context: viewModel.context)) + } } } diff --git a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenModels.swift b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenModels.swift index b8441a0af..cd7790635 100644 --- a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenModels.swift @@ -18,6 +18,7 @@ import Foundation enum RoomNotificationSettingsScreenViewModelAction { case openGlobalSettings + case dismiss } enum RoomNotificationSettingsState { @@ -56,6 +57,17 @@ struct RoomNotificationSettingsScreenViewState: BindableState { var availableCustomRoomNotificationModes: [RoomNotificationModeProxy] = [.allMessages, .mentionsAndKeywordsOnly, .mute] var isRestoringDefaultSetting = false var pendingCustomMode: RoomNotificationModeProxy? + var displayAsUserDefinedRoomSettings = false + var navigationTitle: String + var customSettingsSectionHeader: String + var deletingCustomSetting = false + + func isSelected(mode: RoomNotificationModeProxy) -> Bool { + if case .loaded(let settings) = notificationSettingsState, settings.mode == mode, pendingCustomMode == nil { + return true + } + return false + } } struct RoomNotificationSettingsScreenViewStateBindings { @@ -69,6 +81,7 @@ enum RoomNotificationSettingsScreenViewAction { case changedAllowCustomSettings case setCustomMode(RoomNotificationModeProxy) case customSettingFootnoteLinkTapped + case deleteCustomSettingTapped } struct RoomNotificationSettingsScreenStrings { @@ -87,7 +100,7 @@ struct RoomNotificationSettingsScreenStrings { self.customSettingFootnote = customSettingFootnote } - + func string(for mode: RoomNotificationModeProxy) -> String { switch mode { case .allMessages: diff --git a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenViewModel.swift b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenViewModel.swift index 32ac89647..9a8006dce 100644 --- a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenViewModel.swift @@ -30,11 +30,16 @@ class RoomNotificationSettingsScreenViewModel: RoomNotificationSettingsScreenVie actionsSubject.eraseToAnyPublisher() } - init(notificationSettingsProxy: NotificationSettingsProxyProtocol, roomProxy: RoomProxyProtocol) { + init(notificationSettingsProxy: NotificationSettingsProxyProtocol, roomProxy: RoomProxyProtocol, displayAsUserDefinedRoomSettings: Bool) { let bindings = RoomNotificationSettingsScreenViewStateBindings() self.notificationSettingsProxy = notificationSettingsProxy self.roomProxy = roomProxy - super.init(initialViewState: RoomNotificationSettingsScreenViewState(bindings: bindings)) + let navigationTitle = displayAsUserDefinedRoomSettings ? roomProxy.roomTitle : L10n.screenRoomDetailsNotificationTitle + let customSettingsSectionHeader = displayAsUserDefinedRoomSettings ? L10n.screenRoomNotificationSettingsRoomCustomSettingsTitle : L10n.screenRoomNotificationSettingsCustomSettingsTitle + super.init(initialViewState: RoomNotificationSettingsScreenViewState(bindings: bindings, + displayAsUserDefinedRoomSettings: displayAsUserDefinedRoomSettings, + navigationTitle: navigationTitle, + customSettingsSectionHeader: customSettingsSectionHeader)) setupNotificationSettingsSubscription() fetchNotificationSettings() @@ -50,6 +55,8 @@ class RoomNotificationSettingsScreenViewModel: RoomNotificationSettingsScreenVie setCustomMode(mode) case .customSettingFootnoteLinkTapped: actionsSubject.send(.openGlobalSettings) + case .deleteCustomSettingTapped: + Task { await deleteCustomSetting() } } } @@ -77,6 +84,7 @@ class RoomNotificationSettingsScreenViewModel: RoomNotificationSettingsScreenVie private func fetchRoomNotificationSettings() async { do { + // `isOneToOne` here is not the same as `isDirect` on the room. From the point of view of the push rule, a one-to-one room is a room with exactly two active members. let settings = try await notificationSettingsProxy.getNotificationSettings(roomId: roomProxy.id, isEncrypted: roomProxy.isEncrypted, isOneToOne: roomProxy.activeMembersCount == 2) @@ -153,4 +161,15 @@ class RoomNotificationSettingsScreenViewModel: RoomNotificationSettingsScreenVie message: L10n.screenRoomNotificationSettingsErrorRestoringDefault) } } + + private func deleteCustomSetting() async { + state.deletingCustomSetting = true + do { + try await notificationSettingsProxy.restoreDefaultNotificationMode(roomId: roomProxy.id) + actionsSubject.send(.dismiss) + } catch { + displayError(.restoreDefaultFailed) + } + state.deletingCustomSetting = false + } } diff --git a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsCustomSectionView.swift b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsCustomSectionView.swift new file mode 100644 index 000000000..d0d6f9bb2 --- /dev/null +++ b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsCustomSectionView.swift @@ -0,0 +1,57 @@ +// +// Copyright 2023 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 Compound +import SwiftUI + +struct RoomNotificationSettingsCustomSectionView: View { + @ObservedObject var context: RoomNotificationSettingsScreenViewModel.Context + + var body: some View { + Section { + ForEach(context.viewState.availableCustomRoomNotificationModes, id: \.self) { mode in + ListRow(label: .plain(title: context.viewState.strings.string(for: mode)), + details: (context.viewState.pendingCustomMode == mode) ? .isWaiting(true) : nil, + kind: .selection(isSelected: context.viewState.isSelected(mode: mode)) { + context.send(viewAction: .setCustomMode(mode)) + }) + .disabled(context.viewState.pendingCustomMode != nil) + } + } header: { + Text(context.viewState.customSettingsSectionHeader) + .compoundListSectionHeader() + } + } +} + +struct RoomNotificationSettingsCustomSectionView_Previews: PreviewProvider { + static let viewModel = { + let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init(defaultRoomMode: .allMessages, roomMode: .mentionsAndKeywordsOnly)) + + let roomProxy = RoomProxyMock(with: .init(displayName: "Room", isEncrypted: true, joinedMembersCount: 4)) + + return RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxy, + roomProxy: roomProxy, + displayAsUserDefinedRoomSettings: false) + }() + + static var previews: some View { + Form { + RoomNotificationSettingsCustomSectionView(context: viewModel.context) + } + .compoundForm() + } +} diff --git a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsScreen.swift b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsScreen.swift index 09527e51b..f70bbe296 100644 --- a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsScreen.swift +++ b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsScreen.swift @@ -31,14 +31,13 @@ struct RoomNotificationSettingsScreen: View { } } .compoundList() - .navigationTitle(L10n.screenRoomDetailsNotificationTitle) + .navigationTitle(context.viewState.navigationTitle) .alert(item: $context.alertInfo) .track(screen: .roomNotifications) } // MARK: - Private - @ViewBuilder private var allowCustomSettingSection: some View { Section { ListRow(label: .plain(title: L10n.screenRoomNotificationSettingsAllowCustom), @@ -54,7 +53,6 @@ struct RoomNotificationSettingsScreen: View { } } - @ViewBuilder private var defaultSettingSection: some View { Section { ListRow(label: .plain(title: context.viewState.isRestoringDefaultSetting ? L10n.commonLoading : context.viewState.strings.string(for: context.viewState.notificationSettingsState)), @@ -74,21 +72,8 @@ struct RoomNotificationSettingsScreen: View { } } - @ViewBuilder private var customSettingsSection: some View { - Section { - ListRow(label: .plain(title: L10n.screenRoomNotificationSettingsCustomSettingsTitle), - kind: .inlinePicker(selection: $context.customMode, - items: context.viewState.availableCustomRoomNotificationModes.map { - (title: context.viewState.strings.string(for: $0), tag: $0) - })) - .onChange(of: context.customMode) { mode in - context.send(viewAction: .setCustomMode(mode)) - } - } header: { - Text(L10n.screenRoomNotificationSettingsCustomSettingsTitle) - .compoundListSectionHeader() - } + RoomNotificationSettingsCustomSectionView(context: context) } } @@ -100,10 +85,9 @@ struct RoomNotificationSettingsScreen_Previews: PreviewProvider { let roomProxy = RoomProxyMock(with: .init(displayName: "Room", isEncrypted: true, joinedMembersCount: 4)) - let model = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxy, - roomProxy: roomProxy) - - return model + return RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxy, + roomProxy: roomProxy, + displayAsUserDefinedRoomSettings: false) }() static let viewModelCustom = { @@ -111,12 +95,11 @@ struct RoomNotificationSettingsScreen_Previews: PreviewProvider { let roomProxy = RoomProxyMock(with: .init(displayName: "Room", isEncrypted: true, joinedMembersCount: 4)) - let model = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxy, - roomProxy: roomProxy) - - return model + return RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxy, + roomProxy: roomProxy, + displayAsUserDefinedRoomSettings: false) }() - + static var previews: some View { RoomNotificationSettingsScreen(context: viewModel.context) .previewDisplayName("Default") diff --git a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsUserDefinedScreen.swift b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsUserDefinedScreen.swift new file mode 100644 index 000000000..b0e36dc62 --- /dev/null +++ b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsUserDefinedScreen.swift @@ -0,0 +1,65 @@ +// +// Copyright 2023 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 Compound +import SwiftUI + +struct RoomNotificationSettingsUserDefinedScreen: View { + @ObservedObject var context: RoomNotificationSettingsScreenViewModel.Context + + var body: some View { + Form { + RoomNotificationSettingsCustomSectionView(context: context) + + deleteButton + } + .compoundList() + .navigationTitle(context.viewState.navigationTitle) + .alert(item: $context.alertInfo) + .track(screen: .roomNotifications) + } + + // MARK: - Private + + private var deleteButton: some View { + ListRow(label: .action(title: L10n.screenRoomNotificationSettingsEditRemoveSetting, + systemIcon: .trash, + role: .destructive), + details: context.viewState.deletingCustomSetting ? .isWaiting(true) : nil, + kind: .button { + context.send(viewAction: .deleteCustomSettingTapped) + }) + .disabled(context.viewState.deletingCustomSetting) + } +} + +// MARK: - Previews + +struct RoomNotificationSettingsUserDefinedScreen_Previews: PreviewProvider { + static let viewModel = { + let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init(defaultRoomMode: .mentionsAndKeywordsOnly, roomMode: .mentionsAndKeywordsOnly)) + + let roomProxy = RoomProxyMock(with: .init(displayName: "Room", isEncrypted: true, joinedMembersCount: 4)) + + return RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxy, + roomProxy: roomProxy, + displayAsUserDefinedRoomSettings: true) + }() + + static var previews: some View { + RoomNotificationSettingsUserDefinedScreen(context: viewModel.context) + } +} diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenCoordinator.swift index 5985e1623..bc07c5682 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenCoordinator.swift @@ -18,7 +18,9 @@ import Combine import SwiftUI struct NotificationSettingsEditScreenCoordinatorParameters { - let isDirect: Bool + weak var navigationStackCoordinator: NavigationStackCoordinator? + let chatType: NotificationSettingsChatType + let userSession: UserSessionProtocol let notificationSettings: NotificationSettingsProxyProtocol } @@ -37,15 +39,38 @@ final class NotificationSettingsEditScreenCoordinator: CoordinatorProtocol { init(parameters: NotificationSettingsEditScreenCoordinatorParameters) { self.parameters = parameters - viewModel = NotificationSettingsEditScreenViewModel(isDirect: parameters.isDirect, + viewModel = NotificationSettingsEditScreenViewModel(chatType: parameters.chatType, + userSession: parameters.userSession, notificationSettingsProxy: parameters.notificationSettings) } func start() { viewModel.fetchInitialContent() + + viewModel.actions.sink { [weak self] action in + guard let self else { return } + switch action { + case .requestRoomNotificationSettingsPresentation(let roomID): + Task { await self.presentRoomNotificationSettings(roomID: roomID) } + } + } + .store(in: &cancellables) } func toPresentable() -> AnyView { AnyView(NotificationSettingsEditScreen(context: viewModel.context)) } + + // MARK: - Private + + private func presentRoomNotificationSettings(roomID: String) async { + guard let roomProxy = await parameters.userSession.clientProxy.roomForIdentifier(roomID) else { return } + + let roomNotificationSettingsParameters = RoomNotificationSettingsScreenCoordinatorParameters(navigationStackCoordinator: parameters.navigationStackCoordinator, + notificationSettingsProxy: parameters.notificationSettings, + roomProxy: roomProxy, + displayAsUserDefinedRoomSettings: true) + let roomNotificationSettingsCoordinator = RoomNotificationSettingsScreenCoordinator(parameters: roomNotificationSettingsParameters) + parameters.navigationStackCoordinator?.push(roomNotificationSettingsCoordinator) + } } diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenModels.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenModels.swift index cdf5d8c9f..8bfabdb3d 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenModels.swift @@ -16,7 +16,9 @@ import Foundation -enum NotificationSettingsEditScreenViewModelAction { } +enum NotificationSettingsEditScreenViewModelAction { + case requestRoomNotificationSettingsPresentation(roomID: String) +} enum NotificationSettingsEditScreenDefaultMode { case allMessages @@ -26,22 +28,29 @@ enum NotificationSettingsEditScreenDefaultMode { struct NotificationSettingsEditScreenViewState: BindableState { var bindings: NotificationSettingsEditScreenViewStateBindings var strings: NotificationSettingsEditScreenStrings - var isDirect: Bool + var chatType: NotificationSettingsChatType var availableDefaultModes: [NotificationSettingsEditScreenDefaultMode] = [.allMessages, .mentionsAndKeywordsOnly] var defaultMode: NotificationSettingsEditScreenDefaultMode? var pendingMode: NotificationSettingsEditScreenDefaultMode? + var roomsWithUserDefinedMode: [NotificationSettingsEditScreenRoom] = [] func isSelected(mode: NotificationSettingsEditScreenDefaultMode) -> Bool { pendingMode == nil && defaultMode == mode } + + var displayRoomsWithCustomSettings: Bool { + !roomsWithUserDefinedMode.isEmpty + } } struct NotificationSettingsEditScreenViewStateBindings { + var searchQuery = "" var alertInfo: AlertInfo? } enum NotificationSettingsEditScreenViewAction { case setMode(NotificationSettingsEditScreenDefaultMode) + case selectRoom(roomIdentifier: String) } enum NotificationSettingsEditScreenErrorType: Hashable { @@ -52,22 +61,60 @@ struct NotificationSettingsEditScreenStrings { let navigationTitle: String let modeSectionTitle: String - init(isDirect: Bool) { - if isDirect { + init(chatType: NotificationSettingsChatType) { + switch chatType { + case .oneToOneChat: navigationTitle = L10n.screenNotificationSettingsDirectChats modeSectionTitle = L10n.screenNotificationSettingsEditScreenDirectSectionHeader - } else { + case .groupChat: navigationTitle = L10n.screenNotificationSettingsGroupChats modeSectionTitle = L10n.screenNotificationSettingsEditScreenGroupSectionHeader } } func string(for mode: NotificationSettingsEditScreenDefaultMode) -> String { + switch mode { + case .allMessages: + return L10n.screenNotificationSettingsEditModeAllMessages + case .mentionsAndKeywordsOnly: + return L10n.screenNotificationSettingsEditModeMentionsAndKeywords + } + } + + func string(for mode: RoomNotificationModeProxy) -> String { switch mode { case .allMessages: return L10n.screenNotificationSettingsModeAll case .mentionsAndKeywordsOnly: return L10n.screenNotificationSettingsModeMentions + case .mute: + return L10n.commonMute } } } + +struct NotificationSettingsEditScreenRoom: Identifiable, Equatable { + static let placeholderLastMessage = AttributedString("Hidden last message") + + /// The list item identifier can be a real room identifier, a custom one for invalidated entries + /// or a completely unique one for empty items and skeletons + let id: String + + /// The real room identifier this item points to + let roomId: String? + + var name = "" + + var avatarURL: URL? + + var notificationMode: RoomNotificationModeProxy? + + var isPlaceholder = false + + static func placeholder() -> NotificationSettingsEditScreenRoom { + NotificationSettingsEditScreenRoom(id: UUID().uuidString, + roomId: nil, + name: "Placeholder room name", + isPlaceholder: true) + } +} diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenViewModel.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenViewModel.swift index 5a8eea633..a47c63e73 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenViewModel.swift @@ -21,27 +21,37 @@ typealias NotificationSettingsEditScreenViewModelType = StateStoreViewModel = .init() - private let isDirect: Bool + private let chatType: NotificationSettingsChatType private let notificationSettingsProxy: NotificationSettingsProxyProtocol - @CancellableTask private var fetchSettingsTask: Task? + private let userSession: UserSessionProtocol + private let roomSummaryProvider: RoomSummaryProviderProtocol? + + @CancellableTask private var fetchDefaultRoomNotificationModesTask: Task? + @CancellableTask private var updateRoomsWithUserDefinedModeTask: Task? var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - - init(isDirect: Bool, notificationSettingsProxy: NotificationSettingsProxyProtocol) { + + init(chatType: NotificationSettingsChatType, userSession: UserSessionProtocol, notificationSettingsProxy: NotificationSettingsProxyProtocol) { let bindings = NotificationSettingsEditScreenViewStateBindings() - self.isDirect = isDirect + self.chatType = chatType + self.userSession = userSession self.notificationSettingsProxy = notificationSettingsProxy + roomSummaryProvider = userSession.clientProxy.roomSummaryProvider + super.init(initialViewState: NotificationSettingsEditScreenViewState(bindings: bindings, - strings: NotificationSettingsEditScreenStrings(isDirect: isDirect), - isDirect: isDirect)) + strings: NotificationSettingsEditScreenStrings(chatType: chatType), + chatType: chatType), + imageProvider: userSession.mediaProvider) setupNotificationSettingsSubscription() + setupRoomSummaryProviderSubscription() } func fetchInitialContent() { - fetchSettings() + fetchDefaultRoomNotificationModes() + updateRoomsWithUserDefinedMode() } // MARK: - Public @@ -50,6 +60,8 @@ class NotificationSettingsEditScreenViewModel: NotificationSettingsEditScreenVie switch viewAction { case .setMode(let mode): setMode(mode) + case .selectRoom(let roomIdentifier): + actionsSubject.send(.requestRoomNotificationSettingsPresentation(roomID: roomIdentifier)) } } @@ -63,17 +75,19 @@ class NotificationSettingsEditScreenViewModel: NotificationSettingsEditScreenVie switch callback { case .settingsDidChange: - self.fetchSettings() + self.fetchDefaultRoomNotificationModes() + self.updateRoomsWithUserDefinedMode() } } .store(in: &cancellables) } - private func fetchSettings() { - fetchSettingsTask = Task { + private func fetchDefaultRoomNotificationModes() { + fetchDefaultRoomNotificationModesTask = Task { var mode: RoomNotificationModeProxy? - let encrypted_mode = await notificationSettingsProxy.getDefaultRoomNotificationMode(isEncrypted: true, isOneToOne: isDirect) - let unencrypted_mode = await notificationSettingsProxy.getDefaultRoomNotificationMode(isEncrypted: false, isOneToOne: isDirect) + let isOneToOne = chatType == .oneToOneChat + let encrypted_mode = await notificationSettingsProxy.getDefaultRoomNotificationMode(isEncrypted: true, isOneToOne: isOneToOne) + let unencrypted_mode = await notificationSettingsProxy.getDefaultRoomNotificationMode(isEncrypted: false, isOneToOne: isOneToOne) if encrypted_mode == unencrypted_mode { mode = encrypted_mode } @@ -90,6 +104,71 @@ class NotificationSettingsEditScreenViewModel: NotificationSettingsEditScreenVie } } + private func setupRoomSummaryProviderSubscription() { + guard let roomSummaryProvider else { + MXLog.error("Room summary provider unavailable") + return + } + + roomSummaryProvider.roomListPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.updateRoomsWithUserDefinedMode() + } + .store(in: &cancellables) + } + + private func updateRoomsWithUserDefinedMode() { + guard let roomSummaryProvider else { + MXLog.error("Room summary provider unavailable") + return + } + + updateRoomsWithUserDefinedModeTask = Task { + let roomsWithUserDefinedRules = try await notificationSettingsProxy.getRoomsWithUserDefinedRules() + guard !Task.isCancelled else { return } + + let filteredRoomsSummary = roomSummaryProvider.roomListPublisher.value.filter { summary in + roomsWithUserDefinedRules.contains(where: { summary.id == $0 }) + } + + var roomsWithUserDefinedMode: [NotificationSettingsEditScreenRoom] = [] + + for roomSummary in filteredRoomsSummary { + switch roomSummary { + case .empty, .invalidated: + break + case .filled(let details): + guard let roomProxy = await userSession.clientProxy.roomForIdentifier(details.id) else { continue } + // `isOneToOneRoom` here is not the same as `isDirect` on the room. From the point of view of the push rule, a one-to-one room is a room with exactly two active members. + let isOneToOneRoom = roomProxy.activeMembersCount == 2 + // display only the rooms we're interested in + switch chatType { + case .oneToOneChat where isOneToOneRoom, + .groupChat where !isOneToOneRoom: + await roomsWithUserDefinedMode.append(buildRoom(with: details)) + default: + break + } + } + } + + // Sort the room list + roomsWithUserDefinedMode.sort(by: { $0.name.localizedCompare($1.name) == .orderedAscending }) + + state.roomsWithUserDefinedMode = roomsWithUserDefinedMode + } + } + + private func buildRoom(with details: RoomSummaryDetails) async -> NotificationSettingsEditScreenRoom { + let notificationMode = try? await notificationSettingsProxy.getUserDefinedRoomNotificationMode(roomId: details.id) + return NotificationSettingsEditScreenRoom(id: details.id, + roomId: details.id, + name: details.name, + avatarURL: details.avatarURL, + notificationMode: notificationMode) + } + private func setMode(_ mode: NotificationSettingsEditScreenDefaultMode) { guard state.pendingMode == nil, !state.isSelected(mode: mode) else { return } let roomNotificationModeProxy: RoomNotificationModeProxy @@ -102,9 +181,12 @@ class NotificationSettingsEditScreenViewModel: NotificationSettingsEditScreenVie state.pendingMode = mode Task { do { - try await notificationSettingsProxy.setDefaultRoomNotificationMode(isEncrypted: true, isOneToOne: isDirect, mode: roomNotificationModeProxy) - try await notificationSettingsProxy.setDefaultRoomNotificationMode(isEncrypted: false, isOneToOne: isDirect, mode: roomNotificationModeProxy) + // On modern clients, we don't have different settings for encrypted and non-encrypted rooms. + let isOneToOne = chatType == .oneToOneChat + try await notificationSettingsProxy.setDefaultRoomNotificationMode(isEncrypted: true, isOneToOne: isOneToOne, mode: roomNotificationModeProxy) + try await notificationSettingsProxy.setDefaultRoomNotificationMode(isEncrypted: false, isOneToOne: isOneToOne, mode: roomNotificationModeProxy) } catch { + // In case of failure, we let the user retry let retryAction: () -> Void = { [weak self] in self?.setMode(mode) } diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreen.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreen.swift index 9f73b0804..81151199f 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreen.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreen.swift @@ -14,16 +14,22 @@ // limitations under the License. // +import Compound import SwiftUI struct NotificationSettingsEditScreen: View { @ObservedObject var context: NotificationSettingsEditScreenViewModel.Context + @State var isSearching = false var body: some View { Form { notificationModeSection + + if context.viewState.displayRoomsWithCustomSettings { + roomsWithCustomSettingsSection + } } - .compoundForm() + .compoundList() .navigationTitle(context.viewState.strings.navigationTitle) .alert(item: $context.alertInfo) .track(screen: .settingsDefaultNotifications) @@ -34,25 +40,27 @@ struct NotificationSettingsEditScreen: View { private var notificationModeSection: some View { Section { ForEach(context.viewState.availableDefaultModes, id: \.self) { mode in - Button { - context.send(viewAction: .setMode(mode)) - } label: { - LabeledContent { - if context.viewState.pendingMode == mode { - ProgressView() - } else { - EmptyView() - } - } label: { - Text(context.viewState.strings.string(for: mode)) - } - } - .buttonStyle(.compoundForm(accessory: .selected(context.viewState.isSelected(mode: mode)))) - .disabled(context.viewState.pendingMode != nil) + ListRow(label: .plain(title: context.viewState.strings.string(for: mode)), + details: (context.viewState.pendingMode == mode) ? .isWaiting(true) : nil, + kind: .selection(isSelected: context.viewState.isSelected(mode: mode)) { + context.send(viewAction: .setMode(mode)) + }) + .disabled(context.viewState.pendingMode != nil) } } header: { Text(context.viewState.strings.modeSectionTitle) - .compoundFormSectionHeader() + .compoundListSectionHeader() + } + } + + private var roomsWithCustomSettingsSection: some View { + Section { + ForEach(context.viewState.roomsWithUserDefinedMode, id: \.id) { room in + NotificationSettingsEditScreenRoomCell(room: room, context: context) + } + } header: { + Text(L10n.screenNotificationSettingsEditCustomSettingsSectionTitle) + .compoundListSectionHeader() } .compoundFormSection() } @@ -64,7 +72,13 @@ struct NotificationSettingsEditScreen_Previews: PreviewProvider { static let viewModelGroupChats: NotificationSettingsEditScreenViewModel = { let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init()) notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .allMessages - var viewModel = NotificationSettingsEditScreenViewModel(isDirect: false, + + notificationSettingsProxy.getRoomsWithUserDefinedRulesReturnValue = [RoomSummary].mockRooms.compactMap(\.id) + let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@alice:example.com", + roomSummaryProvider: MockRoomSummaryProvider(state: .loaded(.mockRooms))), + mediaProvider: MockMediaProvider()) + var viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat, + userSession: userSession, notificationSettingsProxy: notificationSettingsProxy) viewModel.fetchInitialContent() return viewModel @@ -73,7 +87,12 @@ struct NotificationSettingsEditScreen_Previews: PreviewProvider { static let viewModelDirectChats: NotificationSettingsEditScreenViewModel = { let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init()) notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly - var viewModel = NotificationSettingsEditScreenViewModel(isDirect: true, + notificationSettingsProxy.getRoomsWithUserDefinedRulesReturnValue = [] + let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@alice:example.com", + roomSummaryProvider: MockRoomSummaryProvider(state: .loaded(.mockRooms))), + mediaProvider: MockMediaProvider()) + var viewModel = NotificationSettingsEditScreenViewModel(chatType: .oneToOneChat, + userSession: userSession, notificationSettingsProxy: notificationSettingsProxy) viewModel.fetchInitialContent() return viewModel @@ -82,7 +101,11 @@ struct NotificationSettingsEditScreen_Previews: PreviewProvider { static let viewModelDirectApplyingChange: NotificationSettingsEditScreenViewModel = { let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init()) notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly - var viewModel = NotificationSettingsEditScreenViewModel(isDirect: true, + notificationSettingsProxy.getRoomsWithUserDefinedRulesReturnValue = [] + let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "John Doe"), mediaProvider: MockMediaProvider()) + + var viewModel = NotificationSettingsEditScreenViewModel(chatType: .oneToOneChat, + userSession: userSession, notificationSettingsProxy: notificationSettingsProxy) viewModel.state.pendingMode = .mentionsAndKeywordsOnly viewModel.fetchInitialContent() diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreenRoomCell.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreenRoomCell.swift new file mode 100644 index 000000000..34818ff1c --- /dev/null +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreenRoomCell.swift @@ -0,0 +1,90 @@ +// +// Copyright 2023 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 Compound +import SwiftUI + +struct NotificationSettingsEditScreenRoomCell: View { + @Environment(\.dynamicTypeSize) var dynamicTypeSize + + let room: NotificationSettingsEditScreenRoom + let context: NotificationSettingsEditScreenViewModel.Context + + var body: some View { + ListRow(label: .action(title: room.name, + icon: avatar), + details: roomDetailsLabel, + kind: .navigationLink { + if let roomId = room.roomId { + context.send(viewAction: .selectRoom(roomIdentifier: roomId)) + } + }) + .lineLimit(1) + .accessibilityIdentifier(A11yIdentifiers.notificationSettingsEditScreen.roomName(room.name)) + } + + @ViewBuilder @MainActor + var avatar: some View { + if dynamicTypeSize < .accessibility3 { + LoadableAvatarImage(url: room.avatarURL, + name: room.name, + contentID: room.roomId, + avatarSize: .room(on: .notificationSettings), + imageProvider: context.imageProvider) + .dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1) + .accessibilityHidden(true) + } + } + + @MainActor + var roomDetailsLabel: ListDetailsLabel? { + guard let mode = room.notificationMode else { return nil } + return .label(title: context.viewState.strings.string(for: mode), + icon: EmptyView()) + } +} + +struct NotificationSettingsEditScreenRoomCell_Previews: PreviewProvider { + static var previews: some View { + let summaryProvider = MockRoomSummaryProvider(state: .loaded(.mockRooms)) + + let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "John Doe", roomSummaryProvider: summaryProvider), + mediaProvider: MockMediaProvider()) + + let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init()) + notificationSettingsProxy.getRoomsWithUserDefinedRulesReturnValue = [] + let viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat, + userSession: userSession, + notificationSettingsProxy: notificationSettingsProxy) + + let rooms: [NotificationSettingsEditScreenRoom] = summaryProvider.roomListPublisher.value.compactMap { summary -> NotificationSettingsEditScreenRoom? in + switch summary { + case .empty, .invalidated: + return nil + case .filled(let details): + return NotificationSettingsEditScreenRoom(id: UUID().uuidString, + roomId: details.id, + name: details.name) + } + } + + return VStack(spacing: 0) { + ForEach(rooms) { room in + NotificationSettingsEditScreenRoomCell(room: room, context: viewModel.context) + } + } + } +} diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift index 8ba32b283..8141c8389 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift @@ -19,6 +19,7 @@ import SwiftUI struct NotificationSettingsScreenCoordinatorParameters { weak var navigationStackCoordinator: NavigationStackCoordinator? + let userSession: UserSessionProtocol let userNotificationCenter: UserNotificationCenterProtocol let notificationSettings: NotificationSettingsProxyProtocol let isModallyPresented: Bool @@ -45,7 +46,8 @@ final class NotificationSettingsScreenCoordinator: CoordinatorProtocol { init(parameters: NotificationSettingsScreenCoordinatorParameters) { self.parameters = parameters - viewModel = NotificationSettingsScreenViewModel(appSettings: ServiceLocator.shared.settings, + viewModel = NotificationSettingsScreenViewModel(userSession: parameters.userSession, + appSettings: ServiceLocator.shared.settings, userNotificationCenter: parameters.userNotificationCenter, notificationSettingsProxy: parameters.notificationSettings, isModallyPresented: parameters.isModallyPresented) @@ -59,8 +61,8 @@ final class NotificationSettingsScreenCoordinator: CoordinatorProtocol { switch action { case .close: self.actionsSubject.send(.close) - case .editDefaultMode(let isDirect): - self.presentEditScreen(isDirect: isDirect) + case .editDefaultMode(let chatType): + self.presentEditScreen(chatType: chatType) } } .store(in: &cancellables) @@ -72,8 +74,10 @@ final class NotificationSettingsScreenCoordinator: CoordinatorProtocol { // MARK: - Private - private func presentEditScreen(isDirect: Bool) { - let editSettingsParameters = NotificationSettingsEditScreenCoordinatorParameters(isDirect: isDirect, + private func presentEditScreen(chatType: NotificationSettingsChatType) { + let editSettingsParameters = NotificationSettingsEditScreenCoordinatorParameters(navigationStackCoordinator: parameters.navigationStackCoordinator, + chatType: chatType, + userSession: parameters.userSession, notificationSettings: parameters.notificationSettings) let editSettingsCoordinator = NotificationSettingsEditScreenCoordinator(parameters: editSettingsParameters) navigationStackCoordinator?.push(editSettingsCoordinator) diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift index 4b85bf0d1..c0b5b422d 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift @@ -19,7 +19,7 @@ import UIKit enum NotificationSettingsScreenViewModelAction { case close - case editDefaultMode(isDirect: Bool) + case editDefaultMode(chatType: NotificationSettingsChatType) } struct NotificationSettingsScreenViewState: BindableState { diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift index 100f513a2..0ff3830af 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift @@ -24,13 +24,15 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy private let appSettings: AppSettings private let userNotificationCenter: UserNotificationCenterProtocol private let notificationSettingsProxy: NotificationSettingsProxyProtocol + private let userSession: UserSessionProtocol @CancellableTask private var fetchSettingsTask: Task? var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - init(appSettings: AppSettings, userNotificationCenter: UserNotificationCenterProtocol, notificationSettingsProxy: NotificationSettingsProxyProtocol, isModallyPresented: Bool) { + init(userSession: UserSessionProtocol, appSettings: AppSettings, userNotificationCenter: UserNotificationCenterProtocol, notificationSettingsProxy: NotificationSettingsProxyProtocol, isModallyPresented: Bool) { + self.userSession = userSession self.appSettings = appSettings self.userNotificationCenter = userNotificationCenter self.notificationSettingsProxy = notificationSettingsProxy @@ -60,9 +62,9 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy case .changedEnableNotifications: toggleNotifications() case .groupChatsTapped: - actionsSubject.send(.editDefaultMode(isDirect: false)) + actionsSubject.send(.editDefaultMode(chatType: .groupChat)) case .directChatsTapped: - actionsSubject.send(.editDefaultMode(isDirect: true)) + actionsSubject.send(.editDefaultMode(chatType: .oneToOneChat)) case .roomMentionChanged: guard let settings = state.settings, settings.roomMentionsEnabled != state.bindings.roomMentionsEnabled else { return diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift index 9ecdd76d2..38e6d29c4 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift @@ -173,7 +173,10 @@ struct NotificationSettingsScreen_Previews: PreviewProvider { notificationSettingsProxy.isRoomMentionEnabledReturnValue = true notificationSettingsProxy.isCallEnabledReturnValue = false - var viewModel = NotificationSettingsScreenViewModel(appSettings: appSettings, + let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "John Doe"), mediaProvider: MockMediaProvider()) + + var viewModel = NotificationSettingsScreenViewModel(userSession: userSession, + appSettings: appSettings, userNotificationCenter: notificationCenter, notificationSettingsProxy: notificationSettingsProxy, isModallyPresented: true) diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift index d740b66d0..b5d1051da 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift @@ -142,6 +142,7 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { private func presentNotificationSettings() { let notificationParameters = NotificationSettingsScreenCoordinatorParameters(navigationStackCoordinator: parameters.navigationStackCoordinator, + userSession: parameters.userSession, userNotificationCenter: UNUserNotificationCenter.current(), notificationSettings: parameters.notificationSettings, isModallyPresented: false) diff --git a/ElementX/Sources/Services/NotificationSettings/NotificationSettingsChatType.swift b/ElementX/Sources/Services/NotificationSettings/NotificationSettingsChatType.swift new file mode 100644 index 000000000..0063514b1 --- /dev/null +++ b/ElementX/Sources/Services/NotificationSettings/NotificationSettingsChatType.swift @@ -0,0 +1,23 @@ +// +// Copyright 2023 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 NotificationSettingsChatType { + /// A one-to-one chat is a chat with exactly two active members + case oneToOneChat + case groupChat +} diff --git a/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxy.swift b/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxy.swift index dbe2e1e69..149c42c4d 100644 --- a/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxy.swift +++ b/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxy.swift @@ -59,6 +59,11 @@ final class NotificationSettingsProxy: NotificationSettingsProxyProtocol { await updatedSettings() } + func getUserDefinedRoomNotificationMode(roomId: String) async throws -> RoomNotificationModeProxy? { + let roomNotificationMode = try await notificationSettings.getUserDefinedRoomNotificationMode(roomId: roomId) + return roomNotificationMode.flatMap { RoomNotificationModeProxy.from(roomNotificationMode: $0) } + } + func getDefaultRoomNotificationMode(isEncrypted: Bool, isOneToOne: Bool) async -> RoomNotificationModeProxy { let roomNotificationMode = await notificationSettings.getDefaultRoomNotificationMode(isEncrypted: isEncrypted, isOneToOne: isOneToOne) return RoomNotificationModeProxy.from(roomNotificationMode: roomNotificationMode) @@ -128,6 +133,10 @@ final class NotificationSettingsProxy: NotificationSettingsProxyProtocol { await updatedSettings() } + func getRoomsWithUserDefinedRules() async throws -> [String] { + await notificationSettings.getRoomsWithUserDefinedRules(enabled: true) + } + // MARK: - Private func updatedSettings() async { diff --git a/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxyProtocol.swift b/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxyProtocol.swift index 9e8b00d71..97b80b85b 100644 --- a/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxyProtocol.swift +++ b/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxyProtocol.swift @@ -28,6 +28,7 @@ protocol NotificationSettingsProxyProtocol { func getNotificationSettings(roomId: String, isEncrypted: Bool, isOneToOne: Bool) async throws -> RoomNotificationSettingsProxyProtocol func setNotificationMode(roomId: String, mode: RoomNotificationModeProxy) async throws + func getUserDefinedRoomNotificationMode(roomId: String) async throws -> RoomNotificationModeProxy? func getDefaultRoomNotificationMode(isEncrypted: Bool, isOneToOne: Bool) async -> RoomNotificationModeProxy func setDefaultRoomNotificationMode(isEncrypted: Bool, isOneToOne: Bool, mode: RoomNotificationModeProxy) async throws func restoreDefaultNotificationMode(roomId: String) async throws @@ -39,4 +40,5 @@ protocol NotificationSettingsProxyProtocol { func setUserMentionEnabled(enabled: Bool) async throws func isCallEnabled() async throws -> Bool func setCallEnabled(enabled: Bool) async throws + func getRoomsWithUserDefinedRules() async throws -> [String] } diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 531e37575..90c4a2133 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -197,7 +197,10 @@ class MockScreen: Identifiable { case .notificationSettingsScreen: let userNotificationCenter = UserNotificationCenterMock() userNotificationCenter.authorizationStatusReturnValue = .denied - let parameters = NotificationSettingsScreenCoordinatorParameters(userNotificationCenter: userNotificationCenter, + let session = MockUserSession(clientProxy: MockClientProxy(userID: "@mock:matrix.org"), + mediaProvider: MockMediaProvider()) + let parameters = NotificationSettingsScreenCoordinatorParameters(userSession: session, + userNotificationCenter: userNotificationCenter, notificationSettings: NotificationSettingsProxyMock(with: .init()), isModallyPresented: false) return NotificationSettingsScreenCoordinator(parameters: parameters) @@ -501,7 +504,8 @@ class MockScreen: Identifiable { let members: [RoomMemberProxyMock] = [.mockInvitedAlice, .mockBob, .mockCharlie] let coordinator = RoomNotificationSettingsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, notificationSettingsProxy: NotificationSettingsProxyMock(with: .init(defaultRoomMode: .allMessages, roomMode: .allMessages)), - roomProxy: RoomProxyMock(with: .init(displayName: "test", members: members)))) + roomProxy: RoomProxyMock(with: .init(displayName: "test", members: members)), + displayAsUserDefinedRoomSettings: false)) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomNotificationSettingsCustomSetting: @@ -509,7 +513,8 @@ class MockScreen: Identifiable { let members: [RoomMemberProxyMock] = [.mockInvitedAlice, .mockBob, .mockCharlie] let coordinator = RoomNotificationSettingsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, notificationSettingsProxy: NotificationSettingsProxyMock(with: .init(defaultRoomMode: .allMessages, roomMode: .mentionsAndKeywordsOnly)), - roomProxy: RoomProxyMock(with: .init(displayName: "test", members: members)))) + roomProxy: RoomProxyMock(with: .init(displayName: "test", members: members)), + displayAsUserDefinedRoomSettings: false)) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .reportContent: diff --git a/UnitTests/Sources/NotificationSettingsEditScreenViewModelTests.swift b/UnitTests/Sources/NotificationSettingsEditScreenViewModelTests.swift index f0e170728..a7eec6800 100644 --- a/UnitTests/Sources/NotificationSettingsEditScreenViewModelTests.swift +++ b/UnitTests/Sources/NotificationSettingsEditScreenViewModelTests.swift @@ -23,12 +23,15 @@ import XCTest class NotificationSettingsEditScreenViewModelTests: XCTestCase { private var viewModel: NotificationSettingsEditScreenViewModelProtocol! private var notificationSettingsProxy: NotificationSettingsProxyMock! + private var userSession: UserSessionProtocol! private var context: NotificationSettingsEditScreenViewModelType.Context { viewModel.context } @MainActor override func setUpWithError() throws { + let clientProxy = MockClientProxy(userID: "@a:b.com") + userSession = MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider()) notificationSettingsProxy = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()) notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .allMessages } @@ -42,7 +45,8 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase { return .mentionsAndKeywordsOnly } } - viewModel = NotificationSettingsEditScreenViewModel(isDirect: false, + viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat, + userSession: userSession, notificationSettingsProxy: notificationSettingsProxy) let deferred = deferFulfillment(viewModel.context.$viewState.map(\.defaultMode) @@ -67,7 +71,8 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase { func testSetModeAllMessages() async throws { notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly - viewModel = NotificationSettingsEditScreenViewModel(isDirect: false, + viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat, + userSession: userSession, notificationSettingsProxy: notificationSettingsProxy) let deferred = deferFulfillment(viewModel.context.$viewState.map(\.defaultMode) .first(where: { !$0.isNil })) @@ -107,7 +112,8 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase { } func testSetModeMentions() async throws { - viewModel = NotificationSettingsEditScreenViewModel(isDirect: false, + viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat, + userSession: userSession, notificationSettingsProxy: notificationSettingsProxy) let deferred = deferFulfillment(viewModel.context.$viewState.map(\.defaultMode) .first(where: { !$0.isNil })) @@ -149,7 +155,8 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase { func testSetModeDirectChats() async throws { notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly // Initialize for direct chats - viewModel = NotificationSettingsEditScreenViewModel(isDirect: true, + viewModel = NotificationSettingsEditScreenViewModel(chatType: .oneToOneChat, + userSession: userSession, notificationSettingsProxy: notificationSettingsProxy) let deferred = deferFulfillment(viewModel.context.$viewState.map(\.defaultMode) .first(where: { !$0.isNil })) @@ -182,7 +189,8 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase { func testSetModeFailure() async throws { notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeThrowableError = NotificationSettingsError.Generic(message: "error") - viewModel = NotificationSettingsEditScreenViewModel(isDirect: true, + viewModel = NotificationSettingsEditScreenViewModel(chatType: .oneToOneChat, + userSession: userSession, notificationSettingsProxy: notificationSettingsProxy) let deferred = deferFulfillment(viewModel.context.$viewState.map(\.defaultMode) .first(where: { !$0.isNil })) @@ -200,4 +208,21 @@ class NotificationSettingsEditScreenViewModelTests: XCTestCase { XCTAssertEqual(pendingModes, [nil, .allMessages, nil]) XCTAssertNotNil(context.viewState.bindings.alertInfo) } + + func testSelectRoom() async throws { + let roomID = "!roomidentifier:matrix.org" + viewModel = NotificationSettingsEditScreenViewModel(chatType: .oneToOneChat, + userSession: userSession, + notificationSettingsProxy: notificationSettingsProxy) + + let deferredActions = deferFulfillment(viewModel.actions.first()) + context.send(viewAction: .selectRoom(roomIdentifier: roomID)) + let sentActions = try await deferredActions.fulfill() + + let expectedAction = NotificationSettingsEditScreenViewModelAction.requestRoomNotificationSettingsPresentation(roomID: roomID) + guard case let .requestRoomNotificationSettingsPresentation(roomID: receivedRoomID) = sentActions, receivedRoomID == roomID else { + XCTFail("Expected action \(expectedAction), but was \(sentActions)") + return + } + } } diff --git a/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift b/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift index 658a0b2c1..e9ec54502 100644 --- a/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift +++ b/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift @@ -24,6 +24,7 @@ class NotificationSettingsScreenViewModelTests: XCTestCase { private var viewModel: NotificationSettingsScreenViewModelProtocol! private var context: NotificationSettingsScreenViewModelType.Context! private var appSettings: AppSettings! + private var userSession: UserSessionProtocol! private var userNotificationCenter: UserNotificationCenterMock! private var notificationSettingsProxy: NotificationSettingsProxyMock! @@ -38,7 +39,11 @@ class NotificationSettingsScreenViewModelTests: XCTestCase { notificationSettingsProxy.isRoomMentionEnabledReturnValue = true notificationSettingsProxy.isCallEnabledReturnValue = true - viewModel = NotificationSettingsScreenViewModel(appSettings: appSettings, + let clientProxy = MockClientProxy(userID: "@a:b.com") + userSession = MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider()) + + viewModel = NotificationSettingsScreenViewModel(userSession: userSession, + appSettings: appSettings, userNotificationCenter: userNotificationCenter, notificationSettingsProxy: notificationSettingsProxy, isModallyPresented: false) diff --git a/UnitTests/Sources/RoomNotificationSettingsScreenViewModelTests.swift b/UnitTests/Sources/RoomNotificationSettingsScreenViewModelTests.swift index e798fd4c4..56b7c759f 100644 --- a/UnitTests/Sources/RoomNotificationSettingsScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomNotificationSettingsScreenViewModelTests.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import MatrixRustSDK import XCTest @@ -25,18 +26,21 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase { var roomProxyMock: RoomProxyMock! var notificationSettingsProxyMock: NotificationSettingsProxyMock! var context: RoomNotificationSettingsScreenViewModelType.Context { viewModel.context } + var cancellables: Set = [] override func setUpWithError() throws { roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", joinedMembersCount: 0)) notificationSettingsProxyMock = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()) viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, - roomProxy: roomProxyMock) + roomProxy: roomProxyMock, + displayAsUserDefinedRoomSettings: false) } func testInitialStateDefaultMode() async throws { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: true)) viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, - roomProxy: roomProxyMock) + roomProxy: roomProxyMock, + displayAsUserDefinedRoomSettings: false) let deferred = deferFulfillment(context.$viewState.map(\.notificationSettingsState) .first(where: \.isLoaded)) notificationSettingsProxyMock.callbacks.send(.settingsDidChange) @@ -48,7 +52,8 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase { func testInitialStateCustomMode() async throws { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: false)) viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, - roomProxy: roomProxyMock) + roomProxy: roomProxyMock, + displayAsUserDefinedRoomSettings: false) let deferred = deferFulfillment(context.$viewState.map(\.notificationSettingsState) .first(where: \.isLoaded)) notificationSettingsProxyMock.callbacks.send(.settingsDidChange) @@ -60,7 +65,8 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase { func testInitialStateFailure() async throws { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneThrowableError = NotificationSettingsError.Generic(message: "error") viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, - roomProxy: roomProxyMock) + roomProxy: roomProxyMock, + displayAsUserDefinedRoomSettings: false) let deferred = deferFulfillment(context.$viewState.map(\.notificationSettingsState) .first(where: \.isError)) notificationSettingsProxyMock.callbacks.send(.settingsDidChange) @@ -77,7 +83,8 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase { func testToggleAllCustomSettingOff() async throws { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: false)) viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, - roomProxy: roomProxyMock) + roomProxy: roomProxyMock, + displayAsUserDefinedRoomSettings: false) let deferred = deferFulfillment(context.$viewState.map(\.notificationSettingsState) .first(where: \.isLoaded)) notificationSettingsProxyMock.callbacks.send(.settingsDidChange) @@ -98,7 +105,8 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase { func testToggleAllCustomSettingOffOn() async throws { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: true)) viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, - roomProxy: roomProxyMock) + roomProxy: roomProxyMock, + displayAsUserDefinedRoomSettings: false) var deferred = deferFulfillment(context.$viewState.map(\.notificationSettingsState).first(where: \.isLoaded)) notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() @@ -116,7 +124,8 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase { func testSetCustomMode() async throws { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: false)) viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, - roomProxy: roomProxyMock) + roomProxy: roomProxyMock, + displayAsUserDefinedRoomSettings: false) let deferredState = deferFulfillment(context.$viewState.map(\.notificationSettingsState).first(where: \.isLoaded)) notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferredState.fulfill() @@ -151,4 +160,70 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase { XCTAssertEqual(notificationSettingsProxyMock.setNotificationModeRoomIdModeCallsCount, 3) } } + + func testDeleteCustomSettingTapped() async throws { + notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: false)) + viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, + roomProxy: roomProxyMock, + displayAsUserDefinedRoomSettings: true) + let deferredState = deferFulfillment(context.$viewState.map(\.notificationSettingsState).first(where: \.isLoaded)) + notificationSettingsProxyMock.callbacks.send(.settingsDidChange) + try await deferredState.fulfill() + + var actionSent: RoomNotificationSettingsScreenViewModelAction? + viewModel.actions + .sink { value in + actionSent = value + } + .store(in: &cancellables) + + let deferredViewState = deferFulfillment(context.$viewState + .map(\.deletingCustomSetting) + .removeDuplicates() + .collect(3).first()) + context.send(viewAction: .deleteCustomSettingTapped) + let states = try await deferredViewState.fulfill() + + // `deletingCustomSetting` must be set to `true` when deleting, and reset to `false` afterwards. + XCTAssertEqual(states, [false, true, false]) + // the `dismiss` action must have been sent + XCTAssertEqual(actionSent, .dismiss) + // `restoreDefaultNotificationMode` should have been called + XCTAssert(notificationSettingsProxyMock.restoreDefaultNotificationModeRoomIdCalled) + XCTAssertEqual(notificationSettingsProxyMock.restoreDefaultNotificationModeRoomIdReceivedInvocations, [roomProxyMock.id]) + // and no alert is expected + XCTAssertNil(context.alertInfo) + } + + func testDeleteCustomSettingTappedFailure() async throws { + notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: false)) + notificationSettingsProxyMock.restoreDefaultNotificationModeRoomIdThrowableError = NotificationSettingsError.Generic(message: "error") + viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, + roomProxy: roomProxyMock, + displayAsUserDefinedRoomSettings: true) + let deferredState = deferFulfillment(context.$viewState.map(\.notificationSettingsState).first(where: \.isLoaded)) + notificationSettingsProxyMock.callbacks.send(.settingsDidChange) + try await deferredState.fulfill() + + var actionSent: RoomNotificationSettingsScreenViewModelAction? + viewModel.actions + .sink { value in + actionSent = value + } + .store(in: &cancellables) + + let deferredViewState = deferFulfillment(context.$viewState + .map(\.deletingCustomSetting) + .removeDuplicates() + .collect(3).first()) + context.send(viewAction: .deleteCustomSettingTapped) + let states = try await deferredViewState.fulfill() + + // `deletingCustomSetting` must be set to `true` when deleting, and reset to `false` afterwards. + XCTAssertEqual(states, [false, true, false]) + // an alert is expected + XCTAssertEqual(context.alertInfo?.id, .restoreDefaultFailed) + // the `dismiss` action must not have been sent + XCTAssertNil(actionSent) + } }