mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Feat/2594 contact shortname on click (#2614)
This commit is contained in:
parent
4b182be500
commit
7a109a747e
4 changed files with 104 additions and 128 deletions
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue