Two related visibility fixes: 1) Light-mode --acid → #0AC18E (Bitcoin Cash brand primary, from whybitcoincash.com's palette per user). Direct swap from #088A66 (darkened variant) to the on-brand primary. Applied across chrome / settings / error / home / approval / messages / bchwallet / siawallet / screenshot editor. Dark mode's #d6ff3d is unchanged. 2) User-Agent no longer includes 'theseus-navigator/<ver>' or 'Electron/<ver>' tokens. Cloudflare's WAF was returning HTTP 503 'Service Unavailable' to any request carrying those (verified directly against whybitcoincash.com — same URL, same headers, only the UA differed; plain Chrome UA got 200, Theseus UA got 503). Strip both tokens via a stockChromeUA() helper called from applyAcceptLanguage(), which whenReady already invokes at boot. Standard practice: Brave, Vivaldi, Slack all do the same. Verified via CDP: navigator.userAgent now reports Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.191 Safari/537.36 — indistinguishable from stock Chrome.
242 lines
9.3 KiB
HTML
242 lines
9.3 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Messages — Theseus</title>
|
|
<style>
|
|
:root{
|
|
--bg:#0b0e14; --panel:#141a24; --panel2:#18202c; --line:rgba(255,255,255,.09);
|
|
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d;
|
|
--ok:#4fd1a5; --err:#f6768a; --warn:#ffc75f;
|
|
}
|
|
@media (prefers-color-scheme: light) {
|
|
:root{
|
|
--bg:#f6f8fb; --panel:#ffffff; --panel2:#f0f4f9; --line:rgba(0,0,0,.10);
|
|
--ink:#1a1f28; --mut:#3c4453; --dim:#697280;
|
|
/* Darker acid for light backgrounds — ~5.5:1 on white. */
|
|
--acid: #0AC18E;
|
|
}
|
|
}
|
|
*{box-sizing:border-box}
|
|
html,body{height:100%}
|
|
body{
|
|
margin:0; background:var(--bg); color:var(--ink);
|
|
font:14px/1.5 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
|
|
display:flex; flex-direction:column;
|
|
}
|
|
header{
|
|
padding:12px 16px; border-bottom:1px solid var(--line);
|
|
background:rgba(11,14,20,.9); display:flex; align-items:center; gap:12px;
|
|
flex-wrap:wrap;
|
|
}
|
|
header h1{font-size:14px;font-weight:600;margin:0;letter-spacing:.2px}
|
|
header .mark{font-size:20px}
|
|
#ident{
|
|
color:var(--mut); font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
|
|
font-size:12px; margin-left:auto; user-select:text;
|
|
}
|
|
#ident b{color:var(--acid); font-weight:500}
|
|
#status{
|
|
font-size:11px; padding:2px 8px; border-radius:999px;
|
|
background:var(--panel); color:var(--mut);
|
|
}
|
|
#status.ok{color:var(--ok); background:rgba(79,209,165,.12)}
|
|
#status.err{color:var(--err); background:rgba(246,118,138,.12)}
|
|
|
|
main{display:grid; grid-template-columns:1fr; gap:0; flex:1; min-height:0}
|
|
#setup, #app{padding:16px; overflow:auto}
|
|
#setup{display:none; max-width:520px; margin:auto; align-self:center}
|
|
#app{display:none; grid-template-rows:1fr auto; gap:12px}
|
|
body.mode-setup #setup{display:block}
|
|
body.mode-app #app{display:grid}
|
|
|
|
#inbox{
|
|
overflow:auto; background:var(--panel); border:1px solid var(--line);
|
|
border-radius:10px; padding:8px;
|
|
}
|
|
.msg{padding:8px 10px; border-radius:8px; margin-bottom:6px; background:var(--panel2)}
|
|
.msg .from{font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:11px; color:var(--mut); margin-bottom:3px}
|
|
.msg .from b{color:var(--acid); font-weight:500}
|
|
.msg .body{white-space:pre-wrap; word-break:break-word}
|
|
.msg .time{float:right; color:var(--dim); font-size:11px}
|
|
.empty{color:var(--dim); text-align:center; padding:24px 0}
|
|
|
|
#compose{display:grid; grid-template-columns:200px 1fr auto; gap:8px}
|
|
input, textarea, button{
|
|
font:inherit; color:var(--ink); background:var(--panel);
|
|
border:1px solid var(--line); border-radius:8px; padding:10px 12px;
|
|
}
|
|
textarea{resize:vertical; min-height:44px; max-height:200px}
|
|
input:focus, textarea:focus{outline:none; border-color:var(--acid)}
|
|
button{cursor:pointer; background:var(--acid); color:#0a0d13; font-weight:600; border-color:transparent}
|
|
button:hover{filter:brightness(1.05)}
|
|
button:disabled{opacity:.5; cursor:default}
|
|
|
|
.lede{color:var(--mut); font-size:13px; margin:0 0 12px}
|
|
#setup label{display:block; margin-bottom:12px; color:var(--mut); font-size:12px}
|
|
#setup textarea{width:100%; margin-top:6px; min-height:80px}
|
|
.row{display:flex; gap:8px; align-items:center; margin-top:8px}
|
|
#setupErr{color:var(--err); font-size:12px; min-height:1em; margin-top:8px}
|
|
#sendErr{color:var(--err); font-size:12px; min-height:1em; margin-top:4px}
|
|
.warn{
|
|
font-size:12px; color:var(--warn); padding:8px 10px;
|
|
background:rgba(255,199,95,.08); border:1px solid rgba(255,199,95,.24);
|
|
border-radius:8px; margin-bottom:12px;
|
|
}
|
|
code.inline{
|
|
font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
|
|
background:rgba(255,255,255,.05); padding:1px 5px; border-radius:4px; font-size:12px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="mode-setup">
|
|
|
|
<header>
|
|
<span class="mark">⛓️✉️</span>
|
|
<h1>Hermes Messages</h1>
|
|
<span id="status">connecting…</span>
|
|
<span id="ident"></span>
|
|
</header>
|
|
|
|
<main>
|
|
|
|
<section id="setup">
|
|
<p class="lede">Hermes derives its Nostr identity at
|
|
<code class="inline">silentmode/messenger/0</code> — a purpose disjoint from your
|
|
wallet's transaction key, so a compromised relay never yields tx-signing power.
|
|
Nothing is stored on disk beyond the vault; close the panel and the identity is dropped.</p>
|
|
<div class="warn">This is a proof build. Use a wallet you don't hold funds in.</div>
|
|
|
|
<div id="vaultBind" style="display:none; margin-bottom:16px">
|
|
<div class="row">
|
|
<button id="btnVault">Sign in with password vault</button>
|
|
<span style="color:var(--mut); font-size:12px">recommended — no mnemonic to re-type</span>
|
|
</div>
|
|
<div style="text-align:center; color:var(--dim); font-size:12px; margin:14px 0">— or —</div>
|
|
</div>
|
|
|
|
<label>
|
|
Mnemonic (12 or 24 words)
|
|
<textarea id="mnemonic" placeholder="word word word …" autocomplete="off" spellcheck="false"></textarea>
|
|
</label>
|
|
<div class="row">
|
|
<button id="btnInit">Open messages</button>
|
|
</div>
|
|
<div id="setupErr"></div>
|
|
</section>
|
|
|
|
<section id="app">
|
|
<div id="inbox"><div class="empty">no messages yet — subscribed to inbox…</div></div>
|
|
<div>
|
|
<div id="compose">
|
|
<input id="to" placeholder="to (e.g. alice.bch or 64-char hex pk)" autocomplete="off">
|
|
<textarea id="text" placeholder="message" rows="1"></textarea>
|
|
<button id="btnSend">Send</button>
|
|
</div>
|
|
<div id="sendErr"></div>
|
|
</div>
|
|
</section>
|
|
|
|
</main>
|
|
|
|
<script>
|
|
const $ = (id) => document.getElementById(id);
|
|
const setMode = (m) => { document.body.className = "mode-" + m; };
|
|
const setStatus = (text, cls = "") => {
|
|
const el = $("status");
|
|
el.textContent = text;
|
|
el.className = cls;
|
|
};
|
|
const shortPk = (pk) => pk ? pk.slice(0, 8) + "…" + pk.slice(-4) : "";
|
|
// Hermes messaging is a .bch-first product — the default TLD is noise in the
|
|
// chat list. Strip it for display; other TLDs stay (they disambiguate).
|
|
const displayName = (name) => name && name.endsWith(".bch") ? name.slice(0, -4) : name;
|
|
|
|
async function refreshInbox() {
|
|
const res = await hermes.inbox();
|
|
if (!res.ok) return;
|
|
const box = $("inbox");
|
|
if (!res.messages.length) {
|
|
box.innerHTML = '<div class="empty">no messages yet — subscribed to inbox…</div>';
|
|
return;
|
|
}
|
|
box.innerHTML = "";
|
|
for (const m of res.messages) {
|
|
const el = document.createElement("div");
|
|
el.className = "msg";
|
|
const from = m.senderName ? `<b>${displayName(m.senderName)}</b> · ${shortPk(m.senderPkHex)}` : shortPk(m.senderPkHex);
|
|
const when = new Date(m.createdAt * 1000).toLocaleTimeString();
|
|
el.innerHTML = `<div class="from">${from}<span class="time">${when}</span></div>` +
|
|
`<div class="body"></div>`;
|
|
el.querySelector(".body").textContent = m.text; // never innerHTML for user content
|
|
box.appendChild(el);
|
|
}
|
|
box.scrollTop = box.scrollHeight;
|
|
}
|
|
|
|
function afterInit(res) {
|
|
if (!res.ok) { $("setupErr").textContent = "error: " + res.err; return false; }
|
|
$("ident").innerHTML = "your npub: <b>" + res.npub.slice(0, 12) + "…" + res.npub.slice(-6) + "</b>"
|
|
+ (res.source ? " <span style='color:var(--dim); font-size:11px'>(" + res.source + ")</span>" : "");
|
|
setStatus(res.relaysConnected + "/" + res.relaysTotal + " relays",
|
|
res.relaysConnected > 0 ? "ok" : "");
|
|
setMode("app");
|
|
refreshInbox();
|
|
return true;
|
|
}
|
|
|
|
$("btnInit").addEventListener("click", async () => {
|
|
const mnemonic = $("mnemonic").value.trim();
|
|
if (!mnemonic) { $("setupErr").textContent = "enter a mnemonic"; return; }
|
|
$("btnInit").disabled = true;
|
|
$("setupErr").textContent = "";
|
|
const res = await hermes.init(mnemonic);
|
|
$("btnInit").disabled = false;
|
|
afterInit(res);
|
|
});
|
|
|
|
$("btnVault").addEventListener("click", async () => {
|
|
$("btnVault").disabled = true;
|
|
$("setupErr").textContent = "";
|
|
const res = await hermes.initFromVault();
|
|
$("btnVault").disabled = false;
|
|
afterInit(res);
|
|
});
|
|
|
|
$("btnSend").addEventListener("click", async () => {
|
|
const to = $("to").value.trim();
|
|
const text = $("text").value.trim();
|
|
if (!to || !text) { $("sendErr").textContent = "to and message required"; return; }
|
|
$("btnSend").disabled = true;
|
|
$("sendErr").textContent = "";
|
|
const res = await hermes.send(to, text);
|
|
$("btnSend").disabled = false;
|
|
if (!res.ok) { $("sendErr").textContent = "error: " + res.err; return; }
|
|
$("sendErr").textContent = "sent to " + res.accepted + "/" + res.total + " relay(s) — recipient pk " + shortPk(res.recipientPkHex);
|
|
$("text").value = "";
|
|
});
|
|
|
|
hermes.onMessage(() => refreshInbox());
|
|
hermes.onStatus((s) => {
|
|
setStatus(s.relaysConnected + "/" + s.relaysTotal + " relays",
|
|
s.relaysConnected > 0 ? "ok" : "err");
|
|
});
|
|
|
|
// Query status on load: maybe already initialised from a previous open.
|
|
hermes.status().then(async (r) => {
|
|
if (r && r.ok && r.ready) {
|
|
$("ident").innerHTML = "your npub: <b>" + r.npub.slice(0, 12) + "…" + r.npub.slice(-6) + "</b>";
|
|
setStatus(r.relaysConnected + "/" + r.relaysTotal + " relays", "ok");
|
|
setMode("app");
|
|
refreshInbox();
|
|
return;
|
|
}
|
|
setStatus("locked", "");
|
|
// Show the vault sign-in shortcut iff the vault is currently unlocked AND
|
|
// was set up from a mnemonic (so it carries the messenger root).
|
|
const v = await hermes.canUseVault();
|
|
if (v && v.ok && v.available) $("vaultBind").style.display = "block";
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|