Replace first hack with a better solution for determining a follow.

Add FollowSetState to MergedFollowListsState and modify it to take into account users from follow sets when displaying a user's follows feed.
This commit is contained in:
KotlinGeekDev
2025-09-26 12:24:42 +01:00
parent 058278956b
commit 9c024e334b
3 changed files with 42 additions and 31 deletions

View File

@@ -20,22 +20,25 @@
*/
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.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class FollowSetState(
@@ -44,20 +47,24 @@ class FollowSetState(
val scope: CoroutineScope,
) {
val user = cache.getOrCreateUser(signer.pubKey)
private val isActive = MutableStateFlow(false)
suspend fun getFollowSetNotes() =
withContext(Dispatchers.Default) {
val followSetNotes = LocalCache.getFollowSetNotesFor(user)
Log.d(this.javaClass.simpleName, "Number of follow sets: ${followSetNotes.size}")
Log.d(this@FollowSetState.javaClass.simpleName, "Number of follow sets: ${followSetNotes.size}")
return@withContext followSetNotes
}
private fun getFollowSetNotesFlow() =
flow {
while (isActive.value) {
val followSetNotes = getFollowSetNotes()
val followSets = followSetNotes.map { mapNoteToFollowSet(it) }
emit(followSets)
}.flowOn(Dispatchers.IO)
delay(1000)
}
}.flowOn(Dispatchers.Default)
val profilesFlow =
getFollowSetNotesFlow()
@@ -65,27 +72,25 @@ class FollowSetState(
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,
)
fun isUserInFollowSets(user: User): Boolean = profilesFlow.value.contains(user.pubkeyHex)
init {
isActive.update { true }
scope.launch(Dispatchers.Default) {
getFollowSetNotesFlow()
.onCompletion {
isActive.update { false }
}.catch {
Log.e(this@FollowSetState.javaClass.simpleName, "Error on flow collection: ${it.message}")
isActive.update { false }
}.collect {}
}
}
}

View File

@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.model.serverList
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSetState
import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState
import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListState
import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState
@@ -37,6 +38,7 @@ import kotlinx.coroutines.flow.stateIn
class MergedFollowListsState(
val kind3List: FollowListState,
val followSetList: FollowSetState,
val hashtagList: HashtagListState,
val geohashList: GeohashListState,
val communityList: CommunityListState,
@@ -44,12 +46,13 @@ class MergedFollowListsState(
) {
fun mergeLists(
kind3: FollowListState.Kind3Follows,
followSetProfiles: Set<String>,
hashtags: Set<String>,
geohashes: Set<String>,
community: Set<CommunityTag>,
): FollowListState.Kind3Follows =
FollowListState.Kind3Follows(
kind3.authors,
kind3.authors + followSetProfiles,
kind3.authorsPlusMe,
kind3.hashtags + hashtags,
kind3.geotags + geohashes,
@@ -59,15 +62,17 @@ class MergedFollowListsState(
val flow: StateFlow<FollowListState.Kind3Follows> =
combine(
kind3List.flow,
followSetList.profilesFlow,
hashtagList.flow,
geohashList.flow,
communityList.flow,
) { kind3, hashtag, geohash, community ->
mergeLists(kind3, hashtag, geohash, community)
) { kind3, followSet, hashtag, geohash, community ->
mergeLists(kind3, followSet, hashtag, geohash, community)
}.onStart {
emit(
mergeLists(
kind3List.flow.value,
followSetList.profilesFlow.value,
hashtagList.flow.value,
geohashList.flow.value,
communityList.flow.value,

View File

@@ -411,6 +411,8 @@ fun observeUserIsFollowing(
): State<Boolean> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user1, accountViewModel)
val isUserInFollowSets = accountViewModel.account.followSetsState.isUserInFollowSets(user2)
println("Is ${user2.toBestDisplayName()} in a Follow set? $isUserInFollowSets")
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
@@ -420,14 +422,13 @@ fun observeUserIsFollowing(
.follows.stateFlow
.sample(1000)
.mapLatest { userState ->
userState.user.isFollowing(user2) ||
accountViewModel.account.isUserInFollowSets(user2)
userState.user.isFollowing(user2) || isUserInFollowSets
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
}
return flow.collectAsStateWithLifecycle(
user1.isFollowing(user2) || accountViewModel.account.isUserInFollowSets(user2),
user1.isFollowing(user2) || isUserInFollowSets,
)
}