Modularize messaging code (#3435)

This commit is contained in:
Phil Oliver 2025-10-12 13:07:03 -04:00 committed by GitHub
parent cd1a54f506
commit 886e9cfede
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 297 additions and 143 deletions

View file

@ -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)

View file

@ -16,16 +16,8 @@
<ID>ComposableParamOrder:EnvironmentCharts.kt$MetricPlottingCanvas</ID>
<ID>ComposableParamOrder:HostMetricsLog.kt$HostMetricsItem</ID>
<ID>ComposableParamOrder:HostMetricsLog.kt$LogLine</ID>
<ID>ComposableParamOrder:Message.kt$MessageScreen</ID>
<ID>ComposableParamOrder:Message.kt$QuickChatRow</ID>
<ID>ComposableParamOrder:MessageActions.kt$MessageActions</ID>
<ID>ComposableParamOrder:MessageActions.kt$MessageStatusButton</ID>
<ID>ComposableParamOrder:MessageItem.kt$MessageItem</ID>
<ID>ComposableParamOrder:MessageList.kt$DeliveryInfo</ID>
<ID>ComposableParamOrder:MessageList.kt$MessageList</ID>
<ID>ComposableParamOrder:PaxMetrics.kt$PaxMetricsChart</ID>
<ID>ComposableParamOrder:PowerMetrics.kt$PowerMetricsChart</ID>
<ID>ComposableParamOrder:QuickChat.kt$OutlinedTextFieldWithCounter</ID>
<ID>ComposableParamOrder:Share.kt$ShareScreen</ID>
<ID>ComposableParamOrder:SignalMetrics.kt$SignalMetricsChart</ID>
<ID>CyclomaticComplexMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID>
@ -54,18 +46,11 @@
<ID>FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
<ID>ForbiddenComment:SafeBluetooth.kt$SafeBluetooth$// TODO: display some kind of UI about restarting BLE</ID>
<ID>LambdaParameterEventTrailing:Channel.kt$onConfirm</ID>
<ID>LambdaParameterEventTrailing:ContactSharing.kt$onSharedContactRequested</ID>
<ID>LambdaParameterEventTrailing:Message.kt$onClick</ID>
<ID>LambdaParameterEventTrailing:Message.kt$onSendMessage</ID>
<ID>LambdaParameterEventTrailing:MessageList.kt$onReply</ID>
<ID>LambdaParameterEventTrailing:QuickChat.kt$onNavigateUp</ID>
<ID>LambdaParameterEventTrailing:TracerouteLog.kt$onNavigateUp</ID>
<ID>LambdaParameterInRestartableEffect:Channel.kt$onConfirm</ID>
<ID>LambdaParameterInRestartableEffect:MessageList.kt$onUnreadChanged</ID>
<ID>LargeClass:MeshService.kt$MeshService : Service</ID>
<ID>LongMethod:EnvironmentMetrics.kt$@Composable fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -&gt; Unit)</ID>
<ID>LongMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID>
<ID>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, )</ID>
<ID>MagicNumber:BluetoothInterface.kt$BluetoothInterface$1000</ID>
<ID>MagicNumber:BluetoothInterface.kt$BluetoothInterface$500</ID>
<ID>MagicNumber:BluetoothInterface.kt$BluetoothInterface$512</ID>
@ -112,7 +97,6 @@
<ID>ModifierMissing:CommonCharts.kt$ChartHeader</ID>
<ID>ModifierMissing:CommonCharts.kt$Legend</ID>
<ID>ModifierMissing:CommonCharts.kt$TimeLabels</ID>
<ID>ModifierMissing:ContactSharing.kt$SharedContactDialog</ID>
<ID>ModifierMissing:Contacts.kt$ContactListView</ID>
<ID>ModifierMissing:Contacts.kt$ContactsScreen</ID>
<ID>ModifierMissing:Contacts.kt$SelectionToolbar</ID>
@ -121,9 +105,6 @@
<ID>ModifierMissing:EnvironmentMetrics.kt$EnvironmentMetricsScreen</ID>
<ID>ModifierMissing:HostMetricsLog.kt$HostMetricsLogScreen</ID>
<ID>ModifierMissing:Main.kt$MainScreen</ID>
<ID>ModifierMissing:MessageActions.kt$MessageStatusButton</ID>
<ID>ModifierMissing:MessageActions.kt$ReactionButton</ID>
<ID>ModifierMissing:MessageActions.kt$ReplyButton</ID>
<ID>ModifierMissing:NetworkDevices.kt$NetworkDevices</ID>
<ID>ModifierMissing:NodeListScreen.kt$NodeListScreen</ID>
<ID>ModifierMissing:PaxMetrics.kt$PaxMetricsItem</ID>
@ -131,9 +112,7 @@
<ID>ModifierMissing:PositionLog.kt$PositionItem</ID>
<ID>ModifierMissing:PositionLog.kt$PositionLogScreen</ID>
<ID>ModifierMissing:PowerMetrics.kt$PowerMetricsScreen</ID>
<ID>ModifierMissing:Reaction.kt$ReactionDialog</ID>
<ID>ModifierMissing:Share.kt$ShareScreen</ID>
<ID>ModifierMissing:SharedContactDialog.kt$SharedContactDialog</ID>
<ID>ModifierMissing:SignalMetrics.kt$SignalMetricsScreen</ID>
<ID>ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)</ID>
<ID>ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier = modifier.width(dp)</ID>
@ -144,7 +123,6 @@
<ID>ModifierNotUsedAtRoot:PowerMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)</ID>
<ID>ModifierNotUsedAtRoot:PowerMetrics.kt$modifier = modifier.width(dp)</ID>
<ID>ModifierNotUsedAtRoot:PowerMetrics.kt$modifier.width(dp)</ID>
<ID>ModifierNotUsedAtRoot:QuickChat.kt$modifier = modifier.fillMaxSize().padding(innerPadding)</ID>
<ID>ModifierNotUsedAtRoot:SignalMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)</ID>
<ID>ModifierNotUsedAtRoot:SignalMetrics.kt$modifier = modifier.width(dp)</ID>
<ID>ModifierNotUsedAtRoot:SignalMetrics.kt$modifier.width(dp)</ID>
@ -178,7 +156,6 @@
<ID>MultipleEmitters:PowerMetrics.kt$PowerMetricsChart</ID>
<ID>MultipleEmitters:SignalMetrics.kt$SignalMetricsChart</ID>
<ID>MutableStateAutoboxing:Contacts.kt$mutableStateOf(2)</ID>
<ID>MutableStateParam:MessageList.kt$selectedIds</ID>
<ID>NestedBlockDepth:MeshService.kt$MeshService$private fun handleReceivedAdmin(fromNodeNum: Int, a: AdminProtos.AdminMessage)</ID>
<ID>NestedBlockDepth:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID>
<ID>NewLineAtEndOfFile:BLEException.kt$com.geeksville.mesh.service.BLEException.kt</ID>
@ -207,15 +184,11 @@
<ID>NoEmptyClassBody:DebugLogFile.kt$BinaryLogFile${ }</ID>
<ID>NoSemicolons:DateUtils.kt$DateUtils$;</ID>
<ID>OptionalAbstractKeyword:SyncContinuation.kt$Continuation$abstract</ID>
<ID>ParameterNaming:ContactSharing.kt$onSharedContactRequested</ID>
<ID>ParameterNaming:Contacts.kt$onDeleteSelected</ID>
<ID>ParameterNaming:Contacts.kt$onMuteSelected</ID>
<ID>ParameterNaming:MessageList.kt$onUnreadChanged</ID>
<ID>ParameterNaming:UsbDevices.kt$onDeviceSelected</ID>
<ID>PreviewPublic:Channel.kt$ModemPresetInfoPreview</ID>
<ID>PreviewPublic:EmptyStateContent.kt$EmptyStateContentPreview</ID>
<ID>PreviewPublic:Reaction.kt$ReactionItemPreview</ID>
<ID>PreviewPublic:Reaction.kt$ReactionRowPreview</ID>
<ID>RethrowCaughtException:SyncContinuation.kt$Continuation$throw ex</ID>
<ID>SwallowedException:BluetoothInterface.kt$BluetoothInterface$ex: CancellationException</ID>
<ID>SwallowedException:Exceptions.kt$ex: Throwable</ID>
@ -246,13 +219,11 @@
<ID>TooManyFunctions:BluetoothInterface.kt$BluetoothInterface : IRadioInterface</ID>
<ID>TooManyFunctions:MeshService.kt$MeshService : Service</ID>
<ID>TooManyFunctions:MeshService.kt$MeshService$&lt;no name provided&gt; : Stub</ID>
<ID>TooManyFunctions:MessageViewModel.kt$MessageViewModel : ViewModel</ID>
<ID>TooManyFunctions:RadioInterfaceService.kt$RadioInterfaceService</ID>
<ID>TooManyFunctions:SafeBluetooth.kt$SafeBluetooth : Closeable</ID>
<ID>TooManyFunctions:UIState.kt$UIViewModel : ViewModel</ID>
<ID>TopLevelPropertyNaming:Constants.kt$const val prefix = "com.geeksville.mesh"</ID>
<ID>UtilityClassWithPublicConstructor:NetworkRepositoryModule.kt$NetworkRepositoryModule</ID>
<ID>ViewModelForwarding:Main.kt$VersionChecks(uIViewModel)</ID>
<ID>Wrapping:Message.kt${ event -&gt; when (event) { is MessageScreenEvent.SendMessage -&gt; { viewModel.sendMessage(event.text, contactKey, event.replyingToPacketId) if (event.replyingToPacketId != null) replyingToPacketId = null messageInputState.clearText() } is MessageScreenEvent.SendReaction -&gt; viewModel.sendReaction(event.emoji, event.messageId, contactKey) is MessageScreenEvent.DeleteMessages -&gt; { viewModel.deleteMessages(event.ids) selectedMessageIds.value = emptySet() showDeleteDialog = false } is MessageScreenEvent.ClearUnreadCount -&gt; viewModel.clearUnreadCount(contactKey, event.lastReadMessageId) is MessageScreenEvent.NodeDetails -&gt; navigateToNodeDetails(event.node.num) is MessageScreenEvent.SetTitle -&gt; viewModel.setTitle(event.title) is MessageScreenEvent.NavigateToMessages -&gt; navigateToMessages(event.contactKey) is MessageScreenEvent.NavigateToNodeDetails -&gt; navigateToNodeDetails(event.nodeNum) MessageScreenEvent.NavigateBack -&gt; onNavigateBack() is MessageScreenEvent.CopyToClipboard -&gt; { clipboardManager.nativeClipboard.setPrimaryClip(ClipData.newPlainText(event.text, event.text)) selectedMessageIds.value = emptySet() } } }</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -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
}
}
}

