move full module and reorganize folders

This commit is contained in:
David Langley
2021-09-02 10:00:59 +01:00
parent 9849534e81
commit 2a9c06fad1
10 changed files with 48 additions and 29 deletions
@@ -0,0 +1,100 @@
//
// Copyright 2021 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
@objc protocol RoomNotificationSettingsCoordinatorBridgePresenterDelegate {
func roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: RoomNotificationSettingsCoordinatorBridgePresenter)
}
/// RoomNotificationSettingsCoordinatorBridgePresenter enables to start RoomNotificationSettingsCoordinator from a view controller.
/// This bridge is used while waiting for global usage of coordinator pattern.
/// It breaks the Coordinator abstraction and it has been introduced for Objective-C compatibility (mainly for integration in legacy view controllers).
/// Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator.
@objcMembers
final class RoomNotificationSettingsCoordinatorBridgePresenter: NSObject {
// MARK: - Properties
// MARK: Private
private let room: MXRoom
private var coordinator: RoomNotificationSettingsCoordinator?
// MARK: Public
weak var delegate: RoomNotificationSettingsCoordinatorBridgePresenterDelegate?
// MARK: - Setup
init(room: MXRoom) {
self.room = room
super.init()
}
// MARK: - Public
// NOTE: Default value feature is not compatible with Objective-C.
// func present(from viewController: UIViewController, animated: Bool) {
// self.present(from: viewController, animated: animated)
// }
func present(from viewController: UIViewController, animated: Bool) {
let roomNotificationSettingsCoordinator = RoomNotificationSettingsCoordinator(room: room)
roomNotificationSettingsCoordinator.delegate = self
let presentable = roomNotificationSettingsCoordinator.toPresentable()
let navigationController = RiotNavigationController(rootViewController: presentable)
navigationController.modalPresentationStyle = .formSheet
presentable.presentationController?.delegate = self
viewController.present(navigationController, animated: animated, completion: nil)
roomNotificationSettingsCoordinator.start()
self.coordinator = roomNotificationSettingsCoordinator
}
func dismiss(animated: Bool, completion: (() -> Void)?) {
guard let coordinator = self.coordinator else {
return
}
coordinator.toPresentable().dismiss(animated: animated) {
self.coordinator = nil
if let completion = completion {
completion()
}
}
}
}
// MARK: - RoomNotificationSettingsCoordinatorDelegate
extension RoomNotificationSettingsCoordinatorBridgePresenter: RoomNotificationSettingsCoordinatorDelegate {
func roomNotificationSettingsCoordinatorDidCancel(_ coordinator: RoomNotificationSettingsCoordinatorType) {
self.delegate?.roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self)
}
func roomNotificationSettingsCoordinatorDidComplete(_ coordinator: RoomNotificationSettingsCoordinatorType) {
self.delegate?.roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self)
}
}
// MARK: - UIAdaptivePresentationControllerDelegate
extension RoomNotificationSettingsCoordinatorBridgePresenter: UIAdaptivePresentationControllerDelegate {
func roomNotificationSettingsCoordinatorDidComplete(_ presentationController: UIPresentationController) {
self.delegate?.roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self)
}
}
@@ -0,0 +1,106 @@
// File created from ScreenTemplate
// $ createScreen.sh Room/NotificationSettings RoomNotificationSettings
/*
Copyright 2021 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
import UIKit
import SwiftUI
final class RoomNotificationSettingsCoordinator: RoomNotificationSettingsCoordinatorType {
// MARK: - Properties
// MARK: Private
private var roomNotificationSettingsViewModel: RoomNotificationSettingsViewModelType
private let roomNotificationSettingsViewController: UIViewController
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
weak var delegate: RoomNotificationSettingsCoordinatorDelegate?
// MARK: - Setup
init(room: MXRoom, presentedModally: Bool = true) {
let roomNotificationService = RoomNotificationSettingsService(room: room)
let avatarData: AvatarType?
let showAvatar = presentedModally
if #available(iOS 14.0.0, *) {
avatarData = showAvatar ? AvatarInput(
mxContentUri: room.summary.avatar,
matrixItemId: room.roomId,
displayName: room.summary.displayname
) : nil
} else {
avatarData = showAvatar ? RoomAvatarViewData(
roomId: room.roomId,
displayName: room.summary.displayname,
avatarUrl: room.summary.avatar,
mediaManager: room.mxSession.mediaManager
) : nil
}
let viewModel: RoomNotificationSettingsViewModel
let viewController: UIViewController
if #available(iOS 14.0.0, *) {
let swiftUIViewModel = RoomNotificationSettingsSwiftUIViewModel(
roomNotificationService: roomNotificationService,
avatarData: avatarData,
displayName: room.summary.displayname,
roomEncrypted: room.summary.isEncrypted)
let avatarService: AvatarServiceType = AvatarService(mediaManager: room.mxSession.mediaManager)
let view = RoomNotificationSettings(viewModel: swiftUIViewModel, presentedModally: presentedModally)
.addDependency(avatarService)
let host = VectorHostingController(rootView: view)
viewModel = swiftUIViewModel
viewController = host
} else {
viewModel = RoomNotificationSettingsViewModel(
roomNotificationService: roomNotificationService,
avatarData: avatarData,
displayName: room.summary.displayname,
roomEncrypted: room.summary.isEncrypted)
viewController = RoomNotificationSettingsViewController.instantiate(with: viewModel)
}
self.roomNotificationSettingsViewModel = viewModel
self.roomNotificationSettingsViewController = viewController
}
// MARK: - Public methods
func start() {
self.roomNotificationSettingsViewModel.coordinatorDelegate = self
}
func toPresentable() -> UIViewController {
return self.roomNotificationSettingsViewController
}
}
// MARK: - RoomNotificationSettingsViewModelCoordinatorDelegate
extension RoomNotificationSettingsCoordinator: RoomNotificationSettingsViewModelCoordinatorDelegate {
func roomNotificationSettingsViewModelDidComplete(_ viewModel: RoomNotificationSettingsViewModelType) {
self.delegate?.roomNotificationSettingsCoordinatorDidComplete(self)
}
func roomNotificationSettingsViewModelDidCancel(_ viewModel: RoomNotificationSettingsViewModelType) {
self.delegate?.roomNotificationSettingsCoordinatorDidCancel(self)
}
}
@@ -0,0 +1,29 @@
// File created from ScreenTemplate
// $ createScreen.sh Room/NotificationSettings RoomNotificationSettings
/*
Copyright 2021 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
protocol RoomNotificationSettingsCoordinatorDelegate: AnyObject {
func roomNotificationSettingsCoordinatorDidComplete(_ coordinator: RoomNotificationSettingsCoordinatorType)
func roomNotificationSettingsCoordinatorDidCancel(_ coordinator: RoomNotificationSettingsCoordinatorType)
}
/// `RoomNotificationSettingsCoordinatorType` is a protocol describing a Coordinator that handles changes to the room navigation settings navigation flow.
protocol RoomNotificationSettingsCoordinatorType: Coordinator, Presentable {
var delegate: RoomNotificationSettingsCoordinatorDelegate? { get }
}
@@ -0,0 +1,43 @@
//
// Copyright 2021 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
/// RoomNotificationSettingsViewController view state
struct RoomNotificationSettingsViewState: RoomNotificationSettingsViewStateType {
let roomEncrypted: Bool
var saving: Bool
var notificationState: RoomNotificationState
var avatarData: AvatarType?
var displayName: String?
}
extension RoomNotificationSettingsViewState {
var notificationOptions: [RoomNotificationState] {
if roomEncrypted {
return [.all, .mute]
} else {
return RoomNotificationState.allCases
}
}
}
extension RoomNotificationSettingsViewState {
var roomEncryptedString: String {
roomEncrypted ? VectorL10n.roomNotifsSettingsEncryptedRoomNotice : ""
}
}
@@ -18,25 +18,6 @@
import Foundation
/// RoomNotificationSettingsViewController view state
struct RoomNotificationSettingsViewState: RoomNotificationSettingsViewStateType {
let roomEncrypted: Bool
var saving: Bool
var notificationState: RoomNotificationState
var avatarData: AvatarType?
var displayName: String?
}
extension RoomNotificationSettingsViewState {
var notificationOptions: [RoomNotificationState] {
if roomEncrypted {
return [.all, .mute]
} else {
return RoomNotificationState.allCases
}
}
}
protocol RoomNotificationSettingsViewStateType {
var saving: Bool { get }
var roomEncrypted: Bool { get }
@@ -46,8 +27,3 @@ protocol RoomNotificationSettingsViewStateType {
var displayName: String? { get }
}
extension RoomNotificationSettingsViewState {
var roomEncryptedString: String {
roomEncrypted ? VectorL10n.roomNotifsSettingsEncryptedRoomNotice : ""
}
}
@@ -0,0 +1,331 @@
//
// Copyright 2021 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
final class RoomNotificationSettingsService: RoomNotificationSettingsServiceType {
typealias Completion = () -> Void
// MARK: - Properties
// MARK: Private
private let room: MXRoom
private var notificationCenterDidUpdateObserver: NSObjectProtocol?
private var notificationCenterDidFailObserver: NSObjectProtocol?
private var observers: [ObjectIdentifier] = []
// MARK: Public
var notificationState: RoomNotificationState {
room.notificationState
}
// MARK: - Setup
init(room: MXRoom) {
self.room = room
}
deinit {
observers.forEach(NotificationCenter.default.removeObserver)
}
// MARK: - Public
func observeNotificationState(listener: @escaping RoomNotificationStateCallback) {
let observer = NotificationCenter.default.addObserver(
forName: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules),
object: nil,
queue: OperationQueue.main) { [weak self] _ in
guard let self = self else { return }
listener(self.room.notificationState)
}
observers += [ObjectIdentifier(observer)]
}
func update(state: RoomNotificationState, completion: @escaping Completion) {
switch state {
case .all:
allMessages(completion: completion)
case .mentionsAndKeywordsOnly:
mentionsOnly(completion: completion)
case .mute:
mute(completion: completion)
}
}
// MARK: - Private
private func mute(completion: @escaping Completion) {
guard !room.isMuted else {
completion()
return
}
if let rule = room.roomPushRule {
removePushRule(rule: rule) {
self.mute(completion: completion)
}
return
}
guard let rule = room.overridePushRule else {
self.addPushRuleToMute(completion: completion)
return
}
guard notificationCenterDidUpdateObserver == nil else {
MXLog.debug("[RoomNotificationSettingsService] Request in progress: ignore push rule update")
completion()
return
}
// if the user defined one, use it
if rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify) {
enablePushRule(rule: rule, completion: completion)
} else {
removePushRule(rule: rule) {
self.addPushRuleToMute(completion: completion)
}
}
}
private func mentionsOnly(completion: @escaping Completion) {
guard !room.isMentionsOnly else {
completion()
return
}
if let rule = room.overridePushRule, room.isMuted {
removePushRule(rule: rule) {
self.mentionsOnly(completion: completion)
}
return
}
guard let rule = room.roomPushRule else {
addPushRuleToMentionOnly(completion: completion)
return
}
guard notificationCenterDidUpdateObserver == nil else {
MXLog.debug("[MXRoom+Riot] Request in progress: ignore push rule update")
completion()
return
}
// if the user defined one, use it
if rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify) {
enablePushRule(rule: rule, completion: completion)
} else {
removePushRule(rule: rule) {
self.addPushRuleToMentionOnly(completion: completion)
}
}
}
private func allMessages(completion: @escaping Completion) {
if !room.isMentionsOnly && !room.isMuted {
completion()
return
}
if let rule = room.overridePushRule, room.isMuted {
removePushRule(rule: rule) {
self.allMessages(completion: completion)
}
return
}
if let rule = room.roomPushRule, room.isMentionsOnly {
removePushRule(rule: rule, completion: completion)
}
}
private func addPushRuleToMentionOnly(completion: @escaping Completion) {
handleUpdateCallback(completion) { [weak self] in
guard let self = self else { return true }
return self.room.roomPushRule != nil
}
handleFailureCallback(completion)
room.mxSession.notificationCenter.addRoomRule(
room.roomId,
notify: false,
sound: false,
highlight: false)
}
private func addPushRuleToMute(completion: @escaping Completion) {
guard let roomId = room.roomId else {
return
}
handleUpdateCallback(completion) { [weak self] in
guard let self = self else { return true }
return self.room.overridePushRule != nil
}
handleFailureCallback(completion)
room.mxSession.notificationCenter.addOverrideRule(
withId: roomId,
conditions: [["kind": "event_match", "key": "room_id", "pattern": roomId]],
notify: false,
sound: false,
highlight: false
)
}
private func removePushRule(rule: MXPushRule, completion: @escaping Completion) {
handleUpdateCallback(completion) { [weak self] in
guard let self = self else { return true }
return self.room.mxSession.notificationCenter.rule(byId: rule.ruleId) == nil
}
handleFailureCallback(completion)
room.mxSession.notificationCenter.removeRule(rule)
}
private func enablePushRule(rule: MXPushRule, completion: @escaping Completion) {
handleUpdateCallback(completion) {
// No way to check whether this notification concerns the push rule. Consider the change is applied.
return true
}
handleFailureCallback(completion)
room.mxSession.notificationCenter.enableRule(rule, isEnabled: true)
}
private func handleUpdateCallback(_ completion: @escaping Completion, releaseCheck: @escaping () -> Bool) {
notificationCenterDidUpdateObserver = NotificationCenter.default.addObserver(
forName: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules),
object: nil,
queue: OperationQueue.main) { [weak self] _ in
guard let self = self else { return }
if releaseCheck() {
self.removeObservers()
completion()
}
}
}
private func handleFailureCallback(_ completion: @escaping Completion) {
notificationCenterDidFailObserver = NotificationCenter.default.addObserver(
forName: NSNotification.Name(rawValue: kMXNotificationCenterDidFailRulesUpdate),
object: nil,
queue: OperationQueue.main) { [weak self] _ in
guard let self = self else { return }
self.removeObservers()
completion()
}
}
func removeObservers() {
if let observer = self.notificationCenterDidUpdateObserver {
NotificationCenter.default.removeObserver(observer)
self.notificationCenterDidUpdateObserver = nil
}
if let observer = self.notificationCenterDidFailObserver {
NotificationCenter.default.removeObserver(observer)
self.notificationCenterDidFailObserver = nil
}
}
}
// We could move these to their own file and make available in global namespace or move to sdk but they are only used here at the moment
fileprivate extension MXRoom {
typealias Completion = () -> Void
func getRoomRule(from rules: [Any]) -> MXPushRule? {
guard let pushRules = rules as? [MXPushRule] else {
return nil
}
return pushRules.first(where: { self.roomId == $0.ruleId })
}
var overridePushRule: MXPushRule? {
guard let overrideRules = mxSession.notificationCenter.rules.global.override else {
return nil
}
return getRoomRule(from: overrideRules)
}
var roomPushRule: MXPushRule? {
guard let roomRules = mxSession.notificationCenter.rules.global.room else {
return nil
}
return getRoomRule(from: roomRules)
}
var notificationState: RoomNotificationState {
if isMuted {
return .mute
}
if isMentionsOnly {
return .mentionsAndKeywordsOnly
}
return .all
}
var isMuted: Bool {
// Check whether an override rule has been defined with the roomm id as rule id.
// This kind of rule is created to mute the room
guard let rule = self.overridePushRule,
rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify),
rule.conditionIsEnabled(kind: .eventMatch, for: roomId) else {
return false
}
return rule.enabled
}
var isMentionsOnly: Bool {
// Check push rules at room level
guard let rule = roomPushRule else { return false }
return rule.enabled && rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify)
}
}
fileprivate extension MXPushRule {
func actionsContains(actionType: MXPushRuleActionType) -> Bool {
guard let actions = actions as? [MXPushRuleAction] else {
return false
}
return actions.contains(where: { $0.actionType == actionType })
}
func conditionIsEnabled(kind: MXPushRuleConditionType, for roomId: String) -> Bool {
guard let conditions = conditions as? [MXPushRuleCondition] else {
return false
}
let ruleContainsCondition = conditions.contains { condition in
guard case kind = MXPushRuleConditionType(identifier: condition.kind),
let key = condition.parameters["key"] as? String,
let pattern = condition.parameters["pattern"] as? String
else { return false }
return key == "room_id" && pattern == roomId
}
return ruleContainsCondition && enabled
}
}