From 2a9c9e4510d807d8528abfa64054010f0417d77a Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 15 Mar 2026 23:25:04 +0100 Subject: [PATCH] fix: allow swipe-to-close when drawer is open on pager screens Keep drawer gestures enabled when the drawer is open so users can swipe left to close it. Only disable gestures when the drawer is closed (to let the pager and ZonedSwipeModifier handle gestures). --- .../ui/components/ZonedSwipeModifier.kt | 90 +++++++++++++++++++ .../amethyst/ui/navigation/AppNavigation.kt | 11 ++- .../AccountSwitcherAndLeftDrawerLayout.kt | 2 + .../chats/rooms/feed/ChatroomListTabs.kt | 8 +- .../ui/screen/loggedIn/home/HomeScreen.kt | 8 +- 5 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZonedSwipeModifier.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZonedSwipeModifier.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZonedSwipeModifier.kt new file mode 100644 index 000000000..440a8d935 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZonedSwipeModifier.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.components + +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.pager.PagerState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp + +private val EDGE_ZONE_WIDTH = 48.dp + +fun Modifier.zonedDrawerSwipe( + pagerState: PagerState, + openDrawer: () -> Unit, +): Modifier = + composed { + val edgeZonePx = with(LocalDensity.current) { EDGE_ZONE_WIDTH.toPx() } + + var gestureStartX by remember { mutableFloatStateOf(0f) } + var gestureStartPage by remember { mutableIntStateOf(0) } + var drawerOpened by remember { mutableStateOf(false) } + + val connection = + remember { + object : NestedScrollConnection { + override fun onPreScroll( + available: Offset, + source: NestedScrollSource, + ): Offset { + if (source != NestedScrollSource.UserInput) return Offset.Zero + if (drawerOpened) return Offset(available.x, 0f) + + // available.x > 0 means user is swiping right + if (available.x > 0f) { + val wasOnFirstPage = gestureStartPage == 0 + val isInEdgeZone = gestureStartX < edgeZonePx + + if (wasOnFirstPage || !isInEdgeZone) { + drawerOpened = true + openDrawer() + return Offset(available.x, 0f) + } + } + return Offset.Zero + } + } + } + + this + .pointerInput(Unit) { + awaitEachGesture { + val down = awaitFirstDown(requireUnconsumed = false) + gestureStartX = down.position.x + gestureStartPage = pagerState.currentPage + drawerOpened = false + } + }.nestedScroll(connection) + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index f4b276b43..d91803756 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -37,8 +37,10 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.core.net.toUri import androidx.core.util.Consumer +import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.crashreports.DisplayCrashMessages @@ -142,7 +144,14 @@ fun AppNavigation( ) { val nav = rememberNav() - AccountSwitcherAndLeftDrawerLayout(accountViewModel, accountSessionManager, nav) { + val navBackStackEntry by nav.controller.currentBackStackEntryAsState() + val isTabPagerRoute = + navBackStackEntry?.destination?.let { dest -> + dest.hasRoute() || dest.hasRoute() + } ?: false + val drawerGesturesEnabled = !isTabPagerRoute || nav.drawerState.isOpen + + AccountSwitcherAndLeftDrawerLayout(accountViewModel, accountSessionManager, nav, drawerGesturesEnabled) { NavHost( navController = nav.controller, startDestination = Route.Home, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt index 84ec07af7..8dc7d01f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt @@ -49,6 +49,7 @@ fun AccountSwitcherAndLeftDrawerLayout( accountViewModel: AccountViewModel, accountSessionManager: AccountSessionManager, nav: INav, + gesturesEnabled: Boolean = true, content: @Composable () -> Unit, ) { val scope = rememberCoroutineScope() @@ -83,6 +84,7 @@ fun AccountSwitcherAndLeftDrawerLayout( ModalNavigationDrawer( drawerState = nav.drawerState, + gesturesEnabled = gesturesEnabled, drawerContent = { DrawerContent(nav, openSheetFunction, accountViewModel) BackHandler(enabled = nav.drawerState.isOpen, nav::closeDrawer) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt index 9f48375a4..5bf51f508 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt @@ -48,6 +48,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState +import com.vitorpamplona.amethyst.ui.components.zonedDrawerSwipe import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes @@ -124,7 +125,12 @@ fun MessagesPager( HorizontalPager( contentPadding = paddingValues, state = pagerState, - userScrollEnabled = false, + userScrollEnabled = true, + modifier = + Modifier.zonedDrawerSwipe( + pagerState = pagerState, + openDrawer = nav::openDrawer, + ), ) { page -> ChatroomListFeedView( feedContentState = tabs[page].feedContentState, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index 86891c7fe..feaeb1543 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.model.TopFilter import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled +import com.vitorpamplona.amethyst.ui.components.zonedDrawerSwipe import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedState import com.vitorpamplona.amethyst.ui.feeds.PagerStateKeys @@ -211,7 +212,12 @@ private fun HomePages( HorizontalPager( contentPadding = it, state = pagerState, - userScrollEnabled = false, + userScrollEnabled = true, + modifier = + Modifier.zonedDrawerSwipe( + pagerState = pagerState, + openDrawer = nav::openDrawer, + ), ) { page -> HomeFeeds( feedState = tabs[page].feedState,