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:
nrobi144
2026-03-23 14:28:40 +02:00
parent 3e41bab5d5
commit f2169e0236
3 changed files with 112 additions and 9 deletions
@@ -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()
}
}
@@ -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 = { _, _ -> },