mirror of
https://github.com/notion-enhancer/notion-enhancer.git
synced 2025-04-07 22:19:02 +00:00
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:
parent
a88c74d1c3
commit
025bbca44c
@ -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");
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
17
src/init.js
17
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")));
|
||||
}
|
||||
|
@ -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()) {
|
||||
|
@ -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,
|
||||
});
|
100
src/worker.js
100
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 });
|
||||
|
Loading…
Reference in New Issue
Block a user