refactor(healthchat): move the session hooks onto UserHooks

This commit is contained in:
Sedat Qaja
2026-10-19 09:18:07 +02:00
parent 1b34a71857
commit 979d20572d
22 changed files with 725 additions and 409 deletions
+4 -30
View File
@@ -12,12 +12,6 @@ import Synchronization
final nonisolated class AppHooks: AppHooksProtocol {
#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())
var appSettingsHook: AppSettingsHookProtocol
@@ -30,38 +24,15 @@ final nonisolated class AppHooks: AppHooksProtocol {
@AppHook(default: DefaultOAuthPresenterHook())
var oAuthPresenterHook: OAuthPresenterHookProtocol
@AppHook(default: DefaultUserSessionHook())
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
// HealthChat app-level hooks. The session-scoped ones live in `UserHooks`.
@AppHook(default: DefaultHomeTabsHook())
var homeTabsHook: HomeTabsHookProtocol
@AppHook(default: DefaultSettingsScreenHook())
var settingsScreenHook: SettingsScreenHookProtocol
@AppHook(default: DefaultUserDetailsEditScreenHook())
var userDetailsEditScreenHook: UserDetailsEditScreenHookProtocol
@AppHook(default: DefaultDeveloperOptionsScreenHook())
var developerOptionsScreenHook: DeveloperOptionsScreenHookProtocol
@AppHook(default: DefaultRecoveryKeyScreenHook())
var recoveryKeyScreenHook: RecoveryKeyScreenHookProtocol
#endif
@AppHook(default: DefaultTracingHook())
@@ -72,6 +43,9 @@ final nonisolated class AppHooks: AppHooksProtocol {
@AppHook(default: DefaultRemoteSettingsHook())
var remoteSettingsHook: RemoteSettingsHookProtocol
@AppHook(default: DefaultUserSessionHook())
var userSessionHook: UserSessionHookProtocol
}
nonisolated protocol AppHooksProtocol: Sendable {
@@ -16,9 +16,8 @@ extension AppHooks {
#if IS_MAIN_APP
registerAppSettingsHook(HealthChatAppSettingsHook())
registerCompoundHook(HealthChatCompoundHook())
registerExtendedProfileSectionHook(HealthChatExtendedProfileSectionHook())
registerHomeTabsHook(HealthChatHomeTabsHook())
registerSettingsScreenHook(HealthChatSettingsScreenHook())
registerUserSessionHook(HealthChatUserSessionHook())
registerUserDetailsEditScreenHook(HealthChatUserDetailsEditScreenHook())
#endif
}
@@ -12,20 +12,14 @@ import SwiftUI
// service is main-actor state — the hook's requirements are all @MainActor anyway.
@MainActor
final class HealthChatExtendedProfileSectionHook: ExtendedProfileSectionHookProtocol {
private var extendedProfileService: ExtendedProfileServiceProtocol?
private let extendedProfileService: ExtendedProfileServiceProtocol
// Constructed during hook registration, before the main actor is reachable.
nonisolated init() { }
func configure(with userSession: UserSessionProtocol?) async {
extendedProfileService = userSession.map { ExtendedProfileService(clientProxy: $0.clientProxy) }
init(extendedProfileService: ExtendedProfileServiceProtocol) {
self.extendedProfileService = extendedProfileService
}
func extendedProfileSection(userID: String) -> AnyView? {
guard let extendedProfileService else {
return nil
}
return AnyView(HealthChatExtendedProfileSection(userID: userID, service: extendedProfileService))
AnyView(HealthChatExtendedProfileSection(userID: userID, service: extendedProfileService))
}
}
#endif
@@ -14,28 +14,19 @@ import SwiftUI
final class HealthChatSettingsScreenHook: SettingsScreenHookProtocol {
// 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.
private var statusLoader: HealthChatAccountStatusLoader?
private let statusLoader: HealthChatAccountStatusLoader
// Constructed during hook registration, before the main actor is reachable.
nonisolated init() { }
func configure(with userSession: UserSessionProtocol?) async {
statusLoader = userSession.map {
let apiService = HealthChatAPIService(tokenProvider: HealthChatOpenIDTokenProvider(clientProxy: $0.clientProxy))
return HealthChatAccountStatusLoader(apiService: apiService)
}
init(statusLoader: HealthChatAccountStatusLoader) {
self.statusLoader = statusLoader
}
func accountStatusSection(completeProfileAction: @escaping () -> Void) -> AnyView? {
guard let statusLoader else {
return nil
}
return AnyView(HealthChatAccountStatusSection(loader: statusLoader,
completeProfileAction: completeProfileAction))
AnyView(HealthChatAccountStatusSection(loader: statusLoader,
completeProfileAction: completeProfileAction))
}
func refreshAccountStatus() {
statusLoader?.load()
statusLoader.load()
}
}
#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
nonisolated protocol ExtendedProfileSectionHookProtocol: Sendable {
@MainActor func configure(with userSession: UserSessionProtocol?) async
/// An extra Form section rendered below the header on the user profile
/// and room member details screens.
@MainActor func extendedProfileSection(userID: String) -> AnyView?
}
struct DefaultExtendedProfileSectionHook: ExtendedProfileSectionHookProtocol {
func configure(with userSession: UserSessionProtocol?) async { }
func extendedProfileSection(userID: String) -> AnyView? {
nil
}
@@ -8,8 +8,6 @@
import SwiftUI
nonisolated protocol SettingsScreenHookProtocol: Sendable {
@MainActor func configure(with userSession: UserSessionProtocol?) async
/// An extra Form section rendered between the user and the account sections,
/// advertising whatever is holding the account back.
/// - Parameter completeProfileAction: Opens the own profile editor.
@@ -20,8 +18,6 @@ nonisolated protocol SettingsScreenHookProtocol: Sendable {
}
struct DefaultSettingsScreenHook: SettingsScreenHookProtocol {
func configure(with userSession: UserSessionProtocol?) async { }
func accountStatusSection(completeProfileAction: @escaping () -> Void) -> AnyView? {
nil
}
+8 -1
View File
@@ -15,6 +15,9 @@ nonisolated struct UserHooks: Sendable {
let recoveryKeyScreenHook: RecoveryKeyScreenHookProtocol
let createRoomScreenHook: CreateRoomScreenHookProtocol
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(),
roomDetailsScreenHook: RoomDetailsScreenHookProtocol = DefaultRoomDetailsScreenHook(),
@@ -22,7 +25,9 @@ nonisolated struct UserHooks: Sendable {
userProfileScreenHook: UserProfileScreenHookProtocol = DefaultUserProfileScreenHook(),
recoveryKeyScreenHook: RecoveryKeyScreenHookProtocol = DefaultRecoveryKeyScreenHook(),
createRoomScreenHook: CreateRoomScreenHookProtocol = DefaultCreateRoomScreenHook(),
securityAndPrivacyScreenHook: SecurityAndPrivacyScreenHookProtocol = DefaultSecurityAndPrivacyScreenHook()) {
securityAndPrivacyScreenHook: SecurityAndPrivacyScreenHookProtocol = DefaultSecurityAndPrivacyScreenHook(),
extendedProfileSectionHook: ExtendedProfileSectionHookProtocol = DefaultExtendedProfileSectionHook(),
settingsScreenHook: SettingsScreenHookProtocol = DefaultSettingsScreenHook()) {
self.roomScreenHook = roomScreenHook
self.roomDetailsScreenHook = roomDetailsScreenHook
self.roomMemberDetailsScreenHook = roomMemberDetailsScreenHook
@@ -30,5 +35,7 @@ nonisolated struct UserHooks: Sendable {
self.recoveryKeyScreenHook = recoveryKeyScreenHook
self.createRoomScreenHook = createRoomScreenHook
self.securityAndPrivacyScreenHook = securityAndPrivacyScreenHook
self.extendedProfileSectionHook = extendedProfileSectionHook
self.settingsScreenHook = settingsScreenHook
}
}