217 lines
6.1 KiB
JavaScript
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");
|
|
})();
|