Merge branch 'develop' into doug/4479_media_size_selection

# Conflicts:
#	Riot/Assets/en.lproj/Vector.strings
This commit is contained in:
Doug
2021-08-27 16:30:01 +01:00
77 changed files with 2762 additions and 111 deletions
@@ -212,9 +212,7 @@ final class SettingsDiscoveryThreePidDetailsViewController: UIViewController {
}
private func renderLoading() {
if self.activityPresenter.isPresenting == false {
self.activityPresenter.presentActivityIndicator(on: self.view, animated: true)
}
self.activityPresenter.presentActivityIndicator(on: self.view, animated: true)
self.operationButton.isEnabled = false
}
@@ -0,0 +1,51 @@
//
// 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 Combine
@available(iOS 14.0, *)
class MockNotificationSettingsService: NotificationSettingsServiceType, ObservableObject {
static let example = MockNotificationSettingsService()
@Published var keywords = Set<String>()
@Published var rules = [MXPushRule]()
@Published var contentRules = [MXPushRule]()
var contentRulesPublisher: AnyPublisher<[MXPushRule], Never> {
$contentRules.eraseToAnyPublisher()
}
var keywordsPublisher: AnyPublisher<Set<String>, Never> {
$keywords.eraseToAnyPublisher()
}
var rulesPublisher: AnyPublisher<[MXPushRule], Never> {
$rules.eraseToAnyPublisher()
}
func add(keyword: String, enabled: Bool) {
keywords.insert(keyword)
}
func remove(keyword: String) {
keywords.remove(keyword)
}
func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) {
}
}
@@ -0,0 +1,32 @@
//
// 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
/**
The actions defined on a push rule, used in the static push rule definitions.
*/
struct NotificationActions {
let notify: Bool
let highlight: Bool
let sound: String?
init(notify: Bool, highlight: Bool = false, sound: String? = nil) {
self.notify = notify
self.highlight = highlight
self.sound = sound
}
}
@@ -0,0 +1,46 @@
//
// 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
/**
Index that determines the state of the push setting.
Silent case is un-unsed on iOS but keeping in for consistency of
definition across the platforms.
*/
enum NotificationIndex {
case off
case silent
case noisy
}
extension NotificationIndex: CaseIterable { }
extension NotificationIndex {
/**
Used to map the on/off checkmarks to an index used in the static push rule definitions.
*/
static func index(when enabled: Bool) -> NotificationIndex {
return enabled ? .noisy : .off
}
/**
Used to map from the checked state back to the index.
*/
var enabled: Bool {
return self != .off
}
}
@@ -0,0 +1,73 @@
//
// 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
/**
The push rule ids used in notification settings and the static rule definitions.
*/
enum NotificationPushRuleId: String {
case suppressBots = ".m.rule.suppress_notices"
case inviteMe = ".m.rule.invite_for_me"
case containDisplayName = ".m.rule.contains_display_name"
case tombstone = ".m.rule.tombstone"
case roomNotif = ".m.rule.roomnotif"
case containUserName = ".m.rule.contains_user_name"
case call = ".m.rule.call"
case oneToOneEncryptedRoom = ".m.rule.encrypted_room_one_to_one"
case oneToOneRoom = ".m.rule.room_one_to_one"
case allOtherMessages = ".m.rule.message"
case encrypted = ".m.rule.encrypted"
case keywords = "_keywords"
}
extension NotificationPushRuleId: Identifiable {
var id: String {
rawValue
}
}
extension NotificationPushRuleId {
var title: String {
switch self {
case .suppressBots:
return VectorL10n.settingsMessagesByABot
case .inviteMe:
return VectorL10n.settingsRoomInvitations
case .containDisplayName:
return VectorL10n.settingsMessagesContainingDisplayName
case .tombstone:
return VectorL10n.settingsRoomUpgrades
case .roomNotif:
return VectorL10n.settingsMessagesContainingAtRoom
case .containUserName:
return VectorL10n.settingsMessagesContainingUserName
case .call:
return VectorL10n.settingsCallInvitations
case .oneToOneEncryptedRoom:
return VectorL10n.settingsEncryptedDirectMessages
case .oneToOneRoom:
return VectorL10n.settingsDirectMessages
case .allOtherMessages:
return VectorL10n.settingsGroupMessages
case .encrypted:
return VectorL10n.settingsEncryptedGroupMessages
case .keywords:
return VectorL10n.settingsMessagesContainingKeywords
}
}
}
@@ -0,0 +1,48 @@
//
// 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
/**
The notification settings screen definitions, used when calling the coordinator.
*/
@objc enum NotificationSettingsScreen: Int {
case defaultNotifications
case mentionsAndKeywords
case other
}
extension NotificationSettingsScreen: CaseIterable { }
extension NotificationSettingsScreen: Identifiable {
var id: Int { self.rawValue }
}
extension NotificationSettingsScreen {
/**
Defines which rules are handled by each of the screens.
*/
var pushRules: [NotificationPushRuleId] {
switch self {
case .defaultNotifications:
return [.oneToOneRoom, .allOtherMessages, .oneToOneEncryptedRoom, .encrypted]
case .mentionsAndKeywords:
return [.containDisplayName, .containUserName, .roomNotif, .keywords]
case .other:
return [.inviteMe, .call, .suppressBots, .tombstone]
}
}
}
@@ -0,0 +1,101 @@
//
// 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
extension NotificationPushRuleId {
/**
A static definition of the push rule actions.
It is defined similarly across Web and Android.
*/
func standardActions(for index: NotificationIndex) -> NotificationStandardActions? {
switch self {
case .containDisplayName:
switch index {
case .off: return .disabled
case .silent: return .notify
case .noisy: return .highlightDefaultSound
}
case .containUserName:
switch index {
case .off: return .disabled
case .silent: return .notify
case .noisy: return .highlightDefaultSound
}
case .roomNotif:
switch index {
case .off: return .disabled
case .silent: return .notify
case .noisy: return .highlight
}
case .oneToOneRoom:
switch index {
case .off: return .dontNotify
case .silent: return .notify
case .noisy: return .notifyDefaultSound
}
case .oneToOneEncryptedRoom:
switch index {
case .off: return .dontNotify
case .silent: return .notify
case .noisy: return .notifyDefaultSound
}
case .allOtherMessages:
switch index {
case .off: return .dontNotify
case .silent: return .notify
case .noisy: return .notifyDefaultSound
}
case .encrypted:
switch index {
case .off: return .dontNotify
case .silent: return .notify
case .noisy: return .notifyDefaultSound
}
case .inviteMe:
switch index {
case .off: return .disabled
case .silent: return .notify
case .noisy: return .notifyDefaultSound
}
case .call:
switch index {
case .off: return .disabled
case .silent: return .notify
case .noisy: return .notifyRingSound
}
case .suppressBots:
switch index {
case .off: return .dontNotify
case .silent: return .disabled
case .noisy: return .notifyDefaultSound
}
case .tombstone:
switch index {
case .off: return .disabled
case .silent: return .notify
case .noisy: return .highlight
}
case .keywords:
switch index {
case .off: return .disabled
case .silent: return .notify
case .noisy: return .highlightDefaultSound
}
}
}
}
@@ -0,0 +1,50 @@
//
// 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
/**
A static definition of the different actions that can be defined on push rules.
It is defined similarly across Web and Android.
*/
enum NotificationStandardActions {
case notify
case notifyDefaultSound
case notifyRingSound
case highlight
case highlightDefaultSound
case dontNotify
case disabled
var actions: NotificationActions? {
switch self {
case .notify:
return NotificationActions(notify: true)
case .notifyDefaultSound:
return NotificationActions(notify: true, sound: "default")
case .notifyRingSound:
return NotificationActions(notify: true, sound: "ring")
case .highlight:
return NotificationActions(notify: true, highlight: true)
case .highlightDefaultSound:
return NotificationActions(notify: true, highlight: true, sound: "default")
case .dontNotify:
return NotificationActions(notify: false)
case .disabled:
return nil
}
}
}
@@ -0,0 +1,101 @@
//
// 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
@available(iOS 14.0, *)
@objc protocol NotificationSettingsCoordinatorBridgePresenterDelegate {
func notificationSettingsCoordinatorBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: NotificationSettingsCoordinatorBridgePresenter)
}
/// NotificationSettingsCoordinatorBridgePresenter enables to start NotificationSettingsCoordinator 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.
@available(iOS 14.0, *)
@objcMembers
final class NotificationSettingsCoordinatorBridgePresenter: NSObject {
// MARK: - Properties
// MARK: Private
private let session: MXSession
private var coordinator: NotificationSettingsCoordinator?
private var router: NavigationRouter?
// MARK: Public
weak var delegate: NotificationSettingsCoordinatorBridgePresenterDelegate?
// MARK: - Setup
init(session: MXSession) {
self.session = session
super.init()
}
// MARK: - Public
func push(from navigationController: UINavigationController, animated: Bool, screen: NotificationSettingsScreen, popCompletion: (() -> Void)?) {
let router = NavigationRouter(navigationController: navigationController)
let notificationSettingsCoordinator = NotificationSettingsCoordinator(session: session, screen: screen)
router.push(notificationSettingsCoordinator, animated: animated) { [weak self] in
self?.coordinator = nil
self?.router = nil
popCompletion?()
}
notificationSettingsCoordinator.start()
self.coordinator = notificationSettingsCoordinator
self.router = router
}
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: - NotificationSettingsCoordinatorDelegate
@available(iOS 14.0, *)
extension NotificationSettingsCoordinatorBridgePresenter: NotificationSettingsCoordinatorDelegate {
func notificationSettingsCoordinatorDidComplete(_ coordinator: NotificationSettingsCoordinatorType) {
self.delegate?.notificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self)
}
}
// MARK: - UIAdaptivePresentationControllerDelegate
@available(iOS 14.0, *)
extension NotificationSettingsCoordinatorBridgePresenter: UIAdaptivePresentationControllerDelegate {
func notificationSettingsCoordinatorDidComplete(_ presentationController: UIPresentationController) {
self.delegate?.notificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self)
}
}
@@ -0,0 +1,76 @@
// File created from ScreenTemplate
// $ createScreen.sh Settings/Notifications NotificationSettings
/*
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 SwiftUI
@available(iOS 14.0, *)
final class NotificationSettingsCoordinator: NotificationSettingsCoordinatorType {
// MARK: - Properties
// MARK: Private
private let session: MXSession
private var notificationSettingsViewModel: NotificationSettingsViewModelType
private let notificationSettingsViewController: UIViewController
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
weak var delegate: NotificationSettingsCoordinatorDelegate?
// MARK: - Setup
init(session: MXSession, screen: NotificationSettingsScreen) {
self.session = session
let notificationSettingsService = NotificationSettingsService(session: session)
let viewModel = NotificationSettingsViewModel(notificationSettingsService: notificationSettingsService, ruleIds: screen.pushRules)
let viewController: UIViewController
switch screen {
case .defaultNotifications:
viewController = VectorHostingController(rootView: DefaultNotificationSettings(viewModel: viewModel))
case .mentionsAndKeywords:
viewController = VectorHostingController(rootView: MentionsAndKeywordNotificationSettings(viewModel: viewModel))
case .other:
viewController = VectorHostingController(rootView: OtherNotificationSettings(viewModel: viewModel))
}
self.notificationSettingsViewModel = viewModel
self.notificationSettingsViewController = viewController
}
// MARK: - Public methods
func start() {
self.notificationSettingsViewModel.coordinatorDelegate = self
}
func toPresentable() -> UIViewController {
return self.notificationSettingsViewController
}
}
// MARK: - NotificationSettingsViewModelCoordinatorDelegate
@available(iOS 14.0, *)
extension NotificationSettingsCoordinator: NotificationSettingsViewModelCoordinatorDelegate {
func notificationSettingsViewModelDidComplete(_ viewModel: NotificationSettingsViewModelType) {
self.delegate?.notificationSettingsCoordinatorDidComplete(self)
}
}
@@ -0,0 +1,28 @@
// File created from ScreenTemplate
// $ createScreen.sh Settings/Notifications NotificationSettings
/*
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 NotificationSettingsCoordinatorDelegate: AnyObject {
func notificationSettingsCoordinatorDidComplete(_ coordinator: NotificationSettingsCoordinatorType)
}
/// `NotificationSettingsCoordinatorType` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow.
protocol NotificationSettingsCoordinatorType: Coordinator, Presentable {
var delegate: NotificationSettingsCoordinatorDelegate? { get }
}
@@ -0,0 +1,123 @@
//
// 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 SwiftUI
/**
A bordered style of text input as defined in:
https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=2039%3A26415
*/
@available(iOS 14.0, *)
struct BorderedInputFieldStyle: TextFieldStyle {
@Environment(\.theme) var theme: Theme
@Environment(\.isEnabled) var isEnabled: Bool
var isEditing: Bool = false
var isError: Bool = false
private var borderColor: Color {
if !isEnabled {
return Color(theme.colors.quinaryContent)
} else if isError {
return Color(theme.colors.alert)
} else if isEditing {
return Color(theme.colors.accent)
}
return Color(theme.colors.quarterlyContent)
}
private var accentColor: Color {
if isError {
return Color(theme.colors.alert)
}
return Color(theme.colors.accent)
}
private var textColor: Color {
if !isEnabled {
return Color(theme.colors.quarterlyContent)
}
return Color(theme.colors.primaryContent)
}
private var backgroundColor: Color {
if !isEnabled && (theme is DarkTheme) {
return Color(theme.colors.quinaryContent)
}
return Color(theme.colors.background)
}
private var borderWidth: CGFloat {
return isEditing || isError ? 2 : 1.5
}
func _body(configuration: TextField<_Label>) -> some View {
let rect = RoundedRectangle(cornerRadius: 8)
return configuration
.font(Font(theme.fonts.callout))
.foregroundColor(textColor)
.accentColor(accentColor)
.frame(height: 48)
.padding(.horizontal, 8)
.background(backgroundColor)
.clipShape(rect)
.overlay(rect.stroke(borderColor, lineWidth: borderWidth))
}
}
@available(iOS 14.0, *)
struct BorderedInputFieldStyle_Previews: PreviewProvider {
static var previews: some View {
Group {
VStack {
TextField("Placeholder", text: .constant(""))
.textFieldStyle(BorderedInputFieldStyle())
TextField("Placeholder", text: .constant(""))
.textFieldStyle(BorderedInputFieldStyle(isEditing: true))
TextField("Placeholder", text: .constant("Web"))
.textFieldStyle(BorderedInputFieldStyle())
TextField("Placeholder", text: .constant("Web"))
.textFieldStyle(BorderedInputFieldStyle(isEditing: true))
TextField("Placeholder", text: .constant("Web"))
.textFieldStyle(BorderedInputFieldStyle())
.disabled(true)
TextField("Placeholder", text: .constant("Web"))
.textFieldStyle(BorderedInputFieldStyle(isEditing: true, isError: true))
}
.padding()
VStack {
TextField("Placeholder", text: .constant(""))
.textFieldStyle(BorderedInputFieldStyle())
TextField("Placeholder", text: .constant(""))
.textFieldStyle(BorderedInputFieldStyle(isEditing: true))
TextField("Placeholder", text: .constant("Web"))
.textFieldStyle(BorderedInputFieldStyle())
TextField("Placeholder", text: .constant("Web"))
.textFieldStyle(BorderedInputFieldStyle(isEditing: true))
TextField("Placeholder", text: .constant("Web"))
.textFieldStyle(BorderedInputFieldStyle())
.disabled(true)
TextField("Placeholder", text: .constant("Web"))
.textFieldStyle(BorderedInputFieldStyle(isEditing: true, isError: true))
}
.padding()
.theme(ThemeIdentifier.dark)
}
}
}
@@ -0,0 +1,75 @@
//
// 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 SwiftUI
/**
A single rounded rect chip to be rendered within `Chips` collection
*/
@available(iOS 14.0, *)
struct Chip: View {
@Environment(\.isEnabled) var isEnabled
@Environment(\.theme) var theme: Theme
let title: String
let onDelete: () -> Void
var backgroundColor: Color {
if !isEnabled {
return Color(theme.colors.quinaryContent)
}
return Color(theme.colors.accent)
}
var foregroundColor: Color {
if !isEnabled {
return Color(theme.colors.tertiaryContent)
}
return Color.white
}
var body: some View {
HStack {
Text(title)
.font(Font(theme.fonts.body))
.lineLimit(1)
Button(action: onDelete) {
Image(systemName: "xmark.circle.fill")
.frame(width: 16, height: 16, alignment: .center)
}
}
.padding(.leading, 12)
.padding(.top, 6)
.padding(.bottom, 6)
.padding(.trailing, 8)
.background(backgroundColor)
.foregroundColor(foregroundColor)
.cornerRadius(20)
}
}
@available(iOS 14.0, *)
struct Chip_Previews: PreviewProvider {
static var previews: some View {
Group {
Chip(title: "My great chip", onDelete: { })
Chip(title: "My great chip", onDelete: { })
.theme(.dark)
}
}
}
@@ -0,0 +1,91 @@
//
// 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 SwiftUI
/**
Renders multiple chips in a flow layout.
*/
@available(iOS 14.0, *)
struct Chips: View {
@State private var frame: CGRect = CGRect.zero
let titles: [String]
let didDeleteChip: (String) -> Void
let verticalSpacing: CGFloat = 16
let horizontalSpacing: CGFloat = 12
var body: some View {
Group {
VStack {
var x = CGFloat.zero
var y = CGFloat.zero
GeometryReader { geo in
ZStack(alignment: .topLeading, content: {
ForEach(titles, id: \.self) { chip in
Chip(title: chip) {
didDeleteChip(chip)
}
.alignmentGuide(.leading) { dimension in
// Align with leading side and move vertically down to next line
// if chip does not fit on trailing side.
if abs(x - dimension.width) > geo.size.width {
x = 0
y -= dimension.height + verticalSpacing
}
let result = x
if chip == titles.last {
// Reset x if it's the last.
x = 0
} else {
// Align next chip to the end of the current one.
x -= dimension.width + horizontalSpacing
}
return result
}
.alignmentGuide(.top) { dimension in
// Use next y value and reset if its the last.
let result = y
if chip == titles.last {
y = 0
}
return result
}
}
})
.background(ViewFrameReader(frame: $frame))
}
}
.frame(height: frame.size.height)
}
}
}
@available(iOS 14.0, *)
struct Chips_Previews: PreviewProvider {
static var chips: [String] = ["Chip1", "Chip2", "Chip3", "Chip4", "Chip5", "Chip6"]
static var previews: some View {
Group {
Chips(titles: chips, didDeleteChip: { _ in })
Chips(titles: chips, didDeleteChip: { _ in })
.theme(.dark)
}
}
}
@@ -0,0 +1,67 @@
//
// 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 SwiftUI
/**
Renders an input field and a collection of chips
with callbacks for addition and deletion.
*/
@available(iOS 14.0, *)
struct ChipsInput: View {
@Environment(\.theme) var theme: Theme
@Environment(\.isEnabled) var isEnabled
@State private var chipText: String = ""
let titles: [String]
let didAddChip: (String) -> Void
let didDeleteChip: (String) -> Void
var placeholder: String = ""
var body: some View {
VStack(spacing: 16) {
TextField(placeholder, text: $chipText, onCommit: {
didAddChip(chipText)
chipText = ""
})
.disabled(!isEnabled)
.disableAutocorrection(true)
.autocapitalization(.none)
.textFieldStyle(FormInputFieldStyle())
Chips(titles: titles, didDeleteChip: didDeleteChip)
.padding(.horizontal)
}
}
}
@available(iOS 14.0, *)
struct ChipsInput_Previews: PreviewProvider {
static var chips = Set<String>(["Website", "Element", "Design", "Matrix/Element"])
static var previews: some View {
ChipsInput(titles: Array(chips)) { chip in
chips.insert(chip)
} didDeleteChip: { chip in
chips.remove(chip)
}
.disabled(true)
}
}
@@ -0,0 +1,44 @@
//
// 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 SwiftUI
@available(iOS 14.0, *)
struct DefaultNotificationSettings: View {
@ObservedObject var viewModel: NotificationSettingsViewModel
var body: some View {
NotificationSettings(viewModel: viewModel)
.navigationBarTitle(VectorL10n.settingsDefault)
}
}
@available(iOS 14.0, *)
struct DefaultNotifications_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
DefaultNotificationSettings(
viewModel: NotificationSettingsViewModel(
notificationSettingsService: MockNotificationSettingsService.example,
ruleIds: NotificationSettingsScreen.defaultNotifications.pushRules
)
)
.navigationBarTitleDisplayMode(.inline)
}
}
}
@@ -0,0 +1,84 @@
//
// 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 SwiftUI
/**
An input field for forms.
*/
@available(iOS 14.0, *)
struct FormInputFieldStyle: TextFieldStyle {
@Environment(\.theme) var theme: Theme
@Environment(\.isEnabled) var isEnabled
private var textColor: Color {
if !isEnabled {
return Color(theme.colors.quarterlyContent)
}
return Color(theme.colors.primaryContent)
}
private var backgroundColor: Color {
if !isEnabled && (theme is DarkTheme) {
return Color(theme.colors.quinaryContent)
}
return Color(theme.colors.background)
}
func _body(configuration: TextField<_Label>) -> some View {
configuration
.font(Font(theme.fonts.callout))
.foregroundColor(textColor)
.frame(minHeight: 48)
.padding(.horizontal)
.background(backgroundColor)
}
}
@available(iOS 14.0, *)
struct FormInputFieldStyle_Previews: PreviewProvider {
static var previews: some View {
Group {
VectorForm {
TextField("Placeholder", text: .constant(""))
.textFieldStyle(FormInputFieldStyle())
TextField("Placeholder", text: .constant("Web"))
.textFieldStyle(FormInputFieldStyle())
TextField("Placeholder", text: .constant("Web"))
.textFieldStyle(FormInputFieldStyle())
.disabled(true)
}
.padding()
VectorForm {
TextField("Placeholder", text: .constant(""))
.textFieldStyle(FormInputFieldStyle())
TextField("Placeholder", text: .constant("Web"))
.textFieldStyle(FormInputFieldStyle())
TextField("Placeholder", text: .constant("Web"))
.textFieldStyle(FormInputFieldStyle())
.disabled(true)
}
.padding()
.theme(ThemeIdentifier.dark)
}
}
}
@@ -0,0 +1,54 @@
//
// 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 SwiftUI
@available(iOS 14.0, *)
struct MentionsAndKeywordNotificationSettings: View {
@ObservedObject var viewModel: NotificationSettingsViewModel
var keywordSection: some View {
SwiftUI.Section(
header: FormSectionHeader(text: VectorL10n.settingsYourKeywords),
footer: FormSectionFooter(text: VectorL10n.settingsMentionsAndKeywordsEncryptionNotice)
) {
NotificationSettingsKeywords(viewModel: viewModel)
}
}
var body: some View {
NotificationSettings(
viewModel: viewModel,
bottomSection: keywordSection
)
.navigationTitle(VectorL10n.settingsMentionsAndKeywords)
}
}
@available(iOS 14.0, *)
struct MentionsAndKeywords_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
MentionsAndKeywordNotificationSettings(
viewModel: NotificationSettingsViewModel(
notificationSettingsService: MockNotificationSettingsService.example,
ruleIds: NotificationSettingsScreen.mentionsAndKeywords.pushRules
)
)
.navigationBarTitleDisplayMode(.inline)
}
}
}
@@ -0,0 +1,73 @@
//
// 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 SwiftUI
/**
Renders the push rule settings that can be enabled/disable.
Also renders an optional bottom section
(used in the case of keywords, for the keyword chips and input).
*/
@available(iOS 14.0, *)
struct NotificationSettings<BottomSection: View>: View {
@ObservedObject var viewModel: NotificationSettingsViewModel
var bottomSection: BottomSection?
var body: some View {
VectorForm {
SwiftUI.Section(
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)
}
}
}
bottomSection
}
.activityIndicator(show: viewModel.viewState.saving)
}
}
@available(iOS 14.0, *)
extension NotificationSettings where BottomSection == EmptyView {
init(viewModel: NotificationSettingsViewModel) {
self.init(viewModel: viewModel, bottomSection: nil)
}
}
@available(iOS 14.0, *)
struct NotificationSettings_Previews: PreviewProvider {
static var previews: some View {
Group {
ForEach(NotificationSettingsScreen.allCases) { screen in
NavigationView {
NotificationSettings(
viewModel: NotificationSettingsViewModel(
notificationSettingsService: MockNotificationSettingsService.example,
ruleIds: screen.pushRules
)
)
.navigationBarTitleDisplayMode(.inline)
}
}
}
}
}
@@ -0,0 +1,46 @@
//
// 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 SwiftUI
/**
Renders the keywords input, driven by 'NotificationSettingsViewModel'.
*/
@available(iOS 14.0, *)
struct NotificationSettingsKeywords: View {
@ObservedObject var viewModel: NotificationSettingsViewModel
var body: some View {
ChipsInput(
titles: viewModel.viewState.keywords,
didAddChip: viewModel.add(keyword:),
didDeleteChip: viewModel.remove(keyword:),
placeholder: VectorL10n.settingsNewKeyword
)
.disabled(!(viewModel.viewState.selectionState[.keywords] ?? false))
}
}
@available(iOS 14.0, *)
struct Keywords_Previews: PreviewProvider {
static let viewModel = NotificationSettingsViewModel(
notificationSettingsService: MockNotificationSettingsService.example,
ruleIds: NotificationSettingsScreen.mentionsAndKeywords.pushRules
)
static var previews: some View {
NotificationSettingsKeywords(viewModel: viewModel)
}
}
@@ -0,0 +1,42 @@
//
// 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 SwiftUI
@available(iOS 14.0, *)
struct OtherNotificationSettings: View {
@ObservedObject var viewModel: NotificationSettingsViewModel
var body: some View {
NotificationSettings(viewModel: viewModel)
.navigationTitle(VectorL10n.settingsOther)
}
}
@available(iOS 14.0, *)
struct OtherNotifications_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
DefaultNotificationSettings(
viewModel: NotificationSettingsViewModel(
notificationSettingsService: MockNotificationSettingsService.example,
ruleIds: NotificationSettingsScreen.other.pushRules
)
)
.navigationBarTitleDisplayMode(.inline)
}
}
}
@@ -0,0 +1,127 @@
//
// 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 Combine
/**
A service for changing notification settings and keywords
*/
@available(iOS 14.0, *)
protocol NotificationSettingsServiceType {
/**
Publisher of all push rules.
*/
var rulesPublisher: AnyPublisher<[MXPushRule], Never> { get }
/**
Publisher of content rules.
*/
var contentRulesPublisher: AnyPublisher<[MXPushRule], Never> { get }
/**
Adds a keyword.
- Parameters:
- keyword: The keyword to add.
- enabled: Whether the keyword should be added in the enabled or disabled state.
*/
func add(keyword: String, enabled: Bool)
/**
Removes a keyword.
- Parameters:
- keyword: The keyword to remove.
*/
func remove(keyword: String)
/**
Updates the push rule actions.
- Parameters:
- 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?)
}
@available(iOS 14.0, *)
class NotificationSettingsService: NotificationSettingsServiceType {
private let session: MXSession
private var cancellables = Set<AnyCancellable>()
@Published private var contentRules = [MXPushRule]()
@Published private var rules = [MXPushRule]()
var rulesPublisher: AnyPublisher<[MXPushRule], Never> {
$rules.eraseToAnyPublisher()
}
var contentRulesPublisher: AnyPublisher<[MXPushRule], Never> {
$contentRules.eraseToAnyPublisher()
}
init(session: MXSession) {
self.session = session
// Publisher of all rule updates
let rulesUpdated = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules))
// Set initial value of the content rules
if let contentRules = session.notificationCenter.rules.global.content as? [MXPushRule] {
self.contentRules = contentRules
}
// Observe future updates to content rules
rulesUpdated
.compactMap({ _ in self.session.notificationCenter.rules.global.content as? [MXPushRule] })
.assign(to: &$contentRules)
// Set initial value of rules
if let flatRules = session.notificationCenter.flatRules as? [MXPushRule] {
rules = flatRules
}
// Observe future updates to rules
rulesUpdated
.compactMap({ _ 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 {
return
}
session.notificationCenter.addContentRuleWithRuleId(matchingPattern: keyword, notify: actions.notify, sound: actions.sound, highlight: actions.highlight)
}
func remove(keyword: String) {
guard let rule = session.notificationCenter.rule(byId: keyword) else { return }
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)
if let actions = actions {
session.notificationCenter.updatePushRuleActions(ruleId,
kind: rule.kind,
notify: actions.notify,
soundName: actions.sound,
highlight: actions.highlight)
}
}
}
@@ -0,0 +1,255 @@
// File created from ScreenTemplate
// $ createScreen.sh Settings/Notifications NotificationSettings
/*
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 Combine
import SwiftUI
@available(iOS 14.0, *)
final class NotificationSettingsViewModel: NotificationSettingsViewModelType, ObservableObject {
// MARK: - Properties
// MARK: Private
private let notificationSettingsService: NotificationSettingsServiceType
// The rule ids this view model allows the ui to enabled/disable.
private let ruleIds: [NotificationPushRuleId]
private var cancellables = Set<AnyCancellable>()
// The ordered array of keywords the UI displays.
// We keep it ordered so keywords don't jump around when being added and removed.
@Published private var keywordsOrdered = [String]()
// MARK: Public
@Published var viewState: NotificationSettingsViewState
weak var coordinatorDelegate: NotificationSettingsViewModelCoordinatorDelegate?
// MARK: - Setup
init(notificationSettingsService: NotificationSettingsServiceType, ruleIds: [NotificationPushRuleId], initialState: NotificationSettingsViewState) {
self.notificationSettingsService = notificationSettingsService
self.ruleIds = ruleIds
self.viewState = initialState
// Observe when the rules are updated, to subsequently update the state of the settings.
notificationSettingsService.rulesPublisher
.sink(receiveValue: rulesUpdated(newRules:))
.store(in: &cancellables)
// Only observe keywords if the current settings view displays it.
if ruleIds.contains(.keywords) {
// Publisher of all the keyword push rules (keyword rules do not start with '.')
let keywordsRules = notificationSettingsService.contentRulesPublisher
.map { $0.filter { !$0.ruleId.starts(with: ".")} }
// Map to just the keyword strings
let keywords = keywordsRules
.map { Set($0.compactMap { $0.ruleId }) }
// Update the keyword set
keywords
.sink { [weak self] updatedKeywords in
guard let self = self else { return }
// We avoid simply assigning the new set as it would cause all keywords to get sorted lexigraphically.
// We first sort lexigraphically, and secondly preserve the order the user added them.
// The following adds/removes any updates while preserving that ordering.
// Remove keywords not in the updated set.
var newKeywordsOrdered = self.keywordsOrdered.filter { keyword in
updatedKeywords.contains(keyword)
}
// Append items in the updated set if they are not already added.
// O(n)² here. Will change keywordsOrdered back to an `OrderedSet` in future to fix this.
updatedKeywords.sorted().forEach { keyword in
if !newKeywordsOrdered.contains(keyword) {
newKeywordsOrdered.append(keyword)
}
}
self.keywordsOrdered = newKeywordsOrdered
}
.store(in: &cancellables)
// Keyword rules were updates, check if we need to update the setting.
keywordsRules
.map { $0.contains { $0.enabled } }
.sink(receiveValue: keywordRuleUpdated(anyEnabled:))
.store(in: &cancellables)
// Update the viewState with the final keywords to be displayed.
$keywordsOrdered
.weakAssign(to: \.viewState.keywords, on: self)
.store(in: &cancellables)
}
}
convenience init(notificationSettingsService: NotificationSettingsServiceType, ruleIds: [NotificationPushRuleId]) {
let ruleState = Dictionary(uniqueKeysWithValues: ruleIds.map({ ($0, selected: true) }))
self.init(notificationSettingsService: notificationSettingsService, ruleIds: ruleIds, initialState: NotificationSettingsViewState(saving: false, ruleIds: ruleIds, selectionState: ruleState))
}
// MARK: - Public
func update(ruleID: NotificationPushRuleId, isChecked: Bool) {
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 enabled = standardActions != .disabled
notificationSettingsService.updatePushRuleActions(
for: ruleID.rawValue,
enabled: enabled,
actions: standardActions.actions
)
}
private func updateKeywords(isChecked: Bool) {
guard !keywordsOrdered.isEmpty else {
self.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,
enabled: enabled,
actions: standardActions.actions
)
}
}
func add(keyword: String) {
if !keywordsOrdered.contains(keyword) {
keywordsOrdered.append(keyword)
}
notificationSettingsService.add(keyword: keyword, enabled: true)
}
func remove(keyword: String) {
keywordsOrdered = keywordsOrdered.filter({ $0 != keyword })
notificationSettingsService.remove(keyword: keyword)
}
// MARK: - Private
private func rulesUpdated(newRules: [MXPushRule]) {
for rule in newRules {
guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId),
ruleIds.contains(ruleId) else { continue }
self.viewState.selectionState[ruleId] = self.isChecked(rule: rule)
}
}
private func keywordRuleUpdated(anyEnabled: Bool) {
if !keywordsOrdered.isEmpty {
self.viewState.selectionState[.keywords] = anyEnabled
}
}
/**
Given a push rule check which index/checked state it matches.
Matcing is done by comparing the rule against the static definitions for that rule.
The same logic is used on android.
*/
private func isChecked(rule: MXPushRule) -> Bool {
guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId) else { return false }
let firstIndex = NotificationIndex.allCases.first { nextIndex in
return ruleMaches(rule: rule, targetRule: ruleId.standardActions(for: nextIndex))
}
guard let index = firstIndex else {
return false
}
return index.enabled
}
/*
Given a rule, check it match the actions in the static definition.
*/
private func ruleMaches(rule: MXPushRule, targetRule: NotificationStandardActions?) -> Bool {
guard let targetRule = targetRule else {
return false
}
if !rule.enabled && targetRule == .disabled {
return true
}
if rule.enabled,
let actions = targetRule.actions,
rule.highlight == actions.highlight,
rule.sound == actions.sound,
rule.notify == actions.notify,
rule.dontNotify == !actions.notify {
return true
}
return false
}
}
fileprivate extension MXPushRule {
func getAction(actionType: MXPushRuleActionType, tweakType: String? = nil) -> MXPushRuleAction? {
guard let actions = actions as? [MXPushRuleAction] else {
return nil
}
return actions.first { action in
var match = action.actionType == actionType
MXLog.debug("action \(action)")
if let tweakType = tweakType,
let actionTweak = action.parameters?["set_tweak"] as? String {
match = match && (tweakType == actionTweak)
}
return match
}
}
var highlight: Bool {
guard let action = getAction(actionType: MXPushRuleActionTypeSetTweak, tweakType: "highlight") else {
return false
}
if let highlight = action.parameters["value"] as? Bool {
return highlight
}
return true
}
var sound: String? {
guard let action = getAction(actionType: MXPushRuleActionTypeSetTweak, tweakType: "sound") else {
return nil
}
return action.parameters["value"] as? String
}
var notify: Bool {
return getAction(actionType: MXPushRuleActionTypeNotify) != nil
}
var dontNotify: Bool {
return getAction(actionType: MXPushRuleActionTypeDontNotify) != nil
}
}
@@ -0,0 +1,28 @@
// File created from ScreenTemplate
// $ createScreen.sh Settings/Notifications NotificationSettings
/*
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 NotificationSettingsViewModelCoordinatorDelegate: AnyObject {
func notificationSettingsViewModelDidComplete(_ viewModel: NotificationSettingsViewModelType)
}
/// Protocol describing the view model used by `NotificationSettingsViewController`
protocol NotificationSettingsViewModelType {
var coordinatorDelegate: NotificationSettingsViewModelCoordinatorDelegate? { get set }
}
@@ -0,0 +1,26 @@
// File created from ScreenTemplate
// $ createScreen.sh Settings/Notifications NotificationSettings
/*
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
struct NotificationSettingsViewState {
var saving: Bool
var ruleIds: [NotificationPushRuleId]
var selectionState: [NotificationPushRuleId: Bool]
var keywords = [String]()
}
+80 -2
View File
@@ -101,6 +101,9 @@ enum
NOTIFICATION_SETTINGS_GLOBAL_SETTINGS_INDEX,
NOTIFICATION_SETTINGS_PIN_MISSED_NOTIFICATIONS_INDEX,
NOTIFICATION_SETTINGS_PIN_UNREAD_INDEX,
NOTIFICATION_SETTINGS_DEFAULT_SETTINGS_INDEX,
NOTIFICATION_SETTINGS_MENTION_AND_KEYWORDS_SETTINGS_INDEX,
NOTIFICATION_SETTINGS_OTHER_SETTINGS_INDEX,
};
enum
@@ -163,6 +166,7 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(void);
#pragma mark - SettingsViewController
@interface SettingsViewController () <DeactivateAccountViewControllerDelegate,
NotificationSettingsCoordinatorBridgePresenterDelegate,
SecureBackupSetupCoordinatorBridgePresenterDelegate,
SignOutAlertPresenterDelegate,
SingleImagePickerPresenterDelegate,
@@ -248,6 +252,7 @@ TableViewSectionsDelegate>
@property (nonatomic) UNNotificationSettings *systemNotificationSettings;
@property (nonatomic, weak) DeactivateAccountViewController *deactivateAccountViewController;
@property (nonatomic, strong) NotificationSettingsCoordinatorBridgePresenter *notificationSettingsBridgePresenter;
@property (nonatomic, strong) SignOutAlertPresenter *signOutAlertPresenter;
@property (nonatomic, weak) UIButton *signOutButton;
@property (nonatomic, strong) SingleImagePickerPresenter *imagePickerPresenter;
@@ -377,10 +382,25 @@ TableViewSectionsDelegate>
{
[sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_SHOW_DECODED_CONTENT];
}
[sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_GLOBAL_SETTINGS_INDEX];
if (@available(iOS 14.0, *)) {
// Don't add Global settings message for iOS 14+
} else {
[sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_GLOBAL_SETTINGS_INDEX];
}
[sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_PIN_MISSED_NOTIFICATIONS_INDEX];
[sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_PIN_UNREAD_INDEX];
sectionNotificationSettings.headerTitle = NSLocalizedStringFromTable(@"settings_notifications_settings", @"Vector", nil);
if (@available(iOS 14.0, *)) {
[sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_DEFAULT_SETTINGS_INDEX];
[sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_MENTION_AND_KEYWORDS_SETTINGS_INDEX];
[sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_OTHER_SETTINGS_INDEX];
} else {
// Don't add new sections on pre iOS 14
}
sectionNotificationSettings.headerTitle = NSLocalizedStringFromTable(@"settings_notifications", @"Vector", nil);
[tmpSections addObject:sectionNotificationSettings];
if (BuildSettings.allowVoIPUsage && BuildSettings.stunServerFallbackUrlString)
@@ -1936,6 +1956,23 @@ TableViewSectionsDelegate>
cell = labelAndSwitchCell;
}
else if (row == NOTIFICATION_SETTINGS_DEFAULT_SETTINGS_INDEX || row == NOTIFICATION_SETTINGS_MENTION_AND_KEYWORDS_SETTINGS_INDEX || row == NOTIFICATION_SETTINGS_OTHER_SETTINGS_INDEX)
{
cell = [self getDefaultTableViewCell:tableView];
if (row == NOTIFICATION_SETTINGS_DEFAULT_SETTINGS_INDEX)
{
cell.textLabel.text = NSLocalizedStringFromTable(@"settings_default", @"Vector", nil);
}
else if (row == NOTIFICATION_SETTINGS_MENTION_AND_KEYWORDS_SETTINGS_INDEX)
{
cell.textLabel.text = NSLocalizedStringFromTable(@"settings_mentions_and_keywords", @"Vector", nil);
}
else if (row == NOTIFICATION_SETTINGS_OTHER_SETTINGS_INDEX)
{
cell.textLabel.text = NSLocalizedStringFromTable(@"settings_other", @"Vector", nil);
}
[cell vc_setAccessoryDisclosureIndicatorWithCurrentTheme];
}
}
else if (section == SECTION_TAG_CALLS)
{
@@ -2752,6 +2789,22 @@ TableViewSectionsDelegate>
}
}
}
else if (section == SECTION_TAG_NOTIFICATIONS)
{
if (@available(iOS 14.0, *)) {
switch (row) {
case NOTIFICATION_SETTINGS_DEFAULT_SETTINGS_INDEX:
[self showNotificationSettings:NotificationSettingsScreenDefaultNotifications];
break;
case NOTIFICATION_SETTINGS_MENTION_AND_KEYWORDS_SETTINGS_INDEX:
[self showNotificationSettings:NotificationSettingsScreenMentionsAndKeywords];
break;
case NOTIFICATION_SETTINGS_OTHER_SETTINGS_INDEX:
[self showNotificationSettings:NotificationSettingsScreenOther];
break;
}
}
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
@@ -4091,6 +4144,31 @@ TableViewSectionsDelegate>
[deactivateAccountViewController dismissViewControllerAnimated:YES completion:nil];
}
#pragma mark - NotificationSettingsCoordinatorBridgePresenter
- (void)showNotificationSettings: (NotificationSettingsScreen)screen API_AVAILABLE(ios(14.0))
{
NotificationSettingsCoordinatorBridgePresenter *notificationSettingsBridgePresenter = [[NotificationSettingsCoordinatorBridgePresenter alloc] initWithSession:self.mainSession];
notificationSettingsBridgePresenter.delegate = self;
MXWeakify(self);
[notificationSettingsBridgePresenter pushFrom:self.navigationController animated:YES screen:screen popCompletion:^{
MXStrongifyAndReturnIfNil(self);
self.notificationSettingsBridgePresenter = nil;
}];
self.notificationSettingsBridgePresenter = notificationSettingsBridgePresenter;
}
#pragma mark - NotificationSettingsCoordinatorBridgePresenterDelegate
- (void)notificationSettingsCoordinatorBridgePresenterDelegateDidComplete:(NotificationSettingsCoordinatorBridgePresenter *)coordinatorBridgePresenter API_AVAILABLE(ios(14.0))
{
[self.notificationSettingsBridgePresenter dismissWithAnimated:YES completion:nil];
self.notificationSettingsBridgePresenter = nil;
}
#pragma mark - SecureBackupSetupCoordinatorBridgePresenter
- (void)showSecureBackupSetupFromSignOutFlow