|
| 1 | +// Browser-side social composers. The CLI only hands a draft to these pages; |
| 2 | +// account authorization and the final publish stay in the user's browser. |
| 3 | +import { Router } from "express"; |
| 4 | +import { page, footer } from "../lib/html.mjs"; |
| 5 | + |
| 6 | +export const socialsRouter = Router(); |
| 7 | + |
| 8 | +export const NOSTR_RELAYS = [ |
| 9 | + "wss://relay.damus.io", |
| 10 | + "wss://nos.lol", |
| 11 | + "wss://relay.primal.net", |
| 12 | +]; |
| 13 | + |
| 14 | +export function nostrComposerPage() { |
| 15 | + const relays = JSON.stringify(NOSTR_RELAYS); |
| 16 | + const body = ` |
| 17 | + <header class="bar"><div class="wrap bar-inner"> |
| 18 | + <a class="brand" href="/"><span class="mark">M</span>MOSHCODE<span class="app">socials</span></a> |
| 19 | + <a class="btn" href="/">Back to the pit</a> |
| 20 | + </div></header> |
| 21 | + <main class="wrap" style="max-width:760px;padding:44px 0 64px"> |
| 22 | + <div class="label acid" style="margin-bottom:10px">NOSTR · KIND 1</div> |
| 23 | + <h1 style="font-size:2rem;margin:0 0 10px">Post from the pit.</h1> |
| 24 | + <p class="dim mono" style="margin:0 0 24px;line-height:1.65">Your draft stayed in the URL fragment—it was never sent to MoshCode. Connect a browser signer, review the text, then publish it to the relays below.</p> |
| 25 | +
|
| 26 | + <div class="card"> |
| 27 | + <div class="card-head"><span class="h">Draft</span><span class="pill" id="count">0 chars</span></div> |
| 28 | + <div class="card-body"> |
| 29 | + <textarea id="message" rows="8" autofocus placeholder="what's moshing?" style="width:100%;resize:vertical"></textarea> |
| 30 | + <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:14px"> |
| 31 | + <button class="btn acid" id="publish" type="button">Connect signer + publish</button> |
| 32 | + <span class="mono dim" id="status" role="status" aria-live="polite">nothing is posted until you click</span> |
| 33 | + </div> |
| 34 | + </div> |
| 35 | + </div> |
| 36 | +
|
| 37 | + <div class="card" style="margin-top:18px"> |
| 38 | + <div class="card-head"><span class="h">Relays</span><span class="pill">publish to any that accept</span></div> |
| 39 | + <div class="card-body mono dim" style="font-size:.78rem;line-height:1.8"> |
| 40 | + ${NOSTR_RELAYS.map((relay) => `<div><span class="beat"></span> ${relay}</div>`).join("")} |
| 41 | + </div> |
| 42 | + </div> |
| 43 | + </main>${footer} |
| 44 | +
|
| 45 | + <script> |
| 46 | + window.wnjParams = { position: "bottom", accent: "green", compactMode: true }; |
| 47 | + </script> |
| 48 | + <script src="https://cdn.jsdelivr.net/npm/window.nostr.js@0.5.0/dist/window.nostr.min.js"></script> |
| 49 | + <script> |
| 50 | + (function () { |
| 51 | + var RELAYS = ${relays}; |
| 52 | + var message = document.getElementById("message"); |
| 53 | + var publish = document.getElementById("publish"); |
| 54 | + var status = document.getElementById("status"); |
| 55 | + var count = document.getElementById("count"); |
| 56 | + message.value = new URLSearchParams(location.hash.slice(1)).get("text") || ""; |
| 57 | +
|
| 58 | + function recount() { count.textContent = Array.from(message.value).length + " chars" } |
| 59 | + recount(); |
| 60 | + message.addEventListener("input", recount); |
| 61 | +
|
| 62 | + function sendToRelay(relay, event) { |
| 63 | + return new Promise(function (resolve) { |
| 64 | + var settled = false; |
| 65 | + var ws; |
| 66 | + function done(ok, detail) { |
| 67 | + if (settled) return; |
| 68 | + settled = true; |
| 69 | + clearTimeout(timer); |
| 70 | + try { ws.close() } catch (_) {} |
| 71 | + resolve({ relay: relay, ok: ok, detail: detail || "" }); |
| 72 | + } |
| 73 | + var timer = setTimeout(function () { done(false, "timeout") }, 8000); |
| 74 | + try { ws = new WebSocket(relay) } catch (error) { done(false, error.message); return } |
| 75 | + ws.addEventListener("open", function () { ws.send(JSON.stringify(["EVENT", event])) }); |
| 76 | + ws.addEventListener("message", function (incoming) { |
| 77 | + try { |
| 78 | + var reply = JSON.parse(incoming.data); |
| 79 | + if (reply[0] === "OK" && reply[1] === event.id) done(Boolean(reply[2]), String(reply[3] || "")); |
| 80 | + } catch (_) {} |
| 81 | + }); |
| 82 | + ws.addEventListener("error", function () { done(false, "connection failed") }); |
| 83 | + ws.addEventListener("close", function () { done(false, "closed without an acknowledgement") }); |
| 84 | + }); |
| 85 | + } |
| 86 | +
|
| 87 | + publish.addEventListener("click", async function () { |
| 88 | + var content = message.value.trim(); |
| 89 | + if (!content) { status.textContent = "write something first"; message.focus(); return } |
| 90 | + publish.disabled = true; |
| 91 | + status.textContent = "waiting for your signer…"; |
| 92 | + try { |
| 93 | + if (!window.nostr) throw new Error("no Nostr signer is available in this browser"); |
| 94 | + var pubkey = await window.nostr.getPublicKey(); |
| 95 | + var event = await window.nostr.signEvent({ |
| 96 | + kind: 1, |
| 97 | + created_at: Math.floor(Date.now() / 1000), |
| 98 | + tags: [], |
| 99 | + content: content, |
| 100 | + pubkey: pubkey |
| 101 | + }); |
| 102 | + status.textContent = "signed—publishing to relays…"; |
| 103 | + var results = await Promise.all(RELAYS.map(function (relay) { return sendToRelay(relay, event) })); |
| 104 | + var accepted = results.filter(function (result) { return result.ok }); |
| 105 | + if (!accepted.length) { |
| 106 | + var reasons = results.map(function (result) { return result.relay + ": " + result.detail }).join(" · "); |
| 107 | + throw new Error("no relay accepted the event (" + reasons + ")"); |
| 108 | + } |
| 109 | + status.textContent = "published to " + accepted.length + "/" + RELAYS.length + " relays 🤘"; |
| 110 | + publish.textContent = "Published"; |
| 111 | + } catch (error) { |
| 112 | + status.textContent = error && error.message ? error.message : String(error); |
| 113 | + publish.disabled = false; |
| 114 | + } |
| 115 | + }); |
| 116 | + })(); |
| 117 | + </script>`; |
| 118 | + |
| 119 | + return page({ title: "moshcode ▸ post to Nostr", body }); |
| 120 | +} |
| 121 | + |
| 122 | +socialsRouter.get("/socials/nostr", (_req, res) => { |
| 123 | + res.type("html").send(nostrComposerPage()); |
| 124 | +}); |
0 commit comments