View file

@ -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<MyNodeEntity?>
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<QuickChatAction>) {
viewModelScope.launch(Dispatchers.IO) {
for (position in actions.indices) {
quickChatActionRepository.setItemPosition(actions[position].uuid, position)
}
}
}
val tracerouteResponse: LiveData<String?>
get() = serviceRepository.tracerouteResponse.asLiveData()

View file

@ -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) {

View file

@ -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()) {

View file

@ -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<NotificationManager>()!!
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

View file

@ -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)
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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)

View file

@ -0,0 +1,7 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>TooManyFunctions:MeshServiceNotifications.kt$MeshServiceNotifications</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}

View file

@ -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)
}

View file

@ -8,9 +8,11 @@
<ID>ComposableParamOrder:MainAppBar.kt$MainAppBar</ID>
<ID>ComposableParamOrder:MaterialBatteryInfo.kt$MaterialBatteryInfo</ID>
<ID>ComposableParamOrder:NodeChip.kt$NodeChip</ID>
<ID>ComposableParamOrder:NodeKeyStatusIcon.kt$NodeKeyStatusIcon</ID>
<ID>ComposableParamOrder:SignalInfo.kt$SignalInfo</ID>
<ID>ComposableParamOrder:SwitchPreference.kt$SwitchPreference</ID>
<ID>ContentSlotReused:AdaptiveTwoPane.kt$second</ID>
<ID>LambdaParameterEventTrailing:ContactSharing.kt$onSharedContactRequested</ID>
<ID>LambdaParameterEventTrailing:MainAppBar.kt$onClickChip</ID>
<ID>MagicNumber:EditIPv4Preference.kt$0xff</ID>
<ID>MagicNumber:EditIPv4Preference.kt$16</ID>
@ -21,6 +23,7 @@
<ID>MagicNumber:EditListPreference.kt$67890</ID>
<ID>MagicNumber:LazyColumnDragAndDropDemo.kt$50</ID>
<ID>ModifierMissing:AdaptiveTwoPane.kt$AdaptiveTwoPane</ID>
<ID>ModifierMissing:ContactSharing.kt$SharedContactDialog</ID>
<ID>ModifierMissing:EmojiPicker.kt$EmojiPicker</ID>
<ID>ModifierMissing:EmojiPicker.kt$EmojiPickerDialog</ID>
<ID>ModifierMissing:IndoorAirQuality.kt$IndoorAirQuality</ID>
@ -33,6 +36,7 @@
<ID>ModifierMissing:SettingsItem.kt$SettingsItem</ID>
<ID>ModifierMissing:SettingsItem.kt$SettingsItemDetail</ID>
<ID>ModifierMissing:SettingsItem.kt$SettingsItemSwitch</ID>
<ID>ModifierMissing:SharedContactDialog.kt$SharedContactDialog</ID>
<ID>ModifierMissing:SimpleAlertDialog.kt$SimpleAlertDialog</ID>
<ID>ModifierMissing:SlidingSelector.kt$OptionLabel</ID>
<ID>ModifierNotUsedAtRoot:TextDividerPreference.kt$modifier = modifier.fillMaxWidth().padding(all = 16.dp)</ID>
@ -44,6 +48,7 @@
<ID>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)) } }</ID>
<ID>MultipleEmitters:PreferenceCategory.kt$PreferenceCategory</ID>
<ID>ParameterNaming:BitwisePreference.kt$onItemSelected</ID>
<ID>ParameterNaming:ContactSharing.kt$onSharedContactRequested</ID>
<ID>ParameterNaming:DropDownPreference.kt$onItemSelected</ID>
<ID>ParameterNaming:EditIPv4Preference.kt$onValueChanged</ID>
<ID>ParameterNaming:EditListPreference.kt$onValuesChanged</ID>

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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)

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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. */

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.sharing
package org.meshtastic.core.ui.share
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope

