diff --git a/src/main/scala/Components.scala b/src/main/scala/Components.scala index cea2ae1..c6fb3c6 100644 --- a/src/main/scala/Components.scala +++ b/src/main/scala/Components.scala @@ -22,7 +22,7 @@ object Components { entry("canonical hex", bytes32.toHex), "if this is a public key:", div( - cls := "pl-2 mb-2", + cls := "mt-2 pl-2 mb-2", entry( "npub", NIP19.encode(XOnlyPublicKey(bytes32)) @@ -66,14 +66,13 @@ object Components { ) def renderEventPointer( + store: Store, evp: snow.EventPointer ): Resource[IO, HtmlDivElement[IO]] = div( cls := "text-md", entry("event id (hex)", evp.id), - if evp.relays.size > 0 then - Some(entry("relay hints", evp.relays.reduce((a, b) => s"$a, $b"))) - else None, + relayHints(store, evp.relays), evp.author.map { pk => entry("author hint (pubkey hex)", pk.value.toHex) }, @@ -82,6 +81,7 @@ object Components { ) def renderProfilePointer( + store: Store, pp: snow.ProfilePointer, sk: Option[PrivateKey] = None ): Resource[IO, HtmlDivElement[IO]] = @@ -90,14 +90,13 @@ object Components { sk.map { k => entry("private key (hex)", k.value.toHex) }, sk.map { k => entry("nsec", NIP19.encode(k)) }, entry("public key (hex)", pp.pubkey.value.toHex), - if pp.relays.size > 0 then - Some(entry("relay hints", pp.relays.reduce((a, b) => s"$a, $b"))) - else None, + relayHints(store, pp.relays), entry("npub", NIP19.encode(pp.pubkey)), nip19_21("nprofile", NIP19.encode(pp)) ) def renderAddressPointer( + store: Store, addr: snow.AddressPointer ): Resource[IO, HtmlDivElement[IO]] = div( @@ -105,15 +104,13 @@ object Components { entry("author (pubkey hex)", addr.author.value.toHex), entry("identifier", addr.d), entry("kind", addr.kind.toString), - if addr.relays.size > 0 then - Some(entry("relay hints", addr.relays.reduce((a, b) => s"$a, $b"))) - else None, + relayHints(store, addr.relays), nip19_21("naddr", NIP19.encode(addr)) ) def renderEvent( - event: Event, - store: Store + store: Store, + event: Event ): Resource[IO, HtmlDivElement[IO]] = div( cls := "text-md", @@ -239,5 +236,49 @@ object Components { ) ) + private def relayHints( + store: Store, + relays: List[String] + ): Resource[IO, HtmlDivElement[IO]] = + 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 "), + span(Styles.mono, cls := "max-w-xl", value), + active.map { + case true => + div( + input.withSelf { self => + ( + onKeyPress --> (_.foreach(evt => + evt.key match { + case "Enter" => + self.value.get.flatMap(url => + if url.startsWith("wss://") || url.startsWith("ws://") + then + store.input.update( + _.trim() ++ " + " ++ url + ) >> active.set(false) + else IO.unit + ) + case _ => IO.unit + } + )) + ) + } + ) + case false => + button( + Styles.buttonSmall, + "add relay hint", + onClick --> (_.foreach(_ => active.set(true))) + ) + } + ) + } + private val external = img(cls := "inline w-4 ml-2", src := "ext.svg") } diff --git a/src/main/scala/Main.scala b/src/main/scala/Main.scala index c62a932..71cdef2 100644 --- a/src/main/scala/Main.scala +++ b/src/main/scala/Main.scala @@ -111,15 +111,16 @@ object Main extends IOWebApp { store.result.map { case Left(msg) => div(msg) case Right(bytes: ByteVector32) => render32Bytes(bytes) - case Right(event: Event) => renderEvent(event, store) - case Right(pp: ProfilePointer) => renderProfilePointer(pp) - case Right(evp: EventPointer) => renderEventPointer(evp) + case Right(event: Event) => renderEvent(store, event) + case Right(pp: ProfilePointer) => renderProfilePointer(store, pp) + case Right(evp: EventPointer) => renderEventPointer(store, evp) case Right(sk: PrivateKey) => renderProfilePointer( + store, ProfilePointer(pubkey = sk.publicKey.xonly), Some(sk) ) - case Right(addr: AddressPointer) => renderAddressPointer(addr) + case Right(addr: AddressPointer) => renderAddressPointer(store, addr) } ) } diff --git a/src/main/scala/Parser.scala b/src/main/scala/Parser.scala index adf0090..ef875ac 100644 --- a/src/main/scala/Parser.scala +++ b/src/main/scala/Parser.scala @@ -12,31 +12,48 @@ type Result = Either[ ] object Parser { - def parseInput(input: String): Result = if input == "" then Left("") - else - ByteVector - .fromHex(input) - .flatMap(b => Try(Right(ByteVector32(b))).toOption) - .getOrElse( - NIP19.decode(input) match { - case Right(pp: ProfilePointer) => Right(pp) - case Right(evp: EventPointer) => Right(evp) - case Right(sk: PrivateKey) => Right(sk) - case Right(addr: AddressPointer) => Right(addr) - case Left(_) => - parse(input) match { - case Left(err: io.circe.ParsingFailure) => - Left("not valid JSON or NIP-19 code") - case Right(json) => - json - .as[Event] - .leftMap { err => - err.pathToRootString match { - case None => s"decoding ${err.pathToRootString}" - case Some(path) => s"field $path is missing or wrong" + val additions = raw" *\+ *".r + + def parseInput(input: String): Result = + if input == "" then Left("") + else { + val spl = additions.split(input) + val result = ByteVector + .fromHex(spl.head) + .flatMap(b => Try(Right(ByteVector32(b))).toOption) + .getOrElse( + NIP19.decode(spl.head) match { + case Right(pp: ProfilePointer) => Right(pp) + case Right(evp: EventPointer) => Right(evp) + case Right(sk: PrivateKey) => Right(sk) + case Right(addr: AddressPointer) => Right(addr) + case Left(_) => + parse(input) match { + case Left(err: io.circe.ParsingFailure) => + Left("not valid JSON or NIP-19 code") + case Right(json) => + json + .as[Event] + .leftMap { err => + err.pathToRootString match { + case None => s"decoding ${err.pathToRootString}" + case Some(path) => s"field $path is missing or wrong" + } } - } - } - } - ) + } + } + ) + + val extraRelays = spl + .drop(1) + .toList + .filter(e => e.startsWith("wss://") || e.startsWith("ws://")) + + result.map { + case a: AddressPointer => a.copy(relays = a.relays ::: extraRelays) + case p: ProfilePointer => p.copy(relays = p.relays ::: extraRelays) + case e: EventPointer => e.copy(relays = e.relays ::: extraRelays) + case r => r + } + } }