Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
99c142b
Add pagination to navigation menus lists
nbradbury Feb 5, 2026
6942c64
Fix pagination and improve menu list UI
nbradbury Feb 5, 2026
c609253
Replace linkable items dropdown with modal bottom sheet
nbradbury Feb 5, 2026
f49765a
Remove unused menu_item_count plural resource
nbradbury Feb 5, 2026
2c62c3e
Simplify navigation menus code
nbradbury Feb 5, 2026
1aa9312
Add mutex protection to pagination functions
nbradbury Feb 5, 2026
aab08fb
Use whitelist for url validation
nbradbury Feb 5, 2026
7d6c964
Fix pagination state reset on error
nbradbury Feb 5, 2026
e8be827
Cancel linkable items loading job when menu item type changes
nbradbury Feb 5, 2026
973124b
Suppress LongMethod detekt warning
nbradbury Feb 5, 2026
4d8b96f
Update URL validation to use WordPress allowed protocols
nbradbury Feb 5, 2026
e3295e1
Fix pagination offset derivation and error state handling
nbradbury Feb 5, 2026
a166ebd
Optimize sortItemsHierarchically with pre-computed children map
nbradbury Feb 5, 2026
89dff5f
Refactor isValidLinkUrl to fix detekt ReturnCount violation
nbradbury Feb 5, 2026
7f420b0
Parallelize menu and location fetching in fetchMenuData
nbradbury Feb 5, 2026
35713ff
Merge branch 'trunk' into issue/menus-parallel-requests
nbradbury Feb 6, 2026
ec20d4c
Merge branch 'trunk' of https://github.com/wordpress-mobile/WordPress…
nbradbury Feb 6, 2026
84db76d
Fixed minor warnings
nbradbury Feb 6, 2026
b692b80
Restored Dangerfile from trunk
nbradbury Feb 6, 2026
35ae941
Restored Dangerfile from trunk, p2
nbradbury Feb 6, 2026
d342f01
Merge branch 'trunk' into issue/menus-parallel-requests
nbradbury Feb 7, 2026
ef97160
Hide FAB on scroll down and show on scroll up with animation
nbradbury Feb 7, 2026
44bd761
Extract shared LazyList observers to reduce code duplication
nbradbury Feb 7, 2026
95fadbe
Merge branch 'trunk' into issue/menus-fab
nbradbury Feb 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
Expand All @@ -21,6 +24,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
Expand Down Expand Up @@ -113,10 +119,16 @@ class NavMenusActivity : BaseAppCompatActivity() {
.collectAsState(initial = navController.currentBackStackEntry)
val currentRoute = currentBackStackEntry?.destination?.route

var isFabVisible by remember { mutableStateOf(true) }

LaunchedEffect(currentRoute) {
isFabVisible = true
}

AppThemeM3 {
Scaffold(
topBar = { NavMenusTopBar(currentRoute) },
floatingActionButton = { NavMenusFab(currentRoute) }
floatingActionButton = { NavMenusFab(currentRoute, isFabVisible) }
) { contentPadding ->
NavHost(
navController = navController,
Expand All @@ -130,6 +142,7 @@ class NavMenusActivity : BaseAppCompatActivity() {
onMenuItemsClick = { viewModel.navigateToMenuItems(it) },
onRefresh = { viewModel.refreshMenus() },
onLoadMore = { viewModel.loadMoreMenus() },
onFabVisibilityChange = { isFabVisible = it },
modifier = Modifier.padding(contentPadding)
)
}
Expand All @@ -153,6 +166,7 @@ class NavMenusActivity : BaseAppCompatActivity() {
onMoveItemUp = { viewModel.moveMenuItemUp(it) },
onMoveItemDown = { viewModel.moveMenuItemDown(it) },
onLoadMore = { viewModel.loadMoreMenuItems() },
onFabVisibilityChange = { isFabVisible = it },
modifier = Modifier.padding(contentPadding)
)
}
Expand Down Expand Up @@ -270,19 +284,42 @@ class NavMenusActivity : BaseAppCompatActivity() {
}

@Composable
private fun NavMenusFab(currentRoute: String?) {
when (currentRoute) {
NavMenuScreen.MenuList.name -> {
FloatingActionButton(onClick = { viewModel.navigateToCreateMenu() }) {
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.create_menu))
private fun NavMenusFab(currentRoute: String?, isVisible: Boolean) {
AnimatedVisibility(
visible = isVisible && (
currentRoute == NavMenuScreen.MenuList.name ||
currentRoute == NavMenuScreen.MenuItemList.name
),
enter = slideInVertically { it * 2 },
exit = slideOutVertically { it * 2 }
) {
when (currentRoute) {
NavMenuScreen.MenuList.name -> {
FloatingActionButton(
onClick = { viewModel.navigateToCreateMenu() }
) {
Icon(
Icons.Default.Add,
contentDescription = stringResource(
R.string.create_menu
)
)
}
}
}
NavMenuScreen.MenuItemList.name -> {
FloatingActionButton(onClick = { viewModel.navigateToCreateMenuItem() }) {
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.add_menu_item))
NavMenuScreen.MenuItemList.name -> {
FloatingActionButton(
onClick = { viewModel.navigateToCreateMenuItem() }
) {
Icon(
Icons.Default.Add,
contentDescription = stringResource(
R.string.add_menu_item
)
)
}
}
else -> {}
}
else -> {}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
Expand All @@ -44,6 +41,7 @@ fun MenuItemListScreen(
onMoveItemUp: (Long) -> Unit,
onMoveItemDown: (Long) -> Unit,
onLoadMore: () -> Unit,
onFabVisibilityChange: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Box(modifier = modifier.fillMaxSize()) {
Expand All @@ -70,7 +68,8 @@ fun MenuItemListScreen(
onEditItemClick = onEditItemClick,
onMoveItemUp = onMoveItemUp,
onMoveItemDown = onMoveItemDown,
onLoadMore = onLoadMore
onLoadMore = onLoadMore,
onFabVisibilityChange = onFabVisibilityChange
)
}
}
Expand All @@ -83,24 +82,19 @@ private fun MenuItemListContent(
onEditItemClick: (Long) -> Unit,
onMoveItemUp: (Long) -> Unit,
onMoveItemDown: (Long) -> Unit,
onLoadMore: () -> Unit
onLoadMore: () -> Unit,
onFabVisibilityChange: (Boolean) -> Unit
) {
val listState = rememberLazyListState()

val lastVisibleItemIndex = remember {
derivedStateOf {
listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
}
}

LaunchedEffect(lastVisibleItemIndex.value, state.items.size, state.canLoadMore) {
val shouldLoadMore = lastVisibleItemIndex.value >= state.items.size - 1 &&
state.canLoadMore &&
!state.isLoadingMore
if (shouldLoadMore) {
onLoadMore()
}
}
ObserveLoadMore(
listState = listState,
itemCount = state.items.size,
canLoadMore = state.canLoadMore,
isLoadingMore = state.isLoadingMore,
onLoadMore = onLoadMore
)
ObserveScrollDirectionForFab(listState, onFabVisibilityChange)

LazyColumn(
state = listState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
Expand Down Expand Up @@ -47,6 +44,7 @@ fun MenuListScreen(
onMenuItemsClick: (Long) -> Unit,
onRefresh: () -> Unit,
onLoadMore: () -> Unit,
onFabVisibilityChange: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
val pullToRefreshState = rememberPullToRefreshState()
Expand Down Expand Up @@ -90,21 +88,14 @@ fun MenuListScreen(
else -> {
val listState = rememberLazyListState()

// Detect when user scrolls to the last item
val lastVisibleItemIndex = remember {
derivedStateOf {
listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
}
}

LaunchedEffect(lastVisibleItemIndex.value, state.menus.size, state.canLoadMore) {
val shouldLoadMore = lastVisibleItemIndex.value >= state.menus.size - 1 &&
state.canLoadMore &&
!state.isLoadingMore
if (shouldLoadMore) {
onLoadMore()
}
}
ObserveLoadMore(
listState = listState,
itemCount = state.menus.size,
canLoadMore = state.canLoadMore,
isLoadingMore = state.isLoadingMore,
onLoadMore = onLoadMore
)
ObserveScrollDirectionForFab(listState, onFabVisibilityChange)

LazyColumn(
state = listState,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.wordpress.android.ui.navmenus.screens

import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow

/**
* Observes the scroll direction of a [LazyListState] and calls
* [onFabVisibilityChange] with `false` when scrolling down
* (to hide the FAB) and `true` when scrolling up (to show it).
*/
@Composable
fun ObserveScrollDirectionForFab(
listState: LazyListState,
onFabVisibilityChange: (Boolean) -> Unit
) {
LaunchedEffect(listState) {
var prevIndex = listState.firstVisibleItemIndex
var prevOffset = listState.firstVisibleItemScrollOffset
snapshotFlow {
listState.firstVisibleItemIndex to
listState.firstVisibleItemScrollOffset
}.collect { (index, offset) ->
val scrollingDown = index > prevIndex ||
(index == prevIndex && offset > prevOffset)
val scrollingUp = index < prevIndex ||
(index == prevIndex && offset < prevOffset)
if (scrollingDown) {
onFabVisibilityChange(false)
} else if (scrollingUp) {
onFabVisibilityChange(true)
}
prevIndex = index
prevOffset = offset
}
}
}

/**
* Observes scroll position and triggers [onLoadMore] when the
* last visible item is near the end of the list.
*/
@Composable
fun ObserveLoadMore(
listState: LazyListState,
itemCount: Int,
canLoadMore: Boolean,
isLoadingMore: Boolean,
onLoadMore: () -> Unit
) {
val lastVisibleItemIndex = remember {
derivedStateOf {
listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
?: 0
}
}

LaunchedEffect(lastVisibleItemIndex.value, itemCount, canLoadMore) {
val shouldLoadMore =
lastVisibleItemIndex.value >= itemCount - 1 &&
canLoadMore &&
!isLoadingMore
if (shouldLoadMore) {
onLoadMore()
}
}
}
Loading