Files
nak/src/main/scala/Components.scala
2023-06-20 15:33:29 -03:00

477 lines
14 KiB
Scala
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import cats.data.{Store => *, *}
import cats.effect.*
import cats.effect.syntax.all.*
import cats.syntax.all.*
import fs2.concurrent.*
import fs2.dom.{Event => _, *}
import io.circe.parser.*
import io.circe.syntax.*
import calico.*
import calico.html.io.{*, given}
import calico.syntax.*
import scodec.bits.ByteVector
import scoin.*
import snow.*
import Utils.*
object Components {
def render32Bytes(
store: Store,
bytes32: ByteVector32
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "text-md",
entry("canonical hex", bytes32.toHex),
"if this is a public key:",
div(
cls := "mt-2 pl-2 mb-2",
entry(
"npub",
NIP19.encode(XOnlyPublicKey(bytes32)),
Some(
selectable(
store,
NIP19.encode(XOnlyPublicKey(bytes32))
)
)
),
nip19_21(
store,
"nprofile",
NIP19.encode(ProfilePointer(XOnlyPublicKey(bytes32)))
)
),
"if this is a private key:",
div(
cls := "pl-2 mb-2",
entry(
"nsec",
NIP19.encode(PrivateKey(bytes32)),
Some(
selectable(
store,
NIP19.encode(PrivateKey(bytes32))
)
)
),
entry(
"npub",
NIP19.encode(PrivateKey(bytes32).publicKey.xonly),
Some(
selectable(
store,
NIP19.encode(PrivateKey(bytes32).publicKey.xonly)
)
)
),
nip19_21(
store,
"nprofile",
NIP19.encode(ProfilePointer(PrivateKey(bytes32).publicKey.xonly))
)
),
"if this is an event id:",
div(
cls := "pl-2 mb-2",
nip19_21(
store,
"nevent",
NIP19.encode(EventPointer(bytes32.toHex))
)
),
div(
cls := "pl-2 mb-2",
entry(
"note",
NIP19.encode(bytes32),
Some(
selectable(
store,
NIP19.encode(bytes32)
)
)
)
)
)
def renderEventPointer(
store: Store,
evp: snow.EventPointer
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "text-md",
entry(
"event id (hex)",
evp.id,
Some(selectable(store, evp.id))
),
relayHints(store, evp.relays),
evp.author.map { pk =>
entry("author hint (pubkey hex)", pk.value.toHex)
},
nip19_21(store, "nevent", NIP19.encode(evp)),
entry(
"note",
NIP19.encode(ByteVector32.fromValidHex(evp.id)),
Some(selectable(store, NIP19.encode(ByteVector32.fromValidHex(evp.id))))
)
)
def renderProfilePointer(
store: Store,
pp: snow.ProfilePointer,
sk: Option[PrivateKey] = None
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "text-md",
sk.map { k =>
entry(
"private key (hex)",
k.value.toHex,
Some(selectable(store, k.value.toHex))
)
},
sk.map { k =>
entry(
"nsec",
NIP19.encode(k),
Some(selectable(store, NIP19.encode(k)))
)
},
entry(
"public key (hex)",
pp.pubkey.value.toHex,
Some(selectable(store, pp.pubkey.value.toHex))
),
relayHints(
store,
pp.relays,
dynamic = if sk.isDefined then false else true
),
entry(
"npub",
NIP19.encode(pp.pubkey),
Some(selectable(store, NIP19.encode(pp.pubkey)))
),
nip19_21(store, "nprofile", NIP19.encode(pp))
)
def renderAddressPointer(
store: Store,
addr: snow.AddressPointer
): Resource[IO, HtmlDivElement[IO]] = {
val nip33atag =
s"${addr.kind}:${addr.author.value.toHex}:${addr.d}"
div(
cls := "text-md",
entry("author (pubkey hex)", addr.author.value.toHex),
entry("identifier (d tag)", addr.d),
entry("kind", addr.kind.toString),
relayHints(store, addr.relays),
nip19_21(store, "naddr", NIP19.encode(addr)),
entry("nip33 'a' tag", nip33atag, Some(selectable(store, nip33atag)))
)
}
def renderEvent(
store: Store,
event: Event
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "text-md",
if event.pubkey.isEmpty then
Some(
div(
cls := "flex items-center",
entry("missing", "pubkey"),
button(
Styles.buttonSmall,
"fill with a debugging key",
onClick --> (_.foreach { _ =>
store.input.set(
event
.copy(pubkey = Some(keyOne.publicKey.xonly))
.asJson
.printWith(jsonPrinter)
)
})
)
)
)
else None,
if event.id.isEmpty then
Some(
div(
cls := "flex items-center",
entry("missing", "id"),
if event.pubkey.isDefined then
Some(
button(
Styles.buttonSmall,
"fill id",
onClick --> (_.foreach(_ =>
store.input.set(
event
.copy(id = Some(event.hash.toHex))
.asJson
.printWith(jsonPrinter)
)
))
)
)
else None
)
)
else None,
if event.sig.isEmpty then
Some(
div(
cls := "flex items-center",
entry("missing", "sig"),
if event.id.isDefined && event.pubkey == Some(
keyOne.publicKey.xonly
)
then
Some(
button(
Styles.buttonSmall,
"sign",
onClick --> (_.foreach(_ =>
store.input.set(
event
.sign(keyOne)
.asJson
.printWith(jsonPrinter)
)
))
)
)
else None
)
)
else None,
entry("serialized event", event.serialized),
entry("implied event id", event.hash.toHex),
entry(
"does the implied event id match the given event id?",
event.id == Some(event.hash.toHex) match {
case true => "yes"; case false => "no"
}
),
entry(
"is signature valid?",
event.isValid match {
case true => "yes"; case false => "no"
}
),
event.id.map(id =>
nip19_21(
store,
"nevent",
NIP19.encode(EventPointer(id, author = event.pubkey))
)
),
if event.kind >= 30000 && event.kind < 40000 then
event.pubkey
.map(author =>
nip19_21(
store,
"naddr",
NIP19.encode(
AddressPointer(
d = event.tags
.collectFirst { case "d" :: v :: _ => v }
.getOrElse(""),
kind = event.kind,
author = author,
relays = List.empty
)
)
)
)
else
event.id.map(id =>
entry(
"note",
NIP19.encode(ByteVector32.fromValidHex(id)),
Some(selectable(store, NIP19.encode(ByteVector32.fromValidHex(id))))
)
)
)
private def entry(
key: String,
value: String,
selectLink: Option[Resource[IO, HtmlSpanElement[IO]]] = None
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "flex items-center space-x-3",
span(cls := "font-bold", key + " "),
span(Styles.mono, cls := "max-w-xl break-all", value),
selectLink
)
private def nip19_21(
store: Store,
key: String,
code: String
): Resource[IO, HtmlDivElement[IO]] =
div(
span(cls := "font-bold", key + " "),
span(Styles.mono, cls := "break-all", code),
selectable(store, code),
a(
href := "nostr:" + code,
external
)
)
private def relayHints(
store: Store,
relays: List[String],
dynamic: Boolean = true
): Resource[IO, HtmlDivElement[IO]] =
if !dynamic && relays.isEmpty then div("")
else
SignallingRef[IO].of(false).toResource.flatMap { active =>
val value =
if relays.size > 0 then relays.reduce((a, b) => s"$a, $b") else ""
div(
cls := "flex items-center space-x-3",
span(cls := "font-bold", "relay hints "),
if relays.size == 0 then div("")
else
// displaying each relay hint
div(
cls := "flex flex-wrap max-w-xl",
relays
.map(url =>
div(
Styles.mono,
cls := "flex items-center rounded py-0.5 px-1 mr-1 mb-1 bg-orange-100",
url,
// removing a relay hint by clicking on the x
div(
cls := "cursor-pointer ml-1 text-rose-600 hover:text-rose-300",
onClick --> (_.foreach(_ => {
store.result.get.flatMap(result =>
store.input.set(
result
.map {
case a: AddressPointer =>
NIP19
.encode(
a.copy(relays =
relays.filterNot(_ == url)
)
)
case p: ProfilePointer =>
NIP19
.encode(
p.copy(relays =
relays.filterNot(_ == url)
)
)
case e: EventPointer =>
NIP19
.encode(
e.copy(relays =
relays.filterNot(_ == url)
)
)
case r => ""
}
.getOrElse("")
)
)
})),
"×"
)
)
)
)
,
active.map {
case true =>
div(
input.withSelf { self =>
(
onKeyPress --> (_.foreach(evt =>
// confirm adding a relay hint
evt.key match {
case "Enter" =>
self.value.get.flatMap(url =>
if url.startsWith("wss://") || url
.startsWith("ws://")
then {
store.result.get.flatMap(result =>
store.input.set(
result
.map {
case a: AddressPointer =>
NIP19
.encode(
a.copy(relays = a.relays :+ url)
)
case p: ProfilePointer =>
NIP19
.encode(
p.copy(relays = p.relays :+ url)
)
case e: EventPointer =>
NIP19
.encode(
e.copy(relays = e.relays :+ url)
)
case r => ""
}
.getOrElse("")
)
)
>> active.set(false)
} else IO.unit
)
case _ => IO.unit
}
))
)
}
)
case false if dynamic =>
// button to add a new relay hint
button(
Styles.buttonSmall,
"add relay hint",
onClick --> (_.foreach(_ => active.set(true)))
)
case false => div("")
}
)
}
private def selectable(
store: Store,
code: String
): Resource[IO, HtmlSpanElement[IO]] =
span(
store.input.map(current =>
if current == code then a("")
else
a(
href := "#/" + code,
onClick --> (_.foreach(evt =>
evt.preventDefault >>
store.input.set(code)
)),
edit
)
)
)
private val edit = img(cls := "inline w-4 ml-2", src := "edit.svg")
private val external = img(cls := "inline w-4 ml-2", src := "ext.svg")
}