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
10 changes: 10 additions & 0 deletions scripts/manifest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
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
100 changes: 98 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 All @@ -34,6 +34,9 @@ const ERR_UNABLE_TO_OPEN =
// The sidebar is always injected to into the top level frame
const TOP_LEVEL_FRAME_ID = 0;

// `true` if the sidepanel is open, false if closed
let sidePanelOpen = false;

const toggleSidebar = memoizeUntilSettled(_toggleSidebar);

// Don't accept objects here as they're not easily memoizable
Expand Down Expand Up @@ -105,7 +108,100 @@ function getPopoverUrl(tabUrl: string | null): string | null {
}

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

chrome.runtime.onConnect.addListener((port) => {
if (port.name === "sidepanel") {
sidePanelOpen = true;

port.onDisconnect.addListener(async () => {
sidePanelOpen = false;
});

port.onMessage.addListener(async (message) => {
// FIXME: keep track based on tab
if (message.type === "keepalive") {
console.debug("browserAction:keepalive", message);
sidePanelOpen = !message.hidden;
}
});
}
});

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

// The Chrome example calls open first:
// https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/functional-samples/cookbook.sidepanel-open/script.js#L9

// Needs be called first so it's from the user gesture. Could be this timing bug:
// - https://stackoverflow.com/a/77213912
// - https://bugs.chromium.org/p/chromium/issues/detail?id=1478648

// FIXME: this works, but then errors out if the sidebar has been hidden via
// await chrome.sidePanel.setOptions({
// tabId,
// enabled: false,
// });

// await chrome.sidePanel.open({
// tabId,
// });

// await chrome.sidePanel.setOptions({
// tabId,
// path: `sidebar.html?tabId=${tabId}`,
// enabled: true,
// });

// TODO: figure out how to toggle based on the current state. I tried wrapping in a chrome.sidePanel.getOptions
// but to check if it's enabled for the tab, but that causes the user gesture to be lost during the check.
// We'll likely need to keep track of current state in a module variable. See comments in
// sidebarDomControllerLiteMv3.ts:isSidebarFrameVisible

if (sidePanelOpen) {
// Force switch over to PixieBrix panel so disabling it closes the whole sidebar
await chrome.sidePanel.open({
tabId,
});

void chrome.sidePanel.setOptions({
tabId,
enabled: false,
});

sidePanelOpen = false;
} else {
// Call setOptions first to handle case where the sidebar has been disabled on the page to hide the sidebar
// Use callback to keep the user gesture context. See bug comment above.
chrome.sidePanel.setOptions(
{
tabId,
path: `sidebar.html?tabId=${tabId}`,
enabled: true,
},
() => {
chrome.sidePanel.open(
{
tabId,
},
() => {
sidePanelOpen = true;

// NOTE: at this point, the sidebar should already be visible on the page, even if not ready.
void rehydrateSidebar({
tabId,
});
},
);
},
);
}
});
} 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
3 changes: 3 additions & 0 deletions src/background/messenger/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export const removeExtensionForEveryTab = getNotifier(
bg,
);

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

export const closeTab = getMethod("CLOSE_TAB", bg);
export const deleteCachedAuthData = getMethod("DELETE_CACHED_AUTH", bg);
export const getCachedAuthData = getMethod("GET_CACHED_AUTH", bg);
Expand Down
7 changes: 7 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 { hideSidebarPanel, showSidebarPanel } from "@/background/sidePanel";

expectContext("background");

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

SHOW_SIDEBAR_PANEL: typeof showSidebarPanel;
HIDE_SIDEBAR_PANEL: typeof hideSidebarPanel;

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

SHOW_SIDEBAR_PANEL: showSidebarPanel,
HIDE_SIDEBAR_PANEL: hideSidebarPanel,

ACTIVATE_TAB: activateTab,
REACTIVATE_EVERY_TAB: reactivateEveryTab,
REMOVE_EXTENSION_EVERY_TAB: removeExtensionForEveryTab,
Expand Down
63 changes: 63 additions & 0 deletions src/background/sidePanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright (C) 2023 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <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;

return new Promise<void>((resolve, reject) => {
// Unlike the Chrome example, call setOptions first to handle the case where the sidebar was closed on the tab
// https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/functional-samples/cookbook.sidepanel-open/script.js#L9

// Use callback form to help prevent the user gesture from getting lost
chrome.sidePanel.setOptions(
{
tabId,
path: `sidebar.html?tabId=${tabId}`,
enabled: true,
},
() => {
chrome.sidePanel.open(
{
tabId,
},
() => {
resolve();
},
);

if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
}
},
);

if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
}
});
}

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

await chrome.sidePanel.setOptions({
tabId,
enabled: false,
});
}
2 changes: 2 additions & 0 deletions src/bricks/effects/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ export class HideSidebar extends EffectABC {
inputSchema: Schema = SCHEMA_EMPTY_OBJECT;

async effect(): Promise<void> {
// XXX: for MV3, do we need to catch a potential user gesture error and rethrow as business error? Would required
// making hideSidebar async and the hide a method instead of notifier
hideSidebar();
}
}
2 changes: 2 additions & 0 deletions src/contentScript/contentScriptCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
notifyContextInvalidated,
} from "@/errors/contextInvalidated";
import { onUncaughtError } from "@/errors/errorHelpers";
import { init as initSidebarController } from "@/contentScript/sidebarDomControllerLite";
import initFloatingActions from "@/components/floatingActions/initFloatingActions";
import { initSidebarActivation } from "@/contentScript/sidebarActivation";
import { initPerformanceMonitoring } from "@/contentScript/performanceMonitoring";
Expand Down Expand Up @@ -68,6 +69,7 @@ export async function init(): Promise<void> {
void initNavigation();

void initSidebarActivation();
initSidebarController();

// Inform `ensureContentScript`
void browser.runtime.sendMessage({ type: ENSURE_CONTENT_SCRIPT_READY });
Expand Down
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
9 changes: 9 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 Expand Up @@ -158,3 +163,7 @@ export function toggleSidebarFrame(): boolean {
insertSidebarFrame();
return true;
}

export function init(): void {
// NOP for MV2
}
Loading
Loading