From 8f82e578f4ed5aa902c9f6eab19d75b23b2d779a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABl?= Date: Thu, 30 Jul 2026 20:54:50 +0200 Subject: [PATCH] feat(healthchat): show profile fields on profile screens Renderer slot in UserProfileView (all three profile screens) shows specialty, practice, address, contact links and further info from the HealthChat API, with inline retry on failure. --- .../api/HealthChatProfileSectionRenderer.kt | 28 +++ .../main/res/values-de/strings_healthchat.xml | 9 + .../main/res/values-fr/strings_healthchat.xml | 9 + .../main/res/values-it/strings_healthchat.xml | 9 + .../main/res/values/strings_healthchat.xml | 9 + features/healthchat/impl/build.gradle.kts | 4 +- ...DefaultHealthChatProfileSectionRenderer.kt | 37 +++ .../section/HealthChatProfileSectionEvents.kt | 13 ++ .../HealthChatProfileSectionPresenter.kt | 77 +++++++ .../section/HealthChatProfileSectionState.kt | 17 ++ .../HealthChatProfileSectionStateProvider.kt | 85 +++++++ .../section/HealthChatProfileSectionView.kt | 212 ++++++++++++++++++ .../HealthChatProfileSectionPresenterTest.kt | 129 +++++++++++ .../HealthChatProfileSectionViewTest.kt | 154 +++++++++++++ features/roomdetails/impl/build.gradle.kts | 2 + .../members/details/RoomMemberDetailsNode.kt | 6 + features/userprofile/impl/build.gradle.kts | 1 + .../userprofile/impl/root/UserProfileNode.kt | 6 + .../userprofile/shared/UserProfileView.kt | 3 + ..._HealthChatProfileSectionView_Day_0_en.png | 3 + ..._HealthChatProfileSectionView_Day_1_en.png | 3 + ..._HealthChatProfileSectionView_Day_2_en.png | 3 + ..._HealthChatProfileSectionView_Day_3_en.png | 3 + ..._HealthChatProfileSectionView_Day_4_en.png | 3 + ..._HealthChatProfileSectionView_Day_5_en.png | 3 + ...ealthChatProfileSectionView_Night_0_en.png | 3 + ...ealthChatProfileSectionView_Night_1_en.png | 3 + ...ealthChatProfileSectionView_Night_2_en.png | 3 + ...ealthChatProfileSectionView_Night_3_en.png | 3 + ...ealthChatProfileSectionView_Night_4_en.png | 3 + ...ealthChatProfileSectionView_Night_5_en.png | 3 + 31 files changed, 845 insertions(+), 1 deletion(-) create mode 100644 features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/HealthChatProfileSectionRenderer.kt create mode 100644 features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/DefaultHealthChatProfileSectionRenderer.kt create mode 100644 features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionEvents.kt create mode 100644 features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionPresenter.kt create mode 100644 features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionState.kt create mode 100644 features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionStateProvider.kt create mode 100644 features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionView.kt create mode 100644 features/healthchat/impl/src/test/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionPresenterTest.kt create mode 100644 features/healthchat/impl/src/test/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionViewTest.kt create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_5_en.png diff --git a/features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/HealthChatProfileSectionRenderer.kt b/features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/HealthChatProfileSectionRenderer.kt new file mode 100644 index 0000000000..971c459af4 --- /dev/null +++ b/features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/HealthChatProfileSectionRenderer.kt @@ -0,0 +1,28 @@ +/* + * 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 + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.element.android.libraries.matrix.api.core.UserId + +/** + * Renders the HealthChat profile fields (specialty, practice, contact details, …) for a user. + * + * 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 { + @Composable + fun Render( + userId: UserId, + isCurrentUser: Boolean, + modifier: Modifier, + ) +} diff --git a/features/healthchat/api/src/main/res/values-de/strings_healthchat.xml b/features/healthchat/api/src/main/res/values-de/strings_healthchat.xml index 4ed4da1bc8..3e6e8159cb 100644 --- a/features/healthchat/api/src/main/res/values-de/strings_healthchat.xml +++ b/features/healthchat/api/src/main/res/values-de/strings_healthchat.xml @@ -11,4 +11,13 @@ Ein Netzwerkfehler ist aufgetreten. Bitte prüfen Sie Ihre Verbindung und versuchen Sie es erneut. Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut. + Fachgebiet + Praxis / Unternehmen + Adresse + E-Mail (geschäftlich) + Telefon (geschäftlich) + Webseite + Weitere Infos / Qualifikationen + Die HealthChat-Profildaten konnten nicht geladen werden. + Profildetails sind sichtbar, sobald Ihr Konto freigegeben ist. diff --git a/features/healthchat/api/src/main/res/values-fr/strings_healthchat.xml b/features/healthchat/api/src/main/res/values-fr/strings_healthchat.xml index 8538d72b49..55ea83ac9c 100644 --- a/features/healthchat/api/src/main/res/values-fr/strings_healthchat.xml +++ b/features/healthchat/api/src/main/res/values-fr/strings_healthchat.xml @@ -11,4 +11,13 @@ Une erreur réseau est survenue. Veuillez vérifier votre connexion et réessayer. Une erreur est survenue. Veuillez réessayer. + Spécialité + Cabinet / Entreprise + Adresse + E-mail (professionnel) + Téléphone (professionnel) + Site web + Informations complémentaires / qualifications + Les détails du profil HealthChat n’ont pas pu être chargés. + Les détails du profil seront visibles dès que votre compte aura été approuvé. diff --git a/features/healthchat/api/src/main/res/values-it/strings_healthchat.xml b/features/healthchat/api/src/main/res/values-it/strings_healthchat.xml index 7e43442ee6..31ffbf5810 100644 --- a/features/healthchat/api/src/main/res/values-it/strings_healthchat.xml +++ b/features/healthchat/api/src/main/res/values-it/strings_healthchat.xml @@ -11,4 +11,13 @@ Si è verificato un errore di rete. Controlla la connessione e riprova. Qualcosa è andato storto. Riprova. + Specializzazione + Studio / Azienda + Indirizzo + E-mail (professionale) + Telefono (professionale) + Sito web + Ulteriori informazioni / qualifiche + Impossibile caricare i dettagli del profilo HealthChat. + I dettagli del profilo saranno visibili non appena il tuo account sarà stato approvato. diff --git a/features/healthchat/api/src/main/res/values/strings_healthchat.xml b/features/healthchat/api/src/main/res/values/strings_healthchat.xml index e210d59366..7de9716380 100644 --- a/features/healthchat/api/src/main/res/values/strings_healthchat.xml +++ b/features/healthchat/api/src/main/res/values/strings_healthchat.xml @@ -12,4 +12,13 @@ A network error occurred. Please check your connection and try again. Something went wrong. Please try again. + Specialty + Practice / Company + Address + Email (business) + Phone (business) + Website + Further info / qualifications + The HealthChat profile details couldn’t be loaded. + Profile details become visible once your account has been approved. diff --git a/features/healthchat/impl/build.gradle.kts b/features/healthchat/impl/build.gradle.kts index f4eabb30b5..cea0f71299 100644 --- a/features/healthchat/impl/build.gradle.kts +++ b/features/healthchat/impl/build.gradle.kts @@ -54,11 +54,13 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) implementation(projects.libraries.core) + implementation(projects.libraries.designsystem) implementation(projects.libraries.matrix.api) implementation(projects.libraries.network) implementation(projects.libraries.sessionStorage.api) + implementation(projects.libraries.uiStrings) - testCommonDependencies(libs) + testCommonDependencies(libs, true) testImplementation(libs.network.mockwebserver) testImplementation(projects.features.healthchat.test) testImplementation(projects.libraries.matrix.test) diff --git a/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/DefaultHealthChatProfileSectionRenderer.kt b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/DefaultHealthChatProfileSectionRenderer.kt new file mode 100644 index 0000000000..f6ce372fb9 --- /dev/null +++ b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/DefaultHealthChatProfileSectionRenderer.kt @@ -0,0 +1,37 @@ +/* + * 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.impl.profile.section + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import dev.zacsweers.metro.ContributesBinding +import io.element.android.features.healthchat.api.HealthChatProfileSectionRenderer +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.UserId + +@ContributesBinding(SessionScope::class) +class DefaultHealthChatProfileSectionRenderer( + private val presenterFactory: HealthChatProfileSectionPresenter.Factory, +) : HealthChatProfileSectionRenderer { + @Composable + override fun Render( + userId: UserId, + isCurrentUser: Boolean, + modifier: Modifier, + ) { + val presenter = remember(userId, isCurrentUser) { + presenterFactory.create(userId = userId, isCurrentUser = isCurrentUser) + } + HealthChatProfileSectionView( + state = presenter.present(), + modifier = modifier, + ) + } +} diff --git a/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionEvents.kt b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionEvents.kt new file mode 100644 index 0000000000..62cad15402 --- /dev/null +++ b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionEvents.kt @@ -0,0 +1,13 @@ +/* + * 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.impl.profile.section + +sealed interface HealthChatProfileSectionEvents { + data object Retry : HealthChatProfileSectionEvents +} diff --git a/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionPresenter.kt b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionPresenter.kt new file mode 100644 index 0000000000..dc6413dedd --- /dev/null +++ b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionPresenter.kt @@ -0,0 +1,77 @@ +/* + * 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.impl.profile.section + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import io.element.android.features.healthchat.api.HealthChatDirectoryService +import io.element.android.features.healthchat.api.HealthChatMember +import io.element.android.features.healthchat.api.HealthChatProfileRepository +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.core.UserId + +@AssistedInject +class HealthChatProfileSectionPresenter( + @Assisted private val userId: UserId, + @Assisted private val isCurrentUser: Boolean, + private val directoryService: HealthChatDirectoryService, + private val profileRepository: HealthChatProfileRepository, +) : Presenter { + @AssistedFactory + interface Factory { + fun create(userId: UserId, isCurrentUser: Boolean): HealthChatProfileSectionPresenter + } + + @Composable + override fun present(): HealthChatProfileSectionState { + var retryTrigger by rememberSaveable { mutableIntStateOf(0) } + // isCurrentUser is fixed for the lifetime of the presenter, so the branch is stable. + val profile = if (isCurrentUser) { + val ownProfile by profileRepository.ownProfile.collectAsState() + LaunchedEffect(retryTrigger) { + profileRepository.refreshOwnProfile() + } + ownProfile + } else { + var memberProfile by remember { + mutableStateOf>(AsyncData.Uninitialized) + } + LaunchedEffect(retryTrigger) { + memberProfile = AsyncData.Loading(memberProfile.dataOrNull()) + directoryService.getMember(userId).fold( + onSuccess = { memberProfile = AsyncData.Success(it) }, + onFailure = { memberProfile = AsyncData.Failure(it, memberProfile.dataOrNull()) }, + ) + } + memberProfile + } + + fun handleEvents(event: HealthChatProfileSectionEvents) { + when (event) { + HealthChatProfileSectionEvents.Retry -> retryTrigger++ + } + } + + return HealthChatProfileSectionState( + profile = profile, + eventSink = ::handleEvents, + ) + } +} diff --git a/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionState.kt b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionState.kt new file mode 100644 index 0000000000..1459a0100c --- /dev/null +++ b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionState.kt @@ -0,0 +1,17 @@ +/* + * 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.impl.profile.section + +import io.element.android.features.healthchat.api.HealthChatMember +import io.element.android.libraries.architecture.AsyncData + +data class HealthChatProfileSectionState( + val profile: AsyncData, + val eventSink: (HealthChatProfileSectionEvents) -> Unit, +) diff --git a/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionStateProvider.kt b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionStateProvider.kt new file mode 100644 index 0000000000..ebfa4e92af --- /dev/null +++ b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionStateProvider.kt @@ -0,0 +1,85 @@ +/* + * 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.impl.profile.section + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.healthchat.api.HealthChatApiException +import io.element.android.features.healthchat.api.HealthChatMember +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.core.UserId + +open class HealthChatProfileSectionStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aHealthChatProfileSectionState(profile = AsyncData.Success(aProfileSectionMember())), + aHealthChatProfileSectionState( + profile = AsyncData.Success( + aProfileSectionMember( + specialty = "Allgemeinmedizin", + practiceName = null, + businessCity = null, + businessEmail = null, + businessTel = null, + street = null, + houseNumber = null, + postalCode = null, + website = null, + furtherInfo = null, + ) + ) + ), + aHealthChatProfileSectionState(profile = AsyncData.Loading()), + aHealthChatProfileSectionState(profile = AsyncData.Failure(HealthChatApiException.NotFound(null))), + aHealthChatProfileSectionState(profile = AsyncData.Failure(HealthChatApiException.NotApproved(null))), + aHealthChatProfileSectionState( + profile = AsyncData.Failure(HealthChatApiException.Network(RuntimeException("no network"))) + ), + ) +} + +fun aHealthChatProfileSectionState( + profile: AsyncData = AsyncData.Uninitialized, + eventSink: (HealthChatProfileSectionEvents) -> Unit = {}, +) = HealthChatProfileSectionState( + profile = profile, + eventSink = eventSink, +) + +fun aProfileSectionMember( + 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", + street: String? = "Musterstrasse", + houseNumber: String? = "12", + postalCode: String? = "8001", + website: String? = "https://praxis-muster.ch", + furtherInfo: String? = "FMH Allgemeine Innere Medizin", +) = HealthChatMember( + userId = userId, + displayName = displayName, + avatarUrl = null, + title = "Dr. med.", + firstName = "Erika", + lastName = "Muster", + specialty = specialty, + practiceName = practiceName, + businessCity = businessCity, + furtherInfo = furtherInfo, + businessEmail = businessEmail, + businessTel = businessTel, + street = street, + houseNumber = houseNumber, + postalCode = postalCode, + website = website, + isApproved = true, +) diff --git a/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionView.kt b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionView.kt new file mode 100644 index 0000000000..2b85e502c9 --- /dev/null +++ b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionView.kt @@ -0,0 +1,212 @@ +/* + * 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.impl.profile.section + +import androidx.activity.compose.LocalActivity +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.healthchat.api.HealthChatApiException +import io.element.android.features.healthchat.api.HealthChatMember +import io.element.android.features.healthchat.api.HealthChatStrings +import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab +import io.element.android.libraries.androidutils.system.openUrlInExternalApp +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.components.list.ListItemContent +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.IconSource +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.ui.strings.CommonStrings + +/** + * The HealthChat profile fields block rendered on the profile screens. + * + * Error behaviour is inline degradation: the host screen's primary job is Matrix identity, + * so loading renders nothing, a missing directory record ([HealthChatApiException.NotFound], + * e.g. system accounts) renders nothing, an unapproved caller gets a single muted hint, and + * any other failure gets a single retry row. Stale data is never shown. + */ +@Composable +fun HealthChatProfileSectionView( + state: HealthChatProfileSectionState, + modifier: Modifier = Modifier, +) { + when (val profile = state.profile) { + AsyncData.Uninitialized, + is AsyncData.Loading -> Unit + is AsyncData.Failure -> when (profile.error) { + is HealthChatApiException.NotFound -> Unit + is HealthChatApiException.NotApproved -> NotApprovedHint(modifier = modifier) + else -> LoadingErrorRow( + onRetry = { state.eventSink(HealthChatProfileSectionEvents.Retry) }, + modifier = modifier, + ) + } + is AsyncData.Success -> ProfileFieldList( + member = profile.data, + modifier = modifier, + ) + } +} + +@Composable +private fun ProfileFieldList( + member: HealthChatMember, + modifier: Modifier = Modifier, +) { + val activity = LocalActivity.current + val context = LocalContext.current + val darkTheme = ElementTheme.isLightTheme.not() + Column(modifier = modifier.fillMaxWidth()) { + member.specialty?.let { + ProfileFieldRow( + value = it, + labelRes = HealthChatStrings.healthchat_field_specialty, + icon = CompoundIcons.Info(), + ) + } + member.practiceName?.let { + ProfileFieldRow( + value = it, + labelRes = HealthChatStrings.healthchat_field_practice_name, + icon = CompoundIcons.Company(), + ) + } + member.formattedAddress()?.let { + ProfileFieldRow( + value = it, + labelRes = HealthChatStrings.healthchat_field_address, + icon = CompoundIcons.LocationPin(), + ) + } + member.businessEmail?.let { + ProfileFieldRow( + value = it, + labelRes = HealthChatStrings.healthchat_field_business_email, + icon = CompoundIcons.Email(), + onClick = { context.openUrlInExternalApp("mailto:$it") }, + ) + } + member.businessTel?.let { + ProfileFieldRow( + value = it, + labelRes = HealthChatStrings.healthchat_field_business_tel, + icon = CompoundIcons.VoiceCall(), + onClick = { context.openUrlInExternalApp("tel:$it") }, + ) + } + member.website?.let { website -> + ProfileFieldRow( + value = website, + labelRes = HealthChatStrings.healthchat_field_website, + icon = CompoundIcons.WebBrowser(), + onClick = { + val url = website.withHttpScheme() + if (activity != null) { + activity.openUrlInChromeCustomTab(null, darkTheme, url) + } else { + context.openUrlInExternalApp(url) + } + }, + ) + } + member.furtherInfo?.let { + ProfileFieldRow( + value = it, + labelRes = HealthChatStrings.healthchat_field_further_info, + icon = CompoundIcons.Document(), + ) + } + } +} + +@Composable +private fun ProfileFieldRow( + value: String, + labelRes: Int, + icon: ImageVector, + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, +) { + ListItem( + headlineContent = { Text(value) }, + supportingContent = { Text(stringResource(labelRes)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(icon)), + onClick = onClick, + modifier = modifier, + ) +} + +@Composable +private fun NotApprovedHint( + modifier: Modifier = Modifier, +) { + Text( + text = stringResource(HealthChatStrings.healthchat_profile_not_approved_hint), + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + ) +} + +@Composable +private fun LoadingErrorRow( + onRetry: () -> Unit, + modifier: Modifier = Modifier, +) { + ListItem( + headlineContent = { Text(stringResource(HealthChatStrings.healthchat_profile_error_loading)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Error())), + trailingContent = ListItemContent.Custom { + TextButton( + text = stringResource(CommonStrings.action_retry), + onClick = onRetry, + ) + }, + modifier = modifier, + ) +} + +/** + * "Musterstrasse 12\n8001 Zürich" — blank parts are dropped, null when nothing is set. + */ +private fun HealthChatMember.formattedAddress(): String? { + val streetLine = listOfNotNull(street, houseNumber).joinToString(" ") + val cityLine = listOfNotNull(postalCode, businessCity).joinToString(" ") + return listOf(streetLine, cityLine) + .filter { it.isNotBlank() } + .joinToString("\n") + .takeIf { it.isNotBlank() } +} + +private fun String.withHttpScheme(): String { + return if (startsWith("http://") || startsWith("https://")) this else "https://$this" +} + +@PreviewsDayNight +@Composable +internal fun HealthChatProfileSectionViewPreview( + @PreviewParameter(HealthChatProfileSectionStateProvider::class) state: HealthChatProfileSectionState +) = ElementPreview { + HealthChatProfileSectionView(state = state) +} diff --git a/features/healthchat/impl/src/test/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionPresenterTest.kt b/features/healthchat/impl/src/test/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionPresenterTest.kt new file mode 100644 index 0000000000..0fca5457d5 --- /dev/null +++ b/features/healthchat/impl/src/test/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionPresenterTest.kt @@ -0,0 +1,129 @@ +/* + * 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.impl.profile.section + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.healthchat.api.HealthChatApiException +import io.element.android.features.healthchat.api.HealthChatDirectoryService +import io.element.android.features.healthchat.api.HealthChatMember +import io.element.android.features.healthchat.api.HealthChatProfileRepository +import io.element.android.features.healthchat.test.FakeHealthChatDirectoryService +import io.element.android.features.healthchat.test.FakeHealthChatProfileRepository +import io.element.android.features.healthchat.test.aHealthChatMember +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.consumeItemsUntilPredicate +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import java.io.IOException + +class HealthChatProfileSectionPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - own profile is loaded through the repository`() = runTest { + val member = aHealthChatMember() + val refreshLambda = lambdaRecorder> { Result.success(member) } + val repository = FakeHealthChatProfileRepository( + refreshOwnProfileLambda = { refreshLambda() }, + ) + val presenter = createPresenter(isCurrentUser = true, profileRepository = repository) + presenter.test { + val state = consumeItemsUntilPredicate { it.profile is AsyncData.Success }.last() + assertThat(state.profile).isEqualTo(AsyncData.Success(member)) + refreshLambda.assertions().isCalledOnce() + } + } + + @Test + fun `present - another member is loaded through the directory service`() = runTest { + val member = aHealthChatMember(userId = A_USER_ID) + val getMemberLambda = lambdaRecorder> { Result.success(member) } + val directoryService = FakeHealthChatDirectoryService( + getMemberLambda = { getMemberLambda(it) }, + ) + val presenter = createPresenter(isCurrentUser = false, directoryService = directoryService) + presenter.test { + val state = consumeItemsUntilPredicate { it.profile is AsyncData.Success }.last() + assertThat(state.profile).isEqualTo(AsyncData.Success(member)) + getMemberLambda.assertions().isCalledOnce().with(value(A_USER_ID)) + } + } + + @Test + fun `present - a NotFound failure is exposed as such`() = runTest { + val error = HealthChatApiException.NotFound(null) + val directoryService = FakeHealthChatDirectoryService( + getMemberLambda = { Result.failure(error) }, + ) + val presenter = createPresenter(isCurrentUser = false, directoryService = directoryService) + presenter.test { + val state = consumeItemsUntilPredicate { it.profile is AsyncData.Failure }.last() + assertThat((state.profile as AsyncData.Failure).error).isEqualTo(error) + } + } + + @Test + fun `present - a NotApproved failure is exposed as such`() = runTest { + val error = HealthChatApiException.NotApproved("Konto wird geprüft") + val directoryService = FakeHealthChatDirectoryService( + getMemberLambda = { Result.failure(error) }, + ) + val presenter = createPresenter(isCurrentUser = false, directoryService = directoryService) + presenter.test { + val state = consumeItemsUntilPredicate { it.profile is AsyncData.Failure }.last() + assertThat((state.profile as AsyncData.Failure).error).isEqualTo(error) + } + } + + @Test + fun `present - Retry loads the member again after a failure`() = runTest { + val member = aHealthChatMember(userId = A_USER_ID) + var callCount = 0 + val directoryService = FakeHealthChatDirectoryService( + getMemberLambda = { + callCount++ + if (callCount == 1) { + Result.failure(HealthChatApiException.Network(IOException("no network"))) + } else { + Result.success(member) + } + }, + ) + val presenter = createPresenter(isCurrentUser = false, directoryService = directoryService) + presenter.test { + val failedState = consumeItemsUntilPredicate { it.profile is AsyncData.Failure }.last() + failedState.eventSink(HealthChatProfileSectionEvents.Retry) + val state = consumeItemsUntilPredicate { it.profile is AsyncData.Success }.last() + assertThat(state.profile).isEqualTo(AsyncData.Success(member)) + assertThat(callCount).isEqualTo(2) + } + } + + private fun createPresenter( + isCurrentUser: Boolean, + userId: UserId = A_USER_ID, + directoryService: HealthChatDirectoryService = FakeHealthChatDirectoryService(), + profileRepository: HealthChatProfileRepository = FakeHealthChatProfileRepository(), + ): HealthChatProfileSectionPresenter { + return HealthChatProfileSectionPresenter( + userId = userId, + isCurrentUser = isCurrentUser, + directoryService = directoryService, + profileRepository = profileRepository, + ) + } +} diff --git a/features/healthchat/impl/src/test/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionViewTest.kt b/features/healthchat/impl/src/test/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionViewTest.kt new file mode 100644 index 0000000000..68aece4b41 --- /dev/null +++ b/features/healthchat/impl/src/test/kotlin/io/element/android/features/healthchat/impl/profile/section/HealthChatProfileSectionViewTest.kt @@ -0,0 +1,154 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalTestApi::class) + +package io.element.android.features.healthchat.impl.profile.section + +import android.content.Intent +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import com.google.common.truth.Truth.assertThat +import io.element.android.features.healthchat.api.HealthChatApiException +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.robolectric.RobolectricTest +import org.junit.Test +import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows.shadowOf + +class HealthChatProfileSectionViewTest : RobolectricTest() { + @Test + fun `a loaded profile renders its fields with values`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder(expectEvents = false) + setContent { + HealthChatProfileSectionView( + state = aHealthChatProfileSectionState( + profile = AsyncData.Success(aProfileSectionMember()), + eventSink = eventsRecorder, + ), + ) + } + onNodeWithText("Allgemeinmedizin").assertExists() + onNodeWithText("Praxis Muster").assertExists() + onNodeWithText("Musterstrasse 12\n8001 Zürich").assertExists() + onNodeWithText("praxis@muster.ch").assertExists() + onNodeWithText("+41 44 123 45 67").assertExists() + onNodeWithText("https://praxis-muster.ch").assertExists() + onNodeWithText("FMH Allgemeine Innere Medizin").assertExists() + } + + @Test + fun `empty fields are omitted`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder(expectEvents = false) + setContent { + HealthChatProfileSectionView( + state = aHealthChatProfileSectionState( + profile = AsyncData.Success( + aProfileSectionMember( + practiceName = null, + businessEmail = null, + businessTel = null, + street = null, + houseNumber = null, + postalCode = null, + businessCity = null, + website = null, + furtherInfo = null, + ) + ), + eventSink = eventsRecorder, + ), + ) + } + onNodeWithText("Allgemeinmedizin").assertExists() + onNodeWithText("Praxis Muster").assertDoesNotExist() + } + + @Test + fun `clicking the email row opens a mailto intent`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder(expectEvents = false) + setContent { + HealthChatProfileSectionView( + state = aHealthChatProfileSectionState( + profile = AsyncData.Success(aProfileSectionMember()), + eventSink = eventsRecorder, + ), + ) + } + onNodeWithText("praxis@muster.ch").performClick() + val intent = shadowOf(RuntimeEnvironment.getApplication()).nextStartedActivity + assertThat(intent.action).isEqualTo(Intent.ACTION_VIEW) + assertThat(intent.data.toString()).isEqualTo("mailto:praxis@muster.ch") + } + + @Test + fun `clicking the phone row opens a tel intent`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder(expectEvents = false) + setContent { + HealthChatProfileSectionView( + state = aHealthChatProfileSectionState( + profile = AsyncData.Success(aProfileSectionMember()), + eventSink = eventsRecorder, + ), + ) + } + onNodeWithText("+41 44 123 45 67").performClick() + val intent = shadowOf(RuntimeEnvironment.getApplication()).nextStartedActivity + assertThat(intent.dataString).isEqualTo("tel:+41 44 123 45 67") + } + + @Test + fun `a loading state renders nothing`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder(expectEvents = false) + setContent { + HealthChatProfileSectionView( + state = aHealthChatProfileSectionState( + profile = AsyncData.Loading(), + eventSink = eventsRecorder, + ), + ) + } + onNodeWithText("Allgemeinmedizin").assertDoesNotExist() + } + + @Test + fun `a NotFound failure renders nothing`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder(expectEvents = false) + val activity = requireNotNull(activity) + setContent { + HealthChatProfileSectionView( + state = aHealthChatProfileSectionState( + profile = AsyncData.Failure(HealthChatApiException.NotFound(null)), + eventSink = eventsRecorder, + ), + ) + } + onNodeWithText(activity.getString(CommonStrings.action_retry)).assertDoesNotExist() + } + + @Test + fun `an error state shows a retry row which emits Retry`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + setContent { + HealthChatProfileSectionView( + state = aHealthChatProfileSectionState( + profile = AsyncData.Failure(HealthChatApiException.Network(RuntimeException("boom"))), + eventSink = eventsRecorder, + ), + ) + } + clickOn(CommonStrings.action_retry) + eventsRecorder.assertSingle(HealthChatProfileSectionEvents.Retry) + } +} diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index b8aa939015..3620860f22 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -41,6 +41,8 @@ dependencies { api(projects.features.roomdetails.api) implementation(libs.coil.compose) implementation(projects.features.call.api) + implementation(projects.features.healthchat.api) + implementation(projects.features.startchat.api) implementation(projects.features.leaveroom.api) implementation(projects.features.userprofile.shared) implementation(projects.services.analytics.compose) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt index ccf2f28ab5..210348e1fb 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt @@ -19,6 +19,7 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.annotations.ContributesNode +import io.element.android.features.healthchat.api.HealthChatProfileSectionRenderer import io.element.android.features.userprofile.shared.UserProfileNodeHelper import io.element.android.features.userprofile.shared.UserProfileView import io.element.android.libraries.architecture.NodeInputs @@ -38,6 +39,8 @@ class RoomMemberDetailsNode( private val analyticsService: AnalyticsService, private val permalinkBuilder: PermalinkBuilder, presenterFactory: RoomMemberDetailsPresenter.Factory, + // HealthChat fork: renders the extended profile fields. + private val healthChatProfileSectionRenderer: HealthChatProfileSectionRenderer, ) : Node(buildContext, plugins = plugins) { data class RoomMemberDetailsInput( val roomMemberId: UserId, @@ -83,6 +86,9 @@ class RoomMemberDetailsNode( onStartCall = ::onStartCall, openAvatarPreview = callback::navigateToAvatarPreview, onVerifyClick = callback::startVerifyUserFlow, + healthChatProfileSection = { userId, isCurrentUser -> + healthChatProfileSectionRenderer.Render(userId, isCurrentUser, Modifier) + }, ) } } diff --git a/features/userprofile/impl/build.gradle.kts b/features/userprofile/impl/build.gradle.kts index b04ca61a23..8ca67b35b2 100644 --- a/features/userprofile/impl/build.gradle.kts +++ b/features/userprofile/impl/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(projects.libraries.mediaviewer.api) implementation(projects.features.call.api) implementation(projects.features.enterprise.api) + implementation(projects.features.healthchat.api) implementation(projects.features.verifysession.api) api(projects.features.userprofile.api) api(projects.features.userprofile.shared) diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfileNode.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfileNode.kt index 71f95a2e84..36e9f55104 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfileNode.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfileNode.kt @@ -19,6 +19,7 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.annotations.ContributesNode +import io.element.android.features.healthchat.api.HealthChatProfileSectionRenderer import io.element.android.features.userprofile.shared.UserProfileNodeHelper import io.element.android.features.userprofile.shared.UserProfileView import io.element.android.libraries.architecture.NodeInputs @@ -37,6 +38,8 @@ class UserProfileNode( private val analyticsService: AnalyticsService, private val permalinkBuilder: PermalinkBuilder, presenterFactory: UserProfilePresenter.Factory, + // HealthChat fork: renders the extended profile fields. + private val healthChatProfileSectionRenderer: HealthChatProfileSectionRenderer, ) : Node(buildContext, plugins = plugins) { data class UserProfileInputs( val userId: UserId @@ -78,6 +81,9 @@ class UserProfileNode( onStartCall = callback::startCall, openAvatarPreview = callback::navigateToAvatarPreview, onVerifyClick = callback::startVerifyUserFlow, + healthChatProfileSection = { userId, isCurrentUser -> + healthChatProfileSectionRenderer.Render(userId, isCurrentUser, Modifier) + }, ) } } diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt index d2d27638b8..0d2fb15758 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt @@ -56,6 +56,8 @@ fun UserProfileView( openAvatarPreview: (username: String, url: String) -> Unit, onVerifyClick: (UserId) -> Unit, modifier: Modifier = Modifier, + // HealthChat fork: slot for the extended profile fields, rendered by features/healthchat. + healthChatProfileSection: @Composable (userId: UserId, isCurrentUser: Boolean) -> Unit = { _, _ -> }, ) { val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) Scaffold( @@ -93,6 +95,7 @@ fun UserProfileView( onCall = { intent -> state.dmRoomId?.let { onStartCall(it, intent) } } ) Spacer(modifier = Modifier.height(26.dp)) + healthChatProfileSection(state.userId, state.isCurrentUser) if (!state.isCurrentUser) { VerifyUserSection(state, onVerifyClick = { onVerifyClick(state.userId) }) BlockUserSection(state) diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_0_en.png new file mode 100644 index 0000000000..b84c7a624a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba8b7332e2f806bfd622cd2bb944faf783f9708e49538303758b9a4a54792f73 +size 52289 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_1_en.png new file mode 100644 index 0000000000..44fe9f7dd6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88ac59a2bb537aa26b3f27418f92b8f6984892efe925c6f605fd51dd0cbedb3b +size 9982 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_2_en.png new file mode 100644 index 0000000000..bd73e322ab --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 +size 3657 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_3_en.png new file mode 100644 index 0000000000..bd73e322ab --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 +size 3657 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_4_en.png new file mode 100644 index 0000000000..8e69401b02 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df8ae4aefac7f11ed1f628cf5d6355dcc462996ea7d05b86f2dbae8e77c6fecb +size 13756 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_5_en.png new file mode 100644 index 0000000000..895a4add38 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3278228fe2747cb14a524b05e55970b631ff632d582d7431860f10136ff071be +size 14424 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_0_en.png new file mode 100644 index 0000000000..8d387bd44b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c5be695ab84fdc084796b23861191a9fd58a98b2794511bb7e75f7b2081a189 +size 50655 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_1_en.png new file mode 100644 index 0000000000..a8f7628012 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acb8aa0c40957ee14a0763473c435664c5a3698b33e83a1b8ae6fef394d9a046 +size 9648 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_2_en.png new file mode 100644 index 0000000000..bd73e322ab --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 +size 3657 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_3_en.png new file mode 100644 index 0000000000..bd73e322ab --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 +size 3657 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_4_en.png new file mode 100644 index 0000000000..034a55b389 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6f2aeea63619087437678c3fc1e91b64e884496bb6446e902ac92e96575aaf12 +size 13272 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_5_en.png new file mode 100644 index 0000000000..5b342962a1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.profile.section_HealthChatProfileSectionView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d84e69e9b1aac2502a854ad1aaed472a63894c078d1cfa974355370e4a017fa +size 13788