mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
#106: Add Analytics UI
This commit is contained in:
@@ -286,6 +286,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
|
||||
private func presentSettingsScreen() {
|
||||
let parameters = SettingsCoordinatorParameters(navigationRouter: navigationRouter,
|
||||
userSession: userSession,
|
||||
bugReportService: bugReportService)
|
||||
let coordinator = SettingsCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self] action in
|
||||
|
||||
@@ -23,6 +23,8 @@ internal typealias AssetImageTypeAlias = ImageAsset.Image
|
||||
// swiftlint:disable identifier_name line_length nesting type_body_length type_name
|
||||
internal enum Asset {
|
||||
internal enum Images {
|
||||
internal static let analyticsCheckmark = ImageAsset(name: "Images/AnalyticsCheckmark")
|
||||
internal static let analyticsLogo = ImageAsset(name: "Images/AnalyticsLogo")
|
||||
internal static let serverSelectionIcon = ImageAsset(name: "Images/Server Selection Icon")
|
||||
internal static let splashScreenPage1 = ImageAsset(name: "Images/Splash Screen Page 1")
|
||||
internal static let splashScreenPage2 = ImageAsset(name: "Images/Splash Screen Page 2")
|
||||
|
||||
@@ -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
|
||||
|
||||
struct AnalyticsPromptCoordinatorParameters {
|
||||
/// The user session to use if analytics are enabled.
|
||||
let userSession: UserSessionProtocol
|
||||
}
|
||||
|
||||
final class AnalyticsPromptCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let parameters: AnalyticsPromptCoordinatorParameters
|
||||
private let analyticsPromptHostingController: UIViewController
|
||||
private var analyticsPromptViewModel: AnalyticsPromptViewModel
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: (@MainActor () -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(parameters: AnalyticsPromptCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = AnalyticsPromptViewModel(termsURL: BuildSettings.analyticsConfiguration.termsURL)
|
||||
|
||||
let view = AnalyticsPrompt(context: viewModel.context)
|
||||
analyticsPromptViewModel = viewModel
|
||||
analyticsPromptHostingController = UIHostingController(rootView: view)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("Did start.")
|
||||
|
||||
analyticsPromptViewModel.callback = { [weak self] result in
|
||||
MXLog.debug("AnalyticsPromptViewModel did complete with result: \(result).")
|
||||
|
||||
guard let self = self else { return }
|
||||
|
||||
switch result {
|
||||
case .enable:
|
||||
Analytics.shared.optIn(with: self.parameters.userSession)
|
||||
self.callback?()
|
||||
case .disable:
|
||||
Analytics.shared.optOut()
|
||||
self.callback?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController { analyticsPromptHostingController }
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
enum AnalyticsPromptViewAction {
|
||||
/// Enable analytics.
|
||||
case enable
|
||||
/// Disable analytics.
|
||||
case disable
|
||||
}
|
||||
|
||||
enum AnalyticsPromptViewModelAction {
|
||||
/// Enable analytics.
|
||||
case enable
|
||||
/// Disable analytics.
|
||||
case disable
|
||||
}
|
||||
|
||||
struct AnalyticsPromptViewState: BindableState {
|
||||
/// Attributed strings created from localized HTML.
|
||||
let strings = AnalyticsPromptStrings()
|
||||
}
|
||||
|
||||
/// A collection of strings for the UI that need to be parsed from HTML
|
||||
struct AnalyticsPromptStrings {
|
||||
let optInContent: AttributedString
|
||||
let point1 = AttributedStringBuilder().fromHTML(ElementL10n.analyticsOptInListItem1) ?? AttributedString(ElementL10n.analyticsOptInListItem1)
|
||||
let point2 = AttributedStringBuilder().fromHTML(ElementL10n.analyticsOptInListItem2) ?? AttributedString(ElementL10n.analyticsOptInListItem2)
|
||||
|
||||
init() {
|
||||
// Create the opt in content with a placeholder.
|
||||
let linkPlaceholder = "{link}"
|
||||
var optInContent = AttributedString(ElementL10n.analyticsOptInContent(ElementInfoPlist.cfBundleName, linkPlaceholder))
|
||||
|
||||
guard let range = optInContent.range(of: linkPlaceholder) else {
|
||||
self.optInContent = AttributedString(ElementL10n.analyticsOptInContent(ElementInfoPlist.cfBundleName,
|
||||
ElementL10n.analyticsOptInContentLink))
|
||||
MXLog.failure("Failed to add a link attribute to the opt in content.")
|
||||
return
|
||||
}
|
||||
|
||||
// Replace the placeholder with a link.
|
||||
var link = AttributedString(ElementL10n.analyticsOptInContentLink)
|
||||
link.link = BuildSettings.analyticsConfiguration.termsURL
|
||||
optInContent.replaceSubrange(range, with: link)
|
||||
|
||||
self.optInContent = optInContent
|
||||
}
|
||||
}
|
||||
@@ -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 Combine
|
||||
import SwiftUI
|
||||
|
||||
typealias AnalyticsPromptViewModelType = StateStoreViewModel<AnalyticsPromptViewState, AnalyticsPromptViewAction>
|
||||
|
||||
class AnalyticsPromptViewModel: AnalyticsPromptViewModelType, AnalyticsPromptViewModelProtocol {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let termsURL: URL
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var callback: (@MainActor (AnalyticsPromptViewModelAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
/// Initialize a view model with the specified prompt type and app display name.
|
||||
init(termsURL: URL) {
|
||||
self.termsURL = termsURL
|
||||
super.init(initialViewState: AnalyticsPromptViewState())
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: AnalyticsPromptViewAction) async {
|
||||
switch viewAction {
|
||||
case .enable:
|
||||
callback?(.enable)
|
||||
case .disable:
|
||||
callback?(.disable)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
@MainActor
|
||||
protocol AnalyticsPromptViewModelProtocol {
|
||||
var callback: (@MainActor (AnalyticsPromptViewModelAction) -> Void)? { get set }
|
||||
var context: AnalyticsPromptViewModelType.Context { get }
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
//
|
||||
// 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 prompt that asks the user whether they would like to enable Analytics or not.
|
||||
struct AnalyticsPrompt: View {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let horizontalPadding: CGFloat = 16
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var context: AnalyticsPromptViewModel.Context
|
||||
|
||||
// MARK: Views
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ScrollView {
|
||||
Spacer()
|
||||
.frame(height: UIConstants.spacerHeight(in: geometry))
|
||||
|
||||
mainContent
|
||||
.readableFrame()
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
.padding(.top, UIConstants.onboardingBreakerScreenTopPadding)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
VStack {
|
||||
buttons
|
||||
.readableFrame()
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
.padding(.bottom, UIConstants.actionButtonBottomPadding)
|
||||
|
||||
Spacer()
|
||||
.frame(height: UIConstants.spacerHeight(in: geometry))
|
||||
}
|
||||
.padding(.top, 8)
|
||||
.background(Color.element.background.ignoresSafeArea())
|
||||
}
|
||||
.background(Color.element.background.ignoresSafeArea())
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
}
|
||||
|
||||
/// The main content of the screen that is shown inside the scroll view.
|
||||
private var mainContent: some View {
|
||||
VStack {
|
||||
Image(uiImage: Asset.Images.analyticsLogo.image)
|
||||
.padding(.bottom, 25)
|
||||
|
||||
Text(ElementL10n.analyticsOptInTitle(ElementInfoPlist.cfBundleName))
|
||||
.font(.element.title2B)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.padding(.bottom, 2)
|
||||
|
||||
Text(context.viewState.strings.optInContent)
|
||||
.font(.element.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
|
||||
Divider()
|
||||
.background(Color.element.quinaryContent)
|
||||
.padding(.vertical, 28)
|
||||
|
||||
checkmarkList
|
||||
}
|
||||
}
|
||||
|
||||
/// The list of re-assurances about analytics.
|
||||
private var checkmarkList: some View {
|
||||
VStack(alignment: .leading) {
|
||||
AnalyticsPromptCheckmarkItem(attributedString: context.viewState.strings.point1)
|
||||
AnalyticsPromptCheckmarkItem(attributedString: context.viewState.strings.point1)
|
||||
AnalyticsPromptCheckmarkItem(string: ElementL10n.analyticsOptInListItem3)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.font(.element.body)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
/// The stack of enable/disable buttons.
|
||||
private var buttons: some View {
|
||||
VStack {
|
||||
Button { context.send(viewAction: .enable) } label: {
|
||||
Text(ElementL10n.actionEnable)
|
||||
.font(.element.bodySB)
|
||||
}
|
||||
.buttonStyle(.elementAction(.xLarge))
|
||||
.accessibilityIdentifier("enableButton")
|
||||
|
||||
Button { context.send(viewAction: .disable) } label: {
|
||||
Text(ElementL10n.actionNotNow)
|
||||
.font(.element.bodySB)
|
||||
.padding(12)
|
||||
}
|
||||
.accessibilityIdentifier("disableButton")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct AnalyticsPrompt_Previews: PreviewProvider {
|
||||
static let viewModel = AnalyticsPromptViewModel(termsURL: BuildSettings.analyticsConfiguration.termsURL)
|
||||
static var previews: some View {
|
||||
AnalyticsPrompt(context: viewModel.context)
|
||||
.tint(.element.accent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
struct AnalyticsPromptCheckmarkItem: View {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let attributedString: AttributedString
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(attributedString: AttributedString) {
|
||||
self.attributedString = attributedString
|
||||
}
|
||||
|
||||
init(string: String) {
|
||||
attributedString = AttributedString(string)
|
||||
}
|
||||
|
||||
// MARK: - Views
|
||||
|
||||
var body: some View {
|
||||
Label { Text(attributedString) } icon: {
|
||||
Image(uiImage: Asset.Images.analyticsCheckmark.image)
|
||||
.foregroundColor(.element.accent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct AnalyticsPromptCheckmarkItem_Previews: PreviewProvider {
|
||||
static let strings = AnalyticsPromptStrings()
|
||||
|
||||
static var previews: some View {
|
||||
VStack(alignment: .leading) {
|
||||
AnalyticsPromptCheckmarkItem(attributedString: strings.point1)
|
||||
AnalyticsPromptCheckmarkItem(attributedString: strings.point2)
|
||||
AnalyticsPromptCheckmarkItem(attributedString: AttributedString("This is a short string."))
|
||||
AnalyticsPromptCheckmarkItem(attributedString: AttributedString("This is a very long string that will be used to test the layout over multiple lines of text to ensure everything is correct."))
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ class AuthenticationCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Shows the splash screen as the root view in the navigation stack.
|
||||
private func showSplashScreen() {
|
||||
let coordinator = SplashScreenCoordinator()
|
||||
|
||||
@@ -129,6 +130,23 @@ class AuthenticationCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
}
|
||||
|
||||
private func showAnalyticsPrompt(with userSession: UserSessionProtocol) {
|
||||
let parameters = AnalyticsPromptCoordinatorParameters(userSession: userSession)
|
||||
let coordinator = AnalyticsPromptCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.callback = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
|
||||
}
|
||||
|
||||
coordinator.start()
|
||||
add(childCoordinator: coordinator)
|
||||
|
||||
navigationRouter.setRootModule(coordinator, hideNavigationBar: true, animated: true) { [weak self] in
|
||||
self?.remove(childCoordinator: coordinator)
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a blocking activity indicator.
|
||||
private func startLoading() {
|
||||
activityIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true))
|
||||
|
||||
@@ -25,6 +25,9 @@ struct UIConstants {
|
||||
/// The padding used between the footer and the bottom of the view.
|
||||
static let actionButtonBottomPadding: CGFloat = 24
|
||||
|
||||
/// The padding used to the top of the view for breaker screens that don't have a navigation bar.
|
||||
static let onboardingBreakerScreenTopPadding: CGFloat = 80
|
||||
|
||||
/// The height to use for top/bottom spacers to pad the views to fit the `maxContentHeight`.
|
||||
static func spacerHeight(in geometry: GeometryProxy) -> CGFloat {
|
||||
max(0, (geometry.size.height - maxContentHeight) / 2)
|
||||
|
||||
@@ -18,6 +18,7 @@ import SwiftUI
|
||||
|
||||
struct SettingsCoordinatorParameters {
|
||||
let navigationRouter: NavigationRouterType
|
||||
let userSession: UserSessionProtocol
|
||||
let bugReportService: BugReportServiceProtocol
|
||||
}
|
||||
|
||||
@@ -60,6 +61,8 @@ final class SettingsCoordinator: Coordinator, Presentable {
|
||||
guard let self = self else { return }
|
||||
MXLog.debug("SettingsViewModel did complete with result: \(result).")
|
||||
switch result {
|
||||
case .toggleAnalytics:
|
||||
self.toggleAnalytics()
|
||||
case .reportBug:
|
||||
self.presentBugReportScreen()
|
||||
case .crash:
|
||||
@@ -82,17 +85,12 @@ final class SettingsCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
/// - Parameters:
|
||||
/// - label: The label to show on the indicator.
|
||||
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
|
||||
private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) {
|
||||
loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
private func stopLoading() {
|
||||
loadingIndicator = nil
|
||||
private func toggleAnalytics() {
|
||||
if ElementSettings.shared.enableAnalytics {
|
||||
Analytics.shared.optOut()
|
||||
} else {
|
||||
Analytics.shared.optIn(with: parameters.userSession)
|
||||
}
|
||||
}
|
||||
|
||||
private func presentBugReportScreen() {
|
||||
@@ -114,6 +112,19 @@ final class SettingsCoordinator: Coordinator, Presentable {
|
||||
self.remove(childCoordinator: coordinator)
|
||||
}
|
||||
}
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
/// - Parameters:
|
||||
/// - label: The label to show on the indicator.
|
||||
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
|
||||
private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) {
|
||||
loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
private func stopLoading() {
|
||||
loadingIndicator = nil
|
||||
}
|
||||
|
||||
/// Show success indicator
|
||||
private func showSuccess(label: String) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import Foundation
|
||||
// MARK: View model
|
||||
|
||||
enum SettingsViewModelAction {
|
||||
case toggleAnalytics
|
||||
case reportBug
|
||||
case crash
|
||||
case logout
|
||||
@@ -32,9 +33,12 @@ struct SettingsViewState: BindableState {
|
||||
var bindings: SettingsViewStateBindings
|
||||
}
|
||||
|
||||
struct SettingsViewStateBindings { }
|
||||
struct SettingsViewStateBindings {
|
||||
var enableAnalytics = ElementSettings.shared.enableAnalytics
|
||||
}
|
||||
|
||||
enum SettingsViewAction {
|
||||
case toggleAnalytics
|
||||
case reportBug
|
||||
case crash
|
||||
case logout
|
||||
|
||||
@@ -40,6 +40,8 @@ class SettingsViewModel: SettingsViewModelType, SettingsViewModelProtocol {
|
||||
|
||||
override func process(viewAction: SettingsViewAction) async {
|
||||
switch viewAction {
|
||||
case .toggleAnalytics:
|
||||
callback?(.toggleAnalytics)
|
||||
case .reportBug:
|
||||
callback?(.reportBug)
|
||||
case .crash:
|
||||
|
||||
@@ -31,43 +31,14 @@ struct SettingsScreen: View {
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Button { context.send(viewAction: .reportBug) } label: {
|
||||
Text(ElementL10n.sendBugReport)
|
||||
}
|
||||
.foregroundColor(Color.element.primaryContent)
|
||||
.accessibilityIdentifier("reportBugButton")
|
||||
|
||||
if BuildSettings.settingsCrashButtonVisible {
|
||||
Button("Crash the app",
|
||||
role: .destructive) { context.send(viewAction: .crash)
|
||||
}
|
||||
|
||||
.accessibilityIdentifier("crashButton")
|
||||
}
|
||||
}
|
||||
.listRowBackground(rowBackgroundColor)
|
||||
analyticsSection
|
||||
.listRowBackground(rowBackgroundColor)
|
||||
|
||||
userInterfaceSection
|
||||
.listRowBackground(rowBackgroundColor)
|
||||
|
||||
Section {
|
||||
Button { showingLogoutConfirmation = true } label: {
|
||||
Text(ElementL10n.actionSignOut)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.foregroundColor(Color.element.primaryContent)
|
||||
.accessibilityIdentifier("logoutButton")
|
||||
.confirmationDialog(ElementL10n.actionSignOutConfirmationSimple,
|
||||
isPresented: $showingLogoutConfirmation,
|
||||
titleVisibility: .visible) {
|
||||
Button(ElementL10n.actionSignOut,
|
||||
role: .destructive) { context.send(viewAction: .logout)
|
||||
}
|
||||
}
|
||||
} footer: {
|
||||
versionText
|
||||
}
|
||||
.listRowBackground(rowBackgroundColor)
|
||||
logoutSection
|
||||
.listRowBackground(rowBackgroundColor)
|
||||
}
|
||||
.introspectTableView { tableView in
|
||||
tableView.backgroundColor = .clear
|
||||
@@ -87,11 +58,28 @@ struct SettingsScreen: View {
|
||||
private var rowBackgroundColor: Color {
|
||||
colorScheme == .light ? .element.background : .element.system
|
||||
}
|
||||
|
||||
private var analyticsSection: some View {
|
||||
Section(ElementL10n.settingsAnalytics) {
|
||||
Button { context.send(viewAction: .reportBug) } label: {
|
||||
Text(ElementL10n.sendBugReport)
|
||||
}
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.accessibilityIdentifier("reportBugButton")
|
||||
|
||||
if BuildSettings.settingsCrashButtonVisible {
|
||||
Button("Crash the app",
|
||||
role: .destructive) { context.send(viewAction: .crash)
|
||||
}
|
||||
.accessibilityIdentifier("crashButton")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var userInterfaceSection: some View {
|
||||
if BuildSettings.settingsShowTimelineStyle {
|
||||
Section(header: Text(ElementL10n.settingsUserInterface)) {
|
||||
Section(ElementL10n.settingsUserInterface) {
|
||||
Picker(ElementL10n.settingsTimelineStyle, selection: $settings.timelineStyle) {
|
||||
ForEach(TimelineStyle.allCases, id: \.self) { style in
|
||||
Text(style.description)
|
||||
@@ -100,7 +88,26 @@ struct SettingsScreen: View {
|
||||
}
|
||||
.accessibilityIdentifier("timelineStylePicker")
|
||||
}
|
||||
.listRowBackground(rowBackgroundColor)
|
||||
}
|
||||
}
|
||||
|
||||
private var logoutSection: some View {
|
||||
Section {
|
||||
Button { showingLogoutConfirmation = true } label: {
|
||||
Text(ElementL10n.actionSignOut)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.accessibilityIdentifier("logoutButton")
|
||||
.confirmationDialog(ElementL10n.actionSignOutConfirmationSimple,
|
||||
isPresented: $showingLogoutConfirmation,
|
||||
titleVisibility: .visible) {
|
||||
Button(ElementL10n.actionSignOut,
|
||||
role: .destructive) { context.send(viewAction: .logout)
|
||||
}
|
||||
}
|
||||
} footer: {
|
||||
versionText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,14 +56,14 @@ class Analytics {
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
/// Opts in to analytics tracking with the supplied session.
|
||||
/// - Parameter session: An optional session to use to when reading/generating the analytics ID.
|
||||
/// Opts in to analytics tracking with the supplied user session.
|
||||
/// - Parameter userSession: The user session to use to when reading/generating the analytics ID.
|
||||
/// The session will be ignored if not running.
|
||||
func optIn(with session: UserSessionProtocol) {
|
||||
func optIn(with userSession: UserSessionProtocol) {
|
||||
ElementSettings.shared.enableAnalytics = true
|
||||
startIfEnabled()
|
||||
|
||||
Task { await useAnalyticsSettings(from: session) }
|
||||
Task { await useAnalyticsSettings(from: userSession) }
|
||||
}
|
||||
|
||||
/// Stops analytics tracking and calls `reset` to clear any IDs and event queues.
|
||||
@@ -95,16 +95,16 @@ class Analytics {
|
||||
// MXLogger.setBuildVersion(ElementInfoPlist.cfBundleShortVersionString)
|
||||
}
|
||||
|
||||
/// Use the analytics settings from the supplied session to configure analytics.
|
||||
/// Use the analytics settings from the supplied user session to configure analytics.
|
||||
/// For now this is only used for (pseudonymous) identification.
|
||||
/// - Parameter session: The session to read analytics settings from.
|
||||
func useAnalyticsSettings(from session: UserSessionProtocol) async {
|
||||
/// - Parameter userSession: The user session to read analytics settings from.
|
||||
func useAnalyticsSettings(from userSession: UserSessionProtocol) async {
|
||||
guard
|
||||
ElementSettings.shared.enableAnalytics,
|
||||
!ElementSettings.shared.isIdentifiedForAnalytics
|
||||
else { return }
|
||||
|
||||
let service = AnalyticsService(session: session)
|
||||
let service = AnalyticsService(userSession: userSession)
|
||||
self.service = service
|
||||
|
||||
switch await service.settings() {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import Foundation
|
||||
|
||||
enum AnalyticsServiceError: Error {
|
||||
/// The session supplied to the service does not have a state of `MXSessionStateRunning`.
|
||||
/// The user session supplied to the service does not have a state of `MXSessionStateRunning`.
|
||||
case sessionIsNotRunning
|
||||
/// The service failed to get or update the analytics settings event from the user's account data.
|
||||
case accountDataFailure
|
||||
@@ -25,12 +25,12 @@ enum AnalyticsServiceError: Error {
|
||||
|
||||
/// A service responsible for handling the `im.vector.analytics` event from the user's account data.
|
||||
class AnalyticsService {
|
||||
let session: UserSessionProtocol
|
||||
let userSession: UserSessionProtocol
|
||||
|
||||
/// Creates an analytics service with the supplied session.
|
||||
/// - Parameter session: The session to use when reading analytics settings from account data.
|
||||
init(session: UserSessionProtocol) {
|
||||
self.session = session
|
||||
/// Creates an analytics service with the supplied user session.
|
||||
/// - Parameter userSession: The user session to use when reading analytics settings from account data.
|
||||
init(userSession: UserSessionProtocol) {
|
||||
self.userSession = userSession
|
||||
}
|
||||
|
||||
/// The analytics settings for the current user. Calling this method will check whether the settings already
|
||||
@@ -41,12 +41,12 @@ class AnalyticsService {
|
||||
func settings() async -> Result<AnalyticsSettings, AnalyticsServiceError> {
|
||||
// Only use the session if it is running otherwise we could wipe out an existing analytics ID.
|
||||
fatalWithoutUnreachableCodeWarning()
|
||||
// guard session.state == .running else {
|
||||
// guard userSession.state == .running else {
|
||||
// MXLog.warning("Aborting attempt to read analytics settings. The session may not be up-to-date.")
|
||||
// return .failure(.sessionIsNotRunning)
|
||||
// }
|
||||
|
||||
let result: Result<AnalyticsSettings?, ClientProxyError> = await session.clientProxy.accountDataEvent(type: AnalyticsSettings.eventType)
|
||||
let result: Result<AnalyticsSettings?, ClientProxyError> = await userSession.clientProxy.accountDataEvent(type: AnalyticsSettings.eventType)
|
||||
switch result {
|
||||
case .failure:
|
||||
return .failure(.accountDataFailure)
|
||||
@@ -57,7 +57,7 @@ class AnalyticsService {
|
||||
}
|
||||
|
||||
let newSettings = AnalyticsSettings.new(currentEvent: settings)
|
||||
switch await session.clientProxy.setAccountData(content: newSettings, type: AnalyticsSettings.eventType) {
|
||||
switch await userSession.clientProxy.setAccountData(content: newSettings, type: AnalyticsSettings.eventType) {
|
||||
case .failure:
|
||||
MXLog.warning("Failed to update analytics settings.")
|
||||
return .failure(.accountDataFailure)
|
||||
|
||||
@@ -21,6 +21,7 @@ enum UITestScreenIdentifier: String {
|
||||
case serverSelection
|
||||
case serverSelectionNonModal
|
||||
case authenticationFlow
|
||||
case analyticsPrompt
|
||||
case simpleRegular
|
||||
case simpleUpgrade
|
||||
case settings
|
||||
|
||||
@@ -76,6 +76,9 @@ class MockScreen: Identifiable {
|
||||
case .serverSelectionNonModal:
|
||||
return ServerSelectionCoordinator(parameters: .init(authenticationService: MockAuthenticationServiceProxy(),
|
||||
hasModalPresentation: false))
|
||||
case .analyticsPrompt:
|
||||
return AnalyticsPromptCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:client.com"),
|
||||
mediaProvider: MockMediaProvider())))
|
||||
case .authenticationFlow:
|
||||
return AuthenticationCoordinator(authenticationService: MockAuthenticationServiceProxy(),
|
||||
navigationRouter: navigationRouter)
|
||||
@@ -85,6 +88,8 @@ class MockScreen: Identifiable {
|
||||
return TemplateCoordinator(parameters: .init(promptType: .upgrade))
|
||||
case .settings:
|
||||
return SettingsCoordinator(parameters: .init(navigationRouter: navigationRouter,
|
||||
userSession: MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:client.com"),
|
||||
mediaProvider: MockMediaProvider()),
|
||||
bugReportService: MockBugReportService()))
|
||||
case .bugReport:
|
||||
return BugReportCoordinator(parameters: .init(bugReportService: MockBugReportService(),
|
||||
|
||||
Reference in New Issue
Block a user