From edb3520c56f8958c8d9c8808abc86f4c50f8a615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABl?= Date: Thu, 6 Aug 2026 13:10:01 +0200 Subject: [PATCH] Add member directory tab to home navigation (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(healthchat): add member directory tab to home navigation Third bottom-nav tab backed by the HealthChat API: searchable member cards with native start-chat, approval-pending banner, retry on failure and navigation into the native profile screens. * fix(healthchat): stop tab back handler swallowing back presses While a screen is pushed on top of Home, the still-composed tab BackHandler intercepted back presses during the transition and invisibly switched to the Chats tab. Gate it on the node's target visibility in both the parent and Home's own backstack. * fix(healthchat): satisfy detekt, ktlint and konsist rules Import alias order, composable parameter order in HomeView, and konsist presenter helper shape. * ci: pass the real repository name to the screenshot recorder recordScreenshots.yml set GITHUB_REPOSITORY from `secrets.GITHUB_REPOSITORY`. No such secret exists on this fork, so the expression expands to an empty string and overrides the built-in environment variable of the same name. recordScreenshots.sh checks it up front and exits 1 with "No repo specified", so the workflow could never record anything here. Use the `github.repository` context instead, which is always populated. Claude-Session: https://claude.ai/code/session_01MQbp1EPM4TSXq8P8Pyq44F * Update screenshots * Revert "Update screenshots" This reverts commit b99e0d643f224601db348c181fce8e35a781a44e. * Reapply "Update screenshots" This reverts commit 85bceb6d6796153d45e7a2a7fe1494b8cddc6e32. * test(screenshots): stop a LayoutLib teardown crash failing the shard tests Paparazzi 2.0.0-alpha05 / LayoutLib 16.2.1 races on HandlerThread startup: HandlerThread_Delegate.run calls Process.setThreadPriority, which throws NoSuchMethodError for Thread.setPosixNicenessInternal and kills the "android.bg" thread. If the test thread reaches getLooper() after that, it gets null, BackgroundThread.ensureThreadReady() NPEs, and the static initialiser of LatencyTracker fails. The JVM then marks that class erroneous for the lifetime of the fork, so every later preview showing the soft keyboard dies with NoClassDefFoundError. LayoutLibErrorFilterStatement already swallowed the bare NoSuchMethodError, but PaparazziLogger re-throws collected background errors from teardown() wrapped in a MultipleFailuresException when there is more than one, and that wrapper slipped through. A recent run reported 17 failures with zero image comparisons failing and zero delta images written. Unwrap the wrapper by reflection and swallow it only when every collected cause matches the known-bug list; anything unrecognised, and anything that cannot be decomposed, is rethrown. Also close a hole this filter had from the start. Paparazzi runs the test as `try { body } finally { teardown() }`, so a teardown error discards whatever the body threw — including a genuine snapshot mismatch. renderFailureCapture is registered inside the Paparazzi rule to record the body's exception so it wins over any teardown error, and the filter refuses to swallow anything if that rule is not registered. Two HorizontalFloatingToolbar previews have been mismatching silently behind this, and may now surface as real failures. Claude-Session: https://claude.ai/code/session_01MQbp1EPM4TSXq8P8Pyq44F --------- Co-authored-by: ElementBot --- .github/workflows/recordScreenshots.yml | 14 +- .../android/appnav/LoggedInFlowNode.kt | 5 + .../healthchat/api/HealthChatMemberDisplay.kt | 12 + .../api/HealthChatProfileSectionRenderer.kt | 2 +- .../api/directory/MemberDirectoryEvents.kt | 26 ++ .../api/directory/MemberDirectoryRenderer.kt | 38 +++ .../api/directory/MemberDirectoryState.kt | 30 +++ .../directory/MemberDirectoryStateFixtures.kt | 99 ++++++++ .../main/res/values-de/strings_healthchat.xml | 17 ++ .../main/res/values-fr/strings_healthchat.xml | 18 ++ .../main/res/values-it/strings_healthchat.xml | 18 ++ .../main/res/values/strings_healthchat.xml | 17 ++ features/healthchat/impl/build.gradle.kts | 3 + .../healthchat/impl/HealthChatErrorMessage.kt | 30 +++ .../DefaultMemberDirectoryRenderer.kt | 48 ++++ .../directory/MemberDirectoryPresenter.kt | 93 +++++++ .../directory/MemberDirectoryStateProvider.kt | 42 ++++ .../impl/directory/MemberDirectoryView.kt | 234 ++++++++++++++++++ .../impl/directory/components/MemberCard.kt | 146 +++++++++++ .../components/MemberDirectoryErrorContent.kt | 63 +++++ .../components/MemberDirectoryHeader.kt | 97 ++++++++ .../MemberDirectoryPendingBanner.kt | 44 ++++ .../directory/di/MemberDirectoryModule.kt | 24 ++ .../directory/MemberDirectoryPresenterTest.kt | 182 ++++++++++++++ .../impl/directory/MemberDirectoryViewTest.kt | 159 ++++++++++++ .../features/home/api/HomeEntryPoint.kt | 4 + features/home/impl/build.gradle.kts | 1 + .../features/home/impl/HomeFlowNode.kt | 35 ++- .../home/impl/HomeNavigationBarItem.kt | 7 + .../features/home/impl/HomePresenter.kt | 5 + .../android/features/home/impl/HomeState.kt | 3 + .../features/home/impl/HomeStateProvider.kt | 7 + .../android/features/home/impl/HomeView.kt | 43 +++- .../home/impl/components/HomeTopBar.kt | 2 + .../home/impl/DefaultHomeEntryPointTest.kt | 3 + .../features/home/impl/HomePresenterTest.kt | 17 ++ .../home/impl/HomeViewBackHandlingTest.kt | 78 ++++++ .../ui/LayoutLibErrorFilterStatement.kt | 122 ++++++++- .../src/test/kotlin/ui/PreviewA11yTest.kt | 4 + .../src/test/kotlin/ui/PreviewShard1Test.kt | 4 + .../src/test/kotlin/ui/PreviewShard2Test.kt | 4 + .../src/test/kotlin/ui/PreviewShard3Test.kt | 4 + .../src/test/kotlin/ui/PreviewShard4Test.kt | 4 + ...rectory.components_MemberCard_Day_0_en.png | 3 + ...ctory.components_MemberCard_Night_0_en.png | 3 + ...s_MemberDirectoryErrorContent_Day_0_en.png | 3 + ...MemberDirectoryErrorContent_Night_0_en.png | 3 + ...ponents_MemberDirectoryHeader_Day_0_en.png | 3 + ...nents_MemberDirectoryHeader_Night_0_en.png | 3 + ..._MemberDirectoryPendingBanner_Day_0_en.png | 3 + ...emberDirectoryPendingBanner_Night_0_en.png | 3 + ...directory_MemberDirectoryView_Day_0_en.png | 3 + ...directory_MemberDirectoryView_Day_1_en.png | 3 + ...directory_MemberDirectoryView_Day_2_en.png | 3 + ...directory_MemberDirectoryView_Day_3_en.png | 3 + ...directory_MemberDirectoryView_Day_4_en.png | 3 + ...directory_MemberDirectoryView_Day_5_en.png | 3 + ...rectory_MemberDirectoryView_Night_0_en.png | 3 + ...rectory_MemberDirectoryView_Night_1_en.png | 3 + ...rectory_MemberDirectoryView_Night_2_en.png | 3 + ...rectory_MemberDirectoryView_Night_3_en.png | 3 + ...rectory_MemberDirectoryView_Night_4_en.png | 3 + ...rectory_MemberDirectoryView_Night_5_en.png | 3 + .../features.home.impl_HomeViewA11y_en.png | 4 +- .../features.home.impl_HomeView_Day_0_en.png | 4 +- .../features.home.impl_HomeView_Day_10_en.png | 4 +- .../features.home.impl_HomeView_Day_11_en.png | 4 +- .../features.home.impl_HomeView_Day_13_en.png | 4 +- .../features.home.impl_HomeView_Day_14_en.png | 4 +- .../features.home.impl_HomeView_Day_15_en.png | 4 +- .../features.home.impl_HomeView_Day_16_en.png | 4 +- .../features.home.impl_HomeView_Day_17_en.png | 3 + .../features.home.impl_HomeView_Day_1_en.png | 4 +- .../features.home.impl_HomeView_Day_2_en.png | 4 +- .../features.home.impl_HomeView_Day_3_en.png | 4 +- .../features.home.impl_HomeView_Day_4_en.png | 4 +- .../features.home.impl_HomeView_Day_5_en.png | 4 +- .../features.home.impl_HomeView_Day_9_en.png | 4 +- ...features.home.impl_HomeView_Night_0_en.png | 4 +- ...eatures.home.impl_HomeView_Night_10_en.png | 4 +- ...eatures.home.impl_HomeView_Night_11_en.png | 4 +- ...eatures.home.impl_HomeView_Night_13_en.png | 4 +- ...eatures.home.impl_HomeView_Night_14_en.png | 4 +- ...eatures.home.impl_HomeView_Night_15_en.png | 4 +- ...eatures.home.impl_HomeView_Night_16_en.png | 4 +- ...eatures.home.impl_HomeView_Night_17_en.png | 3 + ...features.home.impl_HomeView_Night_1_en.png | 4 +- ...features.home.impl_HomeView_Night_2_en.png | 4 +- ...features.home.impl_HomeView_Night_3_en.png | 4 +- ...features.home.impl_HomeView_Night_4_en.png | 4 +- ...features.home.impl_HomeView_Night_5_en.png | 4 +- ...features.home.impl_HomeView_Night_9_en.png | 4 +- ...ner_KnockRequestsBannerView_Night_5_en.png | 4 +- ....list_KnockRequestsListView_Night_3_en.png | 4 +- ...screens.qrcode_ShowQrCodeView_Day_0_en.png | 4 +- ....impl.share_ShareLocationView_Day_2_en.png | 4 +- ....impl.share_ShareLocationView_Day_6_en.png | 4 +- ...mpl.share_ShareLocationView_Night_0_en.png | 4 +- ...mpl.share_ShareLocationView_Night_4_en.png | 4 +- ...mpl.share_ShareLocationView_Night_8_en.png | 4 +- ...eeditor_AttachmentImageEditorView_2_en.png | 4 +- ...essagesViewWithIdentityChange_Day_0_en.png | 4 +- ...sagesViewWithIdentityChange_Night_1_en.png | 4 +- ...veVerifiedUserSendFailureView_Day_2_en.png | 4 +- ...event_ATimelineItemEventRow_Night_0_en.png | 4 +- ...lineItemEventRowDisambiguated_Day_0_en.png | 4 +- ...mEventRowWithReplyInformative_Day_0_en.png | 4 +- ...ineItemEventRowWithReplyOther_Day_0_en.png | 4 +- ...TimelineItemEventRowWithReply_Day_0_en.png | 4 +- ...TimelineItemEventRowWithReply_Day_4_en.png | 4 +- ...TimelineItemEventRowWithReply_Day_8_en.png | 4 +- ...melineItemEventRowWithReply_Night_0_en.png | 4 +- ...melineItemEventRowWithReply_Night_4_en.png | 4 +- ...melineItemEventRowWithReply_Night_8_en.png | 4 +- ...es.messages.impl_MessagesView_Day_0_en.png | 4 +- ...es.messages.impl_MessagesView_Day_4_en.png | 4 +- ...es.messages.impl_MessagesView_Day_8_en.png | 4 +- ....messages.impl_MessagesView_Night_0_en.png | 4 +- ....messages.impl_MessagesView_Night_4_en.png | 4 +- ....messages.impl_MessagesView_Night_8_en.png | 4 +- ...blockedusers_BlockedUsersView_Day_4_en.png | 4 +- ...ockedusers_BlockedUsersView_Night_5_en.png | 4 +- ...orizontalFloatingToolbarNoFab_Day_0_en.png | 4 +- 123 files changed, 1968 insertions(+), 138 deletions(-) create mode 100644 features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/directory/MemberDirectoryEvents.kt create mode 100644 features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/directory/MemberDirectoryRenderer.kt create mode 100644 features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/directory/MemberDirectoryState.kt create mode 100644 features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/directory/MemberDirectoryStateFixtures.kt create mode 100644 features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/HealthChatErrorMessage.kt create mode 100644 features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/DefaultMemberDirectoryRenderer.kt create mode 100644 features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/MemberDirectoryPresenter.kt create mode 100644 features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/MemberDirectoryStateProvider.kt create mode 100644 features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/MemberDirectoryView.kt create mode 100644 features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/components/MemberCard.kt create mode 100644 features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/components/MemberDirectoryErrorContent.kt create mode 100644 features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/components/MemberDirectoryHeader.kt create mode 100644 features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/components/MemberDirectoryPendingBanner.kt create mode 100644 features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/di/MemberDirectoryModule.kt create mode 100644 features/healthchat/impl/src/test/kotlin/io/element/android/features/healthchat/impl/directory/MemberDirectoryPresenterTest.kt create mode 100644 features/healthchat/impl/src/test/kotlin/io/element/android/features/healthchat/impl/directory/MemberDirectoryViewTest.kt create mode 100644 features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomeViewBackHandlingTest.kt create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberCard_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberCard_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryErrorContent_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryErrorContent_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryHeader_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryHeader_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryPendingBanner_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryPendingBanner_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_17_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_17_en.png diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml index 260fd9253e..09f72ea707 100644 --- a/.github/workflows/recordScreenshots.yml +++ b/.github/workflows/recordScreenshots.yml @@ -20,8 +20,12 @@ env: # OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps # make two builds likelier to coexist; the real fix is sizing the containers to the # host. See the "Report runner capacity" step. - GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g -Dsonar.gradle.skipCompile=true - CI_GRADLE_ARG_PROPERTIES: --no-configuration-cache + # Kept character-identical to tests.yml so the record path and the verify path cannot drift + # apart again. These only affect the Gradle daemon — Paparazzi renders in forked test workers + # configured entirely by build.gradle.kts — but keeping them aligned removes a whole class of + # "did record and verify run the same way?" questions. + GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseG1GC + CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache --max-workers=4 jobs: record: @@ -82,5 +86,9 @@ jobs: run: ./.github/workflows/scripts/recordScreenshots.sh env: GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN || secrets.GITHUB_TOKEN }} - GITHUB_REPOSITORY: ${{ secrets.GITHUB_REPOSITORY }} + # Must be the `github.repository` context, not a secret. There is no + # GITHUB_REPOSITORY secret on this fork, so `secrets.GITHUB_REPOSITORY` expands to "" + # and overrides the built-in env var of the same name — recordScreenshots.sh then + # exits 1 with "No repo specified" before Gradle ever runs. + GITHUB_REPOSITORY: ${{ github.repository }} GRADLE_ARGS: ${{ env.CI_GRADLE_ARG_PROPERTIES }} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 14230d7c5a..d8109a198a 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -373,6 +373,11 @@ class LoggedInFlowNode( override fun navigateToBugReport() { callback.navigateToBugReport() } + + // HealthChat fork: open a user profile from the member directory tab. + override fun navigateToUserProfile(userId: UserId) { + backstack.push(NavTarget.UserProfile(userId)) + } } homeEntryPoint.createNode( parentNode = this, diff --git a/features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/HealthChatMemberDisplay.kt b/features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/HealthChatMemberDisplay.kt index df1c894e13..02814fe4bd 100644 --- a/features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/HealthChatMemberDisplay.kt +++ b/features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/HealthChatMemberDisplay.kt @@ -8,6 +8,8 @@ package io.element.android.features.healthchat.api +import io.element.android.libraries.matrix.api.user.MatrixUser + /** * Display name fallback chain, mirroring the web client: * displayName → "title firstName lastName" → businessEmail → [fallback]. @@ -21,3 +23,13 @@ fun HealthChatMember.bestDisplayName(fallback: String): String { businessEmail?.takeIf { it.isNotBlank() }?.let { return it } return fallback } + +/** + * The Matrix representation of a directory member, e.g. to start a DM through the app's + * regular flows. + */ +fun HealthChatMember.toMatrixUser() = MatrixUser( + userId = userId, + displayName = displayName, + avatarUrl = avatarUrl, +) 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 index 971c459af4..ceef8b0c66 100644 --- 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 @@ -18,7 +18,7 @@ import io.element.android.libraries.matrix.api.core.UserId * Injected into the upstream profile screens as a slot so the fork's only upstream diff is * one defaulted parameter plus the injection (same pattern as `InvitePeopleRenderer`). */ -interface HealthChatProfileSectionRenderer { +fun interface HealthChatProfileSectionRenderer { @Composable fun Render( userId: UserId, diff --git a/features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/directory/MemberDirectoryEvents.kt b/features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/directory/MemberDirectoryEvents.kt new file mode 100644 index 0000000000..0025442928 --- /dev/null +++ b/features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/directory/MemberDirectoryEvents.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Novaloop AG + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.healthchat.api.directory + +import io.element.android.libraries.matrix.api.user.MatrixUser + +sealed interface MemberDirectoryEvents { + /** + * Fired by the view on first composition. The tab presenter runs from app start (it is + * composed by the Home presenter), so the first fetch is deferred until the tab is shown. + */ + data object LoadIfNeeded : MemberDirectoryEvents + + /** Header refresh and "try again" — one load path. */ + data object Refresh : MemberDirectoryEvents + + data class StartDm(val matrixUser: MatrixUser) : MemberDirectoryEvents + + data object ClearStartDmState : MemberDirectoryEvents +} diff --git a/features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/directory/MemberDirectoryRenderer.kt b/features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/directory/MemberDirectoryRenderer.kt new file mode 100644 index 0000000000..3cbcc3293d --- /dev/null +++ b/features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/directory/MemberDirectoryRenderer.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026 Novaloop AG + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.healthchat.api.directory + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId + +/** + * Renders the member directory tab content. Injected into the Home screen as a slot so the + * fork's upstream diff stays minimal. + * + * Nullable callbacks hide their affordance entirely (e.g. profile editing and inviting only + * become available once the corresponding screens exist). + */ +fun interface MemberDirectoryRenderer { + @Composable + fun Render( + state: MemberDirectoryState, + contentPadding: PaddingValues, + lazyListState: LazyListState, + onOpenDm: (RoomId) -> Unit, + onMemberClick: ((UserId) -> Unit)?, + onMyProfileClick: (() -> Unit)?, + onEditProfileClick: (() -> Unit)?, + onInviteClick: (() -> Unit)?, + modifier: Modifier, + ) +} diff --git a/features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/directory/MemberDirectoryState.kt b/features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/directory/MemberDirectoryState.kt new file mode 100644 index 0000000000..0cfa553669 --- /dev/null +++ b/features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/directory/MemberDirectoryState.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 Novaloop AG + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.healthchat.api.directory + +import androidx.compose.foundation.text.input.TextFieldState +import io.element.android.features.healthchat.api.HealthChatMemberDirectory +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.core.RoomId + +data class MemberDirectoryState( + val searchQuery: TextFieldState, + val directory: AsyncData, + val startDmActionState: AsyncAction, + val eventSink: (MemberDirectoryEvents) -> Unit, +) { + /** Unknown-while-loading counts as approved so no banner flashes during the first load. */ + val isApproved: Boolean + get() = directory.dataOrNull()?.isApproved != false + + /** The verbatim German server explanation, only when the account is not approved. */ + val pendingMessage: String? + get() = directory.dataOrNull()?.takeIf { !it.isApproved }?.pendingMessage +} diff --git a/features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/directory/MemberDirectoryStateFixtures.kt b/features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/directory/MemberDirectoryStateFixtures.kt new file mode 100644 index 0000000000..c826f0a8f5 --- /dev/null +++ b/features/healthchat/api/src/main/kotlin/io/element/android/features/healthchat/api/directory/MemberDirectoryStateFixtures.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2026 Novaloop AG + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.healthchat.api.directory + +import androidx.compose.foundation.text.input.TextFieldState +import io.element.android.features.healthchat.api.HealthChatMember +import io.element.android.features.healthchat.api.HealthChatMemberDirectory +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.collections.immutable.toImmutableList + +/** + * Sample state builders, shared by previews inside the healthchat feature and by the Home + * screen's previews (same pattern as `aDirectLogoutState` in `features/logout/api`). + */ +fun aMemberDirectoryState( + searchQuery: TextFieldState = TextFieldState(), + directory: AsyncData = AsyncData.Success(aMemberDirectory()), + startDmActionState: AsyncAction = AsyncAction.Uninitialized, + eventSink: (MemberDirectoryEvents) -> Unit = {}, +) = MemberDirectoryState( + searchQuery = searchQuery, + directory = directory, + startDmActionState = startDmActionState, + eventSink = eventSink, +) + +fun aMemberDirectory( + members: List = aMemberList(), + totalCount: Int = members.size, + searchQuery: String = "", + isApproved: Boolean = true, + pendingMessage: String? = null, +) = HealthChatMemberDirectory( + members = members.toImmutableList(), + totalCount = totalCount, + searchQuery = searchQuery, + isApproved = isApproved, + pendingMessage = pendingMessage, +) + +fun aMemberList(): List = listOf( + aDirectoryMember(), + aDirectoryMember( + userId = UserId("@luca.bianchi:healthchat.ch"), + displayName = "Dr. med. Luca Bianchi", + specialty = "Kardiologie", + practiceName = "Herzzentrum Bellevue", + businessCity = "Bern", + businessEmail = null, + businessTel = null, + ), + aDirectoryMember( + userId = UserId("@nora.keller:healthchat.ch"), + displayName = null, + specialty = null, + practiceName = null, + businessCity = null, + businessEmail = "nora.keller@example.ch", + businessTel = null, + ), +) + +fun aDirectoryMember( + userId: UserId = UserId("@erika.muster:healthchat.ch"), + displayName: String? = "Dr. med. Erika Muster", + specialty: String? = "Allgemeinmedizin", + practiceName: String? = "Praxis Muster", + businessCity: String? = "Zürich", + businessEmail: String? = "praxis@muster.ch", + businessTel: String? = "+41 44 123 45 67", + isApproved: Boolean = true, +) = HealthChatMember( + userId = userId, + displayName = displayName, + avatarUrl = null, + title = null, + firstName = null, + lastName = null, + specialty = specialty, + practiceName = practiceName, + businessCity = businessCity, + furtherInfo = null, + businessEmail = businessEmail, + businessTel = businessTel, + street = null, + houseNumber = null, + postalCode = null, + website = null, + isApproved = isApproved, +) 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 3e6e8159cb..ec6ffe282b 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 @@ -20,4 +20,21 @@ Weitere Infos / Qualifikationen Die HealthChat-Profildaten konnten nicht geladen werden. Profildetails sind sichtbar, sobald Ihr Konto freigegeben ist. + Mitglieder + + %1$d Mitglied + %1$d Mitglieder + + Mein Profil + Suche nach Name, Praxis, Ort oder Fachgebiet + Chat starten + Fehler beim Erstellen des Chats + Es sind noch keine Mitglieder vorhanden. + Keine Mitglieder entsprechen Ihrer Suche. + Unbekannt + Konto wird geprüft + Ihr Konto wird derzeit geprüft. Nach der Freigabe können Sie alle Mitglieder sehen und durchsuchen. + Aktualisieren + Profil bearbeiten + Neues Mitglied einladen 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 55ea83ac9c..49df77d036 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 @@ -20,4 +20,22 @@ 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é. + Membres + + %1$d membre + %1$d membres + %1$d membres + + Mon profil + Rechercher par nom, cabinet, ville ou spécialité + Démarrer une discussion + Impossible de démarrer la discussion + Il n’y a pas encore de membres. + Aucun membre ne correspond à votre recherche. + Inconnu + Compte en cours de vérification + Votre compte est en cours de vérification. Une fois approuvé, vous pourrez voir et rechercher tous les membres. + Actualiser + Modifier le profil + Inviter un nouveau membre 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 31ffbf5810..20af35a02b 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 @@ -20,4 +20,22 @@ Ulteriori informazioni / qualifiche Impossibile caricare i dettagli del profilo HealthChat. I dettagli del profilo saranno visibili non appena il tuo account sarà stato approvato. + Membri + + %1$d membro + %1$d membri + %1$d membri + + Il mio profilo + Cerca per nome, studio, città o specializzazione + Avvia chat + Impossibile avviare la chat + Non ci sono ancora membri. + Nessun membro corrisponde alla tua ricerca. + Sconosciuto + Account in fase di verifica + Il tuo account è attualmente in fase di verifica. Una volta approvato potrai vedere e cercare tutti i membri. + Aggiorna + Modifica profilo + Invita un nuovo membro 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 7de9716380..f17fbea476 100644 --- a/features/healthchat/api/src/main/res/values/strings_healthchat.xml +++ b/features/healthchat/api/src/main/res/values/strings_healthchat.xml @@ -21,4 +21,21 @@ Further info / qualifications The HealthChat profile details couldn’t be loaded. Profile details become visible once your account has been approved. + Members + + %1$d member + %1$d members + + My profile + Search by name, practice, city or specialty + Start chat + Failed to start the chat + There are no members yet. + No members match your search. + Unknown + Account under review + Your account is currently under review. Once it has been approved you can see and search all members. + Refresh + Edit profile + Invite a new member diff --git a/features/healthchat/impl/build.gradle.kts b/features/healthchat/impl/build.gradle.kts index cea0f71299..7042791b93 100644 --- a/features/healthchat/impl/build.gradle.kts +++ b/features/healthchat/impl/build.gradle.kts @@ -44,6 +44,7 @@ setupDependencyInjection() dependencies { api(projects.features.healthchat.api) implementation(libs.coroutines.core) + implementation(projects.features.startchat.api) implementation(platform(libs.network.okhttp.bom)) implementation(libs.network.okhttp.okhttp) implementation(libs.network.okhttp.logging) @@ -56,6 +57,7 @@ dependencies { implementation(projects.libraries.core) implementation(projects.libraries.designsystem) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) implementation(projects.libraries.network) implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.uiStrings) @@ -63,6 +65,7 @@ dependencies { testCommonDependencies(libs, true) testImplementation(libs.network.mockwebserver) testImplementation(projects.features.healthchat.test) + testImplementation(projects.features.startchat.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.preferences.api) testImplementation(projects.libraries.preferences.test) diff --git a/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/HealthChatErrorMessage.kt b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/HealthChatErrorMessage.kt new file mode 100644 index 0000000000..10a6a6980f --- /dev/null +++ b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/HealthChatErrorMessage.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 Novaloop AG + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.healthchat.impl + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import io.element.android.features.healthchat.api.HealthChatApiException +import io.element.android.features.healthchat.api.HealthChatStrings + +/** + * User-facing message for a HealthChat failure: the verbatim German server message where one + * exists, localized fallbacks for local/unknown failures. + */ +@Composable +fun Throwable.healthChatUserMessage(): String { + return when (this) { + is HealthChatApiException.Network -> stringResource(HealthChatStrings.healthchat_error_network) + is HealthChatApiException.Unauthorized -> serverMessage + is HealthChatApiException.NotApproved -> serverMessage + is HealthChatApiException.NotFound -> serverMessage + is HealthChatApiException.Http -> serverMessage + else -> null + } ?: stringResource(HealthChatStrings.healthchat_error_unknown) +} diff --git a/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/DefaultMemberDirectoryRenderer.kt b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/DefaultMemberDirectoryRenderer.kt new file mode 100644 index 0000000000..492401dd88 --- /dev/null +++ b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/DefaultMemberDirectoryRenderer.kt @@ -0,0 +1,48 @@ +/* + * 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.directory + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import dev.zacsweers.metro.ContributesBinding +import io.element.android.features.healthchat.api.directory.MemberDirectoryRenderer +import io.element.android.features.healthchat.api.directory.MemberDirectoryState +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId + +@ContributesBinding(SessionScope::class) +class DefaultMemberDirectoryRenderer : MemberDirectoryRenderer { + @Composable + override fun Render( + state: MemberDirectoryState, + contentPadding: PaddingValues, + lazyListState: LazyListState, + onOpenDm: (RoomId) -> Unit, + onMemberClick: ((UserId) -> Unit)?, + onMyProfileClick: (() -> Unit)?, + onEditProfileClick: (() -> Unit)?, + onInviteClick: (() -> Unit)?, + modifier: Modifier, + ) { + MemberDirectoryView( + state = state, + contentPadding = contentPadding, + lazyListState = lazyListState, + onOpenDm = onOpenDm, + onMemberClick = onMemberClick, + onMyProfileClick = onMyProfileClick, + onEditProfileClick = onEditProfileClick, + onInviteClick = onInviteClick, + modifier = modifier, + ) + } +} diff --git a/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/MemberDirectoryPresenter.kt b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/MemberDirectoryPresenter.kt new file mode 100644 index 0000000000..031aea6c21 --- /dev/null +++ b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/MemberDirectoryPresenter.kt @@ -0,0 +1,93 @@ +/* + * 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.directory + +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject +import io.element.android.features.healthchat.api.HealthChatDirectoryService +import io.element.android.features.healthchat.api.HealthChatMemberDirectory +import io.element.android.features.healthchat.api.directory.MemberDirectoryEvents +import io.element.android.features.healthchat.api.directory.MemberDirectoryState +import io.element.android.features.startchat.api.StartDMAction +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.milliseconds + +@Inject +class MemberDirectoryPresenter( + private val directoryService: HealthChatDirectoryService, + private val startDMAction: StartDMAction, +) : Presenter { + @Composable + override fun present(): MemberDirectoryState { + val coroutineScope = rememberCoroutineScope() + val searchQuery = rememberTextFieldState() + // This presenter is composed from app start (by the Home presenter), so the first + // fetch is deferred until the tab is actually shown (LoadIfNeeded from the view). + var hasBeenRequested by rememberSaveable { mutableStateOf(false) } + var loadTrigger by rememberSaveable { mutableIntStateOf(0) } + var directory by remember { mutableStateOf>(AsyncData.Uninitialized) } + val startDmActionState = remember { mutableStateOf>(AsyncAction.Uninitialized) } + + LaunchedEffect(hasBeenRequested, searchQuery.text, loadTrigger) { + if (!hasBeenRequested) return@LaunchedEffect + if (searchQuery.text.isNotEmpty()) { + // Effect restart on typing = debounce. + delay(300.milliseconds) + } + directory = AsyncData.Loading(directory.dataOrNull()) + directoryService.searchMembers(searchQuery.text.toString()).fold( + onSuccess = { directory = AsyncData.Success(it) }, + // No stale fallback: a failed (re)load always shows the error state. + onFailure = { directory = AsyncData.Failure(it) }, + ) + } + + fun handleEvents(event: MemberDirectoryEvents) { + when (event) { + MemberDirectoryEvents.LoadIfNeeded -> { + hasBeenRequested = true + } + MemberDirectoryEvents.Refresh -> { + loadTrigger++ + } + is MemberDirectoryEvents.StartDm -> coroutineScope.launch { + startDMAction.execute( + matrixUser = event.matrixUser, + createIfDmDoesNotExist = startDmActionState.value is AsyncAction.Confirming, + actionState = startDmActionState, + ) + } + MemberDirectoryEvents.ClearStartDmState -> { + startDmActionState.value = AsyncAction.Uninitialized + } + } + } + + return MemberDirectoryState( + searchQuery = searchQuery, + directory = directory, + startDmActionState = startDmActionState.value, + eventSink = ::handleEvents, + ) + } +} diff --git a/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/MemberDirectoryStateProvider.kt b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/MemberDirectoryStateProvider.kt new file mode 100644 index 0000000000..f59e175d2d --- /dev/null +++ b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/MemberDirectoryStateProvider.kt @@ -0,0 +1,42 @@ +/* + * 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.directory + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.healthchat.api.HealthChatApiException +import io.element.android.features.healthchat.api.directory.MemberDirectoryState +import io.element.android.features.healthchat.api.directory.aMemberDirectory +import io.element.android.features.healthchat.api.directory.aMemberDirectoryState +import io.element.android.libraries.architecture.AsyncData + +open class MemberDirectoryStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aMemberDirectoryState(), + aMemberDirectoryState(directory = AsyncData.Uninitialized), + aMemberDirectoryState(directory = AsyncData.Loading(aMemberDirectory())), + aMemberDirectoryState( + directory = AsyncData.Failure( + HealthChatApiException.Http(500, "Fehler beim Laden der Benutzer") + ), + ), + aMemberDirectoryState( + directory = AsyncData.Success( + aMemberDirectory( + members = emptyList(), + isApproved = false, + pendingMessage = "Ihr Konto wird derzeit geprüft. Nach der Freigabe können Sie alle Mitglieder sehen und durchsuchen.", + ) + ), + ), + aMemberDirectoryState( + directory = AsyncData.Success(aMemberDirectory(members = emptyList())), + ), + ) +} diff --git a/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/MemberDirectoryView.kt b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/MemberDirectoryView.kt new file mode 100644 index 0000000000..4af5dc9b99 --- /dev/null +++ b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/MemberDirectoryView.kt @@ -0,0 +1,234 @@ +/* + * 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.directory + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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.features.healthchat.api.HealthChatStrings +import io.element.android.features.healthchat.api.directory.MemberDirectoryEvents +import io.element.android.features.healthchat.api.directory.MemberDirectoryState +import io.element.android.features.healthchat.api.toMatrixUser +import io.element.android.features.healthchat.impl.directory.components.MemberCard +import io.element.android.features.healthchat.impl.directory.components.MemberDirectoryErrorContent +import io.element.android.features.healthchat.impl.directory.components.MemberDirectoryHeader +import io.element.android.features.healthchat.impl.directory.components.MemberDirectoryPendingBanner +import io.element.android.features.healthchat.impl.healthChatUserMessage +import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.components.async.AsyncActionView +import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator +import io.element.android.libraries.designsystem.theme.components.SearchField +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.ui.components.CreateDmConfirmationBottomSheet +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun MemberDirectoryView( + state: MemberDirectoryState, + contentPadding: PaddingValues, + lazyListState: LazyListState, + onOpenDm: (RoomId) -> Unit, + onMemberClick: ((UserId) -> Unit)?, + onMyProfileClick: (() -> Unit)?, + onEditProfileClick: (() -> Unit)?, + onInviteClick: (() -> Unit)?, + modifier: Modifier = Modifier, +) { + LaunchedEffect(Unit) { + state.eventSink(MemberDirectoryEvents.LoadIfNeeded) + } + Box(modifier = modifier.fillMaxSize()) { + when (val directory = state.directory) { + AsyncData.Uninitialized -> FullScreenLoading() + is AsyncData.Loading -> if (directory.prevData == null) { + FullScreenLoading() + } else { + MemberDirectoryContent( + state = state, + contentPadding = contentPadding, + lazyListState = lazyListState, + isRefreshing = true, + onMemberClick = onMemberClick, + onMyProfileClick = onMyProfileClick, + onEditProfileClick = onEditProfileClick, + onInviteClick = onInviteClick, + ) + } + is AsyncData.Failure -> MemberDirectoryErrorContent( + message = directory.error.healthChatUserMessage(), + onRetry = { state.eventSink(MemberDirectoryEvents.Refresh) }, + ) + is AsyncData.Success -> MemberDirectoryContent( + state = state, + contentPadding = contentPadding, + lazyListState = lazyListState, + isRefreshing = false, + onMemberClick = onMemberClick, + onMyProfileClick = onMyProfileClick, + onEditProfileClick = onEditProfileClick, + onInviteClick = onInviteClick, + ) + } + AsyncActionView( + async = state.startDmActionState, + progressDialog = { + AsyncActionViewDefaults.ProgressDialog( + progressText = stringResource(CommonStrings.common_starting_chat), + ) + }, + onSuccess = onOpenDm, + errorMessage = { stringResource(HealthChatStrings.healthchat_screen_member_directory_error_starting_chat) }, + onRetry = null, + onErrorDismiss = { state.eventSink(MemberDirectoryEvents.ClearStartDmState) }, + confirmationDialog = { data -> + if (data is ConfirmingStartDmWithMatrixUser) { + CreateDmConfirmationBottomSheet( + matrixUser = data.matrixUser, + isUserIdentityUnknown = data.isUserIdentityUnknown, + onSendInvite = { + state.eventSink(MemberDirectoryEvents.StartDm(data.matrixUser)) + }, + onDismiss = { + state.eventSink(MemberDirectoryEvents.ClearStartDmState) + }, + ) + } + }, + ) + } +} + +@Composable +private fun MemberDirectoryContent( + state: MemberDirectoryState, + contentPadding: PaddingValues, + lazyListState: LazyListState, + isRefreshing: Boolean, + onMemberClick: ((UserId) -> Unit)?, + onMyProfileClick: (() -> Unit)?, + onEditProfileClick: (() -> Unit)?, + onInviteClick: (() -> Unit)?, + modifier: Modifier = Modifier, +) { + val directory = state.directory.dataOrNull() ?: return + Column(modifier = modifier.fillMaxSize()) { + if (isRefreshing) { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + LazyColumn( + state = lazyListState, + contentPadding = contentPadding, + modifier = Modifier.fillMaxSize(), + ) { + item(key = "header") { + MemberDirectoryHeader( + memberCount = directory.totalCount, + onMyProfileClick = onMyProfileClick, + onEditProfileClick = onEditProfileClick, + onInviteClick = if (state.isApproved) onInviteClick else null, + onRefreshClick = { state.eventSink(MemberDirectoryEvents.Refresh) }, + ) + } + if (!state.isApproved) { + item(key = "pending") { + MemberDirectoryPendingBanner( + message = state.pendingMessage + ?: stringResource(HealthChatStrings.healthchat_screen_member_directory_pending_message), + ) + } + } else { + item(key = "search") { + SearchField( + state = state.searchQuery, + placeholder = stringResource(HealthChatStrings.healthchat_screen_member_directory_search_placeholder), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + ) + } + } + if (directory.members.isEmpty()) { + item(key = "empty") { + val message = if (state.searchQuery.text.isNotEmpty()) { + stringResource(HealthChatStrings.healthchat_screen_member_directory_empty_search) + } else { + stringResource(HealthChatStrings.healthchat_screen_member_directory_empty) + } + Text( + text = message, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + modifier = Modifier + .fillMaxWidth() + .padding(32.dp), + ) + } + } + items(directory.members, key = { it.userId.value }) { member -> + MemberCard( + member = member, + onClick = onMemberClick?.let { callback -> { callback(member.userId) } }, + onStartChatClick = { state.eventSink(MemberDirectoryEvents.StartDm(member.toMatrixUser())) }, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), + ) + } + } + } +} + +@Composable +private fun FullScreenLoading( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } +} + +@PreviewsDayNight +@Composable +internal fun MemberDirectoryViewPreview( + @PreviewParameter(MemberDirectoryStateProvider::class) state: MemberDirectoryState +) = ElementPreview { + MemberDirectoryView( + state = state, + contentPadding = PaddingValues(), + lazyListState = rememberLazyListState(), + onOpenDm = {}, + onMemberClick = {}, + onMyProfileClick = {}, + onEditProfileClick = null, + onInviteClick = null, + ) +} diff --git a/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/components/MemberCard.kt b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/components/MemberCard.kt new file mode 100644 index 0000000000..14bddb5298 --- /dev/null +++ b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/components/MemberCard.kt @@ -0,0 +1,146 @@ +/* + * 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.directory.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +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.HealthChatMember +import io.element.android.features.healthchat.api.HealthChatStrings +import io.element.android.features.healthchat.api.bestDisplayName +import io.element.android.features.healthchat.api.directory.aDirectoryMember +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.ButtonSize +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.OutlinedButton +import io.element.android.libraries.designsystem.theme.components.Surface +import io.element.android.libraries.designsystem.theme.components.Text + +@Composable +fun MemberCard( + member: HealthChatMember, + onClick: (() -> Unit)?, + onStartChatClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Surface( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .let { base -> onClick?.let { base.clickable(onClick = it) } ?: base }, + color = ElementTheme.colors.bgSubtleSecondary, + shape = RoundedCornerShape(12.dp), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + val displayName = member.bestDisplayName( + fallback = stringResource(HealthChatStrings.healthchat_screen_member_directory_unknown_name), + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Avatar( + avatarData = AvatarData( + id = member.userId.value, + name = displayName, + url = member.avatarUrl, + size = AvatarSize.UserPreference, + ), + avatarType = AvatarType.User, + ) + Column(modifier = Modifier.padding(start = 12.dp)) { + Text( + text = displayName, + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textPrimary, + ) + member.specialty?.let { + Text( + text = it, + style = ElementTheme.typography.fontBodySmMedium, + color = ElementTheme.colors.textActionAccent, + ) + } + } + } + Spacer(modifier = Modifier.height(4.dp)) + member.practiceName?.let { MemberCardDetailRow(icon = CompoundIcons.Company(), text = it) } + member.businessCity?.let { MemberCardDetailRow(icon = CompoundIcons.LocationPin(), text = it) } + member.businessEmail?.let { MemberCardDetailRow(icon = CompoundIcons.Email(), text = it) } + member.businessTel?.let { MemberCardDetailRow(icon = CompoundIcons.VoiceCall(), text = it) } + Text( + text = member.userId.value, + style = ElementTheme.typography.fontBodyXsRegular, + color = ElementTheme.colors.textSecondary, + ) + Spacer(modifier = Modifier.height(4.dp)) + OutlinedButton( + text = stringResource(HealthChatStrings.healthchat_screen_member_directory_start_chat), + onClick = onStartChatClick, + size = ButtonSize.Small, + ) + } + } +} + +@Composable +private fun MemberCardDetailRow( + icon: ImageVector, + text: String, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = ElementTheme.colors.iconSecondary, + modifier = Modifier.size(16.dp), + ) + Text( + text = text, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textPrimary, + modifier = Modifier.padding(start = 8.dp), + ) + } +} + +@PreviewsDayNight +@Composable +internal fun MemberCardPreview() = ElementPreview { + MemberCard( + member = aDirectoryMember(), + onClick = {}, + onStartChatClick = {}, + ) +} diff --git a/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/components/MemberDirectoryErrorContent.kt b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/components/MemberDirectoryErrorContent.kt new file mode 100644 index 0000000000..0e6e70392d --- /dev/null +++ b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/components/MemberDirectoryErrorContent.kt @@ -0,0 +1,63 @@ +/* + * 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.directory.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun MemberDirectoryErrorContent( + message: String, + onRetry: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), + ) { + BigIcon(style = BigIcon.Style.AlertSolid) + Text( + text = message, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textPrimary, + textAlign = TextAlign.Center, + ) + Button( + text = stringResource(CommonStrings.action_retry), + onClick = onRetry, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun MemberDirectoryErrorContentPreview() = ElementPreview { + MemberDirectoryErrorContent( + message = "Fehler beim Laden der Benutzer", + onRetry = {}, + ) +} diff --git a/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/components/MemberDirectoryHeader.kt b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/components/MemberDirectoryHeader.kt new file mode 100644 index 0000000000..cf43b4d996 --- /dev/null +++ b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/components/MemberDirectoryHeader.kt @@ -0,0 +1,97 @@ +/* + * 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.directory.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +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.HealthChatStrings +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.features.healthchat.api.R as HealthChatR + +@Composable +fun MemberDirectoryHeader( + memberCount: Int, + onMyProfileClick: (() -> Unit)?, + onEditProfileClick: (() -> Unit)?, + onInviteClick: (() -> Unit)?, + onRefreshClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = pluralStringResource( + HealthChatR.plurals.healthchat_screen_member_directory_member_count, + memberCount, + memberCount, + ), + style = ElementTheme.typography.fontBodyMdMedium, + color = ElementTheme.colors.textSecondary, + modifier = Modifier.weight(1f), + ) + onMyProfileClick?.let { + TextButton( + text = stringResource(HealthChatStrings.healthchat_screen_member_directory_my_profile), + onClick = it, + ) + } + onEditProfileClick?.let { + IconButton(onClick = it) { + Icon( + imageVector = CompoundIcons.Edit(), + contentDescription = stringResource(HealthChatStrings.healthchat_a11y_edit_profile), + ) + } + } + onInviteClick?.let { + IconButton(onClick = it) { + Icon( + imageVector = CompoundIcons.UserAdd(), + contentDescription = stringResource(HealthChatStrings.healthchat_a11y_invite_member), + ) + } + } + IconButton(onClick = onRefreshClick) { + Icon( + imageVector = CompoundIcons.Restart(), + contentDescription = stringResource(HealthChatStrings.healthchat_a11y_refresh), + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun MemberDirectoryHeaderPreview() = ElementPreview { + MemberDirectoryHeader( + memberCount = 42, + onMyProfileClick = {}, + onEditProfileClick = null, + onInviteClick = null, + onRefreshClick = {}, + ) +} diff --git a/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/components/MemberDirectoryPendingBanner.kt b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/components/MemberDirectoryPendingBanner.kt new file mode 100644 index 0000000000..acd462da39 --- /dev/null +++ b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/components/MemberDirectoryPendingBanner.kt @@ -0,0 +1,44 @@ +/* + * 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.directory.components + +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.res.stringResource +import androidx.compose.ui.unit.dp +import io.element.android.features.healthchat.api.HealthChatStrings +import io.element.android.libraries.designsystem.components.Announcement +import io.element.android.libraries.designsystem.components.AnnouncementType +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight + +@Composable +fun MemberDirectoryPendingBanner( + message: String, + modifier: Modifier = Modifier, +) { + Announcement( + title = stringResource(HealthChatStrings.healthchat_screen_member_directory_pending_title), + description = message, + type = AnnouncementType.Informative(), + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + ) +} + +@PreviewsDayNight +@Composable +internal fun MemberDirectoryPendingBannerPreview() = ElementPreview { + MemberDirectoryPendingBanner( + message = "Ihr Konto wird derzeit geprüft. Nach der Freigabe können Sie alle Mitglieder sehen und durchsuchen.", + ) +} diff --git a/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/di/MemberDirectoryModule.kt b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/di/MemberDirectoryModule.kt new file mode 100644 index 0000000000..a3d004f052 --- /dev/null +++ b/features/healthchat/impl/src/main/kotlin/io/element/android/features/healthchat/impl/directory/di/MemberDirectoryModule.kt @@ -0,0 +1,24 @@ +/* + * 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.directory.di + +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo +import io.element.android.features.healthchat.api.directory.MemberDirectoryState +import io.element.android.features.healthchat.impl.directory.MemberDirectoryPresenter +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.SessionScope + +@BindingContainer +@ContributesTo(SessionScope::class) +interface MemberDirectoryModule { + @Binds + fun bindMemberDirectoryPresenter(presenter: MemberDirectoryPresenter): Presenter +} diff --git a/features/healthchat/impl/src/test/kotlin/io/element/android/features/healthchat/impl/directory/MemberDirectoryPresenterTest.kt b/features/healthchat/impl/src/test/kotlin/io/element/android/features/healthchat/impl/directory/MemberDirectoryPresenterTest.kt new file mode 100644 index 0000000000..aae6fa932c --- /dev/null +++ b/features/healthchat/impl/src/test/kotlin/io/element/android/features/healthchat/impl/directory/MemberDirectoryPresenterTest.kt @@ -0,0 +1,182 @@ +/* + * 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.directory + +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +import androidx.compose.runtime.MutableState +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.HealthChatMemberDirectory +import io.element.android.features.healthchat.api.directory.MemberDirectoryEvents +import io.element.android.features.healthchat.test.FakeHealthChatDirectoryService +import io.element.android.features.healthchat.test.aHealthChatMember +import io.element.android.features.invitepeople.test.FakeStartDMAction +import io.element.android.features.startchat.api.StartDMAction +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.test.A_ROOM_ID +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.test +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import java.io.IOException + +class MemberDirectoryPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - nothing is fetched before LoadIfNeeded`() = runTest { + val searchLambda = lambdaRecorder> { + Result.success(aDirectory()) + } + val presenter = createPresenter( + directoryService = FakeHealthChatDirectoryService(searchMembersLambda = { searchLambda(it) }), + ) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.directory).isEqualTo(AsyncData.Uninitialized) + searchLambda.assertions().isNeverCalled() + initialState.eventSink(MemberDirectoryEvents.LoadIfNeeded) + val loadedState = consumeItemsUntilPredicate { it.directory is AsyncData.Success }.last() + assertThat(loadedState.directory.dataOrNull()).isEqualTo(aDirectory()) + searchLambda.assertions().isCalledOnce() + } + } + + @Test + fun `present - typing a search query triggers a debounced server side search`() = runTest { + val queries = mutableListOf() + val presenter = createPresenter( + directoryService = FakeHealthChatDirectoryService( + searchMembersLambda = { query -> + queries.add(query) + Result.success(aDirectory(searchQuery = query)) + }, + ), + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(MemberDirectoryEvents.LoadIfNeeded) + val loadedState = consumeItemsUntilPredicate { it.directory is AsyncData.Success }.last() + // Typing restarts the effect: intermediate values must be debounced away. + loadedState.searchQuery.setTextAndPlaceCursorAtEnd("M") + loadedState.searchQuery.setTextAndPlaceCursorAtEnd("Mü") + loadedState.searchQuery.setTextAndPlaceCursorAtEnd("Müller") + consumeItemsUntilPredicate { it.directory.dataOrNull()?.searchQuery == "Müller" } + assertThat(queries).containsExactly("", "Müller").inOrder() + } + } + + @Test + fun `present - a failure is exposed and Refresh recovers`() = runTest { + var callCount = 0 + val presenter = createPresenter( + directoryService = FakeHealthChatDirectoryService( + searchMembersLambda = { + callCount++ + if (callCount == 1) { + Result.failure(HealthChatApiException.Network(IOException("no network"))) + } else { + Result.success(aDirectory()) + } + }, + ), + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(MemberDirectoryEvents.LoadIfNeeded) + val failedState = consumeItemsUntilPredicate { it.directory is AsyncData.Failure }.last() + // No stale fallback. + assertThat((failedState.directory as AsyncData.Failure).prevData).isNull() + failedState.eventSink(MemberDirectoryEvents.Refresh) + val loadedState = consumeItemsUntilPredicate { it.directory is AsyncData.Success }.last() + assertThat(loadedState.directory.dataOrNull()).isEqualTo(aDirectory()) + } + } + + @Test + fun `present - an unapproved account exposes the pending message and disables search`() = runTest { + val presenter = createPresenter( + directoryService = FakeHealthChatDirectoryService( + searchMembersLambda = { + Result.success( + aDirectory( + members = persistentListOf(), + isApproved = false, + pendingMessage = "Ihr Konto wird derzeit geprüft.", + ) + ) + }, + ), + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(MemberDirectoryEvents.LoadIfNeeded) + val state = consumeItemsUntilPredicate { it.directory is AsyncData.Success }.last() + assertThat(state.isApproved).isFalse() + assertThat(state.pendingMessage).isEqualTo("Ihr Konto wird derzeit geprüft.") + } + } + + @Test + fun `present - StartDm delegates to the StartDMAction with the given user`() = runTest { + val executeLambda = lambdaRecorder { matrixUser: MatrixUser, createIfDmDoesNotExist: Boolean, actionState: MutableState> -> + assertThat(matrixUser.userId).isEqualTo(A_USER_ID) + assertThat(createIfDmDoesNotExist).isFalse() + actionState.value = AsyncAction.Success(A_ROOM_ID) + } + val presenter = createPresenter( + directoryService = FakeHealthChatDirectoryService(searchMembersLambda = { Result.success(aDirectory()) }), + startDMAction = FakeStartDMAction(executeResult = { user, create, actionState -> + executeLambda(user, create, actionState) + }), + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(MemberDirectoryEvents.StartDm(MatrixUser(A_USER_ID))) + val state = consumeItemsUntilPredicate { it.startDmActionState is AsyncAction.Success }.last() + assertThat((state.startDmActionState as AsyncAction.Success).data).isEqualTo(A_ROOM_ID) + executeLambda.assertions().isCalledOnce() + state.eventSink(MemberDirectoryEvents.ClearStartDmState) + val clearedState = consumeItemsUntilPredicate { it.startDmActionState is AsyncAction.Uninitialized }.last() + assertThat(clearedState.startDmActionState).isEqualTo(AsyncAction.Uninitialized) + } + } + + private fun aDirectory( + members: List = listOf(aHealthChatMember()), + searchQuery: String = "", + isApproved: Boolean = true, + pendingMessage: String? = null, + ) = HealthChatMemberDirectory( + members = persistentListOf(*members.toTypedArray()), + totalCount = members.size, + searchQuery = searchQuery, + isApproved = isApproved, + pendingMessage = pendingMessage, + ) + + private fun createPresenter( + directoryService: HealthChatDirectoryService = FakeHealthChatDirectoryService(), + startDMAction: StartDMAction = FakeStartDMAction(), + ) = MemberDirectoryPresenter( + directoryService = directoryService, + startDMAction = startDMAction, + ) +} diff --git a/features/healthchat/impl/src/test/kotlin/io/element/android/features/healthchat/impl/directory/MemberDirectoryViewTest.kt b/features/healthchat/impl/src/test/kotlin/io/element/android/features/healthchat/impl/directory/MemberDirectoryViewTest.kt new file mode 100644 index 0000000000..89d88c57d3 --- /dev/null +++ b/features/healthchat/impl/src/test/kotlin/io/element/android/features/healthchat/impl/directory/MemberDirectoryViewTest.kt @@ -0,0 +1,159 @@ +/* + * 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.directory + +import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.rememberLazyListState +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 io.element.android.features.healthchat.api.HealthChatApiException +import io.element.android.features.healthchat.api.directory.MemberDirectoryEvents +import io.element.android.features.healthchat.api.directory.MemberDirectoryState +import io.element.android.features.healthchat.api.directory.aDirectoryMember +import io.element.android.features.healthchat.api.directory.aMemberDirectory +import io.element.android.features.healthchat.api.directory.aMemberDirectoryState +import io.element.android.features.healthchat.api.toMatrixUser +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalledWithParam +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnceWithParam +import io.element.android.tests.testutils.robolectric.RobolectricTest +import org.junit.Test + +class MemberDirectoryViewTest : RobolectricTest() { + @Test + fun `on composition LoadIfNeeded is emitted`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + setMemberDirectoryView( + state = aMemberDirectoryState( + directory = AsyncData.Uninitialized, + eventSink = eventsRecorder, + ), + ) + eventsRecorder.assertSingle(MemberDirectoryEvents.LoadIfNeeded) + } + + @Test + fun `clicking retry on the error state emits Refresh`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + setMemberDirectoryView( + state = aMemberDirectoryState( + directory = AsyncData.Failure(HealthChatApiException.Http(500, "Fehler beim Laden der Benutzer")), + eventSink = eventsRecorder, + ), + ) + // The verbatim German server message is shown. + onNodeWithText("Fehler beim Laden der Benutzer").assertExists() + clickOn(CommonStrings.action_retry) + eventsRecorder.assertList( + listOf( + MemberDirectoryEvents.LoadIfNeeded, + MemberDirectoryEvents.Refresh, + ) + ) + } + + @Test + fun `clicking start chat emits StartDm with the member as MatrixUser`() = runAndroidComposeUiTest { + val member = aDirectoryMember() + val eventsRecorder = EventsRecorder() + setMemberDirectoryView( + state = aMemberDirectoryState( + directory = AsyncData.Success(aMemberDirectory(members = listOf(member))), + eventSink = eventsRecorder, + ), + ) + val activity = requireNotNull(activity) + onNodeWithText(activity.getString(io.element.android.features.healthchat.api.R.string.healthchat_screen_member_directory_start_chat)) + .performClick() + eventsRecorder.assertList( + listOf( + MemberDirectoryEvents.LoadIfNeeded, + MemberDirectoryEvents.StartDm(member.toMatrixUser()), + ) + ) + } + + @Test + fun `clicking a member card opens the profile`() = runAndroidComposeUiTest { + val member = aDirectoryMember() + val eventsRecorder = EventsRecorder(expectEvents = true) + ensureCalledOnceWithParam(member.userId) { callback -> + setMemberDirectoryView( + state = aMemberDirectoryState( + directory = AsyncData.Success(aMemberDirectory(members = listOf(member))), + eventSink = eventsRecorder, + ), + onMemberClick = callback, + ) + onNodeWithText("Dr. med. Erika Muster").performClick() + } + } + + @Test + fun `an unapproved account shows the pending banner and no search field`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder(expectEvents = true) + setMemberDirectoryView( + state = aMemberDirectoryState( + directory = AsyncData.Success( + aMemberDirectory( + members = emptyList(), + isApproved = false, + pendingMessage = "Ihr Konto wird derzeit geprüft.", + ) + ), + eventSink = eventsRecorder, + ), + ) + onNodeWithText("Ihr Konto wird derzeit geprüft.").assertExists() + val activity = requireNotNull(activity) + onNodeWithText(activity.getString(io.element.android.features.healthchat.api.R.string.healthchat_screen_member_directory_search_placeholder)) + .assertDoesNotExist() + } + + @Test + fun `hidden affordances are not rendered when their callbacks are null`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder(expectEvents = true) + setMemberDirectoryView( + state = aMemberDirectoryState(eventSink = eventsRecorder), + onMyProfileClick = null, + ) + val activity = requireNotNull(activity) + onNodeWithText(activity.getString(io.element.android.features.healthchat.api.R.string.healthchat_screen_member_directory_my_profile)) + .assertDoesNotExist() + } +} + +@OptIn(ExperimentalTestApi::class) +private fun androidx.compose.ui.test.AndroidComposeUiTest.setMemberDirectoryView( + state: MemberDirectoryState, + onMemberClick: ((UserId) -> Unit)? = EnsureNeverCalledWithParam(), + onMyProfileClick: (() -> Unit)? = {}, +) { + setContent { + MemberDirectoryView( + state = state, + contentPadding = PaddingValues(), + lazyListState = rememberLazyListState(), + onOpenDm = {}, + onMemberClick = onMemberClick, + onMyProfileClick = onMyProfileClick, + onEditProfileClick = null, + onInviteClick = null, + ) + } +} diff --git a/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt b/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt index 1a90245611..0886b186ec 100644 --- a/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt +++ b/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt @@ -13,6 +13,7 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.JoinedRoom interface HomeEntryPoint : FeatureEntryPoint { @@ -31,5 +32,8 @@ interface HomeEntryPoint : FeatureEntryPoint { fun navigateToEnterRecoveryKey() fun navigateToRoomSettings(roomId: RoomId) fun navigateToBugReport() + + // HealthChat fork: open a user profile from the member directory tab. + fun navigateToUserProfile(userId: UserId) } } diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts index efdc545e61..44448fc6ad 100644 --- a/features/home/impl/build.gradle.kts +++ b/features/home/impl/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(projects.libraries.push.api) implementation(projects.libraries.sessionStorage.api) implementation(projects.features.announcement.api) + implementation(projects.features.healthchat.api) implementation(projects.features.invite.api) implementation(projects.features.networkmonitor.api) implementation(projects.features.logout.api) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt index 00f285e3f4..5336e08d70 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt @@ -12,6 +12,7 @@ import android.app.Activity import android.os.Parcelable import androidx.activity.compose.LocalActivity import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -22,6 +23,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.LocalNodeTargetVisibility import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.node import com.bumble.appyx.core.plugin.Plugin @@ -32,6 +34,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.directory.MemberDirectoryRenderer import io.element.android.features.home.api.HomeEntryPoint import io.element.android.features.home.impl.components.RoomListMenuAction import io.element.android.features.home.impl.model.RoomListRoomSummary @@ -87,6 +90,8 @@ class HomeFlowNode( private val declineInviteAndBlockUserEntryPoint: DeclineInviteAndBlockEntryPoint, private val changeRoomMemberRolesEntryPoint: ChangeRoomMemberRolesEntryPoint, private val leaveRoomRenderer: LeaveRoomRenderer, + // HealthChat fork: renders the member directory tab. + private val memberDirectoryRenderer: MemberDirectoryRenderer, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, ) : BaseFlowNode( backstack = BackStack( @@ -99,6 +104,10 @@ class HomeFlowNode( private val callback: HomeEntryPoint.Callback = callback() private val stateFlow = launchMolecule { presenter.present() } + // HealthChat fork: whether this node is the visible target within the parent backstack. + // False as soon as another screen (e.g. a member profile) starts being pushed on top. + private val isVisibleNavTarget = mutableStateOf(true) + override fun onBuilt() { super.onBuilt() lifecycle.subscribe( @@ -244,7 +253,24 @@ class HomeFlowNode( onSelectNewOwners = ::navigateToSelectNewOwnersWhenLeavingRoom, modifier = Modifier ) - } + }, + memberDirectoryView = { contentPadding, lazyListState -> + memberDirectoryRenderer.Render( + state = state.memberDirectoryState, + contentPadding = contentPadding, + lazyListState = lazyListState, + onOpenDm = ::navigateToRoom, + onMemberClick = { callback.navigateToUserProfile(it) }, + onMyProfileClick = { callback.navigateToUserProfile(matrixClient.sessionId) }, + // Hidden until the native editor and invite screens exist. + onEditProfileClick = null, + onInviteClick = null, + modifier = Modifier, + ) + }, + // Both levels: Home within the parent backstack (captured in View) and the + // root element within Home's own backstack. + isBackHandlingEnabled = isVisibleNavTarget.value && LocalNodeTargetVisibility.current, ) directLogoutView.Render(state.directLogoutState) } @@ -252,6 +278,13 @@ class HomeFlowNode( @Composable override fun View(modifier: Modifier) { + // HealthChat fork: capture the target visibility at this level, where the + // CompositionLocal still refers to Home within the PARENT backstack (inside + // rootNode it is re-provided for Home's own backstack). + val isVisibleTarget = LocalNodeTargetVisibility.current + SideEffect { + isVisibleNavTarget.value = isVisibleTarget + } BackstackView() } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeNavigationBarItem.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeNavigationBarItem.kt index 035e953b23..8ca102eec0 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeNavigationBarItem.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeNavigationBarItem.kt @@ -11,6 +11,7 @@ package io.element.android.features.home.impl import androidx.annotation.StringRes import androidx.compose.runtime.Composable import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.healthchat.api.HealthChatStrings enum class HomeNavigationBarItem( @StringRes @@ -21,6 +22,11 @@ enum class HomeNavigationBarItem( ), Spaces( labelRes = R.string.screen_home_tab_spaces + ), + + // HealthChat fork: member directory tab. Keep as LAST entry so persisted ordinals stay stable. + Members( + labelRes = HealthChatStrings.healthchat_screen_home_tab_members ); @Composable @@ -29,6 +35,7 @@ enum class HomeNavigationBarItem( ) = when (this) { Chats -> if (isSelected) CompoundIcons.ChatSolid() else CompoundIcons.Chat() Spaces -> if (isSelected) CompoundIcons.SpaceSolid() else CompoundIcons.Space() + Members -> if (isSelected) CompoundIcons.UserProfileSolid() else CompoundIcons.UserProfile() } companion object { diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt index 1e49b1aa70..ece767e711 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt @@ -19,6 +19,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject +import io.element.android.features.healthchat.api.directory.MemberDirectoryState import io.element.android.features.home.impl.roomlist.RoomListState import io.element.android.features.home.impl.spaces.HomeSpacesState import io.element.android.features.logout.api.direct.DirectLogoutState @@ -42,6 +43,8 @@ class HomePresenter( private val indicatorService: IndicatorService, private val roomListPresenter: Presenter, private val homeSpacesPresenter: Presenter, + // HealthChat fork: member directory tab presenter. + private val memberDirectoryPresenter: Presenter, private val logoutPresenter: Presenter, private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, private val sessionStore: SessionStore, @@ -63,6 +66,7 @@ class HomePresenter( val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false) val roomListState = roomListPresenter.present() val homeSpacesState = homeSpacesPresenter.present() + val memberDirectoryState = memberDirectoryPresenter.present() var currentHomeNavigationBarItemOrdinal by rememberSaveable { mutableIntStateOf(HomeNavigationBarItem.Chats.ordinal) } val currentHomeNavigationBarItem by remember { derivedStateOf { @@ -96,6 +100,7 @@ class HomePresenter( currentHomeNavigationBarItem = currentHomeNavigationBarItem, roomListState = roomListState, homeSpacesState = homeSpacesState, + memberDirectoryState = memberDirectoryState, snackbarMessage = snackbarMessage, canReportBug = canReportBug, directLogoutState = directLogoutState, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt index ae59ef8eb9..5852f6d969 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt @@ -8,6 +8,7 @@ package io.element.android.features.home.impl +import io.element.android.features.healthchat.api.directory.MemberDirectoryState import io.element.android.features.home.impl.roomlist.RoomListState import io.element.android.features.home.impl.spacefilters.SpaceFiltersState import io.element.android.features.home.impl.spaces.HomeSpacesState @@ -27,6 +28,8 @@ data class HomeState( val currentHomeNavigationBarItem: HomeNavigationBarItem, val roomListState: RoomListState, val homeSpacesState: HomeSpacesState, + // HealthChat fork: state of the member directory tab. + val memberDirectoryState: MemberDirectoryState, val snackbarMessage: SnackbarMessage?, val canReportBug: Boolean, val directLogoutState: DirectLogoutState, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt index ea9ff7a234..19c1f6e185 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt @@ -9,6 +9,8 @@ package io.element.android.features.home.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.healthchat.api.directory.MemberDirectoryState +import io.element.android.features.healthchat.api.directory.aMemberDirectoryState import io.element.android.features.home.impl.roomlist.RoomListState import io.element.android.features.home.impl.roomlist.RoomListStateProvider import io.element.android.features.home.impl.roomlist.aRoomListState @@ -50,6 +52,9 @@ open class HomeStateProvider : PreviewParameterProvider { homeSpacesState = aHomeSpacesState( spaceRooms = emptyList(), ), + // HealthChat fork: appended LAST so upstream snapshot indices stay stable. + ) + aHomeState( + currentHomeNavigationBarItem = HomeNavigationBarItem.Members, ) } @@ -62,6 +67,7 @@ internal fun aHomeState( currentHomeNavigationBarItem: HomeNavigationBarItem = HomeNavigationBarItem.Chats, roomListState: RoomListState = aRoomListState(), homeSpacesState: HomeSpacesState = aHomeSpacesState(), + memberDirectoryState: MemberDirectoryState = aMemberDirectoryState(), canReportBug: Boolean = true, directLogoutState: DirectLogoutState = aDirectLogoutState(), eventSink: (HomeEvent) -> Unit = {} @@ -75,5 +81,6 @@ internal fun aHomeState( currentHomeNavigationBarItem = currentHomeNavigationBarItem, roomListState = roomListState, homeSpacesState = homeSpacesState, + memberDirectoryState = memberDirectoryState, eventSink = eventSink, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt index 6956ba6ba5..a78cbf6f58 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -86,6 +87,11 @@ fun HomeView( onDeclineInviteAndBlockUser: (roomSummary: RoomListRoomSummary) -> Unit, acceptDeclineInviteView: @Composable () -> Unit, modifier: Modifier = Modifier, + // HealthChat fork: false while another screen is pushed on top of Home, so the + // tab-switching BackHandler cannot swallow back presses during the transition. + isBackHandlingEnabled: Boolean = true, + // HealthChat fork: content of the member directory tab, rendered by features/healthchat. + memberDirectoryView: @Composable (PaddingValues, LazyListState) -> Unit = { _, _ -> }, leaveRoomView: @Composable () -> Unit, ) { val state: RoomListState = homeState.roomListState @@ -121,6 +127,8 @@ fun HomeView( onStartChatClick = { if (firstThrottler.canHandle()) onStartChatClick() }, onCreateSpaceClick = { if (firstThrottler.canHandle()) onCreateSpaceClick() }, onMenuActionClick = onMenuActionClick, + memberDirectoryView = memberDirectoryView, + isBackHandlingEnabled = isBackHandlingEnabled, ) // This overlaid view will only be visible when state.displaySearchResults is true RoomListSearchView( @@ -147,7 +155,9 @@ private fun HomeScaffold( onStartChatClick: () -> Unit, onCreateSpaceClick: () -> Unit, onMenuActionClick: (RoomListMenuAction) -> Unit, + isBackHandlingEnabled: Boolean, modifier: Modifier = Modifier, + memberDirectoryView: @Composable (PaddingValues, LazyListState) -> Unit, ) { fun onRoomClick(room: RoomListRoomSummary) { onRoomClick(room.roomId) @@ -158,7 +168,8 @@ private fun HomeScaffold( val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) val roomListState: RoomListState = state.roomListState - BackHandler(enabled = state.isBackHandlerEnabled) { + // HealthChat fork: also gated on isBackHandlingEnabled (see HomeView docs). + BackHandler(enabled = state.isBackHandlerEnabled && isBackHandlingEnabled) { if (state.currentHomeNavigationBarItem != HomeNavigationBarItem.Chats) { state.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Chats)) } else { @@ -172,6 +183,7 @@ private fun HomeScaffold( val hazeState = rememberHazeState() val roomsLazyListState = rememberLazyListState() val spacesLazyListState = rememberLazyListState() + val membersLazyListState = rememberLazyListState() Scaffold( modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), @@ -208,6 +220,7 @@ private fun HomeScaffold( val lazyListStateTarget = when (item) { HomeNavigationBarItem.Chats -> roomsLazyListState HomeNavigationBarItem.Spaces -> spacesLazyListState + HomeNavigationBarItem.Members -> membersLazyListState } coroutineScope.launch { if (lazyListStateTarget.firstVisibleItemIndex > 10) { @@ -221,15 +234,15 @@ private fun HomeScaffold( state.eventSink(HomeEvent.SelectHomeNavigationBarItem(item)) } }, - floatingActionButton = { - when (state.currentHomeNavigationBarItem) { - HomeNavigationBarItem.Chats -> { - HomeFloatingActionButton(onStartChatClick, CommonStrings.action_create_room) - } - HomeNavigationBarItem.Spaces -> { - HomeFloatingActionButton(onCreateSpaceClick, CommonStrings.action_create_space) - } + floatingActionButton = when (state.currentHomeNavigationBarItem) { + HomeNavigationBarItem.Chats -> { + { HomeFloatingActionButton(onStartChatClick, CommonStrings.action_create_room) } } + HomeNavigationBarItem.Spaces -> { + { HomeFloatingActionButton(onCreateSpaceClick, CommonStrings.action_create_space) } + } + // HealthChat fork: the member directory has no create action. + HomeNavigationBarItem.Members -> null }, ) }, @@ -285,6 +298,18 @@ private fun HomeScaffold( onExploreClick = {}, ) } + // HealthChat fork: member directory tab content. + HomeNavigationBarItem.Members -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .consumeWindowInsets(padding) + .hazeSource(state = hazeState), + ) { + memberDirectoryView(contentPadding, membersLazyListState) + } + } } }, snackbarHost = { SnackbarHost(snackbarHostState) }, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt index a563520e09..d29d23e603 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt @@ -116,6 +116,8 @@ fun HomeTopBar( } } HomeNavigationBarItem.Spaces -> stringResource(selectedNavigationItem.labelRes) + // HealthChat fork: member directory tab title. + HomeNavigationBarItem.Members -> stringResource(selectedNavigationItem.labelRes) } Text( modifier = Modifier.semantics { diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt index 8eee8e1ee2..d58bd7acb9 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt @@ -12,6 +12,7 @@ import com.bumble.appyx.core.modality.BuildContext import com.google.common.truth.Truth.assertThat import io.element.android.features.home.api.HomeEntryPoint import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.services.analytics.test.FakeAnalyticsService @@ -39,6 +40,7 @@ class DefaultHomeEntryPointTest : RobolectricTest() { declineInviteAndBlockUserEntryPoint = { _, _, _ -> lambdaError() }, changeRoomMemberRolesEntryPoint = { _, _, _, _ -> lambdaError() }, leaveRoomRenderer = { _, _, _ -> lambdaError() }, + memberDirectoryRenderer = { _, _, _, _, _, _, _, _, _ -> lambdaError() }, sessionCoroutineScope = backgroundScope, ) } @@ -51,6 +53,7 @@ class DefaultHomeEntryPointTest : RobolectricTest() { override fun navigateToEnterRecoveryKey() = lambdaError() override fun navigateToRoomSettings(roomId: RoomId) = lambdaError() override fun navigateToBugReport() = lambdaError() + override fun navigateToUserProfile(userId: UserId) = lambdaError() } val result = entryPoint.createNode( parentNode = parentNode, diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt index 371a718523..2f2ec442b0 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt @@ -9,6 +9,7 @@ package io.element.android.features.home.impl import com.google.common.truth.Truth.assertThat +import io.element.android.features.healthchat.api.directory.aMemberDirectoryState import io.element.android.features.home.impl.roomlist.aRoomListState import io.element.android.features.home.impl.spaces.HomeSpacesState import io.element.android.features.home.impl.spaces.aHomeSpacesState @@ -145,6 +146,21 @@ class HomePresenterTest { assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces) } } + + @Test + fun `present - NavigationBar change to Members`() = runTest { + val presenter = createHomePresenter( + sessionStore = InMemorySessionStore( + updateUserProfileResult = { _, _, _ -> }, + ), + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Members)) + val finalState = awaitItem() + assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Members) + } + } } internal fun createHomePresenter( @@ -162,6 +178,7 @@ internal fun createHomePresenter( indicatorService = indicatorService, roomListPresenter = { aRoomListState() }, homeSpacesPresenter = homeSpacesPresenter, + memberDirectoryPresenter = { aMemberDirectoryState() }, logoutPresenter = { aDirectLogoutState() }, rageshakeFeatureAvailability = rageshakeFeatureAvailability, sessionStore = sessionStore, diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomeViewBackHandlingTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomeViewBackHandlingTest.kt new file mode 100644 index 0000000000..f4f45856f0 --- /dev/null +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomeViewBackHandlingTest.kt @@ -0,0 +1,78 @@ +/* + * 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.home.impl + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.pressBackKey +import io.element.android.tests.testutils.robolectric.RobolectricTest +import org.junit.Test + +class HomeViewBackHandlingTest : RobolectricTest() { + @Test + fun `back press on the members tab switches to the chats tab`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + setContent { + HomeView( + homeState = aHomeState( + currentHomeNavigationBarItem = HomeNavigationBarItem.Members, + eventSink = eventsRecorder, + ), + onRoomClick = {}, + onSettingsClick = {}, + onSetUpRecoveryClick = {}, + onConfirmRecoveryKeyClick = {}, + onStartChatClick = {}, + onCreateSpaceClick = {}, + onRoomSettingsClick = {}, + onMenuActionClick = {}, + onReportRoomClick = {}, + onDeclineInviteAndBlockUser = {}, + acceptDeclineInviteView = {}, + leaveRoomView = {}, + ) + } + pressBackKey() + eventsRecorder.assertSingle(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Chats)) + } + + @Test + fun `back press is not intercepted while home is not the visible nav target`() = runAndroidComposeUiTest { + // Regression test: while another screen (e.g. a member profile) is being pushed on + // top of Home, the tab BackHandler must not swallow the back press by invisibly + // switching tabs — the back press has to reach the parent backstack pop instead. + val eventsRecorder = EventsRecorder(expectEvents = false) + setContent { + HomeView( + homeState = aHomeState( + currentHomeNavigationBarItem = HomeNavigationBarItem.Members, + eventSink = eventsRecorder, + ), + onRoomClick = {}, + onSettingsClick = {}, + onSetUpRecoveryClick = {}, + onConfirmRecoveryKeyClick = {}, + onStartChatClick = {}, + onCreateSpaceClick = {}, + onRoomSettingsClick = {}, + onMenuActionClick = {}, + onReportRoomClick = {}, + onDeclineInviteAndBlockUser = {}, + acceptDeclineInviteView = {}, + leaveRoomView = {}, + isBackHandlingEnabled = false, + ) + } + pressBackKey() + } +} diff --git a/tests/uitests/src/test/kotlin/ui/LayoutLibErrorFilterStatement.kt b/tests/uitests/src/test/kotlin/ui/LayoutLibErrorFilterStatement.kt index 9f91f30816..5c8b711385 100644 --- a/tests/uitests/src/test/kotlin/ui/LayoutLibErrorFilterStatement.kt +++ b/tests/uitests/src/test/kotlin/ui/LayoutLibErrorFilterStatement.kt @@ -17,24 +17,130 @@ import org.junit.runners.model.Statement * Tracked in https://github.com/cashapp/paparazzi/issues/2342 — fixed in layoutlib 16.2.3. * Remove this workaround once Paparazzi ships with layoutlib >= 16.2.3. * - * Paparazzi's PaparazziLogger collects background thread errors and re-throws them via - * assertNoErrors(). This Statement wrapper catches the known NoSuchMethodError so the test - * can pass despite the LayoutLib bug. + * The bug has two faces, and which one a run gets is a thread race: + * + * 1. `HandlerThread_Delegate.run` calls `Looper.prepare()` and then dies on + * `Process.setThreadPriority`. If the caller reaches `HandlerThread.getLooper()` while the + * thread is still alive it gets the looper and nothing else happens. + * 2. If the thread has already died, `getLooper()` returns null, `BackgroundThread + * .ensureThreadReady()` NPEs, and the static initialiser of `LatencyTracker` fails. The JVM + * then marks that class erroneous forever, so every later preview in the same fork that shows + * the soft keyboard fails with `NoClassDefFoundError`. + * + * Paparazzi's PaparazziLogger collects these background errors and re-throws them from + * `teardown()` — as the error itself when there is one, or wrapped in a `MultipleFailuresException` + * when there are several. This rule swallows them, and only them. + * + * IMPORTANT: Paparazzi runs the test body as `try { body } finally { teardown() }`, so when + * teardown throws it *discards* whatever the body threw — including a real snapshot mismatch. + * [renderFailureCapture] must therefore be registered as an inner rule so the body's exception + * survives and wins. Without it, this rule could silently turn a genuine golden mismatch green. */ class LayoutLibErrorFilterStatement : TestRule { + private var captureRuleRan = false + private var thrownByTestBody: Throwable? = null + + /** + * Must be registered with `@get:Rule(order = 2)` so it is applied INSIDE the Paparazzi rule. + * JUnit 4.13.2 applies the *lowest* `order` outermost, so 0 = this filter, 1 = Paparazzi, + * 2 = the capture below. + */ + val renderFailureCapture: TestRule = TestRule { base, _ -> + object : Statement() { + @Throws(Throwable::class) + override fun evaluate() { + captureRuleRan = true + try { + base.evaluate() + } catch (error: Throwable) { + thrownByTestBody = error + throw error + } + } + } + } + override fun apply(base: Statement, description: Description): Statement { return object : Statement() { @Throws(Throwable::class) override fun evaluate() { try { base.evaluate() - } catch (e: NoSuchMethodError) { - if (e.message?.contains("setPosixNicenessInternal") != true) throw e - } catch (npe: NullPointerException) { - // Also catch this error on `HorizontalFloatingToolbarPreview` and `HorizontalFloatingToolbarNoFabPreview` - if (npe.message?.contains("""Cannot invoke "java.util.ArrayList.size()" because "childrenList" is null""") != true) throw npe + } catch (error: Throwable) { + val fromBody = thrownByTestBody + // A failure raised by the test body itself always wins over a teardown error. + if (fromBody != null && !isKnownLayoutLibBug(fromBody)) throw fromBody + if (!captureRuleRan) { + throw IllegalStateException( + "renderFailureCapture is not registered with @get:Rule(order = 2) on ${description.className}, " + + "so a snapshot mismatch could be hidden by a LayoutLib teardown error. Refusing to filter.", + error, + ) + } + if (!isKnownLayoutLibBug(error)) throw error } } } } } + +private const val MULTIPLE_FAILURES_EXCEPTION = "app.cash.paparazzi.internal.PaparazziLogger\$MultipleFailuresException" + +private val knownLayoutLibBugMarkers = listOf( + // The root bug: LayoutLib calls a JVM-internal method that does not exist. + "setPosixNicenessInternal", + // Its fallout: the poisoned static initialiser and the dead "android.bg" HandlerThread. + "com.android.internal.util.LatencyTracker", + "com.android.internal.os.BackgroundThread", + """Cannot invoke "android.os.Looper.setTraceTag(long)" because "looper" is null""", + // Seen on HorizontalFloatingToolbarPreview and HorizontalFloatingToolbarNoFabPreview. + """Cannot invoke "java.util.ArrayList.size()" because "childrenList" is null""", +) + +/** + * True only when *every* error carried by [error] is a known LayoutLib bug. Anything we cannot + * decompose, and anything with an unrecognised error in it, is reported as a failure. + */ +private fun isKnownLayoutLibBug(error: Throwable): Boolean { + val reports = describeEach(error) ?: return false + return reports.isNotEmpty() && reports.all { report -> knownLayoutLibBugMarkers.any { report.contains(it) } } +} + +/** + * One text report per collected error, or null when a `MultipleFailuresException` cannot be + * unpacked. It is `internal`, and it neither exposes its causes nor prints them via + * `stackTraceToString()`, so reflection is the only way to inspect them individually. + */ +private fun describeEach(error: Throwable): List? { + if (error.javaClass.name != MULTIPLE_FAILURES_EXCEPTION) { + return listOf(error.describe()) + } + val causes = readCauses(error) ?: return null + val reports = mutableListOf() + for (cause in causes) { + val nested = describeEach(cause) ?: return null + reports += nested + } + return reports +} + +@Suppress("UNCHECKED_CAST") +private fun readCauses(error: Throwable): List? { + return try { + val field = error.javaClass.getDeclaredField("causes") + field.isAccessible = true + field.get(error) as? List + } catch (failure: Exception) { + null + } +} + +private fun Throwable.describe(): String { + // `takeWhile { seen.add(it) }` also terminates a self-referential cause chain. + val seen = mutableSetOf() + return generateSequence(this) { it.cause } + .takeWhile { seen.add(it) } + .joinToString(separator = "\n") { error -> + error.stackTrace.joinToString(separator = "\n", prefix = "$error\n") { frame -> "\tat $frame" } + } +} diff --git a/tests/uitests/src/test/kotlin/ui/PreviewA11yTest.kt b/tests/uitests/src/test/kotlin/ui/PreviewA11yTest.kt index f420110f31..9afae1a0dc 100644 --- a/tests/uitests/src/test/kotlin/ui/PreviewA11yTest.kt +++ b/tests/uitests/src/test/kotlin/ui/PreviewA11yTest.kt @@ -39,6 +39,10 @@ class PreviewA11yTest( renderExtensions = setOf(AccessibilityRenderExtension()), ) + // Applied inside the Paparazzi rule so a snapshot mismatch is not discarded by teardown(). + @get:Rule(order = 2) + val renderFailureCapture = layoutLibErrorFilterStatement.renderFailureCapture + @Test fun snapshot() { ScreenshotTest.runTest(paparazzi = paparazziRule, preview = preview, localeStr = "en") diff --git a/tests/uitests/src/test/kotlin/ui/PreviewShard1Test.kt b/tests/uitests/src/test/kotlin/ui/PreviewShard1Test.kt index 17632c212d..35257fa329 100644 --- a/tests/uitests/src/test/kotlin/ui/PreviewShard1Test.kt +++ b/tests/uitests/src/test/kotlin/ui/PreviewShard1Test.kt @@ -34,6 +34,10 @@ class PreviewShard1Test( @get:Rule(order = 1) val paparazziRule = PaparazziPreviewRule.createFor(preview, locale = "en") + // Applied inside the Paparazzi rule so a snapshot mismatch is not discarded by teardown(). + @get:Rule(order = 2) + val renderFailureCapture = layoutLibErrorFilterStatement.renderFailureCapture + @Test fun snapshot() { ScreenshotTest.runTest(paparazzi = paparazziRule, preview = preview, localeStr = "en") diff --git a/tests/uitests/src/test/kotlin/ui/PreviewShard2Test.kt b/tests/uitests/src/test/kotlin/ui/PreviewShard2Test.kt index cec2b028ac..c6dfc498d9 100644 --- a/tests/uitests/src/test/kotlin/ui/PreviewShard2Test.kt +++ b/tests/uitests/src/test/kotlin/ui/PreviewShard2Test.kt @@ -34,6 +34,10 @@ class PreviewShard2Test( @get:Rule(order = 1) val paparazziRule = PaparazziPreviewRule.createFor(preview, locale = "en") + // Applied inside the Paparazzi rule so a snapshot mismatch is not discarded by teardown(). + @get:Rule(order = 2) + val renderFailureCapture = layoutLibErrorFilterStatement.renderFailureCapture + @Test fun snapshot() { ScreenshotTest.runTest(paparazzi = paparazziRule, preview = preview, localeStr = "en") diff --git a/tests/uitests/src/test/kotlin/ui/PreviewShard3Test.kt b/tests/uitests/src/test/kotlin/ui/PreviewShard3Test.kt index c752d0b321..c53364c64e 100644 --- a/tests/uitests/src/test/kotlin/ui/PreviewShard3Test.kt +++ b/tests/uitests/src/test/kotlin/ui/PreviewShard3Test.kt @@ -34,6 +34,10 @@ class PreviewShard3Test( @get:Rule(order = 1) val paparazziRule = PaparazziPreviewRule.createFor(preview, locale = "en") + // Applied inside the Paparazzi rule so a snapshot mismatch is not discarded by teardown(). + @get:Rule(order = 2) + val renderFailureCapture = layoutLibErrorFilterStatement.renderFailureCapture + @Test fun snapshot() { ScreenshotTest.runTest(paparazzi = paparazziRule, preview = preview, localeStr = "en") diff --git a/tests/uitests/src/test/kotlin/ui/PreviewShard4Test.kt b/tests/uitests/src/test/kotlin/ui/PreviewShard4Test.kt index 55881de079..fe5b6d041e 100644 --- a/tests/uitests/src/test/kotlin/ui/PreviewShard4Test.kt +++ b/tests/uitests/src/test/kotlin/ui/PreviewShard4Test.kt @@ -34,6 +34,10 @@ class PreviewShard4Test( @get:Rule(order = 1) val paparazziRule = PaparazziPreviewRule.createFor(preview, locale = "en") + // Applied inside the Paparazzi rule so a snapshot mismatch is not discarded by teardown(). + @get:Rule(order = 2) + val renderFailureCapture = layoutLibErrorFilterStatement.renderFailureCapture + @Test fun snapshot() { ScreenshotTest.runTest(paparazzi = paparazziRule, preview = preview, localeStr = "en") diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberCard_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberCard_Day_0_en.png new file mode 100644 index 0000000000..32f58ea975 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberCard_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f6389635e4bf31d05b63fffea64bf4e031a0772f08d63ea99a2f093afec7577 +size 28208 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberCard_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberCard_Night_0_en.png new file mode 100644 index 0000000000..50ce2922b2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberCard_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33b68ade07f7c6c17d0d781f30523412b42a6804114b101af91096c1c746473a +size 27928 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryErrorContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryErrorContent_Day_0_en.png new file mode 100644 index 0000000000..681b313aa9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryErrorContent_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:425603e1c99ed96fd58c6dc4bf17436135f42fc130af9cba031fdd7abcebdaf3 +size 12001 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryErrorContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryErrorContent_Night_0_en.png new file mode 100644 index 0000000000..9570abc393 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryErrorContent_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:edb075c9bf32a98df531cc5ab5baac6d5b2073e264e701ac3cab1fc4329d36b8 +size 11975 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryHeader_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryHeader_Day_0_en.png new file mode 100644 index 0000000000..e30b6aedcc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryHeader_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5f89026226cc16350afa1ddaf2c79370912804645b0880639204ab0fb7cabed +size 8751 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryHeader_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryHeader_Night_0_en.png new file mode 100644 index 0000000000..486cf93c59 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryHeader_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7aae4d33c3a26f3f974795f21583463af868240d6e5831096b15f0440dc5a8cb +size 8384 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryPendingBanner_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryPendingBanner_Day_0_en.png new file mode 100644 index 0000000000..a9188968c9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryPendingBanner_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54bc768b15b03d7aa359237f94a0c581f793743299eac42e69984edf47d4c407 +size 22908 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryPendingBanner_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryPendingBanner_Night_0_en.png new file mode 100644 index 0000000000..111939486a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory.components_MemberDirectoryPendingBanner_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c55cd38c77016f63d1d58abb601c383eabf06bb33589c0b78524489f1ad81e57 +size 22033 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_0_en.png new file mode 100644 index 0000000000..d187a00939 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4372d95814bf2893940b9568b33d317e57efe8d2e1b1b4d31f13ba63a8b7a24 +size 62499 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_1_en.png new file mode 100644 index 0000000000..e3e5480add --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72e73b036458ee32e207f711cf6656fe7646b23d3d9e096e62932c828dd53189 +size 5244 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_2_en.png new file mode 100644 index 0000000000..91ef97d5f8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8601efbcbb55c49be61521dbea2878975be82e0c967a1ab763b9d79a307513c8 +size 62731 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_3_en.png new file mode 100644 index 0000000000..681b313aa9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:425603e1c99ed96fd58c6dc4bf17436135f42fc130af9cba031fdd7abcebdaf3 +size 12001 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_4_en.png new file mode 100644 index 0000000000..bb32ab1a28 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8bc85d1d728dc5406dbed71d0694eb6e3f155dd6611c9ef5f13c09bf4fb9c8d7 +size 30349 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_5_en.png new file mode 100644 index 0000000000..2d5e9a39fb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68890517e79280bcf737b5b142e5149d3df5d905c416026cb221e50f269314ce +size 22427 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_0_en.png new file mode 100644 index 0000000000..c3884f1f9c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f647a882a0f17d7bb2015fced96fc7b0dc62e12fea7db5e6e1624a331ba2edb3 +size 61112 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_1_en.png new file mode 100644 index 0000000000..6582264383 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:518818c549548b6304d2960242ce7251bb609fa439928539a7556c33223ca8ba +size 5251 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_2_en.png new file mode 100644 index 0000000000..09ce8a0e8b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95b55f3d720f68f22cf3f7a0bb0243343f251a99bee544c8372d7bee9dabcc6a +size 61267 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_3_en.png new file mode 100644 index 0000000000..9570abc393 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:edb075c9bf32a98df531cc5ab5baac6d5b2073e264e701ac3cab1fc4329d36b8 +size 11975 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_4_en.png new file mode 100644 index 0000000000..ff17e81aa5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7dc9b0cd64526244a8c79acbc943a79a44b92a3fc5852509151b83c7d377281 +size 29198 diff --git a/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_5_en.png new file mode 100644 index 0000000000..6228e0bdfd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.healthchat.impl.directory_MemberDirectoryView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:caeaf6ef97c06b9a104a998aadf801a3dc503e6cb151e5a326f48dae36d54204 +size 21420 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png index 344bd350d6..41fb756be6 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b5dde8ed5c776f4701f24dd7d411dd78f48adab261efad71e961c845e24d12f3 -size 144367 +oid sha256:4279fe38dc2cbf4accefd27c494d30026a653285ddd41d998c57a0988fd8f34c +size 145743 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png index b5f80f2faf..2dcd259c30 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8247dedc9811f2b325568973a2b714cd4b46322bcd0bf89af0e662518fa77747 -size 67694 +oid sha256:ff032e275d9b8468999bae5800ddebf5779c6bd3c037ef9e899fd191e7696103 +size 68788 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png index f106244405..5f3dc374ba 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c393b13532fba4d4ef25dda45b6ca3e1f6da6d49c5b2de412beded261a4f9311 -size 36164 +oid sha256:8aa0b89a8027f48e475297605640861805782129d4aa6434b4db0115a3e11997 +size 37341 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png index f2a69ea475..86a5f48ea4 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b493bc5151f15d6ba91bfb0c27b41f3abcf13ee7d511aa59db171cecdbd95f3 -size 31958 +oid sha256:e2948c30cb3ac117b376b4e2ac0358c21a4961a69181f245e302891f4b3c3a93 +size 33767 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png index f4dff45422..c2d9207799 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a33681fe064d5e12f53273bfd9262af8c7b902e237cb8d0641573b0645e291c -size 94687 +oid sha256:2e8250d679bf891c8ba0aac9b75b896e9cb51573dc5744f6c06918328e5bd06d +size 96132 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png index bdad242551..d4a58bd22c 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:022954bdca89572b74bc70f6d91820e99eef8826409c0a06e6145a5c3ec8eaf4 -size 87268 +oid sha256:f55e99dc15e3d6e6de35a4963ec5daebb4ae7951381c2ac068ae558f7321244d +size 89124 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_15_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_15_en.png index edf07a66f3..abddbab3ef 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af36d0e3383935d256b3730e6f955e858ae42a82f54485b879e7a6a71fc25f6c -size 55237 +oid sha256:07b0a251e29984fcbad6d3a60d4ff803e2c2fe9f2572112af9b39686f011bb93 +size 56352 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_16_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_16_en.png index aa313049f5..4e7726b79f 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e55407ac7116b3127a979f630777d0d528a349c38d37829d8bc0c37a316c7c5b -size 42526 +oid sha256:f66ad2e82f0d6a532ceb21f6e870993b88e3724877a56c4cd51fb1f9ad1b1810 +size 43683 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_17_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_17_en.png new file mode 100644 index 0000000000..8715d77372 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_17_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a195d78b1a37e196d21d9dae74d0b3a86c9f553333ed7fd67041e89a7b1e92a1 +size 21184 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png index b5f80f2faf..2dcd259c30 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8247dedc9811f2b325568973a2b714cd4b46322bcd0bf89af0e662518fa77747 -size 67694 +oid sha256:ff032e275d9b8468999bae5800ddebf5779c6bd3c037ef9e899fd191e7696103 +size 68788 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png index b5f80f2faf..2dcd259c30 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8247dedc9811f2b325568973a2b714cd4b46322bcd0bf89af0e662518fa77747 -size 67694 +oid sha256:ff032e275d9b8468999bae5800ddebf5779c6bd3c037ef9e899fd191e7696103 +size 68788 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png index e01335907a..01d36a5ca2 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25e5ac35ddf79fdc78c21dbd031f506da04caac7bdd53f279c88fd4574f72a82 -size 64644 +oid sha256:67fffcd83343ed27d94be8f00d0ae6d831c254210825c5368692bc71f84dbab7 +size 65929 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_4_en.png index fe41a54fcc..4f9adb7665 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c1245da0a4cf56aba8e102e7f297996ed58fc8c50165288dea11ca86fbc0dcc -size 57235 +oid sha256:d16c165b825131579a42cd11642d66403d66901b6b2f717054e482b3d560ce3b +size 58272 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png index b5f80f2faf..2dcd259c30 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8247dedc9811f2b325568973a2b714cd4b46322bcd0bf89af0e662518fa77747 -size 67694 +oid sha256:ff032e275d9b8468999bae5800ddebf5779c6bd3c037ef9e899fd191e7696103 +size 68788 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png index b6779aca93..6798cdbc1c 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0769e243dc60de52c16df23a86eb62167bb60cb756d113f88b9b476c025e402b -size 87117 +oid sha256:2a637d4c8b06fac2126e9d9ad8d6cc2bafa320736bb63d84f00ec747fd9ffdaf +size 88975 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png index fe171e26b7..1b0620e9a0 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a7b2a6ccb43dc572140e2ce658967cd273d1122bf0216269a648920099a68d0 -size 62572 +oid sha256:f41e4f7932ebaa69de11a22136d483b0de273aef58c9ea885b4f5ceeaaf62e65 +size 63415 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png index 420cb7c631..259a52a65c 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d56c1af9fc73f4aaeed33dd8b6113ec64ddbfdf63506aa31ae6996732870baa2 -size 31127 +oid sha256:3b56caa6b22f3b196ffa0aa3f1078e50e8279a34ce80a7a80f3520fab3bd1686 +size 32168 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png index f36b5c2f83..da3b5737bb 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c3fb8ae530989e5a6d6e4193fbac4d4356af34076b684b782a0f8fec31226f1 -size 26657 +oid sha256:34f0ee3e98c2a9dd2f4a2d7f6f0ea882065a76aa1f13244ad089b5f845dff6d1 +size 27623 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png index 48b07fc9bd..a449b2df1c 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d302be6dbaef08b501f4f33d3e01338e068767702a7018bd5bb24ade6eee2cb -size 88558 +oid sha256:3b62578b9e60a843095f86bd044f8d002abb6ae2d1060f31e91d0ac9a4352805 +size 89341 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png index 5cb7d766cd..8dd0458515 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6be5cd35eec917bf1c96d03079df8235d0bc7b52159f54c8a177a64f648e58c2 -size 81212 +oid sha256:148cdf26fd055c0247fc8e64a927e01c5881145f376e98223a3f558282629a7a +size 82151 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_15_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_15_en.png index 2691d1c1df..cbc0f7a7b1 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66d241a4270bef0a1f80c62c302cb5b40b4370f73e8175a746d32c90d1f98208 -size 49882 +oid sha256:9cb7fb35d7b6d091679ee928cec3a7b521d6c34425b9ad7e6b49e372e9ac99b3 +size 50856 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_16_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_16_en.png index 451ca0c3ec..5beb60984f 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1838a9839badf62d4d427a66a15a7d220996bdb0b60f0373249d62e4afae2910 -size 37390 +oid sha256:b07834c3eb7226d88ab19576d29e1b0b7efeb6b2c04fbaa45a7411e507108d81 +size 38388 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_17_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_17_en.png new file mode 100644 index 0000000000..f44be3935f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_17_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6cce1b807bcaea11d702c24fd5341dd1fe615461bc3bfe68b8dec3a8b0f3e03 +size 17275 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png index fe171e26b7..1b0620e9a0 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a7b2a6ccb43dc572140e2ce658967cd273d1122bf0216269a648920099a68d0 -size 62572 +oid sha256:f41e4f7932ebaa69de11a22136d483b0de273aef58c9ea885b4f5ceeaaf62e65 +size 63415 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png index fe171e26b7..1b0620e9a0 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a7b2a6ccb43dc572140e2ce658967cd273d1122bf0216269a648920099a68d0 -size 62572 +oid sha256:f41e4f7932ebaa69de11a22136d483b0de273aef58c9ea885b4f5ceeaaf62e65 +size 63415 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png index 89c4934dd9..31c5c1838a 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8172ca867a8c2e827ead36a22cefe967a5b57c69cae6d5aca1c941e4dd2ba16f -size 60160 +oid sha256:c651e0f5c8a2f2611b6e31dcf519f49d310e4cc476cc2160a272d0b0670dd76b +size 60826 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_4_en.png index 390c9f6818..3306a382c3 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:13740b4fc4cbd24075606e45bf070a8fbb762d298857bc071399def91dda7175 -size 52189 +oid sha256:face1048515f0f05f06ddcdd922397d15458c5eb2a1773fd853986b21380f497 +size 53053 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png index fe171e26b7..1b0620e9a0 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a7b2a6ccb43dc572140e2ce658967cd273d1122bf0216269a648920099a68d0 -size 62572 +oid sha256:f41e4f7932ebaa69de11a22136d483b0de273aef58c9ea885b4f5ceeaaf62e65 +size 63415 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png index 1092dcd1b9..2ca6fbb8d8 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bce0bc8590037705369735e83e576f0818f4528298b4b2face999c8802f855b9 -size 81081 +oid sha256:17838682d1f7ced43bca749e32a9fbcf7ec9d286f72d868013b81ca422e30c07 +size 82039 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en.png index f0bfd0f2b4..33110584a9 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e82c6e4ff563e7ae870431528d9413ac915e16ca0151aa3c86ff5081bfc72478 -size 27563 +oid sha256:dbdf9740221b82aaaf332c95e7299594f0b4fdd258b2e9c733133bdd4a01f692 +size 28378 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_3_en.png index 438d96608d..42ecbe8a68 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae0b92a52a7f2c0533af7d7f0fbddc6497778e7c5dcedc3344a2ba60a6a948f8 -size 40439 +oid sha256:471abc7c54d0f90ad5a923bd21abbf0960bbb9cbbdb22313a779083baf61ab3f +size 40671 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en.png index 5bc0a7e57a..d9b4151b77 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da71297e7073c5c592dc154af7f137c4eb500bf528e1509b95da869a2c61cbd2 -size 30488 +oid sha256:6f91ec4d46970dd80c97efc76361ce26b3e705b037b99bc032205ace3d2c8749 +size 31953 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_2_en.png index a7ce4c15ec..bf25a07679 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffd4f5b8fece04b00ad5d866f6fc11e753e800f956c966e703d1dfc66f1ce8e7 -size 37257 +oid sha256:a52d39cb27572c49ab64b1534ead353687d83870b5d5cfe7158859925b4717f3 +size 34926 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_6_en.png index db11b33396..0c9a67252c 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1ffc8d536a239ca11de23f3f8e5fb14b429b9beee03c80bbca0321ec853f5e0 -size 26638 +oid sha256:9f3fb75974ce37fc3ce5e303ab5573a0d9a769f3c079de5b0f6462b40e53c1cd +size 26713 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_0_en.png index aa539ed8a1..dbd4836467 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc1e9df75619bde2571bad0362f147c627ee44a780fbaa9a2c0e5a49e252d14b -size 19881 +oid sha256:d3260d344a065f1710aff1450dbdaaa0bd1fbd963d5304167124d845c46be433 +size 19903 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_4_en.png index aa539ed8a1..dbd4836467 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc1e9df75619bde2571bad0362f147c627ee44a780fbaa9a2c0e5a49e252d14b -size 19881 +oid sha256:d3260d344a065f1710aff1450dbdaaa0bd1fbd963d5304167124d845c46be433 +size 19903 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_8_en.png index 1f8c2d110d..541b2a97e1 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f139526f69881e08b17d15013d78fb3eeca76eea5b18d378953c37fbd9d76921 -size 44064 +oid sha256:038e12f3caeef6ac8d5389b7cdc68138e089dcac335d0a5904adc55c9bcb7b1c +size 40642 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_2_en.png index 3192cf2a5e..8edee63bc1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1885bbe8b0ea497ac20d7bcdd0c6a93ba0533e898378d35e1cafdc8c9924fd56 -size 279984 +oid sha256:de82970da241172b94f822f8792e6fc721dcfc7f0f67e3523962e253e9750884 +size 255933 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en.png index 693a36b7dd..5f51b27f5e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3dbb8ef39b6a7a0c2f576d8bd101aa97c89da696f23e6d5836846494471c01a1 -size 55799 +oid sha256:3274314ab9e39c23a6b9d66adf80ac8bb7b364ce60a300f96582374a18bc821f +size 54590 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en.png index 0f6de1b844..bd3b0196d6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28512b0fda0b71b98eab03cee15c9d3163825b1a3545670cfa277ddd5434c87a -size 58417 +oid sha256:6f7f7415abbe51858f938e16ad03c6aab983fb3d5e2a53a028bae3c031446646 +size 67848 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en.png index bd73e322ab..93622cedfe 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 -size 3657 +oid sha256:87193a7c9eb9d40234970e5f5be02e80f2b3ffe91e2c9ca6afd58b8bccdd5911 +size 55075 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Night_0_en.png index 3f12875a5b..06dd93b269 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7076c9a44a433cd9eeca536365ae262d39b4ad99a8f854faadec272cb1b82031 -size 294992 +oid sha256:578a3707102c7754f607c3a14f94f32cafa30ee570db174386533c484cb4773c +size 295031 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png index ab6392a424..0435f55de6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94f89a37549a3f198442db467bc7d8539f0b5649d97927bfe731f090d6dfb03d -size 379505 +oid sha256:ff839731834d7d0c94fa9a2c412014ddbaeaa053c5af1c8cbd00a3f3a583f364 +size 379511 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png index 1f0fdabe6d..3ac59caeea 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8bb5a6de6601da8d7fe59f110a054577e2e25dae4bb449f60c04e7d7a1b9551 -size 365073 +oid sha256:1a94d5198e27338675c0e15df5874ba3272ee315b8d77291dc7ccc7c754ed1f4 +size 365082 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png index 84d9d4e5c3..94428abb56 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c609a633ab23eae5729ecb57a17a385bac65e81cbf0180c01e0847f1028039e -size 347890 +oid sha256:36d7cf2510d546d2b330b1d1b1674ce58df0558c547e443794a1e7b2222add87 +size 347897 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png index 01d4dca84b..67490ba700 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef2bf449671b652cb152b3baab92ab100eab77a54229ab49daeab156f2f484d0 -size 369724 +oid sha256:f4431e2d9f30123949ea5676ce60f642234adcf9b5a33f2acc3f80ebf6609a66 +size 369730 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png index a332744358..14a40a5676 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:588967869cbd25ed8b82aff5618175660a8a9e45a5a861b1c7867cf1913f575b -size 364646 +oid sha256:a11044fe90227f1f316a1ade8587e545bb620e11cca76865fc2eb83cdb817fc5 +size 364655 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png index 6e83d6491d..d235670def 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34152c742abc486a48234e22101833bc163cf11979890d06aac41e68e3c404d0 -size 364846 +oid sha256:a00bd766a00b7e7c6615fc53bf469b3ca5056a6c514c096431ae38d99d604306 +size 364845 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png index 77abe20982..422c324695 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c3d87862c6d7fc88d0f23cd54c5b9f77028992fdd7c282a195b964dd17e85ac -size 367855 +oid sha256:a1df8d22766981fc76d6d767dc3f40d98cadc7c51be20417b067b91af604232b +size 367884 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png index a431a9429a..e9d85990e8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93c2df04d65f6078ce3f7067fbae8a7317ec42ed0fc013dee962a3953269f3b7 -size 362813 +oid sha256:66d4b6ea4a697735bbb0fac936b89cde99c5077973ba828894bf50b5dae233b3 +size 362848 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png index 1a8ea3e4e9..0cfd1dc985 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66aba7b4271a990fac85b0bbc7cbdc371fa1e38a4da5bb4e2f62a80b21cb3a8f -size 363012 +oid sha256:545c9e8933e420cc3f8fbba0fbd5162c946c65a20721faeb77c568596ca720cd +size 363045 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png index 5f724184b0..9c5932834e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:179b30927c3db3963bef12243ae69908ad0b99275906197abd02c110f012213f -size 57267 +oid sha256:0dd30568e628750f922a87c2df1fd7751e9d899b9163253c9f2a0c7b03e4a869 +size 56247 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png index c891dbf206..3cd7dd274c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:507ab916afdc3f4d1429ef70c6e19dac8a80713636b064657430c19252494098 -size 55558 +oid sha256:b2a89e79995050e8b24b459172a7c123cc08c64097d5d735cef8c47f913a3bf4 +size 54080 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png index f654a4403c..e4a14ae551 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e11d2920b56154389cffd1d56b986de2a570bff474d4f4861c710239d27c44e0 -size 58442 +oid sha256:39430896a687266d7a60890fdac5dbca6d36c87fe30d07619c9c50e23c3f77be +size 56972 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png index 1f5ddc83fd..b3c490ad32 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f3b1665b2038873c849362b16a04713ef89d17a9b3e44dc828754110e192e33 -size 56407 +oid sha256:30e13e1b91d681088ceba71f21f668a5d8b8f376cb10832f0289b1b14c1103e4 +size 55569 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png index b2e5208390..6b4cb7360c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72460dfd88da9b6ebf586e98a819dcc6d377da3515df35a0a2b7247fa376f7d8 -size 54987 +oid sha256:abf3863e6d5ddc747dfea06a006983345937cd85d0e89b60bc625f4ad16b2691 +size 52906 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png index 46c4466773..1ad21c4d64 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7ba8b81a3df9e797ca317a450a7f1bf0a8391aa6b919136827b5ab8e324cdb7 -size 57057 +oid sha256:289819914863942caad5202131efddd9b880b4af64a36d4b45fb547b5b1ec47f +size 55916 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en.png index 7cca911c45..074820e157 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26e14b77fa3c3aeaea58782309e6de8b9047e07945009322990ebf5f4c283309 -size 52770 +oid sha256:7caacb336793b8ae4b83e9a10a82325e25e8b3c036f881898ed28688250eb6e0 +size 55744 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en.png index 616c06bf46..36d19c6b72 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ce2519906f1aaf34700ca9ba4bdeaa72030f1f5aff3f7e24352a1435ac67d98 -size 53966 +oid sha256:7dfa2767c55a81d3f121b8fa55443b7b05f9369e91639b80e0be349863799ad7 +size 55438 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Day_0_en.png index 58546b8522..ee5c582240 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30357bf18792db5ab20085010251170a724ffe8c00313facd04ee704e806af64 -size 10982 +oid sha256:75de341f705bb22955f9399180519b65ad59c2e1917317d56e50ea4db78f35bc +size 12102