mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Add member directory tab to home navigation (#3)
* feat(healthchat): add member directory tab to home navigation Third bottom-nav tab backed by the HealthChat API: searchable member cards with native start-chat, approval-pending banner, retry on failure and navigation into the native profile screens. * fix(healthchat): stop tab back handler swallowing back presses While a screen is pushed on top of Home, the still-composed tab BackHandler intercepted back presses during the transition and invisibly switched to the Chats tab. Gate it on the node's target visibility in both the parent and Home's own backstack. * fix(healthchat): satisfy detekt, ktlint and konsist rules Import alias order, composable parameter order in HomeView, and konsist presenter helper shape. * ci: pass the real repository name to the screenshot recorder recordScreenshots.yml set GITHUB_REPOSITORY from `secrets.GITHUB_REPOSITORY`. No such secret exists on this fork, so the expression expands to an empty string and overrides the built-in environment variable of the same name. recordScreenshots.sh checks it up front and exits 1 with "No repo specified", so the workflow could never record anything here. Use the `github.repository` context instead, which is always populated. Claude-Session: https://claude.ai/code/session_01MQbp1EPM4TSXq8P8Pyq44F * Update screenshots * Revert "Update screenshots" This reverts commitb99e0d643f. * Reapply "Update screenshots" This reverts commit85bceb6d67. * test(screenshots): stop a LayoutLib teardown crash failing the shard tests Paparazzi 2.0.0-alpha05 / LayoutLib 16.2.1 races on HandlerThread startup: HandlerThread_Delegate.run calls Process.setThreadPriority, which throws NoSuchMethodError for Thread.setPosixNicenessInternal and kills the "android.bg" thread. If the test thread reaches getLooper() after that, it gets null, BackgroundThread.ensureThreadReady() NPEs, and the static initialiser of LatencyTracker fails. The JVM then marks that class erroneous for the lifetime of the fork, so every later preview showing the soft keyboard dies with NoClassDefFoundError. LayoutLibErrorFilterStatement already swallowed the bare NoSuchMethodError, but PaparazziLogger re-throws collected background errors from teardown() wrapped in a MultipleFailuresException when there is more than one, and that wrapper slipped through. A recent run reported 17 failures with zero image comparisons failing and zero delta images written. Unwrap the wrapper by reflection and swallow it only when every collected cause matches the known-bug list; anything unrecognised, and anything that cannot be decomposed, is rethrown. Also close a hole this filter had from the start. Paparazzi runs the test as `try { body } finally { teardown() }`, so a teardown error discards whatever the body threw — including a genuine snapshot mismatch. renderFailureCapture is registered inside the Paparazzi rule to record the body's exception so it wins over any teardown error, and the filter refuses to swallow anything if that rule is not registered. Two HorizontalFloatingToolbar previews have been mismatching silently behind this, and may now surface as real failures. Claude-Session: https://claude.ai/code/session_01MQbp1EPM4TSXq8P8Pyq44F --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
@@ -49,6 +49,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.leaveroom.api)
|
||||
implementation(projects.features.rageshake.api)
|
||||
|
||||
+34
-1
@@ -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
|
||||
@@ -86,6 +89,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<HomeFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
@@ -98,6 +103,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(
|
||||
@@ -246,13 +255,37 @@ class HomeFlowNode(
|
||||
onSelectNewOwners = ::navigateToSelectNewOwnersWhenLeavingRoom,
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
},
|
||||
memberDirectoryView = { contentPadding, lazyListState ->
|
||||
memberDirectoryRenderer.Render(
|
||||
state = state.memberDirectoryState,
|
||||
contentPadding = contentPadding,
|
||||
lazyListState = lazyListState,
|
||||
onOpenDm = { navigateToRoom(roomId = it, eventId = null) },
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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()
|
||||
}
|
||||
|
||||
|
||||
+7
@@ -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 {
|
||||
|
||||
+5
@@ -21,6 +21,7 @@ import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.announcement.api.Announcement
|
||||
import io.element.android.features.announcement.api.AnnouncementService
|
||||
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.rageshake.api.RageshakeFeatureAvailability
|
||||
@@ -49,6 +50,8 @@ class HomePresenter(
|
||||
private val indicatorService: IndicatorService,
|
||||
private val roomListPresenter: Presenter<RoomListState>,
|
||||
private val homeSpacesPresenter: Presenter<HomeSpacesState>,
|
||||
// HealthChat fork: member directory tab presenter.
|
||||
private val memberDirectoryPresenter: Presenter<MemberDirectoryState>,
|
||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||
private val sessionStore: SessionStore,
|
||||
private val announcementService: AnnouncementService,
|
||||
@@ -100,6 +103,7 @@ class HomePresenter(
|
||||
}
|
||||
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 {
|
||||
@@ -136,6 +140,7 @@ class HomePresenter(
|
||||
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
|
||||
roomListState = roomListState,
|
||||
homeSpacesState = homeSpacesState,
|
||||
memberDirectoryState = memberDirectoryState,
|
||||
snackbarMessage = snackbarMessage,
|
||||
canReportBug = canReportBug,
|
||||
showMultiAccountAnnouncement = showMultiAccountAnnouncement,
|
||||
|
||||
@@ -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
|
||||
@@ -26,6 +27,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 showMultiAccountAnnouncement: Boolean,
|
||||
|
||||
+7
@@ -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.RoomListStatePreviewParam
|
||||
import io.element.android.features.home.impl.roomlist.aRoomListState
|
||||
@@ -50,6 +52,9 @@ open class HomeStatePreviewParam : PreviewParameterProvider<HomeState> {
|
||||
),
|
||||
) + aHomeState(
|
||||
showMultiAccountAnnouncement = true,
|
||||
) + aHomeState(
|
||||
// HealthChat fork: appended LAST so upstream snapshot indices stay stable.
|
||||
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,
|
||||
showMultiAccountAnnouncement: Boolean = false,
|
||||
totalUnreadCount: Int = 0,
|
||||
@@ -77,5 +83,6 @@ internal fun aHomeState(
|
||||
roomListState = roomListState,
|
||||
homeSpacesState = homeSpacesState,
|
||||
totalUnreadCount = totalUnreadCount,
|
||||
memberDirectoryState = memberDirectoryState,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
+33
-9
@@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.plus
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
@@ -90,6 +91,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
|
||||
@@ -133,6 +139,8 @@ fun HomeView(
|
||||
onCreateSpaceClick = { if (firstThrottler.canHandle()) onCreateSpaceClick() },
|
||||
onMenuActionClick = onMenuActionClick,
|
||||
onSwitchToAccount = onSwitchToAccount,
|
||||
memberDirectoryView = memberDirectoryView,
|
||||
isBackHandlingEnabled = isBackHandlingEnabled,
|
||||
)
|
||||
|
||||
if (state.globalSearchState.isEnabled) {
|
||||
@@ -171,7 +179,9 @@ private fun HomeScaffold(
|
||||
onCreateSpaceClick: () -> Unit,
|
||||
onMenuActionClick: (RoomListMenuAction) -> Unit,
|
||||
onSwitchToAccount: (SessionId) -> Unit,
|
||||
isBackHandlingEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
memberDirectoryView: @Composable (PaddingValues, LazyListState) -> Unit,
|
||||
) {
|
||||
fun onRoomClick(room: RoomListRoomSummary) {
|
||||
onRoomClick(room.roomId)
|
||||
@@ -182,7 +192,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 {
|
||||
@@ -195,6 +206,7 @@ private fun HomeScaffold(
|
||||
|
||||
val roomsLazyListState = rememberLazyListState()
|
||||
val spacesLazyListState = rememberLazyListState()
|
||||
val membersLazyListState = rememberLazyListState()
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
@@ -242,6 +254,7 @@ private fun HomeScaffold(
|
||||
val lazyListStateTarget = when (item) {
|
||||
HomeNavigationBarItem.Chats -> roomsLazyListState
|
||||
HomeNavigationBarItem.Spaces -> spacesLazyListState
|
||||
HomeNavigationBarItem.Members -> membersLazyListState
|
||||
}
|
||||
coroutineScope.launch {
|
||||
if (lazyListStateTarget.firstVisibleItemIndex > 10) {
|
||||
@@ -255,15 +268,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
|
||||
},
|
||||
totalUnreadCount = state.totalUnreadCount,
|
||||
)
|
||||
@@ -325,6 +338,17 @@ private fun HomeScaffold(
|
||||
onExploreClick = {},
|
||||
)
|
||||
}
|
||||
// HealthChat fork: member directory tab content.
|
||||
HomeNavigationBarItem.Members -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(outerPadding)
|
||||
.consumeWindowInsets(outerPadding),
|
||||
) {
|
||||
memberDirectoryView(lazyColumnContentPadding + contentPadding, membersLazyListState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+2
@@ -133,6 +133,8 @@ fun HomeTopBar(
|
||||
}
|
||||
}
|
||||
HomeNavigationBarItem.Spaces -> null
|
||||
// HealthChat fork: member directory tab title.
|
||||
HomeNavigationBarItem.Members -> stringResource(selectedNavigationItem.labelRes)
|
||||
}
|
||||
displayTitle?.let {
|
||||
val style = when (spaceFiltersState) {
|
||||
|
||||
+3
@@ -15,6 +15,7 @@ import io.element.android.features.invite.test.declineandblock.FakeDeclineInvite
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
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
|
||||
@@ -41,6 +42,7 @@ class DefaultHomeEntryPointTest : RobolectricTest() {
|
||||
declineInviteAndBlockUserEntryPoint = FakeDeclineInviteAndBlockEntryPoint(),
|
||||
changeRoomMemberRolesEntryPoint = { _, _, _, _ -> lambdaError() },
|
||||
leaveRoomRenderer = { _, _, _ -> lambdaError() },
|
||||
memberDirectoryRenderer = { _, _, _, _, _, _, _, _, _ -> lambdaError() },
|
||||
sessionCoroutineScope = backgroundScope,
|
||||
)
|
||||
}
|
||||
@@ -55,6 +57,7 @@ class DefaultHomeEntryPointTest : RobolectricTest() {
|
||||
override fun navigateToBugReport() = lambdaError()
|
||||
override fun navigateToAddAccount() = lambdaError()
|
||||
override fun navigateToSession(sessionId: SessionId) = lambdaError()
|
||||
override fun navigateToUserProfile(userId: UserId) = lambdaError()
|
||||
}
|
||||
val result = entryPoint.createNode(
|
||||
parentNode = parentNode,
|
||||
|
||||
+17
@@ -14,6 +14,7 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.announcement.api.Announcement
|
||||
import io.element.android.features.announcement.api.AnnouncementService
|
||||
import io.element.android.features.announcement.test.FakeAnnouncementService
|
||||
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
|
||||
@@ -322,6 +323,21 @@ class HomePresenterTest {
|
||||
assertThat(awaitItem().totalUnreadCount).isEqualTo(5)
|
||||
}
|
||||
}
|
||||
|
||||
@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 TestScope.createHomePresenter(
|
||||
@@ -342,6 +358,7 @@ internal fun TestScope.createHomePresenter(
|
||||
indicatorService = indicatorService,
|
||||
roomListPresenter = { aRoomListState() },
|
||||
homeSpacesPresenter = homeSpacesPresenter,
|
||||
memberDirectoryPresenter = { aMemberDirectoryState() },
|
||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||
sessionStore = sessionStore,
|
||||
announcementService = announcementService,
|
||||
|
||||
+78
@@ -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<ComponentActivity> {
|
||||
val eventsRecorder = EventsRecorder<HomeEvent>()
|
||||
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<ComponentActivity> {
|
||||
// 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<HomeEvent>(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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user