diff --git a/app/src/test/kotlin/org/meshtastic/app/service/Fakes.kt b/app/src/test/kotlin/org/meshtastic/app/service/Fakes.kt index 46d5ed27c..05562bb9d 100644 --- a/app/src/test/kotlin/org/meshtastic/app/service/Fakes.kt +++ b/app/src/test/kotlin/org/meshtastic/app/service/Fakes.kt @@ -76,6 +76,8 @@ class FakeMeshServiceNotifications : MeshServiceNotifications { override suspend fun markConversationRead(contactKey: String) {} + override suspend fun appendOutgoingMessage(contactKey: String, text: String) {} + override fun cancelLowBatteryNotification(node: Node) {} override fun clearClientNotification(notification: ClientNotification) {} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshServiceNotifications.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshServiceNotifications.kt index 1216f29a3..b73b44ab0 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshServiceNotifications.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshServiceNotifications.kt @@ -74,6 +74,13 @@ interface MeshServiceNotifications { */ suspend fun markConversationRead(contactKey: String) + /** + * Appends an outgoing [text] message attributed to the local user to the currently posted conversation notification + * for [contactKey]. Used so that assistants such as Android Auto can briefly observe the reply in the + * MessagingStyle history before the notification is cancelled. No-op when there is nothing to update. + */ + suspend fun appendOutgoingMessage(contactKey: String, text: String) + fun cancelLowBatteryNotification(node: Node) fun clearClientNotification(notification: ClientNotification) diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceNotificationsImpl.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceNotificationsImpl.kt index 97ff46766..bc982fa25 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceNotificationsImpl.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceNotificationsImpl.kt @@ -440,20 +440,23 @@ class MeshServiceNotificationsImpl( showGroupSummary() } + private fun buildMePerson(): Person { + val ourNode = nodeRepository.value.ourNodeInfo.value + val meName = ourNode?.user?.long_name ?: getString(Res.string.you) + return Person.Builder() + .setName(meName) + .setKey(ourNode?.user?.id ?: DataPacket.ID_LOCAL) + .apply { ourNode?.let { setIcon(createPersonIcon(meName, it.colors.second, it.colors.first)) } } + .build() + } + private fun showGroupSummary() { val activeNotifications = notificationManager.activeNotifications.filter { it.id != SUMMARY_ID && it.notification.group == GROUP_KEY_MESSAGES } - val ourNode = nodeRepository.value.ourNodeInfo.value - val meName = ourNode?.user?.long_name ?: 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 me = buildMePerson() val messagingStyle = NotificationCompat.MessagingStyle(me) @@ -520,6 +523,39 @@ class MeshServiceNotificationsImpl( cancelMessageNotification(contactKey) } + override suspend fun appendOutgoingMessage(contactKey: String, text: String) { + if (text.isEmpty()) return + val ourNode = nodeRepository.value.ourNodeInfo.value + val history = + packetRepository.value + .getMessagesFrom(contactKey, includeFiltered = false) { nodeId -> + if (nodeId == DataPacket.ID_LOCAL) { + ourNode ?: nodeRepository.value.getNode(nodeId) + } else { + nodeRepository.value.getNode(nodeId ?: "") + } + } + .first() + + val unread = history.filter { !it.read } + if (unread.isEmpty()) return + val displayHistory = unread.take(MAX_HISTORY_MESSAGES).reversed() + + val dest = if (contactKey.isNotEmpty()) contactKey.substring(1) else contactKey + val isBroadcast = dest == DataPacket.ID_BROADCAST + + val notification = + createConversationNotification( + contactKey = contactKey, + isBroadcast = isBroadcast, + channelName = null, + history = displayHistory, + isSilent = true, + extraOutgoingMessage = text, + ) + notificationManager.notify(contactKey.hashCode(), notification) + } + override fun cancelLowBatteryNotification(node: Node) = notificationManager.cancel(node.num) override fun clearClientNotification(notification: ClientNotification) = @@ -561,6 +597,7 @@ class MeshServiceNotificationsImpl( channelName: String?, history: List, isSilent: Boolean = false, + extraOutgoingMessage: String? = null, ): Notification { val type = if (isBroadcast) NotificationType.BroadcastMessage else NotificationType.DirectMessage val builder = commonBuilder(type, createOpenMessageIntent(contactKey)) @@ -569,14 +606,7 @@ class MeshServiceNotificationsImpl( builder.setSilent(true) } - val ourNode = nodeRepository.value.ourNodeInfo.value - val meName = ourNode?.user?.long_name ?: 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 me = buildMePerson() val style = NotificationCompat.MessagingStyle(me) @@ -621,6 +651,9 @@ class MeshServiceNotificationsImpl( ) } } + if (!extraOutgoingMessage.isNullOrEmpty()) { + style.addMessage(extraOutgoingMessage, nowMillis, me) + } val lastMessage = history.last() ensureShortcutForNotification(contactKey, isBroadcast, channelName, lastMessage) diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ReplyReceiver.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ReplyReceiver.kt index 8ab6590b7..b00c3c255 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ReplyReceiver.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ReplyReceiver.kt @@ -65,6 +65,7 @@ class ReplyReceiver : scope.launch { try { sendMessage(message, contactKey) + meshServiceNotifications.appendOutgoingMessage(contactKey, message) meshServiceNotifications.markConversationRead(contactKey) } finally { pendingResult.finish() diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMeshServiceNotifications.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMeshServiceNotifications.kt index 923d2e8aa..e1c1c7659 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMeshServiceNotifications.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMeshServiceNotifications.kt @@ -69,6 +69,8 @@ class FakeMeshServiceNotifications : MeshServiceNotifications { override suspend fun markConversationRead(contactKey: String) {} + override suspend fun appendOutgoingMessage(contactKey: String, text: String) {} + override fun cancelLowBatteryNotification(node: Node) {} override fun clearClientNotification(notification: ClientNotification) {} diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshServiceNotifications.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshServiceNotifications.kt index fd30a5be0..f2ad6ca3e 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshServiceNotifications.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshServiceNotifications.kt @@ -158,6 +158,10 @@ class DesktopMeshServiceNotifications(private val notificationManager: Notificat notificationManager.cancel(contactKey.hashCode()) } + override suspend fun appendOutgoingMessage(contactKey: String, text: String) { + // No-op: desktop tray notifications don't carry MessagingStyle history to augment. + } + override fun cancelLowBatteryNotification(node: Node) { notificationManager.cancel(node.num) }