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

@ -37,19 +37,20 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString
import org.meshtastic.core.data.repository.DeviceHardwareRepository
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.database.entity.FirmwareReleaseType
import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.datastore.BootloaderWarningDataSource
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.model.MyNodeInfo
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.prefs.radio.RadioPrefs
import org.meshtastic.core.prefs.radio.isBle
import org.meshtastic.core.prefs.radio.isSerial
import org.meshtastic.core.prefs.radio.isTcp
import org.meshtastic.core.repository.DeviceHardwareRepository
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.firmware_update_battery_low
import org.meshtastic.core.resources.firmware_update_copying
@ -72,7 +73,6 @@ import org.meshtastic.core.resources.firmware_update_unknown_hardware
import org.meshtastic.core.resources.firmware_update_updating
import org.meshtastic.core.resources.firmware_update_validating
import org.meshtastic.core.resources.unknown
import org.meshtastic.core.service.ServiceRepository
import java.io.File
import javax.inject.Inject
@ -95,7 +95,7 @@ constructor(
private val firmwareReleaseRepository: FirmwareReleaseRepository,
private val deviceHardwareRepository: DeviceHardwareRepository,
private val nodeRepository: NodeRepository,
private val serviceRepository: ServiceRepository,
private val radioController: RadioController,
private val radioPrefs: RadioPrefs,
private val bootloaderWarningDataSource: BootloaderWarningDataSource,
private val firmwareUpdateManager: FirmwareUpdateManager,
@ -106,6 +106,8 @@ constructor(
private val _state = MutableStateFlow<FirmwareUpdateState>(FirmwareUpdateState.Idle)
val state: StateFlow<FirmwareUpdateState> = _state.asStateFlow()
val connectionState = radioController.connectionState
private val _selectedReleaseType = MutableStateFlow(FirmwareReleaseType.STABLE)
val selectedReleaseType: StateFlow<FirmwareReleaseType> = _selectedReleaseType.asStateFlow()
@ -429,14 +431,14 @@ constructor(
// Trigger a fresh connection attempt by MeshService
address?.let { currentAddr ->
Logger.i { "Post-update: Requesting MeshService to reconnect to $currentAddr" }
serviceRepository.meshService?.setDeviceAddress("$DFU_RECONNECT_PREFIX$currentAddr")
radioController.setDeviceAddress("$DFU_RECONNECT_PREFIX$currentAddr")
}
// Wait for device to reconnect and settle
val result =
withTimeoutOrNull(VERIFY_TIMEOUT) {
// Wait for both Connected state and node info to be present
serviceRepository.connectionState.first { it is ConnectionState.Connected }
connectionState.first { it is ConnectionState.Connected }
nodeRepository.ourNodeInfo.filterNotNull().first()
delay(VERIFY_DELAY) // Extra buffer for initial config sync
true
@ -462,7 +464,7 @@ constructor(
return !isBatteryLow
}
private suspend fun getDeviceHardware(ourNode: MyNodeEntity): DeviceHardware? {
private suspend fun getDeviceHardware(ourNode: MyNodeInfo): DeviceHardware? {
val nodeInfo = nodeRepository.ourNodeInfo.value
val hwModelInt = nodeInfo?.user?.hw_model?.value
val target = ourNode.pioEnv

View file

@ -33,12 +33,12 @@ import no.nordicsemi.android.dfu.DfuServiceListenerHelper
import org.jetbrains.compose.resources.getString
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.firmware_update_downloading_percent
import org.meshtastic.core.resources.firmware_update_nordic_failed
import org.meshtastic.core.resources.firmware_update_not_found_in_release
import org.meshtastic.core.resources.firmware_update_starting_service
import org.meshtastic.core.service.ServiceRepository
import java.io.File
import javax.inject.Inject
@ -53,7 +53,7 @@ class NordicDfuHandler
constructor(
private val firmwareRetriever: FirmwareRetriever,
@ApplicationContext private val context: Context,
private val serviceRepository: ServiceRepository,
private val radioController: RadioController,
) : FirmwareUpdateHandler {
override suspend fun startUpdate(
@ -113,7 +113,7 @@ constructor(
updateState(FirmwareUpdateState.Processing(ProgressState(startingMsg)))
// n = Nordic (Legacy prefix handling in mesh service)
serviceRepository.meshService?.setDeviceAddress("n")
radioController.setDeviceAddress("n")
DfuServiceInitiator(address)
.setDeviceName(deviceHardware.displayName)

View file

@ -23,12 +23,13 @@ import kotlinx.coroutines.delay
import org.jetbrains.compose.resources.getString
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.firmware_update_downloading_percent
import org.meshtastic.core.resources.firmware_update_rebooting
import org.meshtastic.core.resources.firmware_update_retrieval_failed
import org.meshtastic.core.resources.firmware_update_usb_failed
import org.meshtastic.core.service.ServiceRepository
import java.io.File
import javax.inject.Inject
@ -40,7 +41,8 @@ class UsbUpdateHandler
@Inject
constructor(
private val firmwareRetriever: FirmwareRetriever,
private val serviceRepository: ServiceRepository,
private val radioController: RadioController,
private val nodeRepository: NodeRepository,
) : FirmwareUpdateHandler {
override suspend fun startUpdate(
@ -62,8 +64,8 @@ constructor(
if (firmwareUri != null) {
updateState(FirmwareUpdateState.Processing(ProgressState(rebootingMsg)))
val myNodeNum = serviceRepository.meshService?.getMyNodeInfo()?.myNodeNum ?: 0
serviceRepository.meshService?.rebootToDfu(myNodeNum)
val myNodeNum = nodeRepository.myNodeInfo.value?.myNodeNum ?: 0
radioController.rebootToDfu(myNodeNum)
delay(REBOOT_DELAY)
updateState(FirmwareUpdateState.AwaitingFileSave(null, "firmware.uf2", firmwareUri))
@ -85,8 +87,8 @@ constructor(
null
} else {
updateState(FirmwareUpdateState.Processing(ProgressState(rebootingMsg)))
val myNodeNum = serviceRepository.meshService?.getMyNodeInfo()?.myNodeNum ?: 0
serviceRepository.meshService?.rebootToDfu(myNodeNum)
val myNodeNum = nodeRepository.myNodeInfo.value?.myNodeNum ?: 0
radioController.rebootToDfu(myNodeNum)
delay(REBOOT_DELAY)
updateState(FirmwareUpdateState.AwaitingFileSave(firmwareFile, firmwareFile.name))

View file

@ -21,14 +21,18 @@ import android.net.Uri
import co.touchlab.kermit.Logger
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import no.nordicsemi.kotlin.ble.client.android.CentralManager
import org.jetbrains.compose.resources.getString
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.firmware_update_connecting_attempt
import org.meshtastic.core.resources.firmware_update_downloading_percent
@ -40,7 +44,6 @@ import org.meshtastic.core.resources.firmware_update_retrieval_failed
import org.meshtastic.core.resources.firmware_update_starting_ota
import org.meshtastic.core.resources.firmware_update_uploading
import org.meshtastic.core.resources.firmware_update_waiting_reboot
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.feature.firmware.FirmwareRetriever
import org.meshtastic.feature.firmware.FirmwareUpdateHandler
import org.meshtastic.feature.firmware.FirmwareUpdateState
@ -68,7 +71,8 @@ class Esp32OtaUpdateHandler
@Inject
constructor(
private val firmwareRetriever: FirmwareRetriever,
private val serviceRepository: ServiceRepository,
private val radioController: RadioController,
private val nodeRepository: NodeRepository,
private val centralManager: CentralManager,
@ApplicationContext private val context: Context,
) : FirmwareUpdateHandler {
@ -201,13 +205,11 @@ constructor(
}
private fun triggerRebootOta(mode: Int, hash: ByteArray?) {
val service = serviceRepository.meshService ?: return
try {
val myInfo = service.getMyNodeInfo() ?: return
Logger.i { "ESP32 OTA: Triggering reboot OTA mode $mode with hash" }
service.requestRebootOta(service.getPacketId(), myInfo.myNodeNum, mode, hash)
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
Logger.e(e) { "ESP32 OTA: Failed to trigger reboot OTA" }
val myInfo = nodeRepository.myNodeInfo.value ?: return
val myNodeNum = myInfo.myNodeNum
Logger.i { "ESP32 OTA: Triggering reboot OTA mode $mode with hash" }
CoroutineScope(Dispatchers.IO).launch {
radioController.requestRebootOta(radioController.getPacketId(), myNodeNum, mode, hash)
}
}
@ -216,12 +218,8 @@ constructor(
* interface) cleanly disconnects without reconnection attempts.
*/
private fun disconnectMeshService() {
try {
Logger.i { "ESP32 OTA: Disconnecting mesh service for OTA" }
serviceRepository.meshService?.setDeviceAddress("n")
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
Logger.w(e) { "ESP32 OTA: Error disconnecting mesh service" }
}
Logger.i { "ESP32 OTA: Disconnecting mesh service for OTA" }
radioController.setDeviceAddress("n")
}
private suspend fun obtainFirmwareFile(

View file

@ -33,7 +33,8 @@ import org.junit.Before
import org.junit.Test
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.feature.firmware.FirmwareRetriever
import org.meshtastic.feature.firmware.FirmwareUpdateState
import java.io.IOException
@ -42,12 +43,14 @@ import java.io.IOException
class Esp32OtaUpdateHandlerTest {
private val firmwareRetriever: FirmwareRetriever = mockk()
private val serviceRepository: ServiceRepository = mockk()
private val radioController: RadioController = mockk()
private val nodeRepository: NodeRepository = mockk()
private val centralManager: CentralManager = mockk()
private val context: Context = mockk()
private val contentResolver: ContentResolver = mockk()
private val handler = Esp32OtaUpdateHandler(firmwareRetriever, serviceRepository, centralManager, context)
private val handler =
Esp32OtaUpdateHandler(firmwareRetriever, radioController, nodeRepository, centralManager, context)
@Before
fun setUp() {

View file

@ -87,9 +87,8 @@ import org.meshtastic.core.common.gpsDisabled
import org.meshtastic.core.common.util.DateFormatter
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.database.entity.Packet
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.util.toString
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.calculating
@ -344,7 +343,7 @@ fun MapView(
LaunchedEffect(selectedWaypointId, waypoints) {
if (selectedWaypointId != null && waypoints.containsKey(selectedWaypointId)) {
waypoints[selectedWaypointId]?.data?.waypoint?.let { pt ->
waypoints[selectedWaypointId]?.waypoint?.let { pt ->
val geoPoint = GeoPoint((pt.latitude_i ?: 0) * 1e-7, (pt.longitude_i ?: 0) * 1e-7)
map.controller.setCenter(geoPoint)
map.controller.setZoom(WAYPOINT_ZOOM)
@ -496,7 +495,7 @@ fun MapView(
fun showMarkerLongPressDialog(id: Int) {
performHapticFeedback()
Logger.d { "marker long pressed id=$id" }
val waypoint = waypoints[id]?.data?.waypoint ?: return
val waypoint = waypoints[id]?.waypoint ?: return
// edit only when unlocked or lockedTo myNodeNum
if ((waypoint.locked_to ?: 0) in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) {
showEditWaypointDialog = waypoint
@ -512,13 +511,13 @@ fun MapView(
}
@Suppress("MagicNumber")
fun MapView.onWaypointChanged(waypoints: Collection<Packet>, selectedWaypointId: Int?): List<MarkerWithLabel> {
fun MapView.onWaypointChanged(waypoints: Collection<DataPacket>, selectedWaypointId: Int?): List<MarkerWithLabel> {
return waypoints.mapNotNull { waypoint ->
val pt = waypoint.data.waypoint ?: return@mapNotNull null
val pt = waypoint.waypoint ?: return@mapNotNull null
if (!mapFilterState.showWaypoints) return@mapNotNull null // Use collected mapFilterState
val lock = if ((pt.locked_to ?: 0) != 0) "\uD83D\uDD12" else ""
val time = DateFormatter.formatDateTime(waypoint.received_time)
val label = (pt.name ?: "") + " " + formatAgo((waypoint.received_time / 1000).toInt())
val time = DateFormatter.formatDateTime(waypoint.time)
val label = (pt.name ?: "") + " " + formatAgo((waypoint.time / 1000).toInt())
val emoji = String(Character.toChars(if ((pt.icon ?: 0) == 0) 128205 else pt.icon!!))
val now = nowMillis
val expireTimeMillis = (pt.expire ?: 0) * 1000L
@ -530,7 +529,7 @@ fun MapView(
}
MarkerWithLabel(this, label, emoji).apply {
id = "${pt.id}"
title = "${pt.name} (${getUsername(waypoint.data.from)}$lock)"
title = "${pt.name} (${getUsername(waypoint.from)}$lock)"
snippet = "[$time] ${pt.description} " + getString(Res.string.expires) + ": $expireTimeStr"
position = GeoPoint((pt.latitude_i ?: 0) * 1e-7, (pt.longitude_i ?: 0) * 1e-7)
if (selectedWaypointId == pt.id) {

View file

@ -23,13 +23,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.meshtastic.core.common.BuildConfigProvider
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.model.DataPacket
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.navigation.MapRoutes
import org.meshtastic.core.prefs.map.MapPrefs
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.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.LocalConfig
import javax.inject.Inject
@ -41,12 +41,12 @@ class MapViewModel
constructor(
mapPrefs: MapPrefs,
packetRepository: PacketRepository,
private val nodeRepository: NodeRepository,
serviceRepository: ServiceRepository,
override val nodeRepository: NodeRepository,
radioController: RadioController,
radioConfigRepository: RadioConfigRepository,
buildConfigProvider: BuildConfigProvider,
savedStateHandle: SavedStateHandle,
) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, serviceRepository) {
) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, radioController) {
private val _selectedWaypointId = MutableStateFlow(savedStateHandle.toRoute<MapRoutes.Map>().waypointId)
val selectedWaypointId: StateFlow<Int?> = _selectedWaypointId.asStateFlow()

View file

@ -98,7 +98,7 @@ import org.jetbrains.compose.resources.stringResource
import org.json.JSONObject
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.util.metersIn
import org.meshtastic.core.model.util.mpsToKmph
import org.meshtastic.core.model.util.mpsToMph
@ -272,7 +272,7 @@ fun MapView(
val allNodes by mapViewModel.nodesWithPosition.collectAsStateWithLifecycle(listOf())
val waypoints by mapViewModel.waypoints.collectAsStateWithLifecycle(emptyMap())
val displayableWaypoints = waypoints.values.mapNotNull { it.data.waypoint }
val displayableWaypoints = waypoints.values.mapNotNull { it.waypoint }
val selectedWaypointId by mapViewModel.selectedWaypointId.collectAsStateWithLifecycle()
val tracerouteSelection =

View file

@ -45,14 +45,14 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import org.meshtastic.core.data.model.CustomTileProviderConfig
import org.meshtastic.core.data.repository.CustomTileProviderRepository
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.datastore.UiPreferencesDataSource
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.navigation.MapRoutes
import org.meshtastic.core.prefs.map.GoogleMapsPrefs
import org.meshtastic.core.prefs.map.MapPrefs
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.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.Config
import java.io.File
@ -86,11 +86,11 @@ constructor(
nodeRepository: NodeRepository,
packetRepository: PacketRepository,
radioConfigRepository: RadioConfigRepository,
serviceRepository: ServiceRepository,
radioController: RadioController,
private val customTileProviderRepository: CustomTileProviderRepository,
uiPreferencesDataSource: UiPreferencesDataSource,
savedStateHandle: SavedStateHandle,
) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, serviceRepository) {
) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, radioController) {
private val _selectedWaypointId = MutableStateFlow(savedStateHandle.toRoute<MapRoutes.Map>().waypointId)
val selectedWaypointId: StateFlow<Int?> = _selectedWaypointId.asStateFlow()
@ -344,7 +344,7 @@ constructor(
viewModelScope.launch {
val wpMap = waypoints.first { it.containsKey(wpId) }
wpMap[wpId]?.let { packet ->
val waypoint = packet.data.waypoint!!
val waypoint = packet.waypoint!!
val latLng = LatLng((waypoint.latitude_i ?: 0) / 1e7, (waypoint.longitude_i ?: 0) / 1e7)
cameraPositionState.position = CameraPosition.fromLatLngZoom(latLng, 15f)
}
@ -643,6 +643,9 @@ constructor(
super.onCleared()
(currentTileProvider as? MBTilesProvider)?.close()
}
override fun getUser(userId: String?) =
nodeRepository.getUser(userId ?: org.meshtastic.core.model.DataPacket.ID_BROADCAST)
}
enum class LayerType {

View file

@ -30,7 +30,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
import org.meshtastic.core.ui.component.NodeChip
@Composable

View file

@ -18,7 +18,7 @@ package org.meshtastic.feature.map.model
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.clustering.ClusterItem
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
data class NodeClusterItem(
val node: Node,

View file

@ -16,10 +16,8 @@
*/
package org.meshtastic.feature.map
import android.os.RemoteException
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -29,60 +27,45 @@ import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.database.entity.Packet
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.util.TimeConstants
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.prefs.map.MapPrefs
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.any
import org.meshtastic.core.resources.eight_hours
import org.meshtastic.core.resources.one_day
import org.meshtastic.core.resources.one_hour
import org.meshtastic.core.resources.two_days
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.feature.map.model.TracerouteOverlay
import org.meshtastic.proto.Position
import org.meshtastic.proto.User
import org.meshtastic.proto.Waypoint
@Suppress("MagicNumber")
sealed class LastHeardFilter(val seconds: Long, val label: StringResource) {
data object Any : LastHeardFilter(0L, Res.string.any)
data object OneHour : LastHeardFilter(TimeConstants.ONE_HOUR.inWholeSeconds, Res.string.one_hour)
data object EightHours : LastHeardFilter(TimeConstants.EIGHT_HOURS.inWholeSeconds, Res.string.eight_hours)
data object OneDay : LastHeardFilter(TimeConstants.ONE_DAY.inWholeSeconds, Res.string.one_day)
data object TwoDays : LastHeardFilter(TimeConstants.TWO_DAYS.inWholeSeconds, Res.string.two_days)
companion object {
fun fromSeconds(seconds: Long): LastHeardFilter = entries.find { it.seconds == seconds } ?: Any
val entries = listOf(Any, OneHour, EightHours, OneDay, TwoDays)
}
}
@Suppress("TooManyFunctions")
abstract class BaseMapViewModel(
protected val mapPrefs: MapPrefs,
private val nodeRepository: NodeRepository,
protected open val nodeRepository: NodeRepository,
private val packetRepository: PacketRepository,
private val serviceRepository: ServiceRepository,
private val radioController: RadioController,
) : ViewModel() {
val myNodeInfo = nodeRepository.myNodeInfo
val ourNodeInfo = nodeRepository.ourNodeInfo
val myNodeNum
get() = myNodeInfo.value?.myNodeNum
val myId = nodeRepository.myId
val isConnected =
radioController.connectionState
.map { it is org.meshtastic.core.model.ConnectionState.Connected }
.stateInWhileSubscribed(initialValue = false)
val nodes: StateFlow<List<Node>> =
nodeRepository
.getNodes()
@ -94,79 +77,66 @@ abstract class BaseMapViewModel(
.map { nodes -> nodes.filter { node -> node.validPosition != null } }
.stateInWhileSubscribed(initialValue = emptyList())
val waypoints: StateFlow<Map<Int, Packet>> =
val waypoints: StateFlow<Map<Int, DataPacket>> =
packetRepository
.getWaypoints()
.mapLatest { list ->
list
.associateBy { packet -> packet.data.waypoint!!.id }
.associateBy { packet -> packet.waypoint!!.id }
.filterValues {
val expire = it.data.waypoint!!.expire ?: 0
val expire = it.waypoint?.expire ?: 0
expire == 0 || expire.toLong() > nowSeconds
}
}
.stateInWhileSubscribed(initialValue = emptyMap())
private val showOnlyFavorites = MutableStateFlow(mapPrefs.showOnlyFavorites)
private val showWaypointsOnMap = MutableStateFlow(mapPrefs.showWaypointsOnMap)
private val showPrecisionCircleOnMap = MutableStateFlow(mapPrefs.showPrecisionCircleOnMap)
private val lastHeardFilter = MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardFilter))
private val lastHeardTrackFilter = MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardTrackFilter))
fun setLastHeardFilter(filter: LastHeardFilter) {
mapPrefs.lastHeardFilter = filter.seconds
lastHeardFilter.value = filter
}
fun setLastHeardTrackFilter(filter: LastHeardFilter) {
mapPrefs.lastHeardTrackFilter = filter.seconds
lastHeardTrackFilter.value = filter
}
val ourNodeInfo: StateFlow<Node?> = nodeRepository.ourNodeInfo
fun getNodeByNum(nodeNum: Int): Node? = nodeRepository.nodeDBbyNum.value[nodeNum]
open fun getUser(userId: String?): User = nodeRepository.getUser(userId ?: DataPacket.ID_BROADCAST)
fun getUser(nodeNum: Int): User = nodeRepository.getUser(nodeNum)
fun getNodeOrFallback(nodeNum: Int): Node = getNodeByNum(nodeNum) ?: Node(num = nodeNum, user = getUser(nodeNum))
val isConnected =
serviceRepository.connectionState.map { it.isConnected() }.stateInWhileSubscribed(initialValue = false)
val showOnlyFavoritesOnMap = showOnlyFavorites
fun toggleOnlyFavorites() {
val current = showOnlyFavorites.value
mapPrefs.showOnlyFavorites = !current
showOnlyFavorites.value = !current
val newValue = !showOnlyFavorites.value
showOnlyFavorites.value = newValue
mapPrefs.showOnlyFavorites = newValue
}
private val showWaypoints = MutableStateFlow(mapPrefs.showWaypointsOnMap)
val showWaypointsOnMap = showWaypoints
fun toggleShowWaypointsOnMap() {
val current = showWaypointsOnMap.value
mapPrefs.showWaypointsOnMap = !current
showWaypointsOnMap.value = !current
val newValue = !showWaypoints.value
showWaypoints.value = newValue
mapPrefs.showWaypointsOnMap = newValue
}
private val showPrecisionCircle = MutableStateFlow(mapPrefs.showPrecisionCircleOnMap)
val showPrecisionCircleOnMap = showPrecisionCircle
fun toggleShowPrecisionCircleOnMap() {
val current = showPrecisionCircleOnMap.value
mapPrefs.showPrecisionCircleOnMap = !current
showPrecisionCircleOnMap.value = !current
val newValue = !showPrecisionCircle.value
showPrecisionCircle.value = newValue
mapPrefs.showPrecisionCircleOnMap = newValue
}
fun generatePacketId(): Int? {
return try {
serviceRepository.meshService?.packetId
} catch (ex: RemoteException) {
Logger.e { "RemoteException: ${ex.message}" }
return null
}
private val lastHeardFilterValue = MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardFilter))
val lastHeardFilter = lastHeardFilterValue
fun setLastHeardFilter(filter: LastHeardFilter) {
lastHeardFilterValue.value = filter
mapPrefs.lastHeardFilter = filter.seconds
}
private val lastHeardTrackFilterValue = MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardTrackFilter))
val lastHeardTrackFilter = lastHeardTrackFilterValue
fun setLastHeardTrackFilter(filter: LastHeardFilter) {
lastHeardTrackFilterValue.value = filter
mapPrefs.lastHeardTrackFilter = filter.seconds
}
abstract fun getUser(userId: String?): org.meshtastic.proto.User
fun getNodeOrFallback(nodeNum: Int): Node = nodeRepository.nodeDBbyNum.value[nodeNum] ?: Node(num = nodeNum)
fun deleteWaypoint(id: Int) = viewModelScope.launch(Dispatchers.IO) { packetRepository.deleteWaypoint(id) }
fun sendWaypoint(wpt: Waypoint, contactKey: String = "0${DataPacket.ID_BROADCAST}") {
@ -179,13 +149,11 @@ abstract class BaseMapViewModel(
}
private fun sendDataPacket(p: DataPacket) {
try {
serviceRepository.meshService?.send(p)
} catch (ex: RemoteException) {
Logger.e { "Send DataPacket error: ${ex.message}" }
}
viewModelScope.launch(Dispatchers.IO) { radioController.sendMessage(p) }
}
fun generatePacketId(): Int = radioController.getPacketId()
data class MapFilterState(
val onlyFavorites: Boolean,
val showWaypoints: Boolean,
@ -259,3 +227,17 @@ fun BaseMapViewModel.tracerouteNodeSelection(
nodeLookup = nodesForLookup.associateBy { it.num },
)
}
@Suppress("MagicNumber")
enum class LastHeardFilter(val label: StringResource, val seconds: Long) {
Any(Res.string.any, 0L),
OneHour(Res.string.one_hour, 3600L),
EightHours(Res.string.eight_hours, 28800L),
OneDay(Res.string.one_day, 86400L),
TwoDays(Res.string.two_days, 172800L),
;
companion object {
fun fromSeconds(seconds: Long): LastHeardFilter = entries.find { it.seconds == seconds } ?: Any
}
}

View file

@ -29,10 +29,10 @@ import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.toList
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.prefs.map.MapPrefs
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.ui.util.toPosition
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.feature.map.model.CustomTileSource

View file

@ -40,14 +40,14 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.core.data.model.CustomTileProviderConfig
import org.meshtastic.core.data.repository.CustomTileProviderRepository
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.datastore.UiPreferencesDataSource
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.prefs.map.GoogleMapsPrefs
import org.meshtastic.core.prefs.map.MapPrefs
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.robolectric.RobolectricTestRunner
@OptIn(ExperimentalCoroutinesApi::class)
@ -60,7 +60,7 @@ class MapViewModelTest {
private val nodeRepository = mockk<NodeRepository>(relaxed = true)
private val packetRepository = mockk<PacketRepository>(relaxed = true)
private val radioConfigRepository = mockk<RadioConfigRepository>(relaxed = true)
private val serviceRepository = mockk<ServiceRepository>(relaxed = true)
private val radioController = mockk<RadioController>(relaxed = true)
private val customTileProviderRepository = mockk<CustomTileProviderRepository>(relaxed = true)
private val uiPreferencesDataSource = mockk<UiPreferencesDataSource>(relaxed = true)
private val savedStateHandle = SavedStateHandle(mapOf("waypointId" to null))
@ -81,7 +81,7 @@ class MapViewModelTest {
every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(emptyMap())
every { nodeRepository.getNodes() } returns flowOf(emptyList())
every { packetRepository.getWaypoints() } returns flowOf(emptyList())
every { serviceRepository.connectionState } returns MutableStateFlow(ConnectionState.Disconnected)
every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Disconnected)
viewModel =
MapViewModel(
@ -91,7 +91,7 @@ class MapViewModelTest {
nodeRepository,
packetRepository,
radioConfigRepository,
serviceRepository,
radioController,
customTileProviderRepository,
uiPreferencesDataSource,
savedStateHandle,

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 =

View file

@ -55,7 +55,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.actions
import org.meshtastic.core.resources.direct_message

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,12 +14,11 @@
* 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.node.component
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
@Composable
internal fun InlineMap(node: Node, modifier: Modifier = Modifier) {

View file

@ -31,7 +31,7 @@ import com.google.maps.android.compose.MapsComposeExperimentalApi
import com.google.maps.android.compose.MarkerComposable
import com.google.maps.android.compose.rememberCameraPositionState
import com.google.maps.android.compose.rememberUpdatedMarkerState
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
import org.meshtastic.core.ui.component.NodeChip
import org.meshtastic.core.ui.component.precisionBitsToMeters

View file

@ -33,8 +33,8 @@ import org.meshtastic.core.common.util.bearing
import org.meshtastic.core.common.util.latLongToMeter
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.util.toDistanceString
import org.meshtastic.core.ui.component.precisionBitsToMeters
import org.meshtastic.proto.Config

View file

@ -29,8 +29,9 @@ import androidx.compose.ui.graphics.Color
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.database.entity.asDeviceVersion
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.service.ServiceAction
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.administration
@ -41,7 +42,6 @@ import org.meshtastic.core.resources.latest_alpha_firmware
import org.meshtastic.core.resources.latest_stable_firmware
import org.meshtastic.core.resources.remote_admin
import org.meshtastic.core.resources.request_metadata
import org.meshtastic.core.service.ServiceAction
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusOrange

View file

@ -46,7 +46,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.actions
import org.meshtastic.core.resources.direct_message

View file

@ -35,7 +35,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.util.UnitConversions
import org.meshtastic.core.model.util.UnitConversions.toTempString
import org.meshtastic.core.model.util.toSmallDistanceString

View file

@ -41,7 +41,7 @@ import co.touchlab.kermit.Logger
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.GPSFormat
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.util.metersIn
import org.meshtastic.core.model.util.toString
import org.meshtastic.core.resources.Res

View file

@ -55,8 +55,8 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.util.formatUptime
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.copy

View file

@ -60,7 +60,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.NodeSortOption
import org.meshtastic.core.model.NodeSortOption
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.desc_node_filter_clear
import org.meshtastic.core.resources.node_filter_exclude_infrastructure

View file

@ -51,9 +51,9 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.database.model.isUnmessageableRole
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.isUnmessageableRole
import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit
import org.meshtastic.core.model.util.toDistanceString
import org.meshtastic.core.resources.Res

View file

@ -16,7 +16,7 @@
*/
package org.meshtastic.feature.node.component
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.TelemetryType
sealed class NodeMenuAction {

View file

@ -43,7 +43,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.add_a_note
import org.meshtastic.core.resources.notes

View file

@ -46,7 +46,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.util.toDistanceString
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.exchange_position

View file

@ -26,7 +26,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.channel_1
import org.meshtastic.core.resources.channel_2

View file

@ -47,7 +47,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.TelemetryType
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.logs

View file

@ -59,7 +59,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.details

View file

@ -32,12 +32,12 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.service.ServiceAction
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.resources.UiText
import org.meshtastic.core.service.ServiceAction
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.feature.node.component.NodeMenuAction
import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase
import org.meshtastic.feature.node.metrics.EnvironmentMetricsState

View file

@ -16,14 +16,16 @@
*/
package org.meshtastic.feature.node.detail
import android.os.RemoteException
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.getString
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.model.service.ServiceAction
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.favorite
import org.meshtastic.core.resources.favorite_add
@ -37,8 +39,6 @@ import org.meshtastic.core.resources.mute_remove
import org.meshtastic.core.resources.remove
import org.meshtastic.core.resources.remove_node_text
import org.meshtastic.core.resources.unmute
import org.meshtastic.core.service.ServiceAction
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.ui.util.AlertManager
import javax.inject.Inject
import javax.inject.Singleton
@ -49,6 +49,7 @@ class NodeManagementActions
constructor(
private val nodeRepository: NodeRepository,
private val serviceRepository: ServiceRepository,
private val radioController: RadioController,
private val alertManager: AlertManager,
) {
fun requestRemoveNode(scope: CoroutineScope, node: Node) {
@ -62,13 +63,9 @@ constructor(
fun removeNode(scope: CoroutineScope, nodeNum: Int) {
scope.launch(Dispatchers.IO) {
Logger.i { "Removing node '$nodeNum'" }
try {
val packetId = serviceRepository.meshService?.packetId ?: return@launch
serviceRepository.meshService?.removeByNodenum(packetId, nodeNum)
nodeRepository.deleteNode(nodeNum)
} catch (ex: RemoteException) {
Logger.e { "Remove node error: ${ex.message}" }
}
val packetId = radioController.getPacketId()
radioController.removeByNodenum(packetId, nodeNum)
nodeRepository.deleteNode(nodeNum)
}
}
@ -88,13 +85,7 @@ constructor(
}
fun ignoreNode(scope: CoroutineScope, node: Node) {
scope.launch(Dispatchers.IO) {
try {
serviceRepository.onServiceAction(ServiceAction.Ignore(node))
} catch (ex: RemoteException) {
Logger.e(ex) { "Ignore node error" }
}
}
scope.launch(Dispatchers.IO) { serviceRepository.onServiceAction(ServiceAction.Ignore(node)) }
}
fun requestMuteNode(scope: CoroutineScope, node: Node) {
@ -110,13 +101,7 @@ constructor(
}
fun muteNode(scope: CoroutineScope, node: Node) {
scope.launch(Dispatchers.IO) {
try {
serviceRepository.onServiceAction(ServiceAction.Mute(node))
} catch (ex: RemoteException) {
Logger.e(ex) { "Mute node error" }
}
}
scope.launch(Dispatchers.IO) { serviceRepository.onServiceAction(ServiceAction.Mute(node)) }
}
fun requestFavoriteNode(scope: CoroutineScope, node: Node) {
@ -135,13 +120,7 @@ constructor(
}
fun favoriteNode(scope: CoroutineScope, node: Node) {
scope.launch(Dispatchers.IO) {
try {
serviceRepository.onServiceAction(ServiceAction.Favorite(node))
} catch (ex: RemoteException) {
Logger.e(ex) { "Favorite node error" }
}
}
scope.launch(Dispatchers.IO) { serviceRepository.onServiceAction(ServiceAction.Favorite(node)) }
}
fun setNodeNotes(scope: CoroutineScope, nodeNum: Int, notes: String) {

View file

@ -29,6 +29,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.model.Position
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.model.TelemetryType
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.UiText
@ -44,7 +45,6 @@ import org.meshtastic.core.resources.requesting_from
import org.meshtastic.core.resources.signal_quality
import org.meshtastic.core.resources.traceroute
import org.meshtastic.core.resources.user_info
import org.meshtastic.core.service.ServiceRepository
import javax.inject.Inject
import javax.inject.Singleton
@ -53,7 +53,7 @@ sealed class NodeRequestEffect {
}
@Singleton
class NodeRequestActions @Inject constructor(private val serviceRepository: ServiceRepository) {
class NodeRequestActions @Inject constructor(private val radioController: RadioController) {
private val _effects = MutableSharedFlow<NodeRequestEffect>()
val effects: SharedFlow<NodeRequestEffect> = _effects.asSharedFlow()
@ -67,34 +67,26 @@ class NodeRequestActions @Inject constructor(private val serviceRepository: Serv
fun requestUserInfo(scope: CoroutineScope, destNum: Int, longName: String) {
scope.launch(Dispatchers.IO) {
Logger.i { "Requesting UserInfo for '$destNum'" }
try {
serviceRepository.meshService?.requestUserInfo(destNum)
_effects.emit(
NodeRequestEffect.ShowFeedback(
UiText.Resource(Res.string.requesting_from, Res.string.user_info, longName),
),
)
} catch (ex: android.os.RemoteException) {
Logger.e { "Request NodeInfo error: ${ex.message}" }
}
radioController.requestUserInfo(destNum)
_effects.emit(
NodeRequestEffect.ShowFeedback(
UiText.Resource(Res.string.requesting_from, Res.string.user_info, longName),
),
)
}
}
fun requestNeighborInfo(scope: CoroutineScope, destNum: Int, longName: String) {
scope.launch(Dispatchers.IO) {
Logger.i { "Requesting NeighborInfo for '$destNum'" }
try {
val packetId = serviceRepository.meshService?.packetId ?: return@launch
serviceRepository.meshService?.requestNeighborInfo(packetId, destNum)
_lastRequestNeighborTimes.update { it + (destNum to nowMillis) }
_effects.emit(
NodeRequestEffect.ShowFeedback(
UiText.Resource(Res.string.requesting_from, Res.string.neighbor_info, longName),
),
)
} catch (ex: android.os.RemoteException) {
Logger.e { "Request NeighborInfo error: ${ex.message}" }
}
val packetId = radioController.getPacketId()
radioController.requestNeighborInfo(packetId, destNum)
_lastRequestNeighborTimes.update { it + (destNum to nowMillis) }
_effects.emit(
NodeRequestEffect.ShowFeedback(
UiText.Resource(Res.string.requesting_from, Res.string.neighbor_info, longName),
),
)
}
}
@ -106,61 +98,49 @@ class NodeRequestActions @Inject constructor(private val serviceRepository: Serv
) {
scope.launch(Dispatchers.IO) {
Logger.i { "Requesting position for '$destNum'" }
try {
serviceRepository.meshService?.requestPosition(destNum, position)
_effects.emit(
NodeRequestEffect.ShowFeedback(
UiText.Resource(Res.string.requesting_from, Res.string.position, longName),
),
)
} catch (ex: android.os.RemoteException) {
Logger.e { "Request position error: ${ex.message}" }
}
radioController.requestPosition(destNum, position)
_effects.emit(
NodeRequestEffect.ShowFeedback(
UiText.Resource(Res.string.requesting_from, Res.string.position, longName),
),
)
}
}
fun requestTelemetry(scope: CoroutineScope, destNum: Int, longName: String, type: TelemetryType) {
scope.launch(Dispatchers.IO) {
Logger.i { "Requesting telemetry for '$destNum'" }
try {
val packetId = serviceRepository.meshService?.packetId ?: return@launch
serviceRepository.meshService?.requestTelemetry(packetId, destNum, type.ordinal)
val packetId = radioController.getPacketId()
radioController.requestTelemetry(packetId, destNum, type.ordinal)
val typeRes =
when (type) {
TelemetryType.DEVICE -> Res.string.request_device_metrics
TelemetryType.ENVIRONMENT -> Res.string.request_environment_metrics
TelemetryType.AIR_QUALITY -> Res.string.request_air_quality_metrics
TelemetryType.POWER -> Res.string.request_power_metrics
TelemetryType.LOCAL_STATS -> Res.string.signal_quality
TelemetryType.HOST -> Res.string.request_host_metrics
TelemetryType.PAX -> Res.string.request_pax_metrics
}
val typeRes =
when (type) {
TelemetryType.DEVICE -> Res.string.request_device_metrics
TelemetryType.ENVIRONMENT -> Res.string.request_environment_metrics
TelemetryType.AIR_QUALITY -> Res.string.request_air_quality_metrics
TelemetryType.POWER -> Res.string.request_power_metrics
TelemetryType.LOCAL_STATS -> Res.string.signal_quality
TelemetryType.HOST -> Res.string.request_host_metrics
TelemetryType.PAX -> Res.string.request_pax_metrics
}
_effects.emit(
NodeRequestEffect.ShowFeedback(UiText.Resource(Res.string.requesting_from, typeRes, longName)),
)
} catch (ex: android.os.RemoteException) {
Logger.e { "Request telemetry error: ${ex.message}" }
}
_effects.emit(
NodeRequestEffect.ShowFeedback(UiText.Resource(Res.string.requesting_from, typeRes, longName)),
)
}
}
fun requestTraceroute(scope: CoroutineScope, destNum: Int, longName: String) {
scope.launch(Dispatchers.IO) {
Logger.i { "Requesting traceroute for '$destNum'" }
try {
val packetId = serviceRepository.meshService?.packetId ?: return@launch
serviceRepository.meshService?.requestTraceroute(packetId, destNum)
_lastTracerouteTimes.update { it + (destNum to nowMillis) }
_effects.emit(
NodeRequestEffect.ShowFeedback(
UiText.Resource(Res.string.requesting_from, Res.string.traceroute, longName),
),
)
} catch (ex: android.os.RemoteException) {
Logger.e { "Request traceroute error: ${ex.message}" }
}
val packetId = radioController.getPacketId()
radioController.requestTraceroute(packetId, destNum)
_lastTracerouteTimes.update { it + (destNum to nowMillis) }
_effects.emit(
NodeRequestEffect.ShowFeedback(
UiText.Resource(Res.string.requesting_from, Res.string.traceroute, longName),
),
)
}
}
}

View file

@ -18,9 +18,9 @@ package org.meshtastic.feature.node.domain.usecase
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.database.model.NodeSortOption
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.NodeSortOption
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.feature.node.list.NodeFilterState
import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
import org.meshtastic.proto.Config

View file

@ -23,17 +23,17 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import org.meshtastic.core.data.repository.DeviceHardwareRepository
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.MyNodeInfo
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.util.hasValidEnvironmentMetrics
import org.meshtastic.core.model.util.isDirectSignal
import org.meshtastic.core.repository.DeviceHardwareRepository
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.UiText
import org.meshtastic.core.resources.fallback_node_name
@ -110,7 +110,7 @@ constructor(
nodeRepository.myNodeInfo,
radioConfigRepository.deviceProfileFlow.onStart { emit(DeviceProfile()) },
) { ourNode, myInfo, profile ->
IdentityGroup(ourNode, myInfo?.toMyNodeInfo(), profile)
IdentityGroup(ourNode, myInfo, profile)
}
// 3. Metadata & Request Timestamps

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,12 +14,11 @@
* 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.node.list
import kotlinx.coroutines.flow.map
import org.meshtastic.core.database.model.NodeSortOption
import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.model.NodeSortOption
import javax.inject.Inject
class NodeFilterPreferences @Inject constructor(private val uiPreferencesDataSource: UiPreferencesDataSource) {

View file

@ -67,8 +67,8 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.Node
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.add_favorite
import org.meshtastic.core.resources.channel_invalid

View file

@ -17,11 +17,9 @@
package org.meshtastic.feature.node.list
import android.net.Uri
import android.os.RemoteException
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@ -30,12 +28,13 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.database.model.NodeSortOption
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.NodeSortOption
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.model.util.dispatchMeshtasticUri
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.feature.node.detail.NodeManagementActions
import org.meshtastic.feature.node.domain.usecase.GetFilteredNodesUseCase
@ -53,6 +52,7 @@ constructor(
private val nodeRepository: NodeRepository,
private val radioConfigRepository: RadioConfigRepository,
private val serviceRepository: ServiceRepository,
private val radioController: RadioController,
val nodeManagementActions: NodeManagementActions,
private val getFilteredNodesUseCase: GetFilteredNodesUseCase,
val nodeFilterPreferences: NodeFilterPreferences,
@ -154,11 +154,7 @@ constructor(
radioConfigRepository.replaceAllSettings(channelSet.settings)
val newLoraConfig = channelSet.lora_config
if (newLoraConfig != null) {
try {
serviceRepository.meshService?.setConfig(Config(lora = newLoraConfig).encode())
} catch (ex: RemoteException) {
Logger.e(ex) { "Set config error" }
}
radioController.setLocalConfig(Config(lora = newLoraConfig))
}
}

View file

@ -46,20 +46,20 @@ import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.common.util.toDate
import org.meshtastic.core.common.util.toInstant
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.TracerouteSnapshotRepository
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.TelemetryType
import org.meshtastic.core.model.evaluateTracerouteMapAvailability
import org.meshtastic.core.model.util.UnitConversions
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.okay
import org.meshtastic.core.resources.traceroute
import org.meshtastic.core.resources.view_on_map
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.ui.util.AlertManager
import org.meshtastic.core.ui.util.toMessageRes
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed

View file

@ -16,8 +16,8 @@
*/
package org.meshtastic.feature.node.model
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.database.model.isUnmessageableRole
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.isUnmessageableRole
val Node.isEffectivelyUnmessageable: Boolean
get() = user.is_unmessagable ?: (user.role?.isUnmessageableRole() == true)

View file

@ -18,8 +18,8 @@ package org.meshtastic.feature.node.model
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.model.Node
import org.meshtastic.proto.Config
import org.meshtastic.proto.FirmwareEdition
import org.meshtastic.proto.MeshPacket

View file

@ -16,9 +16,9 @@
*/
package org.meshtastic.feature.node.model
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.service.ServiceAction
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.service.ServiceAction
import org.meshtastic.feature.node.component.NodeMenuAction
import org.meshtastic.proto.Config

View file

@ -23,9 +23,10 @@ import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.ui.util.AlertManager
import org.meshtastic.proto.User
@ -34,6 +35,7 @@ class NodeManagementActionsTest {
private val nodeRepository = mockk<NodeRepository>(relaxed = true)
private val serviceRepository = mockk<ServiceRepository>(relaxed = true)
private val radioController = mockk<RadioController>(relaxed = true)
private val alertManager = mockk<AlertManager>(relaxed = true)
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
@ -42,6 +44,7 @@ class NodeManagementActionsTest {
NodeManagementActions(
nodeRepository = nodeRepository,
serviceRepository = serviceRepository,
radioController = radioController,
alertManager = alertManager,
)

View file

@ -24,9 +24,9 @@ import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.database.model.NodeSortOption
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.NodeSortOption
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.feature.node.list.NodeFilterState
import org.meshtastic.proto.Config
import org.meshtastic.proto.User

View file

@ -40,7 +40,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.administration
import org.meshtastic.core.resources.preserve_favorites

View file

@ -32,11 +32,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.domain.usecase.settings.ExportDataUseCase
import org.meshtastic.core.domain.usecase.settings.IsOtaCapableUseCase
import org.meshtastic.core.domain.usecase.settings.MeshLocationUseCase
@ -45,9 +40,14 @@ import org.meshtastic.core.domain.usecase.settings.SetDatabaseCacheLimitUseCase
import org.meshtastic.core.domain.usecase.settings.SetMeshLogSettingsUseCase
import org.meshtastic.core.domain.usecase.settings.SetProvideLocationUseCase
import org.meshtastic.core.domain.usecase.settings.SetThemeUseCase
import org.meshtastic.core.model.MyNodeInfo
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.prefs.meshlog.MeshLogPrefs
import org.meshtastic.core.prefs.ui.UiPrefs
import org.meshtastic.core.repository.DatabaseManager
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.LocalConfig
import java.io.BufferedWriter
@ -77,7 +77,7 @@ constructor(
private val exportDataUseCase: ExportDataUseCase,
private val isOtaCapableUseCase: IsOtaCapableUseCase,
) : ViewModel() {
val myNodeInfo: StateFlow<MyNodeEntity?> = nodeRepository.myNodeInfo
val myNodeInfo: StateFlow<MyNodeInfo?> = nodeRepository.myNodeInfo
val myNodeNum
get() = myNodeInfo.value?.myNodeNum
@ -170,7 +170,7 @@ constructor(
*/
@Suppress("detekt:CyclomaticComplexMethod", "detekt:LongMethod")
fun saveDataCsv(uri: Uri, filterPortnum: Int? = null) {
viewModelScope.launch(Dispatchers.Main) {
viewModelScope.launch {
val myNodeNum = myNodeNum ?: return@launch
writeToUri(uri) { writer -> exportDataUseCase(writer, myNodeNum, filterPortnum) }
}

View file

@ -37,13 +37,13 @@ import org.meshtastic.core.common.util.nowInstant
import org.meshtastic.core.common.util.toDate
import org.meshtastic.core.common.util.toInstant
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.database.entity.Packet
import org.meshtastic.core.model.getTracerouteResponse
import org.meshtastic.core.model.util.decodeOrNull
import org.meshtastic.core.model.util.toReadableString
import org.meshtastic.core.prefs.meshlog.MeshLogPrefs
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.debug_clear
import org.meshtastic.core.resources.debug_clear_logs_confirm

View file

@ -22,7 +22,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.meshtastic.core.prefs.filter.FilterPrefs
import org.meshtastic.core.service.filter.MessageFilterService
import org.meshtastic.core.repository.MessageFilter
import javax.inject.Inject
@HiltViewModel
@ -30,7 +30,7 @@ class FilterSettingsViewModel
@Inject
constructor(
private val filterPrefs: FilterPrefs,
private val messageFilterService: MessageFilterService,
private val messageFilter: MessageFilter,
) : ViewModel() {
private val _filterEnabled = MutableStateFlow(filterPrefs.filterEnabled)
@ -51,7 +51,7 @@ constructor(
if (current.add(trimmed)) {
filterPrefs.filterWords = current
_filterWords.value = current.toList().sorted()
messageFilterService.rebuildPatterns()
messageFilter.rebuildPatterns()
}
}
@ -60,7 +60,7 @@ constructor(
if (current.remove(word)) {
filterPrefs.filterWords = current
_filterWords.value = current.toList().sorted()
messageFilterService.rebuildPatterns()
messageFilter.rebuildPatterns()
}
}
}

View file

@ -40,7 +40,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.clean_node_database_description
import org.meshtastic.core.resources.clean_node_database_title

View file

@ -24,8 +24,8 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.getString
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.domain.usecase.settings.CleanNodeDatabaseUseCase
import org.meshtastic.core.model.Node
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.are_you_sure
import org.meshtastic.core.resources.clean_node_database_confirmation

View file

@ -43,11 +43,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.data.repository.LocationRepository
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.MyNodeEntity
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.domain.usecase.settings.AdminActionsUseCase
import org.meshtastic.core.domain.usecase.settings.ExportProfileUseCase
import org.meshtastic.core.domain.usecase.settings.ExportSecurityConfigUseCase
@ -59,15 +54,20 @@ import org.meshtastic.core.domain.usecase.settings.RadioResponseResult
import org.meshtastic.core.domain.usecase.settings.ToggleAnalyticsUseCase
import org.meshtastic.core.domain.usecase.settings.ToggleHomoglyphEncodingUseCase
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.MyNodeInfo
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.Position
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs
import org.meshtastic.core.prefs.map.MapConsentPrefs
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.resources.Res
import org.meshtastic.core.resources.UiText
import org.meshtastic.core.resources.cant_shutdown
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.ui.util.getChannelList
import org.meshtastic.feature.settings.navigation.ConfigRoute
import org.meshtastic.feature.settings.navigation.ModuleRoute
@ -217,7 +217,7 @@ constructor(
Logger.d { "RadioConfigViewModel created" }
}
private val myNodeInfo: StateFlow<MyNodeEntity?>
private val myNodeInfo: StateFlow<MyNodeInfo?>
get() = nodeRepository.myNodeInfo
val myNodeNum

View file

@ -33,7 +33,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Node
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.cancel
import org.meshtastic.core.resources.send

View file

@ -24,8 +24,8 @@ import androidx.compose.ui.graphics.Color
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.getColorFrom
import org.meshtastic.core.database.model.getStringResFrom
import org.meshtastic.core.model.getColorFrom
import org.meshtastic.core.model.getStringResFrom
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.tak
import org.meshtastic.core.resources.tak_config

View file

@ -29,8 +29,8 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.isUnmessageableRole
import org.meshtastic.core.model.Capabilities
import org.meshtastic.core.model.isUnmessageableRole
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.hardware_model
import org.meshtastic.core.resources.licensed_amateur_radio

View file

@ -30,9 +30,6 @@ import org.junit.After
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.domain.usecase.settings.ExportDataUseCase
import org.meshtastic.core.domain.usecase.settings.IsOtaCapableUseCase
import org.meshtastic.core.domain.usecase.settings.MeshLocationUseCase
@ -44,8 +41,13 @@ import org.meshtastic.core.domain.usecase.settings.SetThemeUseCase
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.prefs.meshlog.MeshLogPrefs
import org.meshtastic.core.prefs.ui.UiPrefs
import org.meshtastic.core.repository.DatabaseManager
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.robolectric.annotation.Config
@OptIn(ExperimentalCoroutinesApi::class)
@Config(sdk = [34])
class SettingsViewModelTest {
private val testDispatcher = StandardTestDispatcher()
@ -58,14 +60,14 @@ class SettingsViewModelTest {
private val databaseManager: DatabaseManager = mockk(relaxed = true)
private val meshLogPrefs: MeshLogPrefs = mockk(relaxed = true)
private val setThemeUseCase: SetThemeUseCase = mockk(relaxed = true)
private val setAppIntroCompletedUseCase: SetAppIntroCompletedUseCase = mockk(relaxed = true)
private val setProvideLocationUseCase: SetProvideLocationUseCase = mockk(relaxed = true)
private val setDatabaseCacheLimitUseCase: SetDatabaseCacheLimitUseCase = mockk(relaxed = true)
private val setMeshLogSettingsUseCase: SetMeshLogSettingsUseCase = mockk(relaxed = true)
private val meshLocationUseCase: MeshLocationUseCase = mockk(relaxed = true)
private val exportDataUseCase: ExportDataUseCase = mockk(relaxed = true)
private val isOtaCapableUseCase: IsOtaCapableUseCase = mockk(relaxed = true)
private lateinit var setThemeUseCase: SetThemeUseCase
private lateinit var setAppIntroCompletedUseCase: SetAppIntroCompletedUseCase
private lateinit var setProvideLocationUseCase: SetProvideLocationUseCase
private lateinit var setDatabaseCacheLimitUseCase: SetDatabaseCacheLimitUseCase
private lateinit var setMeshLogSettingsUseCase: SetMeshLogSettingsUseCase
private lateinit var meshLocationUseCase: MeshLocationUseCase
private lateinit var exportDataUseCase: ExportDataUseCase
private lateinit var isOtaCapableUseCase: IsOtaCapableUseCase
private lateinit var viewModel: SettingsViewModel
@ -73,6 +75,15 @@ class SettingsViewModelTest {
fun setUp() {
Dispatchers.setMain(testDispatcher)
setThemeUseCase = mockk(relaxed = true)
setAppIntroCompletedUseCase = mockk(relaxed = true)
setProvideLocationUseCase = mockk(relaxed = true)
setDatabaseCacheLimitUseCase = mockk(relaxed = true)
setMeshLogSettingsUseCase = mockk(relaxed = true)
meshLocationUseCase = mockk(relaxed = true)
exportDataUseCase = mockk(relaxed = true)
isOtaCapableUseCase = mockk(relaxed = true)
// Return real StateFlows to avoid ClassCastException
every { databaseManager.cacheLimit } returns MutableStateFlow(100)
every { nodeRepository.myNodeInfo } returns MutableStateFlow(null)

View file

@ -33,8 +33,8 @@ import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.prefs.meshlog.MeshLogPrefs
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.ui.util.AlertManager
@OptIn(ExperimentalCoroutinesApi::class)

View file

@ -23,12 +23,12 @@ import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.prefs.filter.FilterPrefs
import org.meshtastic.core.service.filter.MessageFilterService
import org.meshtastic.core.repository.MessageFilter
class FilterSettingsViewModelTest {
private val filterPrefs: FilterPrefs = mockk(relaxed = true)
private val messageFilterService: MessageFilterService = mockk(relaxed = true)
private val messageFilter: MessageFilter = mockk(relaxed = true)
private lateinit var viewModel: FilterSettingsViewModel
@ -37,7 +37,7 @@ class FilterSettingsViewModelTest {
every { filterPrefs.filterEnabled } returns true
every { filterPrefs.filterWords } returns setOf("apple", "banana")
viewModel = FilterSettingsViewModel(filterPrefs = filterPrefs, messageFilterService = messageFilterService)
viewModel = FilterSettingsViewModel(filterPrefs = filterPrefs, messageFilter = messageFilter)
}
@Test
@ -52,7 +52,7 @@ class FilterSettingsViewModelTest {
viewModel.addFilterWord("cherry")
verify { filterPrefs.filterWords = any() }
verify { messageFilterService.rebuildPatterns() }
verify { messageFilter.rebuildPatterns() }
assertEquals(listOf("apple", "banana", "cherry"), viewModel.filterWords.value)
}
@ -61,7 +61,7 @@ class FilterSettingsViewModelTest {
viewModel.removeFilterWord("apple")
verify { filterPrefs.filterWords = any() }
verify { messageFilterService.rebuildPatterns() }
verify { messageFilter.rebuildPatterns() }
assertEquals(listOf("banana"), viewModel.filterWords.value)
}
}

View file

@ -30,8 +30,8 @@ import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.domain.usecase.settings.CleanNodeDatabaseUseCase
import org.meshtastic.core.model.Node
import org.meshtastic.core.ui.util.AlertManager
@OptIn(ExperimentalCoroutinesApi::class)

View file

@ -34,10 +34,6 @@ import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.data.repository.LocationRepository
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.model.Node
import org.meshtastic.core.domain.usecase.settings.AdminActionsUseCase
import org.meshtastic.core.domain.usecase.settings.ExportProfileUseCase
import org.meshtastic.core.domain.usecase.settings.ExportSecurityConfigUseCase
@ -48,10 +44,14 @@ import org.meshtastic.core.domain.usecase.settings.RadioConfigUseCase
import org.meshtastic.core.domain.usecase.settings.RadioResponseResult
import org.meshtastic.core.domain.usecase.settings.ToggleAnalyticsUseCase
import org.meshtastic.core.domain.usecase.settings.ToggleHomoglyphEncodingUseCase
import org.meshtastic.core.model.Node
import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs
import org.meshtastic.core.prefs.map.MapConsentPrefs
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.proto.ChannelSet
import org.meshtastic.proto.ChannelSettings
import org.meshtastic.proto.Config