feat: auto-dismiss reply notifications when read in chat (#1195)
This commit is contained in:
30 files changed
+4070
-15
No files matched your search
@@ -13,7 +13,6 @@ import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.RemoteInput
|
||||
import com.m57.hermescontrol.MainActivity
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.local.AuthManager
|
||||
import com.m57.hermescontrol.data.remote.NetworkMonitor
|
||||
import com.m57.hermescontrol.data.session.ActiveSessionHolder
|
||||
import com.m57.hermescontrol.data.ws.HermesWsClient
|
||||
@@ -69,6 +68,12 @@ class ChatNotificationService : Service() {
|
||||
private val isAppInForeground = AtomicBoolean(false)
|
||||
internal val lifecycle = ForegroundServiceLifecycle()
|
||||
|
||||
/**
|
||||
* Turn-row correlation used when a reply completes in the background.
|
||||
* Injectable so the resolution path can be exercised without REST.
|
||||
*/
|
||||
internal var turnRowResolver: TurnRowResolver = defaultTurnRowResolver
|
||||
|
||||
@Volatile
|
||||
internal var activeServiceInstance: ChatNotificationService? = null
|
||||
|
||||
@@ -111,10 +116,23 @@ class ChatNotificationService : Service() {
|
||||
.take(100)
|
||||
.replace("\n", " ")
|
||||
.ifBlank { getString(R.string.notif_new_message) }
|
||||
showReplyNotification(
|
||||
preview,
|
||||
val targetSessionId =
|
||||
event.storedSessionId
|
||||
?: ActiveSessionHolder.resolveStoredSessionId(event.sessionId),
|
||||
?: ActiveSessionHolder.resolveStoredSessionId(event.sessionId)
|
||||
showReplyNotification(
|
||||
text = preview,
|
||||
sessionId = targetSessionId,
|
||||
isReplyMessage = true,
|
||||
completionId = event.completionId,
|
||||
// The durable REST row for this turn, when the
|
||||
// boundary armed before the prompt was submitted
|
||||
// still lets us name it unambiguously. Null is a
|
||||
// normal, safe outcome: the notification is then
|
||||
// never auto-dismissed from REST hydration, which
|
||||
// is strictly better than dismissing the wrong
|
||||
// duplicate reply.
|
||||
serverMessageId =
|
||||
coalesceTurnRow(targetSessionId, event.text),
|
||||
)
|
||||
// The wait is over — retire the foreground
|
||||
// service. The reply notification above
|
||||
@@ -167,9 +185,37 @@ class ChatNotificationService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Names the exact REST row this completion produced, using the turn
|
||||
* boundary armed before the prompt was submitted. Any failure to prove that
|
||||
* identity returns null — the notification is still posted, it just will not
|
||||
* be auto-dismissed from REST hydration.
|
||||
*/
|
||||
private suspend fun coalesceTurnRow(
|
||||
sessionId: String?,
|
||||
completionText: String,
|
||||
): Int? {
|
||||
if (sessionId.isNullOrBlank()) return null
|
||||
return try {
|
||||
correlateCompletedTurnRow(
|
||||
scopeId = correlationScopeId(),
|
||||
sessionId = sessionId,
|
||||
completionText = completionText,
|
||||
resolver = turnRowResolver,
|
||||
)
|
||||
} catch (e: kotlinx.coroutines.CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun showReplyNotification(
|
||||
text: String,
|
||||
sessionId: String?,
|
||||
isReplyMessage: Boolean = false,
|
||||
completionId: String? = null,
|
||||
serverMessageId: Int? = null,
|
||||
) {
|
||||
val builder =
|
||||
NotificationCompat
|
||||
@@ -183,6 +229,39 @@ class ChatNotificationService : Service() {
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(buildContentIntent(sessionId))
|
||||
|
||||
var replyGeneration: Long? = null
|
||||
if (isReplyMessage && !sessionId.isNullOrBlank() && !completionId.isNullOrBlank()) {
|
||||
val scopeId = correlationScopeId()
|
||||
val generation =
|
||||
ReplyNotificationTracker.registerPendingReply(
|
||||
scopeId = scopeId,
|
||||
sessionId = sessionId,
|
||||
completionId = completionId,
|
||||
textSnippet = text,
|
||||
serverMessageId = serverMessageId,
|
||||
)
|
||||
replyGeneration = generation
|
||||
builder.addExtras(
|
||||
android.os.Bundle().apply {
|
||||
putString(ReplyNotificationTracker.EXTRA_NOTIF_KIND, ReplyNotificationTracker.KIND_REPLY)
|
||||
putString(ReplyNotificationTracker.EXTRA_SCOPE_ID, scopeId)
|
||||
putString(ReplyNotificationTracker.EXTRA_SESSION_ID, sessionId)
|
||||
putString(ReplyNotificationTracker.EXTRA_COMPLETION_ID, completionId)
|
||||
putString(ReplyNotificationTracker.EXTRA_TEXT_SNIPPET, text)
|
||||
putLong(ReplyNotificationTracker.EXTRA_GENERATION, generation)
|
||||
serverMessageId?.let {
|
||||
putInt(ReplyNotificationTracker.EXTRA_SERVER_MESSAGE_ID, it)
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
builder.addExtras(
|
||||
android.os.Bundle().apply {
|
||||
putString(ReplyNotificationTracker.EXTRA_NOTIF_KIND, ReplyNotificationTracker.KIND_ACTION)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (!sessionId.isNullOrBlank()) {
|
||||
val replyLabel = getString(R.string.notif_reply_placeholder)
|
||||
val remoteInput =
|
||||
@@ -194,6 +273,11 @@ class ChatNotificationService : Service() {
|
||||
val replyIntent =
|
||||
Intent(this, NotificationReplyReceiver::class.java).apply {
|
||||
action = "$packageName.ACTION_NOTIFICATION_REPLY"
|
||||
component =
|
||||
android.content.ComponentName(
|
||||
this@ChatNotificationService,
|
||||
NotificationReplyReceiver::class.java,
|
||||
)
|
||||
setPackage(packageName)
|
||||
putExtra(NotificationReplyReceiver.EXTRA_SESSION_ID, sessionId)
|
||||
}
|
||||
@@ -218,19 +302,32 @@ class ChatNotificationService : Service() {
|
||||
builder.addAction(action)
|
||||
}
|
||||
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
manager.notify(PENDING_NOTIFICATION_ID, builder.build())
|
||||
val notification = builder.build()
|
||||
if (replyGeneration != null) {
|
||||
ReplyNotificationTracker.postReplyNotification(
|
||||
context = this,
|
||||
notification = notification,
|
||||
generation = replyGeneration,
|
||||
)
|
||||
} else {
|
||||
ReplyNotificationTracker.postActionNotification(
|
||||
context = this,
|
||||
notification = notification,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildContentIntent(sessionId: String?): PendingIntent {
|
||||
val intent =
|
||||
Intent(this, MainActivity::class.java).apply {
|
||||
action = MainActivity.ACTION_OPEN_CHAT_FROM_NOTIFICATION
|
||||
component = android.content.ComponentName(this@ChatNotificationService, MainActivity::class.java)
|
||||
setPackage(packageName)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
if (!sessionId.isNullOrBlank()) {
|
||||
putExtra(NotificationReplyReceiver.EXTRA_SESSION_ID, sessionId)
|
||||
}
|
||||
}
|
||||
if (!sessionId.isNullOrBlank()) {
|
||||
intent.putExtra(NotificationReplyReceiver.EXTRA_SESSION_ID, sessionId)
|
||||
}
|
||||
return PendingIntent.getActivity(
|
||||
this,
|
||||
sessionId?.hashCode() ?: 0,
|
||||
|
||||
Reference in new issue
Block a user