From b4ee5ccc5f290ad7ee48185385afe725f1bf0231 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Wed, 17 Jan 2024 20:13:27 +0800 Subject: [PATCH 01/30] POC 3 baseline --- .eslintrc.js | 1 + scripts/__snapshots__/manifest.test.js.snap | 6 +- scripts/manifest.mjs | 10 + src/background/background.ts | 4 +- src/background/browserAction.ts | 100 +------ src/background/contextMenus.ts | 2 +- src/background/messenger/api.ts | 2 + src/background/messenger/registration.ts | 5 + src/background/sidePanel.ts | 74 ++++++ src/background/webextAlert.ts | 26 -- src/bricks/effects/sidebar.ts | 12 +- src/bricks/renderers/customForm.ts | 6 +- src/bricks/transformers/brickFactory.ts | 6 +- .../ephemeralForm/formTransformer.ts | 22 +- .../DisplayTemporaryInfo.test.ts | 2 +- .../temporaryInfo/DisplayTemporaryInfo.ts | 22 +- .../documentBuilder/render/BlockElement.tsx | 4 +- .../documentBuilder/render/ButtonElement.tsx | 4 +- .../documentBuilder/render/ListElement.tsx | 4 +- src/contentScript/contentScriptCore.ts | 6 +- src/contentScript/messenger/api.ts | 6 +- src/contentScript/messenger/registration.ts | 22 +- src/contentScript/pageEditor.ts | 2 +- src/contentScript/pageEditor/dynamic.ts | 4 +- src/contentScript/sidebarActivation.ts | 22 +- src/contentScript/sidebarController.tsx | 248 ++++++------------ src/contentScript/sidebarDomControllerLite.ts | 160 ----------- .../automationanywhere/aaFrameProtocol.ts | 5 +- src/domConstants.ts | 3 - src/pageEditor/panes/insert/useAutoInsert.ts | 8 +- .../sidebar/ActivatedModComponentListItem.tsx | 6 +- .../sidebar/DynamicModComponentListItem.tsx | 8 +- src/sidebar/ConnectedSidebar.tsx | 4 +- src/sidebar/Header.tsx | 22 +- src/sidebar/PanelBody.tsx | 4 +- src/sidebar/SidebarErrorBoundary.tsx | 14 +- src/sidebar/Tabs.test.tsx | 5 +- src/sidebar/Tabs.tsx | 4 +- .../__snapshots__/Header.test.tsx.snap | 40 --- .../__snapshots__/SidebarBody.test.tsx.snap | 20 -- .../activateMod/ActivateModPanel.test.tsx | 5 +- src/sidebar/activateMod/ActivateModPanel.tsx | 4 +- src/sidebar/messenger/api.ts | 20 +- src/sidebar/messenger/registration.ts | 5 + src/sidebar/modLauncher/ModLauncher.tsx | 4 +- src/sidebar/protocol.tsx | 4 + .../sidePanel.tsx} | 17 +- src/sidebar/sidePanel/messenger/api.ts | 115 ++++++++ src/sidebar/sidebar.tsx | 2 + src/sidebar/sidebarSlice.ts | 10 +- src/sidebar/useHideEmptySidebar.ts | 16 +- src/starterBricks/sidebarExtension.test.ts | 10 +- src/starterBricks/sidebarExtension.ts | 4 +- src/tinyPages/restrictedUrlPopup.html | 7 - src/tsconfig.strictNullChecks.json | 3 - src/utils/expectContext.ts | 2 +- src/utils/extensionUtils.ts | 20 +- src/utils/inference/markupInference.ts | 3 - src/utils/notify.tsx | 5 - webpack.config.mjs | 1 - 60 files changed, 488 insertions(+), 694 deletions(-) create mode 100644 src/background/sidePanel.ts delete mode 100644 src/background/webextAlert.ts delete mode 100644 src/contentScript/sidebarDomControllerLite.ts rename src/{contentScript/browserActionInstantHandler.ts => sidebar/sidePanel.tsx} (61%) create mode 100644 src/sidebar/sidePanel/messenger/api.ts diff --git a/.eslintrc.js b/.eslintrc.js index b0117238bf..7e9210e7ba 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/scripts/__snapshots__/manifest.test.js.snap b/scripts/__snapshots__/manifest.test.js.snap index 3408b49781..f751d699ea 100644 --- a/scripts/__snapshots__/manifest.test.js.snap +++ b/scripts/__snapshots__/manifest.test.js.snap @@ -239,7 +239,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", @@ -257,6 +257,7 @@ exports[`customizeManifest mv3 1`] = ` "contextMenus", "devtools", "scripting", + "sidePanel", ], "sandbox": { "pages": [ @@ -264,6 +265,9 @@ exports[`customizeManifest mv3 1`] = ` ], }, "short_name": "PixieBrix", + "side_panel": { + "default_path": "sidebar.html", + }, "storage": { "managed_schema": "managedStorageSchema.json", }, diff --git a/scripts/manifest.mjs b/scripts/manifest.mjs index 556fdcc572..c1c50b5d6c 100644 --- a/scripts/manifest.mjs +++ b/scripts/manifest.mjs @@ -51,6 +51,16 @@ 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", + }; // Update format manifest.web_accessible_resources = [ diff --git a/src/background/background.ts b/src/background/background.ts index b025ab8b58..6c079e0289 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -46,13 +46,15 @@ import { initLogSweep } from "@/telemetry/logging"; import { initModUpdater } from "@/background/modUpdater"; import { initRuntimeLogging } from "@/development/runtimeLogging"; import initWalkthroughModalTrigger from "@/background/walkthroughModalTrigger"; +import { initSidePanel } from "./sidePanel"; void initLocator(); void initMessengerLogging(); void initRuntimeLogging(); registerMessenger(); registerExternalMessenger(); -initBrowserAction(); +void initBrowserAction(); +void initSidePanel(); initInstaller(); void initNavigation(); initExecutor(); diff --git a/src/background/browserAction.ts b/src/background/browserAction.ts index 814e2e572e..2d240554fd 100644 --- a/src/background/browserAction.ts +++ b/src/background/browserAction.ts @@ -15,99 +15,19 @@ * along with this program. If not, see . */ -import { ensureContentScript } from "@/background/contentScript"; -import { rehydrateSidebar } from "@/contentScript/messenger/api"; -import webextAlert from "./webextAlert"; -import { browserAction, type Tab } from "@/mv3/api"; -import { executeScript, isScriptableUrl } 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 { browserAction } from "@/mv3/api"; +import { openSidePanel } from "@/sidebar/sidePanel/messenger/api"; -const ERR_UNABLE_TO_OPEN = - "PixieBrix was unable to open the Sidebar. Try refreshing the page."; +export default async function initBrowserAction(): Promise { + void chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }); -// 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", + // Disable by default, so that it can be enabled on a per-tab basis. + // Without this, the sidePanel remains open as the user changes tabs + void chrome.sidePanel.setOptions({ + enabled: false, }); - // 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, + browserAction.onClicked.addListener(async (tab) => { + await openSidePanel(tab.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); -} - -/** - * 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 { - 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); } diff --git a/src/background/contextMenus.ts b/src/background/contextMenus.ts index 52748c9d90..329115a516 100644 --- a/src/background/contextMenus.ts +++ b/src/background/contextMenus.ts @@ -68,7 +68,7 @@ async function dispatchMenu( ): Promise { expectContext("background"); - const target = { frameId: info.frameId, tabId: tab.id }; + const target = { frameId: info.frameId ?? 0, tabId: tab.id }; if (typeof info.menuItemId !== "string") { throw new TypeError(`Not a PixieBrix menu item: ${info.menuItemId}`); diff --git a/src/background/messenger/api.ts b/src/background/messenger/api.ts index f6cbce86cd..cd0bc7a237 100644 --- a/src/background/messenger/api.ts +++ b/src/background/messenger/api.ts @@ -45,6 +45,8 @@ export const removeExtensionForEveryTab = getNotifier( bg, ); +export const showMySidePanel = getMethod("SHOW_MY_SIDE_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 6339dd1faf..c9940e804b 100644 --- a/src/background/messenger/registration.ts +++ b/src/background/messenger/registration.ts @@ -78,6 +78,7 @@ import { getCachedAuthData, } from "@/background/auth/authStorage"; import { setCopilotProcessData } from "@/background/partnerHandlers"; +import { showMySidePanel } from "@/background/sidePanel"; import { setToolbarBadge } from "@/background/toolbarBadge"; expectContext("background"); @@ -112,6 +113,8 @@ declare global { PING: typeof pong; COLLECT_PERFORMANCE_DIAGNOSTICS: typeof collectPerformanceDiagnostics; + SHOW_MY_SIDE_PANEL: typeof showMySidePanel; + SET_TOOLBAR_BADGE: typeof setToolbarBadge; ACTIVATE_TAB: typeof activateTab; REACTIVATE_EVERY_TAB: typeof reactivateEveryTab; @@ -192,6 +195,8 @@ export default function registerMessenger(): void { PING: pong, COLLECT_PERFORMANCE_DIAGNOSTICS: collectPerformanceDiagnostics, + SHOW_MY_SIDE_PANEL: showMySidePanel, + SET_TOOLBAR_BADGE: setToolbarBadge, ACTIVATE_TAB: activateTab, REACTIVATE_EVERY_TAB: reactivateEveryTab, diff --git a/src/background/sidePanel.ts b/src/background/sidePanel.ts new file mode 100644 index 0000000000..2c9c306d0b --- /dev/null +++ b/src/background/sidePanel.ts @@ -0,0 +1,74 @@ +/* + * 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 { openSidePanel } from "@/sidebar/sidePanel/messenger/api"; +import type { MessengerMeta } from "webext-messenger"; +import { getExtensionConsoleUrl } from "@/utils/extensionUtils"; +import { + DISPLAY_REASON_EXTENSION_CONSOLE, + DISPLAY_REASON_RESTRICTED_URL, +} from "@/tinyPages/restrictedUrlPopupConstants"; +import { isScriptableUrl } from "webext-content-scripts"; + +function getRestrictedPageMessage(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; +} + +function getSidebarPath(tabId: number, url: string | undefined): string { + return getRestrictedPageMessage(url) ?? "sidebar.html?tabId=" + tabId; +} + +export async function showMySidePanel(this: MessengerMeta): Promise { + await openSidePanel(this.trace[0].tab.id); +} + +// TODO: Drop if this is ever implemented: https://github.com/w3c/webextensions/issues/515 +export async function initSidePanel(): Promise { + // TODO: Drop this once the popover URL behavior is merged into sidebar.html + chrome.tabs.onUpdated.addListener((tabId, changeInfo) => { + if (changeInfo.url) { + void chrome.sidePanel.setOptions({ + tabId, + path: getSidebarPath(tabId, changeInfo.url), + }); + } + }); + + // We need to target _all_ tabs, not just those we have access to + const existingTabs = await chrome.tabs.query({}); + await Promise.all( + existingTabs.map(async ({ id, url }) => + chrome.sidePanel.setOptions({ + tabId: id, + path: getSidebarPath(id, url), + enabled: true, + }), + ), + ); +} 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/bricks/effects/sidebar.ts b/src/bricks/effects/sidebar.ts index 9cceb134cd..39e88faa68 100644 --- a/src/bricks/effects/sidebar.ts +++ b/src/bricks/effects/sidebar.ts @@ -18,10 +18,11 @@ 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 { updateSidebar } from "@/contentScript/sidebarController"; +import { showMySidePanel } from "@/background/messenger/api"; import { propertiesToSchema } from "@/validators/generic"; - import { logPromiseDuration } from "@/utils/promiseUtils"; +import sidebarInThisTab from "@/sidebar/messenger/api"; export class ShowSidebar extends EffectABC { constructor() { @@ -64,9 +65,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({ + "ShowSidebar:updateSidebar", + updateSidebar({ force: forcePanel, panelHeading, blueprintId: logger.context.blueprintId, @@ -87,6 +89,6 @@ export class HideSidebar extends EffectABC { inputSchema: Schema = SCHEMA_EMPTY_OBJECT; async effect(): Promise { - hideSidebar(); + sidebarInThisTab.close(); } } 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/bricks/transformers/ephemeralForm/formTransformer.ts b/src/bricks/transformers/ephemeralForm/formTransformer.ts index 12df6f7662..9c9db58fea 100644 --- a/src/bricks/transformers/ephemeralForm/formTransformer.ts +++ b/src/bricks/transformers/ephemeralForm/formTransformer.ts @@ -25,10 +25,10 @@ import { } from "@/contentScript/ephemeralFormProtocol"; import { expectContext } from "@/utils/expectContext"; import { - ensureSidebar, + showSidebar, hideSidebarForm, - HIDE_SIDEBAR_EVENT_NAME, showSidebarForm, + onSidePanelClosure, } from "@/contentScript/sidebarController"; import { showModal } from "@/bricks/transformers/ephemeralForm/modalUtils"; import { getThisFrame } from "webext-messenger"; @@ -157,9 +157,9 @@ export class FormTransformer extends TransformerABC { if (location === "sidebar") { // Ensure the sidebar is visible (which may also be showing persistent panels) - await ensureSidebar(); + await showSidebar(); - showSidebarForm({ + await showSidebarForm({ extensionId: logger.context.extensionId, blueprintId: logger.context.blueprintId, nonce: formNonce, @@ -167,23 +167,13 @@ 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. // 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 09df3896ba..b825669ae3 100644 --- a/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.test.ts +++ b/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.test.ts @@ -165,7 +165,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 6b248c1299..72f97b20ed 100644 --- a/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts +++ b/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts @@ -23,11 +23,11 @@ import { } from "@/types/runtimeTypes"; import { expectContext } from "@/utils/expectContext"; import { - ensureSidebar, - HIDE_SIDEBAR_EVENT_NAME, + showSidebar, hideTemporarySidebarPanel, showTemporarySidebarPanel, updateTemporarySidebarPanel, + onSidePanelClosure, } from "@/contentScript/sidebarController"; import { type PanelPayload, @@ -162,7 +162,7 @@ export async function displayTemporaryInfo({ updatePanelDefinition(newEntry); if (location === "panel") { - updateTemporarySidebarPanel(newEntry); + void updateTemporarySidebarPanel(newEntry); } else { updateTemporaryOverlayPanel(newEntry); } @@ -176,10 +176,10 @@ export async function displayTemporaryInfo({ extensionId: panelEntryMetadata.extensionId, }); - await ensureSidebar(); + await showSidebar(); // Show loading - showTemporarySidebarPanel({ + await showTemporarySidebarPanel({ ...panelEntryMetadata, nonce, payload: { @@ -189,18 +189,10 @@ export async function displayTemporaryInfo({ }, }); - window.addEventListener( - HIDE_SIDEBAR_EVENT_NAME, - () => { - controller.abort(); - }, - { - signal: controller.signal, - }, - ); + onSidePanelClosure(controller); controller.signal.addEventListener("abort", () => { - hideTemporarySidebarPanel(nonce); + void hideTemporarySidebarPanel(nonce); void stopWaitingForTemporaryPanels([nonce]); }); } else { 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/contentScript/contentScriptCore.ts b/src/contentScript/contentScriptCore.ts index ad4ef3a1c2..f9e9f67a0e 100644 --- a/src/contentScript/contentScriptCore.ts +++ b/src/contentScript/contentScriptCore.ts @@ -40,6 +40,7 @@ import initFloatingActions from "@/components/floatingActions/initFloatingAction import { initSidebarActivation } from "@/contentScript/sidebarActivation"; import { initPerformanceMonitoring } from "@/contentScript/performanceMonitoring"; import { initRuntime } from "@/runtime/reducePipeline"; +import { renderPanelsIfVisible } from "./sidebarController"; // Must come before the default handler for ignoring errors. Otherwise, this handler might not be run onUncaughtError((error) => { @@ -69,9 +70,12 @@ export async function init(): Promise { void initSidebarActivation(); - // Inform `ensureContentScript` + // Notify `ensureContentScript` void browser.runtime.sendMessage({ type: ENSURE_CONTENT_SCRIPT_READY }); + // Update `sidePanel` + void renderPanelsIfVisible(); + // Let the partner page know initPartnerIntegrations(); void initFloatingActions(); diff --git a/src/contentScript/messenger/api.ts b/src/contentScript/messenger/api.ts index 28f796d741..17cbff3c11 100644 --- a/src/contentScript/messenger/api.ts +++ b/src/contentScript/messenger/api.ts @@ -41,14 +41,12 @@ 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 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 updateSidebar = getNotifier("UPDATE_SIDEBAR"); +export const sidebarWasLoaded = getNotifier("SIDEBAR_WAS_LOADED"); 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 43e9a159ed..a5c28970f9 100644 --- a/src/contentScript/messenger/registration.ts +++ b/src/contentScript/messenger/registration.ts @@ -32,12 +32,10 @@ import { cancelForm, } from "@/contentScript/ephemeralFormProtocol"; import { - hideSidebar, - showSidebar, - rehydrateSidebar, removeExtensions as removeSidebars, - reloadSidebar, getReservedPanelEntries, + sidebarWasLoaded, + updateSidebar, } from "@/contentScript/sidebarController"; import { insertPanel } from "@/contentScript/pageEditor/insertPanel"; import { insertButton } from "@/contentScript/pageEditor/insertButton"; @@ -103,13 +101,11 @@ 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; + UPDATE_SIDEBAR: typeof updateSidebar; + SIDEBAR_WAS_LOADED: typeof sidebarWasLoaded; + REMOVE_SIDEBARS: typeof removeSidebars; INSERT_PANEL: typeof insertPanel; INSERT_BUTTON: typeof insertButton; @@ -172,12 +168,10 @@ 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, + UPDATE_SIDEBAR: updateSidebar, + SIDEBAR_WAS_LOADED: sidebarWasLoaded, + REMOVE_SIDEBARS: removeSidebars, INSERT_PANEL: insertPanel, INSERT_BUTTON: insertButton, 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/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 6504548d12..c68cc01ec7 100644 --- a/src/contentScript/sidebarActivation.ts +++ b/src/contentScript/sidebarActivation.ts @@ -18,10 +18,10 @@ import { type RegistryId } from "@/types/registryTypes"; import { isRegistryId } from "@/types/helpers"; import { - ensureSidebar, - HIDE_SIDEBAR_EVENT_NAME, + showSidebar, hideModActivationInSidebar, showModActivationInSidebar, + onSidePanelClosure, } from "@/contentScript/sidebarController"; import { isLinked } from "@/auth/token"; import { @@ -57,22 +57,16 @@ async function showSidebarActivationForMods( ): Promise { const controller = new AbortController(); - await ensureSidebar(); - showModActivationInSidebar({ + await showSidebar(); + await showModActivationInSidebar({ modIds, heading: "Activating", }); - window.addEventListener( - HIDE_SIDEBAR_EVENT_NAME, - () => { - controller.abort(); - }, - { - signal: controller.signal, - }, - ); + + onSidePanelClosure(controller); + controller.signal.addEventListener("abort", () => { - hideModActivationInSidebar(); + void hideModActivationInSidebar(); }); } diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index 535f1fdc4d..d65807236d 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -15,20 +15,14 @@ * 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"; import sidebarInThisTab from "@/sidebar/messenger/api"; -import { isEmpty } from "lodash"; +import { isEmpty, throttle } from "lodash"; import { SimpleEventTarget } from "@/utils/SimpleEventTarget"; -import { - insertSidebarFrame, - isSidebarFrameVisible, - removeSidebarFrame, -} from "./sidebarDomControllerLite"; import { type Except } from "type-fest"; -import { type RunArgs, RunReason } from "@/types/runtimeTypes"; +import { RunReason, type RunArgs } from "@/types/runtimeTypes"; import { type UUID } from "@/types/stringTypes"; import { type RegistryId } from "@/types/registryTypes"; import { type ModComponentRef } from "@/types/modComponentTypes"; @@ -42,17 +36,37 @@ 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 { + isSidePanelOpen, + isSidePanelOpenSync, +} from "@/sidebar/sidePanel/messenger/api"; import { getTimedSequence } from "@/types/helpers"; - -export const HIDE_SIDEBAR_EVENT_NAME = "pixiebrix:hideSidebar"; +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 +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) as () => Promise, +); /** * Event listeners triggered when the sidebar shows and is ready to receive messages. */ export const sidebarShowEvents = new SimpleEventTarget(); +export function sidebarWasLoaded(): void { + sidebarShowEvents.emit({ reason: RunReason.MANUAL }); +} + // eslint-disable-next-line local-rules/persistBackgroundData -- Unused there const panels: PanelEntry[] = []; @@ -60,64 +74,13 @@ 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 { - console.debug("sidebarController:showSidebar", { - isSidebarFrameVisible: isSidebarFrameVisible(), - }); - +export async function showSidebar(): Promise { + console.debug("sidebarController:showSidebar"); reportEvent(Events.SIDEBAR_SHOW); - const isAlreadyShowing = isSidebarFrameVisible(); - - if (!isAlreadyShowing) { - 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)) { - // The sidebarSlice handles the race condition with the panels loading by keeping track of the latest pending - // activatePanel request. - void sidebarInThisTab - .activatePanel(getTimedSequence(), { - ...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 }), - ); - }); - } + // 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)(); + await pingSidebar(); } /** @@ -127,7 +90,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"); } @@ -138,87 +101,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(), - }); - - if (!isSidebarFrameVisible()) { - expectContext("contentScript"); - await logPromiseDuration("ensureSidebar", showSidebar()); - } -} - -/** - * 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 + * @param activateOptions options controlling the visible panel in the sidebar */ -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(), - }); +export async function updateSidebar( + activateOptions: ActivatePanelOptions = {}, +): Promise { + await pingSidebar(); - 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(); + if (!isEmpty(activateOptions)) { + // The sidebarSlice handles the race condition with the panels loading by keeping track of the latest pending + // activatePanel request. + await sidebarInThisTab.activatePanel(getTimedSequence(), { + ...activateOptions, + force: activateOptions.force, + }); } } -function renderPanelsIfVisible(): void { +export async function renderPanelsIfVisible(): Promise { expectContext("contentScript"); console.debug("sidebarController:renderPanelsIfVisible"); - if (isSidebarFrameVisible()) { + if (await isSidePanelOpen()) { void sidebarInThisTab.renderPanels(getTimedSequence(), panels); } else { console.debug( @@ -227,10 +132,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"); } @@ -240,10 +147,10 @@ export function showSidebarForm(entry: Except): void { }); } -export function hideSidebarForm(nonce: UUID): void { +export async function hideSidebarForm(nonce: UUID): Promise { expectContext("contentScript"); - if (!isSidebarFrameVisible()) { + if (!(await isSidePanelOpen())) { // Already hidden return; } @@ -251,12 +158,12 @@ export function hideSidebarForm(nonce: UUID): void { void sidebarInThisTab.hideForm(getTimedSequence(), 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", ); @@ -268,12 +175,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", ); @@ -285,10 +192,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; } @@ -307,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(); } /** @@ -337,7 +244,7 @@ export function removeExtensionPoint( ), ); - renderPanelsIfVisible(); + void renderPanelsIfVisible(); } /** @@ -372,7 +279,7 @@ export function reservePanels(refs: ModComponentRef[]): void { } } - renderPanelsIfVisible(); + void renderPanelsIfVisible(); } export function updateHeading(extensionId: UUID, heading: string): void { @@ -391,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", @@ -439,7 +346,7 @@ export function upsertPanel( }); } - renderPanelsIfVisible(); + void renderPanelsIfVisible(); } /** @@ -448,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", ); @@ -474,13 +381,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; } @@ -508,3 +415,18 @@ export function getReservedPanelEntries(): { modActivationPanel: modActivationPanelEntry, }; } + +// 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"); + window.addEventListener( + "resize", + () => { + if (isSidePanelOpenSync() === false) { + controller.abort(); + } + }, + { signal: controller.signal }, + ); +} diff --git a/src/contentScript/sidebarDomControllerLite.ts b/src/contentScript/sidebarDomControllerLite.ts deleted file mode 100644 index d7070b998b..0000000000 --- a/src/contentScript/sidebarDomControllerLite.ts +++ /dev/null @@ -1,160 +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 - */ - -import { MAX_Z_INDEX, PANEL_FRAME_ID } from "@/domConstants"; -import shadowWrap from "@/utils/shadowWrap"; -import { expectContext } from "@/utils/expectContext"; -import { uuidv4 } from "@/types/helpers"; - -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 { - return Boolean(getSidebar()); -} - -/** Removes the element; Returns false if no element was found */ -export function removeSidebarFrame(): 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 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; -} - -/** - * Toggle the sidebar frame. Returns true if the sidebar is now visible, false otherwise. - */ -export function toggleSidebarFrame(): boolean { - console.debug("sidebarDomControllerLite:toggleSidebarFrame", { - isSidebarFrameVisible: isSidebarFrameVisible(), - }); - - if (isSidebarFrameVisible()) { - removeSidebarFrame(); - return false; - } - - insertSidebarFrame(); - return true; -} diff --git a/src/contrib/automationanywhere/aaFrameProtocol.ts b/src/contrib/automationanywhere/aaFrameProtocol.ts index f531bc5917..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 "webext-messenger"; 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/domConstants.ts b/src/domConstants.ts index aea97b11a1..b865278e90 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"; @@ -38,7 +36,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/pageEditor/panes/insert/useAutoInsert.ts b/src/pageEditor/panes/insert/useAutoInsert.ts index 33851d7afb..779d7b2367 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 { openSidePanel } from "@/sidebar/sidePanel/messenger/api"; 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 openSidePanel(chrome.devtools.inspectedWindow.tabId); } reportEvent(Events.MOD_COMPONENT_ADD_NEW, { diff --git a/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx b/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx index 79491d433f..aebc100557 100644 --- a/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx +++ b/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx @@ -33,8 +33,9 @@ import { import { disableOverlay, enableOverlay, - showSidebar, + updateSidebar, } from "@/contentScript/messenger/api"; +import { openSidePanel } from "@/sidebar/sidePanel/messenger/api"; import { thisTab } from "@/pageEditor/utils"; import cx from "classnames"; import { selectSessionId } from "@/pageEditor/slices/sessionSelectors"; @@ -116,7 +117,8 @@ 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, { + await openSidePanel(chrome.devtools.inspectedWindow.tabId); + updateSidebar(thisTab, { extensionId: modComponent.id, force: true, refresh: false, diff --git a/src/pageEditor/sidebar/DynamicModComponentListItem.tsx b/src/pageEditor/sidebar/DynamicModComponentListItem.tsx index 2fe152981e..b994a28114 100644 --- a/src/pageEditor/sidebar/DynamicModComponentListItem.tsx +++ b/src/pageEditor/sidebar/DynamicModComponentListItem.tsx @@ -30,8 +30,9 @@ import { type UUID } from "@/types/stringTypes"; import { disableOverlay, enableOverlay, - showSidebar, + updateSidebar, } from "@/contentScript/messenger/api"; +import { openSidePanel } from "@/sidebar/sidePanel/messenger/api"; import { thisTab } from "@/pageEditor/utils"; import cx from "classnames"; import reportEvent from "@/telemetry/reportEvent"; @@ -154,7 +155,7 @@ const DynamicModComponentListItem: React.FunctionComponent< : undefined } onMouseLeave={isButton ? async () => hideOverlay() : undefined} - onClick={() => { + onClick={async () => { reportEvent(Events.PAGE_EDITOR_OPEN, { sessionId, extensionId: modComponentFormState.uuid, @@ -165,7 +166,8 @@ 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, { + await openSidePanel(chrome.devtools.inspectedWindow.tabId); + updateSidebar(thisTab, { extensionId: modComponentFormState.uuid, force: true, refresh: false, diff --git a/src/sidebar/ConnectedSidebar.tsx b/src/sidebar/ConnectedSidebar.tsx index e9d3283857..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 "webext-messenger"; +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/Header.tsx b/src/sidebar/Header.tsx index 47bb0155f1..c4ed48fa01 100644 --- a/src/sidebar/Header.tsx +++ b/src/sidebar/Header.tsx @@ -19,36 +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"; const Header: React.FunctionComponent = () => { const { logo, showSidebarLogo, customSidebarLogo } = useTheme(); const theme = useGetTheme(); - const wasContextInvalidated = useContextInvalidated(); return (
- {wasContextInvalidated || ( // /* The button doesn't work after invalidation #2359 */ - - )} {showSidebarLogo && (
Please close and re-open the sidebar panel.

-
diff --git a/src/sidebar/Tabs.test.tsx b/src/sidebar/Tabs.test.tsx index 4d1c17ecbc..6574f5b9da 100644 --- a/src/sidebar/Tabs.test.tsx +++ b/src/sidebar/Tabs.test.tsx @@ -25,13 +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 sidebarInThisTab from "@/sidebar/messenger/api"; import { eventKeyForEntry } from "@/sidebar/eventKeyUtils"; import { mockAllApiEndpoints } from "@/testUtils/appApiMock"; mockAllApiEndpoints(); +jest.spyOn(window, "close").mockImplementation(jest.fn()); + const cancelFormSpy = jest.spyOn(messengerApi, "cancelForm"); -const hideSidebarSpy = jest.spyOn(messengerApi, "hideSidebar"); +const hideSidebarSpy = jest.spyOn(sidebarInThisTab, "close"); async function setupPanelsAndRender(options: { sidebarEntries?: Partial; diff --git a/src/sidebar/Tabs.tsx b/src/sidebar/Tabs.tsx index d0e54e9c41..475b396207 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/__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 3ee5dc1f28..018e87e199 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 sidebarInThisTab from "@/sidebar/messenger/api"; import ActivateMultipleModsPanel from "@/sidebar/activateMod/ActivateMultipleModsPanel"; import ErrorBoundary from "@/sidebar/SidebarErrorBoundary"; import { includesQuickBarStarterBrick } from "@/starterBricks/starterBrickModUtils"; @@ -63,7 +63,8 @@ const useRequiredModDefinitionsMock = jest.mocked(useRequiredModDefinitions); const checkModDefinitionPermissionsMock = jest.mocked( checkModDefinitionPermissions, ); -const hideSidebarSpy = jest.spyOn(messengerApi, "hideSidebar"); + +const hideSidebarSpy = jest.spyOn(sidebarInThisTab, "close"); jest.mock("@/starterBricks/starterBrickModUtils", () => { const actualUtils = jest.requireActual( diff --git a/src/sidebar/activateMod/ActivateModPanel.tsx b/src/sidebar/activateMod/ActivateModPanel.tsx index 416a139315..0fd651929d 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/messenger/api.ts b/src/sidebar/messenger/api.ts index a634c38472..ba989c90d6 100644 --- a/src/sidebar/messenger/api.ts +++ b/src/sidebar/messenger/api.ts @@ -16,16 +16,32 @@ */ /* Do not use `registerMethod` in this file */ -import { getMethod, getNotifier } from "webext-messenger"; +import { isContentScript } from "webext-detect-page"; +import { getMethod, getNotifier, getThisFrame } from "webext-messenger"; -const target = { tabId: "this", page: "/sidebar.html" } as const; +const target = { page: "/sidebar.html" }; + +// TODO: move to contentScrpt/sidePanel/messenger/api.ts +// 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), activatePanel: getMethod("SIDEBAR_ACTIVATE_PANEL", target), showForm: getMethod("SIDEBAR_SHOW_FORM", target), hideForm: getMethod("SIDEBAR_HIDE_FORM", target), + /** @deprecated Only from the content script. Use this in the content script: import {pingSidebar} from '@/contentScript/sidebarController'; */ pingSidebar: getMethod("SIDEBAR_PING", target), + close: getNotifier("SIDEBAR_CLOSE", 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..dbe6ff64cd 100644 --- a/src/sidebar/messenger/registration.ts +++ b/src/sidebar/messenger/registration.ts @@ -20,6 +20,7 @@ import { registerMethods } from "webext-messenger"; import { activatePanel, hideActivateMods, + closeSelf, hideForm, hideTemporaryPanel, renderPanels, @@ -44,6 +45,8 @@ declare global { SIDEBAR_SHOW_FORM: typeof showForm; SIDEBAR_HIDE_FORM: typeof hideForm; SIDEBAR_PING: typeof noop; + SIDEBAR_CLOSE: typeof closeSelf; + SIDEBAR_RELOAD: typeof location.reload; SIDEBAR_SHOW_TEMPORARY_PANEL: typeof showTemporaryPanel; SIDEBAR_UPDATE_TEMPORARY_PANEL: typeof updateTemporaryPanel; SIDEBAR_HIDE_TEMPORARY_PANEL: typeof hideTemporaryPanel; @@ -58,7 +61,9 @@ export default function registerMessenger(): void { SIDEBAR_RENDER_PANELS: renderPanels, SIDEBAR_SHOW_FORM: showForm, SIDEBAR_HIDE_FORM: hideForm, + SIDEBAR_CLOSE: closeSelf, 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/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/protocol.tsx b/src/sidebar/protocol.tsx index 689205bf66..621eb6b972 100644 --- a/src/sidebar/protocol.tsx +++ b/src/sidebar/protocol.tsx @@ -217,3 +217,7 @@ export async function showActivateMods( export async function hideActivateMods(sequence: TimedSequence): Promise { runListeners("onHideActivateRecipe", sequence); } + +export async function closeSelf(): Promise { + window.close(); +} diff --git a/src/contentScript/browserActionInstantHandler.ts b/src/sidebar/sidePanel.tsx similarity index 61% rename from src/contentScript/browserActionInstantHandler.ts rename to src/sidebar/sidePanel.tsx index f3ae5ab242..83eeb5c7e1 100644 --- a/src/contentScript/browserActionInstantHandler.ts +++ b/src/sidebar/sidePanel.tsx @@ -15,7 +15,18 @@ * 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"; +/** @file This file defines the internal API for the sidePanel, only meant to be run in the sidePanel itself */ -toggleSidebarFrame(); +import { expectContext } from "@/utils/expectContext"; +import { + getAssociatedTarget, + respondToPings, +} from "@/sidebar/sidePanel/messenger/api"; +import { sidebarWasLoaded } from "@/contentScript/messenger/api"; + +expectContext("sidebar"); + +export function initSidePanel() { + respondToPings(); + sidebarWasLoaded(getAssociatedTarget()); +} diff --git a/src/sidebar/sidePanel/messenger/api.ts b/src/sidebar/sidePanel/messenger/api.ts new file mode 100644 index 0000000000..0f842eee71 --- /dev/null +++ b/src/sidebar/sidePanel/messenger/api.ts @@ -0,0 +1,115 @@ +/* + * 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 { isObject } from "@/utils/objectUtils"; +import { expectContext } from "@/utils/expectContext"; +import { type Target } from "webext-messenger"; +import { getErrorMessage } from "@/errors/errorHelpers"; + +function getAssociatedTabId(): number { + expectContext("sidebar"); + 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 +// TODO: Drop after https://github.com/pixiebrix/webext-messenger/issues/59 +export function respondToPings() { + expectContext("sidebar"); + 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 { + // Sync check where possible + if (isSidePanelOpenSync() === false) { + return false; + } + + try { + await chrome.runtime.sendMessage({ type: PING_MESSAGE }); + return true; + } catch { + return false; + } +} + +export async function openSidePanel(tabId: number): Promise { + // Simultaneously enable and open the side panel. + // If we wait too long before calling .open(), we will lose the "user gesture" permission + // There is no way to know whether the side panel is open yet, so we call it regardless. + void chrome.sidePanel.setOptions({ + tabId, + enabled: true, + }); + + try { + await chrome.sidePanel.open({ tabId }); + } catch (error) { + // In some cases, `openSidePanel` is called as a precaution and it might work if + // it's still part of a user gesture. + // If it's not, it will throw an error *even if the side panel is already open*. + // The following code silences that error iff the side panel is already open. + if ( + getErrorMessage(error).includes("user gesture") && + (await isSidePanelOpen()) + ) { + // The `openSidePanel` call was not required in the first place, the error can be silenced + // TODO: After switching to MV3, verify whether we drop that `openSidePanel` call + return; + } + + throw error; + } +} + +/* 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 +export function isSidePanelOpenSync(): false | "unknown" { + if (!globalThis.window) { + return "unknown"; + } + + return window.outerWidth - window.innerWidth > MINIMUM_SIDEBAR_WIDTH + ? "unknown" + : false; +} diff --git a/src/sidebar/sidebar.tsx b/src/sidebar/sidebar.tsx index 337d10d3c6..486dee6282 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 { initSidePanel } from "./sidePanel"; function init(): void { ReactDOM.render(, document.querySelector("#container")); @@ -47,6 +48,7 @@ registerContribBlocks(); registerBuiltinBricks(); initToaster(); init(); +initSidePanel(); // Handle an embedded AA business copilot frame void initCopilotMessenger(); 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/sidebar/useHideEmptySidebar.ts b/src/sidebar/useHideEmptySidebar.ts index ef06e26afe..9f1d15e7e5 100644 --- a/src/sidebar/useHideEmptySidebar.ts +++ b/src/sidebar/useHideEmptySidebar.ts @@ -16,17 +16,15 @@ */ import useAsyncEffect from "use-async-effect"; -import { getTopLevelFrame } from "webext-messenger"; -import { - getReservedSidebarEntries, - hideSidebar, -} from "@/contentScript/messenger/api"; +import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; +import { getReservedSidebarEntries } from "@/contentScript/messenger/api"; import { useSelector } from "react-redux"; import { selectClosedTabs, selectVisiblePanelCount, } from "@/sidebar/sidebarSelectors"; import { eventKeyForEntry } from "@/sidebar/eventKeyUtils"; +import { closeSelf } from "@/sidebar/protocol"; /** * Hide the sidebar if there are no visible panels. We use this to close the sidebar if the user closes all panels. @@ -37,8 +35,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 @@ -54,8 +53,7 @@ export const useHideEmptySidebar = () => { visiblePanelCount === 0 && openReservedPanels.length === 0 ) { - const topLevelFrame = await getTopLevelFrame(); - void hideSidebar(topLevelFrame); + await closeSelf(); } }, [visiblePanelCount], diff --git a/src/starterBricks/sidebarExtension.test.ts b/src/starterBricks/sidebarExtension.test.ts index 22e35a7a3a..b855b7683c 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,7 @@ describe("sidebarExtension", () => { expect(rootReader.readCount).toBe(0); // Fake the sidebar being added to the page - $(document.body).append(`
`); + isSidePanelOpenMock.mockResolvedValueOnce(true); sidebarShowEvents.emit({ reason: RunReason.MANUAL }); await tick(); @@ -249,7 +253,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/starterBricks/sidebarExtension.ts b/src/starterBricks/sidebarExtension.ts index 3078774873..6474a90541 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/messenger/api"; export type SidebarConfig = { heading: string; @@ -416,7 +416,7 @@ export abstract class SidebarStarterBrickABC extends StarterBrickABC PixieBrix - diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index b384c0c64d..10df572e0d 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -37,7 +37,6 @@ "./background/externalProtocol.ts", "./background/partnerTheme.ts", "./background/toolbarBadge.ts", - "./background/webextAlert.ts", "./background/setToolbarBadge.test.ts", "./bricks/available.ts", "./bricks/effects/cancel.ts", @@ -188,7 +187,6 @@ "./components/quickBar/utils.ts", "./components/selectionToolPopover/SelectionToolPopover.tsx", "./components/walkthroughModal/showWalkthroughModal.ts", - "./contentScript/browserActionInstantHandler.ts", "./contentScript/context.ts", "./contentScript/contextMenus.ts", "./contentScript/elementReference.ts", @@ -199,7 +197,6 @@ "./contentScript/pageState.ts", "./contentScript/ready.ts", "./contentScript/sidebarController.tsx", - "./contentScript/sidebarDomControllerLite.ts", "./contentScript/uipath.ts", "./contentScript/walkthroughModalProtocol.tsx", "./contrib/automationanywhere/aaTypes.ts", 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"; } diff --git a/src/utils/extensionUtils.ts b/src/utils/extensionUtils.ts index 6647cf8ffa..db73434391 100644 --- a/src/utils/extensionUtils.ts +++ b/src/utils/extensionUtils.ts @@ -21,8 +21,6 @@ import { type Promisable } from "type-fest"; import { isScriptableUrl } from "webext-content-scripts"; import { type Runtime } from "webextension-polyfill"; -type TabId = number; - export const SHORTCUTS_URL = "chrome://extensions/shortcuts"; type Command = "toggle-quick-bar"; @@ -92,13 +90,21 @@ export class RuntimeNotFoundError extends Error { override name = "RuntimeNotFoundError"; } -export async function getTabsWithAccess(): Promise { +export async function getTabsWithAccess(): Promise< + Array<{ tabId: number; url: string }> +> { const tabs = await browser.tabs.query({ url: ["*://*/*"], discarded: false, }); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion -- The type isn't tight enough for tabs.query() - return tabs.filter((tab) => isScriptableUrl(tab.url!)).map((tab) => tab.id!); + return ( + tabs + // The type isn't tight enough for tabs.query() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion + .filter((tab) => isScriptableUrl(tab.url!)) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion + .map((tab) => ({ tabId: tab.id!, url: tab.url! })) + ); } /** @@ -117,7 +123,7 @@ export async function forEachTab< } const promises = tabs - .filter((tabId) => tabId !== options?.exclude) - .map((tabId) => callback({ tabId })); + .filter(({ tabId }) => tabId !== options?.exclude) + .map(({ tabId }) => callback({ tabId })); return Promise.allSettled(promises); } 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 { diff --git a/src/utils/notify.tsx b/src/utils/notify.tsx index 508494fca8..11210d91df 100644 --- a/src/utils/notify.tsx +++ b/src/utils/notify.tsx @@ -32,9 +32,6 @@ import { type Except, type RequireAtLeastOne } from "type-fest"; import { getErrorMessage } from "@/errors/errorHelpers"; import { merge, truncate } from "lodash"; -// While correct, the `sidebarDomControllerLite` name implies that it's a small, pure module and it's unlikely to cause issues -// eslint-disable-next-line import/no-restricted-paths -import { SIDEBAR_WIDTH_CSS_PROPERTY } from "@/contentScript/sidebarDomControllerLite"; import ErrorIcon from "@/icons/error.svg?loadAsComponent"; import WarningIcon from "@/icons/warning.svg?loadAsComponent"; @@ -171,8 +168,6 @@ export function showNotification({ const options: ToastOptions = { id, duration, - // Keep the notification centered on the document even when the sidebar is open - style: { marginLeft: `calc(var(${SIDEBAR_WIDTH_CSS_PROPERTY}, 0) * -1)` }, }; const component = ; diff --git a/webpack.config.mjs b/webpack.config.mjs index 96c644ea8e..5e1f5c6af0 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", "contentScript/setExtensionIdInApp", "pageEditor/pageEditor", "extensionConsole/options", From 6d0aa00e0042e8606f9f2678a064b4a48499cf68 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Wed, 17 Jan 2024 20:13:47 +0800 Subject: [PATCH 02/30] First pass --- src/background/browserAction.ts | 76 ++++++++++++++++++- src/background/sidePanel.ts | 4 +- src/background/webextAlert.ts | 26 +++++++ src/bricks/effects/sidebar.ts | 20 ++++- src/bricks/renderers/customForm.ts | 6 +- src/bricks/transformers/brickFactory.ts | 4 +- .../documentBuilder/render/BlockElement.tsx | 4 +- .../documentBuilder/render/ButtonElement.tsx | 4 +- .../documentBuilder/render/ListElement.tsx | 4 +- src/mv3/sidePanelMigration.ts | 26 +++++++ src/sidebar/ConnectedSidebar.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/sidebar/useHideEmptySidebar.ts | 9 +-- 17 files changed, 177 insertions(+), 36 deletions(-) create mode 100644 src/background/webextAlert.ts create mode 100644 src/mv3/sidePanelMigration.ts diff --git a/src/background/browserAction.ts b/src/background/browserAction.ts index 2d240554fd..7c94e8f721 100644 --- a/src/background/browserAction.ts +++ b/src/background/browserAction.ts @@ -15,10 +15,22 @@ * along with this program. If not, see . */ -import { browserAction } from "@/mv3/api"; +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 { openSidePanel } from "@/sidebar/sidePanel/messenger/api"; +import { setActionPopup } from "webext-tools"; +import { getRestrictedPageMessage } from "./sidePanel.js"; export default async function initBrowserAction(): Promise { + if (!isMV3()) { + initBrowserActionMv2(); + return; + } + void chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }); // Disable by default, so that it can be enabled on a per-tab basis. @@ -31,3 +43,65 @@ export default async function initBrowserAction(): Promise { await openSidePanel(tab.id); }); } + +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); +} + +function initBrowserActionMv2(): void { + 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(getRestrictedPageMessage); +} diff --git a/src/background/sidePanel.ts b/src/background/sidePanel.ts index 2c9c306d0b..2944423c65 100644 --- a/src/background/sidePanel.ts +++ b/src/background/sidePanel.ts @@ -24,7 +24,9 @@ import { } from "@/tinyPages/restrictedUrlPopupConstants"; import { isScriptableUrl } from "webext-content-scripts"; -function getRestrictedPageMessage(tabUrl: string | undefined): string | null { +export function getRestrictedPageMessage( + tabUrl: string | undefined, +): string | null { const popoverUrl = browser.runtime.getURL("restrictedUrlPopup.html"); if (tabUrl?.startsWith(getExtensionConsoleUrl())) { diff --git a/src/background/webextAlert.ts b/src/background/webextAlert.ts new file mode 100644 index 0000000000..497fe5e6ff --- /dev/null +++ b/src/background/webextAlert.ts @@ -0,0 +1,26 @@ +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/bricks/effects/sidebar.ts b/src/bricks/effects/sidebar.ts index 39e88faa68..3ffde1ed4d 100644 --- a/src/bricks/effects/sidebar.ts +++ b/src/bricks/effects/sidebar.ts @@ -18,11 +18,16 @@ import { EffectABC } from "@/types/bricks/effectTypes"; import { type BrickArgs, type BrickOptions } from "@/types/runtimeTypes"; import { type Schema, SCHEMA_EMPTY_OBJECT } from "@/types/schemaTypes"; -import { updateSidebar } from "@/contentScript/sidebarController"; +import { + updateSidebar, + hideSidebar, + showSidebar, +} from "@/contentScript/sidebarController"; import { showMySidePanel } from "@/background/messenger/api"; import { propertiesToSchema } from "@/validators/generic"; import { logPromiseDuration } from "@/utils/promiseUtils"; import sidebarInThisTab from "@/sidebar/messenger/api"; +import { isMV3 } from "@/mv3/api"; export class ShowSidebar extends EffectABC { constructor() { @@ -65,7 +70,12 @@ 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(); + if (isMV3()) { + await showMySidePanel(); + } else { + await showSidebar(); + } + void logPromiseDuration( "ShowSidebar:updateSidebar", updateSidebar({ @@ -89,6 +99,10 @@ export class HideSidebar extends EffectABC { inputSchema: Schema = SCHEMA_EMPTY_OBJECT; async effect(): Promise { - sidebarInThisTab.close(); + if (isMV3()) { + sidebarInThisTab.close(); + } else { + hideSidebar(); + } } } diff --git a/src/bricks/renderers/customForm.ts b/src/bricks/renderers/customForm.ts index 5191a9fb8e..d99b2081b5 100644 --- a/src/bricks/renderers/customForm.ts +++ b/src/bricks/renderers/customForm.ts @@ -25,7 +25,6 @@ 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 { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; import { type UUID } from "@/types/stringTypes"; import { type SanitizedIntegrationConfig } from "@/integrations/integrationTypes"; import { @@ -47,6 +46,7 @@ import { ensureJsonObject, isObject } from "@/utils/objectUtils"; import { getOutputReference, validateOutputKey } from "@/runtime/runtimeTypes"; import { type BrickConfig } from "@/bricks/types"; import { isExpression } from "@/utils/expressionUtils"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; interface DatabaseResult { success: boolean; @@ -341,7 +341,7 @@ async function getInitialData( case "state": { const namespace = storage.namespace ?? "blueprint"; - const topLevelFrame = getAssociatedTarget(); + const topLevelFrame = await getTopFrameFromSidebar(); // 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 = getAssociatedTarget(); + const topLevelFrame = await getTopFrameFromSidebar(); // 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 eef5c6822d..1090855158 100644 --- a/src/bricks/transformers/brickFactory.ts +++ b/src/bricks/transformers/brickFactory.ts @@ -50,7 +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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; import { getTopLevelFrame } from "webext-messenger"; import { uuidv4 } from "@/types/helpers"; import { isSpecificError } from "@/errors/errorHelpers"; @@ -346,7 +346,7 @@ class UserDefinedBrick extends BrickABC { // TODO: call top-level contentScript directly after https://github.com/pixiebrix/webext-messenger/issues/72 const topLevelFrame = isBrowserSidebar() - ? getAssociatedTarget() + ? await getTopFrameFromSidebar() : await getTopLevelFrame(); try { diff --git a/src/components/documentBuilder/render/BlockElement.tsx b/src/components/documentBuilder/render/BlockElement.tsx index 1ffb60c6d7..a489091170 100644 --- a/src/components/documentBuilder/render/BlockElement.tsx +++ b/src/components/documentBuilder/render/BlockElement.tsx @@ -26,10 +26,10 @@ import apiVersionOptions from "@/runtime/apiVersionOptions"; import { serializeError } from "serialize-error"; import { type DynamicPath } from "@/components/documentBuilder/documentBuilderTypes"; import { mapPathToTraceBranches } from "@/components/documentBuilder/utils"; -import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; import { type PanelContext } from "@/types/sidebarTypes"; import { type RendererRunPayload } from "@/types/rendererTypes"; import useAsyncState from "@/hooks/useAsyncState"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; type BlockElementProps = { pipeline: BrickPipeline; @@ -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 = getAssociatedTarget(); + const topLevelFrame = await getTopFrameFromSidebar(); return runRendererPipeline(topLevelFrame, { nonce: uuidv4(), diff --git a/src/components/documentBuilder/render/ButtonElement.tsx b/src/components/documentBuilder/render/ButtonElement.tsx index d58b69b01a..790eafb930 100644 --- a/src/components/documentBuilder/render/ButtonElement.tsx +++ b/src/components/documentBuilder/render/ButtonElement.tsx @@ -24,12 +24,12 @@ 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 { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; import { getRootCause, hasSpecificErrorCause } from "@/errors/errorHelpers"; import { SubmitPanelAction } from "@/bricks/errors"; import cx from "classnames"; import { boolean } from "@/utils/typeUtils"; import { mapPathToTraceBranches } from "@/components/documentBuilder/utils"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; type ButtonElementProps = Except & { onClick: BrickPipeline; @@ -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 = getAssociatedTarget(); + const topLevelFrame = await getTopFrameFromSidebar(); try { await runHeadlessPipeline(topLevelFrame, { diff --git a/src/components/documentBuilder/render/ListElement.tsx b/src/components/documentBuilder/render/ListElement.tsx index a124b46326..aca99597e7 100644 --- a/src/components/documentBuilder/render/ListElement.tsx +++ b/src/components/documentBuilder/render/ListElement.tsx @@ -30,12 +30,12 @@ import ErrorBoundary from "@/components/ErrorBoundary"; import { getErrorMessage } from "@/errors/errorHelpers"; import { runMapArgs } from "@/contentScript/messenger/api"; import apiVersionOptions from "@/runtime/apiVersionOptions"; -import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; import useAsyncState from "@/hooks/useAsyncState"; import DelayedRender from "@/components/DelayedRender"; import { isDeferExpression } from "@/utils/expressionUtils"; import { isNullOrBlank } from "@/utils/stringUtils"; import { joinPathParts } from "@/utils/formUtils"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; type DocumentListProps = { array: UnknownObject[]; @@ -66,7 +66,7 @@ const ListElementInternal: React.FC = ({ isLoading, error, } = useAsyncState(async () => { - const topLevelFrame = getAssociatedTarget(); + const topLevelFrame = await getTopFrameFromSidebar(); const elementVariableReference = `@${elementKey}`; diff --git a/src/mv3/sidePanelMigration.ts b/src/mv3/sidePanelMigration.ts new file mode 100644 index 0000000000..62de1f7091 --- /dev/null +++ b/src/mv3/sidePanelMigration.ts @@ -0,0 +1,26 @@ +/* + * 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 helpers useful for the MV3 sidePanel transition */ + +import { getTopLevelFrame } from "webext-messenger"; +import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; +import { isMV3 } from "./api"; + +export const getTopFrameFromSidebar = isMV3() + ? getAssociatedTarget + : getTopLevelFrame; diff --git a/src/sidebar/ConnectedSidebar.tsx b/src/sidebar/ConnectedSidebar.tsx index ea67e142ea..716b1f0545 100644 --- a/src/sidebar/ConnectedSidebar.tsx +++ b/src/sidebar/ConnectedSidebar.tsx @@ -42,9 +42,9 @@ import { ensureExtensionPointsInstalled, getReservedSidebarEntries, } from "@/contentScript/messenger/api"; -import { getAssociatedTarget } from "./sidePanel/messenger/api"; import useAsyncEffect from "use-async-effect"; import activateLinkClickHandler from "@/activation/activateLinkClickHandler"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; /** * Listeners to update the Sidebar's Redux state upon receiving messages from the contentScript. @@ -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 = getAssociatedTarget(); + const topFrame = await getTopFrameFromSidebar(); // Ensure persistent sidebar extension points have been installed to have reserve their panels for the sidebar await ensureExtensionPointsInstalled(topFrame); diff --git a/src/sidebar/PanelBody.tsx b/src/sidebar/PanelBody.tsx index d8e0d8ddc0..a0137d954e 100644 --- a/src/sidebar/PanelBody.tsx +++ b/src/sidebar/PanelBody.tsx @@ -46,9 +46,9 @@ import DelayedRender from "@/components/DelayedRender"; import { runHeadlessPipeline } from "@/contentScript/messenger/api"; import { uuidv4 } from "@/types/helpers"; import apiVersionOptions from "@/runtime/apiVersionOptions"; -import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; import { type DynamicPath } from "@/components/documentBuilder/documentBuilderTypes"; import { mapPathToTraceBranches } from "@/components/documentBuilder/utils"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; // Used for the loading message // import cx from "classnames"; @@ -205,7 +205,7 @@ const PanelBody: React.FunctionComponent<{ ); } - const topLevelFrame = getAssociatedTarget(); + const topLevelFrame = await getTopFrameFromSidebar(); await runHeadlessPipeline(topLevelFrame, { nonce: uuidv4(), diff --git a/src/sidebar/Tabs.tsx b/src/sidebar/Tabs.tsx index 475b396207..1c5de2c602 100644 --- a/src/sidebar/Tabs.tsx +++ b/src/sidebar/Tabs.tsx @@ -57,9 +57,9 @@ import { selectEventData } from "@/telemetry/deployments"; import ErrorBoundary from "@/sidebar/SidebarErrorBoundary"; import { TemporaryPanelTabPane } from "./TemporaryPanelTabPane"; import { MOD_LAUNCHER } from "@/sidebar/modLauncher/constants"; -import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; import { cancelForm } from "@/contentScript/messenger/api"; import { useHideEmptySidebar } from "@/sidebar/useHideEmptySidebar"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; const ActivateModPanel = lazy( async () => @@ -167,7 +167,7 @@ const Tabs: React.FC = () => { if (isTemporaryPanelEntry(panel)) { dispatch(sidebarSlice.actions.removeTemporaryPanel(panel.nonce)); } else if (isFormPanelEntry(panel)) { - const frame = getAssociatedTarget(); + const frame = await getTopFrameFromSidebar(); 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 0fd651929d..407569bef5 100644 --- a/src/sidebar/activateMod/ActivateModPanel.tsx +++ b/src/sidebar/activateMod/ActivateModPanel.tsx @@ -30,7 +30,6 @@ import AsyncButton from "@/components/AsyncButton"; import { useDispatch } from "react-redux"; import sidebarSlice from "@/sidebar/sidebarSlice"; import { reloadMarketplaceEnhancements as reloadMarketplaceEnhancementsInContentScript } from "@/contentScript/messenger/api"; -import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; import cx from "classnames"; import { isEmpty } from "lodash"; import ActivateModInputs from "@/sidebar/activateMod/ActivateModInputs"; @@ -53,6 +52,7 @@ import { type ModDefinition } from "@/types/modDefinitionTypes"; import { openShortcutsTab, SHORTCUTS_URL } from "@/utils/extensionUtils"; import Markdown from "@/components/Markdown"; import { getModActivationInstructions } from "@/utils/modUtils"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; const { actions } = sidebarSlice; @@ -109,7 +109,7 @@ const { setNeedsPermissions, activateStart, activateSuccess, activateError } = activationSlice.actions; async function reloadMarketplaceEnhancements() { - const topFrame = getAssociatedTarget(); + const topFrame = await getTopFrameFromSidebar(); // 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 2e7243c367..9903e1e1f8 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 { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; const ModLauncher: React.FunctionComponent = () => { const { permit } = useFlags(); @@ -45,7 +45,7 @@ const ModLauncher: React.FunctionComponent = () => { source: "ModLauncher", }); - const frame = getAssociatedTarget(); + const frame = await getTopFrameFromSidebar(); showWalkthroughModal(frame); }} diff --git a/src/sidebar/sidebarSlice.ts b/src/sidebar/sidebarSlice.ts index 8165ec16ae..fdc784612c 100644 --- a/src/sidebar/sidebarSlice.ts +++ b/src/sidebar/sidebarSlice.ts @@ -35,13 +35,13 @@ import { resolveTemporaryPanel, } from "@/contentScript/messenger/api"; import { partition, remove, sortBy } from "lodash"; -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"; import { type StorageInterface } from "@/store/StorageInterface"; import { getVisiblePanelCount } from "@/sidebar/utils"; import { MOD_LAUNCHER } from "@/sidebar/modLauncher/constants"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; const emptySidebarState: SidebarState = { panels: [], @@ -126,12 +126,12 @@ function findNextActiveKey( } async function cancelPreexistingForms(forms: UUID[]): Promise { - const topLevelFrame = getAssociatedTarget(); + const topLevelFrame = await getTopFrameFromSidebar(); cancelForm(topLevelFrame, ...forms); } async function cancelPanels(nonces: UUID[]): Promise { - const topLevelFrame = getAssociatedTarget(); + const topLevelFrame = await getTopFrameFromSidebar(); 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 = getAssociatedTarget(); + const topLevelFrame = await getTopFrameFromSidebar(); closeTemporaryPanel(topLevelFrame, nonces); } @@ -153,7 +153,7 @@ async function resolvePanel( nonce: UUID, action: Pick, ): Promise { - const topLevelFrame = getAssociatedTarget(); + const topLevelFrame = await getTopFrameFromSidebar(); resolveTemporaryPanel(topLevelFrame, nonce, action); } diff --git a/src/sidebar/useHideEmptySidebar.ts b/src/sidebar/useHideEmptySidebar.ts index 9f1d15e7e5..c198fccf18 100644 --- a/src/sidebar/useHideEmptySidebar.ts +++ b/src/sidebar/useHideEmptySidebar.ts @@ -16,7 +16,6 @@ */ import useAsyncEffect from "use-async-effect"; -import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; import { getReservedSidebarEntries } from "@/contentScript/messenger/api"; import { useSelector } from "react-redux"; import { @@ -25,6 +24,7 @@ import { } from "@/sidebar/sidebarSelectors"; import { eventKeyForEntry } from "@/sidebar/eventKeyUtils"; import { closeSelf } from "@/sidebar/protocol"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; /** * Hide the sidebar if there are no visible panels. We use this to close the sidebar if the user closes all panels. @@ -35,9 +35,8 @@ export const useHideEmptySidebar = () => { useAsyncEffect( async (isMounted) => { - const reservedPanelEntries = await getReservedSidebarEntries( - getAssociatedTarget(), - ); + const topFrame = await getTopFrameFromSidebar(); + const reservedPanelEntries = await getReservedSidebarEntries(topFrame); // 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 @@ -49,7 +48,7 @@ export const useHideEmptySidebar = () => { ].filter((panel) => panel && !closedTabs[eventKeyForEntry(panel)]); if ( - isMounted && + isMounted() && visiblePanelCount === 0 && openReservedPanels.length === 0 ) { From a7d0da1a447a5bde9c5d33ec943ed84f00ae965d Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Wed, 17 Jan 2024 21:12:18 +0800 Subject: [PATCH 03/30] Second pass --- .../ephemeralForm/formTransformer.ts | 4 +- .../temporaryInfo/DisplayTemporaryInfo.ts | 6 +- .../browserActionInstantHandler.ts | 21 +++ src/contentScript/contentScriptCore.ts | 1 + src/contentScript/messenger/api.ts | 4 +- src/contentScript/messenger/registration.ts | 12 ++ src/contentScript/sidebarActivation.ts | 8 +- src/contentScript/sidebarController.tsx | 73 ++++++-- src/contentScript/sidebarDomControllerLite.ts | 160 ++++++++++++++++++ .../automationanywhere/aaFrameProtocol.ts | 5 +- src/domConstants.ts | 3 + src/pageEditor/panes/insert/useAutoInsert.ts | 6 +- src/sidebar/Header.tsx | 25 ++- src/sidebar/SidebarErrorBoundary.tsx | 19 ++- src/sidebar/messenger/api.ts | 7 +- src/sidebar/modLauncher/ModLauncher.tsx | 1 - src/sidebar/protocol.tsx | 9 +- src/sidebar/sidePanel/messenger/api.ts | 6 + src/starterBricks/sidebarExtension.ts | 2 +- src/tinyPages/restrictedUrlPopup.html | 7 + 20 files changed, 339 insertions(+), 40 deletions(-) create mode 100644 src/contentScript/browserActionInstantHandler.ts create mode 100644 src/contentScript/sidebarDomControllerLite.ts diff --git a/src/bricks/transformers/ephemeralForm/formTransformer.ts b/src/bricks/transformers/ephemeralForm/formTransformer.ts index 9c9db58fea..4fe062cadf 100644 --- a/src/bricks/transformers/ephemeralForm/formTransformer.ts +++ b/src/bricks/transformers/ephemeralForm/formTransformer.ts @@ -28,7 +28,7 @@ import { showSidebar, hideSidebarForm, showSidebarForm, - onSidePanelClosure, + sidePanelClosureSignal, } from "@/contentScript/sidebarController"; import { showModal } from "@/bricks/transformers/ephemeralForm/modalUtils"; import { getThisFrame } from "webext-messenger"; @@ -167,7 +167,7 @@ export class FormTransformer extends TransformerABC { }); // Two-way binding between sidebar and form. Listen for the user (or an action) closing the sidebar - onSidePanelClosure(controller); + sidePanelClosureSignal(); 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 72f97b20ed..84798f6bce 100644 --- a/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts +++ b/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts @@ -27,7 +27,7 @@ import { hideTemporarySidebarPanel, showTemporarySidebarPanel, updateTemporarySidebarPanel, - onSidePanelClosure, + sidePanelClosureSignal, } from "@/contentScript/sidebarController"; import { type PanelPayload, @@ -56,6 +56,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 { mergeSignals, onAbort } from "abort-utils"; // Match naming of the sidebar panel extension point triggers export type RefreshTrigger = "manual" | "statechange"; @@ -189,8 +190,7 @@ export async function displayTemporaryInfo({ }, }); - onSidePanelClosure(controller); - + onAbort(sidePanelClosureSignal(), controller); controller.signal.addEventListener("abort", () => { void hideTemporarySidebarPanel(nonce); void stopWaitingForTemporaryPanels([nonce]); diff --git a/src/contentScript/browserActionInstantHandler.ts b/src/contentScript/browserActionInstantHandler.ts new file mode 100644 index 0000000000..f3ae5ab242 --- /dev/null +++ b/src/contentScript/browserActionInstantHandler.ts @@ -0,0 +1,21 @@ +/* + * 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/contentScriptCore.ts b/src/contentScript/contentScriptCore.ts index f9e9f67a0e..7905302f1d 100644 --- a/src/contentScript/contentScriptCore.ts +++ b/src/contentScript/contentScriptCore.ts @@ -74,6 +74,7 @@ export async function init(): Promise { void browser.runtime.sendMessage({ type: ENSURE_CONTENT_SCRIPT_READY }); // Update `sidePanel` + // TODO: VERIFY: This replaces the old "sidebarController:showSidebar emitting sidebarShowEvents" in `showSidebar` right? void renderPanelsIfVisible(); // Let the partner page know diff --git a/src/contentScript/messenger/api.ts b/src/contentScript/messenger/api.ts index 17cbff3c11..9e4390a10f 100644 --- a/src/contentScript/messenger/api.ts +++ b/src/contentScript/messenger/api.ts @@ -41,12 +41,14 @@ 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 showSidebar = getMethod("SHOW_SIDEBAR"); +export const hideSidebar = getMethod("HIDE_SIDEBAR"); export const getReservedSidebarEntries = getMethod( "GET_RESERVED_SIDEBAR_ENTRIES", ); export const updateSidebar = getNotifier("UPDATE_SIDEBAR"); export const sidebarWasLoaded = getNotifier("SIDEBAR_WAS_LOADED"); +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 a5c28970f9..a4ad610f6f 100644 --- a/src/contentScript/messenger/registration.ts +++ b/src/contentScript/messenger/registration.ts @@ -32,7 +32,11 @@ import { cancelForm, } from "@/contentScript/ephemeralFormProtocol"; import { + hideSidebar, + showSidebar, + rehydrateSidebar, removeExtensions as removeSidebars, + reloadSidebar, getReservedPanelEntries, sidebarWasLoaded, updateSidebar, @@ -105,6 +109,10 @@ declare global { UPDATE_SIDEBAR: typeof updateSidebar; SIDEBAR_WAS_LOADED: typeof sidebarWasLoaded; + REHYDRATE_SIDEBAR: typeof rehydrateSidebar; + SHOW_SIDEBAR: typeof showSidebar; + HIDE_SIDEBAR: typeof hideSidebar; + RELOAD_SIDEBAR: typeof reloadSidebar; REMOVE_SIDEBARS: typeof removeSidebars; INSERT_PANEL: typeof insertPanel; INSERT_BUTTON: typeof insertButton; @@ -171,6 +179,10 @@ export default function registerMessenger(): void { UPDATE_SIDEBAR: updateSidebar, SIDEBAR_WAS_LOADED: sidebarWasLoaded, + REHYDRATE_SIDEBAR: rehydrateSidebar, + SHOW_SIDEBAR: showSidebar, + HIDE_SIDEBAR: hideSidebar, + RELOAD_SIDEBAR: reloadSidebar, REMOVE_SIDEBARS: removeSidebars, INSERT_PANEL: insertPanel, INSERT_BUTTON: insertButton, diff --git a/src/contentScript/sidebarActivation.ts b/src/contentScript/sidebarActivation.ts index c68cc01ec7..9942aa51dc 100644 --- a/src/contentScript/sidebarActivation.ts +++ b/src/contentScript/sidebarActivation.ts @@ -21,7 +21,7 @@ import { showSidebar, hideModActivationInSidebar, showModActivationInSidebar, - onSidePanelClosure, + sidePanelClosureSignal, } from "@/contentScript/sidebarController"; import { isLinked } from "@/auth/token"; import { @@ -55,17 +55,13 @@ async function getInProgressModActivation(): Promise { async function showSidebarActivationForMods( modIds: RegistryId[], ): Promise { - const controller = new AbortController(); - await showSidebar(); await showModActivationInSidebar({ modIds, heading: "Activating", }); - onSidePanelClosure(controller); - - controller.signal.addEventListener("abort", () => { + sidePanelClosureSignal().addEventListener("abort", () => { void hideModActivationInSidebar(); }); } diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index d65807236d..a53abe93a5 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -15,14 +15,16 @@ * 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"; import sidebarInThisTab from "@/sidebar/messenger/api"; import { isEmpty, throttle } from "lodash"; import { SimpleEventTarget } from "@/utils/SimpleEventTarget"; +import * as sidebarMv2 from "./sidebarDomControllerLite"; import { type Except } from "type-fest"; -import { RunReason, type RunArgs } from "@/types/runtimeTypes"; +import { type RunArgs, RunReason } from "@/types/runtimeTypes"; import { type UUID } from "@/types/stringTypes"; import { type RegistryId } from "@/types/registryTypes"; import { type ModComponentRef } from "@/types/modComponentTypes"; @@ -36,13 +38,21 @@ import type { } from "@/types/sidebarTypes"; import { getTemporaryPanelSidebarEntries } from "@/bricks/transformers/temporaryInfo/temporaryPanelProtocol"; import { getFormPanelSidebarEntries } from "@/contentScript/ephemeralFormProtocol"; -import { - isSidePanelOpen, - isSidePanelOpenSync, -} from "@/sidebar/sidePanel/messenger/api"; +import * as sidePanel from "@/sidebar/sidePanel/messenger/api"; +import { memoizeUntilSettled, logPromiseDuration } from "@/utils/promiseUtils"; +import { waitAnimationFrame } from "@/utils/domUtils"; import { getTimedSequence } from "@/types/helpers"; import { backgroundTarget, getMethod } from "webext-messenger"; -import { memoizeUntilSettled } from "@/utils/promiseUtils"; +import { isMV3 } from "@/mv3/api"; + +// eslint-disable-next-line local-rules/persistBackgroundData -- Function +export const isSidePanelOpen = isMV3() + ? sidePanel.isSidePanelOpen + : sidebarMv2.isSidebarFrameVisible; +// eslint-disable-next-line local-rules/persistBackgroundData -- Function +export const isSidePanelOpenSync = isMV3() + ? sidePanel.isSidePanelOpenSync + : sidebarMv2.isSidebarFrameVisible; // - 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) @@ -78,9 +88,18 @@ 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, drop "SIDEBAR_PING" string - await getMethod("SHOW_MY_SIDE_PANEL" as "SIDEBAR_PING", backgroundTarget)(); - await pingSidebar(); + if (isMV3()) { + // 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)(); + } else if (!sidebarMv2.isSidebarFrameVisible()) { + sidebarMv2.insertSidebarFrame(); + } + + try { + await pingSidebar(); + } catch (error) { + throw new Error("The sidebar did not respond in time", { cause: error }); + } } /** @@ -100,6 +119,38 @@ export async function activateExtensionPanel(extensionId: UUID): 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: sidebarMv2.isSidebarFrameVisible(), + }); + + reportEvent(Events.SIDEBAR_HIDE); + sidebarMv2.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(); +} + /** * @param activateOptions options controlling the visible panel in the sidebar */ @@ -418,7 +469,8 @@ export function getReservedPanelEntries(): { // 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 { +export function sidePanelClosureSignal(): AbortSignal { + const controller = new AbortController(); expectContext("contentScript"); window.addEventListener( "resize", @@ -429,4 +481,5 @@ export function onSidePanelClosure(controller: AbortController): void { }, { signal: controller.signal }, ); + return controller.signal; } diff --git a/src/contentScript/sidebarDomControllerLite.ts b/src/contentScript/sidebarDomControllerLite.ts new file mode 100644 index 0000000000..d7070b998b --- /dev/null +++ b/src/contentScript/sidebarDomControllerLite.ts @@ -0,0 +1,160 @@ +/* + * 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 + */ + +import { MAX_Z_INDEX, PANEL_FRAME_ID } from "@/domConstants"; +import shadowWrap from "@/utils/shadowWrap"; +import { expectContext } from "@/utils/expectContext"; +import { uuidv4 } from "@/types/helpers"; + +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 { + return Boolean(getSidebar()); +} + +/** Removes the element; Returns false if no element was found */ +export function removeSidebarFrame(): 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 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; +} + +/** + * Toggle the sidebar frame. Returns true if the sidebar is now visible, false otherwise. + */ +export function toggleSidebarFrame(): boolean { + console.debug("sidebarDomControllerLite:toggleSidebarFrame", { + isSidebarFrameVisible: isSidebarFrameVisible(), + }); + + if (isSidebarFrameVisible()) { + removeSidebarFrame(); + return false; + } + + insertSidebarFrame(); + return true; +} diff --git a/src/contrib/automationanywhere/aaFrameProtocol.ts b/src/contrib/automationanywhere/aaFrameProtocol.ts index 2ce7530e94..eda9912279 100644 --- a/src/contrib/automationanywhere/aaFrameProtocol.ts +++ b/src/contrib/automationanywhere/aaFrameProtocol.ts @@ -18,7 +18,7 @@ import { type UnknownObject } from "@/types/objectTypes"; import { expectContext } from "@/utils/expectContext"; import { getCopilotHostData } from "@/contentScript/messenger/api"; -import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; /** * Runtime event type for setting Co-Pilot data @@ -126,7 +126,8 @@ export async function initCopilotMessenger(): Promise { }); // Fetch the current data from the content script when the frame loads - const data = await getCopilotHostData(getAssociatedTarget()); + const frame = await getTopFrameFromSidebar(); + const data = await getCopilotHostData(frame); console.debug("Setting initial Co-Pilot data", { location: window.location.href, data, diff --git a/src/domConstants.ts b/src/domConstants.ts index b865278e90..aea97b11a1 100644 --- a/src/domConstants.ts +++ b/src/domConstants.ts @@ -23,6 +23,8 @@ 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"; @@ -36,6 +38,7 @@ 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/pageEditor/panes/insert/useAutoInsert.ts b/src/pageEditor/panes/insert/useAutoInsert.ts index 779d7b2367..df765f5ee2 100644 --- a/src/pageEditor/panes/insert/useAutoInsert.ts +++ b/src/pageEditor/panes/insert/useAutoInsert.ts @@ -5,13 +5,17 @@ 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 { updateDynamicElement } from "@/contentScript/messenger/api"; +import { + showSidebar, + updateDynamicElement, +} from "@/contentScript/messenger/api"; import { openSidePanel } from "@/sidebar/sidePanel/messenger/api"; import reportEvent from "@/telemetry/reportEvent"; import { Events } from "@/telemetry/events"; import { type StarterBrickType } from "@/types/starterBrickTypes"; import { ADAPTERS } from "@/pageEditor/starterBricks/adapter"; import notify from "@/utils/notify"; +import { isMV3 } from "@/mv3/api"; const { addElement, toggleInsert } = actions; diff --git a/src/sidebar/Header.tsx b/src/sidebar/Header.tsx index c4ed48fa01..6859f6f187 100644 --- a/src/sidebar/Header.tsx +++ b/src/sidebar/Header.tsx @@ -19,16 +19,39 @@ import React from "react"; import styles from "./ConnectedSidebar.module.scss"; import { Button } from "react-bootstrap"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faCog } from "@fortawesome/free-solid-svg-icons"; +import { faAngleDoubleRight, faCog } from "@fortawesome/free-solid-svg-icons"; +import { hideSidebar } from "@/contentScript/messenger/api"; 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(); + /* The button doesn't work after invalidation #2359 */ + /* In MV3, Chrome offers a native Close button */ + const showCloseButton = !useContextInvalidated() && !isMV3(); return (
+ {showCloseButton && ( + + )} {showSidebarLogo && (
Please close and re-open the sidebar panel.

-
diff --git a/src/sidebar/messenger/api.ts b/src/sidebar/messenger/api.ts index ba989c90d6..15a9a41f6e 100644 --- a/src/sidebar/messenger/api.ts +++ b/src/sidebar/messenger/api.ts @@ -21,12 +21,9 @@ import { getMethod, getNotifier, getThisFrame } from "webext-messenger"; const target = { page: "/sidebar.html" }; -// TODO: move to contentScrpt/sidePanel/messenger/api.ts -// 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())`. + // TODO: Drop if this is ever implemented https://github.com/pixiebrix/webext-messenger/issues/193 // eslint-disable-next-line promise/prefer-await-to-then void getThisFrame().then((frame) => { target.page += "?tabId=" + frame.tabId; @@ -38,7 +35,7 @@ const sidebarInThisTab = { activatePanel: getMethod("SIDEBAR_ACTIVATE_PANEL", target), showForm: getMethod("SIDEBAR_SHOW_FORM", target), hideForm: getMethod("SIDEBAR_HIDE_FORM", target), - /** @deprecated Only from the content script. Use this in the content script: import {pingSidebar} from '@/contentScript/sidebarController'; */ + /** @deprecated Deprecated only from the content script. Use this in the content script: import {pingSidebar} from '@/contentScript/sidebarController'; */ pingSidebar: getMethod("SIDEBAR_PING", target), close: getNotifier("SIDEBAR_CLOSE", target), reload: getNotifier("SIDEBAR_RELOAD", target), diff --git a/src/sidebar/modLauncher/ModLauncher.tsx b/src/sidebar/modLauncher/ModLauncher.tsx index 9903e1e1f8..1398aa09c6 100644 --- a/src/sidebar/modLauncher/ModLauncher.tsx +++ b/src/sidebar/modLauncher/ModLauncher.tsx @@ -46,7 +46,6 @@ const ModLauncher: React.FunctionComponent = () => { }); const frame = await getTopFrameFromSidebar(); - showWalkthroughModal(frame); }} > diff --git a/src/sidebar/protocol.tsx b/src/sidebar/protocol.tsx index 621eb6b972..2cebc0a031 100644 --- a/src/sidebar/protocol.tsx +++ b/src/sidebar/protocol.tsx @@ -27,6 +27,8 @@ import { type FormDefinition } from "@/bricks/transformers/ephemeralForm/formTyp import { type UUID, type TimedSequence } from "@/types/stringTypes"; import { sortBy } from "lodash"; import { getTimedSequence } from "@/types/helpers"; +import { getTopLevelFrame } from "webext-messenger"; +import { hideSidebar } from "@/contentScript/messenger/api"; let lastMessageSeen = getTimedSequence(); // Track activate messages separately. The Sidebar App Redux state has special handling for these messages to account @@ -219,5 +221,10 @@ export async function hideActivateMods(sequence: TimedSequence): Promise { } export async function closeSelf(): Promise { - window.close(); + if (isMV3()) { + window.close(); + } else { + const topLevelFrame = await getTopLevelFrame(); + void hideSidebar(topLevelFrame); + } } diff --git a/src/sidebar/sidePanel/messenger/api.ts b/src/sidebar/sidePanel/messenger/api.ts index 0f842eee71..6dc9c7bd9e 100644 --- a/src/sidebar/sidePanel/messenger/api.ts +++ b/src/sidebar/sidePanel/messenger/api.ts @@ -25,6 +25,8 @@ import { isObject } from "@/utils/objectUtils"; import { expectContext } from "@/utils/expectContext"; import { type Target } from "webext-messenger"; import { getErrorMessage } from "@/errors/errorHelpers"; +import { showSidebar } from "@/contentScript/messenger/api"; +import { isMV3 } from "@/mv3/api"; function getAssociatedTabId(): number { expectContext("sidebar"); @@ -67,6 +69,10 @@ export async function isSidePanelOpen(): Promise { } export async function openSidePanel(tabId: number): Promise { + if (!isMV3()) { + return showSidebar({ tabId }); + } + // Simultaneously enable and open the side panel. // If we wait too long before calling .open(), we will lose the "user gesture" permission // There is no way to know whether the side panel is open yet, so we call it regardless. diff --git a/src/starterBricks/sidebarExtension.ts b/src/starterBricks/sidebarExtension.ts index 6474a90541..9b3398c8b8 100644 --- a/src/starterBricks/sidebarExtension.ts +++ b/src/starterBricks/sidebarExtension.ts @@ -36,6 +36,7 @@ import { sidebarShowEvents, updateHeading, upsertPanel, + isSidePanelOpen, } from "@/contentScript/sidebarController"; import Mustache from "mustache"; import { uuidv4 } from "@/types/helpers"; @@ -62,7 +63,6 @@ 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/messenger/api"; export type SidebarConfig = { heading: string; diff --git a/src/tinyPages/restrictedUrlPopup.html b/src/tinyPages/restrictedUrlPopup.html index b8369b0a24..4081f9d592 100644 --- a/src/tinyPages/restrictedUrlPopup.html +++ b/src/tinyPages/restrictedUrlPopup.html @@ -20,6 +20,13 @@ PixieBrix + From a1631e9299f3c5289a0a367c3aad497a4ff5de13 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Thu, 18 Jan 2024 16:06:21 +0800 Subject: [PATCH 04/30] Third pass --- src/background/browserAction.ts | 16 ++++---- .../temporaryInfo/DisplayTemporaryInfo.ts | 2 +- src/contentScript/messenger/registration.ts | 3 -- src/contentScript/sidebarController.tsx | 39 +++++++++++------- src/pageEditor/panes/insert/useAutoInsert.ts | 6 +-- src/sidebar/protocol.tsx | 7 ++-- src/sidebar/sidePanel.tsx | 17 +++++++- src/sidebar/sidePanel/messenger/api.ts | 41 ++++++------------- src/tsconfig.strictNullChecks.json | 3 ++ src/types/typeOnlyMessengerRegistration.ts | 38 +++++++++++++++++ src/utils/inference/markupInference.ts | 3 ++ src/utils/notify.tsx | 5 +++ webpack.config.mjs | 21 +++++++--- 13 files changed, 132 insertions(+), 69 deletions(-) create mode 100644 src/types/typeOnlyMessengerRegistration.ts diff --git a/src/background/browserAction.ts b/src/background/browserAction.ts index 7c94e8f721..6379164344 100644 --- a/src/background/browserAction.ts +++ b/src/background/browserAction.ts @@ -16,14 +16,14 @@ */ import { ensureContentScript } from "@/background/contentScript"; -import { rehydrateSidebar } from "@/contentScript/messenger/api"; +import { updateSidebar } 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 { openSidePanel } from "@/sidebar/sidePanel/messenger/api"; import { setActionPopup } from "webext-tools"; -import { getRestrictedPageMessage } from "./sidePanel.js"; +import { getRestrictedPageMessage } from "./sidePanel"; export default async function initBrowserAction(): Promise { if (!isMV3()) { @@ -69,11 +69,13 @@ async function _toggleSidebar(tabId: number, tabUrl: string): Promise { 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({ + const contentScriptTarget = { tabId, frameId: TOP_LEVEL_FRAME_ID, - }); + } as const; + + // Chrome adds automatically at document_idle, so it might not be ready yet when the user click the browser action + const contentScriptPromise = ensureContentScript(contentScriptTarget); try { await sidebarTogglePromise; @@ -86,9 +88,7 @@ async function _toggleSidebar(tabId: number, tabUrl: string): Promise { // 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, - }); + updateSidebar(contentScriptTarget); } async function handleBrowserAction(tab: Tab): Promise { diff --git a/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts b/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts index 84798f6bce..b6c86feb76 100644 --- a/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts +++ b/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts @@ -56,7 +56,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 { mergeSignals, onAbort } from "abort-utils"; +import { onAbort } from "abort-utils"; // Match naming of the sidebar panel extension point triggers export type RefreshTrigger = "manual" | "statechange"; diff --git a/src/contentScript/messenger/registration.ts b/src/contentScript/messenger/registration.ts index c315e424f9..78a4f97b57 100644 --- a/src/contentScript/messenger/registration.ts +++ b/src/contentScript/messenger/registration.ts @@ -34,7 +34,6 @@ import { import { hideSidebar, showSidebar, - rehydrateSidebar, removeExtensions as removeSidebars, reloadSidebar, getReservedPanelEntries, @@ -108,7 +107,6 @@ declare global { UPDATE_SIDEBAR: typeof updateSidebar; SIDEBAR_WAS_LOADED: typeof sidebarWasLoaded; - REHYDRATE_SIDEBAR: typeof rehydrateSidebar; SHOW_SIDEBAR: typeof showSidebar; HIDE_SIDEBAR: typeof hideSidebar; RELOAD_SIDEBAR: typeof reloadSidebar; @@ -175,7 +173,6 @@ export default function registerMessenger(): void { UPDATE_SIDEBAR: updateSidebar, SIDEBAR_WAS_LOADED: sidebarWasLoaded, - REHYDRATE_SIDEBAR: rehydrateSidebar, SHOW_SIDEBAR: showSidebar, HIDE_SIDEBAR: hideSidebar, RELOAD_SIDEBAR: reloadSidebar, diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index a53abe93a5..2bc7a4bd20 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"; @@ -39,17 +38,16 @@ import type { import { getTemporaryPanelSidebarEntries } from "@/bricks/transformers/temporaryInfo/temporaryPanelProtocol"; import { getFormPanelSidebarEntries } from "@/contentScript/ephemeralFormProtocol"; import * as sidePanel from "@/sidebar/sidePanel/messenger/api"; -import { memoizeUntilSettled, logPromiseDuration } from "@/utils/promiseUtils"; -import { waitAnimationFrame } from "@/utils/domUtils"; +import { memoizeUntilSettled } from "@/utils/promiseUtils"; import { getTimedSequence } from "@/types/helpers"; import { backgroundTarget, getMethod } from "webext-messenger"; import { isMV3 } from "@/mv3/api"; -// eslint-disable-next-line local-rules/persistBackgroundData -- Function +export const HIDE_SIDEBAR_EVENT_NAME = "pixiebrix:hideSidebar"; + export const isSidePanelOpen = isMV3() ? sidePanel.isSidePanelOpen : sidebarMv2.isSidebarFrameVisible; -// eslint-disable-next-line local-rules/persistBackgroundData -- Function export const isSidePanelOpenSync = isMV3() ? sidePanel.isSidePanelOpenSync : sidebarMv2.isSidebarFrameVisible; @@ -144,7 +142,7 @@ export async function reloadSidebar(): Promise { // Hide and reshow to force a full-refresh of the sidebar - if (isSidebarFrameVisible()) { + if (sidebarMv2.isSidebarFrameVisible()) { hideSidebar(); } @@ -472,14 +470,27 @@ export function getReservedPanelEntries(): { export function sidePanelClosureSignal(): AbortSignal { const controller = new AbortController(); expectContext("contentScript"); - window.addEventListener( - "resize", - () => { - if (isSidePanelOpenSync() === false) { + if (isMV3()) { + window.addEventListener( + "resize", + () => { + if (isSidePanelOpenSync() === false) { + controller.abort(); + } + }, + { signal: controller.signal }, + ); + } else { + window.addEventListener( + HIDE_SIDEBAR_EVENT_NAME, + () => { controller.abort(); - } - }, - { signal: controller.signal }, - ); + }, + { + signal: controller.signal, + }, + ); + } + return controller.signal; } diff --git a/src/pageEditor/panes/insert/useAutoInsert.ts b/src/pageEditor/panes/insert/useAutoInsert.ts index df765f5ee2..779d7b2367 100644 --- a/src/pageEditor/panes/insert/useAutoInsert.ts +++ b/src/pageEditor/panes/insert/useAutoInsert.ts @@ -5,17 +5,13 @@ 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 { openSidePanel } from "@/sidebar/sidePanel/messenger/api"; import reportEvent from "@/telemetry/reportEvent"; import { Events } from "@/telemetry/events"; import { type StarterBrickType } from "@/types/starterBrickTypes"; import { ADAPTERS } from "@/pageEditor/starterBricks/adapter"; import notify from "@/utils/notify"; -import { isMV3 } from "@/mv3/api"; const { addElement, toggleInsert } = actions; diff --git a/src/sidebar/protocol.tsx b/src/sidebar/protocol.tsx index 2cebc0a031..5998f715d0 100644 --- a/src/sidebar/protocol.tsx +++ b/src/sidebar/protocol.tsx @@ -27,8 +27,8 @@ import { type FormDefinition } from "@/bricks/transformers/ephemeralForm/formTyp import { type UUID, type TimedSequence } from "@/types/stringTypes"; import { sortBy } from "lodash"; import { getTimedSequence } from "@/types/helpers"; -import { getTopLevelFrame } from "webext-messenger"; -import { hideSidebar } from "@/contentScript/messenger/api"; +import { getMethod, getTopLevelFrame } from "webext-messenger"; +import { isMV3 } from "@/mv3/api"; let lastMessageSeen = getTimedSequence(); // Track activate messages separately. The Sidebar App Redux state has special handling for these messages to account @@ -225,6 +225,7 @@ export async function closeSelf(): Promise { window.close(); } else { const topLevelFrame = await getTopLevelFrame(); - void hideSidebar(topLevelFrame); + // Called via `getMethod` until we complete the strictNullChecks transition + void getMethod("HIDE_SIDEBAR")(topLevelFrame); } } diff --git a/src/sidebar/sidePanel.tsx b/src/sidebar/sidePanel.tsx index 83eeb5c7e1..ff4f020e29 100644 --- a/src/sidebar/sidePanel.tsx +++ b/src/sidebar/sidePanel.tsx @@ -19,13 +19,28 @@ import { expectContext } from "@/utils/expectContext"; import { + PING_SIDE_PANEL, getAssociatedTarget, - respondToPings, } from "@/sidebar/sidePanel/messenger/api"; import { sidebarWasLoaded } from "@/contentScript/messenger/api"; +import { isObject } from "@/utils/objectUtils"; expectContext("sidebar"); +// Do not use the messenger because it doesn't support retry-less messaging +// TODO: Drop after https://github.com/pixiebrix/webext-messenger/issues/59 +function respondToPings() { + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if ( + isObject(message) && + message.type === PING_SIDE_PANEL && + sender.tab?.id === getAssociatedTarget().tabId + ) { + sendResponse(true); + } + }); +} + export function initSidePanel() { respondToPings(); sidebarWasLoaded(getAssociatedTarget()); diff --git a/src/sidebar/sidePanel/messenger/api.ts b/src/sidebar/sidePanel/messenger/api.ts index 6dc9c7bd9e..deab88ab61 100644 --- a/src/sidebar/sidePanel/messenger/api.ts +++ b/src/sidebar/sidePanel/messenger/api.ts @@ -21,47 +21,30 @@ * to match that expectation and avoid lint issues. */ -import { isObject } from "@/utils/objectUtils"; -import { expectContext } from "@/utils/expectContext"; -import { type Target } from "webext-messenger"; +import { expectContext, forbidContext } from "@/utils/expectContext"; +import { getMethod, type Target } from "webext-messenger"; import { getErrorMessage } from "@/errors/errorHelpers"; -import { showSidebar } from "@/contentScript/messenger/api"; import { isMV3 } from "@/mv3/api"; -function getAssociatedTabId(): number { +export function getAssociatedTarget(): Target { expectContext("sidebar"); const tabId = new URLSearchParams(window.location.search).get("tabId"); - return Number(tabId); + return { tabId: Number(tabId), frameId: 0 }; } -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 -// TODO: Drop after https://github.com/pixiebrix/webext-messenger/issues/59 -export function respondToPings() { - expectContext("sidebar"); - chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - if ( - isObject(message) && - message.type === PING_MESSAGE && - sender.tab?.id === getAssociatedTabId() - ) { - sendResponse(true); - } - }); -} +export const PING_SIDE_PANEL = "PING_SIDE_PANEL"; export async function isSidePanelOpen(): Promise { - // Sync check where possible + forbidContext("sidebar", "The sidebar shouldn't check whether it's open"); + + // Sync check where possible, which is the content script if (isSidePanelOpenSync() === false) { return false; } try { - await chrome.runtime.sendMessage({ type: PING_MESSAGE }); + // Available from any page + await chrome.runtime.sendMessage({ type: PING_SIDE_PANEL }); return true; } catch { return false; @@ -70,7 +53,9 @@ export async function isSidePanelOpen(): Promise { export async function openSidePanel(tabId: number): Promise { if (!isMV3()) { - return showSidebar({ tabId }); + // Called via `getMethod` until we complete the strictNullChecks transition + await getMethod("SHOW_SIDEBAR")({ tabId }); + return; } // Simultaneously enable and open the side panel. diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index dcd7f93572..b67879dbc6 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -36,6 +36,7 @@ "./background/externalProtocol.ts", "./background/partnerTheme.ts", "./background/toolbarBadge.ts", + "./background/webextAlert.ts", "./background/setToolbarBadge.test.ts", "./bricks/available.ts", "./bricks/effects/cancel.ts", @@ -186,6 +187,7 @@ "./components/quickBar/utils.ts", "./components/selectionToolPopover/SelectionToolPopover.tsx", "./components/walkthroughModal/showWalkthroughModal.ts", + "./contentScript/browserActionInstantHandler.ts", "./contentScript/context.ts", "./contentScript/contextMenus.ts", "./contentScript/elementReference.ts", @@ -402,6 +404,7 @@ "./store/settings/settingsSlice.ts", "./store/settings/settingsStorage.ts", "./store/settings/settingsTypes.ts", + "./types/typeOnlyMessengerRegistration.ts", "./store/workshopSlice.ts", "./telemetry/deployments.ts", "./telemetry/dnt.ts", diff --git a/src/types/typeOnlyMessengerRegistration.ts b/src/types/typeOnlyMessengerRegistration.ts new file mode 100644 index 0000000000..a3f074d00e --- /dev/null +++ b/src/types/typeOnlyMessengerRegistration.ts @@ -0,0 +1,38 @@ +/* + * 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 provides Messenger to the strictNullChecks build. + * It must not be imported. + * The actual methods must be registered in the appropriate registration.ts file, + * this is enforced by typescript itself as long as this file is never imported. + * @see https://github.com/pixiebrix/pixiebrix-extension/issues/6526 + */ + +/* eslint-disable import/no-restricted-paths -- Type-only file */ + +import { + type hideSidebar, + type showSidebar, +} from "@/contentScript/sidebarController"; + +declare global { + interface MessengerMethods { + SHOW_SIDEBAR: typeof showSidebar; + HIDE_SIDEBAR: typeof hideSidebar; + } +} diff --git a/src/utils/inference/markupInference.ts b/src/utils/inference/markupInference.ts index 7108d98e25..c2c7bc269a 100644 --- a/src/utils/inference/markupInference.ts +++ b/src/utils/inference/markupInference.ts @@ -18,6 +18,7 @@ import { CONTENT_SCRIPT_READY_ATTRIBUTE, EXTENSION_POINT_DATA_ATTR, + PANEL_FRAME_ID, PIXIEBRIX_DATA_ATTR, } from "@/domConstants"; import { BUTTON_TAGS, UNIQUE_ATTRIBUTES } from "./selectorInference"; @@ -73,6 +74,8 @@ 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 { diff --git a/src/utils/notify.tsx b/src/utils/notify.tsx index 11210d91df..508494fca8 100644 --- a/src/utils/notify.tsx +++ b/src/utils/notify.tsx @@ -32,6 +32,9 @@ import { type Except, type RequireAtLeastOne } from "type-fest"; import { getErrorMessage } from "@/errors/errorHelpers"; import { merge, truncate } from "lodash"; +// While correct, the `sidebarDomControllerLite` name implies that it's a small, pure module and it's unlikely to cause issues +// eslint-disable-next-line import/no-restricted-paths +import { SIDEBAR_WIDTH_CSS_PROPERTY } from "@/contentScript/sidebarDomControllerLite"; import ErrorIcon from "@/icons/error.svg?loadAsComponent"; import WarningIcon from "@/icons/warning.svg?loadAsComponent"; @@ -168,6 +171,8 @@ export function showNotification({ const options: ToastOptions = { id, duration, + // Keep the notification centered on the document even when the sidebar is open + style: { marginLeft: `calc(var(${SIDEBAR_WIDTH_CSS_PROPERTY}, 0) * -1)` }, }; const component = ; diff --git a/webpack.config.mjs b/webpack.config.mjs index fe08b1a991..96c644ea8e 100644 --- a/webpack.config.mjs +++ b/webpack.config.mjs @@ -40,6 +40,10 @@ console.log("SOURCE_VERSION:", process.env.SOURCE_VERSION); console.log("SERVICE_URL:", process.env.SERVICE_URL); console.log("MARKETPLACE_URL:", process.env.MARKETPLACE_URL); console.log("CHROME_EXTENSION_ID:", process.env.CHROME_EXTENSION_ID); +console.log( + "ROLLBAR_BROWSER_ACCESS_TOKEN:", + process.env.ROLLBAR_BROWSER_ACCESS_TOKEN, +); if (!process.env.SOURCE_VERSION) { process.env.SOURCE_VERSION = execSync("git rev-parse --short HEAD") @@ -72,9 +76,12 @@ const isProd = (options) => options.mode === "production"; function mockHeavyDependencies() { if (process.env.DEV_SLIM.toLowerCase() === "true") { - console.warn("Mocking dependencies for development build: @/icons/list"); + console.warn( + "Mocking dependencies for development build: @/icons/list, uipath/robot", + ); return { "@/icons/list": path.resolve("src/__mocks__/@/icons/list"), + "@uipath/robot": path.resolve("src/__mocks__/@uipath/robot"), }; } } @@ -102,6 +109,7 @@ const createConfig = (env, options) => "background/background", "contentScript/contentScript", "contentScript/loadActivationEnhancements", + "contentScript/browserActionInstantHandler", "contentScript/setExtensionIdInApp", "pageEditor/pageEditor", "extensionConsole/options", @@ -171,8 +179,6 @@ const createConfig = (env, options) => // The sourcemap will be inlined if `undefined`. Only inlined sourcemaps work locally // https://bugs.chromium.org/p/chromium/issues/detail?id=974543 - // NOTE: Datadog requires .js.map as the extension: https://github.com/DataDog/datadog-ci/issues/870 - // The [file] already includes the js file extension filename: sourceMapPublicUrl && "[file].map[query]", }), @@ -214,8 +220,7 @@ const createConfig = (env, options) => REDUX_DEV_TOOLS: !isProd(options), NPM_PACKAGE_VERSION: process.env.npm_package_version, ENVIRONMENT: options.mode, - SOURCE_MAP_PUBLIC_PATH: - sourceMapPublicUrl ?? "extension://dynamichost/", + ROLLBAR_PUBLIC_PATH: sourceMapPublicUrl ?? "extension://dynamichost/", // Record telemetry events in development? DEV_EVENT_TELEMETRY: false, SANDBOX_LOGGING: false, @@ -227,7 +232,11 @@ const createConfig = (env, options) => CHROME_EXTENSION_ID: undefined, // If not found, "null" will leave the ENV unset in the bundle - // DataDog RUM/Logging + ROLLBAR_BROWSER_ACCESS_TOKEN: null, + GOOGLE_API_KEY: null, + GOOGLE_APP_ID: null, + + // DataDog RUM DATADOG_APPLICATION_ID: null, DATADOG_CLIENT_TOKEN: null, }), From 36ad1626d1022fd36501f30bc79ed5298ad66d22 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Thu, 18 Jan 2024 23:22:48 +0800 Subject: [PATCH 05/30] Fix tests? --- .../loadActivationEnhancementsCore.test.ts | 6 ++- src/contentScript/sidebarController.tsx | 4 +- src/sidebar/Tabs.test.tsx | 6 +-- .../__snapshots__/Header.test.tsx.snap | 40 +++++++++++++++++++ .../__snapshots__/SidebarBody.test.tsx.snap | 20 ++++++++++ .../activateMod/ActivateModPanel.test.tsx | 5 +-- src/starterBricks/sidebarExtension.test.ts | 10 ++--- 7 files changed, 75 insertions(+), 16 deletions(-) diff --git a/src/contentScript/loadActivationEnhancementsCore.test.ts b/src/contentScript/loadActivationEnhancementsCore.test.ts index fa81e0d850..5ec9789c5f 100644 --- a/src/contentScript/loadActivationEnhancementsCore.test.ts +++ b/src/contentScript/loadActivationEnhancementsCore.test.ts @@ -36,7 +36,11 @@ import { isLinked } from "@/auth/token"; import { array } from "cooky-cutter"; import { MARKETPLACE_URL } from "@/urlConstants"; -jest.mock("@/contentScript/sidebarController"); +jest.mock("@/contentScript/sidebarController", () => ({ + ...jest.requireActual("@/contentScript/sidebarController"), + showSidebar: jest.fn(), + showModActivationInSidebar: jest.fn(), +})); jest.mock("@/auth/token", () => ({ isLinked: jest.fn().mockResolvedValue(true), diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index 2bc7a4bd20..c3a329d6b3 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -465,12 +465,12 @@ export function getReservedPanelEntries(): { }; } -// 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 sidePanelClosureSignal(): AbortSignal { const controller = new AbortController(); expectContext("contentScript"); if (isMV3()) { + // 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 window.addEventListener( "resize", () => { diff --git a/src/sidebar/Tabs.test.tsx b/src/sidebar/Tabs.test.tsx index 6574f5b9da..5f082a7933 100644 --- a/src/sidebar/Tabs.test.tsx +++ b/src/sidebar/Tabs.test.tsx @@ -25,16 +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 sidebarInThisTab from "@/sidebar/messenger/api"; +import * as sidebarController from "@/sidebar/protocol"; import { eventKeyForEntry } from "@/sidebar/eventKeyUtils"; import { mockAllApiEndpoints } from "@/testUtils/appApiMock"; mockAllApiEndpoints(); -jest.spyOn(window, "close").mockImplementation(jest.fn()); +jest.mock("@/sidebar/protocol"); const cancelFormSpy = jest.spyOn(messengerApi, "cancelForm"); -const hideSidebarSpy = jest.spyOn(sidebarInThisTab, "close"); +const hideSidebarSpy = jest.spyOn(sidebarController, "closeSelf"); 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 62b78e9532..4e72e1039f 100644 --- a/src/sidebar/__snapshots__/Header.test.tsx.snap +++ b/src/sidebar/__snapshots__/Header.test.tsx.snap @@ -5,6 +5,26 @@ exports[`Header renders 1`] = `
+
@@ -45,6 +65,26 @@ 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 d41955c7a1..04c4b7a514 100644 --- a/src/sidebar/__snapshots__/SidebarBody.test.tsx.snap +++ b/src/sidebar/__snapshots__/SidebarBody.test.tsx.snap @@ -5,6 +5,26 @@ exports[`SidebarBody it renders 1`] = `
+
diff --git a/src/sidebar/activateMod/ActivateModPanel.test.tsx b/src/sidebar/activateMod/ActivateModPanel.test.tsx index 018e87e199..3ee5dc1f28 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 sidebarInThisTab from "@/sidebar/messenger/api"; +import * as messengerApi from "@/contentScript/messenger/api"; import ActivateMultipleModsPanel from "@/sidebar/activateMod/ActivateMultipleModsPanel"; import ErrorBoundary from "@/sidebar/SidebarErrorBoundary"; import { includesQuickBarStarterBrick } from "@/starterBricks/starterBrickModUtils"; @@ -63,8 +63,7 @@ const useRequiredModDefinitionsMock = jest.mocked(useRequiredModDefinitions); const checkModDefinitionPermissionsMock = jest.mocked( checkModDefinitionPermissions, ); - -const hideSidebarSpy = jest.spyOn(sidebarInThisTab, "close"); +const hideSidebarSpy = jest.spyOn(messengerApi, "hideSidebar"); jest.mock("@/starterBricks/starterBrickModUtils", () => { const actualUtils = jest.requireActual( diff --git a/src/starterBricks/sidebarExtension.test.ts b/src/starterBricks/sidebarExtension.test.ts index b855b7683c..22e35a7a3a 100644 --- a/src/starterBricks/sidebarExtension.test.ts +++ b/src/starterBricks/sidebarExtension.test.ts @@ -39,13 +39,9 @@ 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(); @@ -192,7 +188,7 @@ describe("sidebarExtension", () => { expect(rootReader.readCount).toBe(0); // Fake the sidebar being added to the page - isSidePanelOpenMock.mockResolvedValueOnce(true); + $(document.body).append(`
`); sidebarShowEvents.emit({ reason: RunReason.MANUAL }); await tick(); @@ -253,7 +249,7 @@ describe("sidebarExtension", () => { await extensionPoint.install(); // Fake the sidebar being added to the page - isSidePanelOpenMock.mockResolvedValueOnce(true); + $(document.body).append(`
`); sidebarShowEvents.emit({ reason: RunReason.MANUAL }); await tick(); From 8ffb7c283b9b081625de8f625edfae83a56ea771 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Thu, 18 Jan 2024 23:58:55 +0800 Subject: [PATCH 06/30] knip --- knip.mjs | 2 ++ src/background/sidePanel.ts | 1 + src/contentScript/messenger/api.ts | 3 ++- src/contentScript/sidebarController.tsx | 6 ++++-- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/knip.mjs b/knip.mjs index ccd9aa4050..0a9a4dee85 100644 --- a/knip.mjs +++ b/knip.mjs @@ -26,6 +26,8 @@ const knipConfig = { "scripts/manifest.mjs", // Content script entry point, init() is dynamically imported in src/contentScript/contentScript.ts "src/contentScript/contentScriptCore.ts", + // Type-only strictNullChecks helper + "src/types/typeOnlyMessengerRegistration.ts", ], project: ["src/**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}"], // https://knip.dev/guides/handling-issues#mocks-and-other-implicit-imports diff --git a/src/background/sidePanel.ts b/src/background/sidePanel.ts index 2944423c65..dc2c27a4ae 100644 --- a/src/background/sidePanel.ts +++ b/src/background/sidePanel.ts @@ -53,6 +53,7 @@ export async function showMySidePanel(this: MessengerMeta): Promise { // TODO: Drop if this is ever implemented: https://github.com/w3c/webextensions/issues/515 export async function initSidePanel(): Promise { // TODO: Drop this once the popover URL behavior is merged into sidebar.html + // https://github.com/pixiebrix/pixiebrix-extension/issues/7364 chrome.tabs.onUpdated.addListener((tabId, changeInfo) => { if (changeInfo.url) { void chrome.sidePanel.setOptions({ diff --git a/src/contentScript/messenger/api.ts b/src/contentScript/messenger/api.ts index 249c87144e..91401802c5 100644 --- a/src/contentScript/messenger/api.ts +++ b/src/contentScript/messenger/api.ts @@ -41,7 +41,8 @@ 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 showSidebar = getMethod("SHOW_SIDEBAR"); +// Unused, called via local getMethod() due to strictNullChecks +// export const showSidebar = getMethod("SHOW_SIDEBAR"); export const hideSidebar = getMethod("HIDE_SIDEBAR"); export const getReservedSidebarEntries = getMethod( "GET_RESERVED_SIDEBAR_ENTRIES", diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index c3a329d6b3..24002f1119 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -43,12 +43,14 @@ import { getTimedSequence } from "@/types/helpers"; import { backgroundTarget, getMethod } from "webext-messenger"; import { isMV3 } from "@/mv3/api"; -export const HIDE_SIDEBAR_EVENT_NAME = "pixiebrix:hideSidebar"; +const HIDE_SIDEBAR_EVENT_NAME = "pixiebrix:hideSidebar"; export const isSidePanelOpen = isMV3() ? sidePanel.isSidePanelOpen : sidebarMv2.isSidebarFrameVisible; -export const isSidePanelOpenSync = isMV3() + +// eslint-disable-next-line local-rules/persistBackgroundData -- Function +const isSidePanelOpenSync = isMV3() ? sidePanel.isSidePanelOpenSync : sidebarMv2.isSidebarFrameVisible; From 40287e9ca343ea10ab0a34eea20223f96dc671b7 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Fri, 19 Jan 2024 00:33:06 +0800 Subject: [PATCH 07/30] Discard changes to webpack.config.mjs --- webpack.config.mjs | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/webpack.config.mjs b/webpack.config.mjs index 96c644ea8e..80448fd930 100644 --- a/webpack.config.mjs +++ b/webpack.config.mjs @@ -40,10 +40,6 @@ console.log("SOURCE_VERSION:", process.env.SOURCE_VERSION); console.log("SERVICE_URL:", process.env.SERVICE_URL); console.log("MARKETPLACE_URL:", process.env.MARKETPLACE_URL); console.log("CHROME_EXTENSION_ID:", process.env.CHROME_EXTENSION_ID); -console.log( - "ROLLBAR_BROWSER_ACCESS_TOKEN:", - process.env.ROLLBAR_BROWSER_ACCESS_TOKEN, -); if (!process.env.SOURCE_VERSION) { process.env.SOURCE_VERSION = execSync("git rev-parse --short HEAD") @@ -76,12 +72,9 @@ const isProd = (options) => options.mode === "production"; function mockHeavyDependencies() { if (process.env.DEV_SLIM.toLowerCase() === "true") { - console.warn( - "Mocking dependencies for development build: @/icons/list, uipath/robot", - ); + console.warn("Mocking dependencies for development build: @/icons/list"); return { "@/icons/list": path.resolve("src/__mocks__/@/icons/list"), - "@uipath/robot": path.resolve("src/__mocks__/@uipath/robot"), }; } } @@ -179,6 +172,8 @@ const createConfig = (env, options) => // The sourcemap will be inlined if `undefined`. Only inlined sourcemaps work locally // https://bugs.chromium.org/p/chromium/issues/detail?id=974543 + // NOTE: Datadog requires .js.map as the extension: https://github.com/DataDog/datadog-ci/issues/870 + // The [file] already includes the js file extension filename: sourceMapPublicUrl && "[file].map[query]", }), @@ -220,7 +215,8 @@ const createConfig = (env, options) => REDUX_DEV_TOOLS: !isProd(options), NPM_PACKAGE_VERSION: process.env.npm_package_version, ENVIRONMENT: options.mode, - ROLLBAR_PUBLIC_PATH: sourceMapPublicUrl ?? "extension://dynamichost/", + SOURCE_MAP_PUBLIC_PATH: + sourceMapPublicUrl ?? "extension://dynamichost/", // Record telemetry events in development? DEV_EVENT_TELEMETRY: false, SANDBOX_LOGGING: false, @@ -232,11 +228,7 @@ const createConfig = (env, options) => CHROME_EXTENSION_ID: undefined, // If not found, "null" will leave the ENV unset in the bundle - ROLLBAR_BROWSER_ACCESS_TOKEN: null, - GOOGLE_API_KEY: null, - GOOGLE_APP_ID: null, - - // DataDog RUM + // DataDog RUM/Logging DATADOG_APPLICATION_ID: null, DATADOG_CLIENT_TOKEN: null, }), From 18814af8068cde3178c7b041f78ae67425f29ff8 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Fri, 19 Jan 2024 00:35:23 +0800 Subject: [PATCH 08/30] Lint --- src/bricks/effects/sidebar.ts | 4 ++-- src/bricks/renderers/customForm.ts | 2 +- src/bricks/transformers/brickFactory.ts | 4 +++- src/bricks/transformers/ephemeralForm/formTransformer.ts | 3 ++- .../transformers/temporaryInfo/DisplayTemporaryInfo.ts | 2 +- src/components/documentBuilder/render/BlockElement.tsx | 2 +- src/components/documentBuilder/render/ButtonElement.tsx | 2 +- src/contentScript/messenger/registration.ts | 8 +++++--- src/contentScript/sidebarActivation.ts | 2 +- src/contrib/automationanywhere/aaFrameProtocol.ts | 2 +- src/mv3/sidePanelMigration.ts | 2 ++ src/sidebar/ConnectedSidebar.tsx | 2 +- src/sidebar/PanelBody.tsx | 2 +- src/sidebar/Tabs.tsx | 2 +- src/sidebar/activateMod/ActivateModPanel.tsx | 2 +- src/sidebar/sidePanel/messenger/api.ts | 2 ++ src/sidebar/sidebarSlice.ts | 2 +- src/sidebar/useHideEmptySidebar.ts | 4 ++-- 18 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/bricks/effects/sidebar.ts b/src/bricks/effects/sidebar.ts index 3ffde1ed4d..a1ad8e5a66 100644 --- a/src/bricks/effects/sidebar.ts +++ b/src/bricks/effects/sidebar.ts @@ -24,10 +24,10 @@ import { showSidebar, } from "@/contentScript/sidebarController"; import { showMySidePanel } from "@/background/messenger/api"; -import { propertiesToSchema } from "@/validators/generic"; -import { logPromiseDuration } from "@/utils/promiseUtils"; import sidebarInThisTab from "@/sidebar/messenger/api"; import { isMV3 } from "@/mv3/api"; +import { propertiesToSchema } from "@/validators/generic"; +import { logPromiseDuration } from "@/utils/promiseUtils"; export class ShowSidebar extends EffectABC { constructor() { diff --git a/src/bricks/renderers/customForm.ts b/src/bricks/renderers/customForm.ts index d99b2081b5..b009003029 100644 --- a/src/bricks/renderers/customForm.ts +++ b/src/bricks/renderers/customForm.ts @@ -25,6 +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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; import { type UUID } from "@/types/stringTypes"; import { type SanitizedIntegrationConfig } from "@/integrations/integrationTypes"; import { @@ -46,7 +47,6 @@ import { ensureJsonObject, isObject } from "@/utils/objectUtils"; import { getOutputReference, validateOutputKey } from "@/runtime/runtimeTypes"; import { type BrickConfig } from "@/bricks/types"; import { isExpression } from "@/utils/expressionUtils"; -import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; interface DatabaseResult { success: boolean; diff --git a/src/bricks/transformers/brickFactory.ts b/src/bricks/transformers/brickFactory.ts index 1090855158..4a48d18398 100644 --- a/src/bricks/transformers/brickFactory.ts +++ b/src/bricks/transformers/brickFactory.ts @@ -344,7 +344,9 @@ class UserDefinedBrick extends BrickABC { // renderer. The caller can't run the whole brick in the contentScript because renderers can return React // Components which can't be serialized across messenger boundaries. - // TODO: call top-level contentScript directly after https://github.com/pixiebrix/webext-messenger/issues/72 + // This code can be run either in the sidebar or in a modal. + // The modal is always an iframe in the same tab, + // but the sidebar varies. This code handles the 3 cases. const topLevelFrame = isBrowserSidebar() ? await getTopFrameFromSidebar() : await getTopLevelFrame(); diff --git a/src/bricks/transformers/ephemeralForm/formTransformer.ts b/src/bricks/transformers/ephemeralForm/formTransformer.ts index 4fe062cadf..fbc57d594a 100644 --- a/src/bricks/transformers/ephemeralForm/formTransformer.ts +++ b/src/bricks/transformers/ephemeralForm/formTransformer.ts @@ -35,6 +35,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 { onAbort } from "abort-utils"; // 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 @@ -167,7 +168,7 @@ export class FormTransformer extends TransformerABC { }); // Two-way binding between sidebar and form. Listen for the user (or an action) closing the sidebar - sidePanelClosureSignal(); + onAbort(sidePanelClosureSignal(), 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 b6c86feb76..ea32cf5214 100644 --- a/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts +++ b/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts @@ -24,10 +24,10 @@ import { import { expectContext } from "@/utils/expectContext"; import { showSidebar, + sidePanelClosureSignal, hideTemporarySidebarPanel, showTemporarySidebarPanel, updateTemporarySidebarPanel, - sidePanelClosureSignal, } from "@/contentScript/sidebarController"; import { type PanelPayload, diff --git a/src/components/documentBuilder/render/BlockElement.tsx b/src/components/documentBuilder/render/BlockElement.tsx index a489091170..3e74c47fd3 100644 --- a/src/components/documentBuilder/render/BlockElement.tsx +++ b/src/components/documentBuilder/render/BlockElement.tsx @@ -26,10 +26,10 @@ import apiVersionOptions from "@/runtime/apiVersionOptions"; import { serializeError } from "serialize-error"; import { type DynamicPath } from "@/components/documentBuilder/documentBuilderTypes"; import { mapPathToTraceBranches } from "@/components/documentBuilder/utils"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; import { type PanelContext } from "@/types/sidebarTypes"; import { type RendererRunPayload } from "@/types/rendererTypes"; import useAsyncState from "@/hooks/useAsyncState"; -import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; type BlockElementProps = { pipeline: BrickPipeline; diff --git a/src/components/documentBuilder/render/ButtonElement.tsx b/src/components/documentBuilder/render/ButtonElement.tsx index 790eafb930..815c4a4721 100644 --- a/src/components/documentBuilder/render/ButtonElement.tsx +++ b/src/components/documentBuilder/render/ButtonElement.tsx @@ -24,12 +24,12 @@ 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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; import { getRootCause, hasSpecificErrorCause } from "@/errors/errorHelpers"; import { SubmitPanelAction } from "@/bricks/errors"; import cx from "classnames"; import { boolean } from "@/utils/typeUtils"; import { mapPathToTraceBranches } from "@/components/documentBuilder/utils"; -import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; type ButtonElementProps = Except & { onClick: BrickPipeline; diff --git a/src/contentScript/messenger/registration.ts b/src/contentScript/messenger/registration.ts index 78a4f97b57..e73d021a4c 100644 --- a/src/contentScript/messenger/registration.ts +++ b/src/contentScript/messenger/registration.ts @@ -34,11 +34,11 @@ import { import { hideSidebar, showSidebar, + sidebarWasLoaded, + updateSidebar, removeExtensions as removeSidebars, reloadSidebar, getReservedPanelEntries, - sidebarWasLoaded, - updateSidebar, } from "@/contentScript/sidebarController"; import { insertPanel } from "@/contentScript/pageEditor/insertPanel"; import { insertButton } from "@/contentScript/pageEditor/insertButton"; @@ -103,14 +103,15 @@ declare global { TOGGLE_QUICK_BAR: typeof toggleQuickBar; HANDLE_MENU_ACTION: typeof handleMenuAction; - GET_RESERVED_SIDEBAR_ENTRIES: typeof getReservedPanelEntries; UPDATE_SIDEBAR: typeof updateSidebar; SIDEBAR_WAS_LOADED: typeof sidebarWasLoaded; SHOW_SIDEBAR: typeof showSidebar; HIDE_SIDEBAR: typeof hideSidebar; RELOAD_SIDEBAR: typeof reloadSidebar; + GET_RESERVED_SIDEBAR_ENTRIES: typeof getReservedPanelEntries; REMOVE_SIDEBARS: typeof removeSidebars; + INSERT_PANEL: typeof insertPanel; INSERT_BUTTON: typeof insertButton; @@ -177,6 +178,7 @@ export default function registerMessenger(): void { HIDE_SIDEBAR: hideSidebar, RELOAD_SIDEBAR: reloadSidebar, REMOVE_SIDEBARS: removeSidebars, + INSERT_PANEL: insertPanel, INSERT_BUTTON: insertButton, diff --git a/src/contentScript/sidebarActivation.ts b/src/contentScript/sidebarActivation.ts index 9942aa51dc..770293f2eb 100644 --- a/src/contentScript/sidebarActivation.ts +++ b/src/contentScript/sidebarActivation.ts @@ -19,9 +19,9 @@ import { type RegistryId } from "@/types/registryTypes"; import { isRegistryId } from "@/types/helpers"; import { showSidebar, + sidePanelClosureSignal, hideModActivationInSidebar, showModActivationInSidebar, - sidePanelClosureSignal, } from "@/contentScript/sidebarController"; import { isLinked } from "@/auth/token"; import { diff --git a/src/contrib/automationanywhere/aaFrameProtocol.ts b/src/contrib/automationanywhere/aaFrameProtocol.ts index eda9912279..0b1434b2ed 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 { getCopilotHostData } from "@/contentScript/messenger/api"; import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getCopilotHostData } from "@/contentScript/messenger/api"; /** * Runtime event type for setting Co-Pilot data diff --git a/src/mv3/sidePanelMigration.ts b/src/mv3/sidePanelMigration.ts index 62de1f7091..8ebd8da205 100644 --- a/src/mv3/sidePanelMigration.ts +++ b/src/mv3/sidePanelMigration.ts @@ -24,3 +24,5 @@ import { isMV3 } from "./api"; export const getTopFrameFromSidebar = isMV3() ? getAssociatedTarget : getTopLevelFrame; + +// TODO: Add openSidePanel? diff --git a/src/sidebar/ConnectedSidebar.tsx b/src/sidebar/ConnectedSidebar.tsx index 716b1f0545..ebfca5f51b 100644 --- a/src/sidebar/ConnectedSidebar.tsx +++ b/src/sidebar/ConnectedSidebar.tsx @@ -42,9 +42,9 @@ import { ensureExtensionPointsInstalled, getReservedSidebarEntries, } from "@/contentScript/messenger/api"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; import useAsyncEffect from "use-async-effect"; import activateLinkClickHandler from "@/activation/activateLinkClickHandler"; -import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; /** * Listeners to update the Sidebar's Redux state upon receiving messages from the contentScript. diff --git a/src/sidebar/PanelBody.tsx b/src/sidebar/PanelBody.tsx index a0137d954e..ee4daf1b50 100644 --- a/src/sidebar/PanelBody.tsx +++ b/src/sidebar/PanelBody.tsx @@ -46,9 +46,9 @@ import DelayedRender from "@/components/DelayedRender"; import { runHeadlessPipeline } from "@/contentScript/messenger/api"; import { uuidv4 } from "@/types/helpers"; import apiVersionOptions from "@/runtime/apiVersionOptions"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; import { type DynamicPath } from "@/components/documentBuilder/documentBuilderTypes"; import { mapPathToTraceBranches } from "@/components/documentBuilder/utils"; -import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; // Used for the loading message // import cx from "classnames"; diff --git a/src/sidebar/Tabs.tsx b/src/sidebar/Tabs.tsx index 1c5de2c602..efe2dbd1bf 100644 --- a/src/sidebar/Tabs.tsx +++ b/src/sidebar/Tabs.tsx @@ -57,9 +57,9 @@ import { selectEventData } from "@/telemetry/deployments"; import ErrorBoundary from "@/sidebar/SidebarErrorBoundary"; import { TemporaryPanelTabPane } from "./TemporaryPanelTabPane"; import { MOD_LAUNCHER } from "@/sidebar/modLauncher/constants"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; import { cancelForm } from "@/contentScript/messenger/api"; import { useHideEmptySidebar } from "@/sidebar/useHideEmptySidebar"; -import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; const ActivateModPanel = lazy( async () => diff --git a/src/sidebar/activateMod/ActivateModPanel.tsx b/src/sidebar/activateMod/ActivateModPanel.tsx index 407569bef5..be1d4f3ecf 100644 --- a/src/sidebar/activateMod/ActivateModPanel.tsx +++ b/src/sidebar/activateMod/ActivateModPanel.tsx @@ -30,6 +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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; import cx from "classnames"; import { isEmpty } from "lodash"; import ActivateModInputs from "@/sidebar/activateMod/ActivateModInputs"; @@ -52,7 +53,6 @@ import { type ModDefinition } from "@/types/modDefinitionTypes"; import { openShortcutsTab, SHORTCUTS_URL } from "@/utils/extensionUtils"; import Markdown from "@/components/Markdown"; import { getModActivationInstructions } from "@/utils/modUtils"; -import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; const { actions } = sidebarSlice; diff --git a/src/sidebar/sidePanel/messenger/api.ts b/src/sidebar/sidePanel/messenger/api.ts index deab88ab61..331e721c1f 100644 --- a/src/sidebar/sidePanel/messenger/api.ts +++ b/src/sidebar/sidePanel/messenger/api.ts @@ -67,6 +67,8 @@ export async function openSidePanel(tabId: number): Promise { }); try { + // TODO: Implement toggle, but I don't think it's possible: + // https://github.com/pixiebrix/pixiebrix-extension/issues/7327 await chrome.sidePanel.open({ tabId }); } catch (error) { // In some cases, `openSidePanel` is called as a precaution and it might work if diff --git a/src/sidebar/sidebarSlice.ts b/src/sidebar/sidebarSlice.ts index fdc784612c..5fef57cd57 100644 --- a/src/sidebar/sidebarSlice.ts +++ b/src/sidebar/sidebarSlice.ts @@ -35,13 +35,13 @@ import { resolveTemporaryPanel, } from "@/contentScript/messenger/api"; import { partition, remove, sortBy } from "lodash"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; import { type SubmitPanelAction } from "@/bricks/errors"; import { castDraft, type Draft } from "immer"; import { localStorage } from "redux-persist-webextension-storage"; import { type StorageInterface } from "@/store/StorageInterface"; import { getVisiblePanelCount } from "@/sidebar/utils"; import { MOD_LAUNCHER } from "@/sidebar/modLauncher/constants"; -import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; const emptySidebarState: SidebarState = { panels: [], diff --git a/src/sidebar/useHideEmptySidebar.ts b/src/sidebar/useHideEmptySidebar.ts index c198fccf18..c087214ea1 100644 --- a/src/sidebar/useHideEmptySidebar.ts +++ b/src/sidebar/useHideEmptySidebar.ts @@ -17,14 +17,14 @@ import useAsyncEffect from "use-async-effect"; import { getReservedSidebarEntries } from "@/contentScript/messenger/api"; +import { closeSelf } from "@/sidebar/protocol"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; import { useSelector } from "react-redux"; import { selectClosedTabs, selectVisiblePanelCount, } from "@/sidebar/sidebarSelectors"; import { eventKeyForEntry } from "@/sidebar/eventKeyUtils"; -import { closeSelf } from "@/sidebar/protocol"; -import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; /** * Hide the sidebar if there are no visible panels. We use this to close the sidebar if the user closes all panels. From f66b6a40eaef8551194afabe0a459d5e47b437e2 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Fri, 19 Jan 2024 14:03:42 +0800 Subject: [PATCH 09/30] Move `openSidePanel` to `sidePanelMigration` --- src/background/browserAction.ts | 2 +- src/background/sidePanel.ts | 2 +- src/mv3/sidePanelMigration.ts | 12 +++++++++--- src/pageEditor/panes/insert/useAutoInsert.ts | 2 +- .../sidebar/ActivatedModComponentListItem.tsx | 2 +- .../sidebar/DynamicModComponentListItem.tsx | 2 +- src/sidebar/sidePanel/messenger/api.ts | 12 +++--------- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/background/browserAction.ts b/src/background/browserAction.ts index 6379164344..15beef3c4e 100644 --- a/src/background/browserAction.ts +++ b/src/background/browserAction.ts @@ -21,7 +21,7 @@ import webextAlert from "./webextAlert"; import { browserAction, isMV3, type Tab } from "@/mv3/api"; import { executeScript } from "webext-content-scripts"; import { memoizeUntilSettled } from "@/utils/promiseUtils"; -import { openSidePanel } from "@/sidebar/sidePanel/messenger/api"; +import { openSidePanel } from "@/mv3/sidePanelMigration"; import { setActionPopup } from "webext-tools"; import { getRestrictedPageMessage } from "./sidePanel"; diff --git a/src/background/sidePanel.ts b/src/background/sidePanel.ts index dc2c27a4ae..560f9f7ea4 100644 --- a/src/background/sidePanel.ts +++ b/src/background/sidePanel.ts @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -import { openSidePanel } from "@/sidebar/sidePanel/messenger/api"; +import { openSidePanel } from "@/mv3/sidePanelMigration"; import type { MessengerMeta } from "webext-messenger"; import { getExtensionConsoleUrl } from "@/utils/extensionUtils"; import { diff --git a/src/mv3/sidePanelMigration.ts b/src/mv3/sidePanelMigration.ts index 8ebd8da205..1327d8efc4 100644 --- a/src/mv3/sidePanelMigration.ts +++ b/src/mv3/sidePanelMigration.ts @@ -17,12 +17,18 @@ /** @file Temporary helpers useful for the MV3 sidePanel transition */ -import { getTopLevelFrame } from "webext-messenger"; -import { getAssociatedTarget } from "@/sidebar/sidePanel/messenger/api"; +import { getMethod, getTopLevelFrame } from "webext-messenger"; +import { + _openSidePanel, + getAssociatedTarget, +} from "@/sidebar/sidePanel/messenger/api"; import { isMV3 } from "./api"; export const getTopFrameFromSidebar = isMV3() ? getAssociatedTarget : getTopLevelFrame; -// TODO: Add openSidePanel? +export const openSidePanel = isMV3() + ? _openSidePanel + : // Called via `getMethod` until we complete the strictNullChecks transition + async (tabId: number) => getMethod("SHOW_SIDEBAR")({ tabId }); diff --git a/src/pageEditor/panes/insert/useAutoInsert.ts b/src/pageEditor/panes/insert/useAutoInsert.ts index 779d7b2367..c03782d096 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 { openSidePanel } from "@/sidebar/sidePanel/messenger/api"; +import { openSidePanel } from "@/mv3/sidePanelMigration"; 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 aebc100557..37faa3537a 100644 --- a/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx +++ b/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx @@ -35,7 +35,7 @@ import { enableOverlay, updateSidebar, } from "@/contentScript/messenger/api"; -import { openSidePanel } from "@/sidebar/sidePanel/messenger/api"; +import { openSidePanel } from "@/mv3/sidePanelMigration"; 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 b994a28114..82acca8367 100644 --- a/src/pageEditor/sidebar/DynamicModComponentListItem.tsx +++ b/src/pageEditor/sidebar/DynamicModComponentListItem.tsx @@ -32,7 +32,7 @@ import { enableOverlay, updateSidebar, } from "@/contentScript/messenger/api"; -import { openSidePanel } from "@/sidebar/sidePanel/messenger/api"; +import { openSidePanel } from "@/mv3/sidePanelMigration"; import { thisTab } from "@/pageEditor/utils"; import cx from "classnames"; import reportEvent from "@/telemetry/reportEvent"; diff --git a/src/sidebar/sidePanel/messenger/api.ts b/src/sidebar/sidePanel/messenger/api.ts index 331e721c1f..8747ee13f0 100644 --- a/src/sidebar/sidePanel/messenger/api.ts +++ b/src/sidebar/sidePanel/messenger/api.ts @@ -22,9 +22,8 @@ */ import { expectContext, forbidContext } from "@/utils/expectContext"; -import { getMethod, type Target } from "webext-messenger"; +import { type Target } from "webext-messenger"; import { getErrorMessage } from "@/errors/errorHelpers"; -import { isMV3 } from "@/mv3/api"; export function getAssociatedTarget(): Target { expectContext("sidebar"); @@ -51,13 +50,8 @@ export async function isSidePanelOpen(): Promise { } } -export async function openSidePanel(tabId: number): Promise { - if (!isMV3()) { - // Called via `getMethod` until we complete the strictNullChecks transition - await getMethod("SHOW_SIDEBAR")({ tabId }); - return; - } - +/** @deprecated Use this instead: import { openSidePanel } from "@/mv3/sidePanelMigration"; */ +export async function _openSidePanel(tabId: number): Promise { // Simultaneously enable and open the side panel. // If we wait too long before calling .open(), we will lose the "user gesture" permission // There is no way to know whether the side panel is open yet, so we call it regardless. From 8994081f571ad1ac049db6e50bfca7ee3b85c560 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Fri, 19 Jan 2024 15:05:31 +0800 Subject: [PATCH 10/30] MV2 verified --- src/background/sidePanel.ts | 5 +++++ src/sidebar/messenger/api.ts | 15 ++++++++++++--- src/sidebar/sidePanel.tsx | 7 ++++--- src/sidebar/sidebar.tsx | 7 +++++-- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/background/sidePanel.ts b/src/background/sidePanel.ts index 560f9f7ea4..3a495fafd5 100644 --- a/src/background/sidePanel.ts +++ b/src/background/sidePanel.ts @@ -23,6 +23,7 @@ import { DISPLAY_REASON_RESTRICTED_URL, } from "@/tinyPages/restrictedUrlPopupConstants"; import { isScriptableUrl } from "webext-content-scripts"; +import { isMV3 } from "@/mv3/api"; export function getRestrictedPageMessage( tabUrl: string | undefined, @@ -52,6 +53,10 @@ export async function showMySidePanel(this: MessengerMeta): Promise { // TODO: Drop if this is ever implemented: https://github.com/w3c/webextensions/issues/515 export async function initSidePanel(): Promise { + if (!isMV3()) { + return; + } + // TODO: Drop this once the popover URL behavior is merged into sidebar.html // https://github.com/pixiebrix/pixiebrix-extension/issues/7364 chrome.tabs.onUpdated.addListener((tabId, changeInfo) => { diff --git a/src/sidebar/messenger/api.ts b/src/sidebar/messenger/api.ts index 15a9a41f6e..5b4096e486 100644 --- a/src/sidebar/messenger/api.ts +++ b/src/sidebar/messenger/api.ts @@ -16,12 +16,21 @@ */ /* Do not use `registerMethod` in this file */ +import { isMV3 } from "@/mv3/api"; import { isContentScript } from "webext-detect-page"; -import { getMethod, getNotifier, getThisFrame } from "webext-messenger"; +import { + type Target, + type PageTarget, + getMethod, + getNotifier, + getThisFrame, +} from "webext-messenger"; -const target = { page: "/sidebar.html" }; +const target: Target | PageTarget = isMV3() + ? { page: "/sidebar.html" } + : { tabId: "this", page: "/sidebar.html" }; -if (isContentScript()) { +if (isContentScript() && isMV3()) { // Unavoidable race condition: we can't message the sidebar until we know the tabId. // TODO: Drop if this is ever implemented https://github.com/pixiebrix/webext-messenger/issues/193 // eslint-disable-next-line promise/prefer-await-to-then diff --git a/src/sidebar/sidePanel.tsx b/src/sidebar/sidePanel.tsx index ff4f020e29..cd961a607c 100644 --- a/src/sidebar/sidePanel.tsx +++ b/src/sidebar/sidePanel.tsx @@ -22,8 +22,8 @@ import { PING_SIDE_PANEL, getAssociatedTarget, } from "@/sidebar/sidePanel/messenger/api"; -import { sidebarWasLoaded } from "@/contentScript/messenger/api"; import { isObject } from "@/utils/objectUtils"; +import { isMV3 } from "@/mv3/api"; expectContext("sidebar"); @@ -42,6 +42,7 @@ function respondToPings() { } export function initSidePanel() { - respondToPings(); - sidebarWasLoaded(getAssociatedTarget()); + if (isMV3()) { + respondToPings(); + } } diff --git a/src/sidebar/sidebar.tsx b/src/sidebar/sidebar.tsx index 486dee6282..e3d8b7c23f 100644 --- a/src/sidebar/sidebar.tsx +++ b/src/sidebar/sidebar.tsx @@ -35,9 +35,12 @@ import { initRuntimeLogging } from "@/development/runtimeLogging"; import { initCopilotMessenger } from "@/contrib/automationanywhere/aaFrameProtocol"; import { initPerformanceMonitoring } from "@/telemetry/performance"; import { initSidePanel } from "./sidePanel"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { sidebarWasLoaded } from "@/contentScript/messenger/api"; -function init(): void { +async function init(): Promise { ReactDOM.render(, document.querySelector("#container")); + sidebarWasLoaded(await getTopFrameFromSidebar()); } void initMessengerLogging(); @@ -47,7 +50,7 @@ registerMessenger(); registerContribBlocks(); registerBuiltinBricks(); initToaster(); -init(); +void init(); initSidePanel(); // Handle an embedded AA business copilot frame From 53ad10d34994f209864f5f30f6854c7292fd3769 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Fri, 19 Jan 2024 17:02:01 +0800 Subject: [PATCH 11/30] MV3 verified --- src/tinyPages/restrictedUrlPopup.html | 11 +++++++---- src/tinyPages/restrictedUrlPopup.tsx | 5 +---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/tinyPages/restrictedUrlPopup.html b/src/tinyPages/restrictedUrlPopup.html index 4081f9d592..1514917301 100644 --- a/src/tinyPages/restrictedUrlPopup.html +++ b/src/tinyPages/restrictedUrlPopup.html @@ -21,10 +21,13 @@ PixieBrix diff --git a/src/tinyPages/restrictedUrlPopup.tsx b/src/tinyPages/restrictedUrlPopup.tsx index 86cb2b7ed5..1a03b39c16 100644 --- a/src/tinyPages/restrictedUrlPopup.tsx +++ b/src/tinyPages/restrictedUrlPopup.tsx @@ -15,15 +15,12 @@ * along with this program. If not, see . */ +import "bootstrap/dist/css/bootstrap.min.css"; import "@/extensionContext"; - import RestrictedUrlPopupApp from "@/tinyPages/RestrictedUrlPopupApp"; - import ReactDOM from "react-dom"; import React from "react"; -import "bootstrap/dist/css/bootstrap.min.css"; - ReactDOM.render( , document.querySelector("#container"), From b0ce0db621ed1710ad51d1faa11eb83b7b5891d4 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Sat, 20 Jan 2024 02:46:44 +0800 Subject: [PATCH 12/30] webext-messenger: Fix sidebar API target race condition --- package-lock.json | 26 +++++++++++++------------- package.json | 2 +- src/sidebar/Header.tsx | 1 + src/sidebar/messenger/api.ts | 28 +++++++++++++++++----------- 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3bf4a16317..3060f7fc7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -144,7 +144,7 @@ "webext-content-scripts": "^2.6.1", "webext-detect-page": "^5.0.0", "webext-inject-on-install": "^2.0.0", - "webext-messenger": "^0.25.0-0", + "webext-messenger": "^0.25.0", "webext-patterns": "^1.3.0", "webext-permissions": "^3.1.2", "webext-polyfill-kinda": "^1.0.2", @@ -17313,9 +17313,9 @@ } }, "node_modules/is-network-error": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.0.0.tgz", - "integrity": "sha512-P3fxi10Aji2FZmHTrMPSNFbNC6nnp4U5juPAIjXPHkUNubi4+qK7vvdsaNpAUwXslhYm9oyjEYTxs1xd/+Ph0w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.0.1.tgz", + "integrity": "sha512-OwQXkwBJeESyhFw+OumbJVD58BFBJJI5OM5S1+eyrDKlgDZPX2XNT5gXS56GSD3NPbbwUuMlR1Q71SRp5SobuQ==", "engines": { "node": ">=16" }, @@ -22109,9 +22109,9 @@ } }, "node_modules/p-retry": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.1.0.tgz", - "integrity": "sha512-fJLEQ2KqYBJRuaA/8cKMnqhulqNM+bpcjYtXNex2t3mOXKRYPitAJt9NacSf8XAFzcYahSAbKpobiWDSqHSh2g==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.0.tgz", + "integrity": "sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==", "dependencies": { "@types/retry": "0.12.2", "is-network-error": "^1.0.0", @@ -27363,14 +27363,14 @@ } }, "node_modules/webext-messenger": { - "version": "0.25.0-0", - "resolved": "https://registry.npmjs.org/webext-messenger/-/webext-messenger-0.25.0-0.tgz", - "integrity": "sha512-36p0VH9uYmucyMQvWqhkZuT+hIOdaeF0pSpYEFeJc582EZu1Fwy57hCYie+4RwObwO0yGL4MDPCIfj+43x0xpA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/webext-messenger/-/webext-messenger-0.25.0.tgz", + "integrity": "sha512-+yLxaI1x1Qq7RhZ8IK2VFkqZo/5F43VWiUpV0l5NdhTRp7SAoDZspRfRZiAgI0oCcAsRGAM9YIISmATRDVOLVg==", "dependencies": { - "p-retry": "^6.0.0", + "p-retry": "^6.2.0", "serialize-error": "^11.0.2", - "type-fest": "^4.6.0", - "webext-detect-page": "^4.1.1" + "type-fest": "^4.9.0", + "webext-detect-page": "^4.2.1" } }, "node_modules/webext-messenger/node_modules/webext-detect-page": { diff --git a/package.json b/package.json index 21612495a9..65bacaf85e 100644 --- a/package.json +++ b/package.json @@ -166,7 +166,7 @@ "webext-content-scripts": "^2.6.1", "webext-detect-page": "^5.0.0", "webext-inject-on-install": "^2.0.0", - "webext-messenger": "^0.25.0-0", + "webext-messenger": "^0.25.0", "webext-patterns": "^1.3.0", "webext-permissions": "^3.1.2", "webext-polyfill-kinda": "^1.0.2", diff --git a/src/sidebar/Header.tsx b/src/sidebar/Header.tsx index 6859f6f187..1a6ab5082d 100644 --- a/src/sidebar/Header.tsx +++ b/src/sidebar/Header.tsx @@ -43,6 +43,7 @@ const Header: React.FunctionComponent = () => { theme === "default" ? styles.themeColorOverride : styles.themeColor, )} onClick={async () => { + // This piece of code is MV2-only, it only needs to handle being run in an iframe const topLevelFrame = await getTopLevelFrame(); await hideSidebar(topLevelFrame); }} diff --git a/src/sidebar/messenger/api.ts b/src/sidebar/messenger/api.ts index 5b4096e486..360d4999c4 100644 --- a/src/sidebar/messenger/api.ts +++ b/src/sidebar/messenger/api.ts @@ -19,26 +19,32 @@ import { isMV3 } from "@/mv3/api"; import { isContentScript } from "webext-detect-page"; import { - type Target, type PageTarget, getMethod, getNotifier, getThisFrame, } from "webext-messenger"; -const target: Target | PageTarget = isMV3() - ? { page: "/sidebar.html" } - : { tabId: "this", page: "/sidebar.html" }; +export async function getSidebarInThisTab(): Promise { + if (!isMV3()) { + return { tabId: "this", page: "/sidebar.html" }; + } -if (isContentScript() && isMV3()) { - // Unavoidable race condition: we can't message the sidebar until we know the tabId. - // TODO: Drop if this is ever implemented https://github.com/pixiebrix/webext-messenger/issues/193 - // eslint-disable-next-line promise/prefer-await-to-then - void getThisFrame().then((frame) => { - target.page += "?tabId=" + frame.tabId; - }); + if (!isContentScript()) { + // Probably just other pages importing this file transitively, this is dead code + return { + page: "the sidebar API is only available from the content script", + }; + } + + const frame = await getThisFrame(); + return { + page: "/sidebar.html?tabId=" + frame.tabId, + }; } +const target = getSidebarInThisTab(); + const sidebarInThisTab = { renderPanels: getMethod("SIDEBAR_RENDER_PANELS", target), activatePanel: getMethod("SIDEBAR_ACTIVATE_PANEL", target), From 7b31ab08830c6f0012d1053a780f87234487d6bb Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Sat, 20 Jan 2024 02:47:09 +0800 Subject: [PATCH 13/30] webext-messenger: drop custom ping responder --- src/sidebar/sidePanel.tsx | 21 +-------------------- src/sidebar/sidePanel/messenger/api.ts | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/src/sidebar/sidePanel.tsx b/src/sidebar/sidePanel.tsx index cd961a607c..1375418026 100644 --- a/src/sidebar/sidePanel.tsx +++ b/src/sidebar/sidePanel.tsx @@ -18,31 +18,12 @@ /** @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 { - PING_SIDE_PANEL, - getAssociatedTarget, -} from "@/sidebar/sidePanel/messenger/api"; -import { isObject } from "@/utils/objectUtils"; import { isMV3 } from "@/mv3/api"; expectContext("sidebar"); -// Do not use the messenger because it doesn't support retry-less messaging -// TODO: Drop after https://github.com/pixiebrix/webext-messenger/issues/59 -function respondToPings() { - chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - if ( - isObject(message) && - message.type === PING_SIDE_PANEL && - sender.tab?.id === getAssociatedTarget().tabId - ) { - sendResponse(true); - } - }); -} - export function initSidePanel() { if (isMV3()) { - respondToPings(); + // Just a placeholder for now } } diff --git a/src/sidebar/sidePanel/messenger/api.ts b/src/sidebar/sidePanel/messenger/api.ts index 8747ee13f0..3c8259702d 100644 --- a/src/sidebar/sidePanel/messenger/api.ts +++ b/src/sidebar/sidePanel/messenger/api.ts @@ -21,9 +21,10 @@ * to match that expectation and avoid lint issues. */ -import { expectContext, forbidContext } from "@/utils/expectContext"; -import { type Target } from "webext-messenger"; +import { expectContext } from "@/utils/expectContext"; +import { messenger, type Target } from "webext-messenger"; import { getErrorMessage } from "@/errors/errorHelpers"; +import { getSidebarInThisTab } from "@/sidebar/messenger/api"; export function getAssociatedTarget(): Target { expectContext("sidebar"); @@ -34,7 +35,10 @@ export function getAssociatedTarget(): Target { export const PING_SIDE_PANEL = "PING_SIDE_PANEL"; export async function isSidePanelOpen(): Promise { - forbidContext("sidebar", "The sidebar shouldn't check whether it's open"); + expectContext( + "contentScript", + "isSidePanelOpen only works from the same content script for now", + ); // Sync check where possible, which is the content script if (isSidePanelOpenSync() === false) { @@ -42,8 +46,13 @@ export async function isSidePanelOpen(): Promise { } try { - // Available from any page - await chrome.runtime.sendMessage({ type: PING_SIDE_PANEL }); + // If ever needed, `isSidePanelOpen` could be called from any context, as long as + // `getSidebarInThisTab` is replaced/complemented by a tabid-specific `{page: "/sidebar.html?tabId=123"}` target + await messenger( + "SIDEBAR_PING", + { retry: false }, + await getSidebarInThisTab(), + ); return true; } catch { return false; From 9e017963c0d29e273640d2428985da8f21fc9801 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Sat, 20 Jan 2024 21:25:51 +0800 Subject: [PATCH 14/30] Dead code --- src/sidebar/sidePanel/messenger/api.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/sidebar/sidePanel/messenger/api.ts b/src/sidebar/sidePanel/messenger/api.ts index 3c8259702d..0a8d853cfc 100644 --- a/src/sidebar/sidePanel/messenger/api.ts +++ b/src/sidebar/sidePanel/messenger/api.ts @@ -32,8 +32,6 @@ export function getAssociatedTarget(): Target { return { tabId: Number(tabId), frameId: 0 }; } -export const PING_SIDE_PANEL = "PING_SIDE_PANEL"; - export async function isSidePanelOpen(): Promise { expectContext( "contentScript", From a4071063e00d54845eadb452c3e58c77e1fa1a10 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Mon, 22 Jan 2024 02:43:07 +0800 Subject: [PATCH 15/30] Merge popup into sidePanel --- src/background/browserAction.ts | 23 +++++++- src/background/sidePanel.ts | 40 +++----------- src/sidebar/SidebarBody.tsx | 25 ++++++--- src/sidebar/hooks/useCurrentUrl.tsx | 69 ++++++++++++++++++++++++ src/sidebar/sidePanel/messenger/api.ts | 10 +++- src/tinyPages/RestrictedUrlPopupApp.tsx | 8 ++- src/tinyPages/restrictedUrlPopup.tsx | 4 +- src/tinyPages/restrictedUrlPopupUtils.ts | 35 ++++++++++++ 8 files changed, 163 insertions(+), 51 deletions(-) create mode 100644 src/sidebar/hooks/useCurrentUrl.tsx create mode 100644 src/tinyPages/restrictedUrlPopupUtils.ts diff --git a/src/background/browserAction.ts b/src/background/browserAction.ts index 15beef3c4e..8e46f3e043 100644 --- a/src/background/browserAction.ts +++ b/src/background/browserAction.ts @@ -23,7 +23,26 @@ import { executeScript } from "webext-content-scripts"; import { memoizeUntilSettled } from "@/utils/promiseUtils"; import { openSidePanel } from "@/mv3/sidePanelMigration"; import { setActionPopup } from "webext-tools"; -import { getRestrictedPageMessage } from "./sidePanel"; +import { getReasonByUrl } from "@/tinyPages/restrictedUrlPopupUtils"; + +/** + * 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 undefined if not accessible + */ +function getPopoverUrl(tabUrl: string | undefined): string | null { + const popoverUrl = browser.runtime.getURL("restrictedUrlPopup.html"); + const reason = getReasonByUrl(tabUrl ?? ""); + + if (reason) { + return `${popoverUrl}?reason=${reason}`; + } + + // 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 async function initBrowserAction(): Promise { if (!isMV3()) { @@ -103,5 +122,5 @@ function initBrowserActionMv2(): void { // 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(getRestrictedPageMessage); + setActionPopup(getPopoverUrl); } diff --git a/src/background/sidePanel.ts b/src/background/sidePanel.ts index 3a495fafd5..8b61b21693 100644 --- a/src/background/sidePanel.ts +++ b/src/background/sidePanel.ts @@ -17,36 +17,8 @@ import { openSidePanel } from "@/mv3/sidePanelMigration"; import type { MessengerMeta } from "webext-messenger"; -import { getExtensionConsoleUrl } from "@/utils/extensionUtils"; -import { - DISPLAY_REASON_EXTENSION_CONSOLE, - DISPLAY_REASON_RESTRICTED_URL, -} from "@/tinyPages/restrictedUrlPopupConstants"; -import { isScriptableUrl } from "webext-content-scripts"; import { isMV3 } from "@/mv3/api"; -export function getRestrictedPageMessage( - 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; -} - -function getSidebarPath(tabId: number, url: string | undefined): string { - return getRestrictedPageMessage(url) ?? "sidebar.html?tabId=" + tabId; -} - export async function showMySidePanel(this: MessengerMeta): Promise { await openSidePanel(this.trace[0].tab.id); } @@ -59,11 +31,11 @@ export async function initSidePanel(): Promise { // TODO: Drop this once the popover URL behavior is merged into sidebar.html // https://github.com/pixiebrix/pixiebrix-extension/issues/7364 - chrome.tabs.onUpdated.addListener((tabId, changeInfo) => { - if (changeInfo.url) { + chrome.tabs.onCreated.addListener(({ id: tabId }) => { + if (tabId) { void chrome.sidePanel.setOptions({ tabId, - path: getSidebarPath(tabId, changeInfo.url), + path: "sidebar.html?tabId=" + tabId, }); } }); @@ -71,10 +43,10 @@ export async function initSidePanel(): Promise { // We need to target _all_ tabs, not just those we have access to const existingTabs = await chrome.tabs.query({}); await Promise.all( - existingTabs.map(async ({ id, url }) => + existingTabs.map(async ({ id: tabId, url }) => chrome.sidePanel.setOptions({ - tabId: id, - path: getSidebarPath(id, url), + tabId, + path: "sidebar.html?tabId=" + tabId, enabled: true, }), ), diff --git a/src/sidebar/SidebarBody.tsx b/src/sidebar/SidebarBody.tsx index b123eb3180..1a4e52b12b 100644 --- a/src/sidebar/SidebarBody.tsx +++ b/src/sidebar/SidebarBody.tsx @@ -19,15 +19,26 @@ import React from "react"; import ConnectedSidebar from "./ConnectedSidebar"; import Header from "./Header"; import ErrorBanner from "./ErrorBanner"; +import RestrictedUrlPopupApp from "@/tinyPages/RestrictedUrlPopupApp"; +import useCurrentUrl from "./hooks/useCurrentUrl"; +import { getReasonByUrl } from "@/tinyPages/restrictedUrlPopupUtils"; // Include MemoryRouter because some of our authentication-gate hooks use useLocation. However, there's currently no // navigation in the SidebarApp -const SidebarBody: React.FunctionComponent = () => ( - <> - -
- - -); +function SidebarBody() { + const url = useCurrentUrl(); + const reason = getReasonByUrl(url); + return ( + <> + +
+ {reason ? ( + + ) : ( + + )} + + ); +} export default SidebarBody; diff --git a/src/sidebar/hooks/useCurrentUrl.tsx b/src/sidebar/hooks/useCurrentUrl.tsx new file mode 100644 index 0000000000..c4283ed6ad --- /dev/null +++ b/src/sidebar/hooks/useCurrentUrl.tsx @@ -0,0 +1,69 @@ +/* + * 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 { once } from "lodash"; +import { useEffect, useState } from "react"; +import { type Target } from "@/types/messengerTypes"; +import { SimpleEventTarget } from "@/utils/SimpleEventTarget"; +import { type WebNavigation } from "webextension-polyfill"; +import { expectContext } from "@/utils/expectContext"; +import { + getAssociatedTarget, + getAssociatedTargetUrl, +} from "@/sidebar/sidePanel/messenger/api"; + +let tabUrl: string; + +const urlChanges = new SimpleEventTarget(); + +// The sidebar only cares for the top frame +function isCurrentTopFrame({ tabId, frameId }: Target) { + const targetTab = getAssociatedTarget(); + return frameId === targetTab.frameId && tabId === targetTab.tabId; +} + +async function onNavigation( + target: WebNavigation.OnCommittedDetailsType, +): Promise { + if (isCurrentTopFrame(target)) { + tabUrl = target.url; + urlChanges.emit(target.url); + } +} + +const startWatching = once(async () => { + browser.webNavigation.onCommitted.addListener(onNavigation); + + tabUrl = await getAssociatedTargetUrl(); + urlChanges.emit(tabUrl); +}); + +export default function useCurrentUrl(): string { + expectContext("sidebar"); + + const [url, setUrl] = useState(tabUrl); + + useEffect(() => { + urlChanges.add(setUrl); + void startWatching(); + return () => { + urlChanges.remove(setUrl); + }; + }, [setUrl]); + + return url; +} diff --git a/src/sidebar/sidePanel/messenger/api.ts b/src/sidebar/sidePanel/messenger/api.ts index 0a8d853cfc..2347c54860 100644 --- a/src/sidebar/sidePanel/messenger/api.ts +++ b/src/sidebar/sidePanel/messenger/api.ts @@ -22,14 +22,20 @@ */ import { expectContext } from "@/utils/expectContext"; -import { messenger, type Target } from "webext-messenger"; +import { messenger } from "webext-messenger"; import { getErrorMessage } from "@/errors/errorHelpers"; import { getSidebarInThisTab } from "@/sidebar/messenger/api"; +import { getTabUrl, type Target } from "webext-tools"; +import { once } from "lodash"; -export function getAssociatedTarget(): Target { +export const getAssociatedTarget = once((): Target => { expectContext("sidebar"); const tabId = new URLSearchParams(window.location.search).get("tabId"); return { tabId: Number(tabId), frameId: 0 }; +}); + +export async function getAssociatedTargetUrl(): Promise { + return getTabUrl(getAssociatedTarget()); } export async function isSidePanelOpen(): Promise { diff --git a/src/tinyPages/RestrictedUrlPopupApp.tsx b/src/tinyPages/RestrictedUrlPopupApp.tsx index 58194ffdde..2e046b3a49 100644 --- a/src/tinyPages/RestrictedUrlPopupApp.tsx +++ b/src/tinyPages/RestrictedUrlPopupApp.tsx @@ -94,11 +94,9 @@ const ExtensionConsoleContent: React.FC = () => (
); -const RestrictedUrlPopupApp: React.FC = () => { - const reason = - new URLSearchParams(location.search).get("reason") ?? - DISPLAY_REASON_UNKNOWN; - +const RestrictedUrlPopupApp: React.FC<{ reason: string | null }> = ({ + reason = DISPLAY_REASON_UNKNOWN, +}) => { useEffect(() => { reportEvent(Events.BROWSER_ACTION_RESTRICTED_URL, { reason, diff --git a/src/tinyPages/restrictedUrlPopup.tsx b/src/tinyPages/restrictedUrlPopup.tsx index 1a03b39c16..77f6347961 100644 --- a/src/tinyPages/restrictedUrlPopup.tsx +++ b/src/tinyPages/restrictedUrlPopup.tsx @@ -22,6 +22,8 @@ import ReactDOM from "react-dom"; import React from "react"; ReactDOM.render( - , + , document.querySelector("#container"), ); diff --git a/src/tinyPages/restrictedUrlPopupUtils.ts b/src/tinyPages/restrictedUrlPopupUtils.ts new file mode 100644 index 0000000000..d5f8d3c097 --- /dev/null +++ b/src/tinyPages/restrictedUrlPopupUtils.ts @@ -0,0 +1,35 @@ +/* + * 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 { getExtensionConsoleUrl } from "@/utils/extensionUtils"; +import { + DISPLAY_REASON_EXTENSION_CONSOLE, + DISPLAY_REASON_RESTRICTED_URL, +} from "./restrictedUrlPopupConstants"; +import { isScriptableUrl } from "webext-content-scripts"; + +export function getReasonByUrl(url: string | undefined): string | null { + if (url?.startsWith(getExtensionConsoleUrl())) { + return DISPLAY_REASON_EXTENSION_CONSOLE; + } + + if (!isScriptableUrl(url)) { + return DISPLAY_REASON_RESTRICTED_URL; + } + + return null; +} From 3f545deb29f2b9e31e37abd06048c153ff332aa9 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Mon, 22 Jan 2024 12:30:53 +0800 Subject: [PATCH 16/30] Fix tests --- jest.config.js | 2 +- src/pageEditor/hooks/useCurrentUrl.ts | 39 +++---- src/sidebar/SidebarBody.test.tsx | 17 ++- src/sidebar/SidebarBody.tsx | 16 +-- .../__snapshots__/SidebarBody.test.tsx.snap | 110 ++++++++++++++++++ src/sidebar/hooks/useCurrentUrl.tsx | 35 +++--- src/tinyPages/RestrictedUrlPopupApp.tsx | 85 +++++--------- 7 files changed, 197 insertions(+), 107 deletions(-) diff --git a/jest.config.js b/jest.config.js index 41671f1b97..465c94a259 100644 --- a/jest.config.js +++ b/jest.config.js @@ -21,7 +21,7 @@ const config = { modulePaths: ["/src"], moduleFileExtensions: ["ts", "tsx", "js", "jsx", "yaml", "yml", "json"], testPathIgnorePatterns: ["/selenium/"], - modulePathIgnorePatterns: ["/headers.json"], + modulePathIgnorePatterns: ["/headers.json", "/dist/"], transform: { "^.+\\.[jt]sx?$": "@swc/jest", "^.+\\.mjs$": "@swc/jest", diff --git a/src/pageEditor/hooks/useCurrentUrl.ts b/src/pageEditor/hooks/useCurrentUrl.ts index 8f04abfc5e..36a61f0cb6 100644 --- a/src/pageEditor/hooks/useCurrentUrl.ts +++ b/src/pageEditor/hooks/useCurrentUrl.ts @@ -17,44 +17,39 @@ import { once } from "lodash"; import { useEffect, useState } from "react"; -import { type Target } from "@/types/messengerTypes"; import { SimpleEventTarget } from "@/utils/SimpleEventTarget"; -import { type WebNavigation } from "webextension-polyfill"; +import { type Tabs } from "webextension-polyfill"; import { expectContext } from "@/utils/expectContext"; import { getCurrentURL } from "@/pageEditor/utils"; -let tabUrl: string; -const TOP_LEVEL_FRAME_ID = 0; - +let lastKnownUrl: string; const urlChanges = new SimpleEventTarget(); -// The pageEditor only cares for the top frame -function isCurrentTopFrame({ tabId, frameId }: Target) { - return ( - frameId === TOP_LEVEL_FRAME_ID && - tabId === browser.devtools.inspectedWindow.tabId - ); -} - -async function onNavigation( - target: WebNavigation.OnCommittedDetailsType, +async function onUpdated( + tabId: number, + { url }: Tabs.OnUpdatedChangeInfoType, ): Promise { - if (isCurrentTopFrame(target)) { - tabUrl = target.url; - urlChanges.emit(target.url); + if ( + tabId === browser.devtools.inspectedWindow.tabId && + lastKnownUrl !== url + ) { + lastKnownUrl = url; + urlChanges.emit(url); } } const startWatching = once(async () => { - browser.webNavigation.onCommitted.addListener(onNavigation); - tabUrl = await getCurrentURL(); - urlChanges.emit(tabUrl); + browser.tabs.onUpdated.addListener(onUpdated); + + // Get initial URL + lastKnownUrl = await getCurrentURL(); + urlChanges.emit(lastKnownUrl); }); export default function useCurrentUrl(): string { expectContext("devTools"); - const [url, setUrl] = useState(tabUrl); + const [url, setUrl] = useState(lastKnownUrl); useEffect(() => { urlChanges.add(setUrl); diff --git a/src/sidebar/SidebarBody.test.tsx b/src/sidebar/SidebarBody.test.tsx index f1926feb0a..1c4a514779 100644 --- a/src/sidebar/SidebarBody.test.tsx +++ b/src/sidebar/SidebarBody.test.tsx @@ -19,9 +19,10 @@ import React from "react"; import SidebarBody from "@/sidebar/SidebarBody"; import { render } from "@/sidebar/testHelpers"; import useContextInvalidated from "@/hooks/useContextInvalidated"; +import useCurrentUrl from "@/sidebar/hooks/useCurrentUrl"; jest.mock("@/hooks/useContextInvalidated"); - +jest.mock("@/sidebar/hooks/useCurrentUrl"); jest.mock("@/contentScript/messenger/api", () => ({ ensureExtensionPointsInstalled: jest.fn(), getReservedSidebarEntries: jest.fn().mockResolvedValue({ @@ -32,13 +33,21 @@ jest.mock("@/contentScript/messenger/api", () => ({ })); describe("SidebarBody", () => { - test("it renders", () => { + test("it renders", async () => { + jest.mocked(useCurrentUrl).mockReturnValueOnce("https://www.example.com"); + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + test("it renders error when context is invalidated", async () => { + jest.mocked(useCurrentUrl).mockReturnValueOnce("https://www.example.com"); + jest.mocked(useContextInvalidated).mockReturnValueOnce(true); const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); - test("it renders error when context is invalidated", () => { - jest.mocked(useContextInvalidated).mockReturnValue(true); + test("it renders error when URL is restricted", async () => { + jest.mocked(useCurrentUrl).mockReturnValueOnce("chrome://extensions"); const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); diff --git a/src/sidebar/SidebarBody.tsx b/src/sidebar/SidebarBody.tsx index 1a4e52b12b..2a45f4e4cd 100644 --- a/src/sidebar/SidebarBody.tsx +++ b/src/sidebar/SidebarBody.tsx @@ -21,22 +21,24 @@ import Header from "./Header"; import ErrorBanner from "./ErrorBanner"; import RestrictedUrlPopupApp from "@/tinyPages/RestrictedUrlPopupApp"; import useCurrentUrl from "./hooks/useCurrentUrl"; -import { getReasonByUrl } from "@/tinyPages/restrictedUrlPopupUtils"; +import { getReasonByUrl as getRestrictedReasonByUrl } from "@/tinyPages/restrictedUrlPopupUtils"; // Include MemoryRouter because some of our authentication-gate hooks use useLocation. However, there's currently no // navigation in the SidebarApp function SidebarBody() { const url = useCurrentUrl(); - const reason = getReasonByUrl(url); + const restricted = getRestrictedReasonByUrl(url); return ( <>
- {reason ? ( - - ) : ( - - )} + {url ? ( + restricted ? ( + + ) : ( + + ) + ) : null} ); } diff --git a/src/sidebar/__snapshots__/SidebarBody.test.tsx.snap b/src/sidebar/__snapshots__/SidebarBody.test.tsx.snap index 04c4b7a514..21dc17c867 100644 --- a/src/sidebar/__snapshots__/SidebarBody.test.tsx.snap +++ b/src/sidebar/__snapshots__/SidebarBody.test.tsx.snap @@ -119,6 +119,96 @@ exports[`SidebarBody it renders 1`] = ` `; +exports[`SidebarBody it renders error when URL is restricted 1`] = ` + +
+ +
+ +
+ + + +
+
+
+ This is a restricted browser page. +
+
+ PixieBrix cannot access this page. +
+
+ To open the PixieBrix Sidebar, navigate to a website and then click the PixieBrix toolbar icon again. +
+
+
+ Looking for the Page Editor? + + View the Developer Welcome Page + +
+
+
+`; + exports[`SidebarBody it renders error when context is invalidated 1`] = `
+
diff --git a/src/sidebar/hooks/useCurrentUrl.tsx b/src/sidebar/hooks/useCurrentUrl.tsx index c4283ed6ad..8992b86a06 100644 --- a/src/sidebar/hooks/useCurrentUrl.tsx +++ b/src/sidebar/hooks/useCurrentUrl.tsx @@ -17,45 +17,42 @@ import { once } from "lodash"; import { useEffect, useState } from "react"; -import { type Target } from "@/types/messengerTypes"; import { SimpleEventTarget } from "@/utils/SimpleEventTarget"; -import { type WebNavigation } from "webextension-polyfill"; +import { type Tabs } from "webextension-polyfill"; import { expectContext } from "@/utils/expectContext"; import { getAssociatedTarget, getAssociatedTargetUrl, } from "@/sidebar/sidePanel/messenger/api"; -let tabUrl: string; - +let lastKnownUrl: string; const urlChanges = new SimpleEventTarget(); -// The sidebar only cares for the top frame -function isCurrentTopFrame({ tabId, frameId }: Target) { - const targetTab = getAssociatedTarget(); - return frameId === targetTab.frameId && tabId === targetTab.tabId; -} - -async function onNavigation( - target: WebNavigation.OnCommittedDetailsType, +async function onUpdated( + tabId: number, + { url }: Tabs.OnUpdatedChangeInfoType, ): Promise { - if (isCurrentTopFrame(target)) { - tabUrl = target.url; - urlChanges.emit(target.url); + if (tabId === getAssociatedTarget().tabId && lastKnownUrl !== url) { + lastKnownUrl = url; + urlChanges.emit(url); } } const startWatching = once(async () => { - browser.webNavigation.onCommitted.addListener(onNavigation); + browser.tabs.onUpdated.addListener(onUpdated); + + // Get initial URL + lastKnownUrl = await getAssociatedTargetUrl(); + console.log("Initial URL", lastKnownUrl); - tabUrl = await getAssociatedTargetUrl(); - urlChanges.emit(tabUrl); + urlChanges.emit(lastKnownUrl); }); export default function useCurrentUrl(): string { expectContext("sidebar"); - const [url, setUrl] = useState(tabUrl); + const [url, setUrl] = useState(lastKnownUrl); + console.log("useCurrentUrl", url); useEffect(() => { urlChanges.add(setUrl); diff --git a/src/tinyPages/RestrictedUrlPopupApp.tsx b/src/tinyPages/RestrictedUrlPopupApp.tsx index 2e046b3a49..5231c36aec 100644 --- a/src/tinyPages/RestrictedUrlPopupApp.tsx +++ b/src/tinyPages/RestrictedUrlPopupApp.tsx @@ -1,4 +1,3 @@ -/* eslint-disable jsx-a11y/anchor-is-valid -- options page behaves like a link */ /* * Copyright (C) 2023 PixieBrix, Inc. * @@ -23,69 +22,35 @@ import { DISPLAY_REASON_EXTENSION_CONSOLE, DISPLAY_REASON_UNKNOWN, } from "@/tinyPages/restrictedUrlPopupConstants"; +import { isBrowserSidebar } from "@/utils/expectContext"; -const RestrictedUrlContent: React.FC = () => ( +const RestrictedUrlContent: React.FC = ({ children }) => (
-
This is a restricted browser page.
-
PixieBrix cannot access this page.
- + {children}
To open the PixieBrix Sidebar, navigate to a website and then click the PixieBrix toolbar icon again.
-
- - -
-); - -const ExtensionConsoleContent: React.FC = () => ( -
-
This is the Extension Console.
-
PixieBrix mods cannot run on this page.
+ href="https://www.pixiebrix.com/developers-welcome" + onClick={async (event) => { + if (event.shiftKey || event.ctrlKey || event.metaKey) { + return; + } -
- To open the PixieBrix Sidebar, navigate to a website and then click the - PixieBrix toolbar icon again. -
- -
- -
- Looking for the Page Editor?{" "} - { + event.preventDefault(); await browser.tabs.update({ - url: "https://www.pixiebrix.com/developers-welcome", + url: event.currentTarget.href, }); - window.close(); + + // TODO: Drop after restrictedUrlPopup.html is removed + if (!isBrowserSidebar()) { + window.close(); + } }} > View the Developer Welcome Page @@ -104,10 +69,22 @@ const RestrictedUrlPopupApp: React.FC<{ reason: string | null }> = ({ // eslint-disable-next-line react-hooks/exhaustive-deps -- only run once on mount }, []); - return reason === DISPLAY_REASON_EXTENSION_CONSOLE ? ( - - ) : ( - + return ( + + {reason === DISPLAY_REASON_EXTENSION_CONSOLE ? ( + <> +
This is the Extension Console.
+
PixieBrix mods cannot run on this page.
+ + ) : ( + <> +
+ This is a restricted browser page. +
+
PixieBrix cannot access this page.
+ + )} +
); }; From 81eb66775d549eb7bf58d69ce3f322f15b5a7be4 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Mon, 22 Jan 2024 14:12:02 +0800 Subject: [PATCH 17/30] Backport sidebar URL creation with "tabId" to MV2 --- src/background/sidePanel.ts | 5 ++- src/bricks/renderers/customForm.ts | 6 +-- src/bricks/transformers/brickFactory.ts | 4 +- .../documentBuilder/render/BlockElement.tsx | 4 +- .../documentBuilder/render/ButtonElement.tsx | 4 +- .../documentBuilder/render/ListElement.tsx | 4 +- src/contentScript/sidebarDomControllerLite.ts | 14 ++++--- .../automationanywhere/aaFrameProtocol.ts | 4 +- src/mv3/sidePanelMigration.ts | 11 +----- src/sidebar/ConnectedSidebar.tsx | 36 ++++++++--------- src/sidebar/PanelBody.tsx | 4 +- src/sidebar/SidebarBody.tsx | 18 +++++---- src/sidebar/Tabs.tsx | 4 +- src/sidebar/activateMod/ActivateModPanel.tsx | 4 +- src/sidebar/connectedTarget.tsx | 39 +++++++++++++++++++ src/sidebar/hooks/useCurrentUrl.tsx | 6 +-- src/sidebar/messenger/api.ts | 6 ++- src/sidebar/modLauncher/ModLauncher.tsx | 4 +- src/sidebar/sidePanel/messenger/api.ts | 12 ------ src/sidebar/sidebar.html | 2 +- src/sidebar/sidebar.tsx | 4 +- src/sidebar/sidebarSlice.ts | 10 ++--- src/sidebar/useHideEmptySidebar.ts | 4 +- 23 files changed, 120 insertions(+), 89 deletions(-) create mode 100644 src/sidebar/connectedTarget.tsx diff --git a/src/background/sidePanel.ts b/src/background/sidePanel.ts index 8b61b21693..6fc1e913f5 100644 --- a/src/background/sidePanel.ts +++ b/src/background/sidePanel.ts @@ -18,6 +18,7 @@ import { openSidePanel } from "@/mv3/sidePanelMigration"; import type { MessengerMeta } from "webext-messenger"; import { isMV3 } from "@/mv3/api"; +import { getSidebarPath } from "@/sidebar/messenger/api"; export async function showMySidePanel(this: MessengerMeta): Promise { await openSidePanel(this.trace[0].tab.id); @@ -35,7 +36,7 @@ export async function initSidePanel(): Promise { if (tabId) { void chrome.sidePanel.setOptions({ tabId, - path: "sidebar.html?tabId=" + tabId, + path: getSidebarPath(tabId), }); } }); @@ -46,7 +47,7 @@ export async function initSidePanel(): Promise { existingTabs.map(async ({ id: tabId, url }) => chrome.sidePanel.setOptions({ tabId, - path: "sidebar.html?tabId=" + tabId, + path: getSidebarPath(tabId), enabled: true, }), ), diff --git a/src/bricks/renderers/customForm.ts b/src/bricks/renderers/customForm.ts index b009003029..3ec763acd1 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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; 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 getTopFrameFromSidebar(); + const topLevelFrame = getConnectedTarget(); // 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 getTopFrameFromSidebar(); + const topLevelFrame = getConnectedTarget(); // 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 4a48d18398..cb8a287622 100644 --- a/src/bricks/transformers/brickFactory.ts +++ b/src/bricks/transformers/brickFactory.ts @@ -50,7 +50,7 @@ import { import { type UnknownObject } from "@/types/objectTypes"; import { isPipelineExpression } from "@/utils/expressionUtils"; import { isContentScript } from "webext-detect-page"; -import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; import { getTopLevelFrame } from "webext-messenger"; import { uuidv4 } from "@/types/helpers"; import { isSpecificError } from "@/errors/errorHelpers"; @@ -348,7 +348,7 @@ class UserDefinedBrick extends BrickABC { // The modal is always an iframe in the same tab, // but the sidebar varies. This code handles the 3 cases. const topLevelFrame = isBrowserSidebar() - ? await getTopFrameFromSidebar() + ? getConnectedTarget() : await getTopLevelFrame(); try { diff --git a/src/components/documentBuilder/render/BlockElement.tsx b/src/components/documentBuilder/render/BlockElement.tsx index 3e74c47fd3..e41512d128 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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; 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 getTopFrameFromSidebar(); + const topLevelFrame = getConnectedTarget(); return runRendererPipeline(topLevelFrame, { nonce: uuidv4(), diff --git a/src/components/documentBuilder/render/ButtonElement.tsx b/src/components/documentBuilder/render/ButtonElement.tsx index ecefcc5ed4..040ebd7a2f 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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; import { getRootCause, hasSpecificErrorCause } from "@/errors/errorHelpers"; import { SubmitPanelAction } from "@/bricks/errors"; import cx from "classnames"; @@ -67,7 +67,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 getTopFrameFromSidebar(); + const topLevelFrame = getConnectedTarget(); try { await runHeadlessPipeline(topLevelFrame, { diff --git a/src/components/documentBuilder/render/ListElement.tsx b/src/components/documentBuilder/render/ListElement.tsx index aca99597e7..5dc48bccfa 100644 --- a/src/components/documentBuilder/render/ListElement.tsx +++ b/src/components/documentBuilder/render/ListElement.tsx @@ -35,7 +35,7 @@ import DelayedRender from "@/components/DelayedRender"; import { isDeferExpression } from "@/utils/expressionUtils"; import { isNullOrBlank } from "@/utils/stringUtils"; import { joinPathParts } from "@/utils/formUtils"; -import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; type DocumentListProps = { array: UnknownObject[]; @@ -66,7 +66,7 @@ const ListElementInternal: React.FC = ({ isLoading, error, } = useAsyncState(async () => { - const topLevelFrame = await getTopFrameFromSidebar(); + const topLevelFrame = getConnectedTarget(); const elementVariableReference = `@${elementKey}`; diff --git a/src/contentScript/sidebarDomControllerLite.ts b/src/contentScript/sidebarDomControllerLite.ts index d7070b998b..de6d0f597e 100644 --- a/src/contentScript/sidebarDomControllerLite.ts +++ b/src/contentScript/sidebarDomControllerLite.ts @@ -24,6 +24,8 @@ 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 { getSidebarPath } from "@/sidebar/messenger/api"; +import { getThisFrame } from "webext-messenger"; export const SIDEBAR_WIDTH_CSS_PROPERTY = "--pb-sidebar-width"; const ORIGINAL_MARGIN_CSS_PROPERTY = "--pb-original-margin-right"; @@ -88,7 +90,7 @@ export function removeSidebarFrame(): boolean { } /** Inserts the element; Returns false if it already existed */ -export function insertSidebarFrame(): boolean { +export async function insertSidebarFrame(): Promise { console.debug("sidebarDomControllerLite:insertSidebarFrame", { isSidebarFrameVisible: isSidebarFrameVisible(), }); @@ -100,12 +102,14 @@ export function insertSidebarFrame(): boolean { storeOriginalCSSOnce(); const nonce = uuidv4(); - const actionURL = browser.runtime.getURL("sidebar.html"); + const { tabId } = await getThisFrame(); + const actionUrl = new URL(browser.runtime.getURL(getSidebarPath(tabId))); + actionUrl.searchParams.set("nonce", nonce); setSidebarWidth(SIDEBAR_WIDTH_PX); const iframe = document.createElement("iframe"); - iframe.src = `${actionURL}?nonce=${nonce}`; + iframe.src = actionUrl.href; Object.assign(iframe.style, { position: "fixed", @@ -145,7 +149,7 @@ export function insertSidebarFrame(): boolean { /** * Toggle the sidebar frame. Returns true if the sidebar is now visible, false otherwise. */ -export function toggleSidebarFrame(): boolean { +export async function toggleSidebarFrame(): Promise { console.debug("sidebarDomControllerLite:toggleSidebarFrame", { isSidebarFrameVisible: isSidebarFrameVisible(), }); @@ -155,6 +159,6 @@ export function toggleSidebarFrame(): boolean { return false; } - insertSidebarFrame(); + await insertSidebarFrame(); return true; } diff --git a/src/contrib/automationanywhere/aaFrameProtocol.ts b/src/contrib/automationanywhere/aaFrameProtocol.ts index 0b1434b2ed..fe6406db34 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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; import { getCopilotHostData } from "@/contentScript/messenger/api"; /** @@ -126,7 +126,7 @@ export async function initCopilotMessenger(): Promise { }); // Fetch the current data from the content script when the frame loads - const frame = await getTopFrameFromSidebar(); + const frame = getConnectedTarget(); const data = await getCopilotHostData(frame); console.debug("Setting initial Co-Pilot data", { location: window.location.href, diff --git a/src/mv3/sidePanelMigration.ts b/src/mv3/sidePanelMigration.ts index 1327d8efc4..3e7abc8225 100644 --- a/src/mv3/sidePanelMigration.ts +++ b/src/mv3/sidePanelMigration.ts @@ -17,17 +17,10 @@ /** @file Temporary helpers useful for the MV3 sidePanel transition */ -import { getMethod, getTopLevelFrame } from "webext-messenger"; -import { - _openSidePanel, - getAssociatedTarget, -} from "@/sidebar/sidePanel/messenger/api"; +import { getMethod } from "webext-messenger"; +import { _openSidePanel } from "@/sidebar/sidePanel/messenger/api"; import { isMV3 } from "./api"; -export const getTopFrameFromSidebar = isMV3() - ? getAssociatedTarget - : getTopLevelFrame; - export const openSidePanel = isMV3() ? _openSidePanel : // Called via `getMethod` until we complete the strictNullChecks transition diff --git a/src/sidebar/ConnectedSidebar.tsx b/src/sidebar/ConnectedSidebar.tsx index ebfca5f51b..6024296ea8 100644 --- a/src/sidebar/ConnectedSidebar.tsx +++ b/src/sidebar/ConnectedSidebar.tsx @@ -42,7 +42,7 @@ import { ensureExtensionPointsInstalled, getReservedSidebarEntries, } from "@/contentScript/messenger/api"; -import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; 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 getTopFrameFromSidebar(); + const topFrame = getConnectedTarget(); // Ensure persistent sidebar extension points have been installed to have reserve their panels for the sidebar await ensureExtensionPointsInstalled(topFrame); @@ -153,23 +153,21 @@ const ConnectedSidebar: React.VFC = () => { }, []); return ( -
- - - {sidebarIsEmpty ? ( - - - - ) : ( - - )} - - -
+ + + {sidebarIsEmpty ? ( + + + + ) : ( + + )} + + ); }; diff --git a/src/sidebar/PanelBody.tsx b/src/sidebar/PanelBody.tsx index ee4daf1b50..9d5870b93b 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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; import { type DynamicPath } from "@/components/documentBuilder/documentBuilderTypes"; import { mapPathToTraceBranches } from "@/components/documentBuilder/utils"; @@ -205,7 +205,7 @@ const PanelBody: React.FunctionComponent<{ ); } - const topLevelFrame = await getTopFrameFromSidebar(); + const topLevelFrame = getConnectedTarget(); await runHeadlessPipeline(topLevelFrame, { nonce: uuidv4(), diff --git a/src/sidebar/SidebarBody.tsx b/src/sidebar/SidebarBody.tsx index 2a45f4e4cd..d65063329a 100644 --- a/src/sidebar/SidebarBody.tsx +++ b/src/sidebar/SidebarBody.tsx @@ -27,18 +27,22 @@ import { getReasonByUrl as getRestrictedReasonByUrl } from "@/tinyPages/restrict // navigation in the SidebarApp function SidebarBody() { const url = useCurrentUrl(); + console.log("SidebarBody url", url); + const restricted = getRestrictedReasonByUrl(url); return ( <>
- {url ? ( - restricted ? ( - - ) : ( - - ) - ) : null} +
+ {url ? ( + restricted ? ( + + ) : ( + + ) + ) : null} +
); } diff --git a/src/sidebar/Tabs.tsx b/src/sidebar/Tabs.tsx index efe2dbd1bf..edb87d07e8 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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; 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 getTopFrameFromSidebar(); + const frame = getConnectedTarget(); 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 be1d4f3ecf..6097dc3e48 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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; 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 getTopFrameFromSidebar(); + const topFrame = getConnectedTarget(); // 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/connectedTarget.tsx b/src/sidebar/connectedTarget.tsx new file mode 100644 index 0000000000..9e9d41e91f --- /dev/null +++ b/src/sidebar/connectedTarget.tsx @@ -0,0 +1,39 @@ +/* + * 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 { expectContext } from "@/utils/expectContext"; +import { assertNotNullish } from "@/utils/nullishUtils"; +import { once } from "lodash"; +import { type Target, getTabUrl } from "webext-tools"; + +export const getConnectedTabId = once((): number => { + expectContext("sidebar"); + const tabId = new URLSearchParams(window.location.search).get("tabId"); + assertNotNullish( + tabId, + `No tabId argument was found on this page: ${window.location.href}`, + ); + return Number(tabId); +}); + +export function getConnectedTarget(): Target { + return { tabId: getConnectedTabId(), frameId: 0 }; +} + +export async function getAssociatedTargetUrl(): Promise { + return getTabUrl(getConnectedTabId()); +} diff --git a/src/sidebar/hooks/useCurrentUrl.tsx b/src/sidebar/hooks/useCurrentUrl.tsx index 8992b86a06..8f71a0210d 100644 --- a/src/sidebar/hooks/useCurrentUrl.tsx +++ b/src/sidebar/hooks/useCurrentUrl.tsx @@ -21,9 +21,9 @@ import { SimpleEventTarget } from "@/utils/SimpleEventTarget"; import { type Tabs } from "webextension-polyfill"; import { expectContext } from "@/utils/expectContext"; import { - getAssociatedTarget, + getConnectedTabId, getAssociatedTargetUrl, -} from "@/sidebar/sidePanel/messenger/api"; +} from "@/sidebar/connectedTarget"; let lastKnownUrl: string; const urlChanges = new SimpleEventTarget(); @@ -32,7 +32,7 @@ async function onUpdated( tabId: number, { url }: Tabs.OnUpdatedChangeInfoType, ): Promise { - if (tabId === getAssociatedTarget().tabId && lastKnownUrl !== url) { + if (tabId === getConnectedTabId() && lastKnownUrl !== url) { lastKnownUrl = url; urlChanges.emit(url); } diff --git a/src/sidebar/messenger/api.ts b/src/sidebar/messenger/api.ts index 360d4999c4..2bff61a1f4 100644 --- a/src/sidebar/messenger/api.ts +++ b/src/sidebar/messenger/api.ts @@ -25,6 +25,10 @@ import { getThisFrame, } from "webext-messenger"; +export function getSidebarPath(tabId: number): string { + return "/sidebar.html?tabId=" + tabId; +} + export async function getSidebarInThisTab(): Promise { if (!isMV3()) { return { tabId: "this", page: "/sidebar.html" }; @@ -39,7 +43,7 @@ export async function getSidebarInThisTab(): Promise { const frame = await getThisFrame(); return { - page: "/sidebar.html?tabId=" + frame.tabId, + page: getSidebarPath(frame.tabId), }; } diff --git a/src/sidebar/modLauncher/ModLauncher.tsx b/src/sidebar/modLauncher/ModLauncher.tsx index 1398aa09c6..73f6ebd405 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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; const ModLauncher: React.FunctionComponent = () => { const { permit } = useFlags(); @@ -45,7 +45,7 @@ const ModLauncher: React.FunctionComponent = () => { source: "ModLauncher", }); - const frame = await getTopFrameFromSidebar(); + const frame = getConnectedTarget(); showWalkthroughModal(frame); }} > diff --git a/src/sidebar/sidePanel/messenger/api.ts b/src/sidebar/sidePanel/messenger/api.ts index 2347c54860..e8fa095bef 100644 --- a/src/sidebar/sidePanel/messenger/api.ts +++ b/src/sidebar/sidePanel/messenger/api.ts @@ -25,18 +25,6 @@ import { expectContext } from "@/utils/expectContext"; import { messenger } from "webext-messenger"; import { getErrorMessage } from "@/errors/errorHelpers"; import { getSidebarInThisTab } from "@/sidebar/messenger/api"; -import { getTabUrl, type Target } from "webext-tools"; -import { once } from "lodash"; - -export const getAssociatedTarget = once((): Target => { - expectContext("sidebar"); - const tabId = new URLSearchParams(window.location.search).get("tabId"); - return { tabId: Number(tabId), frameId: 0 }; -}); - -export async function getAssociatedTargetUrl(): Promise { - return getTabUrl(getAssociatedTarget()); -} export async function isSidePanelOpen(): Promise { expectContext( diff --git a/src/sidebar/sidebar.html b/src/sidebar/sidebar.html index c91a0cf5e7..4b5dcec41a 100644 --- a/src/sidebar/sidebar.html +++ b/src/sidebar/sidebar.html @@ -20,7 +20,7 @@ - + diff --git a/src/sidebar/sidebar.tsx b/src/sidebar/sidebar.tsx index e3d8b7c23f..84e158fbcd 100644 --- a/src/sidebar/sidebar.tsx +++ b/src/sidebar/sidebar.tsx @@ -35,12 +35,12 @@ import { initRuntimeLogging } from "@/development/runtimeLogging"; import { initCopilotMessenger } from "@/contrib/automationanywhere/aaFrameProtocol"; import { initPerformanceMonitoring } from "@/telemetry/performance"; import { initSidePanel } from "./sidePanel"; -import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; import { sidebarWasLoaded } from "@/contentScript/messenger/api"; async function init(): Promise { ReactDOM.render(, document.querySelector("#container")); - sidebarWasLoaded(await getTopFrameFromSidebar()); + sidebarWasLoaded(getConnectedTarget()); } void initMessengerLogging(); diff --git a/src/sidebar/sidebarSlice.ts b/src/sidebar/sidebarSlice.ts index 5fef57cd57..b342fbb89b 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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; 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 getTopFrameFromSidebar(); + const topLevelFrame = getConnectedTarget(); cancelForm(topLevelFrame, ...forms); } async function cancelPanels(nonces: UUID[]): Promise { - const topLevelFrame = await getTopFrameFromSidebar(); + const topLevelFrame = getConnectedTarget(); 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 getTopFrameFromSidebar(); + const topLevelFrame = getConnectedTarget(); closeTemporaryPanel(topLevelFrame, nonces); } @@ -153,7 +153,7 @@ async function resolvePanel( nonce: UUID, action: Pick, ): Promise { - const topLevelFrame = await getTopFrameFromSidebar(); + const topLevelFrame = getConnectedTarget(); resolveTemporaryPanel(topLevelFrame, nonce, action); } diff --git a/src/sidebar/useHideEmptySidebar.ts b/src/sidebar/useHideEmptySidebar.ts index c087214ea1..a2276f8fa8 100644 --- a/src/sidebar/useHideEmptySidebar.ts +++ b/src/sidebar/useHideEmptySidebar.ts @@ -18,7 +18,7 @@ import useAsyncEffect from "use-async-effect"; import { getReservedSidebarEntries } from "@/contentScript/messenger/api"; import { closeSelf } from "@/sidebar/protocol"; -import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; import { useSelector } from "react-redux"; import { selectClosedTabs, @@ -35,7 +35,7 @@ export const useHideEmptySidebar = () => { useAsyncEffect( async (isMounted) => { - const topFrame = await getTopFrameFromSidebar(); + const topFrame = getConnectedTarget(); const reservedPanelEntries = await getReservedSidebarEntries(topFrame); // We don't want to hide the Sidebar if there are any open reserved panels. From a76a4e4537fe02264789e5e9a7d64539d210bcf9 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Mon, 22 Jan 2024 15:55:59 +0800 Subject: [PATCH 18/30] wip --- src/background/browserAction.ts | 14 +++++++++++--- src/contentScript/sidebarController.tsx | 2 +- src/contentScript/sidebarDomControllerLite.ts | 15 +++++++-------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/background/browserAction.ts b/src/background/browserAction.ts index 8e46f3e043..04021ba107 100644 --- a/src/background/browserAction.ts +++ b/src/background/browserAction.ts @@ -16,10 +16,9 @@ */ import { ensureContentScript } from "@/background/contentScript"; -import { updateSidebar } from "@/contentScript/messenger/api"; import webextAlert from "./webextAlert"; import { browserAction, isMV3, type Tab } from "@/mv3/api"; -import { executeScript } from "webext-content-scripts"; +import { executeFunction, executeScript } from "webext-content-scripts"; import { memoizeUntilSettled } from "@/utils/promiseUtils"; import { openSidePanel } from "@/mv3/sidePanelMigration"; import { setActionPopup } from "webext-tools"; @@ -78,6 +77,15 @@ async function _toggleSidebar(tabId: number, tabUrl: string): Promise { // 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 preparationPromise = executeFunction( + tabId, + (tabId: number) => { + // Temporary. No need to namespace it because it's in the isolated world + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window as any).tabId = tabId; + }, + tabId, + ); const sidebarTogglePromise = executeScript({ tabId, frameId: TOP_LEVEL_FRAME_ID, @@ -97,6 +105,7 @@ async function _toggleSidebar(tabId: number, tabUrl: string): Promise { const contentScriptPromise = ensureContentScript(contentScriptTarget); try { + await preparationPromise; await sidebarTogglePromise; } catch (error) { webextAlert(ERR_UNABLE_TO_OPEN); @@ -107,7 +116,6 @@ async function _toggleSidebar(tabId: number, tabUrl: string): Promise { // 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; - updateSidebar(contentScriptTarget); } async function handleBrowserAction(tab: Tab): Promise { diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index 24002f1119..85675a198c 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -92,7 +92,7 @@ export async function showSidebar(): Promise { // 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)(); } else if (!sidebarMv2.isSidebarFrameVisible()) { - sidebarMv2.insertSidebarFrame(); + await sidebarMv2.insertSidebarFrame(); } try { diff --git a/src/contentScript/sidebarDomControllerLite.ts b/src/contentScript/sidebarDomControllerLite.ts index de6d0f597e..1d52fb07a6 100644 --- a/src/contentScript/sidebarDomControllerLite.ts +++ b/src/contentScript/sidebarDomControllerLite.ts @@ -90,7 +90,7 @@ export function removeSidebarFrame(): boolean { } /** Inserts the element; Returns false if it already existed */ -export async function insertSidebarFrame(): Promise { +export function insertSidebarFrame(): boolean { console.debug("sidebarDomControllerLite:insertSidebarFrame", { isSidebarFrameVisible: isSidebarFrameVisible(), }); @@ -102,8 +102,9 @@ export async function insertSidebarFrame(): Promise { storeOriginalCSSOnce(); const nonce = uuidv4(); - const { tabId } = await getThisFrame(); - const actionUrl = new URL(browser.runtime.getURL(getSidebarPath(tabId))); + const actionUrl = new URL( + browser.runtime.getURL(getSidebarPath(window.sidebarControllerTabId)), + ); actionUrl.searchParams.set("nonce", nonce); setSidebarWidth(SIDEBAR_WIDTH_PX); @@ -147,18 +148,16 @@ export async function insertSidebarFrame(): Promise { } /** - * Toggle the sidebar frame. Returns true if the sidebar is now visible, false otherwise. + * Toggle the sidebar frame */ -export async function toggleSidebarFrame(): Promise { +export function toggleSidebarFrame(): void { console.debug("sidebarDomControllerLite:toggleSidebarFrame", { isSidebarFrameVisible: isSidebarFrameVisible(), }); if (isSidebarFrameVisible()) { removeSidebarFrame(); - return false; } - await insertSidebarFrame(); - return true; + insertSidebarFrame(); } From a1209356f96ccc7ad504435c21364a44eb71a9d3 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Mon, 22 Jan 2024 15:56:05 +0800 Subject: [PATCH 19/30] Revert "wip" This reverts commit a76a4e4537fe02264789e5e9a7d64539d210bcf9. --- src/background/browserAction.ts | 14 +++----------- src/contentScript/sidebarController.tsx | 2 +- src/contentScript/sidebarDomControllerLite.ts | 15 ++++++++------- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/background/browserAction.ts b/src/background/browserAction.ts index 04021ba107..8e46f3e043 100644 --- a/src/background/browserAction.ts +++ b/src/background/browserAction.ts @@ -16,9 +16,10 @@ */ import { ensureContentScript } from "@/background/contentScript"; +import { updateSidebar } from "@/contentScript/messenger/api"; import webextAlert from "./webextAlert"; import { browserAction, isMV3, type Tab } from "@/mv3/api"; -import { executeFunction, executeScript } from "webext-content-scripts"; +import { executeScript } from "webext-content-scripts"; import { memoizeUntilSettled } from "@/utils/promiseUtils"; import { openSidePanel } from "@/mv3/sidePanelMigration"; import { setActionPopup } from "webext-tools"; @@ -77,15 +78,6 @@ async function _toggleSidebar(tabId: number, tabUrl: string): Promise { // 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 preparationPromise = executeFunction( - tabId, - (tabId: number) => { - // Temporary. No need to namespace it because it's in the isolated world - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (window as any).tabId = tabId; - }, - tabId, - ); const sidebarTogglePromise = executeScript({ tabId, frameId: TOP_LEVEL_FRAME_ID, @@ -105,7 +97,6 @@ async function _toggleSidebar(tabId: number, tabUrl: string): Promise { const contentScriptPromise = ensureContentScript(contentScriptTarget); try { - await preparationPromise; await sidebarTogglePromise; } catch (error) { webextAlert(ERR_UNABLE_TO_OPEN); @@ -116,6 +107,7 @@ async function _toggleSidebar(tabId: number, tabUrl: string): Promise { // 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; + updateSidebar(contentScriptTarget); } async function handleBrowserAction(tab: Tab): Promise { diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index 85675a198c..24002f1119 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -92,7 +92,7 @@ export async function showSidebar(): Promise { // 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)(); } else if (!sidebarMv2.isSidebarFrameVisible()) { - await sidebarMv2.insertSidebarFrame(); + sidebarMv2.insertSidebarFrame(); } try { diff --git a/src/contentScript/sidebarDomControllerLite.ts b/src/contentScript/sidebarDomControllerLite.ts index 1d52fb07a6..de6d0f597e 100644 --- a/src/contentScript/sidebarDomControllerLite.ts +++ b/src/contentScript/sidebarDomControllerLite.ts @@ -90,7 +90,7 @@ export function removeSidebarFrame(): boolean { } /** Inserts the element; Returns false if it already existed */ -export function insertSidebarFrame(): boolean { +export async function insertSidebarFrame(): Promise { console.debug("sidebarDomControllerLite:insertSidebarFrame", { isSidebarFrameVisible: isSidebarFrameVisible(), }); @@ -102,9 +102,8 @@ export function insertSidebarFrame(): boolean { storeOriginalCSSOnce(); const nonce = uuidv4(); - const actionUrl = new URL( - browser.runtime.getURL(getSidebarPath(window.sidebarControllerTabId)), - ); + const { tabId } = await getThisFrame(); + const actionUrl = new URL(browser.runtime.getURL(getSidebarPath(tabId))); actionUrl.searchParams.set("nonce", nonce); setSidebarWidth(SIDEBAR_WIDTH_PX); @@ -148,16 +147,18 @@ export function insertSidebarFrame(): boolean { } /** - * Toggle the sidebar frame + * Toggle the sidebar frame. Returns true if the sidebar is now visible, false otherwise. */ -export function toggleSidebarFrame(): void { +export async function toggleSidebarFrame(): Promise { console.debug("sidebarDomControllerLite:toggleSidebarFrame", { isSidebarFrameVisible: isSidebarFrameVisible(), }); if (isSidebarFrameVisible()) { removeSidebarFrame(); + return false; } - insertSidebarFrame(); + await insertSidebarFrame(); + return true; } From 19110b905200ef29564733c424a5c5bcf24ae462 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Mon, 22 Jan 2024 15:56:12 +0800 Subject: [PATCH 20/30] Revert "Backport sidebar URL creation with "tabId" to MV2" This reverts commit 81eb66775d549eb7bf58d69ce3f322f15b5a7be4. --- src/background/sidePanel.ts | 5 +-- src/bricks/renderers/customForm.ts | 6 +-- src/bricks/transformers/brickFactory.ts | 4 +- .../documentBuilder/render/BlockElement.tsx | 4 +- .../documentBuilder/render/ButtonElement.tsx | 4 +- .../documentBuilder/render/ListElement.tsx | 4 +- src/contentScript/sidebarDomControllerLite.ts | 14 +++---- .../automationanywhere/aaFrameProtocol.ts | 4 +- src/mv3/sidePanelMigration.ts | 11 +++++- src/sidebar/ConnectedSidebar.tsx | 36 +++++++++-------- src/sidebar/PanelBody.tsx | 4 +- src/sidebar/SidebarBody.tsx | 18 ++++----- src/sidebar/Tabs.tsx | 4 +- src/sidebar/activateMod/ActivateModPanel.tsx | 4 +- src/sidebar/connectedTarget.tsx | 39 ------------------- src/sidebar/hooks/useCurrentUrl.tsx | 6 +-- src/sidebar/messenger/api.ts | 6 +-- src/sidebar/modLauncher/ModLauncher.tsx | 4 +- src/sidebar/sidePanel/messenger/api.ts | 12 ++++++ src/sidebar/sidebar.html | 2 +- src/sidebar/sidebar.tsx | 4 +- src/sidebar/sidebarSlice.ts | 10 ++--- src/sidebar/useHideEmptySidebar.ts | 4 +- 23 files changed, 89 insertions(+), 120 deletions(-) delete mode 100644 src/sidebar/connectedTarget.tsx diff --git a/src/background/sidePanel.ts b/src/background/sidePanel.ts index 6fc1e913f5..8b61b21693 100644 --- a/src/background/sidePanel.ts +++ b/src/background/sidePanel.ts @@ -18,7 +18,6 @@ import { openSidePanel } from "@/mv3/sidePanelMigration"; import type { MessengerMeta } from "webext-messenger"; import { isMV3 } from "@/mv3/api"; -import { getSidebarPath } from "@/sidebar/messenger/api"; export async function showMySidePanel(this: MessengerMeta): Promise { await openSidePanel(this.trace[0].tab.id); @@ -36,7 +35,7 @@ export async function initSidePanel(): Promise { if (tabId) { void chrome.sidePanel.setOptions({ tabId, - path: getSidebarPath(tabId), + path: "sidebar.html?tabId=" + tabId, }); } }); @@ -47,7 +46,7 @@ export async function initSidePanel(): Promise { existingTabs.map(async ({ id: tabId, url }) => chrome.sidePanel.setOptions({ tabId, - path: getSidebarPath(tabId), + path: "sidebar.html?tabId=" + tabId, enabled: true, }), ), diff --git a/src/bricks/renderers/customForm.ts b/src/bricks/renderers/customForm.ts index 3ec763acd1..b009003029 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 { getConnectedTarget } from "@/sidebar/connectedTarget"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; 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 = getConnectedTarget(); + const topLevelFrame = await getTopFrameFromSidebar(); // 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 = getConnectedTarget(); + const topLevelFrame = await getTopFrameFromSidebar(); // 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 cb8a287622..4a48d18398 100644 --- a/src/bricks/transformers/brickFactory.ts +++ b/src/bricks/transformers/brickFactory.ts @@ -50,7 +50,7 @@ import { import { type UnknownObject } from "@/types/objectTypes"; import { isPipelineExpression } from "@/utils/expressionUtils"; import { isContentScript } from "webext-detect-page"; -import { getConnectedTarget } from "@/sidebar/connectedTarget"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; import { getTopLevelFrame } from "webext-messenger"; import { uuidv4 } from "@/types/helpers"; import { isSpecificError } from "@/errors/errorHelpers"; @@ -348,7 +348,7 @@ class UserDefinedBrick extends BrickABC { // The modal is always an iframe in the same tab, // but the sidebar varies. This code handles the 3 cases. const topLevelFrame = isBrowserSidebar() - ? getConnectedTarget() + ? await getTopFrameFromSidebar() : await getTopLevelFrame(); try { diff --git a/src/components/documentBuilder/render/BlockElement.tsx b/src/components/documentBuilder/render/BlockElement.tsx index e41512d128..3e74c47fd3 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 { getConnectedTarget } from "@/sidebar/connectedTarget"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; 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 = getConnectedTarget(); + const topLevelFrame = await getTopFrameFromSidebar(); return runRendererPipeline(topLevelFrame, { nonce: uuidv4(), diff --git a/src/components/documentBuilder/render/ButtonElement.tsx b/src/components/documentBuilder/render/ButtonElement.tsx index 040ebd7a2f..ecefcc5ed4 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 { getConnectedTarget } from "@/sidebar/connectedTarget"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; import { getRootCause, hasSpecificErrorCause } from "@/errors/errorHelpers"; import { SubmitPanelAction } from "@/bricks/errors"; import cx from "classnames"; @@ -67,7 +67,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 = getConnectedTarget(); + const topLevelFrame = await getTopFrameFromSidebar(); try { await runHeadlessPipeline(topLevelFrame, { diff --git a/src/components/documentBuilder/render/ListElement.tsx b/src/components/documentBuilder/render/ListElement.tsx index 5dc48bccfa..aca99597e7 100644 --- a/src/components/documentBuilder/render/ListElement.tsx +++ b/src/components/documentBuilder/render/ListElement.tsx @@ -35,7 +35,7 @@ import DelayedRender from "@/components/DelayedRender"; import { isDeferExpression } from "@/utils/expressionUtils"; import { isNullOrBlank } from "@/utils/stringUtils"; import { joinPathParts } from "@/utils/formUtils"; -import { getConnectedTarget } from "@/sidebar/connectedTarget"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; type DocumentListProps = { array: UnknownObject[]; @@ -66,7 +66,7 @@ const ListElementInternal: React.FC = ({ isLoading, error, } = useAsyncState(async () => { - const topLevelFrame = getConnectedTarget(); + const topLevelFrame = await getTopFrameFromSidebar(); const elementVariableReference = `@${elementKey}`; diff --git a/src/contentScript/sidebarDomControllerLite.ts b/src/contentScript/sidebarDomControllerLite.ts index de6d0f597e..d7070b998b 100644 --- a/src/contentScript/sidebarDomControllerLite.ts +++ b/src/contentScript/sidebarDomControllerLite.ts @@ -24,8 +24,6 @@ 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 { getSidebarPath } from "@/sidebar/messenger/api"; -import { getThisFrame } from "webext-messenger"; export const SIDEBAR_WIDTH_CSS_PROPERTY = "--pb-sidebar-width"; const ORIGINAL_MARGIN_CSS_PROPERTY = "--pb-original-margin-right"; @@ -90,7 +88,7 @@ export function removeSidebarFrame(): boolean { } /** Inserts the element; Returns false if it already existed */ -export async function insertSidebarFrame(): Promise { +export function insertSidebarFrame(): boolean { console.debug("sidebarDomControllerLite:insertSidebarFrame", { isSidebarFrameVisible: isSidebarFrameVisible(), }); @@ -102,14 +100,12 @@ export async function insertSidebarFrame(): Promise { storeOriginalCSSOnce(); const nonce = uuidv4(); - const { tabId } = await getThisFrame(); - const actionUrl = new URL(browser.runtime.getURL(getSidebarPath(tabId))); - actionUrl.searchParams.set("nonce", nonce); + const actionURL = browser.runtime.getURL("sidebar.html"); setSidebarWidth(SIDEBAR_WIDTH_PX); const iframe = document.createElement("iframe"); - iframe.src = actionUrl.href; + iframe.src = `${actionURL}?nonce=${nonce}`; Object.assign(iframe.style, { position: "fixed", @@ -149,7 +145,7 @@ export async function insertSidebarFrame(): Promise { /** * Toggle the sidebar frame. Returns true if the sidebar is now visible, false otherwise. */ -export async function toggleSidebarFrame(): Promise { +export function toggleSidebarFrame(): boolean { console.debug("sidebarDomControllerLite:toggleSidebarFrame", { isSidebarFrameVisible: isSidebarFrameVisible(), }); @@ -159,6 +155,6 @@ export async function toggleSidebarFrame(): Promise { return false; } - await insertSidebarFrame(); + insertSidebarFrame(); return true; } diff --git a/src/contrib/automationanywhere/aaFrameProtocol.ts b/src/contrib/automationanywhere/aaFrameProtocol.ts index fe6406db34..0b1434b2ed 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 { getConnectedTarget } from "@/sidebar/connectedTarget"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; import { getCopilotHostData } from "@/contentScript/messenger/api"; /** @@ -126,7 +126,7 @@ export async function initCopilotMessenger(): Promise { }); // Fetch the current data from the content script when the frame loads - const frame = getConnectedTarget(); + const frame = await getTopFrameFromSidebar(); const data = await getCopilotHostData(frame); console.debug("Setting initial Co-Pilot data", { location: window.location.href, diff --git a/src/mv3/sidePanelMigration.ts b/src/mv3/sidePanelMigration.ts index 3e7abc8225..1327d8efc4 100644 --- a/src/mv3/sidePanelMigration.ts +++ b/src/mv3/sidePanelMigration.ts @@ -17,10 +17,17 @@ /** @file Temporary helpers useful for the MV3 sidePanel transition */ -import { getMethod } from "webext-messenger"; -import { _openSidePanel } from "@/sidebar/sidePanel/messenger/api"; +import { getMethod, getTopLevelFrame } from "webext-messenger"; +import { + _openSidePanel, + getAssociatedTarget, +} from "@/sidebar/sidePanel/messenger/api"; import { isMV3 } from "./api"; +export const getTopFrameFromSidebar = isMV3() + ? getAssociatedTarget + : getTopLevelFrame; + export const openSidePanel = isMV3() ? _openSidePanel : // Called via `getMethod` until we complete the strictNullChecks transition diff --git a/src/sidebar/ConnectedSidebar.tsx b/src/sidebar/ConnectedSidebar.tsx index 6024296ea8..ebfca5f51b 100644 --- a/src/sidebar/ConnectedSidebar.tsx +++ b/src/sidebar/ConnectedSidebar.tsx @@ -42,7 +42,7 @@ import { ensureExtensionPointsInstalled, getReservedSidebarEntries, } from "@/contentScript/messenger/api"; -import { getConnectedTarget } from "@/sidebar/connectedTarget"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; 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 = getConnectedTarget(); + const topFrame = await getTopFrameFromSidebar(); // Ensure persistent sidebar extension points have been installed to have reserve their panels for the sidebar await ensureExtensionPointsInstalled(topFrame); @@ -153,21 +153,23 @@ const ConnectedSidebar: React.VFC = () => { }, []); return ( - - - {sidebarIsEmpty ? ( - - - - ) : ( - - )} - - +
+ + + {sidebarIsEmpty ? ( + + + + ) : ( + + )} + + +
); }; diff --git a/src/sidebar/PanelBody.tsx b/src/sidebar/PanelBody.tsx index 9d5870b93b..ee4daf1b50 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 { getConnectedTarget } from "@/sidebar/connectedTarget"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; import { type DynamicPath } from "@/components/documentBuilder/documentBuilderTypes"; import { mapPathToTraceBranches } from "@/components/documentBuilder/utils"; @@ -205,7 +205,7 @@ const PanelBody: React.FunctionComponent<{ ); } - const topLevelFrame = getConnectedTarget(); + const topLevelFrame = await getTopFrameFromSidebar(); await runHeadlessPipeline(topLevelFrame, { nonce: uuidv4(), diff --git a/src/sidebar/SidebarBody.tsx b/src/sidebar/SidebarBody.tsx index d65063329a..2a45f4e4cd 100644 --- a/src/sidebar/SidebarBody.tsx +++ b/src/sidebar/SidebarBody.tsx @@ -27,22 +27,18 @@ import { getReasonByUrl as getRestrictedReasonByUrl } from "@/tinyPages/restrict // navigation in the SidebarApp function SidebarBody() { const url = useCurrentUrl(); - console.log("SidebarBody url", url); - const restricted = getRestrictedReasonByUrl(url); return ( <>
-
- {url ? ( - restricted ? ( - - ) : ( - - ) - ) : null} -
+ {url ? ( + restricted ? ( + + ) : ( + + ) + ) : null} ); } diff --git a/src/sidebar/Tabs.tsx b/src/sidebar/Tabs.tsx index edb87d07e8..efe2dbd1bf 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 { getConnectedTarget } from "@/sidebar/connectedTarget"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; 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 = getConnectedTarget(); + const frame = await getTopFrameFromSidebar(); 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 6097dc3e48..be1d4f3ecf 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 { getConnectedTarget } from "@/sidebar/connectedTarget"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; 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 = getConnectedTarget(); + const topFrame = await getTopFrameFromSidebar(); // 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/connectedTarget.tsx b/src/sidebar/connectedTarget.tsx deleted file mode 100644 index 9e9d41e91f..0000000000 --- a/src/sidebar/connectedTarget.tsx +++ /dev/null @@ -1,39 +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 . - */ - -import { expectContext } from "@/utils/expectContext"; -import { assertNotNullish } from "@/utils/nullishUtils"; -import { once } from "lodash"; -import { type Target, getTabUrl } from "webext-tools"; - -export const getConnectedTabId = once((): number => { - expectContext("sidebar"); - const tabId = new URLSearchParams(window.location.search).get("tabId"); - assertNotNullish( - tabId, - `No tabId argument was found on this page: ${window.location.href}`, - ); - return Number(tabId); -}); - -export function getConnectedTarget(): Target { - return { tabId: getConnectedTabId(), frameId: 0 }; -} - -export async function getAssociatedTargetUrl(): Promise { - return getTabUrl(getConnectedTabId()); -} diff --git a/src/sidebar/hooks/useCurrentUrl.tsx b/src/sidebar/hooks/useCurrentUrl.tsx index 8f71a0210d..8992b86a06 100644 --- a/src/sidebar/hooks/useCurrentUrl.tsx +++ b/src/sidebar/hooks/useCurrentUrl.tsx @@ -21,9 +21,9 @@ import { SimpleEventTarget } from "@/utils/SimpleEventTarget"; import { type Tabs } from "webextension-polyfill"; import { expectContext } from "@/utils/expectContext"; import { - getConnectedTabId, + getAssociatedTarget, getAssociatedTargetUrl, -} from "@/sidebar/connectedTarget"; +} from "@/sidebar/sidePanel/messenger/api"; let lastKnownUrl: string; const urlChanges = new SimpleEventTarget(); @@ -32,7 +32,7 @@ async function onUpdated( tabId: number, { url }: Tabs.OnUpdatedChangeInfoType, ): Promise { - if (tabId === getConnectedTabId() && lastKnownUrl !== url) { + if (tabId === getAssociatedTarget().tabId && lastKnownUrl !== url) { lastKnownUrl = url; urlChanges.emit(url); } diff --git a/src/sidebar/messenger/api.ts b/src/sidebar/messenger/api.ts index 2bff61a1f4..360d4999c4 100644 --- a/src/sidebar/messenger/api.ts +++ b/src/sidebar/messenger/api.ts @@ -25,10 +25,6 @@ import { getThisFrame, } from "webext-messenger"; -export function getSidebarPath(tabId: number): string { - return "/sidebar.html?tabId=" + tabId; -} - export async function getSidebarInThisTab(): Promise { if (!isMV3()) { return { tabId: "this", page: "/sidebar.html" }; @@ -43,7 +39,7 @@ export async function getSidebarInThisTab(): Promise { const frame = await getThisFrame(); return { - page: getSidebarPath(frame.tabId), + page: "/sidebar.html?tabId=" + frame.tabId, }; } diff --git a/src/sidebar/modLauncher/ModLauncher.tsx b/src/sidebar/modLauncher/ModLauncher.tsx index 73f6ebd405..1398aa09c6 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 { getConnectedTarget } from "@/sidebar/connectedTarget"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; const ModLauncher: React.FunctionComponent = () => { const { permit } = useFlags(); @@ -45,7 +45,7 @@ const ModLauncher: React.FunctionComponent = () => { source: "ModLauncher", }); - const frame = getConnectedTarget(); + const frame = await getTopFrameFromSidebar(); showWalkthroughModal(frame); }} > diff --git a/src/sidebar/sidePanel/messenger/api.ts b/src/sidebar/sidePanel/messenger/api.ts index e8fa095bef..2347c54860 100644 --- a/src/sidebar/sidePanel/messenger/api.ts +++ b/src/sidebar/sidePanel/messenger/api.ts @@ -25,6 +25,18 @@ import { expectContext } from "@/utils/expectContext"; import { messenger } from "webext-messenger"; import { getErrorMessage } from "@/errors/errorHelpers"; import { getSidebarInThisTab } from "@/sidebar/messenger/api"; +import { getTabUrl, type Target } from "webext-tools"; +import { once } from "lodash"; + +export const getAssociatedTarget = once((): Target => { + expectContext("sidebar"); + const tabId = new URLSearchParams(window.location.search).get("tabId"); + return { tabId: Number(tabId), frameId: 0 }; +}); + +export async function getAssociatedTargetUrl(): Promise { + return getTabUrl(getAssociatedTarget()); +} export async function isSidePanelOpen(): Promise { expectContext( diff --git a/src/sidebar/sidebar.html b/src/sidebar/sidebar.html index 4b5dcec41a..c91a0cf5e7 100644 --- a/src/sidebar/sidebar.html +++ b/src/sidebar/sidebar.html @@ -20,7 +20,7 @@ - + diff --git a/src/sidebar/sidebar.tsx b/src/sidebar/sidebar.tsx index 84e158fbcd..e3d8b7c23f 100644 --- a/src/sidebar/sidebar.tsx +++ b/src/sidebar/sidebar.tsx @@ -35,12 +35,12 @@ import { initRuntimeLogging } from "@/development/runtimeLogging"; import { initCopilotMessenger } from "@/contrib/automationanywhere/aaFrameProtocol"; import { initPerformanceMonitoring } from "@/telemetry/performance"; import { initSidePanel } from "./sidePanel"; -import { getConnectedTarget } from "@/sidebar/connectedTarget"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; import { sidebarWasLoaded } from "@/contentScript/messenger/api"; async function init(): Promise { ReactDOM.render(, document.querySelector("#container")); - sidebarWasLoaded(getConnectedTarget()); + sidebarWasLoaded(await getTopFrameFromSidebar()); } void initMessengerLogging(); diff --git a/src/sidebar/sidebarSlice.ts b/src/sidebar/sidebarSlice.ts index b342fbb89b..5fef57cd57 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 { getConnectedTarget } from "@/sidebar/connectedTarget"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; 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 = getConnectedTarget(); + const topLevelFrame = await getTopFrameFromSidebar(); cancelForm(topLevelFrame, ...forms); } async function cancelPanels(nonces: UUID[]): Promise { - const topLevelFrame = getConnectedTarget(); + const topLevelFrame = await getTopFrameFromSidebar(); 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 = getConnectedTarget(); + const topLevelFrame = await getTopFrameFromSidebar(); closeTemporaryPanel(topLevelFrame, nonces); } @@ -153,7 +153,7 @@ async function resolvePanel( nonce: UUID, action: Pick, ): Promise { - const topLevelFrame = getConnectedTarget(); + const topLevelFrame = await getTopFrameFromSidebar(); resolveTemporaryPanel(topLevelFrame, nonce, action); } diff --git a/src/sidebar/useHideEmptySidebar.ts b/src/sidebar/useHideEmptySidebar.ts index a2276f8fa8..c087214ea1 100644 --- a/src/sidebar/useHideEmptySidebar.ts +++ b/src/sidebar/useHideEmptySidebar.ts @@ -18,7 +18,7 @@ import useAsyncEffect from "use-async-effect"; import { getReservedSidebarEntries } from "@/contentScript/messenger/api"; import { closeSelf } from "@/sidebar/protocol"; -import { getConnectedTarget } from "@/sidebar/connectedTarget"; +import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; import { useSelector } from "react-redux"; import { selectClosedTabs, @@ -35,7 +35,7 @@ export const useHideEmptySidebar = () => { useAsyncEffect( async (isMounted) => { - const topFrame = getConnectedTarget(); + const topFrame = await getTopFrameFromSidebar(); const reservedPanelEntries = await getReservedSidebarEntries(topFrame); // We don't want to hide the Sidebar if there are any open reserved panels. From cc230e3ec29244dc80889e16c119b860c56948a0 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Mon, 22 Jan 2024 16:20:14 +0800 Subject: [PATCH 21/30] Restore MV2 support and reorganize target linking --- src/background/sidePanel.ts | 5 +- src/bricks/renderers/customForm.ts | 6 +- src/bricks/transformers/brickFactory.ts | 4 +- .../documentBuilder/render/BlockElement.tsx | 4 +- .../documentBuilder/render/ButtonElement.tsx | 4 +- .../documentBuilder/render/ListElement.tsx | 4 +- src/contentScript/sidebarDomControllerLite.ts | 5 +- .../automationanywhere/aaFrameProtocol.ts | 4 +- src/mv3/sidePanelMigration.ts | 11 +- src/sidebar/ConnectedSidebar.tsx | 36 +- src/sidebar/PanelBody.tsx | 4 +- src/sidebar/SidebarBody.tsx | 18 +- src/sidebar/Tabs.tsx | 4 +- .../ConnectedSidebar.test.tsx.snap | 400 +++++++++--------- .../__snapshots__/SidebarBody.test.tsx.snap | 50 ++- src/sidebar/activateMod/ActivateModPanel.tsx | 4 +- src/sidebar/connectedTarget.tsx | 49 +++ src/sidebar/hooks/useCurrentUrl.tsx | 6 +- src/sidebar/messenger/api.ts | 28 +- src/sidebar/modLauncher/ModLauncher.tsx | 4 +- src/sidebar/sidePanel/messenger/api.ts | 32 +- src/sidebar/sidebar.tsx | 4 +- src/sidebar/sidebarSlice.ts | 10 +- src/sidebar/useHideEmptySidebar.ts | 4 +- 24 files changed, 363 insertions(+), 337 deletions(-) create mode 100644 src/sidebar/connectedTarget.tsx diff --git a/src/background/sidePanel.ts b/src/background/sidePanel.ts index 8b61b21693..427bb474f6 100644 --- a/src/background/sidePanel.ts +++ b/src/background/sidePanel.ts @@ -18,6 +18,7 @@ import { openSidePanel } from "@/mv3/sidePanelMigration"; import type { MessengerMeta } from "webext-messenger"; import { isMV3 } from "@/mv3/api"; +import { getSidebarPath } from "@/sidebar/sidePanel/messenger/api"; export async function showMySidePanel(this: MessengerMeta): Promise { await openSidePanel(this.trace[0].tab.id); @@ -35,7 +36,7 @@ export async function initSidePanel(): Promise { if (tabId) { void chrome.sidePanel.setOptions({ tabId, - path: "sidebar.html?tabId=" + tabId, + path: getSidebarPath(tabId), }); } }); @@ -46,7 +47,7 @@ export async function initSidePanel(): Promise { existingTabs.map(async ({ id: tabId, url }) => chrome.sidePanel.setOptions({ tabId, - path: "sidebar.html?tabId=" + tabId, + path: getSidebarPath(tabId), enabled: true, }), ), diff --git a/src/bricks/renderers/customForm.ts b/src/bricks/renderers/customForm.ts index b009003029..8732c0bd80 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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; 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 getTopFrameFromSidebar(); + const topLevelFrame = await getConnectedTarget(); // 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 getTopFrameFromSidebar(); + const topLevelFrame = await getConnectedTarget(); // 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 4a48d18398..d45a21543d 100644 --- a/src/bricks/transformers/brickFactory.ts +++ b/src/bricks/transformers/brickFactory.ts @@ -50,7 +50,7 @@ import { import { type UnknownObject } from "@/types/objectTypes"; import { isPipelineExpression } from "@/utils/expressionUtils"; import { isContentScript } from "webext-detect-page"; -import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; import { getTopLevelFrame } from "webext-messenger"; import { uuidv4 } from "@/types/helpers"; import { isSpecificError } from "@/errors/errorHelpers"; @@ -348,7 +348,7 @@ class UserDefinedBrick extends BrickABC { // The modal is always an iframe in the same tab, // but the sidebar varies. This code handles the 3 cases. const topLevelFrame = isBrowserSidebar() - ? await getTopFrameFromSidebar() + ? await getConnectedTarget() : await getTopLevelFrame(); try { diff --git a/src/components/documentBuilder/render/BlockElement.tsx b/src/components/documentBuilder/render/BlockElement.tsx index 3e74c47fd3..f2989dd7c2 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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; 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 getTopFrameFromSidebar(); + const topLevelFrame = await getConnectedTarget(); return runRendererPipeline(topLevelFrame, { nonce: uuidv4(), diff --git a/src/components/documentBuilder/render/ButtonElement.tsx b/src/components/documentBuilder/render/ButtonElement.tsx index ecefcc5ed4..5680a336e4 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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; import { getRootCause, hasSpecificErrorCause } from "@/errors/errorHelpers"; import { SubmitPanelAction } from "@/bricks/errors"; import cx from "classnames"; @@ -67,7 +67,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 getTopFrameFromSidebar(); + const topLevelFrame = await getConnectedTarget(); try { await runHeadlessPipeline(topLevelFrame, { diff --git a/src/components/documentBuilder/render/ListElement.tsx b/src/components/documentBuilder/render/ListElement.tsx index aca99597e7..7500d3d617 100644 --- a/src/components/documentBuilder/render/ListElement.tsx +++ b/src/components/documentBuilder/render/ListElement.tsx @@ -35,7 +35,7 @@ import DelayedRender from "@/components/DelayedRender"; import { isDeferExpression } from "@/utils/expressionUtils"; import { isNullOrBlank } from "@/utils/stringUtils"; import { joinPathParts } from "@/utils/formUtils"; -import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; type DocumentListProps = { array: UnknownObject[]; @@ -66,7 +66,7 @@ const ListElementInternal: React.FC = ({ isLoading, error, } = useAsyncState(async () => { - const topLevelFrame = await getTopFrameFromSidebar(); + const topLevelFrame = await getConnectedTarget(); const elementVariableReference = `@${elementKey}`; diff --git a/src/contentScript/sidebarDomControllerLite.ts b/src/contentScript/sidebarDomControllerLite.ts index d7070b998b..4491a4f45f 100644 --- a/src/contentScript/sidebarDomControllerLite.ts +++ b/src/contentScript/sidebarDomControllerLite.ts @@ -100,12 +100,13 @@ export function insertSidebarFrame(): boolean { storeOriginalCSSOnce(); const nonce = uuidv4(); - const actionURL = browser.runtime.getURL("sidebar.html"); + const actionUrl = new URL(browser.runtime.getURL("sidebar.html")); + actionUrl.searchParams.set("nonce", nonce); setSidebarWidth(SIDEBAR_WIDTH_PX); const iframe = document.createElement("iframe"); - iframe.src = `${actionURL}?nonce=${nonce}`; + iframe.src = actionUrl.href; Object.assign(iframe.style, { position: "fixed", diff --git a/src/contrib/automationanywhere/aaFrameProtocol.ts b/src/contrib/automationanywhere/aaFrameProtocol.ts index 0b1434b2ed..35b238e512 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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; import { getCopilotHostData } from "@/contentScript/messenger/api"; /** @@ -126,7 +126,7 @@ export async function initCopilotMessenger(): Promise { }); // Fetch the current data from the content script when the frame loads - const frame = await getTopFrameFromSidebar(); + const frame = await getConnectedTarget(); const data = await getCopilotHostData(frame); console.debug("Setting initial Co-Pilot data", { location: window.location.href, diff --git a/src/mv3/sidePanelMigration.ts b/src/mv3/sidePanelMigration.ts index 1327d8efc4..3e7abc8225 100644 --- a/src/mv3/sidePanelMigration.ts +++ b/src/mv3/sidePanelMigration.ts @@ -17,17 +17,10 @@ /** @file Temporary helpers useful for the MV3 sidePanel transition */ -import { getMethod, getTopLevelFrame } from "webext-messenger"; -import { - _openSidePanel, - getAssociatedTarget, -} from "@/sidebar/sidePanel/messenger/api"; +import { getMethod } from "webext-messenger"; +import { _openSidePanel } from "@/sidebar/sidePanel/messenger/api"; import { isMV3 } from "./api"; -export const getTopFrameFromSidebar = isMV3() - ? getAssociatedTarget - : getTopLevelFrame; - export const openSidePanel = isMV3() ? _openSidePanel : // Called via `getMethod` until we complete the strictNullChecks transition diff --git a/src/sidebar/ConnectedSidebar.tsx b/src/sidebar/ConnectedSidebar.tsx index ebfca5f51b..957380a8f1 100644 --- a/src/sidebar/ConnectedSidebar.tsx +++ b/src/sidebar/ConnectedSidebar.tsx @@ -42,7 +42,7 @@ import { ensureExtensionPointsInstalled, getReservedSidebarEntries, } from "@/contentScript/messenger/api"; -import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; 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 getTopFrameFromSidebar(); + const topFrame = await getConnectedTarget(); // Ensure persistent sidebar extension points have been installed to have reserve their panels for the sidebar await ensureExtensionPointsInstalled(topFrame); @@ -153,23 +153,21 @@ const ConnectedSidebar: React.VFC = () => { }, []); return ( -
- - - {sidebarIsEmpty ? ( - - - - ) : ( - - )} - - -
+ + + {sidebarIsEmpty ? ( + + + + ) : ( + + )} + + ); }; diff --git a/src/sidebar/PanelBody.tsx b/src/sidebar/PanelBody.tsx index ee4daf1b50..8651d4a6ca 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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; import { type DynamicPath } from "@/components/documentBuilder/documentBuilderTypes"; import { mapPathToTraceBranches } from "@/components/documentBuilder/utils"; @@ -205,7 +205,7 @@ const PanelBody: React.FunctionComponent<{ ); } - const topLevelFrame = await getTopFrameFromSidebar(); + const topLevelFrame = await getConnectedTarget(); await runHeadlessPipeline(topLevelFrame, { nonce: uuidv4(), diff --git a/src/sidebar/SidebarBody.tsx b/src/sidebar/SidebarBody.tsx index 2a45f4e4cd..d65063329a 100644 --- a/src/sidebar/SidebarBody.tsx +++ b/src/sidebar/SidebarBody.tsx @@ -27,18 +27,22 @@ import { getReasonByUrl as getRestrictedReasonByUrl } from "@/tinyPages/restrict // navigation in the SidebarApp function SidebarBody() { const url = useCurrentUrl(); + console.log("SidebarBody url", url); + const restricted = getRestrictedReasonByUrl(url); return ( <>
- {url ? ( - restricted ? ( - - ) : ( - - ) - ) : null} +
+ {url ? ( + restricted ? ( + + ) : ( + + ) + ) : null} +
); } diff --git a/src/sidebar/Tabs.tsx b/src/sidebar/Tabs.tsx index efe2dbd1bf..9aea9d6e15 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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; 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 getTopFrameFromSidebar(); + const frame = await getConnectedTarget(); cancelForm(frame, panel.nonce); } else if (isModActivationPanelEntry(panel)) { dispatch(sidebarSlice.actions.hideModActivationPanel()); diff --git a/src/sidebar/__snapshots__/ConnectedSidebar.test.tsx.snap b/src/sidebar/__snapshots__/ConnectedSidebar.test.tsx.snap index a9866b1442..b1f2dede9b 100644 --- a/src/sidebar/__snapshots__/ConnectedSidebar.test.tsx.snap +++ b/src/sidebar/__snapshots__/ConnectedSidebar.test.tsx.snap @@ -6,149 +6,145 @@ exports[`SidebarApp renders 1`] = ` class="full-height" >
diff --git a/src/sidebar/activateMod/ActivateModPanel.tsx b/src/sidebar/activateMod/ActivateModPanel.tsx index be1d4f3ecf..b0e05dc594 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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; 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 getTopFrameFromSidebar(); + const topFrame = await getConnectedTarget(); // 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/connectedTarget.tsx b/src/sidebar/connectedTarget.tsx new file mode 100644 index 0000000000..a659e11635 --- /dev/null +++ b/src/sidebar/connectedTarget.tsx @@ -0,0 +1,49 @@ +/* + * 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 { isMV3 } from "@/mv3/api"; +import { expectContext } from "@/utils/expectContext"; +import { assertNotNullish } from "@/utils/nullishUtils"; +import { once } from "lodash"; +import { type TopLevelFrame, getTopLevelFrame } from "webext-messenger"; +import { getTabUrl } from "webext-tools"; + +function getConnectedTabIdMv3(): number { + expectContext("sidebar"); + const tabId = new URLSearchParams(window.location.search).get("tabId"); + assertNotNullish( + tabId, + `No tabId argument was found on this page: ${window.location.href}`, + ); + return Number(tabId); +} + +async function getConnectedTabIdMv2() { + const { tabId } = await getTopLevelFrame(); + return tabId; +} + +export const getConnectedTabId = once( + isMV3() ? getConnectedTabIdMv3 : getConnectedTabIdMv2, +); +export const getConnectedTarget = isMV3() + ? (): TopLevelFrame => ({ tabId: getConnectedTabIdMv3(), frameId: 0 }) + : getTopLevelFrame; + +export async function getAssociatedTargetUrl(): Promise { + return getTabUrl(await getConnectedTarget()); +} diff --git a/src/sidebar/hooks/useCurrentUrl.tsx b/src/sidebar/hooks/useCurrentUrl.tsx index 8992b86a06..8f71a0210d 100644 --- a/src/sidebar/hooks/useCurrentUrl.tsx +++ b/src/sidebar/hooks/useCurrentUrl.tsx @@ -21,9 +21,9 @@ import { SimpleEventTarget } from "@/utils/SimpleEventTarget"; import { type Tabs } from "webextension-polyfill"; import { expectContext } from "@/utils/expectContext"; import { - getAssociatedTarget, + getConnectedTabId, getAssociatedTargetUrl, -} from "@/sidebar/sidePanel/messenger/api"; +} from "@/sidebar/connectedTarget"; let lastKnownUrl: string; const urlChanges = new SimpleEventTarget(); @@ -32,7 +32,7 @@ async function onUpdated( tabId: number, { url }: Tabs.OnUpdatedChangeInfoType, ): Promise { - if (tabId === getAssociatedTarget().tabId && lastKnownUrl !== url) { + if (tabId === getConnectedTabId() && lastKnownUrl !== url) { lastKnownUrl = url; urlChanges.emit(url); } diff --git a/src/sidebar/messenger/api.ts b/src/sidebar/messenger/api.ts index 360d4999c4..83fc39661b 100644 --- a/src/sidebar/messenger/api.ts +++ b/src/sidebar/messenger/api.ts @@ -16,32 +16,8 @@ */ /* Do not use `registerMethod` in this file */ -import { isMV3 } from "@/mv3/api"; -import { isContentScript } from "webext-detect-page"; -import { - type PageTarget, - getMethod, - getNotifier, - getThisFrame, -} from "webext-messenger"; - -export async function getSidebarInThisTab(): Promise { - if (!isMV3()) { - return { tabId: "this", page: "/sidebar.html" }; - } - - if (!isContentScript()) { - // Probably just other pages importing this file transitively, this is dead code - return { - page: "the sidebar API is only available from the content script", - }; - } - - const frame = await getThisFrame(); - return { - page: "/sidebar.html?tabId=" + frame.tabId, - }; -} +import { getMethod, getNotifier } from "webext-messenger"; +import { getSidebarInThisTab } from "@/sidebar/sidePanel/messenger/api"; const target = getSidebarInThisTab(); diff --git a/src/sidebar/modLauncher/ModLauncher.tsx b/src/sidebar/modLauncher/ModLauncher.tsx index 1398aa09c6..6dbe231e48 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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; const ModLauncher: React.FunctionComponent = () => { const { permit } = useFlags(); @@ -45,7 +45,7 @@ const ModLauncher: React.FunctionComponent = () => { source: "ModLauncher", }); - const frame = await getTopFrameFromSidebar(); + const frame = await getConnectedTarget(); showWalkthroughModal(frame); }} > diff --git a/src/sidebar/sidePanel/messenger/api.ts b/src/sidebar/sidePanel/messenger/api.ts index 2347c54860..033dd4ecb2 100644 --- a/src/sidebar/sidePanel/messenger/api.ts +++ b/src/sidebar/sidePanel/messenger/api.ts @@ -24,18 +24,30 @@ import { expectContext } from "@/utils/expectContext"; import { messenger } from "webext-messenger"; import { getErrorMessage } from "@/errors/errorHelpers"; -import { getSidebarInThisTab } from "@/sidebar/messenger/api"; -import { getTabUrl, type Target } from "webext-tools"; -import { once } from "lodash"; +import { isMV3 } from "@/mv3/api"; +import { isContentScript } from "webext-detect-page"; +import { type PageTarget, getThisFrame } from "webext-messenger"; -export const getAssociatedTarget = once((): Target => { - expectContext("sidebar"); - const tabId = new URLSearchParams(window.location.search).get("tabId"); - return { tabId: Number(tabId), frameId: 0 }; -}); +export function getSidebarPath(tabId: number): string { + return "/sidebar.html?tabId=" + tabId; +} + +export async function getSidebarInThisTab(): Promise { + if (!isMV3()) { + return { tabId: "this", page: "/sidebar.html" }; + } + + if (!isContentScript()) { + // Probably just other pages importing this file transitively, this is dead code + return { + page: "the sidebar API is only available from the content script", + }; + } -export async function getAssociatedTargetUrl(): Promise { - return getTabUrl(getAssociatedTarget()); + const frame = await getThisFrame(); + return { + page: getSidebarPath(frame.tabId), + }; } export async function isSidePanelOpen(): Promise { diff --git a/src/sidebar/sidebar.tsx b/src/sidebar/sidebar.tsx index e3d8b7c23f..18243947c7 100644 --- a/src/sidebar/sidebar.tsx +++ b/src/sidebar/sidebar.tsx @@ -35,12 +35,12 @@ import { initRuntimeLogging } from "@/development/runtimeLogging"; import { initCopilotMessenger } from "@/contrib/automationanywhere/aaFrameProtocol"; import { initPerformanceMonitoring } from "@/telemetry/performance"; import { initSidePanel } from "./sidePanel"; -import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; import { sidebarWasLoaded } from "@/contentScript/messenger/api"; async function init(): Promise { ReactDOM.render(, document.querySelector("#container")); - sidebarWasLoaded(await getTopFrameFromSidebar()); + sidebarWasLoaded(await getConnectedTarget()); } void initMessengerLogging(); diff --git a/src/sidebar/sidebarSlice.ts b/src/sidebar/sidebarSlice.ts index 5fef57cd57..6bb5539064 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 { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; 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 getTopFrameFromSidebar(); + const topLevelFrame = await getConnectedTarget(); cancelForm(topLevelFrame, ...forms); } async function cancelPanels(nonces: UUID[]): Promise { - const topLevelFrame = await getTopFrameFromSidebar(); + const topLevelFrame = await getConnectedTarget(); 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 getTopFrameFromSidebar(); + const topLevelFrame = await getConnectedTarget(); closeTemporaryPanel(topLevelFrame, nonces); } @@ -153,7 +153,7 @@ async function resolvePanel( nonce: UUID, action: Pick, ): Promise { - const topLevelFrame = await getTopFrameFromSidebar(); + const topLevelFrame = await getConnectedTarget(); resolveTemporaryPanel(topLevelFrame, nonce, action); } diff --git a/src/sidebar/useHideEmptySidebar.ts b/src/sidebar/useHideEmptySidebar.ts index c087214ea1..cad8eb4f9f 100644 --- a/src/sidebar/useHideEmptySidebar.ts +++ b/src/sidebar/useHideEmptySidebar.ts @@ -18,7 +18,7 @@ import useAsyncEffect from "use-async-effect"; import { getReservedSidebarEntries } from "@/contentScript/messenger/api"; import { closeSelf } from "@/sidebar/protocol"; -import { getTopFrameFromSidebar } from "@/mv3/sidePanelMigration"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; import { useSelector } from "react-redux"; import { selectClosedTabs, @@ -35,7 +35,7 @@ export const useHideEmptySidebar = () => { useAsyncEffect( async (isMounted) => { - const topFrame = await getTopFrameFromSidebar(); + const topFrame = await getConnectedTarget(); const reservedPanelEntries = await getReservedSidebarEntries(topFrame); // We don't want to hide the Sidebar if there are any open reserved panels. From eacc85545025d096ab9a8a91d827eb51a35d2e0e Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Mon, 22 Jan 2024 17:06:52 +0800 Subject: [PATCH 22/30] Revert page editor changes --- src/pageEditor/hooks/useCurrentUrl.ts | 39 +++++++++++-------- src/sidebar/SidebarBody.test.tsx | 14 +++++-- src/sidebar/SidebarBody.tsx | 4 +- src/sidebar/connectedTarget.tsx | 3 +- ...rrentUrl.tsx => useConnectedTargetUrl.tsx} | 17 +++++--- 5 files changed, 47 insertions(+), 30 deletions(-) rename src/sidebar/hooks/{useCurrentUrl.tsx => useConnectedTargetUrl.tsx} (84%) diff --git a/src/pageEditor/hooks/useCurrentUrl.ts b/src/pageEditor/hooks/useCurrentUrl.ts index 0ae3a1c885..708551efd5 100644 --- a/src/pageEditor/hooks/useCurrentUrl.ts +++ b/src/pageEditor/hooks/useCurrentUrl.ts @@ -17,39 +17,44 @@ import { once } from "lodash"; import { useEffect, useState } from "react"; +import { type Target } from "@/types/messengerTypes"; import { SimpleEventTarget } from "@/utils/SimpleEventTarget"; -import { type Tabs } from "webextension-polyfill"; +import { type WebNavigation } from "webextension-polyfill"; import { expectContext } from "@/utils/expectContext"; import { getCurrentURL } from "@/pageEditor/utils"; -let lastKnownUrl: string; +let tabUrl: string; +const TOP_LEVEL_FRAME_ID = 0; + const urlChanges = new SimpleEventTarget(); -async function onUpdated( - tabId: number, - { url }: Tabs.OnUpdatedChangeInfoType, +// The pageEditor only cares for the top frame +function isCurrentTopFrame({ tabId, frameId }: Target) { + return ( + frameId === TOP_LEVEL_FRAME_ID && + tabId === browser.devtools.inspectedWindow.tabId + ); +} + +async function onNavigation( + target: WebNavigation.OnCommittedDetailsType, ): Promise { - if ( - tabId === browser.devtools.inspectedWindow.tabId && - lastKnownUrl !== url - ) { - lastKnownUrl = url; - urlChanges.emit(url); + if (isCurrentTopFrame(target)) { + tabUrl = target.url; + urlChanges.emit(target.url); } } const startWatching = once(async () => { - browser.tabs.onUpdated.addListener(onUpdated); - - // Get initial URL - lastKnownUrl = await getCurrentURL(); - urlChanges.emit(lastKnownUrl); + browser.webNavigation.onCommitted.addListener(onNavigation); + tabUrl = await getCurrentURL(); + urlChanges.emit(tabUrl); }); export default function useCurrentUrl(): string { expectContext("pageEditor"); - const [url, setUrl] = useState(lastKnownUrl); + const [url, setUrl] = useState(tabUrl); useEffect(() => { urlChanges.add(setUrl); diff --git a/src/sidebar/SidebarBody.test.tsx b/src/sidebar/SidebarBody.test.tsx index 1c4a514779..3353baa31e 100644 --- a/src/sidebar/SidebarBody.test.tsx +++ b/src/sidebar/SidebarBody.test.tsx @@ -19,7 +19,7 @@ import React from "react"; import SidebarBody from "@/sidebar/SidebarBody"; import { render } from "@/sidebar/testHelpers"; import useContextInvalidated from "@/hooks/useContextInvalidated"; -import useCurrentUrl from "@/sidebar/hooks/useCurrentUrl"; +import useConnectedTargetUrl from "@/sidebar/hooks/useConnectedTargetUrl"; jest.mock("@/hooks/useContextInvalidated"); jest.mock("@/sidebar/hooks/useCurrentUrl"); @@ -34,20 +34,26 @@ jest.mock("@/contentScript/messenger/api", () => ({ describe("SidebarBody", () => { test("it renders", async () => { - jest.mocked(useCurrentUrl).mockReturnValueOnce("https://www.example.com"); + jest + .mocked(useConnectedTargetUrl) + .mockReturnValueOnce("https://www.example.com"); const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); test("it renders error when context is invalidated", async () => { - jest.mocked(useCurrentUrl).mockReturnValueOnce("https://www.example.com"); + jest + .mocked(useConnectedTargetUrl) + .mockReturnValueOnce("https://www.example.com"); jest.mocked(useContextInvalidated).mockReturnValueOnce(true); const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); test("it renders error when URL is restricted", async () => { - jest.mocked(useCurrentUrl).mockReturnValueOnce("chrome://extensions"); + jest + .mocked(useConnectedTargetUrl) + .mockReturnValueOnce("chrome://extensions"); const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); diff --git a/src/sidebar/SidebarBody.tsx b/src/sidebar/SidebarBody.tsx index d65063329a..35a13756e9 100644 --- a/src/sidebar/SidebarBody.tsx +++ b/src/sidebar/SidebarBody.tsx @@ -20,13 +20,13 @@ import ConnectedSidebar from "./ConnectedSidebar"; import Header from "./Header"; import ErrorBanner from "./ErrorBanner"; import RestrictedUrlPopupApp from "@/tinyPages/RestrictedUrlPopupApp"; -import useCurrentUrl from "./hooks/useCurrentUrl"; +import useConnectedTargetUrl from "./hooks/useConnectedTargetUrl"; import { getReasonByUrl as getRestrictedReasonByUrl } from "@/tinyPages/restrictedUrlPopupUtils"; // Include MemoryRouter because some of our authentication-gate hooks use useLocation. However, there's currently no // navigation in the SidebarApp function SidebarBody() { - const url = useCurrentUrl(); + const url = useConnectedTargetUrl(); console.log("SidebarBody url", url); const restricted = getRestrictedReasonByUrl(url); diff --git a/src/sidebar/connectedTarget.tsx b/src/sidebar/connectedTarget.tsx index a659e11635..f4d2c5c189 100644 --- a/src/sidebar/connectedTarget.tsx +++ b/src/sidebar/connectedTarget.tsx @@ -40,10 +40,11 @@ async function getConnectedTabIdMv2() { export const getConnectedTabId = once( isMV3() ? getConnectedTabIdMv3 : getConnectedTabIdMv2, ); + export const getConnectedTarget = isMV3() ? (): TopLevelFrame => ({ tabId: getConnectedTabIdMv3(), frameId: 0 }) : getTopLevelFrame; -export async function getAssociatedTargetUrl(): Promise { +export async function getConnectedTargetUrl(): Promise { return getTabUrl(await getConnectedTarget()); } diff --git a/src/sidebar/hooks/useCurrentUrl.tsx b/src/sidebar/hooks/useConnectedTargetUrl.tsx similarity index 84% rename from src/sidebar/hooks/useCurrentUrl.tsx rename to src/sidebar/hooks/useConnectedTargetUrl.tsx index 8f71a0210d..f7609ef153 100644 --- a/src/sidebar/hooks/useCurrentUrl.tsx +++ b/src/sidebar/hooks/useConnectedTargetUrl.tsx @@ -22,7 +22,7 @@ import { type Tabs } from "webextension-polyfill"; import { expectContext } from "@/utils/expectContext"; import { getConnectedTabId, - getAssociatedTargetUrl, + getConnectedTargetUrl, } from "@/sidebar/connectedTarget"; let lastKnownUrl: string; @@ -32,7 +32,14 @@ async function onUpdated( tabId: number, { url }: Tabs.OnUpdatedChangeInfoType, ): Promise { - if (tabId === getConnectedTabId() && lastKnownUrl !== url) { + if ( + // Exclude non-URL updates + url && + // Exclude other tabs + tabId === (await getConnectedTabId()) && + // No URL updates + lastKnownUrl !== url + ) { lastKnownUrl = url; urlChanges.emit(url); } @@ -42,18 +49,16 @@ const startWatching = once(async () => { browser.tabs.onUpdated.addListener(onUpdated); // Get initial URL - lastKnownUrl = await getAssociatedTargetUrl(); + lastKnownUrl = await getConnectedTargetUrl(); console.log("Initial URL", lastKnownUrl); urlChanges.emit(lastKnownUrl); }); -export default function useCurrentUrl(): string { +export default function useConnectedTargetUrl(): string { expectContext("sidebar"); const [url, setUrl] = useState(lastKnownUrl); - console.log("useCurrentUrl", url); - useEffect(() => { urlChanges.add(setUrl); void startWatching(); From bbf0bd3898f8c7bd8c861ded9d3edc7175e197db Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Mon, 22 Jan 2024 17:34:22 +0800 Subject: [PATCH 23/30] Fix tests --- src/sidebar/SidebarBody.test.tsx | 2 +- src/sidebar/SidebarBody.tsx | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/sidebar/SidebarBody.test.tsx b/src/sidebar/SidebarBody.test.tsx index 3353baa31e..633b0840f5 100644 --- a/src/sidebar/SidebarBody.test.tsx +++ b/src/sidebar/SidebarBody.test.tsx @@ -22,7 +22,7 @@ import useContextInvalidated from "@/hooks/useContextInvalidated"; import useConnectedTargetUrl from "@/sidebar/hooks/useConnectedTargetUrl"; jest.mock("@/hooks/useContextInvalidated"); -jest.mock("@/sidebar/hooks/useCurrentUrl"); +jest.mock("@/sidebar/hooks/useConnectedTargetUrl"); jest.mock("@/contentScript/messenger/api", () => ({ ensureExtensionPointsInstalled: jest.fn(), getReservedSidebarEntries: jest.fn().mockResolvedValue({ diff --git a/src/sidebar/SidebarBody.tsx b/src/sidebar/SidebarBody.tsx index 35a13756e9..9dfbc30c5d 100644 --- a/src/sidebar/SidebarBody.tsx +++ b/src/sidebar/SidebarBody.tsx @@ -27,8 +27,6 @@ import { getReasonByUrl as getRestrictedReasonByUrl } from "@/tinyPages/restrict // navigation in the SidebarApp function SidebarBody() { const url = useConnectedTargetUrl(); - console.log("SidebarBody url", url); - const restricted = getRestrictedReasonByUrl(url); return ( <> From 81070ec6d2750dc3afd362c6bec93a2afc5f02e7 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Mon, 22 Jan 2024 17:43:32 +0800 Subject: [PATCH 24/30] Cleanup --- src/sidebar/messenger/api.ts | 4 ++-- src/sidebar/sidePanel/messenger/api.ts | 6 +++--- src/tinyPages/RestrictedUrlPopupApp.tsx | 22 ++++++++-------------- src/tinyPages/restrictedUrlPopup.html | 11 ++++------- 4 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/sidebar/messenger/api.ts b/src/sidebar/messenger/api.ts index 83fc39661b..c77d24987e 100644 --- a/src/sidebar/messenger/api.ts +++ b/src/sidebar/messenger/api.ts @@ -17,9 +17,9 @@ /* Do not use `registerMethod` in this file */ import { getMethod, getNotifier } from "webext-messenger"; -import { getSidebarInThisTab } from "@/sidebar/sidePanel/messenger/api"; +import { getSidebarTargetForCurrentTab } from "@/sidebar/sidePanel/messenger/api"; -const target = getSidebarInThisTab(); +const target = getSidebarTargetForCurrentTab(); const sidebarInThisTab = { renderPanels: getMethod("SIDEBAR_RENDER_PANELS", target), diff --git a/src/sidebar/sidePanel/messenger/api.ts b/src/sidebar/sidePanel/messenger/api.ts index 033dd4ecb2..311ff83c27 100644 --- a/src/sidebar/sidePanel/messenger/api.ts +++ b/src/sidebar/sidePanel/messenger/api.ts @@ -32,7 +32,7 @@ export function getSidebarPath(tabId: number): string { return "/sidebar.html?tabId=" + tabId; } -export async function getSidebarInThisTab(): Promise { +export async function getSidebarTargetForCurrentTab(): Promise { if (!isMV3()) { return { tabId: "this", page: "/sidebar.html" }; } @@ -63,11 +63,11 @@ export async function isSidePanelOpen(): Promise { try { // If ever needed, `isSidePanelOpen` could be called from any context, as long as - // `getSidebarInThisTab` is replaced/complemented by a tabid-specific `{page: "/sidebar.html?tabId=123"}` target + // `getSidebarTargetForCurrentTab` is replaced/complemented by a tabid-specific `{page: "/sidebar.html?tabId=123"}` target await messenger( "SIDEBAR_PING", { retry: false }, - await getSidebarInThisTab(), + await getSidebarTargetForCurrentTab(), ); return true; } catch { diff --git a/src/tinyPages/RestrictedUrlPopupApp.tsx b/src/tinyPages/RestrictedUrlPopupApp.tsx index 5231c36aec..545826fc5f 100644 --- a/src/tinyPages/RestrictedUrlPopupApp.tsx +++ b/src/tinyPages/RestrictedUrlPopupApp.tsx @@ -69,21 +69,15 @@ const RestrictedUrlPopupApp: React.FC<{ reason: string | null }> = ({ // eslint-disable-next-line react-hooks/exhaustive-deps -- only run once on mount }, []); - return ( + return reason === DISPLAY_REASON_EXTENSION_CONSOLE ? ( - {reason === DISPLAY_REASON_EXTENSION_CONSOLE ? ( - <> -
This is the Extension Console.
-
PixieBrix mods cannot run on this page.
- - ) : ( - <> -
- This is a restricted browser page. -
-
PixieBrix cannot access this page.
- - )} +
This is the Extension Console.
+
PixieBrix mods cannot run on this page.
+
+ ) : ( + +
This is a restricted browser page.
+
PixieBrix cannot access this page.
); }; diff --git a/src/tinyPages/restrictedUrlPopup.html b/src/tinyPages/restrictedUrlPopup.html index 1514917301..4081f9d592 100644 --- a/src/tinyPages/restrictedUrlPopup.html +++ b/src/tinyPages/restrictedUrlPopup.html @@ -21,13 +21,10 @@ PixieBrix From ad5fe5c08b2613cc0c7c38828b2ae4f4b5ce83ea Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Mon, 22 Jan 2024 21:49:25 +0800 Subject: [PATCH 25/30] Add support for frames in `getConnectedTarget` --- src/bricks/transformers/brickFactory.ts | 10 ++-------- src/contrib/automationanywhere/aaFrameProtocol.ts | 4 +++- src/sidebar/connectedTarget.tsx | 15 +++++++++++---- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/bricks/transformers/brickFactory.ts b/src/bricks/transformers/brickFactory.ts index d45a21543d..cf9f2381f5 100644 --- a/src/bricks/transformers/brickFactory.ts +++ b/src/bricks/transformers/brickFactory.ts @@ -51,7 +51,6 @@ import { type UnknownObject } from "@/types/objectTypes"; import { isPipelineExpression } from "@/utils/expressionUtils"; import { isContentScript } from "webext-detect-page"; import { getConnectedTarget } from "@/sidebar/connectedTarget"; -import { getTopLevelFrame } from "webext-messenger"; import { uuidv4 } from "@/types/helpers"; import { isSpecificError } from "@/errors/errorHelpers"; import { HeadlessModeError } from "@/bricks/errors"; @@ -62,7 +61,6 @@ 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; @@ -344,12 +342,8 @@ class UserDefinedBrick extends BrickABC { // renderer. The caller can't run the whole brick in the contentScript because renderers can return React // Components which can't be serialized across messenger boundaries. - // This code can be run either in the sidebar or in a modal. - // The modal is always an iframe in the same tab, - // but the sidebar varies. This code handles the 3 cases. - const topLevelFrame = isBrowserSidebar() - ? await getConnectedTarget() - : await getTopLevelFrame(); + // Note: This code can be run either in the sidebar or in a modal + const topLevelFrame = await getConnectedTarget(); try { return await runHeadlessPipeline(topLevelFrame, { diff --git a/src/contrib/automationanywhere/aaFrameProtocol.ts b/src/contrib/automationanywhere/aaFrameProtocol.ts index 35b238e512..b39aa602a9 100644 --- a/src/contrib/automationanywhere/aaFrameProtocol.ts +++ b/src/contrib/automationanywhere/aaFrameProtocol.ts @@ -125,8 +125,10 @@ export async function initCopilotMessenger(): Promise { // necessary to pass to the Co-Pilot frame. }); - // Fetch the current data from the content script when the frame loads + // Note: This code can be run either in the sidebar or in a modal const frame = await getConnectedTarget(); + + // Fetch the current data from the content script when the frame loads const data = await getCopilotHostData(frame); console.debug("Setting initial Co-Pilot data", { location: window.location.href, diff --git a/src/sidebar/connectedTarget.tsx b/src/sidebar/connectedTarget.tsx index f4d2c5c189..e912e44dbd 100644 --- a/src/sidebar/connectedTarget.tsx +++ b/src/sidebar/connectedTarget.tsx @@ -16,7 +16,7 @@ */ import { isMV3 } from "@/mv3/api"; -import { expectContext } from "@/utils/expectContext"; +import { expectContext, isBrowserSidebar } from "@/utils/expectContext"; import { assertNotNullish } from "@/utils/nullishUtils"; import { once } from "lodash"; import { type TopLevelFrame, getTopLevelFrame } from "webext-messenger"; @@ -41,9 +41,16 @@ export const getConnectedTabId = once( isMV3() ? getConnectedTabIdMv3 : getConnectedTabIdMv2, ); -export const getConnectedTarget = isMV3() - ? (): TopLevelFrame => ({ tabId: getConnectedTabIdMv3(), frameId: 0 }) - : getTopLevelFrame; +/** + * @returns the Target for the top level frame for the current tab + * @context sidePanel, sidebar iframe, content script iframes + */ +// TODO: Drop support for "content script iframes" because it doesn't belong to `@/sidebar/connectedTarget` +// https://github.com/pixiebrix/pixiebrix-extension/pull/7354#discussion_r1461563961 +export const getConnectedTarget = + isMV3() && isBrowserSidebar() + ? (): TopLevelFrame => ({ tabId: getConnectedTabIdMv3(), frameId: 0 }) + : getTopLevelFrame; export async function getConnectedTargetUrl(): Promise { return getTabUrl(await getConnectedTarget()); From 832bdc381562fce160c5fd54f5bd076f0ac0cef5 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Tue, 23 Jan 2024 12:49:48 +0800 Subject: [PATCH 26/30] Extract ternary from SidebarBody --- src/sidebar/SidebarBody.tsx | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/sidebar/SidebarBody.tsx b/src/sidebar/SidebarBody.tsx index 9dfbc30c5d..04af909c06 100644 --- a/src/sidebar/SidebarBody.tsx +++ b/src/sidebar/SidebarBody.tsx @@ -23,24 +23,25 @@ import RestrictedUrlPopupApp from "@/tinyPages/RestrictedUrlPopupApp"; import useConnectedTargetUrl from "./hooks/useConnectedTargetUrl"; import { getReasonByUrl as getRestrictedReasonByUrl } from "@/tinyPages/restrictedUrlPopupUtils"; +const SidebarReady: React.FC<{ url: string }> = ({ url }) => { + const restricted = getRestrictedReasonByUrl(url); + + return restricted ? ( + + ) : ( + + ); +}; + // Include MemoryRouter because some of our authentication-gate hooks use useLocation. However, there's currently no // navigation in the SidebarApp function SidebarBody() { const url = useConnectedTargetUrl(); - const restricted = getRestrictedReasonByUrl(url); return ( <>
-
- {url ? ( - restricted ? ( - - ) : ( - - ) - ) : null} -
+
{url && }
); } From 01c88eff09fe817f86ae38c699b4386760d62ebe Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Tue, 23 Jan 2024 13:41:07 +0800 Subject: [PATCH 27/30] s/sidePanelClosureSignal/sidePanelOnClose/ --- src/bricks/transformers/ephemeralForm/formTransformer.ts | 5 ++--- .../transformers/temporaryInfo/DisplayTemporaryInfo.ts | 7 ++++--- src/contentScript/sidebarActivation.ts | 6 ++---- src/contentScript/sidebarController.tsx | 7 ++++++- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/bricks/transformers/ephemeralForm/formTransformer.ts b/src/bricks/transformers/ephemeralForm/formTransformer.ts index fbc57d594a..d09a9546e3 100644 --- a/src/bricks/transformers/ephemeralForm/formTransformer.ts +++ b/src/bricks/transformers/ephemeralForm/formTransformer.ts @@ -28,14 +28,13 @@ import { showSidebar, hideSidebarForm, showSidebarForm, - sidePanelClosureSignal, + sidePanelOnClose, } from "@/contentScript/sidebarController"; import { showModal } from "@/bricks/transformers/ephemeralForm/modalUtils"; 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 { onAbort } from "abort-utils"; // 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 @@ -168,7 +167,7 @@ export class FormTransformer extends TransformerABC { }); // Two-way binding between sidebar and form. Listen for the user (or an action) closing the sidebar - onAbort(sidePanelClosureSignal(), controller); + sidePanelOnClose(controller.abort.bind(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 ea32cf5214..7ce646e5a9 100644 --- a/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts +++ b/src/bricks/transformers/temporaryInfo/DisplayTemporaryInfo.ts @@ -24,7 +24,7 @@ import { import { expectContext } from "@/utils/expectContext"; import { showSidebar, - sidePanelClosureSignal, + sidePanelOnClose, hideTemporarySidebarPanel, showTemporarySidebarPanel, updateTemporarySidebarPanel, @@ -56,7 +56,6 @@ 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 { onAbort } from "abort-utils"; // Match naming of the sidebar panel extension point triggers export type RefreshTrigger = "manual" | "statechange"; @@ -190,7 +189,9 @@ export async function displayTemporaryInfo({ }, }); - onAbort(sidePanelClosureSignal(), controller); + // Abort on sidebar close + sidePanelOnClose(controller.abort.bind(controller)); + controller.signal.addEventListener("abort", () => { void hideTemporarySidebarPanel(nonce); void stopWaitingForTemporaryPanels([nonce]); diff --git a/src/contentScript/sidebarActivation.ts b/src/contentScript/sidebarActivation.ts index 770293f2eb..6c5dcdeb11 100644 --- a/src/contentScript/sidebarActivation.ts +++ b/src/contentScript/sidebarActivation.ts @@ -19,7 +19,7 @@ import { type RegistryId } from "@/types/registryTypes"; import { isRegistryId } from "@/types/helpers"; import { showSidebar, - sidePanelClosureSignal, + sidePanelOnClose, hideModActivationInSidebar, showModActivationInSidebar, } from "@/contentScript/sidebarController"; @@ -61,9 +61,7 @@ async function showSidebarActivationForMods( heading: "Activating", }); - sidePanelClosureSignal().addEventListener("abort", () => { - void hideModActivationInSidebar(); - }); + sidePanelOnClose(hideModActivationInSidebar); } function getNextUrlFromActivateUrl(activateUrl: string): string | null { diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index 24002f1119..1c118252bb 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -467,7 +467,7 @@ export function getReservedPanelEntries(): { }; } -export function sidePanelClosureSignal(): AbortSignal { +export function sidePanelOnCloseSignal(): AbortSignal { const controller = new AbortController(); expectContext("contentScript"); if (isMV3()) { @@ -496,3 +496,8 @@ export function sidePanelClosureSignal(): AbortSignal { return controller.signal; } + +export function sidePanelOnClose(callback: () => void): void { + const signal = sidePanelOnCloseSignal(); + signal.addEventListener("abort", callback, { once: true }); +} From b36d35d50cbb4bfe1c7cf9c1c25816d3f6ba5f50 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Tue, 23 Jan 2024 21:25:09 +0800 Subject: [PATCH 28/30] Lint --- 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 1c118252bb..fb773bad10 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -467,7 +467,7 @@ export function getReservedPanelEntries(): { }; } -export function sidePanelOnCloseSignal(): AbortSignal { +function sidePanelOnCloseSignal(): AbortSignal { const controller = new AbortController(); expectContext("contentScript"); if (isMV3()) { From 4a5f655eee93aee75bd48f45c053bd55512b0d8a Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Wed, 24 Jan 2024 15:12:08 +0800 Subject: [PATCH 29/30] Restore "Looking for the Extension Console?" --- src/tinyPages/RestrictedUrlPopupApp.tsx | 43 ++++++++++++++++--------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/tinyPages/RestrictedUrlPopupApp.tsx b/src/tinyPages/RestrictedUrlPopupApp.tsx index 545826fc5f..691a0804a9 100644 --- a/src/tinyPages/RestrictedUrlPopupApp.tsx +++ b/src/tinyPages/RestrictedUrlPopupApp.tsx @@ -23,6 +23,24 @@ import { DISPLAY_REASON_UNKNOWN, } from "@/tinyPages/restrictedUrlPopupConstants"; import { isBrowserSidebar } from "@/utils/expectContext"; +import { getExtensionConsoleUrl } from "@/utils/extensionUtils"; + +// TODO: Move to utils folder after the isBrowserSidebar condition is dropped +async function openInActiveTab(event: React.MouseEvent) { + if (event.shiftKey || event.ctrlKey || event.metaKey) { + return; + } + + event.preventDefault(); + await browser.tabs.update({ + url: event.currentTarget.href, + }); + + // TODO: Drop after restrictedUrlPopup.html is removed + if (!isBrowserSidebar()) { + window.close(); + } +} const RestrictedUrlContent: React.FC = ({ children }) => (
@@ -33,25 +51,20 @@ const RestrictedUrlContent: React.FC = ({ children }) => (

+
+ Looking for the Extension Console?{" "} + + Open the Extension Console + +
+
Looking for the Page Editor?{" "} { - if (event.shiftKey || event.ctrlKey || event.metaKey) { - return; - } - - event.preventDefault(); - await browser.tabs.update({ - url: event.currentTarget.href, - }); + href="https://www.pixi - // TODO: Drop after restrictedUrlPopup.html is removed - if (!isBrowserSidebar()) { - window.close(); - } - }} + // TODO: Move to utils folder after ebrix.com/developers-welcome" + onClick={openInActiveTab} > View the Developer Welcome Page From d24f7786aa7b8c70bec177f86eb4a079e517e320 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Wed, 24 Jan 2024 18:27:53 +0800 Subject: [PATCH 30/30] Snapshot update --- src/sidebar/__snapshots__/SidebarBody.test.tsx.snap | 10 ++++++++++ src/tinyPages/RestrictedUrlPopupApp.tsx | 4 +--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/sidebar/__snapshots__/SidebarBody.test.tsx.snap b/src/sidebar/__snapshots__/SidebarBody.test.tsx.snap index 66334f0a46..95396be803 100644 --- a/src/sidebar/__snapshots__/SidebarBody.test.tsx.snap +++ b/src/sidebar/__snapshots__/SidebarBody.test.tsx.snap @@ -198,6 +198,16 @@ exports[`SidebarBody it renders error when URL is restricted 1`] = ` To open the PixieBrix Sidebar, navigate to a website and then click the PixieBrix toolbar icon again.

+
+ Looking for the Extension Console? + + Open the Extension Console + +
diff --git a/src/tinyPages/RestrictedUrlPopupApp.tsx b/src/tinyPages/RestrictedUrlPopupApp.tsx index 691a0804a9..93f678e362 100644 --- a/src/tinyPages/RestrictedUrlPopupApp.tsx +++ b/src/tinyPages/RestrictedUrlPopupApp.tsx @@ -61,9 +61,7 @@ const RestrictedUrlContent: React.FC = ({ children }) => (
Looking for the Page Editor?{" "} View the Developer Welcome Page