// src/ui/mesh-panel.js // PeerJS mesh UI: join a room, see peers, chat, and share news signals // with other browsers over the internet. import { createMesh } from "../mesh/browsermesh.js"; export function mountMeshPanel(root, { onShareSignals } = {}) { let mesh = null; const chatLog = []; root.innerHTML = `
Browser mesh offline
id: —
Peers (0)
No peers yet. Open this page in another tab/device and join the same room.
Mesh chat
`; const $ = (s) => root.querySelector(s); const statusEl = $("[data-mesh-status]"); const idEl = $("[data-id]"); const peersEl = $("[data-peers]"); const peerCount = $("[data-peer-count]"); const chatEl = $("[data-chat]"); function setStatus(text, cls = "") { statusEl.textContent = text; statusEl.className = `pill ${cls}`; } function renderPeers(peers) { peerCount.textContent = peers.length; peersEl.innerHTML = peers.length ? peers.map((p) => `
${esc(p.name)} ${p.id.slice(0, 10)}
`).join("") : "No peers yet."; } function pushChat(who, text, cls = "") { chatLog.push({ who, text, cls }); chatEl.innerHTML = chatLog.slice(-100).map((m) => `
${esc(m.who)}: ${esc(m.text)}
`).join(""); chatEl.scrollTop = chatEl.scrollHeight; } function join() { const room = $("[data-room]").value.trim() || "hearthnet"; const name = $("[data-name]").value.trim() || `peer-${Math.random().toString(36).slice(2, 6)}`; setStatus("connecting…", "warn"); mesh = createMesh({ room, name, onStatus: (st) => { if (st.error) setStatus(`error: ${st.error}`, "err"); else setStatus(st.isAnchor ? "online · anchor" : "online", "ok"); idEl.textContent = `id: ${st.selfId || "—"} · anchor: ${st.anchorId}`; }, onPeers: renderPeers, onMessage: ({ fromName, type, payload }) => { if (type === "chat") pushChat(fromName || "peer", payload?.text || ""); else if (type === "signals") { const names = (payload?.signals || []).map((s) => s.name).join(", "); pushChat(fromName || "peer", `shared active signals: ${names}`, "sys"); onShareSignals?.(payload?.signals || [], fromName); } }, }); mesh.join(); $("[data-join]").disabled = true; $("[data-leave]").disabled = false; pushChat("system", `joined room "${room}" as ${name}`, "sys"); } function leave() { mesh?.leave(); mesh = null; renderPeers([]); setStatus("offline"); $("[data-join]").disabled = false; $("[data-leave]").disabled = true; pushChat("system", "left mesh", "sys"); } function sendMsg() { const inp = $("[data-msg]"); const text = inp.value.trim(); if (!text || !mesh) return; mesh.broadcast("chat", { text }); pushChat("you", text, "self"); inp.value = ""; } $("[data-join]").onclick = join; $("[data-leave]").onclick = leave; $("[data-send]").onclick = sendMsg; $("[data-msg]").addEventListener("keydown", (e) => { if (e.key === "Enter") sendMsg(); }); return { shareSignals: (signals) => mesh?.broadcast("signals", { signals }), get mesh() { return mesh; }, }; } function esc(s) { return String(s || "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); }