Add profile editor, member invite and status banners (#4)

* feat(healthchat): add profile editor, member invite and status banners

Native 13-field profile editor with diff-only saves and avatar upload
through the HealthChat API (replaces the upstream display-name editor),
email invitation for approved accounts, and non-dismissible account
status banners in settings and the member directory.

* fix(healthchat): satisfy detekt and konsist rules

Konsist presenter helper shape, no '=' in state parameter docs, present-tense invite callback, and complete renderer KDoc.
This commit is contained in:
2026-08-06 13:28:24 +02:00
committed by GitHub
parent edb3520c56
commit 7f43998fa5
74 changed files with 2946 additions and 26 deletions
@@ -72,6 +72,7 @@ dependencies {
implementation(projects.libraries.sessionStorage.api)
implementation(projects.libraries.uiUtils)
implementation(projects.libraries.fullscreenintent.api)
implementation(projects.features.healthchat.api)
implementation(projects.features.rageshake.api)
implementation(projects.features.lockscreen.api)
implementation(projects.features.analytics.api)
@@ -21,6 +21,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.deactivation.api.AccountDeactivationEntryPoint
import io.element.android.features.healthchat.api.HealthChatEntryPoint
import io.element.android.features.licenses.api.OpenSourceLicensesEntryPoint
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.logout.api.LogoutEntryPoint
@@ -34,7 +35,6 @@ import io.element.android.features.preferences.impl.labs.LabsNode
import io.element.android.features.preferences.impl.notifications.NotificationSettingsNode
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingNode
import io.element.android.features.preferences.impl.root.PreferencesRootNode
import io.element.android.features.preferences.impl.user.editprofile.EditUserProfileNode
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.appyx.canPop
@@ -59,6 +59,8 @@ class PreferencesFlowNode(
private val logoutEntryPoint: LogoutEntryPoint,
private val openSourceLicensesEntryPoint: OpenSourceLicensesEntryPoint,
private val accountDeactivationEntryPoint: AccountDeactivationEntryPoint,
// HealthChat fork: the native 13-field profile editor.
private val healthChatEntryPoint: HealthChatEntryPoint,
) : BaseFlowNode<PreferencesFlowNode.NavTarget>(
backstack = BackStack(
initialElement = plugins.filterIsInstance<PreferencesEntryPoint.Params>().first().initialElement.toNavTarget(),
@@ -283,13 +285,18 @@ class PreferencesFlowNode(
createNode<AdvancedSettingsNode>(buildContext)
}
is NavTarget.UserProfile -> {
val inputs = EditUserProfileNode.Inputs(navTarget.matrixUser)
val callback = object : EditUserProfileNode.Callback {
override fun onDone() {
backstack.pop()
}
}
createNode<EditUserProfileNode>(buildContext, listOf(inputs, callback))
// HealthChat fork: the native 13-field editor replaces the upstream
// display-name editor (the server derives the display name).
healthChatEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
params = HealthChatEntryPoint.Params(HealthChatEntryPoint.InitialTarget.EditOwnProfile),
callback = object : HealthChatEntryPoint.Callback {
override fun onDone() {
backstack.pop()
}
},
)
}
NavTarget.LockScreenSettings -> {
lockScreenEntryPoint.createNode(
@@ -19,6 +19,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.healthchat.api.HealthChatAccountStatusRenderer
import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutView
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
@@ -33,6 +34,8 @@ class PreferencesRootNode(
@Assisted plugins: List<Plugin>,
private val presenter: PreferencesRootPresenter,
private val directLogoutView: DirectLogoutView,
// HealthChat fork: renders the account status banners.
private val healthChatAccountStatusRenderer: HealthChatAccountStatusRenderer,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun navigateToAddAccount()
@@ -98,7 +101,14 @@ class PreferencesRootNode(
callback.startSignOutFlow()
}
},
onDeactivateClick = callback::startAccountDeactivationFlow
onDeactivateClick = callback::startAccountDeactivationFlow,
healthChatAccountStatus = {
healthChatAccountStatusRenderer.Render(
onCompleteProfileClick = { callback.navigateToUserProfile(state.myUser) },
includePendingApproval = true,
modifier = Modifier,
)
},
)
directLogoutView.Render(state = state.directLogoutState)
@@ -64,6 +64,8 @@ fun PreferencesRootView(
onSignOutClick: () -> Unit,
onDeactivateClick: () -> Unit,
modifier: Modifier = Modifier,
// HealthChat fork: account status banners, rendered by features/healthchat.
healthChatAccountStatus: @Composable () -> Unit = {},
) {
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
@@ -89,6 +91,7 @@ fun PreferencesRootView(
HorizontalDivider()
}
// User status will be added here
healthChatAccountStatus()
// 'Account' section
ManageAccountSection(
state = state,
@@ -46,6 +46,7 @@ class DefaultPreferencesEntryPointTest {
logoutEntryPoint = FakeLogoutEntryPoint(),
openSourceLicensesEntryPoint = FakeOpenSourceLicensesEntryPoint(),
accountDeactivationEntryPoint = FakeAccountDeactivationEntryPoint(),
healthChatEntryPoint = { _, _, _, _ -> lambdaError() },
)
}
val callback = object : PreferencesEntryPoint.Callback {