import { CLIENT_ID } from "./config.js"; import { signIn, restoreSession, clearSession, isConfigured, } from "./goostr.js"; import { pool } from "./relay.js"; const $ = (id) => document.getElementById(id); const views = ["loading", "signin", "home"]; function show(view) { for (const v of views) $(`view-${v}`).classList.toggle("hidden", v !== view); } function setStatus(id, msg, kind = "") { const el = $(id); el.textContent = msg; el.className = "status" + (kind ? " " + kind : ""); } let signer = null; let currentProfile = null; let publishedProfileTs = 0; // most-recent kind-0 we've published or rendered const DEFAULT_AVATAR = `data:image/svg+xml;utf8,${encodeURIComponent( ` 👤 ` )}`; // ---------- sign-in ---------- $("signin-go").addEventListener("click", async () => { $("signin-go").disabled = true; try { const { signer: s, profile, isNewIdentity } = await signIn({ clientId: CLIENT_ID, onStatus: (msg) => setStatus("signin-status", msg), }); signer = s; currentProfile = profile; setStatus("signin-status", "Connecting to relays…"); const unsubStatus = pool.onStatus((snap) => { const open = Object.values(snap).filter((s) => s === "open").length; setStatus("signin-status", `Connecting to relays… ${open}/${Object.keys(snap).length}`); }); await pool.connectAll({ minRelays: 1 }); unsubStatus(); if (isNewIdentity) { // First time: publish a kind-0 with the Google name + picture so this // user shows up properly on nostr. publishProfileEvent(profile); } enterHome(profile); } catch (err) { console.error(err); setStatus("signin-status", err.message || String(err), "error"); $("signin-go").disabled = false; } }); $("sign-out").addEventListener("click", () => { clearSession(); signer = null; location.reload(); }); // ---------- home / live profile / notes ---------- function publishProfileEvent(profile) { const event = signer.sign({ kind: 0, content: JSON.stringify({ name: profile.name, picture: profile.picture, // `nip05` would be nice here but we don't host one }), tags: [], }); pool.publish(event); publishedProfileTs = event.created_at; } function enterHome(profile) { $("me-name").textContent = profile.name || "anonymous"; $("me-pic").src = profile.picture || DEFAULT_AVATAR; $("me-npub").textContent = signer.npub; show("home"); pool.onStatus(renderRelayDots); startLiveProfile(signer.pubkey, profile); loadNotes(); } function renderRelayDots(snap) { const host = $("relay-dots"); host.innerHTML = ""; for (const [url, state] of Object.entries(snap)) { const dot = document.createElement("span"); dot.className = `relay-dot ${state}`; dot.title = `${url} — ${state}`; host.appendChild(dot); } } function safeJSON(s) { try { return JSON.parse(s); } catch { return {}; } } function startLiveProfile(pubkey, googleProfile) { // Long-lived sub for the latest kind-0. If a relay has a newer event than // what's on screen, upgrade. If no relay returns anything within a few // seconds, our google-side profile may not have been published anywhere — // republish to make sure it lands. let sawOne = false; pool.subscribe( [{ kinds: [0], authors: [pubkey] }], (ev) => { sawOne = true; if (ev.created_at <= publishedProfileTs) return; const p = safeJSON(ev.content); publishedProfileTs = ev.created_at; if (p.name) $("me-name").textContent = p.name; if (p.picture) $("me-pic").src = p.picture; }, null ); setTimeout(() => { if (!sawOne && googleProfile?.name) { // No relay had our profile. Publish it now. publishProfileEvent(googleProfile); } }, 6000); } let notesById = new Map(); function loadNotes() { const tbody = $("notes-table").querySelector("tbody"); tbody.innerHTML = ""; setStatus("notes-status", "Loading…"); notesById = new Map(); pool.subscribe( [{ kinds: [1], authors: [signer.pubkey], limit: 50 }], (ev) => { if (notesById.has(ev.id)) return; notesById.set(ev.id, ev); renderRows([...notesById.values()], tbody); setStatus("notes-status", ""); }, null ); setTimeout(() => { if (notesById.size === 0) { setStatus("notes-status", "No notes yet. Publish one above."); } }, 8000); } function renderRows(rows, tbody) { const sorted = [...rows].sort((a, b) => b.created_at - a.created_at); tbody.innerHTML = ""; for (const ev of sorted) { const tr = document.createElement("tr"); const t = document.createElement("td"); t.textContent = new Date(ev.created_at * 1000).toLocaleString(); const c = document.createElement("td"); c.textContent = ev.content; tr.appendChild(t); tr.appendChild(c); tbody.appendChild(tr); } } $("note-send").addEventListener("click", () => { const text = $("note-text").value.trim(); if (!text) return; $("note-send").disabled = true; const event = signer.sign({ kind: 1, content: text, tags: [] }); pool.publish(event); $("note-text").value = ""; $("note-send").disabled = false; notesById.set(event.id, event); renderRows([...notesById.values()], $("notes-table").querySelector("tbody")); setStatus("notes-status", ""); }); // ---------- boot ---------- (async function boot() { if (!isConfigured(CLIENT_ID)) { $("setup-banner").classList.remove("hidden"); show("signin"); setStatus("signin-status", "Configure CLIENT_ID in public/config.js first.", "error"); $("signin-go").disabled = true; return; } const restored = restoreSession(); if (restored) { signer = restored.signer; currentProfile = restored.profile; show("loading"); $("loading-text").textContent = "Welcome back. Connecting to relays…"; pool.onStatus(renderRelayDots); await pool.connectAll({ minRelays: 1 }); enterHome(restored.profile); return; } show("signin"); })();