refactor: expose my 31989s as a LocalCache-observed StateFlow on Account

Account.myAppRecommendations is now LocalCache.observeEvents (indexed
kind+author filter) stated in the account scope, seeded with a
synchronous scan so the editor's first frame still sorts correctly.
The editor and the Recommend button collect it instead of rescanning
the cache on every newEventBundles emission; isAppRecommended is gone.
The synchronous scan stays only as the seed and for the mutex-guarded
read-modify-write publishers, which must read current cache truth.

https://claude.ai/code/session_015dX5vWqvXUYD8rzPYX8vTB
This commit is contained in:
Claude
2026-06-12 15:29:30 +00:00
parent eed490f579
commit cbe4b030e1
3 changed files with 30 additions and 35 deletions
@@ -1620,17 +1620,27 @@ class Account(
*/
private val appRecommendationsMutex = Mutex()
/**
* Synchronous cache scan, used to seed [myAppRecommendations] and by the
* read-modify-write publishers below, which must read current truth from
* the cache while holding [appRecommendationsMutex].
*/
fun myAppRecommendationEvents(): List<AppRecommendationEvent> =
cache.addressables
.filterIntoSet(AppRecommendationEvent.KIND, signer.pubKey)
.mapNotNull { it.event as? AppRecommendationEvent }
fun isAppRecommended(app: AppDefinitionEvent): Boolean {
val address = app.addressTag()
return myAppRecommendationEvents().any { event ->
event.recommendationAddresses().any { it == address }
}
}
/**
* My kind 31989 recommendation events (one per handled kind), kept in
* sync as the cache consumes new versions. UI should collect this
* instead of rescanning the cache on every event bundle.
*/
val myAppRecommendations: StateFlow<List<AppRecommendationEvent>> =
cache
.observeEvents<AppRecommendationEvent>(
Filter(kinds = listOf(AppRecommendationEvent.KIND), authors = listOf(signer.pubKey)),
).flowOn(Dispatchers.IO)
.stateIn(scope, SharingStarted.WhileSubscribed(30000), myAppRecommendationEvents())
/**
* Returns a createdAt strictly greater than whatever AppRecommendationEvent
@@ -52,7 +52,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
@@ -69,12 +68,12 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
@@ -97,7 +96,6 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip89AppHandlers.PlatformType
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppMetadata
import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -431,16 +429,13 @@ private fun RecommendAppButton(
note: Note,
accountViewModel: AccountViewModel,
) {
val myPubkey = accountViewModel.userProfile().pubkeyHex
val myRecommendations by accountViewModel.account.myAppRecommendations.collectAsStateWithLifecycle()
val isRecommended by
produceState(initialValue = false, key1 = noteEvent) {
value = withContext(Dispatchers.IO) { accountViewModel.account.isAppRecommended(noteEvent) }
LocalCache.live.newEventBundles.collect { bundle ->
val touchesMine = bundle.any { (it.event as? AppRecommendationEvent)?.pubKey == myPubkey }
if (touchesMine) {
value = withContext(Dispatchers.IO) { accountViewModel.account.isAppRecommended(noteEvent) }
}
val isRecommended =
remember(myRecommendations, noteEvent) {
val address = noteEvent.addressTag()
myRecommendations.any { event ->
event.recommendationAddresses().any { it == address }
}
}
@@ -68,7 +68,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.apps.recommendations.dataso
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kindDisplayName
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -83,31 +82,22 @@ fun ProfileAppRecommendationsScreen(
// relays so the list below has candidates while this screen is open.
ProfileAppRecommendationsFilterAssemblerSubscription(accountViewModel)
// Ticks whenever LocalCache emits a bundle that touches my recommendations
// or any app definition, so the snapshots below recompute.
var recommendationsTick by remember { mutableIntStateOf(0) }
// Ticks whenever LocalCache emits a bundle with a new app definition, so
// the candidate snapshot below recomputes.
var appDefinitionsTick by remember { mutableIntStateOf(0) }
LaunchedEffect(myPubkey) {
launch(Dispatchers.IO) {
LocalCache.live.newEventBundles.collect { bundle ->
var newRecommendations = false
var newDefinitions = false
bundle.forEach { note ->
val event = note.event
if (event is AppRecommendationEvent && event.pubKey == myPubkey) newRecommendations = true
if (event is AppDefinitionEvent) newDefinitions = true
}
if (newRecommendations) recommendationsTick++
if (newDefinitions) appDefinitionsTick++
if (bundle.any { it.event is AppDefinitionEvent }) appDefinitionsTick++
}
}
}
val myRecommendationEvents by accountViewModel.account.myAppRecommendations.collectAsStateWithLifecycle()
val recommendedAddresses =
remember(recommendationsTick) {
accountViewModel.account
.myAppRecommendationEvents()
.flatMapTo(mutableSetOf()) { event -> event.recommendations().map { it.address } }
remember(myRecommendationEvents) {
myRecommendationEvents.flatMapTo(mutableSetOf()) { event -> event.recommendations().map { it.address } }
}
// The recommended set used for ORDERING only. It tracks recommendedAddresses