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())
+ }
+}