View file

Before

Width:  |  Height:  |  Size: 690 B

After

Width:  |  Height:  |  Size: 690 B

Before After
Before After

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}

View file

@ -0,0 +1,32 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ComposableParamOrder:Message.kt$MessageScreen</ID>
<ID>ComposableParamOrder:Message.kt$QuickChatRow</ID>
<ID>ComposableParamOrder:MessageActions.kt$MessageActions</ID>
<ID>ComposableParamOrder:MessageActions.kt$MessageStatusButton</ID>
<ID>ComposableParamOrder:MessageItem.kt$MessageItem</ID>
<ID>ComposableParamOrder:MessageList.kt$DeliveryInfo</ID>
<ID>ComposableParamOrder:MessageList.kt$MessageList</ID>
<ID>ComposableParamOrder:QuickChat.kt$OutlinedTextFieldWithCounter</ID>
<ID>LambdaParameterEventTrailing:Message.kt$onClick</ID>
<ID>LambdaParameterEventTrailing:Message.kt$onSendMessage</ID>
<ID>LambdaParameterEventTrailing:MessageList.kt$onReply</ID>
<ID>LambdaParameterEventTrailing:QuickChat.kt$onNavigateUp</ID>
<ID>LambdaParameterInRestartableEffect:MessageList.kt$onUnreadChanged</ID>
<ID>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, )</ID>
<ID>ModifierMissing:Message.kt$MessageScreen</ID>
<ID>ModifierMissing:MessageActions.kt$MessageStatusButton</ID>
<ID>ModifierMissing:MessageActions.kt$ReactionButton</ID>
<ID>ModifierMissing:MessageActions.kt$ReplyButton</ID>
<ID>ModifierMissing:Reaction.kt$ReactionDialog</ID>
<ID>ModifierNotUsedAtRoot:QuickChat.kt$modifier = modifier.fillMaxSize().padding(innerPadding)</ID>
<ID>MutableStateParam:MessageList.kt$selectedIds</ID>
<ID>ParameterNaming:MessageList.kt$onUnreadChanged</ID>
<ID>PreviewPublic:Reaction.kt$ReactionItemPreview</ID>
<ID>PreviewPublic:Reaction.kt$ReactionRowPreview</ID>
<ID>TooManyFunctions:MessageViewModel.kt$MessageViewModel : ViewModel</ID>
<ID>Wrapping:Message.kt${ event -&gt; when (event) { is MessageScreenEvent.SendMessage -&gt; { viewModel.sendMessage(event.text, contactKey, event.replyingToPacketId) if (event.replyingToPacketId != null) replyingToPacketId = null messageInputState.clearText() } is MessageScreenEvent.SendReaction -&gt; viewModel.sendReaction(event.emoji, event.messageId, contactKey) is MessageScreenEvent.DeleteMessages -&gt; { viewModel.deleteMessages(event.ids) selectedMessageIds.value = emptySet() showDeleteDialog = false } is MessageScreenEvent.ClearUnreadCount -&gt; viewModel.clearUnreadCount(contactKey, event.lastReadMessageId) is MessageScreenEvent.NodeDetails -&gt; navigateToNodeDetails(event.node.num) is MessageScreenEvent.SetTitle -&gt; viewModel.setTitle(event.title) is MessageScreenEvent.NavigateToMessages -&gt; navigateToMessages(event.contactKey) is MessageScreenEvent.NavigateToNodeDetails -&gt; navigateToNodeDetails(event.nodeNum) MessageScreenEvent.NavigateBack -&gt; onNavigateBack() is MessageScreenEvent.CopyToClipboard -&gt; { clipboardManager.nativeClipboard.setPrimaryClip(ClipData.newPlainText(event.text, event.text)) selectedMessageIds.value = emptySet() } } }</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -15,13 +15,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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

