diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 0f5da4015..b8c927cdd 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -1710,6 +1710,47 @@ class NotificationSettingsProxyMock: NotificationSettingsProxyProtocol { setCallEnabledEnabledReceivedInvocations.append(enabled) try await setCallEnabledEnabledClosure?(enabled) } + //MARK: - isInviteForMeEnabled + + var isInviteForMeEnabledThrowableError: Error? + var isInviteForMeEnabledCallsCount = 0 + var isInviteForMeEnabledCalled: Bool { + return isInviteForMeEnabledCallsCount > 0 + } + var isInviteForMeEnabledReturnValue: Bool! + var isInviteForMeEnabledClosure: (() async throws -> Bool)? + + func isInviteForMeEnabled() async throws -> Bool { + if let error = isInviteForMeEnabledThrowableError { + throw error + } + isInviteForMeEnabledCallsCount += 1 + if let isInviteForMeEnabledClosure = isInviteForMeEnabledClosure { + return try await isInviteForMeEnabledClosure() + } else { + return isInviteForMeEnabledReturnValue + } + } + //MARK: - setInviteForMeEnabled + + var setInviteForMeEnabledEnabledThrowableError: Error? + var setInviteForMeEnabledEnabledCallsCount = 0 + var setInviteForMeEnabledEnabledCalled: Bool { + return setInviteForMeEnabledEnabledCallsCount > 0 + } + var setInviteForMeEnabledEnabledReceivedEnabled: Bool? + var setInviteForMeEnabledEnabledReceivedInvocations: [Bool] = [] + var setInviteForMeEnabledEnabledClosure: ((Bool) async throws -> Void)? + + func setInviteForMeEnabled(enabled: Bool) async throws { + if let error = setInviteForMeEnabledEnabledThrowableError { + throw error + } + setInviteForMeEnabledEnabledCallsCount += 1 + setInviteForMeEnabledEnabledReceivedEnabled = enabled + setInviteForMeEnabledEnabledReceivedInvocations.append(enabled) + try await setInviteForMeEnabledEnabledClosure?(enabled) + } //MARK: - getRoomsWithUserDefinedRules var getRoomsWithUserDefinedRulesThrowableError: Error? diff --git a/ElementX/Sources/Mocks/NotificationSettingsProxyMock.swift b/ElementX/Sources/Mocks/NotificationSettingsProxyMock.swift index 459ac8f81..a36c480b5 100644 --- a/ElementX/Sources/Mocks/NotificationSettingsProxyMock.swift +++ b/ElementX/Sources/Mocks/NotificationSettingsProxyMock.swift @@ -42,6 +42,7 @@ extension NotificationSettingsProxyMock { getRoomsWithUserDefinedRulesReturnValue = [] isRoomMentionEnabledReturnValue = true isCallEnabledReturnValue = true + isInviteForMeEnabledReturnValue = true setNotificationModeRoomIdModeClosure = { [weak self] _, mode in guard let self else { return } @@ -83,6 +84,14 @@ extension NotificationSettingsProxyMock { } } + setInviteForMeEnabledEnabledClosure = { [weak self] enabled in + guard let self else { return } + self.isInviteForMeEnabledReturnValue = enabled + Task { + self.callbacks.send(.settingsDidChange) + } + } + canHomeserverPushEncryptedEventsToDeviceClosure = { configuration.canHomeserverPushEncryptedEvents } diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift index 5eea116ab..7000ded5b 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift @@ -44,6 +44,7 @@ struct NotificationSettingsScreenViewStateBindings { var enableNotifications = false var roomMentionsEnabled = false var callsEnabled = false + var invitationsEnabled = false var alertInfo: AlertInfo? } @@ -52,6 +53,7 @@ struct NotificationSettingsScreenSettings { let directChatsMode: RoomNotificationModeProxy let roomMentionsEnabled: Bool? let callsEnabled: Bool? + let invitationsEnabled: Bool? // 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`) let inconsistentSettings: [NotificationSettingsScreenInvalidSetting] @@ -93,6 +95,7 @@ enum NotificationSettingsScreenViewAction { case directChatsTapped case roomMentionChanged case callsChanged + case invitationsChanged case close case fixConfigurationMismatchTapped } diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift index a06af98a5..29a0d63e9 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift @@ -73,6 +73,11 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy return } Task { await enableCalls(state.bindings.callsEnabled) } + case .invitationsChanged: + guard let settings = state.settings, settings.invitationsEnabled != state.bindings.invitationsEnabled else { + return + } + Task { await enableInvitations(state.bindings.invitationsEnabled) } case .close: actionsSubject.send(.close) case .fixConfigurationMismatchTapped: @@ -146,6 +151,7 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy // The following calls may fail if the associated push rule doesn't exist let roomMentionsEnabled = try? await notificationSettingsProxy.isRoomMentionEnabled() let callEnabled = try? await notificationSettingsProxy.isCallEnabled() + let invitationsEnabled = try? await notificationSettingsProxy.isInviteForMeEnabled() guard !Task.isCancelled else { return } @@ -153,11 +159,13 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy directChatsMode: directChatsMode, roomMentionsEnabled: roomMentionsEnabled, callsEnabled: callEnabled, + invitationsEnabled: invitationsEnabled, inconsistentSettings: inconsistentSettings) state.settings = notificationSettings state.bindings.roomMentionsEnabled = notificationSettings.roomMentionsEnabled ?? false state.bindings.callsEnabled = notificationSettings.callsEnabled ?? false + state.bindings.invitationsEnabled = notificationSettings.invitationsEnabled ?? false } } @@ -187,7 +195,7 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy guard let notificationSettings = state.settings else { return } do { state.applyingChange = true - MXLog.info("[NotificationSettingsScreenViewMode] setRoomMentionEnabled(\(enable))") + MXLog.info("setRoomMentionEnabled(\(enable))") try await notificationSettingsProxy.setRoomMentionEnabled(enabled: enable) } catch { state.bindings.alertInfo = AlertInfo(id: .alert) @@ -200,7 +208,7 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy guard let notificationSettings = state.settings else { return } do { state.applyingChange = true - MXLog.info("[NotificationSettingsScreenViewMode] setCallEnabled(\(enable))") + MXLog.info("setCallEnabled(\(enable))") try await notificationSettingsProxy.setCallEnabled(enabled: enable) } catch { state.bindings.alertInfo = AlertInfo(id: .alert) @@ -208,6 +216,19 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy } state.applyingChange = false } + + func enableInvitations(_ enable: Bool) async { + guard let notificationSettings = state.settings else { return } + do { + state.applyingChange = true + MXLog.info("setInviteForMeEnabled(\(enable))") + try await notificationSettingsProxy.setInviteForMeEnabled(enabled: enable) + } catch { + state.bindings.alertInfo = AlertInfo(id: .alert) + state.bindings.callsEnabled = notificationSettings.invitationsEnabled ?? false + } + state.applyingChange = false + } } extension UNUserNotificationCenter { diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift index 2a5f30e2b..d99a399ff 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift @@ -37,6 +37,9 @@ struct NotificationSettingsScreen: View { if context.viewState.showCallsSettings, context.viewState.settings?.callsEnabled != nil { callsSection } + if context.viewState.settings?.invitationsEnabled != nil { + additionalSettingsSection + } } } } @@ -153,6 +156,21 @@ struct NotificationSettingsScreen: View { } } + private var additionalSettingsSection: some View { + Section { + ListRow(label: .plain(title: L10n.screenNotificationSettingsInviteForMeLabel), + kind: .toggle($context.invitationsEnabled)) + .disabled(context.viewState.settings?.invitationsEnabled == nil) + .allowsHitTesting(!context.viewState.applyingChange) + .onChange(of: context.invitationsEnabled) { _ in + context.send(viewAction: .invitationsChanged) + } + } header: { + Text(L10n.screenNotificationSettingsAdditionalSettingsSectionTitle) + .compoundListSectionHeader() + } + } + private var configurationMismatchSection: some View { Section { ListRow(kind: .custom { @@ -242,7 +260,9 @@ struct NotificationSettingsScreen_Previews: PreviewProvider, TestablePreview { static var previews: some View { NotificationSettingsScreen(context: viewModel.context) + .snapshot(delay: 0.1) NotificationSettingsScreen(context: viewModelConfigurationMismatch.context) + .snapshot(delay: 0.1) .previewDisplayName("Configuration mismatch") } } diff --git a/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxy.swift b/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxy.swift index df9eabf2f..f5ec28b10 100644 --- a/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxy.swift +++ b/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxy.swift @@ -141,6 +141,18 @@ final class NotificationSettingsProxy: NotificationSettingsProxyProtocol { await updatedSettings() } + func isInviteForMeEnabled() async throws -> Bool { + try await notificationSettings.isInviteForMeEnabled() + } + + func setInviteForMeEnabled(enabled: Bool) async throws { + let backgroundTask = await backgroundTaskService?.startBackgroundTask(withName: "setInviteForMeEnabled") + defer { backgroundTask?.stop() } + + try await notificationSettings.setInviteForMeEnabled(enabled: enabled) + await updatedSettings() + } + func getRoomsWithUserDefinedRules() async throws -> [String] { await notificationSettings.getRoomsWithUserDefinedRules(enabled: true) } diff --git a/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxyProtocol.swift b/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxyProtocol.swift index 35c530801..cc270815c 100644 --- a/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxyProtocol.swift +++ b/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxyProtocol.swift @@ -40,6 +40,8 @@ protocol NotificationSettingsProxyProtocol { func setUserMentionEnabled(enabled: Bool) async throws func isCallEnabled() async throws -> Bool func setCallEnabled(enabled: Bool) async throws + func isInviteForMeEnabled() async throws -> Bool + func setInviteForMeEnabled(enabled: Bool) async throws func getRoomsWithUserDefinedRules() async throws -> [String] func canHomeserverPushEncryptedEventsToDevice() async -> Bool } diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.notificationSettingsScreen.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.notificationSettingsScreen.png index e139d5b8b..32d672d23 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.notificationSettingsScreen.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.notificationSettingsScreen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9704334caaadde07dcdf78efa200853d9315bf630296adfae54963fc5ec5d989 -size 103239 +oid sha256:ef3b3ede5511460271ea3879651efd4424b5d16b4242965c8c068a55e5c7fc4c +size 113310 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.notificationSettingsScreen.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.notificationSettingsScreen.png index cfebdfec9..ca9f8dbbe 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.notificationSettingsScreen.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.notificationSettingsScreen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3d623d6e0ddd1ec3387a38461a58e1381494ee21c45344e31ad3b708604ca2a -size 126931 +oid sha256:e924d67a0b598ddc9a015f774512fc1460195c8b7292862b44cc353dba36278b +size 139993 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.notificationSettingsScreen.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.notificationSettingsScreen.png index b6e3b8692..b01744cae 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.notificationSettingsScreen.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.notificationSettingsScreen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2162569e78dffee8d4095998dc31a3672ff3dbb43f0b1f58a44850a18c4c5f6 -size 117932 +oid sha256:af2f6e2f7e5b16d2bd08224e4a1cea85766196e54cead0ea1ed07529caae3da0 +size 132174 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.notificationSettingsScreen.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.notificationSettingsScreen.png index 3de0bb1bd..9756f624b 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.notificationSettingsScreen.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.notificationSettingsScreen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba9fd9cbc5f49630dd1853f2d49611f1d4bb47166ae7ddff79319e0c1d0088bf -size 165797 +oid sha256:e61e23b4a91d4510c1386d1c3a39c99a43c394fadc6e3d73949ce74aab34adc6 +size 182864 diff --git a/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift b/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift index 8cee59381..6cafcf6ca 100644 --- a/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift +++ b/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift @@ -388,4 +388,84 @@ class NotificationSettingsScreenViewModelTests: XCTestCase { XCTAssertNotNil(context.alertInfo) } + + func testToggleInvitationsOff() async throws { + notificationSettingsProxy.isInviteForMeEnabledReturnValue = true + + let deferredInitialFetch = deferFulfillment(viewModel.context.$viewState) { state in + state.settings != nil + } + + viewModel.fetchInitialContent() + + try await deferredInitialFetch.fulfill() + + context.invitationsEnabled = false + let deferred = deferFulfillment(notificationSettingsProxy.callbacks) { callback in + callback == .settingsDidChange + } + + context.send(viewAction: .invitationsChanged) + + try await deferred.fulfill() + + XCTAssert(notificationSettingsProxy.setInviteForMeEnabledEnabledCalled) + XCTAssertEqual(notificationSettingsProxy.setInviteForMeEnabledEnabledReceivedEnabled, false) + } + + func testToggleInvitationsOn() async throws { + notificationSettingsProxy.isInviteForMeEnabledReturnValue = false + + let deferredInitialFetch = deferFulfillment(viewModel.context.$viewState) { state in + state.settings != nil + } + + viewModel.fetchInitialContent() + + try await deferredInitialFetch.fulfill() + + context.invitationsEnabled = true + + let deferred = deferFulfillment(notificationSettingsProxy.callbacks) { callback in + callback == .settingsDidChange + } + + context.send(viewAction: .invitationsChanged) + + try await deferred.fulfill() + + XCTAssert(notificationSettingsProxy.setInviteForMeEnabledEnabledCalled) + XCTAssertEqual(notificationSettingsProxy.setInviteForMeEnabledEnabledReceivedEnabled, true) + } + + func testToggleInvitesFailure() async throws { + notificationSettingsProxy.setInviteForMeEnabledEnabledThrowableError = NotificationSettingsError.Generic(msg: "error") + notificationSettingsProxy.isInviteForMeEnabledReturnValue = false + + let deferredInitialFetch = deferFulfillment(viewModel.context.$viewState) { state in + state.settings != nil + } + + viewModel.fetchInitialContent() + + try await deferredInitialFetch.fulfill() + + context.invitationsEnabled = true + + var deferred = deferFulfillment(context.$viewState) { state in + state.applyingChange == true + } + + context.send(viewAction: .invitationsChanged) + + try await deferred.fulfill() + + deferred = deferFulfillment(context.$viewState) { state in + state.applyingChange == false + } + + try await deferred.fulfill() + + XCTAssertNotNil(context.alertInfo) + } } diff --git a/UnitTests/__Snapshots__/PreviewTests/test_notificationSettingsScreen.1.png b/UnitTests/__Snapshots__/PreviewTests/test_notificationSettingsScreen.1.png index 79723fbd5..5a4956470 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_notificationSettingsScreen.1.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_notificationSettingsScreen.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d858bdd5b55a445ce924c4bba25a8ca420086c033ca5e17b4a73cf571d90c15 -size 97653 +oid sha256:6ca120ad1ccae3415ebf22f9db7fc8389e2b21682ae336de2545651c2027cc87 +size 155222 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_notificationSettingsScreen.Configuration-mismatch.png b/UnitTests/__Snapshots__/PreviewTests/test_notificationSettingsScreen.Configuration-mismatch.png index 79723fbd5..310f3f9e0 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_notificationSettingsScreen.Configuration-mismatch.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_notificationSettingsScreen.Configuration-mismatch.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d858bdd5b55a445ce924c4bba25a8ca420086c033ca5e17b4a73cf571d90c15 -size 97653 +oid sha256:321892e72a5da150eb1a2db3b81576eac4a9474aec8cee5c68c8c3315b3dbe46 +size 126720 diff --git a/changelog.d/2205.change b/changelog.d/2205.change new file mode 100644 index 000000000..500744a24 --- /dev/null +++ b/changelog.d/2205.change @@ -0,0 +1 @@ +Add notification toggle for invitations. \ No newline at end of file