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
@@ -19,6 +19,7 @@ import SwiftUI
struct NotificationSettingsScreenCoordinatorParameters {
let userNotificationCenter: UserNotificationCenterProtocol
let notificationSettings: NotificationSettingsProxyProtocol
}
enum NotificationSettingsScreenCoordinatorAction { }
@@ -37,10 +38,13 @@ final class NotificationSettingsScreenCoordinator: CoordinatorProtocol {
self.parameters = parameters
viewModel = NotificationSettingsScreenViewModel(appSettings: ServiceLocator.shared.settings,
userNotificationCenter: parameters.userNotificationCenter)
userNotificationCenter: parameters.userNotificationCenter,
notificationSettingsProxy: parameters.notificationSettings)
}
func start() { }
func start() {
viewModel.fetchInitialContent()
}
func toPresentable() -> AnyView {
AnyView(NotificationSettingsScreen(context: viewModel.context))
@@ -23,14 +23,31 @@ struct NotificationSettingsScreenViewState: BindableState {
var bindings: NotificationSettingsScreenViewStateBindings
var strings = NotificationSettingsScreenStrings()
var isUserPermissionGranted: Bool?
var allowedNotificationModes: [RoomNotificationModeProxy] = [.allMessages, .mentionsAndKeywordsOnly]
var showSystemNotificationsAlert: Bool {
bindings.enableNotifications && isUserPermissionGranted == false
}
var settings: NotificationSettingsScreenSettings?
var applyingChange = false
}
struct NotificationSettingsScreenViewStateBindings {
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 {
@@ -45,9 +62,29 @@ struct NotificationSettingsScreenStrings {
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 {
case linkClicked(url: URL)
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 let appSettings: AppSettings
private let userNotificationCenter: UserNotificationCenterProtocol
private let notificationSettingsProxy: NotificationSettingsProxyProtocol
@CancellableTask private var fetchSettingsTask: Task<Void, Error>?
var actions: AnyPublisher<NotificationSettingsScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(appSettings: AppSettings, userNotificationCenter: UserNotificationCenterProtocol) {
init(appSettings: AppSettings, userNotificationCenter: UserNotificationCenterProtocol, notificationSettingsProxy: NotificationSettingsProxyProtocol) {
self.appSettings = appSettings
self.userNotificationCenter = userNotificationCenter
self.notificationSettingsProxy = notificationSettingsProxy
let bindings = NotificationSettingsScreenViewStateBindings(enableNotifications: appSettings.enableNotifications)
super.init(initialViewState: NotificationSettingsScreenViewState(bindings: bindings))
@@ -39,6 +43,50 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy
.weakAssign(to: \.state.bindings.enableNotifications, on: self)
.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
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
.sink { [weak self] _ in
@@ -52,25 +100,88 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy
await readSystemAuthorizationStatus()
}
}
// MARK: - Public
override func process(viewAction: NotificationSettingsScreenViewAction) {
switch viewAction {
case .linkClicked(let url):
MXLog.warning("Link clicked: \(url)")
case .changedEnableNotifications:
toggleNotifications()
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 {
// 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
func readSystemAuthorizationStatus() async {
state.isUserPermissionGranted = await userNotificationCenter.authorizationStatus() == .authorized
private func enableRoomMention(_ enable: Bool) async {
guard let notificationSettings = state.settings else { return }
do {
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() {
appSettings.enableNotifications.toggle()
func enableCalls(_ enable: Bool) async {
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 {
var actions: AnyPublisher<NotificationSettingsScreenViewModelAction, Never> { get }
var context: NotificationSettingsScreenViewModelType.Context { get }
func fetchInitialContent()
}
@@ -25,9 +25,19 @@ struct NotificationSettingsScreen: View {
userPermissionSection
}
enableNotificationSection
if context.enableNotifications {
roomsNotificationSection
if context.viewState.settings?.roomMentionsEnabled != nil {
mentionsSection
}
if context.viewState.settings?.callsEnabled != nil {
callsSection
}
}
}
.compoundForm()
.navigationTitle(L10n.screenNotificationSettingsTitle)
.alert(item: $context.alertInfo)
.track(screen: .settingsNotifications)
}
@@ -69,6 +79,87 @@ struct NotificationSettingsScreen: View {
}
.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
@@ -78,7 +169,22 @@ struct NotificationSettingsScreen_Previews: PreviewProvider {
let appSettings = AppSettings()
let notificationCenter = UserNotificationCenterMock()
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
}()
@@ -21,6 +21,7 @@ struct SettingsScreenCoordinatorParameters {
weak var userIndicatorController: UserIndicatorControllerProtocol?
let userSession: UserSessionProtocol
let bugReportService: BugReportServiceProtocol
let notificationSettings: NotificationSettingsProxyProtocol
}
enum SettingsScreenCoordinatorAction {
@@ -140,9 +141,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
}
private func presentNotificationSettings() {
let notificationParameters = NotificationSettingsScreenCoordinatorParameters(
userNotificationCenter: UNUserNotificationCenter.current()
)
let notificationParameters = NotificationSettingsScreenCoordinatorParameters(userNotificationCenter: UNUserNotificationCenter.current(),
notificationSettings: parameters.notificationSettings)
let coordinator = NotificationSettingsScreenCoordinator(parameters: notificationParameters)
parameters.navigationStackCoordinator?.push(coordinator)
}