Notification settings screen (#1414)

This commit is contained in:
Nicolas Mauri
2023-07-31 11:11:01 +02:00
committed by GitHub
parent 62f78fb6d0
commit d38b6f74b1
13 changed files with 523 additions and 30 deletions
@@ -260,7 +260,16 @@
"screen_media_upload_preview_error_failed_sending" = "Failed uploading media, please try again."; "screen_media_upload_preview_error_failed_sending" = "Failed uploading media, please try again.";
"screen_migration_message" = "This is a one time process, thanks for waiting."; "screen_migration_message" = "This is a one time process, thanks for waiting.";
"screen_migration_title" = "Setting up your account."; "screen_migration_title" = "Setting up your account.";
"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_enable_notifications" = "Enable notifications on this device"; "screen_notification_settings_enable_notifications" = "Enable notifications on this device";
"screen_notification_settings_group_chats" = "Group chats";
"screen_notification_settings_mentions_section_title" = "Mentions";
"screen_notification_settings_mode_all" = "All";
"screen_notification_settings_mode_mentions" = "Mentions";
"screen_notification_settings_notification_section_title" = "Notify me for";
"screen_notification_settings_room_mention_label" = "Notify me on @room";
"screen_notification_settings_system_notifications_action_required" = "To receive notifications, please change your %1$@."; "screen_notification_settings_system_notifications_action_required" = "To receive notifications, please change your %1$@.";
"screen_notification_settings_system_notifications_action_required_content_link" = "system settings"; "screen_notification_settings_system_notifications_action_required_content_link" = "system settings";
"screen_notification_settings_system_notifications_turned_off" = "System notifications turned off"; "screen_notification_settings_system_notifications_turned_off" = "System notifications turned off";
@@ -299,14 +299,21 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
// MARK: Settings // MARK: Settings
private func presentSettingsScreen(animated: Bool) { private func presentSettingsScreen(animated: Bool) {
Task {
await asyncPresentSettingsScreen(animated: animated)
}
}
private func asyncPresentSettingsScreen(animated: Bool) async {
let settingsNavigationStackCoordinator = NavigationStackCoordinator() let settingsNavigationStackCoordinator = NavigationStackCoordinator()
let userIndicatorController = UserIndicatorController(rootCoordinator: settingsNavigationStackCoordinator) let userIndicatorController = UserIndicatorController(rootCoordinator: settingsNavigationStackCoordinator)
let parameters = SettingsScreenCoordinatorParameters(navigationStackCoordinator: settingsNavigationStackCoordinator, let parameters = await SettingsScreenCoordinatorParameters(navigationStackCoordinator: settingsNavigationStackCoordinator,
userIndicatorController: userIndicatorController, userIndicatorController: userIndicatorController,
userSession: userSession, userSession: userSession,
bugReportService: bugReportService) bugReportService: bugReportService,
notificationSettings: userSession.clientProxy.notificationSettings())
let settingsScreenCoordinator = SettingsScreenCoordinator(parameters: parameters) let settingsScreenCoordinator = SettingsScreenCoordinator(parameters: parameters)
settingsScreenCoordinator.callback = { [weak self] action in settingsScreenCoordinator.callback = { [weak self] action in
guard let self else { return } guard let self else { return }
+18
View File
@@ -672,8 +672,26 @@ public enum L10n {
public static var screenMigrationMessage: String { return L10n.tr("Localizable", "screen_migration_message") } public static var screenMigrationMessage: String { return L10n.tr("Localizable", "screen_migration_message") }
/// Setting up your account. /// Setting up your account.
public static var screenMigrationTitle: String { return L10n.tr("Localizable", "screen_migration_title") } public static var screenMigrationTitle: String { return L10n.tr("Localizable", "screen_migration_title") }
/// Additional settings
public static var screenNotificationSettingsAdditionalSettingsSectionTitle: String { return L10n.tr("Localizable", "screen_notification_settings_additional_settings_section_title") }
/// Audio and video calls
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") }
/// Enable notifications on this device /// Enable notifications on this device
public static var screenNotificationSettingsEnableNotifications: String { return L10n.tr("Localizable", "screen_notification_settings_enable_notifications") } public static var screenNotificationSettingsEnableNotifications: String { return L10n.tr("Localizable", "screen_notification_settings_enable_notifications") }
/// Group chats
public static var screenNotificationSettingsGroupChats: String { return L10n.tr("Localizable", "screen_notification_settings_group_chats") }
/// Mentions
public static var screenNotificationSettingsMentionsSectionTitle: String { return L10n.tr("Localizable", "screen_notification_settings_mentions_section_title") }
/// All
public static var screenNotificationSettingsModeAll: String { return L10n.tr("Localizable", "screen_notification_settings_mode_all") }
/// Mentions
public static var screenNotificationSettingsModeMentions: String { return L10n.tr("Localizable", "screen_notification_settings_mode_mentions") }
/// Notify me for
public static var screenNotificationSettingsNotificationSectionTitle: String { return L10n.tr("Localizable", "screen_notification_settings_notification_section_title") }
/// Notify me on @room
public static var screenNotificationSettingsRoomMentionLabel: String { return L10n.tr("Localizable", "screen_notification_settings_room_mention_label") }
/// To receive notifications, please change your %1$@. /// To receive notifications, please change your %1$@.
public static func screenNotificationSettingsSystemNotificationsActionRequired(_ p1: Any) -> String { public static func screenNotificationSettingsSystemNotificationsActionRequired(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_notification_settings_system_notifications_action_required", String(describing: p1)) return L10n.tr("Localizable", "screen_notification_settings_system_notifications_action_required", String(describing: p1))
@@ -51,5 +51,19 @@ extension NotificationSettingsProxyMock {
self.callbacks.send(.settingsDidChange) self.callbacks.send(.settingsDidChange)
} }
} }
setRoomMentionEnabledEnabledClosure = { [weak self] enabled in
guard let self else { return }
self.isRoomMentionEnabledReturnValue = enabled
Task {
self.callbacks.send(.settingsDidChange)
}
}
setCallEnabledEnabledClosure = { [weak self] enabled in
guard let self else { return }
self.isCallEnabledReturnValue = enabled
Task {
self.callbacks.send(.settingsDidChange)
}
}
} }
} }
@@ -19,6 +19,7 @@ import SwiftUI
struct NotificationSettingsScreenCoordinatorParameters { struct NotificationSettingsScreenCoordinatorParameters {
let userNotificationCenter: UserNotificationCenterProtocol let userNotificationCenter: UserNotificationCenterProtocol
let notificationSettings: NotificationSettingsProxyProtocol
} }
enum NotificationSettingsScreenCoordinatorAction { } enum NotificationSettingsScreenCoordinatorAction { }
@@ -37,10 +38,13 @@ final class NotificationSettingsScreenCoordinator: CoordinatorProtocol {
self.parameters = parameters self.parameters = parameters
viewModel = NotificationSettingsScreenViewModel(appSettings: ServiceLocator.shared.settings, viewModel = NotificationSettingsScreenViewModel(appSettings: ServiceLocator.shared.settings,
userNotificationCenter: parameters.userNotificationCenter) userNotificationCenter: parameters.userNotificationCenter,
notificationSettingsProxy: parameters.notificationSettings)
} }
func start() { } func start() {
viewModel.fetchInitialContent()
}
func toPresentable() -> AnyView { func toPresentable() -> AnyView {
AnyView(NotificationSettingsScreen(context: viewModel.context)) AnyView(NotificationSettingsScreen(context: viewModel.context))
@@ -23,14 +23,31 @@ struct NotificationSettingsScreenViewState: BindableState {
var bindings: NotificationSettingsScreenViewStateBindings var bindings: NotificationSettingsScreenViewStateBindings
var strings = NotificationSettingsScreenStrings() var strings = NotificationSettingsScreenStrings()
var isUserPermissionGranted: Bool? var isUserPermissionGranted: Bool?
var allowedNotificationModes: [RoomNotificationModeProxy] = [.allMessages, .mentionsAndKeywordsOnly]
var showSystemNotificationsAlert: Bool { var showSystemNotificationsAlert: Bool {
bindings.enableNotifications && isUserPermissionGranted == false bindings.enableNotifications && isUserPermissionGranted == false
} }
var settings: NotificationSettingsScreenSettings?
var applyingChange = false
} }
struct NotificationSettingsScreenViewStateBindings { struct NotificationSettingsScreenViewStateBindings {
var enableNotifications = false var enableNotifications = false
var roomMentionsEnabled = false
var callsEnabled = false
var alertInfo: AlertInfo<NotificationSettingsScreenErrorType>?
}
struct NotificationSettingsScreenSettings {
let groupChatsMode: RoomNotificationModeProxy
let directChatsMode: RoomNotificationModeProxy
let roomMentionsEnabled: Bool?
let callsEnabled: 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: Bool
} }
struct NotificationSettingsScreenStrings { struct NotificationSettingsScreenStrings {
@@ -45,9 +62,29 @@ struct NotificationSettingsScreenStrings {
return text return text
}() }()
func string(for mode: RoomNotificationModeProxy) -> String {
switch mode {
case .allMessages:
return L10n.screenNotificationSettingsModeAll
case .mentionsAndKeywordsOnly:
return L10n.screenNotificationSettingsModeMentions
case .mute:
return L10n.commonMute
}
}
} }
enum NotificationSettingsScreenViewAction { enum NotificationSettingsScreenViewAction {
case linkClicked(url: URL) case linkClicked(url: URL)
case changedEnableNotifications case changedEnableNotifications
case groupChatsTapped
case directChatsTapped
case roomMentionChanged
case callsChanged
}
enum NotificationSettingsScreenErrorType: Hashable {
/// A specific error message shown in an alert.
case alert
} }
@@ -23,14 +23,18 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy
private var actionsSubject: PassthroughSubject<NotificationSettingsScreenViewModelAction, Never> = .init() private var actionsSubject: PassthroughSubject<NotificationSettingsScreenViewModelAction, Never> = .init()
private let appSettings: AppSettings private let appSettings: AppSettings
private let userNotificationCenter: UserNotificationCenterProtocol private let userNotificationCenter: UserNotificationCenterProtocol
private let notificationSettingsProxy: NotificationSettingsProxyProtocol
@CancellableTask private var fetchSettingsTask: Task<Void, Error>?
var actions: AnyPublisher<NotificationSettingsScreenViewModelAction, Never> { var actions: AnyPublisher<NotificationSettingsScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher() actionsSubject.eraseToAnyPublisher()
} }
init(appSettings: AppSettings, userNotificationCenter: UserNotificationCenterProtocol) { init(appSettings: AppSettings, userNotificationCenter: UserNotificationCenterProtocol, notificationSettingsProxy: NotificationSettingsProxyProtocol) {
self.appSettings = appSettings self.appSettings = appSettings
self.userNotificationCenter = userNotificationCenter self.userNotificationCenter = userNotificationCenter
self.notificationSettingsProxy = notificationSettingsProxy
let bindings = NotificationSettingsScreenViewStateBindings(enableNotifications: appSettings.enableNotifications) let bindings = NotificationSettingsScreenViewStateBindings(enableNotifications: appSettings.enableNotifications)
super.init(initialViewState: NotificationSettingsScreenViewState(bindings: bindings)) super.init(initialViewState: NotificationSettingsScreenViewState(bindings: bindings))
@@ -39,6 +43,50 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy
.weakAssign(to: \.state.bindings.enableNotifications, on: self) .weakAssign(to: \.state.bindings.enableNotifications, on: self)
.store(in: &cancellables) .store(in: &cancellables)
setupDidBecomeActiveSubscription()
setupNotificationSettingsSubscription()
}
func fetchInitialContent() {
fetchSettings()
}
// MARK: - Public
override func process(viewAction: NotificationSettingsScreenViewAction) {
switch viewAction {
case .linkClicked(let url):
MXLog.warning("Link clicked: \(url)")
case .changedEnableNotifications:
toggleNotifications()
case .groupChatsTapped:
break
case .directChatsTapped:
break
case .roomMentionChanged:
guard let settings = state.settings, settings.roomMentionsEnabled != state.bindings.roomMentionsEnabled else {
return
}
Task { await enableRoomMention(state.bindings.roomMentionsEnabled) }
case .callsChanged:
guard let settings = state.settings, settings.callsEnabled != state.bindings.callsEnabled else {
return
}
Task { await enableCalls(state.bindings.callsEnabled) }
}
}
// MARK: - Private
func readSystemAuthorizationStatus() async {
state.isUserPermissionGranted = await userNotificationCenter.authorizationStatus() == .authorized
}
func toggleNotifications() {
appSettings.enableNotifications.toggle()
}
private func setupDidBecomeActiveSubscription() {
// Refresh authorization status uppon UIApplication.didBecomeActiveNotification notification // Refresh authorization status uppon UIApplication.didBecomeActiveNotification notification
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification) NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
.sink { [weak self] _ in .sink { [weak self] _ in
@@ -52,25 +100,88 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy
await readSystemAuthorizationStatus() await readSystemAuthorizationStatus()
} }
} }
// MARK: - Public
override func process(viewAction: NotificationSettingsScreenViewAction) { private func setupNotificationSettingsSubscription() {
switch viewAction { notificationSettingsProxy.callbacks
case .linkClicked(let url): .receive(on: DispatchQueue.main)
MXLog.warning("Link clicked: \(url)") .sink { [weak self] callback in
case .changedEnableNotifications: guard let self else { return }
toggleNotifications()
switch callback {
case .settingsDidChange:
self.fetchSettings()
}
}
.store(in: &cancellables)
}
private func fetchSettings() {
fetchSettingsTask = Task {
// Group chats
// A group chat is a chat having more than 2 active members
let groupChatActiveMembers: UInt64 = 3
var groupChatsMode = await notificationSettingsProxy.getDefaultNotificationRoomMode(isEncrypted: false, activeMembersCount: groupChatActiveMembers)
let encryptedGroupChatsMode = await notificationSettingsProxy.getDefaultNotificationRoomMode(isEncrypted: true, activeMembersCount: groupChatActiveMembers)
// Direct chats
// A direct chat is a chat having exactly 2 active members
let directChatActiveMembers: UInt64 = 2
var directChatsMode = await notificationSettingsProxy.getDefaultNotificationRoomMode(isEncrypted: false, activeMembersCount: directChatActiveMembers)
let encryptedDirectChatsMode = await notificationSettingsProxy.getDefaultNotificationRoomMode(isEncrypted: true, activeMembersCount: directChatActiveMembers)
// 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`)
var inconsistencyDetected = false
if groupChatsMode != encryptedGroupChatsMode {
groupChatsMode = .allMessages
inconsistencyDetected = true
}
if directChatsMode != encryptedDirectChatsMode {
directChatsMode = .allMessages
inconsistencyDetected = true
}
// 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()
guard !Task.isCancelled else { return }
let notificationSettings = NotificationSettingsScreenSettings(groupChatsMode: groupChatsMode,
directChatsMode: directChatsMode,
roomMentionsEnabled: roomMentionsEnabled,
callsEnabled: callEnabled,
inconsistentSettings: inconsistencyDetected)
state.settings = notificationSettings
state.bindings.roomMentionsEnabled = notificationSettings.roomMentionsEnabled ?? false
state.bindings.callsEnabled = notificationSettings.callsEnabled ?? false
} }
} }
// MARK: - Private private func enableRoomMention(_ enable: Bool) async {
guard let notificationSettings = state.settings else { return }
func readSystemAuthorizationStatus() async { do {
state.isUserPermissionGranted = await userNotificationCenter.authorizationStatus() == .authorized state.applyingChange = true
MXLog.info("[NotificationSettingsScreenViewMode] setRoomMentionEnabled(\(enable))")
try await notificationSettingsProxy.setRoomMentionEnabled(enabled: enable)
} catch {
state.bindings.alertInfo = AlertInfo(id: .alert)
state.bindings.roomMentionsEnabled = notificationSettings.roomMentionsEnabled ?? false
}
state.applyingChange = false
} }
func toggleNotifications() { func enableCalls(_ enable: Bool) async {
appSettings.enableNotifications.toggle() guard let notificationSettings = state.settings else { return }
do {
state.applyingChange = true
MXLog.info("[NotificationSettingsScreenViewMode] setCallEnabled(\(enable))")
try await notificationSettingsProxy.setCallEnabled(enabled: enable)
} catch {
state.bindings.alertInfo = AlertInfo(id: .alert)
state.bindings.callsEnabled = notificationSettings.callsEnabled ?? false
}
state.applyingChange = false
} }
} }
@@ -20,4 +20,6 @@ import Combine
protocol NotificationSettingsScreenViewModelProtocol { protocol NotificationSettingsScreenViewModelProtocol {
var actions: AnyPublisher<NotificationSettingsScreenViewModelAction, Never> { get } var actions: AnyPublisher<NotificationSettingsScreenViewModelAction, Never> { get }
var context: NotificationSettingsScreenViewModelType.Context { get } var context: NotificationSettingsScreenViewModelType.Context { get }
func fetchInitialContent()
} }
@@ -25,9 +25,19 @@ struct NotificationSettingsScreen: View {
userPermissionSection userPermissionSection
} }
enableNotificationSection enableNotificationSection
if context.enableNotifications {
roomsNotificationSection
if context.viewState.settings?.roomMentionsEnabled != nil {
mentionsSection
}
if context.viewState.settings?.callsEnabled != nil {
callsSection
}
}
} }
.compoundForm() .compoundForm()
.navigationTitle(L10n.screenNotificationSettingsTitle) .navigationTitle(L10n.screenNotificationSettingsTitle)
.alert(item: $context.alertInfo)
.track(screen: .settingsNotifications) .track(screen: .settingsNotifications)
} }
@@ -69,6 +79,87 @@ struct NotificationSettingsScreen: View {
} }
.compoundFormSection() .compoundFormSection()
} }
private var roomsNotificationSection: some View {
Section {
// Group chats
Button {
context.send(viewAction: .groupChatsTapped)
} label: {
LabeledContent {
if let settings = context.viewState.settings {
Text(context.viewState.strings.string(for: settings.groupChatsMode))
} else {
ProgressView()
}
} label: {
Text(L10n.screenNotificationSettingsGroupChats)
}
}
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.notifications)
.buttonStyle(.compoundForm(accessory: context.viewState.settings.flatMap { _ in .navigationLink }))
.disabled(context.viewState.settings == nil)
// Direct chats
Button {
context.send(viewAction: .directChatsTapped)
} label: {
LabeledContent {
if let settings = context.viewState.settings {
Text(context.viewState.strings.string(for: settings.directChatsMode))
} else {
ProgressView()
}
} label: {
Text(L10n.screenNotificationSettingsDirectChats)
}
}
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.notifications)
.buttonStyle(.compoundForm(accessory: context.viewState.settings.flatMap { _ in .navigationLink }))
.disabled(context.viewState.settings == nil)
} header: {
Text(L10n.screenNotificationSettingsNotificationSectionTitle)
.compoundFormSectionHeader()
}
.compoundFormSection()
}
private var mentionsSection: some View {
Section {
Toggle(isOn: $context.roomMentionsEnabled) {
Text(L10n.screenNotificationSettingsRoomMentionLabel)
}
.toggleStyle(.compoundForm)
.onChange(of: context.roomMentionsEnabled) { _ in
context.send(viewAction: .roomMentionChanged)
}
.disabled(context.viewState.settings?.roomMentionsEnabled == nil)
.allowsHitTesting(!context.viewState.applyingChange)
} header: {
Text(L10n.screenNotificationSettingsMentionsSectionTitle)
.compoundFormSectionHeader()
}
.compoundFormSection()
}
private var callsSection: some View {
Section {
Toggle(isOn: $context.callsEnabled) {
Text(L10n.screenNotificationSettingsCallsLabel)
}
.toggleStyle(.compoundForm)
.onChange(of: context.callsEnabled) { _ in
context.send(viewAction: .callsChanged)
}
.disabled(context.viewState.settings?.callsEnabled == nil)
.allowsHitTesting(!context.viewState.applyingChange)
} header: {
Text(L10n.screenNotificationSettingsAdditionalSettingsSectionTitle)
.compoundFormSectionHeader()
}
.compoundFormSection()
}
} }
// MARK: - Previews // MARK: - Previews
@@ -78,7 +169,22 @@ struct NotificationSettingsScreen_Previews: PreviewProvider {
let appSettings = AppSettings() let appSettings = AppSettings()
let notificationCenter = UserNotificationCenterMock() let notificationCenter = UserNotificationCenterMock()
notificationCenter.authorizationStatusReturnValue = .notDetermined notificationCenter.authorizationStatusReturnValue = .notDetermined
var viewModel = NotificationSettingsScreenViewModel(appSettings: appSettings, userNotificationCenter: notificationCenter) let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init())
notificationSettingsProxy.getDefaultNotificationRoomModeIsEncryptedActiveMembersCountClosure = { isEncrypted, activeMembersCount in
switch (isEncrypted, activeMembersCount) {
case (_, 2):
return .allMessages
default:
return .mentionsAndKeywordsOnly
}
}
notificationSettingsProxy.isRoomMentionEnabledReturnValue = true
notificationSettingsProxy.isCallEnabledReturnValue = false
var viewModel = NotificationSettingsScreenViewModel(appSettings: appSettings,
userNotificationCenter: notificationCenter,
notificationSettingsProxy: notificationSettingsProxy)
viewModel.fetchInitialContent()
return viewModel return viewModel
}() }()
@@ -21,6 +21,7 @@ struct SettingsScreenCoordinatorParameters {
weak var userIndicatorController: UserIndicatorControllerProtocol? weak var userIndicatorController: UserIndicatorControllerProtocol?
let userSession: UserSessionProtocol let userSession: UserSessionProtocol
let bugReportService: BugReportServiceProtocol let bugReportService: BugReportServiceProtocol
let notificationSettings: NotificationSettingsProxyProtocol
} }
enum SettingsScreenCoordinatorAction { enum SettingsScreenCoordinatorAction {
@@ -140,9 +141,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
} }
private func presentNotificationSettings() { private func presentNotificationSettings() {
let notificationParameters = NotificationSettingsScreenCoordinatorParameters( let notificationParameters = NotificationSettingsScreenCoordinatorParameters(userNotificationCenter: UNUserNotificationCenter.current(),
userNotificationCenter: UNUserNotificationCenter.current() notificationSettings: parameters.notificationSettings)
)
let coordinator = NotificationSettingsScreenCoordinator(parameters: notificationParameters) let coordinator = NotificationSettingsScreenCoordinator(parameters: notificationParameters)
parameters.navigationStackCoordinator?.push(coordinator) parameters.navigationStackCoordinator?.push(coordinator)
} }
@@ -169,7 +169,8 @@ class MockScreen: Identifiable {
userIndicatorController: nil, userIndicatorController: nil,
userSession: MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"), userSession: MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"),
mediaProvider: MockMediaProvider()), mediaProvider: MockMediaProvider()),
bugReportService: BugReportServiceMock())) bugReportService: BugReportServiceMock(),
notificationSettings: NotificationSettingsProxyMock(with: .init())))
navigationStackCoordinator.setRootCoordinator(coordinator) navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator return navigationStackCoordinator
case .bugReport: case .bugReport:
@@ -195,7 +196,8 @@ class MockScreen: Identifiable {
case .notificationSettingsScreen: case .notificationSettingsScreen:
let userNotificationCenter = UserNotificationCenterMock() let userNotificationCenter = UserNotificationCenterMock()
userNotificationCenter.authorizationStatusReturnValue = .denied userNotificationCenter.authorizationStatusReturnValue = .denied
let parameters = NotificationSettingsScreenCoordinatorParameters(userNotificationCenter: userNotificationCenter) let parameters = NotificationSettingsScreenCoordinatorParameters(userNotificationCenter: userNotificationCenter,
notificationSettings: NotificationSettingsProxyMock(with: .init()))
return NotificationSettingsScreenCoordinator(parameters: parameters) return NotificationSettingsScreenCoordinator(parameters: parameters)
case .onboarding: case .onboarding:
return OnboardingCoordinator() return OnboardingCoordinator()
@@ -95,6 +95,7 @@ final class NotificationManagerTests: XCTestCase {
} }
func test_whenRegisteredAndPusherTagIsSetInSettings_tagNotGenerated() async throws { func test_whenRegisteredAndPusherTagIsSetInSettings_tagNotGenerated() async throws {
notificationCenter.requestAuthorizationOptionsReturnValue = true
appSettings.pusherProfileTag = "12345" appSettings.pusherProfileTag = "12345"
_ = await notificationManager.register(with: Data()) _ = await notificationManager.register(with: Data())
XCTAssertEqual(appSettings.pusherProfileTag, "12345") XCTAssertEqual(appSettings.pusherProfileTag, "12345")
@@ -14,6 +14,7 @@
// limitations under the License. // limitations under the License.
// //
import MatrixRustSDK
import XCTest import XCTest
@testable import ElementX @testable import ElementX
@@ -24,6 +25,7 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
private var context: NotificationSettingsScreenViewModelType.Context! private var context: NotificationSettingsScreenViewModelType.Context!
private var appSettings: AppSettings! private var appSettings: AppSettings!
private var userNotificationCenter: UserNotificationCenterMock! private var userNotificationCenter: UserNotificationCenterMock!
private var notificationSettingsProxy: NotificationSettingsProxyMock!
@MainActor override func setUpWithError() throws { @MainActor override func setUpWithError() throws {
AppSettings.reset() AppSettings.reset()
@@ -31,8 +33,14 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
userNotificationCenter = UserNotificationCenterMock() userNotificationCenter = UserNotificationCenterMock()
userNotificationCenter.authorizationStatusReturnValue = .authorized userNotificationCenter.authorizationStatusReturnValue = .authorized
appSettings = AppSettings() appSettings = AppSettings()
notificationSettingsProxy = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())
notificationSettingsProxy.getDefaultNotificationRoomModeIsEncryptedActiveMembersCountReturnValue = .allMessages
notificationSettingsProxy.isRoomMentionEnabledReturnValue = true
notificationSettingsProxy.isCallEnabledReturnValue = true
viewModel = NotificationSettingsScreenViewModel(appSettings: appSettings, viewModel = NotificationSettingsScreenViewModel(appSettings: appSettings,
userNotificationCenter: userNotificationCenter) userNotificationCenter: userNotificationCenter,
notificationSettingsProxy: notificationSettingsProxy)
context = viewModel.context context = viewModel.context
} }
@@ -42,9 +50,183 @@ class NotificationSettingsScreenViewModelTests: XCTestCase {
XCTAssertTrue(appSettings.enableNotifications) XCTAssertTrue(appSettings.enableNotifications)
} }
func testOptOut() { func testDisableNotifications() {
appSettings.enableNotifications = true appSettings.enableNotifications = true
context.send(viewAction: .changedEnableNotifications) context.send(viewAction: .changedEnableNotifications)
XCTAssertFalse(appSettings.enableNotifications) XCTAssertFalse(appSettings.enableNotifications)
} }
func testFetchSettings() async throws {
notificationSettingsProxy.getDefaultNotificationRoomModeIsEncryptedActiveMembersCountClosure = { isEncrypted, activeMembersCount in
switch (isEncrypted, activeMembersCount) {
case (_, 2):
return .allMessages
case (_, _):
return .mentionsAndKeywordsOnly
}
}
let deferred = deferFulfillment(viewModel.context.$viewState.map(\.settings)
.first(where: { $0 != nil }))
notificationSettingsProxy.callbacks.send(.settingsDidChange)
try await deferred.fulfill()
XCTAssertEqual(notificationSettingsProxy.getDefaultNotificationRoomModeIsEncryptedActiveMembersCountCallsCount, 4)
XCTAssert(notificationSettingsProxy.isRoomMentionEnabledCalled)
XCTAssert(notificationSettingsProxy.isCallEnabledCalled)
XCTAssertEqual(context.viewState.settings?.groupChatsMode, .mentionsAndKeywordsOnly)
XCTAssertEqual(context.viewState.settings?.directChatsMode, .allMessages)
XCTAssertEqual(context.viewState.settings?.inconsistentSettings, false)
XCTAssertNil(context.viewState.bindings.alertInfo)
}
func testInconsistentGroupChatsSettings() async throws {
notificationSettingsProxy.getDefaultNotificationRoomModeIsEncryptedActiveMembersCountClosure = { isEncrypted, activeMembersCount in
switch (isEncrypted, activeMembersCount) {
case (true, 3):
return .allMessages
case (false, 3):
return .mentionsAndKeywordsOnly
default:
return .allMessages
}
}
let deferred = deferFulfillment(viewModel.context.$viewState.map(\.settings)
.first(where: { $0 != nil }))
notificationSettingsProxy.callbacks.send(.settingsDidChange)
try await deferred.fulfill()
XCTAssertEqual(context.viewState.settings?.groupChatsMode, .allMessages)
XCTAssertEqual(context.viewState.settings?.inconsistentSettings, true)
}
func testInconsistentDirectChatsSettings() async throws {
notificationSettingsProxy.getDefaultNotificationRoomModeIsEncryptedActiveMembersCountClosure = { isEncrypted, activeMembersCount in
switch (isEncrypted, activeMembersCount) {
case (true, 2):
return .allMessages
case (false, 2):
return .mentionsAndKeywordsOnly
default:
return .allMessages
}
}
let deferred = deferFulfillment(viewModel.context.$viewState.map(\.settings)
.first(where: { $0 != nil }))
notificationSettingsProxy.callbacks.send(.settingsDidChange)
try await deferred.fulfill()
XCTAssertEqual(context.viewState.settings?.directChatsMode, .allMessages)
XCTAssertEqual(context.viewState.settings?.inconsistentSettings, true)
}
func testToggleRoomMentionOff() async throws {
notificationSettingsProxy.isRoomMentionEnabledReturnValue = true
let deferredInitialFetch = deferFulfillment(viewModel.context.$viewState.map(\.settings)
.first(where: { $0 != nil }))
notificationSettingsProxy.callbacks.send(.settingsDidChange)
try await deferredInitialFetch.fulfill()
context.roomMentionsEnabled = false
let deferred = deferFulfillment(notificationSettingsProxy.callbacks
.first(where: { $0 == .settingsDidChange }))
context.send(viewAction: .roomMentionChanged)
try await deferred.fulfill()
XCTAssert(notificationSettingsProxy.setRoomMentionEnabledEnabledCalled)
XCTAssertEqual(notificationSettingsProxy.setRoomMentionEnabledEnabledReceivedEnabled, false)
}
func testToggleRoomMentionOn() async throws {
notificationSettingsProxy.isRoomMentionEnabledReturnValue = false
let deferredInitialFetch = deferFulfillment(viewModel.context.$viewState.map(\.settings)
.first(where: { $0 != nil }))
viewModel.fetchInitialContent()
try await deferredInitialFetch.fulfill()
context.roomMentionsEnabled = true
let deferred = deferFulfillment(notificationSettingsProxy.callbacks
.first(where: { $0 == .settingsDidChange }))
context.send(viewAction: .roomMentionChanged)
try await deferred.fulfill()
XCTAssert(notificationSettingsProxy.setRoomMentionEnabledEnabledCalled)
XCTAssertEqual(notificationSettingsProxy.setRoomMentionEnabledEnabledReceivedEnabled, true)
}
func testToggleRoomMentionFailure() async throws {
notificationSettingsProxy.setRoomMentionEnabledEnabledThrowableError = NotificationSettingsError.Generic(message: "error")
notificationSettingsProxy.isRoomMentionEnabledReturnValue = false
let deferredInitialFetch = deferFulfillment(viewModel.context.$viewState.map(\.settings)
.first(where: { $0 != nil }))
viewModel.fetchInitialContent()
try await deferredInitialFetch.fulfill()
context.roomMentionsEnabled = true
let deferred = deferFulfillment(context.$viewState.map(\.applyingChange)
.removeDuplicates()
.collect(3)
.first())
context.send(viewAction: .roomMentionChanged)
let states = try await deferred.fulfill()
XCTAssertEqual(states, [false, true, false])
XCTAssertNotNil(context.alertInfo)
}
func testToggleCallsOff() async throws {
notificationSettingsProxy.isCallEnabledReturnValue = true
let deferredInitialFetch = deferFulfillment(viewModel.context.$viewState.map(\.settings)
.first(where: { $0 != nil }))
viewModel.fetchInitialContent()
try await deferredInitialFetch.fulfill()
context.callsEnabled = false
let deferred = deferFulfillment(notificationSettingsProxy.callbacks
.first(where: { $0 == .settingsDidChange }))
context.send(viewAction: .callsChanged)
try await deferred.fulfill()
XCTAssert(notificationSettingsProxy.setCallEnabledEnabledCalled)
XCTAssertEqual(notificationSettingsProxy.setCallEnabledEnabledReceivedEnabled, false)
}
func testToggleCallsOn() async throws {
notificationSettingsProxy.isCallEnabledReturnValue = false
let deferredInitialFetch = deferFulfillment(viewModel.context.$viewState.map(\.settings)
.first(where: { $0 != nil }))
viewModel.fetchInitialContent()
try await deferredInitialFetch.fulfill()
context.callsEnabled = true
let deferred = deferFulfillment(notificationSettingsProxy.callbacks
.first(where: { $0 == .settingsDidChange }))
context.send(viewAction: .callsChanged)
try await deferred.fulfill()
XCTAssert(notificationSettingsProxy.setCallEnabledEnabledCalled)
XCTAssertEqual(notificationSettingsProxy.setCallEnabledEnabledReceivedEnabled, true)
}
func testToggleCallsFailure() async throws {
notificationSettingsProxy.setCallEnabledEnabledThrowableError = NotificationSettingsError.Generic(message: "error")
notificationSettingsProxy.isCallEnabledReturnValue = false
let deferredInitialFetch = deferFulfillment(viewModel.context.$viewState.map(\.settings)
.first(where: { $0 != nil }))
viewModel.fetchInitialContent()
try await deferredInitialFetch.fulfill()
context.callsEnabled = true
let deferred = deferFulfillment(context.$viewState.map(\.applyingChange)
.removeDuplicates()
.collect(3)
.first())
context.send(viewAction: .callsChanged)
let states = try await deferred.fulfill()
XCTAssertEqual(states, [false, true, false])
XCTAssertNotNil(context.alertInfo)
}
} }