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 78d7f2e7e..7952ff5c2 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 @@ -491,6 +491,29 @@ class DesktopLocalCache : ICacheProvider { eventStream.emitDeletedNotes(notes) } + // ----- Profile count cache ----- + + private val followerCounts = ConcurrentHashMap() + private val followingCounts = ConcurrentHashMap() + + fun getCachedFollowerCount(pubkey: HexKey): Int = followerCounts[pubkey] ?: 0 + + fun getCachedFollowingCount(pubkey: HexKey): Int = followingCounts[pubkey] ?: 0 + + fun cacheFollowerCount( + pubkey: HexKey, + count: Int, + ) { + followerCounts[pubkey] = count + } + + fun cacheFollowingCount( + pubkey: HexKey, + count: Int, + ) { + followingCounts[pubkey] = count + } + // ----- Stats ----- fun userCount(): Int = users.size() @@ -503,6 +526,8 @@ class DesktopLocalCache : ICacheProvider { addressableNotes.clear() deletedEvents.clear() _followedUsers.value = emptySet() + followerCounts.clear() + followingCounts.clear() } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt index 80a9919cf..32ea06258 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt @@ -127,12 +127,22 @@ fun UserProfileScreen( val relayStatuses by relayManager.relayStatuses.collectAsState() val connectedRelays = remember(relayStatuses) { relayStatuses.keys } - // User metadata - var displayName by remember { mutableStateOf(null) } - var about by remember { mutableStateOf(null) } - var picture by remember { mutableStateOf(null) } - var followersCount by remember { mutableStateOf(0) } - var followingCount by remember { mutableStateOf(0) } + // User metadata — seed from cache so returning to profile is instant + val cachedUser = remember(pubKeyHex) { localCache.getUserIfExists(pubKeyHex) } + val cachedMetadata = remember(pubKeyHex) { cachedUser?.metadataOrNull() } + var displayName by remember { mutableStateOf(cachedMetadata?.bestName()) } + var about by remember { + mutableStateOf( + cachedMetadata + ?.flow + ?.value + ?.info + ?.about, + ) + } + var picture by remember { mutableStateOf(cachedMetadata?.profilePicture()) } + var followersCount by remember { mutableStateOf(localCache.getCachedFollowerCount(pubKeyHex)) } + var followingCount by remember { mutableStateOf(localCache.getCachedFollowingCount(pubKeyHex)) } // Profile editing state (only for own profile) val isOwnProfile = account != null && pubKeyHex == account.pubKeyHex @@ -279,8 +289,9 @@ fun UserProfileScreen( pubKeyHex = pubKeyHex, onEvent = { event, _, _, _ -> if (event is ContactListEvent) { - // Count the number of people this user follows - followingCount = event.verifiedFollowKeySet().size + val count = event.verifiedFollowKeySet().size + followingCount = count + localCache.cacheFollowingCount(pubKeyHex, count) } }, onEose = { _, _ -> }, @@ -314,7 +325,9 @@ fun UserProfileScreen( onEvent = { event, _, _, _ -> // Count unique authors who follow this user if (followerAuthors.add(event.pubKey)) { - followersCount = followerAuthors.size + val count = followerAuthors.size + followersCount = count + localCache.cacheFollowerCount(pubKeyHex, count) } }, onEose = { _, _ -> }, diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopCachePipelineTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopCachePipelineTest.kt index 9c92e54f3..e3f2e77ea 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopCachePipelineTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopCachePipelineTest.kt @@ -568,4 +568,69 @@ class DesktopCachePipelineTest { assertEquals(1, filtered.size, "applyFilter should only include followed users") assertEquals(followedPubKey, filtered.first().author?.pubkeyHex) } + + // ----------------------------------------------------------------------- + // 10. Profile count caching + // ----------------------------------------------------------------------- + + @Test + fun `profile follower count is cached and survives clear of note cache`() { + val cache = DesktopLocalCache() + + assertEquals(0, cache.getCachedFollowerCount(userPubKey)) + + cache.cacheFollowerCount(userPubKey, 42) + assertEquals(42, cache.getCachedFollowerCount(userPubKey)) + + // Updating again overwrites + cache.cacheFollowerCount(userPubKey, 100) + assertEquals(100, cache.getCachedFollowerCount(userPubKey)) + } + + @Test + fun `profile following count is cached`() { + val cache = DesktopLocalCache() + + cache.cacheFollowingCount(userPubKey, 150) + assertEquals(150, cache.getCachedFollowingCount(userPubKey)) + } + + @Test + fun `clear resets profile count caches`() { + val cache = DesktopLocalCache() + cache.cacheFollowerCount(userPubKey, 42) + cache.cacheFollowingCount(userPubKey, 150) + + cache.clear() + + assertEquals(0, cache.getCachedFollowerCount(userPubKey)) + assertEquals(0, cache.getCachedFollowingCount(userPubKey)) + } + + @Test + fun `metadata is available from cache after consumption`() { + val cache = DesktopLocalCache() + val metadata = + com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent( + id = "meta1".padEnd(64, '0'), + pubKey = userPubKey, + createdAt = System.currentTimeMillis() / 1000, + tags = emptyArray(), + content = """{"name":"TestUser","display_name":"Test User","about":"A test user"}""", + sig = dummySig, + ) + + cache.consume(metadata, relayUrl) + + val user = cache.getUserIfExists(userPubKey)!! + val cached = user.metadataOrNull() + assertTrue(cached != null, "Metadata should be cached after consumption") + assertEquals("Test User", cached.bestName()) + assertEquals( + "A test user", + cached.flow.value + ?.info + ?.about, + ) + } }