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 b948f67cb..31fd76865 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
@@ -168,9 +168,10 @@ import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
+import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent
import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent
import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent
-import com.vitorpamplona.quartz.nip58Badges.profiles.BadgeProfilesEvent
+import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent
import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
@@ -843,7 +844,12 @@ object LocalCache : ILocalCache, ICacheProvider {
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
}
- is BadgeProfilesEvent -> {
+ is AcceptedBadgeSetEvent -> {
+ event.badgeAwardEvents().mapNotNull { checkGetOrCreateNote(it) } +
+ event.badgeAwardDefinitions().map { getOrCreateAddressableNote(it) }
+ }
+
+ is ProfileBadgesEvent -> {
event.badgeAwardEvents().mapNotNull { checkGetOrCreateNote(it) } +
event.badgeAwardDefinitions().map { getOrCreateAddressableNote(it) }
}
@@ -2539,6 +2545,7 @@ object LocalCache : ILocalCache, ICacheProvider {
): Boolean =
try {
when (event) {
+ is AcceptedBadgeSetEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is AdvertisedRelayListEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is AppDefinitionEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is AppRecommendationEvent -> consumeBaseReplaceable(event, relay, wasVerified)
@@ -2551,7 +2558,6 @@ object LocalCache : ILocalCache, ICacheProvider {
is AudioTrackEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is BadgeAwardEvent -> consumeRegularEvent(event, relay, wasVerified)
is BadgeDefinitionEvent -> consumeBaseReplaceable(event, relay, wasVerified)
- is BadgeProfilesEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is BlockedRelayListEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is BlossomServersEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is BroadcastRelayListEvent -> consumeBaseReplaceable(event, relay, wasVerified)
@@ -2635,6 +2641,7 @@ object LocalCache : ILocalCache, ICacheProvider {
is PictureEvent -> consumeRegularEvent(event, relay, wasVerified)
is PrivateDmEvent -> consumeRegularEvent(event, relay, wasVerified)
is PrivateOutboxRelayListEvent -> consumeBaseReplaceable(event, relay, wasVerified)
+ is ProfileBadgesEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is ProxyRelayListEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is PinListEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is PublicMessageEvent -> consumeRegularEvent(event, relay, wasVerified)
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt
index 51c7061ef..43051262b 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt
@@ -30,14 +30,16 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEve
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
-import com.vitorpamplona.quartz.nip58Badges.profiles.BadgeProfilesEvent
+import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent
+import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent
val FollowAndMutesFromKeyKinds =
listOf(
PeopleListEvent.KIND,
FollowListEvent.KIND,
MuteListEvent.KIND,
- BadgeProfilesEvent.KIND,
+ AcceptedBadgeSetEvent.KIND,
+ ProfileBadgesEvent.KIND,
EmojiPackSelectionEvent.KIND,
EphemeralChatListEvent.KIND,
ChannelListEvent.KIND,
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMetadata.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMetadata.kt
index d6655b4c8..2115dc570 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMetadata.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMetadata.kt
@@ -26,7 +26,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
-import com.vitorpamplona.quartz.nip58Badges.profiles.BadgeProfilesEvent
+import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent
+import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
val UserProfileMetadataKinds =
@@ -34,7 +35,8 @@ val UserProfileMetadataKinds =
MetadataEvent.KIND,
AdvertisedRelayListEvent.KIND,
ContactListEvent.KIND,
- BadgeProfilesEvent.KIND,
+ AcceptedBadgeSetEvent.KIND,
+ ProfileBadgesEvent.KIND,
)
fun filterUserProfileMetadata(
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt
index cfcdc773a..9992202f9 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt
@@ -32,13 +32,14 @@ import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
+import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
-import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMapNotNull
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
@@ -49,11 +50,18 @@ import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.BadgePictureModifier
import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
+import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent
import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent
import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent
-import com.vitorpamplona.quartz.nip58Badges.profiles.BadgeProfilesEvent
+import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent
import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
@Composable
fun DisplayBadges(
@@ -61,25 +69,44 @@ fun DisplayBadges(
accountViewModel: AccountViewModel,
nav: INav,
) {
- LoadAddressableNote(
- BadgeProfilesEvent.createAddress(baseUser.pubkeyHex),
- accountViewModel,
- ) { note ->
- if (note != null) {
- WatchAndRenderBadgeList(note, accountViewModel, nav)
- }
- }
+ val oldDesign = AcceptedBadgeSetEvent.createAddress(baseUser.pubkeyHex)
+ val newDesign = ProfileBadgesEvent.createAddress(baseUser.pubkeyHex)
+
+ val oldNote = accountViewModel.getOrCreateAddressableNote(oldDesign)
+ val newNote = accountViewModel.getOrCreateAddressableNote(newDesign)
+
+ WatchAndRenderBadgeList(oldNote, newNote, accountViewModel, nav)
}
@Composable
private fun WatchAndRenderBadgeList(
- note: AddressableNote,
+ oldNote: AddressableNote,
+ newNote: AddressableNote,
accountViewModel: AccountViewModel,
nav: INav,
) {
- val badgeList by observeNoteEventAndMapNotNull(note, accountViewModel) { event: BadgeProfilesEvent ->
- event.badgeAwardEvents().toImmutableList()
- }
+ // Subscribe in the relay for changes in this note.
+ EventFinderFilterAssemblerSubscription(oldNote, accountViewModel)
+ EventFinderFilterAssemblerSubscription(newNote, accountViewModel)
+
+ // Subscribe in the LocalCache for changes that arrive in the device
+ val flow =
+ remember(oldNote, newNote) {
+ combine(
+ oldNote.flow().metadata.stateFlow,
+ newNote.flow().metadata.stateFlow,
+ ) { oldNote, newNote ->
+ val oldProfileBadgeEvent = oldNote.note.event as? AcceptedBadgeSetEvent
+ val newProfileBadgeEvent = newNote.note.event as? ProfileBadgesEvent
+
+ newProfileBadgeEvent?.badgeAwardEvents()?.toImmutableList()
+ ?: oldProfileBadgeEvent?.badgeAwardEvents()?.toImmutableList()
+ }.distinctUntilChanged()
+ .flowOn(Dispatchers.IO)
+ }
+
+ // Subscribe in the LocalCache for changes that arrive in the device
+ val badgeList by flow.collectAsStateWithLifecycle(persistentListOf())
badgeList?.let { list -> RenderBadgeList(list, accountViewModel, nav) }
}
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt
index f55fae3d9..bb6b5e0f0 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt
@@ -233,9 +233,10 @@ import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
+import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent
import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent
import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent
-import com.vitorpamplona.quartz.nip58Badges.profiles.BadgeProfilesEvent
+import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent
@@ -552,6 +553,7 @@ fun RelayInformationBody(
@Suppress("DEPRECATION")
fun kindDisplayName(kind: Int): Int =
when (kind) {
+ AcceptedBadgeSetEvent.KIND -> R.string.kind_accepted_badge_set
AdvertisedRelayListEvent.KIND -> R.string.kind_outbox_relays
AppDefinitionEvent.KIND -> R.string.kind_apps
AppRecommendationEvent.KIND -> R.string.kind_app_recommendations
@@ -564,7 +566,6 @@ fun kindDisplayName(kind: Int): Int =
AttestorProficiencyEvent.KIND -> R.string.attestor_proficiency
BadgeAwardEvent.KIND -> R.string.kind_badge_awards
BadgeDefinitionEvent.KIND -> R.string.kind_badge_definitions
- BadgeProfilesEvent.KIND -> R.string.kind_profile_badges
BlockedRelayListEvent.KIND -> R.string.kind_blocked_relays
BlossomServersEvent.KIND -> R.string.kind_blossom_servers
BlossomAuthorizationEvent.KIND -> R.string.kind_blossom_auth
@@ -651,6 +652,7 @@ fun kindDisplayName(kind: Int): Int =
OtsEvent.KIND -> R.string.kind_ots
PaymentTargetsEvent.KIND -> R.string.kind_pay_to
PeopleListEvent.KIND -> R.string.kind_people_lists
+ ProfileBadgesEvent.KIND -> R.string.kind_profile_badges
PictureEvent.KIND -> R.string.kind_pictures
PinListEvent.KIND -> R.string.kind_pins
ZapPollEvent.KIND -> R.string.kind_zap_poll
diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml
index c4372a5f7..866522906 100644
--- a/amethyst/src/main/res/values/strings.xml
+++ b/amethyst/src/main/res/values/strings.xml
@@ -1744,6 +1744,7 @@
Audio Track
Badge Awards
Badge Definitions
+ Accepted Badge Set
Profile Badges
Blocked Relays
Blossom Servers
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/BadgeProfilesEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/AcceptedBadgeSetEvent.kt
similarity index 94%
rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/BadgeProfilesEvent.kt
rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/AcceptedBadgeSetEvent.kt
index b16598a3d..fe5f01e65 100644
--- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/BadgeProfilesEvent.kt
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/AcceptedBadgeSetEvent.kt
@@ -18,7 +18,7 @@
* 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.nip58Badges.profiles
+package com.vitorpamplona.quartz.nip58Badges.accepted
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Address
@@ -34,11 +34,11 @@ import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip31Alts.alt
-import com.vitorpamplona.quartz.nip58Badges.profiles.tags.AcceptedBadge
+import com.vitorpamplona.quartz.nip58Badges.accepted.tags.AcceptedBadge
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
-class BadgeProfilesEvent(
+class AcceptedBadgeSetEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
@@ -79,7 +79,7 @@ class BadgeProfilesEvent(
fun build(
acceptedBadges: List = emptyList(),
createdAt: Long = TimeUtils.now(),
- initializer: TagArrayBuilder.() -> Unit = {},
+ initializer: TagArrayBuilder.() -> Unit = {},
) = eventTemplate(KIND, "", createdAt) {
dTag(STANDARD_D_TAG)
alt(ALT_DESCRIPTION)
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/TagArrayBuilderExt.kt
new file mode 100644
index 000000000..74e1d111a
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/TagArrayBuilderExt.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.nip58Badges.accepted
+
+import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
+import com.vitorpamplona.quartz.nip58Badges.accepted.tags.AcceptedBadge
+
+fun TagArrayBuilder.acceptedBadges(badges: List) = addAll(AcceptedBadge.assemble(badges))
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/TagArrayExt.kt
similarity index 92%
rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/TagArrayExt.kt
rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/TagArrayExt.kt
index 1574c11c3..b662d9df4 100644
--- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/TagArrayExt.kt
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/TagArrayExt.kt
@@ -18,12 +18,12 @@
* 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.nip58Badges.profiles
+package com.vitorpamplona.quartz.nip58Badges.accepted
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents
-import com.vitorpamplona.quartz.nip58Badges.profiles.tags.AcceptedBadge
+import com.vitorpamplona.quartz.nip58Badges.accepted.tags.AcceptedBadge
fun TagArray.acceptedBadges() = AcceptedBadge.parseAll(this)
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/tags/AcceptedBadge.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/tags/AcceptedBadge.kt
similarity index 97%
rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/tags/AcceptedBadge.kt
rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/tags/AcceptedBadge.kt
index e0ed90e0f..fd7c08a01 100644
--- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/tags/AcceptedBadge.kt
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/tags/AcceptedBadge.kt
@@ -18,7 +18,7 @@
* 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.nip58Badges.profiles.tags
+package com.vitorpamplona.quartz.nip58Badges.accepted.tags
import androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.nip01Core.core.TagArray
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/ProfileBadgesEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/ProfileBadgesEvent.kt
new file mode 100644
index 000000000..1680530b6
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/ProfileBadgesEvent.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.quartz.nip58Badges.profile
+
+import androidx.compose.runtime.Immutable
+import com.vitorpamplona.quartz.nip01Core.core.Address
+import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
+import com.vitorpamplona.quartz.nip01Core.core.HexKey
+import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
+import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
+import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
+import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
+import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
+import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
+import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
+import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
+import com.vitorpamplona.quartz.nip31Alts.alt
+import com.vitorpamplona.quartz.nip58Badges.accepted.tags.AcceptedBadge
+import com.vitorpamplona.quartz.utils.TimeUtils
+
+@Immutable
+class ProfileBadgesEvent(
+ id: HexKey,
+ pubKey: HexKey,
+ createdAt: Long,
+ tags: Array>,
+ content: String,
+ sig: HexKey,
+) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
+ EventHintProvider,
+ AddressHintProvider,
+ PubKeyHintProvider {
+ override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
+
+ override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
+
+ override fun eventHints() = tags.mapNotNull(ETag::parseAsHint)
+
+ override fun linkedEventIds() = tags.mapNotNull(ETag::parseId)
+
+ override fun addressHints() = tags.mapNotNull(ATag::parseAsHint)
+
+ override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId)
+
+ fun acceptedBadges() = tags.acceptedBadges()
+
+ fun badgeAwardEvents() = tags.badgeAwardEvents()
+
+ fun badgeAwardDefinitions() = tags.badgeAwardDefinitions()
+
+ companion object {
+ const val KIND = 10008
+ const val ALT_DESCRIPTION = "List of accepted badges by the author"
+
+ fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, "")
+
+ fun createAddressTag(pubKey: HexKey): ATag = ATag(KIND, pubKey, "", null)
+
+ fun build(
+ acceptedBadges: List = emptyList(),
+ createdAt: Long = TimeUtils.now(),
+ initializer: TagArrayBuilder.() -> Unit = {},
+ ) = eventTemplate(KIND, "", createdAt) {
+ alt(ALT_DESCRIPTION)
+ if (acceptedBadges.isNotEmpty()) {
+ acceptedBadges(acceptedBadges)
+ }
+ initializer()
+ }
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/TagArrayBuilderExt.kt
similarity index 87%
rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/TagArrayBuilderExt.kt
rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/TagArrayBuilderExt.kt
index 459f04433..904cd211e 100644
--- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/TagArrayBuilderExt.kt
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/TagArrayBuilderExt.kt
@@ -18,9 +18,9 @@
* 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.nip58Badges.profiles
+package com.vitorpamplona.quartz.nip58Badges.profile
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
-import com.vitorpamplona.quartz.nip58Badges.profiles.tags.AcceptedBadge
+import com.vitorpamplona.quartz.nip58Badges.accepted.tags.AcceptedBadge
-fun TagArrayBuilder.acceptedBadges(badges: List) = addAll(AcceptedBadge.assemble(badges))
+fun TagArrayBuilder.acceptedBadges(badges: List) = addAll(AcceptedBadge.assemble(badges))
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/TagArrayExt.kt
new file mode 100644
index 000000000..478737e30
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/TagArrayExt.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.nip58Badges.profile
+
+import com.vitorpamplona.quartz.nip01Core.core.TagArray
+import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses
+import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents
+import com.vitorpamplona.quartz.nip58Badges.accepted.tags.AcceptedBadge
+
+fun TagArray.acceptedBadges() = AcceptedBadge.parseAll(this)
+
+fun TagArray.badgeAwardEvents() = taggedEvents()
+
+fun TagArray.badgeAwardDefinitions() = taggedAddresses()
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/tags/AcceptedBadge.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/tags/AcceptedBadge.kt
new file mode 100644
index 000000000..2d10142fa
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/tags/AcceptedBadge.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.nip58Badges.profile.tags
+
+import androidx.compose.runtime.Stable
+import com.vitorpamplona.quartz.nip01Core.core.TagArray
+import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
+import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
+
+/**
+ * Represents a paired `a` (badge definition) and `e` (badge award) tag
+ * as specified in NIP-58 Profile Badges.
+ *
+ * Clients SHOULD ignore unpaired `a` or `e` tags.
+ */
+@Stable
+class AcceptedBadge(
+ val badgeDefinition: ATag,
+ val badgeAward: ETag,
+) {
+ companion object {
+ /**
+ * Parses ordered consecutive pairs of `a` and `e` tags from a tag array.
+ * Tags must appear as sequential pairs: `a`, `e`, `a`, `e`, ...
+ * Unpaired tags are ignored per the spec.
+ */
+ fun parseAll(tags: TagArray): List {
+ val result = mutableListOf()
+ var i = 0
+ while (i < tags.size - 1) {
+ val aTag = ATag.parse(tags[i])
+ if (aTag != null) {
+ val eTag = ETag.parse(tags[i + 1])
+ if (eTag != null) {
+ result.add(AcceptedBadge(aTag, eTag))
+ i += 2
+ continue
+ }
+ }
+ i++
+ }
+ return result
+ }
+
+ fun assemble(badges: List): List> =
+ badges.flatMap { badge ->
+ listOf(
+ badge.badgeDefinition.toATagArray(),
+ badge.badgeAward.toTagArray(),
+ )
+ }
+ }
+}
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 6c77feac1..06b51ad2e 100644
--- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt
@@ -140,9 +140,10 @@ import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
+import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent
import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent
import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent
-import com.vitorpamplona.quartz.nip58Badges.profiles.BadgeProfilesEvent
+import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent
@@ -268,6 +269,7 @@ class EventFactory {
sig: HexKey,
): T =
when (kind) {
+ AcceptedBadgeSetEvent.KIND -> AcceptedBadgeSetEvent(id, pubKey, createdAt, tags, content, sig)
AdvertisedRelayListEvent.KIND -> AdvertisedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
AppDefinitionEvent.KIND -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
AppRecommendationEvent.KIND -> AppRecommendationEvent(id, pubKey, createdAt, tags, content, sig)
@@ -281,7 +283,6 @@ 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)
- BadgeProfilesEvent.KIND -> BadgeProfilesEvent(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)
@@ -439,6 +440,7 @@ class EventFactory {
P2POrderEvent.KIND -> P2POrderEvent(id, pubKey, createdAt, tags, content, sig)
PictureEvent.KIND -> PictureEvent(id, pubKey, createdAt, tags, content, sig)
PinListEvent.KIND -> PinListEvent(id, pubKey, createdAt, tags, content, sig)
+ ProfileBadgesEvent.KIND -> ProfileBadgesEvent(id, pubKey, createdAt, tags, content, sig)
ZapPollEvent.KIND -> ZapPollEvent(id, pubKey, createdAt, tags, content, sig)
PollEvent.KIND -> PollEvent(id, pubKey, createdAt, tags, content, sig)
PollResponseEvent.KIND -> PollResponseEvent(id, pubKey, createdAt, tags, content, sig)