Googstr/public/app.js
Jason Tudisco 21093eb800 Initial commit: Goostr static site
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 08:37:10 -06:00

217 lines
6.1 KiB
JavaScript

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(
`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>
<rect width='64' height='64' rx='32' fill='#1e2128'/>
<text x='32' y='42' font-size='32' text-anchor='middle' font-family='sans-serif'>👤</text>
</svg>`
)}`;
// ---------- 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");
})();