mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 08:04:45 +00:00
feat(nsites): dedicated nSites browse feed (mirrors nApplets)
Add a bottom-nav nSites feed for NIP-5A static sites (kinds 15128/35128), mirroring the nApplets feed: a follow-list FeedFilterSpinner in the top bar (persisted as defaultNsitesFollowList + liveNsitesFollowLists author-matcher) and rows rendered through the shared NoteCompose path, so they reuse the author header, StaticWebsiteCard (with Open), and reaction bar. New: NsitesScreen, NsitesTopBar, the Nsites discovery datasource trio, Route.Nsites, NavBarItem.NSITES. Wired into AppNavigation, the relay coordinator, and the follow-list settings persistence. Also includes the earlier display rebrand to nApplet/nSite. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
This commit is contained in:
@@ -106,6 +106,7 @@ private object PrefKeys {
|
||||
const val DEFAULT_POLLS_FOLLOW_LIST = "defaultPollsFollowList"
|
||||
const val DEFAULT_PICTURES_FOLLOW_LIST = "defaultPicturesFollowList"
|
||||
const val DEFAULT_NAPPLETS_FOLLOW_LIST = "defaultNappletsFollowList"
|
||||
const val DEFAULT_NSITES_FOLLOW_LIST = "defaultNsitesFollowList"
|
||||
const val DEFAULT_WORKOUTS_FOLLOW_LIST = "defaultWorkoutsFollowList"
|
||||
const val DEFAULT_CALENDARS_FOLLOW_LIST = "defaultCalendarsFollowList"
|
||||
const val DEFAULT_PRODUCTS_FOLLOW_LIST = "defaultProductsFollowList"
|
||||
@@ -392,6 +393,7 @@ object LocalPreferences {
|
||||
putString(PrefKeys.DEFAULT_POLLS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultPollsFollowList.value))
|
||||
putString(PrefKeys.DEFAULT_PICTURES_FOLLOW_LIST, JsonMapper.toJson(settings.defaultPicturesFollowList.value))
|
||||
putString(PrefKeys.DEFAULT_NAPPLETS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultNappletsFollowList.value))
|
||||
putString(PrefKeys.DEFAULT_NSITES_FOLLOW_LIST, JsonMapper.toJson(settings.defaultNsitesFollowList.value))
|
||||
putString(PrefKeys.DEFAULT_WORKOUTS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultWorkoutsFollowList.value))
|
||||
putString(PrefKeys.DEFAULT_CALENDARS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultCalendarsFollowList.value))
|
||||
putString(PrefKeys.DEFAULT_PRODUCTS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultProductsFollowList.value))
|
||||
@@ -717,6 +719,7 @@ object LocalPreferences {
|
||||
defaultPollsFollowList = MutableStateFlow(followListPrefs.polls),
|
||||
defaultPicturesFollowList = MutableStateFlow(followListPrefs.pictures),
|
||||
defaultNappletsFollowList = MutableStateFlow(followListPrefs.napplets),
|
||||
defaultNsitesFollowList = MutableStateFlow(followListPrefs.nsites),
|
||||
defaultWorkoutsFollowList = MutableStateFlow(followListPrefs.workouts),
|
||||
defaultCalendarsFollowList = MutableStateFlow(followListPrefs.calendars),
|
||||
defaultProductsFollowList = MutableStateFlow(followListPrefs.products),
|
||||
@@ -808,6 +811,7 @@ object LocalPreferences {
|
||||
val polls: TopFilter,
|
||||
val pictures: TopFilter,
|
||||
val napplets: TopFilter,
|
||||
val nsites: TopFilter,
|
||||
val workouts: TopFilter,
|
||||
val calendars: TopFilter,
|
||||
val products: TopFilter,
|
||||
@@ -862,6 +866,7 @@ object LocalPreferences {
|
||||
polls = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_POLLS_FOLLOW_LIST, null), TopFilter.Global),
|
||||
pictures = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_PICTURES_FOLLOW_LIST, null), TopFilter.Global),
|
||||
napplets = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_NAPPLETS_FOLLOW_LIST, null), TopFilter.Global),
|
||||
nsites = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_NSITES_FOLLOW_LIST, null), TopFilter.Global),
|
||||
workouts = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_WORKOUTS_FOLLOW_LIST, null), TopFilter.Global),
|
||||
calendars = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_CALENDARS_FOLLOW_LIST, null), TopFilter.Global),
|
||||
products = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_PRODUCTS_FOLLOW_LIST, null), TopFilter.AroundMe),
|
||||
|
||||
@@ -537,6 +537,9 @@ class Account(
|
||||
// Napplets read from the local cache (no outbox subscription yet), so only the author-matcher is needed.
|
||||
val liveNappletsFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultNappletsFollowList)
|
||||
|
||||
// nSites likewise read from the local cache; only the author-matcher is needed.
|
||||
val liveNsitesFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultNsitesFollowList)
|
||||
|
||||
val liveWorkoutsFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultWorkoutsFollowList)
|
||||
val liveWorkoutsFollowListsPerRelay = OutboxLoaderState(liveWorkoutsFollowLists, cache, scope).flow
|
||||
|
||||
|
||||
@@ -190,6 +190,7 @@ class AccountSettings(
|
||||
val defaultPollsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||
val defaultPicturesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||
val defaultNappletsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||
val defaultNsitesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||
val defaultWorkoutsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||
val defaultCalendarsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||
val defaultProductsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.AroundMe),
|
||||
@@ -652,6 +653,17 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultNsitesFollowList(name: FeedDefinition) {
|
||||
changeDefaultNsitesFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultNsitesFollowList(name: TopFilter) {
|
||||
if (defaultNsitesFollowList.value != name) {
|
||||
defaultNsitesFollowList.tryEmit(name)
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultWorkoutsFollowList(name: FeedDefinition) {
|
||||
changeDefaultWorkoutsFollowList(name.code)
|
||||
}
|
||||
|
||||
+2
@@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource.Napplet
|
||||
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
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nsites.datasource.NsitesFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource.PicturesFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.datasource.OnePodcastFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.datasource.PodcastEpisodesFilterAssembler
|
||||
@@ -141,6 +142,7 @@ class RelaySubscriptionsCoordinator(
|
||||
val onePodcast = OnePodcastFilterAssembler(client)
|
||||
val softwareApps = SoftwareAppsFilterAssembler(client)
|
||||
val napplets = NappletsFilterAssembler(client)
|
||||
val nsites = NsitesFilterAssembler(client)
|
||||
val badges = BadgesFilterAssembler(client)
|
||||
val profileBadges = ProfileBadgesFilterAssembler(client)
|
||||
val profileAppRecommendations = ProfileAppRecommendationsFilterAssembler(client)
|
||||
|
||||
@@ -152,6 +152,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser.ImportFollowListPic
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser.ImportFollowListSelectUserScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nsites.NsitesScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.PicturesScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.pinnednotes.PinnedNotesScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.PodcastEpisodesScreen
|
||||
@@ -291,6 +292,7 @@ fun BuildNavigation(
|
||||
composableFromEnd<Route.Workouts> { WorkoutsScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.SoftwareApps> { SoftwareAppsScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.Napplets> { NappletsScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.Nsites> { NsitesScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.NappletPermissions> { NappletPermissionsScreen(accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.SoftwareAppDetail> { SoftwareAppDetailScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
|
||||
composableFromEnd<Route.Calendars> { CalendarsScreen(accountViewModel, nav) }
|
||||
|
||||
+9
@@ -53,6 +53,7 @@ enum class NavBarItem {
|
||||
WORKOUTS,
|
||||
SOFTWARE_APPS,
|
||||
NAPPLETS,
|
||||
NSITES,
|
||||
CALENDARS,
|
||||
CALENDAR_COLLECTIONS,
|
||||
SHORTS,
|
||||
@@ -229,6 +230,13 @@ val NavBarCatalog: Map<NavBarItem, NavBarItemDef> =
|
||||
icon = MaterialSymbols.Apps,
|
||||
resolveRoute = { Route.Napplets },
|
||||
),
|
||||
NavBarItem.NSITES to
|
||||
NavBarItemDef(
|
||||
id = NavBarItem.NSITES,
|
||||
labelRes = R.string.nsites,
|
||||
icon = MaterialSymbols.Language,
|
||||
resolveRoute = { Route.Nsites },
|
||||
),
|
||||
NavBarItem.CALENDARS to
|
||||
NavBarItemDef(
|
||||
id = NavBarItem.CALENDARS,
|
||||
@@ -393,6 +401,7 @@ val DrawerFeedsItems: List<NavBarItem> =
|
||||
NavBarItem.WORKOUTS,
|
||||
NavBarItem.SOFTWARE_APPS,
|
||||
NavBarItem.NAPPLETS,
|
||||
NavBarItem.NSITES,
|
||||
NavBarItem.CALENDARS,
|
||||
NavBarItem.CALENDAR_COLLECTIONS,
|
||||
NavBarItem.SHORTS,
|
||||
|
||||
@@ -89,6 +89,8 @@ sealed class Route {
|
||||
|
||||
@Serializable object Napplets : Route()
|
||||
|
||||
@Serializable object Nsites : Route()
|
||||
|
||||
@Serializable object NappletPermissions : Route()
|
||||
|
||||
@Serializable data class SoftwareAppDetail(
|
||||
|
||||
+4
-1
@@ -95,9 +95,12 @@ private fun PreloadFor(
|
||||
|
||||
NavBarItem.SOFTWARE_APPS -> SoftwareAppsFilterAssemblerSubscription(accountViewModel)
|
||||
|
||||
// Napplets read directly from the local cache; no dedicated relay subscription yet.
|
||||
// Napplets & nSites read directly from the local cache; their screens open the discovery
|
||||
// subscription themselves, so there's nothing to preload here.
|
||||
NavBarItem.NAPPLETS -> {}
|
||||
|
||||
NavBarItem.NSITES -> {}
|
||||
|
||||
NavBarItem.CALENDARS,
|
||||
NavBarItem.CALENDAR_COLLECTIONS,
|
||||
-> CalendarsFilterAssemblerSubscription(accountViewModel)
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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.nsites
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nsites.datasource.NsitesFilterAssemblerSubscription
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent
|
||||
|
||||
/**
|
||||
* Lists the NIP-5A static-site manifests currently in the local cache (kinds 15128/35128). Mirrors the
|
||||
* nApplets browse screen: a follow-list filter in the top bar and each row rendered through the shared
|
||||
* [NoteCompose] — the same path the main feed uses for these events — so it gets the author header, the
|
||||
* [com.vitorpamplona.amethyst.commons.ui.note.StaticWebsiteCard] (with its Open button), and the
|
||||
* standard reaction bar for free.
|
||||
*/
|
||||
@Composable
|
||||
fun NsitesScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
// Pull nSite manifests from the user's relays into LocalCache while this screen is open.
|
||||
NsitesFilterAssemblerSubscription(accountViewModel)
|
||||
|
||||
val nsites by remember {
|
||||
Amethyst.instance.cache.observeEvents<Event>(
|
||||
Filter(kinds = listOf(RootSiteEvent.KIND, NamedSiteEvent.KIND)),
|
||||
)
|
||||
}.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
val followFilter by accountViewModel.account.liveNsitesFollowLists
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
val visible =
|
||||
remember(nsites, followFilter) {
|
||||
nsites.filter { followFilter.matchAuthor(it.pubKey) }
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { NsitesTopBar(accountViewModel, nav) },
|
||||
) { padding ->
|
||||
if (visible.isEmpty()) {
|
||||
Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
stringResource(R.string.nsite_none_found),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(Modifier.fillMaxSize().padding(padding)) {
|
||||
items(visible, key = { it.id }) { event ->
|
||||
val note = remember(event.id) { Amethyst.instance.cache.getOrCreateNote(event) }
|
||||
NoteCompose(
|
||||
baseNote = note,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
quotesLeft = 3,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+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.nsites
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.TopFilter
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.FeedFilterSpinner
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
/**
|
||||
* Top bar for the nSites browse screen, matching the other feed screens (Pictures, nApplets, …): the
|
||||
* avatar drawer + search, wrapping a centered follow-list [FeedFilterSpinner] that filters which
|
||||
* authors' nSites are shown.
|
||||
*/
|
||||
@Composable
|
||||
fun NsitesTopBar(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
UserDrawerSearchTopBar(accountViewModel, nav) {
|
||||
val listName: TopFilter by accountViewModel.account.settings.defaultNsitesFollowList
|
||||
.collectAsStateWithLifecycle()
|
||||
val allLists by accountViewModel.feedStates.feedListOptions.kind3GlobalPeopleRoutes
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
FeedFilterSpinner(
|
||||
placeholderCode = listName,
|
||||
explainer = stringRes(R.string.select_list_to_filter),
|
||||
options = allLists,
|
||||
onSelect = { selected: FeedDefinition -> accountViewModel.account.settings.changeDefaultNsitesFollowList(selected) },
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
+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.nsites.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 nSite-discovery subscription. Like its nApplet sibling it carries no
|
||||
* `FeedContentState` — [com.vitorpamplona.amethyst.ui.screen.loggedIn.nsites.NsitesScreen] reads the
|
||||
* manifests straight out of `LocalCache`, so this only needs the account (for relay selection) and a
|
||||
* scope.
|
||||
*/
|
||||
class NsitesQueryState(
|
||||
val account: Account,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
|
||||
/**
|
||||
* Subscribes to NIP-5A static-site manifests (kinds 15128/35128) on the user's read relays while an
|
||||
* nSite 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 NsitesFilterAssembler(
|
||||
client: INostrClient,
|
||||
) : ComposeSubscriptionManager<NsitesQueryState>() {
|
||||
val group =
|
||||
listOf(
|
||||
NsitesFilterSubAssembler(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.nsites.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 NsitesFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
|
||||
val state =
|
||||
remember(accountViewModel.account) {
|
||||
NsitesQueryState(accountViewModel.account, accountViewModel.viewModelScope)
|
||||
}
|
||||
|
||||
LifecycleAwareKeyDataSourceSubscription(state, accountViewModel.dataSources().nsites)
|
||||
}
|
||||
+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.nsites.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.nip5aStaticWebsites.NamedSiteEvent
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent
|
||||
|
||||
private const val NSITE_PAGE_LIMIT = 200
|
||||
|
||||
/**
|
||||
* Emits one REQ per read relay for both static-site manifest kinds. A single subscription per account
|
||||
* (deduped by [distinct]) is enough — the screen wants "what nSites exist", not a per-author feed — so
|
||||
* this stays far lighter than the follow-list feed assemblers.
|
||||
*/
|
||||
class NsitesFilterSubAssembler(
|
||||
client: INostrClient,
|
||||
allKeys: () -> Set<NsitesQueryState>,
|
||||
) : SingleSubEoseManager<NsitesQueryState>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
keys: List<NsitesQueryState>,
|
||||
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(RootSiteEvent.KIND, NamedSiteEvent.KIND),
|
||||
limit = NSITE_PAGE_LIMIT,
|
||||
since = since?.get(relay)?.time,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun distinct(key: NsitesQueryState) = key.account.signer.pubKey
|
||||
}
|
||||
@@ -663,6 +663,8 @@
|
||||
<string name="software_apps">Apps</string>
|
||||
<string name="route_software_apps">Apps</string>
|
||||
<string name="napplets">nApplets</string>
|
||||
<string name="nsites">nSites</string>
|
||||
<string name="nsite_none_found">No nSites found yet.</string>
|
||||
<string name="napplet_permissions">nApplet permissions</string>
|
||||
<string name="napplet_manage_permissions">Manage permissions</string>
|
||||
<string name="napplet_permissions_empty">No nApplet permissions yet</string>
|
||||
|
||||
Reference in New Issue
Block a user