Feat/2594 contact shortname on click (#2614)

This commit is contained in:
DaneEvans 2025-08-02 23:44:24 +10:00 committed by GitHub
parent 4b182be500
commit 7a109a747e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 104 additions and 128 deletions

View file

@ -45,7 +45,11 @@ sealed class ContactsRoutes {
fun NavGraphBuilder.contactsGraph(navController: NavHostController, uiViewModel: UIViewModel) {
navigation<ContactsRoutes.ContactsGraph>(startDestination = ContactsRoutes.Contacts) {
composable<ContactsRoutes.Contacts> {
ContactsScreen(uiViewModel, onNavigateToMessages = { navController.navigate(ContactsRoutes.Messages(it)) })
ContactsScreen(
uiViewModel,
onNavigateToMessages = { navController.navigate(ContactsRoutes.Messages(it)) },
onNavigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) },
)
}
composable<ContactsRoutes.Messages>(
deepLinks =

View file

@ -124,8 +124,8 @@ enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector,
companion object {
fun NavDestination.isTopLevel(): Boolean = listOf<Route>(
NodesRoutes.Nodes,
ContactsRoutes.Contacts,
NodesRoutes.Nodes,
MapRoutes.Map,
ChannelsRoutes.Channels,
ConnectionsRoutes.Connections,

View file

@ -50,8 +50,8 @@ import androidx.compose.ui.unit.dp
import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.R
import com.geeksville.mesh.model.Contact
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.components.SecurityIcon
import com.geeksville.mesh.ui.common.theme.AppTheme
@Suppress("LongMethod")
@Composable
@ -61,39 +61,32 @@ fun ContactItem(
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
onLongClick: () -> Unit = {},
onNodeChipClick: () -> Unit = {},
channels: AppOnlyProtos.ChannelSet? = null,
) = with(contact) {
Card(
modifier = modifier
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
)
modifier =
modifier
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
.background(color = if (selected) Color.Gray else MaterialTheme.colorScheme.background)
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 6.dp),
shape = RoundedCornerShape(12.dp),
) {
val colors = if (contact.nodeColors != null) {
AssistChipDefaults.assistChipColors(
labelColor = Color(contact.nodeColors.first),
containerColor = Color(contact.nodeColors.second),
)
} else {
AssistChipDefaults.assistChipColors()
}
val colors =
if (contact.nodeColors != null) {
AssistChipDefaults.assistChipColors(
labelColor = Color(contact.nodeColors.first),
containerColor = Color(contact.nodeColors.second),
)
} else {
AssistChipDefaults.assistChipColors()
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Row(modifier = Modifier.fillMaxWidth().padding(8.dp), verticalAlignment = Alignment.CenterVertically) {
AssistChip(
onClick = { },
modifier = Modifier
.padding(end = 8.dp)
.width(72.dp),
onClick = onNodeChipClick,
modifier = Modifier.padding(end = 8.dp).width(72.dp),
label = {
Text(
text = shortName,
@ -103,29 +96,20 @@ fun ContactItem(
textAlign = TextAlign.Center,
)
},
colors = colors
colors = colors,
)
Column(
modifier = Modifier.weight(1f),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = longName,
modifier = Modifier.weight(1f)
)
Column(modifier = Modifier.weight(1f)) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(text = longName, modifier = Modifier.weight(1f))
Row(verticalAlignment = Alignment.CenterVertically) {
// Show unlock icon for broadcast with default PSK
val isBroadcast = contact.contactKey.getOrNull(1) == '^' ||
contact.contactKey.endsWith("^all") ||
contact.contactKey.endsWith("^broadcast")
val isBroadcast =
contact.contactKey.getOrNull(1) == '^' ||
contact.contactKey.endsWith("^all") ||
contact.contactKey.endsWith("^broadcast")
if (isBroadcast && channels != null) {
val channelIndex = contact.contactKey[0].digitToIntOrNull()
channelIndex?.let { index ->
SecurityIcon(channels, index)
}
channelIndex?.let { index -> SecurityIcon(channels, index) }
Spacer(modifier = Modifier.width(8.dp))
}
Text(
@ -137,9 +121,7 @@ fun ContactItem(
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
@ -152,19 +134,13 @@ fun ContactItem(
maxLines = 2,
)
AnimatedVisibility(visible = isMuted) {
Icon(
imageVector = Icons.AutoMirrored.TwoTone.VolumeOff,
contentDescription = null,
)
Icon(imageVector = Icons.AutoMirrored.TwoTone.VolumeOff, contentDescription = null)
}
AnimatedVisibility(visible = unreadCount > 0) {
Text(
text = unreadCount.toString(),
modifier = Modifier
.background(
MaterialTheme.colorScheme.primary,
shape = CircleShape
)
modifier =
Modifier.background(MaterialTheme.colorScheme.primary, shape = CircleShape)
.padding(horizontal = 6.dp, vertical = 3.dp),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.bodySmall,
@ -181,7 +157,8 @@ fun ContactItem(
private fun ContactItemPreview() {
AppTheme {
ContactItem(
contact = Contact(
contact =
Contact(
contactKey = "0^all",
shortName = stringResource(R.string.some_username),
longName = stringResource(R.string.unknown_username),
@ -190,7 +167,7 @@ private fun ContactItemPreview() {
unreadCount = 2,
messageCount = 10,
isMuted = true,
isUnmessageable = false
isUnmessageable = false,
),
selected = false,
)

View file

@ -68,7 +68,8 @@ import java.util.concurrent.TimeUnit
@Composable
fun ContactsScreen(
uiViewModel: UIViewModel = hiltViewModel(),
onNavigateToMessages: (String) -> Unit = {}
onNavigateToMessages: (String) -> Unit = {},
onNavigateToNodeDetails: (Int) -> Unit = {},
) {
var showMuteDialog by remember { mutableStateOf(false) }
var showDeleteDialog by remember { mutableStateOf(false) }
@ -81,9 +82,8 @@ fun ContactsScreen(
val contacts by uiViewModel.contactList.collectAsStateWithLifecycle()
// Derived state for selected contacts and count
val selectedContacts = remember(contacts, selectedContactKeys) {
contacts.filter { it.contactKey in selectedContactKeys }
}
val selectedContacts =
remember(contacts, selectedContactKeys) { contacts.filter { it.contactKey in selectedContactKeys } }
val selectedCount = remember(selectedContacts) { selectedContacts.sumOf { it.messageCount } }
val isAllMuted = remember(selectedContacts) { selectedContacts.all { it.isMuted } }
@ -102,6 +102,21 @@ fun ContactsScreen(
}
}
val onNodeChipClick: (Contact) -> Unit = { contact ->
if (contact.contactKey.contains("!")) {
// if it's a node, look up the nodeNum including the !
val nodeKey = contact.contactKey.substring(1)
val node = uiViewModel.getNode(nodeKey)
if (node != null) {
// navigate to node details.
onNavigateToNodeDetails(node.num)
}
} else {
// Channels
}
}
val onContactLongClick: (Contact) -> Unit = { contact ->
// Enter selection mode and select the item on long press
if (!isSelectionModeActive) {
@ -122,20 +137,16 @@ fun ContactsScreen(
SelectionToolbar(
selectedCount = selectedContactKeys.size,
onCloseSelection = { selectedContactKeys.clear() },
onMuteSelected = {
showMuteDialog = true
},
onDeleteSelected = {
showDeleteDialog = true
},
onMuteSelected = { showMuteDialog = true },
onDeleteSelected = { showDeleteDialog = true },
onSelectAll = {
selectedContactKeys.clear()
selectedContactKeys.addAll(contacts.map { it.contactKey })
},
isAllMuted = isAllMuted // Pass the derived state
isAllMuted = isAllMuted, // Pass the derived state
)
}
}
},
) { paddingValues ->
val channels by uiViewModel.channels.collectAsStateWithLifecycle()
ContactListView(
@ -144,7 +155,8 @@ fun ContactsScreen(
onClick = onContactClick,
onLongClick = onContactLongClick,
contentPadding = paddingValues,
channels = channels
channels = channels,
onNodeChipClick = onNodeChipClick,
)
}
DeleteConfirmationDialog(
@ -155,7 +167,7 @@ fun ContactsScreen(
showDeleteDialog = false
uiViewModel.deleteContacts(selectedContactKeys.toList())
selectedContactKeys.clear()
}
},
)
MuteNotificationsDialog(
@ -165,7 +177,7 @@ fun ContactsScreen(
showMuteDialog = false
uiViewModel.setMuteUntil(selectedContactKeys.toList(), muteUntil)
selectedContactKeys.clear()
}
},
)
}
@ -174,7 +186,7 @@ fun ContactsScreen(
fun MuteNotificationsDialog(
showDialog: Boolean,
onDismiss: () -> Unit,
onConfirm: (Long) -> Unit // Lambda to handle the confirmed mute duration
onConfirm: (Long) -> Unit, // Lambda to handle the confirmed mute duration
) {
if (showDialog) {
// Options for mute duration
@ -183,7 +195,7 @@ fun MuteNotificationsDialog(
R.string.unmute to 0L,
R.string.mute_8_hours to TimeUnit.HOURS.toMillis(8),
R.string.mute_1_week to TimeUnit.DAYS.toMillis(7),
R.string.mute_always to Long.MAX_VALUE
R.string.mute_always to Long.MAX_VALUE,
)
}
@ -192,32 +204,21 @@ fun MuteNotificationsDialog(
AlertDialog(
onDismissRequest = onDismiss, // Dismiss the dialog when clicked outside
title = {
Text(text = stringResource(R.string.mute_notifications))
},
title = { Text(text = stringResource(R.string.mute_notifications)) },
text = {
Column {
muteOptions.forEachIndexed { index, (stringRes, _) ->
val isSelected = index == selectedOptionIndex
val text = stringResource(stringRes)
Row(
modifier = Modifier
.fillMaxWidth()
.selectable(
selected = isSelected,
onClick = { selectedOptionIndex = index }
)
modifier =
Modifier.fillMaxWidth()
.selectable(selected = isSelected, onClick = { selectedOptionIndex = index })
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(
selected = isSelected,
onClick = { selectedOptionIndex = index }
)
Text(
text = text,
modifier = Modifier.padding(start = 8.dp)
)
RadioButton(selected = isSelected, onClick = { selectedOptionIndex = index })
Text(text = text, modifier = Modifier.padding(start = 8.dp))
}
}
}
@ -228,18 +229,18 @@ fun MuteNotificationsDialog(
val selectedMuteDuration = muteOptions[selectedOptionIndex].second
onConfirm(selectedMuteDuration)
onDismiss() // Dismiss the dialog after confirming
}
},
) {
Text(stringResource(R.string.okay))
}
},
dismissButton = {
Button(
onClick = onDismiss // Dismiss the dialog on cancel
onClick = onDismiss, // Dismiss the dialog on cancel
) {
Text(stringResource(R.string.cancel))
}
}
},
)
}
}
@ -249,44 +250,38 @@ fun DeleteConfirmationDialog(
showDialog: Boolean,
selectedCount: Int, // Number of items to be deleted
onDismiss: () -> Unit,
onConfirm: () -> Unit // Lambda to handle the delete action
onConfirm: () -> Unit, // Lambda to handle the delete action
) {
if (showDialog) {
val deleteMessage = pluralStringResource(
id = R.plurals.delete_messages,
count = selectedCount,
formatArgs = arrayOf(selectedCount) // Pass the count as a format argument
)
val deleteMessage =
pluralStringResource(
id = R.plurals.delete_messages,
count = selectedCount,
formatArgs = arrayOf(selectedCount), // Pass the count as a format argument
)
AlertDialog(
onDismissRequest = onDismiss,
title = {
// Optional: You could add a title here if needed, e.g., "Confirm Deletion"
},
text = {
Text(text = deleteMessage)
},
text = { Text(text = deleteMessage) },
confirmButton = {
Button(
onClick = {
onConfirm()
onDismiss() // Dismiss the dialog after confirming
}
},
) {
Text(stringResource(R.string.delete))
}
},
dismissButton = {
Button(
onClick = onDismiss
) {
Text(stringResource(R.string.cancel))
}
},
properties = DialogProperties(
dismissButton = { Button(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } },
properties =
DialogProperties(
dismissOnClickOutside = true, // Allow dismissing by clicking outside
dismissOnBackPress = true // Allow dismissing with the back button
)
dismissOnBackPress = true, // Allow dismissing with the back button
),
)
}
}
@ -299,7 +294,7 @@ fun SelectionToolbar(
onMuteSelected: () -> Unit,
onDeleteSelected: () -> Unit,
onSelectAll: () -> Unit,
isAllMuted: Boolean
isAllMuted: Boolean,
) {
TopAppBar(
title = { Text(text = "$selectedCount") },
@ -311,16 +306,18 @@ fun SelectionToolbar(
actions = {
IconButton(onClick = onMuteSelected) {
Icon(
imageVector = if (isAllMuted) {
imageVector =
if (isAllMuted) {
Icons.AutoMirrored.TwoTone.VolumeUp
} else {
Icons.AutoMirrored.TwoTone.VolumeMute
},
contentDescription = if (isAllMuted) {
contentDescription =
if (isAllMuted) {
"Unmute selected"
} else {
"Mute selected"
}
},
)
}
IconButton(onClick = onDeleteSelected) {
@ -329,7 +326,7 @@ fun SelectionToolbar(
IconButton(onClick = onSelectAll) {
Icon(Icons.Default.SelectAll, contentDescription = stringResource(R.string.select_all))
}
}
},
)
}
@ -340,14 +337,11 @@ fun ContactListView(
onClick: (Contact) -> Unit,
onLongClick: (Contact) -> Unit,
contentPadding: PaddingValues,
channels: AppOnlyProtos.ChannelSet? = null
channels: AppOnlyProtos.ChannelSet? = null,
onNodeChipClick: (Contact) -> Unit,
) {
val haptics = LocalHapticFeedback.current
LazyColumn(
modifier = Modifier
.fillMaxSize(),
contentPadding = contentPadding,
) {
LazyColumn(modifier = Modifier.fillMaxSize(), contentPadding = contentPadding) {
items(contacts, key = { it.contactKey }) { contact ->
val selected by remember { derivedStateOf { selectedList.contains(contact.contactKey) } }
@ -359,7 +353,8 @@ fun ContactListView(
onLongClick(contact)
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
},
channels = channels
channels = channels,
onNodeChipClick = { onNodeChipClick(contact) },
)
}
}