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