Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#7228: chrome.sidePanel POC #7232

Closed
wants to merge 14 commits into from
7 changes: 5 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@atlaskit/tree": "^8.8.7",
"@cfworker/json-schema": "^1.12.7",
"@datadog/browser-rum": "^5.6.0",
"@emotion/react": "^11.11.3",
fregante marked this conversation as resolved.
Show resolved Hide resolved
"@floating-ui/dom": "^1.5.3",
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
Expand Down Expand Up @@ -167,7 +168,7 @@
"webext-base-css": "^1.4.4",
"webext-content-scripts": "^2.6.0",
"webext-detect-page": "^4.2.1",
"webext-inject-on-install": "^2.0.0",
"webext-inject-on-install": "^2.0.0-2",
"webext-messenger": "^0.25.0-0",
"webext-patterns": "^1.3.0",
"webext-permissions": "^3.1.2",
Expand Down Expand Up @@ -199,6 +200,7 @@
"@testing-library/user-event": "^14.5.2",
"@total-typescript/ts-reset": "^0.5.1",
"@types/chrome": "^0.0.254",
"@types/dom-navigation": "^1.0.3",
"@types/dompurify": "^3.0.5",
"@types/downloadjs": "^1.4.6",
"@types/holderjs": "^2.9.4",
Expand Down
6 changes: 6 additions & 0 deletions scripts/manifest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ function updateManifestToV3(manifestV2) {
manifest.permissions = [...permissions, "scripting"];
manifest.host_permissions = origins;

// Add sidePanel
manifest.permissions.push("sidePanel");
manifest.side_panel = {
Copy link
Contributor

@twschiller twschiller Jan 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: need a "generic" sidebar that indicates it can only be used in the context of a web page tab?

default_path: "sidebar.html",
};

// Update format
manifest.web_accessible_resources = [
{
Expand Down
24 changes: 22 additions & 2 deletions src/background/browserAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import { ensureContentScript } from "@/background/contentScript";
import { rehydrateSidebar } from "@/contentScript/messenger/api";
import webextAlert from "./webextAlert";
import { browserAction, type Tab } from "@/mv3/api";
import { browserAction, isMV3, type Tab } from "@/mv3/api";
import { executeScript, isScriptableUrl } from "webext-content-scripts";
import { memoizeUntilSettled } from "@/utils/promiseUtils";
import { getExtensionConsoleUrl } from "@/utils/extensionUtils";
Expand Down Expand Up @@ -105,7 +105,27 @@ function getPopoverUrl(tabUrl: string | null): string | null {
}

export default function initBrowserAction(): void {
browserAction.onClicked.addListener(handleBrowserAction);
if (isMV3()) {
void chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false });

browserAction.onClicked.addListener(async (tab) => {
const tabId = tab.id;

// Call open then setOptions
// https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/functional-samples/cookbook.sidepanel-open/script.js#L9
await chrome.sidePanel.open({
tabId,
});

await chrome.sidePanel.setOptions({
tabId,
path: `sidebar.html?tabId=${tabId}`,
enabled: true,
});
});
} else {
browserAction.onClicked.addListener(handleBrowserAction);
}

// Track the active tab URL. We need to update the popover every time status the active tab/active URL changes.
// https://github.com/facebook/react/blob/bbb9cb116dbf7b6247721aa0c4bcb6ec249aa8af/packages/react-devtools-extensions/src/background/tabsManager.js#L29
Expand Down
2 changes: 2 additions & 0 deletions src/background/messenger/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export const removeExtensionForEveryTab = getNotifier(
bg,
);

export const showSidebarPanel = getMethod("SHOW_SIDEBAR_PANEL", bg);

export const closeTab = getMethod("CLOSE_TAB", bg);
export const deleteCachedAuthData = getMethod("DELETE_CACHED_AUTH", bg);
export const getCachedAuthData = getMethod("GET_CACHED_AUTH", bg);
Expand Down
5 changes: 5 additions & 0 deletions src/background/messenger/registration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import {
getCachedAuthData,
} from "@/background/auth/authStorage";
import { setCopilotProcessData } from "@/background/partnerHandlers";
import { showSidebarPanel } from "@/background/sidePanel";

expectContext("background");

Expand Down Expand Up @@ -114,6 +115,8 @@ declare global {
PING: typeof pong;
COLLECT_PERFORMANCE_DIAGNOSTICS: typeof collectPerformanceDiagnostics;

SHOW_SIDEBAR_PANEL: typeof showSidebarPanel;

ACTIVATE_TAB: typeof activateTab;
REACTIVATE_EVERY_TAB: typeof reactivateEveryTab;
REMOVE_EXTENSION_EVERY_TAB: typeof removeExtensionForEveryTab;
Expand Down Expand Up @@ -195,6 +198,8 @@ export default function registerMessenger(): void {
PING: pong,
COLLECT_PERFORMANCE_DIAGNOSTICS: collectPerformanceDiagnostics,

SHOW_SIDEBAR_PANEL: showSidebarPanel,

ACTIVATE_TAB: activateTab,
REACTIVATE_EVERY_TAB: reactivateEveryTab,
REMOVE_EXTENSION_EVERY_TAB: removeExtensionForEveryTab,
Expand Down
34 changes: 34 additions & 0 deletions src/background/sidePanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/

import type { MessengerMeta } from "webext-messenger";

export async function showSidebarPanel(this: MessengerMeta): Promise<void> {
const tabId = this.trace[0].tab.id;

// Call open then setOptions
// https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/functional-samples/cookbook.sidepanel-open/script.js#L9
await chrome.sidePanel.open({
tabId,
});

await chrome.sidePanel.setOptions({
tabId,
path: `sidebar.html?tabId=${tabId}`,
enabled: true,
});
}
8 changes: 8 additions & 0 deletions src/bricks/effects/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { hideSidebar, showSidebar } from "@/contentScript/sidebarController";
import { propertiesToSchema } from "@/validators/generic";

import { logPromiseDuration } from "@/utils/promiseUtils";
import { isMV3 } from "@/mv3/api";
import { BusinessError } from "@/errors/businessErrors";

export class ShowSidebar extends EffectABC {
constructor() {
Expand Down Expand Up @@ -87,6 +89,12 @@ export class HideSidebar extends EffectABC {
inputSchema: Schema = SCHEMA_EMPTY_OBJECT;

async effect(): Promise<void> {
if (isMV3()) {
// No way to programmatically hide yet, even from user gesture
// https://developer.chrome.com/docs/extensions/reference/api/sidePanel
throw new BusinessError("Hide Sidebar is not supported in Chromium MV3");
}

hideSidebar();
}
}
2 changes: 1 addition & 1 deletion src/contentScript/sidebarController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
insertSidebarFrame,
isSidebarFrameVisible,
removeSidebarFrame,
} from "./sidebarDomControllerLite";
} from "@/contentScript/sidebarDomControllerLite";
import { type Except } from "type-fest";
import { type RunArgs, RunReason } from "@/types/runtimeTypes";
import { type UUID } from "@/types/stringTypes";
Expand Down
5 changes: 5 additions & 0 deletions src/contentScript/sidebarDomControllerLite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ 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 { isMV3 } from "@/mv3/api";

if (isMV3()) {
throw new Error("sidebarDomControllerLite.ts should not be imported in MV3");
}

export const SIDEBAR_WIDTH_CSS_PROPERTY = "--pb-sidebar-width";
const ORIGINAL_MARGIN_CSS_PROPERTY = "--pb-original-margin-right";
Expand Down
82 changes: 82 additions & 0 deletions src/contentScript/sidebarDomControllerLiteMv3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/

/**
* @file This file MUST not have dependencies as it's meant to be tiny
* and imported by browserActionInstantHandler.ts
*/

import { isMV3 } from "@/mv3/api";
import { showSidebarPanel } from "@/background/messenger/api";

if (!isMV3()) {
throw new Error(
"sidebarDomControllerLiteMv3.ts should not be imported in MV2",
);
}

// TODO: drop constant. Is referenced by notify.tsx to calculate offset
export const SIDEBAR_WIDTH_CSS_PROPERTY = "--pb-sidebar-width";

/**
* Return true if side panel is open. The PixieBrix sidebar might not be initialized yet.
*/
export function isSidebarFrameVisible(): boolean {
// TODO: implement this. Used as performance optimization for determining whether or not to run sidebar mods.

// This method will have to become async unless we listen and set a synchronous module variable.
// Ideally it would stay synchronous so we don't have to refactor all the code that calls it.

// Potential options:
// - Can we use chrome.sidePanel.getOptions() to signal if it's open?
// - Might be able to subscribe via https://stackoverflow.com/a/77106777/402560. But may need extra machinery
// to handle background worker restarts.

// For now, mark as true so mods run
return true;
}

/** Removes the element; Returns false if no element was found */
export function removeSidebarFrame(): boolean {
throw new Error("Hiding sidebar is not supported in MV3");
}

/** Inserts the element; Returns false if it already existed */
export function insertSidebarFrame(): boolean {
// FIXME: this loses the user gesture when called from DevTools (e.g., RenderPanel)
// That could be this timing bug:
// - https://stackoverflow.com/a/77213912
// - https://bugs.chromium.org/p/chromium/issues/detail?id=1478648
// Or, it could be that user gestures in DevTools don't count

void showSidebarPanel();

return true;
}

/**
* Toggle the sidebar frame. Returns true if the sidebar is now visible, false otherwise.
*/
export function toggleSidebarFrame(): boolean {
if (isSidebarFrameVisible()) {
removeSidebarFrame();
return false;
}

insertSidebarFrame();
return true;
}
2 changes: 1 addition & 1 deletion src/contrib/automationanywhere/aaFrameProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import { type UnknownObject } from "@/types/objectTypes";
import { expectContext } from "@/utils/expectContext";
import { getTopLevelFrame } from "webext-messenger";
import { getTopLevelFrame } from "@/sidebar/sidePanel";
import { getCopilotHostData } from "@/contentScript/messenger/api";

/**
Expand Down
2 changes: 1 addition & 1 deletion src/sidebar/ConnectedSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import {
ensureExtensionPointsInstalled,
getReservedSidebarEntries,
} from "@/contentScript/messenger/api";
import { getTopLevelFrame } from "webext-messenger";
import { getTopLevelFrame } from "./sidePanel";
import useAsyncEffect from "use-async-effect";
import activateLinkClickHandler from "@/activation/activateLinkClickHandler";

Expand Down
36 changes: 20 additions & 16 deletions src/sidebar/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import useTheme, { useGetTheme } from "@/hooks/useTheme";
import cx from "classnames";
import useContextInvalidated from "@/hooks/useContextInvalidated";
import { getTopLevelFrame } from "webext-messenger";
import { isMV3 } from "@/mv3/api";

const Header: React.FunctionComponent = () => {
const { logo, showSidebarLogo, customSidebarLogo } = useTheme();
Expand All @@ -33,22 +34,25 @@ const Header: React.FunctionComponent = () => {

return (
<div className="d-flex py-2 pl-2 pr-0 justify-content-between align-content-center">
{wasContextInvalidated || ( // /* The button doesn't work after invalidation #2359 */
<Button
className={cx(
styles.button,
theme === "default" ? styles.themeColorOverride : styles.themeColor,
)}
onClick={async () => {
const topLevelFrame = await getTopLevelFrame();
await hideSidebar(topLevelFrame);
}}
size="sm"
variant="link"
>
<FontAwesomeIcon icon={faAngleDoubleRight} className="fa-lg" />
</Button>
)}
{wasContextInvalidated ||
isMV3() || ( // /* The button doesn't work after invalidation #2359 nor in sidePanel */
<Button
className={cx(
styles.button,
theme === "default"
? styles.themeColorOverride
: styles.themeColor,
)}
onClick={async () => {
const topLevelFrame = await getTopLevelFrame();
await hideSidebar(topLevelFrame);
}}
size="sm"
variant="link"
>
<FontAwesomeIcon icon={faAngleDoubleRight} className="fa-lg" />
</Button>
)}
{showSidebarLogo && (
<div className="align-self-center">
<img
Expand Down
Loading
Loading