From 2102ebc01ec2077feb7edbebc931a7bbbd4a541c Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Fri, 11 Aug 2023 16:10:23 +0200 Subject: [PATCH] Edit the default notification settings (#1468) --- ElementX.xcodeproj/project.pbxproj | 44 ++++ .../en.lproj/Localizable.strings | 3 + .../RoomFlowCoordinator.swift | 3 +- ElementX/Sources/Generated/Strings.swift | 6 + .../Mocks/Generated/GeneratedMocks.swift | 50 +++-- .../Mocks/NotificationSettingsProxyMock.swift | 13 +- ...icationSettingsEditScreenCoordinator.swift | 51 +++++ ...NotificationSettingsEditScreenModels.swift | 73 +++++++ ...ificationSettingsEditScreenViewModel.swift | 120 +++++++++++ ...nSettingsEditScreenViewModelProtocol.swift | 25 +++ .../View/NotificationSettingsEditScreen.swift | 100 +++++++++ ...otificationSettingsScreenCoordinator.swift | 23 +- .../NotificationSettingsScreenModels.swift | 1 + .../NotificationSettingsScreenViewModel.swift | 14 +- .../View/NotificationSettingsScreen.swift | 2 +- .../SettingsScreenCoordinator.swift | 3 +- .../NotificationSettingsProxy.swift | 27 +-- .../NotificationSettingsProxyProtocol.swift | 3 +- .../RoomNotificationModeProxy.swift | 11 + ...otificationSettingsEditScreenUITests.swift | 21 ++ ...tionSettingsEditScreenViewModelTests.swift | 203 ++++++++++++++++++ ...ficationSettingsScreenViewModelTests.swift | 10 +- .../Sources/RoomDetailsViewModelTests.swift | 2 + 23 files changed, 760 insertions(+), 48 deletions(-) create mode 100644 ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenCoordinator.swift create mode 100644 ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenModels.swift create mode 100644 ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenViewModel.swift create mode 100644 ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenViewModelProtocol.swift create mode 100644 ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreen.swift create mode 100644 UITests/Sources/NotificationSettingsEditScreenUITests.swift create mode 100644 UnitTests/Sources/NotificationSettingsEditScreenViewModelTests.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index b5fb841cd..45f0f9d84 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -69,6 +69,7 @@ 172E6E9A612ADCF10A62CF13 /* BugReportServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */; }; 1772AFA97DDA51CF1B293A78 /* RoomAttachmentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E6A9B9DFEE964962C179DE3 /* RoomAttachmentPicker.swift */; }; 17780569FB41E9BAC60D4710 /* UNUserNotificationCenter+Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E685274772980BDEFF6691E /* UNUserNotificationCenter+Settings.swift */; }; + 1830E5431DB426E2F3660D58 /* NotificationSettingsEditScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46F52419AEEDA2C006CB7181 /* NotificationSettingsEditScreenUITests.swift */; }; 18867F4F1C8991EEC56EA932 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897DF5E9A70CE05A632FC8AF /* UTType.swift */; }; 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */; }; 19FE025AE9BA2959B6589B0D /* RoomMemberDetailsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC575D1895FA62591451A93 /* RoomMemberDetailsScreen.swift */; }; @@ -133,6 +134,7 @@ 2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */; }; 2E8C6672D0EE7D5B1BEDB8E2 /* ServerConfirmationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7478623CECC9438014244BA /* ServerConfirmationScreen.swift */; }; 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086B997409328F091EBA43CE /* RoomScreenUITests.swift */; }; + 2F623DA1122140A987B34D08 /* NotificationSettingsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA7BB497B2F539C17E88F6B7 /* NotificationSettingsEditScreenViewModelProtocol.swift */; }; 2F66701B15657A87B4AC3A0A /* WaitlistScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09CE2B7AD979BDEE09FEDB08 /* WaitlistScreenModels.swift */; }; 2F94054F50E312AF30BE07F3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B21E611DADDEF00307E7AC /* String.swift */; }; 308BD9343B95657FAA583FB7 /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 19CD5B074D7DD44AF4C58BB6 /* SwiftState */; }; @@ -211,6 +213,7 @@ 4E0D9E09B52CEC4C0E6211A8 /* MediaPickerScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F49FB9EE2913234F06CE68 /* MediaPickerScreenCoordinator.swift */; }; 4E8F17EBA24FBBA6ABB62ECB /* MockBackgroundTaskService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3948D16F021DFDB2CD26EAA8 /* MockBackgroundTaskService.swift */; }; 4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */; }; + 4EA1CE0E88EA68E862FF0EA2 /* NotificationSettingsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B564D748B67A156F413CD97 /* NotificationSettingsEditScreenModels.swift */; }; 4FC085B1E5D1EB804495E2F4 /* MockMediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD6E621CC5E6D4830D96D2D /* MockMediaProvider.swift */; }; 4FC1EFE4968A259CBBACFAFB /* RoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */; }; 4FDC8A9764CFDA90CE035725 /* Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB2253D36E81E045E1CB432 /* Duration.swift */; }; @@ -225,6 +228,7 @@ 520EEDAFBC778AB0B41F2F53 /* ClientMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE6170EFE6A161B0A68AB61 /* ClientMock.swift */; }; 5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */; }; 53A55748D5F587C9061F98BF /* ServerConfigurationScreenViewStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */; }; + 53A59720F4729D9BBFFB7CAB /* NotificationSettingsEditScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD9CB3B9DFA353AB2B7CD9F8 /* NotificationSettingsEditScreenCoordinator.swift */; }; 53C1E7F6A7D6409D89F36ED7 /* AggregatedReactionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */; }; 53DEF39F0C4DE02E3FC56D91 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 800631D7250B7F93195035F1 /* KeychainAccess */; }; 53F1196F9C69512306A2693F /* TextRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28C19F54A0C4FC9AB7ABD583 /* TextRoomTimelineItemContent.swift */; }; @@ -500,6 +504,7 @@ A9A5801D5EE3D4D91F6DDADB /* AnalyticsSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C2527813FDAE23E72A9063 /* AnalyticsSettingsScreenViewModelTests.swift */; }; A9D349478F7D4A2B1E40CEF9 /* LegalInformationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8977176AB534AA41630395BC /* LegalInformationScreenViewModelProtocol.swift */; }; AA050DF4AEE54A641BA7CA22 /* RoomSummaryProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */; }; + AA93B3F9B5DD097DEF79F981 /* NotificationSettingsEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB0328F2887BF0A65BC5D49 /* NotificationSettingsEditScreen.swift */; }; AAF0BBED840DF4A53EE85E77 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = C2C69B8BA5A9702E7A8BC08F /* MatrixRustSDK */; }; ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */; }; AC69B6DF15FC451AB2945036 /* UserSessionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEBA759D1347CFFB3D84ED1F /* UserSessionStoreProtocol.swift */; }; @@ -569,6 +574,7 @@ BF675964C9159F718589C36A /* AnalyticsSettingsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16037EE9E9A52AF37B7818E3 /* AnalyticsSettingsScreenUITests.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 */; }; C13128AAA787A4C2CBE4EE82 /* MessageForwardingScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC10CCC8D68B863E20660DBC /* MessageForwardingScreenViewModelProtocol.swift */; }; C1910A16BDF131FECA77BE22 /* EmojiPickerScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */; }; C1A5C386319835FB0C77736B /* ReportContentScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A16CD2C62CB7DB78A4238485 /* ReportContentScreenCoordinator.swift */; }; @@ -638,6 +644,7 @@ D55AF9B5B55FEED04771A461 /* RoomFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */; }; D5C805F49B2C75DC3793E780 /* EmojiItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */; }; D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */; }; + D5FE90A6AF5FD5AE91BD37C7 /* NotificationSettingsEditScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 780258F1B9D15E30549FF4BE /* NotificationSettingsEditScreenViewModel.swift */; }; D63974A88CF2BC721F109C77 /* Compound in Frameworks */ = {isa = PBXBuildFile; productRef = DCA3C4A997AD28E6918D4CE5 /* Compound */; }; D6661A94DBD97658B2ADBD6A /* MapTilerStaticMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A4D29F2683F5772AC72406F /* MapTilerStaticMap.swift */; }; D7CDBAE82782BD0529DECB5F /* AttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BD6ED18E2EB61E28C340AD /* AttributedString.swift */; }; @@ -897,6 +904,7 @@ 1ABDE6F66532CBEB0E016F94 /* RoomProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyMock.swift; sourceTree = ""; }; 1B1EE0908B2BF9212436AD3E /* SessionVerificationScreenStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenStateMachine.swift; sourceTree = ""; }; 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineItem.swift; sourceTree = ""; }; + 1B564D748B67A156F413CD97 /* NotificationSettingsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenModels.swift; sourceTree = ""; }; 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenModels.swift; sourceTree = ""; }; 1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreen.swift; sourceTree = ""; }; 1BC4437C107D52ED19357DFC /* OnboardingViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModelProtocol.swift; sourceTree = ""; }; @@ -1007,6 +1015,7 @@ 46B59EC4B0C93254089EAACB /* MigrationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenViewModelTests.swift; sourceTree = ""; }; 46C208DA43CE25D13E670F40 /* UITestsAppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsAppCoordinator.swift; sourceTree = ""; }; 46D560DDA3B20C82766ACFAD /* NotificationSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenViewModel.swift; sourceTree = ""; }; + 46F52419AEEDA2C006CB7181 /* NotificationSettingsEditScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenUITests.swift; sourceTree = ""; }; 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = ""; }; 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = ""; }; 47873756E45B46683D97DC32 /* LegalInformationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenModels.swift; sourceTree = ""; }; @@ -1144,6 +1153,7 @@ 75697AB5E64A12F1F069F511 /* EncryptedHistoryRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedHistoryRoomTimelineView.swift; sourceTree = ""; }; 75910F5A36EA8FF9BAD08D18 /* MigrationScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenUITests.swift; sourceTree = ""; }; 772334731A8BF8E6D90B194D /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = ""; }; + 780258F1B9D15E30549FF4BE /* NotificationSettingsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModel.swift; sourceTree = ""; }; 78910787F967CBC6042A101E /* StartChatScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModelProtocol.swift; sourceTree = ""; }; 78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitCoordinatorTests.swift; sourceTree = ""; }; 7893780A1FD6E3F38B3E9049 /* UserIndicatorControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerMock.swift; sourceTree = ""; }; @@ -1175,6 +1185,7 @@ 84A87D0471D438A233C2CF4A /* RoomMemberDetailsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenViewModel.swift; sourceTree = ""; }; 84B7A28A6606D58D1E38C55A /* ExpiringTaskRunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpiringTaskRunnerTests.swift; sourceTree = ""; }; 851EF6258DF8B7EF129DC3AC /* WelcomeScreenScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreenScreenViewModelTests.swift; sourceTree = ""; }; + 8544F7058D31DBEB8DBFF0F5 /* NotificationSettingsEditScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModelTests.swift; sourceTree = ""; }; 854BCEAF2A832176FAACD2CB /* SplashScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenCoordinator.swift; sourceTree = ""; }; 85EB16E7FE59A947CA441531 /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = ""; }; 86873A768B13069BB5CAECF6 /* InvitesScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1277,6 +1288,7 @@ ACCC1874C122E2BBE648B8F5 /* LegalInformationScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenUITests.swift; sourceTree = ""; }; AD378D580A41E42560C60E9C /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = ""; }; AD6B522BD637845AB9570B10 /* RoomNotificationSettingsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxy.swift; sourceTree = ""; }; + AD9CB3B9DFA353AB2B7CD9F8 /* NotificationSettingsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenCoordinator.swift; sourceTree = ""; }; ADB35E2DB4EFE8E6F3959629 /* InviteUsersScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenUITests.swift; sourceTree = ""; }; ADD9E0FFA29EAACFF3AB9732 /* SessionVerificationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModel.swift; sourceTree = ""; }; ADE6170EFE6A161B0A68AB61 /* ClientMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientMock.swift; sourceTree = ""; }; @@ -1499,8 +1511,10 @@ F8CEB4634C0DD7779C4AB504 /* CreateRoomScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomScreenUITests.swift; sourceTree = ""; }; F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineView.swift; sourceTree = ""; }; F9ED8E731E21055F728E5FED /* TimelineStartRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStartRoomTimelineView.swift; sourceTree = ""; }; + FA7BB497B2F539C17E88F6B7 /* NotificationSettingsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModelProtocol.swift; sourceTree = ""; }; FB0D6CB491777E7FC6B5BA12 /* CreateRoomScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomScreen.swift; sourceTree = ""; }; FB7BAD55A4E2B8E5828CD64C /* SoftLogoutScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenViewModel.swift; sourceTree = ""; }; + FBB0328F2887BF0A65BC5D49 /* NotificationSettingsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreen.swift; sourceTree = ""; }; FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorProtocol.swift; sourceTree = ""; }; FC2D505742FDA21FCDC4C18A /* AudioRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRoomTimelineView.swift; sourceTree = ""; }; FD1275D9CE0FFBA6E8E85426 /* UserIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorController.swift; sourceTree = ""; }; @@ -2110,6 +2124,18 @@ path = Room; sourceTree = ""; }; + 441115752CA2408F26A72D11 /* NotificationSettingsEditScreen */ = { + isa = PBXGroup; + children = ( + AD9CB3B9DFA353AB2B7CD9F8 /* NotificationSettingsEditScreenCoordinator.swift */, + 1B564D748B67A156F413CD97 /* NotificationSettingsEditScreenModels.swift */, + 780258F1B9D15E30549FF4BE /* NotificationSettingsEditScreenViewModel.swift */, + FA7BB497B2F539C17E88F6B7 /* NotificationSettingsEditScreenViewModelProtocol.swift */, + 8081F6F9AE11BB314A838EC9 /* View */, + ); + path = NotificationSettingsEditScreen; + sourceTree = ""; + }; 448435400B561C40E514BE1C /* FilePreviewScreen */ = { isa = PBXGroup; children = ( @@ -2452,6 +2478,7 @@ 09C599CB430ABF160C1EE55C /* AnalyticsSettingsScreen */, 1CA6CD0DE6F0445156361B6D /* DeveloperOptionsScreen */, 38A1C74493B816B8753F5BC2 /* LegalInformationScreen */, + 441115752CA2408F26A72D11 /* NotificationSettingsEditScreen */, 7B91CB64534AD870924CCFEF /* NotificationSettingsScreen */, B364E08924AD15820350CDD9 /* SettingsScreen */, ); @@ -2525,6 +2552,7 @@ F875D71347DC81EAE7687446 /* NavigationRootCoordinatorTests.swift */, 78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */, 9C698E30698EC59302A8EEBD /* NavigationStackCoordinatorTests.swift */, + 8544F7058D31DBEB8DBFF0F5 /* NotificationSettingsEditScreenViewModelTests.swift */, 514363244AE7D68080D44C6F /* NotificationSettingsScreenViewModelTests.swift */, 00A941F289F6AB876BA3361A /* OnboardingViewModelTests.swift */, 6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */, @@ -2706,6 +2734,14 @@ path = Client; sourceTree = ""; }; + 8081F6F9AE11BB314A838EC9 /* View */ = { + isa = PBXGroup; + children = ( + FBB0328F2887BF0A65BC5D49 /* NotificationSettingsEditScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 823ED0EC3F1B6CF47D284011 /* Tools */ = { isa = PBXGroup; children = ( @@ -2906,6 +2942,7 @@ 39B6C8690AEA1E49FF1BAF95 /* MediaUploadPreviewScreenUITests.swift */, 59846FA04E1DBBFDD8829C2A /* MessageForwardingScreenUITests.swift */, 75910F5A36EA8FF9BAD08D18 /* MigrationScreenUITests.swift */, + 46F52419AEEDA2C006CB7181 /* NotificationSettingsEditScreenUITests.swift */, B83BC0DC9A2DF2DD60F9B6E9 /* NotificationSettingsScreenUITests.swift */, 0C88046D6A070D9827181C4D /* OnboardingUITests.swift */, 4132F882A984ED971338EE9D /* ReportContentScreenUITests.swift */, @@ -4186,6 +4223,7 @@ 69C7B956B74BEC3DB88224EA /* NavigationSplitCoordinatorTests.swift in Sources */, 4BB282209EA82015D0DF8F89 /* NavigationStackCoordinatorTests.swift in Sources */, 1B2DADC008EE211AF1DA5292 /* NotificationManagerTests.swift in Sources */, + C11939FDC40716C4387275A4 /* NotificationSettingsEditScreenViewModelTests.swift in Sources */, E3AC72E3E58F364EF15C1CC7 /* NotificationSettingsScreenViewModelTests.swift in Sources */, F9F6D2883BBEBB9A3789A137 /* OnboardingViewModelTests.swift in Sources */, 27E9263DA75E266690A37EB1 /* PermalinkBuilderTests.swift in Sources */, @@ -4495,6 +4533,11 @@ 652ACCF104A8CEF30788963C /* NotificationManager.swift in Sources */, 06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */, C4C84901ABAC9B17564AB7EB /* NotificationName.swift in Sources */, + AA93B3F9B5DD097DEF79F981 /* NotificationSettingsEditScreen.swift in Sources */, + 53A59720F4729D9BBFFB7CAB /* NotificationSettingsEditScreenCoordinator.swift in Sources */, + 4EA1CE0E88EA68E862FF0EA2 /* NotificationSettingsEditScreenModels.swift in Sources */, + D5FE90A6AF5FD5AE91BD37C7 /* NotificationSettingsEditScreenViewModel.swift in Sources */, + 2F623DA1122140A987B34D08 /* NotificationSettingsEditScreenViewModelProtocol.swift in Sources */, B93FA0DA1504B301CAEE141B /* NotificationSettingsProxy.swift in Sources */, 4BAB8222DBA0B4207D1223E0 /* NotificationSettingsProxyMock.swift in Sources */, A1BA8D6BABAFA9BAAEAA3FFD /* NotificationSettingsProxyProtocol.swift in Sources */, @@ -4775,6 +4818,7 @@ 7FB0BDE26838F1A92782D5E1 /* MediaUploadPreviewScreenUITests.swift in Sources */, 6713835120D94BAA8ED7E3E5 /* MessageForwardingScreenUITests.swift in Sources */, 51C240F4660F7269203A9B3A /* MigrationScreenUITests.swift in Sources */, + 1830E5431DB426E2F3660D58 /* NotificationSettingsEditScreenUITests.swift in Sources */, AF4232E6F08C3DB86FFA9BBD /* NotificationSettingsScreenUITests.swift in Sources */, 6B15FF984906AAFCF9DC4F58 /* OnboardingUITests.swift in Sources */, BA0D3DDCEDD97502DAC4B6E9 /* ReportContentScreenUITests.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index e2cfa3060..2c00c9d7f 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -266,6 +266,9 @@ "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_failed_updating_default_mode" = "An error occurred while updating the notification setting."; +"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"; "screen_notification_settings_group_chats" = "Group chats"; "screen_notification_settings_mentions_section_title" = "Mentions"; diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 6d16c10a5..77d57fc9b 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -640,7 +640,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { private func presentNotificationSettingsScreen(animated: Bool) async { let navigationCoordinator = NavigationStackCoordinator() - let parameters = await NotificationSettingsScreenCoordinatorParameters(userNotificationCenter: UNUserNotificationCenter.current(), + let parameters = await NotificationSettingsScreenCoordinatorParameters(navigationStackCoordinator: navigationCoordinator, + userNotificationCenter: UNUserNotificationCenter.current(), notificationSettings: userSession.clientProxy.notificationSettings(), isModallyPresented: true) let coordinator = NotificationSettingsScreenCoordinator(parameters: parameters) diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 800998529..171c2f55f 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -688,6 +688,12 @@ 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") } + /// 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") } + /// 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 + public static var screenNotificationSettingsEditScreenGroupSectionHeader: String { return L10n.tr("Localizable", "screen_notification_settings_edit_screen_group_section_header") } /// Enable notifications on this device public static var screenNotificationSettingsEnableNotifications: String { return L10n.tr("Localizable", "screen_notification_settings_enable_notifications") } /// Group chats diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 675ac9215..e38036a96 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -365,27 +365,47 @@ class NotificationSettingsProxyMock: NotificationSettingsProxyProtocol { setNotificationModeRoomIdModeReceivedInvocations.append((roomId: roomId, mode: mode)) try await setNotificationModeRoomIdModeClosure?(roomId, mode) } - //MARK: - getDefaultNotificationRoomMode + //MARK: - getDefaultRoomNotificationMode - var getDefaultNotificationRoomModeIsEncryptedIsOneToOneCallsCount = 0 - var getDefaultNotificationRoomModeIsEncryptedIsOneToOneCalled: Bool { - return getDefaultNotificationRoomModeIsEncryptedIsOneToOneCallsCount > 0 + var getDefaultRoomNotificationModeIsEncryptedIsOneToOneCallsCount = 0 + var getDefaultRoomNotificationModeIsEncryptedIsOneToOneCalled: Bool { + return getDefaultRoomNotificationModeIsEncryptedIsOneToOneCallsCount > 0 } - var getDefaultNotificationRoomModeIsEncryptedIsOneToOneReceivedArguments: (isEncrypted: Bool, isOneToOne: Bool)? - var getDefaultNotificationRoomModeIsEncryptedIsOneToOneReceivedInvocations: [(isEncrypted: Bool, isOneToOne: Bool)] = [] - var getDefaultNotificationRoomModeIsEncryptedIsOneToOneReturnValue: RoomNotificationModeProxy! - var getDefaultNotificationRoomModeIsEncryptedIsOneToOneClosure: ((Bool, Bool) async -> RoomNotificationModeProxy)? + var getDefaultRoomNotificationModeIsEncryptedIsOneToOneReceivedArguments: (isEncrypted: Bool, isOneToOne: Bool)? + var getDefaultRoomNotificationModeIsEncryptedIsOneToOneReceivedInvocations: [(isEncrypted: Bool, isOneToOne: Bool)] = [] + var getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue: RoomNotificationModeProxy! + var getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure: ((Bool, Bool) async -> RoomNotificationModeProxy)? - func getDefaultNotificationRoomMode(isEncrypted: Bool, isOneToOne: Bool) async -> RoomNotificationModeProxy { - getDefaultNotificationRoomModeIsEncryptedIsOneToOneCallsCount += 1 - getDefaultNotificationRoomModeIsEncryptedIsOneToOneReceivedArguments = (isEncrypted: isEncrypted, isOneToOne: isOneToOne) - getDefaultNotificationRoomModeIsEncryptedIsOneToOneReceivedInvocations.append((isEncrypted: isEncrypted, isOneToOne: isOneToOne)) - if let getDefaultNotificationRoomModeIsEncryptedIsOneToOneClosure = getDefaultNotificationRoomModeIsEncryptedIsOneToOneClosure { - return await getDefaultNotificationRoomModeIsEncryptedIsOneToOneClosure(isEncrypted, isOneToOne) + func getDefaultRoomNotificationMode(isEncrypted: Bool, isOneToOne: Bool) async -> RoomNotificationModeProxy { + getDefaultRoomNotificationModeIsEncryptedIsOneToOneCallsCount += 1 + getDefaultRoomNotificationModeIsEncryptedIsOneToOneReceivedArguments = (isEncrypted: isEncrypted, isOneToOne: isOneToOne) + getDefaultRoomNotificationModeIsEncryptedIsOneToOneReceivedInvocations.append((isEncrypted: isEncrypted, isOneToOne: isOneToOne)) + if let getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure { + return await getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure(isEncrypted, isOneToOne) } else { - return getDefaultNotificationRoomModeIsEncryptedIsOneToOneReturnValue + return getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue } } + //MARK: - setDefaultRoomNotificationMode + + var setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeThrowableError: Error? + var setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount = 0 + var setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCalled: Bool { + return setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount > 0 + } + var setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedArguments: (isEncrypted: Bool, isOneToOne: Bool, mode: RoomNotificationModeProxy)? + var setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedInvocations: [(isEncrypted: Bool, isOneToOne: Bool, mode: RoomNotificationModeProxy)] = [] + var setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeClosure: ((Bool, Bool, RoomNotificationModeProxy) async throws -> Void)? + + func setDefaultRoomNotificationMode(isEncrypted: Bool, isOneToOne: Bool, mode: RoomNotificationModeProxy) async throws { + if let error = setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeThrowableError { + throw error + } + setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount += 1 + setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedArguments = (isEncrypted: isEncrypted, isOneToOne: isOneToOne, mode: mode) + setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedInvocations.append((isEncrypted: isEncrypted, isOneToOne: isOneToOne, mode: mode)) + try await setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeClosure?(isEncrypted, isOneToOne, mode) + } //MARK: - restoreDefaultNotificationMode var restoreDefaultNotificationModeRoomIdThrowableError: Error? diff --git a/ElementX/Sources/Mocks/NotificationSettingsProxyMock.swift b/ElementX/Sources/Mocks/NotificationSettingsProxyMock.swift index 6ecf96530..1ff727dc4 100644 --- a/ElementX/Sources/Mocks/NotificationSettingsProxyMock.swift +++ b/ElementX/Sources/Mocks/NotificationSettingsProxyMock.swift @@ -35,7 +35,7 @@ extension NotificationSettingsProxyMock { callbacks = configuration.callback getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = configuration.roomMode - getDefaultNotificationRoomModeIsEncryptedIsOneToOneReturnValue = configuration.defaultRoomMode + getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = configuration.defaultRoomMode setNotificationModeRoomIdModeClosure = { [weak self] _, mode in guard let self else { return } @@ -44,6 +44,15 @@ extension NotificationSettingsProxyMock { self.callbacks.send(.settingsDidChange) } } + + setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeClosure = { [weak self] _, _, mode in + guard let self else { return } + self.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = mode + Task { + self.callbacks.send(.settingsDidChange) + } + } + restoreDefaultNotificationModeRoomIdClosure = { [weak self] _ in guard let self else { return } self.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: configuration.defaultRoomMode, isDefault: true)) @@ -51,6 +60,7 @@ extension NotificationSettingsProxyMock { self.callbacks.send(.settingsDidChange) } } + setRoomMentionEnabledEnabledClosure = { [weak self] enabled in guard let self else { return } self.isRoomMentionEnabledReturnValue = enabled @@ -58,6 +68,7 @@ extension NotificationSettingsProxyMock { self.callbacks.send(.settingsDidChange) } } + setCallEnabledEnabledClosure = { [weak self] enabled in guard let self else { return } self.isCallEnabledReturnValue = enabled diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenCoordinator.swift new file mode 100644 index 000000000..5985e1623 --- /dev/null +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenCoordinator.swift @@ -0,0 +1,51 @@ +// +// Copyright 2022 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 Combine +import SwiftUI + +struct NotificationSettingsEditScreenCoordinatorParameters { + let isDirect: Bool + let notificationSettings: NotificationSettingsProxyProtocol +} + +enum NotificationSettingsEditScreenCoordinatorAction { } + +final class NotificationSettingsEditScreenCoordinator: CoordinatorProtocol { + private let parameters: NotificationSettingsEditScreenCoordinatorParameters + private var viewModel: NotificationSettingsEditScreenViewModelProtocol + private let actionsSubject: PassthroughSubject = .init() + private var cancellables: Set = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: NotificationSettingsEditScreenCoordinatorParameters) { + self.parameters = parameters + + viewModel = NotificationSettingsEditScreenViewModel(isDirect: parameters.isDirect, + notificationSettingsProxy: parameters.notificationSettings) + } + + func start() { + viewModel.fetchInitialContent() + } + + func toPresentable() -> AnyView { + AnyView(NotificationSettingsEditScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenModels.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenModels.swift new file mode 100644 index 000000000..cdf5d8c9f --- /dev/null +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenModels.swift @@ -0,0 +1,73 @@ +// +// Copyright 2022 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 NotificationSettingsEditScreenViewModelAction { } + +enum NotificationSettingsEditScreenDefaultMode { + case allMessages + case mentionsAndKeywordsOnly +} + +struct NotificationSettingsEditScreenViewState: BindableState { + var bindings: NotificationSettingsEditScreenViewStateBindings + var strings: NotificationSettingsEditScreenStrings + var isDirect: Bool + var availableDefaultModes: [NotificationSettingsEditScreenDefaultMode] = [.allMessages, .mentionsAndKeywordsOnly] + var defaultMode: NotificationSettingsEditScreenDefaultMode? + var pendingMode: NotificationSettingsEditScreenDefaultMode? + + func isSelected(mode: NotificationSettingsEditScreenDefaultMode) -> Bool { + pendingMode == nil && defaultMode == mode + } +} + +struct NotificationSettingsEditScreenViewStateBindings { + var alertInfo: AlertInfo? +} + +enum NotificationSettingsEditScreenViewAction { + case setMode(NotificationSettingsEditScreenDefaultMode) +} + +enum NotificationSettingsEditScreenErrorType: Hashable { + case setModeFailed +} + +struct NotificationSettingsEditScreenStrings { + let navigationTitle: String + let modeSectionTitle: String + + init(isDirect: Bool) { + if isDirect { + navigationTitle = L10n.screenNotificationSettingsDirectChats + modeSectionTitle = L10n.screenNotificationSettingsEditScreenDirectSectionHeader + } else { + navigationTitle = L10n.screenNotificationSettingsGroupChats + modeSectionTitle = L10n.screenNotificationSettingsEditScreenGroupSectionHeader + } + } + + func string(for mode: NotificationSettingsEditScreenDefaultMode) -> String { + switch mode { + case .allMessages: + return L10n.screenNotificationSettingsModeAll + case .mentionsAndKeywordsOnly: + return L10n.screenNotificationSettingsModeMentions + } + } +} diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenViewModel.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenViewModel.swift new file mode 100644 index 000000000..5a8eea633 --- /dev/null +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenViewModel.swift @@ -0,0 +1,120 @@ +// +// Copyright 2022 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 Combine +import SwiftUI + +typealias NotificationSettingsEditScreenViewModelType = StateStoreViewModel + +class NotificationSettingsEditScreenViewModel: NotificationSettingsEditScreenViewModelType, NotificationSettingsEditScreenViewModelProtocol { + private var actionsSubject: PassthroughSubject = .init() + private let isDirect: Bool + private let notificationSettingsProxy: NotificationSettingsProxyProtocol + @CancellableTask private var fetchSettingsTask: Task? + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(isDirect: Bool, notificationSettingsProxy: NotificationSettingsProxyProtocol) { + let bindings = NotificationSettingsEditScreenViewStateBindings() + self.isDirect = isDirect + self.notificationSettingsProxy = notificationSettingsProxy + super.init(initialViewState: NotificationSettingsEditScreenViewState(bindings: bindings, + strings: NotificationSettingsEditScreenStrings(isDirect: isDirect), + isDirect: isDirect)) + + setupNotificationSettingsSubscription() + } + + func fetchInitialContent() { + fetchSettings() + } + + // MARK: - Public + + override func process(viewAction: NotificationSettingsEditScreenViewAction) { + switch viewAction { + case .setMode(let mode): + setMode(mode) + } + } + + // MARK: - Private + + private func setupNotificationSettingsSubscription() { + notificationSettingsProxy.callbacks + .receive(on: DispatchQueue.main) + .sink { [weak self] callback in + guard let self else { return } + + switch callback { + case .settingsDidChange: + self.fetchSettings() + } + } + .store(in: &cancellables) + } + + private func fetchSettings() { + fetchSettingsTask = Task { + var mode: RoomNotificationModeProxy? + let encrypted_mode = await notificationSettingsProxy.getDefaultRoomNotificationMode(isEncrypted: true, isOneToOne: isDirect) + let unencrypted_mode = await notificationSettingsProxy.getDefaultRoomNotificationMode(isEncrypted: false, isOneToOne: isDirect) + if encrypted_mode == unencrypted_mode { + mode = encrypted_mode + } + guard !Task.isCancelled else { return } + + switch mode { + case .allMessages: + state.defaultMode = .allMessages + case .mentionsAndKeywordsOnly: + state.defaultMode = .mentionsAndKeywordsOnly + default: + state.defaultMode = nil + } + } + } + + private func setMode(_ mode: NotificationSettingsEditScreenDefaultMode) { + guard state.pendingMode == nil, !state.isSelected(mode: mode) else { return } + let roomNotificationModeProxy: RoomNotificationModeProxy + switch mode { + case .allMessages: + roomNotificationModeProxy = .allMessages + case .mentionsAndKeywordsOnly: + roomNotificationModeProxy = .mentionsAndKeywordsOnly + } + 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) + } catch { + let retryAction: () -> Void = { [weak self] in + self?.setMode(mode) + } + state.bindings.alertInfo = AlertInfo(id: .setModeFailed, + title: L10n.commonError, + message: L10n.screenNotificationSettingsEditFailedUpdatingDefaultMode, + primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil), + secondaryButton: .init(title: L10n.actionRetry, action: retryAction)) + } + state.pendingMode = nil + } + } +} diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenViewModelProtocol.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenViewModelProtocol.swift new file mode 100644 index 000000000..60f46b806 --- /dev/null +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenViewModelProtocol.swift @@ -0,0 +1,25 @@ +// +// Copyright 2022 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 Combine + +@MainActor +protocol NotificationSettingsEditScreenViewModelProtocol { + var actions: AnyPublisher { get } + var context: NotificationSettingsEditScreenViewModelType.Context { get } + + func fetchInitialContent() +} diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreen.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreen.swift new file mode 100644 index 000000000..9f73b0804 --- /dev/null +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreen.swift @@ -0,0 +1,100 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct NotificationSettingsEditScreen: View { + @ObservedObject var context: NotificationSettingsEditScreenViewModel.Context + + var body: some View { + Form { + notificationModeSection + } + .compoundForm() + .navigationTitle(context.viewState.strings.navigationTitle) + .alert(item: $context.alertInfo) + .track(screen: .settingsDefaultNotifications) + } + + // MARK: - Private + + 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) + } + } header: { + Text(context.viewState.strings.modeSectionTitle) + .compoundFormSectionHeader() + } + .compoundFormSection() + } +} + +// MARK: - Previews + +struct NotificationSettingsEditScreen_Previews: PreviewProvider { + static let viewModelGroupChats: NotificationSettingsEditScreenViewModel = { + let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init()) + notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .allMessages + var viewModel = NotificationSettingsEditScreenViewModel(isDirect: false, + notificationSettingsProxy: notificationSettingsProxy) + viewModel.fetchInitialContent() + return viewModel + }() + + static let viewModelDirectChats: NotificationSettingsEditScreenViewModel = { + let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init()) + notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly + var viewModel = NotificationSettingsEditScreenViewModel(isDirect: true, + notificationSettingsProxy: notificationSettingsProxy) + viewModel.fetchInitialContent() + return viewModel + }() + + static let viewModelDirectApplyingChange: NotificationSettingsEditScreenViewModel = { + let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init()) + notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly + var viewModel = NotificationSettingsEditScreenViewModel(isDirect: true, + notificationSettingsProxy: notificationSettingsProxy) + viewModel.state.pendingMode = .mentionsAndKeywordsOnly + viewModel.fetchInitialContent() + return viewModel + }() + + static var previews: some View { + NotificationSettingsEditScreen(context: viewModelGroupChats.context) + .previewDisplayName("Group Chats") + NotificationSettingsEditScreen(context: viewModelDirectChats.context) + .previewDisplayName("Direct Chats") + NotificationSettingsEditScreen(context: viewModelDirectApplyingChange.context) + .previewDisplayName("Applying change") + } +} diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift index c24ab7ca7..8ba32b283 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift @@ -18,6 +18,7 @@ import Combine import SwiftUI struct NotificationSettingsScreenCoordinatorParameters { + weak var navigationStackCoordinator: NavigationStackCoordinator? let userNotificationCenter: UserNotificationCenterProtocol let notificationSettings: NotificationSettingsProxyProtocol let isModallyPresented: Bool @@ -32,7 +33,11 @@ final class NotificationSettingsScreenCoordinator: CoordinatorProtocol { private var viewModel: NotificationSettingsScreenViewModelProtocol private let actionsSubject: PassthroughSubject = .init() private var cancellables: Set = .init() - + + private var navigationStackCoordinator: NavigationStackCoordinator? { + parameters.navigationStackCoordinator + } + var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } @@ -50,15 +55,27 @@ final class NotificationSettingsScreenCoordinator: CoordinatorProtocol { viewModel.fetchInitialContent() viewModel.actions.sink { [weak self] action in + guard let self else { return } switch action { case .close: - self?.actionsSubject.send(.close) + self.actionsSubject.send(.close) + case .editDefaultMode(let isDirect): + self.presentEditScreen(isDirect: isDirect) } } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(NotificationSettingsScreen(context: viewModel.context)) } + + // MARK: - Private + + private func presentEditScreen(isDirect: Bool) { + let editSettingsParameters = NotificationSettingsEditScreenCoordinatorParameters(isDirect: isDirect, + 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 3d760cfe6..4b85bf0d1 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift @@ -19,6 +19,7 @@ import UIKit enum NotificationSettingsScreenViewModelAction { case close + case editDefaultMode(isDirect: Bool) } struct NotificationSettingsScreenViewState: BindableState { diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift index 219210f41..100f513a2 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift @@ -60,9 +60,9 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy case .changedEnableNotifications: toggleNotifications() case .groupChatsTapped: - break + actionsSubject.send(.editDefaultMode(isDirect: false)) case .directChatsTapped: - break + actionsSubject.send(.editDefaultMode(isDirect: true)) case .roomMentionChanged: guard let settings = state.settings, settings.roomMentionsEnabled != state.bindings.roomMentionsEnabled else { return @@ -120,14 +120,12 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy private func fetchSettings() { fetchSettingsTask = Task { // Group chats - // A group chat is a chat having more than 2 active members - var groupChatsMode = await notificationSettingsProxy.getDefaultNotificationRoomMode(isEncrypted: false, isOneToOne: false) - let encryptedGroupChatsMode = await notificationSettingsProxy.getDefaultNotificationRoomMode(isEncrypted: true, isOneToOne: false) + var groupChatsMode = await notificationSettingsProxy.getDefaultRoomNotificationMode(isEncrypted: false, isOneToOne: false) + let encryptedGroupChatsMode = await notificationSettingsProxy.getDefaultRoomNotificationMode(isEncrypted: true, isOneToOne: false) // Direct chats - // A direct chat is a chat having exactly 2 active members - var directChatsMode = await notificationSettingsProxy.getDefaultNotificationRoomMode(isEncrypted: false, isOneToOne: true) - let encryptedDirectChatsMode = await notificationSettingsProxy.getDefaultNotificationRoomMode(isEncrypted: true, isOneToOne: true) + var directChatsMode = await notificationSettingsProxy.getDefaultRoomNotificationMode(isEncrypted: false, isOneToOne: true) + let encryptedDirectChatsMode = await notificationSettingsProxy.getDefaultRoomNotificationMode(isEncrypted: true, isOneToOne: true) // Old clients were having specific settings for encrypted and unencrypted rooms, // so it's possible for `group chats` and `direct chats` settings to be inconsistent (e.g. encrypted `direct chats` can have a different mode that unencrypted `direct chats`) diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift index 10bcee8a7..bc0ccf84a 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift @@ -183,7 +183,7 @@ struct NotificationSettingsScreen_Previews: PreviewProvider { let notificationCenter = UserNotificationCenterMock() notificationCenter.authorizationStatusReturnValue = .notDetermined let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init()) - notificationSettingsProxy.getDefaultNotificationRoomModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in + notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in switch (isEncrypted, isOneToOne) { case (_, true): return .allMessages diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift index df31c5bd2..d740b66d0 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift @@ -141,7 +141,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { } private func presentNotificationSettings() { - let notificationParameters = NotificationSettingsScreenCoordinatorParameters(userNotificationCenter: UNUserNotificationCenter.current(), + let notificationParameters = NotificationSettingsScreenCoordinatorParameters(navigationStackCoordinator: parameters.navigationStackCoordinator, + userNotificationCenter: UNUserNotificationCenter.current(), notificationSettings: parameters.notificationSettings, isModallyPresented: false) let coordinator = NotificationSettingsScreenCoordinator(parameters: notificationParameters) diff --git a/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxy.swift b/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxy.swift index db6aabf2c..dbe2e1e69 100644 --- a/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxy.swift +++ b/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxy.swift @@ -55,23 +55,22 @@ final class NotificationSettingsProxy: NotificationSettingsProxyProtocol { let backgroundTask = await backgroundTaskService?.startBackgroundTask(withName: "setNotificationMode") defer { backgroundTask?.stop() } - let roomNotificationMode: RoomNotificationMode - switch mode { - case .allMessages: - roomNotificationMode = .allMessages - case .mentionsAndKeywordsOnly: - roomNotificationMode = .mentionsAndKeywordsOnly - case .mute: - roomNotificationMode = .mute - } - try await notificationSettings.setRoomNotificationMode(roomId: roomId, mode: roomNotificationMode) + try await notificationSettings.setRoomNotificationMode(roomId: roomId, mode: mode.roomNotificationMode) await updatedSettings() } - func getDefaultNotificationRoomMode(isEncrypted: Bool, isOneToOne: Bool) async -> RoomNotificationModeProxy { + func getDefaultRoomNotificationMode(isEncrypted: Bool, isOneToOne: Bool) async -> RoomNotificationModeProxy { let roomNotificationMode = await notificationSettings.getDefaultRoomNotificationMode(isEncrypted: isEncrypted, isOneToOne: isOneToOne) return RoomNotificationModeProxy.from(roomNotificationMode: roomNotificationMode) } + + func setDefaultRoomNotificationMode(isEncrypted: Bool, isOneToOne: Bool, mode: RoomNotificationModeProxy) async throws { + let backgroundTask = await backgroundTaskService?.startBackgroundTask(withName: "setDefaultRoomNotificationMode") + defer { backgroundTask?.stop() } + + try await notificationSettings.setDefaultRoomNotificationMode(isEncrypted: isEncrypted, isOneToOne: isOneToOne, mode: mode.roomNotificationMode) + await updatedSettings() + } func restoreDefaultNotificationMode(roomId: String) async throws { let backgroundTask = await backgroundTaskService?.startBackgroundTask(withName: "restoreDefaultNotificationMode") @@ -132,7 +131,11 @@ final class NotificationSettingsProxy: NotificationSettingsProxyProtocol { // MARK: - Private func updatedSettings() async { - _ = await callbacks.values.first(where: { $0 == .settingsDidChange }) + // The timeout avoids having to wait indefinitely. This can happen when setting a mode that is already the current mode, + // as in this case no API call is made by the RustSDK and the push rules are therefore not updated. + _ = await callbacks + .timeout(.seconds(2.0), scheduler: DispatchQueue.main, options: nil, customError: nil) + .values.first(where: { $0 == .settingsDidChange }) } @MainActor diff --git a/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxyProtocol.swift b/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxyProtocol.swift index 8cf24dbe1..9e8b00d71 100644 --- a/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxyProtocol.swift +++ b/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxyProtocol.swift @@ -28,7 +28,8 @@ protocol NotificationSettingsProxyProtocol { func getNotificationSettings(roomId: String, isEncrypted: Bool, isOneToOne: Bool) async throws -> RoomNotificationSettingsProxyProtocol func setNotificationMode(roomId: String, mode: RoomNotificationModeProxy) async throws - func getDefaultNotificationRoomMode(isEncrypted: Bool, isOneToOne: Bool) async -> 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 func containsKeywordsRules() async -> Bool func unmuteRoom(roomId: String, isEncrypted: Bool, isOneToOne: Bool) async throws diff --git a/ElementX/Sources/Services/NotificationSettings/RoomNotificationModeProxy.swift b/ElementX/Sources/Services/NotificationSettings/RoomNotificationModeProxy.swift index 6abe1b264..d70c94844 100644 --- a/ElementX/Sources/Services/NotificationSettings/RoomNotificationModeProxy.swift +++ b/ElementX/Sources/Services/NotificationSettings/RoomNotificationModeProxy.swift @@ -34,4 +34,15 @@ extension RoomNotificationModeProxy { return .mute } } + + var roomNotificationMode: RoomNotificationMode { + switch self { + case .allMessages: + return .allMessages + case .mentionsAndKeywordsOnly: + return .mentionsAndKeywordsOnly + case .mute: + return .mute + } + } } diff --git a/UITests/Sources/NotificationSettingsEditScreenUITests.swift b/UITests/Sources/NotificationSettingsEditScreenUITests.swift new file mode 100644 index 000000000..0ecf6aafd --- /dev/null +++ b/UITests/Sources/NotificationSettingsEditScreenUITests.swift @@ -0,0 +1,21 @@ +// +// Copyright 2022 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 ElementX +import XCTest + +@MainActor +class NotificationSettingsEditScreenUITests: XCTestCase { } diff --git a/UnitTests/Sources/NotificationSettingsEditScreenViewModelTests.swift b/UnitTests/Sources/NotificationSettingsEditScreenViewModelTests.swift new file mode 100644 index 000000000..f0e170728 --- /dev/null +++ b/UnitTests/Sources/NotificationSettingsEditScreenViewModelTests.swift @@ -0,0 +1,203 @@ +// +// Copyright 2022 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 MatrixRustSDK +import XCTest + +@testable import ElementX + +@MainActor +class NotificationSettingsEditScreenViewModelTests: XCTestCase { + private var viewModel: NotificationSettingsEditScreenViewModelProtocol! + private var notificationSettingsProxy: NotificationSettingsProxyMock! + + private var context: NotificationSettingsEditScreenViewModelType.Context { + viewModel.context + } + + @MainActor override func setUpWithError() throws { + notificationSettingsProxy = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()) + notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .allMessages + } + + func testFetchSettings() async throws { + notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in + switch (isEncrypted, isOneToOne) { + case (_, true): + return .allMessages + case (_, _): + return .mentionsAndKeywordsOnly + } + } + viewModel = NotificationSettingsEditScreenViewModel(isDirect: false, + notificationSettingsProxy: notificationSettingsProxy) + + let deferred = deferFulfillment(viewModel.context.$viewState.map(\.defaultMode) + .first(where: { !$0.isNil })) + viewModel.fetchInitialContent() + try await deferred.fulfill() + + // `getDefaultRoomNotificationModeIsEncryptedIsOneToOne` must have been called twice (for encrypted and unencrypted group chats) + let invocations = notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReceivedInvocations + + XCTAssertEqual(invocations.count, 2) + // First call for encrypted group chats + XCTAssertEqual(invocations[0].isEncrypted, true) + XCTAssertEqual(invocations[0].isOneToOne, false) + // Second call for unencrypted group chats + XCTAssertEqual(invocations[1].isEncrypted, false) + XCTAssertEqual(invocations[1].isOneToOne, false) + + XCTAssertEqual(context.viewState.defaultMode, .mentionsAndKeywordsOnly) + XCTAssertNil(context.viewState.bindings.alertInfo) + } + + func testSetModeAllMessages() async throws { + notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly + viewModel = NotificationSettingsEditScreenViewModel(isDirect: false, + notificationSettingsProxy: notificationSettingsProxy) + let deferred = deferFulfillment(viewModel.context.$viewState.map(\.defaultMode) + .first(where: { !$0.isNil })) + viewModel.fetchInitialContent() + try await deferred.fulfill() + + // Set mode to .allMessages + let deferredViewState = deferFulfillment(context.$viewState + .map(\.pendingMode) + .removeDuplicates() + .collect(3).first()) + context.send(viewAction: .setMode(.allMessages)) + let pendingModes = try await deferredViewState.fulfill() + + XCTAssertEqual(pendingModes, [nil, .allMessages, nil]) + + // `setDefaultRoomNotificationModeIsEncryptedIsOneToOneMode` must have been called twice (for encrypted and unencrypted group chats) + let invocations = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedInvocations + XCTAssertEqual(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount, 2) + // First call for encrypted group chats + XCTAssertEqual(invocations[0].isEncrypted, true) + XCTAssertEqual(invocations[0].isOneToOne, false) + XCTAssertEqual(invocations[0].mode, .allMessages) + // Second call for unencrypted group chats + XCTAssertEqual(invocations[1].isEncrypted, false) + XCTAssertEqual(invocations[1].isOneToOne, false) + XCTAssertEqual(invocations[1].mode, .allMessages) + + // The default mode should be updated + let deferredNewViewState = deferFulfillment(context.$viewState + .map(\.defaultMode) + .first(where: { $0 == .allMessages })) + try await deferredNewViewState.fulfill() + + XCTAssertEqual(context.viewState.defaultMode, .allMessages) + XCTAssertNil(context.viewState.bindings.alertInfo) + } + + func testSetModeMentions() async throws { + viewModel = NotificationSettingsEditScreenViewModel(isDirect: false, + notificationSettingsProxy: notificationSettingsProxy) + let deferred = deferFulfillment(viewModel.context.$viewState.map(\.defaultMode) + .first(where: { !$0.isNil })) + viewModel.fetchInitialContent() + try await deferred.fulfill() + + // Set mode to .allMessages + let deferredViewState = deferFulfillment(context.$viewState + .map(\.pendingMode) + .removeDuplicates() + .collect(3).first()) + context.send(viewAction: .setMode(.mentionsAndKeywordsOnly)) + let pendingModes = try await deferredViewState.fulfill() + + XCTAssertEqual(pendingModes, [nil, .mentionsAndKeywordsOnly, nil]) + + // `setDefaultRoomNotificationModeIsEncryptedIsOneToOneMode` must have been called twice (for encrypted and unencrypted group chats) + let invocations = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedInvocations + XCTAssertEqual(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount, 2) + // First call for encrypted group chats + XCTAssertEqual(invocations[0].isEncrypted, true) + XCTAssertEqual(invocations[0].isOneToOne, false) + XCTAssertEqual(invocations[0].mode, .mentionsAndKeywordsOnly) + // Second call for unencrypted group chats + XCTAssertEqual(invocations[1].isEncrypted, false) + XCTAssertEqual(invocations[1].isOneToOne, false) + XCTAssertEqual(invocations[1].mode, .mentionsAndKeywordsOnly) + + // The default mode should be updated + let deferredNewViewState = deferFulfillment(context.$viewState + .map(\.defaultMode) + .first(where: { $0 == .mentionsAndKeywordsOnly })) + try await deferredNewViewState.fulfill() + + XCTAssertEqual(context.viewState.defaultMode, .mentionsAndKeywordsOnly) + XCTAssertNil(context.viewState.bindings.alertInfo) + } + + func testSetModeDirectChats() async throws { + notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly + // Initialize for direct chats + viewModel = NotificationSettingsEditScreenViewModel(isDirect: true, + notificationSettingsProxy: notificationSettingsProxy) + let deferred = deferFulfillment(viewModel.context.$viewState.map(\.defaultMode) + .first(where: { !$0.isNil })) + viewModel.fetchInitialContent() + try await deferred.fulfill() + + // Set mode to .allMessages + let deferredViewState = deferFulfillment(context.$viewState + .map(\.pendingMode) + .removeDuplicates() + .collect(3).first()) + context.send(viewAction: .setMode(.allMessages)) + let pendingModes = try await deferredViewState.fulfill() + + XCTAssertEqual(pendingModes, [nil, .allMessages, nil]) + + // `setDefaultRoomNotificationModeIsEncryptedIsOneToOneMode` must have been called twice (for encrypted and unencrypted direct chats) + let invocations = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedInvocations + XCTAssertEqual(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount, 2) + // First call for encrypted direct chats + XCTAssertEqual(invocations[0].isEncrypted, true) + XCTAssertEqual(invocations[0].isOneToOne, true) + XCTAssertEqual(invocations[0].mode, .allMessages) + // Second call for unencrypted direct chats + XCTAssertEqual(invocations[1].isEncrypted, false) + XCTAssertEqual(invocations[1].isOneToOne, true) + XCTAssertEqual(invocations[1].mode, .allMessages) + } + + func testSetModeFailure() async throws { + notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly + notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeThrowableError = NotificationSettingsError.Generic(message: "error") + viewModel = NotificationSettingsEditScreenViewModel(isDirect: true, + notificationSettingsProxy: notificationSettingsProxy) + let deferred = deferFulfillment(viewModel.context.$viewState.map(\.defaultMode) + .first(where: { !$0.isNil })) + viewModel.fetchInitialContent() + try await deferred.fulfill() + + // Set mode to .allMessages + let deferredViewState = deferFulfillment(context.$viewState + .map(\.pendingMode) + .removeDuplicates() + .collect(3).first()) + context.send(viewAction: .setMode(.allMessages)) + let pendingModes = try await deferredViewState.fulfill() + + XCTAssertEqual(pendingModes, [nil, .allMessages, nil]) + XCTAssertNotNil(context.viewState.bindings.alertInfo) + } +} diff --git a/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift b/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift index 534d4b52e..658a0b2c1 100644 --- a/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift +++ b/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift @@ -34,7 +34,7 @@ class NotificationSettingsScreenViewModelTests: XCTestCase { userNotificationCenter.authorizationStatusReturnValue = .authorized appSettings = AppSettings() notificationSettingsProxy = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()) - notificationSettingsProxy.getDefaultNotificationRoomModeIsEncryptedIsOneToOneReturnValue = .allMessages + notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .allMessages notificationSettingsProxy.isRoomMentionEnabledReturnValue = true notificationSettingsProxy.isCallEnabledReturnValue = true @@ -58,7 +58,7 @@ class NotificationSettingsScreenViewModelTests: XCTestCase { } func testFetchSettings() async throws { - notificationSettingsProxy.getDefaultNotificationRoomModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in + notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in switch (isEncrypted, isOneToOne) { case (_, true): return .allMessages @@ -71,7 +71,7 @@ class NotificationSettingsScreenViewModelTests: XCTestCase { notificationSettingsProxy.callbacks.send(.settingsDidChange) try await deferred.fulfill() - XCTAssertEqual(notificationSettingsProxy.getDefaultNotificationRoomModeIsEncryptedIsOneToOneCallsCount, 4) + XCTAssertEqual(notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneCallsCount, 4) XCTAssert(notificationSettingsProxy.isRoomMentionEnabledCalled) XCTAssert(notificationSettingsProxy.isCallEnabledCalled) @@ -82,7 +82,7 @@ class NotificationSettingsScreenViewModelTests: XCTestCase { } func testInconsistentGroupChatsSettings() async throws { - notificationSettingsProxy.getDefaultNotificationRoomModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in + notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in switch (isEncrypted, isOneToOne) { case (true, false): return .allMessages @@ -103,7 +103,7 @@ class NotificationSettingsScreenViewModelTests: XCTestCase { } func testInconsistentDirectChatsSettings() async throws { - notificationSettingsProxy.getDefaultNotificationRoomModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in + notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in switch (isEncrypted, isOneToOne) { case (true, true): return .allMessages diff --git a/UnitTests/Sources/RoomDetailsViewModelTests.swift b/UnitTests/Sources/RoomDetailsViewModelTests.swift index afd91f4cb..7c42b7817 100644 --- a/UnitTests/Sources/RoomDetailsViewModelTests.swift +++ b/UnitTests/Sources/RoomDetailsViewModelTests.swift @@ -426,6 +426,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase { notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() + _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first() + XCTAssertEqual(context.viewState.notificationShortcutButtonTitle, L10n.commonUnmute) XCTAssertEqual(context.viewState.notificationShortcutButtonImage, Image(systemName: "bell.slash.fill")) }