mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-06 12:28:29 +00:00
Edit the default notification settings (#1468)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
+51
@@ -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<NotificationSettingsEditScreenCoordinatorAction, Never> = .init()
|
||||
private var cancellables: Set<AnyCancellable> = .init()
|
||||
|
||||
var actions: AnyPublisher<NotificationSettingsEditScreenCoordinatorAction, Never> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
+73
@@ -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<NotificationSettingsEditScreenErrorType>?
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
+120
@@ -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<NotificationSettingsEditScreenViewState, NotificationSettingsEditScreenViewAction>
|
||||
|
||||
class NotificationSettingsEditScreenViewModel: NotificationSettingsEditScreenViewModelType, NotificationSettingsEditScreenViewModelProtocol {
|
||||
private var actionsSubject: PassthroughSubject<NotificationSettingsEditScreenViewModelAction, Never> = .init()
|
||||
private let isDirect: Bool
|
||||
private let notificationSettingsProxy: NotificationSettingsProxyProtocol
|
||||
@CancellableTask private var fetchSettingsTask: Task<Void, Error>?
|
||||
|
||||
var actions: AnyPublisher<NotificationSettingsEditScreenViewModelAction, Never> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -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<NotificationSettingsEditScreenViewModelAction, Never> { get }
|
||||
var context: NotificationSettingsEditScreenViewModelType.Context { get }
|
||||
|
||||
func fetchInitialContent()
|
||||
}
|
||||
+100
@@ -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")
|
||||
}
|
||||
}
|
||||
+20
-3
@@ -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<NotificationSettingsScreenCoordinatorAction, Never> = .init()
|
||||
private var cancellables: Set<AnyCancellable> = .init()
|
||||
|
||||
|
||||
private var navigationStackCoordinator: NavigationStackCoordinator? {
|
||||
parameters.navigationStackCoordinator
|
||||
}
|
||||
|
||||
var actions: AnyPublisher<NotificationSettingsScreenCoordinatorAction, Never> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ import UIKit
|
||||
|
||||
enum NotificationSettingsScreenViewModelAction {
|
||||
case close
|
||||
case editDefaultMode(isDirect: Bool)
|
||||
}
|
||||
|
||||
struct NotificationSettingsScreenViewState: BindableState {
|
||||
|
||||
+6
-8
@@ -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`)
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user