Final touches on the Attestation cards

This commit is contained in:
Vitor Pamplona
2026-03-20 21:16:59 -04:00
parent 2ef634ca01
commit df6dc17047
12 changed files with 588 additions and 84 deletions
@@ -784,11 +784,11 @@ private fun RenderNoteRow(
}
is AttestorRecommendationEvent -> {
RenderAttestorRecommendation(baseNote, backgroundColor, accountViewModel, nav)
RenderAttestorRecommendation(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
}
is AttestorProficiencyEvent -> {
RenderAttestorProficiency(baseNote, backgroundColor, accountViewModel, nav)
RenderAttestorProficiency(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
}
is AudioTrackEvent -> {
@@ -46,6 +46,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -53,13 +55,28 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.UserCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.KindChip
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.AttestationStatus
@@ -71,16 +88,75 @@ import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@Preview
@Composable
fun RenderAttestationPreview() {
val event =
AttestationEvent(
id = "f8e05e4fa964d9dfbeb339ae0450d0b3424aaf8f6083d95f38c780335c3dbd56",
pubKey = "c4f5e7a75a8ce3683d529cff06368439c529e5243c6b125ba68789198856cac7",
createdAt = 1773941524,
content = "This is Frank's new npub.",
sig = "61dde5dc5738bfa637aa04451eec63d45f649902a0772abbaf55711aad5b7ce376bc7712c937f9ffc504655c4fd7e39f78139dc3f1a90953d150b43f3e2428fb",
tags =
arrayOf(
arrayOf("d", "af5aa898:fe108febb997:1773941524"),
arrayOf("e", "fe108febb99796c4091775e00aa1fc3ffc489ad22fdf1f8c559b2472815c09c7"),
arrayOf("s", "verified"),
arrayOf("v", "valid"),
arrayOf("client", "attestr.xyz"),
),
)
LocalCache.justConsume(event, null, true)
val note = LocalCache.getOrCreateNote(event.id)
ThemeComparisonColumn(
toPreview = {
RenderAttestation(
baseNote = note,
quotesLeft = 3,
backgroundColor = remember { mutableStateOf(Color.Transparent) },
accountViewModel = mockAccountViewModel(),
nav = EmptyNav(),
)
},
)
}
@Composable
fun RenderAttestation(
note: Note,
baseNote: Note,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteEvent = note.event as? AttestationEvent ?: return
val noteState by baseNote
.flow()
.metadata.stateFlow
.collectAsStateWithLifecycle()
val noteEvent = noteState.note.event as? AttestationEvent ?: return
RenderAttestation(
baseNote,
noteEvent,
quotesLeft,
backgroundColor,
accountViewModel,
nav,
)
}
@Composable
fun RenderAttestation(
note: Note,
noteEvent: AttestationEvent,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
val validity = remember(noteEvent) { noteEvent.validity() }
val status = remember(noteEvent) { noteEvent.status() }
val validFrom = remember(noteEvent) { noteEvent.validFrom() }
@@ -91,6 +167,10 @@ fun RenderAttestation(
val statusIcon = remember(status, validity) { attestationIcon(status, validity) }
val statusLabel = attestationStatusLabel(status, validity)
val aboutAddress = remember(noteEvent) { noteEvent.assertionAddress() }
val aboutEvent = remember(noteEvent) { noteEvent.assertionEventId() }
val aboutPubkey = remember(noteEvent) { noteEvent.assertionPubkey() }
Column(
modifier =
Modifier
@@ -119,7 +199,7 @@ fun RenderAttestation(
}
if (validFrom != null || validTo != null) {
Spacer(modifier = Modifier.height(6.dp))
Spacer(modifier = DoubleVertSpacer)
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
validFrom?.let {
Text(
@@ -138,24 +218,11 @@ fun RenderAttestation(
}
}
content?.let {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
if (quotesLeft > 0) {
note.replyTo?.firstOrNull()?.let {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringRes(R.string.attestation_attests_to),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
if (aboutAddress != null) {
LoadAddressableNote(aboutAddress, accountViewModel) {
if (it != null) {
Spacer(modifier = DoubleVertSpacer)
NoteCompose(
baseNote = it,
modifier = MaterialTheme.colorScheme.replyModifier,
@@ -169,6 +236,49 @@ fun RenderAttestation(
)
}
}
} else if (aboutEvent != null) {
LoadNote(aboutEvent, accountViewModel) {
if (it != null) {
Spacer(modifier = DoubleVertSpacer)
NoteCompose(
baseNote = it,
modifier = MaterialTheme.colorScheme.replyModifier,
isQuotedNote = true,
unPackReply = ReplyRenderType.NONE,
makeItShort = true,
quotesLeft = quotesLeft - 1,
parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
} else if (aboutPubkey != null) {
LoadUser(aboutPubkey, accountViewModel) {
if (it != null) {
Spacer(modifier = DoubleVertSpacer)
UserCompose(it, accountViewModel = accountViewModel, nav = nav)
}
}
}
}
content?.let {
Spacer(modifier = DoubleVertSpacer)
TranslatableRichTextViewer(
content = it,
canPreview = true,
quotesLeft = quotesLeft,
modifier = Modifier.fillMaxWidth(),
tags = remember(note) { note.event?.tags?.toImmutableListOfLists() ?: EmptyTagList },
backgroundColor = backgroundColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
)
}
}
}
@Composable
@@ -179,9 +289,18 @@ fun RenderAttestationRequest(
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteEvent = note.event as? AttestationRequestEvent ?: return
val noteState by note
.flow()
.metadata.stateFlow
.collectAsStateWithLifecycle()
val noteEvent = noteState.note.event as? AttestationRequestEvent ?: return
val content = remember(noteEvent) { noteEvent.content.ifBlank { null } }
val aboutAddress = remember(noteEvent) { noteEvent.assertionAddress() }
val aboutEvent = remember(noteEvent) { noteEvent.assertionEventId() }
val aboutPubkey = remember(noteEvent) { noteEvent.assertionPubkey() }
Column(
modifier =
Modifier
@@ -213,18 +332,32 @@ fun RenderAttestationRequest(
}
content?.let {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
Spacer(modifier = DoubleVertSpacer)
TranslatableRichTextViewer(
content = it,
canPreview = true,
quotesLeft = quotesLeft,
modifier = Modifier.fillMaxWidth(),
tags = remember(note) { note.event?.tags?.toImmutableListOfLists() ?: EmptyTagList },
backgroundColor = backgroundColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
)
}
}
if (quotesLeft > 0) {
note.replyTo?.firstOrNull()?.let {
Spacer(modifier = Modifier.height(8.dp))
if (aboutAddress != null) {
LoadAddressableNote(aboutAddress, accountViewModel) {
if (it != null) {
Spacer(modifier = DoubleVertSpacer)
Text(
text = stringRes(R.string.attestation_requests_attestation_to),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
NoteCompose(
baseNote = it,
modifier = MaterialTheme.colorScheme.replyModifier,
@@ -238,19 +371,62 @@ fun RenderAttestationRequest(
)
}
}
} else if (aboutEvent != null) {
LoadNote(aboutEvent, accountViewModel) {
if (it != null) {
Spacer(modifier = DoubleVertSpacer)
Text(
text = stringRes(R.string.attestation_requests_attestation_to),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
NoteCompose(
baseNote = it,
modifier = MaterialTheme.colorScheme.replyModifier,
isQuotedNote = true,
unPackReply = ReplyRenderType.NONE,
makeItShort = true,
quotesLeft = quotesLeft - 1,
parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
} else if (aboutPubkey != null) {
LoadUser(aboutPubkey, accountViewModel) {
if (it != null) {
Spacer(modifier = DoubleVertSpacer)
Text(
text = stringRes(R.string.attestation_requests_attestation_to),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
UserCompose(it, accountViewModel = accountViewModel, nav = nav)
}
}
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun RenderAttestorRecommendation(
note: Note,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteEvent = note.event as? AttestorRecommendationEvent ?: return
val noteState by note
.flow()
.metadata.stateFlow
.collectAsStateWithLifecycle()
val noteEvent = noteState.note.event as? AttestorRecommendationEvent ?: return
val kinds = remember(noteEvent) { noteEvent.kinds() }
val description = remember(noteEvent) { noteEvent.description() }
val aboutPubKey = remember(noteEvent) { noteEvent.dTag() }
Column(
modifier =
@@ -282,25 +458,45 @@ fun RenderAttestorRecommendation(
)
}
LoadUser(aboutPubKey, accountViewModel) {
if (it != null) {
Spacer(modifier = DoubleVertSpacer)
UserCompose(it, accountViewModel = accountViewModel, nav = nav)
}
}
if (kinds.isNotEmpty()) {
Spacer(modifier = Modifier.height(6.dp))
Spacer(modifier = DoubleVertSpacer)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text =
stringRes(
R.string.attestor_recommendation_for_kinds,
kinds.joinToString(", "),
),
text = stringRes(R.string.attestor_recommendation_for_kinds),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
// Kinds
kinds.forEach { kind ->
KindChip(kind)
}
}
}
description?.let {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
Spacer(modifier = DoubleVertSpacer)
TranslatableRichTextViewer(
content = it,
canPreview = true,
quotesLeft = quotesLeft,
modifier = Modifier.fillMaxWidth(),
tags = remember(note) { note.event?.tags?.toImmutableListOfLists() ?: EmptyTagList },
backgroundColor = backgroundColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
)
}
}
@@ -310,11 +506,17 @@ fun RenderAttestorRecommendation(
@Composable
fun RenderAttestorProficiency(
note: Note,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
val noteEvent = note.event as? AttestorProficiencyEvent ?: return
val noteState by note
.flow()
.metadata.stateFlow
.collectAsStateWithLifecycle()
val noteEvent = noteState.note.event as? AttestorProficiencyEvent ?: return
val kinds = remember(noteEvent) { noteEvent.kinds() }
val description = remember(noteEvent) { noteEvent.description() }
@@ -368,10 +570,17 @@ fun RenderAttestorProficiency(
description?.let {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
TranslatableRichTextViewer(
content = it,
canPreview = true,
quotesLeft = quotesLeft,
modifier = Modifier.fillMaxWidth(),
tags = remember(note) { note.event?.tags?.toImmutableListOfLists() ?: EmptyTagList },
backgroundColor = backgroundColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
)
}
}
@@ -669,7 +669,7 @@ val zaps = setOf(9734, 9735, 9041, 17375, 23194, 23195)
val reports = setOf(ReportEvent.KIND, MuteListEvent.KIND, DeletionEvent.KIND, RequestToVanishEvent.KIND)
@Composable
private fun KindChip(kind: Int) {
fun KindChip(kind: Int) {
val nameResId = kindDisplayName(kind)
val name = if (nameResId != -1) stringResource(nameResId) else "k$kind"
val (bg, fg) =
@@ -633,9 +633,9 @@ private fun FullBleedNoteCompose(
} else if (noteEvent is AttestationRequestEvent) {
RenderAttestationRequest(baseNote, 3, backgroundColor, accountViewModel, nav)
} else if (noteEvent is AttestorRecommendationEvent) {
RenderAttestorRecommendation(baseNote, backgroundColor, accountViewModel, nav)
RenderAttestorRecommendation(baseNote, 3, backgroundColor, accountViewModel, nav)
} else if (noteEvent is AttestorProficiencyEvent) {
RenderAttestorProficiency(baseNote, backgroundColor, accountViewModel, nav)
RenderAttestorProficiency(baseNote, 3, backgroundColor, accountViewModel, nav)
} else if (noteEvent is AdvertisedRelayListEvent) {
DisplayNIP65RelayList(baseNote, backgroundColor, accountViewModel, nav)
} else if (noteEvent is LnZapEvent) {
+2 -1
View File
@@ -1907,8 +1907,9 @@
<string name="attestation_request">Attestation Request</string>
<string name="attestation_request_description">Requesting attestation for an event</string>
<string name="attestor_recommendation">Attestor Recommendation</string>
<string name="attestor_recommendation_for_kinds">Recommended for kinds: %1$s</string>
<string name="attestor_recommendation_for_kinds">Recommended for kinds: </string>
<string name="attestor_proficiency">Attestor Proficiency</string>
<string name="attestor_proficiency_for_kinds">Proficient in verifying kinds: %1$s</string>
<string name="attestation_attests_to">Attests to</string>
<string name="attestation_requests_attestation_to">Requests attestation to</string>
</resources>
@@ -23,13 +23,26 @@ package com.vitorpamplona.quartz.experimental.attestations.attestation
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.AttestationStatus
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.Validity
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
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.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
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
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.serialization.json.JsonNull.content
@Immutable
class AttestationEvent(
@@ -39,7 +52,22 @@ class AttestationEvent(
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
EventHintProvider,
AddressHintProvider,
PubKeyHintProvider {
override fun eventHints(): List<EventIdHint> = tags.mapNotNull(ETag::parseAsHint)
override fun linkedEventIds(): List<HexKey> = tags.mapNotNull(ETag::parseId)
override fun addressHints(): List<AddressHint> = tags.mapNotNull(ATag::parseAsHint)
override fun linkedAddressIds(): List<String> = tags.mapNotNull(ATag::parseAddressId)
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
fun validity() = tags.validity()
fun status() = tags.status()
@@ -50,25 +78,91 @@ class AttestationEvent(
fun request() = tags.request()
fun requestId() = tags.requestId()
fun requestAddress() = tags.requestAddress()
fun isRevoked() = status() == AttestationStatus.REVOKED
fun assertionAddrId() = tags.firstNotNullOfOrNull(ATag::parseAddressId)
fun assertionAddress() = tags.firstNotNullOfOrNull(ATag::parseAddress)
fun assertionATag() = tags.firstNotNullOfOrNull(ATag::parse)
fun assertionEventId() = tags.firstNotNullOfOrNull(ETag::parseId)
fun assertionETag() = tags.firstNotNullOfOrNull(ETag::parse)
fun assertionPubkey() = tags.firstNotNullOfOrNull(PTag::parseKey)
fun assertionPTag() = tags.firstNotNullOfOrNull(PTag::parse)
companion object {
const val KIND = 31871
const val ALT_DESCRIPTION = "Attestation"
fun build(
fun buildEvent(
dTagId: String,
about: EventHintBundle<Event>,
content: String = "",
validity: Validity? = null,
status: AttestationStatus? = null,
validFrom: Long? = null,
validTo: Long? = null,
requestAddress: String? = null,
requestAddress: EventHintBundle<AttestationRequestEvent>? = null,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<AttestationEvent>.() -> Unit = {},
) = eventTemplate(KIND, content, createdAt) {
alt(ALT_DESCRIPTION)
dTag(dTagId)
about(about)
validity?.let { validity(it) }
status?.let { status(it) }
validFrom?.let { validFrom(it) }
validTo?.let { validTo(it) }
requestAddress?.let { request(it) }
initializer()
}
fun buildReplaceable(
dTagId: String,
about: EventHintBundle<BaseReplaceableEvent>,
content: String = "",
validity: Validity? = null,
status: AttestationStatus? = null,
validFrom: Long? = null,
validTo: Long? = null,
requestAddress: EventHintBundle<AttestationRequestEvent>? = null,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<AttestationEvent>.() -> Unit = {},
) = eventTemplate(KIND, content, createdAt) {
alt(ALT_DESCRIPTION)
dTag(dTagId)
aboutReplaceable(about)
validity?.let { validity(it) }
status?.let { status(it) }
validFrom?.let { validFrom(it) }
validTo?.let { validTo(it) }
requestAddress?.let { request(it) }
initializer()
}
fun buildAddress(
dTagId: String,
about: EventHintBundle<BaseAddressableEvent>,
content: String = "",
validity: Validity? = null,
status: AttestationStatus? = null,
validFrom: Long? = null,
validTo: Long? = null,
requestAddress: EventHintBundle<AttestationRequestEvent>? = null,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<AttestationEvent>.() -> Unit = {},
) = eventTemplate(KIND, content, createdAt) {
alt(ALT_DESCRIPTION)
dTag(dTagId)
aboutAddressable(about)
validity?.let { validity(it) }
status?.let { status(it) }
validFrom?.let { validFrom(it) }
@@ -27,7 +27,14 @@ import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.Valid
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.ValidToTag
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.Validity
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.ValidityTag
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
fun TagArrayBuilder<AttestationEvent>.validity(validity: Validity) = addUnique(ValidityTag.assemble(validity))
@@ -37,4 +44,10 @@ fun TagArrayBuilder<AttestationEvent>.validFrom(timestamp: Long) = addUnique(Val
fun TagArrayBuilder<AttestationEvent>.validTo(timestamp: Long) = addUnique(ValidToTag.assemble(timestamp))
fun TagArrayBuilder<AttestationEvent>.request(requestAddress: String) = addUnique(RequestTag.assemble(requestAddress))
fun TagArrayBuilder<AttestationEvent>.request(request: EventHintBundle<AttestationRequestEvent>) = addUnique(RequestTag.assemble(request.event.address(), request.relay))
fun TagArrayBuilder<AttestationEvent>.aboutAddressable(request: EventHintBundle<BaseAddressableEvent>) = addUnique(ATag.assemble(request.event.address(), request.relay))
fun TagArrayBuilder<AttestationEvent>.aboutReplaceable(request: EventHintBundle<BaseReplaceableEvent>) = addUnique(ATag.assemble(request.event.address(), request.relay))
fun TagArrayBuilder<AttestationEvent>.about(request: EventHintBundle<Event>) = addUnique(ETag.assemble(request.event.id, request.relay, request.event.pubKey))
@@ -36,3 +36,7 @@ fun TagArray.validFrom() = firstNotNullOfOrNull(ValidFromTag::parse)
fun TagArray.validTo() = firstNotNullOfOrNull(ValidToTag::parse)
fun TagArray.request() = firstNotNullOfOrNull(RequestTag::parse)
fun TagArray.requestId() = firstNotNullOfOrNull(RequestTag::parseAddressId)
fun TagArray.requestAddress() = firstNotNullOfOrNull(RequestTag::parseAddress)
@@ -20,22 +20,108 @@
*/
package com.vitorpamplona.quartz.experimental.attestations.attestation.tags
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip72ModCommunities.approval.tags.ApprovedAddressTag
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import com.vitorpamplona.quartz.utils.arrayOfNotNull
import com.vitorpamplona.quartz.utils.ensure
class RequestTag {
class RequestTag(
val address: Address,
val relayHint: NormalizedRelayUrl? = null,
) {
fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag)
fun toTagArray() = assemble(address, relayHint)
fun toTagIdOnly() = assemble(address, null)
companion object {
const val TAG_NAME = "request"
fun isTag(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
fun isTagged(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && !Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)
fun parse(tag: Array<String>): String? {
fun isTagged(
tag: Array<String>,
addressId: String,
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == addressId
fun isTagged(
tag: Array<String>,
address: ApprovedAddressTag,
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == address.toTag()
fun isIn(
tag: Array<String>,
addressIds: Set<String>,
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in addressIds
fun parse(tag: Array<String>): ApprovedAddressTag? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(!Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
val address = Address.parse(tag[1]) ?: return null
val relayHint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }
return ApprovedAddressTag(address, relayHint)
}
fun parseValidAddress(tag: Array<String>): String? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(!Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
return Address.parse(tag[1])?.toValue()
}
fun parseAddress(tag: Array<String>): Address? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
val address = Address.parse(tag[1]) ?: return null
ensure(address.kind != CommunityDefinitionEvent.KIND) { return null }
return address
}
fun parseAddressId(tag: Array<String>): String? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(!Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
return tag[1]
}
fun assemble(requestAddress: String) = arrayOf(TAG_NAME, requestAddress)
fun parseAsHint(tag: Array<String>): AddressHint? {
ensure(tag.has(2)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(!Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
ensure(tag[1].contains(':')) { return null }
ensure(tag[2].isNotEmpty()) { return null }
val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2])
ensure(relayHint != null) { return null }
return AddressHint(tag[1], relayHint)
}
fun assemble(
aTagId: HexKey,
relay: NormalizedRelayUrl?,
) = arrayOfNotNull(TAG_NAME, aTagId, relay?.url)
fun assemble(
address: Address,
relay: NormalizedRelayUrl?,
) = arrayOfNotNull(TAG_NAME, address.toValue(), relay?.url)
fun assemble(
kind: Int,
pubKey: String,
dTag: String,
relay: NormalizedRelayUrl?,
) = assemble(Address.assemble(kind, pubKey, dTag), relay)
}
}
@@ -21,13 +21,27 @@
package com.vitorpamplona.quartz.experimental.attestations.request
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.attestations.request.attestorPubKeys
import com.vitorpamplona.quartz.experimental.attestations.request.cashuToken
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
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.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
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
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.let
@Immutable
class AttestationRequestEvent(
@@ -37,15 +51,45 @@ class AttestationRequestEvent(
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
EventHintProvider,
AddressHintProvider,
PubKeyHintProvider {
override fun eventHints(): List<EventIdHint> = tags.mapNotNull(ETag::parseAsHint)
override fun linkedEventIds(): List<HexKey> = tags.mapNotNull(ETag::parseId)
override fun addressHints(): List<AddressHint> = tags.mapNotNull(ATag::parseAsHint)
override fun linkedAddressIds(): List<String> = tags.mapNotNull(ATag::parseAddressId)
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
fun cashuToken() = tags.cashuToken()
fun assertionAddrId() = tags.firstNotNullOfOrNull(ATag::parseAddressId)
fun assertionAddress() = tags.firstNotNullOfOrNull(ATag::parseAddress)
fun assertionATag() = tags.firstNotNullOfOrNull(ATag::parse)
fun assertionEventId() = tags.firstNotNullOfOrNull(ETag::parseId)
fun assertionETag() = tags.firstNotNullOfOrNull(ETag::parse)
fun assertionPubkey() = tags.firstNotNullOfOrNull(PTag::parseKey)
fun assertionPTag() = tags.firstNotNullOfOrNull(PTag::parse)
companion object {
const val KIND = 31872
const val ALT_DESCRIPTION = "Attestation Request"
fun build(
fun buildEvent(
dTagId: String,
about: EventHintBundle<Event>,
content: String = "",
attestorPubKeys: List<HexKey> = emptyList(),
cashuToken: String? = null,
@@ -54,6 +98,41 @@ class AttestationRequestEvent(
) = eventTemplate(KIND, content, createdAt) {
alt(ALT_DESCRIPTION)
dTag(dTagId)
about(about)
attestorPubKeys(attestorPubKeys)
cashuToken?.let { cashuToken(it) }
initializer()
}
fun buildReplaceable(
dTagId: String,
about: EventHintBundle<BaseReplaceableEvent>,
content: String = "",
attestorPubKeys: List<HexKey> = emptyList(),
cashuToken: String? = null,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<AttestationRequestEvent>.() -> Unit = {},
) = eventTemplate(KIND, content, createdAt) {
alt(ALT_DESCRIPTION)
dTag(dTagId)
aboutReplaceable(about)
attestorPubKeys(attestorPubKeys)
cashuToken?.let { cashuToken(it) }
initializer()
}
fun buildAddress(
dTagId: String,
about: EventHintBundle<BaseAddressableEvent>,
content: String = "",
attestorPubKeys: List<HexKey> = emptyList(),
cashuToken: String? = null,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<AttestationRequestEvent>.() -> Unit = {},
) = eventTemplate(KIND, content, createdAt) {
alt(ALT_DESCRIPTION)
dTag(dTagId)
aboutAddressable(about)
attestorPubKeys(attestorPubKeys)
cashuToken?.let { cashuToken(it) }
initializer()
@@ -21,10 +21,22 @@
package com.vitorpamplona.quartz.experimental.attestations.request
import com.vitorpamplona.quartz.experimental.attestations.request.tags.CashuTokenTag
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
fun TagArrayBuilder<AttestationRequestEvent>.attestorPubKeys(pubKeys: List<HexKey>) = addAll(pubKeys.map { PTag.assemble(it, null) })
fun TagArrayBuilder<AttestationRequestEvent>.cashuToken(token: String) = addUnique(CashuTokenTag.assemble(token))
fun TagArrayBuilder<AttestationRequestEvent>.aboutAddressable(request: EventHintBundle<BaseAddressableEvent>) = addUnique(ATag.assemble(request.event.address(), request.relay))
fun TagArrayBuilder<AttestationRequestEvent>.aboutReplaceable(request: EventHintBundle<BaseReplaceableEvent>) = addUnique(ATag.assemble(request.event.address(), request.relay))
fun TagArrayBuilder<AttestationRequestEvent>.about(request: EventHintBundle<Event>) = addUnique(ETag.assemble(request.event.id, request.relay, request.event.pubKey))
@@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable
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.hints.PubKeyHintProvider
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip01Core.tags.aTag.aTag
@@ -47,7 +48,12 @@ class CalendarRSVPEvent(
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
PubKeyHintProvider {
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
fun status() = tags.firstNotNullOfOrNull(RSVPStatusTag.Companion::parse)
fun statusValue() = tags.firstNotNullOfOrNull(RSVPStatusTag.Companion::parseValue)