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)
|
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 -----
|
// ----- Stats -----
|
||||||
|
|
||||||
fun userCount(): Int = users.size()
|
fun userCount(): Int = users.size()
|
||||||
@@ -503,6 +526,8 @@ class DesktopLocalCache : ICacheProvider {
|
|||||||
addressableNotes.clear()
|
addressableNotes.clear()
|
||||||
deletedEvents.clear()
|
deletedEvents.clear()
|
||||||
_followedUsers.value = emptySet()
|
_followedUsers.value = emptySet()
|
||||||
|
followerCounts.clear()
|
||||||
|
followingCounts.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-9
@@ -127,12 +127,22 @@ fun UserProfileScreen(
|
|||||||
val relayStatuses by relayManager.relayStatuses.collectAsState()
|
val relayStatuses by relayManager.relayStatuses.collectAsState()
|
||||||
val connectedRelays = remember(relayStatuses) { relayStatuses.keys }
|
val connectedRelays = remember(relayStatuses) { relayStatuses.keys }
|
||||||
|
|
||||||
// User metadata
|
// User metadata — seed from cache so returning to profile is instant
|
||||||
var displayName by remember { mutableStateOf<String?>(null) }
|
val cachedUser = remember(pubKeyHex) { localCache.getUserIfExists(pubKeyHex) }
|
||||||
var about by remember { mutableStateOf<String?>(null) }
|
val cachedMetadata = remember(pubKeyHex) { cachedUser?.metadataOrNull() }
|
||||||
var picture by remember { mutableStateOf<String?>(null) }
|
var displayName by remember { mutableStateOf(cachedMetadata?.bestName()) }
|
||||||
var followersCount by remember { mutableStateOf(0) }
|
var about by remember {
|
||||||
var followingCount by remember { mutableStateOf(0) }
|
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)
|
// Profile editing state (only for own profile)
|
||||||
val isOwnProfile = account != null && pubKeyHex == account.pubKeyHex
|
val isOwnProfile = account != null && pubKeyHex == account.pubKeyHex
|
||||||
@@ -279,8 +289,9 @@ fun UserProfileScreen(
|
|||||||
pubKeyHex = pubKeyHex,
|
pubKeyHex = pubKeyHex,
|
||||||
onEvent = { event, _, _, _ ->
|
onEvent = { event, _, _, _ ->
|
||||||
if (event is ContactListEvent) {
|
if (event is ContactListEvent) {
|
||||||
// Count the number of people this user follows
|
val count = event.verifiedFollowKeySet().size
|
||||||
followingCount = event.verifiedFollowKeySet().size
|
followingCount = count
|
||||||
|
localCache.cacheFollowingCount(pubKeyHex, count)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEose = { _, _ -> },
|
onEose = { _, _ -> },
|
||||||
@@ -314,7 +325,9 @@ fun UserProfileScreen(
|
|||||||
onEvent = { event, _, _, _ ->
|
onEvent = { event, _, _, _ ->
|
||||||
// Count unique authors who follow this user
|
// Count unique authors who follow this user
|
||||||
if (followerAuthors.add(event.pubKey)) {
|
if (followerAuthors.add(event.pubKey)) {
|
||||||
followersCount = followerAuthors.size
|
val count = followerAuthors.size
|
||||||
|
followersCount = count
|
||||||
|
localCache.cacheFollowerCount(pubKeyHex, count)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEose = { _, _ -> },
|
onEose = { _, _ -> },
|
||||||
|
|||||||
Vendored
+65
@@ -568,4 +568,69 @@ class DesktopCachePipelineTest {
|
|||||||
assertEquals(1, filtered.size, "applyFilter should only include followed users")
|
assertEquals(1, filtered.size, "applyFilter should only include followed users")
|
||||||
assertEquals(followedPubKey, filtered.first().author?.pubkeyHex)
|
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