mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 16:14:40 +00:00
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:
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-17
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user