mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-21 00:52:43 +02:00
Merge commit 'aaadcc73674cc8886e363693a7d7c08ac9b4f516' into feature/4260_merge_foss_1_10_2
# Conflicts: # Config/AppVersion.xcconfig # Podfile # Podfile.lock # Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved # Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift # Riot/Modules/Application/LegacyAppDelegate.m # Riot/Modules/Authentication/AuthenticationCoordinator.swift # Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift # Riot/Modules/ContextMenu/ActionProviders/RoomActionProvider.swift # Riot/Modules/Home/AllChats/AllChatsViewController.swift # Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift # Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift # Riot/Modules/Room/Settings/RoomSettingsViewController.m # fastlane/Fastfile
This commit is contained in:
+4
@@ -41,6 +41,10 @@ extension MXPushRule: NotificationPushRuleType {
|
||||
return false
|
||||
}
|
||||
|
||||
var ruleActions: NotificationActions? {
|
||||
.init(notify: notify, highlight: highlight, sound: sound)
|
||||
}
|
||||
|
||||
private func getAction(actionType: MXPushRuleActionType, tweakType: String? = nil) -> MXPushRuleAction? {
|
||||
guard let actions = actions as? [MXPushRuleAction] else {
|
||||
return nil
|
||||
|
||||
+4
-2
@@ -16,10 +16,12 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct MockNotificationPushRule: NotificationPushRuleType {
|
||||
struct MockNotificationPushRule: NotificationPushRuleType, Equatable {
|
||||
var ruleId: String!
|
||||
var enabled: Bool
|
||||
var ruleActions: NotificationActions? = NotificationStandardActions.notifyDefaultSound.actions
|
||||
|
||||
func matches(standardActions: NotificationStandardActions?) -> Bool {
|
||||
false
|
||||
standardActions?.actions == ruleActions
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import Foundation
|
||||
|
||||
/// The actions defined on a push rule, used in the static push rule definitions.
|
||||
struct NotificationActions {
|
||||
struct NotificationActions: Equatable {
|
||||
let notify: Bool
|
||||
let highlight: Bool
|
||||
let sound: String?
|
||||
|
||||
+3
-3
@@ -22,7 +22,7 @@ extension NotificationPushRuleId {
|
||||
/// It is defined similarly across Web and Android.
|
||||
/// - Parameter index: The notification index for which to get the actions for.
|
||||
/// - Returns: The associated `NotificationStandardActions`.
|
||||
func standardActions(for index: NotificationIndex) -> NotificationStandardActions? {
|
||||
func standardActions(for index: NotificationIndex) -> NotificationStandardActions {
|
||||
switch self {
|
||||
case .containDisplayName:
|
||||
switch index {
|
||||
@@ -42,7 +42,7 @@ extension NotificationPushRuleId {
|
||||
case .silent: return .notify
|
||||
case .noisy: return .highlight
|
||||
}
|
||||
case .oneToOneRoom:
|
||||
case .oneToOneRoom, .oneToOnePollStart, .msc3930oneToOnePollStart, .oneToOnePollEnd, .msc3930oneToOnePollEnd:
|
||||
switch index {
|
||||
case .off: return .dontNotify
|
||||
case .silent: return .notify
|
||||
@@ -54,7 +54,7 @@ extension NotificationPushRuleId {
|
||||
case .silent: return .notify
|
||||
case .noisy: return .notifyDefaultSound
|
||||
}
|
||||
case .allOtherMessages:
|
||||
case .allOtherMessages, .pollStart, .msc3930pollStart, .pollEnd, .msc3930pollEnd:
|
||||
switch index {
|
||||
case .off: return .dontNotify
|
||||
case .silent: return .notify
|
||||
|
||||
@@ -30,6 +30,18 @@ enum NotificationPushRuleId: String {
|
||||
case allOtherMessages = ".m.rule.message"
|
||||
case encrypted = ".m.rule.encrypted"
|
||||
case keywords = "_keywords"
|
||||
// poll started event
|
||||
case pollStart = ".m.rule.poll_start"
|
||||
case msc3930pollStart = ".org.matrix.msc3930.rule.poll_start"
|
||||
// poll started event (one to one)
|
||||
case oneToOnePollStart = ".m.rule.poll_start_one_to_one"
|
||||
case msc3930oneToOnePollStart = ".org.matrix.msc3930.rule.poll_start_one_to_one"
|
||||
// poll ended event
|
||||
case pollEnd = ".m.rule.poll_end"
|
||||
case msc3930pollEnd = ".org.matrix.msc3930.rule.poll_end"
|
||||
// poll ended event (one to one)
|
||||
case oneToOnePollEnd = ".m.rule.poll_end_one_to_one"
|
||||
case msc3930oneToOnePollEnd = ".org.matrix.msc3930.rule.poll_end_one_to_one"
|
||||
}
|
||||
|
||||
extension NotificationPushRuleId: Identifiable {
|
||||
@@ -65,6 +77,20 @@ extension NotificationPushRuleId {
|
||||
return VectorL10n.settingsEncryptedGroupMessages
|
||||
case .keywords:
|
||||
return VectorL10n.settingsMessagesContainingKeywords
|
||||
case .pollStart, .msc3930pollStart, .oneToOnePollStart, .msc3930oneToOnePollStart, .pollEnd, .msc3930pollEnd, .oneToOnePollEnd, .msc3930oneToOnePollEnd:
|
||||
// They don't need to be rendered on the UI
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
var syncedRules: [NotificationPushRuleId] {
|
||||
switch self {
|
||||
case .oneToOneRoom:
|
||||
return [.oneToOnePollStart, .msc3930oneToOnePollStart, .oneToOnePollEnd, .msc3930oneToOnePollEnd]
|
||||
case .allOtherMessages:
|
||||
return [.pollStart, .msc3930pollStart, .pollEnd, .msc3930pollEnd]
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,5 +19,13 @@ import Foundation
|
||||
protocol NotificationPushRuleType {
|
||||
var ruleId: String! { get }
|
||||
var enabled: Bool { get }
|
||||
var ruleActions: NotificationActions? { get }
|
||||
|
||||
func matches(standardActions: NotificationStandardActions?) -> Bool
|
||||
}
|
||||
|
||||
extension NotificationPushRuleType {
|
||||
var pushRuleId: NotificationPushRuleId? {
|
||||
ruleId.flatMap(NotificationPushRuleId.init(rawValue:))
|
||||
}
|
||||
}
|
||||
|
||||
+52
-13
@@ -44,7 +44,9 @@ class MXNotificationSettingsService: NotificationSettingsServiceType {
|
||||
|
||||
// Observe future updates to content rules
|
||||
rulesUpdated
|
||||
.compactMap { _ in self.session.notificationCenter.rules.global.content as? [MXPushRule] }
|
||||
.compactMap { [weak self] _ in
|
||||
self?.session.notificationCenter.rules.global.content as? [MXPushRule]
|
||||
}
|
||||
.assign(to: &$contentRules)
|
||||
|
||||
// Set initial value of rules
|
||||
@@ -53,14 +55,15 @@ class MXNotificationSettingsService: NotificationSettingsServiceType {
|
||||
}
|
||||
// Observe future updates to rules
|
||||
rulesUpdated
|
||||
.compactMap { _ in self.session.notificationCenter.flatRules as? [MXPushRule] }
|
||||
.compactMap { [weak self] _ in
|
||||
self?.session.notificationCenter.flatRules as? [MXPushRule]
|
||||
}
|
||||
.assign(to: &$rules)
|
||||
}
|
||||
|
||||
func add(keyword: String, enabled: Bool) {
|
||||
let index = NotificationIndex.index(when: enabled)
|
||||
guard let actions = NotificationPushRuleId.keywords.standardActions(for: index)?.actions
|
||||
else {
|
||||
guard let actions = NotificationPushRuleId.keywords.standardActions(for: index).actions else {
|
||||
return
|
||||
}
|
||||
session.notificationCenter.addContentRuleWithRuleId(matchingPattern: keyword, notify: actions.notify, sound: actions.sound, highlight: actions.highlight)
|
||||
@@ -71,16 +74,52 @@ class MXNotificationSettingsService: NotificationSettingsServiceType {
|
||||
session.notificationCenter.removeRule(rule)
|
||||
}
|
||||
|
||||
func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) {
|
||||
guard let rule = session.notificationCenter.rule(byId: ruleId) else { return }
|
||||
session.notificationCenter.enableRule(rule, isEnabled: enabled)
|
||||
func updatePushRuleActions(for ruleId: String,
|
||||
enabled: Bool,
|
||||
actions: NotificationActions?) async throws {
|
||||
|
||||
if let actions = actions {
|
||||
session.notificationCenter.updatePushRuleActions(ruleId,
|
||||
kind: rule.kind,
|
||||
notify: actions.notify,
|
||||
soundName: actions.sound,
|
||||
highlight: actions.highlight)
|
||||
guard let rule = session.notificationCenter.rule(byId: ruleId) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let actions = actions else {
|
||||
try await session.notificationCenter.enableRule(pushRule: rule, isEnabled: enabled)
|
||||
return
|
||||
}
|
||||
|
||||
// Updating the actions before enabling the rule allows the homeserver to triggers just one sync update
|
||||
try await session.notificationCenter.updatePushRuleActions(ruleId,
|
||||
kind: rule.kind,
|
||||
notify: actions.notify,
|
||||
soundName: actions.sound,
|
||||
highlight: actions.highlight)
|
||||
|
||||
try await session.notificationCenter.enableRule(pushRule: rule, isEnabled: enabled)
|
||||
}
|
||||
}
|
||||
|
||||
private extension MXNotificationCenter {
|
||||
func enableRule(pushRule: MXPushRule, isEnabled: Bool) async throws {
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||
enableRule(pushRule, isEnabled: isEnabled) { error in
|
||||
if let error = error {
|
||||
continuation.resume(with: .failure(error))
|
||||
} else {
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updatePushRuleActions(ruleId: String, kind: __MXPushRuleKind, notify: Bool, soundName: String, highlight: Bool) async throws {
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||
updatePushRuleActions(ruleId, kind: kind, notify: notify, soundName: soundName, highlight: highlight) { error in
|
||||
if let error = error {
|
||||
continuation.resume(with: .failure(error))
|
||||
} else {
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-1
@@ -44,5 +44,11 @@ class MockNotificationSettingsService: NotificationSettingsServiceType, Observab
|
||||
keywords.remove(keyword)
|
||||
}
|
||||
|
||||
func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) { }
|
||||
func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) async throws {
|
||||
guard let ruleIndex = rules.firstIndex(where: { $0.ruleId == ruleId }) else {
|
||||
return
|
||||
}
|
||||
|
||||
rules[ruleIndex] = MockNotificationPushRule(ruleId: ruleId, enabled: enabled, ruleActions: actions)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -40,5 +40,5 @@ protocol NotificationSettingsServiceType {
|
||||
/// - ruleId: The id of the rule.
|
||||
/// - enabled: Whether the rule should be enabled or disabled.
|
||||
/// - actions: The actions to update with.
|
||||
func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?)
|
||||
func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) async throws
|
||||
}
|
||||
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
//
|
||||
// Copyright 2023 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.
|
||||
//
|
||||
|
||||
@testable import RiotSwiftUI
|
||||
import XCTest
|
||||
|
||||
final class NotificationSettingsViewModelTests: XCTestCase {
|
||||
private var viewModel: NotificationSettingsViewModel!
|
||||
private var notificationService: MockNotificationSettingsService!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
notificationService = .init()
|
||||
}
|
||||
|
||||
func testAllTheRulesAreChecked() throws {
|
||||
viewModel = .init(notificationSettingsService: notificationService, ruleIds: .default)
|
||||
|
||||
XCTAssertEqual(viewModel.viewState.selectionState.count, 4)
|
||||
XCTAssertTrue(viewModel.viewState.selectionState.values.allSatisfy { $0 })
|
||||
}
|
||||
|
||||
func testUpdateRule() async {
|
||||
viewModel = .init(notificationSettingsService: notificationService, ruleIds: .default)
|
||||
notificationService.rules = [MockNotificationPushRule].default
|
||||
|
||||
await viewModel.update(ruleID: .encrypted, isChecked: false)
|
||||
XCTAssertEqual(viewModel.viewState.selectionState.count, 4)
|
||||
XCTAssertEqual(viewModel.viewState.selectionState[.encrypted], false)
|
||||
}
|
||||
|
||||
func testUpdateOneToOneRuleAlsoUpdatesPollRules() async {
|
||||
setupWithPollRules()
|
||||
|
||||
await viewModel.update(ruleID: .oneToOneRoom, isChecked: false)
|
||||
|
||||
XCTAssertEqual(viewModel.viewState.selectionState.count, 8)
|
||||
XCTAssertEqual(viewModel.viewState.selectionState[.oneToOneRoom], false)
|
||||
XCTAssertEqual(viewModel.viewState.selectionState[.oneToOnePollStart], false)
|
||||
XCTAssertEqual(viewModel.viewState.selectionState[.oneToOnePollEnd], false)
|
||||
|
||||
// unrelated poll rules stay the same
|
||||
XCTAssertEqual(viewModel.viewState.selectionState[.allOtherMessages], true)
|
||||
XCTAssertEqual(viewModel.viewState.selectionState[.pollStart], true)
|
||||
XCTAssertEqual(viewModel.viewState.selectionState[.pollEnd], true)
|
||||
}
|
||||
|
||||
func testUpdateMessageRuleAlsoUpdatesPollRules() async {
|
||||
setupWithPollRules()
|
||||
|
||||
await viewModel.update(ruleID: .allOtherMessages, isChecked: false)
|
||||
XCTAssertEqual(viewModel.viewState.selectionState.count, 8)
|
||||
XCTAssertEqual(viewModel.viewState.selectionState[.allOtherMessages], false)
|
||||
XCTAssertEqual(viewModel.viewState.selectionState[.pollStart], false)
|
||||
XCTAssertEqual(viewModel.viewState.selectionState[.pollEnd], false)
|
||||
|
||||
// unrelated poll rules stay the same
|
||||
XCTAssertEqual(viewModel.viewState.selectionState[.oneToOneRoom], true)
|
||||
XCTAssertEqual(viewModel.viewState.selectionState[.oneToOnePollStart], true)
|
||||
XCTAssertEqual(viewModel.viewState.selectionState[.oneToOnePollEnd], true)
|
||||
}
|
||||
|
||||
func testMismatchingRulesAreHandled() async {
|
||||
setupWithPollRules()
|
||||
|
||||
await viewModel.update(ruleID: .allOtherMessages, isChecked: false)
|
||||
|
||||
// simulating a "mismatch" on the poll started rule
|
||||
await viewModel.update(ruleID: .pollStart, isChecked: true)
|
||||
|
||||
XCTAssertEqual(viewModel.viewState.selectionState.count, 8)
|
||||
|
||||
// The other messages rule ui flag should match the loudest related poll rule
|
||||
XCTAssertEqual(viewModel.viewState.selectionState[.allOtherMessages], true)
|
||||
}
|
||||
|
||||
func testMismatchingOneToOneRulesAreHandled() async {
|
||||
setupWithPollRules()
|
||||
|
||||
await viewModel.update(ruleID: .oneToOneRoom, isChecked: false)
|
||||
// simulating a "mismatch" on the one to one poll started rule
|
||||
await viewModel.update(ruleID: .oneToOnePollStart, isChecked: true)
|
||||
|
||||
XCTAssertEqual(viewModel.viewState.selectionState.count, 8)
|
||||
|
||||
// The one to one room rule ui flag should match the loudest related poll rule
|
||||
XCTAssertEqual(viewModel.viewState.selectionState[.oneToOneRoom], true)
|
||||
|
||||
// the oneToOneRoom rule should be flagged as "out of sync"
|
||||
XCTAssertTrue(viewModel.isRuleOutOfSync(.oneToOneRoom))
|
||||
XCTAssertFalse(viewModel.isRuleOutOfSync(.allOtherMessages))
|
||||
}
|
||||
}
|
||||
|
||||
private extension NotificationSettingsViewModelTests {
|
||||
func setupWithPollRules() {
|
||||
viewModel = .init(notificationSettingsService: notificationService, ruleIds: .default + .polls)
|
||||
notificationService.rules = [MockNotificationPushRule].default + [MockNotificationPushRule].polls
|
||||
}
|
||||
}
|
||||
|
||||
private extension Array where Element == NotificationPushRuleId {
|
||||
static var `default`: [NotificationPushRuleId] {
|
||||
[.oneToOneRoom, .allOtherMessages, .oneToOneEncryptedRoom, .encrypted]
|
||||
}
|
||||
|
||||
static var polls: [NotificationPushRuleId] {
|
||||
[.pollStart, .pollEnd, .oneToOnePollStart, .oneToOnePollEnd]
|
||||
}
|
||||
}
|
||||
|
||||
private extension Array where Element == MockNotificationPushRule {
|
||||
static var `default`: [MockNotificationPushRule] {
|
||||
[NotificationPushRuleId]
|
||||
.default
|
||||
.map { ruleId in
|
||||
MockNotificationPushRule(ruleId: ruleId.rawValue, enabled: true)
|
||||
}
|
||||
}
|
||||
|
||||
static var polls: [MockNotificationPushRule] {
|
||||
[NotificationPushRuleId]
|
||||
.polls
|
||||
.map { ruleId in
|
||||
MockNotificationPushRule(ruleId: ruleId.rawValue, enabled: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import SwiftUI
|
||||
/// Also renders an optional bottom section.
|
||||
/// Used in the case of keywords, for the keyword chips and input.
|
||||
struct NotificationSettings<BottomSection: View>: View {
|
||||
@Environment(\.theme) var theme: ThemeSwiftUI
|
||||
@ObservedObject var viewModel: NotificationSettingsViewModel
|
||||
|
||||
var bottomSection: BottomSection?
|
||||
@@ -31,15 +32,28 @@ struct NotificationSettings<BottomSection: View>: View {
|
||||
header: FormSectionHeader(text: VectorL10n.settingsNotifyMeFor)
|
||||
) {
|
||||
ForEach(viewModel.viewState.ruleIds) { ruleId in
|
||||
let checked = viewModel.viewState.selectionState[ruleId] ?? false
|
||||
FormPickerItem(title: ruleId.title, selected: checked) {
|
||||
viewModel.update(ruleID: ruleId, isChecked: !checked)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
let checked = viewModel.viewState.selectionState[ruleId] ?? false
|
||||
FormPickerItem(title: ruleId.title, selected: checked) {
|
||||
Task {
|
||||
await viewModel.update(ruleID: ruleId, isChecked: !checked)
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.isRuleOutOfSync(ruleId) {
|
||||
Text(VectorL10n.settingsPushRulesError)
|
||||
.font(theme.fonts.caption1)
|
||||
.foregroundColor(theme.colors.alert)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
bottomSection
|
||||
}
|
||||
.activityIndicator(show: viewModel.viewState.saving)
|
||||
.disabled(viewModel.viewState.saving)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+154
-38
@@ -49,7 +49,9 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob
|
||||
|
||||
// Observe when the rules are updated, to subsequently update the state of the settings.
|
||||
notificationSettingsService.rulesPublisher
|
||||
.sink(receiveValue: rulesUpdated(newRules:))
|
||||
.sink { [weak self] newRules in
|
||||
self?.rulesUpdated(newRules: newRules)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
// Only observe keywords if the current settings view displays it.
|
||||
@@ -88,7 +90,9 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob
|
||||
// Keyword rules were updates, check if we need to update the setting.
|
||||
keywordsRules
|
||||
.map { $0.contains { $0.enabled } }
|
||||
.sink(receiveValue: keywordRuleUpdated(anyEnabled:))
|
||||
.sink { [weak self] in
|
||||
self?.keywordRuleUpdated(anyEnabled: $0)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
// Update the viewState with the final keywords to be displayed.
|
||||
@@ -105,35 +109,27 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func update(ruleID: NotificationPushRuleId, isChecked: Bool) {
|
||||
@MainActor
|
||||
func update(ruleID: NotificationPushRuleId, isChecked: Bool) async {
|
||||
let index = NotificationIndex.index(when: isChecked)
|
||||
if ruleID == .keywords {
|
||||
// Keywords is handled differently to other settings
|
||||
updateKeywords(isChecked: isChecked)
|
||||
return
|
||||
}
|
||||
// Get the static definition and update the actions and enabled state.
|
||||
guard let standardActions = ruleID.standardActions(for: index) else { return }
|
||||
let standardActions = ruleID.standardActions(for: index)
|
||||
let enabled = standardActions != .disabled
|
||||
notificationSettingsService.updatePushRuleActions(
|
||||
for: ruleID.rawValue,
|
||||
enabled: enabled,
|
||||
actions: standardActions.actions
|
||||
)
|
||||
}
|
||||
|
||||
private func updateKeywords(isChecked: Bool) {
|
||||
guard !keywordsOrdered.isEmpty else {
|
||||
viewState.selectionState[.keywords]?.toggle()
|
||||
return
|
||||
}
|
||||
// Get the static definition and update the actions and enabled state for every keyword.
|
||||
let index = NotificationIndex.index(when: isChecked)
|
||||
guard let standardActions = NotificationPushRuleId.keywords.standardActions(for: index) else { return }
|
||||
let enabled = standardActions != .disabled
|
||||
keywordsOrdered.forEach { keyword in
|
||||
notificationSettingsService.updatePushRuleActions(
|
||||
for: keyword,
|
||||
|
||||
switch ruleID {
|
||||
case .keywords: // Keywords is handled differently to other settings
|
||||
await updateKeywords(isChecked: isChecked)
|
||||
|
||||
case .oneToOneRoom, .allOtherMessages:
|
||||
await updatePushAction(
|
||||
id: ruleID,
|
||||
enabled: enabled,
|
||||
standardActions: standardActions,
|
||||
then: ruleID.syncedRules
|
||||
)
|
||||
|
||||
default:
|
||||
try? await notificationSettingsService.updatePushRuleActions(
|
||||
for: ruleID.rawValue,
|
||||
enabled: enabled,
|
||||
actions: standardActions.actions
|
||||
)
|
||||
@@ -152,17 +148,94 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob
|
||||
notificationSettingsService.remove(keyword: keyword)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
func isRuleOutOfSync(_ ruleId: NotificationPushRuleId) -> Bool {
|
||||
viewState.outOfSyncRules.contains(ruleId) && viewState.saving == false
|
||||
}
|
||||
}
|
||||
|
||||
private func rulesUpdated(newRules: [NotificationPushRuleType]) {
|
||||
for rule in newRules {
|
||||
guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId),
|
||||
ruleIds.contains(ruleId) else { continue }
|
||||
viewState.selectionState[ruleId] = isChecked(rule: rule)
|
||||
// MARK: - Private
|
||||
|
||||
private extension NotificationSettingsViewModel {
|
||||
@MainActor
|
||||
func updateKeywords(isChecked: Bool) async {
|
||||
guard !keywordsOrdered.isEmpty else {
|
||||
viewState.selectionState[.keywords]?.toggle()
|
||||
return
|
||||
}
|
||||
|
||||
// Get the static definition and update the actions and enabled state for every keyword.
|
||||
let index = NotificationIndex.index(when: isChecked)
|
||||
let standardActions = NotificationPushRuleId.keywords.standardActions(for: index)
|
||||
let enabled = standardActions != .disabled
|
||||
let keywordsToUpdate = keywordsOrdered
|
||||
|
||||
await withThrowingTaskGroup(of: Void.self) { group in
|
||||
for keyword in keywordsToUpdate {
|
||||
group.addTask {
|
||||
try await self.notificationSettingsService.updatePushRuleActions(
|
||||
for: keyword,
|
||||
enabled: enabled,
|
||||
actions: standardActions.actions
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updatePushAction(id: NotificationPushRuleId,
|
||||
enabled: Bool,
|
||||
standardActions: NotificationStandardActions,
|
||||
then rules: [NotificationPushRuleId]) async {
|
||||
await MainActor.run {
|
||||
viewState.saving = true
|
||||
}
|
||||
|
||||
do {
|
||||
// update the 'parent rule' first
|
||||
try await notificationSettingsService.updatePushRuleActions(for: id.rawValue, enabled: enabled, actions: standardActions.actions)
|
||||
|
||||
// synchronize all the 'children rules' with the parent rule
|
||||
await withThrowingTaskGroup(of: Void.self) { group in
|
||||
for ruleId in rules {
|
||||
group.addTask {
|
||||
try await self.notificationSettingsService.updatePushRuleActions(for: ruleId.rawValue, enabled: enabled, actions: standardActions.actions)
|
||||
}
|
||||
}
|
||||
}
|
||||
await completeUpdate()
|
||||
} catch {
|
||||
await completeUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
private func keywordRuleUpdated(anyEnabled: Bool) {
|
||||
@MainActor
|
||||
func completeUpdate() {
|
||||
viewState.saving = false
|
||||
}
|
||||
|
||||
func rulesUpdated(newRules: [NotificationPushRuleType]) {
|
||||
var outOfSyncRules: Set<NotificationPushRuleId> = .init()
|
||||
|
||||
for rule in newRules {
|
||||
guard
|
||||
let ruleId = rule.pushRuleId,
|
||||
ruleIds.contains(ruleId)
|
||||
else {
|
||||
continue
|
||||
}
|
||||
|
||||
let relatedSyncedRules = ruleId.syncedRules(in: newRules)
|
||||
viewState.selectionState[ruleId] = isChecked(rule: rule, syncedRules: relatedSyncedRules)
|
||||
|
||||
if isOutOfSync(rule: rule, syncedRules: relatedSyncedRules) {
|
||||
outOfSyncRules.insert(ruleId)
|
||||
}
|
||||
}
|
||||
|
||||
viewState.outOfSyncRules = outOfSyncRules
|
||||
}
|
||||
|
||||
func keywordRuleUpdated(anyEnabled: Bool) {
|
||||
if !keywordsOrdered.isEmpty {
|
||||
viewState.selectionState[.keywords] = anyEnabled
|
||||
}
|
||||
@@ -174,8 +247,10 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob
|
||||
/// The same logic is used on android.
|
||||
/// - Parameter rule: The push rule type to check.
|
||||
/// - Returns: Wether it should be displayed as checked or not checked.
|
||||
private func isChecked(rule: NotificationPushRuleType) -> Bool {
|
||||
guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId) else { return false }
|
||||
func defaultIsChecked(rule: NotificationPushRuleType) -> Bool {
|
||||
guard let ruleId = rule.pushRuleId else {
|
||||
return false
|
||||
}
|
||||
|
||||
let firstIndex = NotificationIndex.allCases.first { nextIndex in
|
||||
rule.matches(standardActions: ruleId.standardActions(for: nextIndex))
|
||||
@@ -187,4 +262,45 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob
|
||||
|
||||
return index.enabled
|
||||
}
|
||||
|
||||
func isChecked(rule: NotificationPushRuleType, syncedRules: [NotificationPushRuleType]) -> Bool {
|
||||
guard let ruleId = rule.pushRuleId else {
|
||||
return false
|
||||
}
|
||||
|
||||
switch ruleId {
|
||||
case .oneToOneRoom, .allOtherMessages:
|
||||
let ruleIsChecked = defaultIsChecked(rule: rule)
|
||||
let someSyncedRuleIsChecked = syncedRules.contains(where: { defaultIsChecked(rule: $0) })
|
||||
// The "loudest" rule will be applied when there is a clash between a rule and its dependent rules.
|
||||
return ruleIsChecked || someSyncedRuleIsChecked
|
||||
default:
|
||||
return defaultIsChecked(rule: rule)
|
||||
}
|
||||
}
|
||||
|
||||
func isOutOfSync(rule: NotificationPushRuleType, syncedRules: [NotificationPushRuleType]) -> Bool {
|
||||
guard let ruleId = rule.pushRuleId else {
|
||||
return false
|
||||
}
|
||||
|
||||
switch ruleId {
|
||||
case .oneToOneRoom, .allOtherMessages:
|
||||
let ruleIsChecked = defaultIsChecked(rule: rule)
|
||||
return syncedRules.contains(where: { defaultIsChecked(rule: $0) != ruleIsChecked })
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationPushRuleId {
|
||||
func syncedRules(in rules: [NotificationPushRuleType]) -> [NotificationPushRuleType] {
|
||||
rules.filter {
|
||||
guard let ruleId = $0.pushRuleId else {
|
||||
return false
|
||||
}
|
||||
return syncedRules.contains(ruleId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -22,5 +22,6 @@ struct NotificationSettingsViewState {
|
||||
var saving: Bool
|
||||
var ruleIds: [NotificationPushRuleId]
|
||||
var selectionState: [NotificationPushRuleId: Bool]
|
||||
var outOfSyncRules: Set<NotificationPushRuleId> = .init()
|
||||
var keywords = [String]()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user