From aae4c7e66371c9028fdcb5fd5ee2dcd5d51b0a93 Mon Sep 17 00:00:00 2001 From: dragonwocky Date: Thu, 25 Jan 2024 11:44:28 +1100 Subject: [PATCH] chore: perf optimisations, overflow topbar to the left when comments & updates sidebars are open --- src/common/events.js | 33 ++++++++++++++--------- src/core/client.mjs | 42 ++++++++++++++--------------- src/core/islands/Modal.mjs | 3 +-- src/core/islands/Panel.mjs | 4 +-- src/core/islands/Tooltip.mjs | 6 ++++- src/core/islands/TopbarButton.mjs | 2 +- src/core/menu/index.html | 4 --- src/core/menu/menu.css | 5 ++-- src/core/menu/menu.mjs | 16 +++++------ src/extensions/titlebar/buttons.mjs | 2 +- src/extensions/titlebar/client.mjs | 6 ++--- src/extensions/topbar/client.css | 28 +++++++++++++++++++ src/extensions/topbar/client.mjs | 24 +++++++++++------ src/extensions/topbar/mod.json | 1 + src/load.mjs | 5 ++++ 15 files changed, 113 insertions(+), 68 deletions(-) create mode 100644 src/extensions/topbar/client.css diff --git a/src/common/events.js b/src/common/events.js index ff620ee..a87ef9e 100644 --- a/src/common/events.js +++ b/src/common/events.js @@ -58,30 +58,36 @@ const _state = {}, callback(state); } return state; - }; + }, + dumpState = () => _state; let documentObserver, mutationListeners = []; const mutationQueue = [], - addMutationListener = (selector, callback) => { - mutationListeners.push([selector, callback]); + addMutationListener = (selector, callback, attributesOnly = false) => { + mutationListeners.push([selector, callback, attributesOnly]); }, removeMutationListener = (callback) => { mutationListeners = mutationListeners.filter(([, c]) => c !== callback); }, - onSelectorMutated = (mutation, selector) => - mutation.target?.matches(`${selector}, ${selector} *`) || - [...(mutation.addedNodes || [])].some( - (node) => - node instanceof HTMLElement && - (node?.matches(`${selector}, ${selector} *`) || - node?.querySelector(selector)) - ), + selectorMutated = (mutation, selector, attributesOnly) => { + const matchesTarget = mutation.target?.matches(selector); + if (attributesOnly) return matchesTarget; + const descendsFromTarget = mutation.target?.matches(`${selector} *`), + addedToTarget = [...(mutation.addedNodes || [])].some( + (node) => + node instanceof HTMLElement && + (node?.matches(`${selector}, ${selector} *`) || + node?.querySelector(selector)) + ); + return matchesTarget || descendsFromTarget || addedToTarget; + }, handleMutations = () => { while (mutationQueue.length) { const mutation = mutationQueue.shift(); - for (const [selector, callback] of mutationListeners) { - if (onSelectorMutated(mutation, selector)) callback(mutation); + for (const [selector, callback, attributesOnly] of mutationListeners) { + const matches = selectorMutated(mutation, selector, attributesOnly); + if (matches) callback(mutation); } } }, @@ -169,6 +175,7 @@ Object.assign((globalThis.__enhancerApi ??= {}), { debounce, setState, useState, + dumpState, addMutationListener, removeMutationListener, addKeyListener, diff --git a/src/core/client.mjs b/src/core/client.mjs index d4c2a50..0409aae 100644 --- a/src/core/client.mjs +++ b/src/core/client.mjs @@ -11,6 +11,7 @@ import { sendTelemetryPing } from "./sendTelemetry.mjs"; import { Modal, Frame } from "./islands/Modal.mjs"; import { MenuButton } from "./islands/MenuButton.mjs"; import { TopbarButton } from "./islands/TopbarButton.mjs"; +import { Tooltip } from "./islands/Tooltip.mjs"; import { Panel } from "./islands/Panel.mjs"; const shouldLoadThemeOverrides = async (api, db) => { @@ -45,8 +46,8 @@ const shouldLoadThemeOverrides = async (api, db) => { const insertMenu = async (api, 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 } = api, - { platform, enhancerUrl, onMessage } = api, + { html, addMutationListener, removeMutationListener } = api, + { addKeyListener, platform, enhancerUrl, onMessage } = api, menuButtonIconStyle = await db.get("menuButtonIconStyle"), openMenuHotkey = await db.get("openMenuHotkey"), menuPing = { @@ -59,19 +60,11 @@ const insertMenu = async (api, db) => { const updateMenuTheme = () => { const darkMode = document.body.classList.contains("dark"), notionTheme = darkMode ? "dark" : "light"; - if (menuPing.theme === notionTheme) return; menuPing.theme = notionTheme; _contentWindow?.postMessage?.(menuPing, "*"); - }, - triggerMenuRender = (contentWindow) => { - _contentWindow ??= contentWindow; - if (!$modal.hasAttribute("open")) return; - _contentWindow?.focus?.(); - delete menuPing.theme; - updateMenuTheme(); }; - const $modal = html`<${Modal} onopen=${triggerMenuRender}> + const $modal = html`<${Modal}> <${Frame} title="notion-enhancer menu" src="${enhancerUrl("core/menu/index.html")}" @@ -81,7 +74,8 @@ const insertMenu = async (api, db) => { const apiKey = "__enhancerApi"; this.contentWindow[apiKey] = globalThis[apiKey]; } - triggerMenuRender(this.contentWindow); + _contentWindow = this.contentWindow; + updateMenuTheme(); }} /> `, @@ -95,12 +89,17 @@ const insertMenu = async (api, db) => { >notion-enhancer `; const appendToDom = () => { - if (!document.contains($modal)) document.body.append($modal); - if (!document.querySelector(notionSidebar)?.contains($button)) - document.querySelector(notionSettingsAndMembers)?.after($button); + const $settings = document.querySelector(notionSettingsAndMembers); + document.body.append($modal); + $settings?.after($button); + const appended = document.contains($modal) && document.contains($button); + if (appended) removeMutationListener(appendToDom); }; + html`<${Tooltip}> + Configure the notion-enhancer and its mods + `.attach($button, "right"); addMutationListener(notionSidebar, appendToDom); - addMutationListener("body", updateMenuTheme); + addMutationListener(".notion-app-inner", updateMenuTheme, true); appendToDom(); addKeyListener(openMenuHotkey, (event) => { @@ -108,7 +107,6 @@ const insertMenu = async (api, db) => { event.stopPropagation(); $modal.open(); }); - window.addEventListener("focus", triggerMenuRender); window.addEventListener("message", (event) => { // from embedded menu if (event.data?.channel !== "notion-enhancer") return; @@ -124,8 +122,8 @@ const insertMenu = async (api, db) => { const insertPanel = async (api, db) => { const notionFrame = ".notion-frame", togglePanelHotkey = await db.get("togglePanelHotkey"), - { addPanelView, addMutationListener } = api, - { html, setState } = api; + { addMutationListener, removeMutationListener } = api, + { html, setState, addPanelView } = api; const $panel = html`<${Panel} hotkey="${togglePanelHotkey}" @@ -142,9 +140,9 @@ const insertPanel = async (api, db) => { appendToDom = () => { const $frame = document.querySelector(notionFrame); if (!$frame) return; - if (!$frame.contains($panel)) $frame.append($panel); - if (!$frame.style.flexDirection !== "row") - $frame.style.flexDirection = "row"; + $frame.append($panel); + $frame.style.flexDirection = "row"; + removeMutationListener(appendToDom); }; addMutationListener(notionFrame, appendToDom); appendToDom(); diff --git a/src/core/islands/Modal.mjs b/src/core/islands/Modal.mjs index 62c1f7a..a9b3680 100644 --- a/src/core/islands/Modal.mjs +++ b/src/core/islands/Modal.mjs @@ -38,11 +38,10 @@ function Modal(props, ...children) { await new Promise(requestAnimationFrame); } $modal.setAttribute("open", ""); - $modal.onopen?.(); + setTimeout(() => $modal.onopen?.(), 200); }; $modal.close = () => { _openQueued = false; - $modal.onbeforeclose?.(); $modal.removeAttribute("open"); if ($modal.contains(document.activeElement)) { document.activeElement.blur(); diff --git a/src/core/islands/Panel.mjs b/src/core/islands/Panel.mjs index fa7fc4f..e096db6 100644 --- a/src/core/islands/Panel.mjs +++ b/src/core/islands/Panel.mjs @@ -144,7 +144,7 @@ function Panel({ icon="panel-right" />`, addToTopbar = () => { - if (document.contains($topbarToggle)) return; + if (document.contains($topbarToggle)) removeMutationListener(addToTopbar); document.querySelector(topbarFavorite)?.after($topbarToggle); }; $panelToggle.onclick = $topbarToggle.onclick = () => $panel.toggle(); @@ -158,7 +158,7 @@ function Panel({ panelIcon = await topbarDatabase.get("panelIcon"); if (panelButton === "Text") { $topbarToggle.innerHTML = `${$topbarToggle.ariaLabel}`; - } else if (panelIcon.content) $topbarToggle.innerHTML = panelIcon.content; + } else if (panelIcon?.content) $topbarToggle.innerHTML = panelIcon.content; }); let preDragWidth, dragStartX, _animatedAt; diff --git a/src/core/islands/Tooltip.mjs b/src/core/islands/Tooltip.mjs index dc3e420..c267eb6 100644 --- a/src/core/islands/Tooltip.mjs +++ b/src/core/islands/Tooltip.mjs @@ -73,7 +73,11 @@ function Tooltip(props, ...children) { y = () => { const rect = $target.getBoundingClientRect(); if (["left", "right"].includes(alignment)) { - return event.clientY - $tooltip.clientHeight / 2; + // match mouse alignment if hovering over large + // target e.g. panel resize handle, otherwise centre + return rect.height > $tooltip.clientHeight * 2 + ? event.clientY - $tooltip.clientHeight / 2 + : rect.top + rect.height / 2 - $tooltip.clientHeight / 2; } else if (alignment === "top") { return rect.top - $tooltip.clientHeight - 6; } else if (alignment === "bottom") { diff --git a/src/core/islands/TopbarButton.mjs b/src/core/islands/TopbarButton.mjs index 65b3584..53a5ddc 100644 --- a/src/core/islands/TopbarButton.mjs +++ b/src/core/islands/TopbarButton.mjs @@ -7,7 +7,7 @@ "use strict"; function TopbarButton({ icon, ...props }, ...children) { - const { html, extendProps, addMutationListener } = globalThis.__enhancerApi; + const { html, extendProps } = globalThis.__enhancerApi; extendProps(props, { tabindex: 0, role: "button", diff --git a/src/core/menu/index.html b/src/core/menu/index.html index 9f81db7..6574a31 100644 --- a/src/core/menu/index.html +++ b/src/core/menu/index.html @@ -35,10 +35,6 @@
-
-
-
-
diff --git a/src/core/menu/menu.css b/src/core/menu/menu.css index fefed04..a4aba54 100644 --- a/src/core/menu/menu.css +++ b/src/core/menu/menu.css @@ -58,7 +58,8 @@ body > #skeleton .row { display: flex; align-items: center; padding: 0 15px; - height: 30px; + margin: 2px 0; + height: 27px; } body > #skeleton .shimmer { height: 14px; @@ -88,7 +89,7 @@ body > #skeleton .shimmer::before { ); } body > #skeleton .row-group { - height: 21px; + height: 24px; margin-top: 18px; } body > #skeleton .row-group:first-child { diff --git a/src/core/menu/menu.mjs b/src/core/menu/menu.mjs index 999b726..2d3efba 100644 --- a/src/core/menu/menu.mjs +++ b/src/core/menu/menu.mjs @@ -126,7 +126,10 @@ const renderMenu = async () => { categories=${categories} />`, $main = html` -
+
@@ -197,19 +200,14 @@ const importApi = () => { useState(["rerender"], renderMenu); }; -window.addEventListener("focus", async () => { - await importApi().then(hookIntoState); - const { setState } = globalThis.__enhancerApi; - setState({ focus: true, rerender: true }); -}); window.addEventListener("message", async (event) => { if (event.data?.channel !== "notion-enhancer") return; await importApi().then(hookIntoState); const { setState, useState } = globalThis.__enhancerApi; setState({ rerender: true, - hotkey: event.data?.hotkey ?? useState(["hotkey"]), - theme: event.data?.theme ?? useState(["theme"]), - icon: event.data?.icon ?? useState(["icon"]), + hotkey: event.data?.hotkey ?? useState(["hotkey"])[0], + theme: event.data?.theme ?? useState(["theme"])[0], + icon: event.data?.icon ?? useState(["icon"])[0], }); }); diff --git a/src/extensions/titlebar/buttons.mjs b/src/extensions/titlebar/buttons.mjs index bded463..4504e65 100644 --- a/src/extensions/titlebar/buttons.mjs +++ b/src/extensions/titlebar/buttons.mjs @@ -63,7 +63,7 @@ const createWindowButtons = async () => { window.addEventListener("resize", resizeWindow); resizeWindow(); - return html`
${$minimize}${$maximize}${$unmaximize}${$close}
`; + return html`
${$minimize}${$maximize}${$unmaximize}${$close}
`; }; if (globalThis.IS_TABS) { diff --git a/src/extensions/titlebar/client.mjs b/src/extensions/titlebar/client.mjs index d226c76..c41ba01 100644 --- a/src/extensions/titlebar/client.mjs +++ b/src/extensions/titlebar/client.mjs @@ -12,12 +12,12 @@ export default async (api, db) => { const titlebarStyle = await db.get("titlebarStyle"); if (titlebarStyle === "Disabled") return; - const { onMessage, addMutationListener } = api, + const { onMessage, addMutationListener, removeMutationListener } = api, $buttons = await createWindowButtons(), topbarMore = ".notion-topbar-more-button", addToTopbar = () => { - if (document.contains($buttons)) return; - document.querySelector(topbarMore)?.after($buttons) + if (document.contains($buttons)) removeMutationListener(addToTopbar); + document.querySelector(topbarMore)?.after($buttons); }, showIfNoTabBar = async () => { const { isShowingTabBar } = await __electronApi.electronAppFeatures.get(); diff --git a/src/extensions/topbar/client.css b/src/extensions/topbar/client.css new file mode 100644 index 0000000..737f339 --- /dev/null +++ b/src/extensions/topbar/client.css @@ -0,0 +1,28 @@ +/** + * notion-enhancer: topbar + * (c) 2024 dragonwocky (https://dragonwocky.me/) + * (https://notion-enhancer.github.io/) under the MIT license + */ + +/* overflow topbar to the left when comments & updates sidebars are open */ +.notion-topbar-action-buttons { + flex-direction: row-reverse; +} +.notion-topbar-action-buttons > div { + flex-wrap: nowrap; +} +.notion-topbar-action-buttons > :nth-child(1) { + order: 2; +} + +/* position comments & updates sidebars below topbar */ +.notion-cursor-listener + aside[style*="align-self: flex-end"][style*="height: 100vh"] { + height: calc(100vh - 45px) !important; + top: 45px !important; +} +.notion-cursor-listener + aside[style*="align-self: flex-end"][style*="height: 100vh"] + [style*="margin-top: 45px"] { + margin-top: 0 !important; +} diff --git a/src/extensions/topbar/client.mjs b/src/extensions/topbar/client.mjs index 7888b27..8ffb2dc 100644 --- a/src/extensions/topbar/client.mjs +++ b/src/extensions/topbar/client.mjs @@ -16,7 +16,8 @@ const pinLabel = "Pin always on top", unpinTooltip = "Unpin window from always on top"; export default async function (api, db) { - const { html, sendMessage, addMutationListener } = api, + const { html, sendMessage } = api, + { addMutationListener, removeMutationListener } = api, displayLabel = ($btn) => { if ($btn.innerHTML === $btn.ariaLabel) return; $btn.style.width = "auto"; @@ -42,7 +43,8 @@ export default async function (api, db) { let icon = shareIcon?.content; icon ??= ``; if (shareButton === "Icon") displayIcon($btn, icon); - if (shareButton === "Disabled") $btn.style.display = "none"; + if (shareButton === "Disabled" && $btn.style.display !== "none") + $btn.style.display = "none"; }); const commentsSelector = ".notion-topbar-comments-button", @@ -53,7 +55,8 @@ export default async function (api, db) { icon = commentsIcon?.content; if (commentsButton === "Text") displayLabel($btn); if (commentsButton === "Icon" && icon) displayIcon($btn, icon); - if (commentsButton === "Disabled") $btn.style.display = "none"; + if (commentsButton === "Disabled" && $btn.style.display !== "none") + $btn.style.display = "none"; }); const updatesSelector = ".notion-topbar-updates-button", @@ -64,7 +67,8 @@ export default async function (api, db) { icon = updatesIcon?.content; if (updatesButton === "Text") displayLabel($btn); if (updatesButton === "Icon" && icon) displayIcon($btn, icon); - if (updatesButton === "Disabled") $btn.style.display = "none"; + if (updatesButton === "Disabled" && $btn.style.display !== "none") + $btn.style.display = "none"; }); const favoriteSelector = ".notion-topbar-favorite-button", @@ -75,7 +79,8 @@ export default async function (api, db) { icon = favoriteIcon?.content; if (favoriteButton === "Text") displayLabel($btn); if (favoriteButton === "Icon" && icon) displayIcon($btn, icon); - if (favoriteButton === "Disabled") $btn.style.display = "none"; + if (favoriteButton === "Disabled" && $btn.style.display !== "none") + $btn.style.display = "none"; }); const moreSelector = ".notion-topbar-more-button", @@ -87,7 +92,8 @@ export default async function (api, db) { $btn.ariaLabel = "More"; if (moreButton === "Text") displayLabel($btn); if (moreButton === "Icon" && icon) displayIcon($btn, icon); - if (moreButton === "Disabled") $btn.style.display = "none"; + if (moreButton === "Disabled" && $btn.style.display !== "none") + $btn.style.display = "none"; }); const alwaysOnTopButton = await db.get("alwaysOnTopButton"); @@ -122,8 +128,10 @@ export default async function (api, db) { icon="pin-off" />`, addToTopbar = () => { - if (document.contains($pin) && document.contains($unpin)) return; - document.querySelector(topbarFavorite)?.after($pin, $unpin); + const $topbarFavorite = document.querySelector(topbarFavorite); + if (!$topbarFavorite) return; + $topbarFavorite.after($pin, $unpin); + removeMutationListener(addToTopbar); }; html`<${Tooltip}>${pinTooltip}`.attach($pin, "bottom"); html`<${Tooltip}>${unpinTooltip}`.attach($unpin, "bottom"); diff --git a/src/extensions/topbar/mod.json b/src/extensions/topbar/mod.json index 5368439..2803cf0 100644 --- a/src/extensions/topbar/mod.json +++ b/src/extensions/topbar/mod.json @@ -118,6 +118,7 @@ "extensions": ["svg"] } ], + "clientStyles": ["client.css"], "clientScripts": ["client.mjs"], "electronScripts": [[".webpack/main/index", "electron.cjs"]] } diff --git a/src/load.mjs b/src/load.mjs index 3d026fb..d39672a 100644 --- a/src/load.mjs +++ b/src/load.mjs @@ -43,6 +43,7 @@ export default (async () => { // their local states will be cleared (e.g., // references to registered hotkeys) + const _state = globalThis.__enhancerApi?.dumpState?.(); await Promise.all([ // i.e. if (not_menu) or (is_menu && not_electron), then import !(!IS_MENU || !IS_ELECTRON) || import(enhancerUrl("assets/icons.svg.js")), @@ -55,6 +56,10 @@ export default (async () => { import(enhancerUrl("common/events.js")), import(enhancerUrl("common/markup.js")), ]); + // copy across state from prev. module imports if + // useState / setState are called early, otherwise + // e.g. menu will not persist theme from initial msg + globalThis.__enhancerApi.setState(_state ?? {}); const { getMods, isEnabled, modDatabase } = globalThis.__enhancerApi; for (const mod of await getMods()) {