mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 08:15:24 +00:00
refactor(healthchat): move the session hooks onto UserHooks
This commit is contained in:
+437
-142
File diff suppressed because it is too large
Load Diff
@@ -12,12 +12,6 @@ import Synchronization
|
|||||||
|
|
||||||
final nonisolated class AppHooks: AppHooksProtocol {
|
final nonisolated class AppHooks: AppHooksProtocol {
|
||||||
#if IS_MAIN_APP
|
#if IS_MAIN_APP
|
||||||
func configure(with userSession: UserSessionProtocol?) async {
|
|
||||||
await userSessionHook.configure(with: userSession)
|
|
||||||
await extendedProfileSectionHook.configure(with: userSession)
|
|
||||||
await settingsScreenHook.configure(with: userSession)
|
|
||||||
}
|
|
||||||
|
|
||||||
@AppHook(default: DefaultAppSettingsHook())
|
@AppHook(default: DefaultAppSettingsHook())
|
||||||
var appSettingsHook: AppSettingsHookProtocol
|
var appSettingsHook: AppSettingsHookProtocol
|
||||||
|
|
||||||
@@ -30,38 +24,15 @@ final nonisolated class AppHooks: AppHooksProtocol {
|
|||||||
@AppHook(default: DefaultOAuthPresenterHook())
|
@AppHook(default: DefaultOAuthPresenterHook())
|
||||||
var oAuthPresenterHook: OAuthPresenterHookProtocol
|
var oAuthPresenterHook: OAuthPresenterHookProtocol
|
||||||
|
|
||||||
@AppHook(default: DefaultUserSessionHook())
|
// HealthChat app-level hooks. The session-scoped ones live in `UserHooks`.
|
||||||
var userSessionHook: UserSessionHookProtocol
|
|
||||||
|
|
||||||
@AppHook(default: DefaultRoomScreenHook())
|
|
||||||
var roomScreenHook: RoomScreenHookProtocol
|
|
||||||
|
|
||||||
@AppHook(default: DefaultRoomDetailsScreenHook())
|
|
||||||
var roomDetailsScreenHook: RoomDetailsScreenHookProtocol
|
|
||||||
|
|
||||||
@AppHook(default: DefaultRoomMemberDetailsScreenHook())
|
|
||||||
var roomMemberDetailsScreenHook: RoomMemberDetailsScreenHookProtocol
|
|
||||||
|
|
||||||
@AppHook(default: DefaultUserProfileScreenHook())
|
|
||||||
var userProfileScreenHook: UserProfileScreenHookProtocol
|
|
||||||
|
|
||||||
@AppHook(default: DefaultExtendedProfileSectionHook())
|
|
||||||
var extendedProfileSectionHook: ExtendedProfileSectionHookProtocol
|
|
||||||
|
|
||||||
@AppHook(default: DefaultHomeTabsHook())
|
@AppHook(default: DefaultHomeTabsHook())
|
||||||
var homeTabsHook: HomeTabsHookProtocol
|
var homeTabsHook: HomeTabsHookProtocol
|
||||||
|
|
||||||
@AppHook(default: DefaultSettingsScreenHook())
|
|
||||||
var settingsScreenHook: SettingsScreenHookProtocol
|
|
||||||
|
|
||||||
@AppHook(default: DefaultUserDetailsEditScreenHook())
|
@AppHook(default: DefaultUserDetailsEditScreenHook())
|
||||||
var userDetailsEditScreenHook: UserDetailsEditScreenHookProtocol
|
var userDetailsEditScreenHook: UserDetailsEditScreenHookProtocol
|
||||||
|
|
||||||
@AppHook(default: DefaultDeveloperOptionsScreenHook())
|
@AppHook(default: DefaultDeveloperOptionsScreenHook())
|
||||||
var developerOptionsScreenHook: DeveloperOptionsScreenHookProtocol
|
var developerOptionsScreenHook: DeveloperOptionsScreenHookProtocol
|
||||||
|
|
||||||
@AppHook(default: DefaultRecoveryKeyScreenHook())
|
|
||||||
var recoveryKeyScreenHook: RecoveryKeyScreenHookProtocol
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@AppHook(default: DefaultTracingHook())
|
@AppHook(default: DefaultTracingHook())
|
||||||
@@ -72,6 +43,9 @@ final nonisolated class AppHooks: AppHooksProtocol {
|
|||||||
|
|
||||||
@AppHook(default: DefaultRemoteSettingsHook())
|
@AppHook(default: DefaultRemoteSettingsHook())
|
||||||
var remoteSettingsHook: RemoteSettingsHookProtocol
|
var remoteSettingsHook: RemoteSettingsHookProtocol
|
||||||
|
|
||||||
|
@AppHook(default: DefaultUserSessionHook())
|
||||||
|
var userSessionHook: UserSessionHookProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
nonisolated protocol AppHooksProtocol: Sendable {
|
nonisolated protocol AppHooksProtocol: Sendable {
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ extension AppHooks {
|
|||||||
#if IS_MAIN_APP
|
#if IS_MAIN_APP
|
||||||
registerAppSettingsHook(HealthChatAppSettingsHook())
|
registerAppSettingsHook(HealthChatAppSettingsHook())
|
||||||
registerCompoundHook(HealthChatCompoundHook())
|
registerCompoundHook(HealthChatCompoundHook())
|
||||||
registerExtendedProfileSectionHook(HealthChatExtendedProfileSectionHook())
|
|
||||||
registerHomeTabsHook(HealthChatHomeTabsHook())
|
registerHomeTabsHook(HealthChatHomeTabsHook())
|
||||||
registerSettingsScreenHook(HealthChatSettingsScreenHook())
|
registerUserSessionHook(HealthChatUserSessionHook())
|
||||||
registerUserDetailsEditScreenHook(HealthChatUserDetailsEditScreenHook())
|
registerUserDetailsEditScreenHook(HealthChatUserDetailsEditScreenHook())
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,20 +12,14 @@ import SwiftUI
|
|||||||
// service is main-actor state — the hook's requirements are all @MainActor anyway.
|
// service is main-actor state — the hook's requirements are all @MainActor anyway.
|
||||||
@MainActor
|
@MainActor
|
||||||
final class HealthChatExtendedProfileSectionHook: ExtendedProfileSectionHookProtocol {
|
final class HealthChatExtendedProfileSectionHook: ExtendedProfileSectionHookProtocol {
|
||||||
private var extendedProfileService: ExtendedProfileServiceProtocol?
|
private let extendedProfileService: ExtendedProfileServiceProtocol
|
||||||
|
|
||||||
// Constructed during hook registration, before the main actor is reachable.
|
init(extendedProfileService: ExtendedProfileServiceProtocol) {
|
||||||
nonisolated init() { }
|
self.extendedProfileService = extendedProfileService
|
||||||
|
|
||||||
func configure(with userSession: UserSessionProtocol?) async {
|
|
||||||
extendedProfileService = userSession.map { ExtendedProfileService(clientProxy: $0.clientProxy) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func extendedProfileSection(userID: String) -> AnyView? {
|
func extendedProfileSection(userID: String) -> AnyView? {
|
||||||
guard let extendedProfileService else {
|
AnyView(HealthChatExtendedProfileSection(userID: userID, service: extendedProfileService))
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return AnyView(HealthChatExtendedProfileSection(userID: userID, service: extendedProfileService))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -14,28 +14,19 @@ import SwiftUI
|
|||||||
final class HealthChatSettingsScreenHook: SettingsScreenHookProtocol {
|
final class HealthChatSettingsScreenHook: SettingsScreenHookProtocol {
|
||||||
// Owned here rather than by the section: the settings screen asks for the section on
|
// Owned here rather than by the section: the settings screen asks for the section on
|
||||||
// every body pass. Nothing is fetched until the screen appears and asks for a refresh.
|
// every body pass. Nothing is fetched until the screen appears and asks for a refresh.
|
||||||
private var statusLoader: HealthChatAccountStatusLoader?
|
private let statusLoader: HealthChatAccountStatusLoader
|
||||||
|
|
||||||
// Constructed during hook registration, before the main actor is reachable.
|
init(statusLoader: HealthChatAccountStatusLoader) {
|
||||||
nonisolated init() { }
|
self.statusLoader = statusLoader
|
||||||
|
|
||||||
func configure(with userSession: UserSessionProtocol?) async {
|
|
||||||
statusLoader = userSession.map {
|
|
||||||
let apiService = HealthChatAPIService(tokenProvider: HealthChatOpenIDTokenProvider(clientProxy: $0.clientProxy))
|
|
||||||
return HealthChatAccountStatusLoader(apiService: apiService)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func accountStatusSection(completeProfileAction: @escaping () -> Void) -> AnyView? {
|
func accountStatusSection(completeProfileAction: @escaping () -> Void) -> AnyView? {
|
||||||
guard let statusLoader else {
|
AnyView(HealthChatAccountStatusSection(loader: statusLoader,
|
||||||
return nil
|
completeProfileAction: completeProfileAction))
|
||||||
}
|
|
||||||
return AnyView(HealthChatAccountStatusSection(loader: statusLoader,
|
|
||||||
completeProfileAction: completeProfileAction))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshAccountStatus() {
|
func refreshAccountStatus() {
|
||||||
statusLoader?.load()
|
statusLoader.load()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2026 Novaloop AG
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
// Please see LICENSE files in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
#if IS_MAIN_APP
|
||||||
|
/// Builds the HealthChat session-scoped hooks when a user session is created.
|
||||||
|
nonisolated struct HealthChatUserSessionHook: UserSessionHookProtocol {
|
||||||
|
func makeUserHooks(for clientProxy: ClientProxyProtocol) -> UserHooks {
|
||||||
|
let apiService = HealthChatAPIService(tokenProvider: HealthChatOpenIDTokenProvider(clientProxy: clientProxy))
|
||||||
|
return UserHooks(extendedProfileSectionHook: HealthChatExtendedProfileSectionHook(extendedProfileService: ExtendedProfileService(clientProxy: clientProxy)),
|
||||||
|
settingsScreenHook: HealthChatSettingsScreenHook(statusLoader: HealthChatAccountStatusLoader(apiService: apiService)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(_ userSettings: UserSettings, forHomeserver homeserver: String) -> UserSettings {
|
||||||
|
userSettings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -8,16 +8,12 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
nonisolated protocol ExtendedProfileSectionHookProtocol: Sendable {
|
nonisolated protocol ExtendedProfileSectionHookProtocol: Sendable {
|
||||||
@MainActor func configure(with userSession: UserSessionProtocol?) async
|
|
||||||
|
|
||||||
/// An extra Form section rendered below the header on the user profile
|
/// An extra Form section rendered below the header on the user profile
|
||||||
/// and room member details screens.
|
/// and room member details screens.
|
||||||
@MainActor func extendedProfileSection(userID: String) -> AnyView?
|
@MainActor func extendedProfileSection(userID: String) -> AnyView?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DefaultExtendedProfileSectionHook: ExtendedProfileSectionHookProtocol {
|
struct DefaultExtendedProfileSectionHook: ExtendedProfileSectionHookProtocol {
|
||||||
func configure(with userSession: UserSessionProtocol?) async { }
|
|
||||||
|
|
||||||
func extendedProfileSection(userID: String) -> AnyView? {
|
func extendedProfileSection(userID: String) -> AnyView? {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
nonisolated protocol SettingsScreenHookProtocol: Sendable {
|
nonisolated protocol SettingsScreenHookProtocol: Sendable {
|
||||||
@MainActor func configure(with userSession: UserSessionProtocol?) async
|
|
||||||
|
|
||||||
/// An extra Form section rendered between the user and the account sections,
|
/// An extra Form section rendered between the user and the account sections,
|
||||||
/// advertising whatever is holding the account back.
|
/// advertising whatever is holding the account back.
|
||||||
/// - Parameter completeProfileAction: Opens the own profile editor.
|
/// - Parameter completeProfileAction: Opens the own profile editor.
|
||||||
@@ -20,8 +18,6 @@ nonisolated protocol SettingsScreenHookProtocol: Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct DefaultSettingsScreenHook: SettingsScreenHookProtocol {
|
struct DefaultSettingsScreenHook: SettingsScreenHookProtocol {
|
||||||
func configure(with userSession: UserSessionProtocol?) async { }
|
|
||||||
|
|
||||||
func accountStatusSection(completeProfileAction: @escaping () -> Void) -> AnyView? {
|
func accountStatusSection(completeProfileAction: @escaping () -> Void) -> AnyView? {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ nonisolated struct UserHooks: Sendable {
|
|||||||
let recoveryKeyScreenHook: RecoveryKeyScreenHookProtocol
|
let recoveryKeyScreenHook: RecoveryKeyScreenHookProtocol
|
||||||
let createRoomScreenHook: CreateRoomScreenHookProtocol
|
let createRoomScreenHook: CreateRoomScreenHookProtocol
|
||||||
let securityAndPrivacyScreenHook: SecurityAndPrivacyScreenHookProtocol
|
let securityAndPrivacyScreenHook: SecurityAndPrivacyScreenHookProtocol
|
||||||
|
// HealthChat: session-scoped hooks of the fork. Kept here, next to upstream's, so screens reach them the same way.
|
||||||
|
let extendedProfileSectionHook: ExtendedProfileSectionHookProtocol
|
||||||
|
let settingsScreenHook: SettingsScreenHookProtocol
|
||||||
|
|
||||||
init(roomScreenHook: RoomScreenHookProtocol = DefaultRoomScreenHook(),
|
init(roomScreenHook: RoomScreenHookProtocol = DefaultRoomScreenHook(),
|
||||||
roomDetailsScreenHook: RoomDetailsScreenHookProtocol = DefaultRoomDetailsScreenHook(),
|
roomDetailsScreenHook: RoomDetailsScreenHookProtocol = DefaultRoomDetailsScreenHook(),
|
||||||
@@ -22,7 +25,9 @@ nonisolated struct UserHooks: Sendable {
|
|||||||
userProfileScreenHook: UserProfileScreenHookProtocol = DefaultUserProfileScreenHook(),
|
userProfileScreenHook: UserProfileScreenHookProtocol = DefaultUserProfileScreenHook(),
|
||||||
recoveryKeyScreenHook: RecoveryKeyScreenHookProtocol = DefaultRecoveryKeyScreenHook(),
|
recoveryKeyScreenHook: RecoveryKeyScreenHookProtocol = DefaultRecoveryKeyScreenHook(),
|
||||||
createRoomScreenHook: CreateRoomScreenHookProtocol = DefaultCreateRoomScreenHook(),
|
createRoomScreenHook: CreateRoomScreenHookProtocol = DefaultCreateRoomScreenHook(),
|
||||||
securityAndPrivacyScreenHook: SecurityAndPrivacyScreenHookProtocol = DefaultSecurityAndPrivacyScreenHook()) {
|
securityAndPrivacyScreenHook: SecurityAndPrivacyScreenHookProtocol = DefaultSecurityAndPrivacyScreenHook(),
|
||||||
|
extendedProfileSectionHook: ExtendedProfileSectionHookProtocol = DefaultExtendedProfileSectionHook(),
|
||||||
|
settingsScreenHook: SettingsScreenHookProtocol = DefaultSettingsScreenHook()) {
|
||||||
self.roomScreenHook = roomScreenHook
|
self.roomScreenHook = roomScreenHook
|
||||||
self.roomDetailsScreenHook = roomDetailsScreenHook
|
self.roomDetailsScreenHook = roomDetailsScreenHook
|
||||||
self.roomMemberDetailsScreenHook = roomMemberDetailsScreenHook
|
self.roomMemberDetailsScreenHook = roomMemberDetailsScreenHook
|
||||||
@@ -30,5 +35,7 @@ nonisolated struct UserHooks: Sendable {
|
|||||||
self.recoveryKeyScreenHook = recoveryKeyScreenHook
|
self.recoveryKeyScreenHook = recoveryKeyScreenHook
|
||||||
self.createRoomScreenHook = createRoomScreenHook
|
self.createRoomScreenHook = createRoomScreenHook
|
||||||
self.securityAndPrivacyScreenHook = securityAndPrivacyScreenHook
|
self.securityAndPrivacyScreenHook = securityAndPrivacyScreenHook
|
||||||
|
self.extendedProfileSectionHook = extendedProfileSectionHook
|
||||||
|
self.settingsScreenHook = settingsScreenHook
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ final class HealthChatMembersFlowCoordinator {
|
|||||||
let parameters = UserProfileScreenCoordinatorParameters(userID: userID,
|
let parameters = UserProfileScreenCoordinatorParameters(userID: userID,
|
||||||
isPresentedModally: false,
|
isPresentedModally: false,
|
||||||
userSession: flowParameters.userSession,
|
userSession: flowParameters.userSession,
|
||||||
appHooks: flowParameters.appHooks,
|
|
||||||
analytics: flowParameters.analytics,
|
analytics: flowParameters.analytics,
|
||||||
userIndicatorController: flowParameters.userIndicatorController)
|
userIndicatorController: flowParameters.userIndicatorController)
|
||||||
let coordinator = UserProfileScreenCoordinator(parameters: parameters)
|
let coordinator = UserProfileScreenCoordinator(parameters: parameters)
|
||||||
@@ -148,7 +147,7 @@ final class HealthChatMembersFlowCoordinator {
|
|||||||
private func presentProfileEditor() {
|
private func presentProfileEditor() {
|
||||||
let parameters = UserDetailsEditScreenCoordinatorParameters(orientationManager: flowParameters.windowManager,
|
let parameters = UserDetailsEditScreenCoordinatorParameters(orientationManager: flowParameters.windowManager,
|
||||||
userSession: flowParameters.userSession,
|
userSession: flowParameters.userSession,
|
||||||
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: flowParameters.appSettings),
|
mediaUploadingPreprocessor: MediaUploadingPreprocessor(userSettings: flowParameters.userSession.userSettings),
|
||||||
navigationStackCoordinator: navigationStackCoordinator,
|
navigationStackCoordinator: navigationStackCoordinator,
|
||||||
userIndicatorController: flowParameters.userIndicatorController)
|
userIndicatorController: flowParameters.userIndicatorController)
|
||||||
let coordinator = flowParameters.appHooks.userDetailsEditScreenHook.makeCoordinator(parameters: parameters)
|
let coordinator = flowParameters.appHooks.userDetailsEditScreenHook.makeCoordinator(parameters: parameters)
|
||||||
|
|||||||
@@ -237,16 +237,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func setupObservers() {
|
private func setupObservers() {
|
||||||
extraTabActions?
|
setupExtraTabObserver()
|
||||||
.sink { [weak self] action in
|
|
||||||
switch action {
|
|
||||||
case .presentRoom(let roomID):
|
|
||||||
self?.handleAppRoute(.room(roomID: roomID, via: []), animated: true)
|
|
||||||
case .presentCall(let roomID, let isVoiceCall):
|
|
||||||
self?.handleAppRoute(.call(roomID: roomID, isVoiceCall: isVoiceCall), animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
|
|
||||||
chatsTabFlowCoordinator.actionsPublisher
|
chatsTabFlowCoordinator.actionsPublisher
|
||||||
.sink { [weak self] action in
|
.sink { [weak self] action in
|
||||||
@@ -358,6 +349,20 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// HealthChat: routes the Members tab's actions through the normal app-route handling.
|
||||||
|
private func setupExtraTabObserver() {
|
||||||
|
extraTabActions?
|
||||||
|
.sink { [weak self] action in
|
||||||
|
switch action {
|
||||||
|
case .presentRoom(let roomID):
|
||||||
|
self?.handleAppRoute(.room(roomID: roomID, via: []), animated: true)
|
||||||
|
case .presentCall(let roomID, let isVoiceCall):
|
||||||
|
self?.handleAppRoute(.call(roomID: roomID, isVoiceCall: isVoiceCall), animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Onboarding
|
// MARK: - Onboarding
|
||||||
|
|
||||||
private func attemptStartingOnboarding() {
|
private func attemptStartingOnboarding() {
|
||||||
|
|||||||
+1
-1
@@ -214,7 +214,7 @@ struct HealthChatProfileEditScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
|
|
||||||
return HealthChatProfileEditScreenViewModel(userSession: UserSessionMock(.init(clientProxy: ClientProxyMock(.init()))),
|
return HealthChatProfileEditScreenViewModel(userSession: UserSessionMock(.init(clientProxy: ClientProxyMock(.init()))),
|
||||||
apiService: apiService,
|
apiService: apiService,
|
||||||
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: .volatile()),
|
mediaUploadingPreprocessor: MediaUploadingPreprocessor(userSettings: .mock()),
|
||||||
userIndicatorController: UserIndicatorControllerMock())
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ enum RoomMemberDetailsScreenViewModelAction {
|
|||||||
|
|
||||||
struct RoomMemberDetailsScreenViewState: BindableState {
|
struct RoomMemberDetailsScreenViewState: BindableState {
|
||||||
let userID: String
|
let userID: String
|
||||||
let appHooks: AppHooks
|
let extendedProfileSectionHook: ExtendedProfileSectionHookProtocol
|
||||||
var memberDetails: RoomMemberDetails?
|
var memberDetails: RoomMemberDetails?
|
||||||
var verificationState: UserIdentityVerificationState?
|
var verificationState: UserIdentityVerificationState?
|
||||||
var isOwnMemberDetails = false
|
var isOwnMemberDetails = false
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
|
|||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
self.analytics = analytics
|
self.analytics = analytics
|
||||||
|
|
||||||
let initialViewState = RoomMemberDetailsScreenViewState(userID: userID, appHooks: appHooks, bindings: .init())
|
let initialViewState = RoomMemberDetailsScreenViewState(userID: userID, extendedProfileSectionHook: userSession.userHooks.extendedProfileSectionHook, bindings: .init())
|
||||||
|
|
||||||
super.init(initialViewState: userSession.userHooks.roomMemberDetailsScreenHook.update(initialViewState),
|
super.init(initialViewState: userSession.userHooks.roomMemberDetailsScreenHook.update(initialViewState),
|
||||||
mediaProvider: userSession.mediaProvider)
|
mediaProvider: userSession.mediaProvider)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ struct RoomMemberDetailsScreen: View {
|
|||||||
Form {
|
Form {
|
||||||
headerSection
|
headerSection
|
||||||
|
|
||||||
context.viewState.appHooks.extendedProfileSectionHook.extendedProfileSection(userID: context.viewState.userID)
|
context.viewState.extendedProfileSectionHook.extendedProfileSection(userID: context.viewState.userID)
|
||||||
|
|
||||||
if context.viewState.showVerifyIdentitySection {
|
if context.viewState.showVerifyIdentitySection {
|
||||||
verificationSection
|
verificationSection
|
||||||
|
|||||||
@@ -10,22 +10,30 @@ import SwiftUI
|
|||||||
|
|
||||||
enum SettingsScreenViewModelAction {
|
enum SettingsScreenViewModelAction {
|
||||||
case close
|
case close
|
||||||
|
|
||||||
case userDetails
|
case userDetails
|
||||||
case userStatusEmojiPicker(EmojiPickerScreenContinuation)
|
case userStatusEmojiPicker(EmojiPickerScreenContinuation)
|
||||||
case linkNewDevice
|
|
||||||
|
case addAccount
|
||||||
|
|
||||||
case manageAccount(url: URL)
|
case manageAccount(url: URL)
|
||||||
case analytics
|
case linkNewDevice
|
||||||
case appLock
|
|
||||||
case reportBug
|
|
||||||
case about
|
|
||||||
case blockedUsers
|
|
||||||
case secureBackup
|
|
||||||
case notifications
|
case notifications
|
||||||
case advancedSettings
|
case secureBackup
|
||||||
case labs
|
case moderationAndSafety
|
||||||
case developerOptions
|
case reportBug
|
||||||
|
|
||||||
case logout
|
case logout
|
||||||
case deactivateAccount
|
case deactivateAccount
|
||||||
|
|
||||||
|
case mediaUploadQuality
|
||||||
|
case appLock
|
||||||
|
case locationSharing
|
||||||
|
case analytics
|
||||||
|
case labs
|
||||||
|
case about
|
||||||
|
|
||||||
|
case developerOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SettingsScreenSecuritySectionMode {
|
enum SettingsScreenSecuritySectionMode {
|
||||||
@@ -38,6 +46,7 @@ struct SettingsScreenViewState: BindableState {
|
|||||||
var userProfile: UserProfile
|
var userProfile: UserProfile
|
||||||
var showUserStatusInput = false
|
var showUserStatusInput = false
|
||||||
var showLinkNewDeviceButton: Bool
|
var showLinkNewDeviceButton: Bool
|
||||||
|
var showAddAccountButton: Bool
|
||||||
var accountProfileURL: URL?
|
var accountProfileURL: URL?
|
||||||
var showAccountDeactivation: Bool
|
var showAccountDeactivation: Bool
|
||||||
var showDeveloperOptions: Bool
|
var showDeveloperOptions: Bool
|
||||||
@@ -45,16 +54,14 @@ struct SettingsScreenViewState: BindableState {
|
|||||||
var securitySectionMode = SettingsScreenSecuritySectionMode.none
|
var securitySectionMode = SettingsScreenSecuritySectionMode.none
|
||||||
var showSecuritySectionBadge = false
|
var showSecuritySectionBadge = false
|
||||||
|
|
||||||
var showBlockedUsers = false
|
|
||||||
let showAnalyticsSettings: Bool
|
let showAnalyticsSettings: Bool
|
||||||
|
|
||||||
let isBugReportServiceEnabled: Bool
|
let isBugReportServiceEnabled: Bool
|
||||||
|
|
||||||
let navigationBarVisibility: Visibility
|
let navigationBarVisibility: Visibility
|
||||||
|
let settingsScreenHook: SettingsScreenHookProtocol
|
||||||
|
|
||||||
let appHooks: AppHooks
|
var bindings: SettingsScreenViewStateBindings
|
||||||
|
|
||||||
var bindings = SettingsScreenViewStateBindings()
|
|
||||||
|
|
||||||
var userStatusRowMode: SettingsScreenUserStatusRow.Mode {
|
var userStatusRowMode: SettingsScreenUserStatusRow.Mode {
|
||||||
if bindings.isShowingCustomStatusField {
|
if bindings.isShowingCustomStatusField {
|
||||||
@@ -68,6 +75,8 @@ struct SettingsScreenViewState: BindableState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct SettingsScreenViewStateBindings {
|
struct SettingsScreenViewStateBindings {
|
||||||
|
private let userSettings: UserSettings
|
||||||
|
|
||||||
var isPresentingStatusPicker = false
|
var isPresentingStatusPicker = false
|
||||||
var customStatusEmoji: Character = "😄"
|
var customStatusEmoji: Character = "😄"
|
||||||
var isShowingCustomStatusField = false {
|
var isShowingCustomStatusField = false {
|
||||||
@@ -79,28 +88,45 @@ struct SettingsScreenViewStateBindings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var isPresentingAccountDeactivationConfirmation = false
|
var isPresentingAccountDeactivationConfirmation = false
|
||||||
|
|
||||||
|
var appAppearance: AppAppearance {
|
||||||
|
get { userSettings.app.appAppearance }
|
||||||
|
set { userSettings.app.appAppearance = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
init(userSettings: UserSettings) {
|
||||||
|
self.userSettings = userSettings
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SettingsScreenViewAction {
|
enum SettingsScreenViewAction {
|
||||||
case close
|
case close
|
||||||
|
|
||||||
case userDetails
|
case userDetails
|
||||||
case userStatus(UserStatusAction)
|
case userStatus(UserStatusAction)
|
||||||
case analytics
|
|
||||||
case appLock
|
case addAccount
|
||||||
case reportBug
|
|
||||||
case about
|
|
||||||
case blockedUsers
|
|
||||||
case secureBackup
|
|
||||||
case linkNewDevice
|
|
||||||
case manageAccount(url: URL)
|
case manageAccount(url: URL)
|
||||||
|
case linkNewDevice
|
||||||
case notifications
|
case notifications
|
||||||
case enableDeveloperOptions
|
case secureBackup
|
||||||
case developerOptions
|
case moderationAndSafety
|
||||||
case advancedSettings
|
case reportBug
|
||||||
case labs
|
|
||||||
case logout
|
case logout
|
||||||
case deactivateAccount
|
case deactivateAccount
|
||||||
|
|
||||||
|
case mediaUploadQuality
|
||||||
|
case appLock
|
||||||
|
case locationSharing
|
||||||
|
case analytics
|
||||||
|
case labs
|
||||||
|
case about
|
||||||
|
|
||||||
|
case enableDeveloperOptions
|
||||||
|
case developerOptions
|
||||||
|
|
||||||
enum UserStatusAction {
|
enum UserStatusAction {
|
||||||
/// Show status picker sheet to select a preset status.
|
/// Show status picker sheet to select a preset status.
|
||||||
case pickStatus
|
case pickStatus
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import SwiftUI
|
|||||||
typealias SettingsScreenViewModelType = StateStoreViewModelV2<SettingsScreenViewState, SettingsScreenViewAction>
|
typealias SettingsScreenViewModelType = StateStoreViewModelV2<SettingsScreenViewState, SettingsScreenViewAction>
|
||||||
|
|
||||||
class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewModelProtocol {
|
class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewModelProtocol {
|
||||||
private let appSettings: AppSettings
|
private let userSettings: UserSettings
|
||||||
private let clientProxy: ClientProxyProtocol
|
private let clientProxy: ClientProxyProtocol
|
||||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
|
|
||||||
@@ -22,30 +22,36 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
|
|||||||
actionsSubject.eraseToAnyPublisher()
|
actionsSubject.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(userSession: UserSessionProtocol, appSettings: AppSettings, appHooks: AppHooks, isBugReportServiceEnabled: Bool, isInSecondaryWindow: Bool, userIndicatorController: UserIndicatorControllerProtocol) {
|
init(userSession: UserSessionProtocol, isBugReportServiceEnabled: Bool, isInSecondaryWindow: Bool, userIndicatorController: UserIndicatorControllerProtocol) {
|
||||||
self.appSettings = appSettings
|
userSettings = userSession.userSettings
|
||||||
clientProxy = userSession.clientProxy
|
clientProxy = userSession.clientProxy
|
||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
|
|
||||||
super.init(initialViewState: .init(deviceID: userSession.clientProxy.deviceID,
|
super.init(initialViewState: .init(deviceID: userSession.clientProxy.deviceID,
|
||||||
userProfile: userSession.clientProxy.userProfilePublisher.value,
|
userProfile: userSession.clientProxy.userProfilePublisher.value,
|
||||||
showLinkNewDeviceButton: appSettings.linkNewDeviceEnabled,
|
showLinkNewDeviceButton: userSettings.app.linkNewDeviceEnabled,
|
||||||
|
showAddAccountButton: userSettings.app.multiAccountEnabled,
|
||||||
showAccountDeactivation: userSession.clientProxy.canDeactivateAccount,
|
showAccountDeactivation: userSession.clientProxy.canDeactivateAccount,
|
||||||
showDeveloperOptions: appSettings.developerOptionsEnabled,
|
showDeveloperOptions: userSettings.app.developerOptionsEnabled,
|
||||||
showAnalyticsSettings: appSettings.canPromptForAnalytics,
|
showAnalyticsSettings: userSettings.app.canPromptForAnalytics,
|
||||||
isBugReportServiceEnabled: isBugReportServiceEnabled,
|
isBugReportServiceEnabled: isBugReportServiceEnabled,
|
||||||
navigationBarVisibility: isInSecondaryWindow ? .hidden : .automatic,
|
navigationBarVisibility: isInSecondaryWindow ? .hidden : .automatic,
|
||||||
appHooks: appHooks),
|
settingsScreenHook: userSession.userHooks.settingsScreenHook,
|
||||||
|
bindings: .init(userSettings: userSettings)),
|
||||||
mediaProvider: userSession.mediaProvider)
|
mediaProvider: userSession.mediaProvider)
|
||||||
|
|
||||||
appSettings.developerOptionsEnabledPublisher
|
userSettings.app.developerOptionsEnabledPublisher
|
||||||
.weakAssign(to: \.state.showDeveloperOptions, on: self)
|
.weakAssign(to: \.state.showDeveloperOptions, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
appSettings.linkNewDeviceEnabledPublisher
|
userSettings.app.linkNewDeviceEnabledPublisher
|
||||||
.weakAssign(to: \.state.showLinkNewDeviceButton, on: self)
|
.weakAssign(to: \.state.showLinkNewDeviceButton, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
userSettings.app.multiAccountEnabledPublisher
|
||||||
|
.weakAssign(to: \.state.showAddAccountButton, on: self)
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
userSession.clientProxy.userProfilePublisher
|
userSession.clientProxy.userProfilePublisher
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.weakAssign(to: \.state.userProfile, on: self)
|
.weakAssign(to: \.state.userProfile, on: self)
|
||||||
@@ -73,18 +79,6 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
userSession.clientProxy.ignoredUsersPublisher
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.map {
|
|
||||||
guard let blockedUsers = $0 else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return !blockedUsers.isEmpty
|
|
||||||
}
|
|
||||||
.weakAssign(to: \.state.showBlockedUsers, on: self)
|
|
||||||
.store(in: &cancellables)
|
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
if case .success(true) = await userSession.clientProxy.isUserStatusSupported() {
|
if case .success(true) = await userSession.clientProxy.isUserStatusSupported() {
|
||||||
state.showUserStatusInput = true
|
state.showUserStatusInput = true
|
||||||
@@ -98,6 +92,7 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
|
|||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .close:
|
case .close:
|
||||||
actionsSubject.send(.close)
|
actionsSubject.send(.close)
|
||||||
|
|
||||||
case .userDetails:
|
case .userDetails:
|
||||||
actionsSubject.send(.userDetails)
|
actionsSubject.send(.userDetails)
|
||||||
case .userStatus(.pickStatus):
|
case .userStatus(.pickStatus):
|
||||||
@@ -114,36 +109,45 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
|
|||||||
case .userStatus(.cancel):
|
case .userStatus(.cancel):
|
||||||
state.bindings.isPresentingStatusPicker = false
|
state.bindings.isPresentingStatusPicker = false
|
||||||
state.bindings.isShowingCustomStatusField = false
|
state.bindings.isShowingCustomStatusField = false
|
||||||
case .linkNewDevice:
|
|
||||||
actionsSubject.send(.linkNewDevice)
|
case .addAccount:
|
||||||
|
actionsSubject.send(.addAccount)
|
||||||
|
|
||||||
case let .manageAccount(url):
|
case let .manageAccount(url):
|
||||||
actionsSubject.send(.manageAccount(url: url))
|
actionsSubject.send(.manageAccount(url: url))
|
||||||
case .analytics:
|
case .linkNewDevice:
|
||||||
actionsSubject.send(.analytics)
|
actionsSubject.send(.linkNewDevice)
|
||||||
case .appLock:
|
|
||||||
actionsSubject.send(.appLock)
|
|
||||||
case .reportBug:
|
|
||||||
actionsSubject.send(.reportBug)
|
|
||||||
case .about:
|
|
||||||
actionsSubject.send(.about)
|
|
||||||
case .blockedUsers:
|
|
||||||
actionsSubject.send(.blockedUsers)
|
|
||||||
case .logout:
|
|
||||||
actionsSubject.send(.logout)
|
|
||||||
case .secureBackup:
|
|
||||||
actionsSubject.send(.secureBackup)
|
|
||||||
case .notifications:
|
case .notifications:
|
||||||
actionsSubject.send(.notifications)
|
actionsSubject.send(.notifications)
|
||||||
case .advancedSettings:
|
case .secureBackup:
|
||||||
actionsSubject.send(.advancedSettings)
|
actionsSubject.send(.secureBackup)
|
||||||
case .labs:
|
case .moderationAndSafety:
|
||||||
actionsSubject.send(.labs)
|
actionsSubject.send(.moderationAndSafety)
|
||||||
case .enableDeveloperOptions:
|
case .reportBug:
|
||||||
appSettings.developerOptionsEnabled.toggle()
|
actionsSubject.send(.reportBug)
|
||||||
case .developerOptions:
|
|
||||||
actionsSubject.send(.developerOptions)
|
case .logout:
|
||||||
|
actionsSubject.send(.logout)
|
||||||
case .deactivateAccount:
|
case .deactivateAccount:
|
||||||
actionsSubject.send(.deactivateAccount)
|
actionsSubject.send(.deactivateAccount)
|
||||||
|
|
||||||
|
case .mediaUploadQuality:
|
||||||
|
actionsSubject.send(.mediaUploadQuality)
|
||||||
|
case .appLock:
|
||||||
|
actionsSubject.send(.appLock)
|
||||||
|
case .locationSharing:
|
||||||
|
actionsSubject.send(.locationSharing)
|
||||||
|
case .analytics:
|
||||||
|
actionsSubject.send(.analytics)
|
||||||
|
case .labs:
|
||||||
|
actionsSubject.send(.labs)
|
||||||
|
case .about:
|
||||||
|
actionsSubject.send(.about)
|
||||||
|
|
||||||
|
case .enableDeveloperOptions:
|
||||||
|
userSettings.app.developerOptionsEnabled.toggle()
|
||||||
|
case .developerOptions:
|
||||||
|
actionsSubject.send(.developerOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,44 +13,30 @@ import SwiftUI
|
|||||||
struct SettingsScreen: View {
|
struct SettingsScreen: View {
|
||||||
@Bindable var context: SettingsScreenViewModel.Context
|
@Bindable var context: SettingsScreenViewModel.Context
|
||||||
|
|
||||||
private var shouldHideManageAccountSection: Bool {
|
|
||||||
context.viewState.accountProfileURL == nil &&
|
|
||||||
!context.viewState.showBlockedUsers &&
|
|
||||||
!context.viewState.showLinkNewDeviceButton
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
userSection
|
userSection
|
||||||
|
|
||||||
if context.viewState.showUserStatusInput {
|
context.viewState.settingsScreenHook.accountStatusSection {
|
||||||
userStatusSection
|
|
||||||
}
|
|
||||||
|
|
||||||
context.viewState.appHooks.settingsScreenHook.accountStatusSection {
|
|
||||||
context.send(viewAction: .userDetails)
|
context.send(viewAction: .userDetails)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !shouldHideManageAccountSection {
|
if context.viewState.showAddAccountButton {
|
||||||
manageAccountSection
|
addAccountSection
|
||||||
}
|
}
|
||||||
|
|
||||||
manageMyAppSection
|
accountSettingsSection
|
||||||
|
|
||||||
generalSection
|
|
||||||
|
|
||||||
signOutSection
|
signOutSection
|
||||||
|
|
||||||
if context.viewState.showDeveloperOptions {
|
appSettingsSection
|
||||||
developerOptionsSection
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.compoundList()
|
.compoundList()
|
||||||
.navigationTitle(L10n.commonSettings)
|
.navigationTitle(L10n.commonSettings)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbarVisibility(context.viewState.navigationBarVisibility, for: .navigationBar)
|
.toolbarVisibility(context.viewState.navigationBarVisibility, for: .navigationBar)
|
||||||
.toolbar { toolbar }
|
.toolbar { toolbar }
|
||||||
.onAppear { context.viewState.appHooks.settingsScreenHook.refreshAccountStatus() }
|
.onAppear { context.viewState.settingsScreenHook.refreshAccountStatus() }
|
||||||
.sheet(isPresented: $context.isPresentingStatusPicker) {
|
.sheet(isPresented: $context.isPresentingStatusPicker) {
|
||||||
SettingsScreenUserStatusPickerView { action in
|
SettingsScreenUserStatusPickerView { action in
|
||||||
context.send(viewAction: .userStatus(action))
|
context.send(viewAction: .userStatus(action))
|
||||||
@@ -75,15 +61,9 @@ struct SettingsScreen: View {
|
|||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
HStack(spacing: 6) {
|
Text(context.viewState.userProfile.displayName ?? "")
|
||||||
Text(context.viewState.userProfile.displayName ?? "")
|
.font(.compound.headingMD)
|
||||||
|
.foregroundColor(.compound.textPrimary)
|
||||||
if let statusEmoji = context.viewState.userProfile.status.displayed?.emoji {
|
|
||||||
Text(String(statusEmoji))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.font(.compound.headingMD)
|
|
||||||
.foregroundColor(.compound.textPrimary)
|
|
||||||
|
|
||||||
Text(context.viewState.userProfile.id)
|
Text(context.viewState.userProfile.id)
|
||||||
.font(.compound.bodySM)
|
.font(.compound.bodySM)
|
||||||
@@ -98,47 +78,27 @@ struct SettingsScreen: View {
|
|||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
if context.viewState.showUserStatusInput {
|
||||||
|
SettingsScreenUserStatusRow(mode: context.viewState.userStatusRowMode) { action in
|
||||||
private var userStatusSection: some View {
|
context.send(viewAction: .userStatus(action))
|
||||||
Section {
|
}
|
||||||
SettingsScreenUserStatusRow(mode: context.viewState.userStatusRowMode) { action in
|
|
||||||
context.send(viewAction: .userStatus(action))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var manageMyAppSection: some View {
|
private var addAccountSection: some View {
|
||||||
Section {
|
Section {
|
||||||
ListRow(label: .default(title: L10n.screenNotificationSettingsTitle,
|
ListRow(label: .default(title: L10n.screenSettingsAddAccount,
|
||||||
icon: \.notifications),
|
icon: \.plus),
|
||||||
kind: .navigationLink {
|
kind: .navigationLink {
|
||||||
context.send(viewAction: .notifications)
|
context.send(viewAction: .addAccount)
|
||||||
})
|
})
|
||||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.notifications)
|
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.addAccount)
|
||||||
|
|
||||||
ListRow(label: .default(title: L10n.commonScreenLock,
|
|
||||||
icon: \.lock),
|
|
||||||
kind: .navigationLink {
|
|
||||||
context.send(viewAction: .appLock)
|
|
||||||
})
|
|
||||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.screenLock)
|
|
||||||
|
|
||||||
switch context.viewState.securitySectionMode {
|
|
||||||
case .secureBackup:
|
|
||||||
ListRow(label: .default(title: L10n.commonEncryption,
|
|
||||||
icon: \.key),
|
|
||||||
details: context.viewState.showSecuritySectionBadge ? .icon(securitySectionBadge) : nil,
|
|
||||||
kind: .navigationLink { context.send(viewAction: .secureBackup) })
|
|
||||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.secureBackup)
|
|
||||||
default:
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var manageAccountSection: some View {
|
private var accountSettingsSection: some View {
|
||||||
Section {
|
Section {
|
||||||
if let url = context.viewState.accountProfileURL {
|
if let url = context.viewState.accountProfileURL {
|
||||||
ListRow(label: .default(title: L10n.actionManageAccountAndDevices,
|
ListRow(label: .default(title: L10n.actionManageAccountAndDevices,
|
||||||
@@ -157,39 +117,30 @@ struct SettingsScreen: View {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if context.viewState.showBlockedUsers {
|
ListRow(label: .default(title: L10n.screenNotificationSettingsTitle,
|
||||||
ListRow(label: .default(title: L10n.commonBlockedUsers,
|
icon: \.notifications),
|
||||||
icon: \.block),
|
kind: .navigationLink {
|
||||||
kind: .navigationLink {
|
context.send(viewAction: .notifications)
|
||||||
context.send(viewAction: .blockedUsers)
|
})
|
||||||
})
|
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.notifications)
|
||||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.blockedUsers)
|
|
||||||
|
switch context.viewState.securitySectionMode {
|
||||||
|
case .secureBackup:
|
||||||
|
ListRow(label: .default(title: L10n.commonEncryption,
|
||||||
|
icon: \.key),
|
||||||
|
details: context.viewState.showSecuritySectionBadge ? .icon(securitySectionBadge) : nil,
|
||||||
|
kind: .navigationLink { context.send(viewAction: .secureBackup) })
|
||||||
|
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.secureBackup)
|
||||||
|
default:
|
||||||
|
EmptyView()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var generalSection: some View {
|
|
||||||
Section {
|
|
||||||
ListRow(label: .default(title: L10n.commonAdvancedSettings,
|
|
||||||
icon: \.settings),
|
|
||||||
kind: .navigationLink {
|
|
||||||
context.send(viewAction: .advancedSettings)
|
|
||||||
})
|
|
||||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.advancedSettings)
|
|
||||||
|
|
||||||
ListRow(label: .default(title: L10n.screenAdvancedSettingsLabs,
|
ListRow(label: .default(title: L10n.commonModerationAndSafety,
|
||||||
icon: \.labs),
|
icon: \.admin),
|
||||||
kind: .navigationLink {
|
kind: .navigationLink {
|
||||||
context.send(viewAction: .labs)
|
context.send(viewAction: .moderationAndSafety)
|
||||||
})
|
})
|
||||||
|
|
||||||
ListRow(label: .default(title: L10n.commonAbout,
|
|
||||||
icon: \.info),
|
|
||||||
kind: .navigationLink {
|
|
||||||
context.send(viewAction: .about)
|
|
||||||
})
|
|
||||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.about)
|
|
||||||
|
|
||||||
if context.viewState.isBugReportServiceEnabled {
|
if context.viewState.isBugReportServiceEnabled {
|
||||||
ListRow(label: .default(title: L10n.commonReportAProblem,
|
ListRow(label: .default(title: L10n.commonReportAProblem,
|
||||||
icon: \.chatProblem),
|
icon: \.chatProblem),
|
||||||
@@ -198,15 +149,9 @@ struct SettingsScreen: View {
|
|||||||
})
|
})
|
||||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.reportBug)
|
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.reportBug)
|
||||||
}
|
}
|
||||||
|
} header: {
|
||||||
if context.viewState.showAnalyticsSettings {
|
Text(L10n.commonAccountSettings)
|
||||||
ListRow(label: .default(title: L10n.commonAnalytics,
|
.compoundListSectionHeader()
|
||||||
icon: \.chart),
|
|
||||||
kind: .navigationLink {
|
|
||||||
context.send(viewAction: .analytics)
|
|
||||||
})
|
|
||||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.analytics)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,21 +173,68 @@ struct SettingsScreen: View {
|
|||||||
context.send(viewAction: .deactivateAccount)
|
context.send(viewAction: .deactivateAccount)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} footer: {
|
|
||||||
if !context.viewState.showDeveloperOptions {
|
|
||||||
versionSection
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var developerOptionsSection: some View {
|
private var appSettingsSection: some View {
|
||||||
Section {
|
Section {
|
||||||
ListRow(label: .default(title: L10n.commonDeveloperOptions,
|
ListRow(label: .default(title: L10n.commonAppearance,
|
||||||
icon: \.code),
|
icon: \.darkMode),
|
||||||
|
kind: .picker(selection: $context.appAppearance,
|
||||||
|
items: AppAppearance.allCases.map { (title: $0.name, tag: $0) }))
|
||||||
|
|
||||||
|
ListRow(label: .default(title: L10n.commonMediaUploadQuality,
|
||||||
|
icon: \.image),
|
||||||
kind: .navigationLink {
|
kind: .navigationLink {
|
||||||
context.send(viewAction: .developerOptions)
|
context.send(viewAction: .mediaUploadQuality)
|
||||||
})
|
})
|
||||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.developerOptions)
|
|
||||||
|
ListRow(label: .default(title: L10n.commonScreenLock,
|
||||||
|
icon: \.lock),
|
||||||
|
kind: .navigationLink {
|
||||||
|
context.send(viewAction: .appLock)
|
||||||
|
})
|
||||||
|
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.screenLock)
|
||||||
|
|
||||||
|
ListRow(label: .default(title: L10n.commonLocationSharing,
|
||||||
|
icon: \.locationPin),
|
||||||
|
kind: .navigationLink {
|
||||||
|
context.send(viewAction: .locationSharing)
|
||||||
|
})
|
||||||
|
|
||||||
|
if context.viewState.showAnalyticsSettings {
|
||||||
|
ListRow(label: .default(title: L10n.commonAnalytics,
|
||||||
|
icon: \.chart),
|
||||||
|
kind: .navigationLink {
|
||||||
|
context.send(viewAction: .analytics)
|
||||||
|
})
|
||||||
|
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.analytics)
|
||||||
|
}
|
||||||
|
|
||||||
|
ListRow(label: .default(title: L10n.screenAdvancedSettingsLabs,
|
||||||
|
icon: \.labs),
|
||||||
|
kind: .navigationLink {
|
||||||
|
context.send(viewAction: .labs)
|
||||||
|
})
|
||||||
|
|
||||||
|
ListRow(label: .default(title: L10n.commonAbout,
|
||||||
|
icon: \.info),
|
||||||
|
kind: .navigationLink {
|
||||||
|
context.send(viewAction: .about)
|
||||||
|
})
|
||||||
|
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.about)
|
||||||
|
|
||||||
|
if context.viewState.showDeveloperOptions {
|
||||||
|
ListRow(label: .default(title: L10n.commonDeveloperOptions,
|
||||||
|
icon: \.code),
|
||||||
|
kind: .navigationLink {
|
||||||
|
context.send(viewAction: .developerOptions)
|
||||||
|
})
|
||||||
|
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.developerOptions)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text(L10n.commonAppSettings)
|
||||||
|
.compoundListSectionHeader()
|
||||||
} footer: {
|
} footer: {
|
||||||
versionSection
|
versionSection
|
||||||
}
|
}
|
||||||
@@ -287,6 +279,19 @@ struct SettingsScreen: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension AppAppearance {
|
||||||
|
var name: String {
|
||||||
|
switch self {
|
||||||
|
case .system:
|
||||||
|
L10n.themeSystem
|
||||||
|
case .light:
|
||||||
|
L10n.themeLight
|
||||||
|
case .dark:
|
||||||
|
L10n.themeDark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Previews
|
// MARK: - Previews
|
||||||
|
|
||||||
@available(iOS 26.0, *)
|
@available(iOS 26.0, *)
|
||||||
@@ -299,7 +304,7 @@ struct SettingsScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
SettingsScreen(context: viewModel.context)
|
SettingsScreen(context: viewModel.context)
|
||||||
}
|
}
|
||||||
.snapshotPreferences(expect: viewModel.context.observe(\.viewState.accountProfileURL).map { $0 != nil })
|
.snapshotPreferences(expect: viewModel.context.observe(\.viewState.accountProfileURL).map { $0 != nil })
|
||||||
.frame(height: 1100)
|
.frame(height: 1300)
|
||||||
.previewLayout(.sizeThatFits)
|
.previewLayout(.sizeThatFits)
|
||||||
.previewDisplayName("Default")
|
.previewDisplayName("Default")
|
||||||
|
|
||||||
@@ -307,19 +312,21 @@ struct SettingsScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
SettingsScreen(context: bugReportDisabledViewModel.context)
|
SettingsScreen(context: bugReportDisabledViewModel.context)
|
||||||
}
|
}
|
||||||
.snapshotPreferences(expect: bugReportDisabledViewModel.context.observe(\.viewState.accountProfileURL).map { $0 != nil })
|
.snapshotPreferences(expect: bugReportDisabledViewModel.context.observe(\.viewState.accountProfileURL).map { $0 != nil })
|
||||||
.frame(height: 1050)
|
.frame(height: 1250)
|
||||||
.previewLayout(.sizeThatFits)
|
.previewLayout(.sizeThatFits)
|
||||||
.previewDisplayName("Bug report disabled")
|
.previewDisplayName("Bug report disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func makeViewModel(isBugReportServiceEnabled: Bool = true) -> SettingsScreenViewModel {
|
static func makeViewModel(isBugReportServiceEnabled: Bool = true) -> SettingsScreenViewModel {
|
||||||
|
let userSettings = UserSettings.mock()
|
||||||
|
userSettings.app.multiAccountEnabled = true
|
||||||
|
userSettings.app.linkNewDeviceEnabled = true
|
||||||
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@alice:example.com",
|
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@alice:example.com",
|
||||||
deviceID: "AAAAAAAAAAA",
|
deviceID: "AAAAAAAAAAA",
|
||||||
displayName: "Alice Liddell",
|
displayName: "Alice Liddell",
|
||||||
status: .mockFocussing))))
|
status: .mockFocussing)),
|
||||||
|
userSettings: userSettings))
|
||||||
return SettingsScreenViewModel(userSession: userSession,
|
return SettingsScreenViewModel(userSession: userSession,
|
||||||
appSettings: .volatile(),
|
|
||||||
appHooks: AppHooks(),
|
|
||||||
isBugReportServiceEnabled: isBugReportServiceEnabled,
|
isBugReportServiceEnabled: isBugReportServiceEnabled,
|
||||||
isInSecondaryWindow: false,
|
isInSecondaryWindow: false,
|
||||||
userIndicatorController: UserIndicatorControllerMock())
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ struct UserProfileScreenViewState: BindableState {
|
|||||||
let userID: String
|
let userID: String
|
||||||
let isOwnUser: Bool
|
let isOwnUser: Bool
|
||||||
let isPresentedModally: Bool
|
let isPresentedModally: Bool
|
||||||
let appHooks: AppHooks
|
let extendedProfileSectionHook: ExtendedProfileSectionHookProtocol
|
||||||
|
|
||||||
var userProfile: UserProfile?
|
var userProfile: UserProfile?
|
||||||
var isIdentityKnown = false
|
var isIdentityKnown = false
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
|
|||||||
let initialViewState = UserProfileScreenViewState(userID: userID,
|
let initialViewState = UserProfileScreenViewState(userID: userID,
|
||||||
isOwnUser: userID == userSession.clientProxy.userID,
|
isOwnUser: userID == userSession.clientProxy.userID,
|
||||||
isPresentedModally: isPresentedModally,
|
isPresentedModally: isPresentedModally,
|
||||||
appHooks: appHooks,
|
extendedProfileSectionHook: userSession.userHooks.extendedProfileSectionHook,
|
||||||
bindings: .init())
|
bindings: .init())
|
||||||
|
|
||||||
super.init(initialViewState: userSession.userHooks.userProfileScreenHook.update(initialViewState),
|
super.init(initialViewState: userSession.userHooks.userProfileScreenHook.update(initialViewState),
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ struct UserProfileScreen: View {
|
|||||||
Form {
|
Form {
|
||||||
headerSection
|
headerSection
|
||||||
|
|
||||||
context.viewState.appHooks.extendedProfileSectionHook.extendedProfileSection(userID: context.viewState.userID)
|
context.viewState.extendedProfileSectionHook.extendedProfileSection(userID: context.viewState.userID)
|
||||||
}
|
}
|
||||||
.compoundList()
|
.compoundList()
|
||||||
.navigationTitle(L10n.screenRoomMemberDetailsTitle)
|
.navigationTitle(L10n.screenRoomMemberDetailsTitle)
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ final class HealthChatProfileEditScreenViewModelTests {
|
|||||||
private func makeViewModel() -> HealthChatProfileEditScreenViewModel {
|
private func makeViewModel() -> HealthChatProfileEditScreenViewModel {
|
||||||
HealthChatProfileEditScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
HealthChatProfileEditScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
||||||
apiService: apiService,
|
apiService: apiService,
|
||||||
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: .volatile()),
|
mediaUploadingPreprocessor: MediaUploadingPreprocessor(userSettings: .mock()),
|
||||||
userIndicatorController: UserIndicatorControllerMock())
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user