feat(theseus/chrome): "Open link in new window" on the link context menu
A standalone page window on the same session (cookies, bns:// protocol, session-wide bcnr preload) with Theseus's fingerprint + WebRTC policy and no toolbar. Loads BCNR-first like a tab: a dotted host with a BCNR record goes over bns://, otherwise clearnet; collision names follow the configured policy without the "Open with…" interstitial. Cross-host navigations inside the window stay BCNR-first; popups go to the main window's tabs. Its own context menu offers open-in-tab / open-in-window / copy link and back/forward/reload. Add-on page bridges (wallet inject) are tab-scoped and don't run in these windows. openLinkWindow is exported for the test harness. Verified in the dev app: coinspectrum.x opened as bns://coinspectrum.x with the page title; navigate.st stayed https.
This commit is contained in:
parent
43d0021bf6
commit
ed48646c71
1 changed files with 87 additions and 1 deletions
88
main.js
88
main.js
|
|
@ -2686,6 +2686,7 @@ function createTab(initial, opts = {}) {
|
||||||
items.push(
|
items.push(
|
||||||
{ label: "Open link in new tab", click: () => createTab(p.linkURL) },
|
{ label: "Open link in new tab", click: () => createTab(p.linkURL) },
|
||||||
{ label: "Open link in new background tab", click: () => createTab(p.linkURL, { background: true }) },
|
{ label: "Open link in new background tab", click: () => createTab(p.linkURL, { background: true }) },
|
||||||
|
{ label: "Open link in new window", click: () => openLinkWindow(p.linkURL) },
|
||||||
{ label: "Copy link address", click: () => clipboard.writeText(p.linkURL) },
|
{ label: "Copy link address", click: () => clipboard.writeText(p.linkURL) },
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
);
|
);
|
||||||
|
|
@ -4760,6 +4761,91 @@ ipcMain.handle("hermes-send", async (_e, { to, text } = {}) => {
|
||||||
} catch (e) { return hErr(e); }
|
} catch (e) { return hErr(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- "Open link in new window" ----
|
||||||
|
// A standalone page window: same session (cookies, the bns:// protocol, the
|
||||||
|
// session-wide bcnr preload), Theseus's fingerprint + WebRTC policy, no
|
||||||
|
// toolbar. Loads BCNR-first like a tab does: a dotted host with a BCNR
|
||||||
|
// record goes over bns://, anything else over clearnet. Collision names
|
||||||
|
// follow the configured policy without the "Open with…" interstitial.
|
||||||
|
// Add-on page bridges (the wallet inject) are tab-scoped and don't run here.
|
||||||
|
const linkWindows = new Set();
|
||||||
|
async function targetUrlFor(input) {
|
||||||
|
let u;
|
||||||
|
try { u = new URL(String(input).includes("://") ? String(input) : "https://" + String(input)); } catch { return null; }
|
||||||
|
if (u.protocol !== "http:" && u.protocol !== "https:") return u.href;
|
||||||
|
const host = u.hostname.toLowerCase();
|
||||||
|
if (!isBnsHost(host)) return u.href;
|
||||||
|
let entry = null;
|
||||||
|
try { entry = await resolveHost(host); } catch {}
|
||||||
|
if (!entry) return u.href;
|
||||||
|
const policy = settings.collisionPolicy || "bcnr-first";
|
||||||
|
if (!isBcnrNativeTld(tldOf(host)) && policy === "icann-first") return u.href;
|
||||||
|
return `bns://${host}${u.pathname}${u.search}${u.hash}`;
|
||||||
|
}
|
||||||
|
async function openLinkWindow(input) {
|
||||||
|
const target = await targetUrlFor(input);
|
||||||
|
if (!target) return null;
|
||||||
|
const w = new BrowserWindow({
|
||||||
|
width: 1100, height: 760, title: "Theseus Navigator",
|
||||||
|
backgroundColor: nativeTheme.shouldUseDarkColors ? "#0b0e14" : "#ffffff",
|
||||||
|
icon: app.isPackaged ? path.join(process.resourcesPath, "icon.ico") : path.join(__dirname, "build", "icon.ico"),
|
||||||
|
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true },
|
||||||
|
});
|
||||||
|
w.setMenuBarVisibility(false);
|
||||||
|
const wc = w.webContents;
|
||||||
|
styleScrollbars(wc);
|
||||||
|
try { wc.setWebRTCIPHandlingPolicy(webrtcPolicy()); } catch {}
|
||||||
|
try { wc.setBackgroundThrottling(settings.backgroundThrottle); } catch {}
|
||||||
|
applyFingerprint(wc);
|
||||||
|
wc.on("page-title-updated", (_e, title) => { try { w.setTitle(title ? `${title} — Theseus` : "Theseus Navigator"); } catch {} });
|
||||||
|
// Cross-host navigations inside the window stay BCNR-first too. Same-host
|
||||||
|
// ones go through Chromium untouched (form POSTs must survive).
|
||||||
|
wc.on("will-navigate", (e, u) => {
|
||||||
|
try {
|
||||||
|
const p = new URL(u);
|
||||||
|
if (p.protocol !== "http:" && p.protocol !== "https:") return;
|
||||||
|
if (!isBnsHost(p.hostname)) return;
|
||||||
|
let cur = ""; try { cur = new URL(wc.getURL()).hostname; } catch {}
|
||||||
|
if (cur === p.hostname) return;
|
||||||
|
e.preventDefault();
|
||||||
|
targetUrlFor(u).then((t) => { if (t) wc.loadURL(t).catch(() => {}); });
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
// Popups from a page here go to the main window's tabs when it exists —
|
||||||
|
// one place for tabs — otherwise to another plain window.
|
||||||
|
wc.setWindowOpenHandler(({ url }) => {
|
||||||
|
if (url && url !== "about:blank") {
|
||||||
|
if (win && !win.isDestroyed()) { createTab(url); try { win.focus(); } catch {} }
|
||||||
|
else openLinkWindow(url);
|
||||||
|
}
|
||||||
|
return { action: "deny" };
|
||||||
|
});
|
||||||
|
wc.on("context-menu", (_e, p) => {
|
||||||
|
const items = [];
|
||||||
|
const haveMain = !!win && !win.isDestroyed();
|
||||||
|
if (p.linkURL) {
|
||||||
|
items.push(
|
||||||
|
{ label: "Open link in new tab", enabled: haveMain, click: () => { createTab(p.linkURL); try { win.focus(); } catch {} } },
|
||||||
|
{ label: "Open link in new window", click: () => openLinkWindow(p.linkURL) },
|
||||||
|
{ label: "Copy link address", click: () => clipboard.writeText(p.linkURL) },
|
||||||
|
{ type: "separator" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (p.isEditable) items.push({ role: "cut" }, { role: "copy" }, { role: "paste" }, { type: "separator" });
|
||||||
|
else if (p.selectionText) items.push({ role: "copy" }, { type: "separator" });
|
||||||
|
items.push(
|
||||||
|
{ label: "Back", enabled: wc.navigationHistory.canGoBack(), click: () => wc.navigationHistory.goBack() },
|
||||||
|
{ label: "Forward", enabled: wc.navigationHistory.canGoForward(), click: () => wc.navigationHistory.goForward() },
|
||||||
|
{ label: "Reload", click: () => wc.reload() },
|
||||||
|
);
|
||||||
|
Menu.buildFromTemplate(items).popup({ window: w });
|
||||||
|
});
|
||||||
|
linkWindows.add(w);
|
||||||
|
w.on("closed", () => linkWindows.delete(w));
|
||||||
|
wc.loadURL(target).catch(() => {});
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
function openHermesWindow() {
|
function openHermesWindow() {
|
||||||
if (hermesWin && !hermesWin.isDestroyed()) {
|
if (hermesWin && !hermesWin.isDestroyed()) {
|
||||||
hermesWin.focus(); return;
|
hermesWin.focus(); return;
|
||||||
|
|
@ -5056,4 +5142,4 @@ if (!process.env.THESEUS_NO_AUTOSTART) {
|
||||||
app.on("window-all-closed", () => { stopTor(); if (process.platform !== "darwin") app.quit(); });
|
app.on("window-all-closed", () => { stopTor(); if (process.platform !== "darwin") app.quit(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { serveBns, resolveHost, isBnsHost, nativeTld, dualTld, registryOf };
|
module.exports = { serveBns, resolveHost, isBnsHost, nativeTld, dualTld, registryOf, openLinkWindow };
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue