Edit the room Notification setting (#1394)

This commit is contained in:
Nicolas Mauri
2023-07-26 11:27:14 +02:00
committed by GitHub
parent ed28bc5d02
commit ed68ed1c91
28 changed files with 836 additions and 22 deletions
@@ -0,0 +1,40 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import SwiftUI
struct RoomNotificationSettingsScreenCoordinatorParameters {
let notificationSettingsProxy: NotificationSettingsProxyProtocol
let roomProxy: RoomProxyProtocol
}
final class RoomNotificationSettingsScreenCoordinator: CoordinatorProtocol {
private let parameters: RoomNotificationSettingsScreenCoordinatorParameters
private var viewModel: RoomNotificationSettingsScreenViewModelProtocol
init(parameters: RoomNotificationSettingsScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: parameters.notificationSettingsProxy,
roomProxy: parameters.roomProxy)
}
func start() { }
func toPresentable() -> AnyView {
AnyView(RoomNotificationSettingsScreen(context: viewModel.context))
}
}
@@ -0,0 +1,111 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
enum RoomNotificationSettingsScreenViewModelAction { }
enum RoomNotificationSettingsState {
case loading
case loaded(settings: RoomNotificationSettingsProxyProtocol)
case error
}
extension RoomNotificationSettingsState {
var isLoading: Bool {
if case .loading = self {
return true
}
return false
}
var isLoaded: Bool {
if case .loaded = self {
return true
}
return false
}
var isError: Bool {
if case .error = self {
return true
}
return false
}
}
struct RoomNotificationSettingsScreenViewState: BindableState {
var bindings: RoomNotificationSettingsScreenViewStateBindings
let strings = RoomNotificationSettingsScreenStrings()
var notificationSettingsState: RoomNotificationSettingsState = .loading
var availableCustomRoomNotificationModes: [RoomNotificationModeProxy] = [.allMessages, .mentionsAndKeywordsOnly, .mute]
var isRestoringDefautSetting = false
var pendingCustomMode: RoomNotificationModeProxy?
}
struct RoomNotificationSettingsScreenViewStateBindings {
var allowCustomSetting = false
var customMode: RoomNotificationModeProxy = .allMessages
/// Information describing the currently displayed alert.
var alertInfo: AlertInfo<RoomNotificationSettingsScreenErrorType>?
}
enum RoomNotificationSettingsScreenViewAction {
case changedAllowCustomSettings
case setCustomMode(RoomNotificationModeProxy)
}
struct RoomNotificationSettingsScreenStrings {
let customSettingFootnote: AttributedString
init() {
let linkPlaceholder = "{link}"
var customSettingFootnote = AttributedString(L10n.screenRoomNotificationSettingsDefaultSettingFootnote(linkPlaceholder))
var linkString = AttributedString(L10n.screenRoomNotificationSettingsDefaultSettingFootnoteContentLink)
linkString.bold()
customSettingFootnote.replace(linkPlaceholder, with: linkString)
self.customSettingFootnote = customSettingFootnote
}
func string(for mode: RoomNotificationModeProxy) -> String {
switch mode {
case .allMessages:
return L10n.screenRoomNotificationSettingsModeAllMessages
case .mentionsAndKeywordsOnly:
return L10n.screenRoomNotificationSettingsModeMentionsAndKeywords
case .mute:
return L10n.commonMute
}
}
func string(for state: RoomNotificationSettingsState) -> String {
switch state {
case .loading:
return L10n.commonLoading
case .loaded(let settings):
return string(for: settings.mode)
case .error:
return L10n.commonError
}
}
}
enum RoomNotificationSettingsScreenErrorType: Hashable {
case loadingSettingsFailed
case setModeFailed
case restoreDefaultFailed
}
@@ -0,0 +1,154 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import SwiftUI
typealias RoomNotificationSettingsScreenViewModelType = StateStoreViewModel<RoomNotificationSettingsScreenViewState, RoomNotificationSettingsScreenViewAction>
class RoomNotificationSettingsScreenViewModel: RoomNotificationSettingsScreenViewModelType, RoomNotificationSettingsScreenViewModelProtocol {
private let actionsSubject: PassthroughSubject<RoomNotificationSettingsScreenViewModelAction, Never> = .init()
private let notificationSettingsProxy: NotificationSettingsProxyProtocol
private let roomProxy: RoomProxyProtocol
@CancellableTask private var fetchNotificationSettingsTask: Task<Void, Error>?
var actions: AnyPublisher<RoomNotificationSettingsScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(notificationSettingsProxy: NotificationSettingsProxyProtocol, roomProxy: RoomProxyProtocol) {
let bindings = RoomNotificationSettingsScreenViewStateBindings()
self.notificationSettingsProxy = notificationSettingsProxy
self.roomProxy = roomProxy
super.init(initialViewState: RoomNotificationSettingsScreenViewState(bindings: bindings))
setupNotificationSettingsSubscription()
fetchNotificationSettings()
}
// MARK: - Public
override func process(viewAction: RoomNotificationSettingsScreenViewAction) {
switch viewAction {
case .changedAllowCustomSettings:
toogleCustomSetting()
case .setCustomMode(let mode):
setCustomMode(mode)
}
}
// MARK: - Private
private func setupNotificationSettingsSubscription() {
notificationSettingsProxy.callbacks
.receive(on: DispatchQueue.main)
.sink { [weak self] callback in
guard let self else { return }
switch callback {
case .settingsDidChange:
self.fetchNotificationSettings()
}
}
.store(in: &cancellables)
}
private func fetchNotificationSettings() {
fetchNotificationSettingsTask = Task {
await fetchRoomNotificationSettings()
}
}
private func fetchRoomNotificationSettings() async {
do {
let settings = try await notificationSettingsProxy.getNotificationSettings(roomId: roomProxy.id,
isEncrypted: roomProxy.isEncrypted,
activeMembersCount: UInt64(roomProxy.activeMembersCount))
guard !Task.isCancelled else { return }
state.notificationSettingsState = .loaded(settings: settings)
if !state.isRestoringDefautSetting {
state.bindings.allowCustomSetting = !settings.isDefault
}
if state.pendingCustomMode == nil {
state.bindings.customMode = settings.mode
}
} catch {
state.notificationSettingsState = .error
displayError(.loadingSettingsFailed)
}
}
private func toogleCustomSetting() {
guard case .loaded(let settings) = state.notificationSettingsState else { return }
guard state.bindings.allowCustomSetting == settings.isDefault else { return }
if state.bindings.allowCustomSetting {
setCustomMode(settings.mode)
} else {
restoreDefaultSetting()
}
}
private func restoreDefaultSetting() {
state.isRestoringDefautSetting = true
Task {
do {
try await notificationSettingsProxy.restoreDefaultNotificationMode(roomId: roomProxy.id)
} catch {
displayError(.restoreDefaultFailed)
}
state.isRestoringDefautSetting = false
}
}
private func setCustomMode(_ mode: RoomNotificationModeProxy) {
// Check if the new mode is already the current one
if case .loaded(let currentSettings) = state.notificationSettingsState {
if !currentSettings.isDefault, currentSettings.mode == mode {
return
}
}
state.pendingCustomMode = mode
Task {
do {
try await notificationSettingsProxy.setNotificationMode(roomId: roomProxy.id, mode: mode)
} catch {
displayError(.setModeFailed)
}
state.pendingCustomMode = nil
}
}
private func displayError(_ type: RoomNotificationSettingsScreenErrorType) {
switch type {
case .loadingSettingsFailed:
state.bindings.alertInfo = AlertInfo(id: type,
title: L10n.commonError,
message: L10n.screenRoomNotificationSettingsErrorLoadingSettings)
case .setModeFailed:
state.bindings.alertInfo = AlertInfo(id: type,
title: L10n.commonError,
message: L10n.screenRoomNotificationSettingsErrorSettingMode)
case .restoreDefaultFailed:
state.bindings.alertInfo = AlertInfo(id: type,
title: L10n.commonError,
message: L10n.screenRoomNotificationSettingsErrorRestoringDefault)
}
}
}
@@ -0,0 +1,23 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
@MainActor
protocol RoomNotificationSettingsScreenViewModelProtocol {
var actions: AnyPublisher<RoomNotificationSettingsScreenViewModelAction, Never> { get }
var context: RoomNotificationSettingsScreenViewModelType.Context { get }
}
@@ -0,0 +1,132 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
struct RoomNotificationSettingsScreen: View {
@ObservedObject var context: RoomNotificationSettingsScreenViewModel.Context
var body: some View {
Form {
allowCustomSettingSection
if !context.allowCustomSetting {
defaultSettingSection
} else {
customSettingsSection
}
}
.compoundForm()
.navigationTitle(L10n.screenRoomDetailsNotificationTitle)
.alert(item: $context.alertInfo)
.track(screen: .roomNotifications)
}
// MARK: - Private
@ViewBuilder
private var allowCustomSettingSection: some View {
Section {
Toggle(isOn: $context.allowCustomSetting) {
Text(L10n.screenRoomNotificationSettingsAllowCustom)
}
.toggleStyle(.compoundForm)
.accessibilityIdentifier(A11yIdentifiers.roomNotificationSettingsScreen.allowCustomSetting)
.disabled(context.viewState.notificationSettingsState.isLoading)
.onChange(of: context.allowCustomSetting) { _ in
context.send(viewAction: .changedAllowCustomSettings)
}
} footer: {
Text(L10n.screenRoomNotificationSettingsAllowCustomFootnote)
.compoundFormSectionFooter()
}
.compoundFormSection()
}
@ViewBuilder
private var defaultSettingSection: some View {
Section {
if context.viewState.isRestoringDefautSetting {
Text(L10n.commonLoading)
.foregroundColor(.compound.textPlaceholder)
} else {
Text(context.viewState.strings.string(for: context.viewState.notificationSettingsState))
.foregroundColor(.compound.textPrimary)
}
} header: {
Text(L10n.screenRoomNotificationSettingsDefaultSettingTitle)
.compoundFormSectionHeader()
} footer: {
Text(context.viewState.strings.customSettingFootnote)
.compoundFormSectionFooter()
}
.compoundFormSection()
}
@ViewBuilder
private var customSettingsSection: some View {
Section {
Picker("", selection: $context.customMode) {
ForEach(context.viewState.availableCustomRoomNotificationModes, id: \.self) { mode in
Text(context.viewState.strings.string(for: mode))
.tag(mode)
}
}
.onChange(of: context.customMode) { mode in
context.send(viewAction: .setCustomMode(mode))
}
.labelsHidden()
.pickerStyle(.inline)
} header: {
Text(L10n.screenRoomNotificationSettingsCustomSettingsTitle)
.compoundFormSectionHeader()
}
.compoundFormSection()
}
}
// MARK: - Previews
struct RoomNotificationSettingsScreen_Previews: PreviewProvider {
static let viewModel = {
let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init(defaultRoomMode: .mentionsAndKeywordsOnly, roomMode: .mentionsAndKeywordsOnly))
let roomProxy = RoomProxyMock(with: .init(displayName: "Room", isEncrypted: true, joinedMembersCount: 4))
let model = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxy,
roomProxy: roomProxy)
return model
}()
static let viewModelCustom = {
let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init(defaultRoomMode: .allMessages, roomMode: .mentionsAndKeywordsOnly))
let roomProxy = RoomProxyMock(with: .init(displayName: "Room", isEncrypted: true, joinedMembersCount: 4))
let model = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxy,
roomProxy: roomProxy)
return model
}()
static var previews: some View {
RoomNotificationSettingsScreen(context: viewModel.context)
.previewDisplayName("Default")
RoomNotificationSettingsScreen(context: viewModelCustom.context)
.previewDisplayName("Custom")
}
}