mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
feat(napplet): relay subscription to discover napplet manifests
NappletsScreen previously showed only what was already cached. Adds a lightweight discovery subscription so the list actually populates: - NappletsFilterAssembler / NappletsFilterSubAssembler (SingleSubEoseManager): one REQ per read relay for kinds 15129/35129, deduped to a single subscription per account. No follow-list/feed-state machinery — the screen reads LocalCache directly, so the query state carries only the account + scope. - Registered as an app-lifetime singleton in RelaySubscriptionsCoordinator (the EOSE manager opens its relay sub at construction, so it can't be per-screen). - NappletsScreen invokes NappletsFilterAssemblerSubscription, which subscribes on STARTED and tears down after the lifecycle grace window. :amethyst:compileFdroidDebugKotlin passes; spotless clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
This commit is contained in:
@@ -225,9 +225,9 @@ Deferred to v2; v1 nails the single-applet boundary first.
|
||||
- ✅ **Privacy:** the host routes blob fetches through the user's Tor SOCKS proxy
|
||||
when active; the port is passed in by the launcher (main process) so the sandbox
|
||||
process never touches the account-bound HTTP stack.
|
||||
- **Relay discovery:** `NappletsScreen` reads only what's already in `LocalCache` —
|
||||
no dedicated subscription fetches napplet manifests yet, so the list is empty
|
||||
until one arrives via another feed. A `NappletsFilterAssemblerSubscription` is the
|
||||
next step.
|
||||
- ✅ **Relay discovery:** `NappletsFilterAssembler` (registered in
|
||||
`RelaySubscriptionsCoordinator`, invoked by `NappletsScreen`) REQs kinds
|
||||
15129/35129 from the user's read relays while the screen is open, so manifests
|
||||
flow into `LocalCache` for the list to render.
|
||||
- **Consent UX:** reuse `commons/.../ui/signing` styling; show the manifest title
|
||||
and a per-capability rationale; batch-grant on first run.
|
||||
|
||||
+2
@@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.datasource.Live
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.datasource.LongsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource.MusicPlaylistsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource.MusicTracksFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource.NappletsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestRoomFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestRoomLivenessAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestsFilterAssembler
|
||||
@@ -139,6 +140,7 @@ class RelaySubscriptionsCoordinator(
|
||||
val podcasts = PodcastsFilterAssembler(client)
|
||||
val onePodcast = OnePodcastFilterAssembler(client)
|
||||
val softwareApps = SoftwareAppsFilterAssembler(client)
|
||||
val napplets = NappletsFilterAssembler(client)
|
||||
val badges = BadgesFilterAssembler(client)
|
||||
val profileBadges = ProfileBadgesFilterAssembler(client)
|
||||
val profileAppRecommendations = ProfileAppRecommendationsFilterAssembler(client)
|
||||
|
||||
+5
@@ -49,6 +49,7 @@ import com.vitorpamplona.amethyst.napplet.NappletLauncher
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource.NappletsFilterAssemblerSubscription
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent
|
||||
@@ -66,6 +67,10 @@ fun NappletsScreen(
|
||||
nav: INav,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
// Pull napplet manifests from the user's relays into LocalCache while this screen is open.
|
||||
NappletsFilterAssemblerSubscription(accountViewModel)
|
||||
|
||||
val napplets by remember {
|
||||
Amethyst.instance.cache.observeEvents<Event>(
|
||||
Filter(kinds = listOf(RootNappletEvent.KIND, NamedNappletEvent.KIND)),
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
/**
|
||||
* Keyspace for the napplet-discovery subscription. Unlike a feed datasource it carries no
|
||||
* `FeedContentState` — [com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.NappletsScreen]
|
||||
* reads the manifests straight out of `LocalCache`, so this only needs the account (for relay
|
||||
* selection) and a scope.
|
||||
*/
|
||||
class NappletsQueryState(
|
||||
val account: Account,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
|
||||
/**
|
||||
* Subscribes to NIP-5D napplet manifests (kinds 15129/35129) on the user's read relays while a
|
||||
* napplet screen is open, dumping them into `LocalCache` for the screen to observe. Registered as
|
||||
* an app-lifetime singleton in `RelaySubscriptionsCoordinator` (the underlying EOSE manager opens
|
||||
* its relay subscription at construction, so it must not be created per screen).
|
||||
*/
|
||||
@Stable
|
||||
class NappletsFilterAssembler(
|
||||
client: INostrClient,
|
||||
) : ComposeSubscriptionManager<NappletsQueryState>() {
|
||||
val group =
|
||||
listOf(
|
||||
NappletsFilterSubAssembler(client, ::allKeys),
|
||||
)
|
||||
|
||||
override fun invalidateKeys() = invalidateFilters()
|
||||
|
||||
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
|
||||
|
||||
override fun destroy() = group.forEach { it.destroy() }
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
fun NappletsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
|
||||
val state =
|
||||
remember(accountViewModel.account) {
|
||||
NappletsQueryState(accountViewModel.account, accountViewModel.viewModelScope)
|
||||
}
|
||||
|
||||
LifecycleAwareKeyDataSourceSubscription(state, accountViewModel.dataSources().napplets)
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent
|
||||
import com.vitorpamplona.quartz.nip5dNapplets.RootNappletEvent
|
||||
|
||||
private const val NAPPLET_PAGE_LIMIT = 200
|
||||
|
||||
/**
|
||||
* Emits one REQ per read relay for both napplet manifest kinds. A single subscription per account
|
||||
* (deduped by [distinct]) is enough — the screen wants "what napplets exist", not a per-author
|
||||
* feed — so this stays far lighter than the follow-list feed assemblers.
|
||||
*/
|
||||
class NappletsFilterSubAssembler(
|
||||
client: INostrClient,
|
||||
allKeys: () -> Set<NappletsQueryState>,
|
||||
) : SingleSubEoseManager<NappletsQueryState>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
keys: List<NappletsQueryState>,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> {
|
||||
if (keys.isEmpty()) return emptyList()
|
||||
|
||||
return keys.flatMap { key ->
|
||||
key.account.homeRelays.flow.value.map { relay ->
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = listOf(RootNappletEvent.KIND, NamedNappletEvent.KIND),
|
||||
limit = NAPPLET_PAGE_LIMIT,
|
||||
since = since?.get(relay)?.time,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun distinct(key: NappletsQueryState) = key.account.signer.pubKey
|
||||
}
|
||||
Reference in New Issue
Block a user