diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 6a0a31f9e6..b316174452 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -54,6 +54,7 @@ import com.vitorpamplona.quartz.experimental.attestations.recommendation.Attesto import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId @@ -3371,6 +3372,10 @@ object LocalCache : ILocalCache, ICacheProvider { consumeBaseReplaceable(event, relay, wasVerified) } + is BirdexEvent -> { + consumeBaseReplaceable(event, relay, wasVerified) + } + is CommentEvent -> { consumeRegularEvent(event, relay, wasVerified) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index ffbc121bcd..dbb4a20e1d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -117,6 +117,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderAttestorRecommendation import com.vitorpamplona.amethyst.ui.note.types.RenderAudioHeader import com.vitorpamplona.amethyst.ui.note.types.RenderAudioTrack import com.vitorpamplona.amethyst.ui.note.types.RenderBadgeAward +import com.vitorpamplona.amethyst.ui.note.types.RenderBirdex import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarCollectionEvent import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarDateSlotEvent import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarRSVPEvent @@ -216,6 +217,7 @@ import com.vitorpamplona.quartz.experimental.attestations.recommendation.Attesto import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.forks.IForkableEvent @@ -1264,6 +1266,10 @@ private fun RenderNoteRow( RenderFundraiser(baseNote, makeItShort, accountViewModel, nav) } + is BirdexEvent -> { + RenderBirdex(baseNote, makeItShort, accountViewModel) + } + is HighlightEvent -> { RenderHighlight( baseNote, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Birdex.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Birdex.kt new file mode 100644 index 0000000000..8346a62aed --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Birdex.kt @@ -0,0 +1,89 @@ +/* + * 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.amethyst.ui.note.types + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.amethyst.ui.theme.replyModifier +import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent + +/** How many species names to list before collapsing into a "+N more" suffix. */ +private const val SPECIES_PREVIEW_LIMIT = 6 + +/** + * Minimal, fixed-size summary card for a Birdstar "Birdex" (kind 12473). + * + * The event has no body and no images, only a species list. To keep the card + * bounded regardless of how many species a Birdex holds, we show the count and a + * short preview of scientific names with a "+N more" suffix — no images, no + * expansion, no network calls. The card is identical in the feed and the opened + * view (it ignores [makeItShort]). + */ +@Composable +fun RenderBirdex( + baseNote: Note, + makeItShort: Boolean, + accountViewModel: AccountViewModel, +) { + val noteEvent = baseNote.event as? BirdexEvent ?: return + + val names = remember(noteEvent) { noteEvent.speciesNames() } + val preview = remember(names) { names.take(SPECIES_PREVIEW_LIMIT) } + val remaining = names.size - preview.size + + Column(MaterialTheme.colorScheme.replyModifier.padding(10.dp)) { + Text( + text = pluralStringResource(R.plurals.birdex_species_count, names.size, names.size), + style = MaterialTheme.typography.titleMedium, + ) + + if (preview.isNotEmpty()) { + Spacer(Modifier.height(6.dp)) + val joined = preview.joinToString(", ") + Text( + text = + if (remaining > 0) { + stringRes(R.string.birdex_species_preview_more, joined, remaining.toString()) + } else { + joined + }, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.placeholderText, + overflow = TextOverflow.Ellipsis, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt index e3b823bf55..02146a7145 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt @@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.experimental.agora.FundraiserEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent @@ -68,6 +69,7 @@ class FollowPackFeedNewThreadFeedFilter( NipTextEvent.KIND, ClassifiedsEvent.KIND, FundraiserEvent.KIND, + BirdexEvent.KIND, LongTextNoteEvent.KIND, ) } @@ -137,6 +139,7 @@ class FollowPackFeedNewThreadFeedFilter( noteEvent is TextNoteEvent || noteEvent is ClassifiedsEvent || noteEvent is FundraiserEvent || + noteEvent is BirdexEvent || noteEvent.isRenderableRepost() || (noteEvent is LongTextNoteEvent && noteEvent.content.isNotEmpty()) || (noteEvent is WikiNoteEvent && noteEvent.content.isNotEmpty()) || diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt index e1d086328b..b63c4117da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt @@ -37,6 +37,7 @@ import com.vitorpamplona.quartz.experimental.attestations.recommendation.Attesto import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent @@ -70,6 +71,7 @@ class HomeNewThreadFeedFilter( WikiNoteEvent.KIND, ClassifiedsEvent.KIND, FundraiserEvent.KIND, + BirdexEvent.KIND, LongTextNoteEvent.KIND, LiveChessGameEndEvent.KIND, AttestationEvent.KIND, @@ -126,6 +128,7 @@ class HomeNewThreadFeedFilter( noteEvent is TextNoteEvent || noteEvent is ClassifiedsEvent || noteEvent is FundraiserEvent || + noteEvent is BirdexEvent || noteEvent.isRenderableRepost() || (noteEvent is LongTextNoteEvent && noteEvent.content.isNotEmpty()) || (noteEvent is WikiNoteEvent && noteEvent.content.isNotEmpty()) || diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt index d451ca0325..e3fa8265e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt @@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.experimental.agora.FundraiserEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent @@ -78,6 +79,7 @@ class UserProfileMutualFeedFilter( it.event is TextNoteEvent || it.event is ClassifiedsEvent || it.event is FundraiserEvent || + it.event is BirdexEvent || it.event.isRenderableRepost() || it.event is LongTextNoteEvent || it.event is WikiNoteEvent || diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt index cfcd046558..fff7c182d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt @@ -35,6 +35,7 @@ import com.vitorpamplona.quartz.experimental.attestations.recommendation.Attesto import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent @@ -84,6 +85,7 @@ class UserProfileNewThreadFeedFilter( it.event is CommentEvent || it.event is ClassifiedsEvent || it.event is FundraiserEvent || + it.event is BirdexEvent || it.event.isRenderableRepost() || it.event is LongTextNoteEvent || it.event is WikiNoteEvent || diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 9bde3c4686..689c8391a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -147,6 +147,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderAttestation import com.vitorpamplona.amethyst.ui.note.types.RenderAttestationRequest import com.vitorpamplona.amethyst.ui.note.types.RenderAttestorProficiency import com.vitorpamplona.amethyst.ui.note.types.RenderAttestorRecommendation +import com.vitorpamplona.amethyst.ui.note.types.RenderBirdex import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarDateSlotEvent import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarTimeSlotEvent import com.vitorpamplona.amethyst.ui.note.types.RenderCashuMint @@ -229,6 +230,7 @@ import com.vitorpamplona.quartz.experimental.attestations.recommendation.Attesto import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.forks.IForkableEvent @@ -769,6 +771,8 @@ private fun FullBleedNoteCompose( RenderGoal(baseNote, accountViewModel, nav) } else if (noteEvent is FundraiserEvent) { RenderFundraiser(baseNote, makeItShort = false, accountViewModel, nav) + } else if (noteEvent is BirdexEvent) { + RenderBirdex(baseNote, makeItShort = false, accountViewModel) } else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) { RenderRepost(baseNote, quotesLeft = 3, backgroundColor, accountViewModel, nav) } else if (noteEvent is RelayDiscoveryEvent) { diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 40bed9f73f..400852fb5f 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -3138,6 +3138,11 @@ %1$s funded of %2$s sats goal Ends %1$s On-chain donation + + Birdex · %1$d species + Birdex · %1$d species + + %1$s +%2$s more Goal amount (sats) 100000 Describe your goal diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/birdstar/BirdexEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/birdstar/BirdexEvent.kt new file mode 100644 index 0000000000..4b170db4b8 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/birdstar/BirdexEvent.kt @@ -0,0 +1,71 @@ +/* + * 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.experimental.birdstar + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.firstTagValue +import com.vitorpamplona.quartz.nip01Core.core.mapValueTagged + +/** + * Birdstar "Birdex" species collection (kind 12473). + * + * An app-specific **replaceable** kind published by the Birdstar app + * (`birdstar.app`) — a birdwatching life-list. It is **not** defined by any NIP; + * the schema below is derived from events seen in the wild. Being replaceable, + * each author keeps a single, latest Birdex. + * + * The event carries no body and no images — its payload is the species list, + * one entry per observed species, as alternating tags: + * + * - `n` — the species' scientific name (e.g. `Icterus galbula`). + * - `i` — an external identity reference for the species, a Wikidata entity URL + * (NIP-73 style, e.g. `https://www.wikidata.org/entity/Q805774`). + * - `alt` — a human-readable summary written by the publisher + * (e.g. `Birdex: 24 species`). + * + * Amethyst renders a minimal, fixed-size summary card from [speciesNames] and + * [speciesCount]; it does not resolve the Wikidata references to images. + */ +@Immutable +class BirdexEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + /** Scientific names of the collected species, in event order, from the `n` tags. */ + fun speciesNames() = tags.mapValueTagged("n") { it } + + /** Number of collected species (one `n` tag per species). */ + fun speciesCount() = tags.count { it.size > 1 && it[0] == "n" } + + /** Publisher-provided human-readable summary, from the `alt` tag (may be null). */ + fun summary() = tags.firstTagValue("alt") + + companion object { + const val KIND = 12473 + const val ALT_DESCRIPTION = "A Birdex species collection" + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt index f0b180b0c3..6356a1667a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt @@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.experimental.attestations.recommendation.Attesto import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent @@ -341,6 +342,7 @@ class EventFactory { AudioTrackEvent.KIND -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig) BadgeAwardEvent.KIND -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig) BadgeDefinitionEvent.KIND -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig) + BirdexEvent.KIND -> BirdexEvent(id, pubKey, createdAt, tags, content, sig) BidEvent.KIND -> BidEvent(id, pubKey, createdAt, tags, content, sig) BidConfirmationEvent.KIND -> BidConfirmationEvent(id, pubKey, createdAt, tags, content, sig) BlockedRelayListEvent.KIND -> BlockedRelayListEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/birdstar/BirdexEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/birdstar/BirdexEventTest.kt new file mode 100644 index 0000000000..c0ab794c9f --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/birdstar/BirdexEventTest.kt @@ -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.experimental.birdstar + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.utils.EventFactory +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertTrue + +class BirdexEventTest { + private fun sampleEvent(): Event = + EventFactory.create( + id = "a099d4db563041bb289d3704f983fc148fc805860303a4f479a8264dc6a2d7cc", + pubKey = "932614571afcbad4d17a191ee281e39eebbb41b93fac8fd87829622aeb112f4d", + createdAt = 1_780_836_939L, + kind = BirdexEvent.KIND, + tags = + arrayOf( + arrayOf("alt", "Birdex: 3 species"), + arrayOf("i", "https://www.wikidata.org/entity/Q805774"), + arrayOf("n", "Icterus galbula"), + arrayOf("i", "https://www.wikidata.org/entity/Q738534"), + arrayOf("n", "Baeolophus bicolor"), + arrayOf("i", "https://www.wikidata.org/entity/Q829683"), + arrayOf("n", "Mimus polyglottos"), + arrayOf("client", "birdstar.app"), + ), + content = "", + sig = "00".repeat(64), + ) + + @Test + fun factoryBuildsBirdexForKind12473() { + val event = sampleEvent() + assertTrue( + event is BirdexEvent, + "Expected a BirdexEvent but got ${event::class.simpleName}", + ) + } + + @Test + fun kind12473IsNowKnown() { + assertTrue(EventFactory.isKnownKind(BirdexEvent.KIND), "kind 12473 should be a known kind") + } + + @Test + fun parsesBirdexFields() { + val event = sampleEvent() + assertIs(event) + + assertEquals(3, event.speciesCount()) + assertEquals( + listOf("Icterus galbula", "Baeolophus bicolor", "Mimus polyglottos"), + event.speciesNames(), + ) + assertEquals("Birdex: 3 species", event.summary()) + } +}