mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: Add bottom-nav scroll-to-top handling for nodes and conversations (#3674)
This commit is contained in:
parent
00276bc5d4
commit
bc8ff26167
8 changed files with 176 additions and 13 deletions
|
|
@ -28,6 +28,8 @@ import androidx.navigation.NavHostController
|
|||
import com.geeksville.mesh.repository.radio.MeshActivity
|
||||
import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
|
|
@ -38,6 +40,7 @@ import kotlinx.coroutines.flow.launchIn
|
|||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.meshtastic.core.analytics.platform.PlatformAnalytics
|
||||
|
|
@ -54,6 +57,7 @@ import org.meshtastic.core.service.MeshServiceNotifications
|
|||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.client_notification
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
import org.meshtastic.core.ui.component.toSharedContact
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.proto.AdminProtos
|
||||
|
|
@ -127,6 +131,13 @@ constructor(
|
|||
val meshActivity: SharedFlow<MeshActivity> =
|
||||
radioInterfaceService.meshActivity.shareIn(viewModelScope, SharingStarted.Eagerly, 0)
|
||||
|
||||
private val scrollToTopEventChannel = Channel<ScrollToTopEvent>(capacity = Channel.CONFLATED)
|
||||
val scrollToTopEventFlow: Flow<ScrollToTopEvent> = scrollToTopEventChannel.receiveAsFlow()
|
||||
|
||||
fun emitScrollToTopEvent(event: ScrollToTopEvent) {
|
||||
scrollToTopEventChannel.trySend(event)
|
||||
}
|
||||
|
||||
data class AlertData(
|
||||
val title: String,
|
||||
val message: String? = null,
|
||||
|
|
|
|||
|
|
@ -25,15 +25,17 @@ import androidx.navigation.navigation
|
|||
import androidx.navigation.toRoute
|
||||
import com.geeksville.mesh.ui.contact.ContactsScreen
|
||||
import com.geeksville.mesh.ui.sharing.ShareScreen
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.meshtastic.core.navigation.ChannelsRoutes
|
||||
import org.meshtastic.core.navigation.ContactsRoutes
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
import org.meshtastic.feature.messaging.MessageScreen
|
||||
import org.meshtastic.feature.messaging.QuickChatScreen
|
||||
|
||||
@Suppress("LongMethod")
|
||||
fun NavGraphBuilder.contactsGraph(navController: NavHostController) {
|
||||
fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopEvents: Flow<ScrollToTopEvent>) {
|
||||
navigation<ContactsRoutes.ContactsGraph>(startDestination = ContactsRoutes.Contacts) {
|
||||
composable<ContactsRoutes.Contacts>(
|
||||
deepLinks = listOf(navDeepLink<ContactsRoutes.Contacts>(basePath = "$DEEP_LINK_BASE_URI/contacts")),
|
||||
|
|
@ -48,6 +50,7 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController) {
|
|||
onNavigateToMessages = { navController.navigate(ContactsRoutes.Messages(it)) },
|
||||
onNavigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) },
|
||||
onNavigateToShare = { navController.navigate(ChannelsRoutes.ChannelsGraph) },
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
)
|
||||
}
|
||||
composable<ContactsRoutes.Messages>(
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import androidx.navigation.NavHostController
|
|||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.navigation
|
||||
import androidx.navigation.navDeepLink
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.navigation.ContactsRoutes
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
|
|
@ -54,6 +55,7 @@ import org.meshtastic.core.strings.position_log
|
|||
import org.meshtastic.core.strings.power
|
||||
import org.meshtastic.core.strings.signal
|
||||
import org.meshtastic.core.strings.traceroute
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
import org.meshtastic.feature.map.node.NodeMapScreen
|
||||
import org.meshtastic.feature.map.node.NodeMapViewModel
|
||||
import org.meshtastic.feature.node.detail.NodeDetailScreen
|
||||
|
|
@ -68,12 +70,15 @@ import org.meshtastic.feature.node.metrics.PowerMetricsScreen
|
|||
import org.meshtastic.feature.node.metrics.SignalMetricsScreen
|
||||
import org.meshtastic.feature.node.metrics.TracerouteLogScreen
|
||||
|
||||
fun NavGraphBuilder.nodesGraph(navController: NavHostController) {
|
||||
fun NavGraphBuilder.nodesGraph(navController: NavHostController, scrollToTopEvents: Flow<ScrollToTopEvent>) {
|
||||
navigation<NodesRoutes.NodesGraph>(startDestination = NodesRoutes.Nodes) {
|
||||
composable<NodesRoutes.Nodes>(
|
||||
deepLinks = listOf(navDeepLink<NodesRoutes.Nodes>(basePath = "$DEEP_LINK_BASE_URI/nodes")),
|
||||
) {
|
||||
NodeListScreen(navigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) })
|
||||
NodeListScreen(
|
||||
navigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) },
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
)
|
||||
}
|
||||
nodeDetailGraph(navController)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ import org.meshtastic.core.strings.should_update
|
|||
import org.meshtastic.core.strings.should_update_firmware
|
||||
import org.meshtastic.core.strings.traceroute
|
||||
import org.meshtastic.core.ui.component.MultipleChoiceAlertDialog
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
import org.meshtastic.core.ui.component.SimpleAlertDialog
|
||||
import org.meshtastic.core.ui.icon.Conversations
|
||||
import org.meshtastic.core.ui.icon.Map
|
||||
|
|
@ -380,9 +381,37 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
|
|||
}
|
||||
},
|
||||
onClick = {
|
||||
navController.navigate(destination.route) {
|
||||
popUpTo(navController.graph.findStartDestination().id) { saveState = true }
|
||||
launchSingleTop = true
|
||||
val isRepress = destination == topLevelDestination
|
||||
if (isRepress) {
|
||||
when (destination) {
|
||||
TopLevelDestination.Nodes -> {
|
||||
val onNodesList = currentDestination?.hasRoute(NodesRoutes.Nodes::class) == true
|
||||
if (!onNodesList) {
|
||||
navController.navigate(destination.route) {
|
||||
popUpTo(navController.graph.findStartDestination().id) { saveState = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
uIViewModel.emitScrollToTopEvent(ScrollToTopEvent.NodesTabPressed)
|
||||
}
|
||||
TopLevelDestination.Conversations -> {
|
||||
val onConversationsList =
|
||||
currentDestination?.hasRoute(ContactsRoutes.Contacts::class) == true
|
||||
if (!onConversationsList) {
|
||||
navController.navigate(destination.route) {
|
||||
popUpTo(navController.graph.findStartDestination().id) { saveState = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
uIViewModel.emitScrollToTopEvent(ScrollToTopEvent.ConversationsTabPressed)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
} else {
|
||||
navController.navigate(destination.route) {
|
||||
popUpTo(navController.graph.findStartDestination().id) { saveState = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -394,8 +423,8 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
|
|||
startDestination = NodesRoutes.NodesGraph,
|
||||
modifier = Modifier.fillMaxSize().recalculateWindowInsets().safeDrawingPadding().imePadding(),
|
||||
) {
|
||||
contactsGraph(navController)
|
||||
nodesGraph(navController)
|
||||
contactsGraph(navController, uIViewModel.scrollToTopEventFlow)
|
||||
nodesGraph(navController, uIViewModel.scrollToTopEventFlow)
|
||||
mapGraph(navController)
|
||||
channelsGraph(navController)
|
||||
connectionsGraph(navController)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ 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.foundation.selection.selectable
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.twotone.VolumeMute
|
||||
|
|
@ -45,11 +47,13 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -60,6 +64,8 @@ import androidx.compose.ui.window.DialogProperties
|
|||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.model.Contact
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import org.jetbrains.compose.resources.pluralStringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.entity.ContactSettings
|
||||
|
|
@ -84,6 +90,8 @@ import org.meshtastic.core.strings.okay
|
|||
import org.meshtastic.core.strings.select_all
|
||||
import org.meshtastic.core.strings.unmute
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
import org.meshtastic.core.ui.component.smartScrollToTop
|
||||
import org.meshtastic.proto.AppOnlyProtos
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
|
@ -91,11 +99,12 @@ import java.util.concurrent.TimeUnit
|
|||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun ContactsScreen(
|
||||
onNavigateToShare: () -> Unit,
|
||||
viewModel: ContactsViewModel = hiltViewModel(),
|
||||
onClickNodeChip: (Int) -> Unit = {},
|
||||
onNavigateToMessages: (String) -> Unit = {},
|
||||
onNavigateToNodeDetails: (Int) -> Unit = {},
|
||||
onNavigateToShare: () -> Unit,
|
||||
scrollToTopEvents: Flow<ScrollToTopEvent>? = null,
|
||||
) {
|
||||
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
|
||||
val ourNode by viewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
|
|
@ -109,6 +118,17 @@ fun ContactsScreen(
|
|||
// State for contacts list
|
||||
val contacts by viewModel.contactList.collectAsStateWithLifecycle()
|
||||
|
||||
val contactsListState = rememberLazyListState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(scrollToTopEvents) {
|
||||
scrollToTopEvents?.collectLatest { event ->
|
||||
if (event is ScrollToTopEvent.ConversationsTabPressed) {
|
||||
contactsListState.smartScrollToTop(coroutineScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Derived state for selected contacts and count
|
||||
val selectedContacts =
|
||||
remember(contacts, selectedContactKeys) { contacts.filter { it.contactKey in selectedContactKeys } }
|
||||
|
|
@ -205,8 +225,9 @@ fun ContactsScreen(
|
|||
selectedList = selectedContactKeys,
|
||||
onClick = onContactClick,
|
||||
onLongClick = onContactLongClick,
|
||||
channels = channels,
|
||||
onNodeChipClick = onNodeChipClick,
|
||||
listState = contactsListState,
|
||||
channels = channels,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -424,11 +445,12 @@ fun ContactListView(
|
|||
selectedList: List<String>,
|
||||
onClick: (Contact) -> Unit,
|
||||
onLongClick: (Contact) -> Unit,
|
||||
channels: AppOnlyProtos.ChannelSet? = null,
|
||||
onNodeChipClick: (Contact) -> Unit,
|
||||
listState: LazyListState,
|
||||
channels: AppOnlyProtos.ChannelSet? = null,
|
||||
) {
|
||||
val haptics = LocalHapticFeedback.current
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize(), state = listState) {
|
||||
items(contacts, key = { it.contactKey }) { contact ->
|
||||
val selected by remember { derivedStateOf { selectedList.contains(contact.contactKey) } }
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue