mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
fix: duplicate LazyColumn keys for broadcast contacts (#3840)
This commit is contained in:
parent
1c0dc486e2
commit
af9a837511
2 changed files with 126 additions and 91 deletions
|
|
@ -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<String>,
|
||||
contactSettings: Map<String, ContactSettings>,
|
||||
|
|
@ -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<Contact>,
|
||||
private fun ContactListViewPaged(
|
||||
contacts: LazyPagingItems<Contact>,
|
||||
channelPlaceholders: List<Contact>,
|
||||
selectedList: List<String>,
|
||||
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<Contact>,
|
||||
visiblePlaceholders: List<Contact>,
|
||||
selectedList: List<String>,
|
||||
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<Contact>,
|
||||
selectedList: List<String>,
|
||||
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<Contact>,
|
||||
selectedList: List<String>,
|
||||
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<Contact>,
|
||||
channelPlaceholders: List<Contact>,
|
||||
selectedList: List<String>,
|
||||
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<Contact>) {
|
||||
contacts.apply {
|
||||
when {
|
||||
loadState.append is LoadState.Loading -> {
|
||||
item(key = "append_loading") {
|
||||
Box(modifier = Modifier.fillMaxWidth().padding(16.dp), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Contact>, 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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue