mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-07-08 12:26:23 -04:00
Show outgoing message status text (#6121)
Co-authored-by: James Rich <james.a.rich@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
e6bd6a9012
commit
cd3b3c2b70
8
.skills/compose-ui/strings-index.txt
generated
8
.skills/compose-ui/strings-index.txt
generated
@@ -960,9 +960,17 @@ message
|
||||
message_delivery_status
|
||||
message_device_managed
|
||||
message_input_label
|
||||
message_routing_error_max_retransmit
|
||||
message_routing_error_no_channel
|
||||
message_routing_error_pki_failed
|
||||
message_routing_error_pki_send_fail_public_key
|
||||
message_routing_error_pki_unknown_pubkey
|
||||
message_routing_error_too_large
|
||||
message_status_delivered
|
||||
message_status_enroute
|
||||
message_status_queued
|
||||
message_status_recipient_delivered
|
||||
message_status_relayed_not_confirmed
|
||||
message_status_sfpp_confirmed
|
||||
message_status_sfpp_routing
|
||||
message_status_unknown
|
||||
|
||||
@@ -18,12 +18,18 @@ package org.meshtastic.core.model
|
||||
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.delivery_confirmed
|
||||
import org.meshtastic.core.resources.error
|
||||
import org.meshtastic.core.resources.message_delivery_status
|
||||
import org.meshtastic.core.resources.message_routing_error_max_retransmit
|
||||
import org.meshtastic.core.resources.message_routing_error_no_channel
|
||||
import org.meshtastic.core.resources.message_routing_error_pki_failed
|
||||
import org.meshtastic.core.resources.message_routing_error_pki_send_fail_public_key
|
||||
import org.meshtastic.core.resources.message_routing_error_pki_unknown_pubkey
|
||||
import org.meshtastic.core.resources.message_routing_error_too_large
|
||||
import org.meshtastic.core.resources.message_status_delivered
|
||||
import org.meshtastic.core.resources.message_status_enroute
|
||||
import org.meshtastic.core.resources.message_status_queued
|
||||
import org.meshtastic.core.resources.message_status_recipient_delivered
|
||||
import org.meshtastic.core.resources.message_status_relayed_not_confirmed
|
||||
import org.meshtastic.core.resources.message_status_sfpp_confirmed
|
||||
import org.meshtastic.core.resources.message_status_sfpp_routing
|
||||
import org.meshtastic.core.resources.message_status_unknown
|
||||
@@ -72,6 +78,70 @@ fun getStringResFrom(routingError: Int): StringResource = when (routingError) {
|
||||
else -> Res.string.unrecognized
|
||||
}
|
||||
|
||||
fun getMessageRoutingErrorStringResFrom(routingError: Int): StringResource = when (routingError) {
|
||||
Routing.Error.GOT_NAK.value,
|
||||
Routing.Error.TIMEOUT.value,
|
||||
Routing.Error.MAX_RETRANSMIT.value,
|
||||
Routing.Error.NO_RESPONSE.value,
|
||||
-> Res.string.message_routing_error_max_retransmit
|
||||
|
||||
Routing.Error.NO_CHANNEL.value -> Res.string.message_routing_error_no_channel
|
||||
|
||||
Routing.Error.TOO_LARGE.value -> Res.string.message_routing_error_too_large
|
||||
|
||||
Routing.Error.PKI_FAILED.value -> Res.string.message_routing_error_pki_failed
|
||||
|
||||
Routing.Error.PKI_UNKNOWN_PUBKEY.value -> Res.string.message_routing_error_pki_unknown_pubkey
|
||||
|
||||
Routing.Error.PKI_SEND_FAIL_PUBLIC_KEY.value -> Res.string.message_routing_error_pki_send_fail_public_key
|
||||
|
||||
else -> getStringResFrom(routingError)
|
||||
}
|
||||
|
||||
fun getMessageStatusStringRes(
|
||||
status: MessageStatus?,
|
||||
routingError: Int,
|
||||
isDirectMessage: Boolean = false,
|
||||
): Pair<StringResource, StringResource> {
|
||||
val title = if (routingError > 0) Res.string.error else Res.string.message_delivery_status
|
||||
val text =
|
||||
when (status) {
|
||||
MessageStatus.RECEIVED -> Res.string.message_status_recipient_delivered
|
||||
|
||||
MessageStatus.QUEUED -> Res.string.message_status_enroute
|
||||
|
||||
MessageStatus.ENROUTE -> Res.string.message_status_enroute
|
||||
|
||||
MessageStatus.SFPP_ROUTING -> Res.string.message_status_sfpp_routing
|
||||
|
||||
MessageStatus.SFPP_CONFIRMED -> Res.string.message_status_sfpp_confirmed
|
||||
|
||||
MessageStatus.DELIVERED ->
|
||||
if (isDirectMessage) {
|
||||
Res.string.message_status_relayed_not_confirmed
|
||||
} else {
|
||||
Res.string.message_status_delivered
|
||||
}
|
||||
|
||||
MessageStatus.ERROR -> getMessageRoutingErrorStringResFrom(routingError)
|
||||
|
||||
MessageStatus.UNKNOWN,
|
||||
null,
|
||||
-> Res.string.message_status_unknown
|
||||
}
|
||||
return title to text
|
||||
}
|
||||
|
||||
fun isMessageStatusRetryable(status: MessageStatus?, routingError: Int, isDirectMessage: Boolean = false): Boolean =
|
||||
when {
|
||||
status == MessageStatus.DELIVERED && isDirectMessage -> true
|
||||
status != MessageStatus.ERROR -> false
|
||||
routingError in nonRetryableMessageRoutingErrors -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
private val nonRetryableMessageRoutingErrors = setOf(Routing.Error.NO_CHANNEL.value, Routing.Error.TOO_LARGE.value)
|
||||
|
||||
data class Message(
|
||||
val uuid: Long,
|
||||
val receivedTime: Long,
|
||||
@@ -109,28 +179,9 @@ data class Message(
|
||||
fun displayedText(searching: Boolean = false): String =
|
||||
if (showTranslated && translatedText != null && !searching) translatedText else text
|
||||
|
||||
fun getStatusStringRes(): Pair<StringResource, StringResource> {
|
||||
val title = if (routingError > 0) Res.string.error else Res.string.message_delivery_status
|
||||
val text =
|
||||
when (status) {
|
||||
MessageStatus.RECEIVED -> Res.string.delivery_confirmed
|
||||
fun getStatusStringRes(isDirectMessage: Boolean = false): Pair<StringResource, StringResource> =
|
||||
getMessageStatusStringRes(status, routingError, isDirectMessage)
|
||||
|
||||
MessageStatus.QUEUED -> Res.string.message_status_queued
|
||||
|
||||
MessageStatus.ENROUTE -> Res.string.message_status_enroute
|
||||
|
||||
MessageStatus.SFPP_ROUTING -> Res.string.message_status_sfpp_routing
|
||||
|
||||
MessageStatus.SFPP_CONFIRMED -> Res.string.message_status_sfpp_confirmed
|
||||
|
||||
MessageStatus.DELIVERED -> Res.string.message_status_delivered
|
||||
|
||||
MessageStatus.ERROR -> getStringResFrom(routingError)
|
||||
|
||||
MessageStatus.UNKNOWN,
|
||||
null,
|
||||
-> Res.string.message_status_unknown
|
||||
}
|
||||
return title to text
|
||||
}
|
||||
fun isStatusRetryable(isDirectMessage: Boolean = false): Boolean =
|
||||
isMessageStatusRetryable(status, routingError, isDirectMessage)
|
||||
}
|
||||
|
||||
@@ -20,10 +20,25 @@ import okio.ByteString.Companion.encodeUtf8
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.error
|
||||
import org.meshtastic.core.resources.message_delivery_status
|
||||
import org.meshtastic.core.resources.message_routing_error_max_retransmit
|
||||
import org.meshtastic.core.resources.message_routing_error_no_channel
|
||||
import org.meshtastic.core.resources.message_routing_error_pki_failed
|
||||
import org.meshtastic.core.resources.message_routing_error_pki_send_fail_public_key
|
||||
import org.meshtastic.core.resources.message_routing_error_pki_unknown_pubkey
|
||||
import org.meshtastic.core.resources.message_routing_error_too_large
|
||||
import org.meshtastic.core.resources.message_status_delivered
|
||||
import org.meshtastic.core.resources.message_status_enroute
|
||||
import org.meshtastic.core.resources.message_status_recipient_delivered
|
||||
import org.meshtastic.core.resources.message_status_relayed_not_confirmed
|
||||
import org.meshtastic.core.resources.message_status_unknown
|
||||
import org.meshtastic.core.resources.routing_error_max_retransmit
|
||||
import org.meshtastic.core.resources.routing_error_no_channel
|
||||
import org.meshtastic.core.resources.routing_error_no_route
|
||||
import org.meshtastic.core.resources.routing_error_none
|
||||
import org.meshtastic.core.resources.routing_error_pki_failed
|
||||
import org.meshtastic.core.resources.routing_error_pki_send_fail_public_key
|
||||
import org.meshtastic.core.resources.routing_error_pki_unknown_pubkey
|
||||
import org.meshtastic.core.resources.routing_error_too_large
|
||||
import org.meshtastic.core.resources.unrecognized
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Routing
|
||||
@@ -215,6 +230,51 @@ class MessageTest {
|
||||
assertEquals(Res.string.message_status_delivered, text)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getStatusStringRes_returnsDirectImplicitAckResourceForDirectMessages() {
|
||||
val (title, text) = messageWith(status = MessageStatus.DELIVERED).getStatusStringRes(isDirectMessage = true)
|
||||
|
||||
assertEquals(Res.string.message_delivery_status, title)
|
||||
assertEquals(Res.string.message_status_relayed_not_confirmed, text)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getStatusStringRes_returnsRecipientDeliveryResource() {
|
||||
val message =
|
||||
Message(
|
||||
uuid = 1L,
|
||||
receivedTime = 0L,
|
||||
node = Node.createFallback(1, "Node"),
|
||||
text = "hello",
|
||||
fromLocal = true,
|
||||
time = "now",
|
||||
read = false,
|
||||
status = MessageStatus.RECEIVED,
|
||||
routingError = 0,
|
||||
packetId = 1,
|
||||
emojis = emptyList(),
|
||||
snr = 0f,
|
||||
rssi = 0,
|
||||
hopsAway = 0,
|
||||
replyId = null,
|
||||
)
|
||||
|
||||
val (title, text) = message.getStatusStringRes()
|
||||
|
||||
assertEquals(Res.string.message_delivery_status, title)
|
||||
assertEquals(Res.string.message_status_recipient_delivered, text)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getStatusStringRes_returnsSendingResourceForPendingMessages() {
|
||||
for (status in listOf(MessageStatus.QUEUED, MessageStatus.ENROUTE)) {
|
||||
val (title, text) = messageWith(status = status).getStatusStringRes()
|
||||
|
||||
assertEquals(Res.string.message_delivery_status, title)
|
||||
assertEquals(Res.string.message_status_enroute, text)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getStatusStringRes_returnsErrorResourcesForRoutingFailures() {
|
||||
val message =
|
||||
@@ -242,6 +302,31 @@ class MessageTest {
|
||||
assertEquals(Res.string.routing_error_no_route, text)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getStatusStringRes_returnsMessageSpecificRoutingResources() {
|
||||
val mappings =
|
||||
listOf(
|
||||
Routing.Error.MAX_RETRANSMIT.value to Res.string.message_routing_error_max_retransmit,
|
||||
Routing.Error.GOT_NAK.value to Res.string.message_routing_error_max_retransmit,
|
||||
Routing.Error.TIMEOUT.value to Res.string.message_routing_error_max_retransmit,
|
||||
Routing.Error.NO_RESPONSE.value to Res.string.message_routing_error_max_retransmit,
|
||||
Routing.Error.NO_CHANNEL.value to Res.string.message_routing_error_no_channel,
|
||||
Routing.Error.PKI_FAILED.value to Res.string.message_routing_error_pki_failed,
|
||||
Routing.Error.PKI_SEND_FAIL_PUBLIC_KEY.value to
|
||||
Res.string.message_routing_error_pki_send_fail_public_key,
|
||||
Routing.Error.PKI_UNKNOWN_PUBKEY.value to Res.string.message_routing_error_pki_unknown_pubkey,
|
||||
Routing.Error.TOO_LARGE.value to Res.string.message_routing_error_too_large,
|
||||
)
|
||||
|
||||
for ((routingError, expectedText) in mappings) {
|
||||
val (title, text) =
|
||||
messageWith(status = MessageStatus.ERROR, routingError = routingError).getStatusStringRes()
|
||||
|
||||
assertEquals(Res.string.error, title)
|
||||
assertEquals(expectedText, text)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getStatusStringRes_returnsUnknownForMissingStatus() {
|
||||
val message =
|
||||
@@ -274,4 +359,66 @@ class MessageTest {
|
||||
assertEquals(Res.string.routing_error_none, getStringResFrom(Routing.Error.NONE.value))
|
||||
assertEquals(Res.string.unrecognized, getStringResFrom(Int.MAX_VALUE))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getStringResFrom_keepsSharedRoutingResourcesForGenericContexts() {
|
||||
val mappings =
|
||||
listOf(
|
||||
Routing.Error.MAX_RETRANSMIT.value to Res.string.routing_error_max_retransmit,
|
||||
Routing.Error.NO_CHANNEL.value to Res.string.routing_error_no_channel,
|
||||
Routing.Error.PKI_FAILED.value to Res.string.routing_error_pki_failed,
|
||||
Routing.Error.PKI_SEND_FAIL_PUBLIC_KEY.value to Res.string.routing_error_pki_send_fail_public_key,
|
||||
Routing.Error.PKI_UNKNOWN_PUBKEY.value to Res.string.routing_error_pki_unknown_pubkey,
|
||||
Routing.Error.TOO_LARGE.value to Res.string.routing_error_too_large,
|
||||
)
|
||||
|
||||
for ((routingError, expectedText) in mappings) {
|
||||
assertEquals(expectedText, getStringResFrom(routingError))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isMessageStatusRetryable_allowsRetryForRetryableMessageStatusesOnly() {
|
||||
assertTrue(
|
||||
messageWith(status = MessageStatus.ERROR, routingError = Routing.Error.MAX_RETRANSMIT.value)
|
||||
.isStatusRetryable(),
|
||||
)
|
||||
val retryableRoutingErrors =
|
||||
listOf(
|
||||
Routing.Error.PKI_FAILED.value,
|
||||
Routing.Error.PKI_SEND_FAIL_PUBLIC_KEY.value,
|
||||
Routing.Error.PKI_UNKNOWN_PUBKEY.value,
|
||||
)
|
||||
|
||||
for (routingError in retryableRoutingErrors) {
|
||||
assertTrue(messageWith(status = MessageStatus.ERROR, routingError = routingError).isStatusRetryable())
|
||||
}
|
||||
assertTrue(messageWith(status = MessageStatus.DELIVERED).isStatusRetryable(isDirectMessage = true))
|
||||
|
||||
val nonRetryableRoutingErrors = listOf(Routing.Error.NO_CHANNEL.value, Routing.Error.TOO_LARGE.value)
|
||||
|
||||
for (routingError in nonRetryableRoutingErrors) {
|
||||
assertFalse(messageWith(status = MessageStatus.ERROR, routingError = routingError).isStatusRetryable())
|
||||
}
|
||||
assertFalse(messageWith(status = MessageStatus.DELIVERED).isStatusRetryable())
|
||||
assertFalse(messageWith(status = MessageStatus.ENROUTE).isStatusRetryable())
|
||||
}
|
||||
|
||||
private fun messageWith(status: MessageStatus?, routingError: Int = 0) = Message(
|
||||
uuid = 1L,
|
||||
receivedTime = 0L,
|
||||
node = Node.createFallback(1, "Node"),
|
||||
text = "hello",
|
||||
fromLocal = true,
|
||||
time = "now",
|
||||
read = false,
|
||||
status = status,
|
||||
routingError = routingError,
|
||||
packetId = 1,
|
||||
emojis = emptyList(),
|
||||
snr = 0f,
|
||||
rssi = 0,
|
||||
hopsAway = 0,
|
||||
replyId = null,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -990,9 +990,17 @@
|
||||
<string name="message_delivery_status">Message delivery status</string>
|
||||
<string name="message_device_managed">This radio is managed and can only be changed by a remote admin.</string>
|
||||
<string name="message_input_label">Message</string>
|
||||
<string name="message_routing_error_max_retransmit">Failed to deliver to mesh</string>
|
||||
<string name="message_routing_error_no_channel">No channel selected</string>
|
||||
<string name="message_routing_error_pki_failed">Could not send encrypted message</string>
|
||||
<string name="message_routing_error_pki_send_fail_public_key">Recipient key unavailable</string>
|
||||
<string name="message_routing_error_pki_unknown_pubkey">Recipient needs your key</string>
|
||||
<string name="message_routing_error_too_large">Message is too large to send</string>
|
||||
<string name="message_status_delivered">Delivered to mesh</string>
|
||||
<string name="message_status_enroute">Waiting to be acknowledged</string>
|
||||
<string name="message_status_enroute">Sending...</string>
|
||||
<string name="message_status_queued">Queued for sending</string>
|
||||
<string name="message_status_recipient_delivered">Delivered to recipient</string>
|
||||
<string name="message_status_relayed_not_confirmed">Relayed, not confirmed by recipient</string>
|
||||
<string name="message_status_sfpp_confirmed">Confirmed on SF++ chain</string>
|
||||
<string name="message_status_sfpp_routing">Routing via SF++ chain…</string>
|
||||
<string name="message_status_unknown">Unknown</string>
|
||||
|
||||
@@ -53,8 +53,8 @@ import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.model.ContactKey
|
||||
import org.meshtastic.core.model.Message
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.core.model.Reaction
|
||||
@@ -113,12 +113,15 @@ internal fun MessageListPaged(
|
||||
|
||||
// Optimization: Pre-calculate map for O(1) lookup in list items to avoid O(N) linear search during scrolling.
|
||||
val nodeMap = remember(state.nodes) { state.nodes.associateBy { it.num } }
|
||||
val isDirectMessageConversation =
|
||||
remember(state.contactKey) { ContactKey(state.contactKey).addressString != NodeAddress.ID_BROADCAST }
|
||||
|
||||
var showStatusDialog by remember { mutableStateOf<Message?>(null) }
|
||||
showStatusDialog?.let { message ->
|
||||
MessageStatusDialog(
|
||||
message = message,
|
||||
resendOption = message.status?.equals(MessageStatus.ERROR) ?: false,
|
||||
isDirectMessage = isDirectMessageConversation,
|
||||
resendOption = message.isStatusRetryable(isDirectMessageConversation),
|
||||
onResend = {
|
||||
handlers.onDeleteMessages(listOf(message.uuid))
|
||||
handlers.onSendMessage(message.text, state.contactKey)
|
||||
@@ -165,6 +168,7 @@ internal fun MessageListPaged(
|
||||
inSelectionMode = inSelectionMode,
|
||||
coroutineScope = coroutineScope,
|
||||
haptics = haptics,
|
||||
isDirectMessageConversation = isDirectMessageConversation,
|
||||
onShowStatusDialog = { showStatusDialog = it },
|
||||
onShowReactions = { showReactionDialog = it },
|
||||
modifier = modifier,
|
||||
@@ -182,6 +186,7 @@ private fun MessageListPagedContent(
|
||||
inSelectionMode: Boolean,
|
||||
coroutineScope: CoroutineScope,
|
||||
haptics: HapticFeedback,
|
||||
isDirectMessageConversation: Boolean,
|
||||
onShowStatusDialog: (Message) -> Unit,
|
||||
onShowReactions: (List<Reaction>) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -249,6 +254,7 @@ private fun MessageListPagedContent(
|
||||
inSelectionMode = inSelectionMode,
|
||||
coroutineScope = coroutineScope,
|
||||
haptics = haptics,
|
||||
isDirectMessageConversation = isDirectMessageConversation,
|
||||
listState = listState,
|
||||
onShowStatusDialog = onShowStatusDialog,
|
||||
onShowReactions = onShowReactions,
|
||||
@@ -267,6 +273,7 @@ private fun MessageListPagedContent(
|
||||
inSelectionMode = inSelectionMode,
|
||||
coroutineScope = coroutineScope,
|
||||
haptics = haptics,
|
||||
isDirectMessageConversation = isDirectMessageConversation,
|
||||
listState = listState,
|
||||
onShowStatusDialog = onShowStatusDialog,
|
||||
onShowReactions = onShowReactions,
|
||||
@@ -309,6 +316,7 @@ private fun RenderPagedChatMessageRow(
|
||||
inSelectionMode: Boolean,
|
||||
coroutineScope: CoroutineScope,
|
||||
haptics: HapticFeedback,
|
||||
isDirectMessageConversation: Boolean,
|
||||
listState: LazyListState,
|
||||
onShowStatusDialog: (Message) -> Unit,
|
||||
onShowReactions: (List<Reaction>) -> Unit,
|
||||
@@ -381,6 +389,7 @@ private fun RenderPagedChatMessageRow(
|
||||
quickEmojis = quickEmojis,
|
||||
searchQuery = state.searchQuery,
|
||||
translationAvailable = state.translationAvailable,
|
||||
isDirectMessage = isDirectMessageConversation,
|
||||
onTranslate = { handlers.onTranslate(message) },
|
||||
onToggleTranslation = { handlers.onToggleTranslation(message) },
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -48,10 +49,12 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.role
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -63,6 +66,7 @@ import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.Reaction
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.a11y_message_from
|
||||
import org.meshtastic.core.resources.action_show_message_status
|
||||
import org.meshtastic.core.resources.filter_message_label
|
||||
import org.meshtastic.core.resources.message_translated_label
|
||||
import org.meshtastic.core.resources.reply
|
||||
@@ -88,6 +92,8 @@ import org.meshtastic.core.ui.theme.MessageItemColors
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
|
||||
import org.meshtastic.core.ui.util.createClipEntry
|
||||
|
||||
internal const val MESSAGE_STATUS_LABEL_TEST_TAG = "message_status_label"
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
@@ -117,6 +123,7 @@ fun MessageItem(
|
||||
hasSameNext: Boolean = false,
|
||||
searchQuery: String = "",
|
||||
translationAvailable: Boolean = false,
|
||||
isDirectMessage: Boolean = false,
|
||||
onTranslate: () -> Unit = {},
|
||||
onToggleTranslation: () -> Unit = {},
|
||||
) = Column(
|
||||
@@ -137,6 +144,8 @@ fun MessageItem(
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val isLocal = node.num == ourNode.num
|
||||
val statusString = message.getStatusStringRes(isDirectMessage)
|
||||
val isDirectImplicitAck = message.status == MessageStatus.DELIVERED && isDirectMessage
|
||||
// While searching, always show the original text — FTS matches and highlights apply to it, not the translation.
|
||||
val showsTranslation = message.showTranslated && message.translatedText != null && searchQuery.isEmpty()
|
||||
val bodyText = message.displayedText(searching = searchQuery.isNotEmpty())
|
||||
@@ -169,7 +178,7 @@ fun MessageItem(
|
||||
activeSheet = null
|
||||
onDelete()
|
||||
},
|
||||
statusString = message.getStatusStringRes(),
|
||||
statusString = statusString,
|
||||
status =
|
||||
if (isLocal) {
|
||||
message.status
|
||||
@@ -380,12 +389,19 @@ fun MessageItem(
|
||||
)
|
||||
}
|
||||
if (message.fromLocal) {
|
||||
MessageStatusIcon(
|
||||
status = message.status ?: MessageStatus.UNKNOWN,
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
val status = message.status ?: MessageStatus.UNKNOWN
|
||||
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterStart) {
|
||||
MessageStatusLabel(
|
||||
status = status,
|
||||
text = stringResource(statusString.second),
|
||||
metadataStyle = metadataStyle,
|
||||
isWarning = isDirectImplicitAck,
|
||||
onStatusClick = onStatusClick,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(modifier = Modifier.padding(start = 16.dp), text = message.time, style = metadataStyle)
|
||||
}
|
||||
}
|
||||
@@ -411,6 +427,46 @@ private enum class ActiveSheet {
|
||||
Emoji,
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageStatusLabel(
|
||||
status: MessageStatus,
|
||||
text: String,
|
||||
metadataStyle: TextStyle,
|
||||
isWarning: Boolean,
|
||||
onStatusClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val statusColor = messageStatusColor(status, isWarning = isWarning)
|
||||
Row(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(MESSAGE_STATUS_LABEL_TEST_TAG)
|
||||
.clickable(
|
||||
onClickLabel = stringResource(Res.string.action_show_message_status),
|
||||
role = Role.Button,
|
||||
onClick = onStatusClick,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(3.dp),
|
||||
) {
|
||||
MessageStatusIcon(
|
||||
status = status,
|
||||
modifier = Modifier.size(14.dp),
|
||||
tint = statusColor,
|
||||
includeContentDescription = false,
|
||||
)
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
style = metadataStyle,
|
||||
color = statusColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun translationRowStateFor(message: Message, translationAvailable: Boolean): TranslationRowState? = when {
|
||||
// Toggling a persisted translation is just a DB flag flip — offer it even when
|
||||
// the translation engine is no longer available for the current locale.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.meshtastic.feature.messaging.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -33,6 +34,7 @@ import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.sample_message
|
||||
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.proto.Routing
|
||||
|
||||
@Suppress("PreviewPublic")
|
||||
@PreviewLightDark
|
||||
@@ -84,6 +86,130 @@ fun MessageItemSignedPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("PreviewPublic")
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
fun MessageItemStatusStatesPreview() {
|
||||
val ourNode = NodePreviewParameterProvider().mickeyMouse
|
||||
val messages =
|
||||
listOf(
|
||||
StatusPreviewMessage(
|
||||
outgoingStatusPreviewMessage(
|
||||
text = "Explicit recipient ACK",
|
||||
time = "10:00",
|
||||
status = MessageStatus.RECEIVED,
|
||||
node = ourNode,
|
||||
),
|
||||
),
|
||||
StatusPreviewMessage(
|
||||
outgoingStatusPreviewMessage(
|
||||
text = "Channel implicit ACK",
|
||||
time = "10:01",
|
||||
status = MessageStatus.DELIVERED,
|
||||
node = ourNode,
|
||||
),
|
||||
),
|
||||
StatusPreviewMessage(
|
||||
outgoingStatusPreviewMessage(
|
||||
text = "Direct implicit ACK",
|
||||
time = "10:02",
|
||||
status = MessageStatus.DELIVERED,
|
||||
node = ourNode,
|
||||
),
|
||||
isDirectMessage = true,
|
||||
),
|
||||
StatusPreviewMessage(
|
||||
outgoingStatusPreviewMessage(
|
||||
text = "Queued for send",
|
||||
time = "10:03",
|
||||
status = MessageStatus.QUEUED,
|
||||
node = ourNode,
|
||||
),
|
||||
),
|
||||
StatusPreviewMessage(
|
||||
outgoingStatusPreviewMessage(
|
||||
text = "No ACK received",
|
||||
time = "10:04",
|
||||
status = MessageStatus.ERROR,
|
||||
routingError = Routing.Error.MAX_RETRANSMIT.value,
|
||||
node = ourNode,
|
||||
),
|
||||
),
|
||||
StatusPreviewMessage(
|
||||
outgoingStatusPreviewMessage(
|
||||
text = "No channel selected",
|
||||
time = "10:05",
|
||||
status = MessageStatus.ERROR,
|
||||
routingError = Routing.Error.NO_CHANNEL.value,
|
||||
node = ourNode,
|
||||
),
|
||||
),
|
||||
StatusPreviewMessage(
|
||||
outgoingStatusPreviewMessage(
|
||||
text = "Encrypted send failed",
|
||||
time = "10:06",
|
||||
status = MessageStatus.ERROR,
|
||||
routingError = Routing.Error.PKI_FAILED.value,
|
||||
node = ourNode,
|
||||
),
|
||||
),
|
||||
StatusPreviewMessage(
|
||||
outgoingStatusPreviewMessage(
|
||||
text = "Recipient key unavailable",
|
||||
time = "10:07",
|
||||
status = MessageStatus.ERROR,
|
||||
routingError = Routing.Error.PKI_SEND_FAIL_PUBLIC_KEY.value,
|
||||
node = ourNode,
|
||||
),
|
||||
),
|
||||
StatusPreviewMessage(
|
||||
outgoingStatusPreviewMessage(
|
||||
text = "Recipient needs your key",
|
||||
time = "10:08",
|
||||
status = MessageStatus.ERROR,
|
||||
routingError = Routing.Error.PKI_UNKNOWN_PUBKEY.value,
|
||||
node = ourNode,
|
||||
),
|
||||
),
|
||||
StatusPreviewMessage(
|
||||
outgoingStatusPreviewMessage(
|
||||
text = "Too large to send",
|
||||
time = "10:09",
|
||||
status = MessageStatus.ERROR,
|
||||
routingError = Routing.Error.TOO_LARGE.value,
|
||||
node = ourNode,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
AppTheme {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.background).padding(vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
messages.forEach { preview ->
|
||||
MessageItem(
|
||||
message = preview.message,
|
||||
node = preview.message.node,
|
||||
selected = false,
|
||||
ourNode = ourNode,
|
||||
onReply = {},
|
||||
sendReaction = {},
|
||||
onShowReactions = {},
|
||||
onClick = {},
|
||||
onLongClick = {},
|
||||
onDoubleClick = {},
|
||||
onClickChip = {},
|
||||
onNavigateToOriginalMessage = {},
|
||||
onStatusClick = {},
|
||||
isDirectMessage = preview.isDirectMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun MessageItemPreview() {
|
||||
@@ -269,3 +395,30 @@ private fun MessageItemPreview() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class StatusPreviewMessage(val message: Message, val isDirectMessage: Boolean = false)
|
||||
|
||||
private fun outgoingStatusPreviewMessage(
|
||||
text: String,
|
||||
time: String,
|
||||
status: MessageStatus,
|
||||
node: org.meshtastic.core.model.Node,
|
||||
routingError: Int = 0,
|
||||
) = Message(
|
||||
text = text,
|
||||
time = time,
|
||||
fromLocal = true,
|
||||
status = status,
|
||||
snr = 0f,
|
||||
rssi = 0,
|
||||
hopsAway = 0,
|
||||
uuid = time.filter(Char::isDigit).toLong(),
|
||||
receivedTime = nowMillis,
|
||||
node = node,
|
||||
read = false,
|
||||
routingError = routingError,
|
||||
packetId = time.filter(Char::isDigit).toInt(),
|
||||
emojis = listOf(),
|
||||
replyId = null,
|
||||
viaMqtt = false,
|
||||
)
|
||||
|
||||
@@ -663,8 +663,14 @@ fun UnreadMessagesDivider(modifier: Modifier = Modifier) {
|
||||
// region ── MessageStatusDialog ──
|
||||
|
||||
@Composable
|
||||
fun MessageStatusDialog(message: Message, resendOption: Boolean, onResend: () -> Unit, onDismiss: () -> Unit) {
|
||||
val (title, text) = message.getStatusStringRes()
|
||||
fun MessageStatusDialog(
|
||||
message: Message,
|
||||
resendOption: Boolean,
|
||||
onResend: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
isDirectMessage: Boolean = false,
|
||||
) {
|
||||
val (title, text) = message.getStatusStringRes(isDirectMessage)
|
||||
DeliveryInfo(
|
||||
title = title,
|
||||
resendOption = resendOption,
|
||||
|
||||
@@ -17,8 +17,12 @@
|
||||
package org.meshtastic.feature.messaging.component
|
||||
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.resources.Res
|
||||
@@ -32,9 +36,18 @@ import org.meshtastic.core.ui.icon.MessageEnroute
|
||||
import org.meshtastic.core.ui.icon.MessageError
|
||||
import org.meshtastic.core.ui.icon.MqttDelivered
|
||||
import org.meshtastic.core.ui.icon.Warning
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusBlue
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
|
||||
|
||||
@Composable
|
||||
fun MessageStatusIcon(status: MessageStatus, modifier: Modifier = Modifier) {
|
||||
fun MessageStatusIcon(
|
||||
status: MessageStatus,
|
||||
modifier: Modifier = Modifier,
|
||||
tint: Color = Color.Unspecified,
|
||||
includeContentDescription: Boolean = true,
|
||||
) {
|
||||
val icon =
|
||||
when (status) {
|
||||
MessageStatus.RECEIVED -> MeshtasticIcons.Acknowledged
|
||||
@@ -46,9 +59,40 @@ fun MessageStatusIcon(status: MessageStatus, modifier: Modifier = Modifier) {
|
||||
MessageStatus.ERROR -> MeshtasticIcons.MessageError
|
||||
else -> MeshtasticIcons.Warning
|
||||
}
|
||||
val contentDescription =
|
||||
if (includeContentDescription) {
|
||||
stringResource(Res.string.message_delivery_status)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
Icon(
|
||||
modifier = modifier,
|
||||
imageVector = icon,
|
||||
contentDescription = stringResource(Res.string.message_delivery_status),
|
||||
contentDescription = contentDescription,
|
||||
tint = tint.takeOrElse { LocalContentColor.current },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun messageStatusColor(status: MessageStatus, isWarning: Boolean = false): Color {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
if (isWarning) {
|
||||
return colorScheme.StatusYellow
|
||||
}
|
||||
return when (status) {
|
||||
MessageStatus.RECEIVED,
|
||||
MessageStatus.DELIVERED,
|
||||
MessageStatus.SFPP_CONFIRMED,
|
||||
-> colorScheme.StatusGreen
|
||||
|
||||
MessageStatus.QUEUED,
|
||||
MessageStatus.UNKNOWN,
|
||||
-> colorScheme.StatusYellow
|
||||
|
||||
MessageStatus.ENROUTE,
|
||||
MessageStatus.SFPP_ROUTING,
|
||||
-> colorScheme.StatusBlue
|
||||
|
||||
MessageStatus.ERROR -> colorScheme.StatusRed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,16 +58,10 @@ import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.core.model.Reaction
|
||||
import org.meshtastic.core.model.getStringResFrom
|
||||
import org.meshtastic.core.model.getMessageStatusStringRes
|
||||
import org.meshtastic.core.model.isMessageStatusRetryable
|
||||
import org.meshtastic.core.model.util.getShortDateTime
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.delivery_confirmed
|
||||
import org.meshtastic.core.resources.error
|
||||
import org.meshtastic.core.resources.message_delivery_status
|
||||
import org.meshtastic.core.resources.message_status_delivered
|
||||
import org.meshtastic.core.resources.message_status_enroute
|
||||
import org.meshtastic.core.resources.message_status_queued
|
||||
import org.meshtastic.core.resources.message_status_unknown
|
||||
import org.meshtastic.core.resources.react
|
||||
import org.meshtastic.core.resources.you
|
||||
import org.meshtastic.core.ui.component.Rssi
|
||||
@@ -206,23 +200,13 @@ internal fun ReactionDialog(
|
||||
|
||||
var showStatusDialog by remember { mutableStateOf<Reaction?>(null) }
|
||||
showStatusDialog?.let { reaction ->
|
||||
val title = if (reaction.routingError > 0) Res.string.error else Res.string.message_delivery_status
|
||||
val text =
|
||||
when (reaction.status) {
|
||||
MessageStatus.RECEIVED -> Res.string.delivery_confirmed
|
||||
MessageStatus.QUEUED -> Res.string.message_status_queued
|
||||
MessageStatus.ENROUTE -> Res.string.message_status_enroute
|
||||
MessageStatus.DELIVERED -> Res.string.message_status_delivered
|
||||
MessageStatus.SFPP_ROUTING -> Res.string.message_status_enroute
|
||||
MessageStatus.SFPP_CONFIRMED -> Res.string.delivery_confirmed
|
||||
MessageStatus.ERROR -> getStringResFrom(reaction.routingError)
|
||||
MessageStatus.UNKNOWN -> Res.string.message_status_unknown
|
||||
}
|
||||
val isDirectMessage = NodeAddress.fromString(reaction.to) !is NodeAddress.Broadcast
|
||||
val (title, text) = getMessageStatusStringRes(reaction.status, reaction.routingError, isDirectMessage)
|
||||
|
||||
DeliveryInfo(
|
||||
title = title,
|
||||
text = text,
|
||||
resendOption = reaction.status == MessageStatus.ERROR,
|
||||
resendOption = isMessageStatusRetryable(reaction.status, reaction.routingError, isDirectMessage),
|
||||
onConfirm = {
|
||||
onResend(reaction)
|
||||
showStatusDialog = null
|
||||
|
||||
@@ -19,12 +19,18 @@ package org.meshtastic.feature.messaging.component
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.v2.runComposeUiTest
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.model.Message
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
|
||||
import org.meshtastic.proto.Routing
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
class MessageItemTest {
|
||||
@@ -145,4 +151,106 @@ class MessageItemTest {
|
||||
// Verify that the node containing the message text exists and matches the text
|
||||
onNodeWithContentDescription("Message from ${testNode.user.long_name}: Hello World").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun localMessage_displaysDeliveryStatusText() = runComposeUiTest {
|
||||
val testNode = NodePreviewParameterProvider().mickeyMouse
|
||||
val message = localMessage(node = testNode, status = MessageStatus.RECEIVED)
|
||||
|
||||
setContent {
|
||||
MessageItem(message = message, node = testNode, selected = false, onStatusClick = {}, ourNode = testNode)
|
||||
}
|
||||
|
||||
onNodeWithText("Delivered to recipient", useUnmergedTree = true).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun localDirectMessage_displaysImplicitAckWarningText() = runComposeUiTest {
|
||||
val testNode = NodePreviewParameterProvider().mickeyMouse
|
||||
val message = localMessage(node = testNode, status = MessageStatus.DELIVERED)
|
||||
|
||||
setContent {
|
||||
MessageItem(
|
||||
message = message,
|
||||
node = testNode,
|
||||
selected = false,
|
||||
onStatusClick = {},
|
||||
ourNode = testNode,
|
||||
isDirectMessage = true,
|
||||
)
|
||||
}
|
||||
|
||||
onNodeWithText("Relayed, not confirmed by recipient", useUnmergedTree = true).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun localMessage_displaysRoutingErrorStatusText() = runComposeUiTest {
|
||||
val testNode = NodePreviewParameterProvider().mickeyMouse
|
||||
val message =
|
||||
localMessage(
|
||||
node = testNode,
|
||||
status = MessageStatus.ERROR,
|
||||
routingError = Routing.Error.MAX_RETRANSMIT.value,
|
||||
)
|
||||
|
||||
setContent {
|
||||
MessageItem(message = message, node = testNode, selected = false, onStatusClick = {}, ourNode = testNode)
|
||||
}
|
||||
|
||||
onNodeWithText("Failed to deliver to mesh", useUnmergedTree = true).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun localMessageStatus_invokesStatusClick() = runComposeUiTest {
|
||||
val testNode = NodePreviewParameterProvider().mickeyMouse
|
||||
val message = localMessage(node = testNode, status = MessageStatus.QUEUED)
|
||||
var statusClicks = 0
|
||||
|
||||
setContent {
|
||||
MessageItem(
|
||||
message = message,
|
||||
node = testNode,
|
||||
selected = false,
|
||||
onStatusClick = { statusClicks += 1 },
|
||||
ourNode = testNode,
|
||||
)
|
||||
}
|
||||
|
||||
onNodeWithText("Sending...", useUnmergedTree = true).assertIsDisplayed()
|
||||
onNodeWithTag(MESSAGE_STATUS_LABEL_TEST_TAG, useUnmergedTree = true).performClick()
|
||||
|
||||
assertEquals(1, statusClicks)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun localMessageStatus_doesNotExposeGenericIconDescription() = runComposeUiTest {
|
||||
val testNode = NodePreviewParameterProvider().mickeyMouse
|
||||
val message = localMessage(node = testNode, status = MessageStatus.ENROUTE)
|
||||
|
||||
setContent {
|
||||
MessageItem(message = message, node = testNode, selected = false, onStatusClick = {}, ourNode = testNode)
|
||||
}
|
||||
|
||||
onNodeWithText("Sending...", useUnmergedTree = true).assertIsDisplayed()
|
||||
onNodeWithContentDescription("Message delivery status", useUnmergedTree = true).assertDoesNotExist()
|
||||
}
|
||||
|
||||
private fun localMessage(node: Node, status: MessageStatus, routingError: Int = 0) = Message(
|
||||
text = "Local message",
|
||||
time = "10:00",
|
||||
fromLocal = true,
|
||||
status = status,
|
||||
snr = 2.5f,
|
||||
rssi = 90,
|
||||
hopsAway = 0,
|
||||
uuid = 1L,
|
||||
receivedTime = nowMillis,
|
||||
node = node,
|
||||
read = false,
|
||||
routingError = routingError,
|
||||
packetId = 1234,
|
||||
emojis = listOf(),
|
||||
replyId = null,
|
||||
viaMqtt = false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.meshtastic.feature.messaging.EditQuickChatDialogPreview
|
||||
import org.meshtastic.feature.messaging.MessageInputPreview
|
||||
import org.meshtastic.feature.messaging.QuickChatItemPreview
|
||||
import org.meshtastic.feature.messaging.component.MessageItemSignedPreview
|
||||
import org.meshtastic.feature.messaging.component.MessageItemStatusStatesPreview
|
||||
import org.meshtastic.feature.messaging.component.MessageSearchBarPreview
|
||||
import org.meshtastic.feature.messaging.component.ReactionItemPreview
|
||||
|
||||
@@ -67,3 +68,10 @@ fun ScreenshotMessageSearchBar() {
|
||||
fun ScreenshotMessageItemSigned() {
|
||||
MessageItemSignedPreview()
|
||||
}
|
||||
|
||||
@PreviewTest
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
fun ScreenshotMessageItemStatusStates() {
|
||||
MessageItemStatusStatesPreview()
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
Reference in New Issue
Block a user