From da469754c42b32c200715a84f4427fa393978812 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 12 Sep 2025 16:18:12 -0400 Subject: [PATCH] Adds a All User Follows to fix: https://github.com/vitorpamplona/amethyst/issues/1431 --- .../amethyst/model/AccountSettings.kt | 3 + .../topNavFeeds/FeedTopNavFilterState.kt | 3 + .../AllUserFollowsByOutboxTopNavFilter.kt | 94 +++++++++++++++++++ .../AllUserFollowsByProxyTopNavFilter.kt | 84 +++++++++++++++++ .../allUserFollows/AllUserFollowsFeedFlow.kt | 69 ++++++++++++++ .../amethyst/ui/screen/FollowListState.kt | 16 +++- amethyst/src/main/res/values/strings.xml | 1 + 7 files changed, 267 insertions(+), 3 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByOutboxTopNavFilter.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByProxyTopNavFilter.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsFeedFlow.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index 3c3268fd7..c23628065 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -97,6 +97,9 @@ val GLOBAL_FOLLOWS = " Global " // This has spaces to avoid mixing with a potential NIP-51 list with the same name. val ALL_FOLLOWS = " All Follows " +// This has spaces to avoid mixing with a potential NIP-51 list with the same name. +val ALL_USER_FOLLOWS = " All User Follows " + // This has spaces to avoid mixing with a potential NIP-51 list with the same name. val AROUND_ME = " Around Me " 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 a825fed96..efc97e01c 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 @@ -21,11 +21,13 @@ package com.vitorpamplona.amethyst.model.topNavFeeds import com.vitorpamplona.amethyst.model.ALL_FOLLOWS +import com.vitorpamplona.amethyst.model.ALL_USER_FOLLOWS import com.vitorpamplona.amethyst.model.AROUND_ME import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsFeedFlow +import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsFeedFlow import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.AroundMeFeedFlow import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalFeedFlow import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.NoteFeedFlow @@ -60,6 +62,7 @@ class FeedTopNavFilterState( when (listName) { GLOBAL_FOLLOWS -> GlobalFeedFlow(followsRelays, proxyRelays) ALL_FOLLOWS -> AllFollowsFeedFlow(allFollows, followsRelays, blockedRelays, proxyRelays) + ALL_USER_FOLLOWS -> AllUserFollowsFeedFlow(allFollows, followsRelays, blockedRelays, proxyRelays) AROUND_ME -> AroundMeFeedFlow(locationFlow, followsRelays, proxyRelays) else -> { val note = LocalCache.checkGetOrCreateAddressableNote(listName) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByOutboxTopNavFilter.kt new file mode 100644 index 000000000..b3bf7538e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByOutboxTopNavFilter.kt @@ -0,0 +1,94 @@ +/** + * 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.allUserFollows + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine + +/** + * This is a big OR filter on all fields. + */ +@Immutable +class AllUserFollowsByOutboxTopNavFilter( + val authors: Set, + val defaultRelays: StateFlow>, + val blockedRelays: StateFlow>, +) : IFeedTopNavFilter { + override fun matchAuthor(pubkey: HexKey): Boolean = pubkey in authors + + override fun match(noteEvent: Event): Boolean = + when (noteEvent) { + is LiveActivitiesEvent -> { + noteEvent.participantsIntersect(authors) + } + + is CommentEvent -> { + // ignore follows and checks only the root scope + noteEvent.pubKey in authors + } + + else -> { + noteEvent.pubKey in authors + } + } + + override fun toPerRelayFlow(cache: LocalCache): Flow { + val authorsPerRelay = OutboxRelayLoader().toAuthorsPerRelayFlow(authors, cache) { it } + + return combine(authorsPerRelay, defaultRelays, blockedRelays) { perRelayAuthors, default, blockedRelays -> + val allRelays = perRelayAuthors.keys.filter { it !in blockedRelays }.ifEmpty { default } + + AuthorsTopNavPerRelayFilterSet( + allRelays.associateWith { + AuthorsTopNavPerRelayFilter( + authors = perRelayAuthors[it] ?: emptySet(), + ) + }, + ) + } + } + + override fun startValue(cache: LocalCache): AuthorsTopNavPerRelayFilterSet { + val authorsPerRelay = OutboxRelayLoader().authorsPerRelaySnapshot(authors, cache) { it } + + val allRelays = authorsPerRelay.keys.filter { it !in blockedRelays.value }.ifEmpty { defaultRelays.value } + + return AuthorsTopNavPerRelayFilterSet( + allRelays.associateWith { + AuthorsTopNavPerRelayFilter( + authors = authorsPerRelay[it] ?: emptySet(), + ) + }, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByProxyTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByProxyTopNavFilter.kt new file mode 100644 index 000000000..6064ce5b2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByProxyTopNavFilter.kt @@ -0,0 +1,84 @@ +/** + * 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.allUserFollows + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +/** + * This is a big OR filter on all fields. + */ +@Immutable +class AllUserFollowsByProxyTopNavFilter( + val authors: Set, + val proxyRelays: Set, +) : IFeedTopNavFilter { + override fun matchAuthor(pubkey: HexKey): Boolean = pubkey in authors + + override fun match(noteEvent: Event): Boolean = + when (noteEvent) { + is LiveActivitiesEvent -> { + noteEvent.participantsIntersect(authors) + } + + is CommentEvent -> { + // ignore follows and checks only the root scope + noteEvent.pubKey in authors + } + + else -> { + noteEvent.pubKey in authors + } + } + + // forces the use of the Proxy on all connections, replacing the outbox model. + override fun toPerRelayFlow(cache: LocalCache): Flow = + MutableStateFlow( + AuthorsTopNavPerRelayFilterSet( + proxyRelays.associateWith { + AuthorsTopNavPerRelayFilter( + authors = authors, + ) + }, + ), + ) + + override fun startValue(cache: LocalCache): AuthorsTopNavPerRelayFilterSet { + // forces the use of the Proxy on all connections, replacing the outbox model. + return AuthorsTopNavPerRelayFilterSet( + proxyRelays.associateWith { + AuthorsTopNavPerRelayFilter( + authors = authors, + ) + }, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsFeedFlow.kt new file mode 100644 index 000000000..9ad834025 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsFeedFlow.kt @@ -0,0 +1,69 @@ +/** + * 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.allUserFollows + +import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine + +class AllUserFollowsFeedFlow( + val allFollows: StateFlow, + val followsRelays: StateFlow>, + val blockedRelays: StateFlow>, + val proxyRelays: StateFlow>, +) : IFeedFlowsType { + fun convert( + kind3: FollowListState.Kind3Follows?, + proxyRelays: Set, + ): IFeedTopNavFilter = + if (kind3 != null) { + if (proxyRelays.isEmpty()) { + AllUserFollowsByOutboxTopNavFilter( + authors = kind3.authors, + defaultRelays = followsRelays, + blockedRelays = blockedRelays, + ) + } else { + AllUserFollowsByProxyTopNavFilter( + authors = kind3.authors, + proxyRelays = proxyRelays, + ) + } + } else { + AllUserFollowsByOutboxTopNavFilter( + authors = emptySet(), + defaultRelays = followsRelays, + blockedRelays = blockedRelays, + ) + } + + override fun flow() = combine(allFollows, proxyRelays, ::convert) + + override fun startValue(): IFeedTopNavFilter = convert(allFollows.value, proxyRelays.value) + + override suspend fun startValue(collector: FlowCollector) { + collector.emit(startValue()) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt index 361988be8..5ccdd707d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.ALL_FOLLOWS +import com.vitorpamplona.amethyst.model.ALL_USER_FOLLOWS import com.vitorpamplona.amethyst.model.AROUND_ME import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote @@ -82,6 +83,15 @@ class FollowListState( unpackList = listOf(ContactListEvent.blockListFor(account.signer.pubKey)), ) + val kind3FollowUsers = + PeopleListOutBoxFeedDefinition( + code = ALL_USER_FOLLOWS, + name = ResourceName(R.string.follow_list_kind3follows_users_only), + type = CodeNameType.HARDCODED, + kinds = DEFAULT_FEED_KINDS, + unpackList = listOf(ContactListEvent.blockListFor(account.signer.pubKey)), + ) + val globalFollow = GlobalFeedDefinition( code = GLOBAL_FOLLOWS, @@ -107,7 +117,7 @@ class FollowListState( unpackList = listOf(MuteListEvent.blockListFor(account.userProfile().pubkeyHex)), ) - val defaultLists = persistentListOf(kind3Follow, aroundMe, globalFollow, muteListFollow) + val defaultLists = persistentListOf(kind3Follow, kind3FollowUsers, aroundMe, globalFollow, muteListFollow) fun getPeopleLists(): List = account @@ -218,7 +228,7 @@ class FollowListState( checkNotInMainThread() emit( listOf( - listOf(kind3Follow, aroundMe, globalFollow), + listOf(kind3Follow, kind3FollowUsers, aroundMe, globalFollow), myLivePeopleListsFlow, myLiveKind3FollowsFlow, listOf(muteListFollow), @@ -234,7 +244,7 @@ class FollowListState( checkNotInMainThread() emit( listOf( - listOf(kind3Follow, aroundMe, globalFollow), + listOf(kind3Follow, kind3FollowUsers, aroundMe, globalFollow), myLivePeopleListsFlow, listOf(muteListFollow), ).flatten().toImmutableList(), diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 0c41605dc..338456739 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -502,6 +502,7 @@ Follow List All Follows + All User Follows Follows via Proxy Around Me Global