mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 09:13:23 +00:00
refactor: use indexed LocalCache.observeNotes for app version chip
Replace the global newEventBundles + full-cache rescan with an index-driven LocalCache.observeNotes(kind 30063 / author) observer, the established idiom (NestLobbyScreen, OpenPollsState, DvmContentDiscovery). The FilterIndex only wakes the observer when a matching release is inserted, instead of re-scanning the whole addressables map on every event app-wide. Extract the NIP-82 release parsing (dTag-prefix + kind-30063 collision handling) into shared helpers reused by findLatestNip82Release and findAllNip82Releases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EbCTsBoGtBCJ1rTKCcar6w
This commit is contained in:
@@ -43,6 +43,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.produceState
|
||||
@@ -59,6 +60,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
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.richtext.MediaUrlImage
|
||||
@@ -90,10 +92,13 @@ import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAss
|
||||
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.SoftwareReleaseEvent
|
||||
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.asSoftwareRelease
|
||||
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.isNip82SoftwareRelease
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
* NIP-82 kind 32267 — compact feed card. Renders icon, name, latest version
|
||||
@@ -188,65 +193,73 @@ fun RenderSoftwareApplication(
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the latest NIP-82 [SoftwareReleaseEvent] for [app] from
|
||||
* [LocalCache] and exposes the version string. Releases (kind 30063) are
|
||||
* separate events that point back to the app via an `i` tag, so they are not
|
||||
* indexed as replies to the app note and never ping its flows. We therefore
|
||||
* re-scan on every [LocalCache.live] new-event bundle so a newer release that
|
||||
* arrives while the card is visible updates the version chip without a manual
|
||||
* refresh.
|
||||
* Latest NIP-82 [SoftwareReleaseEvent] version for [app], kept live.
|
||||
*
|
||||
* Releases (kind 30063) are separate events that point back to the app via an
|
||||
* `i` tag rather than an `a` tag, so they are never indexed as replies to the
|
||||
* app note and never ping its flows. Instead we register an index-driven
|
||||
* [LocalCache.observeNotes] observer keyed on kind-30063 / author, so a newer
|
||||
* release arriving while the card is visible updates the version chip without
|
||||
* a manual refresh. The observer only wakes on matching insertions, so it is
|
||||
* far cheaper than re-scanning the cache on every new-event bundle.
|
||||
*/
|
||||
@Composable
|
||||
fun produceLatestReleaseVersion(app: SoftwareApplicationEvent) =
|
||||
produceState<String?>(initialValue = null, key1 = app.id) {
|
||||
value =
|
||||
withContext(Dispatchers.Default) {
|
||||
findLatestNip82Release(app)?.version()
|
||||
}
|
||||
|
||||
LocalCache.live.newEventBundles.collect {
|
||||
val newVersion =
|
||||
withContext(Dispatchers.Default) {
|
||||
findLatestNip82Release(app)?.version()
|
||||
}
|
||||
if (newVersion != value) {
|
||||
value = newVersion
|
||||
}
|
||||
fun produceLatestReleaseVersion(app: SoftwareApplicationEvent): State<String?> {
|
||||
val flow =
|
||||
remember(app.pubKey, app.id) {
|
||||
val filter =
|
||||
Filter(
|
||||
kinds = listOf(SoftwareReleaseEvent.KIND),
|
||||
authors = listOf(app.pubKey),
|
||||
)
|
||||
LocalCache
|
||||
.observeNotes(filter)
|
||||
.map { notes -> latestNip82Release(notes, app)?.version() }
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.Default)
|
||||
}
|
||||
}
|
||||
|
||||
fun findLatestNip82Release(app: SoftwareApplicationEvent): SoftwareReleaseEvent? {
|
||||
val prefix = "${app.dTag()}@"
|
||||
val notes =
|
||||
LocalCache.addressables.filterIntoSet(SoftwareReleaseEvent.KIND, app.pubKey) { _, addr ->
|
||||
val ev = addr.event ?: return@filterIntoSet false
|
||||
ev.isNip82SoftwareRelease() && ev.dTag().startsWith(prefix)
|
||||
}
|
||||
return notes
|
||||
.mapNotNull {
|
||||
when (val ev = it.event) {
|
||||
is SoftwareReleaseEvent -> ev
|
||||
null -> null
|
||||
else -> if (ev.isNip82SoftwareRelease()) ev.asSoftwareRelease() else null
|
||||
}
|
||||
}.maxByOrNull { it.createdAt }
|
||||
return flow.collectAsStateWithLifecycle(initialValue = null)
|
||||
}
|
||||
|
||||
fun findLatestNip82Release(app: SoftwareApplicationEvent): SoftwareReleaseEvent? = latestNip82Release(nip82ReleaseNotesFor(app), app)
|
||||
|
||||
/** Picks the newest NIP-82 release for [app] out of an already-narrowed [notes] collection. */
|
||||
private fun latestNip82Release(
|
||||
notes: Collection<Note>,
|
||||
app: SoftwareApplicationEvent,
|
||||
): SoftwareReleaseEvent? {
|
||||
val prefix = "${app.dTag()}@"
|
||||
return notes
|
||||
.mapNotNull { it.asNip82ReleaseFor(prefix) }
|
||||
.maxByOrNull { it.createdAt }
|
||||
}
|
||||
|
||||
/** kind-30063 addressables authored by [app] whose `d` tag is `<app-id>@<version>`. */
|
||||
private fun nip82ReleaseNotesFor(app: SoftwareApplicationEvent): Set<Note> {
|
||||
val prefix = "${app.dTag()}@"
|
||||
return LocalCache.addressables.filterIntoSet(SoftwareReleaseEvent.KIND, app.pubKey) { _, addr ->
|
||||
val ev = addr.event ?: return@filterIntoSet false
|
||||
ev.isNip82SoftwareRelease() && ev.dTag().startsWith(prefix)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-reads this note as the NIP-82 release for [prefix] (`<app-id>@`). kind 30063
|
||||
* collides with NIP-51 `ReleaseArtifactSetEvent`, which is what `EventFactory`
|
||||
* builds, so we re-parse matching tag arrays as the NIP-82 form.
|
||||
*/
|
||||
private fun Note.asNip82ReleaseFor(prefix: String): SoftwareReleaseEvent? =
|
||||
when (val ev = event) {
|
||||
null -> null
|
||||
is SoftwareReleaseEvent -> ev.takeIf { it.dTag().startsWith(prefix) }
|
||||
else -> if (ev.isNip82SoftwareRelease() && ev.dTag().startsWith(prefix)) ev.asSoftwareRelease() else null
|
||||
}
|
||||
|
||||
fun findAllNip82Releases(app: SoftwareApplicationEvent): List<SoftwareReleaseEvent> {
|
||||
val prefix = "${app.dTag()}@"
|
||||
val notes =
|
||||
LocalCache.addressables.filterIntoSet(SoftwareReleaseEvent.KIND, app.pubKey) { _, addr ->
|
||||
val ev = addr.event ?: return@filterIntoSet false
|
||||
ev.isNip82SoftwareRelease() && ev.dTag().startsWith(prefix)
|
||||
}
|
||||
return notes
|
||||
.mapNotNull {
|
||||
when (val ev = it.event) {
|
||||
is SoftwareReleaseEvent -> ev
|
||||
null -> null
|
||||
else -> if (ev.isNip82SoftwareRelease()) ev.asSoftwareRelease() else null
|
||||
}
|
||||
}.sortedByDescending { it.createdAt }
|
||||
return nip82ReleaseNotesFor(app)
|
||||
.mapNotNull { it.asNip82ReleaseFor(prefix) }
|
||||
.sortedByDescending { it.createdAt }
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
Reference in New Issue
Block a user