diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt index d32ba9093f..c8a7bd3b7d 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt @@ -66,6 +66,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import java.util.concurrent.ConcurrentHashMap @@ -91,6 +92,27 @@ class DesktopLocalCache : ICacheProvider { private val _followedUsers = MutableStateFlow>(emptySet()) val followedUsers: StateFlow> = _followedUsers.asStateFlow() + /** + * Active user's pubkey (hex). Set from Main.kt on login. When set, only + * kind-3 events from this pubkey update [_followedUsers] and + * [lastContactListEvent]. Other users' kind-3 events still flow through + * [contactListEvents] for consumers like the WoT service. + */ + @Volatile + var accountPubkey: HexKey? = null + + /** + * Fires for every accepted kind-3 event (both the active user's and + * other users' — filtered downstream). Buffered so slow consumers don't + * block the consume path. + */ + private val _contactListEvents = + MutableSharedFlow( + extraBufferCapacity = 64, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + val contactListEvents: SharedFlow = _contactListEvents.asSharedFlow() + /** Increments on each metadata update — observe to recompose when user names change. */ private val _metadataVersion = MutableStateFlow(0L) val metadataVersion: StateFlow = _metadataVersion.asStateFlow() @@ -472,19 +494,28 @@ class DesktopLocalCache : ICacheProvider { /** * Consumes a kind 3 contact list event (replaceable). - * Updates the cached followedUsers set. + * + * Tracks the newest kind-3 per author (not a single global scalar) so + * ingesting other users' follow lists (e.g. for WoT scoring) doesn't + * corrupt the active user's state. Only the active user's kind-3 updates + * [_followedUsers] / [lastContactListEvent]. Every accepted event fans + * out on [_contactListEvents] for downstream consumers. */ - private var lastContactListCreatedAt = 0L + private val lastContactListByAuthor = ConcurrentHashMap() var lastContactListEvent: ContactListEvent? = null private set private fun consumeContactList(event: ContactListEvent): Boolean { - // Replaceable event — only accept newer contact lists - if (event.createdAt <= lastContactListCreatedAt) return false - lastContactListCreatedAt = event.createdAt - lastContactListEvent = event - _followedUsers.value = event.verifiedFollowKeySet() + // Replaceable event — only accept newer contact lists per author + val prev = lastContactListByAuthor[event.pubKey] ?: 0L + if (event.createdAt <= prev) return false + lastContactListByAuthor[event.pubKey] = event.createdAt + + if (event.pubKey == accountPubkey) { + lastContactListEvent = event + _followedUsers.value = event.verifiedFollowKeySet() + } // Store in addressableNotes too — Kind3FollowListState.getFollowListEvent // reads from getOrCreateAddressableNote(...) and would otherwise see a @@ -493,6 +524,8 @@ class DesktopLocalCache : ICacheProvider { val addressableNote = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey) addressableNote.loadEvent(event, author, emptyList()) + + _contactListEvents.tryEmit(event) return true } @@ -786,7 +819,9 @@ class DesktopLocalCache : ICacheProvider { followerCounts.clear() followingCounts.clear() notesByAuthor.clear() - lastContactListCreatedAt = 0L + lastContactListByAuthor.clear() + lastContactListEvent = null + accountPubkey = null } }