mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
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.
This commit is contained in:
+16
@@ -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
|
||||
@@ -102,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(
|
||||
@@ -262,6 +268,9 @@ class HomeFlowNode(
|
||||
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)
|
||||
}
|
||||
@@ -269,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()
|
||||
}
|
||||
|
||||
|
||||
+7
-1
@@ -90,6 +90,9 @@ fun HomeView(
|
||||
leaveRoomView: @Composable () -> Unit,
|
||||
// HealthChat fork: content of the member directory tab, rendered by features/healthchat.
|
||||
memberDirectoryView: @Composable (PaddingValues, LazyListState) -> Unit = { _, _ -> },
|
||||
// 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,
|
||||
) {
|
||||
val state: RoomListState = homeState.roomListState
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
@@ -125,6 +128,7 @@ fun HomeView(
|
||||
onCreateSpaceClick = { if (firstThrottler.canHandle()) onCreateSpaceClick() },
|
||||
onMenuActionClick = onMenuActionClick,
|
||||
memberDirectoryView = memberDirectoryView,
|
||||
isBackHandlingEnabled = isBackHandlingEnabled,
|
||||
)
|
||||
// This overlaid view will only be visible when state.displaySearchResults is true
|
||||
RoomListSearchView(
|
||||
@@ -152,6 +156,7 @@ private fun HomeScaffold(
|
||||
onCreateSpaceClick: () -> Unit,
|
||||
onMenuActionClick: (RoomListMenuAction) -> Unit,
|
||||
memberDirectoryView: @Composable (PaddingValues, LazyListState) -> Unit,
|
||||
isBackHandlingEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun onRoomClick(room: RoomListRoomSummary) {
|
||||
@@ -163,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 {
|
||||
|
||||
+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