// 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 = `
`;
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]));
}