feat/decoupling (#4685)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-03-03 07:15:28 -06:00 committed by GitHub
parent 40244f8337
commit 2c49db8041
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
254 changed files with 5132 additions and 2666 deletions

View file

@ -24,7 +24,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.database.model.Message
import org.meshtastic.core.model.Message
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider

View file

@ -102,9 +102,9 @@ import org.jetbrains.compose.resources.pluralStringResource
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.HomoglyphCharacterStringTransformer
import org.meshtastic.core.database.entity.QuickChatAction
import org.meshtastic.core.database.model.Message
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.Message
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.util.getChannel
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.alert_bell_text

View file

@ -61,11 +61,10 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.entity.Packet
import org.meshtastic.core.database.entity.Reaction
import org.meshtastic.core.database.model.Message
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Message
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.Reaction
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.new_messages_below
import org.meshtastic.feature.messaging.component.MessageItem
@ -545,7 +544,7 @@ private fun MessageStatusDialog(
remember(message.relayNode, nodes, ourNode) {
derivedStateOf {
message.relayNode?.let { relayNodeId ->
Packet.getRelayNode(relayNodeId, nodes, ourNode?.num)?.user?.long_name
Node.getRelayNode(relayNodeId, nodes, ourNode?.num)?.user?.long_name
}
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,10 +14,9 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.messaging
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
/** Defines the various user interactions that can occur on the MessageScreen. */
internal sealed interface MessageScreenEvent {

View file

@ -32,21 +32,21 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.data.repository.QuickChatActionRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.entity.ContactSettings
import org.meshtastic.core.database.model.Message
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.domain.usecase.SendMessageUseCase
import org.meshtastic.core.model.ContactSettings
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.Message
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.service.ServiceAction
import org.meshtastic.core.prefs.emoji.CustomEmojiPrefs
import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs
import org.meshtastic.core.prefs.ui.UiPrefs
import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.core.service.ServiceAction
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.repository.MeshServiceNotifications
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.usecase.SendMessageUseCase
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.ChannelSet
import javax.inject.Inject

View file

@ -62,10 +62,10 @@ import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.database.entity.Reaction
import org.meshtastic.core.database.model.Message
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Message
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.Reaction
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.filter_message_label
import org.meshtastic.core.resources.message_delivery_status

View file

@ -57,12 +57,11 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.entity.Packet
import org.meshtastic.core.database.entity.Reaction
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.database.model.getStringResFrom
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.Reaction
import org.meshtastic.core.model.getStringResFrom
import org.meshtastic.core.model.util.getShortDateTime
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.delivery_confirmed
@ -148,7 +147,9 @@ internal fun ReactionRow(
AnimatedVisibility(emojiGroups.isNotEmpty(), modifier = modifier) {
LazyRow(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
items(emojiGroups.entries.toList()) { (emoji, reactions) ->
items(emojiGroups.entries.toList()) { entry ->
val emoji = entry.key
val reactions = entry.value
val localReaction = reactions.find { it.user.id == DataPacket.ID_LOCAL || it.user.id == myId }
ReactionItem(
emoji = emoji,
@ -218,7 +219,7 @@ internal fun ReactionDialog(
val relayNodeName =
reaction.relayNode?.let { relayNodeId ->
Packet.getRelayNode(relayNodeId, nodes, ourNode?.num)?.user?.long_name
Node.getRelayNode(relayNodeId, nodes, ourNode?.num)?.user?.long_name
}
DeliveryInfo(
@ -236,7 +237,9 @@ internal fun ReactionDialog(
}
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
items(groupedEmojis.entries.toList()) { (emoji, reactions) ->
items(groupedEmojis.entries.toList()) { entry ->
val emoji = entry.key
val reactions = entry.value
val localReaction = reactions.find { it.user.id == DataPacket.ID_LOCAL || it.user.id == myId }
val isSending =
localReaction?.status == MessageStatus.QUEUED || localReaction?.status == MessageStatus.ENROUTE

View file

@ -20,7 +20,7 @@ import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.meshtastic.core.domain.MessageQueue
import org.meshtastic.core.repository.MessageQueue
import org.meshtastic.feature.messaging.domain.worker.WorkManagerMessageQueue
@Module

View file

@ -22,10 +22,10 @@ import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.PacketRepository
@HiltWorker
class SendMessageWorker
@ -47,18 +47,16 @@ constructor(
return Result.retry()
}
val packetEntity =
val packetData =
packetRepository.getPacketByPacketId(packetId)
?: return Result.failure() // Packet no longer exists in DB? Do not retry.
val packetData = packetEntity.packet.data
return try {
radioController.sendMessage(packetData)
packetRepository.updateMessageStatus(packetData, MessageStatus.ENROUTE)
Result.success()
} catch (e: Exception) {
packetRepository.updateMessageStatus(packetData, MessageStatus.ERROR)
packetRepository.updateMessageStatus(packetData, MessageStatus.QUEUED)
Result.retry()
}
}

View file

@ -20,7 +20,7 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.workDataOf
import org.meshtastic.core.domain.MessageQueue
import org.meshtastic.core.repository.MessageQueue
import javax.inject.Inject
import javax.inject.Singleton

View file

@ -65,8 +65,8 @@ import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.pluralStringResource
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.database.entity.ContactSettings
import org.meshtastic.core.model.Contact
import org.meshtastic.core.model.ContactSettings
import org.meshtastic.core.model.util.TimeConstants
import org.meshtastic.core.model.util.formatMuteRemainingTime
import org.meshtastic.core.model.util.getChannel

View file

@ -28,16 +28,15 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.entity.ContactSettings
import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.database.entity.Packet
import org.meshtastic.core.model.Contact
import org.meshtastic.core.model.ContactSettings
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MyNodeInfo
import org.meshtastic.core.model.util.getChannel
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.ChannelSet
import javax.inject.Inject
@ -59,7 +58,7 @@ constructor(
val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(initialValue = ChannelSet())
// Combine node info and myId to reduce argument count in subsequent combines
private val identityFlow: Flow<Pair<MyNodeEntity?, String?>> =
private val identityFlow: Flow<Pair<MyNodeInfo?, String?>> =
combine(nodeRepository.myNodeInfo, nodeRepository.myId) { info, id -> Pair(info, id) }
/**
@ -78,42 +77,42 @@ constructor(
settings,
->
val (myNodeInfo, myId) = identity
val myNodeNum = myNodeInfo?.myNodeNum ?: return@combine emptyList()
val myNodeNum = myNodeInfo?.myNodeNum ?: return@combine emptyList<Contact>()
// Add empty channel placeholders (always show Broadcast contacts, even when empty)
val placeholder =
(0 until channelSet.settings.size).associate { ch ->
val contactKey = "$ch${DataPacket.ID_BROADCAST}"
val data = DataPacket(bytes = null, dataType = 1, time = 0L, channel = ch)
contactKey to Packet(0L, myNodeNum, 1, contactKey, 0L, true, data)
contactKey to data
}
(contacts + (placeholder - contacts.keys)).values.collectionsMap { packet ->
val data = packet.data
val contactKey = packet.contact_key
(contacts + (placeholder - contacts.keys)).entries.collectionsMap { entry ->
val contactKey = entry.key
val packetData = entry.value
// Determine if this is my message (originated on this device)
val fromLocal = (data.from == DataPacket.ID_LOCAL || (myId != null && data.from == myId))
val toBroadcast = data.to == DataPacket.ID_BROADCAST
val fromLocal =
(packetData.from == DataPacket.ID_LOCAL || (myId != null && packetData.from == myId))
val toBroadcast = packetData.to == DataPacket.ID_BROADCAST
// grab usernames from NodeInfo
val userId = if (fromLocal) data.to else data.from
val userId = if (fromLocal) packetData.to else packetData.from
val user = nodeRepository.getUser(userId ?: DataPacket.ID_BROADCAST)
val node = nodeRepository.getNode(userId ?: DataPacket.ID_BROADCAST)
val shortName = user.short_name
val longName =
if (toBroadcast) {
channelSet.getChannel(data.channel)?.name ?: "Channel ${data.channel}"
channelSet.getChannel(packetData.channel)?.name ?: "Channel ${packetData.channel}"
} else {
user.long_name
}
Contact(
contactKey = contactKey,
shortName = if (toBroadcast) data.channel.toString() else shortName,
shortName = if (toBroadcast) packetData.channel.toString() else shortName,
longName = longName,
lastMessageTime = if (data.time != 0L) data.time else null,
lastMessageText = if (fromLocal) data.text else "$shortName: ${data.text}",
lastMessageTime = if (packetData.time != 0L) packetData.time else null,
lastMessageText = if (fromLocal) packetData.text else "$shortName: ${packetData.text}",
unreadCount = packetRepository.getUnreadCount(contactKey),
messageCount = packetRepository.getMessageCount(contactKey),
isMuted = settings[contactKey]?.isMuted == true,
@ -140,36 +139,41 @@ constructor(
val myId = params.myId
packetRepository.getContactsPaged().map { pagingData ->
pagingData.map { packet ->
val data = packet.data
val contactKey = packet.contact_key
pagingData.map { packetData: DataPacket ->
val contactKey =
"${packetData.channel}${packetData.to}" // This might be wrong, need to check how contactKey
// is derived in PagingSource
// Determine if this is my message (originated on this device)
val fromLocal = (data.from == DataPacket.ID_LOCAL || (myId != null && data.from == myId))
val toBroadcast = data.to == DataPacket.ID_BROADCAST
val fromLocal =
(packetData.from == DataPacket.ID_LOCAL || (myId != null && packetData.from == myId))
val toBroadcast = packetData.to == DataPacket.ID_BROADCAST
// grab usernames from NodeInfo
val userId = if (fromLocal) data.to else data.from
val userId = if (fromLocal) packetData.to else packetData.from
val user = nodeRepository.getUser(userId ?: DataPacket.ID_BROADCAST)
val node = nodeRepository.getNode(userId ?: DataPacket.ID_BROADCAST)
val shortName = user.short_name
val longName =
if (toBroadcast) {
channelSet.getChannel(data.channel)?.name ?: "Channel ${data.channel}"
channelSet.getChannel(packetData.channel)?.name ?: "Channel ${packetData.channel}"
} else {
user.long_name
}
val contactKeyComputed =
if (toBroadcast) "${packetData.channel}${DataPacket.ID_BROADCAST}" else contactKey
Contact(
contactKey = contactKey,
shortName = if (toBroadcast) data.channel.toString() else shortName,
contactKey = contactKeyComputed,
shortName = if (toBroadcast) packetData.channel.toString() else shortName,
longName = longName,
lastMessageTime = if (data.time != 0L) data.time else null,
lastMessageText = if (fromLocal) data.text else "$shortName: ${data.text}",
unreadCount = packetRepository.getUnreadCount(contactKey),
messageCount = packetRepository.getMessageCount(contactKey),
isMuted = settings[contactKey]?.isMuted == true,
lastMessageTime = if (packetData.time != 0L) packetData.time else null,
lastMessageText = if (fromLocal) packetData.text else "$shortName: ${packetData.text}",
unreadCount = packetRepository.getUnreadCount(contactKeyComputed),
messageCount = packetRepository.getMessageCount(contactKeyComputed),
isMuted = settings[contactKeyComputed]?.isMuted == true,
isUnmessageable = user.is_unmessagable ?: false,
nodeColors =
if (!toBroadcast) {

View file

@ -30,17 +30,16 @@ import io.mockk.just
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import okio.ByteString.Companion.toByteString
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.database.entity.Packet
import org.meshtastic.core.database.entity.PacketEntity
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.PacketRepository
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
@ -62,11 +61,8 @@ class SendMessageWorkerTest {
fun `doWork returns success when packet is sent successfully`() = runTest {
// Arrange
val packetId = 12345
val dataPacket = DataPacket("dest", 0, "Hello")
val packet = mockk<Packet>(relaxed = true)
val packetEntity = PacketEntity(packet = packet)
every { packet.data } returns dataPacket
coEvery { packetRepository.getPacketByPacketId(packetId) } returns packetEntity
val dataPacket = DataPacket(to = "dest", bytes = "Hello".encodeToByteArray().toByteString(), dataType = 0)
coEvery { packetRepository.getPacketByPacketId(packetId) } returns dataPacket
every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Connected)
coEvery { radioController.sendMessage(any()) } just Runs
coEvery { packetRepository.updateMessageStatus(any(), any()) } just Runs
@ -99,11 +95,8 @@ class SendMessageWorkerTest {
fun `doWork returns retry when radio is disconnected`() = runTest {
// Arrange
val packetId = 12345
val dataPacket = DataPacket("dest", 0, "Hello")
val packet = mockk<Packet>(relaxed = true)
val packetEntity = PacketEntity(packet = packet)
every { packet.data } returns dataPacket
coEvery { packetRepository.getPacketByPacketId(packetId) } returns packetEntity
val dataPacket = DataPacket(to = "dest", bytes = "Hello".encodeToByteArray().toByteString(), dataType = 0)
coEvery { packetRepository.getPacketByPacketId(packetId) } returns dataPacket
every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Disconnected)
val worker =