diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 31b5b9f12d..fd3b47abf1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -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 = 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> = + cache + .observeEvents( + 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt index 94aef17a82..f44b20c33b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt @@ -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 } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/apps/recommendations/ProfileAppRecommendationsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/apps/recommendations/ProfileAppRecommendationsScreen.kt index 917f48c7a4..5bd22acc76 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/apps/recommendations/ProfileAppRecommendationsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/apps/recommendations/ProfileAppRecommendationsScreen.kt @@ -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