mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Notification settings screen (#1414)
This commit is contained in:
+6
-2
@@ -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))
|
||||
|
||||
+37
@@ -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
|
||||
}
|
||||
|
||||
+127
-16
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -20,4 +20,6 @@ import Combine
|
||||
protocol NotificationSettingsScreenViewModelProtocol {
|
||||
var actions: AnyPublisher<NotificationSettingsScreenViewModelAction, Never> { get }
|
||||
var context: NotificationSettingsScreenViewModelType.Context { get }
|
||||
|
||||
func fetchInitialContent()
|
||||
}
|
||||
|
||||
+107
-1
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user