Silent Mode Mail is no longer an extension. The 0.1.0 panel was a black
screen (iframe to mail.silentmode.st which sends X-Frame-Options: DENY;
the browser rendered the block page and still fired the iframe's load
event, so the 5-second "did it embed?" watchdog saw it as loaded and the
fallback never swapped in). The whole extension existed to put one "Open
Mail" button in a panel — that is exactly what a sidebar quicklink does,
without taking a panel slot or an Extensions row. So:
- settings.quickLinks default list gains silentmail at the top
(Silent Mode Mail → https://mail.silentmode.st/).
- migrateAegisRename grew a mail retirement block: any existing
extensions/mail/ folder on the user's box moves to
addons-backups/mail-migrated-<stamp>/ on first boot of this
Theseus, so the old extension vanishes from Settings instead of
sitting there with a broken panel.
- "mail" added to the first-party reserved-ids list so no community
add-on can claim the id after retirement.
Nostr NIP-07 signer finally ships. The bundled-addons/nostr/ folder was
sitting untracked in the main checkout, so the detached-HEAD build
worktree 0.3.130 used never saw it (same reason build/, tor/, snapshots/
have to be hand-copied into the worktree). This build committed the
folder and copied it in. Puts window.nostr on https + localhost pages;
identity is Hermes's key (one npub for DMs and public notes). Per-origin
read/sign grants; kinds 0/3/5/10002 re-prompt every time with no
remember option.
Sidebar panel apps free their render tree after idle. closeLeftPanel
used to keep the loaded page warm forever — great for fast reopen, bad
for RAM when the user docks a heavy panel (Hermes, Pithos, Aegis) once
and never comes back. A 2-minute idle timer now blanks the panel to
about:blank after close; reopen within the window still reuses the
loaded page (cancelLeftPanelIdleUnload on open), reopen after the
window reloads fresh. LEFT_PANEL_IDLE_MS = 2 * 60 * 1000. Right-side
sidebar view (webapp tabs — Telegram, X, WhatsApp, AI chat) is longer-
lived by design and untouched here.
Startpage layout:
- Game.X takes SilentMode.X's slot in the Silent Mode cards (stub
description + a built-in gamepad brand mark until game.x goes
live). SilentMode.X moves to the quick-links strip as the 5th
slot. Still TODO: push the swap into the remote home-cards.json
for existing profiles.
- Light-theme palette flip under body.light. The @media
(prefers-color-scheme: light) block only triggered on OS-level
light preference; picking the "light" background preset left --ink
white against a white page, so hero titles, card titles, slot
labels and the ".bch" extension read as pale ghosts. Added a full
body.light { --ink; --mut; --card; --line; ... } block plus
overrides for the hero + card translucent panels (dark-glass →
light-glass on light theme) and the ctx-menu / modal. Dark theme
untouched.
156 lines
6.9 KiB
HTML
156 lines
6.9 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Nostr</title>
|
|
<style>
|
|
:root { color-scheme: light dark;
|
|
--bg:#0e131c; --panel:#141a24; --line:rgba(255,255,255,.09);
|
|
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d;
|
|
--btn:#1c2432; --btn-h:#242e40; --err:#ff9081; --purple:#8e5cf7; }
|
|
@media (prefers-color-scheme: light) {
|
|
:root { --bg:#f8faff; --panel:#ffffff; --line:rgba(0,0,0,.10);
|
|
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5;
|
|
--btn:#f0f3fa; --btn-h:#e3e8f2; --acid:#088A66; --err:#c81f0f; --purple:#6d28d9; }
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html, body { margin: 0; height: 100%; }
|
|
body { background: var(--bg); color: var(--ink);
|
|
font: 13px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
|
display: flex; flex-direction: column; }
|
|
header { display: flex; align-items: center; gap: 8px;
|
|
padding: 10px 14px; border-bottom: 1px solid var(--line); background: var(--panel); }
|
|
header .t { font-weight: 600; display: flex; gap: 8px; align-items: center; }
|
|
header .t svg { width: 16px; height: 16px; display: block; }
|
|
header .m { color: var(--dim); font-size: 11.5px; flex: 1; text-align: right; }
|
|
main { flex: 1; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 14px; }
|
|
h2 { font-size: 11px; text-transform: uppercase; letter-spacing: .6px;
|
|
color: var(--dim); margin: 0 0 6px; font-weight: 600; }
|
|
.card { border: 1px solid var(--line); border-radius: 10px; background: var(--panel); padding: 12px; }
|
|
.key { font-family: ui-monospace, Consolas, monospace; font-size: 11.5px;
|
|
word-break: break-all; color: var(--ink); }
|
|
.muted { color: var(--mut); font-size: 12px; }
|
|
.warn { color: var(--err); }
|
|
button { border: 1px solid var(--line); border-radius: 7px; background: var(--btn);
|
|
color: var(--ink); cursor: pointer; font: inherit; padding: 6px 10px; }
|
|
button:hover:not(:disabled) { background: var(--btn-h); }
|
|
button:disabled { opacity: .45; cursor: default; }
|
|
button.sm { font-size: 11.5px; padding: 4px 8px; }
|
|
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
.spread { justify-content: space-between; }
|
|
table { width: 100%; border-collapse: collapse; }
|
|
td { padding: 7px 0; border-top: 1px solid var(--line); vertical-align: middle; }
|
|
tr:first-child td { border-top: 0; }
|
|
td.o { word-break: break-all; }
|
|
.lvl { font-size: 10.5px; color: var(--dim); }
|
|
.empty { color: var(--dim); font-size: 12px; }
|
|
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--purple); flex: none; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<div class="t">
|
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
<rect x="1" y="1" width="22" height="22" rx="5.5" fill="#8e5cf7"/>
|
|
<circle cx="9.3" cy="9.3" r="3.6" fill="none" stroke="#fff" stroke-width="2.1"/>
|
|
<path d="M11.9 11.9 19 19M16.2 16.2l-2 2M18.1 18.1l-1.6 1.6" stroke="#fff" stroke-width="2.1" stroke-linecap="round"/>
|
|
</svg>
|
|
<span>Nostr</span>
|
|
</div>
|
|
<div class="m" id="hdr"></div>
|
|
</header>
|
|
<main>
|
|
<div>
|
|
<h2>Your identity</h2>
|
|
<div class="card" id="idCard"><span class="muted">Checking…</span></div>
|
|
</div>
|
|
|
|
<div>
|
|
<h2>Sites using your key</h2>
|
|
<div class="card" id="grants"><span class="empty">Loading…</span></div>
|
|
</div>
|
|
|
|
<div class="muted">
|
|
Theseus signs with the same key Hermes uses, so a note you post and a message
|
|
someone sends you belong to one identity. Your key never leaves Theseus — sites
|
|
ask it to sign and get back only the signature.
|
|
</div>
|
|
</main>
|
|
<script>
|
|
(() => {
|
|
"use strict";
|
|
const S = window.silentmode;
|
|
const $ = (id) => document.getElementById(id);
|
|
const el = (tag, props, ...kids) => {
|
|
const n = Object.assign(document.createElement(tag), props || {});
|
|
for (const k of kids.flat()) if (k != null && k !== false) n.append(k);
|
|
return n;
|
|
};
|
|
const put = (node, ...kids) => { node.textContent = ""; for (const k of kids.flat()) if (k != null && k !== false) node.append(k); };
|
|
|
|
async function copy(text, msg) {
|
|
try { await navigator.clipboard.writeText(text); flash(msg || "Copied"); }
|
|
catch { flash("Could not copy"); }
|
|
}
|
|
let flashT = null;
|
|
function flash(t) {
|
|
$("hdr").textContent = t;
|
|
clearTimeout(flashT);
|
|
flashT = setTimeout(() => { $("hdr").textContent = ""; }, 1600);
|
|
}
|
|
|
|
async function drawIdentity() {
|
|
let st;
|
|
try { st = await S.invoke("status", {}); }
|
|
catch (e) { put($("idCard"), el("span", { className: "warn", textContent: e?.message || String(e) })); return; }
|
|
|
|
if (!st || !st.ready) {
|
|
put($("idCard"),
|
|
el("div", { className: "muted", textContent: st?.reason || "No Nostr identity yet." }));
|
|
return;
|
|
}
|
|
put($("idCard"),
|
|
el("div", { className: "row spread" },
|
|
el("div", { className: "row" }, el("span", { className: "dot" }), el("strong", { textContent: "Connected" })),
|
|
el("button", { className: "sm", textContent: "Copy npub", onclick: () => copy(st.npub, "npub copied") })),
|
|
el("div", { className: "key", style: "margin-top:8px", textContent: st.npub }),
|
|
el("div", { className: "muted", style: "margin-top:6px" },
|
|
"Shared with Hermes — the same identity signs your messages."));
|
|
}
|
|
|
|
async function drawGrants() {
|
|
let r;
|
|
try { r = await S.invoke("listGrants", {}); }
|
|
catch (e) { put($("grants"), el("span", { className: "warn", textContent: e?.message || String(e) })); return; }
|
|
const list = (r && r.grants) || [];
|
|
if (!list.length) {
|
|
put($("grants"), el("span", { className: "empty" },
|
|
"No site has been allowed yet. The first time one asks, Theseus will show you what it wants."));
|
|
return;
|
|
}
|
|
const rows = list.map((g) => el("tr", {},
|
|
el("td", { className: "o" },
|
|
el("div", { textContent: g.origin }),
|
|
el("div", { className: "lvl", textContent: g.level === "sign" ? "can sign and encrypt" : "can see your public key" })),
|
|
el("td", { style: "text-align:right;width:84px" },
|
|
el("button", { className: "sm", textContent: "Revoke", onclick: async () => {
|
|
try { await S.invoke("revoke", { origin: g.origin }); flash("Revoked"); refresh(); }
|
|
catch (e) { flash(e?.message || "Failed"); }
|
|
} }))));
|
|
put($("grants"),
|
|
el("table", {}, el("tbody", {}, rows)),
|
|
el("div", { className: "row", style: "margin-top:10px;justify-content:flex-end" },
|
|
el("button", { className: "sm", textContent: "Revoke all", onclick: async () => {
|
|
try { await S.invoke("revokeAll", {}); flash("All revoked"); refresh(); }
|
|
catch (e) { flash(e?.message || "Failed"); }
|
|
} })));
|
|
}
|
|
|
|
function refresh() { drawIdentity(); drawGrants(); }
|
|
refresh();
|
|
// The list changes when a site is allowed from a prompt in another tab.
|
|
document.addEventListener("visibilitychange", () => { if (!document.hidden) refresh(); });
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|