diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index b39ed2f9d..38ea3abce 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -204,6 +204,7 @@ dependencies {
implementation(projects.core.strings)
implementation(projects.core.ui)
implementation(projects.feature.intro)
+ implementation(projects.feature.messaging)
implementation(projects.feature.map)
implementation(projects.feature.node)
implementation(projects.feature.settings)
diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml
index 294449788..2f527c472 100644
--- a/app/detekt-baseline.xml
+++ b/app/detekt-baseline.xml
@@ -16,16 +16,8 @@
ComposableParamOrder:EnvironmentCharts.kt$MetricPlottingCanvas
ComposableParamOrder:HostMetricsLog.kt$HostMetricsItem
ComposableParamOrder:HostMetricsLog.kt$LogLine
- ComposableParamOrder:Message.kt$MessageScreen
- ComposableParamOrder:Message.kt$QuickChatRow
- ComposableParamOrder:MessageActions.kt$MessageActions
- ComposableParamOrder:MessageActions.kt$MessageStatusButton
- ComposableParamOrder:MessageItem.kt$MessageItem
- ComposableParamOrder:MessageList.kt$DeliveryInfo
- ComposableParamOrder:MessageList.kt$MessageList
ComposableParamOrder:PaxMetrics.kt$PaxMetricsChart
ComposableParamOrder:PowerMetrics.kt$PowerMetricsChart
- ComposableParamOrder:QuickChat.kt$OutlinedTextFieldWithCounter
ComposableParamOrder:Share.kt$ShareScreen
ComposableParamOrder:SignalMetrics.kt$SignalMetricsChart
CyclomaticComplexMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)
@@ -54,18 +46,11 @@
FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt
ForbiddenComment:SafeBluetooth.kt$SafeBluetooth$// TODO: display some kind of UI about restarting BLE
LambdaParameterEventTrailing:Channel.kt$onConfirm
- LambdaParameterEventTrailing:ContactSharing.kt$onSharedContactRequested
- LambdaParameterEventTrailing:Message.kt$onClick
- LambdaParameterEventTrailing:Message.kt$onSendMessage
- LambdaParameterEventTrailing:MessageList.kt$onReply
- LambdaParameterEventTrailing:QuickChat.kt$onNavigateUp
LambdaParameterEventTrailing:TracerouteLog.kt$onNavigateUp
LambdaParameterInRestartableEffect:Channel.kt$onConfirm
- LambdaParameterInRestartableEffect:MessageList.kt$onUnreadChanged
LargeClass:MeshService.kt$MeshService : Service
LongMethod:EnvironmentMetrics.kt$@Composable fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit)
LongMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)
- LongParameterList:MessageViewModel.kt$MessageViewModel$( private val nodeRepository: NodeRepository, radioConfigRepository: RadioConfigRepository, quickChatActionRepository: QuickChatActionRepository, private val serviceRepository: ServiceRepository, private val packetRepository: PacketRepository, private val uiPrefs: UiPrefs, private val meshServiceNotifications: MeshServiceNotifications, )
MagicNumber:BluetoothInterface.kt$BluetoothInterface$1000
MagicNumber:BluetoothInterface.kt$BluetoothInterface$500
MagicNumber:BluetoothInterface.kt$BluetoothInterface$512
@@ -112,7 +97,6 @@
ModifierMissing:CommonCharts.kt$ChartHeader
ModifierMissing:CommonCharts.kt$Legend
ModifierMissing:CommonCharts.kt$TimeLabels
- ModifierMissing:ContactSharing.kt$SharedContactDialog
ModifierMissing:Contacts.kt$ContactListView
ModifierMissing:Contacts.kt$ContactsScreen
ModifierMissing:Contacts.kt$SelectionToolbar
@@ -121,9 +105,6 @@
ModifierMissing:EnvironmentMetrics.kt$EnvironmentMetricsScreen
ModifierMissing:HostMetricsLog.kt$HostMetricsLogScreen
ModifierMissing:Main.kt$MainScreen
- ModifierMissing:MessageActions.kt$MessageStatusButton
- ModifierMissing:MessageActions.kt$ReactionButton
- ModifierMissing:MessageActions.kt$ReplyButton
ModifierMissing:NetworkDevices.kt$NetworkDevices
ModifierMissing:NodeListScreen.kt$NodeListScreen
ModifierMissing:PaxMetrics.kt$PaxMetricsItem
@@ -131,9 +112,7 @@
ModifierMissing:PositionLog.kt$PositionItem
ModifierMissing:PositionLog.kt$PositionLogScreen
ModifierMissing:PowerMetrics.kt$PowerMetricsScreen
- ModifierMissing:Reaction.kt$ReactionDialog
ModifierMissing:Share.kt$ShareScreen
- ModifierMissing:SharedContactDialog.kt$SharedContactDialog
ModifierMissing:SignalMetrics.kt$SignalMetricsScreen
ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)
ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier = modifier.width(dp)
@@ -144,7 +123,6 @@
ModifierNotUsedAtRoot:PowerMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)
ModifierNotUsedAtRoot:PowerMetrics.kt$modifier = modifier.width(dp)
ModifierNotUsedAtRoot:PowerMetrics.kt$modifier.width(dp)
- ModifierNotUsedAtRoot:QuickChat.kt$modifier = modifier.fillMaxSize().padding(innerPadding)
ModifierNotUsedAtRoot:SignalMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)
ModifierNotUsedAtRoot:SignalMetrics.kt$modifier = modifier.width(dp)
ModifierNotUsedAtRoot:SignalMetrics.kt$modifier.width(dp)
@@ -178,7 +156,6 @@
MultipleEmitters:PowerMetrics.kt$PowerMetricsChart
MultipleEmitters:SignalMetrics.kt$SignalMetricsChart
MutableStateAutoboxing:Contacts.kt$mutableStateOf(2)
- MutableStateParam:MessageList.kt$selectedIds
NestedBlockDepth:MeshService.kt$MeshService$private fun handleReceivedAdmin(fromNodeNum: Int, a: AdminProtos.AdminMessage)
NestedBlockDepth:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)
NewLineAtEndOfFile:BLEException.kt$com.geeksville.mesh.service.BLEException.kt
@@ -207,15 +184,11 @@
NoEmptyClassBody:DebugLogFile.kt$BinaryLogFile${ }
NoSemicolons:DateUtils.kt$DateUtils$;
OptionalAbstractKeyword:SyncContinuation.kt$Continuation$abstract
- ParameterNaming:ContactSharing.kt$onSharedContactRequested
ParameterNaming:Contacts.kt$onDeleteSelected
ParameterNaming:Contacts.kt$onMuteSelected
- ParameterNaming:MessageList.kt$onUnreadChanged
ParameterNaming:UsbDevices.kt$onDeviceSelected
PreviewPublic:Channel.kt$ModemPresetInfoPreview
PreviewPublic:EmptyStateContent.kt$EmptyStateContentPreview
- PreviewPublic:Reaction.kt$ReactionItemPreview
- PreviewPublic:Reaction.kt$ReactionRowPreview
RethrowCaughtException:SyncContinuation.kt$Continuation$throw ex
SwallowedException:BluetoothInterface.kt$BluetoothInterface$ex: CancellationException
SwallowedException:Exceptions.kt$ex: Throwable
@@ -246,13 +219,11 @@
TooManyFunctions:BluetoothInterface.kt$BluetoothInterface : IRadioInterface
TooManyFunctions:MeshService.kt$MeshService : Service
TooManyFunctions:MeshService.kt$MeshService$<no name provided> : Stub
- TooManyFunctions:MessageViewModel.kt$MessageViewModel : ViewModel
TooManyFunctions:RadioInterfaceService.kt$RadioInterfaceService
TooManyFunctions:SafeBluetooth.kt$SafeBluetooth : Closeable
TooManyFunctions:UIState.kt$UIViewModel : ViewModel
TopLevelPropertyNaming:Constants.kt$const val prefix = "com.geeksville.mesh"
UtilityClassWithPublicConstructor:NetworkRepositoryModule.kt$NetworkRepositoryModule
ViewModelForwarding:Main.kt$VersionChecks(uIViewModel)
- Wrapping:Message.kt${ event -> when (event) { is MessageScreenEvent.SendMessage -> { viewModel.sendMessage(event.text, contactKey, event.replyingToPacketId) if (event.replyingToPacketId != null) replyingToPacketId = null messageInputState.clearText() } is MessageScreenEvent.SendReaction -> viewModel.sendReaction(event.emoji, event.messageId, contactKey) is MessageScreenEvent.DeleteMessages -> { viewModel.deleteMessages(event.ids) selectedMessageIds.value = emptySet() showDeleteDialog = false } is MessageScreenEvent.ClearUnreadCount -> viewModel.clearUnreadCount(contactKey, event.lastReadMessageId) is MessageScreenEvent.NodeDetails -> navigateToNodeDetails(event.node.num) is MessageScreenEvent.SetTitle -> viewModel.setTitle(event.title) is MessageScreenEvent.NavigateToMessages -> navigateToMessages(event.contactKey) is MessageScreenEvent.NavigateToNodeDetails -> navigateToNodeDetails(event.nodeNum) MessageScreenEvent.NavigateBack -> onNavigateBack() is MessageScreenEvent.CopyToClipboard -> { clipboardManager.nativeClipboard.setPrimaryClip(ClipData.newPlainText(event.text, event.text)) selectedMessageIds.value = emptySet() } } }
diff --git a/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt b/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt
index 675870516..e47b6e687 100644
--- a/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt
+++ b/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt
@@ -17,39 +17,40 @@
package com.geeksville.mesh
-import android.app.Application
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
-import com.geeksville.mesh.service.MeshServiceNotifications
+import com.geeksville.mesh.service.MeshServiceNotificationsImpl
+import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.meshtastic.core.common.BuildConfigProvider
+import org.meshtastic.core.service.MeshServiceNotifications
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
-object ApplicationModule {
+interface ApplicationModule {
- @Provides fun provideProcessLifecycleOwner(): LifecycleOwner = ProcessLifecycleOwner.get()
+ @Binds fun bindMeshServiceNotifications(impl: MeshServiceNotificationsImpl): MeshServiceNotifications
- @Provides
- fun provideProcessLifecycle(processLifecycleOwner: LifecycleOwner): Lifecycle = processLifecycleOwner.lifecycle
+ companion object {
+ @Provides fun provideProcessLifecycleOwner(): LifecycleOwner = ProcessLifecycleOwner.get()
- @Provides
- fun providesMeshServiceNotifications(application: Application): MeshServiceNotifications =
- MeshServiceNotifications(application)
+ @Provides
+ fun provideProcessLifecycle(processLifecycleOwner: LifecycleOwner): Lifecycle = processLifecycleOwner.lifecycle
- @Singleton
- @Provides
- fun provideBuildConfigProvider(): BuildConfigProvider = object : BuildConfigProvider {
- override val isDebug: Boolean = BuildConfig.DEBUG
- override val applicationId: String = BuildConfig.APPLICATION_ID
- override val versionCode: Int = BuildConfig.VERSION_CODE
- override val versionName: String = BuildConfig.VERSION_NAME
- override val absoluteMinFwVersion: String = BuildConfig.ABS_MIN_FW_VERSION
- override val minFwVersion: String = BuildConfig.MIN_FW_VERSION
+ @Singleton
+ @Provides
+ fun provideBuildConfigProvider(): BuildConfigProvider = object : BuildConfigProvider {
+ override val isDebug: Boolean = BuildConfig.DEBUG
+ override val applicationId: String = BuildConfig.APPLICATION_ID
+ override val versionCode: Int = BuildConfig.VERSION_CODE
+ override val versionName: String = BuildConfig.VERSION_NAME
+ override val absoluteMinFwVersion: String = BuildConfig.ABS_MIN_FW_VERSION
+ override val minFwVersion: String = BuildConfig.MIN_FW_VERSION
+ }
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
index 3eb3e1a7a..ecbf900bc 100644
--- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
@@ -31,10 +31,7 @@ import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import com.geeksville.mesh.repository.radio.MeshActivity
import com.geeksville.mesh.repository.radio.RadioInterfaceService
-import com.geeksville.mesh.service.MeshServiceNotifications
-import com.geeksville.mesh.ui.sharing.toSharedContact
import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -53,17 +50,17 @@ 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.PacketRepository
-import org.meshtastic.core.data.repository.QuickChatActionRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.entity.MyNodeEntity
-import org.meshtastic.core.database.entity.QuickChatAction
import org.meshtastic.core.database.entity.asDeviceVersion
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.model.util.toChannelSet
import org.meshtastic.core.service.IMeshService
+import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.R
+import org.meshtastic.core.ui.component.toSharedContact
import org.meshtastic.proto.AdminProtos
import org.meshtastic.proto.AppOnlyProtos
import org.meshtastic.proto.ConfigProtos.Config
@@ -130,7 +127,6 @@ constructor(
private val serviceRepository: ServiceRepository,
radioInterfaceService: RadioInterfaceService,
meshLogRepository: MeshLogRepository,
- private val quickChatActionRepository: QuickChatActionRepository,
firmwareReleaseRepository: FirmwareReleaseRepository,
private val uiPreferencesDataSource: UiPreferencesDataSource,
private val meshServiceNotifications: MeshServiceNotifications,
@@ -217,12 +213,6 @@ constructor(
.map { it.coerceAtLeast(0) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), 0)
- val quickChatActions
- get() =
- quickChatActionRepository
- .getAllActions()
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
-
// hardware info about our local device (can be null)
val myNodeInfo: StateFlow
get() = nodeDB.myNodeInfo
@@ -337,20 +327,6 @@ constructor(
}
}
- fun addQuickChatAction(action: QuickChatAction) =
- viewModelScope.launch(Dispatchers.IO) { quickChatActionRepository.upsert(action) }
-
- fun deleteQuickChatAction(action: QuickChatAction) =
- viewModelScope.launch(Dispatchers.IO) { quickChatActionRepository.delete(action) }
-
- fun updateActionPositions(actions: List) {
- viewModelScope.launch(Dispatchers.IO) {
- for (position in actions.indices) {
- quickChatActionRepository.setItemPosition(actions[position].uuid, position)
- }
- }
- }
-
val tracerouteResponse: LiveData
get() = serviceRepository.tracerouteResponse.asLiveData()
diff --git a/app/src/main/java/com/geeksville/mesh/navigation/ContactsNavigation.kt b/app/src/main/java/com/geeksville/mesh/navigation/ContactsNavigation.kt
index 9230e1a7b..baee9ecf1 100644
--- a/app/src/main/java/com/geeksville/mesh/navigation/ContactsNavigation.kt
+++ b/app/src/main/java/com/geeksville/mesh/navigation/ContactsNavigation.kt
@@ -24,13 +24,13 @@ import androidx.navigation.navDeepLink
import androidx.navigation.navigation
import androidx.navigation.toRoute
import com.geeksville.mesh.ui.contact.ContactsScreen
-import com.geeksville.mesh.ui.message.MessageScreen
-import com.geeksville.mesh.ui.message.QuickChatScreen
import com.geeksville.mesh.ui.sharing.ShareScreen
import org.meshtastic.core.navigation.ChannelsRoutes
import org.meshtastic.core.navigation.ContactsRoutes
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.navigation.NodesRoutes
+import org.meshtastic.feature.messaging.MessageScreen
+import org.meshtastic.feature.messaging.QuickChatScreen
@Suppress("LongMethod")
fun NavGraphBuilder.contactsGraph(navController: NavHostController) {
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
index 40027ce33..45bf98d24 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
@@ -81,6 +81,8 @@ import org.meshtastic.core.prefs.mesh.MeshPrefs
import org.meshtastic.core.prefs.ui.UiPrefs
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.service.IMeshService
+import org.meshtastic.core.service.MeshServiceNotifications
+import org.meshtastic.core.service.SERVICE_NOTIFY_ID
import org.meshtastic.core.service.ServiceAction
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.R
@@ -354,7 +356,7 @@ class MeshService : Service() {
try {
ServiceCompat.startForeground(
this,
- MeshServiceNotifications.SERVICE_NOTIFY_ID,
+ SERVICE_NOTIFY_ID,
notification,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (hasLocationPermission()) {
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt
similarity index 93%
rename from app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt
rename to app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt
index d1acdfc90..445f87ad3 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt
@@ -37,13 +37,17 @@ import androidx.core.net.toUri
import com.geeksville.mesh.MainActivity
import com.geeksville.mesh.R.raw
import com.geeksville.mesh.service.ReplyReceiver.Companion.KEY_TEXT_REPLY
+import dagger.hilt.android.qualifiers.ApplicationContext
import org.meshtastic.core.database.entity.NodeEntity
import org.meshtastic.core.model.util.formatUptime
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
+import org.meshtastic.core.service.MeshServiceNotifications
+import org.meshtastic.core.service.SERVICE_NOTIFY_ID
import org.meshtastic.core.strings.R
import org.meshtastic.proto.MeshProtos
import org.meshtastic.proto.TelemetryProtos
import org.meshtastic.proto.TelemetryProtos.LocalStats
+import javax.inject.Inject
/**
* Manages the creation and display of all app notifications.
@@ -52,14 +56,14 @@ import org.meshtastic.proto.TelemetryProtos.LocalStats
* notifications for various events like new messages, alerts, and service status changes.
*/
@Suppress("TooManyFunctions")
-class MeshServiceNotifications(private val context: Context) {
+class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext private val context: Context) :
+ MeshServiceNotifications {
private val notificationManager = context.getSystemService()!!
companion object {
private const val FIFTEEN_MINUTES_IN_MILLIS = 15L * 60 * 1000
const val MAX_BATTERY_LEVEL = 100
- const val SERVICE_NOTIFY_ID = 101
private val NOTIFICATION_LIGHT_COLOR = Color.BLUE
}
@@ -139,7 +143,7 @@ class MeshServiceNotifications(private val context: Context) {
}
}
- fun clearNotifications() {
+ override fun clearNotifications() {
notificationManager.cancelAll()
}
@@ -147,7 +151,7 @@ class MeshServiceNotifications(private val context: Context) {
* Creates all necessary notification channels on devices running Android O or newer. This should be called once
* when the service is created.
*/
- fun initChannels() {
+ override fun initChannels() {
NotificationType.allTypes().forEach { type -> createNotificationChannel(type) }
}
@@ -212,9 +216,9 @@ class MeshServiceNotifications(private val context: Context) {
var cachedMessage: String? = null
// region Public Notification Methods
- fun updateServiceStateNotification(
+ override fun updateServiceStateNotification(
summaryString: String?,
- telemetry: TelemetryProtos.Telemetry? = cachedTelemetry,
+ telemetry: TelemetryProtos.Telemetry?,
): Notification {
val hasLocalStats = telemetry?.hasLocalStats() == true
val hasDeviceMetrics = telemetry?.hasDeviceMetrics() == true
@@ -249,39 +253,39 @@ class MeshServiceNotifications(private val context: Context) {
return notification
}
- fun updateMessageNotification(contactKey: String, name: String, message: String, isBroadcast: Boolean) {
+ override fun updateMessageNotification(contactKey: String, name: String, message: String, isBroadcast: Boolean) {
val notification = createMessageNotification(contactKey, name, message, isBroadcast)
// Use a consistent, unique ID for each message conversation.
notificationManager.notify(contactKey.hashCode(), notification)
}
- fun showAlertNotification(contactKey: String, name: String, alert: String) {
+ override fun showAlertNotification(contactKey: String, name: String, alert: String) {
val notification = createAlertNotification(contactKey, name, alert)
// Use a consistent, unique ID for each alert source.
notificationManager.notify(name.hashCode(), notification)
}
- fun showNewNodeSeenNotification(node: NodeEntity) {
+ override fun showNewNodeSeenNotification(node: NodeEntity) {
val notification = createNewNodeSeenNotification(node.user.shortName, node.user.longName)
notificationManager.notify(node.num, notification)
}
- fun showOrUpdateLowBatteryNotification(node: NodeEntity, isRemote: Boolean) {
+ override fun showOrUpdateLowBatteryNotification(node: NodeEntity, isRemote: Boolean) {
val notification = createLowBatteryNotification(node, isRemote)
notificationManager.notify(node.num, notification)
}
- fun showClientNotification(clientNotification: MeshProtos.ClientNotification) {
+ override fun showClientNotification(clientNotification: MeshProtos.ClientNotification) {
val notification =
createClientNotification(context.getString(R.string.client_notification), clientNotification.message)
notificationManager.notify(clientNotification.toString().hashCode(), notification)
}
- fun cancelMessageNotification(contactKey: String) = notificationManager.cancel(contactKey.hashCode())
+ override fun cancelMessageNotification(contactKey: String) = notificationManager.cancel(contactKey.hashCode())
- fun cancelLowBatteryNotification(node: NodeEntity) = notificationManager.cancel(node.num)
+ override fun cancelLowBatteryNotification(node: NodeEntity) = notificationManager.cancel(node.num)
- fun clearClientNotification(notification: MeshProtos.ClientNotification) =
+ override fun clearClientNotification(notification: MeshProtos.ClientNotification) =
notificationManager.cancel(notification.toString().hashCode())
// endregion
diff --git a/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt b/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt
index 257405509..d3032b4bf 100644
--- a/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt
@@ -22,6 +22,7 @@ import androidx.core.app.RemoteInput
import dagger.hilt.android.AndroidEntryPoint
import jakarta.inject.Inject
import org.meshtastic.core.model.DataPacket
+import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.core.service.ServiceRepository
/**
@@ -58,7 +59,7 @@ class ReplyReceiver : BroadcastReceiver() {
val contactKey = intent.getStringExtra(CONTACT_KEY) ?: ""
val message = remoteInput.getCharSequence(KEY_TEXT_REPLY)?.toString() ?: ""
sendMessage(message, contactKey)
- MeshServiceNotifications(context).cancelMessageNotification(contactKey)
+ MeshServiceNotificationsImpl(context).cancelMessageNotification(contactKey)
}
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt
index 2ce8596aa..5e1468819 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt
@@ -97,7 +97,6 @@ import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
import com.geeksville.mesh.ui.connections.DeviceType
import com.geeksville.mesh.ui.connections.components.ConnectionsNavIcon
import com.geeksville.mesh.ui.metrics.annotateTraceroute
-import com.geeksville.mesh.ui.sharing.SharedContactDialog
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
@@ -119,6 +118,7 @@ import org.meshtastic.core.ui.icon.Map
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Nodes
import org.meshtastic.core.ui.icon.Settings
+import org.meshtastic.core.ui.share.SharedContactDialog
import org.meshtastic.core.ui.theme.StatusColors.StatusBlue
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.feature.settings.navigation.settingsGraph
diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetailList.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetailList.kt
index eb75360a8..07415b55d 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetailList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetailList.kt
@@ -36,10 +36,10 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
-import com.geeksville.mesh.ui.sharing.SharedContactDialog
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.strings.R
+import org.meshtastic.core.ui.component.SharedContactDialog
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
import org.meshtastic.core.ui.theme.AppTheme
diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeListScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeListScreen.kt
index 4248ca6dd..56b4647f9 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeListScreen.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeListScreen.kt
@@ -57,14 +57,14 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.geeksville.mesh.ui.sharing.AddContactFAB
-import com.geeksville.mesh.ui.sharing.supportsQrCodeSharing
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.R
+import org.meshtastic.core.ui.component.AddContactFAB
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.rememberTimeTickWithLifecycle
+import org.meshtastic.core.ui.component.supportsQrCodeSharing
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
import org.meshtastic.feature.node.component.NodeActionDialogs
import org.meshtastic.feature.node.component.NodeFilterTextField
diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt
index 2acf3e70b..eae8e27dd 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt
@@ -469,7 +469,7 @@ private fun QrCodeImage(
) = Image(
painter =
channelSet.qrCode(shouldAddChannel)?.let { BitmapPainter(it.asImageBitmap()) }
- ?: painterResource(id = com.geeksville.mesh.R.drawable.qrcode),
+ ?: painterResource(id = org.meshtastic.core.ui.R.drawable.qrcode),
contentDescription = stringResource(R.string.qr_code),
modifier = modifier,
contentScale = ContentScale.Inside,
diff --git a/build.gradle.kts b/build.gradle.kts
index 4f6f4a1e4..6d1947e60 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -87,6 +87,7 @@ dependencies {
kover(projects.core.network)
kover(projects.core.prefs)
kover(projects.feature.intro)
+ kover(projects.feature.messaging)
kover(projects.feature.map)
kover(projects.feature.node)
kover(projects.feature.settings)
diff --git a/core/service/detekt-baseline.xml b/core/service/detekt-baseline.xml
new file mode 100644
index 000000000..5e0807579
--- /dev/null
+++ b/core/service/detekt-baseline.xml
@@ -0,0 +1,7 @@
+
+
+
+
+ TooManyFunctions:MeshServiceNotifications.kt$MeshServiceNotifications
+
+
diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/MeshServiceNotifications.kt b/core/service/src/main/kotlin/org/meshtastic/core/service/MeshServiceNotifications.kt
new file mode 100644
index 000000000..4d4c346f3
--- /dev/null
+++ b/core/service/src/main/kotlin/org/meshtastic/core/service/MeshServiceNotifications.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2025 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.meshtastic.core.service
+
+import android.app.Notification
+import org.meshtastic.core.database.entity.NodeEntity
+import org.meshtastic.proto.MeshProtos
+import org.meshtastic.proto.TelemetryProtos
+
+const val SERVICE_NOTIFY_ID = 101
+
+interface MeshServiceNotifications {
+ fun clearNotifications()
+
+ fun initChannels()
+
+ fun updateServiceStateNotification(summaryString: String?, telemetry: TelemetryProtos.Telemetry?): Notification
+
+ fun updateMessageNotification(contactKey: String, name: String, message: String, isBroadcast: Boolean)
+
+ fun showAlertNotification(contactKey: String, name: String, alert: String)
+
+ fun showNewNodeSeenNotification(node: NodeEntity)
+
+ fun showOrUpdateLowBatteryNotification(node: NodeEntity, isRemote: Boolean)
+
+ fun showClientNotification(clientNotification: MeshProtos.ClientNotification)
+
+ fun cancelMessageNotification(contactKey: String)
+
+ fun cancelLowBatteryNotification(node: NodeEntity)
+
+ fun clearClientNotification(notification: MeshProtos.ClientNotification)
+}
diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts
index c4b6ba28d..999ce4f9b 100644
--- a/core/ui/build.gradle.kts
+++ b/core/ui/build.gradle.kts
@@ -24,12 +24,15 @@ plugins {
android { namespace = "org.meshtastic.core.ui" }
dependencies {
+ implementation(projects.core.data)
implementation(projects.core.database)
implementation(projects.core.model)
implementation(projects.core.prefs)
implementation(projects.core.proto)
+ implementation(projects.core.service)
implementation(projects.core.strings)
+ implementation(libs.accompanist.permissions)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.material.iconsExtended)
implementation(libs.androidx.compose.material3)
@@ -38,4 +41,7 @@ dependencies {
implementation(libs.androidx.emoji2.emojipicker)
implementation(libs.androidx.hilt.lifecycle.viewmodel.compose)
implementation(libs.guava)
+ implementation(libs.zxing.core)
+ implementation(libs.zxing.android.embedded)
+ implementation(libs.timber)
}
diff --git a/core/ui/detekt-baseline.xml b/core/ui/detekt-baseline.xml
index a50dd0eb0..22891816e 100644
--- a/core/ui/detekt-baseline.xml
+++ b/core/ui/detekt-baseline.xml
@@ -8,9 +8,11 @@
ComposableParamOrder:MainAppBar.kt$MainAppBar
ComposableParamOrder:MaterialBatteryInfo.kt$MaterialBatteryInfo
ComposableParamOrder:NodeChip.kt$NodeChip
+ ComposableParamOrder:NodeKeyStatusIcon.kt$NodeKeyStatusIcon
ComposableParamOrder:SignalInfo.kt$SignalInfo
ComposableParamOrder:SwitchPreference.kt$SwitchPreference
ContentSlotReused:AdaptiveTwoPane.kt$second
+ LambdaParameterEventTrailing:ContactSharing.kt$onSharedContactRequested
LambdaParameterEventTrailing:MainAppBar.kt$onClickChip
MagicNumber:EditIPv4Preference.kt$0xff
MagicNumber:EditIPv4Preference.kt$16
@@ -21,6 +23,7 @@
MagicNumber:EditListPreference.kt$67890
MagicNumber:LazyColumnDragAndDropDemo.kt$50
ModifierMissing:AdaptiveTwoPane.kt$AdaptiveTwoPane
+ ModifierMissing:ContactSharing.kt$SharedContactDialog
ModifierMissing:EmojiPicker.kt$EmojiPicker
ModifierMissing:EmojiPicker.kt$EmojiPickerDialog
ModifierMissing:IndoorAirQuality.kt$IndoorAirQuality
@@ -33,6 +36,7 @@
ModifierMissing:SettingsItem.kt$SettingsItem
ModifierMissing:SettingsItem.kt$SettingsItemDetail
ModifierMissing:SettingsItem.kt$SettingsItemSwitch
+ ModifierMissing:SharedContactDialog.kt$SharedContactDialog
ModifierMissing:SimpleAlertDialog.kt$SimpleAlertDialog
ModifierMissing:SlidingSelector.kt$OptionLabel
ModifierNotUsedAtRoot:TextDividerPreference.kt$modifier = modifier.fillMaxWidth().padding(all = 16.dp)
@@ -44,6 +48,7 @@
ModifierReused:TextDividerPreference.kt$Row(modifier = modifier.fillMaxWidth().padding(all = 16.dp), verticalAlignment = Alignment.CenterVertically) { Text( text = title, style = MaterialTheme.typography.bodyLarge, color = if (!enabled) { MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) } else { Color.Unspecified }, ) if (trailingIcon != null) { Icon(trailingIcon, "trailingIcon", modifier = modifier.fillMaxWidth().wrapContentWidth(Alignment.End)) } }
MultipleEmitters:PreferenceCategory.kt$PreferenceCategory
ParameterNaming:BitwisePreference.kt$onItemSelected
+ ParameterNaming:ContactSharing.kt$onSharedContactRequested
ParameterNaming:DropDownPreference.kt$onItemSelected
ParameterNaming:EditIPv4Preference.kt$onValueChanged
ParameterNaming:EditListPreference.kt$onValuesChanged
diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/ContactSharing.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ContactSharing.kt
similarity index 95%
rename from app/src/main/java/com/geeksville/mesh/ui/sharing/ContactSharing.kt
rename to core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ContactSharing.kt
index 81b89898c..6aedd455c 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/sharing/ContactSharing.kt
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ContactSharing.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.sharing
+package org.meshtastic.core.ui.component
import android.Manifest
import android.graphics.Bitmap
@@ -57,9 +57,8 @@ import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DeviceVersion
-import org.meshtastic.core.strings.R
-import org.meshtastic.core.ui.component.CopyIconButton
-import org.meshtastic.core.ui.component.SimpleAlertDialog
+import org.meshtastic.core.ui.R
+import org.meshtastic.core.ui.share.SharedContactDialog
import org.meshtastic.proto.AdminProtos
import org.meshtastic.proto.MeshProtos
import timber.log.Timber
@@ -128,16 +127,17 @@ fun AddContactFAB(
}
},
) {
- Icon(imageVector = Icons.TwoTone.QrCodeScanner, contentDescription = stringResource(R.string.scan_qr_code))
+ Icon(
+ imageVector = Icons.TwoTone.QrCodeScanner,
+ contentDescription = stringResource(org.meshtastic.core.strings.R.string.scan_qr_code),
+ )
}
}
@Composable
private fun QrCodeImage(uri: Uri, modifier: Modifier = Modifier) = Image(
- painter =
- uri.qrCode?.let { BitmapPainter(it.asImageBitmap()) }
- ?: painterResource(id = com.geeksville.mesh.R.drawable.qrcode),
- contentDescription = stringResource(R.string.qr_code),
+ painter = uri.qrCode?.let { BitmapPainter(it.asImageBitmap()) } ?: painterResource(id = R.drawable.qrcode),
+ contentDescription = stringResource(org.meshtastic.core.strings.R.string.qr_code),
modifier = modifier,
contentScale = ContentScale.Inside,
)
@@ -165,7 +165,7 @@ fun SharedContactDialog(contact: Node?, onDismiss: () -> Unit) {
val sharedContact = AdminProtos.SharedContact.newBuilder().setUser(contact.user).setNodeNum(contact.num).build()
val uri = sharedContact.getSharedContactUrl()
SimpleAlertDialog(
- title = R.string.share_contact,
+ title = org.meshtastic.core.strings.R.string.share_contact,
text = {
Column {
Text(contact.user.longName)
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeKeyStatusIcon.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/NodeKeyStatusIcon.kt
similarity index 98%
rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeKeyStatusIcon.kt
rename to core/ui/src/main/kotlin/org/meshtastic/core/ui/component/NodeKeyStatusIcon.kt
index d73143585..33cf89e6f 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeKeyStatusIcon.kt
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/NodeKeyStatusIcon.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package org.meshtastic.feature.node.component
+package org.meshtastic.core.ui.component
import android.util.Base64
import androidx.annotation.StringRes
@@ -57,7 +57,6 @@ import androidx.compose.ui.window.Dialog
import com.google.protobuf.ByteString
import org.meshtastic.core.model.Channel
import org.meshtastic.core.strings.R
-import org.meshtastic.core.ui.component.CopyIconButton
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/SharedContactDialog.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactDialog.kt
similarity index 94%
rename from app/src/main/java/com/geeksville/mesh/ui/sharing/SharedContactDialog.kt
rename to core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactDialog.kt
index 8174ddaf2..1fe6c2681 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/sharing/SharedContactDialog.kt
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactDialog.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.sharing
+package org.meshtastic.core.ui.share
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.HorizontalDivider
@@ -28,6 +28,8 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.SimpleAlertDialog
+import org.meshtastic.core.ui.component.compareUsers
+import org.meshtastic.core.ui.component.userFieldsToString
import org.meshtastic.proto.AdminProtos
/** A dialog for importing a shared contact that was scanned from a QR code. */
diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/SharedContactViewModel.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactViewModel.kt
similarity index 97%
rename from app/src/main/java/com/geeksville/mesh/ui/sharing/SharedContactViewModel.kt
rename to core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactViewModel.kt
index c007c12ee..b8d4ef686 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/sharing/SharedContactViewModel.kt
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactViewModel.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.sharing
+package org.meshtastic.core.ui.share
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
diff --git a/app/src/main/res/drawable-nodpi/qrcode.png b/core/ui/src/main/res/drawable-nodpi/qrcode.png
similarity index 100%
rename from app/src/main/res/drawable-nodpi/qrcode.png
rename to core/ui/src/main/res/drawable-nodpi/qrcode.png
diff --git a/feature/messaging/build.gradle.kts b/feature/messaging/build.gradle.kts
new file mode 100644
index 000000000..536988256
--- /dev/null
+++ b/feature/messaging/build.gradle.kts
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2025 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+plugins {
+ alias(libs.plugins.kover)
+ alias(libs.plugins.meshtastic.android.library)
+ alias(libs.plugins.meshtastic.android.library.compose)
+ alias(libs.plugins.meshtastic.hilt)
+}
+
+android { namespace = "org.meshtastic.feature.messaging" }
+
+dependencies {
+ implementation(projects.core.data)
+ implementation(projects.core.database)
+ implementation(projects.core.model)
+ implementation(projects.core.prefs)
+ implementation(projects.core.proto)
+ implementation(projects.core.service)
+ implementation(projects.core.strings)
+ implementation(projects.core.ui)
+
+ implementation(libs.androidx.compose.material.iconsExtended)
+ implementation(libs.androidx.compose.material3)
+ implementation(libs.androidx.compose.ui.text)
+ implementation(libs.androidx.compose.ui.tooling.preview)
+ implementation(libs.androidx.hilt.lifecycle.viewmodel.compose)
+ implementation(libs.timber)
+
+ debugImplementation(libs.androidx.compose.ui.test.manifest)
+
+ androidTestImplementation(libs.androidx.compose.ui.test.junit4)
+ androidTestImplementation(libs.androidx.test.ext.junit)
+}
diff --git a/feature/messaging/detekt-baseline.xml b/feature/messaging/detekt-baseline.xml
new file mode 100644
index 000000000..8f7848890
--- /dev/null
+++ b/feature/messaging/detekt-baseline.xml
@@ -0,0 +1,32 @@
+
+
+
+
+ ComposableParamOrder:Message.kt$MessageScreen
+ ComposableParamOrder:Message.kt$QuickChatRow
+ ComposableParamOrder:MessageActions.kt$MessageActions
+ ComposableParamOrder:MessageActions.kt$MessageStatusButton
+ ComposableParamOrder:MessageItem.kt$MessageItem
+ ComposableParamOrder:MessageList.kt$DeliveryInfo
+ ComposableParamOrder:MessageList.kt$MessageList
+ ComposableParamOrder:QuickChat.kt$OutlinedTextFieldWithCounter
+ LambdaParameterEventTrailing:Message.kt$onClick
+ LambdaParameterEventTrailing:Message.kt$onSendMessage
+ LambdaParameterEventTrailing:MessageList.kt$onReply
+ LambdaParameterEventTrailing:QuickChat.kt$onNavigateUp
+ LambdaParameterInRestartableEffect:MessageList.kt$onUnreadChanged
+ LongParameterList:MessageViewModel.kt$MessageViewModel$( private val nodeRepository: NodeRepository, radioConfigRepository: RadioConfigRepository, quickChatActionRepository: QuickChatActionRepository, private val serviceRepository: ServiceRepository, private val packetRepository: PacketRepository, private val uiPrefs: UiPrefs, private val meshServiceNotifications: MeshServiceNotifications, )
+ ModifierMissing:Message.kt$MessageScreen
+ ModifierMissing:MessageActions.kt$MessageStatusButton
+ ModifierMissing:MessageActions.kt$ReactionButton
+ ModifierMissing:MessageActions.kt$ReplyButton
+ ModifierMissing:Reaction.kt$ReactionDialog
+ ModifierNotUsedAtRoot:QuickChat.kt$modifier = modifier.fillMaxSize().padding(innerPadding)
+ MutableStateParam:MessageList.kt$selectedIds
+ ParameterNaming:MessageList.kt$onUnreadChanged
+ PreviewPublic:Reaction.kt$ReactionItemPreview
+ PreviewPublic:Reaction.kt$ReactionRowPreview
+ TooManyFunctions:MessageViewModel.kt$MessageViewModel : ViewModel
+ Wrapping:Message.kt${ event -> when (event) { is MessageScreenEvent.SendMessage -> { viewModel.sendMessage(event.text, contactKey, event.replyingToPacketId) if (event.replyingToPacketId != null) replyingToPacketId = null messageInputState.clearText() } is MessageScreenEvent.SendReaction -> viewModel.sendReaction(event.emoji, event.messageId, contactKey) is MessageScreenEvent.DeleteMessages -> { viewModel.deleteMessages(event.ids) selectedMessageIds.value = emptySet() showDeleteDialog = false } is MessageScreenEvent.ClearUnreadCount -> viewModel.clearUnreadCount(contactKey, event.lastReadMessageId) is MessageScreenEvent.NodeDetails -> navigateToNodeDetails(event.node.num) is MessageScreenEvent.SetTitle -> viewModel.setTitle(event.title) is MessageScreenEvent.NavigateToMessages -> navigateToMessages(event.contactKey) is MessageScreenEvent.NavigateToNodeDetails -> navigateToNodeDetails(event.nodeNum) MessageScreenEvent.NavigateBack -> onNavigateBack() is MessageScreenEvent.CopyToClipboard -> { clipboardManager.nativeClipboard.setPrimaryClip(ClipData.newPlainText(event.text, event.text)) selectedMessageIds.value = emptySet() } } }
+
+
diff --git a/app/src/androidTest/java/com/geeksville/mesh/compose/MessageItemTest.kt b/feature/messaging/src/androidTest/kotlin/org/meshtastic/feature/messaging/component/MessageItemTest.kt
similarity index 97%
rename from app/src/androidTest/java/com/geeksville/mesh/compose/MessageItemTest.kt
rename to feature/messaging/src/androidTest/kotlin/org/meshtastic/feature/messaging/component/MessageItemTest.kt
index f9938dcd2..6de58c9dd 100644
--- a/app/src/androidTest/java/com/geeksville/mesh/compose/MessageItemTest.kt
+++ b/feature/messaging/src/androidTest/kotlin/org/meshtastic/feature/messaging/component/MessageItemTest.kt
@@ -15,13 +15,12 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.compose
+package org.meshtastic.feature.messaging.component
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.geeksville.mesh.ui.message.components.MessageItem
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/Message.kt
similarity index 99%
rename from app/src/main/java/com/geeksville/mesh/ui/message/Message.kt
rename to feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/Message.kt
index f95d45d78..0076c9d91 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt
+++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/Message.kt
@@ -17,7 +17,7 @@
@file:Suppress("TooManyFunctions")
-package com.geeksville.mesh.ui.message
+package org.meshtastic.feature.messaging
import android.content.ClipData
import androidx.compose.animation.AnimatedVisibility
@@ -91,7 +91,6 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.geeksville.mesh.ui.sharing.SharedContactDialog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.meshtastic.core.database.entity.QuickChatAction
@@ -100,9 +99,10 @@ import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.util.getChannel
import org.meshtastic.core.strings.R
+import org.meshtastic.core.ui.component.NodeKeyStatusIcon
import org.meshtastic.core.ui.component.SecurityIcon
+import org.meshtastic.core.ui.component.SharedContactDialog
import org.meshtastic.core.ui.theme.AppTheme
-import org.meshtastic.feature.node.component.NodeKeyStatusIcon
import org.meshtastic.proto.AppOnlyProtos
import java.nio.charset.StandardCharsets
@@ -122,7 +122,7 @@ private const val ROUNDED_CORNER_PERCENT = 100
*/
@Suppress("LongMethod", "CyclomaticComplexMethod") // Due to multiple states and event handling
@Composable
-internal fun MessageScreen(
+fun MessageScreen(
contactKey: String,
message: String,
viewModel: MessageViewModel = hiltViewModel(),
diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/MessageList.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageList.kt
similarity index 98%
rename from app/src/main/java/com/geeksville/mesh/ui/message/MessageList.kt
rename to feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageList.kt
index 93fb72289..c07a3c90f 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/message/MessageList.kt
+++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageList.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.message
+package org.meshtastic.feature.messaging
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.fillMaxSize
@@ -46,8 +46,6 @@ import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import com.geeksville.mesh.ui.message.components.MessageItem
-import com.geeksville.mesh.ui.message.components.ReactionDialog
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
@@ -57,6 +55,8 @@ import org.meshtastic.core.database.model.Message
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.strings.R
+import org.meshtastic.feature.messaging.component.MessageItem
+import org.meshtastic.feature.messaging.component.ReactionDialog
@Composable
fun DeliveryInfo(
diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/MessageScreenEvent.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageScreenEvent.kt
similarity index 90%
rename from app/src/main/java/com/geeksville/mesh/ui/message/MessageScreenEvent.kt
rename to feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageScreenEvent.kt
index 43552ae1e..62bf93aa8 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/message/MessageScreenEvent.kt
+++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageScreenEvent.kt
@@ -15,14 +15,11 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.message
+package org.meshtastic.feature.messaging
import org.meshtastic.core.database.model.Node
-/**
- * Defines the various user interactions that can occur on the [MessageScreen]. These events are typically handled by
- * the [com.geeksville.mesh.model.UIViewModel].
- */
+/** Defines the various user interactions that can occur on the MessageScreen. */
internal sealed interface MessageScreenEvent {
/** Send a new text message. */
data class SendMessage(val text: String, val replyingToPacketId: Int? = null) : MessageScreenEvent
diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/MessageViewModel.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt
similarity index 98%
rename from app/src/main/java/com/geeksville/mesh/ui/message/MessageViewModel.kt
rename to feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt
index 0f7419196..f20817102 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/message/MessageViewModel.kt
+++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt
@@ -15,12 +15,11 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.message
+package org.meshtastic.feature.messaging
import android.os.RemoteException
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.geeksville.mesh.service.MeshServiceNotifications
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -41,6 +40,7 @@ import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.DeviceVersion
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.proto.channelSet
diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/QuickChat.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/QuickChat.kt
similarity index 98%
rename from app/src/main/java/com/geeksville/mesh/ui/message/QuickChat.kt
rename to feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/QuickChat.kt
index 786dabef5..348cf3c05 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/message/QuickChat.kt
+++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/QuickChat.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.message
+package org.meshtastic.feature.messaging
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -71,7 +71,6 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.geeksville.mesh.model.UIViewModel
import org.meshtastic.core.database.entity.QuickChatAction
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.MainAppBar
@@ -81,9 +80,9 @@ import org.meshtastic.core.ui.component.rememberDragDropState
import org.meshtastic.core.ui.theme.AppTheme
@Composable
-internal fun QuickChatScreen(
+fun QuickChatScreen(
modifier: Modifier = Modifier,
- viewModel: UIViewModel = hiltViewModel(),
+ viewModel: QuickChatViewModel = hiltViewModel(),
onNavigateUp: () -> Unit,
) {
val actions by viewModel.quickChatActions.collectAsStateWithLifecycle()
diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/QuickChatViewModel.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/QuickChatViewModel.kt
new file mode 100644
index 000000000..c49cdd6f0
--- /dev/null
+++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/QuickChatViewModel.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2025 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.meshtastic.feature.messaging
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import org.meshtastic.core.data.repository.QuickChatActionRepository
+import org.meshtastic.core.database.entity.QuickChatAction
+import javax.inject.Inject
+
+@HiltViewModel
+class QuickChatViewModel @Inject constructor(private val quickChatActionRepository: QuickChatActionRepository) :
+ ViewModel() {
+ val quickChatActions
+ get() =
+ quickChatActionRepository
+ .getAllActions()
+ .stateIn(viewModelScope, SharingStarted.Companion.WhileSubscribed(5_000), emptyList())
+
+ fun updateActionPositions(actions: List) {
+ viewModelScope.launch(Dispatchers.IO) {
+ for (position in actions.indices) {
+ quickChatActionRepository.setItemPosition(actions[position].uuid, position)
+ }
+ }
+ }
+
+ fun addQuickChatAction(action: QuickChatAction) =
+ viewModelScope.launch(Dispatchers.IO) { quickChatActionRepository.upsert(action) }
+
+ fun deleteQuickChatAction(action: QuickChatAction) =
+ viewModelScope.launch(Dispatchers.IO) { quickChatActionRepository.delete(action) }
+}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageActions.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageActions.kt
similarity index 92%
rename from app/src/main/java/com/geeksville/mesh/ui/message/components/MessageActions.kt
rename to feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageActions.kt
index 187af2a20..69e59484a 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageActions.kt
+++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageActions.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.message.components
+package org.meshtastic.feature.messaging.component
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Row
@@ -44,7 +44,7 @@ import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
@Composable
-fun ReactionButton(onSendReaction: (String) -> Unit = {}) {
+internal fun ReactionButton(onSendReaction: (String) -> Unit = {}) {
var showEmojiPickerDialog by remember { mutableStateOf(false) }
if (showEmojiPickerDialog) {
EmojiPickerDialog(
@@ -61,7 +61,7 @@ fun ReactionButton(onSendReaction: (String) -> Unit = {}) {
}
@Composable
-fun ReplyButton(onClick: () -> Unit = {}) = IconButton(
+private fun ReplyButton(onClick: () -> Unit = {}) = IconButton(
onClick = onClick,
content = {
Icon(imageVector = Icons.AutoMirrored.Filled.Reply, contentDescription = stringResource(R.string.reply))
@@ -69,7 +69,7 @@ fun ReplyButton(onClick: () -> Unit = {}) = IconButton(
)
@Composable
-fun MessageStatusButton(onStatusClick: () -> Unit = {}, status: MessageStatus, fromLocal: Boolean) =
+private fun MessageStatusButton(onStatusClick: () -> Unit = {}, status: MessageStatus, fromLocal: Boolean) =
AnimatedVisibility(visible = fromLocal) {
IconButton(onClick = onStatusClick) {
Icon(
@@ -89,7 +89,7 @@ fun MessageStatusButton(onStatusClick: () -> Unit = {}, status: MessageStatus, f
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
-fun MessageActions(
+internal fun MessageActions(
modifier: Modifier = Modifier,
isLocal: Boolean = false,
status: MessageStatus?,
diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt
similarity index 99%
rename from app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt
rename to feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt
index efdfa044e..65bc8a2e4 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt
+++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.message.components
+package org.meshtastic.feature.messaging.component
import androidx.compose.foundation.background
import androidx.compose.foundation.border
diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/components/Reaction.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt
similarity index 95%
rename from app/src/main/java/com/geeksville/mesh/ui/message/components/Reaction.kt
rename to feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt
index 0baf6676c..b5caa23bb 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/message/components/Reaction.kt
+++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.message.components
+package org.meshtastic.feature.messaging.component
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
@@ -77,7 +77,7 @@ private fun ReactionItem(emoji: String, emojiCount: Int = 1, onClick: () -> Unit
@OptIn(ExperimentalLayoutApi::class)
@Composable
-fun ReactionRow(
+internal fun ReactionRow(
modifier: Modifier = Modifier,
reactions: List = emptyList(),
onSendReaction: (String) -> Unit = {},
@@ -104,10 +104,10 @@ fun ReactionRow(
}
}
-fun reduceEmojis(emojis: List): Map = emojis.groupingBy { it }.eachCount()
+private fun reduceEmojis(emojis: List): Map = emojis.groupingBy { it }.eachCount()
@Composable
-fun ReactionDialog(reactions: List, onDismiss: () -> Unit = {}) =
+internal fun ReactionDialog(reactions: List, onDismiss: () -> Unit = {}) =
BottomSheetDialog(onDismiss = onDismiss, modifier = Modifier.fillMaxHeight(fraction = .3f)) {
val groupedEmojis = reactions.groupBy { it.emoji }
var selectedEmoji by remember { mutableStateOf(null) }
@@ -145,7 +145,7 @@ fun ReactionDialog(reactions: List, onDismiss: () -> Unit = {}) =
@PreviewLightDark
@Composable
-fun ReactionItemPreview() {
+private fun ReactionItemPreview() {
AppTheme {
Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
ReactionItem(emoji = "\uD83D\uDE42")
@@ -157,7 +157,7 @@ fun ReactionItemPreview() {
@Preview
@Composable
-fun ReactionRowPreview() {
+private fun ReactionRowPreview() {
AppTheme {
ReactionRow(
reactions =
diff --git a/feature/node/detekt-baseline.xml b/feature/node/detekt-baseline.xml
index 0d1144994..921b8a5c5 100644
--- a/feature/node/detekt-baseline.xml
+++ b/feature/node/detekt-baseline.xml
@@ -7,7 +7,6 @@
ComposableParamOrder:LinkedCoordinates.kt$LinkedCoordinates
ComposableParamOrder:NodeFilterTextField.kt$NodeFilterTextField
ComposableParamOrder:NodeItem.kt$NodeItem
- ComposableParamOrder:NodeKeyStatusIcon.kt$NodeKeyStatusIcon
ComposableParamOrder:SatelliteCountInfo.kt$SatelliteCountInfo
ComposableParamOrder:TracerouteButton.kt$TracerouteButton
ModifierMissing:NodeStatusIcons.kt$NodeStatusIcons
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeItem.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeItem.kt
index 3116d3410..e69381984 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeItem.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeItem.kt
@@ -52,6 +52,7 @@ import org.meshtastic.core.model.util.toDistanceString
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.MaterialBatteryInfo
import org.meshtastic.core.ui.component.NodeChip
+import org.meshtastic.core.ui.component.NodeKeyStatusIcon
import org.meshtastic.core.ui.component.SignalInfo
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
import org.meshtastic.core.ui.theme.AppTheme
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 46ac9f060..56a4d214d 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -32,6 +32,7 @@ include(
":core:strings",
":core:ui",
":feature:intro",
+ ":feature:messaging",
":feature:map",
":feature:node",
":feature:settings",