From 025bbca44c45cbc5b5189b1b8308ead309413e68 Mon Sep 17 00:00:00 2001 From: dragonwocky Date: Thu, 3 Aug 2023 18:25:16 +1000 Subject: [PATCH] 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 --- src/core/client.mjs | 24 ++--- src/core/menu/islands/Onboarding.mjs | 14 +-- src/core/menu/menu.mjs | 8 +- src/{core => extensions}/tweaks/client.css | 0 src/{core => extensions}/tweaks/client.mjs | 0 src/{core => extensions}/tweaks/mod.json | 0 src/init.js | 17 ++-- src/load.mjs | 8 +- src/{api => shared}/events.js | 0 src/{api/interface.js => shared/markup.js} | 0 src/{api/mods.js => shared/registry.js} | 0 src/{api => shared}/system.js | 69 +++++++------- src/worker.js | 100 +++++++-------------- 13 files changed, 107 insertions(+), 133 deletions(-) rename src/{core => extensions}/tweaks/client.css (100%) rename src/{core => extensions}/tweaks/client.mjs (100%) rename src/{core => extensions}/tweaks/mod.json (100%) rename src/{api => shared}/events.js (100%) rename src/{api/interface.js => shared/markup.js} (100%) rename src/{api/mods.js => shared/registry.js} (100%) rename src/{api => shared}/system.js (75%) diff --git a/src/core/client.mjs b/src/core/client.mjs index 6ff0257..9066492 100644 --- a/src/core/client.mjs +++ b/src/core/client.mjs @@ -37,14 +37,15 @@ const doThemeOverride = async (db) => { `); }; -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"); diff --git a/src/core/menu/islands/Onboarding.mjs b/src/core/menu/islands/Onboarding.mjs index 2167901..f7b4528 100644 --- a/src/core/menu/islands/Onboarding.mjs +++ b/src/core/menu/islands/Onboarding.mjs @@ -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. -
-
+ 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. +

The notion-enhancer is free and open-source software distributed under the MIT License 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. diff --git a/src/core/menu/menu.mjs b/src/core/menu/menu.mjs index 9212250..a73a57d 100644 --- a/src/core/menu/menu.mjs +++ b/src/core/menu/menu.mjs @@ -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 diff --git a/src/core/tweaks/client.css b/src/extensions/tweaks/client.css similarity index 100% rename from src/core/tweaks/client.css rename to src/extensions/tweaks/client.css diff --git a/src/core/tweaks/client.mjs b/src/extensions/tweaks/client.mjs similarity index 100% rename from src/core/tweaks/client.mjs rename to src/extensions/tweaks/client.mjs diff --git a/src/core/tweaks/mod.json b/src/extensions/tweaks/mod.json similarity index 100% rename from src/core/tweaks/mod.json rename to src/extensions/tweaks/mod.json diff --git a/src/init.js b/src/init.js index de5529a..18a3889 100644 --- a/src/init.js +++ b/src/init.js @@ -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"))); } diff --git a/src/load.mjs b/src/load.mjs index 303e979..81efb4e 100644 --- a/src/load.mjs +++ b/src/load.mjs @@ -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()) { diff --git a/src/api/events.js b/src/shared/events.js similarity index 100% rename from src/api/events.js rename to src/shared/events.js diff --git a/src/api/interface.js b/src/shared/markup.js similarity index 100% rename from src/api/interface.js rename to src/shared/markup.js diff --git a/src/api/mods.js b/src/shared/registry.js similarity index 100% rename from src/api/mods.js rename to src/shared/registry.js diff --git a/src/api/system.js b/src/shared/system.js similarity index 75% rename from src/api/system.js rename to src/shared/system.js index 04b0ed4..06a0e0b 100644 --- a/src/api/system.js +++ b/src/shared/system.js @@ -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,15 +103,35 @@ 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()); + }; + +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 = () => { - if (IS_ELECTRON && require("electron").app) { + if (IS_ELECTRON && !IS_RENDERER) { const { app } = require("electron"), args = process.argv.slice(1).filter((arg) => arg !== "--startup"); app.relaunch({ args }); @@ -114,26 +139,6 @@ const readFile = (file) => { } 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 ??= {}; Object.assign(globalThis.__enhancerApi, { platform, @@ -145,6 +150,6 @@ Object.assign(globalThis.__enhancerApi, { invokeInWorker, readFile, readJson, - reloadApp, initDatabase, + reloadApp, }); diff --git a/src/worker.js b/src/worker.js index 18a1bbc..7e87dfd 100644 --- a/src/worker.js +++ b/src/worker.js @@ -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", - }); - tabQueue.delete(sender.tab.id); - } - } else if (msg.message === "reload-app") { - reloadNotionTabs(); - } + if (message === "load-complete") { + if (!tabQueue.has(sender?.tab?.id)) return; + chrome.tabs.sendMessage(sender.tab.id, openMenu); + tabQueue.delete(sender.tab.id); } + if (message === "reload-app") reloadNotionTabs(); }); }); } + +globalThis.__enhancerApi ??= {}; +Object.assign(globalThis.__enhancerApi, { queryDatabase });