View file

@ -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(),

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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(

View file

@ -15,14 +15,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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

View file

@ -15,12 +15,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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()

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<QuickChatAction>) {
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) }
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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?,

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.message.components
package org.meshtastic.feature.messaging.component
import androidx.compose.foundation.background
import androidx.compose.foundation.border

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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<Reaction> = emptyList(),
onSendReaction: (String) -> Unit = {},
@ -104,10 +104,10 @@ fun ReactionRow(
}
}
fun reduceEmojis(emojis: List<String>): Map<String, Int> = emojis.groupingBy { it }.eachCount()
private fun reduceEmojis(emojis: List<String>): Map<String, Int> = emojis.groupingBy { it }.eachCount()
@Composable
fun ReactionDialog(reactions: List<Reaction>, onDismiss: () -> Unit = {}) =
internal fun ReactionDialog(reactions: List<Reaction>, onDismiss: () -> Unit = {}) =
BottomSheetDialog(onDismiss = onDismiss, modifier = Modifier.fillMaxHeight(fraction = .3f)) {
val groupedEmojis = reactions.groupBy { it.emoji }
var selectedEmoji by remember { mutableStateOf<String?>(null) }
@ -145,7 +145,7 @@ fun ReactionDialog(reactions: List<Reaction>, 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 =

View file

@ -7,7 +7,6 @@
<ID>ComposableParamOrder:LinkedCoordinates.kt$LinkedCoordinates</ID>
<ID>ComposableParamOrder:NodeFilterTextField.kt$NodeFilterTextField</ID>
<ID>ComposableParamOrder:NodeItem.kt$NodeItem</ID>
<ID>ComposableParamOrder:NodeKeyStatusIcon.kt$NodeKeyStatusIcon</ID>
<ID>ComposableParamOrder:SatelliteCountInfo.kt$SatelliteCountInfo</ID>
<ID>ComposableParamOrder:TracerouteButton.kt$TracerouteButton</ID>
<ID>ModifierMissing:NodeStatusIcons.kt$NodeStatusIcons</ID>

View file

@ -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

View file

@ -32,6 +32,7 @@ include(
":core:strings",
":core:ui",
":feature:intro",
":feature:messaging",
":feature:map",
":feature:node",
":feature:settings",