mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: add dialog for message status information
This commit is contained in:
parent
056f6b28cf
commit
a075dfbd3a
16 changed files with 803 additions and 103 deletions
|
|
@ -36,8 +36,9 @@ import com.geeksville.mesh.database.entity.QuickChatAction
|
|||
AutoMigration (from = 7, to = 8),
|
||||
AutoMigration (from = 8, to = 9),
|
||||
AutoMigration (from = 9, to = 10),
|
||||
AutoMigration (from = 10, to = 11),
|
||||
],
|
||||
version = 10,
|
||||
version = 11,
|
||||
exportSchema = true,
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
|
|
|
|||
|
|
@ -50,8 +50,8 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Laz
|
|||
packetDao.updateMessageId(d, id)
|
||||
}
|
||||
|
||||
suspend fun getDataPacketById(requestId: Int) = withContext(Dispatchers.IO) {
|
||||
packetDao.getDataPacketById(requestId)
|
||||
suspend fun getPacketById(requestId: Int) = withContext(Dispatchers.IO) {
|
||||
packetDao.getPacketById(requestId)
|
||||
}
|
||||
|
||||
suspend fun deleteMessages(uuidList: List<Long>) = withContext(Dispatchers.IO) {
|
||||
|
|
|
|||
|
|
@ -130,10 +130,15 @@ interface PacketDao {
|
|||
)
|
||||
fun getDataPackets(): List<DataPacket>
|
||||
|
||||
@Transaction
|
||||
fun getDataPacketById(requestId: Int): DataPacket? {
|
||||
return getDataPackets().lastOrNull { it.id == requestId }
|
||||
}
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM MyNodeInfo))
|
||||
AND packet_id = :requestId
|
||||
ORDER BY received_time DESC
|
||||
"""
|
||||
)
|
||||
fun getPacketById(requestId: Int): Packet?
|
||||
|
||||
@Transaction
|
||||
fun getQueuedPackets(): List<DataPacket>? =
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ data class Packet(
|
|||
@ColumnInfo(name = "contact_key") val contact_key: String,
|
||||
@ColumnInfo(name = "received_time") val received_time: Long,
|
||||
@ColumnInfo(name = "read", defaultValue = "1") val read: Boolean,
|
||||
@ColumnInfo(name = "data") val data: DataPacket
|
||||
@ColumnInfo(name = "data") val data: DataPacket,
|
||||
@ColumnInfo(name = "packet_id", defaultValue = "0") val packetId: Int = 0,
|
||||
@ColumnInfo(name = "routing_error", defaultValue = "-1") var routingError: Int = -1,
|
||||
)
|
||||
|
||||
@Entity(tableName = "contact_settings")
|
||||
|
|
|
|||
52
app/src/main/java/com/geeksville/mesh/model/Message.kt
Normal file
52
app/src/main/java/com/geeksville/mesh/model/Message.kt
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package com.geeksville.mesh.model
|
||||
|
||||
import com.geeksville.mesh.MeshProtos.Routing
|
||||
import com.geeksville.mesh.MeshUser
|
||||
import com.geeksville.mesh.MessageStatus
|
||||
import com.geeksville.mesh.R
|
||||
|
||||
val Routing.Error.stringRes: Int
|
||||
get() = when (this) {
|
||||
Routing.Error.NONE -> R.string.routing_error_none
|
||||
Routing.Error.NO_ROUTE -> R.string.routing_error_no_route
|
||||
Routing.Error.GOT_NAK -> R.string.routing_error_got_nak
|
||||
Routing.Error.TIMEOUT -> R.string.routing_error_timeout
|
||||
Routing.Error.NO_INTERFACE -> R.string.routing_error_no_interface
|
||||
Routing.Error.MAX_RETRANSMIT -> R.string.routing_error_max_retransmit
|
||||
Routing.Error.NO_CHANNEL -> R.string.routing_error_no_channel
|
||||
Routing.Error.TOO_LARGE -> R.string.routing_error_too_large
|
||||
Routing.Error.NO_RESPONSE -> R.string.routing_error_no_response
|
||||
Routing.Error.DUTY_CYCLE_LIMIT -> R.string.routing_error_duty_cycle_limit
|
||||
Routing.Error.BAD_REQUEST -> R.string.routing_error_bad_request
|
||||
Routing.Error.NOT_AUTHORIZED -> R.string.routing_error_not_authorized
|
||||
Routing.Error.PKI_FAILED -> R.string.routing_error_pki_failed
|
||||
Routing.Error.PKI_UNKNOWN_PUBKEY -> R.string.routing_error_pki_unknown_pubkey
|
||||
else -> R.string.unrecognized
|
||||
}
|
||||
|
||||
data class Message(
|
||||
val uuid: Long,
|
||||
val receivedTime: Long,
|
||||
val user: MeshUser,
|
||||
val text: String,
|
||||
val time: Long,
|
||||
val read: Boolean,
|
||||
val status: MessageStatus?,
|
||||
val routingError: Int,
|
||||
) {
|
||||
private fun getStatusStringRes(value: Int): Int {
|
||||
val error = Routing.Error.forNumber(value) ?: Routing.Error.UNRECOGNIZED
|
||||
return error.stringRes
|
||||
}
|
||||
|
||||
fun getStatusStringRes(): Pair<Int, Int> {
|
||||
val title = if (routingError > 0) R.string.error else R.string.message_delivery_status
|
||||
val text = when (status) {
|
||||
MessageStatus.RECEIVED -> R.string.delivery_confirmed
|
||||
MessageStatus.QUEUED -> R.string.message_status_queued
|
||||
MessageStatus.ENROUTE -> R.string.message_status_enroute
|
||||
else -> getStatusStringRes(routingError)
|
||||
}
|
||||
return title to text
|
||||
}
|
||||
}
|
||||
|
|
@ -463,7 +463,7 @@ class RadioConfigViewModel @Inject constructor(
|
|||
val parsed = MeshProtos.Routing.parseFrom(data.payload)
|
||||
debug(debugMsg.format(parsed.errorReason.name))
|
||||
if (parsed.errorReason != MeshProtos.Routing.Error.NONE) {
|
||||
setResponseStateError(parsed.errorReason.name)
|
||||
setResponseStateError(app.getString(parsed.errorReason.stringRes))
|
||||
} else if (packet.from == destNum && route.isEmpty()) {
|
||||
requestIds.update { it.apply { remove(data.requestId) } }
|
||||
if (requestIds.value.isEmpty()) setResponseStateSuccess()
|
||||
|
|
|
|||
|
|
@ -130,16 +130,6 @@ data class Contact(
|
|||
val isMuted: Boolean,
|
||||
)
|
||||
|
||||
data class Message(
|
||||
val uuid: Long,
|
||||
val receivedTime: Long,
|
||||
val user: MeshUser,
|
||||
val text: String,
|
||||
val time: Long,
|
||||
val read: Boolean,
|
||||
val status: MessageStatus?,
|
||||
)
|
||||
|
||||
// return time if within 24 hours, otherwise date
|
||||
internal fun getShortDateTime(time: Long): String? {
|
||||
val date = if (time != 0L) Date(time) else return null
|
||||
|
|
@ -181,7 +171,6 @@ class UIViewModel @Inject constructor(
|
|||
|
||||
private val _channels = MutableStateFlow(channelSet {})
|
||||
val channels: StateFlow<AppOnlyProtos.ChannelSet> get() = _channels
|
||||
val channelSet get() = channels.value
|
||||
|
||||
private val _quickChatActions = MutableStateFlow<List<QuickChatAction>>(emptyList())
|
||||
val quickChatActions: StateFlow<List<QuickChatAction>> = _quickChatActions
|
||||
|
|
@ -339,6 +328,7 @@ class UIViewModel @Inject constructor(
|
|||
time = it.data.time,
|
||||
read = it.read,
|
||||
status = it.data.status,
|
||||
routingError = it.routingError,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -601,13 +601,14 @@ class MeshService : Service(), Logging {
|
|||
val contactKey = "${dataPacket.channel}$contactId"
|
||||
|
||||
val packetToSave = Packet(
|
||||
0L, // autoGenerated
|
||||
myNodeNum,
|
||||
dataPacket.dataType,
|
||||
contactKey,
|
||||
System.currentTimeMillis(),
|
||||
fromLocal,
|
||||
dataPacket
|
||||
uuid = 0L, // autoGenerated
|
||||
myNodeNum = myNodeNum,
|
||||
packetId = dataPacket.id,
|
||||
port_num = dataPacket.dataType,
|
||||
contact_key = contactKey,
|
||||
received_time = System.currentTimeMillis(),
|
||||
read = fromLocal,
|
||||
data = dataPacket
|
||||
)
|
||||
serviceScope.handledLaunch {
|
||||
packetRepository.get().apply {
|
||||
|
|
@ -686,13 +687,12 @@ class MeshService : Service(), Logging {
|
|||
// We always send ACKs to other apps, because they might care about the messages they sent
|
||||
shouldBroadcast = true
|
||||
val u = MeshProtos.Routing.parseFrom(data.payload)
|
||||
val isAck = u.errorReasonValue == MeshProtos.Routing.Error.NONE_VALUE
|
||||
|
||||
if (u.errorReason == MeshProtos.Routing.Error.DUTY_CYCLE_LIMIT) {
|
||||
radioConfigRepository.setErrorMessage(getString(R.string.error_duty_cycle))
|
||||
}
|
||||
|
||||
handleAckNak(isAck, fromId, data.requestId)
|
||||
handleAckNak(data.requestId, fromId, u.errorReasonValue)
|
||||
queueResponse.remove(data.requestId)?.complete(true)
|
||||
}
|
||||
|
||||
|
|
@ -1011,7 +1011,7 @@ class MeshService : Service(), Logging {
|
|||
private suspend fun getDataPacketById(packetId: Int): DataPacket? = withTimeoutOrNull(1000) {
|
||||
var dataPacket: DataPacket? = null
|
||||
while (dataPacket == null) {
|
||||
dataPacket = packetRepository.get().getDataPacketById(packetId)
|
||||
dataPacket = packetRepository.get().getPacketById(packetId)?.data
|
||||
if (dataPacket == null) delay(100)
|
||||
}
|
||||
dataPacket
|
||||
|
|
@ -1031,14 +1031,21 @@ class MeshService : Service(), Logging {
|
|||
/**
|
||||
* Handle an ack/nak packet by updating sent message status
|
||||
*/
|
||||
private fun handleAckNak(isAck: Boolean, fromId: String, requestId: Int) {
|
||||
private fun handleAckNak(requestId: Int, fromId: String, routingError: Int) {
|
||||
serviceScope.handledLaunch {
|
||||
val p = getDataPacketById(requestId)
|
||||
val isAck = routingError == MeshProtos.Routing.Error.NONE_VALUE
|
||||
val p = packetRepository.get().getPacketById(requestId)
|
||||
// distinguish real ACKs coming from the intended receiver
|
||||
val m = if (isAck && fromId == p?.to) MessageStatus.RECEIVED
|
||||
else if (isAck) MessageStatus.DELIVERED else MessageStatus.ERROR
|
||||
if (p != null && p.status != MessageStatus.RECEIVED)
|
||||
packetRepository.get().updateMessageStatus(p, m)
|
||||
val m = when {
|
||||
isAck && fromId == p?.data?.to -> MessageStatus.RECEIVED
|
||||
isAck -> MessageStatus.DELIVERED
|
||||
else -> MessageStatus.ERROR
|
||||
}
|
||||
if (p != null && p.data.status != MessageStatus.RECEIVED) {
|
||||
p.data.status = m
|
||||
p.routingError = routingError
|
||||
packetRepository.get().update(p)
|
||||
}
|
||||
serviceBroadcasts.broadcastMessageStatus(requestId, m)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import androidx.compose.material.TopAppBar
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.twotone.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.twotone.Warning
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -42,7 +43,6 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -686,15 +686,15 @@ private fun NavButton(@StringRes title: Int, enabled: Boolean, onClick: () -> Un
|
|||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_twotone_warning_24),
|
||||
"warning",
|
||||
imageVector = Icons.TwoTone.Warning,
|
||||
contentDescription = "warning",
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Text(
|
||||
text = "${stringResource(title)}?\n")
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_twotone_warning_24),
|
||||
"warning",
|
||||
imageVector = Icons.TwoTone.Warning,
|
||||
contentDescription = "warning",
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.geeksville.mesh.ui
|
|||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -20,14 +21,19 @@ import androidx.compose.material.LocalTextStyle
|
|||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.twotone.Cloud
|
||||
import androidx.compose.material.icons.twotone.CloudDone
|
||||
import androidx.compose.material.icons.twotone.CloudOff
|
||||
import androidx.compose.material.icons.twotone.CloudUpload
|
||||
import androidx.compose.material.icons.twotone.HowToReg
|
||||
import androidx.compose.material.icons.twotone.Warning
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
|
@ -54,6 +60,7 @@ internal fun MessageItem(
|
|||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = {},
|
||||
onChipClick: () -> Unit = {},
|
||||
onStatusClick: () -> Unit = {},
|
||||
) {
|
||||
val fromLocal = shortName == null
|
||||
val messageColor = if (fromLocal) R.color.colorMyMsg else R.color.colorMsg
|
||||
|
|
@ -134,18 +141,19 @@ internal fun MessageItem(
|
|||
fontSize = MaterialTheme.typography.caption.fontSize,
|
||||
)
|
||||
AnimatedVisibility(visible = fromLocal) {
|
||||
val icon = when (messageStatus) {
|
||||
MessageStatus.RECEIVED -> R.drawable.ic_twotone_how_to_reg_24
|
||||
MessageStatus.QUEUED -> R.drawable.ic_twotone_cloud_upload_24
|
||||
MessageStatus.DELIVERED -> R.drawable.cloud_on
|
||||
MessageStatus.ENROUTE -> R.drawable.ic_twotone_cloud_24
|
||||
MessageStatus.ERROR -> R.drawable.cloud_off
|
||||
else -> R.drawable.ic_twotone_warning_24
|
||||
}
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = icon),
|
||||
imageVector = when (messageStatus) {
|
||||
MessageStatus.RECEIVED -> Icons.TwoTone.HowToReg
|
||||
MessageStatus.QUEUED -> Icons.TwoTone.CloudUpload
|
||||
MessageStatus.DELIVERED -> Icons.TwoTone.CloudDone
|
||||
MessageStatus.ENROUTE -> Icons.TwoTone.Cloud
|
||||
MessageStatus.ERROR -> Icons.TwoTone.CloudOff
|
||||
else -> Icons.TwoTone.Warning
|
||||
},
|
||||
contentDescription = stringResource(R.string.message_delivery_status),
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp)
|
||||
.clickable { onStatusClick() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,11 +9,14 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.model.Message
|
||||
import com.geeksville.mesh.ui.components.SimpleAlertDialog
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
|
|
@ -34,6 +37,13 @@ internal fun MessageListView(
|
|||
AutoScrollToBottom(listState, messages)
|
||||
UpdateUnreadCount(listState, messages, onUnreadChanged)
|
||||
|
||||
var showStatusDialog by remember { mutableStateOf<Message?>(null) }
|
||||
if (showStatusDialog != null) {
|
||||
val msg = showStatusDialog ?: return
|
||||
val (title, text) = msg.getStatusStringRes()
|
||||
SimpleAlertDialog(title = title, text = text) { showStatusDialog = null }
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
|
|
@ -52,6 +62,7 @@ internal fun MessageListView(
|
|||
onClick = { onClick(msg) },
|
||||
onLongClick = { onLongClick(msg) },
|
||||
onChipClick = { onChipClick(msg) },
|
||||
onStatusClick = { showStatusDialog = msg }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue