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.
116 lines
6.4 KiB
JavaScript
116 lines
6.4 KiB
JavaScript
// Preload for tab webContents that host the built-in home page. All non-
|
|
// settings tabs get this preload since we don't know in advance whether a
|
|
// tab will land on home.html; the corresponding IPC handlers in main.js
|
|
// validate the sender's URL is our own home.html file:// and reject any
|
|
// origin-mismatched call, so a third-party page can inspect the API's
|
|
// SHAPE but can't invoke it against local user data.
|
|
const { contextBridge, ipcRenderer } = require("electron");
|
|
// Web tabs run this preload in iframes too (nodeIntegrationInSubFrames), so
|
|
// the password hooks below see logins inside frames. Everything else here
|
|
// stays top-frame only, as it always was.
|
|
if (window.top === window) {
|
|
// A click in the page closes the left panel (web app / add-on) unless it is
|
|
// pinned. Main ignores this unless a panel is open and the tab is active.
|
|
window.addEventListener("pointerdown", () => { ipcRenderer.send("tab-pointerdown"); }, true);
|
|
contextBridge.exposeInMainWorld("home", {
|
|
getCards: () => ipcRenderer.invoke("home-cards-get"),
|
|
setCards: (cards) => ipcRenderer.invoke("home-cards-set", cards),
|
|
resetCards: () => ipcRenderer.invoke("home-cards-reset"),
|
|
navigate: (url) => ipcRenderer.invoke("navigate", url),
|
|
// Current default search engine — used to populate the search box
|
|
// placeholder with the actual engine name (e.g. "Search the web with
|
|
// Startpage") instead of hardcoding DuckDuckGo.
|
|
getEngines: () => ipcRenderer.invoke("search-engines"),
|
|
// Main pushes an updated list here after a successful remote refresh
|
|
// (only when the user has not customised locally). home.html re-renders.
|
|
onCards: (cb) => ipcRenderer.on("home-cards", (_e, list) => cb(list)),
|
|
// Current Theseus version — the startpage renders a small
|
|
// "Theseus v0.3.X — What's new" link beside the hero that points at
|
|
// the release notes. IPC is the same one Settings uses; no network.
|
|
getVersion: () => ipcRenderer.invoke("app-version"),
|
|
});
|
|
// Same preload also serves the branded error page (error.html). Handlers in
|
|
// main.js sender-check for error.html so a random page seeing the API shape
|
|
// can't drive navigation.
|
|
contextBridge.exposeInMainWorld("errorpage", {
|
|
retry: (url) => ipcRenderer.invoke("error-retry", url),
|
|
goHome: () => ipcRenderer.invoke("error-home"),
|
|
searchFor: (text) => ipcRenderer.invoke("error-search", text),
|
|
registerOnSirius: (host) => ipcRenderer.invoke("error-register", host),
|
|
openExternal: (url) => ipcRenderer.invoke("error-open-external", url),
|
|
});
|
|
}
|
|
|
|
// ---- Password manager hooks -------------------------------------------------
|
|
// Runs in this preload's isolated world in every frame of a web tab (a login
|
|
// form inside an iframe counts too); nothing is exposed to the page. Two
|
|
// reports go to main, which takes the site from the committed URL of the
|
|
// frame that sent them, never from here:
|
|
// pw-form a login field got focus -> main may offer saved logins under it
|
|
// pw-capture a form carrying a password was sent -> main may offer to save it
|
|
(() => {
|
|
if (!/^(https?|bns):$/.test(location.protocol)) return;
|
|
const visible = (el) => {
|
|
const r = el.getBoundingClientRect();
|
|
if (r.width < 4 || r.height < 4) return false;
|
|
const cs = getComputedStyle(el);
|
|
return cs.visibility !== "hidden" && cs.display !== "none";
|
|
};
|
|
const TEXTY = /^(text|email|tel|)$/i;
|
|
const passwords = (scope) => [...(scope || document).querySelectorAll("input[type=password]")].filter((el) => !el.disabled && visible(el));
|
|
// The username is the closest text-like field before the password in the
|
|
// same form (or page), which is how almost every login form is laid out.
|
|
function usernameFor(pw) {
|
|
const scope = pw.form || document;
|
|
const inputs = [...scope.querySelectorAll("input")].filter((el) => !el.disabled && visible(el));
|
|
const at = inputs.indexOf(pw);
|
|
for (let i = at - 1; i >= 0; i--) if (TEXTY.test(inputs[i].type || "text")) return inputs[i];
|
|
return null;
|
|
}
|
|
function capture() {
|
|
try {
|
|
const pws = passwords().filter((el) => el.value);
|
|
if (!pws.length) return;
|
|
// Sign-up: password + confirmation (same value). Change-password:
|
|
// current, new[, confirm] -> the new one is second.
|
|
const signup = pws.length >= 2 || /new-password/i.test(pws[0].autocomplete || "");
|
|
const pw = pws.length >= 3 ? pws[1] : pws.length === 2 && pws[0].value !== pws[1].value ? pws[1] : pws[0];
|
|
const user = usernameFor(pws[0]);
|
|
ipcRenderer.send("pw-capture", { username: user ? user.value : "", password: pw.value, signup });
|
|
} catch {}
|
|
}
|
|
document.addEventListener("submit", capture, true);
|
|
// Script-driven logins never fire submit: catch the button press and Enter.
|
|
document.addEventListener("click", (e) => {
|
|
const b = e.target instanceof Element ? e.target.closest("button, input[type=submit], input[type=button], [role=button]") : null;
|
|
if (b && passwords().some((el) => el.value)) capture();
|
|
}, true);
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter" && e.target instanceof HTMLInputElement && (e.target.type === "password" || TEXTY.test(e.target.type))) {
|
|
if (passwords().some((el) => el.value)) capture();
|
|
}
|
|
}, true);
|
|
|
|
// A focused username or password field of a login form (one password
|
|
// field, empty) asks main to show saved logins under it.
|
|
function loginField(el) {
|
|
if (!(el instanceof HTMLInputElement) || el.disabled || el.readOnly) return null;
|
|
if (el.type === "password") return passwords(el.form || document).length === 1 ? el : null;
|
|
if (!TEXTY.test(el.type || "text")) return null;
|
|
const pws = passwords(el.form || document);
|
|
return pws.length === 1 && usernameFor(pws[0]) === el ? el : null;
|
|
}
|
|
function offer(el) {
|
|
if (!el || el.value) return;
|
|
const r = el.getBoundingClientRect();
|
|
ipcRenderer.send("pw-form", { x: r.left, y: r.top, h: r.height });
|
|
}
|
|
document.addEventListener("focusin", (e) => offer(loginField(e.target)), true);
|
|
// Typing means the user is not taking the offer.
|
|
document.addEventListener("input", (e) => { if (loginField(e.target)) ipcRenderer.send("pw-form-dismiss"); }, true);
|
|
document.addEventListener("keydown", (e) => { if (e.key === "Escape") ipcRenderer.send("pw-form-dismiss"); }, true);
|
|
// An autofocused field is already focused when this runs.
|
|
const early = () => offer(loginField(document.activeElement));
|
|
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", early, { once: true });
|
|
else early();
|
|
})();
|