mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
Settings about section (#1041)
* Fixes #1011 - Add legal information screen in settings, move all settings related screens under Source/Screen/Settings * Fix warnings after sliding sync api change
This commit is contained in:
+30
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
final class AnalyticsSettingsScreenCoordinator: CoordinatorProtocol {
|
||||
private let viewModel: AnalyticsSettingsScreenViewModel
|
||||
|
||||
init() {
|
||||
viewModel = AnalyticsSettingsScreenViewModel()
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(AnalyticsSettingsScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
struct AnalyticsSettingsScreenViewState: BindableState {
|
||||
/// Attributed strings created from localized HTML.
|
||||
let strings: AnalyticsSettingsScreenStrings
|
||||
var bindings: AnalyticsSettingsScreenViewStateBindings
|
||||
}
|
||||
|
||||
struct AnalyticsSettingsScreenViewStateBindings {
|
||||
var enableAnalytics: Bool
|
||||
}
|
||||
|
||||
enum AnalyticsSettingsScreenViewAction {
|
||||
case toggleAnalytics
|
||||
}
|
||||
|
||||
struct AnalyticsSettingsScreenStrings {
|
||||
let sectionFooter: AttributedString
|
||||
|
||||
init(termsURL: URL) {
|
||||
let content = AttributedString(L10n.screenAnalyticsSettingsHelpUsImprove(InfoPlistReader.main.bundleDisplayName))
|
||||
// Create the 'read terms' with a placeholder.
|
||||
let linkPlaceholder = "{link}"
|
||||
var readTerms = AttributedString(L10n.screenAnalyticsSettingsReadTerms(linkPlaceholder))
|
||||
readTerms.replace(linkPlaceholder,
|
||||
with: L10n.screenAnalyticsSettingsReadTermsContentLink,
|
||||
asLinkTo: termsURL)
|
||||
sectionFooter = content + "\n\n" + readTerms
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// 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 AnalyticsSettingsScreenViewModelType = StateStoreViewModel<AnalyticsSettingsScreenViewState, AnalyticsSettingsScreenViewAction>
|
||||
|
||||
class AnalyticsSettingsScreenViewModel: AnalyticsSettingsScreenViewModelType, AnalyticsSettingsScreenViewModelProtocol {
|
||||
init() {
|
||||
let strings = AnalyticsSettingsScreenStrings(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
|
||||
let bindings = AnalyticsSettingsScreenViewStateBindings(enableAnalytics: ServiceLocator.shared.analytics.isEnabled)
|
||||
let state = AnalyticsSettingsScreenViewState(strings: strings, bindings: bindings)
|
||||
|
||||
super.init(initialViewState: state)
|
||||
|
||||
ServiceLocator.shared.settings.$analyticsConsentState
|
||||
.map { $0 == .optedIn }
|
||||
.weakAssign(to: \.state.bindings.enableAnalytics, on: self)
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
override func process(viewAction: AnalyticsSettingsScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .toggleAnalytics:
|
||||
if ServiceLocator.shared.analytics.isEnabled {
|
||||
ServiceLocator.shared.analytics.optOut()
|
||||
} else {
|
||||
ServiceLocator.shared.analytics.optIn()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// 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 AnalyticsSettingsScreenViewModelProtocol {
|
||||
var context: AnalyticsSettingsScreenViewModelType.Context { get }
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// 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 AnalyticsSettingsScreen: View {
|
||||
@ObservedObject var context: AnalyticsSettingsScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
analyticsSection
|
||||
}
|
||||
.compoundForm()
|
||||
.navigationTitle(L10n.commonAnalytics)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
var analyticsSection: some View {
|
||||
Section {
|
||||
Toggle(isOn: $context.enableAnalytics) {
|
||||
Label(L10n.screenAnalyticsSettingsShareData, systemImage: "chart.bar")
|
||||
}
|
||||
.toggleStyle(.compoundForm)
|
||||
.onChange(of: context.enableAnalytics) { _ in
|
||||
context.send(viewAction: .toggleAnalytics)
|
||||
}
|
||||
} footer: {
|
||||
Text(context.viewState.strings.sectionFooter)
|
||||
.compoundFormSectionFooter()
|
||||
.tint(.compound.textLinkExternal)
|
||||
}
|
||||
.compoundFormSection()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct AnalyticsSettingsScreen_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let viewModel = AnalyticsSettingsScreenViewModel()
|
||||
AnalyticsSettingsScreen(context: viewModel.context)
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
enum DeveloperOptionsScreenCoordinatorAction {
|
||||
case clearCache
|
||||
}
|
||||
|
||||
final class DeveloperOptionsScreenCoordinator: CoordinatorProtocol {
|
||||
private var viewModel: DeveloperOptionsScreenViewModelProtocol
|
||||
|
||||
var callback: ((DeveloperOptionsScreenCoordinatorAction) -> Void)?
|
||||
|
||||
init() {
|
||||
viewModel = DeveloperOptionsScreenViewModel()
|
||||
viewModel.callback = { [weak self] action in
|
||||
switch action {
|
||||
case .clearCache:
|
||||
self?.callback?(.clearCache)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(DeveloperOptionsScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// 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 DeveloperOptionsScreenViewModelAction {
|
||||
case clearCache
|
||||
}
|
||||
|
||||
struct DeveloperOptionsScreenViewState: BindableState {
|
||||
var bindings: DeveloperOptionsScreenViewStateBindings
|
||||
}
|
||||
|
||||
struct DeveloperOptionsScreenViewStateBindings {
|
||||
var shouldCollapseRoomStateEvents: Bool
|
||||
var userSuggestionsEnabled: Bool
|
||||
}
|
||||
|
||||
enum DeveloperOptionsScreenViewAction {
|
||||
case changedShouldCollapseRoomStateEvents
|
||||
case changedUserSuggestionsEnabled
|
||||
case clearCache
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
typealias DeveloperOptionsScreenViewModelType = StateStoreViewModel<DeveloperOptionsScreenViewState, DeveloperOptionsScreenViewAction>
|
||||
|
||||
class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, DeveloperOptionsScreenViewModelProtocol {
|
||||
var callback: ((DeveloperOptionsScreenViewModelAction) -> Void)?
|
||||
|
||||
private let appSettings: AppSettings
|
||||
|
||||
init() {
|
||||
appSettings = ServiceLocator.shared.settings
|
||||
let bindings = DeveloperOptionsScreenViewStateBindings(shouldCollapseRoomStateEvents: appSettings.shouldCollapseRoomStateEvents,
|
||||
userSuggestionsEnabled: appSettings.userSuggestionsEnabled)
|
||||
let state = DeveloperOptionsScreenViewState(bindings: bindings)
|
||||
|
||||
super.init(initialViewState: state)
|
||||
|
||||
appSettings.$shouldCollapseRoomStateEvents
|
||||
.weakAssign(to: \.state.bindings.shouldCollapseRoomStateEvents, on: self)
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
override func process(viewAction: DeveloperOptionsScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .changedShouldCollapseRoomStateEvents:
|
||||
appSettings.shouldCollapseRoomStateEvents = state.bindings.shouldCollapseRoomStateEvents
|
||||
case .changedUserSuggestionsEnabled:
|
||||
appSettings.userSuggestionsEnabled = state.bindings.userSuggestionsEnabled
|
||||
case .clearCache:
|
||||
callback?(.clearCache)
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -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 Foundation
|
||||
|
||||
@MainActor
|
||||
protocol DeveloperOptionsScreenViewModelProtocol {
|
||||
var callback: ((DeveloperOptionsScreenViewModelAction) -> Void)? { get set }
|
||||
var context: DeveloperOptionsScreenViewModelType.Context { get }
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// 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 DeveloperOptionsScreen: View {
|
||||
@ObservedObject var context: DeveloperOptionsScreenViewModel.Context
|
||||
@State private var showConfetti = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Toggle(isOn: $context.shouldCollapseRoomStateEvents) {
|
||||
Text("Collapse room state events")
|
||||
}
|
||||
.onChange(of: context.shouldCollapseRoomStateEvents) { _ in
|
||||
context.send(viewAction: .changedShouldCollapseRoomStateEvents)
|
||||
}
|
||||
|
||||
Toggle(isOn: $context.userSuggestionsEnabled) {
|
||||
Text("User suggestions")
|
||||
}
|
||||
.onChange(of: context.userSuggestionsEnabled) { _ in
|
||||
context.send(viewAction: .changedUserSuggestionsEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button {
|
||||
showConfetti = true
|
||||
} label: {
|
||||
Text("🥳")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
context.send(viewAction: .clearCache)
|
||||
} label: {
|
||||
Text("Clear cache")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
.overlay(effectsView)
|
||||
.elementFormStyle()
|
||||
.navigationTitle(L10n.commonDeveloperOptions)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var effectsView: some View {
|
||||
if showConfetti {
|
||||
EffectsView(effect: .confetti)
|
||||
.ignoresSafeArea()
|
||||
.allowsHitTesting(false)
|
||||
.task { await removeConfettiAfterDelay() }
|
||||
}
|
||||
}
|
||||
|
||||
private func removeConfettiAfterDelay() async {
|
||||
try? await Task.sleep(for: .seconds(4))
|
||||
showConfetti = false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct DeveloperOptionsScreen_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let viewModel = DeveloperOptionsScreenViewModel()
|
||||
DeveloperOptionsScreen(context: viewModel.context)
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
final class LegalInformationScreenCoordinator: CoordinatorProtocol {
|
||||
private let viewModel = LegalInformationScreenViewModel()
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(LegalInformationScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
+23
@@ -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 Foundation
|
||||
|
||||
enum LegalInformationScreenViewModelAction { }
|
||||
|
||||
struct LegalInformationScreenViewState: BindableState { }
|
||||
|
||||
enum LegalInformationScreenViewAction { }
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// 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 LegalInformationScreenViewModelType = StateStoreViewModel<LegalInformationScreenViewState, LegalInformationScreenViewAction>
|
||||
|
||||
class LegalInformationScreenViewModel: LegalInformationScreenViewModelType, LegalInformationScreenViewModelProtocol {
|
||||
init() {
|
||||
super.init(initialViewState: LegalInformationScreenViewState())
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// 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 LegalInformationScreenViewModelProtocol {
|
||||
var context: LegalInformationScreenViewModelType.Context { get }
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// 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 LegalInformationScreen: View {
|
||||
@ObservedObject var context: LegalInformationScreenViewModel.Context
|
||||
@Environment(\.openURL) private var openURL
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Button(L10n.commonCopyright) {
|
||||
openURL(URL(staticString: "https://element.io/copyright"))
|
||||
}
|
||||
Button(L10n.commonAcceptableUsePolicy) {
|
||||
openURL(URL(staticString: "https://element.io/acceptable-use-policy-terms"))
|
||||
}
|
||||
Button(L10n.commonPrivacyPolicy) {
|
||||
openURL(URL(staticString: "https://element.io/privacy"))
|
||||
}
|
||||
}
|
||||
.buttonStyle(.compoundForm(accessory: .navigationLink))
|
||||
.compoundFormSection()
|
||||
}
|
||||
.compoundForm()
|
||||
.navigationTitle(L10n.commonAbout)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct LegalInformationScreen_Previews: PreviewProvider {
|
||||
static let viewModel = LegalInformationScreenViewModel()
|
||||
static var previews: some View {
|
||||
LegalInformationScreen(context: viewModel.context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
//
|
||||
// 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 SettingsScreenCoordinatorParameters {
|
||||
weak var navigationStackCoordinator: NavigationStackCoordinator?
|
||||
weak var userIndicatorController: UserIndicatorControllerProtocol?
|
||||
let userSession: UserSessionProtocol
|
||||
let bugReportService: BugReportServiceProtocol
|
||||
}
|
||||
|
||||
enum SettingsScreenCoordinatorAction {
|
||||
case dismiss
|
||||
case logout
|
||||
case clearCache
|
||||
}
|
||||
|
||||
final class SettingsScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: SettingsScreenCoordinatorParameters
|
||||
private var viewModel: SettingsScreenViewModelProtocol
|
||||
|
||||
var callback: ((SettingsScreenCoordinatorAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(parameters: SettingsScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = SettingsScreenViewModel(withUserSession: parameters.userSession)
|
||||
viewModel.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .close:
|
||||
self.callback?(.dismiss)
|
||||
case .analytics:
|
||||
self.presentAnalyticsScreen()
|
||||
case .reportBug:
|
||||
self.presentBugReportScreen()
|
||||
case .about:
|
||||
self.presentLegalInformationScreen()
|
||||
case .sessionVerification:
|
||||
self.verifySession()
|
||||
case .developerOptions:
|
||||
self.presentDeveloperOptions()
|
||||
case .logout:
|
||||
self.callback?(.logout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(SettingsScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func presentAnalyticsScreen() {
|
||||
let coordinator = AnalyticsSettingsScreenCoordinator()
|
||||
parameters.navigationStackCoordinator?.push(coordinator)
|
||||
}
|
||||
|
||||
private func presentBugReportScreen() {
|
||||
let params = BugReportScreenCoordinatorParameters(bugReportService: parameters.bugReportService,
|
||||
userID: parameters.userSession.userID,
|
||||
deviceID: parameters.userSession.deviceID,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
screenshot: nil,
|
||||
isModallyPresented: false)
|
||||
let coordinator = BugReportScreenCoordinator(parameters: params)
|
||||
coordinator.completion = { [weak self] result in
|
||||
switch result {
|
||||
case .finish:
|
||||
self?.showSuccess(label: L10n.actionDone)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
self?.parameters.navigationStackCoordinator?.pop()
|
||||
}
|
||||
|
||||
parameters.navigationStackCoordinator?.push(coordinator)
|
||||
}
|
||||
|
||||
private func presentLegalInformationScreen() {
|
||||
parameters.navigationStackCoordinator?.push(LegalInformationScreenCoordinator())
|
||||
}
|
||||
|
||||
private func verifySession() {
|
||||
guard let sessionVerificationController = parameters.userSession.sessionVerificationController else {
|
||||
fatalError("The sessionVerificationController should aways be valid at this point")
|
||||
}
|
||||
|
||||
let verificationParameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController)
|
||||
let coordinator = SessionVerificationScreenCoordinator(parameters: verificationParameters)
|
||||
|
||||
coordinator.callback = { [weak self] in
|
||||
self?.parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||
}
|
||||
|
||||
parameters.navigationStackCoordinator?.setSheetCoordinator(coordinator) { [weak self] in
|
||||
self?.parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func presentDeveloperOptions() {
|
||||
let coordinator = DeveloperOptionsScreenCoordinator()
|
||||
|
||||
coordinator.callback = { [weak self] action in
|
||||
switch action {
|
||||
case .clearCache:
|
||||
self?.callback?(.clearCache)
|
||||
}
|
||||
}
|
||||
|
||||
parameters.navigationStackCoordinator?.push(coordinator)
|
||||
}
|
||||
|
||||
private func showSuccess(label: String) {
|
||||
parameters.userIndicatorController?.submitIndicator(UserIndicator(title: label))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// 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
|
||||
import UIKit
|
||||
|
||||
enum SettingsScreenViewModelAction {
|
||||
case close
|
||||
case analytics
|
||||
case reportBug
|
||||
case about
|
||||
case sessionVerification
|
||||
case developerOptions
|
||||
case logout
|
||||
}
|
||||
|
||||
struct SettingsScreenViewState: BindableState {
|
||||
var bindings: SettingsScreenViewStateBindings
|
||||
var deviceID: String?
|
||||
var userID: String
|
||||
var userAvatarURL: URL?
|
||||
var userDisplayName: String?
|
||||
var showSessionVerificationSection: Bool
|
||||
var showDeveloperOptions: Bool
|
||||
}
|
||||
|
||||
struct SettingsScreenViewStateBindings {
|
||||
var timelineStyle: TimelineStyle
|
||||
}
|
||||
|
||||
enum SettingsScreenViewAction {
|
||||
case close
|
||||
case analytics
|
||||
case reportBug
|
||||
case about
|
||||
case sessionVerification
|
||||
case logout
|
||||
case changedTimelineStyle
|
||||
case developerOptions
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// 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 SettingsScreenViewModelType = StateStoreViewModel<SettingsScreenViewState, SettingsScreenViewAction>
|
||||
|
||||
class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewModelProtocol {
|
||||
private let userSession: UserSessionProtocol
|
||||
|
||||
var callback: ((SettingsScreenViewModelAction) -> Void)?
|
||||
|
||||
init(withUserSession userSession: UserSessionProtocol) {
|
||||
self.userSession = userSession
|
||||
let bindings = SettingsScreenViewStateBindings(timelineStyle: ServiceLocator.shared.settings.timelineStyle)
|
||||
|
||||
var showSessionVerificationSection = false
|
||||
if let sessionVerificationController = userSession.sessionVerificationController {
|
||||
showSessionVerificationSection = !sessionVerificationController.isVerified
|
||||
}
|
||||
|
||||
super.init(initialViewState: .init(bindings: bindings,
|
||||
deviceID: userSession.deviceID,
|
||||
userID: userSession.userID,
|
||||
showSessionVerificationSection: showSessionVerificationSection,
|
||||
showDeveloperOptions: ServiceLocator.shared.settings.canShowDeveloperOptions),
|
||||
imageProvider: userSession.mediaProvider)
|
||||
|
||||
ServiceLocator.shared.settings.$timelineStyle
|
||||
.weakAssign(to: \.state.bindings.timelineStyle, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
userSession.clientProxy.avatarURLPublisher
|
||||
.weakAssign(to: \.state.userAvatarURL, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
Task {
|
||||
await userSession.clientProxy.loadUserAvatarURL()
|
||||
}
|
||||
|
||||
Task {
|
||||
if case let .success(userDisplayName) = await self.userSession.clientProxy.loadUserDisplayName() {
|
||||
state.userDisplayName = userDisplayName
|
||||
}
|
||||
}
|
||||
|
||||
userSession.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
switch callback {
|
||||
case .sessionVerificationNeeded:
|
||||
self?.state.showSessionVerificationSection = true
|
||||
case .didVerifySession:
|
||||
self?.state.showSessionVerificationSection = false
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
override func process(viewAction: SettingsScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .close:
|
||||
callback?(.close)
|
||||
case .analytics:
|
||||
callback?(.analytics)
|
||||
case .reportBug:
|
||||
callback?(.reportBug)
|
||||
case .about:
|
||||
callback?(.about)
|
||||
case .logout:
|
||||
callback?(.logout)
|
||||
case .sessionVerification:
|
||||
callback?(.sessionVerification)
|
||||
case .changedTimelineStyle:
|
||||
ServiceLocator.shared.settings.timelineStyle = state.bindings.timelineStyle
|
||||
case .developerOptions:
|
||||
callback?(.developerOptions)
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -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 Foundation
|
||||
|
||||
@MainActor
|
||||
protocol SettingsScreenViewModelProtocol {
|
||||
var callback: ((SettingsScreenViewModelAction) -> Void)? { get set }
|
||||
var context: SettingsScreenViewModelType.Context { get }
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
//
|
||||
// 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 Compound
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsScreen: View {
|
||||
@State private var showingLogoutConfirmation = false
|
||||
|
||||
@ObservedObject var context: SettingsScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
userSection
|
||||
|
||||
if context.viewState.showSessionVerificationSection {
|
||||
sessionVerificationSection
|
||||
}
|
||||
|
||||
simplifiedSection
|
||||
|
||||
if context.viewState.showDeveloperOptions {
|
||||
developerOptionsSection
|
||||
}
|
||||
|
||||
signOutSection
|
||||
}
|
||||
.compoundForm()
|
||||
.navigationTitle(L10n.commonSettings)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
doneButton
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var versionText: some View {
|
||||
Text(L10n.settingsVersionNumber(InfoPlistReader.main.bundleShortVersionString, InfoPlistReader.main.bundleVersion))
|
||||
}
|
||||
|
||||
private var userSection: some View {
|
||||
Section {
|
||||
HStack(spacing: 12) {
|
||||
LoadableAvatarImage(url: context.viewState.userAvatarURL,
|
||||
name: context.viewState.userDisplayName,
|
||||
contentID: context.viewState.userID,
|
||||
avatarSize: .user(on: .settings),
|
||||
imageProvider: context.imageProvider)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(context.viewState.userDisplayName ?? "")
|
||||
.font(.compound.headingMD)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
Text(context.viewState.userID)
|
||||
.font(.compound.bodySM)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
.compoundFormSection()
|
||||
}
|
||||
|
||||
private var sessionVerificationSection: some View {
|
||||
Section {
|
||||
Button { context.send(viewAction: .sessionVerification) } label: {
|
||||
Label(L10n.actionCompleteVerification, systemImage: "checkmark.shield")
|
||||
}
|
||||
.buttonStyle(.compoundForm())
|
||||
}
|
||||
.compoundFormSection()
|
||||
}
|
||||
|
||||
private var developerOptionsSection: some View {
|
||||
Section {
|
||||
Button { context.send(viewAction: .developerOptions) } label: {
|
||||
Label(L10n.commonDeveloperOptions, systemImage: "hammer.circle")
|
||||
}
|
||||
.buttonStyle(.compoundForm(accessory: .navigationLink))
|
||||
.accessibilityIdentifier("developerOptionsButton")
|
||||
}
|
||||
.compoundFormSection()
|
||||
}
|
||||
|
||||
private var simplifiedSection: some View {
|
||||
Section {
|
||||
Picker(selection: $context.timelineStyle) {
|
||||
ForEach(TimelineStyle.allCases, id: \.self) { style in
|
||||
Text(style.name)
|
||||
.tag(style)
|
||||
}
|
||||
} label: {
|
||||
Label(L10n.commonMessageLayout, systemImage: "rectangle.grid.1x2")
|
||||
}
|
||||
.labelStyle(.compoundFormRow())
|
||||
.accessibilityIdentifier("timelineStylePicker")
|
||||
.onChange(of: context.timelineStyle) { _ in
|
||||
context.send(viewAction: .changedTimelineStyle)
|
||||
}
|
||||
|
||||
// Analytics
|
||||
Button { context.send(viewAction: .analytics) } label: {
|
||||
Label(L10n.commonAnalytics, systemImage: "chart.bar")
|
||||
}
|
||||
.buttonStyle(.compoundForm(accessory: .navigationLink))
|
||||
.accessibilityIdentifier("analyticsButton")
|
||||
|
||||
// Report Bug
|
||||
Button { context.send(viewAction: .reportBug) } label: {
|
||||
Label(L10n.actionReportBug, systemImage: "ladybug")
|
||||
}
|
||||
.buttonStyle(.compoundForm(accessory: .navigationLink))
|
||||
.accessibilityIdentifier("reportBugButton")
|
||||
|
||||
// About
|
||||
Button { context.send(viewAction: .about) } label: {
|
||||
Label(L10n.commonAbout, systemImage: "questionmark.circle")
|
||||
}
|
||||
.buttonStyle(.compoundForm(accessory: .navigationLink))
|
||||
.accessibilityIdentifier("aboutButton")
|
||||
}
|
||||
.compoundFormSection()
|
||||
}
|
||||
|
||||
private var signOutSection: some View {
|
||||
Section {
|
||||
Button { showingLogoutConfirmation = true } label: {
|
||||
Label(L10n.screenSignoutPreferenceItem, systemImage: "rectangle.portrait.and.arrow.right")
|
||||
}
|
||||
.buttonStyle(.compoundForm())
|
||||
.accessibilityIdentifier("logoutButton")
|
||||
.alert(L10n.screenSignoutConfirmationDialogTitle, isPresented: $showingLogoutConfirmation) {
|
||||
Button(L10n.screenSignoutConfirmationDialogSubmit,
|
||||
role: .destructive,
|
||||
action: logout)
|
||||
} message: {
|
||||
Text(L10n.screenSignoutConfirmationDialogContent)
|
||||
}
|
||||
} footer: {
|
||||
VStack {
|
||||
versionText
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
context.viewState.deviceID.map(Text.init)
|
||||
}
|
||||
.compoundFormSectionFooter()
|
||||
.padding(.top, 24)
|
||||
}
|
||||
.compoundFormSection()
|
||||
}
|
||||
|
||||
private var doneButton: some View {
|
||||
Button(L10n.actionDone, action: close)
|
||||
}
|
||||
|
||||
private func close() {
|
||||
context.send(viewAction: .close)
|
||||
}
|
||||
|
||||
private func logout() {
|
||||
context.send(viewAction: .logout)
|
||||
}
|
||||
}
|
||||
|
||||
private extension TimelineStyle {
|
||||
var name: String {
|
||||
switch self {
|
||||
case .plain:
|
||||
return L10n.commonModern
|
||||
case .bubbles:
|
||||
return L10n.commonBubbles
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct SettingsScreen_Previews: PreviewProvider {
|
||||
static let viewModel = {
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@userid:example.com"),
|
||||
mediaProvider: MockMediaProvider())
|
||||
return SettingsScreenViewModel(withUserSession: userSession)
|
||||
}()
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
SettingsScreen(context: viewModel.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user