fix(badges): preserve NIP-58 a/e pair order, clickable rows, accepted-first sort
Three fixes to the Profile-badges flow: 1. TagArrayBuilder groups tags by name, so calling acceptedBadges() on the builder scrambled [a1, e1, a2, e2] into [a1, a2, e1, e2] when serializing. AcceptedBadge.parseAll expects adjacent (a, e) pairs, so only one mangled badge survived, making each Accept toggle appear to replace the previous one. Rewrite the build() of both ProfileBadgesEvent (kind 10008) and AcceptedBadgeSetEvent (kind 30008) to collect the prefix tags (d / alt / initializer) via the builder, then append AcceptedBadge.assemble(...) verbatim to keep the pair order intact. 2. Rows in ProfileBadgesScreen were not clickable. Thread nav through AwardRow / StaticAwardRow and wrap the row body in Modifier.clickable that routes to the badge definition's thread. The Switch keeps consuming its own clicks, so toggling still works. 3. Sort accepted badges to the top of the list, then the rest by createdAt desc, so the user can see at a glance which badges are currently shown on the profile.
This commit is contained in:
+43
-7
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.profile
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -56,6 +57,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFind
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -116,13 +118,17 @@ fun ProfileBadgesScreen(
|
||||
}
|
||||
|
||||
val receivedAwards =
|
||||
remember(myPubkey, bundleTick) {
|
||||
remember(myPubkey, bundleTick, acceptedAwardIds) {
|
||||
LocalCache.notes
|
||||
.filterIntoSet { _, it ->
|
||||
val event = it.event
|
||||
event is BadgeAwardEvent && event.awardeeIds().contains(myPubkey)
|
||||
}.mapNotNull { it.event as? BadgeAwardEvent }
|
||||
.sortedByDescending { it.createdAt }
|
||||
.sortedWith(
|
||||
// Accepted badges on top, then most recent first.
|
||||
compareByDescending<BadgeAwardEvent> { acceptedAwardIds.contains(it.id) }
|
||||
.thenByDescending { it.createdAt },
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@@ -156,6 +162,7 @@ fun ProfileBadgesScreen(
|
||||
award = award,
|
||||
isAccepted = acceptedAwardIds.contains(award.id),
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
@@ -170,26 +177,43 @@ private fun AwardRow(
|
||||
award: BadgeAwardEvent,
|
||||
isAccepted: Boolean,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val defAddr = award.awardDefinition().firstOrNull()
|
||||
|
||||
if (defAddr == null) {
|
||||
StaticAwardRow(definition = null, isAccepted = isAccepted, accountViewModel = accountViewModel, award = award)
|
||||
StaticAwardRow(
|
||||
definition = null,
|
||||
defNote = null,
|
||||
isAccepted = isAccepted,
|
||||
accountViewModel = accountViewModel,
|
||||
award = award,
|
||||
nav = nav,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
LoadAddressableNote(defAddr, accountViewModel) { defNote ->
|
||||
if (defNote == null) {
|
||||
StaticAwardRow(definition = null, isAccepted = isAccepted, accountViewModel = accountViewModel, award = award)
|
||||
StaticAwardRow(
|
||||
definition = null,
|
||||
defNote = null,
|
||||
isAccepted = isAccepted,
|
||||
accountViewModel = accountViewModel,
|
||||
award = award,
|
||||
nav = nav,
|
||||
)
|
||||
} else {
|
||||
// Ask relays for the definition if we don't have it yet, then watch.
|
||||
EventFinderFilterAssemblerSubscription(defNote, accountViewModel)
|
||||
val definition by observeNoteEvent<BadgeDefinitionEvent>(defNote, accountViewModel)
|
||||
StaticAwardRow(
|
||||
definition = definition,
|
||||
defNote = defNote,
|
||||
isAccepted = isAccepted,
|
||||
accountViewModel = accountViewModel,
|
||||
award = award,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -198,15 +222,27 @@ private fun AwardRow(
|
||||
@Composable
|
||||
private fun StaticAwardRow(
|
||||
definition: BadgeDefinitionEvent?,
|
||||
defNote: com.vitorpamplona.amethyst.model.AddressableNote?,
|
||||
isAccepted: Boolean,
|
||||
accountViewModel: AccountViewModel,
|
||||
award: BadgeAwardEvent,
|
||||
nav: INav,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
val rowModifier =
|
||||
if (defNote != null) {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 12.dp),
|
||||
.clickable {
|
||||
routeFor(defNote, accountViewModel.account)?.let { nav.nav(it) }
|
||||
}.padding(horizontal = 20.dp, vertical = 12.dp)
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 12.dp)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = rowModifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
BadgeThumb(definition)
|
||||
|
||||
+14
-8
@@ -25,10 +25,11 @@ import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.core.tagArray
|
||||
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.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
@@ -80,13 +81,18 @@ class AcceptedBadgeSetEvent(
|
||||
acceptedBadges: List<AcceptedBadge> = emptyList(),
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<AcceptedBadgeSetEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, "", createdAt) {
|
||||
dTag(STANDARD_D_TAG)
|
||||
alt(ALT_DESCRIPTION)
|
||||
if (acceptedBadges.isNotEmpty()) {
|
||||
acceptedBadges(acceptedBadges)
|
||||
}
|
||||
initializer()
|
||||
): EventTemplate<AcceptedBadgeSetEvent> {
|
||||
// TagArrayBuilder groups tags by name, which would scramble the
|
||||
// alternating a/e pairs NIP-58 requires. Build the prefix tags via
|
||||
// the builder, then append the ordered pair tags verbatim.
|
||||
val prefix =
|
||||
tagArray<AcceptedBadgeSetEvent> {
|
||||
dTag(STANDARD_D_TAG)
|
||||
alt(ALT_DESCRIPTION)
|
||||
initializer()
|
||||
}
|
||||
val pairs = AcceptedBadge.assemble(acceptedBadges).toTypedArray()
|
||||
return EventTemplate(createdAt, KIND, prefix + pairs, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-7
@@ -25,10 +25,11 @@ 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.core.tagArray
|
||||
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.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
|
||||
@@ -78,12 +79,17 @@ class ProfileBadgesEvent(
|
||||
acceptedBadges: List<AcceptedBadge> = emptyList(),
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<ProfileBadgesEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, "", createdAt) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
if (acceptedBadges.isNotEmpty()) {
|
||||
acceptedBadges(acceptedBadges)
|
||||
}
|
||||
initializer()
|
||||
): EventTemplate<ProfileBadgesEvent> {
|
||||
// TagArrayBuilder groups tags by name, which would scramble the
|
||||
// alternating a/e pairs NIP-58 requires. Build the prefix tags via
|
||||
// the builder, then append the ordered pair tags verbatim.
|
||||
val prefix =
|
||||
tagArray<ProfileBadgesEvent> {
|
||||
alt(ALT_DESCRIPTION)
|
||||
initializer()
|
||||
}
|
||||
val pairs = AcceptedBadge.assemble(acceptedBadges).toTypedArray()
|
||||
return EventTemplate(createdAt, KIND, prefix + pairs, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user