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 2 #7266

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5cdc92a
sidePanel wip
fregante Dec 29, 2023
00422f3
Use sidePanel.open(tabId)
fregante Dec 29, 2023
d4247a2
Hide "close" button
fregante Dec 29, 2023
ecc9884
Merge with main
twschiller Jan 3, 2024
278d7e8
Selected changes by Todd
fregante Jan 4, 2024
05925d9
Enable per-tab sidebar
fregante Jan 4, 2024
3c41725
Fix bad merge
fregante Jan 4, 2024
92edfb6
Last mixed implementation
fregante Jan 4, 2024
a3c4ab6
savegame
fregante Jan 4, 2024
dc55a3f
Work so far; untested
fregante Jan 4, 2024
2cdd398
Merge remote-tracking branch 'origin/main' into F/mv3/sidePanel-2
fregante Jan 4, 2024
1252e5a
Clean last isSidebarFrameVisible issues
fregante Jan 4, 2024
5129b0b
First successful run
fregante Jan 4, 2024
d1527c1
Fix lockfile updates
fregante Jan 4, 2024
767b728
Fix restrictedUrlPopup sizing
fregante Jan 4, 2024
9c191ff
Revert default path change
fregante Jan 4, 2024
7257548
Fix strictNullChecks issue
fregante Jan 5, 2024
1f335ab
HIDE_SIDEBAR_EVENT_NAME replacement
fregante Jan 7, 2024
46ea078
Fix tests
fregante Jan 7, 2024
484b613
Fix CI/snapshots/tests setup
fregante Jan 7, 2024
44bca81
Drop redundant ensureSidebar
fregante Jan 7, 2024
286f639
Drop PANEL_FRAME_ID and fix most tests
fregante Jan 7, 2024
e2c040e
Drop test expectation
fregante Jan 7, 2024
a78860d
Merge remote-tracking branch 'origin/main' into F/mv3/sidePanel-2
fregante Jan 7, 2024
2ccc8d0
isSidePanelOpen optimization
fregante Jan 7, 2024
7c321b8
Fix `SidebarErrorBoundary` reload logic
fregante Jan 7, 2024
f1dde6e
sidePanel init cleanup
fregante Jan 8, 2024
c61abc1
pingSidebar improvements
fregante Jan 8, 2024
0210732
Fix lodash.throttle bad types
fregante Jan 8, 2024
e0a7a47
s/getTopLevelFrame/getAssociatedTarget/
fregante Jan 8, 2024
15dd16f
Follow review
fregante Jan 9, 2024
358bfc6
Merge remote-tracking branch 'origin/main' into F/mv3/sidePanel-2
fregante Jan 11, 2024
48d6d6e
Lint
fregante Jan 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
6 changes: 5 additions & 1 deletion scripts/__snapshots__/manifest.test.js.snap

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

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 = {
default_path: "sidebar.html",
};

// Update format
manifest.web_accessible_resources = [
Expand Down
2 changes: 1 addition & 1 deletion src/background/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ void initMessengerLogging();
void initRuntimeLogging();
registerMessenger();
registerExternalMessenger();
initBrowserAction();
void initBrowserAction();
initInstaller();
void initNavigation();
initExecutor();
Expand Down
108 changes: 19 additions & 89 deletions src/background/browserAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,99 +15,29 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

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 { browserAction } from "@/mv3/api";
import {
DISPLAY_REASON_EXTENSION_CONSOLE,
DISPLAY_REASON_RESTRICTED_URL,
} from "@/tinyPages/restrictedUrlPopupConstants";
import { setActionPopup } from "webext-tools";
getSidebarPath,
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> {
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<void> {
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
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, tab.url);
});

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,
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.url) {
// TODO: Drop this once the popover URL behavior is merged into sidebar.html
void chrome.sidePanel.setOptions({
tabId,
path: getSidebarPath(tabId, changeInfo.url),
});
}
});
}

async function handleBrowserAction(tab: Tab): Promise<void> {
// 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);
}
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 showMySidePanel = getMethod("SHOW_MY_SIDE_PANEL", bg);
export const hideMySidePanel = getMethod("HIDE_MY_SIDE_PANEL", bg);

