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 05abfd8e37..fbeddd2fb3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Stable -import androidx.compose.ui.util.fastAny import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.commons.richtext.RichTextParser @@ -58,6 +57,7 @@ import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayLis import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSetState import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache @@ -94,7 +94,6 @@ import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler import com.vitorpamplona.amethyst.service.uploads.FileHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.EventProcessor -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSet import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent @@ -217,8 +216,6 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext import java.math.BigDecimal import java.util.Locale import kotlin.coroutines.cancellation.CancellationException @@ -270,6 +267,7 @@ class Account( val blockedRelayList = BlockedRelayListState(signer, cache, blockedRelayListDecryptionCache, scope, settings) val kind3FollowList = FollowListState(signer, cache, scope, settings) + val followSetsState = FollowSetState(signer, cache, scope) val ephemeralChatListDecryptionCache = EphemeralChatListDecryptionCache(signer) val ephemeralChatList = EphemeralChatListState(signer, cache, ephemeralChatListDecryptionCache, scope, settings) @@ -322,7 +320,7 @@ class Account( val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, proxyRelayList, cache, scope).flow // Merges all follow lists to create a single All Follows feed. - val allFollows = MergedFollowListsState(kind3FollowList, hashtagList, geohashList, communityList, scope) + val allFollows = MergedFollowListsState(kind3FollowList, followSetsState, hashtagList, geohashList, communityList, scope) val privateDMDecryptionCache = PrivateDMCache(signer) val privateZapsDecryptionCache = PrivateZapCache(signer) @@ -834,37 +832,6 @@ class Account( fun upgradeAttestations() = otsState.upgradeAttestationsIfNeeded(::sendAutomatic) - suspend fun getFollowSetNotes() = - withContext(Dispatchers.Default) { - val followSetNotes = LocalCache.getFollowSetNotesFor(userProfile()) - Log.d(this@Account.javaClass.simpleName, "Number of follow sets: ${followSetNotes.size}") - return@withContext followSetNotes - } - - fun isUserInFollowSets(user: User): Boolean = - runBlocking(scope.coroutineContext) { - LocalCache.getFollowSetNotesFor(userProfile()).fastAny { it -> - val listEvent = it.event as PeopleListEvent - val isInPublicSets = - listEvent - .publicPeople() - .fastAny { it.toTagArray().value() == user.pubkeyHex } - val isInPrivateSets = - listEvent - .privatePeople(signer) - ?.fastAny { it.toTagArray().value() == user.pubkeyHex } ?: false - - isInPublicSets || isInPrivateSets - } - } - - fun mapNoteToFollowSet(note: Note): FollowSet = - FollowSet - .mapEventToSet( - event = note.event as PeopleListEvent, - signer, - ) - suspend fun follow(user: User) = sendMyPublicAndPrivateOutbox(kind3FollowList.follow(user)) suspend fun unfollow(user: User) = sendMyPublicAndPrivateOutbox(kind3FollowList.unfollow(user)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt new file mode 100644 index 0000000000..85977a43c7 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetState.kt @@ -0,0 +1,91 @@ +/** + * 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.nip51Lists.followSets + +import androidx.compose.ui.util.fastAny +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.quartz.nip01Core.core.value +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.utils.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext + +class FollowSetState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, +) { + val user = cache.getOrCreateUser(signer.pubKey) + + suspend fun getFollowSetNotes() = + withContext(Dispatchers.Default) { + val followSetNotes = LocalCache.getFollowSetNotesFor(user) + Log.d(this.javaClass.simpleName, "Number of follow sets: ${followSetNotes.size}") + return@withContext followSetNotes + } + + private fun getFollowSetNotesFlow() = + flow { + val followSetNotes = getFollowSetNotes() + val followSets = followSetNotes.map { mapNoteToFollowSet(it) } + emit(followSets) + }.flowOn(Dispatchers.IO) + + val profilesFlow = + getFollowSetNotesFlow() + .map { it -> + it.flatMapTo(mutableSetOf()) { it.profiles }.toSet() + }.stateIn(scope, SharingStarted.Eagerly, emptySet()) + + fun isUserInFollowSets(user: User): Boolean = + runBlocking(scope.coroutineContext) { + LocalCache.getFollowSetNotesFor(user).fastAny { it -> + val listEvent = it.event as PeopleListEvent + val isInPublicSets = + listEvent + .publicPeople() + .fastAny { it.toTagArray().value() == user.pubkeyHex } + val isInPrivateSets = + listEvent + .privatePeople(signer) + ?.fastAny { it.toTagArray().value() == user.pubkeyHex } ?: false + + isInPublicSets || isInPrivateSets + } + } + + fun mapNoteToFollowSet(note: Note): FollowSet = + FollowSet + .mapEventToSet( + event = note.event as PeopleListEvent, + signer, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FollowSetFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FollowSetFeedFilter.kt index 04eea40ce5..7ca7271e1d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FollowSetFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FollowSetFeedFilter.kt @@ -21,20 +21,20 @@ package com.vitorpamplona.amethyst.ui.dal import android.util.Log -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSet +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSetState import kotlinx.coroutines.runBlocking class FollowSetFeedFilter( - val account: Account, + val followSetState: FollowSetState, ) : FeedFilter() { - override fun feedKey(): String = account.userProfile().pubkeyHex + "-followsets" + override fun feedKey(): String = followSetState.user.pubkeyHex + "-followsets" override fun feed(): List = - runBlocking(account.scope.coroutineContext) { + runBlocking(followSetState.scope.coroutineContext) { try { - val fetchedSets = account.getFollowSetNotes() - val followSets = fetchedSets.map { account.mapNoteToFollowSet(it) } + val fetchedSets = followSetState.getFollowSetNotes() + val followSets = fetchedSets.map { followSetState.mapNoteToFollowSet(it) } println("Updated follow set size for feed filter: ${followSets.size}") followSets } catch (e: Exception) {