feat: add DM inbox relay subscriptions to notification service
The service now maintains two independent subscriptions: - svc:notif: notification events on NIP-65 inbox relays - svc:giftwrap: NIP-59 gift wraps on NIP-17 DM inbox relays This ensures DM relays that aren't also notification inbox relays stay connected when the app backgrounds. Both subscriptions update reactively when relay lists change. Also updates PULL_NOTIFICATION.md with detailed "why" explanations for each resilience layer and documents the DM relay architecture. https://claude.ai/code/session_01LEPfmgGnwjB9a5SDFw5U8t
This commit is contained in:
+122
@@ -38,14 +38,22 @@ import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications.filterNotificationsToPubkey
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications.filterSummaryNotificationsToPubkey
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip59GiftWraps.filterGiftWrapsToPubkey
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
@@ -119,6 +127,9 @@ class NotificationRelayService : Service() {
|
||||
private var connectedRelayCount = 0
|
||||
private var foregroundStarted = false
|
||||
|
||||
private val subIdNotifications = "svc:notif"
|
||||
private val subIdGiftWraps = "svc:giftwrap"
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -187,6 +198,16 @@ class NotificationRelayService : Service() {
|
||||
override fun onDestroy() {
|
||||
Log.d(TAG, "Service destroyed")
|
||||
relayServiceCollectorJob?.cancel()
|
||||
|
||||
// Clean up our subscriptions so the relay pool can disconnect
|
||||
// relays that are no longer needed by anyone
|
||||
try {
|
||||
Amethyst.instance.client.unsubscribe(subIdNotifications)
|
||||
Amethyst.instance.client.unsubscribe(subIdGiftWraps)
|
||||
} catch (e: Exception) {
|
||||
// App might already be torn down
|
||||
}
|
||||
|
||||
scope.cancel()
|
||||
|
||||
if (isEnabled(this)) {
|
||||
@@ -241,6 +262,14 @@ class NotificationRelayService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
// Maintain notification + DM subscriptions on inbox relays.
|
||||
// When the UI backgrounds, its subscriptions drop and relays that are
|
||||
// only needed for those subscriptions disconnect. By opening our own
|
||||
// subscriptions here, we ensure inbox + DM relay connections persist.
|
||||
launch {
|
||||
maintainInboxSubscriptions()
|
||||
}
|
||||
|
||||
// Monitor connected relay count to update the notification
|
||||
launch {
|
||||
Amethyst.instance.client.connectedRelaysFlow().collectLatest { relays ->
|
||||
@@ -254,6 +283,99 @@ class NotificationRelayService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches the logged-in account's notification inbox relays and DM inbox relays,
|
||||
* maintaining lightweight subscriptions on them. This ensures these relay connections
|
||||
* persist even when the UI's subscriptions drop.
|
||||
*
|
||||
* Notification relays: NIP-65 inbox + local relays → subscribe to mentions, zaps, etc.
|
||||
* DM relays: NIP-17 DM relays + NIP-65 inbox + private outbox + local relays → gift wraps
|
||||
*/
|
||||
private suspend fun maintainInboxSubscriptions() {
|
||||
// Wait for account to be available
|
||||
Amethyst.instance.sessionManager.accountContent.collectLatest { state ->
|
||||
val account =
|
||||
(state as? com.vitorpamplona.amethyst.ui.screen.AccountState.LoggedIn)?.account
|
||||
|
||||
if (account == null || !account.isWriteable()) {
|
||||
// No account or read-only — close any existing subscriptions
|
||||
Amethyst.instance.client.unsubscribe(subIdNotifications)
|
||||
Amethyst.instance.client.unsubscribe(subIdGiftWraps)
|
||||
return@collectLatest
|
||||
}
|
||||
|
||||
// Reactively update subscriptions when relay lists change
|
||||
combine(
|
||||
account.notificationRelays.flow,
|
||||
account.dmRelays.flow,
|
||||
) { notifRelays, dmRelays ->
|
||||
Pair(notifRelays, dmRelays)
|
||||
}.collectLatest { (notifRelays, dmRelays) ->
|
||||
updateNotificationSubscription(account, notifRelays)
|
||||
updateGiftWrapSubscription(account, dmRelays)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNotificationSubscription(
|
||||
account: Account,
|
||||
relays: Set<NormalizedRelayUrl>,
|
||||
) {
|
||||
if (relays.isEmpty()) {
|
||||
Amethyst.instance.client.unsubscribe(subIdNotifications)
|
||||
return
|
||||
}
|
||||
|
||||
val pubkey = account.signer.pubKey
|
||||
val since = TimeUtils.oneWeekAgo()
|
||||
val filters = mutableMapOf<NormalizedRelayUrl, List<Filter>>()
|
||||
|
||||
relays.forEach { relay ->
|
||||
val relayFilters =
|
||||
(
|
||||
filterSummaryNotificationsToPubkey(relay, pubkey, since) +
|
||||
filterNotificationsToPubkey(relay, pubkey, since)
|
||||
).map { it.filter }
|
||||
|
||||
if (relayFilters.isNotEmpty()) {
|
||||
filters[relay] = relayFilters
|
||||
}
|
||||
}
|
||||
|
||||
if (filters.isNotEmpty()) {
|
||||
Amethyst.instance.client.subscribe(subIdNotifications, filters)
|
||||
Log.d(TAG) { "Subscribed to notifications on ${filters.size} relays" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateGiftWrapSubscription(
|
||||
account: Account,
|
||||
relays: Set<NormalizedRelayUrl>,
|
||||
) {
|
||||
if (relays.isEmpty()) {
|
||||
Amethyst.instance.client.unsubscribe(subIdGiftWraps)
|
||||
return
|
||||
}
|
||||
|
||||
val pubkey = account.signer.pubKey
|
||||
val since = TimeUtils.oneWeekAgo()
|
||||
val filters = mutableMapOf<NormalizedRelayUrl, List<Filter>>()
|
||||
|
||||
relays.forEach { relay ->
|
||||
val relayFilters =
|
||||
filterGiftWrapsToPubkey(relay, pubkey, since).map { it.filter }
|
||||
|
||||
if (relayFilters.isNotEmpty()) {
|
||||
filters[relay] = relayFilters
|
||||
}
|
||||
}
|
||||
|
||||
if (filters.isNotEmpty()) {
|
||||
Amethyst.instance.client.subscribe(subIdGiftWraps, filters)
|
||||
Log.d(TAG) { "Subscribed to gift wraps on ${filters.size} DM relays" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNotification(connectedRelays: Int) {
|
||||
val notification = buildNotification(connectedRelays)
|
||||
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
Reference in New Issue
Block a user