export const closeTab = getMethod("CLOSE_TAB", bg);
export const deleteCachedAuthData = getMethod("DELETE_CACHED_AUTH", bg);
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 { hideMySidePanel, showMySidePanel } from "@/background/sidePanel";

expectContext("background");

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

SHOW_MY_SIDE_PANEL: typeof showMySidePanel;
HIDE_MY_SIDE_PANEL: typeof hideMySidePanel;

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_MY_SIDE_PANEL: showMySidePanel,
HIDE_MY_SIDE_PANEL: hideMySidePanel,

ACTIVATE_TAB: activateTab,
REACTIVATE_EVERY_TAB: reactivateEveryTab,
REMOVE_EXTENSION_EVERY_TAB: removeExtensionForEveryTab,
Expand Down
30 changes: 30 additions & 0 deletions src/background/sidePanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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 {
openSidePanel,
hideSidePanel,
} from "@/sidebar/sidePanel/messenger/api";
import type { MessengerMeta } from "webext-messenger";

export async function showMySidePanel(this: MessengerMeta): Promise<void> {
await openSidePanel(this.trace[0].tab.id, this.trace[0].url);
}

export async function hideMySidePanel(this: MessengerMeta): Promise<void> {
await hideSidePanel(this.trace[0].tab.id);
}
26 changes: 0 additions & 26 deletions src/background/webextAlert.ts

This file was deleted.

9 changes: 5 additions & 4 deletions src/bricks/effects/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
import { EffectABC } from "@/types/bricks/effectTypes";
import { type BrickArgs, type BrickOptions } from "@/types/runtimeTypes";
import { type Schema, SCHEMA_EMPTY_OBJECT } from "@/types/schemaTypes";
import { hideSidebar, showSidebar } from "@/contentScript/sidebarController";
import { rehydrateSidebar } from "@/contentScript/sidebarController";
import { hideMySidePanel, showMySidePanel } from "@/background/messenger/api";
import { propertiesToSchema } from "@/validators/generic";

import { logPromiseDuration } from "@/utils/promiseUtils";

export class ShowSidebar extends EffectABC {
Expand Down Expand Up @@ -64,9 +64,10 @@ export class ShowSidebar extends EffectABC {
): Promise<void> {
// 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",
fregante marked this conversation as resolved.
Show resolved Hide resolved
showSidebar({
rehydrateSidebar({
force: forcePanel,
panelHeading,
blueprintId: logger.context.blueprintId,
Expand All @@ -87,6 +88,6 @@ export class HideSidebar extends EffectABC {
inputSchema: Schema = SCHEMA_EMPTY_OBJECT;

async effect(): Promise<void> {
hideSidebar();
await hideMySidePanel();
}
}
22 changes: 6 additions & 16 deletions src/bricks/transformers/ephemeralForm/formTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,16 @@ import {
} from "@/contentScript/ephemeralFormProtocol";
import { expectContext } from "@/utils/expectContext";
import {
ensureSidebar,
showSidebar,
hideSidebarForm,
HIDE_SIDEBAR_EVENT_NAME,
showSidebarForm,
} 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 { onSidePanelClosure } from "@/sidebar/sidePanel/messenger/api";

// The modes for createFrameSrc are different than the location argument for FormTransformer. The mode for the frame
// just determines the layout container of the form
Expand Down Expand Up @@ -157,33 +157,23 @@ 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,
form: formDefinition,
});

// 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ jest.mock("@/bricks/transformers/ephemeralForm/modalUtils", () => ({
const showModalMock = jest.mocked(showModal);

jest.mock("@/contentScript/sidebarController", () => ({
ensureSidebar: jest.fn(),
showSidebar: jest.fn(),
showTemporarySidebarPanel: jest.fn(),
updateTemporarySidebarPanel: jest.fn(),
}));
Expand Down Expand Up @@ -177,7 +177,7 @@ describe("DisplayTemporaryInfo", () => {

let payload: PanelPayload;
showTemporarySidebarPanelMock.mockImplementation(
(entry: TemporaryPanelEntry) => {
async (entry: TemporaryPanelEntry) => {
payload = entry.payload;
},
);
Expand Down
Loading
Loading