fix: more resilient ipc for db operations, proper fallback handling

+ minor refactoring of files from api/ to shared/
+ adjusting wording of onboarding terms
+ inject menu button b/w the `settings & members` and the `new page` buttons
This commit is contained in:
dragonwocky 2023-08-03 18:25:16 +10:00
parent a88c74d1c3
commit 025bbca44c
Signed by: dragonwocky
GPG Key ID: 7998D08F7D7BD7A8
13 changed files with 107 additions and 133 deletions

View File

@ -37,14 +37,15 @@ const doThemeOverride = async (db) => {
</style>`);
};
const insertMenu = async (db) => {
const initMenu = async (db) => {
const notionSidebar = `.notion-sidebar-container .notion-sidebar > :nth-child(3) > div > :nth-child(2)`,
notionSettingsAndMembers = `${notionSidebar} > [role="button"]:nth-child(3)`,
{ html, addKeyListener, addMutationListener } = globalThis.__enhancerApi,
{ platform, enhancerUrl, onMessage } = globalThis.__enhancerApi,
menuButtonIconStyle = await db.get("menuButtonIconStyle"),
openMenuHotkey = await db.get("openMenuHotkey"),
renderPing = {
namespace: "notion-enhancer",
channel: "notion-enhancer",
hotkey: openMenuHotkey,
icon: menuButtonIconStyle,
};
@ -88,12 +89,15 @@ const insertMenu = async (db) => {
: " text-[16px]"}"
>notion-enhancer
<//>`;
document.body.append($modal);
addMutationListener(notionSidebar, () => {
if (document.contains($button)) return;
document.querySelector(notionSidebar)?.append($button);
});
document.querySelector(notionSidebar)?.append($button);
const insertMenu = () => {
if (!document.contains($modal)) document.body.append($modal);
if (!document.querySelector(notionSidebar)?.contains($button)) {
document.querySelector(notionSettingsAndMembers)?.after($button);
}
};
addMutationListener(notionSidebar, insertMenu);
insertMenu();
addMutationListener("body", sendThemePing);
window.addEventListener("focus", sendRenderPing);
@ -102,7 +106,7 @@ const insertMenu = async (db) => {
$modal.open();
});
window.addEventListener("message", (event) => {
if (event.data?.namespace !== "notion-enhancer") return;
if (event.data?.channel !== "notion-enhancer") return;
if (event.data?.action === "close-menu") $modal.close();
if (event.data?.action === "open-menu") $modal.open();
});
@ -115,7 +119,7 @@ export default async (api, db) => {
await Promise.all([
overrideThemes(db),
insertCustomStyles(db),
insertMenu(db),
initMenu(db),
sendTelemetryPing(),
]);
api.sendMessage("notion-enhancer", "load-complete");

View File

@ -29,12 +29,12 @@ function Onboarding() {
<${Description}>
In order for the notion-enhancer to function, it may access, collect,
process and/or store data on your device (including workspace content,
device metadata, and notion-enhancer configuration) according to its
privacy policy. Unless otherwise stated for telemetry purposes, the
notion-enhancer will never transmit any of your data from your device.
Telemetry can be disabled at any time through the menu.
<br />
<br />
device metadata, and notion-enhancer configuration) as described in its
privacy policy. Unless otherwise stated, the notion-enhancer will never
transmit your information from your device. Collection of anonymous
telemetry data is enabled by default and can be disabled at any time
through the menu.
<br /><br />
The notion-enhancer is free and open-source software distributed under
the <a href="${tsAndCs}#license">MIT License</a> without warranty of any
kind. In no event shall the authors be liable for any consequences of
@ -72,7 +72,7 @@ function Onboarding() {
>Check out the usage guide.
<//>
<${Tile}
href="https://notion-enhancer.github.io/getting-started/basic-usage/"
href="https://notion-enhancer.github.io/documentation/mods/"
icon="package-plus"
title="Something missing?"
>Build your own extension.

View File

@ -160,7 +160,7 @@ window.addEventListener("focus", () => {
setState({ focus: true, rerender: true });
});
window.addEventListener("message", (event) => {
if (event.data?.namespace !== "notion-enhancer") return;
if (event.data?.channel !== "notion-enhancer") return;
const [hotkey, theme, icon] = useState(["hotkey", "theme", "icon"]);
setState({
rerender: true,
@ -176,13 +176,13 @@ useState(["hotkey"], ([hotkey]) => {
setState({ hotkeyRegistered: true });
addKeyListener(hotkey, (event) => {
event.preventDefault();
const msg = { namespace: "notion-enhancer", action: "open-menu" };
const msg = { channel: "notion-enhancer", action: "open-menu" };
parent?.postMessage(msg, "*");
});
addKeyListener("Escape", () => {
const [popupOpen] = useState(["popupOpen"]);
if (!popupOpen) {
const msg = { namespace: "notion-enhancer", action: "close-menu" };
const msg = { channel: "notion-enhancer", action: "close-menu" };
parent?.postMessage(msg, "*");
} else setState({ rerender: true });
});
@ -199,7 +199,7 @@ useState(["rerender"], async () => {
// but extension:// pages can access chrome apis
// => notion-enhancer api is imported directly
if (typeof globalThis.__enhancerApi === "undefined") {
await import("../../api/system.js");
await import("../../shared/system.js");
// in electron this isn't necessary, as a) scripts are
// not running in an isolated execution context and b)
// the notion:// protocol csp bypass allows scripts to

View File

@ -14,15 +14,15 @@ const isElectron = () => {
};
if (isElectron()) {
require("./api/system.js");
require("./api/mods.js");
require("./shared/system.js");
require("./shared/registry.js");
const { enhancerUrl } = globalThis.__enhancerApi,
{ getMods, isEnabled, modDatabase } = globalThis.__enhancerApi;
// calling require("electron") in a process require()-d
// from these paths throws "websocket connection to __ failed"
// and triggers infinite loading => ignore for now, but
// requires further investigation later
// and triggers infinite loading => ignore for now, but will
// require further investigation later
const ignoredPaths = [
"shared/sqliteTypes",
"shared/TimeSource",
@ -37,7 +37,7 @@ if (isElectron()) {
module.exports = async (target, __exports, __eval) => {
if (ignoredPaths.includes(target)) return;
if (target === "main/main") require("./worker.js");
if (target.startsWith("main/")) require("./worker.js");
// clientStyles
// clientScripts
@ -63,9 +63,6 @@ if (isElectron()) {
}
};
} else {
// clientStyles
// clientScripts
import(chrome.runtime.getURL("/api/system.js")).then(() => {
import(chrome.runtime.getURL("/load.mjs"));
});
import(chrome.runtime.getURL("/shared/system.js")) //
.then(() => import(chrome.runtime.getURL("/load.mjs")));
}

View File

@ -20,10 +20,12 @@ export default (async () => {
import(enhancerUrl("vendor/twind.min.js")),
import(enhancerUrl("vendor/lucide.min.js")),
import(enhancerUrl("vendor/htm.min.js")),
import(enhancerUrl("api/events.js")),
import(enhancerUrl("api/mods.js")),
]);
await import(enhancerUrl("api/interface.js"));
await Promise.all([
import(enhancerUrl("shared/events.js")),
import(enhancerUrl("shared/registry.js")),
import(enhancerUrl("shared/markup.js")),
]);
const { getMods, isEnabled, modDatabase } = globalThis.__enhancerApi;
for (const mod of await getMods()) {

View File

@ -6,7 +6,8 @@
"use strict";
const IS_ELECTRON = typeof module !== "undefined";
const IS_ELECTRON = typeof module !== "undefined",
IS_RENDERER = IS_ELECTRON && process.type === "renderer";
// expected values: 'linux', 'win32', 'darwin' (== macos), 'firefox'
// and 'chromium' (inc. chromium-based browsers like edge and brave)
@ -26,16 +27,18 @@ const platform = IS_ELECTRON
IS_ELECTRON
? `notion://www.notion.so/__notion-enhancer/${target.replace(/^\//, "")}`
: chrome.runtime.getURL(target),
// should only be used from an electron main process, does nothing elsewhere
notionRequire = (target) => IS_ELECTRON && require(`../../../${target}`);
// require a file from the root of notion's app/ folder,
// only available in an electron main process
notionRequire = (target) =>
IS_ELECTRON && !IS_RENDERER ? require(`../../../${target}`) : undefined;
let __port;
const onMessage = (channel, listener) => {
// from worker to client
if (IS_ELECTRON) {
if (IS_RENDERER) {
const { ipcRenderer } = require("electron");
ipcRenderer.on(channel, listener);
} else {
} else if (!IS_ELECTRON) {
__port ??= chrome.runtime.connect();
__port.onMessage.addListener((msg) => {
if (msg?.channel !== channel || msg?.invocation) return;
@ -45,19 +48,21 @@ const onMessage = (channel, listener) => {
},
sendMessage = (channel, message) => {
// to worker from client
if (IS_ELECTRON) {
if (IS_RENDERER) {
const { ipcRenderer } = require("electron");
ipcRenderer.send(channel, message);
} else {
} else if (!IS_ELECTRON) {
__port ??= chrome.runtime.connect();
__port.postMessage({ channel, message });
}
},
invokeInWorker = (channel, message) => {
if (IS_ELECTRON) {
// sends a payload to the worker/main
// process and waits for a response
if (IS_RENDERER) {
const { ipcRenderer } = require("electron");
return ipcRenderer.invoke(channel, message);
} else {
} else if (!IS_ELECTRON) {
// polyfills the electron.ipcRenderer.invoke method in
// the browser: uses a long-lived ipc connection to
// pass messages and handle responses asynchronously
@ -98,40 +103,40 @@ const readFile = (file) => {
if (IS_ELECTRON) {
if (!file.startsWith("http")) {
const { resolve } = require("path");
return require(resolve(`${__dirname}/../${file}`), "utf-8");
return require(resolve(`${__dirname}/../${file}`));
}
const notionProtocol = "notion://www.notion.so/";
file = file.replace(/^https:\/\/www\.notion\.so\//, notionProtocol);
} else file = file.startsWith("http") ? file : enhancerUrl(file);
return fetch(file).then((res) => res.json());
},
reloadApp = () => {
if (IS_ELECTRON && require("electron").app) {
const { app } = require("electron"),
args = process.argv.slice(1).filter((arg) => arg !== "--startup");
app.relaunch({ args });
app.exit();
} else sendMessage("notion-enhancer", "reload-app");
};
const initDatabase = (namespace, fallbacks = {}) => {
// all db operations are performed via ipc:
// with nodeintegration disabled, sqlite cannot
// be require()-d from the renderer process
const operation = (type, args = {}) =>
invokeInWorker("notion-enhancer:db", {
namespace,
fallbacks,
operation: type,
args,
const query = (query, args = {}) =>
IS_ELECTRON && !IS_RENDERER
? globalThis.__enhancerApi.queryDatabase(namespace, query, args)
: invokeInWorker("notion-enhancer:db", {
action: "query-database",
data: { namespace, query, args },
});
return {
get: (key) => operation("get", { key }),
set: (key, value) => operation("set", { key, value }),
remove: (keys) => operation("remove", { keys }),
export: () => operation("export"),
import: (obj) => operation("import", { obj }),
get: (key) => query("get", { key, fallbacks }),
set: (key, value) => query("set", { key, value }),
remove: (keys) => query("remove", { keys }),
export: () => query("export"),
import: (obj) => query("import", { obj }),
};
},
reloadApp = () => {
if (IS_ELECTRON && !IS_RENDERER) {
const { app } = require("electron"),
args = process.argv.slice(1).filter((arg) => arg !== "--startup");
app.relaunch({ args });
app.exit();
} else sendMessage("notion-enhancer", "reload-app");
};
globalThis.__enhancerApi ??= {};
@ -145,6 +150,6 @@ Object.assign(globalThis.__enhancerApi, {
invokeInWorker,
readFile,
readJson,
reloadApp,
initDatabase,
reloadApp,
});

View File

@ -54,7 +54,7 @@ const initDatabase = async () => {
};
return db;
},
executeOperation = async (namespace, fallbacks, operation, args) => {
queryDatabase = async (namespace, query, args) => {
namespace ??= "";
if (Array.isArray(namespace)) namespace = namespace.join("__");
if (namespace?.length) namespace += "__";
@ -62,7 +62,7 @@ const initDatabase = async () => {
key.startsWith(namespace) ? key : namespace + key;
await (__db ??= initDatabase());
switch (operation) {
switch (query) {
case "get": {
const key = namespaceify(args.key);
let value;
@ -71,7 +71,7 @@ const initDatabase = async () => {
value = JSON.parse(__statements.select.get(key)?.value);
} catch {}
} else value = (await chrome.storage.local.get([key]))[key];
return value ?? fallbacks[key];
return value ?? args.fallbacks[args.key];
}
case "set": {
const key = namespaceify(args.key),
@ -114,95 +114,61 @@ const initDatabase = async () => {
};
if (IS_ELECTRON) {
const { app, ipcMain } = require("electron"),
reloadApp = () => {
const args = process.argv.slice(1).filter((arg) => arg !== "--startup");
app.relaunch({ args });
app.exit();
};
ipcMain.handle("notion-enhancer:db", (_event, message) => {
return executeOperation(
message.namespace,
message.fallbacks,
message.operation,
message.args
);
const { ipcMain } = require("electron"),
{ reloadApp } = globalThis.__enhancerApi;
ipcMain.handle("notion-enhancer:db", ({}, message) => {
if (message?.action !== "query-database") return;
const { namespace, query, args } = message.data;
return queryDatabase(namespace, query, args);
});
ipcMain.on("notion-enhancer", (_event, message) => {
if (message === "open-menu") {
// todo
} else if (message === "reload-app") {
reloadApp();
}
ipcMain.on("notion-enhancer", ({ sender }, message) => {
if (message === "reload-app") reloadApp();
});
} else {
const notionUrl = "https://www.notion.so/",
isNotionTab = (tab) => tab?.url?.startsWith(notionUrl);
const tabQueue = new Set(),
openMenu = { channel: "notion-enhancer", message: "open-menu" },
openEnhancerMenu = async (tab) => {
if (!isNotionTab(tab)) {
const openTabs = await chrome.tabs.query({
windowId: chrome.windows.WINDOW_ID_CURRENT,
});
tab = openTabs.find(isNotionTab);
const windowId = chrome.windows.WINDOW_ID_CURRENT,
windowTabs = await chrome.tabs.query({ windowId });
tab = windowTabs.find(isNotionTab);
tab ??= await chrome.tabs.create({ url: notionUrl });
}
chrome.tabs.highlight({ tabs: [tab.index] });
if (tab.status === "complete") {
chrome.tabs.sendMessage(tab.id, {
channel: "notion-enhancer",
message: "open-menu",
});
chrome.tabs.sendMessage(tab.id, openMenu);
} else tabQueue.add(tab.id);
},
reloadNotionTabs = async () => {
const openTabs = await chrome.tabs.query({
windowId: chrome.windows.WINDOW_ID_CURRENT,
}),
notionTabs = openTabs.filter(isNotionTab);
notionTabs.forEach((tab) => chrome.tabs.reload(tab.id));
const windowId = chrome.windows.WINDOW_ID_CURRENT;
(await chrome.tabs.query({ windowId }))
.filter(isNotionTab)
.forEach((tab) => chrome.tabs.reload(tab.id));
};
// listen for invoke: https://developer.chrome.com/docs/extensions/mv3/messaging/
ipcMain.handle("notion-enhancer:db", (_event, message) => {
return executeOperation(
message.namespace,
message.fallbacks,
message.operation,
message.args
);
});
chrome.action.onClicked.addListener(openEnhancerMenu);
chrome.runtime.onConnect.addListener((port) => {
port.onMessage.addListener((msg, sender) => {
if (msg?.channel === "notion-enhancer:db") {
const { invocation } = msg,
res = executeOperation(
msg.namespace,
msg.fallbacks,
msg.operation,
msg.args
);
if (msg?.channel !== "notion-enhancer") return;
const { message, invocation } = msg;
if (message.action === "query-database") {
const { namespace, query, args } = message.data,
res = queryDatabase(namespace, query, args);
if (invocation) port.postMessage({ invocation, message: res });
}
if (msg?.channel === "notion-enhancer") {
if (sender.tab && msg.message === "load-complete") {
if (tabQueue.has(sender.tab.id)) {
chrome.tabs.sendMessage(sender.tab.id, {
channel: "notion-enhancer",
message: "open-menu",
});
if (message === "load-complete") {
if (!tabQueue.has(sender?.tab?.id)) return;
chrome.tabs.sendMessage(sender.tab.id, openMenu);
tabQueue.delete(sender.tab.id);
}
} else if (msg.message === "reload-app") {
reloadNotionTabs();
}
}
if (message === "reload-app") reloadNotionTabs();
});
});
}
globalThis.__enhancerApi ??= {};
Object.assign(globalThis.__enhancerApi, { queryDatabase });