feat(auto): append outgoing reply to MessagingStyle for brief confirmation

Before cancelling a conversation notification in response to an inline
reply, post one final update that appends the outgoing text to the
MessagingStyle history, attributed to the local user. This gives
assistants such as Android Auto a tick to observe the sent message in
the notification's message history and surface a 'reply sent' style
confirmation before markConversationRead cancels the notification.

Extract the 'me' Person construction into buildMePerson() and share it
between showGroupSummary and createConversationNotification. The
conversation builder now optionally takes an extraOutgoingMessage which
is appended to the MessagingStyle (actions and when-timestamp continue
to be anchored on the last incoming message).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich
2026-04-17 09:44:08 -05:00
parent 6d70d154e6
commit eb3a27a3d3
6 changed files with 65 additions and 16 deletions

View File

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

View File

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

View File

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

View File

@@ -65,6 +65,7 @@ class ReplyReceiver :
scope.launch {
try {
sendMessage(message, contactKey)
meshServiceNotifications.appendOutgoingMessage(contactKey, message)
meshServiceNotifications.markConversationRead(contactKey)
} finally {
pendingResult.finish()

View File

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

View File

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