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

View File

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

View File

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

View File

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

View File

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

View File

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