mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: Unmessageable (#1858)
This commit is contained in:
parent
f74855be95
commit
87076321ba
12 changed files with 130 additions and 10 deletions
|
|
@ -28,6 +28,7 @@ import com.geeksville.mesh.database.entity.MyNodeEntity
|
|||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.NodeSortOption
|
||||
import com.geeksville.mesh.model.isUnmessageableRole
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
|
@ -96,11 +97,22 @@ class NodeRepository @Inject constructor(
|
|||
sort: NodeSortOption = NodeSortOption.LAST_HEARD,
|
||||
filter: String = "",
|
||||
includeUnknown: Boolean = true,
|
||||
includeUnmessageable: Boolean = true,
|
||||
) = nodeInfoDao.getNodes(
|
||||
sort = sort.sqlValue,
|
||||
filter = filter,
|
||||
includeUnknown = includeUnknown,
|
||||
).mapLatest { list -> list.map { it.toModel() } }.flowOn(dispatchers.io).conflate()
|
||||
).mapLatest { list ->
|
||||
list.map { it.toModel() }.filter { node ->
|
||||
val isUnmessageable: Boolean = if (node.user.hasIsUnmessagable()) {
|
||||
node.user.isUnmessagable
|
||||
} else {
|
||||
// for older firmwares
|
||||
node.user.role?.isUnmessageableRole() == true
|
||||
}
|
||||
return@filter includeUnmessageable || !isUnmessageable
|
||||
}
|
||||
}.flowOn(dispatchers.io).conflate()
|
||||
|
||||
suspend fun upsert(node: NodeEntity) = withContext(dispatchers.io) {
|
||||
nodeInfoDao.upsert(node)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
package com.geeksville.mesh.model
|
||||
|
||||
import android.graphics.Color
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.PaxcountProtos
|
||||
|
|
@ -143,3 +144,13 @@ data class Node(
|
|||
).joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
fun ConfigProtos.Config.DeviceConfig.Role?.isUnmessageableRole(): Boolean = this in listOf(
|
||||
ConfigProtos.Config.DeviceConfig.Role.REPEATER,
|
||||
ConfigProtos.Config.DeviceConfig.Role.ROUTER,
|
||||
ConfigProtos.Config.DeviceConfig.Role.ROUTER_LATE,
|
||||
ConfigProtos.Config.DeviceConfig.Role.SENSOR,
|
||||
ConfigProtos.Config.DeviceConfig.Role.TRACKER,
|
||||
ConfigProtos.Config.DeviceConfig.Role.TAK,
|
||||
ConfigProtos.Config.DeviceConfig.Role.TAK_TRACKER,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ data class NodesUiState(
|
|||
val sort: NodeSortOption = NodeSortOption.LAST_HEARD,
|
||||
val filter: String = "",
|
||||
val includeUnknown: Boolean = false,
|
||||
val includeUnmessageable: Boolean = true,
|
||||
val gpsFormat: Int = 0,
|
||||
val distanceUnits: Int = 0,
|
||||
val tempInFahrenheit: Boolean = false,
|
||||
|
|
@ -166,6 +167,7 @@ data class Contact(
|
|||
val unreadCount: Int,
|
||||
val messageCount: Int,
|
||||
val isMuted: Boolean,
|
||||
val isUnmessageable: Boolean,
|
||||
)
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -261,6 +263,8 @@ class UIViewModel @Inject constructor(
|
|||
private val nodeSortOption = MutableStateFlow(NodeSortOption.LAST_HEARD)
|
||||
private val includeUnknown = MutableStateFlow(preferences.getBoolean("include-unknown", false))
|
||||
private val showDetails = MutableStateFlow(preferences.getBoolean("show-details", false))
|
||||
private val includeUnmessageable =
|
||||
MutableStateFlow(preferences.getBoolean("include-unmessageable", true))
|
||||
|
||||
fun setSortOption(sort: NodeSortOption) {
|
||||
nodeSortOption.value = sort
|
||||
|
|
@ -271,6 +275,11 @@ class UIViewModel @Inject constructor(
|
|||
preferences.edit { putBoolean("show-details", showDetails.value) }
|
||||
}
|
||||
|
||||
fun toggleIncludeUnmessageable() {
|
||||
includeUnmessageable.value = !includeUnmessageable.value
|
||||
preferences.edit { putBoolean("include-unmessageable", includeUnmessageable.value) }
|
||||
}
|
||||
|
||||
fun toggleIncludeUnknown() {
|
||||
includeUnknown.value = !includeUnknown.value
|
||||
preferences.edit { putBoolean("include-unknown", includeUnknown.value) }
|
||||
|
|
@ -292,6 +301,8 @@ class UIViewModel @Inject constructor(
|
|||
tempInFahrenheit = profile.moduleConfig.telemetry.environmentDisplayFahrenheit,
|
||||
showDetails = showDetails,
|
||||
)
|
||||
}.combine(includeUnmessageable) { state, includeUnmessageable ->
|
||||
state.copy(includeUnmessageable = includeUnmessageable)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
|
|
@ -299,7 +310,7 @@ class UIViewModel @Inject constructor(
|
|||
)
|
||||
|
||||
val nodeList: StateFlow<List<Node>> = nodesUiState.flatMapLatest { state ->
|
||||
nodeDB.getNodes(state.sort, state.filter, state.includeUnknown)
|
||||
nodeDB.getNodes(state.sort, state.filter, state.includeUnknown, state.includeUnmessageable)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
|
|
@ -395,6 +406,7 @@ class UIViewModel @Inject constructor(
|
|||
unreadCount = packetRepository.getUnreadCount(contactKey),
|
||||
messageCount = packetRepository.getMessageCount(contactKey),
|
||||
isMuted = settings[contactKey]?.isMuted == true,
|
||||
isUnmessageable = user.isUnmessagable,
|
||||
)
|
||||
}
|
||||
}.stateIn(
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ private fun ContactItemPreview() {
|
|||
unreadCount = 2,
|
||||
messageCount = 10,
|
||||
isMuted = true,
|
||||
isUnmessageable = false,
|
||||
),
|
||||
selected = false,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ import androidx.compose.material.icons.filled.Verified
|
|||
import androidx.compose.material.icons.filled.WaterDrop
|
||||
import androidx.compose.material.icons.filled.Work
|
||||
import androidx.compose.material.icons.outlined.Navigation
|
||||
import androidx.compose.material.icons.outlined.NoCell
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -95,6 +96,7 @@ import com.geeksville.mesh.model.MetricsState
|
|||
import com.geeksville.mesh.model.MetricsViewModel
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.model.isUnmessageableRole
|
||||
import com.geeksville.mesh.navigation.Route
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.preview.NodePreviewParameterProvider
|
||||
|
|
@ -341,6 +343,7 @@ fun DeviceHardwareImage(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun NodeDetailsContent(
|
||||
node: Node,
|
||||
|
|
@ -382,6 +385,18 @@ private fun NodeDetailsContent(
|
|||
icon = Icons.Default.Work,
|
||||
value = node.user.role.name
|
||||
)
|
||||
val unmessageable = if (node.user.hasIsUnmessagable()) {
|
||||
node.user.isUnmessagable
|
||||
} else {
|
||||
node.user.role?.isUnmessageableRole() == true
|
||||
}
|
||||
if (unmessageable) {
|
||||
NodeDetailRow(
|
||||
label = stringResource(R.string.unmonitored_or_infrastructure),
|
||||
icon = Icons.Outlined.NoCell,
|
||||
value = ""
|
||||
)
|
||||
}
|
||||
if (node.deviceMetrics.uptimeSeconds > 0) {
|
||||
NodeDetailRow(
|
||||
label = stringResource(R.string.uptime),
|
||||
|
|
|
|||
|
|
@ -30,10 +30,13 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.NoCell
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.AssistChipDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -58,6 +61,7 @@ import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
|
|||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.isUnmessageableRole
|
||||
import com.geeksville.mesh.ui.components.NodeKeyStatusIcon
|
||||
import com.geeksville.mesh.ui.components.NodeMenu
|
||||
import com.geeksville.mesh.ui.components.NodeMenuAction
|
||||
|
|
@ -110,7 +114,11 @@ fun NodeItem(
|
|||
}
|
||||
|
||||
val (detailsShown, showDetails) = remember { mutableStateOf(expanded) }
|
||||
|
||||
val unmessageable = if (thatNode.user.hasIsUnmessagable()) {
|
||||
thatNode.user.isUnmessagable
|
||||
} else {
|
||||
thatNode.user.role?.isUnmessageableRole() == true
|
||||
}
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -183,6 +191,21 @@ fun NodeItem(
|
|||
currentTimeMillis = currentTimeMillis
|
||||
)
|
||||
}
|
||||
if (unmessageable) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.NoCell,
|
||||
contentDescription = stringResource(R.string.unmessageable),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.unmonitored_or_infrastructure)
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
|
|
|
|||
|
|
@ -95,6 +95,8 @@ fun NodeScreen(
|
|||
onToggleIncludeUnknown = model::toggleIncludeUnknown,
|
||||
showDetails = state.showDetails,
|
||||
onToggleShowDetails = model::toggleShowDetails,
|
||||
includeUnmessageable = state.includeUnmessageable,
|
||||
onToggleIncludeUnmessageable = model::toggleIncludeUnmessageable
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ private fun ShareScreenPreview() {
|
|||
unreadCount = 2,
|
||||
messageCount = 10,
|
||||
isMuted = true,
|
||||
isUnmessageable = false,
|
||||
),
|
||||
),
|
||||
onConfirm = {},
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ fun NodeFilterTextField(
|
|||
onToggleIncludeUnknown: () -> Unit,
|
||||
showDetails: Boolean,
|
||||
onToggleShowDetails: () -> Unit,
|
||||
includeUnmessageable: Boolean,
|
||||
onToggleIncludeUnmessageable: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.background(MaterialTheme.colorScheme.background),
|
||||
|
|
@ -88,6 +90,8 @@ fun NodeFilterTextField(
|
|||
onToggleIncludeUnknown = onToggleIncludeUnknown,
|
||||
showDetails = showDetails,
|
||||
onToggleShowDetails = onToggleShowDetails,
|
||||
includeUnmessageable = includeUnmessageable,
|
||||
onToggleIncludeUnmessageable = onToggleIncludeUnmessageable
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -154,6 +158,8 @@ private fun NodeSortButton(
|
|||
onToggleIncludeUnknown: () -> Unit,
|
||||
showDetails: Boolean,
|
||||
onToggleShowDetails: () -> Unit,
|
||||
onToggleIncludeUnmessageable: () -> Unit,
|
||||
includeUnmessageable: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) = Box(modifier) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
|
@ -228,6 +234,27 @@ private fun NodeSortButton(
|
|||
}
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
onToggleIncludeUnmessageable()
|
||||
expanded = false
|
||||
},
|
||||
text = {
|
||||
Row {
|
||||
AnimatedVisibility(visible = includeUnmessageable) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Done,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(id = R.string.node_filter_include_unmessageable),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -245,6 +272,8 @@ private fun NodeFilterTextFieldPreview() {
|
|||
onToggleIncludeUnknown = {},
|
||||
showDetails = false,
|
||||
onToggleShowDetails = {},
|
||||
includeUnmessageable = false,
|
||||
onToggleIncludeUnmessageable = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import androidx.compose.ui.unit.dp
|
|||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.isUnmessageableRole
|
||||
import com.geeksville.mesh.ui.supportsQrCodeSharing
|
||||
|
||||
@Suppress("LongMethod")
|
||||
|
|
@ -49,6 +50,12 @@ fun NodeMenu(
|
|||
onAction: (NodeMenuAction) -> Unit,
|
||||
firmwareVersion: String? = null,
|
||||
) {
|
||||
val isUnmessageable = if (node.user.hasIsUnmessagable()) {
|
||||
node.user.isUnmessagable
|
||||
} else {
|
||||
// for older firmwares
|
||||
node.user.role?.isUnmessageableRole() == true
|
||||
}
|
||||
var displayFavoriteDialog by remember { mutableStateOf(false) }
|
||||
var displayIgnoreDialog by remember { mutableStateOf(false) }
|
||||
var displayRemoveDialog by remember { mutableStateOf(false) }
|
||||
|
|
@ -104,13 +111,15 @@ fun NodeMenu(
|
|||
) {
|
||||
|
||||
if (showFullMenu) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
onAction(NodeMenuAction.DirectMessage(node))
|
||||
},
|
||||
text = { Text(stringResource(R.string.direct_message)) }
|
||||
)
|
||||
if (!isUnmessageable) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
onAction(NodeMenuAction.DirectMessage(node))
|
||||
},
|
||||
text = { Text(stringResource(R.string.direct_message)) }
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
package com.geeksville.mesh.ui.preview
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import com.geeksville.mesh.DeviceMetrics.Companion.currentTime
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.deviceMetrics
|
||||
|
|
@ -37,6 +38,7 @@ class NodePreviewParameterProvider : PreviewParameterProvider<Node> {
|
|||
longName = "Mickey Mouse"
|
||||
shortName = "MM"
|
||||
hwModel = MeshProtos.HardwareModel.TBEAM
|
||||
role = ConfigProtos.Config.DeviceConfig.Role.ROUTER
|
||||
},
|
||||
position = position {
|
||||
latitudeI = 338125110
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue