diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d486b3dd4..b1fdde668 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -221,6 +221,8 @@
+
+
.
+ */
+package com.geeksville.mesh.service
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
+import org.meshtastic.core.data.repository.PacketRepository
+import org.meshtastic.core.service.MeshServiceNotifications
+import javax.inject.Inject
+
+/** A [BroadcastReceiver] that handles "Mark as read" actions from notifications. */
+@AndroidEntryPoint
+class MarkAsReadReceiver : BroadcastReceiver() {
+ @Inject lateinit var packetRepository: PacketRepository
+
+ @Inject lateinit var meshServiceNotifications: MeshServiceNotifications
+
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+
+ companion object {
+ const val MARK_AS_READ_ACTION = "com.geeksville.mesh.MARK_AS_READ_ACTION"
+ const val CONTACT_KEY = "contactKey"
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action == MARK_AS_READ_ACTION) {
+ val contactKey = intent.getStringExtra(CONTACT_KEY) ?: return
+ val pendingResult = goAsync()
+ scope.launch {
+ try {
+ packetRepository.clearUnreadCount(contactKey, System.currentTimeMillis())
+ meshServiceNotifications.cancelMessageNotification(contactKey)
+ } finally {
+ pendingResult.finish()
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt
index 5cc46bc67..8366c2977 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt
@@ -521,11 +521,12 @@ constructor(
}
private fun rememberReaction(packet: MeshPacket) = scope.handledLaunch {
+ val emoji = packet.decoded.payload.toByteArray().decodeToString()
val reaction =
ReactionEntity(
replyId = packet.decoded.replyId,
userId = dataMapper.toNodeID(packet.from),
- emoji = packet.decoded.payload.toByteArray().decodeToString(),
+ emoji = emoji,
timestamp = System.currentTimeMillis(),
snr = packet.rxSnr,
rssi = packet.rxRssi,
@@ -537,6 +538,31 @@ constructor(
},
)
packetRepository.get().insertReaction(reaction)
+
+ // Find the original packet to get the contactKey
+ packetRepository.get().getPacketByPacketId(packet.decoded.replyId)?.let { original ->
+ val contactKey = original.packet.contact_key
+ val isMuted = packetRepository.get().getContactSettings(contactKey).isMuted
+ if (!isMuted) {
+ val channelName =
+ if (original.packet.data.to == DataPacket.ID_BROADCAST) {
+ radioConfigRepository.channelSetFlow
+ .first()
+ .settingsList
+ .getOrNull(original.packet.data.channel)
+ ?.name
+ } else {
+ null
+ }
+ serviceNotifications.updateReactionNotification(
+ contactKey,
+ getSenderName(dataMapper.toDataPacket(packet)!!),
+ emoji,
+ original.packet.data.to == DataPacket.ID_BROADCAST,
+ channelName,
+ )
+ }
+ }
}
private fun currentTransport(address: String? = meshPrefs.deviceAddress): String = when (address?.firstOrNull()) {
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt
index 223d6505e..5721e1db9 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt
@@ -24,21 +24,33 @@ import android.app.TaskStackBuilder
import android.content.ContentResolver.SCHEME_ANDROID_RESOURCE
import android.content.Context
import android.content.Intent
+import android.graphics.Canvas
import android.graphics.Color
+import android.graphics.Paint
import android.media.AudioAttributes
import android.media.RingtoneManager
import androidx.core.app.NotificationCompat
import androidx.core.app.Person
import androidx.core.app.RemoteInput
import androidx.core.content.getSystemService
+import androidx.core.graphics.createBitmap
+import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri
import com.geeksville.mesh.MainActivity
import com.geeksville.mesh.R.raw
+import com.geeksville.mesh.service.MarkAsReadReceiver.Companion.MARK_AS_READ_ACTION
+import com.geeksville.mesh.service.ReactionReceiver.Companion.REACT_ACTION
import com.geeksville.mesh.service.ReplyReceiver.Companion.KEY_TEXT_REPLY
import com.meshtastic.core.strings.getString
+import dagger.Lazy
import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.flow.first
import org.jetbrains.compose.resources.StringResource
+import org.meshtastic.core.data.repository.NodeRepository
+import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.database.entity.NodeEntity
+import org.meshtastic.core.database.model.Message
+import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.util.formatUptime
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.service.MeshServiceNotifications
@@ -47,7 +59,9 @@ import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.client_notification
import org.meshtastic.core.strings.low_battery_message
import org.meshtastic.core.strings.low_battery_title
+import org.meshtastic.core.strings.mark_as_read
import org.meshtastic.core.strings.meshtastic_alerts_notifications
+import org.meshtastic.core.strings.meshtastic_app_name
import org.meshtastic.core.strings.meshtastic_broadcast_notifications
import org.meshtastic.core.strings.meshtastic_low_battery_notifications
import org.meshtastic.core.strings.meshtastic_low_battery_temporary_remote_notifications
@@ -58,6 +72,7 @@ import org.meshtastic.core.strings.meshtastic_waypoints_notifications
import org.meshtastic.core.strings.new_node_seen
import org.meshtastic.core.strings.no_local_stats
import org.meshtastic.core.strings.reply
+import org.meshtastic.core.strings.you
import org.meshtastic.proto.MeshProtos
import org.meshtastic.proto.TelemetryProtos
import org.meshtastic.proto.TelemetryProtos.LocalStats
@@ -69,9 +84,14 @@ import javax.inject.Inject
* This class centralizes notification logic, including channel creation, builder configuration, and displaying
* notifications for various events like new messages, alerts, and service status changes.
*/
-@Suppress("TooManyFunctions")
-class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext private val context: Context) :
- MeshServiceNotifications {
+@Suppress("TooManyFunctions", "LongParameterList")
+class MeshServiceNotificationsImpl
+@Inject
+constructor(
+ @ApplicationContext private val context: Context,
+ private val packetRepository: Lazy,
+ private val nodeRepository: Lazy,
+) : MeshServiceNotifications {
private val notificationManager = context.getSystemService()!!
@@ -79,6 +99,13 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva
private const val FIFTEEN_MINUTES_IN_MILLIS = 15L * 60 * 1000
const val MAX_BATTERY_LEVEL = 100
private val NOTIFICATION_LIGHT_COLOR = Color.BLUE
+ private const val MAX_HISTORY_MESSAGES = 10
+ private const val MIN_CONTEXT_MESSAGES = 3
+ private const val SNIPPET_LENGTH = 30
+ private const val GROUP_KEY_MESSAGES = "com.geeksville.mesh.GROUP_MESSAGES"
+ private const val SUMMARY_ID = 1
+ private const val PERSON_ICON_SIZE = 128
+ private const val PERSON_ICON_TEXT_SIZE_RATIO = 0.5f
}
/**
@@ -280,21 +307,108 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva
return notification
}
- override fun updateMessageNotification(
+ override suspend fun updateMessageNotification(
contactKey: String,
name: String,
message: String,
isBroadcast: Boolean,
channelName: String?,
) {
- val notification = createMessageNotification(contactKey, name, message, isBroadcast, channelName)
- // Use a consistent, unique ID for each message conversation.
+ showConversationNotification(contactKey, isBroadcast, channelName)
+ }
+
+ override suspend fun updateReactionNotification(
+ contactKey: String,
+ name: String,
+ emoji: String,
+ isBroadcast: Boolean,
+ channelName: String?,
+ ) {
+ showConversationNotification(contactKey, isBroadcast, channelName)
+ }
+
+ override suspend fun updateWaypointNotification(
+ contactKey: String,
+ name: String,
+ message: String,
+ waypointId: Int,
+ ) {
+ val notification = createWaypointNotification(name, message, waypointId)
notificationManager.notify(contactKey.hashCode(), notification)
}
- override fun updateWaypointNotification(contactKey: String, name: String, message: String, waypointId: Int) {
- val notification = createWaypointNotification(name, message, waypointId)
+ private suspend fun showConversationNotification(contactKey: String, isBroadcast: Boolean, channelName: String?) {
+ val ourNode = nodeRepository.get().ourNodeInfo.value
+ val history =
+ packetRepository
+ .get()
+ .getMessagesFrom(contactKey) { nodeId ->
+ if (nodeId == DataPacket.ID_LOCAL) {
+ ourNode ?: nodeRepository.get().getNode(nodeId)
+ } else {
+ nodeRepository.get().getNode(nodeId ?: "")
+ }
+ }
+ .first()
+
+ val unread = history.filter { !it.read }
+ val displayHistory =
+ if (unread.size < MIN_CONTEXT_MESSAGES) {
+ history.take(MIN_CONTEXT_MESSAGES).reversed()
+ } else {
+ unread.take(MAX_HISTORY_MESSAGES).reversed()
+ }
+
+ if (displayHistory.isEmpty()) return
+
+ val notification = createConversationNotification(contactKey, isBroadcast, channelName, displayHistory)
notificationManager.notify(contactKey.hashCode(), notification)
+ showGroupSummary()
+ }
+
+ private fun showGroupSummary() {
+ val activeNotifications =
+ notificationManager.activeNotifications.filter {
+ it.id != SUMMARY_ID && it.notification.group == GROUP_KEY_MESSAGES
+ }
+
+ val ourNode = nodeRepository.get().ourNodeInfo.value
+ val meName = ourNode?.user?.longName ?: getString(Res.string.you)
+ val me =
+ Person.Builder()
+ .setName(meName)
+ .setKey(ourNode?.user?.id ?: DataPacket.ID_LOCAL)
+ .apply { ourNode?.let { setIcon(createPersonIcon(meName, it.colors.second, it.colors.first)) } }
+ .build()
+
+ val messagingStyle =
+ NotificationCompat.MessagingStyle(me)
+ .setGroupConversation(true)
+ .setConversationTitle(getString(Res.string.meshtastic_app_name))
+
+ activeNotifications.forEach { sbn ->
+ val senderTitle = sbn.notification.extras.getCharSequence(Notification.EXTRA_TITLE)
+ val messageText = sbn.notification.extras.getCharSequence(Notification.EXTRA_TEXT)
+ val postTime = sbn.postTime
+
+ if (senderTitle != null && messageText != null) {
+ // For the summary, we're creating a generic Person for the sender from the active notification's title.
+ // We don't have the original Person object or its colors/ID, so we're just using the name.
+ val senderPerson = Person.Builder().setName(senderTitle).build()
+ messagingStyle.addMessage(messageText, postTime, senderPerson)
+ }
+ }
+
+ val summaryNotification =
+ commonBuilder(NotificationType.DirectMessage)
+ .setSmallIcon(com.geeksville.mesh.R.drawable.app_icon)
+ .setStyle(messagingStyle)
+ .setGroup(GROUP_KEY_MESSAGES)
+ .setGroupSummary(true)
+ .setAutoCancel(true)
+ .build()
+
+ notificationManager.notify(SUMMARY_ID, summaryNotification)
}
override fun showAlertNotification(contactKey: String, name: String, alert: String) {
@@ -354,35 +468,88 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva
return builder.build()
}
- private fun createMessageNotification(
+ @Suppress("LongMethod")
+ private fun createConversationNotification(
contactKey: String,
- name: String,
- message: String,
isBroadcast: Boolean,
- channelName: String? = null,
+ channelName: String?,
+ history: List,
): Notification {
val type = if (isBroadcast) NotificationType.BroadcastMessage else NotificationType.DirectMessage
val builder = commonBuilder(type, createOpenMessageIntent(contactKey))
- val person = Person.Builder().setName(name).build()
+ val ourNode = nodeRepository.get().ourNodeInfo.value
+ val meName = ourNode?.user?.longName ?: getString(Res.string.you)
+ val me =
+ Person.Builder()
+ .setName(meName)
+ .setKey(ourNode?.user?.id ?: DataPacket.ID_LOCAL)
+ .apply { ourNode?.let { setIcon(createPersonIcon(meName, it.colors.second, it.colors.first)) } }
+ .build()
+
val style =
- NotificationCompat.MessagingStyle(person)
+ NotificationCompat.MessagingStyle(me)
.setGroupConversation(channelName != null)
.setConversationTitle(channelName)
- .addMessage(message, System.currentTimeMillis(), person)
+
+ history.forEach { msg ->
+ // Use the node attached to the message directly to ensure correct identification
+ val person =
+ Person.Builder()
+ .setName(msg.node.user.longName)
+ .setKey(msg.node.user.id)
+ .setIcon(createPersonIcon(msg.node.user.shortName, msg.node.colors.second, msg.node.colors.first))
+ .build()
+
+ val text =
+ msg.originalMessage?.let { original ->
+ "↩️ \"${original.node.user.shortName}: ${original.text.take(SNIPPET_LENGTH)}...\": ${msg.text}"
+ } ?: msg.text
+
+ style.addMessage(text, msg.receivedTime, person)
+
+ // Add reactions as separate "messages" in history if they exist
+ msg.emojis.forEach { reaction ->
+ val reactorNode = nodeRepository.get().getNode(reaction.user.id)
+ val reactor =
+ Person.Builder()
+ .setName(reaction.user.longName)
+ .setKey(reaction.user.id)
+ .setIcon(
+ createPersonIcon(
+ reaction.user.shortName,
+ reactorNode.colors.second,
+ reactorNode.colors.first,
+ ),
+ )
+ .build()
+ style.addMessage(
+ "${reaction.emoji} to \"${msg.text.take(SNIPPET_LENGTH)}...\"",
+ reaction.timestamp,
+ reactor,
+ )
+ }
+ }
+ val lastMessage = history.last()
builder
.setCategory(Notification.CATEGORY_MESSAGE)
.setAutoCancel(true)
.setStyle(style)
+ .setGroup(GROUP_KEY_MESSAGES)
.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
- .setWhen(System.currentTimeMillis())
+ .setWhen(lastMessage.receivedTime)
.setShowWhen(true)
-
- // Only add reply action for direct messages, not broadcasts
- if (!isBroadcast) {
- builder.addAction(createReplyAction(contactKey))
- }
+ .addAction(createReplyAction(contactKey))
+ .addAction(createMarkAsReadAction(contactKey))
+ .addAction(
+ createReactionAction(
+ contactKey = contactKey,
+ packetId = lastMessage.packetId,
+ toId = lastMessage.node.user.id,
+ channelIndex = lastMessage.node.channel,
+ ),
+ )
return builder.build()
}
@@ -395,6 +562,7 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva
.setCategory(Notification.CATEGORY_MESSAGE)
.setAutoCancel(true)
.setStyle(style)
+ .setGroup(GROUP_KEY_MESSAGES)
.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
.setWhen(System.currentTimeMillis())
.setShowWhen(true)
@@ -517,6 +685,51 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva
.build()
}
+ private fun createMarkAsReadAction(contactKey: String): NotificationCompat.Action {
+ val label = getString(Res.string.mark_as_read)
+ val intent =
+ Intent(context, MarkAsReadReceiver::class.java).apply {
+ action = MARK_AS_READ_ACTION
+ putExtra(MarkAsReadReceiver.CONTACT_KEY, contactKey)
+ }
+ val pendingIntent =
+ PendingIntent.getBroadcast(
+ context,
+ contactKey.hashCode(),
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
+ )
+
+ return NotificationCompat.Action.Builder(android.R.drawable.ic_menu_view, label, pendingIntent).build()
+ }
+
+ private fun createReactionAction(
+ contactKey: String,
+ packetId: Int,
+ toId: String,
+ channelIndex: Int,
+ ): NotificationCompat.Action {
+ val label = "👍"
+ val intent =
+ Intent(context, ReactionReceiver::class.java).apply {
+ action = REACT_ACTION
+ putExtra(ReactionReceiver.EXTRA_CONTACT_KEY, contactKey)
+ putExtra(ReactionReceiver.EXTRA_PACKET_ID, packetId)
+ putExtra(ReactionReceiver.EXTRA_TO_ID, toId)
+ putExtra(ReactionReceiver.EXTRA_CHANNEL_INDEX, channelIndex)
+ putExtra(ReactionReceiver.EXTRA_EMOJI, "👍")
+ }
+ val pendingIntent =
+ PendingIntent.getBroadcast(
+ context,
+ packetId,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
+ )
+
+ return NotificationCompat.Action.Builder(android.R.drawable.ic_menu_add, label, pendingIntent).build()
+ }
+
private fun commonBuilder(
type: NotificationType,
contentIntent: PendingIntent? = null,
@@ -529,6 +742,33 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentIntent(contentIntent ?: openAppIntent)
}
+
+ private fun createPersonIcon(name: String, backgroundColor: Int, foregroundColor: Int): IconCompat {
+ val bitmap = createBitmap(PERSON_ICON_SIZE, PERSON_ICON_SIZE)
+ val canvas = Canvas(bitmap)
+ val paint = Paint(Paint.ANTI_ALIAS_FLAG)
+
+ // Draw background circle
+ paint.color = backgroundColor
+ canvas.drawCircle(PERSON_ICON_SIZE / 2f, PERSON_ICON_SIZE / 2f, PERSON_ICON_SIZE / 2f, paint)
+
+ // Draw initials
+ paint.color = foregroundColor
+ paint.textSize = PERSON_ICON_SIZE * PERSON_ICON_TEXT_SIZE_RATIO
+ paint.textAlign = Paint.Align.CENTER
+ val initial =
+ if (name.isNotEmpty()) {
+ val codePoint = name.codePointAt(0)
+ String(Character.toChars(codePoint)).uppercase()
+ } else {
+ "?"
+ }
+ val xPos = canvas.width / 2f
+ val yPos = (canvas.height / 2f - (paint.descent() + paint.ascent()) / 2f)
+ canvas.drawText(initial, xPos, yPos, paint)
+
+ return IconCompat.createWithBitmap(bitmap)
+ }
// endregion
}
diff --git a/app/src/main/java/com/geeksville/mesh/service/ReactionReceiver.kt b/app/src/main/java/com/geeksville/mesh/service/ReactionReceiver.kt
new file mode 100644
index 000000000..6482659cc
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/service/ReactionReceiver.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2025-2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * 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 com.geeksville.mesh.service
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
+import org.meshtastic.core.model.DataPacket
+import org.meshtastic.core.service.MeshServiceNotifications
+import org.meshtastic.proto.Portnums
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class ReactionReceiver : BroadcastReceiver() {
+ @Inject lateinit var commandSender: MeshCommandSender
+
+ @Inject lateinit var meshServiceNotifications: MeshServiceNotifications
+
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+
+ companion object {
+ const val REACT_ACTION = "com.geeksville.mesh.REACT_ACTION"
+ const val EXTRA_PACKET_ID = "packetId"
+ const val EXTRA_EMOJI = "emoji"
+ const val EXTRA_CONTACT_KEY = "contactKey"
+ const val EXTRA_TO_ID = "toId"
+ const val EXTRA_CHANNEL_INDEX = "channelIndex"
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action != REACT_ACTION) return
+
+ val pendingResult = goAsync()
+ scope.launch {
+ try {
+ val packetId = intent.getIntExtra(EXTRA_PACKET_ID, 0)
+ val emoji = intent.getStringExtra(EXTRA_EMOJI)
+ val toId = intent.getStringExtra(EXTRA_TO_ID)
+ val channelIndex = intent.getIntExtra(EXTRA_CHANNEL_INDEX, 0)
+ val contactKey = intent.getStringExtra(EXTRA_CONTACT_KEY)
+
+ @Suppress("ComplexCondition")
+ if (packetId == 0 || emoji.isNullOrEmpty() || toId.isNullOrEmpty() || contactKey.isNullOrEmpty()) {
+ return@launch
+ }
+
+ // Reactions are text messages with a replyId and emoji set
+ val reactionPacket =
+ DataPacket(
+ to = toId,
+ channel = channelIndex,
+ bytes = emoji.toByteArray(Charsets.UTF_8),
+ dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
+ replyId = packetId,
+ emoji = emoji.codePointAt(0),
+ )
+ commandSender.sendData(reactionPacket)
+
+ // Dismiss the notification after reacting
+ meshServiceNotifications.cancelMessageNotification(contactKey)
+ } finally {
+ pendingResult.finish()
+ }
+ }
+ }
+}
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 d3032b4bf..a80839176 100644
--- a/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package com.geeksville.mesh.service
import android.content.BroadcastReceiver
@@ -59,7 +58,7 @@ class ReplyReceiver : BroadcastReceiver() {
val contactKey = intent.getStringExtra(CONTACT_KEY) ?: ""
val message = remoteInput.getCharSequence(KEY_TEXT_REPLY)?.toString() ?: ""
sendMessage(message, contactKey)
- MeshServiceNotificationsImpl(context).cancelMessageNotification(contactKey)
+ meshServiceNotifications.cancelMessageNotification(contactKey)
}
}
}
diff --git a/app/src/test/java/com/geeksville/mesh/service/Fakes.kt b/app/src/test/java/com/geeksville/mesh/service/Fakes.kt
index 8e1f784b3..e243e0d27 100644
--- a/app/src/test/java/com/geeksville/mesh/service/Fakes.kt
+++ b/app/src/test/java/com/geeksville/mesh/service/Fakes.kt
@@ -81,7 +81,7 @@ class FakeMeshServiceNotifications : MeshServiceNotifications {
telemetry: TelemetryProtos.Telemetry?,
): Notification = null as Notification
- override fun updateMessageNotification(
+ override suspend fun updateMessageNotification(
contactKey: String,
name: String,
message: String,
@@ -89,7 +89,20 @@ class FakeMeshServiceNotifications : MeshServiceNotifications {
channelName: String?,
) {}
- override fun updateWaypointNotification(contactKey: String, name: String, message: String, waypointId: Int) {}
+ override suspend fun updateWaypointNotification(
+ contactKey: String,
+ name: String,
+ message: String,
+ waypointId: Int,
+ ) {}
+
+ override suspend fun updateReactionNotification(
+ contactKey: String,
+ name: String,
+ emoji: String,
+ isBroadcast: Boolean,
+ channelName: String?,
+ ) {}
override fun showAlertNotification(contactKey: String, name: String, alert: String) {}
diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepository.kt
index ff65e44ac..d69914f11 100644
--- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepository.kt
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/PacketRepository.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package org.meshtastic.core.data.repository
import androidx.paging.Pager
@@ -46,7 +45,7 @@ constructor(
private val dispatchers: CoroutineDispatchers,
) {
fun getWaypoints(): Flow> =
- dbManager.currentDb.flatMapLatest { db -> db.packetDao().getAllPackets(PortNum.WAYPOINT_APP_VALUE) }
+ dbManager.currentDb.flatMapLatest { db -> db.packetDao().getAllWaypointsFlow() }
fun getContacts(): Flow