diff --git a/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt b/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt index 25577600c..2effcacdb 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt @@ -24,8 +24,8 @@ 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.LazyListScope 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 @@ -59,6 +59,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedback import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.unit.dp @@ -68,7 +69,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.compose.itemKey import com.geeksville.mesh.model.Contact import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest @@ -289,7 +289,7 @@ fun ContactsScreen( @Suppress("LongMethod") @Composable -fun MuteNotificationsDialog( +private fun MuteNotificationsDialog( showDialog: Boolean, selectedContactKeys: List, contactSettings: Map, @@ -384,7 +384,7 @@ fun MuteNotificationsDialog( } @Composable -fun DeleteConfirmationDialog( +private fun DeleteConfirmationDialog( showDialog: Boolean, selectedCount: Int, // Number of items to be deleted onDismiss: () -> Unit, @@ -426,7 +426,7 @@ fun DeleteConfirmationDialog( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun SelectionToolbar( +private fun SelectionToolbar( selectedCount: Int, onCloseSelection: () -> Unit, onMuteSelected: () -> Unit, @@ -468,27 +468,127 @@ fun SelectionToolbar( ) } -/** - * Non-paginated contact list view. - * - * NOTE: This is kept for ShareScreen which displays a simple contact picker. The main ContactsScreen uses - * [ContactListViewPaged] for better performance. - * - * @see ContactListViewPaged for the paginated version - */ @Composable -fun ContactListView( - contacts: List, +private fun ContactListViewPaged( + contacts: LazyPagingItems, + channelPlaceholders: List, selectedList: List, onClick: (Contact) -> Unit, onLongClick: (Contact) -> Unit, onNodeChipClick: (Contact) -> Unit, listState: LazyListState, + modifier: Modifier = Modifier, channels: AppOnlyProtos.ChannelSet? = null, ) { val haptics = LocalHapticFeedback.current - LazyColumn(modifier = Modifier.fillMaxSize(), state = listState) { - items(contacts, key = { it.contactKey }) { contact -> + + if (contacts.loadState.refresh is LoadState.Loading && contacts.itemCount == 0) { + Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } + return + } + + val visiblePlaceholders = rememberVisiblePlaceholders(contacts, channelPlaceholders) + + ContactListContentInternal( + contacts = contacts, + visiblePlaceholders = visiblePlaceholders, + selectedList = selectedList, + onClick = onClick, + onLongClick = onLongClick, + onNodeChipClick = onNodeChipClick, + listState = listState, + modifier = modifier, + channels = channels, + haptics = haptics, + ) +} + +@Composable +private fun ContactListContentInternal( + contacts: LazyPagingItems, + visiblePlaceholders: List, + selectedList: List, + onClick: (Contact) -> Unit, + onLongClick: (Contact) -> Unit, + onNodeChipClick: (Contact) -> Unit, + listState: LazyListState, + channels: AppOnlyProtos.ChannelSet?, + haptics: HapticFeedback, + modifier: Modifier = Modifier, +) { + LazyColumn(modifier = modifier.fillMaxSize(), state = listState) { + contactListPlaceholdersItems( + visiblePlaceholders = visiblePlaceholders, + selectedList = selectedList, + onClick = onClick, + onLongClick = onLongClick, + onNodeChipClick = onNodeChipClick, + channels = channels, + haptics = haptics, + ) + + contactListPagedItems( + contacts = contacts, + selectedList = selectedList, + onClick = onClick, + onLongClick = onLongClick, + onNodeChipClick = onNodeChipClick, + channels = channels, + haptics = haptics, + ) + + contactListAppendLoadingItem(contacts = contacts) + } +} + +private fun LazyListScope.contactListPlaceholdersItems( + visiblePlaceholders: List, + selectedList: List, + onClick: (Contact) -> Unit, + onLongClick: (Contact) -> Unit, + onNodeChipClick: (Contact) -> Unit, + channels: AppOnlyProtos.ChannelSet?, + haptics: HapticFeedback, +) { + items( + count = visiblePlaceholders.size, + key = { index -> "placeholder_${visiblePlaceholders[index].contactKey}" }, + ) { index -> + val placeholder = visiblePlaceholders[index] + val selected by remember { derivedStateOf { selectedList.contains(placeholder.contactKey) } } + + ContactItem( + contact = placeholder, + selected = selected, + onClick = { onClick(placeholder) }, + onLongClick = { + onLongClick(placeholder) + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + }, + channels = channels, + onNodeChipClick = { onNodeChipClick(placeholder) }, + ) + } +} + +private fun LazyListScope.contactListPagedItems( + contacts: LazyPagingItems, + selectedList: List, + onClick: (Contact) -> Unit, + onLongClick: (Contact) -> Unit, + onNodeChipClick: (Contact) -> Unit, + channels: AppOnlyProtos.ChannelSet?, + haptics: HapticFeedback, +) { + items( + count = contacts.itemCount, + key = { index -> + val contact = contacts[index] + contact?.let { "${it.contactKey}#$index" } ?: "contact_placeholder_$index" + }, + ) { index -> + val contact = contacts[index] + if (contact != null) { val selected by remember { derivedStateOf { selectedList.contains(contact.contactKey) } } ContactItem( @@ -506,78 +606,13 @@ fun ContactListView( } } -@Composable -fun ContactListViewPaged( - contacts: LazyPagingItems, - channelPlaceholders: List, - selectedList: List, - onClick: (Contact) -> Unit, - onLongClick: (Contact) -> Unit, - onNodeChipClick: (Contact) -> Unit, - listState: LazyListState, - modifier: Modifier = Modifier, - channels: AppOnlyProtos.ChannelSet? = null, -) { - val haptics = LocalHapticFeedback.current - - // Handle initial loading state - if (contacts.loadState.refresh is LoadState.Loading && contacts.itemCount == 0) { - Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } - return - } - - val visiblePlaceholders = rememberVisiblePlaceholders(contacts, channelPlaceholders) - - LazyColumn(modifier = modifier.fillMaxSize(), state = listState) { - // Show channel placeholders first - items( - count = visiblePlaceholders.size, - key = { index -> "placeholder_${visiblePlaceholders[index].contactKey}" }, - ) { index -> - val placeholder = visiblePlaceholders[index] - val selected by remember { derivedStateOf { selectedList.contains(placeholder.contactKey) } } - - ContactItem( - contact = placeholder, - selected = selected, - onClick = { onClick(placeholder) }, - onLongClick = { - onLongClick(placeholder) - haptics.performHapticFeedback(HapticFeedbackType.LongPress) - }, - channels = channels, - onNodeChipClick = { onNodeChipClick(placeholder) }, - ) - } - - // Show paged contacts - items(count = contacts.itemCount, key = contacts.itemKey { it.contactKey }) { index -> - val contact = contacts[index] - if (contact != null) { - val selected by remember { derivedStateOf { selectedList.contains(contact.contactKey) } } - - ContactItem( - contact = contact, - selected = selected, - onClick = { onClick(contact) }, - onLongClick = { - onLongClick(contact) - haptics.performHapticFeedback(HapticFeedbackType.LongPress) - }, - channels = channels, - onNodeChipClick = { onNodeChipClick(contact) }, - ) - } - } - - // Loading indicator when loading more contacts - contacts.apply { - when { - loadState.append is LoadState.Loading -> { - item(key = "append_loading") { - Box(modifier = Modifier.fillMaxWidth().padding(16.dp), contentAlignment = Alignment.Center) { - CircularProgressIndicator() - } +private fun LazyListScope.contactListAppendLoadingItem(contacts: LazyPagingItems) { + contacts.apply { + when { + loadState.append is LoadState.Loading -> { + item(key = "append_loading") { + Box(modifier = Modifier.fillMaxWidth().padding(16.dp), contentAlignment = Alignment.Center) { + CircularProgressIndicator() } } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/Share.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/Share.kt index b16ec8666..688b19c35 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/sharing/Share.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/Share.kt @@ -22,7 +22,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material3.Button @@ -82,7 +82,7 @@ fun ShareScreen(contacts: List, onConfirm: (String) -> Unit, onNavigate contentPadding = PaddingValues(6.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - items(contacts, key = { it.contactKey }) { contact -> + itemsIndexed(contacts, key = { index, contact -> "${contact.contactKey}#$index" }) { _, contact -> val selected = contact.contactKey == selectedContact ContactItem( contact = contact,