mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-06 07:24:49 +00:00
Add member directory tab to home navigation (#3)
* feat(healthchat): add member directory tab to home navigation Third bottom-nav tab backed by the HealthChat API: searchable member cards with native start-chat, approval-pending banner, retry on failure and navigation into the native profile screens. * fix(healthchat): stop tab back handler swallowing back presses While a screen is pushed on top of Home, the still-composed tab BackHandler intercepted back presses during the transition and invisibly switched to the Chats tab. Gate it on the node's target visibility in both the parent and Home's own backstack. * fix(healthchat): satisfy detekt, ktlint and konsist rules Import alias order, composable parameter order in HomeView, and konsist presenter helper shape. * ci: pass the real repository name to the screenshot recorder recordScreenshots.yml set GITHUB_REPOSITORY from `secrets.GITHUB_REPOSITORY`. No such secret exists on this fork, so the expression expands to an empty string and overrides the built-in environment variable of the same name. recordScreenshots.sh checks it up front and exits 1 with "No repo specified", so the workflow could never record anything here. Use the `github.repository` context instead, which is always populated. Claude-Session: https://claude.ai/code/session_01MQbp1EPM4TSXq8P8Pyq44F * Update screenshots * Revert "Update screenshots" This reverts commitb99e0d643f. * Reapply "Update screenshots" This reverts commit85bceb6d67. * test(screenshots): stop a LayoutLib teardown crash failing the shard tests Paparazzi 2.0.0-alpha05 / LayoutLib 16.2.1 races on HandlerThread startup: HandlerThread_Delegate.run calls Process.setThreadPriority, which throws NoSuchMethodError for Thread.setPosixNicenessInternal and kills the "android.bg" thread. If the test thread reaches getLooper() after that, it gets null, BackgroundThread.ensureThreadReady() NPEs, and the static initialiser of LatencyTracker fails. The JVM then marks that class erroneous for the lifetime of the fork, so every later preview showing the soft keyboard dies with NoClassDefFoundError. LayoutLibErrorFilterStatement already swallowed the bare NoSuchMethodError, but PaparazziLogger re-throws collected background errors from teardown() wrapped in a MultipleFailuresException when there is more than one, and that wrapper slipped through. A recent run reported 17 failures with zero image comparisons failing and zero delta images written. Unwrap the wrapper by reflection and swallow it only when every collected cause matches the known-bug list; anything unrecognised, and anything that cannot be decomposed, is rethrown. Also close a hole this filter had from the start. Paparazzi runs the test as `try { body } finally { teardown() }`, so a teardown error discards whatever the body threw — including a genuine snapshot mismatch. renderFailureCapture is registered inside the Paparazzi rule to record the body's exception so it wins over any teardown error, and the filter refuses to swallow anything if that rule is not registered. Two HorizontalFloatingToolbar previews have been mismatching silently behind this, and may now surface as real failures. Claude-Session: https://claude.ai/code/session_01MQbp1EPM4TSXq8P8Pyq44F --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
+12
@@ -8,6 +8,8 @@
|
||||
|
||||
package io.element.android.features.healthchat.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
/**
|
||||
* Display name fallback chain, mirroring the web client:
|
||||
* displayName → "title firstName lastName" → businessEmail → [fallback].
|
||||
@@ -21,3 +23,13 @@ fun HealthChatMember.bestDisplayName(fallback: String): String {
|
||||
businessEmail?.takeIf { it.isNotBlank() }?.let { return it }
|
||||
return fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* The Matrix representation of a directory member, e.g. to start a DM through the app's
|
||||
* regular flows.
|
||||
*/
|
||||
fun HealthChatMember.toMatrixUser() = MatrixUser(
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
avatarUrl = avatarUrl,
|
||||
)
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ import io.element.android.libraries.matrix.api.core.UserId
|
||||
* Injected into the upstream profile screens as a slot so the fork's only upstream diff is
|
||||
* one defaulted parameter plus the injection (same pattern as `InvitePeopleRenderer`).
|
||||
*/
|
||||
interface HealthChatProfileSectionRenderer {
|
||||
fun interface HealthChatProfileSectionRenderer {
|
||||
@Composable
|
||||
fun Render(
|
||||
userId: UserId,
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Novaloop AG
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.healthchat.api.directory
|
||||
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
sealed interface MemberDirectoryEvents {
|
||||
/**
|
||||
* Fired by the view on first composition. The tab presenter runs from app start (it is
|
||||
* composed by the Home presenter), so the first fetch is deferred until the tab is shown.
|
||||
*/
|
||||
data object LoadIfNeeded : MemberDirectoryEvents
|
||||
|
||||
/** Header refresh and "try again" — one load path. */
|
||||
data object Refresh : MemberDirectoryEvents
|
||||
|
||||
data class StartDm(val matrixUser: MatrixUser) : MemberDirectoryEvents
|
||||
|
||||
data object ClearStartDmState : MemberDirectoryEvents
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Novaloop AG
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.healthchat.api.directory
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
/**
|
||||
* Renders the member directory tab content. Injected into the Home screen as a slot so the
|
||||
* fork's upstream diff stays minimal.
|
||||
*
|
||||
* Nullable callbacks hide their affordance entirely (e.g. profile editing and inviting only
|
||||
* become available once the corresponding screens exist).
|
||||
*/
|
||||
fun interface MemberDirectoryRenderer {
|
||||
@Composable
|
||||
fun Render(
|
||||
state: MemberDirectoryState,
|
||||
contentPadding: PaddingValues,
|
||||
lazyListState: LazyListState,
|
||||
onOpenDm: (RoomId) -> Unit,
|
||||
onMemberClick: ((UserId) -> Unit)?,
|
||||
onMyProfileClick: (() -> Unit)?,
|
||||
onEditProfileClick: (() -> Unit)?,
|
||||
onInviteClick: (() -> Unit)?,
|
||||
modifier: Modifier,
|
||||
)
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Novaloop AG
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.healthchat.api.directory
|
||||
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import io.element.android.features.healthchat.api.HealthChatMemberDirectory
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
data class MemberDirectoryState(
|
||||
val searchQuery: TextFieldState,
|
||||
val directory: AsyncData<HealthChatMemberDirectory>,
|
||||
val startDmActionState: AsyncAction<RoomId>,
|
||||
val eventSink: (MemberDirectoryEvents) -> Unit,
|
||||
) {
|
||||
/** Unknown-while-loading counts as approved so no banner flashes during the first load. */
|
||||
val isApproved: Boolean
|
||||
get() = directory.dataOrNull()?.isApproved != false
|
||||
|
||||
/** The verbatim German server explanation, only when the account is not approved. */
|
||||
val pendingMessage: String?
|
||||
get() = directory.dataOrNull()?.takeIf { !it.isApproved }?.pendingMessage
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Novaloop AG
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.healthchat.api.directory
|
||||
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import io.element.android.features.healthchat.api.HealthChatMember
|
||||
import io.element.android.features.healthchat.api.HealthChatMemberDirectory
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Sample state builders, shared by previews inside the healthchat feature and by the Home
|
||||
* screen's previews (same pattern as `aDirectLogoutState` in `features/logout/api`).
|
||||
*/
|
||||
fun aMemberDirectoryState(
|
||||
searchQuery: TextFieldState = TextFieldState(),
|
||||
directory: AsyncData<HealthChatMemberDirectory> = AsyncData.Success(aMemberDirectory()),
|
||||
startDmActionState: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||
eventSink: (MemberDirectoryEvents) -> Unit = {},
|
||||
) = MemberDirectoryState(
|
||||
searchQuery = searchQuery,
|
||||
directory = directory,
|
||||
startDmActionState = startDmActionState,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
fun aMemberDirectory(
|
||||
members: List<HealthChatMember> = aMemberList(),
|
||||
totalCount: Int = members.size,
|
||||
searchQuery: String = "",
|
||||
isApproved: Boolean = true,
|
||||
pendingMessage: String? = null,
|
||||
) = HealthChatMemberDirectory(
|
||||
members = members.toImmutableList(),
|
||||
totalCount = totalCount,
|
||||
searchQuery = searchQuery,
|
||||
isApproved = isApproved,
|
||||
pendingMessage = pendingMessage,
|
||||
)
|
||||
|
||||
fun aMemberList(): List<HealthChatMember> = listOf(
|
||||
aDirectoryMember(),
|
||||
aDirectoryMember(
|
||||
userId = UserId("@luca.bianchi:healthchat.ch"),
|
||||
displayName = "Dr. med. Luca Bianchi",
|
||||
specialty = "Kardiologie",
|
||||
practiceName = "Herzzentrum Bellevue",
|
||||
businessCity = "Bern",
|
||||
businessEmail = null,
|
||||
businessTel = null,
|
||||
),
|
||||
aDirectoryMember(
|
||||
userId = UserId("@nora.keller:healthchat.ch"),
|
||||
displayName = null,
|
||||
specialty = null,
|
||||
practiceName = null,
|
||||
businessCity = null,
|
||||
businessEmail = "nora.keller@example.ch",
|
||||
businessTel = null,
|
||||
),
|
||||
)
|
||||
|
||||
fun aDirectoryMember(
|
||||
userId: UserId = UserId("@erika.muster:healthchat.ch"),
|
||||
displayName: String? = "Dr. med. Erika Muster",
|
||||
specialty: String? = "Allgemeinmedizin",
|
||||
practiceName: String? = "Praxis Muster",
|
||||
businessCity: String? = "Zürich",
|
||||
businessEmail: String? = "praxis@muster.ch",
|
||||
businessTel: String? = "+41 44 123 45 67",
|
||||
isApproved: Boolean = true,
|
||||
) = HealthChatMember(
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
avatarUrl = null,
|
||||
title = null,
|
||||
firstName = null,
|
||||
lastName = null,
|
||||
specialty = specialty,
|
||||
practiceName = practiceName,
|
||||
businessCity = businessCity,
|
||||
furtherInfo = null,
|
||||
businessEmail = businessEmail,
|
||||
businessTel = businessTel,
|
||||
street = null,
|
||||
houseNumber = null,
|
||||
postalCode = null,
|
||||
website = null,
|
||||
isApproved = isApproved,
|
||||
)
|
||||
@@ -20,4 +20,21 @@
|
||||
<string name="healthchat_field_further_info">Weitere Infos / Qualifikationen</string>
|
||||
<string name="healthchat_profile_error_loading">Die HealthChat-Profildaten konnten nicht geladen werden.</string>
|
||||
<string name="healthchat_profile_not_approved_hint">Profildetails sind sichtbar, sobald Ihr Konto freigegeben ist.</string>
|
||||
<string name="healthchat_screen_home_tab_members">Mitglieder</string>
|
||||
<plurals name="healthchat_screen_member_directory_member_count">
|
||||
<item quantity="one">%1$d Mitglied</item>
|
||||
<item quantity="other">%1$d Mitglieder</item>
|
||||
</plurals>
|
||||
<string name="healthchat_screen_member_directory_my_profile">Mein Profil</string>
|
||||
<string name="healthchat_screen_member_directory_search_placeholder">Suche nach Name, Praxis, Ort oder Fachgebiet</string>
|
||||
<string name="healthchat_screen_member_directory_start_chat">Chat starten</string>
|
||||
<string name="healthchat_screen_member_directory_error_starting_chat">Fehler beim Erstellen des Chats</string>
|
||||
<string name="healthchat_screen_member_directory_empty">Es sind noch keine Mitglieder vorhanden.</string>
|
||||
<string name="healthchat_screen_member_directory_empty_search">Keine Mitglieder entsprechen Ihrer Suche.</string>
|
||||
<string name="healthchat_screen_member_directory_unknown_name">Unbekannt</string>
|
||||
<string name="healthchat_screen_member_directory_pending_title">Konto wird geprüft</string>
|
||||
<string name="healthchat_screen_member_directory_pending_message">Ihr Konto wird derzeit geprüft. Nach der Freigabe können Sie alle Mitglieder sehen und durchsuchen.</string>
|
||||
<string name="healthchat_a11y_refresh">Aktualisieren</string>
|
||||
<string name="healthchat_a11y_edit_profile">Profil bearbeiten</string>
|
||||
<string name="healthchat_a11y_invite_member">Neues Mitglied einladen</string>
|
||||
</resources>
|
||||
|
||||
@@ -20,4 +20,22 @@
|
||||
<string name="healthchat_field_further_info">Informations complémentaires / qualifications</string>
|
||||
<string name="healthchat_profile_error_loading">Les détails du profil HealthChat n’ont pas pu être chargés.</string>
|
||||
<string name="healthchat_profile_not_approved_hint">Les détails du profil seront visibles dès que votre compte aura été approuvé.</string>
|
||||
<string name="healthchat_screen_home_tab_members">Membres</string>
|
||||
<plurals name="healthchat_screen_member_directory_member_count">
|
||||
<item quantity="one">%1$d membre</item>
|
||||
<item quantity="many">%1$d membres</item>
|
||||
<item quantity="other">%1$d membres</item>
|
||||
</plurals>
|
||||
<string name="healthchat_screen_member_directory_my_profile">Mon profil</string>
|
||||
<string name="healthchat_screen_member_directory_search_placeholder">Rechercher par nom, cabinet, ville ou spécialité</string>
|
||||
<string name="healthchat_screen_member_directory_start_chat">Démarrer une discussion</string>
|
||||
<string name="healthchat_screen_member_directory_error_starting_chat">Impossible de démarrer la discussion</string>
|
||||
<string name="healthchat_screen_member_directory_empty">Il n’y a pas encore de membres.</string>
|
||||
<string name="healthchat_screen_member_directory_empty_search">Aucun membre ne correspond à votre recherche.</string>
|
||||
<string name="healthchat_screen_member_directory_unknown_name">Inconnu</string>
|
||||
<string name="healthchat_screen_member_directory_pending_title">Compte en cours de vérification</string>
|
||||
<string name="healthchat_screen_member_directory_pending_message">Votre compte est en cours de vérification. Une fois approuvé, vous pourrez voir et rechercher tous les membres.</string>
|
||||
<string name="healthchat_a11y_refresh">Actualiser</string>
|
||||
<string name="healthchat_a11y_edit_profile">Modifier le profil</string>
|
||||
<string name="healthchat_a11y_invite_member">Inviter un nouveau membre</string>
|
||||
</resources>
|
||||
|
||||
@@ -20,4 +20,22 @@
|
||||
<string name="healthchat_field_further_info">Ulteriori informazioni / qualifiche</string>
|
||||
<string name="healthchat_profile_error_loading">Impossibile caricare i dettagli del profilo HealthChat.</string>
|
||||
<string name="healthchat_profile_not_approved_hint">I dettagli del profilo saranno visibili non appena il tuo account sarà stato approvato.</string>
|
||||
<string name="healthchat_screen_home_tab_members">Membri</string>
|
||||
<plurals name="healthchat_screen_member_directory_member_count">
|
||||
<item quantity="one">%1$d membro</item>
|
||||
<item quantity="many">%1$d membri</item>
|
||||
<item quantity="other">%1$d membri</item>
|
||||
</plurals>
|
||||
<string name="healthchat_screen_member_directory_my_profile">Il mio profilo</string>
|
||||
<string name="healthchat_screen_member_directory_search_placeholder">Cerca per nome, studio, città o specializzazione</string>
|
||||
<string name="healthchat_screen_member_directory_start_chat">Avvia chat</string>
|
||||
<string name="healthchat_screen_member_directory_error_starting_chat">Impossibile avviare la chat</string>
|
||||
<string name="healthchat_screen_member_directory_empty">Non ci sono ancora membri.</string>
|
||||
<string name="healthchat_screen_member_directory_empty_search">Nessun membro corrisponde alla tua ricerca.</string>
|
||||
<string name="healthchat_screen_member_directory_unknown_name">Sconosciuto</string>
|
||||
<string name="healthchat_screen_member_directory_pending_title">Account in fase di verifica</string>
|
||||
<string name="healthchat_screen_member_directory_pending_message">Il tuo account è attualmente in fase di verifica. Una volta approvato potrai vedere e cercare tutti i membri.</string>
|
||||
<string name="healthchat_a11y_refresh">Aggiorna</string>
|
||||
<string name="healthchat_a11y_edit_profile">Modifica profilo</string>
|
||||
<string name="healthchat_a11y_invite_member">Invita un nuovo membro</string>
|
||||
</resources>
|
||||
|
||||
@@ -21,4 +21,21 @@
|
||||
<string name="healthchat_field_further_info">Further info / qualifications</string>
|
||||
<string name="healthchat_profile_error_loading">The HealthChat profile details couldn’t be loaded.</string>
|
||||
<string name="healthchat_profile_not_approved_hint">Profile details become visible once your account has been approved.</string>
|
||||
<string name="healthchat_screen_home_tab_members">Members</string>
|
||||
<plurals name="healthchat_screen_member_directory_member_count">
|
||||
<item quantity="one">%1$d member</item>
|
||||
<item quantity="other">%1$d members</item>
|
||||
</plurals>
|
||||
<string name="healthchat_screen_member_directory_my_profile">My profile</string>
|
||||
<string name="healthchat_screen_member_directory_search_placeholder">Search by name, practice, city or specialty</string>
|
||||
<string name="healthchat_screen_member_directory_start_chat">Start chat</string>
|
||||
<string name="healthchat_screen_member_directory_error_starting_chat">Failed to start the chat</string>
|
||||
<string name="healthchat_screen_member_directory_empty">There are no members yet.</string>
|
||||
<string name="healthchat_screen_member_directory_empty_search">No members match your search.</string>
|
||||
<string name="healthchat_screen_member_directory_unknown_name">Unknown</string>
|
||||
<string name="healthchat_screen_member_directory_pending_title">Account under review</string>
|
||||
<string name="healthchat_screen_member_directory_pending_message">Your account is currently under review. Once it has been approved you can see and search all members.</string>
|
||||
<string name="healthchat_a11y_refresh">Refresh</string>
|
||||
<string name="healthchat_a11y_edit_profile">Edit profile</string>
|
||||
<string name="healthchat_a11y_invite_member">Invite a new member</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user