mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
feat: render Zapstore app curation sets (kind 30267)
- AppCurationSetEvent reads the `name` tag Zapstore uses instead of NIP-51's `title` (titleOrName, also used for NIP-50 indexing) and the NIP-82 `f` platform tags (platforms()). - The set card shows the name and platform chips, and lists each kind 32267 app as a compact row (icon, name, summary) that opens the app detail screen, instead of a full app card with screenshots per member. Rows show the app id until the app event loads. - ListCard gains an optional subtitle slot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011cHtfJhTUzHEgKXy7hQvmo
This commit is contained in:
@@ -95,7 +95,7 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`.
|
|||||||
| 30175 | PersonaEvent | buzz/apPersonas | `personaOrNull()?.let { listOfNotNull(it.displayName, it.systemPrompt).joinToString("\n") } ?: ""` |
|
| 30175 | PersonaEvent | buzz/apPersonas | `personaOrNull()?.let { listOfNotNull(it.displayName, it.systemPrompt).joinToString("\n") } ?: ""` |
|
||||||
| 30176 | TeamEvent | buzz/teams | `teamOrNull()?.let { listOfNotNull(it.name, it.description, it.instructions).joinToString("\n") } ?: ""` |
|
| 30176 | TeamEvent | buzz/teams | `teamOrNull()?.let { listOfNotNull(it.name, it.description, it.instructions).joinToString("\n") } ?: ""` |
|
||||||
| 30177 | ManagedAgentEvent | buzz/managedAgents | `agentOrNull()?.let { listOfNotNull(it.name, it.systemPrompt).joinToString("\n") } ?: ""` |
|
| 30177 | ManagedAgentEvent | buzz/managedAgents | `agentOrNull()?.let { listOfNotNull(it.name, it.systemPrompt).joinToString("\n") } ?: ""` |
|
||||||
| 30267 | AppCurationSetEvent | nip51Lists/appCurationSet | `listOfNotNull(title(), description())` NL |
|
| 30267 | AppCurationSetEvent | nip51Lists/appCurationSet | `listOfNotNull(titleOrName(), description())` NL |
|
||||||
| 30296 | InteractiveStoryPrologueEvent | experimental/interactiveStories | inherited base: `listOfNotNull(title(), summary(), content)` NL |
|
| 30296 | InteractiveStoryPrologueEvent | experimental/interactiveStories | inherited base: `listOfNotNull(title(), summary(), content)` NL |
|
||||||
| 30297 | InteractiveStorySceneEvent | experimental/interactiveStories | inherited base: `listOfNotNull(title(), summary(), content)` NL |
|
| 30297 | InteractiveStorySceneEvent | experimental/interactiveStories | inherited base: `listOfNotNull(title(), summary(), content)` NL |
|
||||||
| 30311 | LiveActivitiesEvent | nip53LiveActivities/streaming | `listOfNotNull(title(), summary(), content)` NL |
|
| 30311 | LiveActivitiesEvent | nip53LiveActivities/streaming | `listOfNotNull(title(), summary(), content)` NL |
|
||||||
|
|||||||
+109
-2
@@ -20,21 +20,48 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.commons.ui.note.types.lists
|
package com.vitorpamplona.amethyst.commons.ui.note.types.lists
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.key
|
import androidx.compose.runtime.key
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.commons.model.Note
|
import com.vitorpamplona.amethyst.commons.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||||
|
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteEvent
|
||||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||||
import com.vitorpamplona.amethyst.commons.resources.kind_app_curation_set
|
import com.vitorpamplona.amethyst.commons.resources.kind_app_curation_set
|
||||||
import com.vitorpamplona.amethyst.commons.resources.kind_favorite_algo_feeds
|
import com.vitorpamplona.amethyst.commons.resources.kind_favorite_algo_feeds
|
||||||
import com.vitorpamplona.amethyst.commons.resources.kind_git_repositories
|
import com.vitorpamplona.amethyst.commons.resources.kind_git_repositories
|
||||||
import com.vitorpamplona.amethyst.commons.resources.kind_simple_groups
|
import com.vitorpamplona.amethyst.commons.resources.kind_simple_groups
|
||||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.commons.ui.note.types.AppIcon
|
||||||
|
import com.vitorpamplona.amethyst.commons.ui.note.types.Chip
|
||||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||||
|
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||||
|
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||||
import com.vitorpamplona.quartz.nip51Lists.appCurationSet.AppCurationSetEvent
|
import com.vitorpamplona.quartz.nip51Lists.appCurationSet.AppCurationSetEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent
|
import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.gitRepositoryList.GitRepositoryListEvent
|
import com.vitorpamplona.quartz.nip51Lists.gitRepositoryList.GitRepositoryListEvent
|
||||||
@@ -94,7 +121,14 @@ fun RenderFavoriteAlgoFeedsList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** NIP-51 kind 30267: "references to multiple software applications". */
|
/**
|
||||||
|
* NIP-51 kind 30267: "references to multiple software applications".
|
||||||
|
*
|
||||||
|
* Zapstore names these with a `name` tag instead of `title`, and pins them to the platforms the
|
||||||
|
* apps were picked for with NIP-82 `f` tags. Each app is a compact row — icon, name, summary —
|
||||||
|
* rather than the full app card, which carries screenshots and a reactions row that would turn a
|
||||||
|
* twenty-app set into a wall.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun RenderAppCurationSet(
|
fun RenderAppCurationSet(
|
||||||
baseNote: Note,
|
baseNote: Note,
|
||||||
@@ -106,19 +140,92 @@ fun RenderAppCurationSet(
|
|||||||
val noteEvent = baseNote.event as? AppCurationSetEvent ?: return
|
val noteEvent = baseNote.event as? AppCurationSetEvent ?: return
|
||||||
|
|
||||||
val apps = remember(noteEvent) { noteEvent.apps() }
|
val apps = remember(noteEvent) { noteEvent.apps() }
|
||||||
|
val platforms = remember(noteEvent) { noteEvent.platforms() }
|
||||||
|
|
||||||
ListCard(
|
ListCard(
|
||||||
title = listTitle(noteEvent.title(), noteEvent.dTag(), Res.string.kind_app_curation_set),
|
title = listTitle(noteEvent.titleOrName(), noteEvent.dTag(), Res.string.kind_app_curation_set),
|
||||||
description = noteEvent.description(),
|
description = noteEvent.description(),
|
||||||
items = apps,
|
items = apps,
|
||||||
// Not a PrivateTagArrayEvent: an app set has no encrypted half to be locked out of.
|
// Not a PrivateTagArrayEvent: an app set has no encrypted half to be locked out of.
|
||||||
hasUnreadablePrivateItems = false,
|
hasUnreadablePrivateItems = false,
|
||||||
backgroundColor = backgroundColor,
|
backgroundColor = backgroundColor,
|
||||||
|
subtitle = if (platforms.isNotEmpty()) ({ PlatformChips(platforms) }) else null,
|
||||||
) { app ->
|
) { app ->
|
||||||
key(app.address.toValue()) {
|
key(app.address.toValue()) {
|
||||||
|
if (app.address.kind == SoftwareApplicationEvent.KIND) {
|
||||||
|
SoftwareAppMemberRow(app.address, accountViewModel, nav)
|
||||||
|
} else {
|
||||||
AddressMemberRow(app.address, quotesLeft, backgroundColor, accountViewModel, nav)
|
AddressMemberRow(app.address, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun PlatformChips(platforms: List<String>) {
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp, vertical = 4.dp),
|
||||||
|
) {
|
||||||
|
platforms.forEach { Chip(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A NIP-82 app (kind 32267) as one line of a set: its icon, name and summary, opening the app's
|
||||||
|
* detail screen. Until the app event arrives the row shows its app id, so the list keeps its
|
||||||
|
* shape instead of jumping as nineteen apps load one by one.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun SoftwareAppMemberRow(
|
||||||
|
address: Address,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
LoadAddressableNote(address) { note ->
|
||||||
|
val app = note?.let { observeNoteEvent<SoftwareApplicationEvent>(it, accountViewModel).value }
|
||||||
|
|
||||||
|
val name = app?.name()?.takeIf { it.isNotBlank() } ?: address.dTag
|
||||||
|
val summary = app?.summary()?.takeIf { it.isNotBlank() }
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { nav.nav(Route.SoftwareAppDetail(address)) }
|
||||||
|
.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||||
|
) {
|
||||||
|
AppIcon(icon = app?.icon(), name = name, sizeDp = 40)
|
||||||
|
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
color = if (app == null) MaterialTheme.colorScheme.grayText else Color.Unspecified,
|
||||||
|
)
|
||||||
|
summary?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.grayText,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+4
@@ -75,6 +75,8 @@ fun <T> ListCard(
|
|||||||
/** True when the event carries private members this viewer has no key for. */
|
/** True when the event carries private members this viewer has no key for. */
|
||||||
hasUnreadablePrivateItems: Boolean,
|
hasUnreadablePrivateItems: Boolean,
|
||||||
backgroundColor: MutableState<Color>,
|
backgroundColor: MutableState<Color>,
|
||||||
|
/** Extra lines under the description that only some list kinds carry, e.g. an app set's platforms. */
|
||||||
|
subtitle: (@Composable () -> Unit)? = null,
|
||||||
itemContent: @Composable (T) -> Unit,
|
itemContent: @Composable (T) -> Unit,
|
||||||
) {
|
) {
|
||||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||||
@@ -100,6 +102,8 @@ fun <T> ListCard(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subtitle?.invoke()
|
||||||
|
|
||||||
// Only when there is nothing else to show. A card with visible members already reads as a list,
|
// Only when there is nothing else to show. A card with visible members already reads as a list,
|
||||||
// and most lists keep something private, so saying so on every one of them is noise. The line
|
// and most lists keep something private, so saying so on every one of them is noise. The line
|
||||||
// exists for the case that would otherwise be a title and blank space — an all-private list,
|
// exists for the case that would otherwise be a title and blank space — an all-private list,
|
||||||
|
|||||||
+1
-1
@@ -391,7 +391,7 @@ object SearchFieldExtractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
is AppCurationSetEvent -> {
|
is AppCurationSetEvent -> {
|
||||||
tiers(event, event.title(), event.description(), null)
|
tiers(event, event.titleOrName(), event.description(), null)
|
||||||
}
|
}
|
||||||
|
|
||||||
is RelaySetEvent -> {
|
is RelaySetEvent -> {
|
||||||
|
|||||||
+12
-2
@@ -21,6 +21,7 @@
|
|||||||
package com.vitorpamplona.quartz.nip51Lists.appCurationSet
|
package com.vitorpamplona.quartz.nip51Lists.appCurationSet
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.shared.PlatformTag
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
@@ -36,6 +37,7 @@ import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
|
|||||||
import com.vitorpamplona.quartz.nip51Lists.remove
|
import com.vitorpamplona.quartz.nip51Lists.remove
|
||||||
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
|
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
|
||||||
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
|
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.tags.NameTag
|
||||||
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
|
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
@@ -52,12 +54,12 @@ class AppCurationSetEvent(
|
|||||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||||
AddressHintProvider,
|
AddressHintProvider,
|
||||||
SearchableEvent {
|
SearchableEvent {
|
||||||
override fun indexableContent() = listOfNotNull(title(), description()).joinToString("\n")
|
override fun indexableContent() = listOfNotNull(titleOrName(), description()).joinToString("\n")
|
||||||
|
|
||||||
// The read path: the same fields indexableContent() joins, handed over without
|
// The read path: the same fields indexableContent() joins, handed over without
|
||||||
// building the joined string a scan would throw away.
|
// building the joined string a scan would throw away.
|
||||||
override fun forEachIndexableField(visitor: IndexableFieldVisitor) {
|
override fun forEachIndexableField(visitor: IndexableFieldVisitor) {
|
||||||
if (!visitor.visit(title())) return
|
if (!visitor.visit(titleOrName())) return
|
||||||
visitor.visit(description())
|
visitor.visit(description())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,12 +69,20 @@ class AppCurationSetEvent(
|
|||||||
|
|
||||||
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
|
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
|
||||||
|
|
||||||
|
/** Zapstore names its sets with a `name` tag rather than NIP-51's `title`. */
|
||||||
|
fun name() = tags.firstNotNullOfOrNull(NameTag::parse)
|
||||||
|
|
||||||
|
fun titleOrName() = title() ?: name()
|
||||||
|
|
||||||
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
|
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
|
||||||
|
|
||||||
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
|
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
|
||||||
|
|
||||||
fun apps() = tags.mapNotNull(AddressBookmark::parse)
|
fun apps() = tags.mapNotNull(AddressBookmark::parse)
|
||||||
|
|
||||||
|
/** NIP-82 `f` tags: the platforms the curated apps were picked for (e.g. `android-arm64-v8a`). */
|
||||||
|
fun platforms() = tags.mapNotNull(PlatformTag::parse)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val KIND = 30267
|
const val KIND = 30267
|
||||||
|
|
||||||
|
|||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.quartz.nip51Lists.appCurationSet
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent
|
||||||
|
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.shared.Platform
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.crypto.verify
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertIs
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class AppCurationSetEventTest {
|
||||||
|
private val zapstoreGames =
|
||||||
|
"""
|
||||||
|
{"id":"0389c8b10882df52226b116745e65060561d5165abd842953d9a7bc466cae479","pubkey":"aa1f96f685d0ac3e28a52feb87a20399a91afb3ac3137afeb7698dfcc99bc454","created_at":1775252298,"kind":30267,"tags":[["name","Games"],["d","games"],["f","android-arm64-v8a"],["h","acfeaea6e51420e8068fac446ca9d17d7a9ef6a5d20d93894e50fee3d4902a84"],["a","32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:com.gitlab.ardash.appleflinger.android"],["a","32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:com.adilhanney.ricochlime"],["a","32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:net.minetest.minetest"],["a","32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:de.rainerhock.eightbitwonders"],["a","32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:org.moire.opensudoku"],["a","32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:de.chadenas.cpudefense"],["a","32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:de.sesu8642.feudaltactics"],["a","32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:com.peaceray.codeword"],["a","32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:com.serwylo.retrowars"],["a","32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:de.mlex.same"],["a","32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:com.github.thewierdnut.endless_mobile"],["a","32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:com.tacticmaster"],["a","32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:io.github.lime3ds.android"],["a","32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:com.unciv.app"],["a","32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:org.wesnoth.Wesnoth"],["a","32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:com.agateau.burgerparty"],["a","32267:b95162b5280fa639f779d1e96d05bfa4a00159214c2a578b5244b2fb75b642d3:com.magius.lightningreaction"],["a","32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:de.z11.roboyard"],["a","32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:me.lecaro.breakout"]],"content":"","sig":"b46c90094ba55ea46e86dba9e3ec47f77b6e7369e0244644da7d1b4a3d13031d9a385861d976529a47887ed2063bcbc8176e6dc40d3b305fddd81c2688a7b38e"}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun parsesZapstoreCurationSet() {
|
||||||
|
val event = Event.fromJson(zapstoreGames)
|
||||||
|
|
||||||
|
assertIs<AppCurationSetEvent>(event)
|
||||||
|
assertTrue(event.verify())
|
||||||
|
|
||||||
|
assertEquals("games", event.dTag())
|
||||||
|
// Zapstore names the set with `name`, not NIP-51's `title`.
|
||||||
|
assertNull(event.title())
|
||||||
|
assertEquals("Games", event.titleOrName())
|
||||||
|
assertNull(event.description())
|
||||||
|
assertEquals(listOf(Platform.ANDROID_ARM64_V8A), event.platforms())
|
||||||
|
|
||||||
|
val apps = event.apps()
|
||||||
|
assertEquals(19, apps.size)
|
||||||
|
assertTrue(apps.all { it.address.kind == SoftwareApplicationEvent.KIND })
|
||||||
|
assertEquals("com.gitlab.ardash.appleflinger.android", apps.first().address.dTag)
|
||||||
|
assertEquals("me.lecaro.breakout", apps.last().address.dTag)
|
||||||
|
assertEquals(
|
||||||
|
"b95162b5280fa639f779d1e96d05bfa4a00159214c2a578b5244b2fb75b642d3",
|
||||||
|
apps.single { it.address.dTag == "com.magius.lightningreaction" }.address.pubKeyHex,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals("Games", event.indexableContent())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun titleWinsOverName() {
|
||||||
|
val template =
|
||||||
|
AppCurationSetEvent.build(title = "Nostr apps", dTag = "nostr", createdAt = 1729302793) {
|
||||||
|
add(arrayOf("name", "ignored"))
|
||||||
|
}
|
||||||
|
val tags = template.tags
|
||||||
|
val event = AppCurationSetEvent("0".repeat(64), "1".repeat(64), template.createdAt, tags, "", "2".repeat(128))
|
||||||
|
|
||||||
|
assertEquals("Nostr apps", event.titleOrName())
|
||||||
|
assertEquals("ignored", event.name())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user