feat(cache): cache profile metadata and follower/following counts
Profile screen now reads initial values from cache so navigating back shows data instantly instead of re-fetching everything from scratch. - Seed displayName/about/picture from User.metadataOrNull() on compose - Add followerCounts/followingCounts maps to DesktopLocalCache - Write counts on each update, read on profile open - Follower count still ticks up live (good UX) but starts from cached value - 4 new tests for profile count caching + metadata cache reads Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vendored
+25
@@ -491,6 +491,29 @@ class DesktopLocalCache : ICacheProvider {
|
||||
eventStream.emitDeletedNotes(notes)
|
||||
}
|
||||
|
||||
// ----- Profile count cache -----
|
||||
|
||||
private val followerCounts = ConcurrentHashMap<HexKey, Int>()
|
||||
private val followingCounts = ConcurrentHashMap<HexKey, Int>()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+22
-9
@@ -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<String?>(null) }
|
||||
var about by remember { mutableStateOf<String?>(null) }
|
||||
var picture by remember { mutableStateOf<String?>(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 = { _, _ -> },
|
||||
|
||||
Vendored
+65
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user