diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationRelayService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationRelayService.kt index 28cd04414c..3411379e4c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationRelayService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationRelayService.kt @@ -36,6 +36,7 @@ import android.os.SystemClock import androidx.core.app.NotificationCompat import androidx.core.app.ServiceCompat import androidx.core.content.ContextCompat +import androidx.core.net.toUri import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R @@ -79,6 +80,10 @@ class NotificationRelayService : Service() { companion object { private const val TAG = "NotificationRelayService" private const val CHANNEL_ID = "notification_relay_service" + + /** Parsed back into `Route.ActiveSubscriptions` by `MainActivity.uriToRoute`. */ + private const val ACTIVE_SUBSCRIPTIONS_URI = "activesubs" + private const val NOTIFICATION_ID = 9832 private const val ACTION_START = "com.vitorpamplona.amethyst.START_NOTIFICATION_SERVICE" @@ -141,6 +146,9 @@ class NotificationRelayService : Service() { private var relayServiceCollectorJob: Job? = null private var connectedRelayCount = 0 + /** Last non-empty per-job breakdown, kept so a reconnect does not blank the expanded view. */ + private var lastBreakdown: List = emptyList() + override fun onBind(intent: Intent?): IBinder? = null override fun onCreate() { @@ -336,9 +344,12 @@ class NotificationRelayService : Service() { pluralStringRes(this, R.plurals.always_on_notif_connected, connectedRelays, connectedRelays) } + // Tapping goes to the screen that answers the question the notification raises — "why is it + // connected to N relays" — rather than to whatever tab was last open. val openAppIntent = Intent(this, MainActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + data = ACTIVE_SUBSCRIPTIONS_URI.toUri() } val pendingIntent = PendingIntent.getActivity( @@ -351,8 +362,16 @@ class NotificationRelayService : Service() { // Expanded only. The collapsed line stays the bare count it has always been — that is all // most people want from an ongoing notification — and the per-job breakdown appears solely // when someone deliberately expands it to ask why the phone is talking to N relays. - // Skipped entirely when nothing is attributed yet, so we never render an empty section. - val breakdown = RelayPurposeSummary.lines(this).takeIf { it.isNotEmpty() } + // + // Held across reconnects rather than recomputed blindly: the breakdown is derived from the + // *connected* relays, so a drop to zero (the "connecting…" state) would otherwise empty it and + // the expanded view would collapse to a single line exactly when someone is most likely + // looking at it. What each connection is *for* does not change while it is re-establishing, + // so the last known answer is still the right one; only the count above it goes stale, and + // that count is already labelled "connecting". + val fresh = RelayPurposeSummary.lines(this) + if (fresh.isNotEmpty()) lastBreakdown = fresh + val breakdown = fresh.ifEmpty { lastBreakdown }.takeIf { it.isNotEmpty() } return NotificationCompat .Builder(this, CHANNEL_ID) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 2f2ac4eba9..0af898dac4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -203,6 +203,13 @@ fun isWalletConnectRoute(uri: String) = uri.startsWith("dlnwc?value=") || uri.st fun isMarmotGroupRoute(uri: String) = uri.startsWith("marmot:") +/** + * Tapping the always-on service notification. That notification's whole subject is the relay + * connections it is holding open, so it lands on the screen that explains them rather than on + * whatever tab happened to be last open. + */ +fun isActiveSubscriptionsRoute(uri: String) = uri.startsWith("activesubs", true) || uri.startsWith("nostr:activesubs", true) + private val MARMOT_HEX = Regex("^[0-9a-fA-F]+$") fun uriToRoute( @@ -213,6 +220,9 @@ fun uriToRoute( val scrollTo = runCatching { java.net.URI(uri.removePrefix(NOSTR_URI_PREFIX)).findParameterValue("scrollTo") }.getOrNull() return Route.Notification(scrollToEventId = scrollTo) } + if (isActiveSubscriptionsRoute(uri)) { + return Route.ActiveSubscriptions + } if (isHashtagRoute(uri)) { return Route.Hashtag(uri.removePrefix(NOSTR_URI_PREFIX).removePrefix("hashtag?id=").lowercase()) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 41ca6d47ae..723466368e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -592,7 +592,7 @@ fun BuildNavigation( composableFromEndArgs { UpdateZapAmountScreen(accountViewModel, nav, it.nip47) } composableFromEndArgs { AllRelayListScreen(accountViewModel, nav) } - composableFromEndArgs { ActiveSubscriptionsScreen() } + composableFromEndArgs { ActiveSubscriptionsScreen(accountViewModel, nav) } composableFromEnd { EventSyncScreen(accountViewModel, nav) } composableFromEnd { RequestToVanishScreen(accountViewModel, nav) } composableFromEnd { VanishEventsScreen(accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt index 8026d2a9c7..d9a2d09b87 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt @@ -104,6 +104,7 @@ fun buildSettingsCatalog( symEntry(R.string.calendar_reminder_settings_title, MaterialSymbols.CalendarMonth, R.string.calendar_reminder_search_keywords, Route.CalendarReminderSettings), symEntry(R.string.ots_explorer_settings, MaterialSymbols.Search, R.string.ots_explorer_search_keywords, Route.OtsSettings), symEntry(R.string.namecoin_settings, MaterialSymbols.Security, R.string.namecoin_search_keywords, Route.NamecoinSettings), + symEntry(R.string.active_subs_title, MaterialSymbols.CloudSync, R.string.active_subs_search_keywords, Route.ActiveSubscriptions), symEntry(R.string.resource_usage_title, MaterialSymbols.Bolt, R.string.resource_usage_search_keywords, Route.ResourceUsage), ), )