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