- Removes the deprecated hacks from Contact Lists

- Removes the use of kind 3 to show hashtags which fixes issues when people unfollowing the hashtag and it would just affect the InterestListEvent
This commit is contained in:
Vitor Pamplona
2025-10-22 18:31:00 -04:00
parent 2d551d4adf
commit ee80361478
11 changed files with 69 additions and 86 deletions
@@ -38,8 +38,8 @@ import com.vitorpamplona.amethyst.model.nip01UserMetadata.UserMetadataState
import com.vitorpamplona.amethyst.model.nip02FollowLists.DeclaredFollowsPerOutboxRelay
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxOrProxyRelays
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListReusedOutboxOrProxyRelays
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowsPerOutboxRelay
import com.vitorpamplona.amethyst.model.nip02FollowLists.Kind3FollowListState
import com.vitorpamplona.amethyst.model.nip03Timestamp.OtsState
import com.vitorpamplona.amethyst.model.nip17Dms.DmInboxRelayState
import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState
@@ -266,7 +266,7 @@ class Account(
val blockedRelayListDecryptionCache = BlockedRelayListDecryptionCache(signer)
val blockedRelayList = BlockedRelayListState(signer, cache, blockedRelayListDecryptionCache, scope, settings)
val kind3FollowList = FollowListState(signer, cache, scope, settings)
val kind3FollowList = Kind3FollowListState(signer, cache, scope, settings)
val followSetsState = FollowSetState(signer, cache, scope)
val ephemeralChatListDecryptionCache = EphemeralChatListDecryptionCache(signer)
@@ -37,7 +37,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
class DeclaredFollowsPerOutboxRelay(
kind3Follows: FollowListState,
kind3Follows: Kind3FollowListState,
val cache: LocalCache,
scope: CoroutineScope,
) {
@@ -42,7 +42,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
class FollowListOutboxOrProxyRelays(
kind3Follows: FollowListState,
kind3Follows: Kind3FollowListState,
blockedRelayList: BlockedRelayListState,
proxyRelayList: ProxyRelayListState,
val cache: LocalCache,
@@ -42,7 +42,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
class FollowListReusedOutboxOrProxyRelays(
kind3Follows: FollowListState,
kind3Follows: Kind3FollowListState,
blockedRelayList: BlockedRelayListState,
proxyRelayList: ProxyRelayListState,
val cache: LocalCache,
@@ -42,7 +42,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
class FollowsPerOutboxRelay(
kind3Follows: FollowListState,
kind3Follows: Kind3FollowListState,
blockedRelayList: BlockedRelayListState,
proxyRelayList: ProxyRelayListState,
val cache: LocalCache,
@@ -25,12 +25,10 @@ import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
@@ -45,7 +43,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
class FollowListState(
class Kind3FollowListState(
val signer: NostrSigner,
val cache: LocalCache,
val scope: CoroutineScope,
@@ -77,8 +75,8 @@ class FollowListState(
// Creates a long-term reference for all follows of a user
val userList =
flow
.map {
it.authors.mapNotNull {
.map { kind3Follows ->
kind3Follows.authors.mapNotNull {
cache.checkGetOrCreateUser(it)
}
}.flowOn(Dispatchers.IO)
@@ -96,15 +94,9 @@ class FollowListState(
*/
@Immutable
class Kind3Follows(
val authors: Set<String> = emptySet(),
val authorsPlusMe: Set<String>,
val hashtags: Set<String> = emptySet(),
val geotags: Set<String> = emptySet(),
val communities: Set<String> = emptySet(),
) {
val geotagScopes: Set<String> = geotags.mapTo(mutableSetOf<String>()) { GeohashId.toScope(it) }
val hashtagScopes: Set<String> = hashtags.mapTo(mutableSetOf<String>()) { HashtagId.toScope(it) }
}
val authors: Set<HexKey> = emptySet(),
val authorsPlusMe: Set<HexKey>,
)
fun buildKind3Follows(latestContactList: ContactListEvent?): Kind3Follows {
// makes sure the output include only valid p tags
@@ -113,19 +105,6 @@ class FollowListState(
return Kind3Follows(
authors = verifiedFollowingUsers,
authorsPlusMe = verifiedFollowingUsers + signer.pubKey,
hashtags =
latestContactList
?.unverifiedFollowTagSet()
?.map { it.lowercase() }
?.toSet() ?: emptySet(),
geotags =
latestContactList
?.geohashes()
?.toSet() ?: emptySet(),
communities =
latestContactList
?.verifiedFollowAddressSet()
?.toSet() ?: emptySet(),
)
}
@@ -20,12 +20,15 @@
*/
package com.vitorpamplona.amethyst.model.serverList
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.nip02FollowLists.Kind3FollowListState
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
import com.vitorpamplona.quartz.nip72ModCommunities.follow.tags.CommunityTag
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
@@ -37,38 +40,50 @@ import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.flow.stateIn
class MergedFollowListsState(
val kind3List: FollowListState,
val kind3List: Kind3FollowListState,
val followSetList: FollowSetState,
val hashtagList: HashtagListState,
val geohashList: GeohashListState,
val communityList: CommunityListState,
val scope: CoroutineScope,
) {
/**
This contains a big OR of everything the user wants to see in the a single feed.
*/
@Immutable
class AllFollows(
val authors: Set<String> = emptySet(),
val hashtags: Set<String> = emptySet(),
val geotags: Set<String> = emptySet(),
val communities: Set<String> = emptySet(),
) {
val geotagScopes: Set<String> = geotags.mapTo(mutableSetOf<String>()) { GeohashId.toScope(it) }
val hashtagScopes: Set<String> = hashtags.mapTo(mutableSetOf<String>()) { HashtagId.toScope(it) }
}
fun mergeLists(
kind3: FollowListState.Kind3Follows,
kind3: Kind3FollowListState.Kind3Follows,
followSetProfiles: Set<String>,
hashtags: Set<String>,
geohashes: Set<String>,
community: Set<CommunityTag>,
): FollowListState.Kind3Follows =
FollowListState.Kind3Follows(
kind3.authors + followSetProfiles,
kind3.authorsPlusMe,
kind3.hashtags + hashtags,
kind3.geotags + geohashes,
kind3.communities + community.map { it.address.toValue() },
): AllFollows =
AllFollows(
authors = kind3.authors + followSetProfiles,
hashtags = hashtags,
geotags = geohashes,
communities = community.mapTo(mutableSetOf()) { it.address.toValue() },
)
val flow: StateFlow<FollowListState.Kind3Follows> =
val flow: StateFlow<AllFollows> =
combine(
kind3List.flow,
followSetList.profilesFlow,
hashtagList.flow,
geohashList.flow,
communityList.flow,
) { kind3, followSet, hashtag, geohash, community ->
mergeLists(kind3, followSet, hashtag, geohash, community)
}.onStart {
::mergeLists,
).onStart {
emit(
mergeLists(
kind3List.flow.value,
@@ -83,6 +98,12 @@ class MergedFollowListsState(
.stateIn(
scope,
SharingStarted.Eagerly,
kind3List.flow.value,
mergeLists(
kind3List.flow.value,
followSetList.profilesFlow.value,
hashtagList.flow.value,
geohashList.flow.value,
communityList.flow.value,
),
)
}
@@ -25,7 +25,7 @@ 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.serverList.MergedFollowListsState
import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsFeedFlow
import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsFeedFlow
import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.AroundMeFeedFlow
@@ -49,7 +49,7 @@ import kotlinx.coroutines.flow.transformLatest
class FeedTopNavFilterState(
val feedFilterListName: MutableStateFlow<String>,
val allFollows: StateFlow<FollowListState.Kind3Follows>,
val allFollows: StateFlow<MergedFollowListsState.AllFollows>,
val locationFlow: StateFlow<LocationState.LocationResult>,
val followsRelays: StateFlow<Set<NormalizedRelayUrl>>,
val blockedRelays: StateFlow<Set<NormalizedRelayUrl>>,
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState
import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -29,31 +29,31 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
class AllFollowsFeedFlow(
val allFollows: StateFlow<FollowListState.Kind3Follows?>,
val allFollows: StateFlow<MergedFollowListsState.AllFollows?>,
val followsRelays: StateFlow<Set<NormalizedRelayUrl>>,
val blockedRelays: StateFlow<Set<NormalizedRelayUrl>>,
val proxyRelays: StateFlow<Set<NormalizedRelayUrl>>,
) : IFeedFlowsType {
fun convert(
kind3: FollowListState.Kind3Follows?,
allFollows: MergedFollowListsState.AllFollows?,
proxyRelays: Set<NormalizedRelayUrl>,
): IFeedTopNavFilter =
if (kind3 != null) {
if (allFollows != null) {
if (proxyRelays.isEmpty()) {
AllFollowsByOutboxTopNavFilter(
authors = kind3.authors,
hashtags = kind3.hashtags,
geotags = kind3.geotags,
communities = kind3.communities,
authors = allFollows.authors,
hashtags = allFollows.hashtags,
geotags = allFollows.geotags,
communities = allFollows.communities,
defaultRelays = followsRelays,
blockedRelays = blockedRelays,
)
} else {
AllFollowsByProxyTopNavFilter(
authors = kind3.authors,
hashtags = kind3.hashtags,
geotags = kind3.geotags,
communities = kind3.communities,
authors = allFollows.authors,
hashtags = allFollows.hashtags,
geotags = allFollows.geotags,
communities = allFollows.communities,
proxyRelays = proxyRelays,
)
}
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState
import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -29,25 +29,25 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
class AllUserFollowsFeedFlow(
val allFollows: StateFlow<FollowListState.Kind3Follows?>,
val allFollows: StateFlow<MergedFollowListsState.AllFollows?>,
val followsRelays: StateFlow<Set<NormalizedRelayUrl>>,
val blockedRelays: StateFlow<Set<NormalizedRelayUrl>>,
val proxyRelays: StateFlow<Set<NormalizedRelayUrl>>,
) : IFeedFlowsType {
fun convert(
kind3: FollowListState.Kind3Follows?,
allFollows: MergedFollowListsState.AllFollows?,
proxyRelays: Set<NormalizedRelayUrl>,
): IFeedTopNavFilter =
if (kind3 != null) {
if (allFollows != null) {
if (proxyRelays.isEmpty()) {
AllUserFollowsByOutboxTopNavFilter(
authors = kind3.authors,
authors = allFollows.authors,
defaultRelays = followsRelays,
blockedRelays = blockedRelays,
)
} else {
AllUserFollowsByProxyTopNavFilter(
authors = kind3.authors,
authors = allFollows.authors,
proxyRelays = proxyRelays,
)
}
@@ -23,14 +23,11 @@ package com.vitorpamplona.quartz.nip02FollowList
import androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag
import com.vitorpamplona.quartz.nip31Alts.AltTag
@@ -45,12 +42,7 @@ class ContactListEvent(
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
AddressHintProvider,
PubKeyHintProvider {
override fun addressHints() = tags.mapNotNull(ATag::parseAsHint)
override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId)
override fun pubKeyHints() = tags.mapNotNull(ContactTag::parseAsHint)
override fun linkedPubKeys() = tags.mapNotNull(ContactTag::parseKey)
@@ -60,17 +52,8 @@ class ContactListEvent(
*/
fun verifiedFollowKeySet(): Set<HexKey> = tags.mapNotNullTo(mutableSetOf(), ContactTag::parseValidKey)
/**
* Returns a list of a-tags that are verified as correct.
*/
@Deprecated("Use CommunityListEvent instead.")
fun verifiedFollowAddressSet(): Set<HexKey> = tags.mapNotNullTo(mutableSetOf(), ATag::parseValidAddress)
fun unverifiedFollowKeySet() = tags.mapNotNull(ContactTag::parseKey)
@Deprecated("Use HashtagListEvent instead.")
fun unverifiedFollowTagSet() = tags.hashtags()
fun follows() = tags.mapNotNull(ContactTag::parseValid)
fun relays(): Map<NormalizedRelayUrl, ReadWrite>? {