diff --git a/app/src/main/java/com/geeksville/mesh/navigation/ContactsRoutes.kt b/app/src/main/java/com/geeksville/mesh/navigation/ContactsRoutes.kt index 2a4cfad72..0a5bac7cc 100644 --- a/app/src/main/java/com/geeksville/mesh/navigation/ContactsRoutes.kt +++ b/app/src/main/java/com/geeksville/mesh/navigation/ContactsRoutes.kt @@ -45,7 +45,11 @@ sealed class ContactsRoutes { fun NavGraphBuilder.contactsGraph(navController: NavHostController, uiViewModel: UIViewModel) { navigation(startDestination = ContactsRoutes.Contacts) { composable { - ContactsScreen(uiViewModel, onNavigateToMessages = { navController.navigate(ContactsRoutes.Messages(it)) }) + ContactsScreen( + uiViewModel, + onNavigateToMessages = { navController.navigate(ContactsRoutes.Messages(it)) }, + onNavigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) }, + ) } composable( deepLinks = diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt index c96eb7ebe..9e7b44ac4 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -124,8 +124,8 @@ enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector, companion object { fun NavDestination.isTopLevel(): Boolean = listOf( - NodesRoutes.Nodes, ContactsRoutes.Contacts, + NodesRoutes.Nodes, MapRoutes.Map, ChannelsRoutes.Channels, ConnectionsRoutes.Connections, diff --git a/app/src/main/java/com/geeksville/mesh/ui/contact/ContactItem.kt b/app/src/main/java/com/geeksville/mesh/ui/contact/ContactItem.kt index 2f81890a0..07548fff3 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/contact/ContactItem.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/contact/ContactItem.kt @@ -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, ) 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 ab7396e64..9580b8157 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 @@ -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) }, ) } }