Migrates to the new 10008 profile badges kind

This commit is contained in:
Vitor Pamplona
2026-03-31 16:52:24 -04:00
parent 49850e773f
commit a4e3625f26
15 changed files with 296 additions and 35 deletions
@@ -168,9 +168,10 @@ import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent 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.award.BadgeAwardEvent
import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent 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.WrappedEvent
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
@@ -843,7 +844,12 @@ object LocalCache : ILocalCache, ICacheProvider {
event.taggedAddresses().map { getOrCreateAddressableNote(it) } 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.badgeAwardEvents().mapNotNull { checkGetOrCreateNote(it) } +
event.badgeAwardDefinitions().map { getOrCreateAddressableNote(it) } event.badgeAwardDefinitions().map { getOrCreateAddressableNote(it) }
} }
@@ -2539,6 +2545,7 @@ object LocalCache : ILocalCache, ICacheProvider {
): Boolean = ): Boolean =
try { try {
when (event) { when (event) {
is AcceptedBadgeSetEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is AdvertisedRelayListEvent -> consumeBaseReplaceable(event, relay, wasVerified) is AdvertisedRelayListEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is AppDefinitionEvent -> consumeBaseReplaceable(event, relay, wasVerified) is AppDefinitionEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is AppRecommendationEvent -> 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 AudioTrackEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is BadgeAwardEvent -> consumeRegularEvent(event, relay, wasVerified) is BadgeAwardEvent -> consumeRegularEvent(event, relay, wasVerified)
is BadgeDefinitionEvent -> consumeBaseReplaceable(event, relay, wasVerified) is BadgeDefinitionEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is BadgeProfilesEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is BlockedRelayListEvent -> consumeBaseReplaceable(event, relay, wasVerified) is BlockedRelayListEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is BlossomServersEvent -> consumeBaseReplaceable(event, relay, wasVerified) is BlossomServersEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is BroadcastRelayListEvent -> 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 PictureEvent -> consumeRegularEvent(event, relay, wasVerified)
is PrivateDmEvent -> consumeRegularEvent(event, relay, wasVerified) is PrivateDmEvent -> consumeRegularEvent(event, relay, wasVerified)
is PrivateOutboxRelayListEvent -> consumeBaseReplaceable(event, relay, wasVerified) is PrivateOutboxRelayListEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is ProfileBadgesEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is ProxyRelayListEvent -> consumeBaseReplaceable(event, relay, wasVerified) is ProxyRelayListEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is PinListEvent -> consumeBaseReplaceable(event, relay, wasVerified) is PinListEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is PublicMessageEvent -> consumeRegularEvent(event, relay, wasVerified) is PublicMessageEvent -> consumeRegularEvent(event, relay, wasVerified)
@@ -30,14 +30,16 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEve
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent 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 = val FollowAndMutesFromKeyKinds =
listOf( listOf(
PeopleListEvent.KIND, PeopleListEvent.KIND,
FollowListEvent.KIND, FollowListEvent.KIND,
MuteListEvent.KIND, MuteListEvent.KIND,
BadgeProfilesEvent.KIND, AcceptedBadgeSetEvent.KIND,
ProfileBadgesEvent.KIND,
EmojiPackSelectionEvent.KIND, EmojiPackSelectionEvent.KIND,
EphemeralChatListEvent.KIND, EphemeralChatListEvent.KIND,
ChannelListEvent.KIND, ChannelListEvent.KIND,
@@ -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.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent 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 import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
val UserProfileMetadataKinds = val UserProfileMetadataKinds =
@@ -34,7 +35,8 @@ val UserProfileMetadataKinds =
MetadataEvent.KIND, MetadataEvent.KIND,
AdvertisedRelayListEvent.KIND, AdvertisedRelayListEvent.KIND,
ContactListEvent.KIND, ContactListEvent.KIND,
BadgeProfilesEvent.KIND, AcceptedBadgeSetEvent.KIND,
ProfileBadgesEvent.KIND,
) )
fun filterUserProfileMetadata( fun filterUserProfileMetadata(
@@ -32,13 +32,14 @@ import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User 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.observeNoteEvent
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMapNotNull
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.navigation.navs.INav 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.BadgePictureModifier
import com.vitorpamplona.amethyst.ui.theme.Size35Modifier import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag 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.award.BadgeAwardEvent
import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent 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.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList 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 @Composable
fun DisplayBadges( fun DisplayBadges(
@@ -61,25 +69,44 @@ fun DisplayBadges(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
LoadAddressableNote( val oldDesign = AcceptedBadgeSetEvent.createAddress(baseUser.pubkeyHex)
BadgeProfilesEvent.createAddress(baseUser.pubkeyHex), val newDesign = ProfileBadgesEvent.createAddress(baseUser.pubkeyHex)
accountViewModel,
) { note -> val oldNote = accountViewModel.getOrCreateAddressableNote(oldDesign)
if (note != null) { val newNote = accountViewModel.getOrCreateAddressableNote(newDesign)
WatchAndRenderBadgeList(note, accountViewModel, nav)
} WatchAndRenderBadgeList(oldNote, newNote, accountViewModel, nav)
}
} }
@Composable @Composable
private fun WatchAndRenderBadgeList( private fun WatchAndRenderBadgeList(
note: AddressableNote, oldNote: AddressableNote,
newNote: AddressableNote,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
val badgeList by observeNoteEventAndMapNotNull(note, accountViewModel) { event: BadgeProfilesEvent -> // Subscribe in the relay for changes in this note.
event.badgeAwardEvents().toImmutableList() 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) } badgeList?.let { list -> RenderBadgeList(list, accountViewModel, nav) }
} }
@@ -233,9 +233,10 @@ import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent 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.award.BadgeAwardEvent
import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent 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.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent
@@ -552,6 +553,7 @@ fun RelayInformationBody(
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
fun kindDisplayName(kind: Int): Int = fun kindDisplayName(kind: Int): Int =
when (kind) { when (kind) {
AcceptedBadgeSetEvent.KIND -> R.string.kind_accepted_badge_set
AdvertisedRelayListEvent.KIND -> R.string.kind_outbox_relays AdvertisedRelayListEvent.KIND -> R.string.kind_outbox_relays
AppDefinitionEvent.KIND -> R.string.kind_apps AppDefinitionEvent.KIND -> R.string.kind_apps
AppRecommendationEvent.KIND -> R.string.kind_app_recommendations AppRecommendationEvent.KIND -> R.string.kind_app_recommendations
@@ -564,7 +566,6 @@ fun kindDisplayName(kind: Int): Int =
AttestorProficiencyEvent.KIND -> R.string.attestor_proficiency AttestorProficiencyEvent.KIND -> R.string.attestor_proficiency
BadgeAwardEvent.KIND -> R.string.kind_badge_awards BadgeAwardEvent.KIND -> R.string.kind_badge_awards
BadgeDefinitionEvent.KIND -> R.string.kind_badge_definitions BadgeDefinitionEvent.KIND -> R.string.kind_badge_definitions
BadgeProfilesEvent.KIND -> R.string.kind_profile_badges
BlockedRelayListEvent.KIND -> R.string.kind_blocked_relays BlockedRelayListEvent.KIND -> R.string.kind_blocked_relays
BlossomServersEvent.KIND -> R.string.kind_blossom_servers BlossomServersEvent.KIND -> R.string.kind_blossom_servers
BlossomAuthorizationEvent.KIND -> R.string.kind_blossom_auth BlossomAuthorizationEvent.KIND -> R.string.kind_blossom_auth
@@ -651,6 +652,7 @@ fun kindDisplayName(kind: Int): Int =
OtsEvent.KIND -> R.string.kind_ots OtsEvent.KIND -> R.string.kind_ots
PaymentTargetsEvent.KIND -> R.string.kind_pay_to PaymentTargetsEvent.KIND -> R.string.kind_pay_to
PeopleListEvent.KIND -> R.string.kind_people_lists PeopleListEvent.KIND -> R.string.kind_people_lists
ProfileBadgesEvent.KIND -> R.string.kind_profile_badges
PictureEvent.KIND -> R.string.kind_pictures PictureEvent.KIND -> R.string.kind_pictures
PinListEvent.KIND -> R.string.kind_pins PinListEvent.KIND -> R.string.kind_pins
ZapPollEvent.KIND -> R.string.kind_zap_poll ZapPollEvent.KIND -> R.string.kind_zap_poll
+1
View File
@@ -1744,6 +1744,7 @@
<string name="kind_audio_track">Audio Track</string> <string name="kind_audio_track">Audio Track</string>
<string name="kind_badge_awards">Badge Awards</string> <string name="kind_badge_awards">Badge Awards</string>
<string name="kind_badge_definitions">Badge Definitions</string> <string name="kind_badge_definitions">Badge Definitions</string>
<string name="kind_accepted_badge_set">Accepted Badge Set</string>
<string name="kind_profile_badges">Profile Badges</string> <string name="kind_profile_badges">Profile Badges</string>
<string name="kind_blocked_relays">Blocked Relays</string> <string name="kind_blocked_relays">Blocked Relays</string>
<string name="kind_blossom_servers">Blossom Servers</string> <string name="kind_blossom_servers">Blossom Servers</string>
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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 androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Address 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.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip31Alts.alt 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 import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable @Immutable
class BadgeProfilesEvent( class AcceptedBadgeSetEvent(
id: HexKey, id: HexKey,
pubKey: HexKey, pubKey: HexKey,
createdAt: Long, createdAt: Long,
@@ -79,7 +79,7 @@ class BadgeProfilesEvent(
fun build( fun build(
acceptedBadges: List<AcceptedBadge> = emptyList(), acceptedBadges: List<AcceptedBadge> = emptyList(),
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<BadgeProfilesEvent>.() -> Unit = {}, initializer: TagArrayBuilder<AcceptedBadgeSetEvent>.() -> Unit = {},
) = eventTemplate(KIND, "", createdAt) { ) = eventTemplate(KIND, "", createdAt) {
dTag(STANDARD_D_TAG) dTag(STANDARD_D_TAG)
alt(ALT_DESCRIPTION) alt(ALT_DESCRIPTION)
@@ -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<AcceptedBadgeSetEvent>.acceptedBadges(badges: List<AcceptedBadge>) = addAll(AcceptedBadge.assemble(badges))
@@ -18,12 +18,12 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.core.TagArray
import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents 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) fun TagArray.acceptedBadges() = AcceptedBadge.parseAll(this)
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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 androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.core.TagArray
@@ -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<Array<String>>,
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<AcceptedBadge> = emptyList(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<ProfileBadgesEvent>.() -> Unit = {},
) = eventTemplate(KIND, "", createdAt) {
alt(ALT_DESCRIPTION)
if (acceptedBadges.isNotEmpty()) {
acceptedBadges(acceptedBadges)
}
initializer()
}
}
}
@@ -18,9 +18,9 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip58Badges.profiles.tags.AcceptedBadge import com.vitorpamplona.quartz.nip58Badges.accepted.tags.AcceptedBadge
fun TagArrayBuilder<BadgeProfilesEvent>.acceptedBadges(badges: List<AcceptedBadge>) = addAll(AcceptedBadge.assemble(badges)) fun TagArrayBuilder<ProfileBadgesEvent>.acceptedBadges(badges: List<AcceptedBadge>) = addAll(AcceptedBadge.assemble(badges))
@@ -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()
@@ -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<AcceptedBadge> {
val result = mutableListOf<AcceptedBadge>()
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<AcceptedBadge>): List<Array<String>> =
badges.flatMap { badge ->
listOf(
badge.badgeDefinition.toATagArray(),
badge.badgeAward.toTagArray(),
)
}
}
}
@@ -140,9 +140,10 @@ import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent 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.award.BadgeAwardEvent
import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent 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.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent
@@ -268,6 +269,7 @@ class EventFactory {
sig: HexKey, sig: HexKey,
): T = ): T =
when (kind) { when (kind) {
AcceptedBadgeSetEvent.KIND -> AcceptedBadgeSetEvent(id, pubKey, createdAt, tags, content, sig)
AdvertisedRelayListEvent.KIND -> AdvertisedRelayListEvent(id, pubKey, createdAt, tags, content, sig) AdvertisedRelayListEvent.KIND -> AdvertisedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
AppDefinitionEvent.KIND -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig) AppDefinitionEvent.KIND -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
AppRecommendationEvent.KIND -> AppRecommendationEvent(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) AudioTrackEvent.KIND -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig)
BadgeAwardEvent.KIND -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig) BadgeAwardEvent.KIND -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig)
BadgeDefinitionEvent.KIND -> BadgeDefinitionEvent(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) BidEvent.KIND -> BidEvent(id, pubKey, createdAt, tags, content, sig)
BidConfirmationEvent.KIND -> BidConfirmationEvent(id, pubKey, createdAt, tags, content, sig) BidConfirmationEvent.KIND -> BidConfirmationEvent(id, pubKey, createdAt, tags, content, sig)
BlockedRelayListEvent.KIND -> BlockedRelayListEvent(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) P2POrderEvent.KIND -> P2POrderEvent(id, pubKey, createdAt, tags, content, sig)
PictureEvent.KIND -> PictureEvent(id, pubKey, createdAt, tags, content, sig) PictureEvent.KIND -> PictureEvent(id, pubKey, createdAt, tags, content, sig)
PinListEvent.KIND -> PinListEvent(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) ZapPollEvent.KIND -> ZapPollEvent(id, pubKey, createdAt, tags, content, sig)
PollEvent.KIND -> PollEvent(id, pubKey, createdAt, tags, content, sig) PollEvent.KIND -> PollEvent(id, pubKey, createdAt, tags, content, sig)
PollResponseEvent.KIND -> PollResponseEvent(id, pubKey, createdAt, tags, content, sig) PollResponseEvent.KIND -> PollResponseEvent(id, pubKey, createdAt, tags, content, sig)