mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 09:13:23 +00:00
feat: live relay subscription for connected-apps manifest screen
Replaces the one-shot fetchAll with a ComposeSubscriptionManager that holds an open relay subscription to NIP-5D manifests (kinds 15129/35129) for exactly the set of authors stored in the permission ledger, while ConnectedAppsScreen is in composition. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
This commit is contained in:
+2
@@ -54,6 +54,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.ConnectedAppsFilterAssembler
|
||||
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
|
||||
@@ -146,6 +147,7 @@ class RelaySubscriptionsCoordinator(
|
||||
val onePodcast = OnePodcastFilterAssembler(client)
|
||||
val softwareApps = SoftwareAppsFilterAssembler(client)
|
||||
val napplets = NappletsFilterAssembler(client)
|
||||
val connectedApps = ConnectedAppsFilterAssembler(client)
|
||||
val nsites = NsitesFilterAssembler(client)
|
||||
val badges = BadgesFilterAssembler(client)
|
||||
val profileBadges = ProfileBadgesFilterAssembler(client)
|
||||
|
||||
+6
-24
@@ -68,14 +68,13 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchAll
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource.ConnectedAppsFilterAssemblerSubscription
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent
|
||||
import com.vitorpamplona.quartz.nip5dNapplets.NappletManifest
|
||||
import com.vitorpamplona.quartz.nip5dNapplets.RootNappletEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import com.vitorpamplona.amethyst.commons.R as CommonsR
|
||||
|
||||
@@ -93,6 +92,7 @@ fun ConnectedAppsScreen(
|
||||
val signerLedger = remember { NostrSignerPermissionLedger(Amethyst.instance.signerPermissionStore) }
|
||||
|
||||
var items by remember { mutableStateOf<List<ConnectedAppEntry>?>(null) }
|
||||
var authors by remember { mutableStateOf<Set<HexKey>>(emptySet()) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val initial =
|
||||
@@ -100,29 +100,11 @@ fun ConnectedAppsScreen(
|
||||
loadConnectedApps(capabilityLedger, signerLedger)
|
||||
}
|
||||
items = initial
|
||||
|
||||
// Fetch manifests for all entries so the reactive cards can display title + icon.
|
||||
launch(Dispatchers.IO) {
|
||||
if (initial.isEmpty()) return@launch
|
||||
val authors = initial.map { it.coordinate.substringBefore(':') }.toSet()
|
||||
val filter =
|
||||
Filter(
|
||||
kinds = listOf(RootNappletEvent.KIND, NamedNappletEvent.KIND),
|
||||
authors = authors.toList(),
|
||||
)
|
||||
val relays = accountViewModel.account.homeRelays.flow.value
|
||||
if (relays.isEmpty()) return@launch
|
||||
runCatching {
|
||||
val events =
|
||||
accountViewModel.account.client.fetchAll(
|
||||
filters = relays.associateWith { listOf(filter) },
|
||||
timeoutMs = 15_000L,
|
||||
)
|
||||
events.forEach { LocalCache.justConsume(it, null, false) }
|
||||
}
|
||||
}
|
||||
authors = initial.map { it.coordinate.substringBefore(':') }.toSet()
|
||||
}
|
||||
|
||||
ConnectedAppsFilterAssemblerSubscription(accountViewModel, authors)
|
||||
|
||||
Scaffold(
|
||||
topBar = { TopBarWithBackButton(stringResource(R.string.napplet_permissions_title), nav) },
|
||||
) { padding ->
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
|
||||
/**
|
||||
* Keyspace for the connected-apps manifest subscription. Carries the account (for relay selection)
|
||||
* and the specific authors whose napplet manifests should be fetched — the set of pubkeys that have
|
||||
* been granted permissions in the user's ledger.
|
||||
*/
|
||||
class ConnectedAppsQueryState(
|
||||
val account: Account,
|
||||
val authors: Set<HexKey>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Live subscription for NIP-5D napplet manifests (kinds 15129/35129) while
|
||||
* [com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.ConnectedAppsScreen] is open.
|
||||
* Unlike [NappletsFilterAssembler] (which follows the global follow list), this assembler
|
||||
* only fetches manifests for the specific authors that have entries in the permission ledger.
|
||||
*/
|
||||
@Stable
|
||||
class ConnectedAppsFilterAssembler(
|
||||
client: INostrClient,
|
||||
) : ComposeSubscriptionManager<ConnectedAppsQueryState>() {
|
||||
val group =
|
||||
listOf(
|
||||
ConnectedAppsSubAssembler(client, ::allKeys),
|
||||
)
|
||||
|
||||
override fun invalidateKeys() = invalidateFilters()
|
||||
|
||||
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
|
||||
|
||||
override fun destroy() = group.forEach { it.destroy() }
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
@Composable
|
||||
fun ConnectedAppsFilterAssemblerSubscription(
|
||||
accountViewModel: AccountViewModel,
|
||||
authors: Set<HexKey>,
|
||||
) {
|
||||
val state =
|
||||
remember(accountViewModel.account, authors) {
|
||||
ConnectedAppsQueryState(accountViewModel.account, authors)
|
||||
}
|
||||
|
||||
LifecycleAwareKeyDataSourceSubscription(state, accountViewModel.dataSources().connectedApps)
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.service.relayClient.eoseManagers.PerUserEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource.subassemblies.filterNappletsByAuthors
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
|
||||
/**
|
||||
* Builds relay REQs for napplet manifests limited to the authors stored in
|
||||
* [ConnectedAppsQueryState.authors], queried against the account's home relays.
|
||||
* The filter is purposely narrow — we only want manifests for apps the user has already
|
||||
* connected to, not the full follow-list.
|
||||
*/
|
||||
class ConnectedAppsSubAssembler(
|
||||
client: INostrClient,
|
||||
allKeys: () -> Set<ConnectedAppsQueryState>,
|
||||
) : PerUserEoseManager<ConnectedAppsQueryState>(client, allKeys) {
|
||||
override fun user(key: ConnectedAppsQueryState) = key.account.userProfile()
|
||||
|
||||
override fun updateFilter(
|
||||
key: ConnectedAppsQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> {
|
||||
if (key.authors.isEmpty()) return emptyList()
|
||||
val relays = key.account.homeRelays.flow.value
|
||||
if (relays.isEmpty()) return emptyList()
|
||||
return relays.flatMap { relay ->
|
||||
filterNappletsByAuthors(relay, key.authors, since?.get(relay)?.time)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user