From 5cdc92add3c3af2586a0a522b84974fbb36c0a9c Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Fri, 29 Dec 2023 18:45:55 +0800 Subject: [PATCH 01/29] sidePanel wip --- package-lock.json | 7 ++++--- package.json | 2 +- scripts/manifest.mjs | 6 ++++++ src/background/browserAction.ts | 8 ++++++-- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index c24d4cb8a4..58f7c3437d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -174,7 +174,7 @@ "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.5.1", "@total-typescript/ts-reset": "^0.5.1", - "@types/chrome": "^0.0.216", + "@types/chrome": "^0.0.254", "@types/dom-navigation": "^1.0.3", "@types/dompurify": "^3.0.5", "@types/downloadjs": "^1.4.6", @@ -7799,8 +7799,9 @@ } }, "node_modules/@types/chrome": { - "version": "0.0.216", - "integrity": "sha512-nJezaOm7yX2Zen+9NhNuGBE2WtssH/ssbqIeULL13wMlCwvjNmSaUeueFEixMODKyCY2vlhkD/jERSvEG0wWEA==", + "version": "0.0.254", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.254.tgz", + "integrity": "sha512-svkOGKwA+6ZZuk9xtrYun8MYpNY/9hD17rgZ19v3KunhsK1ZOKaMESw12/1AXLh1u3UPA8jQIRi2370DXv9wgw==", "dev": true, "dependencies": { "@types/filesystem": "*", diff --git a/package.json b/package.json index 3a09b99d1e..30d19c9e8d 100644 --- a/package.json +++ b/package.json @@ -197,7 +197,7 @@ "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.5.1", "@total-typescript/ts-reset": "^0.5.1", - "@types/chrome": "^0.0.216", + "@types/chrome": "^0.0.254", "@types/dom-navigation": "^1.0.3", "@types/dompurify": "^3.0.5", "@types/downloadjs": "^1.4.6", diff --git a/scripts/manifest.mjs b/scripts/manifest.mjs index d1ff4ba0a5..114b55506a 100644 --- a/scripts/manifest.mjs +++ b/scripts/manifest.mjs @@ -52,6 +52,12 @@ function updateManifestToV3(manifestV2) { manifest.permissions = [...permissions, "scripting"]; manifest.host_permissions = origins; + // Add sidePanel + manifest.permissions.push("sidePanel"); + manifest.side_panel = { + default_panel: "sidebar.html", + }; + // Update format manifest.web_accessible_resources = [ { diff --git a/src/background/browserAction.ts b/src/background/browserAction.ts index 814e2e572e..bc0e2365c8 100644 --- a/src/background/browserAction.ts +++ b/src/background/browserAction.ts @@ -18,7 +18,7 @@ import { ensureContentScript } from "@/background/contentScript"; import { rehydrateSidebar } from "@/contentScript/messenger/api"; import webextAlert from "./webextAlert"; -import { browserAction, type Tab } from "@/mv3/api"; +import { browserAction, isMV3, type Tab } from "@/mv3/api"; import { executeScript, isScriptableUrl } from "webext-content-scripts"; import { memoizeUntilSettled } from "@/utils/promiseUtils"; import { getExtensionConsoleUrl } from "@/utils/extensionUtils"; @@ -105,7 +105,11 @@ function getPopoverUrl(tabUrl: string | null): string | null { } export default function initBrowserAction(): void { - browserAction.onClicked.addListener(handleBrowserAction); + if (isMV3()) { + chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }); + } else { + browserAction.onClicked.addListener(handleBrowserAction); + } // Track the active tab URL. We need to update the popover every time status the active tab/active URL changes. // https://github.com/facebook/react/blob/bbb9cb116dbf7b6247721aa0c4bcb6ec249aa8af/packages/react-devtools-extensions/src/background/tabsManager.js#L29 From 00422f399216a4182a9fe015a77cbf3407dd5cd6 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Fri, 29 Dec 2023 23:44:12 +0800 Subject: [PATCH 02/29] Use sidePanel.open(tabId) --- package-lock.json | 7 +++-- package.json | 2 +- scripts/manifest.mjs | 2 +- src/background/browserAction.ts | 7 ++++- .../automationanywhere/aaFrameProtocol.ts | 2 +- src/sidebar/ConnectedSidebar.tsx | 2 +- src/sidebar/sidePanel.tsx | 31 +++++++++++++++++++ src/sidebar/useHideEmptySidebar.ts | 2 +- 8 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 src/sidebar/sidePanel.tsx diff --git a/package-lock.json b/package-lock.json index 58f7c3437d..a3e9b432b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -142,7 +142,7 @@ "webext-additional-permissions": "^2.4.0", "webext-base-css": "^1.4.4", "webext-content-scripts": "^2.6.0", - "webext-detect-page": "^4.1.1", + "webext-detect-page": "^4.2.0", "webext-inject-on-install": "^2.0.0-2", "webext-messenger": "^0.25.0-0", "webext-patterns": "^1.3.0", @@ -27408,8 +27408,9 @@ } }, "node_modules/webext-detect-page": { - "version": "4.1.1", - "integrity": "sha512-s8DIJESB8Hv1YRT7Yrv42C5P/NJrnD7V/Es0UopWmaww5Ugc8qqkYXh9SYdAbxiNmsXWaHwwvjYEJopQ3cMfcg==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/webext-detect-page/-/webext-detect-page-4.2.0.tgz", + "integrity": "sha512-UW9HjKIKq2fnbvABBol+Kp3oncsCfBqyW2Gii+ds+BPcJXPUNcR5r4bo2ZJOv1banYY4bT5xeg8rfKhq9v7nRg==" }, "node_modules/webext-inject-on-install": { "version": "2.0.0-2", diff --git a/package.json b/package.json index 30d19c9e8d..36855f7417 100644 --- a/package.json +++ b/package.json @@ -165,7 +165,7 @@ "webext-additional-permissions": "^2.4.0", "webext-base-css": "^1.4.4", "webext-content-scripts": "^2.6.0", - "webext-detect-page": "^4.1.1", + "webext-detect-page": "^4.2.0", "webext-inject-on-install": "^2.0.0-2", "webext-messenger": "^0.25.0-0", "webext-patterns": "^1.3.0", diff --git a/scripts/manifest.mjs b/scripts/manifest.mjs index 114b55506a..1dcff55959 100644 --- a/scripts/manifest.mjs +++ b/scripts/manifest.mjs @@ -55,7 +55,7 @@ function updateManifestToV3(manifestV2) { // Add sidePanel manifest.permissions.push("sidePanel"); manifest.side_panel = { - default_panel: "sidebar.html", + default_path: "sidebar.html", }; // Update format diff --git a/src/background/browserAction.ts b/src/background/browserAction.ts index bc0e2365c8..49b494eff9 100644 --- a/src/background/browserAction.ts +++ b/src/background/browserAction.ts @@ -106,7 +106,12 @@ function getPopoverUrl(tabUrl: string | null): string | null { export default function initBrowserAction(): void { if (isMV3()) { - chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }); + chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }); + + browserAction.onClicked.addListener((tab) => { + console.log("xxxxxxxxxx", { tabId: tab.id }); + chrome.sidePanel.open({ tabId: tab.id }); + }); } else { browserAction.onClicked.addListener(handleBrowserAction); } diff --git a/src/contrib/automationanywhere/aaFrameProtocol.ts b/src/contrib/automationanywhere/aaFrameProtocol.ts index 2ae20a0296..56977ba3f6 100644 --- a/src/contrib/automationanywhere/aaFrameProtocol.ts +++ b/src/contrib/automationanywhere/aaFrameProtocol.ts @@ -17,7 +17,7 @@ import { type UnknownObject } from "@/types/objectTypes"; import { expectContext } from "@/utils/expectContext"; -import { getTopLevelFrame } from "webext-messenger"; +import { getTopLevelFrame } from "@/sidebar/sidePanel"; import { getCopilotHostData } from "@/contentScript/messenger/api"; /** diff --git a/src/sidebar/ConnectedSidebar.tsx b/src/sidebar/ConnectedSidebar.tsx index e9d3283857..5e337cd97e 100644 --- a/src/sidebar/ConnectedSidebar.tsx +++ b/src/sidebar/ConnectedSidebar.tsx @@ -42,7 +42,7 @@ import { ensureExtensionPointsInstalled, getReservedSidebarEntries, } from "@/contentScript/messenger/api"; -import { getTopLevelFrame } from "webext-messenger"; +import { getTopLevelFrame } from "./sidePanel"; import useAsyncEffect from "use-async-effect"; import activateLinkClickHandler from "@/activation/activateLinkClickHandler"; diff --git a/src/sidebar/sidePanel.tsx b/src/sidebar/sidePanel.tsx new file mode 100644 index 0000000000..5bdb3bc1e1 --- /dev/null +++ b/src/sidebar/sidePanel.tsx @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 PixieBrix, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { isSidePanel } from "webext-detect-page"; +import { getTopLevelFrame as getTopLevelFrameViaMessenger } from "webext-messenger"; + +export async function getTopLevelFrame() { + if (isSidePanel()) { + const [tab] = await chrome.tabs.query({ + active: true, + currentWindow: true, + }); + return { tabId: tab.id, frameId: 0 }; + } + + return getTopLevelFrameViaMessenger(); +} diff --git a/src/sidebar/useHideEmptySidebar.ts b/src/sidebar/useHideEmptySidebar.ts index ef06e26afe..1372b6ee38 100644 --- a/src/sidebar/useHideEmptySidebar.ts +++ b/src/sidebar/useHideEmptySidebar.ts @@ -16,7 +16,7 @@ */ import useAsyncEffect from "use-async-effect"; -import { getTopLevelFrame } from "webext-messenger"; +import { getTopLevelFrame } from "./sidePanel"; import { getReservedSidebarEntries, hideSidebar, From d4247a2ee693d0177be608599a94483408d71328 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Sat, 30 Dec 2023 00:21:11 +0800 Subject: [PATCH 03/29] Hide "close" button --- src/sidebar/Header.tsx | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/sidebar/Header.tsx b/src/sidebar/Header.tsx index 47bb0155f1..986f9e87d6 100644 --- a/src/sidebar/Header.tsx +++ b/src/sidebar/Header.tsx @@ -25,6 +25,7 @@ import useTheme, { useGetTheme } from "@/hooks/useTheme"; import cx from "classnames"; import useContextInvalidated from "@/hooks/useContextInvalidated"; import { getTopLevelFrame } from "webext-messenger"; +import { isMV3 } from "@/mv3/api"; const Header: React.FunctionComponent = () => { const { logo, showSidebarLogo, customSidebarLogo } = useTheme(); @@ -33,22 +34,25 @@ const Header: React.FunctionComponent = () => { return (
- {wasContextInvalidated || ( // /* The button doesn't work after invalidation #2359 */ - - )} + {wasContextInvalidated || + isMV3() || ( // /* The button doesn't work after invalidation #2359 nor in sidePanel */ + + )} {showSidebarLogo && (
Date: Thu, 4 Jan 2024 15:18:16 +0800 Subject: [PATCH 04/29] Selected changes by Todd --- scripts/manifest.mjs | 4 ++ src/background/messenger/api.ts | 3 ++ src/background/messenger/registration.ts | 7 +++ src/background/sidePanel.ts | 63 ++++++++++++++++++++++++ src/bricks/effects/sidebar.ts | 2 + src/contentScript/sidebarController.tsx | 2 +- 6 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/background/sidePanel.ts diff --git a/scripts/manifest.mjs b/scripts/manifest.mjs index 1dcff55959..c2781fe21a 100644 --- a/scripts/manifest.mjs +++ b/scripts/manifest.mjs @@ -51,9 +51,13 @@ function updateManifestToV3(manifestV2) { const { permissions, origins } = normalizeManifestPermissions(manifest); manifest.permissions = [...permissions, "scripting"]; manifest.host_permissions = origins; + // Sidebar Panel open() is only available in Chrome 116+ + // https://developer.chrome.com/docs/extensions/reference/api/sidePanel#method-open + manifest.minimum_chrome_version = "116.0"; // Add sidePanel manifest.permissions.push("sidePanel"); + manifest.side_panel = { default_path: "sidebar.html", }; diff --git a/src/background/messenger/api.ts b/src/background/messenger/api.ts index e198eacc72..8a8c0b0b57 100644 --- a/src/background/messenger/api.ts +++ b/src/background/messenger/api.ts @@ -45,6 +45,9 @@ export const removeExtensionForEveryTab = getNotifier( bg, ); +export const showSidebarPanel = getMethod("SHOW_SIDEBAR_PANEL", bg); +export const hideSidebarPanel = getMethod("HIDE_SIDEBAR_PANEL", bg); + export const closeTab = getMethod("CLOSE_TAB", bg); export const deleteCachedAuthData = getMethod("DELETE_CACHED_AUTH", bg); export const getCachedAuthData = getMethod("GET_CACHED_AUTH", bg); diff --git a/src/background/messenger/registration.ts b/src/background/messenger/registration.ts index 926e90b642..fea3b82681 100644 --- a/src/background/messenger/registration.ts +++ b/src/background/messenger/registration.ts @@ -80,6 +80,7 @@ import { getCachedAuthData, } from "@/background/auth/authStorage"; import { setCopilotProcessData } from "@/background/partnerHandlers"; +import { hideSidebarPanel, showSidebarPanel } from "@/background/sidePanel"; expectContext("background"); @@ -114,6 +115,9 @@ declare global { PING: typeof pong; COLLECT_PERFORMANCE_DIAGNOSTICS: typeof collectPerformanceDiagnostics; + SHOW_SIDEBAR_PANEL: typeof showSidebarPanel; + HIDE_SIDEBAR_PANEL: typeof hideSidebarPanel; + ACTIVATE_TAB: typeof activateTab; REACTIVATE_EVERY_TAB: typeof reactivateEveryTab; REMOVE_EXTENSION_EVERY_TAB: typeof removeExtensionForEveryTab; @@ -195,6 +199,9 @@ export default function registerMessenger(): void { PING: pong, COLLECT_PERFORMANCE_DIAGNOSTICS: collectPerformanceDiagnostics, + SHOW_SIDEBAR_PANEL: showSidebarPanel, + HIDE_SIDEBAR_PANEL: hideSidebarPanel, + ACTIVATE_TAB: activateTab, REACTIVATE_EVERY_TAB: reactivateEveryTab, REMOVE_EXTENSION_EVERY_TAB: removeExtensionForEveryTab, diff --git a/src/background/sidePanel.ts b/src/background/sidePanel.ts new file mode 100644 index 0000000000..249eb60801 --- /dev/null +++ b/src/background/sidePanel.ts @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 PixieBrix, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import type { MessengerMeta } from "webext-messenger"; + +export async function showSidebarPanel(this: MessengerMeta): Promise { + const tabId = this.trace[0].tab.id; + + return new Promise((resolve, reject) => { + // Unlike the Chrome example, call setOptions first to handle the case where the sidebar was closed on the tab + // https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/functional-samples/cookbook.sidepanel-open/script.js#L9 + + // Use callback form to help prevent the user gesture from getting lost + chrome.sidePanel.setOptions( + { + tabId, + path: `sidebar.html?tabId=${tabId}`, + enabled: true, + }, + () => { + chrome.sidePanel.open( + { + tabId, + }, + () => { + resolve(); + }, + ); + + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError); + } + }, + ); + + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError); + } + }); +} + +export async function hideSidebarPanel(this: MessengerMeta): Promise { + const tabId = this.trace[0].tab.id; + + await chrome.sidePanel.setOptions({ + tabId, + enabled: false, + }); +} diff --git a/src/bricks/effects/sidebar.ts b/src/bricks/effects/sidebar.ts index 9cceb134cd..baaa83a318 100644 --- a/src/bricks/effects/sidebar.ts +++ b/src/bricks/effects/sidebar.ts @@ -87,6 +87,8 @@ export class HideSidebar extends EffectABC { inputSchema: Schema = SCHEMA_EMPTY_OBJECT; async effect(): Promise { + // XXX: for MV3, do we need to catch a potential user gesture error and rethrow as business error? Would required + // making hideSidebar async and the hide a method instead of notifier hideSidebar(); } } diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index d9dfb0eb18..2fae7cce75 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -26,7 +26,7 @@ import { insertSidebarFrame, isSidebarFrameVisible, removeSidebarFrame, -} from "./sidebarDomControllerLite"; +} from "@/contentScript/sidebarDomControllerLite"; import { type Except } from "type-fest"; import { type RunArgs, RunReason } from "@/types/runtimeTypes"; import { type UUID } from "@/types/stringTypes"; From 05925d9039eb1db49394b346a5b4356ddcc2afec Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Thu, 4 Jan 2024 15:20:54 +0800 Subject: [PATCH 05/29] Enable per-tab sidebar --- scripts/manifest.mjs | 2 +- src/background/browserAction.ts | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/scripts/manifest.mjs b/scripts/manifest.mjs index c2781fe21a..c22f2330df 100644 --- a/scripts/manifest.mjs +++ b/scripts/manifest.mjs @@ -59,7 +59,7 @@ function updateManifestToV3(manifestV2) { manifest.permissions.push("sidePanel"); manifest.side_panel = { - default_path: "sidebar.html", + default_path: "sidebar-empty.html", }; // Update format diff --git a/src/background/browserAction.ts b/src/background/browserAction.ts index 49b494eff9..3ca09faab5 100644 --- a/src/background/browserAction.ts +++ b/src/background/browserAction.ts @@ -106,17 +106,27 @@ function getPopoverUrl(tabUrl: string | null): string | null { export default function initBrowserAction(): void { if (isMV3()) { - chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }); + void chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }); + // Disable by default, so that it can be enabled on a per-tab basis + void chrome.sidePanel.setOptions({ + enabled: false, + }); browserAction.onClicked.addListener((tab) => { console.log("xxxxxxxxxx", { tabId: tab.id }); - chrome.sidePanel.open({ tabId: tab.id }); + // Simultaneously define, enable, and open the side panel + void chrome.sidePanel.setOptions({ + tabId: tab.id, + path: getPopoverUrl(tab.url) ?? "sidebar.html?tabId=" + tab.id, + enabled: true, + }); + void chrome.sidePanel.open({ tabId: tab.id }); }); } else { browserAction.onClicked.addListener(handleBrowserAction); - } - // Track the active tab URL. We need to update the popover every time status the active tab/active URL changes. - // https://github.com/facebook/react/blob/bbb9cb116dbf7b6247721aa0c4bcb6ec249aa8af/packages/react-devtools-extensions/src/background/tabsManager.js#L29 - setActionPopup(getPopoverUrl); + // Track the active tab URL. We need to update the popover every time status the active tab/active URL changes. + // https://github.com/facebook/react/blob/bbb9cb116dbf7b6247721aa0c4bcb6ec249aa8af/packages/react-devtools-extensions/src/background/tabsManager.js#L29 + setActionPopup(getPopoverUrl); + } } From 3c41725145eae5e6423df5720f7c7414cd7e7ce0 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Thu, 4 Jan 2024 16:02:46 +0800 Subject: [PATCH 06/29] Fix bad merge --- package-lock.json | 1 - package.json | 1 - 2 files changed, 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2626c24165..c7aed0072f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "@atlaskit/tree": "^8.8.7", "@cfworker/json-schema": "^1.12.7", "@datadog/browser-rum": "^5.6.0", - "@emotion/react": "^11.11.3", "@floating-ui/dom": "^1.5.3", "@fortawesome/fontawesome-svg-core": "1.2.36", "@fortawesome/free-brands-svg-icons": "^5.15.4", diff --git a/package.json b/package.json index 0cb8e118aa..7491e767ff 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "@atlaskit/tree": "^8.8.7", "@cfworker/json-schema": "^1.12.7", "@datadog/browser-rum": "^5.6.0", - "@emotion/react": "^11.11.3", "@floating-ui/dom": "^1.5.3", "@fortawesome/fontawesome-svg-core": "1.2.36", "@fortawesome/free-brands-svg-icons": "^5.15.4", From 92edfb6116d4d9c22e58e3ee4f9cf0c14227739c Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Thu, 4 Jan 2024 19:02:09 +0800 Subject: [PATCH 07/29] Last mixed implementation --- src/background/browserAction.ts | 78 ++++------ src/background/sidePanel.ts | 1 + src/contentScript/sidebarController.tsx | 54 ++++++- src/contentScript/sidebarDomControllerLite.ts | 144 ++++++++++-------- src/mv3/sidePanel.ts | 84 ++++++++++ src/pageEditor/panes/insert/useAutoInsert.ts | 8 +- .../sidebar/ActivatedModComponentListItem.tsx | 9 +- .../sidebar/DynamicModComponentListItem.tsx | 9 +- 8 files changed, 255 insertions(+), 132 deletions(-) create mode 100644 src/mv3/sidePanel.ts diff --git a/src/background/browserAction.ts b/src/background/browserAction.ts index 3ca09faab5..9488ded1ce 100644 --- a/src/background/browserAction.ts +++ b/src/background/browserAction.ts @@ -19,14 +19,10 @@ import { ensureContentScript } from "@/background/contentScript"; import { rehydrateSidebar } from "@/contentScript/messenger/api"; import webextAlert from "./webextAlert"; import { browserAction, isMV3, type Tab } from "@/mv3/api"; -import { executeScript, isScriptableUrl } from "webext-content-scripts"; +import { executeScript } from "webext-content-scripts"; import { memoizeUntilSettled } from "@/utils/promiseUtils"; -import { getExtensionConsoleUrl } from "@/utils/extensionUtils"; -import { - DISPLAY_REASON_EXTENSION_CONSOLE, - DISPLAY_REASON_RESTRICTED_URL, -} from "@/tinyPages/restrictedUrlPopupConstants"; import { setActionPopup } from "webext-tools"; +import { getPopoverUrl, getSidebarPath, openSidePanel } from "@/mv3/sidePanel"; const ERR_UNABLE_TO_OPEN = "PixieBrix was unable to open the Sidebar. Try refreshing the page."; @@ -82,51 +78,35 @@ async function handleBrowserAction(tab: Tab): Promise { await toggleSidebar(tab.id, url); } -/** - * Show a popover on restricted URLs because we're unable to inject content into the page. Previously we'd open - * the Extension Console, but that was confusing because the action was inconsistent with how the button behaves - * other pages. - * @param tabUrl the url of the tab, or null if not accessible - */ -function getPopoverUrl(tabUrl: string | null): string | null { - const popoverUrl = browser.runtime.getURL("restrictedUrlPopup.html"); - - if (tabUrl?.startsWith(getExtensionConsoleUrl())) { - return `${popoverUrl}?reason=${DISPLAY_REASON_EXTENSION_CONSOLE}`; - } - - if (!isScriptableUrl(tabUrl)) { - return `${popoverUrl}?reason=${DISPLAY_REASON_RESTRICTED_URL}`; - } - - // The popup is disabled, and the extension will receive browserAction.onClicked events. - // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/setPopup#popup - return null; -} - -export default function initBrowserAction(): void { - if (isMV3()) { - void chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }); +async function initBrowserActionMv3(): Promise { + void chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }); - // Disable by default, so that it can be enabled on a per-tab basis - void chrome.sidePanel.setOptions({ - enabled: false, - }); - browserAction.onClicked.addListener((tab) => { - console.log("xxxxxxxxxx", { tabId: tab.id }); - // Simultaneously define, enable, and open the side panel + // Disable by default, so that it can be enabled on a per-tab basis + void chrome.sidePanel.setOptions({ + enabled: false, + }); + browserAction.onClicked.addListener(async (tab) => { + await openSidePanel(tab.id, tab.url); + }); + chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + if (changeInfo.url) { + // TODO: Drop this once the popover URL behavior is merged into sidebar.html void chrome.sidePanel.setOptions({ - tabId: tab.id, - path: getPopoverUrl(tab.url) ?? "sidebar.html?tabId=" + tab.id, - enabled: true, + tabId, + path: getSidebarPath(tabId, changeInfo.url), }); - void chrome.sidePanel.open({ tabId: tab.id }); - }); - } else { - browserAction.onClicked.addListener(handleBrowserAction); + } + }); +} - // Track the active tab URL. We need to update the popover every time status the active tab/active URL changes. - // https://github.com/facebook/react/blob/bbb9cb116dbf7b6247721aa0c4bcb6ec249aa8af/packages/react-devtools-extensions/src/background/tabsManager.js#L29 - setActionPopup(getPopoverUrl); - } +async function initBrowserActionMv2() { + browserAction.onClicked.addListener(handleBrowserAction); + + // Track the active tab URL. We need to update the popover every time status the active tab/active URL changes. + // https://github.com/facebook/react/blob/bbb9cb116dbf7b6247721aa0c4bcb6ec249aa8af/packages/react-devtools-extensions/src/background/tabsManager.js#L29 + setActionPopup(getPopoverUrl); } + +// eslint-disable-next-line local-rules/persistBackgroundData -- Function +const initBrowserAction = isMV3() ? initBrowserActionMv3 : initBrowserActionMv2; +export default initBrowserAction; diff --git a/src/background/sidePanel.ts b/src/background/sidePanel.ts index 249eb60801..30b8c21e96 100644 --- a/src/background/sidePanel.ts +++ b/src/background/sidePanel.ts @@ -20,6 +20,7 @@ import type { MessengerMeta } from "webext-messenger"; export async function showSidebarPanel(this: MessengerMeta): Promise { const tabId = this.trace[0].tab.id; + // TODO: Use the native promises if possible return new Promise((resolve, reject) => { // Unlike the Chrome example, call setOptions first to handle the case where the sidebar was closed on the tab // https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/functional-samples/cookbook.sidepanel-open/script.js#L9 diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index 2fae7cce75..8bc2938cc3 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -44,6 +44,8 @@ import { getTemporaryPanelSidebarEntries } from "@/bricks/transformers/temporary import { getFormPanelSidebarEntries } from "@/contentScript/ephemeralFormProtocol"; import { logPromiseDuration } from "@/utils/promiseUtils"; import { waitAnimationFrame } from "@/utils/domUtils"; +import { isMV3 } from "@/mv3/api"; +import { showSidebarPanel } from "@/background/messenger/api"; export const HIDE_SIDEBAR_EVENT_NAME = "pixiebrix:hideSidebar"; @@ -66,7 +68,55 @@ let modActivationPanelEntry: ModActivationPanelEntry | null = null; * Attach the sidebar to the page if it's not already attached. Then re-renders all panels. * @param activateOptions options controlling the visible panel in the sidebar */ -export async function showSidebar( +export const showSidebar: typeof showSidebarMv2 /* Ensure that the API matches */ = + isMV3() ? showSidebarMv3 : showSidebarMv2; + +async function showSidebarMv3( + activateOptions: ActivatePanelOptions = {}, +): Promise { + console.debug("sidebarController:showSidebar"); + reportEvent(Events.SIDEBAR_SHOW); + await showSidebarPanel(); + + try { + await sidebarInThisTab.pingSidebar(); + } catch (error) { + throw new Error("The sidebar did not respond in time", { cause: error }); + } + + if (activateOptions.refresh ?? true) { + // Run the sidebar extension points available on the page. If the sidebar is already in the page, running + // all the callbacks ensures the content is up-to-date + + // Currently, this runs the listening SidebarExtensionPoint.run callbacks in not particular order. Also note that + // we're not awaiting their resolution (because they may contain long-running bricks). + if (!isSidebarFrameVisible()) { + console.error( + "Pre-condition failed: sidebar is not attached in the page for call to sidebarShowEvents.emit", + ); + } + + console.debug("sidebarController:showSidebar emitting sidebarShowEvents", { + isSidebarFrameVisible: isSidebarFrameVisible(), + }); + + sidebarShowEvents.emit({ reason: RunReason.MANUAL }); + } + + if (!isEmpty(activateOptions)) { + const seqNum = renderSequenceNumber; + renderSequenceNumber++; + + // The sidebarSlice handles the race condition with the panels loading by keeping track of the latest pending + // activatePanel request. + await sidebarInThisTab.activatePanel(seqNum, { + ...activateOptions, + force: activateOptions.force, + }); + } +} + +async function showSidebarMv2( activateOptions: ActivatePanelOptions = {}, ): Promise { console.debug("sidebarController:showSidebar", { @@ -77,7 +127,7 @@ export async function showSidebar( const isAlreadyShowing = isSidebarFrameVisible(); if (!isAlreadyShowing) { - insertSidebarFrame(); + await insertSidebarFrame(); } try { diff --git a/src/contentScript/sidebarDomControllerLite.ts b/src/contentScript/sidebarDomControllerLite.ts index d7070b998b..a978dc38c4 100644 --- a/src/contentScript/sidebarDomControllerLite.ts +++ b/src/contentScript/sidebarDomControllerLite.ts @@ -18,12 +18,16 @@ /** * @file This file MUST not have dependencies as it's meant to be tiny * and imported by browserActionInstantHandler.ts + * @file MV3 NOTE: This file should eventually be dropped as it's just a shim + * for the old API. The new sidePanel does not depend on the DOM. */ import { MAX_Z_INDEX, PANEL_FRAME_ID } from "@/domConstants"; import shadowWrap from "@/utils/shadowWrap"; import { expectContext } from "@/utils/expectContext"; import { uuidv4 } from "@/types/helpers"; +import { hideSidebarPanel, showSidebarPanel } from "@/background/messenger/api"; +import { isMV3 } from "@/mv3/api"; export const SIDEBAR_WIDTH_CSS_PROPERTY = "--pb-sidebar-width"; const ORIGINAL_MARGIN_CSS_PROPERTY = "--pb-original-margin-right"; @@ -68,82 +72,94 @@ function getSidebar(): Element | null { * Return true if the sidebar frame is in the DOM. The sidebar might not be initialized yet. */ export function isSidebarFrameVisible(): boolean { + if (isMV3()) { + console.warn( + "isSidebarFrameVisible: MV3 requires a different implementation", + ); + return null as unknown as false; + } + return Boolean(getSidebar()); } /** Removes the element; Returns false if no element was found */ -export function removeSidebarFrame(): boolean { - const sidebar = getSidebar(); +export const removeSidebarFrame = isMV3() + ? hideSidebarPanel + : (): boolean => { + const sidebar = getSidebar(); - console.debug("sidebarDomControllerLite:removeSidebarFrame", { - isSidebarFrameVisible: Boolean(sidebar), - }); + console.debug("sidebarDomControllerLite:removeSidebarFrame", { + isSidebarFrameVisible: Boolean(sidebar), + }); - if (sidebar) { - sidebar.remove(); - setSidebarWidth(0); - } + if (sidebar) { + sidebar.remove(); + setSidebarWidth(0); + } - return Boolean(sidebar); -} + return Boolean(sidebar); + }; /** Inserts the element; Returns false if it already existed */ -export function insertSidebarFrame(): boolean { - console.debug("sidebarDomControllerLite:insertSidebarFrame", { - isSidebarFrameVisible: isSidebarFrameVisible(), - }); - - if (isSidebarFrameVisible()) { - console.debug("insertSidebarFrame: sidebar frame already exists"); - return false; - } - - storeOriginalCSSOnce(); - const nonce = uuidv4(); - const actionURL = browser.runtime.getURL("sidebar.html"); - - setSidebarWidth(SIDEBAR_WIDTH_PX); - - const iframe = document.createElement("iframe"); - iframe.src = `${actionURL}?nonce=${nonce}`; - - Object.assign(iframe.style, { - position: "fixed", - top: 0, - right: 0, - // `-1` keeps it under the QuickBar #4130 - zIndex: MAX_Z_INDEX - 1, - - // Note that it can't use the variable because the frame is in the shadow DOM - width: CSS.px(SIDEBAR_WIDTH_PX), - height: "100%", - border: 0, - borderLeft: "1px solid lightgray", - - // Note that it can't use our CSS variables because this element lives on the host - background: "#f9f8fa", - }); - - const wrapper = shadowWrap(iframe); - wrapper.id = PANEL_FRAME_ID; - html.append(wrapper); - - iframe.animate([{ translate: "50%" }, { translate: 0 }], { - duration: 500, - easing: "cubic-bezier(0.23, 1, 0.32, 1)", - }); - - if (!isSidebarFrameVisible()) { - console.error( - "Post-condition failed: isSidebarFrameVisible is false after insertSidebarFrame", - ); - } - - return true; -} +export const insertSidebarFrame = isMV3() + ? showSidebarPanel + : (): boolean => { + console.debug("sidebarDomControllerLite:insertSidebarFrame", { + isSidebarFrameVisible: isSidebarFrameVisible(), + }); + + if (isSidebarFrameVisible()) { + console.debug("insertSidebarFrame: sidebar frame already exists"); + return false; + } + + storeOriginalCSSOnce(); + const nonce = uuidv4(); + const actionURL = browser.runtime.getURL("sidebar.html"); + + setSidebarWidth(SIDEBAR_WIDTH_PX); + + const iframe = document.createElement("iframe"); + iframe.src = `${actionURL}?nonce=${nonce}`; + + Object.assign(iframe.style, { + position: "fixed", + top: 0, + right: 0, + // `-1` keeps it under the QuickBar #4130 + zIndex: MAX_Z_INDEX - 1, + + // Note that it can't use the variable because the frame is in the shadow DOM + width: CSS.px(SIDEBAR_WIDTH_PX), + height: "100%", + border: 0, + borderLeft: "1px solid lightgray", + + // Note that it can't use our CSS variables because this element lives on the host + background: "#f9f8fa", + }); + + const wrapper = shadowWrap(iframe); + wrapper.id = PANEL_FRAME_ID; + html.append(wrapper); + + iframe.animate([{ translate: "50%" }, { translate: 0 }], { + duration: 500, + easing: "cubic-bezier(0.23, 1, 0.32, 1)", + }); + + if (!isSidebarFrameVisible()) { + console.error( + "Post-condition failed: isSidebarFrameVisible is false after insertSidebarFrame", + ); + } + + return true; + }; /** * Toggle the sidebar frame. Returns true if the sidebar is now visible, false otherwise. + * MV2 only */ export function toggleSidebarFrame(): boolean { console.debug("sidebarDomControllerLite:toggleSidebarFrame", { diff --git a/src/mv3/sidePanel.ts b/src/mv3/sidePanel.ts new file mode 100644 index 0000000000..b2d5b393d2 --- /dev/null +++ b/src/mv3/sidePanel.ts @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2023 PixieBrix, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** @file Temporary API shims useful for the MV3 transition */ + +import * as contentScriptApi from "@/contentScript/messenger/api"; +import { isMV3 } from "./api"; +import { getCurrentURL, thisTab } from "@/pageEditor/utils"; + +import { getExtensionConsoleUrl } from "@/utils/extensionUtils"; +import { + DISPLAY_REASON_EXTENSION_CONSOLE, + DISPLAY_REASON_RESTRICTED_URL, +} from "@/tinyPages/restrictedUrlPopupConstants"; +import { type ActivatePanelOptions } from "@/types/sidebarTypes"; +import { isScriptableUrl } from "webext-content-scripts"; + +/** + * Show a popover on restricted URLs because we're unable to inject content into the page. Previously we'd open + * the Extension Console, but that was confusing because the action was inconsistent with how the button behaves + * other pages. + * @param tabUrl the url of the tab, or null if not accessible + */ +export function getPopoverUrl(tabUrl: string | null): string | null { + const popoverUrl = browser.runtime.getURL("restrictedUrlPopup.html"); + + if (tabUrl?.startsWith(getExtensionConsoleUrl())) { + return `${popoverUrl}?reason=${DISPLAY_REASON_EXTENSION_CONSOLE}`; + } + + if (!isScriptableUrl(tabUrl)) { + return `${popoverUrl}?reason=${DISPLAY_REASON_RESTRICTED_URL}`; + } + + // The popup is disabled, and the extension will receive browserAction.onClicked events. + // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/setPopup#popup + return null; +} + +export function getSidebarPath(tabId: number, url: string): string { + return getPopoverUrl(url) ?? "sidebar.html?tabId=" + tabId; +} + +// Inline type ensures we match the API +export async function showSidebarFromPageEditor( + activateOptions?: ActivatePanelOptions, +) { + if (isMV3()) { + return openSidePanel( + chrome.devtools.inspectedWindow.tabId, + await getCurrentURL(), + ); + } + + return contentScriptApi.showSidebar(thisTab, activateOptions); +} + +export async function openSidePanel(tabId: number, url: string) { + // Simultaneously define, enable, and open the side panel + void chrome.sidePanel.setOptions({ + tabId, + path: getSidebarPath(tabId, url), + enabled: true, + }); + await chrome.sidePanel.open({ tabId }); + // NOTE: at this point, the sidebar should already be visible on the page, even if not ready. + await contentScriptApi.rehydrateSidebar({ + tabId, + }); +} diff --git a/src/pageEditor/panes/insert/useAutoInsert.ts b/src/pageEditor/panes/insert/useAutoInsert.ts index 33851d7afb..ace106cdbe 100644 --- a/src/pageEditor/panes/insert/useAutoInsert.ts +++ b/src/pageEditor/panes/insert/useAutoInsert.ts @@ -5,10 +5,8 @@ import { internalStarterBrickMetaFactory } from "@/pageEditor/starterBricks/base import { type ModComponentFormState } from "@/pageEditor/starterBricks/formStateTypes"; import { getExampleBrickPipeline } from "@/pageEditor/exampleStarterBrickConfigs"; import { actions } from "@/pageEditor/slices/editorSlice"; -import { - showSidebar, - updateDynamicElement, -} from "@/contentScript/messenger/api"; +import { updateDynamicElement } from "@/contentScript/messenger/api"; +import { showSidebarFromPageEditor } from "@/mv3/sidePanel"; import reportEvent from "@/telemetry/reportEvent"; import { Events } from "@/telemetry/events"; import { type StarterBrickType } from "@/types/starterBrickTypes"; @@ -60,7 +58,7 @@ function useAutoInsert(type: StarterBrickType): void { if (config.elementType === "actionPanel") { // For convenience, open the side panel if it's not already open so that the user doesn't // have to manually toggle it - void showSidebar(thisTab); + void showSidebarFromPageEditor(); } reportEvent(Events.MOD_COMPONENT_ADD_NEW, { diff --git a/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx b/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx index 79491d433f..d89187ed94 100644 --- a/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx +++ b/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx @@ -30,11 +30,8 @@ import { ExtensionIcon, NotAvailableIcon, } from "@/pageEditor/sidebar/ExtensionIcons"; -import { - disableOverlay, - enableOverlay, - showSidebar, -} from "@/contentScript/messenger/api"; +import { disableOverlay, enableOverlay } from "@/contentScript/messenger/api"; +import { showSidebarFromPageEditor } from "@/mv3/sidePanel"; import { thisTab } from "@/pageEditor/utils"; import cx from "classnames"; import { selectSessionId } from "@/pageEditor/slices/sessionSelectors"; @@ -116,7 +113,7 @@ const ActivatedModComponentListItem: React.FunctionComponent<{ if (type === "actionPanel") { // Switch the sidepanel over to the panel. However, don't refresh because the user might be switching // frequently between extensions within the same blueprint. - void showSidebar(thisTab, { + void showSidebarFromPageEditor({ extensionId: modComponent.id, force: true, refresh: false, diff --git a/src/pageEditor/sidebar/DynamicModComponentListItem.tsx b/src/pageEditor/sidebar/DynamicModComponentListItem.tsx index b963bf1fdb..faae413589 100644 --- a/src/pageEditor/sidebar/DynamicModComponentListItem.tsx +++ b/src/pageEditor/sidebar/DynamicModComponentListItem.tsx @@ -27,11 +27,8 @@ import { UnsavedChangesIcon, } from "@/pageEditor/sidebar/ExtensionIcons"; import { type UUID } from "@/types/stringTypes"; -import { - disableOverlay, - enableOverlay, - showSidebar, -} from "@/contentScript/messenger/api"; +import { disableOverlay, enableOverlay } from "@/contentScript/messenger/api"; +import { showSidebarFromPageEditor } from "@/mv3/sidePanel"; import { thisTab } from "@/pageEditor/utils"; import cx from "classnames"; import reportEvent from "@/telemetry/reportEvent"; @@ -165,7 +162,7 @@ const DynamicModComponentListItem: React.FunctionComponent< if (modComponentFormState.type === "actionPanel") { // Switch the sidepanel over to the panel. However, don't refresh because the user might be switching // frequently between extensions within the same blueprint. - void showSidebar(thisTab, { + void showSidebarFromPageEditor({ extensionId: modComponentFormState.uuid, force: true, refresh: false, From a3c4ab649f84ab80073a78829e1a324f7b74f88d Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Thu, 4 Jan 2024 21:11:31 +0800 Subject: [PATCH 08/29] savegame --- src/background/browserAction.ts | 78 +------- src/bricks/effects/sidebar.ts | 2 +- .../ephemeralForm/formTransformer.ts | 1 - .../temporaryInfo/DisplayTemporaryInfo.ts | 1 - .../browserActionInstantHandler.ts | 21 --- src/contentScript/messenger/api.ts | 2 - src/contentScript/messenger/registration.ts | 9 - src/contentScript/sidebarActivation.ts | 1 - src/contentScript/sidebarController.tsx | 129 +------------ src/contentScript/sidebarDomControllerLite.ts | 176 ------------------ src/mv3/sidePanel.ts | 84 --------- src/pageEditor/panes/insert/useAutoInsert.ts | 2 +- .../sidebar/ActivatedModComponentListItem.tsx | 2 +- .../sidebar/DynamicModComponentListItem.tsx | 2 +- src/sidebar/Header.tsx | 26 +-- src/sidebar/SidebarErrorBoundary.tsx | 7 +- src/sidebar/Tabs.test.tsx | 2 +- .../activateMod/ActivateModPanel.test.tsx | 2 +- src/sidebar/messenger/api.ts | 4 +- src/sidebar/messenger/registration.ts | 2 + src/sidebar/sidePanel.tsx | 103 ++++++++-- src/sidebar/sidebar.tsx | 2 + src/sidebar/useHideEmptySidebar.ts | 14 +- src/starterBricks/sidebarExtension.ts | 4 +- src/tsconfig.strictNullChecks.json | 2 - src/utils/notify.tsx | 5 - webpack.config.mjs | 1 - 27 files changed, 124 insertions(+), 560 deletions(-) delete mode 100644 src/contentScript/browserActionInstantHandler.ts delete mode 100644 src/contentScript/sidebarDomControllerLite.ts delete mode 100644 src/mv3/sidePanel.ts diff --git a/src/background/browserAction.ts b/src/background/browserAction.ts index 9488ded1ce..a17d755ac2 100644 --- a/src/background/browserAction.ts +++ b/src/background/browserAction.ts @@ -15,70 +15,10 @@ * along with this program. If not, see . */ -import { ensureContentScript } from "@/background/contentScript"; -import { rehydrateSidebar } from "@/contentScript/messenger/api"; -import webextAlert from "./webextAlert"; -import { browserAction, isMV3, type Tab } from "@/mv3/api"; -import { executeScript } from "webext-content-scripts"; -import { memoizeUntilSettled } from "@/utils/promiseUtils"; -import { setActionPopup } from "webext-tools"; -import { getPopoverUrl, getSidebarPath, openSidePanel } from "@/mv3/sidePanel"; +import { browserAction, type Tab } from "@/mv3/api"; +import { getSidebarPath, openSidePanel } from "@/sidebar/sidePanel"; -const ERR_UNABLE_TO_OPEN = - "PixieBrix was unable to open the Sidebar. Try refreshing the page."; - -// The sidebar is always injected to into the top level frame -const TOP_LEVEL_FRAME_ID = 0; - -const toggleSidebar = memoizeUntilSettled(_toggleSidebar); - -// Don't accept objects here as they're not easily memoizable -async function _toggleSidebar(tabId: number, tabUrl: string): Promise { - console.debug("browserAction:toggleSidebar", tabId, tabUrl); - - // Load the raw toggle script first, then the content script. The browser executes them - // in order, but we don't need to use `Promise.all` to await them at the same time as we - // want to catch each error separately. - const sidebarTogglePromise = executeScript({ - tabId, - frameId: TOP_LEVEL_FRAME_ID, - files: ["browserActionInstantHandler.js"], - matchAboutBlank: false, - allFrames: false, - // Run at end instead of idle to ensure immediate feedback to clicking the browser action icon - runAt: "document_end", - }); - - // Chrome adds automatically at document_idle, so it might not be ready yet when the user click the browser action - const contentScriptPromise = ensureContentScript({ - tabId, - frameId: TOP_LEVEL_FRAME_ID, - }); - - try { - await sidebarTogglePromise; - } catch (error) { - webextAlert(ERR_UNABLE_TO_OPEN); - throw error; - } - - // NOTE: at this point, the sidebar should already be visible on the page, even if not ready. - // Avoid showing any alerts or notifications: further messaging can appear in the sidebar itself. - // Any errors are automatically reported by the global error handler. - await contentScriptPromise; - await rehydrateSidebar({ - tabId, - }); -} - -async function handleBrowserAction(tab: Tab): Promise { - // The URL might not be available in certain circumstances. This silences these - // cases and just treats them as "not allowed on this page" - const url = String(tab.url); - await toggleSidebar(tab.id, url); -} - -async function initBrowserActionMv3(): Promise { +export default async function initBrowserAction(): Promise { void chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }); // Disable by default, so that it can be enabled on a per-tab basis @@ -98,15 +38,3 @@ async function initBrowserActionMv3(): Promise { } }); } - -async function initBrowserActionMv2() { - browserAction.onClicked.addListener(handleBrowserAction); - - // Track the active tab URL. We need to update the popover every time status the active tab/active URL changes. - // https://github.com/facebook/react/blob/bbb9cb116dbf7b6247721aa0c4bcb6ec249aa8af/packages/react-devtools-extensions/src/background/tabsManager.js#L29 - setActionPopup(getPopoverUrl); -} - -// eslint-disable-next-line local-rules/persistBackgroundData -- Function -const initBrowserAction = isMV3() ? initBrowserActionMv3 : initBrowserActionMv2; -export default initBrowserAction; diff --git a/src/bricks/effects/sidebar.ts b/src/bricks/effects/sidebar.ts index baaa83a318..d709b616a9 100644 --- a/src/bricks/effects/sidebar.ts +++ b/src/bricks/effects/sidebar.ts @@ -18,7 +18,7 @@ import { EffectABC } from "@/types/bricks/effectTypes"; import { type BrickArgs, type BrickOptions } from "@/types/runtimeTypes"; import { type Schema, SCHEMA_EMPTY_OBJECT } from "@/types/schemaTypes"; -import { hideSidebar, showSidebar } from "@/contentScript/sidebarController"; +import { showSidebar } from "@/contentScript/sidebarController"; import { propertiesToSchema } from "@/validators/generic"; import { logPromiseDuration } from "@/utils/promiseUtils"; diff --git a/src/bricks/transformers/ephemeralForm/formTransformer.ts b/src/bricks/transformers/ephemeralForm/formTransformer.ts index 12df6f7662..327d1f66e5 100644 --- a/src/bricks/transformers/ephemeralForm/formTransformer.ts +++ b/src/bricks/transformers/ephemeralForm/formTransformer.ts @@ -27,7 +27,6 @@ import { expectContext } from "@/utils/expectContext"; import { ensureSidebar, hideSidebarForm, - HIDE_SIDEBAR_EVENT_NAME, showSidebarForm, } from "@/contentScript/sidebarController"; import { showModal } from "@/bricks/transformers/ephemeralForm/modalUtils"; diff --git a/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts b/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts index 6b248c1299..975cb18645 100644 --- a/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts +++ b/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts @@ -24,7 +24,6 @@ import { import { expectContext } from "@/utils/expectContext"; import { ensureSidebar, - HIDE_SIDEBAR_EVENT_NAME, hideTemporarySidebarPanel, showTemporarySidebarPanel, updateTemporarySidebarPanel, diff --git a/src/contentScript/browserActionInstantHandler.ts b/src/contentScript/browserActionInstantHandler.ts deleted file mode 100644 index f3ae5ab242..0000000000 --- a/src/contentScript/browserActionInstantHandler.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (C) 2023 PixieBrix, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -/** @file This file MUST be lightweight and free of any logic and dependencies, it's meant to be instant */ -import { toggleSidebarFrame } from "@/contentScript/sidebarDomControllerLite"; - -toggleSidebarFrame(); diff --git a/src/contentScript/messenger/api.ts b/src/contentScript/messenger/api.ts index 28f796d741..9b24bd8768 100644 --- a/src/contentScript/messenger/api.ts +++ b/src/contentScript/messenger/api.ts @@ -46,8 +46,6 @@ export const rehydrateSidebar = getMethod("REHYDRATE_SIDEBAR"); export const getReservedSidebarEntries = getMethod( "GET_RESERVED_SIDEBAR_ENTRIES", ); -export const showSidebar = getMethod("SHOW_SIDEBAR"); -export const hideSidebar = getMethod("HIDE_SIDEBAR"); export const reloadSidebar = getMethod("RELOAD_SIDEBAR"); export const removeSidebars = getMethod("REMOVE_SIDEBARS"); export const insertPanel = getMethod("INSERT_PANEL"); diff --git a/src/contentScript/messenger/registration.ts b/src/contentScript/messenger/registration.ts index 43e9a159ed..2d8a924cab 100644 --- a/src/contentScript/messenger/registration.ts +++ b/src/contentScript/messenger/registration.ts @@ -32,9 +32,6 @@ import { cancelForm, } from "@/contentScript/ephemeralFormProtocol"; import { - hideSidebar, - showSidebar, - rehydrateSidebar, removeExtensions as removeSidebars, reloadSidebar, getReservedPanelEntries, @@ -103,9 +100,6 @@ declare global { TOGGLE_QUICK_BAR: typeof toggleQuickBar; HANDLE_MENU_ACTION: typeof handleMenuAction; - REHYDRATE_SIDEBAR: typeof rehydrateSidebar; - SHOW_SIDEBAR: typeof showSidebar; - HIDE_SIDEBAR: typeof hideSidebar; GET_RESERVED_SIDEBAR_ENTRIES: typeof getReservedPanelEntries; RELOAD_SIDEBAR: typeof reloadSidebar; REMOVE_SIDEBARS: typeof removeSidebars; @@ -172,9 +166,6 @@ export default function registerMessenger(): void { TOGGLE_QUICK_BAR: toggleQuickBar, HANDLE_MENU_ACTION: handleMenuAction, - REHYDRATE_SIDEBAR: rehydrateSidebar, - SHOW_SIDEBAR: showSidebar, - HIDE_SIDEBAR: hideSidebar, RELOAD_SIDEBAR: reloadSidebar, REMOVE_SIDEBARS: removeSidebars, diff --git a/src/contentScript/sidebarActivation.ts b/src/contentScript/sidebarActivation.ts index 6504548d12..6d2d2ebfb8 100644 --- a/src/contentScript/sidebarActivation.ts +++ b/src/contentScript/sidebarActivation.ts @@ -19,7 +19,6 @@ import { type RegistryId } from "@/types/registryTypes"; import { isRegistryId } from "@/types/helpers"; import { ensureSidebar, - HIDE_SIDEBAR_EVENT_NAME, hideModActivationInSidebar, showModActivationInSidebar, } from "@/contentScript/sidebarController"; diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index 8bc2938cc3..882756a7de 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -22,11 +22,6 @@ import { expectContext } from "@/utils/expectContext"; import sidebarInThisTab from "@/sidebar/messenger/api"; import { isEmpty } from "lodash"; import { SimpleEventTarget } from "@/utils/SimpleEventTarget"; -import { - insertSidebarFrame, - isSidebarFrameVisible, - removeSidebarFrame, -} from "@/contentScript/sidebarDomControllerLite"; import { type Except } from "type-fest"; import { type RunArgs, RunReason } from "@/types/runtimeTypes"; import { type UUID } from "@/types/stringTypes"; @@ -44,11 +39,8 @@ import { getTemporaryPanelSidebarEntries } from "@/bricks/transformers/temporary import { getFormPanelSidebarEntries } from "@/contentScript/ephemeralFormProtocol"; import { logPromiseDuration } from "@/utils/promiseUtils"; import { waitAnimationFrame } from "@/utils/domUtils"; -import { isMV3 } from "@/mv3/api"; import { showSidebarPanel } from "@/background/messenger/api"; -export const HIDE_SIDEBAR_EVENT_NAME = "pixiebrix:hideSidebar"; - /** * Sequence number for ensuring render requests are handled in order */ @@ -68,10 +60,7 @@ let modActivationPanelEntry: ModActivationPanelEntry | null = null; * Attach the sidebar to the page if it's not already attached. Then re-renders all panels. * @param activateOptions options controlling the visible panel in the sidebar */ -export const showSidebar: typeof showSidebarMv2 /* Ensure that the API matches */ = - isMV3() ? showSidebarMv3 : showSidebarMv2; - -async function showSidebarMv3( +export async function showSidebar( activateOptions: ActivatePanelOptions = {}, ): Promise { console.debug("sidebarController:showSidebar"); @@ -116,67 +105,6 @@ async function showSidebarMv3( } } -async function showSidebarMv2( - activateOptions: ActivatePanelOptions = {}, -): Promise { - console.debug("sidebarController:showSidebar", { - isSidebarFrameVisible: isSidebarFrameVisible(), - }); - - reportEvent(Events.SIDEBAR_SHOW); - const isAlreadyShowing = isSidebarFrameVisible(); - - if (!isAlreadyShowing) { - await insertSidebarFrame(); - } - - try { - await sidebarInThisTab.pingSidebar(); - } catch (error) { - throw new Error("The sidebar did not respond in time", { cause: error }); - } - - if (!isAlreadyShowing || (activateOptions.refresh ?? true)) { - // Run the sidebar extension points available on the page. If the sidebar is already in the page, running - // all the callbacks ensures the content is up-to-date - - // Currently, this runs the listening SidebarExtensionPoint.run callbacks in not particular order. Also note that - // we're not awaiting their resolution (because they may contain long-running bricks). - if (!isSidebarFrameVisible()) { - console.error( - "Pre-condition failed: sidebar is not attached in the page for call to sidebarShowEvents.emit", - ); - } - - console.debug("sidebarController:showSidebar emitting sidebarShowEvents", { - isSidebarFrameVisible: isSidebarFrameVisible(), - }); - - sidebarShowEvents.emit({ reason: RunReason.MANUAL }); - } - - if (!isEmpty(activateOptions)) { - const seqNum = renderSequenceNumber; - renderSequenceNumber++; - - // The sidebarSlice handles the race condition with the panels loading by keeping track of the latest pending - // activatePanel request. - void sidebarInThisTab - .activatePanel(seqNum, { - ...activateOptions, - // If the sidebar wasn't showing, force the behavior. (Otherwise, there's a race on the initial activation, - // where depending on when the message is received, the sidebar might already be showing a panel) - force: activateOptions.force || !isAlreadyShowing, - }) - // eslint-disable-next-line promise/prefer-await-to-then -- not in an async method - .catch((error: unknown) => { - reportError( - new Error("Error activating sidebar panel", { cause: error }), - ); - }); - } -} - /** * Force-show the panel for the given extension id * @param extensionId the extension UUID @@ -212,66 +140,13 @@ export async function ensureSidebar(): Promise { } } -/** - * Hide the sidebar. Dispatches HIDE_SIDEBAR_EVENT_NAME event even if the sidebar is not currently visible. - * @see HIDE_SIDEBAR_EVENT_NAME - */ -export function hideSidebar(): void { - console.debug("sidebarController:hideSidebar", { - isSidebarFrameVisible: isSidebarFrameVisible(), - }); - - reportEvent(Events.SIDEBAR_HIDE); - removeSidebarFrame(); - window.dispatchEvent(new CustomEvent(HIDE_SIDEBAR_EVENT_NAME)); -} - /** * Reload the sidebar and its content. * * Known limitations: * - Does not reload ephemeral forms */ -export async function reloadSidebar(): Promise { - console.debug("sidebarController:reloadSidebar"); - - // Hide and reshow to force a full-refresh of the sidebar - - if (isSidebarFrameVisible()) { - hideSidebar(); - } - - await showSidebar(); -} - -/** - * Rehydrate the already visible sidebar. - * - * For use with background/browserAction. - * - `browserAction` calls toggleSidebarFrame to immediately adds the sidebar iframe - * - It injects the content script - * - It calls this method via messenger to complete the sidebar initialization - */ -export async function rehydrateSidebar(): Promise { - // Ensure DOM state is ready for accurate call to isSidebarFrameVisible. Shouldn't strictly be necessary, but - // giving it a try and shouldn't impact performance. The background page has limited ability to determine when it's - // OK to call rehydrateSidebar via messenger. See background/browserAction.ts. - await waitAnimationFrame(); - - // To assist with debugging race conditions in sidebar initialization - console.debug("sidebarController:rehydrateSidebar", { - isSidebarFrameVisible: isSidebarFrameVisible(), - }); - - if (isSidebarFrameVisible()) { - // `showSidebar` includes the logic to hydrate it - // `refresh: true` is the default, but be explicit that the sidebarShowEvents must run. - void showSidebar({ refresh: true }); - } else { - // `hideSidebar` includes events to cleanup the sidebar - hideSidebar(); - } -} +export async function reloadSidebar(): Promise {} function renderPanelsIfVisible(): void { expectContext("contentScript"); diff --git a/src/contentScript/sidebarDomControllerLite.ts b/src/contentScript/sidebarDomControllerLite.ts deleted file mode 100644 index a978dc38c4..0000000000 --- a/src/contentScript/sidebarDomControllerLite.ts +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 2023 PixieBrix, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -/** - * @file This file MUST not have dependencies as it's meant to be tiny - * and imported by browserActionInstantHandler.ts - * @file MV3 NOTE: This file should eventually be dropped as it's just a shim - * for the old API. The new sidePanel does not depend on the DOM. - */ - -import { MAX_Z_INDEX, PANEL_FRAME_ID } from "@/domConstants"; -import shadowWrap from "@/utils/shadowWrap"; -import { expectContext } from "@/utils/expectContext"; -import { uuidv4 } from "@/types/helpers"; -import { hideSidebarPanel, showSidebarPanel } from "@/background/messenger/api"; -import { isMV3 } from "@/mv3/api"; - -export const SIDEBAR_WIDTH_CSS_PROPERTY = "--pb-sidebar-width"; -const ORIGINAL_MARGIN_CSS_PROPERTY = "--pb-original-margin-right"; - -// Use ? because it's not defined during header generation. But otherwise it will always be defined. -// eslint-disable-next-line local-rules/persistBackgroundData -- Static -const html: HTMLElement = globalThis.document?.documentElement; -const SIDEBAR_WIDTH_PX = 400; - -function storeOriginalCSSOnce() { - if (html.style.getPropertyValue(ORIGINAL_MARGIN_CSS_PROPERTY)) { - return; - } - - // Store the original margin so it can be reused in future calculations. It must also persist across sessions - html.style.setProperty( - ORIGINAL_MARGIN_CSS_PROPERTY, - getComputedStyle(html).getPropertyValue("margin-right"), - ); - - // Make margin dynamic so it always follows the original margin AND the sidebar width, if open - html.style.setProperty( - "margin-right", - `calc(var(${ORIGINAL_MARGIN_CSS_PROPERTY}) + var(${SIDEBAR_WIDTH_CSS_PROPERTY}))`, - ); -} - -function setSidebarWidth(pixels: number): void { - html.style.setProperty(SIDEBAR_WIDTH_CSS_PROPERTY, `${pixels}px`); -} - -/** - * Returns the sidebar frame if it's in the DOM, or null otherwise. The sidebar might not be initialized yet. - */ -function getSidebar(): Element | null { - expectContext("contentScript"); - - return html.querySelector(`#${PANEL_FRAME_ID}`); -} - -/** - * Return true if the sidebar frame is in the DOM. The sidebar might not be initialized yet. - */ -export function isSidebarFrameVisible(): boolean { - if (isMV3()) { - console.warn( - "isSidebarFrameVisible: MV3 requires a different implementation", - ); - return null as unknown as false; - } - - return Boolean(getSidebar()); -} - -/** Removes the element; Returns false if no element was found */ -export const removeSidebarFrame = isMV3() - ? hideSidebarPanel - : (): boolean => { - const sidebar = getSidebar(); - - console.debug("sidebarDomControllerLite:removeSidebarFrame", { - isSidebarFrameVisible: Boolean(sidebar), - }); - - if (sidebar) { - sidebar.remove(); - setSidebarWidth(0); - } - - return Boolean(sidebar); - }; - -/** Inserts the element; Returns false if it already existed */ -export const insertSidebarFrame = isMV3() - ? showSidebarPanel - : (): boolean => { - console.debug("sidebarDomControllerLite:insertSidebarFrame", { - isSidebarFrameVisible: isSidebarFrameVisible(), - }); - - if (isSidebarFrameVisible()) { - console.debug("insertSidebarFrame: sidebar frame already exists"); - return false; - } - - storeOriginalCSSOnce(); - const nonce = uuidv4(); - const actionURL = browser.runtime.getURL("sidebar.html"); - - setSidebarWidth(SIDEBAR_WIDTH_PX); - - const iframe = document.createElement("iframe"); - iframe.src = `${actionURL}?nonce=${nonce}`; - - Object.assign(iframe.style, { - position: "fixed", - top: 0, - right: 0, - // `-1` keeps it under the QuickBar #4130 - zIndex: MAX_Z_INDEX - 1, - - // Note that it can't use the variable because the frame is in the shadow DOM - width: CSS.px(SIDEBAR_WIDTH_PX), - height: "100%", - border: 0, - borderLeft: "1px solid lightgray", - - // Note that it can't use our CSS variables because this element lives on the host - background: "#f9f8fa", - }); - - const wrapper = shadowWrap(iframe); - wrapper.id = PANEL_FRAME_ID; - html.append(wrapper); - - iframe.animate([{ translate: "50%" }, { translate: 0 }], { - duration: 500, - easing: "cubic-bezier(0.23, 1, 0.32, 1)", - }); - - if (!isSidebarFrameVisible()) { - console.error( - "Post-condition failed: isSidebarFrameVisible is false after insertSidebarFrame", - ); - } - - return true; - }; - -/** - * Toggle the sidebar frame. Returns true if the sidebar is now visible, false otherwise. - * MV2 only - */ -export function toggleSidebarFrame(): boolean { - console.debug("sidebarDomControllerLite:toggleSidebarFrame", { - isSidebarFrameVisible: isSidebarFrameVisible(), - }); - - if (isSidebarFrameVisible()) { - removeSidebarFrame(); - return false; - } - - insertSidebarFrame(); - return true; -} diff --git a/src/mv3/sidePanel.ts b/src/mv3/sidePanel.ts deleted file mode 100644 index b2d5b393d2..0000000000 --- a/src/mv3/sidePanel.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2023 PixieBrix, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -/** @file Temporary API shims useful for the MV3 transition */ - -import * as contentScriptApi from "@/contentScript/messenger/api"; -import { isMV3 } from "./api"; -import { getCurrentURL, thisTab } from "@/pageEditor/utils"; - -import { getExtensionConsoleUrl } from "@/utils/extensionUtils"; -import { - DISPLAY_REASON_EXTENSION_CONSOLE, - DISPLAY_REASON_RESTRICTED_URL, -} from "@/tinyPages/restrictedUrlPopupConstants"; -import { type ActivatePanelOptions } from "@/types/sidebarTypes"; -import { isScriptableUrl } from "webext-content-scripts"; - -/** - * Show a popover on restricted URLs because we're unable to inject content into the page. Previously we'd open - * the Extension Console, but that was confusing because the action was inconsistent with how the button behaves - * other pages. - * @param tabUrl the url of the tab, or null if not accessible - */ -export function getPopoverUrl(tabUrl: string | null): string | null { - const popoverUrl = browser.runtime.getURL("restrictedUrlPopup.html"); - - if (tabUrl?.startsWith(getExtensionConsoleUrl())) { - return `${popoverUrl}?reason=${DISPLAY_REASON_EXTENSION_CONSOLE}`; - } - - if (!isScriptableUrl(tabUrl)) { - return `${popoverUrl}?reason=${DISPLAY_REASON_RESTRICTED_URL}`; - } - - // The popup is disabled, and the extension will receive browserAction.onClicked events. - // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/setPopup#popup - return null; -} - -export function getSidebarPath(tabId: number, url: string): string { - return getPopoverUrl(url) ?? "sidebar.html?tabId=" + tabId; -} - -// Inline type ensures we match the API -export async function showSidebarFromPageEditor( - activateOptions?: ActivatePanelOptions, -) { - if (isMV3()) { - return openSidePanel( - chrome.devtools.inspectedWindow.tabId, - await getCurrentURL(), - ); - } - - return contentScriptApi.showSidebar(thisTab, activateOptions); -} - -export async function openSidePanel(tabId: number, url: string) { - // Simultaneously define, enable, and open the side panel - void chrome.sidePanel.setOptions({ - tabId, - path: getSidebarPath(tabId, url), - enabled: true, - }); - await chrome.sidePanel.open({ tabId }); - // NOTE: at this point, the sidebar should already be visible on the page, even if not ready. - await contentScriptApi.rehydrateSidebar({ - tabId, - }); -} diff --git a/src/pageEditor/panes/insert/useAutoInsert.ts b/src/pageEditor/panes/insert/useAutoInsert.ts index ace106cdbe..f09cf75f3c 100644 --- a/src/pageEditor/panes/insert/useAutoInsert.ts +++ b/src/pageEditor/panes/insert/useAutoInsert.ts @@ -6,7 +6,7 @@ import { type ModComponentFormState } from "@/pageEditor/starterBricks/formState import { getExampleBrickPipeline } from "@/pageEditor/exampleStarterBrickConfigs"; import { actions } from "@/pageEditor/slices/editorSlice"; import { updateDynamicElement } from "@/contentScript/messenger/api"; -import { showSidebarFromPageEditor } from "@/mv3/sidePanel"; +import { showSidebarFromPageEditor } from "@/sidebar/sidePanel"; import reportEvent from "@/telemetry/reportEvent"; import { Events } from "@/telemetry/events"; import { type StarterBrickType } from "@/types/starterBrickTypes"; diff --git a/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx b/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx index d89187ed94..8014d197d3 100644 --- a/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx +++ b/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx @@ -31,7 +31,7 @@ import { NotAvailableIcon, } from "@/pageEditor/sidebar/ExtensionIcons"; import { disableOverlay, enableOverlay } from "@/contentScript/messenger/api"; -import { showSidebarFromPageEditor } from "@/mv3/sidePanel"; +import { showSidebarFromPageEditor } from "@/sidebar/sidePanel"; import { thisTab } from "@/pageEditor/utils"; import cx from "classnames"; import { selectSessionId } from "@/pageEditor/slices/sessionSelectors"; diff --git a/src/pageEditor/sidebar/DynamicModComponentListItem.tsx b/src/pageEditor/sidebar/DynamicModComponentListItem.tsx index faae413589..c7c133f49f 100644 --- a/src/pageEditor/sidebar/DynamicModComponentListItem.tsx +++ b/src/pageEditor/sidebar/DynamicModComponentListItem.tsx @@ -28,7 +28,7 @@ import { } from "@/pageEditor/sidebar/ExtensionIcons"; import { type UUID } from "@/types/stringTypes"; import { disableOverlay, enableOverlay } from "@/contentScript/messenger/api"; -import { showSidebarFromPageEditor } from "@/mv3/sidePanel"; +import { showSidebarFromPageEditor } from "@/sidebar/sidePanel"; import { thisTab } from "@/pageEditor/utils"; import cx from "classnames"; import reportEvent from "@/telemetry/reportEvent"; diff --git a/src/sidebar/Header.tsx b/src/sidebar/Header.tsx index 986f9e87d6..c4ed48fa01 100644 --- a/src/sidebar/Header.tsx +++ b/src/sidebar/Header.tsx @@ -19,40 +19,16 @@ import React from "react"; import styles from "./ConnectedSidebar.module.scss"; import { Button } from "react-bootstrap"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faAngleDoubleRight, faCog } from "@fortawesome/free-solid-svg-icons"; -import { hideSidebar } from "@/contentScript/messenger/api"; +import { faCog } from "@fortawesome/free-solid-svg-icons"; import useTheme, { useGetTheme } from "@/hooks/useTheme"; import cx from "classnames"; -import useContextInvalidated from "@/hooks/useContextInvalidated"; -import { getTopLevelFrame } from "webext-messenger"; -import { isMV3 } from "@/mv3/api"; const Header: React.FunctionComponent = () => { const { logo, showSidebarLogo, customSidebarLogo } = useTheme(); const theme = useGetTheme(); - const wasContextInvalidated = useContextInvalidated(); return (
- {wasContextInvalidated || - isMV3() || ( // /* The button doesn't work after invalidation #2359 nor in sidePanel */ - - )} {showSidebarLogo && (
; diff --git a/src/sidebar/activateMod/ActivateModPanel.test.tsx b/src/sidebar/activateMod/ActivateModPanel.test.tsx index 7194223e1a..6232062422 100644 --- a/src/sidebar/activateMod/ActivateModPanel.test.tsx +++ b/src/sidebar/activateMod/ActivateModPanel.test.tsx @@ -67,7 +67,7 @@ const useRequiredModDefinitionsMock = jest.mocked(useRequiredModDefinitions); const checkModDefinitionPermissionsMock = jest.mocked( checkModDefinitionPermissions, ); -const hideSidebarSpy = jest.spyOn(messengerApi, "hideSidebar"); +const hideSidebarSpy = jest.spyOn(messengerApi, "reloadSidebar"); // TODO: Temporary mock rename just to silence errors jest.mock("@/starterBricks/starterBrickModUtils", () => { const actualUtils = jest.requireActual( diff --git a/src/sidebar/messenger/api.ts b/src/sidebar/messenger/api.ts index a634c38472..552e0fe12a 100644 --- a/src/sidebar/messenger/api.ts +++ b/src/sidebar/messenger/api.ts @@ -17,8 +17,9 @@ /* Do not use `registerMethod` in this file */ import { getMethod, getNotifier } from "webext-messenger"; +import { getAssociatedTabId } from "@/sidebar/sidePanel"; -const target = { tabId: "this", page: "/sidebar.html" } as const; +const target = { page: "/sidebar.html?tabId=" + getAssociatedTabId() } as const; const sidebarInThisTab = { renderPanels: getMethod("SIDEBAR_RENDER_PANELS", target), @@ -26,6 +27,7 @@ const sidebarInThisTab = { showForm: getMethod("SIDEBAR_SHOW_FORM", target), hideForm: getMethod("SIDEBAR_HIDE_FORM", target), pingSidebar: getMethod("SIDEBAR_PING", target), + reload: getNotifier("SIDEBAR_RELOAD", target), showTemporaryPanel: getMethod("SIDEBAR_SHOW_TEMPORARY_PANEL", target), updateTemporaryPanel: getNotifier("SIDEBAR_UPDATE_TEMPORARY_PANEL", target), hideTemporaryPanel: getMethod("SIDEBAR_HIDE_TEMPORARY_PANEL", target), diff --git a/src/sidebar/messenger/registration.ts b/src/sidebar/messenger/registration.ts index 172a334b57..9cb1274d7c 100644 --- a/src/sidebar/messenger/registration.ts +++ b/src/sidebar/messenger/registration.ts @@ -44,6 +44,7 @@ declare global { SIDEBAR_SHOW_FORM: typeof showForm; SIDEBAR_HIDE_FORM: typeof hideForm; SIDEBAR_PING: typeof noop; + SIDEBAR_RELOAD: typeof location.reload; SIDEBAR_SHOW_TEMPORARY_PANEL: typeof showTemporaryPanel; SIDEBAR_UPDATE_TEMPORARY_PANEL: typeof updateTemporaryPanel; SIDEBAR_HIDE_TEMPORARY_PANEL: typeof hideTemporaryPanel; @@ -59,6 +60,7 @@ export default function registerMessenger(): void { SIDEBAR_SHOW_FORM: showForm, SIDEBAR_HIDE_FORM: hideForm, SIDEBAR_PING: noop, + SIDEBAR_RELOAD: location.reload.bind(location), SIDEBAR_SHOW_TEMPORARY_PANEL: showTemporaryPanel, SIDEBAR_UPDATE_TEMPORARY_PANEL: updateTemporaryPanel, SIDEBAR_HIDE_TEMPORARY_PANEL: hideTemporaryPanel, diff --git a/src/sidebar/sidePanel.tsx b/src/sidebar/sidePanel.tsx index 5bdb3bc1e1..1cea13255e 100644 --- a/src/sidebar/sidePanel.tsx +++ b/src/sidebar/sidePanel.tsx @@ -15,17 +15,98 @@ * along with this program. If not, see . */ -import { isSidePanel } from "webext-detect-page"; -import { getTopLevelFrame as getTopLevelFrameViaMessenger } from "webext-messenger"; - -export async function getTopLevelFrame() { - if (isSidePanel()) { - const [tab] = await chrome.tabs.query({ - active: true, - currentWindow: true, - }); - return { tabId: tab.id, frameId: 0 }; +import { getCurrentURL } from "@/pageEditor/utils"; +import { getExtensionConsoleUrl } from "@/utils/extensionUtils"; +import { + DISPLAY_REASON_EXTENSION_CONSOLE, + DISPLAY_REASON_RESTRICTED_URL, +} from "@/tinyPages/restrictedUrlPopupConstants"; +import { type ActivatePanelOptions } from "@/types/sidebarTypes"; +import { isScriptableUrl } from "webext-content-scripts"; + +import { assertNotNullish } from "@/utils/nullishUtils"; +import { isObject } from "@/utils/objectUtils"; +import { type Target } from "webext-messenger"; + +export function getAssociatedTabId(): number { + const tabId = new URLSearchParams(window.location.search).get("tabId"); + assertNotNullish( + tabId, + "getAssociatedTabId is only available in a sidepanel page", + ); + return Number(tabId); +} + +export function getTopLevelFrame(): Target { + return { + tabId: getAssociatedTabId(), + frameId: 0, + }; +} + +const PING_MESSAGE = "PING_SIDE_PANEL"; +// Do not use the messenger because it doesn't support retry-less messaging +export function initSidePanelPingResponder() { + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if ( + isObject(message) && + message.type === PING_MESSAGE && + sender.tab?.id === getAssociatedTabId() + ) { + sendResponse(true); + } + }); +} + +export async function isSidePanelOpen(): Promise { + const response = await chrome.runtime.sendMessage< + { type: string }, + boolean | undefined + >({ type: PING_MESSAGE }); + return Boolean(response); // TODO: Drop Boolean() after strictNullChecks migration +} + +/** + * Show a popover on restricted URLs because we're unable to inject content into the page. Previously we'd open + * the Extension Console, but that was confusing because the action was inconsistent with how the button behaves + * other pages. + * @param tabUrl the url of the tab, or null if not accessible + */ +export function getPopoverUrl(tabUrl: string | null): string | null { + const popoverUrl = browser.runtime.getURL("restrictedUrlPopup.html"); + + if (tabUrl?.startsWith(getExtensionConsoleUrl())) { + return `${popoverUrl}?reason=${DISPLAY_REASON_EXTENSION_CONSOLE}`; } - return getTopLevelFrameViaMessenger(); + if (!isScriptableUrl(tabUrl)) { + return `${popoverUrl}?reason=${DISPLAY_REASON_RESTRICTED_URL}`; + } + + // The popup is disabled, and the extension will receive browserAction.onClicked events. + // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/setPopup#popup + return null; +} + +export function getSidebarPath(tabId: number, url: string): string { + return getPopoverUrl(url) ?? "sidebar.html?tabId=" + tabId; +} + +export async function showSidebarFromPageEditor( + activateOptions?: ActivatePanelOptions, +) { + await openSidePanel( + chrome.devtools.inspectedWindow.tabId, + await getCurrentURL(), + ); +} + +export async function openSidePanel(tabId: number, url: string) { + // Simultaneously define, enable, and open the side panel + void chrome.sidePanel.setOptions({ + tabId, + path: getSidebarPath(tabId, url), + enabled: true, + }); + await chrome.sidePanel.open({ tabId }); } diff --git a/src/sidebar/sidebar.tsx b/src/sidebar/sidebar.tsx index 337d10d3c6..b2528c47d4 100644 --- a/src/sidebar/sidebar.tsx +++ b/src/sidebar/sidebar.tsx @@ -34,6 +34,7 @@ import { initToaster } from "@/utils/notify"; import { initRuntimeLogging } from "@/development/runtimeLogging"; import { initCopilotMessenger } from "@/contrib/automationanywhere/aaFrameProtocol"; import { initPerformanceMonitoring } from "@/telemetry/performance"; +import { initSidePanelPingResponder } from "./sidePanel"; function init(): void { ReactDOM.render(, document.querySelector("#container")); @@ -47,6 +48,7 @@ registerContribBlocks(); registerBuiltinBricks(); initToaster(); init(); +initSidePanelPingResponder(); // Handle an embedded AA business copilot frame void initCopilotMessenger(); diff --git a/src/sidebar/useHideEmptySidebar.ts b/src/sidebar/useHideEmptySidebar.ts index 1372b6ee38..94f2a50ee3 100644 --- a/src/sidebar/useHideEmptySidebar.ts +++ b/src/sidebar/useHideEmptySidebar.ts @@ -17,10 +17,7 @@ import useAsyncEffect from "use-async-effect"; import { getTopLevelFrame } from "./sidePanel"; -import { - getReservedSidebarEntries, - hideSidebar, -} from "@/contentScript/messenger/api"; +import { getReservedSidebarEntries } from "@/contentScript/messenger/api"; import { useSelector } from "react-redux"; import { selectClosedTabs, @@ -54,8 +51,13 @@ export const useHideEmptySidebar = () => { visiblePanelCount === 0 && openReservedPanels.length === 0 ) { - const topLevelFrame = await getTopLevelFrame(); - void hideSidebar(topLevelFrame); + // TODO: Move to own function + await chrome.sidePanel.setOptions({ + tabId: Number( + new URLSearchParams(window.location.search).get("tabId"), + ), + enabled: false, + }); } }, [visiblePanelCount], diff --git a/src/starterBricks/sidebarExtension.ts b/src/starterBricks/sidebarExtension.ts index 3078774873..1bdd7063e4 100644 --- a/src/starterBricks/sidebarExtension.ts +++ b/src/starterBricks/sidebarExtension.ts @@ -52,7 +52,6 @@ import { mergeReaders } from "@/bricks/readers/readerUtils"; import BackgroundLogger from "@/telemetry/BackgroundLogger"; import { NoRendererError } from "@/errors/businessErrors"; import { serializeError } from "serialize-error"; -import { isSidebarFrameVisible } from "@/contentScript/sidebarDomControllerLite"; import { type Schema } from "@/types/schemaTypes"; import { type ResolvedModComponent } from "@/types/modComponentTypes"; import { type Brick } from "@/types/brickTypes"; @@ -63,6 +62,7 @@ import { type Reader } from "@/types/bricks/readerTypes"; import { type StarterBrick } from "@/types/starterBrickTypes"; import { isLoadedInIframe } from "@/utils/iframeUtils"; import makeServiceContextFromDependencies from "@/integrations/util/makeServiceContextFromDependencies"; +import { isSidePanelOpen } from "@/sidebar/sidePanel"; export type SidebarConfig = { heading: string; @@ -416,7 +416,7 @@ export abstract class SidebarStarterBrickABC extends StarterBrickABC; diff --git a/webpack.config.mjs b/webpack.config.mjs index 32dbb06994..ded3700753 100644 --- a/webpack.config.mjs +++ b/webpack.config.mjs @@ -109,7 +109,6 @@ const createConfig = (env, options) => "background/background", "contentScript/contentScript", "contentScript/loadActivationEnhancements", - "contentScript/browserActionInstantHandler", "pageEditor/pageEditor", "extensionConsole/options", "sidebar/sidebar", From dc55a3f2bea9ef0a3bda67c6ca75495c3ec9bd57 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Thu, 4 Jan 2024 23:33:00 +0800 Subject: [PATCH 09/29] Work so far; untested --- .eslintrc.js | 1 + src/background/browserAction.ts | 7 +- src/background/messenger/api.ts | 4 +- src/background/messenger/registration.ts | 10 +- src/background/sidePanel.ts | 50 ++------- src/bricks/effects/sidebar.ts | 11 +- src/contentScript/messenger/api.ts | 1 - src/contentScript/messenger/registration.ts | 6 +- src/contentScript/sidebarController.tsx | 88 +++++---------- .../automationanywhere/aaFrameProtocol.ts | 5 +- src/pageEditor/panes/insert/useAutoInsert.ts | 2 +- src/pageEditor/sidePanel.ts | 29 +++++ .../sidebar/ActivatedModComponentListItem.tsx | 2 +- .../sidebar/DynamicModComponentListItem.tsx | 2 +- src/sidebar/ConnectedSidebar.tsx | 4 +- src/sidebar/Tabs.test.tsx | 2 +- .../activateMod/ActivateModPanel.test.tsx | 2 +- src/sidebar/messenger/api.ts | 15 ++- src/sidebar/sidePanel.tsx | 96 +--------------- src/sidebar/sidePanel/messenger/api.ts | 103 ++++++++++++++++++ src/sidebar/sidebar.tsx | 7 +- src/sidebar/useHideEmptySidebar.ts | 7 +- src/starterBricks/sidebarExtension.ts | 2 +- 23 files changed, 222 insertions(+), 234 deletions(-) create mode 100644 src/pageEditor/sidePanel.ts create mode 100644 src/sidebar/sidePanel/messenger/api.ts diff --git a/.eslintrc.js b/.eslintrc.js index 83f2aab833..34a02dc97f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -21,6 +21,7 @@ const restrictedZones = [ // These can be imported from anywhere except: [ `../${exporter}/messenger`, + `../${exporter}/sidePanel/messenger`, `../${exporter}/types.ts`, // `../${exporter}/**/*Types.ts`, // TODO: Globs don't seem to work `../${exporter}/pageEditor/types.ts`, diff --git a/src/background/browserAction.ts b/src/background/browserAction.ts index a17d755ac2..bed045fe81 100644 --- a/src/background/browserAction.ts +++ b/src/background/browserAction.ts @@ -15,8 +15,11 @@ * along with this program. If not, see . */ -import { browserAction, type Tab } from "@/mv3/api"; -import { getSidebarPath, openSidePanel } from "@/sidebar/sidePanel"; +import { browserAction } from "@/mv3/api"; +import { + getSidebarPath, + openSidePanel, +} from "@/sidebar/sidePanel/messenger/api"; export default async function initBrowserAction(): Promise { void chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }); diff --git a/src/background/messenger/api.ts b/src/background/messenger/api.ts index 8a8c0b0b57..b19bf9ff44 100644 --- a/src/background/messenger/api.ts +++ b/src/background/messenger/api.ts @@ -45,8 +45,8 @@ export const removeExtensionForEveryTab = getNotifier( bg, ); -export const showSidebarPanel = getMethod("SHOW_SIDEBAR_PANEL", bg); -export const hideSidebarPanel = getMethod("HIDE_SIDEBAR_PANEL", bg); +export const showMySidePanel = getMethod("SHOW_MY_SIDE_PANEL", bg); +export const hideMySidePanel = getMethod("HIDE_MY_SIDE_PANEL", bg); export const closeTab = getMethod("CLOSE_TAB", bg); export const deleteCachedAuthData = getMethod("DELETE_CACHED_AUTH", bg); diff --git a/src/background/messenger/registration.ts b/src/background/messenger/registration.ts index fea3b82681..b3937b5f01 100644 --- a/src/background/messenger/registration.ts +++ b/src/background/messenger/registration.ts @@ -80,7 +80,7 @@ import { getCachedAuthData, } from "@/background/auth/authStorage"; import { setCopilotProcessData } from "@/background/partnerHandlers"; -import { hideSidebarPanel, showSidebarPanel } from "@/background/sidePanel"; +import { hideMySidePanel, showMySidePanel } from "@/background/sidePanel"; expectContext("background"); @@ -115,8 +115,8 @@ declare global { PING: typeof pong; COLLECT_PERFORMANCE_DIAGNOSTICS: typeof collectPerformanceDiagnostics; - SHOW_SIDEBAR_PANEL: typeof showSidebarPanel; - HIDE_SIDEBAR_PANEL: typeof hideSidebarPanel; + SHOW_MY_SIDE_PANEL: typeof showMySidePanel; + HIDE_MY_SIDE_PANEL: typeof hideMySidePanel; ACTIVATE_TAB: typeof activateTab; REACTIVATE_EVERY_TAB: typeof reactivateEveryTab; @@ -199,8 +199,8 @@ export default function registerMessenger(): void { PING: pong, COLLECT_PERFORMANCE_DIAGNOSTICS: collectPerformanceDiagnostics, - SHOW_SIDEBAR_PANEL: showSidebarPanel, - HIDE_SIDEBAR_PANEL: hideSidebarPanel, + SHOW_MY_SIDE_PANEL: showMySidePanel, + HIDE_MY_SIDE_PANEL: hideMySidePanel, ACTIVATE_TAB: activateTab, REACTIVATE_EVERY_TAB: reactivateEveryTab, diff --git a/src/background/sidePanel.ts b/src/background/sidePanel.ts index 30b8c21e96..90b8834cb0 100644 --- a/src/background/sidePanel.ts +++ b/src/background/sidePanel.ts @@ -15,50 +15,16 @@ * along with this program. If not, see . */ +import { + openSidePanel, + hideSidePanel, +} from "@/sidebar/sidePanel/messenger/api"; import type { MessengerMeta } from "webext-messenger"; -export async function showSidebarPanel(this: MessengerMeta): Promise { - const tabId = this.trace[0].tab.id; - - // TODO: Use the native promises if possible - return new Promise((resolve, reject) => { - // Unlike the Chrome example, call setOptions first to handle the case where the sidebar was closed on the tab - // https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/functional-samples/cookbook.sidepanel-open/script.js#L9 - - // Use callback form to help prevent the user gesture from getting lost - chrome.sidePanel.setOptions( - { - tabId, - path: `sidebar.html?tabId=${tabId}`, - enabled: true, - }, - () => { - chrome.sidePanel.open( - { - tabId, - }, - () => { - resolve(); - }, - ); - - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError); - } - }, - ); - - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError); - } - }); +export async function showMySidePanel(this: MessengerMeta): Promise { + await openSidePanel(this.trace[0].tab.id, this.trace[0].url); } -export async function hideSidebarPanel(this: MessengerMeta): Promise { - const tabId = this.trace[0].tab.id; - - await chrome.sidePanel.setOptions({ - tabId, - enabled: false, - }); +export async function hideMySidePanel(this: MessengerMeta): Promise { + await hideSidePanel(this.trace[0].tab.id); } diff --git a/src/bricks/effects/sidebar.ts b/src/bricks/effects/sidebar.ts index d709b616a9..fa57ff9bff 100644 --- a/src/bricks/effects/sidebar.ts +++ b/src/bricks/effects/sidebar.ts @@ -18,9 +18,9 @@ import { EffectABC } from "@/types/bricks/effectTypes"; import { type BrickArgs, type BrickOptions } from "@/types/runtimeTypes"; import { type Schema, SCHEMA_EMPTY_OBJECT } from "@/types/schemaTypes"; -import { showSidebar } from "@/contentScript/sidebarController"; +import { rehydrateSidebar } from "@/contentScript/sidebarController"; +import { hideMySidePanel, showMySidePanel } from "@/background/messenger/api"; import { propertiesToSchema } from "@/validators/generic"; - import { logPromiseDuration } from "@/utils/promiseUtils"; export class ShowSidebar extends EffectABC { @@ -64,9 +64,10 @@ export class ShowSidebar extends EffectABC { ): Promise { // Don't pass extensionId here because the extensionId in showOptions refers to the extensionId of the panel, // not the extensionId of the extension toggling the sidebar + await showMySidePanel(); void logPromiseDuration( "ShowSidebar:showSidebar", - showSidebar({ + rehydrateSidebar({ force: forcePanel, panelHeading, blueprintId: logger.context.blueprintId, @@ -87,8 +88,6 @@ export class HideSidebar extends EffectABC { inputSchema: Schema = SCHEMA_EMPTY_OBJECT; async effect(): Promise { - // XXX: for MV3, do we need to catch a potential user gesture error and rethrow as business error? Would required - // making hideSidebar async and the hide a method instead of notifier - hideSidebar(); + await hideMySidePanel(); } } diff --git a/src/contentScript/messenger/api.ts b/src/contentScript/messenger/api.ts index 9b24bd8768..978192a52b 100644 --- a/src/contentScript/messenger/api.ts +++ b/src/contentScript/messenger/api.ts @@ -46,7 +46,6 @@ export const rehydrateSidebar = getMethod("REHYDRATE_SIDEBAR"); export const getReservedSidebarEntries = getMethod( "GET_RESERVED_SIDEBAR_ENTRIES", ); -export const reloadSidebar = getMethod("RELOAD_SIDEBAR"); export const removeSidebars = getMethod("REMOVE_SIDEBARS"); export const insertPanel = getMethod("INSERT_PANEL"); export const insertButton = getMethod("INSERT_BUTTON"); diff --git a/src/contentScript/messenger/registration.ts b/src/contentScript/messenger/registration.ts index 2d8a924cab..f90aa218e8 100644 --- a/src/contentScript/messenger/registration.ts +++ b/src/contentScript/messenger/registration.ts @@ -32,8 +32,8 @@ import { cancelForm, } from "@/contentScript/ephemeralFormProtocol"; import { + rehydrateSidebar, removeExtensions as removeSidebars, - reloadSidebar, getReservedPanelEntries, } from "@/contentScript/sidebarController"; import { insertPanel } from "@/contentScript/pageEditor/insertPanel"; @@ -100,8 +100,8 @@ declare global { TOGGLE_QUICK_BAR: typeof toggleQuickBar; HANDLE_MENU_ACTION: typeof handleMenuAction; + REHYDRATE_SIDEBAR: typeof rehydrateSidebar; GET_RESERVED_SIDEBAR_ENTRIES: typeof getReservedPanelEntries; - RELOAD_SIDEBAR: typeof reloadSidebar; REMOVE_SIDEBARS: typeof removeSidebars; INSERT_PANEL: typeof insertPanel; @@ -166,7 +166,7 @@ export default function registerMessenger(): void { TOGGLE_QUICK_BAR: toggleQuickBar, HANDLE_MENU_ACTION: handleMenuAction, - RELOAD_SIDEBAR: reloadSidebar, + REHYDRATE_SIDEBAR: rehydrateSidebar, REMOVE_SIDEBARS: removeSidebars, INSERT_PANEL: insertPanel, diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index 882756a7de..930084f749 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -15,7 +15,6 @@ * along with this program. If not, see . */ -import reportError from "@/telemetry/reportError"; import reportEvent from "@/telemetry/reportEvent"; import { Events } from "@/telemetry/events"; import { expectContext } from "@/utils/expectContext"; @@ -23,7 +22,7 @@ import sidebarInThisTab from "@/sidebar/messenger/api"; import { isEmpty } from "lodash"; import { SimpleEventTarget } from "@/utils/SimpleEventTarget"; import { type Except } from "type-fest"; -import { type RunArgs, RunReason } from "@/types/runtimeTypes"; +import { type RunArgs } from "@/types/runtimeTypes"; import { type UUID } from "@/types/stringTypes"; import { type RegistryId } from "@/types/registryTypes"; import { type ModComponentRef } from "@/types/modComponentTypes"; @@ -37,9 +36,8 @@ import type { } from "@/types/sidebarTypes"; import { getTemporaryPanelSidebarEntries } from "@/bricks/transformers/temporaryInfo/temporaryPanelProtocol"; import { getFormPanelSidebarEntries } from "@/contentScript/ephemeralFormProtocol"; -import { logPromiseDuration } from "@/utils/promiseUtils"; -import { waitAnimationFrame } from "@/utils/domUtils"; -import { showSidebarPanel } from "@/background/messenger/api"; +import { showMySidePanel } from "@/background/messenger/api"; +import { isSidePanelOpen } from "@/sidebar/sidePanel/messenger/api"; /** * Sequence number for ensuring render requests are handled in order @@ -60,49 +58,10 @@ let modActivationPanelEntry: ModActivationPanelEntry | null = null; * Attach the sidebar to the page if it's not already attached. Then re-renders all panels. * @param activateOptions options controlling the visible panel in the sidebar */ -export async function showSidebar( - activateOptions: ActivatePanelOptions = {}, -): Promise { +export async function showSidebar(): Promise { console.debug("sidebarController:showSidebar"); reportEvent(Events.SIDEBAR_SHOW); - await showSidebarPanel(); - - try { - await sidebarInThisTab.pingSidebar(); - } catch (error) { - throw new Error("The sidebar did not respond in time", { cause: error }); - } - - if (activateOptions.refresh ?? true) { - // Run the sidebar extension points available on the page. If the sidebar is already in the page, running - // all the callbacks ensures the content is up-to-date - - // Currently, this runs the listening SidebarExtensionPoint.run callbacks in not particular order. Also note that - // we're not awaiting their resolution (because they may contain long-running bricks). - if (!isSidebarFrameVisible()) { - console.error( - "Pre-condition failed: sidebar is not attached in the page for call to sidebarShowEvents.emit", - ); - } - - console.debug("sidebarController:showSidebar emitting sidebarShowEvents", { - isSidebarFrameVisible: isSidebarFrameVisible(), - }); - - sidebarShowEvents.emit({ reason: RunReason.MANUAL }); - } - - if (!isEmpty(activateOptions)) { - const seqNum = renderSequenceNumber; - renderSequenceNumber++; - - // The sidebarSlice handles the race condition with the panels loading by keeping track of the latest pending - // activatePanel request. - await sidebarInThisTab.activatePanel(seqNum, { - ...activateOptions, - force: activateOptions.force, - }); - } + await showMySidePanel(); } /** @@ -112,7 +71,7 @@ export async function showSidebar( export async function activateExtensionPanel(extensionId: UUID): Promise { expectContext("contentScript"); - if (!isSidebarFrameVisible()) { + if (!(await isSidePanelOpen())) { console.warn("sidebar is not attached to the page"); } @@ -129,24 +88,29 @@ export async function activateExtensionPanel(extensionId: UUID): Promise { * Awaitable version of showSidebar which does not reload existing panels if the sidebar is already visible * @see showSidebar */ -export async function ensureSidebar(): Promise { - console.debug("sidebarController:ensureSidebar", { - isSidebarFrameVisible: isSidebarFrameVisible(), - }); +export const ensureSidebar = showSidebar; // TODO: Verify behavior match - if (!isSidebarFrameVisible()) { - expectContext("contentScript"); - await logPromiseDuration("ensureSidebar", showSidebar()); +export async function rehydrateSidebar( + activateOptions: ActivatePanelOptions = {}, +): Promise { + try { + await sidebarInThisTab.pingSidebar(); + } catch (error) { + throw new Error("The sidebar did not respond in time", { cause: error }); } -} -/** - * Reload the sidebar and its content. - * - * Known limitations: - * - Does not reload ephemeral forms - */ -export async function reloadSidebar(): Promise {} + if (!isEmpty(activateOptions)) { + const seqNum = renderSequenceNumber; + renderSequenceNumber++; + + // The sidebarSlice handles the race condition with the panels loading by keeping track of the latest pending + // activatePanel request. + await sidebarInThisTab.activatePanel(seqNum, { + ...activateOptions, + force: activateOptions.force, + }); + } +} function renderPanelsIfVisible(): void { expectContext("contentScript"); diff --git a/src/contrib/automationanywhere/aaFrameProtocol.ts b/src/contrib/automationanywhere/aaFrameProtocol.ts index 09f07994e9..2ce7530e94 100644 --- a/src/contrib/automationanywhere/aaFrameProtocol.ts +++ b/src/contrib/automationanywhere/aaFrameProtocol.ts @@ -17,8 +17,8 @@ import { type UnknownObject } from "@/types/objectTypes"; import { expectContext } from "@/utils/expectContext"; -import { getTopLevelFrame } from "@/sidebar/sidePanel"; import { getCopilotHostData } from "@/contentScript/messenger/api"; +import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; /** * Runtime event type for setting Co-Pilot data @@ -126,8 +126,7 @@ export async function initCopilotMessenger(): Promise { }); // Fetch the current data from the content script when the frame loads - const frame = await getTopLevelFrame(); - const data = await getCopilotHostData(frame); + const data = await getCopilotHostData(getAssociatedTarget()); console.debug("Setting initial Co-Pilot data", { location: window.location.href, data, diff --git a/src/pageEditor/panes/insert/useAutoInsert.ts b/src/pageEditor/panes/insert/useAutoInsert.ts index f09cf75f3c..b98fdb7a62 100644 --- a/src/pageEditor/panes/insert/useAutoInsert.ts +++ b/src/pageEditor/panes/insert/useAutoInsert.ts @@ -6,7 +6,7 @@ import { type ModComponentFormState } from "@/pageEditor/starterBricks/formState import { getExampleBrickPipeline } from "@/pageEditor/exampleStarterBrickConfigs"; import { actions } from "@/pageEditor/slices/editorSlice"; import { updateDynamicElement } from "@/contentScript/messenger/api"; -import { showSidebarFromPageEditor } from "@/sidebar/sidePanel"; +import { showSidebarFromPageEditor } from "@/pageEditor/sidePanel"; import reportEvent from "@/telemetry/reportEvent"; import { Events } from "@/telemetry/events"; import { type StarterBrickType } from "@/types/starterBrickTypes"; diff --git a/src/pageEditor/sidePanel.ts b/src/pageEditor/sidePanel.ts new file mode 100644 index 0000000000..4273f6a40a --- /dev/null +++ b/src/pageEditor/sidePanel.ts @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 PixieBrix, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { type ActivatePanelOptions } from "@/types/sidebarTypes"; +import { getCurrentURL } from "@/pageEditor/utils"; +import { openSidePanel } from "@/sidebar/sidePanel/messenger/api"; + +export async function showSidebarFromPageEditor( + activateOptions?: ActivatePanelOptions, +) { + await openSidePanel( + chrome.devtools.inspectedWindow.tabId, + await getCurrentURL(), + ); +} diff --git a/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx b/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx index 8014d197d3..ec11fe1192 100644 --- a/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx +++ b/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx @@ -31,7 +31,7 @@ import { NotAvailableIcon, } from "@/pageEditor/sidebar/ExtensionIcons"; import { disableOverlay, enableOverlay } from "@/contentScript/messenger/api"; -import { showSidebarFromPageEditor } from "@/sidebar/sidePanel"; +import { showSidebarFromPageEditor } from "@/pageEditor/sidePanel"; import { thisTab } from "@/pageEditor/utils"; import cx from "classnames"; import { selectSessionId } from "@/pageEditor/slices/sessionSelectors"; diff --git a/src/pageEditor/sidebar/DynamicModComponentListItem.tsx b/src/pageEditor/sidebar/DynamicModComponentListItem.tsx index c7c133f49f..d0793ed499 100644 --- a/src/pageEditor/sidebar/DynamicModComponentListItem.tsx +++ b/src/pageEditor/sidebar/DynamicModComponentListItem.tsx @@ -28,7 +28,7 @@ import { } from "@/pageEditor/sidebar/ExtensionIcons"; import { type UUID } from "@/types/stringTypes"; import { disableOverlay, enableOverlay } from "@/contentScript/messenger/api"; -import { showSidebarFromPageEditor } from "@/sidebar/sidePanel"; +import { showSidebarFromPageEditor } from "@/pageEditor/sidePanel"; import { thisTab } from "@/pageEditor/utils"; import cx from "classnames"; import reportEvent from "@/telemetry/reportEvent"; diff --git a/src/sidebar/ConnectedSidebar.tsx b/src/sidebar/ConnectedSidebar.tsx index 5e337cd97e..ea67e142ea 100644 --- a/src/sidebar/ConnectedSidebar.tsx +++ b/src/sidebar/ConnectedSidebar.tsx @@ -42,7 +42,7 @@ import { ensureExtensionPointsInstalled, getReservedSidebarEntries, } from "@/contentScript/messenger/api"; -import { getTopLevelFrame } from "./sidePanel"; +import { getAssociatedTarget } from "./sidePanel/messenger/api"; import useAsyncEffect from "use-async-effect"; import activateLinkClickHandler from "@/activation/activateLinkClickHandler"; @@ -98,7 +98,7 @@ const ConnectedSidebar: React.VFC = () => { // We could instead consider moving the initial panel logic to SidebarApp.tsx and pass the entries as the // initial state to the sidebarSlice reducer. useAsyncEffect(async () => { - const topFrame = await getTopLevelFrame(); + const topFrame = getAssociatedTarget(); // Ensure persistent sidebar extension points have been installed to have reserve their panels for the sidebar await ensureExtensionPointsInstalled(topFrame); diff --git a/src/sidebar/Tabs.test.tsx b/src/sidebar/Tabs.test.tsx index 07cbba938d..baea0ffad8 100644 --- a/src/sidebar/Tabs.test.tsx +++ b/src/sidebar/Tabs.test.tsx @@ -31,7 +31,7 @@ import { mockAllApiEndpoints } from "@/testUtils/appApiMock"; mockAllApiEndpoints(); const cancelFormSpy = jest.spyOn(messengerApi, "cancelForm"); -const hideSidebarSpy = jest.spyOn(messengerApi, "reloadSidebar"); // TODO: Temporary mock rename just to silence errors +const hideSidebarSpy = jest.spyOn(messengerApi, "hideSidebar"); async function setupPanelsAndRender(options: { sidebarEntries?: Partial; diff --git a/src/sidebar/activateMod/ActivateModPanel.test.tsx b/src/sidebar/activateMod/ActivateModPanel.test.tsx index 6232062422..7194223e1a 100644 --- a/src/sidebar/activateMod/ActivateModPanel.test.tsx +++ b/src/sidebar/activateMod/ActivateModPanel.test.tsx @@ -67,7 +67,7 @@ const useRequiredModDefinitionsMock = jest.mocked(useRequiredModDefinitions); const checkModDefinitionPermissionsMock = jest.mocked( checkModDefinitionPermissions, ); -const hideSidebarSpy = jest.spyOn(messengerApi, "reloadSidebar"); // TODO: Temporary mock rename just to silence errors +const hideSidebarSpy = jest.spyOn(messengerApi, "hideSidebar"); jest.mock("@/starterBricks/starterBrickModUtils", () => { const actualUtils = jest.requireActual( diff --git a/src/sidebar/messenger/api.ts b/src/sidebar/messenger/api.ts index 552e0fe12a..cf8e6390e0 100644 --- a/src/sidebar/messenger/api.ts +++ b/src/sidebar/messenger/api.ts @@ -16,10 +16,19 @@ */ /* Do not use `registerMethod` in this file */ -import { getMethod, getNotifier } from "webext-messenger"; -import { getAssociatedTabId } from "@/sidebar/sidePanel"; +import { expectContext } from "@/utils/expectContext"; +import { getMethod, getNotifier, getThisFrame } from "webext-messenger"; -const target = { page: "/sidebar.html?tabId=" + getAssociatedTabId() } as const; +expectContext("contentScript"); + +const target = { page: "/sidebar.html" }; + +// Unavoidable race condition: we can't message the sidebar until we know the tabId. +// If this causes issues (unlikely), we can make `getMethod` accept an async function +// that generates the target, like `getMethod('FOO', getThisFramesSideBarUrl())`. +void getThisFrame().then((frame) => { + target.page += "?tabId=" + frame.tabId; +}); const sidebarInThisTab = { renderPanels: getMethod("SIDEBAR_RENDER_PANELS", target), diff --git a/src/sidebar/sidePanel.tsx b/src/sidebar/sidePanel.tsx index 1cea13255e..6d0853c42b 100644 --- a/src/sidebar/sidePanel.tsx +++ b/src/sidebar/sidePanel.tsx @@ -15,98 +15,8 @@ * along with this program. If not, see . */ -import { getCurrentURL } from "@/pageEditor/utils"; -import { getExtensionConsoleUrl } from "@/utils/extensionUtils"; -import { - DISPLAY_REASON_EXTENSION_CONSOLE, - DISPLAY_REASON_RESTRICTED_URL, -} from "@/tinyPages/restrictedUrlPopupConstants"; -import { type ActivatePanelOptions } from "@/types/sidebarTypes"; -import { isScriptableUrl } from "webext-content-scripts"; +/** @file This file defines the internal API for the sidePanel, only meant to be run in the sidePanel itself */ -import { assertNotNullish } from "@/utils/nullishUtils"; -import { isObject } from "@/utils/objectUtils"; -import { type Target } from "webext-messenger"; +import { expectContext } from "@/utils/expectContext"; -export function getAssociatedTabId(): number { - const tabId = new URLSearchParams(window.location.search).get("tabId"); - assertNotNullish( - tabId, - "getAssociatedTabId is only available in a sidepanel page", - ); - return Number(tabId); -} - -export function getTopLevelFrame(): Target { - return { - tabId: getAssociatedTabId(), - frameId: 0, - }; -} - -const PING_MESSAGE = "PING_SIDE_PANEL"; -// Do not use the messenger because it doesn't support retry-less messaging -export function initSidePanelPingResponder() { - chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - if ( - isObject(message) && - message.type === PING_MESSAGE && - sender.tab?.id === getAssociatedTabId() - ) { - sendResponse(true); - } - }); -} - -export async function isSidePanelOpen(): Promise { - const response = await chrome.runtime.sendMessage< - { type: string }, - boolean | undefined - >({ type: PING_MESSAGE }); - return Boolean(response); // TODO: Drop Boolean() after strictNullChecks migration -} - -/** - * Show a popover on restricted URLs because we're unable to inject content into the page. Previously we'd open - * the Extension Console, but that was confusing because the action was inconsistent with how the button behaves - * other pages. - * @param tabUrl the url of the tab, or null if not accessible - */ -export function getPopoverUrl(tabUrl: string | null): string | null { - const popoverUrl = browser.runtime.getURL("restrictedUrlPopup.html"); - - if (tabUrl?.startsWith(getExtensionConsoleUrl())) { - return `${popoverUrl}?reason=${DISPLAY_REASON_EXTENSION_CONSOLE}`; - } - - if (!isScriptableUrl(tabUrl)) { - return `${popoverUrl}?reason=${DISPLAY_REASON_RESTRICTED_URL}`; - } - - // The popup is disabled, and the extension will receive browserAction.onClicked events. - // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/setPopup#popup - return null; -} - -export function getSidebarPath(tabId: number, url: string): string { - return getPopoverUrl(url) ?? "sidebar.html?tabId=" + tabId; -} - -export async function showSidebarFromPageEditor( - activateOptions?: ActivatePanelOptions, -) { - await openSidePanel( - chrome.devtools.inspectedWindow.tabId, - await getCurrentURL(), - ); -} - -export async function openSidePanel(tabId: number, url: string) { - // Simultaneously define, enable, and open the side panel - void chrome.sidePanel.setOptions({ - tabId, - path: getSidebarPath(tabId, url), - enabled: true, - }); - await chrome.sidePanel.open({ tabId }); -} +expectContext("sidePanel"); diff --git a/src/sidebar/sidePanel/messenger/api.ts b/src/sidebar/sidePanel/messenger/api.ts new file mode 100644 index 0000000000..313c5292a2 --- /dev/null +++ b/src/sidebar/sidePanel/messenger/api.ts @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2023 PixieBrix, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file This file defines the public API for the sidePanel, with some + * exceptions that use `expectContext`. It uses the `messenger/api.ts` name + * to match that expectation and avoid lint issues. + */ + +import { getExtensionConsoleUrl } from "@/utils/extensionUtils"; +import { + DISPLAY_REASON_EXTENSION_CONSOLE, + DISPLAY_REASON_RESTRICTED_URL, +} from "@/tinyPages/restrictedUrlPopupConstants"; +import { isScriptableUrl } from "webext-content-scripts"; + +import { isObject } from "@/utils/objectUtils"; +import { expectContext } from "@/utils/expectContext"; +import { type Target } from "webext-messenger"; + +export function getAssociatedTabId(): number { + expectContext("sidePanel"); + const tabId = new URLSearchParams(window.location.search).get("tabId"); + return Number(tabId); +} + +export function getAssociatedTarget(): Target { + return { tabId: getAssociatedTabId(), frameId: 0 }; +} + +const PING_MESSAGE = "PING_SIDE_PANEL"; +// Do not use the messenger because it doesn't support retry-less messaging +export function initSidePanelPingResponder() { + expectContext("sidePanel"); + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if ( + isObject(message) && + message.type === PING_MESSAGE && + sender.tab?.id === getAssociatedTabId() + ) { + sendResponse(true); + } + }); +} + +export async function isSidePanelOpen(): Promise { + const response = await chrome.runtime.sendMessage< + { type: string }, + boolean | undefined + >({ type: PING_MESSAGE }); + return Boolean(response); // TODO: Drop Boolean() after strictNullChecks migration +} + +export function getPopoverUrl(tabUrl: string | undefined): string | null { + const popoverUrl = browser.runtime.getURL("restrictedUrlPopup.html"); + + if (tabUrl?.startsWith(getExtensionConsoleUrl())) { + return `${popoverUrl}?reason=${DISPLAY_REASON_EXTENSION_CONSOLE}`; + } + + if (!isScriptableUrl(tabUrl)) { + return `${popoverUrl}?reason=${DISPLAY_REASON_RESTRICTED_URL}`; + } + + // The popup is disabled, and the extension will receive browserAction.onClicked events. + // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/setPopup#popup + return null; +} + +export function getSidebarPath(tabId: number, url: string): string { + return getPopoverUrl(url) ?? "sidebar.html?tabId=" + tabId; +} + +export async function openSidePanel(tabId: number, url: string) { + // Simultaneously define, enable, and open the side panel + void chrome.sidePanel.setOptions({ + tabId, + path: getSidebarPath(tabId, url), + enabled: true, + }); + await chrome.sidePanel.open({ tabId }); +} + +export async function hideSidePanel(tabId: number) { + void chrome.sidePanel.setOptions({ + tabId, + enabled: false, + }); +} diff --git a/src/sidebar/sidebar.tsx b/src/sidebar/sidebar.tsx index b2528c47d4..4bfea25d1a 100644 --- a/src/sidebar/sidebar.tsx +++ b/src/sidebar/sidebar.tsx @@ -34,7 +34,8 @@ import { initToaster } from "@/utils/notify"; import { initRuntimeLogging } from "@/development/runtimeLogging"; import { initCopilotMessenger } from "@/contrib/automationanywhere/aaFrameProtocol"; import { initPerformanceMonitoring } from "@/telemetry/performance"; -import { initSidePanelPingResponder } from "./sidePanel"; +import { initSidePanelPingResponder } from "./sidePanel/messenger/api"; +import { rehydrateSidebar } from "@/contentScript/sidebarController"; function init(): void { ReactDOM.render(, document.querySelector("#container")); @@ -50,5 +51,9 @@ initToaster(); init(); initSidePanelPingResponder(); +// The sidePanel is that one that requests data from the content script +// FIXME: This should be moved elsewhere, because it also needs to listen to URL changes in the connected page +void rehydrateSidebar(); + // Handle an embedded AA business copilot frame void initCopilotMessenger(); diff --git a/src/sidebar/useHideEmptySidebar.ts b/src/sidebar/useHideEmptySidebar.ts index 94f2a50ee3..8c1392365e 100644 --- a/src/sidebar/useHideEmptySidebar.ts +++ b/src/sidebar/useHideEmptySidebar.ts @@ -16,7 +16,7 @@ */ import useAsyncEffect from "use-async-effect"; -import { getTopLevelFrame } from "./sidePanel"; +import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; import { getReservedSidebarEntries } from "@/contentScript/messenger/api"; import { useSelector } from "react-redux"; import { @@ -34,8 +34,9 @@ export const useHideEmptySidebar = () => { useAsyncEffect( async (isMounted) => { - const topFrame = await getTopLevelFrame(); - const reservedPanelEntries = await getReservedSidebarEntries(topFrame); + const reservedPanelEntries = await getReservedSidebarEntries( + getAssociatedTarget(), + ); // We don't want to hide the Sidebar if there are any open reserved panels. // Otherwise, we would hide the Sidebar when a user re-renders a panel, e.g. when using diff --git a/src/starterBricks/sidebarExtension.ts b/src/starterBricks/sidebarExtension.ts index 1bdd7063e4..6474a90541 100644 --- a/src/starterBricks/sidebarExtension.ts +++ b/src/starterBricks/sidebarExtension.ts @@ -62,7 +62,7 @@ import { type Reader } from "@/types/bricks/readerTypes"; import { type StarterBrick } from "@/types/starterBrickTypes"; import { isLoadedInIframe } from "@/utils/iframeUtils"; import makeServiceContextFromDependencies from "@/integrations/util/makeServiceContextFromDependencies"; -import { isSidePanelOpen } from "@/sidebar/sidePanel"; +import { isSidePanelOpen } from "@/sidebar/sidePanel/messenger/api"; export type SidebarConfig = { heading: string; From 1252e5a7119ebb8f269a580f3ae5b14d2786b874 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Thu, 4 Jan 2024 23:55:21 +0800 Subject: [PATCH 10/29] Clean last isSidebarFrameVisible issues --- package-lock.json | 24 +++++++-- package.json | 2 +- src/background/background.ts | 2 +- .../ephemeralForm/formTransformer.ts | 4 +- .../DisplayTemporaryInfo.test.ts | 2 +- .../temporaryInfo/DisplayTemporaryInfo.ts | 6 +-- src/contentScript/pageEditor.ts | 2 +- src/contentScript/sidebarActivation.ts | 4 +- src/contentScript/sidebarController.tsx | 50 ++++++++++--------- src/utils/expectContext.ts | 12 ++--- 10 files changed, 61 insertions(+), 47 deletions(-) diff --git a/package-lock.json b/package-lock.json index c7aed0072f..0f2318d67c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -143,7 +143,7 @@ "uuid": "^9.0.1", "webext-base-css": "^1.4.4", "webext-content-scripts": "^2.6.0", - "webext-detect-page": "^4.2.1", + "webext-detect-page": "^5.0.0", "webext-inject-on-install": "^2.0.0-2", "webext-messenger": "^0.25.0-0", "webext-patterns": "^1.3.0", @@ -27292,9 +27292,15 @@ } }, "node_modules/webext-detect-page": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/webext-detect-page/-/webext-detect-page-4.2.1.tgz", - "integrity": "sha512-saiMkdwrjR5WIoW+clqFCZiGLqbe5Bp3udnPpsaFj6gL3uYTYhpkSjc7givG6gwE3g0oXLPIKOhhP52MrHdK+w==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webext-detect-page/-/webext-detect-page-5.0.0.tgz", + "integrity": "sha512-ptkUyczkFOWKPfc1lPKptb3DkfK0FJB5f+nLwiNrWX2by1uo8nZdCdCu/92J1u+5Z65oPWd1s63orc8ojPbnOw==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/fregante" + } }, "node_modules/webext-inject-on-install": { "version": "2.0.0", @@ -27322,6 +27328,11 @@ "webext-detect-page": "^4.1.1" } }, + "node_modules/webext-messenger/node_modules/webext-detect-page": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/webext-detect-page/-/webext-detect-page-4.2.1.tgz", + "integrity": "sha512-saiMkdwrjR5WIoW+clqFCZiGLqbe5Bp3udnPpsaFj6gL3uYTYhpkSjc7givG6gwE3g0oXLPIKOhhP52MrHdK+w==" + }, "node_modules/webext-patterns": { "version": "1.3.0", "integrity": "sha512-X9HMnic9ZtvSFKi2cdh0l+sxyj7f9oLedaa2JfxjnyEqGBz8OJjaHQ40jmraX1DJLTHOpqr+rCz1r3MW2+doUg==", @@ -27383,6 +27394,11 @@ "url": "https://github.com/sponsors/fregante" } }, + "node_modules/webext-tools/node_modules/webext-detect-page": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/webext-detect-page/-/webext-detect-page-4.2.1.tgz", + "integrity": "sha512-saiMkdwrjR5WIoW+clqFCZiGLqbe5Bp3udnPpsaFj6gL3uYTYhpkSjc7givG6gwE3g0oXLPIKOhhP52MrHdK+w==" + }, "node_modules/webextension-polyfill": { "version": "0.10.0", "integrity": "sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==" diff --git a/package.json b/package.json index 7491e767ff..434110a54e 100644 --- a/package.json +++ b/package.json @@ -166,7 +166,7 @@ "uuid": "^9.0.1", "webext-base-css": "^1.4.4", "webext-content-scripts": "^2.6.0", - "webext-detect-page": "^4.2.1", + "webext-detect-page": "^5.0.0", "webext-inject-on-install": "^2.0.0-2", "webext-messenger": "^0.25.0-0", "webext-patterns": "^1.3.0", diff --git a/src/background/background.ts b/src/background/background.ts index b025ab8b58..25b4a68ac5 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -52,7 +52,7 @@ void initMessengerLogging(); void initRuntimeLogging(); registerMessenger(); registerExternalMessenger(); -initBrowserAction(); +void initBrowserAction(); initInstaller(); void initNavigation(); initExecutor(); diff --git a/src/bricks/transformers/ephemeralForm/formTransformer.ts b/src/bricks/transformers/ephemeralForm/formTransformer.ts index 327d1f66e5..25864df6a3 100644 --- a/src/bricks/transformers/ephemeralForm/formTransformer.ts +++ b/src/bricks/transformers/ephemeralForm/formTransformer.ts @@ -158,7 +158,7 @@ export class FormTransformer extends TransformerABC { // Ensure the sidebar is visible (which may also be showing persistent panels) await ensureSidebar(); - showSidebarForm({ + await showSidebarForm({ extensionId: logger.context.extensionId, blueprintId: logger.context.blueprintId, nonce: formNonce, @@ -182,7 +182,7 @@ export class FormTransformer extends TransformerABC { // NOTE: we're not hiding the side panel here to avoid closing the sidebar if the user already had it open. // In the future we might creating/sending a closeIfEmpty message to the sidebar, so that it would close // if this form was the only entry in the panel - hideSidebarForm(formNonce); + void hideSidebarForm(formNonce); void cancelForm(formNonce); }); } else { diff --git a/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.test.ts b/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.test.ts index 49169747cd..f667a0e708 100644 --- a/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.test.ts +++ b/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.test.ts @@ -177,7 +177,7 @@ describe("DisplayTemporaryInfo", () => { let payload: PanelPayload; showTemporarySidebarPanelMock.mockImplementation( - (entry: TemporaryPanelEntry) => { + async (entry: TemporaryPanelEntry) => { payload = entry.payload; }, ); diff --git a/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts b/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts index 975cb18645..aa41fea69e 100644 --- a/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts +++ b/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts @@ -161,7 +161,7 @@ export async function displayTemporaryInfo({ updatePanelDefinition(newEntry); if (location === "panel") { - updateTemporarySidebarPanel(newEntry); + void updateTemporarySidebarPanel(newEntry); } else { updateTemporaryOverlayPanel(newEntry); } @@ -178,7 +178,7 @@ export async function displayTemporaryInfo({ await ensureSidebar(); // Show loading - showTemporarySidebarPanel({ + await showTemporarySidebarPanel({ ...panelEntryMetadata, nonce, payload: { @@ -199,7 +199,7 @@ export async function displayTemporaryInfo({ ); controller.signal.addEventListener("abort", () => { - hideTemporarySidebarPanel(nonce); + void hideTemporarySidebarPanel(nonce); void stopWaitingForTemporaryPanels([nonce]); }); } else { diff --git a/src/contentScript/pageEditor.ts b/src/contentScript/pageEditor.ts index fe89f8f1de..6eb4bb8d0f 100644 --- a/src/contentScript/pageEditor.ts +++ b/src/contentScript/pageEditor.ts @@ -199,7 +199,7 @@ export async function runRendererBlock({ } if (location === "panel") { - showTemporarySidebarPanel({ + await showTemporarySidebarPanel({ // Pass extension id so previous run is cancelled extensionId, blueprintId, diff --git a/src/contentScript/sidebarActivation.ts b/src/contentScript/sidebarActivation.ts index 6d2d2ebfb8..c16254b047 100644 --- a/src/contentScript/sidebarActivation.ts +++ b/src/contentScript/sidebarActivation.ts @@ -57,7 +57,7 @@ async function showSidebarActivationForMods( const controller = new AbortController(); await ensureSidebar(); - showModActivationInSidebar({ + await showModActivationInSidebar({ modIds, heading: "Activating", }); @@ -71,7 +71,7 @@ async function showSidebarActivationForMods( }, ); controller.signal.addEventListener("abort", () => { - hideModActivationInSidebar(); + void hideModActivationInSidebar(); }); } diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index 930084f749..ae3d2948ed 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -112,12 +112,12 @@ export async function rehydrateSidebar( } } -function renderPanelsIfVisible(): void { +async function renderPanelsIfVisible(): Promise { expectContext("contentScript"); console.debug("sidebarController:renderPanelsIfVisible"); - if (isSidebarFrameVisible()) { + if (await isSidePanelOpen()) { const seqNum = renderSequenceNumber; renderSequenceNumber++; void sidebarInThisTab.renderPanels(seqNum, panels); @@ -128,10 +128,12 @@ function renderPanelsIfVisible(): void { } } -export function showSidebarForm(entry: Except): void { +export async function showSidebarForm( + entry: Except, +): Promise { expectContext("contentScript"); - if (!isSidebarFrameVisible()) { + if (!(await isSidePanelOpen())) { throw new Error("Cannot add sidebar form if the sidebar is not visible"); } @@ -140,10 +142,10 @@ export function showSidebarForm(entry: Except): void { void sidebarInThisTab.showForm(seqNum, { type: "form", ...entry }); } -export function hideSidebarForm(nonce: UUID): void { +export async function hideSidebarForm(nonce: UUID): Promise { expectContext("contentScript"); - if (!isSidebarFrameVisible()) { + if (!(await isSidePanelOpen())) { // Already hidden return; } @@ -153,12 +155,12 @@ export function hideSidebarForm(nonce: UUID): void { void sidebarInThisTab.hideForm(seqNum, nonce); } -export function showTemporarySidebarPanel( +export async function showTemporarySidebarPanel( entry: Except, -): void { +): Promise { expectContext("contentScript"); - if (!isSidebarFrameVisible()) { + if (!(await isSidePanelOpen())) { throw new Error( "Cannot add temporary sidebar panel if the sidebar is not visible", ); @@ -171,12 +173,12 @@ export function showTemporarySidebarPanel( }); } -export function updateTemporarySidebarPanel( +export async function updateTemporarySidebarPanel( entry: Except, -): void { +): Promise { expectContext("contentScript"); - if (!isSidebarFrameVisible()) { + if (!(await isSidePanelOpen())) { throw new Error( "Cannot add temporary sidebar panel if the sidebar is not visible", ); @@ -189,10 +191,10 @@ export function updateTemporarySidebarPanel( }); } -export function hideTemporarySidebarPanel(nonce: UUID): void { +export async function hideTemporarySidebarPanel(nonce: UUID): Promise { expectContext("contentScript"); - if (!isSidebarFrameVisible()) { + if (!(await isSidePanelOpen())) { return; } @@ -212,7 +214,7 @@ export function removeExtensions(extensionIds: UUID[]): void { // `panels` is const, so replace the contents const current = panels.splice(0, panels.length); panels.push(...current.filter((x) => !extensionIds.includes(x.extensionId))); - renderPanelsIfVisible(); + void renderPanelsIfVisible(); } /** @@ -242,7 +244,7 @@ export function removeExtensionPoint( ), ); - renderPanelsIfVisible(); + void renderPanelsIfVisible(); } /** @@ -277,7 +279,7 @@ export function reservePanels(refs: ModComponentRef[]): void { } } - renderPanelsIfVisible(); + void renderPanelsIfVisible(); } export function updateHeading(extensionId: UUID, heading: string): void { @@ -296,7 +298,7 @@ export function updateHeading(extensionId: UUID, heading: string): void { entry.extensionPointId, { ...entry }, ); - renderPanelsIfVisible(); + void renderPanelsIfVisible(); } else { console.warn( "updateHeading: No panel exists for extension %s", @@ -344,7 +346,7 @@ export function upsertPanel( }); } - renderPanelsIfVisible(); + void renderPanelsIfVisible(); } /** @@ -353,12 +355,12 @@ export function upsertPanel( * @param entry the mod activation panel entry * @throws Error if the sidebar frame is not visible */ -export function showModActivationInSidebar( +export async function showModActivationInSidebar( entry: Except, -): void { +): Promise { expectContext("contentScript"); - if (!isSidebarFrameVisible()) { + if (!(await isSidePanelOpen())) { throw new Error( "Cannot activate mods in the sidebar if the sidebar is not visible", ); @@ -380,13 +382,13 @@ export function showModActivationInSidebar( * Hide the mod activation panel in the sidebar. * @see showModActivationInSidebar */ -export function hideModActivationInSidebar(): void { +export async function hideModActivationInSidebar(): Promise { expectContext("contentScript"); // Clear out in in-memory tracking modActivationPanelEntry = null; - if (!isSidebarFrameVisible()) { + if (!(await isSidePanelOpen())) { return; } diff --git a/src/utils/expectContext.ts b/src/utils/expectContext.ts index b83873d012..4da949b34f 100644 --- a/src/utils/expectContext.ts +++ b/src/utils/expectContext.ts @@ -20,6 +20,8 @@ import { isContentScript, isExtensionContext, isWebPage, + contextNames, + isSidePanel, } from "webext-detect-page"; function isBrowserSidebar(): boolean { @@ -48,14 +50,7 @@ function createError( } // eslint-disable-next-line local-rules/persistBackgroundData -- Static -const contexts = [ - "web", - "extension", - "background", - "contentScript", - "devTools", - "sidebar", -] as const; +const contexts = [...contextNames, "sidebar"] as const; // eslint-disable-next-line local-rules/persistBackgroundData -- Functions const contextMap = new Map<(typeof contexts)[number], () => boolean>([ @@ -64,6 +59,7 @@ const contextMap = new Map<(typeof contexts)[number], () => boolean>([ ["background", isBackground], ["contentScript", isContentScript], ["sidebar", isBrowserSidebar], + ["sidePanel", isSidePanel], ["devTools", () => "devtools" in chrome], ]); From 5129b0b5d60ec643e236e9cc0e92ad3b424e6264 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Fri, 5 Jan 2024 00:09:42 +0800 Subject: [PATCH 11/29] First successful run --- src/sidebar/messenger/api.ts | 20 +++++++++++--------- src/sidebar/sidePanel.tsx | 10 +++++++++- src/sidebar/sidePanel/messenger/api.ts | 4 ++-- src/sidebar/useHideEmptySidebar.ts | 9 ++------- src/utils/expectContext.ts | 2 -- 5 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/sidebar/messenger/api.ts b/src/sidebar/messenger/api.ts index cf8e6390e0..aef431e1d6 100644 --- a/src/sidebar/messenger/api.ts +++ b/src/sidebar/messenger/api.ts @@ -16,19 +16,21 @@ */ /* Do not use `registerMethod` in this file */ -import { expectContext } from "@/utils/expectContext"; +import { isContentScript } from "webext-detect-page"; import { getMethod, getNotifier, getThisFrame } from "webext-messenger"; -expectContext("contentScript"); - const target = { page: "/sidebar.html" }; -// Unavoidable race condition: we can't message the sidebar until we know the tabId. -// If this causes issues (unlikely), we can make `getMethod` accept an async function -// that generates the target, like `getMethod('FOO', getThisFramesSideBarUrl())`. -void getThisFrame().then((frame) => { - target.page += "?tabId=" + frame.tabId; -}); +// This should be an expectContext, but it's the usual "everyone imports the registry" problem +if (isContentScript()) { + // Unavoidable race condition: we can't message the sidebar until we know the tabId. + // If this causes issues (unlikely), we can make `getMethod` accept an async function + // that generates the target, like `getMethod('FOO', getThisFramesSideBarUrl())`. + // eslint-disable-next-line promise/prefer-await-to-then + void getThisFrame().then((frame) => { + target.page += "?tabId=" + frame.tabId; + }); +} const sidebarInThisTab = { renderPanels: getMethod("SIDEBAR_RENDER_PANELS", target), diff --git a/src/sidebar/sidePanel.tsx b/src/sidebar/sidePanel.tsx index 6d0853c42b..3c35328270 100644 --- a/src/sidebar/sidePanel.tsx +++ b/src/sidebar/sidePanel.tsx @@ -18,5 +18,13 @@ /** @file This file defines the internal API for the sidePanel, only meant to be run in the sidePanel itself */ import { expectContext } from "@/utils/expectContext"; +import { + getAssociatedTabId, + hideSidePanel, +} from "@/sidebar/sidePanel/messenger/api"; -expectContext("sidePanel"); +expectContext("sidebar"); + +export async function hideSelf() { + return hideSidePanel(getAssociatedTabId()); +} diff --git a/src/sidebar/sidePanel/messenger/api.ts b/src/sidebar/sidePanel/messenger/api.ts index 313c5292a2..21e2b3c066 100644 --- a/src/sidebar/sidePanel/messenger/api.ts +++ b/src/sidebar/sidePanel/messenger/api.ts @@ -33,7 +33,7 @@ import { expectContext } from "@/utils/expectContext"; import { type Target } from "webext-messenger"; export function getAssociatedTabId(): number { - expectContext("sidePanel"); + expectContext("sidebar"); const tabId = new URLSearchParams(window.location.search).get("tabId"); return Number(tabId); } @@ -45,7 +45,7 @@ export function getAssociatedTarget(): Target { const PING_MESSAGE = "PING_SIDE_PANEL"; // Do not use the messenger because it doesn't support retry-less messaging export function initSidePanelPingResponder() { - expectContext("sidePanel"); + expectContext("sidebar"); chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if ( isObject(message) && diff --git a/src/sidebar/useHideEmptySidebar.ts b/src/sidebar/useHideEmptySidebar.ts index 8c1392365e..c9e5976c99 100644 --- a/src/sidebar/useHideEmptySidebar.ts +++ b/src/sidebar/useHideEmptySidebar.ts @@ -24,6 +24,7 @@ import { selectVisiblePanelCount, } from "@/sidebar/sidebarSelectors"; import { eventKeyForEntry } from "@/sidebar/eventKeyUtils"; +import { hideSelf } from "./sidePanel"; /** * Hide the sidebar if there are no visible panels. We use this to close the sidebar if the user closes all panels. @@ -52,13 +53,7 @@ export const useHideEmptySidebar = () => { visiblePanelCount === 0 && openReservedPanels.length === 0 ) { - // TODO: Move to own function - await chrome.sidePanel.setOptions({ - tabId: Number( - new URLSearchParams(window.location.search).get("tabId"), - ), - enabled: false, - }); + await hideSelf(); } }, [visiblePanelCount], diff --git a/src/utils/expectContext.ts b/src/utils/expectContext.ts index 4da949b34f..076834ee6c 100644 --- a/src/utils/expectContext.ts +++ b/src/utils/expectContext.ts @@ -21,7 +21,6 @@ import { isExtensionContext, isWebPage, contextNames, - isSidePanel, } from "webext-detect-page"; function isBrowserSidebar(): boolean { @@ -59,7 +58,6 @@ const contextMap = new Map<(typeof contexts)[number], () => boolean>([ ["background", isBackground], ["contentScript", isContentScript], ["sidebar", isBrowserSidebar], - ["sidePanel", isSidePanel], ["devTools", () => "devtools" in chrome], ]); From d1527c10b3885786ce9f45f3e7cb71d7661bf87e Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Fri, 5 Jan 2024 00:16:51 +0800 Subject: [PATCH 12/29] Fix lockfile updates --- package-lock.json | 6 ++---- package.json | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0f2318d67c..229b027bbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -144,7 +144,7 @@ "webext-base-css": "^1.4.4", "webext-content-scripts": "^2.6.0", "webext-detect-page": "^5.0.0", - "webext-inject-on-install": "^2.0.0-2", + "webext-inject-on-install": "^2.0.0", "webext-messenger": "^0.25.0-0", "webext-patterns": "^1.3.0", "webext-permissions": "^3.1.2", @@ -176,7 +176,6 @@ "@testing-library/user-event": "^14.5.2", "@total-typescript/ts-reset": "^0.5.1", "@types/chrome": "^0.0.254", - "@types/dom-navigation": "^1.0.3", "@types/dompurify": "^3.0.5", "@types/downloadjs": "^1.4.6", "@types/holderjs": "^2.9.4", @@ -7849,8 +7848,7 @@ "node_modules/@types/dom-navigation": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/dom-navigation/-/dom-navigation-1.0.3.tgz", - "integrity": "sha512-nbkhQ2o6UUBn1uLwSrA//rFkqs8XRk5d7cE6jzzTl8MAIUs/nFMtFqVESwiYY1HGZxflzbFlXsZ8NNzTaoFs1Q==", - "dev": true + "integrity": "sha512-nbkhQ2o6UUBn1uLwSrA//rFkqs8XRk5d7cE6jzzTl8MAIUs/nFMtFqVESwiYY1HGZxflzbFlXsZ8NNzTaoFs1Q==" }, "node_modules/@types/dompurify": { "version": "3.0.5", diff --git a/package.json b/package.json index 434110a54e..c08b179c5e 100644 --- a/package.json +++ b/package.json @@ -167,7 +167,7 @@ "webext-base-css": "^1.4.4", "webext-content-scripts": "^2.6.0", "webext-detect-page": "^5.0.0", - "webext-inject-on-install": "^2.0.0-2", + "webext-inject-on-install": "^2.0.0", "webext-messenger": "^0.25.0-0", "webext-patterns": "^1.3.0", "webext-permissions": "^3.1.2", @@ -199,7 +199,6 @@ "@testing-library/user-event": "^14.5.2", "@total-typescript/ts-reset": "^0.5.1", "@types/chrome": "^0.0.254", - "@types/dom-navigation": "^1.0.3", "@types/dompurify": "^3.0.5", "@types/downloadjs": "^1.4.6", "@types/holderjs": "^2.9.4", From 767b7283efe3a523114dc171a8e10a19d26adef6 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Fri, 5 Jan 2024 00:33:45 +0800 Subject: [PATCH 13/29] Fix restrictedUrlPopup sizing --- src/tinyPages/restrictedUrlPopup.html | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/tinyPages/restrictedUrlPopup.html b/src/tinyPages/restrictedUrlPopup.html index 4081f9d592..b8369b0a24 100644 --- a/src/tinyPages/restrictedUrlPopup.html +++ b/src/tinyPages/restrictedUrlPopup.html @@ -20,13 +20,6 @@ PixieBrix - From 9c191ff337ccbfa6d608fe020682f03d7ef3eef3 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Fri, 5 Jan 2024 07:26:02 +0800 Subject: [PATCH 14/29] Revert default path change --- scripts/manifest.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/manifest.mjs b/scripts/manifest.mjs index c22f2330df..c2781fe21a 100644 --- a/scripts/manifest.mjs +++ b/scripts/manifest.mjs @@ -59,7 +59,7 @@ function updateManifestToV3(manifestV2) { manifest.permissions.push("sidePanel"); manifest.side_panel = { - default_path: "sidebar-empty.html", + default_path: "sidebar.html", }; // Update format From 725754898f581b4663c2db2ef2d113215a7e6f0d Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Fri, 5 Jan 2024 17:05:35 +0800 Subject: [PATCH 15/29] Fix strictNullChecks issue --- src/contentScript/sidebarController.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index ae3d2948ed..257cbbba4b 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -36,8 +36,8 @@ import type { } from "@/types/sidebarTypes"; import { getTemporaryPanelSidebarEntries } from "@/bricks/transformers/temporaryInfo/temporaryPanelProtocol"; import { getFormPanelSidebarEntries } from "@/contentScript/ephemeralFormProtocol"; -import { showMySidePanel } from "@/background/messenger/api"; import { isSidePanelOpen } from "@/sidebar/sidePanel/messenger/api"; +import { backgroundTarget, getMethod } from "webext-messenger"; /** * Sequence number for ensuring render requests are handled in order @@ -61,7 +61,8 @@ let modActivationPanelEntry: ModActivationPanelEntry | null = null; export async function showSidebar(): Promise { console.debug("sidebarController:showSidebar"); reportEvent(Events.SIDEBAR_SHOW); - await showMySidePanel(); + // TODO: Import from background/messenger/api.ts after the strictNullChecks migration + await getMethod("SHOW_MY_SIDE_PANEL" as "PING", backgroundTarget)(); } /** From 1f335abafb8d68a7eae5de383e2dc5ae93c183ae Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Sun, 7 Jan 2024 14:23:02 +0800 Subject: [PATCH 16/29] HIDE_SIDEBAR_EVENT_NAME replacement --- .../ephemeralForm/formTransformer.ts | 13 ++----------- .../temporaryInfo/DisplayTemporaryInfo.ts | 11 ++--------- src/contentScript/sidebarActivation.ts | 13 ++++--------- src/sidebar/sidePanel/messenger/api.ts | 17 +++++++++++++++++ 4 files changed, 25 insertions(+), 29 deletions(-) diff --git a/src/bricks/transformers/ephemeralForm/formTransformer.ts b/src/bricks/transformers/ephemeralForm/formTransformer.ts index 25864df6a3..97dc90785c 100644 --- a/src/bricks/transformers/ephemeralForm/formTransformer.ts +++ b/src/bricks/transformers/ephemeralForm/formTransformer.ts @@ -34,6 +34,7 @@ import { getThisFrame } from "webext-messenger"; import { type BrickConfig } from "@/bricks/types"; import { type FormDefinition } from "@/bricks/transformers/ephemeralForm/formTypes"; import { isExpression } from "@/utils/expressionUtils"; +import { onSidePanelClosure } from "@/sidebar/sidePanel/messenger/api"; // The modes for createFrameSrc are different than the location argument for FormTransformer. The mode for the frame // just determines the layout container of the form @@ -166,17 +167,7 @@ export class FormTransformer extends TransformerABC { }); // Two-way binding between sidebar and form. Listen for the user (or an action) closing the sidebar - window.addEventListener( - HIDE_SIDEBAR_EVENT_NAME, - () => { - controller.abort(); - }, - { - // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener - // The listener will be removed when the given AbortSignal object's abort() method is called. - signal: controller.signal, - }, - ); + onSidePanelClosure(controller); controller.signal.addEventListener("abort", () => { // NOTE: we're not hiding the side panel here to avoid closing the sidebar if the user already had it open. diff --git a/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts b/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts index aa41fea69e..a8e02a42b6 100644 --- a/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts +++ b/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts @@ -55,6 +55,7 @@ import { TransformerABC } from "@/types/bricks/transformerTypes"; import { type Schema } from "@/types/schemaTypes"; import { type Location } from "@/types/starterBrickTypes"; import { assumeNotNullish_UNSAFE } from "@/utils/nullishUtils"; +import { onSidePanelClosure } from "@/sidebar/sidePanel/messenger/api"; // Match naming of the sidebar panel extension point triggers export type RefreshTrigger = "manual" | "statechange"; @@ -188,15 +189,7 @@ export async function displayTemporaryInfo({ }, }); - window.addEventListener( - HIDE_SIDEBAR_EVENT_NAME, - () => { - controller.abort(); - }, - { - signal: controller.signal, - }, - ); + onSidePanelClosure(controller); controller.signal.addEventListener("abort", () => { void hideTemporarySidebarPanel(nonce); diff --git a/src/contentScript/sidebarActivation.ts b/src/contentScript/sidebarActivation.ts index c16254b047..e9850a40c1 100644 --- a/src/contentScript/sidebarActivation.ts +++ b/src/contentScript/sidebarActivation.ts @@ -32,6 +32,7 @@ import { Events } from "@/telemetry/events"; import { isLoadedInIframe } from "@/utils/iframeUtils"; import { getActivatedModIds } from "@/store/extensionsStorage"; import { DEFAULT_SERVICE_URL } from "@/urlConstants"; +import { onSidePanelClosure } from "@/sidebar/sidePanel/messenger/api"; let listener: EventListener | null; @@ -61,15 +62,9 @@ async function showSidebarActivationForMods( modIds, heading: "Activating", }); - window.addEventListener( - HIDE_SIDEBAR_EVENT_NAME, - () => { - controller.abort(); - }, - { - signal: controller.signal, - }, - ); + + onSidePanelClosure(controller); + controller.signal.addEventListener("abort", () => { void hideModActivationInSidebar(); }); diff --git a/src/sidebar/sidePanel/messenger/api.ts b/src/sidebar/sidePanel/messenger/api.ts index 21e2b3c066..ab3181de33 100644 --- a/src/sidebar/sidePanel/messenger/api.ts +++ b/src/sidebar/sidePanel/messenger/api.ts @@ -32,6 +32,9 @@ import { isObject } from "@/utils/objectUtils"; import { expectContext } from "@/utils/expectContext"; import { type Target } from "webext-messenger"; +// Approximate sidebar width in pixels. Used to determine whether it's open +const MINIMUM_SIDEBAR_WIDTH = 300; + export function getAssociatedTabId(): number { expectContext("sidebar"); const tabId = new URLSearchParams(window.location.search).get("tabId"); @@ -101,3 +104,17 @@ export async function hideSidePanel(tabId: number) { enabled: false, }); } + +export function onSidePanelClosure(controller?: AbortController): void { + expectContext("contentScript"); + const getDifference = () => window.outerWidth - window.innerWidth; + window.addEventListener( + "resize", + () => { + if (getDifference() < MINIMUM_SIDEBAR_WIDTH) { + controller.abort(); + } + }, + { signal: controller.signal }, + ); +} From 46ea07896e6bc0d3623ea85bc61d1567c67d566c Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Sun, 7 Jan 2024 14:28:58 +0800 Subject: [PATCH 17/29] Fix tests --- src/sidebar/Tabs.test.tsx | 3 ++- src/sidebar/activateMod/ActivateModPanel.test.tsx | 5 +++-- src/sidebar/useHideEmptySidebar.ts | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/sidebar/Tabs.test.tsx b/src/sidebar/Tabs.test.tsx index baea0ffad8..1f165fdbc1 100644 --- a/src/sidebar/Tabs.test.tsx +++ b/src/sidebar/Tabs.test.tsx @@ -25,13 +25,14 @@ import { MOD_LAUNCHER } from "@/sidebar/modLauncher/constants"; import { waitForEffect } from "@/testUtils/testHelpers"; import userEvent from "@testing-library/user-event"; import * as messengerApi from "@/contentScript/messenger/api"; +import { hideSelf as hideSidebar } from "@/sidebar/sidePanel"; import { eventKeyForEntry } from "@/sidebar/eventKeyUtils"; import { mockAllApiEndpoints } from "@/testUtils/appApiMock"; mockAllApiEndpoints(); const cancelFormSpy = jest.spyOn(messengerApi, "cancelForm"); -const hideSidebarSpy = jest.spyOn(messengerApi, "hideSidebar"); +const hideSidebarSpy = jest.mocked(hideSidebar); async function setupPanelsAndRender(options: { sidebarEntries?: Partial; diff --git a/src/sidebar/activateMod/ActivateModPanel.test.tsx b/src/sidebar/activateMod/ActivateModPanel.test.tsx index 7194223e1a..349ec798e6 100644 --- a/src/sidebar/activateMod/ActivateModPanel.test.tsx +++ b/src/sidebar/activateMod/ActivateModPanel.test.tsx @@ -39,7 +39,7 @@ import { marketplaceListingFactory, modDefinitionToMarketplacePackage, } from "@/testUtils/factories/marketplaceFactories"; -import * as messengerApi from "@/contentScript/messenger/api"; +import { hideSelf as hideSidebar } from "@/sidebar/sidePanel"; import ActivateMultipleModsPanel from "@/sidebar/activateMod/ActivateMultipleModsPanel"; import ErrorBoundary from "@/sidebar/SidebarErrorBoundary"; import { includesQuickBarStarterBrick } from "@/starterBricks/starterBrickModUtils"; @@ -67,7 +67,8 @@ const useRequiredModDefinitionsMock = jest.mocked(useRequiredModDefinitions); const checkModDefinitionPermissionsMock = jest.mocked( checkModDefinitionPermissions, ); -const hideSidebarSpy = jest.spyOn(messengerApi, "hideSidebar"); + +const hideSidebarSpy = jest.mocked(hideSidebar); jest.mock("@/starterBricks/starterBrickModUtils", () => { const actualUtils = jest.requireActual( diff --git a/src/sidebar/useHideEmptySidebar.ts b/src/sidebar/useHideEmptySidebar.ts index c9e5976c99..1bbeaa87e5 100644 --- a/src/sidebar/useHideEmptySidebar.ts +++ b/src/sidebar/useHideEmptySidebar.ts @@ -24,7 +24,7 @@ import { selectVisiblePanelCount, } from "@/sidebar/sidebarSelectors"; import { eventKeyForEntry } from "@/sidebar/eventKeyUtils"; -import { hideSelf } from "./sidePanel"; +import { hideSelf } from "@/sidebar/sidePanel"; /** * Hide the sidebar if there are no visible panels. We use this to close the sidebar if the user closes all panels. From 484b6132a7518ef57235bfcb085ad415cc606e3c Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Sun, 7 Jan 2024 15:16:04 +0800 Subject: [PATCH 18/29] Fix CI/snapshots/tests setup --- scripts/__snapshots__/manifest.test.js.snap | 6 ++- src/background/webextAlert.ts | 26 ------------ src/contentScript/sidebarController.tsx | 10 ++++- src/sidebar/Tabs.test.tsx | 6 ++- .../__snapshots__/Header.test.tsx.snap | 40 ------------------- .../__snapshots__/SidebarBody.test.tsx.snap | 20 ---------- .../activateMod/ActivateModPanel.test.tsx | 4 +- src/sidebar/sidePanel/messenger/api.ts | 2 +- src/tsconfig.strictNullChecks.json | 1 - 9 files changed, 20 insertions(+), 95 deletions(-) delete mode 100644 src/background/webextAlert.ts diff --git a/scripts/__snapshots__/manifest.test.js.snap b/scripts/__snapshots__/manifest.test.js.snap index fbed4fe58b..85dd80b473 100644 --- a/scripts/__snapshots__/manifest.test.js.snap +++ b/scripts/__snapshots__/manifest.test.js.snap @@ -205,7 +205,7 @@ exports[`customizeManifest mv3 1`] = ` "48": "icons/logo48.png", }, "manifest_version": 3, - "minimum_chrome_version": "95.0", + "minimum_chrome_version": "116.0", "name": "PixieBrix - Development", "optional_permissions": [ "clipboardWrite", @@ -223,6 +223,7 @@ exports[`customizeManifest mv3 1`] = ` "contextMenus", "devtools", "scripting", + "sidePanel", ], "sandbox": { "pages": [ @@ -230,6 +231,9 @@ exports[`customizeManifest mv3 1`] = ` ], }, "short_name": "PixieBrix", + "side_panel": { + "default_path": "sidebar.html", + }, "storage": { "managed_schema": "managedStorageSchema.json", }, diff --git a/src/background/webextAlert.ts b/src/background/webextAlert.ts deleted file mode 100644 index 497fe5e6ff..0000000000 --- a/src/background/webextAlert.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { isBackgroundWorker } from "webext-detect-page"; - -function windowAlert(message: string): void { - const url = new URL(browser.runtime.getURL("alert.html")); - url.searchParams.set("title", chrome.runtime.getManifest().name); - url.searchParams.set("message", message); - - const width = 420; - const height = 150; - - void browser.windows.create({ - url: url.href, - focused: true, - height, - width, - top: Math.round((screen.availHeight - height) / 2), - left: Math.round((screen.availWidth - width) / 2), - type: "popup", - }); -} - -// No alert() in background workers -// eslint-disable-next-line local-rules/persistBackgroundData -- Function -const webextAlert = isBackgroundWorker() ? windowAlert : alert; - -export default webextAlert; diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index 257cbbba4b..35271e1084 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -61,8 +61,14 @@ let modActivationPanelEntry: ModActivationPanelEntry | null = null; export async function showSidebar(): Promise { console.debug("sidebarController:showSidebar"); reportEvent(Events.SIDEBAR_SHOW); - // TODO: Import from background/messenger/api.ts after the strictNullChecks migration - await getMethod("SHOW_MY_SIDE_PANEL" as "PING", backgroundTarget)(); + // TODO: Import from background/messenger/api.ts after the strictNullChecks migration, drop "SIDEBAR_PING" string + await getMethod("SHOW_MY_SIDE_PANEL" as "SIDEBAR_PING", backgroundTarget)(); + + try { + await sidebarInThisTab.pingSidebar(); + } catch (error) { + throw new Error("The sidebar did not respond in time", { cause: error }); + } } /** diff --git a/src/sidebar/Tabs.test.tsx b/src/sidebar/Tabs.test.tsx index 1f165fdbc1..6549c52521 100644 --- a/src/sidebar/Tabs.test.tsx +++ b/src/sidebar/Tabs.test.tsx @@ -25,14 +25,16 @@ import { MOD_LAUNCHER } from "@/sidebar/modLauncher/constants"; import { waitForEffect } from "@/testUtils/testHelpers"; import userEvent from "@testing-library/user-event"; import * as messengerApi from "@/contentScript/messenger/api"; -import { hideSelf as hideSidebar } from "@/sidebar/sidePanel"; +import * as sidePanel from "@/sidebar/sidePanel"; import { eventKeyForEntry } from "@/sidebar/eventKeyUtils"; import { mockAllApiEndpoints } from "@/testUtils/appApiMock"; mockAllApiEndpoints(); +jest.mock("@/sidebar/sidePanel"); + const cancelFormSpy = jest.spyOn(messengerApi, "cancelForm"); -const hideSidebarSpy = jest.mocked(hideSidebar); +const hideSidebarSpy = jest.spyOn(sidePanel, "hideSelf"); async function setupPanelsAndRender(options: { sidebarEntries?: Partial; diff --git a/src/sidebar/__snapshots__/Header.test.tsx.snap b/src/sidebar/__snapshots__/Header.test.tsx.snap index 4e72e1039f..62b78e9532 100644 --- a/src/sidebar/__snapshots__/Header.test.tsx.snap +++ b/src/sidebar/__snapshots__/Header.test.tsx.snap @@ -5,26 +5,6 @@ exports[`Header renders 1`] = `
-
@@ -65,26 +45,6 @@ exports[`Header renders sidebar header logo per organization theme 1`] = `
-
diff --git a/src/sidebar/__snapshots__/SidebarBody.test.tsx.snap b/src/sidebar/__snapshots__/SidebarBody.test.tsx.snap index 04c4b7a514..d41955c7a1 100644 --- a/src/sidebar/__snapshots__/SidebarBody.test.tsx.snap +++ b/src/sidebar/__snapshots__/SidebarBody.test.tsx.snap @@ -5,26 +5,6 @@ exports[`SidebarBody it renders 1`] = `
-
diff --git a/src/sidebar/activateMod/ActivateModPanel.test.tsx b/src/sidebar/activateMod/ActivateModPanel.test.tsx index 349ec798e6..a05addca99 100644 --- a/src/sidebar/activateMod/ActivateModPanel.test.tsx +++ b/src/sidebar/activateMod/ActivateModPanel.test.tsx @@ -39,7 +39,7 @@ import { marketplaceListingFactory, modDefinitionToMarketplacePackage, } from "@/testUtils/factories/marketplaceFactories"; -import { hideSelf as hideSidebar } from "@/sidebar/sidePanel"; +import * as sidePanel from "@/sidebar/sidePanel"; import ActivateMultipleModsPanel from "@/sidebar/activateMod/ActivateMultipleModsPanel"; import ErrorBoundary from "@/sidebar/SidebarErrorBoundary"; import { includesQuickBarStarterBrick } from "@/starterBricks/starterBrickModUtils"; @@ -68,7 +68,7 @@ const checkModDefinitionPermissionsMock = jest.mocked( checkModDefinitionPermissions, ); -const hideSidebarSpy = jest.mocked(hideSidebar); +const hideSidebarSpy = jest.spyOn(sidePanel, "hideSelf"); jest.mock("@/starterBricks/starterBrickModUtils", () => { const actualUtils = jest.requireActual( diff --git a/src/sidebar/sidePanel/messenger/api.ts b/src/sidebar/sidePanel/messenger/api.ts index ab3181de33..73fc63609d 100644 --- a/src/sidebar/sidePanel/messenger/api.ts +++ b/src/sidebar/sidePanel/messenger/api.ts @@ -105,7 +105,7 @@ export async function hideSidePanel(tabId: number) { }); } -export function onSidePanelClosure(controller?: AbortController): void { +export function onSidePanelClosure(controller: AbortController): void { expectContext("contentScript"); const getDifference = () => window.outerWidth - window.innerWidth; window.addEventListener( diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index 9ecf4e4c65..ca47609541 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -36,7 +36,6 @@ "./background/dataStore.ts", "./background/externalProtocol.ts", "./background/partnerTheme.ts", - "./background/webextAlert.ts", "./bricks/available.ts", "./bricks/effects/cancel.ts", "./bricks/effects/clipboard.ts", From 44bca81f31af42bbc1d68680147b6b6921719cd8 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Sun, 7 Jan 2024 15:28:22 +0800 Subject: [PATCH 19/29] Drop redundant ensureSidebar --- src/bricks/transformers/ephemeralForm/formTransformer.ts | 4 ++-- .../transformers/temporaryInfo/DisplayTemporaryInfo.test.ts | 2 +- .../transformers/temporaryInfo/DisplayTemporaryInfo.ts | 4 ++-- src/contentScript/loadActivationEnhancementsCore.test.ts | 2 +- src/contentScript/pageEditor/dynamic.ts | 4 ++-- src/contentScript/sidebarActivation.ts | 4 ++-- src/contentScript/sidebarController.tsx | 6 ------ src/sidebar/sidePanel/messenger/api.ts | 1 + 8 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/bricks/transformers/ephemeralForm/formTransformer.ts b/src/bricks/transformers/ephemeralForm/formTransformer.ts index 97dc90785c..ef20d22a10 100644 --- a/src/bricks/transformers/ephemeralForm/formTransformer.ts +++ b/src/bricks/transformers/ephemeralForm/formTransformer.ts @@ -25,7 +25,7 @@ import { } from "@/contentScript/ephemeralFormProtocol"; import { expectContext } from "@/utils/expectContext"; import { - ensureSidebar, + showSidebar, hideSidebarForm, showSidebarForm, } from "@/contentScript/sidebarController"; @@ -157,7 +157,7 @@ export class FormTransformer extends TransformerABC { if (location === "sidebar") { // Ensure the sidebar is visible (which may also be showing persistent panels) - await ensureSidebar(); + await showSidebar(); await showSidebarForm({ extensionId: logger.context.extensionId, diff --git a/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.test.ts b/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.test.ts index f667a0e708..4b7a63f459 100644 --- a/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.test.ts +++ b/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.test.ts @@ -67,7 +67,7 @@ jest.mock("@/bricks/transformers/ephemeralForm/modalUtils", () => ({ const showModalMock = jest.mocked(showModal); jest.mock("@/contentScript/sidebarController", () => ({ - ensureSidebar: jest.fn(), + showSidebar: jest.fn(), showTemporarySidebarPanel: jest.fn(), updateTemporarySidebarPanel: jest.fn(), })); diff --git a/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts b/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts index a8e02a42b6..f5ee0d0b41 100644 --- a/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts +++ b/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts @@ -23,7 +23,7 @@ import { } from "@/types/runtimeTypes"; import { expectContext } from "@/utils/expectContext"; import { - ensureSidebar, + showSidebar, hideTemporarySidebarPanel, showTemporarySidebarPanel, updateTemporarySidebarPanel, @@ -176,7 +176,7 @@ export async function displayTemporaryInfo({ extensionId: panelEntryMetadata.extensionId, }); - await ensureSidebar(); + await showSidebar(); // Show loading await showTemporarySidebarPanel({ diff --git a/src/contentScript/loadActivationEnhancementsCore.test.ts b/src/contentScript/loadActivationEnhancementsCore.test.ts index aee8ffaa37..750e45a8b7 100644 --- a/src/contentScript/loadActivationEnhancementsCore.test.ts +++ b/src/contentScript/loadActivationEnhancementsCore.test.ts @@ -37,7 +37,7 @@ import { array } from "cooky-cutter"; import { MARKETPLACE_URL } from "@/urlConstants"; jest.mock("@/contentScript/sidebarController", () => ({ - ensureSidebar: jest.fn(), + showSidebar: jest.fn(), showModActivationInSidebar: jest.fn(), hideModActivationInSidebar: jest.fn(), })); diff --git a/src/contentScript/pageEditor/dynamic.ts b/src/contentScript/pageEditor/dynamic.ts index d6974d1dd1..5f53cd6228 100644 --- a/src/contentScript/pageEditor/dynamic.ts +++ b/src/contentScript/pageEditor/dynamic.ts @@ -27,7 +27,7 @@ import { type TriggerDefinition } from "@/starterBricks/triggerExtension"; import type { DynamicDefinition } from "@/contentScript/pageEditor/types"; import { activateExtensionPanel, - ensureSidebar, + showSidebar, } from "@/contentScript/sidebarController"; import { type TourDefinition } from "@/starterBricks/tourExtension"; import { type JsonObject } from "type-fest"; @@ -132,7 +132,7 @@ export async function updateDynamicElement({ await runEditorExtension(extensionConfig.id, starterBrick); if (starterBrick.kind === "actionPanel") { - await ensureSidebar(); + await showSidebar(); await activateExtensionPanel(extensionConfig.id); } } diff --git a/src/contentScript/sidebarActivation.ts b/src/contentScript/sidebarActivation.ts index e9850a40c1..936b5fa878 100644 --- a/src/contentScript/sidebarActivation.ts +++ b/src/contentScript/sidebarActivation.ts @@ -18,7 +18,7 @@ import { type RegistryId } from "@/types/registryTypes"; import { isRegistryId } from "@/types/helpers"; import { - ensureSidebar, + showSidebar, hideModActivationInSidebar, showModActivationInSidebar, } from "@/contentScript/sidebarController"; @@ -57,7 +57,7 @@ async function showSidebarActivationForMods( ): Promise { const controller = new AbortController(); - await ensureSidebar(); + await showSidebar(); await showModActivationInSidebar({ modIds, heading: "Activating", diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index 35271e1084..8c25e365ea 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -91,12 +91,6 @@ export async function activateExtensionPanel(extensionId: UUID): Promise { }); } -/** - * Awaitable version of showSidebar which does not reload existing panels if the sidebar is already visible - * @see showSidebar - */ -export const ensureSidebar = showSidebar; // TODO: Verify behavior match - export async function rehydrateSidebar( activateOptions: ActivatePanelOptions = {}, ): Promise { diff --git a/src/sidebar/sidePanel/messenger/api.ts b/src/sidebar/sidePanel/messenger/api.ts index 73fc63609d..bdd7f64bc3 100644 --- a/src/sidebar/sidePanel/messenger/api.ts +++ b/src/sidebar/sidePanel/messenger/api.ts @@ -47,6 +47,7 @@ export function getAssociatedTarget(): Target { const PING_MESSAGE = "PING_SIDE_PANEL"; // Do not use the messenger because it doesn't support retry-less messaging +// TODO: Drop after https://github.com/pixiebrix/webext-messenger/issues/59 export function initSidePanelPingResponder() { expectContext("sidebar"); chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { From 286f639e0c1cab9196dcb61c7e972ea1206aa6d2 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Sun, 7 Jan 2024 15:58:29 +0800 Subject: [PATCH 20/29] Drop PANEL_FRAME_ID and fix most tests --- src/domConstants.ts | 3 --- src/sidebar/activateMod/ActivateModPanel.test.tsx | 2 ++ src/starterBricks/sidebarExtension.test.ts | 11 ++++++++--- src/utils/inference/markupInference.ts | 3 --- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/domConstants.ts b/src/domConstants.ts index 9f3e34ab54..cea8a1b4a4 100644 --- a/src/domConstants.ts +++ b/src/domConstants.ts @@ -23,8 +23,6 @@ export const MAX_Z_INDEX = NOTIFICATIONS_Z_INDEX - 1; // Let notifications alway export const CONTENT_SCRIPT_READY_ATTRIBUTE = "data-pb-ready"; -export const PANEL_FRAME_ID = "pixiebrix-extension"; - export const PIXIEBRIX_DATA_ATTR = "data-pb-uuid"; export const PIXIEBRIX_QUICK_BAR_CONTAINER_ID = "pixiebrix-quickbar-container"; @@ -40,7 +38,6 @@ export const EXTENSION_POINT_DATA_ATTR = "data-pb-extension-point"; */ // When adding additional properties, be sure to make sure they're compatible with :not export const PRIVATE_ATTRIBUTES_SELECTOR = ` - #${PANEL_FRAME_ID}, #${PIXIEBRIX_QUICK_BAR_CONTAINER_ID}, [${PIXIEBRIX_DATA_ATTR}], [${CONTENT_SCRIPT_READY_ATTRIBUTE}], diff --git a/src/sidebar/activateMod/ActivateModPanel.test.tsx b/src/sidebar/activateMod/ActivateModPanel.test.tsx index a05addca99..d58d09f673 100644 --- a/src/sidebar/activateMod/ActivateModPanel.test.tsx +++ b/src/sidebar/activateMod/ActivateModPanel.test.tsx @@ -68,6 +68,8 @@ const checkModDefinitionPermissionsMock = jest.mocked( checkModDefinitionPermissions, ); +jest.mock("@/sidebar/sidePanel"); + const hideSidebarSpy = jest.spyOn(sidePanel, "hideSelf"); jest.mock("@/starterBricks/starterBrickModUtils", () => { diff --git a/src/starterBricks/sidebarExtension.test.ts b/src/starterBricks/sidebarExtension.test.ts index 22e35a7a3a..87bcd20568 100644 --- a/src/starterBricks/sidebarExtension.test.ts +++ b/src/starterBricks/sidebarExtension.test.ts @@ -39,9 +39,13 @@ import { } from "@/contentScript/sidebarController"; import { setPageState } from "@/contentScript/pageState"; import { modMetadataFactory } from "@/testUtils/factories/modComponentFactories"; -import { PANEL_FRAME_ID } from "@/domConstants"; import brickRegistry from "@/bricks/registry"; import { sleep } from "@/utils/timeUtils"; +import { isSidePanelOpen } from "@/sidebar/sidePanel/messenger/api"; + +jest.mock("@/sidebar/sidePanel/messenger/api"); + +const isSidePanelOpenMock = jest.mocked(isSidePanelOpen); const rootReader = new RootReader(); @@ -188,7 +192,8 @@ describe("sidebarExtension", () => { expect(rootReader.readCount).toBe(0); // Fake the sidebar being added to the page - $(document.body).append(`
`); + isSidePanelOpenMock.mockResolvedValueOnce(true); + expect(isSidePanelOpenMock).toHaveBeenCalledTimes(1); sidebarShowEvents.emit({ reason: RunReason.MANUAL }); await tick(); @@ -249,7 +254,7 @@ describe("sidebarExtension", () => { await extensionPoint.install(); // Fake the sidebar being added to the page - $(document.body).append(`
`); + isSidePanelOpenMock.mockResolvedValueOnce(true); sidebarShowEvents.emit({ reason: RunReason.MANUAL }); await tick(); diff --git a/src/utils/inference/markupInference.ts b/src/utils/inference/markupInference.ts index c2c7bc269a..7108d98e25 100644 --- a/src/utils/inference/markupInference.ts +++ b/src/utils/inference/markupInference.ts @@ -18,7 +18,6 @@ import { CONTENT_SCRIPT_READY_ATTRIBUTE, EXTENSION_POINT_DATA_ATTR, - PANEL_FRAME_ID, PIXIEBRIX_DATA_ATTR, } from "@/domConstants"; import { BUTTON_TAGS, UNIQUE_ATTRIBUTES } from "./selectorInference"; @@ -74,8 +73,6 @@ const TEMPLATE_ATTR_EXCLUDE_PATTERNS = [ ]; const TEMPLATE_VALUE_EXCLUDE_PATTERNS = new Map([ ["class", [/^ember-view$/]], - // eslint-disable-next-line security/detect-non-literal-regexp -- Our variables - ["id", [new RegExp(`^${PANEL_FRAME_ID}$`)]], ]); class SkipElement extends Error { From e2c040ee2b86b344dac9aee969f2c59890841dc0 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Mon, 8 Jan 2024 00:29:37 +0800 Subject: [PATCH 21/29] Drop test expectation --- src/starterBricks/sidebarExtension.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/starterBricks/sidebarExtension.test.ts b/src/starterBricks/sidebarExtension.test.ts index 87bcd20568..b855b7683c 100644 --- a/src/starterBricks/sidebarExtension.test.ts +++ b/src/starterBricks/sidebarExtension.test.ts @@ -193,7 +193,6 @@ describe("sidebarExtension", () => { // Fake the sidebar being added to the page isSidePanelOpenMock.mockResolvedValueOnce(true); - expect(isSidePanelOpenMock).toHaveBeenCalledTimes(1); sidebarShowEvents.emit({ reason: RunReason.MANUAL }); await tick(); From 2ccc8d04e62159406f5becf5c68d6b838e8ca5f4 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Mon, 8 Jan 2024 02:34:17 +0800 Subject: [PATCH 22/29] isSidePanelOpen optimization --- src/sidebar/sidePanel/messenger/api.ts | 33 +++++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/sidebar/sidePanel/messenger/api.ts b/src/sidebar/sidePanel/messenger/api.ts index bdd7f64bc3..cdeba76d48 100644 --- a/src/sidebar/sidePanel/messenger/api.ts +++ b/src/sidebar/sidePanel/messenger/api.ts @@ -27,14 +27,10 @@ import { DISPLAY_REASON_RESTRICTED_URL, } from "@/tinyPages/restrictedUrlPopupConstants"; import { isScriptableUrl } from "webext-content-scripts"; - import { isObject } from "@/utils/objectUtils"; import { expectContext } from "@/utils/expectContext"; import { type Target } from "webext-messenger"; -// Approximate sidebar width in pixels. Used to determine whether it's open -const MINIMUM_SIDEBAR_WIDTH = 300; - export function getAssociatedTabId(): number { expectContext("sidebar"); const tabId = new URLSearchParams(window.location.search).get("tabId"); @@ -62,6 +58,11 @@ export function initSidePanelPingResponder() { } export async function isSidePanelOpen(): Promise { + // Sync check where possible + if (isSidePanelOpenSync() === false) { + return false; + } + const response = await chrome.runtime.sendMessage< { type: string }, boolean | undefined @@ -106,13 +107,33 @@ export async function hideSidePanel(tabId: number) { }); } +// Approximate sidebar width in pixels. Used to determine whether it's open +const MINIMUM_SIDEBAR_WIDTH = 300; + +/** + * Determines whether the sidebar is open. + * @returns false when it's definitely closed + * @returns 'unknown' when it cannot be determined + */ +// The type cannot be `undefined` due to strictNullChecks +function isSidePanelOpenSync(): false | "unknown" { + if (!globalThis.window) { + return "unknown"; + } + + return window.outerWidth - window.innerWidth > MINIMUM_SIDEBAR_WIDTH + ? "unknown" + : false; +} + +// TODO: It doesn't work when the dev tools are open on the side +// Official event requested in https://github.com/w3c/webextensions/issues/517 export function onSidePanelClosure(controller: AbortController): void { expectContext("contentScript"); - const getDifference = () => window.outerWidth - window.innerWidth; window.addEventListener( "resize", () => { - if (getDifference() < MINIMUM_SIDEBAR_WIDTH) { + if (isSidePanelOpenSync() === false) { controller.abort(); } }, From 7c321b8b75c24755acdd77b4ca3f888410c56873 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Mon, 8 Jan 2024 03:08:26 +0800 Subject: [PATCH 23/29] Fix `SidebarErrorBoundary` reload logic --- src/sidebar/SidebarErrorBoundary.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/sidebar/SidebarErrorBoundary.tsx b/src/sidebar/SidebarErrorBoundary.tsx index e76d2d7a24..9a7f3f8115 100644 --- a/src/sidebar/SidebarErrorBoundary.tsx +++ b/src/sidebar/SidebarErrorBoundary.tsx @@ -20,15 +20,9 @@ import { isEmpty } from "lodash"; import { faRedo } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Alert, Button } from "react-bootstrap"; -import sidebarInThisTab from "@/sidebar/messenger/api"; import ErrorBoundary from "@/components/ErrorBoundary"; class SidebarErrorBoundary extends ErrorBoundary { - async reloadSidebar() { - sidebarInThisTab.reload(); - // FIXME: Should this also wait for the sidebar to finish loading? - } - override render(): React.ReactNode { if (this.state.hasError) { return ( @@ -44,7 +38,12 @@ class SidebarErrorBoundary extends ErrorBoundary {

Please close and re-open the sidebar panel.

-
From f1dde6e0cb776841d049210008815d5228d58200 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Mon, 8 Jan 2024 15:49:54 +0800 Subject: [PATCH 24/29] sidePanel init cleanup --- src/bricks/effects/sidebar.ts | 4 ++-- src/contentScript/messenger/api.ts | 2 +- src/contentScript/messenger/registration.ts | 6 +++--- src/contentScript/sidebarController.tsx | 2 +- src/sidebar/sidePanel.tsx | 11 +++++++++++ src/sidebar/sidePanel/messenger/api.ts | 2 +- src/sidebar/sidebar.tsx | 9 ++------- 7 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/bricks/effects/sidebar.ts b/src/bricks/effects/sidebar.ts index fa57ff9bff..4fcc5714f0 100644 --- a/src/bricks/effects/sidebar.ts +++ b/src/bricks/effects/sidebar.ts @@ -18,7 +18,7 @@ import { EffectABC } from "@/types/bricks/effectTypes"; import { type BrickArgs, type BrickOptions } from "@/types/runtimeTypes"; import { type Schema, SCHEMA_EMPTY_OBJECT } from "@/types/schemaTypes"; -import { rehydrateSidebar } from "@/contentScript/sidebarController"; +import { updateSidebar } from "@/contentScript/sidebarController"; import { hideMySidePanel, showMySidePanel } from "@/background/messenger/api"; import { propertiesToSchema } from "@/validators/generic"; import { logPromiseDuration } from "@/utils/promiseUtils"; @@ -67,7 +67,7 @@ export class ShowSidebar extends EffectABC { await showMySidePanel(); void logPromiseDuration( "ShowSidebar:showSidebar", - rehydrateSidebar({ + updateSidebar({ force: forcePanel, panelHeading, blueprintId: logger.context.blueprintId, diff --git a/src/contentScript/messenger/api.ts b/src/contentScript/messenger/api.ts index 978192a52b..7bd0b76af6 100644 --- a/src/contentScript/messenger/api.ts +++ b/src/contentScript/messenger/api.ts @@ -41,7 +41,7 @@ export const removeInstalledExtension = getNotifier( export const resetTab = getNotifier("RESET_TAB"); export const toggleQuickBar = getMethod("TOGGLE_QUICK_BAR"); export const handleMenuAction = getMethod("HANDLE_MENU_ACTION"); -export const rehydrateSidebar = getMethod("REHYDRATE_SIDEBAR"); +export const updateSidebar = getNotifier("UPDATE_SIDEBAR"); export const getReservedSidebarEntries = getMethod( "GET_RESERVED_SIDEBAR_ENTRIES", diff --git a/src/contentScript/messenger/registration.ts b/src/contentScript/messenger/registration.ts index f90aa218e8..1bb5371006 100644 --- a/src/contentScript/messenger/registration.ts +++ b/src/contentScript/messenger/registration.ts @@ -32,7 +32,7 @@ import { cancelForm, } from "@/contentScript/ephemeralFormProtocol"; import { - rehydrateSidebar, + updateSidebar, removeExtensions as removeSidebars, getReservedPanelEntries, } from "@/contentScript/sidebarController"; @@ -100,7 +100,7 @@ declare global { TOGGLE_QUICK_BAR: typeof toggleQuickBar; HANDLE_MENU_ACTION: typeof handleMenuAction; - REHYDRATE_SIDEBAR: typeof rehydrateSidebar; + UPDATE_SIDEBAR: typeof updateSidebar; GET_RESERVED_SIDEBAR_ENTRIES: typeof getReservedPanelEntries; REMOVE_SIDEBARS: typeof removeSidebars; @@ -166,7 +166,7 @@ export default function registerMessenger(): void { TOGGLE_QUICK_BAR: toggleQuickBar, HANDLE_MENU_ACTION: handleMenuAction, - REHYDRATE_SIDEBAR: rehydrateSidebar, + UPDATE_SIDEBAR: updateSidebar, REMOVE_SIDEBARS: removeSidebars, INSERT_PANEL: insertPanel, diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index 8c25e365ea..1b85e54889 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -91,7 +91,7 @@ export async function activateExtensionPanel(extensionId: UUID): Promise { }); } -export async function rehydrateSidebar( +export async function updateSidebar( activateOptions: ActivatePanelOptions = {}, ): Promise { try { diff --git a/src/sidebar/sidePanel.tsx b/src/sidebar/sidePanel.tsx index 3c35328270..fa506d8e38 100644 --- a/src/sidebar/sidePanel.tsx +++ b/src/sidebar/sidePanel.tsx @@ -20,11 +20,22 @@ import { expectContext } from "@/utils/expectContext"; import { getAssociatedTabId, + getAssociatedTarget, hideSidePanel, + respondToPings, } from "@/sidebar/sidePanel/messenger/api"; +import { updateSidebar } from "@/contentScript/messenger/api"; expectContext("sidebar"); export async function hideSelf() { return hideSidePanel(getAssociatedTabId()); } + +export function initSidePanel() { + respondToPings(); + + // The sidePanel can load independently of the content script, so it should + // automatically fetch the data from the content script when it loads. + updateSidebar(getAssociatedTarget()); +} diff --git a/src/sidebar/sidePanel/messenger/api.ts b/src/sidebar/sidePanel/messenger/api.ts index cdeba76d48..1f8aa55e85 100644 --- a/src/sidebar/sidePanel/messenger/api.ts +++ b/src/sidebar/sidePanel/messenger/api.ts @@ -44,7 +44,7 @@ export function getAssociatedTarget(): Target { const PING_MESSAGE = "PING_SIDE_PANEL"; // Do not use the messenger because it doesn't support retry-less messaging // TODO: Drop after https://github.com/pixiebrix/webext-messenger/issues/59 -export function initSidePanelPingResponder() { +export function respondToPings() { expectContext("sidebar"); chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if ( diff --git a/src/sidebar/sidebar.tsx b/src/sidebar/sidebar.tsx index 4bfea25d1a..2db6f877ce 100644 --- a/src/sidebar/sidebar.tsx +++ b/src/sidebar/sidebar.tsx @@ -34,8 +34,7 @@ import { initToaster } from "@/utils/notify"; import { initRuntimeLogging } from "@/development/runtimeLogging"; import { initCopilotMessenger } from "@/contrib/automationanywhere/aaFrameProtocol"; import { initPerformanceMonitoring } from "@/telemetry/performance"; -import { initSidePanelPingResponder } from "./sidePanel/messenger/api"; -import { rehydrateSidebar } from "@/contentScript/sidebarController"; +import { initSidePanel } from "./sidePanel"; function init(): void { ReactDOM.render(, document.querySelector("#container")); @@ -49,11 +48,7 @@ registerContribBlocks(); registerBuiltinBricks(); initToaster(); init(); -initSidePanelPingResponder(); - -// The sidePanel is that one that requests data from the content script -// FIXME: This should be moved elsewhere, because it also needs to listen to URL changes in the connected page -void rehydrateSidebar(); +void initSidePanel(); // Handle an embedded AA business copilot frame void initCopilotMessenger(); From c61abc1b61d4b332feef924e705a6b6fe02148c4 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Mon, 8 Jan 2024 16:38:10 +0800 Subject: [PATCH 25/29] pingSidebar improvements --- src/contentScript/sidebarController.tsx | 30 +++++++++++++++---------- src/sidebar/messenger/api.ts | 1 + 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index 1b85e54889..a37f0ba5a2 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -19,7 +19,7 @@ import reportEvent from "@/telemetry/reportEvent"; import { Events } from "@/telemetry/events"; import { expectContext } from "@/utils/expectContext"; import sidebarInThisTab from "@/sidebar/messenger/api"; -import { isEmpty } from "lodash"; +import { isEmpty, throttle } from "lodash"; import { SimpleEventTarget } from "@/utils/SimpleEventTarget"; import { type Except } from "type-fest"; import { type RunArgs } from "@/types/runtimeTypes"; @@ -38,6 +38,21 @@ import { getTemporaryPanelSidebarEntries } from "@/bricks/transformers/temporary import { getFormPanelSidebarEntries } from "@/contentScript/ephemeralFormProtocol"; import { isSidePanelOpen } from "@/sidebar/sidePanel/messenger/api"; import { backgroundTarget, getMethod } from "webext-messenger"; +import { memoizeUntilSettled } from "@/utils/promiseUtils"; + +// - Only start one ping at a time +// - Limit to one request every second (if the user closes the sidebar that quickly, we likely see those errors anyway) +// - Throw custom error if the sidebar doesn't respond in time +export const pingSidebar = memoizeUntilSettled( + throttle(async () => { + try { + await sidebarInThisTab.pingSidebar(); + } catch (error) { + // TODO: Use TimeoutError after https://github.com/sindresorhus/p-timeout/issues/41 + throw new Error("The sidebar did not respond in time", { cause: error }); + } + }, 1000), +); /** * Sequence number for ensuring render requests are handled in order @@ -63,12 +78,7 @@ export async function showSidebar(): Promise { reportEvent(Events.SIDEBAR_SHOW); // TODO: Import from background/messenger/api.ts after the strictNullChecks migration, drop "SIDEBAR_PING" string await getMethod("SHOW_MY_SIDE_PANEL" as "SIDEBAR_PING", backgroundTarget)(); - - try { - await sidebarInThisTab.pingSidebar(); - } catch (error) { - throw new Error("The sidebar did not respond in time", { cause: error }); - } + await pingSidebar(); } /** @@ -94,11 +104,7 @@ export async function activateExtensionPanel(extensionId: UUID): Promise { export async function updateSidebar( activateOptions: ActivatePanelOptions = {}, ): Promise { - try { - await sidebarInThisTab.pingSidebar(); - } catch (error) { - throw new Error("The sidebar did not respond in time", { cause: error }); - } + await pingSidebar(); if (!isEmpty(activateOptions)) { const seqNum = renderSequenceNumber; diff --git a/src/sidebar/messenger/api.ts b/src/sidebar/messenger/api.ts index aef431e1d6..1617911e96 100644 --- a/src/sidebar/messenger/api.ts +++ b/src/sidebar/messenger/api.ts @@ -37,6 +37,7 @@ const sidebarInThisTab = { activatePanel: getMethod("SIDEBAR_ACTIVATE_PANEL", target), showForm: getMethod("SIDEBAR_SHOW_FORM", target), hideForm: getMethod("SIDEBAR_HIDE_FORM", target), + /** @deprecated Instead: import {pingSidebar} from '@/contentScript/sidebarController'; */ pingSidebar: getMethod("SIDEBAR_PING", target), reload: getNotifier("SIDEBAR_RELOAD", target), showTemporaryPanel: getMethod("SIDEBAR_SHOW_TEMPORARY_PANEL", target), From 0210732068f4d837e7cefde5b189a5397d9190df Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Mon, 8 Jan 2024 17:06:45 +0800 Subject: [PATCH 26/29] Fix lodash.throttle bad types --- src/contentScript/sidebarController.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index a37f0ba5a2..65260058ae 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -51,7 +51,7 @@ export const pingSidebar = memoizeUntilSettled( // TODO: Use TimeoutError after https://github.com/sindresorhus/p-timeout/issues/41 throw new Error("The sidebar did not respond in time", { cause: error }); } - }, 1000), + }, 1000) as () => Promise, ); /** From e0a7a475af62bc50eabdcc49200f5452834172f5 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Mon, 8 Jan 2024 23:50:59 +0800 Subject: [PATCH 27/29] s/getTopLevelFrame/getAssociatedTarget/ --- src/bricks/renderers/customForm.ts | 6 +++--- src/bricks/transformers/brickFactory.ts | 6 +++++- src/components/documentBuilder/render/BlockElement.tsx | 4 ++-- .../documentBuilder/render/ButtonElement.tsx | 4 ++-- src/components/documentBuilder/render/ListElement.tsx | 4 ++-- src/sidebar/PanelBody.tsx | 4 ++-- src/sidebar/Tabs.tsx | 4 ++-- src/sidebar/activateMod/ActivateModPanel.tsx | 4 ++-- src/sidebar/modLauncher/ModLauncher.tsx | 4 ++-- src/sidebar/sidebarSlice.ts | 10 +++++----- src/utils/expectContext.ts | 2 +- 11 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/bricks/renderers/customForm.ts b/src/bricks/renderers/customForm.ts index 9dd60ab184..5191a9fb8e 100644 --- a/src/bricks/renderers/customForm.ts +++ b/src/bricks/renderers/customForm.ts @@ -25,7 +25,7 @@ import { validateRegistryId } from "@/types/helpers"; import { BusinessError, PropError } from "@/errors/businessErrors"; import { getPageState, setPageState } from "@/contentScript/messenger/api"; import { isEmpty, isPlainObject, set } from "lodash"; -import { getTopLevelFrame } from "webext-messenger"; +import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; import { type UUID } from "@/types/stringTypes"; import { type SanitizedIntegrationConfig } from "@/integrations/integrationTypes"; import { @@ -341,7 +341,7 @@ async function getInitialData( case "state": { const namespace = storage.namespace ?? "blueprint"; - const topLevelFrame = await getTopLevelFrame(); + const topLevelFrame = getAssociatedTarget(); // Target the top level frame. Inline panels aren't generally available, so the renderer will always be in the // sidebar which runs in the context of the top-level frame return getPageState(topLevelFrame, { @@ -411,7 +411,7 @@ async function setData( } case "state": { - const topLevelFrame = await getTopLevelFrame(); + const topLevelFrame = getAssociatedTarget(); // Target the top level frame. Inline panels aren't generally available, so the renderer will always be in the // sidebar which runs in the context of the top-level frame await setPageState(topLevelFrame, { diff --git a/src/bricks/transformers/brickFactory.ts b/src/bricks/transformers/brickFactory.ts index f89b921c26..eef5c6822d 100644 --- a/src/bricks/transformers/brickFactory.ts +++ b/src/bricks/transformers/brickFactory.ts @@ -50,6 +50,7 @@ import { import { type UnknownObject } from "@/types/objectTypes"; import { isPipelineExpression } from "@/utils/expressionUtils"; import { isContentScript } from "webext-detect-page"; +import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; import { getTopLevelFrame } from "webext-messenger"; import { uuidv4 } from "@/types/helpers"; import { isSpecificError } from "@/errors/errorHelpers"; @@ -61,6 +62,7 @@ import { unionSchemaDefinitionTypes, } from "@/utils/schemaUtils"; import type BaseRegistry from "@/registry/memoryRegistry"; +import { isBrowserSidebar } from "@/utils/expectContext"; // Interface to avoid circular dependency with the implementation type BrickRegistryProtocol = BaseRegistry; @@ -343,7 +345,9 @@ class UserDefinedBrick extends BrickABC { // Components which can't be serialized across messenger boundaries. // TODO: call top-level contentScript directly after https://github.com/pixiebrix/webext-messenger/issues/72 - const topLevelFrame = await getTopLevelFrame(); + const topLevelFrame = isBrowserSidebar() + ? getAssociatedTarget() + : await getTopLevelFrame(); try { return await runHeadlessPipeline(topLevelFrame, { diff --git a/src/components/documentBuilder/render/BlockElement.tsx b/src/components/documentBuilder/render/BlockElement.tsx index 232fb6c118..1ffb60c6d7 100644 --- a/src/components/documentBuilder/render/BlockElement.tsx +++ b/src/components/documentBuilder/render/BlockElement.tsx @@ -26,7 +26,7 @@ import apiVersionOptions from "@/runtime/apiVersionOptions"; import { serializeError } from "serialize-error"; import { type DynamicPath } from "@/components/documentBuilder/documentBuilderTypes"; import { mapPathToTraceBranches } from "@/components/documentBuilder/utils"; -import { getTopLevelFrame } from "webext-messenger"; +import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; import { type PanelContext } from "@/types/sidebarTypes"; import { type RendererRunPayload } from "@/types/rendererTypes"; import useAsyncState from "@/hooks/useAsyncState"; @@ -54,7 +54,7 @@ const BlockElement: React.FC = ({ pipeline, tracePath }) => { error, } = useAsyncState(async () => { // We currently only support associating the sidebar with the content script in the top-level frame (frameId: 0) - const topLevelFrame = await getTopLevelFrame(); + const topLevelFrame = getAssociatedTarget(); return runRendererPipeline(topLevelFrame, { nonce: uuidv4(), diff --git a/src/components/documentBuilder/render/ButtonElement.tsx b/src/components/documentBuilder/render/ButtonElement.tsx index bb822c3f2f..d58b69b01a 100644 --- a/src/components/documentBuilder/render/ButtonElement.tsx +++ b/src/components/documentBuilder/render/ButtonElement.tsx @@ -24,7 +24,7 @@ import DocumentContext from "@/components/documentBuilder/render/DocumentContext import { type Except } from "type-fest"; import apiVersionOptions from "@/runtime/apiVersionOptions"; import { type DynamicPath } from "@/components/documentBuilder/documentBuilderTypes"; -import { getTopLevelFrame } from "webext-messenger"; +import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; import { getRootCause, hasSpecificErrorCause } from "@/errors/errorHelpers"; import { SubmitPanelAction } from "@/bricks/errors"; import cx from "classnames"; @@ -65,7 +65,7 @@ const ButtonElement: React.FC = ({ setCounter((previous) => previous + 1); // We currently only support associating the sidebar with the content script in the top-level frame (frameId: 0) - const topLevelFrame = await getTopLevelFrame(); + const topLevelFrame = getAssociatedTarget(); try { await runHeadlessPipeline(topLevelFrame, { diff --git a/src/components/documentBuilder/render/ListElement.tsx b/src/components/documentBuilder/render/ListElement.tsx index 84062de880..a124b46326 100644 --- a/src/components/documentBuilder/render/ListElement.tsx +++ b/src/components/documentBuilder/render/ListElement.tsx @@ -30,7 +30,7 @@ import ErrorBoundary from "@/components/ErrorBoundary"; import { getErrorMessage } from "@/errors/errorHelpers"; import { runMapArgs } from "@/contentScript/messenger/api"; import apiVersionOptions from "@/runtime/apiVersionOptions"; -import { getTopLevelFrame } from "webext-messenger"; +import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; import useAsyncState from "@/hooks/useAsyncState"; import DelayedRender from "@/components/DelayedRender"; import { isDeferExpression } from "@/utils/expressionUtils"; @@ -66,7 +66,7 @@ const ListElementInternal: React.FC = ({ isLoading, error, } = useAsyncState(async () => { - const topLevelFrame = await getTopLevelFrame(); + const topLevelFrame = getAssociatedTarget(); const elementVariableReference = `@${elementKey}`; diff --git a/src/sidebar/PanelBody.tsx b/src/sidebar/PanelBody.tsx index 709135d716..d8e0d8ddc0 100644 --- a/src/sidebar/PanelBody.tsx +++ b/src/sidebar/PanelBody.tsx @@ -46,7 +46,7 @@ import DelayedRender from "@/components/DelayedRender"; import { runHeadlessPipeline } from "@/contentScript/messenger/api"; import { uuidv4 } from "@/types/helpers"; import apiVersionOptions from "@/runtime/apiVersionOptions"; -import { getTopLevelFrame } from "webext-messenger"; +import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; import { type DynamicPath } from "@/components/documentBuilder/documentBuilderTypes"; import { mapPathToTraceBranches } from "@/components/documentBuilder/utils"; @@ -205,7 +205,7 @@ const PanelBody: React.FunctionComponent<{ ); } - const topLevelFrame = await getTopLevelFrame(); + const topLevelFrame = getAssociatedTarget(); await runHeadlessPipeline(topLevelFrame, { nonce: uuidv4(), diff --git a/src/sidebar/Tabs.tsx b/src/sidebar/Tabs.tsx index 66b96edefd..074450cbff 100644 --- a/src/sidebar/Tabs.tsx +++ b/src/sidebar/Tabs.tsx @@ -57,7 +57,7 @@ import { selectEventData } from "@/telemetry/deployments"; import ErrorBoundary from "@/sidebar/SidebarErrorBoundary"; import { TemporaryPanelTabPane } from "./TemporaryPanelTabPane"; import { MOD_LAUNCHER } from "@/sidebar/modLauncher/constants"; -import { getTopLevelFrame } from "webext-messenger"; +import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; import { cancelForm } from "@/contentScript/messenger/api"; import { useHideEmptySidebar } from "@/sidebar/useHideEmptySidebar"; @@ -167,7 +167,7 @@ const Tabs: React.FC = () => { if (isTemporaryPanelEntry(panel)) { dispatch(sidebarSlice.actions.removeTemporaryPanel(panel.nonce)); } else if (isFormPanelEntry(panel)) { - const frame = await getTopLevelFrame(); + const frame = getAssociatedTarget(); cancelForm(frame, panel.nonce); } else if (isModActivationPanelEntry(panel)) { dispatch(sidebarSlice.actions.hideModActivationPanel()); diff --git a/src/sidebar/activateMod/ActivateModPanel.tsx b/src/sidebar/activateMod/ActivateModPanel.tsx index af3984247d..9a59096649 100644 --- a/src/sidebar/activateMod/ActivateModPanel.tsx +++ b/src/sidebar/activateMod/ActivateModPanel.tsx @@ -30,7 +30,7 @@ import AsyncButton from "@/components/AsyncButton"; import { useDispatch } from "react-redux"; import sidebarSlice from "@/sidebar/sidebarSlice"; import { reloadMarketplaceEnhancements as reloadMarketplaceEnhancementsInContentScript } from "@/contentScript/messenger/api"; -import { getTopLevelFrame } from "webext-messenger"; +import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; import cx from "classnames"; import { isEmpty } from "lodash"; import ActivateModInputs from "@/sidebar/activateMod/ActivateModInputs"; @@ -109,7 +109,7 @@ const { setNeedsPermissions, activateStart, activateSuccess, activateError } = activationSlice.actions; async function reloadMarketplaceEnhancements() { - const topFrame = await getTopLevelFrame(); + const topFrame = getAssociatedTarget(); // Make sure the content script has the most recent state of the store before reloading. // Prevents race condition where the content script reloads before the store is persisted. await persistor.flush(); diff --git a/src/sidebar/modLauncher/ModLauncher.tsx b/src/sidebar/modLauncher/ModLauncher.tsx index 8f10e26466..2e7243c367 100644 --- a/src/sidebar/modLauncher/ModLauncher.tsx +++ b/src/sidebar/modLauncher/ModLauncher.tsx @@ -23,7 +23,7 @@ import useFlags from "@/hooks/useFlags"; import reportEvent from "@/telemetry/reportEvent"; import { Events } from "@/telemetry/events"; import { showWalkthroughModal } from "@/contentScript/messenger/api"; -import { getTopLevelFrame } from "webext-messenger"; +import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; const ModLauncher: React.FunctionComponent = () => { const { permit } = useFlags(); @@ -45,7 +45,7 @@ const ModLauncher: React.FunctionComponent = () => { source: "ModLauncher", }); - const frame = await getTopLevelFrame(); + const frame = getAssociatedTarget(); showWalkthroughModal(frame); }} diff --git a/src/sidebar/sidebarSlice.ts b/src/sidebar/sidebarSlice.ts index 72654ee1a5..8165ec16ae 100644 --- a/src/sidebar/sidebarSlice.ts +++ b/src/sidebar/sidebarSlice.ts @@ -35,7 +35,7 @@ import { resolveTemporaryPanel, } from "@/contentScript/messenger/api"; import { partition, remove, sortBy } from "lodash"; -import { getTopLevelFrame } from "webext-messenger"; +import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; import { type SubmitPanelAction } from "@/bricks/errors"; import { castDraft, type Draft } from "immer"; import { localStorage } from "redux-persist-webextension-storage"; @@ -126,12 +126,12 @@ function findNextActiveKey( } async function cancelPreexistingForms(forms: UUID[]): Promise { - const topLevelFrame = await getTopLevelFrame(); + const topLevelFrame = getAssociatedTarget(); cancelForm(topLevelFrame, ...forms); } async function cancelPanels(nonces: UUID[]): Promise { - const topLevelFrame = await getTopLevelFrame(); + const topLevelFrame = getAssociatedTarget(); cancelTemporaryPanel(topLevelFrame, nonces); } @@ -140,7 +140,7 @@ async function cancelPanels(nonces: UUID[]): Promise { * @param nonces panel nonces */ async function closePanels(nonces: UUID[]): Promise { - const topLevelFrame = await getTopLevelFrame(); + const topLevelFrame = getAssociatedTarget(); closeTemporaryPanel(topLevelFrame, nonces); } @@ -153,7 +153,7 @@ async function resolvePanel( nonce: UUID, action: Pick, ): Promise { - const topLevelFrame = await getTopLevelFrame(); + const topLevelFrame = getAssociatedTarget(); resolveTemporaryPanel(topLevelFrame, nonce, action); } diff --git a/src/utils/expectContext.ts b/src/utils/expectContext.ts index 076834ee6c..b6f18e3205 100644 --- a/src/utils/expectContext.ts +++ b/src/utils/expectContext.ts @@ -23,7 +23,7 @@ import { contextNames, } from "webext-detect-page"; -function isBrowserSidebar(): boolean { +export function isBrowserSidebar(): boolean { return isExtensionContext() && location.pathname === "/sidebar.html"; } From 15dd16f81bf18d3d07f6d85d0f95124d6a71c7fc Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Tue, 9 Jan 2024 19:53:19 +0800 Subject: [PATCH 28/29] Follow review Co-authored-by: Graham Langford <30706330+grahamlangford@users.noreply.github.com> --- src/bricks/effects/sidebar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bricks/effects/sidebar.ts b/src/bricks/effects/sidebar.ts index 4fcc5714f0..ca847d4724 100644 --- a/src/bricks/effects/sidebar.ts +++ b/src/bricks/effects/sidebar.ts @@ -66,7 +66,7 @@ export class ShowSidebar extends EffectABC { // not the extensionId of the extension toggling the sidebar await showMySidePanel(); void logPromiseDuration( - "ShowSidebar:showSidebar", + "ShowSidebar:updateSidebar", updateSidebar({ force: forcePanel, panelHeading, From 48d6d6e8ad2a50779d9a66e300d28beef4bfa427 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Thu, 11 Jan 2024 15:51:49 +0800 Subject: [PATCH 29/29] Lint --- src/contentScript/sidebarController.tsx | 2 +- src/sidebar/sidePanel/messenger/api.ts | 2 +- src/sidebar/sidebar.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index 65260058ae..700db736ee 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -43,7 +43,7 @@ import { memoizeUntilSettled } from "@/utils/promiseUtils"; // - Only start one ping at a time // - Limit to one request every second (if the user closes the sidebar that quickly, we likely see those errors anyway) // - Throw custom error if the sidebar doesn't respond in time -export const pingSidebar = memoizeUntilSettled( +const pingSidebar = memoizeUntilSettled( throttle(async () => { try { await sidebarInThisTab.pingSidebar(); diff --git a/src/sidebar/sidePanel/messenger/api.ts b/src/sidebar/sidePanel/messenger/api.ts index 1f8aa55e85..4f19b0ca7c 100644 --- a/src/sidebar/sidePanel/messenger/api.ts +++ b/src/sidebar/sidePanel/messenger/api.ts @@ -70,7 +70,7 @@ export async function isSidePanelOpen(): Promise { return Boolean(response); // TODO: Drop Boolean() after strictNullChecks migration } -export function getPopoverUrl(tabUrl: string | undefined): string | null { +function getPopoverUrl(tabUrl: string | undefined): string | null { const popoverUrl = browser.runtime.getURL("restrictedUrlPopup.html"); if (tabUrl?.startsWith(getExtensionConsoleUrl())) { diff --git a/src/sidebar/sidebar.tsx b/src/sidebar/sidebar.tsx index 2db6f877ce..486dee6282 100644 --- a/src/sidebar/sidebar.tsx +++ b/src/sidebar/sidebar.tsx @@ -48,7 +48,7 @@ registerContribBlocks(); registerBuiltinBricks(); initToaster(); init(); -void initSidePanel(); +initSidePanel(); // Handle an embedded AA business copilot frame void initCopilotMessenger();