From 8032e8fa49422c560ad90942087e16cf206b82a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Jun 2026 21:01:53 +0000 Subject: [PATCH 1/2] refactor: resolve TopFilter.Mine to a real author filter in the shared flow The shared FeedTopNavFilterState.loadFlowsFor() mapped TopFilter.Mine to AllFollowsFeedFlow, so "Mine" silently fell back to all-follows. Every screen that offered the Mine chip (badges, communities, music tracks/playlists, git repositories, nApplets, nSites, and the browser app rows) had to special-case TopFilter.Mine on both the relay sub-assembler and the local DAL / display filter to scope content to the user. Introduce MineFeedFlow, which mirrors AllFollowsFeedFlow's outbox/proxy split but pins the author set to the logged-in user's own pubkey. Because both liveXFollowLists (DAL) and liveXFollowListsPerRelay (relay) derive from this flow, the generic author paths now narrow to the user, so the per-screen Mine branches are pure redundancy and are removed. This also makes outbox-change invalidation automatic: the Mine relay set now comes from liveXFollowListsPerRelay, an OutboxLoaderState over the user's own outbox, so a NIP-65 update re-emits through the existing followsPerRelayFlow collector without a screen-specific trigger. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01YWUwciqZiXTkbYVSyiF2P4 --- .../topNavFeeds/FeedTopNavFilterState.kt | 3 +- .../model/topNavFeeds/mine/MineFeedFlow.kt | 72 +++++++++++++++++++ .../loggedIn/badges/dal/BadgesFeedFilter.kt | 26 ++----- .../badges/datasource/BadgesSubAssembler.kt | 10 +-- .../badges/datasource/FilterBadges.kt | 21 ------ .../screen/loggedIn/browser/BrowserScreen.kt | 24 +++---- .../list/dal/CommunitiesFeedFilter.kt | 22 ------ .../datasource/CommunitiesListSubAssembler.kt | 10 +-- .../list/datasource/FilterCommunitiesMine.kt | 51 ------------- .../dal/GitRepositoriesFeedFilter.kt | 29 ++------ .../datasource/GitRepositoriesSubAssembler.kt | 11 +-- .../FilterGitRepositoriesMine.kt | 55 -------------- .../music/dal/MusicPlaylistsFeedFilter.kt | 26 +------ .../music/dal/MusicTracksFeedFilter.kt | 26 +------ .../datasource/MusicPlaylistsSubAssembler.kt | 10 +-- .../datasource/MusicTracksSubAssembler.kt | 10 +-- .../subassemblies/FilterMusicEventsMine.kt | 54 -------------- .../loggedIn/napplets/NappletsScreen.kt | 12 ++-- .../datasource/NappletsFilterSubAssembler.kt | 9 +-- .../subassemblies/FilterNappletsMine.kt | 57 --------------- .../ui/screen/loggedIn/nsites/NsitesScreen.kt | 12 ++-- .../datasource/NsitesFilterSubAssembler.kt | 9 +-- .../subassemblies/FilterNsitesMine.kt | 57 --------------- 23 files changed, 122 insertions(+), 494 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/mine/MineFeedFlow.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/datasource/FilterCommunitiesMine.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesMine.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/subassemblies/FilterMusicEventsMine.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/datasource/subassemblies/FilterNappletsMine.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nsites/datasource/subassemblies/FilterNsitesMine.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt index 96c439dfbf..00e9aa2fd5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt @@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.favoriteAlgoFeeds.FavoriteAl import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalFeedFlow import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagFeedFlow import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.MultiHashtagFeedFlow +import com.vitorpamplona.amethyst.model.topNavFeeds.mine.MineFeedFlow import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.NoteFeedFlow import com.vitorpamplona.amethyst.model.topNavFeeds.relay.RelayFeedFlow import com.vitorpamplona.amethyst.service.location.LocationState @@ -93,7 +94,7 @@ class FeedTopNavFilterState( } TopFilter.Mine -> { - AllFollowsFeedFlow(allFollows, followsRelays, blockedRelays, proxyRelays) + MineFeedFlow(signer.pubKey, blockedRelays, proxyRelays) } is TopFilter.Community, is TopFilter.PeopleList, is TopFilter.MuteList -> { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/mine/MineFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/mine/MineFeedFlow.kt new file mode 100644 index 0000000000..dd8f0b6818 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/mine/MineFeedFlow.kt @@ -0,0 +1,72 @@ +/* + * 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.model.topNavFeeds.mine + +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByProxyTopNavFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map + +/** + * Resolves the "Mine" top-nav selection to an author filter scoped to the logged-in user's own + * pubkey. Mirrors [com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsFeedFlow]'s + * outbox/proxy split: in proxy mode it pins the author to the configured proxy relays, otherwise it + * resolves the user's own NIP-65 outbox via OutboxRelayLoader. + * + * This is the single source of truth for "Mine". Both the relay sub-assemblers (through each screen's + * `liveXFollowListsPerRelay`) and the local DAL filters (through `liveXFollowLists`) consume the + * produced [AuthorsByOutboxTopNavFilter] / [AuthorsByProxyTopNavFilter], so screens no longer need a + * dedicated `TopFilter.Mine` branch: the generic author path already narrows to the user. It also + * means outbox changes re-invalidate automatically — the per-relay flow is an `OutboxLoaderState` + * over the user's own outbox, so a NIP-65 update re-emits without a screen-specific trigger. + */ +class MineFeedFlow( + val myPubkey: HexKey, + val blockedRelays: StateFlow>, + val proxyRelays: StateFlow>, +) : IFeedFlowsType { + fun convert(proxyRelays: Set): IFeedTopNavFilter = + if (proxyRelays.isEmpty()) { + AuthorsByOutboxTopNavFilter( + authors = setOf(myPubkey), + blockedRelays = blockedRelays, + ) + } else { + AuthorsByProxyTopNavFilter( + authors = setOf(myPubkey), + proxyRelays = proxyRelays, + ) + } + + override fun flow(): Flow = proxyRelays.map(::convert) + + override fun startValue(): IFeedTopNavFilter = convert(proxyRelays.value) + + override suspend fun startValue(collector: FlowCollector) { + collector.emit(startValue()) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/dal/BadgesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/dal/BadgesFeedFilter.kt index 7c932c0ddf..babc7e96d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/dal/BadgesFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/dal/BadgesFeedFilter.kt @@ -47,35 +47,17 @@ class BadgesFeedFilter( override fun showHiddenKey(): Boolean = followList().wantsToSeeNegativeStuff() - private fun myPubkey(): String = account.userProfile().pubkeyHex - override fun feed(): List { + val params = buildFilterParams(account) val notes = - if (followList() == TopFilter.Mine) { - val me = myPubkey() - LocalCache.addressables.filterIntoSet(BadgeDefinitionEvent.KIND) { _, it -> - val noteEvent = it.event - noteEvent is BadgeDefinitionEvent && noteEvent.pubKey == me - } - } else { - val params = buildFilterParams(account) - LocalCache.addressables.filterIntoSet(BadgeDefinitionEvent.KIND) { _, it -> - val noteEvent = it.event - noteEvent is BadgeDefinitionEvent && params.match(noteEvent, it.relays) - } + LocalCache.addressables.filterIntoSet(BadgeDefinitionEvent.KIND) { _, it -> + val noteEvent = it.event + noteEvent is BadgeDefinitionEvent && params.match(noteEvent, it.relays) } return sort(notes) } override fun applyFilter(newItems: Set): Set { - if (followList() == TopFilter.Mine) { - val me = myPubkey() - return newItems.filterTo(HashSet()) { - val noteEvent = it.event - noteEvent is BadgeDefinitionEvent && noteEvent.pubKey == me - } - } - val params = buildFilterParams(account) return newItems.filterTo(HashSet()) { val noteEvent = it.event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/datasource/BadgesSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/datasource/BadgesSubAssembler.kt index 5971b8b407..f4d5b0288a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/datasource/BadgesSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/datasource/BadgesSubAssembler.kt @@ -42,15 +42,11 @@ class BadgesSubAssembler( key: BadgesQueryState, since: SincePerRelayMap?, ): List { - val listName = key.listName() + // "Mine" needs no special-case: the shared TopFilter.Mine flow now resolves to an author + // filter scoped to the user, so followsPerRelay() already carries authors=[me]. val defaultSince = key.feedStates.badgesFeed.lastNoteCreatedAtIfFilled() - return if (listName == TopFilter.Mine) { - val outbox = key.account.outboxRelays.flow.value - filterBadgesMine(key.account.userProfile().pubkeyHex, outbox, since) - } else { - makeBadgesFilter(key.followsPerRelay(), since, defaultSince) - } + return makeBadgesFilter(key.followsPerRelay(), since, defaultSince) } override fun user(key: BadgesQueryState) = key.account.userProfile() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/datasource/FilterBadges.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/datasource/FilterBadges.kt index ba08ae6329..a4151d33a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/datasource/FilterBadges.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/datasource/FilterBadges.kt @@ -48,27 +48,6 @@ fun makeBadgesFilter( else -> emptyList() } -fun filterBadgesMine( - pubkey: HexKey, - relays: Set, - since: SincePerRelayMap?, -): List { - if (relays.isEmpty() || pubkey.isEmpty()) return emptyList() - val authors = listOf(pubkey) - return relays.map { relay -> - RelayBasedFilter( - relay = relay, - filter = - Filter( - kinds = listOf(BadgeDefinitionEvent.KIND), - authors = authors, - limit = BADGE_FEED_LIMIT, - since = since?.get(relay)?.time, - ), - ) - } -} - private fun filterBadgesByAuthorsOnRelay( relay: NormalizedRelayUrl, authors: Set, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserScreen.kt index 6d7fb4dbcd..a74ac3db4e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserScreen.kt @@ -90,7 +90,6 @@ import com.vitorpamplona.amethyst.favorites.FavoriteAppsRegistry import com.vitorpamplona.amethyst.favorites.PreloadFavoriteNostrApps import com.vitorpamplona.amethyst.favorites.rememberNappletIconModel import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.TopFilter import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route @@ -189,22 +188,17 @@ private fun BrowserLauncher( }.collectAsStateWithLifecycle(emptyList()) val nsiteFollows by accountViewModel.account.liveNsitesFollowLists.collectAsStateWithLifecycle() - val nsiteListName by accountViewModel.account.settings.defaultNsitesFollowList - .collectAsStateWithLifecycle() val nappletFollows by accountViewModel.account.liveNappletsFollowLists.collectAsStateWithLifecycle() - val nappletListName by accountViewModel.account.settings.defaultNappletsFollowList - .collectAsStateWithLifecycle() - val myPubkey = accountViewModel.account.userProfile().pubkeyHex // Drop ones already pinned — they show under Favorites, not twice. val favoriteCoordinates = remember(apps) { apps.filterIsInstance().mapTo(HashSet()) { it.coordinate } } val followedNsites = - remember(nsiteNotes, nsiteFollows, nsiteListName, myPubkey, favoriteCoordinates) { - nsiteNotes.toDiscoverApps(nsiteListName == TopFilter.Mine, myPubkey, nsiteFollows::matchAuthor, favoriteCoordinates) + remember(nsiteNotes, nsiteFollows, favoriteCoordinates) { + nsiteNotes.toDiscoverApps(nsiteFollows::matchAuthor, favoriteCoordinates) } val followedNapplets = - remember(nappletNotes, nappletFollows, nappletListName, myPubkey, favoriteCoordinates) { - nappletNotes.toDiscoverApps(nappletListName == TopFilter.Mine, myPubkey, nappletFollows::matchAuthor, favoriteCoordinates) + remember(nappletNotes, nappletFollows, favoriteCoordinates) { + nappletNotes.toDiscoverApps(nappletFollows::matchAuthor, favoriteCoordinates) } // What the user actually typed, excluding any selected ghost-completion suffix (selection.min is the @@ -606,20 +600,18 @@ private data class DiscoverNostrApp( private const val DISCOVER_NOSTR_LIMIT = 12 /** - * Keeps the [Note]s authored by the followed set (or by the user, in the "Mine" case — the shared - * matcher resolves Mine to all-follows, so it can't serve that case), drops ones already pinned, maps - * each to its launchable [DiscoverNostrApp], and caps the result. + * Keeps the [Note]s authored by the followed set, drops ones already pinned, maps each to its + * launchable [DiscoverNostrApp], and caps the result. Covers the "Mine" selection too: the shared + * matcher now resolves Mine to the user's own pubkey, so [matchAuthor] already narrows to the user. */ private fun List.toDiscoverApps( - mine: Boolean, - myPubkey: String, matchAuthor: (String) -> Boolean, excludeCoordinates: Set, ): List = asSequence() .filter { note -> val author = note.event?.pubKey ?: return@filter false - if (mine) author == myPubkey else matchAuthor(author) + matchAuthor(author) }.mapNotNull { it.toDiscoverNostrApp() } .filter { it.app.coordinate !in excludeCoordinates } .take(DISCOVER_NOSTR_LIMIT) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/dal/CommunitiesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/dal/CommunitiesFeedFilter.kt index 3b5decf1a0..67864beb81 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/dal/CommunitiesFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/dal/CommunitiesFeedFilter.kt @@ -24,7 +24,6 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.TopFilter -import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.model.mapNotNullIntoSet import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.DiscoverCommunityFeedFilter @@ -40,19 +39,7 @@ class CommunitiesFeedFilter( override fun followList(): TopFilter = account.settings.defaultCommunitiesFollowList.value - private fun myPubkey(): String = account.userProfile().pubkeyHex - override fun feed(): List { - if (followList() == TopFilter.Mine) { - val me = myPubkey() - val notes = - LocalCache.addressables.filterIntoSet(CommunityDefinitionEvent.KIND) { _, note -> - val noteEvent = note.event - noteEvent is CommunityDefinitionEvent && noteEvent.pubKey == me - } - return sort(notes) - } - val filterParams = FilterByListParams.create( followLists = account.liveCommunitiesFollowLists.value, @@ -77,15 +64,6 @@ class CommunitiesFeedFilter( override fun applyFilter(newItems: Set): Set = innerApplyFilter(newItems) override fun innerApplyFilter(collection: Collection): Set { - if (followList() == TopFilter.Mine) { - val me = myPubkey() - return collection - .filterTo(HashSet()) { - val noteEvent = it.event - noteEvent is CommunityDefinitionEvent && noteEvent.pubKey == me - } - } - val filterParams = FilterByListParams.create( followLists = account.liveCommunitiesFollowLists.value, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/datasource/CommunitiesListSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/datasource/CommunitiesListSubAssembler.kt index 61cef800a0..99af61deea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/datasource/CommunitiesListSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/datasource/CommunitiesListSubAssembler.kt @@ -43,15 +43,11 @@ class CommunitiesListSubAssembler( key: CommunitiesListQueryState, since: SincePerRelayMap?, ): List { - val listName = key.listName() + // "Mine" needs no special-case: the shared TopFilter.Mine flow now resolves to an author + // filter scoped to the user, so followsPerRelay() already carries authors=[me]. val defaultSince = key.feedStates.communitiesList.lastNoteCreatedAtIfFilled() - return if (listName == TopFilter.Mine) { - val outbox = key.account.outboxRelays.flow.value - filterCommunitiesMine(key.account.userProfile().pubkeyHex, outbox, since) - } else { - makeCommunitiesFilter(key.followsPerRelay(), since, defaultSince) - } + return makeCommunitiesFilter(key.followsPerRelay(), since, defaultSince) } override fun user(key: CommunitiesListQueryState) = key.account.userProfile() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/datasource/FilterCommunitiesMine.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/datasource/FilterCommunitiesMine.kt deleted file mode 100644 index 12593aae07..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/datasource/FilterCommunitiesMine.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.communities.list.datasource - -import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent - -private const val COMMUNITIES_MINE_LIMIT = 300 - -fun filterCommunitiesMine( - pubkey: HexKey, - relays: Set, - since: SincePerRelayMap?, -): List { - if (relays.isEmpty() || pubkey.isEmpty()) return emptyList() - val authors = listOf(pubkey) - return relays.map { relay -> - RelayBasedFilter( - relay = relay, - filter = - Filter( - kinds = listOf(CommunityDefinitionEvent.KIND), - authors = authors, - limit = COMMUNITIES_MINE_LIMIT, - since = since?.get(relay)?.time, - ), - ) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/dal/GitRepositoriesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/dal/GitRepositoriesFeedFilter.kt index 0f383daee6..9a26e16960 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/dal/GitRepositoriesFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/dal/GitRepositoriesFeedFilter.kt @@ -48,36 +48,17 @@ class GitRepositoriesFeedFilter( override fun showHiddenKey(): Boolean = followList().wantsToSeeNegativeStuff() override fun feed(): List { + val params = buildFilterParams(account) val notes = - if (followList() == TopFilter.Mine) { - val me = account.userProfile().pubkeyHex - LocalCache.addressables.filterIntoSet(GitRepositoryEvent.KIND) { _, it -> isMine(it, me) } - } else { - val params = buildFilterParams(account) - LocalCache.addressables.filterIntoSet(GitRepositoryEvent.KIND) { _, it -> - val noteEvent = it.event - noteEvent is GitRepositoryEvent && params.match(noteEvent, it.relays) - } + LocalCache.addressables.filterIntoSet(GitRepositoryEvent.KIND) { _, it -> + val noteEvent = it.event + noteEvent is GitRepositoryEvent && params.match(noteEvent, it.relays) } return sort(notes) } - override fun applyFilter(newItems: Set): Set { - if (followList() == TopFilter.Mine) { - val me = account.userProfile().pubkeyHex - return newItems.filterTo(HashSet()) { isMine(it, me) } - } - return innerApplyFilter(newItems) - } - - private fun isMine( - note: Note, - me: String, - ): Boolean { - val noteEvent = note.event - return noteEvent is GitRepositoryEvent && noteEvent.pubKey == me - } + override fun applyFilter(newItems: Set): Set = innerApplyFilter(newItems) fun buildFilterParams(account: Account): FilterByListParams = FilterByListParams.create( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/GitRepositoriesSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/GitRepositoriesSubAssembler.kt index 1c837ee555..758fc1792f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/GitRepositoriesSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/GitRepositoriesSubAssembler.kt @@ -24,7 +24,6 @@ import com.vitorpamplona.amethyst.model.TopFilter import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource.subassemblies.filterGitRepositoriesMine import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription @@ -43,13 +42,9 @@ class GitRepositoriesSubAssembler( key: GitRepositoriesQueryState, since: SincePerRelayMap?, ): List { - // "Mine" bypasses the follow-list machinery: query the user's own repositories by author - // against their outbox relays (same pattern as music/badges), because the shared - // TopFilter.Mine flow falls back to all-follows. - if (key.listName() == TopFilter.Mine) { - val outbox = key.account.outboxRelays.flow.value - return filterGitRepositoriesMine(key.account.userProfile().pubkeyHex, outbox, since) - } + // "Mine" needs no special-case: the shared TopFilter.Mine flow now resolves to an author + // filter scoped to the user, so followsPerRelay() already carries authors=[me] against the + // user's own outbox. val feedSettings = key.followsPerRelay() return makeGitRepositoriesFilter(feedSettings, since, key.feedStates.gitRepositoriesFeed.lastNoteCreatedAtIfFilled()) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesMine.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesMine.kt deleted file mode 100644 index 7e6396f168..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesMine.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.gitRepositories.datasource.subassemblies - -import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent - -/** - * Builds the relay filters for the "Mine" repository selector: the user's own repository - * announcements, queried by author against their own outbox relays. Mirrors - * `filterNappletsMine` / `filterMusicEventsMine` — the only correct source for "my own" - * content, since the shared `TopFilter.Mine` flow falls back to all-follows. - */ -fun filterGitRepositoriesMine( - pubkey: HexKey, - relays: Set, - since: SincePerRelayMap?, -): List { - if (relays.isEmpty() || pubkey.isEmpty()) return emptyList() - val authors = listOf(pubkey) - return relays.map { relay -> - RelayBasedFilter( - relay = relay, - filter = - Filter( - kinds = listOf(GitRepositoryEvent.KIND), - authors = authors, - limit = 200, - since = since?.get(relay)?.time, - ), - ) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicPlaylistsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicPlaylistsFeedFilter.kt index 27f0139926..4c5c60bc1d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicPlaylistsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicPlaylistsFeedFilter.kt @@ -54,32 +54,12 @@ class MusicPlaylistsFeedFilter( override fun showHiddenKey(): Boolean = followList().wantsToSeeNegativeStuff() override fun feed(): List { - val notes = - if (followList() == TopFilter.Mine) { - val me = account.userProfile().pubkeyHex - LocalCache.addressables.filterIntoSet(MusicPlaylistEvent.KIND) { _, it -> isMine(it, me) } - } else { - val params = buildFilterParams(account) - LocalCache.addressables.filterIntoSet(MusicPlaylistEvent.KIND) { _, it -> accept(it, params) } - } + val params = buildFilterParams(account) + val notes = LocalCache.addressables.filterIntoSet(MusicPlaylistEvent.KIND) { _, it -> accept(it, params) } return sort(notes) } - override fun applyFilter(newItems: Set): Set { - if (followList() == TopFilter.Mine) { - val me = account.userProfile().pubkeyHex - return newItems.filterTo(HashSet()) { isMine(it, me) } - } - return innerApplyFilter(newItems) - } - - private fun isMine( - note: Note, - me: String, - ): Boolean { - val noteEvent = note.event - return noteEvent is MusicPlaylistEvent && noteEvent.pubKey == me - } + override fun applyFilter(newItems: Set): Set = innerApplyFilter(newItems) fun buildFilterParams(account: Account): FilterByListParams = FilterByListParams.create( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicTracksFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicTracksFeedFilter.kt index 1be4565825..3bf7cab6cc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicTracksFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicTracksFeedFilter.kt @@ -55,32 +55,12 @@ class MusicTracksFeedFilter( override fun showHiddenKey(): Boolean = followList().wantsToSeeNegativeStuff() override fun feed(): List { - val notes = - if (followList() == TopFilter.Mine) { - val me = account.userProfile().pubkeyHex - LocalCache.addressables.filterIntoSet(MusicTrackEvent.KIND) { _, it -> isMine(it, me) } - } else { - val params = buildFilterParams(account) - LocalCache.addressables.filterIntoSet(MusicTrackEvent.KIND) { _, it -> accept(it, params) } - } + val params = buildFilterParams(account) + val notes = LocalCache.addressables.filterIntoSet(MusicTrackEvent.KIND) { _, it -> accept(it, params) } return sort(notes) } - override fun applyFilter(newItems: Set): Set { - if (followList() == TopFilter.Mine) { - val me = account.userProfile().pubkeyHex - return newItems.filterTo(HashSet()) { isMine(it, me) } - } - return innerApplyFilter(newItems) - } - - private fun isMine( - note: Note, - me: String, - ): Boolean { - val noteEvent = note.event - return noteEvent is MusicTrackEvent && noteEvent.pubKey == me - } + override fun applyFilter(newItems: Set): Set = innerApplyFilter(newItems) fun buildFilterParams(account: Account): FilterByListParams = FilterByListParams.create( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/MusicPlaylistsSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/MusicPlaylistsSubAssembler.kt index ece543191c..bf6fadb434 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/MusicPlaylistsSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/MusicPlaylistsSubAssembler.kt @@ -24,8 +24,6 @@ import com.vitorpamplona.amethyst.model.TopFilter import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource.subassemblies.MUSIC_PLAYLIST_KINDS -import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource.subassemblies.filterMusicEventsMine import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription @@ -54,12 +52,8 @@ class MusicPlaylistsSubAssembler( key: MusicPlaylistsQueryState, since: SincePerRelayMap?, ): List { - // "Mine" bypasses the follow-list machinery: query the user's own playlists by author - // against their outbox relays (same pattern as badges/communities). - if (key.listName() == TopFilter.Mine) { - val outbox = key.account.outboxRelays.flow.value - return filterMusicEventsMine(key.account.userProfile().pubkeyHex, MUSIC_PLAYLIST_KINDS, outbox, since) - } + // "Mine" needs no special-case: the shared TopFilter.Mine flow now resolves to an author + // filter scoped to the user, so followsPerRelay() already carries authors=[me]. val feedSettings = key.followsPerRelay() // REQ now only asks for kind 34139 (playlists), keyed to this screen's follow // list selector — no cross-feed cursor min needed. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/MusicTracksSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/MusicTracksSubAssembler.kt index a6d1acbcec..7fe7db28cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/MusicTracksSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/MusicTracksSubAssembler.kt @@ -24,8 +24,6 @@ import com.vitorpamplona.amethyst.model.TopFilter import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource.subassemblies.MUSIC_TRACK_KINDS -import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource.subassemblies.filterMusicEventsMine import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription @@ -44,12 +42,8 @@ class MusicTracksSubAssembler( key: MusicTracksQueryState, since: SincePerRelayMap?, ): List { - // "Mine" bypasses the follow-list machinery: query the user's own tracks by author - // against their outbox relays (same pattern as badges/communities). - if (key.listName() == TopFilter.Mine) { - val outbox = key.account.outboxRelays.flow.value - return filterMusicEventsMine(key.account.userProfile().pubkeyHex, MUSIC_TRACK_KINDS, outbox, since) - } + // "Mine" needs no special-case: the shared TopFilter.Mine flow now resolves to an author + // filter scoped to the user, so followsPerRelay() already carries authors=[me]. val feedSettings = key.followsPerRelay() // REQ now only asks for kind 36787 (tracks), so the `since` cursor lines up with // the tracks feed alone — no cross-feed min needed. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/subassemblies/FilterMusicEventsMine.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/subassemblies/FilterMusicEventsMine.kt deleted file mode 100644 index c1bb44495c..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/subassemblies/FilterMusicEventsMine.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.music.datasource.subassemblies - -import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl - -/** - * Builds the relay filters for the "Mine" music selector: the user's own tracks/playlists, - * queried by author against their outbox relays. Mirrors `filterBadgesMine` / - * `filterCommunitiesMine`. The `kinds` list scopes it to tracks (36787) or playlists (34139). - */ -fun filterMusicEventsMine( - pubkey: HexKey, - kinds: List, - relays: Set, - since: SincePerRelayMap?, -): List { - if (relays.isEmpty() || pubkey.isEmpty()) return emptyList() - val authors = listOf(pubkey) - return relays.map { relay -> - RelayBasedFilter( - relay = relay, - filter = - Filter( - kinds = kinds, - authors = authors, - limit = 200, - since = since?.get(relay)?.time, - ), - ) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/NappletsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/NappletsScreen.kt index f4a9b4be84..6774fafbf1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/NappletsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/NappletsScreen.kt @@ -39,7 +39,6 @@ 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.model.TopFilter import com.vitorpamplona.amethyst.napplet.NappletLauncher import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.NoteCompose @@ -77,17 +76,14 @@ fun NappletsScreen( val followFilter by accountViewModel.account.liveNappletsFollowLists .collectAsStateWithLifecycle() - val listName by accountViewModel.account.settings.defaultNappletsFollowList - .collectAsStateWithLifecycle() - val myPubkey = accountViewModel.account.userProfile().pubkeyHex val visible = - remember(napplets, followFilter, listName, myPubkey) { + remember(napplets, followFilter) { napplets.filter { note -> val author = note.event?.pubKey ?: return@filter false - // "Mine" matches the user's own napplets; the shared author-matcher resolves Mine to - // all-follows (see the SubAssembler), so it can't be used for the Mine case here. - if (listName == TopFilter.Mine) author == myPubkey else followFilter.matchAuthor(author) + // Covers "Mine" too: the shared author-matcher now resolves Mine to the user's own + // pubkey, so matchAuthor already narrows to the user. + followFilter.matchAuthor(author) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/datasource/NappletsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/datasource/NappletsFilterSubAssembler.kt index 99a8bf02df..4fd0cb09a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/datasource/NappletsFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/datasource/NappletsFilterSubAssembler.kt @@ -24,7 +24,6 @@ import com.vitorpamplona.amethyst.model.TopFilter import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource.subassemblies.filterNappletsMine import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription @@ -51,12 +50,8 @@ class NappletsFilterSubAssembler( key: NappletsQueryState, since: SincePerRelayMap?, ): List { - // "Mine" bypasses the follow-list machinery: query the user's own napplets by author against - // their outbox relays (same pattern as badges/music). The shared TopFilter.Mine flow falls - // back to all-follows, so it can't be used here. - if (key.listName() == TopFilter.Mine) { - return filterNappletsMine(key.account.userProfile().pubkeyHex, key.account.outboxRelays.flow.value, since) - } + // "Mine" needs no special-case: the shared TopFilter.Mine flow now resolves to an author + // filter scoped to the user, so followsPerRelay() already carries authors=[me]. return makeNappletsFilter(key.followsPerRelay(), since) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/datasource/subassemblies/FilterNappletsMine.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/datasource/subassemblies/FilterNappletsMine.kt deleted file mode 100644 index 8c08162c15..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/datasource/subassemblies/FilterNappletsMine.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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.subassemblies - -import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource.NAPPLET_PAGE_LIMIT -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent -import com.vitorpamplona.quartz.nip5dNapplets.RootNappletEvent - -/** - * Builds the relay filters for the "Mine" napplet selector: the user's own napplet manifests, - * queried by author against their own outbox relays. Mirrors `filterBadgesMine` / - * `filterMusicEventsMine` — the only correct source for "my own" content, since the shared - * `TopFilter.Mine` flow falls back to all-follows. - */ -fun filterNappletsMine( - pubkey: HexKey, - relays: Set, - since: SincePerRelayMap?, -): List { - if (relays.isEmpty() || pubkey.isEmpty()) return emptyList() - val authors = listOf(pubkey) - return relays.map { relay -> - RelayBasedFilter( - relay = relay, - filter = - Filter( - kinds = listOf(RootNappletEvent.KIND, NamedNappletEvent.KIND), - authors = authors, - limit = NAPPLET_PAGE_LIMIT, - since = since?.get(relay)?.time, - ), - ) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nsites/NsitesScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nsites/NsitesScreen.kt index 6206cc5bfa..81d6e77f5d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nsites/NsitesScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nsites/NsitesScreen.kt @@ -39,7 +39,6 @@ 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.model.TopFilter import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -75,17 +74,14 @@ fun NsitesScreen( val followFilter by accountViewModel.account.liveNsitesFollowLists .collectAsStateWithLifecycle() - val listName by accountViewModel.account.settings.defaultNsitesFollowList - .collectAsStateWithLifecycle() - val myPubkey = accountViewModel.account.userProfile().pubkeyHex val visible = - remember(nsites, followFilter, listName, myPubkey) { + remember(nsites, followFilter) { nsites.filter { note -> val author = note.event?.pubKey ?: return@filter false - // "Mine" matches the user's own sites; the shared author-matcher resolves Mine to - // all-follows (see the SubAssembler), so it can't be used for the Mine case here. - if (listName == TopFilter.Mine) author == myPubkey else followFilter.matchAuthor(author) + // Covers "Mine" too: the shared author-matcher now resolves Mine to the user's own + // pubkey, so matchAuthor already narrows to the user. + followFilter.matchAuthor(author) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nsites/datasource/NsitesFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nsites/datasource/NsitesFilterSubAssembler.kt index 0d62658980..40cf038347 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nsites/datasource/NsitesFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nsites/datasource/NsitesFilterSubAssembler.kt @@ -24,7 +24,6 @@ import com.vitorpamplona.amethyst.model.TopFilter import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.amethyst.ui.screen.loggedIn.nsites.datasource.subassemblies.filterNsitesMine import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription @@ -51,12 +50,8 @@ class NsitesFilterSubAssembler( key: NsitesQueryState, since: SincePerRelayMap?, ): List { - // "Mine" bypasses the follow-list machinery: query the user's own nSites by author against - // their outbox relays (same pattern as badges/music). The shared TopFilter.Mine flow falls - // back to all-follows, so it can't be used here. - if (key.listName() == TopFilter.Mine) { - return filterNsitesMine(key.account.userProfile().pubkeyHex, key.account.outboxRelays.flow.value, since) - } + // "Mine" needs no special-case: the shared TopFilter.Mine flow now resolves to an author + // filter scoped to the user, so followsPerRelay() already carries authors=[me]. return makeNsitesFilter(key.followsPerRelay(), since) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nsites/datasource/subassemblies/FilterNsitesMine.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nsites/datasource/subassemblies/FilterNsitesMine.kt deleted file mode 100644 index f4436e0c2a..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nsites/datasource/subassemblies/FilterNsitesMine.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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.subassemblies - -import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.amethyst.ui.screen.loggedIn.nsites.datasource.NSITE_PAGE_LIMIT -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent -import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent - -/** - * Builds the relay filters for the "Mine" nSite selector: the user's own static-site manifests, - * queried by author against their own outbox relays. Mirrors `filterBadgesMine` / - * `filterMusicEventsMine` — the only correct source for "my own" content, since the shared - * `TopFilter.Mine` flow falls back to all-follows. - */ -fun filterNsitesMine( - pubkey: HexKey, - relays: Set, - since: SincePerRelayMap?, -): List { - if (relays.isEmpty() || pubkey.isEmpty()) return emptyList() - val authors = listOf(pubkey) - return relays.map { relay -> - RelayBasedFilter( - relay = relay, - filter = - Filter( - kinds = listOf(RootSiteEvent.KIND, NamedSiteEvent.KIND), - authors = authors, - limit = NSITE_PAGE_LIMIT, - since = since?.get(relay)?.time, - ), - ) - } -} From 3a368195396748225184b20df7fe7e0a0292b07d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Jun 2026 21:28:11 +0000 Subject: [PATCH 2/2] feat: scope "Mine" relay set to outbox + local + private + proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mine should download the user's own events from their outbox (NIP-65 write), private-storage, local and proxy relays. The previous filterXMine path used account.outboxRelays, which also included broadcast relays — write-only blast targets that don't serve reads — so it queried the wrong set. Add AccountMineRelayState (a sibling of AccountOutboxRelayState with broadcast swapped for proxy) and feed it into the shared TopFilter.Mine resolver. MineFeedFlow now pins authors=[me] to that fixed relay union via AuthorsByProxyTopNavFilter — no per-author outbox resolution needed, since the only author is the user and their relays are known from account state. Both the DAL (liveXFollowLists) and the relay sub-assemblers (liveXFollowListsPerRelay) consume it, and the per-relay flow re-emits when the mine relay set changes. Add MineFeedFlowTest covering author scoping, the empty-relay case, and re-emission on relay-set change. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01YWUwciqZiXTkbYVSyiF2P4 --- .../vitorpamplona/amethyst/model/Account.kt | 3 + .../AccountMineRelayState.kt | 65 ++++++++++++++ .../topNavFeeds/FeedTopNavFilterState.kt | 3 +- .../model/topNavFeeds/mine/MineFeedFlow.kt | 46 +++++----- .../topNavFeeds/mine/MineFeedFlowTest.kt | 87 +++++++++++++++++++ 5 files changed, 177 insertions(+), 27 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountMineRelayState.kt create mode 100644 amethyst/src/test/java/com/vitorpamplona/amethyst/model/topNavFeeds/mine/MineFeedFlowTest.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 98db5ad4ff..2a5f8b5d84 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.model.localRelays.ForwardKind0ToLocalRelayStat import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.marmot.KeyPackageRelayListState import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountHomeRelayState +import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountMineRelayState import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountOutboxRelayState import com.vitorpamplona.amethyst.model.nip01UserMetadata.NotificationInboxRelayState import com.vitorpamplona.amethyst.model.nip01UserMetadata.UserMetadataState @@ -416,6 +417,7 @@ class Account( // Relay settings val homeRelays = AccountHomeRelayState(nip65RelayList, privateStorageRelayList, localRelayList, scope) val outboxRelays = AccountOutboxRelayState(nip65RelayList, privateStorageRelayList, localRelayList, broadcastRelayList, scope) + val mineRelays = AccountMineRelayState(nip65RelayList, privateStorageRelayList, localRelayList, proxyRelayList, scope) val dmRelays = DmInboxRelayState(dmRelayList, nip65RelayList, privateStorageRelayList, localRelayList, scope) val notificationRelays = NotificationInboxRelayState(nip65RelayList, localRelayList, scope) @@ -508,6 +510,7 @@ class Account( followsRelays = defaultGlobalRelays.flow, blockedRelays = blockedRelayList.flow, proxyRelays = proxyRelayList.flow, + mineRelays = mineRelays.flow, relayFeeds = relayFeedsList.flow, caches = feedDecryptionCaches, signer = signer, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountMineRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountMineRelayState.kt new file mode 100644 index 0000000000..7205249d3b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountMineRelayState.kt @@ -0,0 +1,65 @@ +/* + * 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.model.nip01UserMetadata + +import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState +import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState +import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.stateIn + +/** + * The set of relays to read the user's *own* events from for the "Mine" top-nav selection: + * the user's NIP-65 outbox, their private-storage relays, their local relays and their proxy + * relays. Deliberately mirrors [AccountOutboxRelayState] **minus broadcast**: broadcast relays are + * write-only blast targets, so reading the user's own content back from them is wrong — they don't + * serve reads and would only waste a subscription. + */ +class AccountMineRelayState( + nip65: Nip65RelayListState, + privateStorage: PrivateStorageRelayListState, + local: LocalRelayListState, + proxy: ProxyRelayListState, + scope: CoroutineScope, +) { + val flow = + combine( + nip65.outboxFlow, + privateStorage.flow, + local.flow, + proxy.flow, + ) { nip65Outbox, privateOutBox, localRelays, proxyRelays -> + nip65Outbox + privateOutBox + localRelays + proxyRelays + }.flowOn(Dispatchers.IO) + .stateIn( + scope, + SharingStarted.Eagerly, + nip65.outboxFlow.value + + privateStorage.flow.value + + local.flow.value + + proxy.flow.value, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt index 00e9aa2fd5..1758c14a8a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt @@ -63,6 +63,7 @@ class FeedTopNavFilterState( val followsRelays: StateFlow>, val blockedRelays: StateFlow>, val proxyRelays: StateFlow>, + val mineRelays: StateFlow>, val relayFeeds: StateFlow>, val caches: FeedDecryptionCaches, val signer: NostrSigner, @@ -94,7 +95,7 @@ class FeedTopNavFilterState( } TopFilter.Mine -> { - MineFeedFlow(signer.pubKey, blockedRelays, proxyRelays) + MineFeedFlow(signer.pubKey, mineRelays) } is TopFilter.Community, is TopFilter.PeopleList, is TopFilter.MuteList -> { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/mine/MineFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/mine/MineFeedFlow.kt index dd8f0b6818..694366f9b8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/mine/MineFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/mine/MineFeedFlow.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.model.topNavFeeds.mine import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByProxyTopNavFilter import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -33,38 +32,33 @@ import kotlinx.coroutines.flow.map /** * Resolves the "Mine" top-nav selection to an author filter scoped to the logged-in user's own - * pubkey. Mirrors [com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsFeedFlow]'s - * outbox/proxy split: in proxy mode it pins the author to the configured proxy relays, otherwise it - * resolves the user's own NIP-65 outbox via OutboxRelayLoader. + * pubkey, pinned to the user's own relays — their outbox, private-storage, local and proxy relays + * (see [com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountMineRelayState]). Unlike the + * follow filters, "Mine" doesn't need per-author outbox resolution from cache: the only author is + * the user, and the user's relays are already known from their own account state — so we pin the + * fixed set directly via [AuthorsByProxyTopNavFilter] (it associates each given relay with the + * authors, which is exactly "query my relays for my events"). * - * This is the single source of truth for "Mine". Both the relay sub-assemblers (through each screen's - * `liveXFollowListsPerRelay`) and the local DAL filters (through `liveXFollowLists`) consume the - * produced [AuthorsByOutboxTopNavFilter] / [AuthorsByProxyTopNavFilter], so screens no longer need a - * dedicated `TopFilter.Mine` branch: the generic author path already narrows to the user. It also - * means outbox changes re-invalidate automatically — the per-relay flow is an `OutboxLoaderState` - * over the user's own outbox, so a NIP-65 update re-emits without a screen-specific trigger. + * This is the single source of truth for "Mine". Both the relay sub-assemblers (through each + * screen's `liveXFollowListsPerRelay`) and the local DAL filters (through `liveXFollowLists`) + * consume the produced [AuthorsByProxyTopNavFilter], so screens no longer need a dedicated + * `TopFilter.Mine` branch: the generic author path already narrows to the user. It also makes + * relay-set changes re-invalidate automatically — `liveXFollowListsPerRelay` re-emits whenever + * [mineRelays] changes, through the sub-assemblers' existing `followsPerRelayFlow` collector. */ class MineFeedFlow( val myPubkey: HexKey, - val blockedRelays: StateFlow>, - val proxyRelays: StateFlow>, + val mineRelays: StateFlow>, ) : IFeedFlowsType { - fun convert(proxyRelays: Set): IFeedTopNavFilter = - if (proxyRelays.isEmpty()) { - AuthorsByOutboxTopNavFilter( - authors = setOf(myPubkey), - blockedRelays = blockedRelays, - ) - } else { - AuthorsByProxyTopNavFilter( - authors = setOf(myPubkey), - proxyRelays = proxyRelays, - ) - } + fun convert(relays: Set): IFeedTopNavFilter = + AuthorsByProxyTopNavFilter( + authors = setOf(myPubkey), + proxyRelays = relays, + ) - override fun flow(): Flow = proxyRelays.map(::convert) + override fun flow(): Flow = mineRelays.map(::convert) - override fun startValue(): IFeedTopNavFilter = convert(proxyRelays.value) + override fun startValue(): IFeedTopNavFilter = convert(mineRelays.value) override suspend fun startValue(collector: FlowCollector) { collector.emit(startValue()) diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/topNavFeeds/mine/MineFeedFlowTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/topNavFeeds/mine/MineFeedFlowTest.kt new file mode 100644 index 0000000000..a072649b6c --- /dev/null +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/topNavFeeds/mine/MineFeedFlowTest.kt @@ -0,0 +1,87 @@ +/* + * 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.model.topNavFeeds.mine + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByProxyTopNavFilter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class MineFeedFlowTest { + private val me = "00".repeat(32) + private val someoneIFollow = "11".repeat(32) + + private val outbox = NormalizedRelayUrl("wss://outbox.example.com/") + private val local = NormalizedRelayUrl("ws://127.0.0.1:4869/") + private val proxy = NormalizedRelayUrl("wss://proxy.example.com/") + + @Test + fun resolvesToAuthorFilterScopedToSelf_pinnedToTheGivenRelays() { + val relays = setOf(outbox, local, proxy) + val flow = MineFeedFlow(myPubkey = me, mineRelays = MutableStateFlow(relays)) + + val filter = flow.startValue() + + assertTrue(filter is AuthorsByProxyTopNavFilter) + filter as AuthorsByProxyTopNavFilter + + // Author scoped to the user; rejects a follow's posts. + assertEquals(setOf(me), filter.authors) + assertTrue(filter.matchAuthor(me)) + assertFalse(filter.matchAuthor(someoneIFollow)) + + // Every "mine" relay is queried for the user's own events (no broadcast in this set). + assertEquals(relays, filter.proxyRelays) + } + + @Test + fun emptyRelaySet_stillScopesAuthorButQueriesNothing() { + val flow = MineFeedFlow(myPubkey = me, mineRelays = MutableStateFlow(emptySet())) + + val filter = flow.startValue() as AuthorsByProxyTopNavFilter + + // DAL still narrows to the user from cache even when no relays are configured. + assertTrue(filter.matchAuthor(me)) + assertFalse(filter.matchAuthor(someoneIFollow)) + assertTrue(filter.proxyRelays.isEmpty()) + } + + @Test + fun flow_reEmits_whenMineRelaysChange() = + runTest { + val relays = MutableStateFlow(setOf(outbox)) + val flow = MineFeedFlow(myPubkey = me, mineRelays = relays) + + val before = flow.flow().first() as AuthorsByProxyTopNavFilter + assertEquals(setOf(outbox), before.proxyRelays) + + relays.value = setOf(outbox, local, proxy) + val after = flow.flow().first() as AuthorsByProxyTopNavFilter + assertEquals(setOf(outbox, local, proxy), after.proxyRelays) + assertTrue(after.matchAuthor(me)) + assertFalse(after.matchAuthor(someoneIFollow)) + } +}