From b1099439b4e42697f11bb35d7f92f5c70f22e661 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Thu, 25 Apr 2024 20:45:31 -0400 Subject: [PATCH] Unavailable overlay for sidebar forms and temp panels on nav --- src/sidebar/ConnectedSidebar.tsx | 29 +++++++++- src/sidebar/Tabs.module.scss | 1 + src/sidebar/Tabs.tsx | 2 + src/sidebar/UnavailableOverlay.tsx | 48 ++++++++++++++++ src/sidebar/connectedTarget.tsx | 2 +- src/sidebar/unavailableOverlay.module.scss | 57 +++++++++++++++++++ src/store/sidebar/sidebarSlice.ts | 9 +++ .../factories/sidebarEntryFactories.ts | 7 ++- src/types/sidebarTypes.ts | 6 ++ webpack.config.mjs | 2 +- 10 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 src/sidebar/UnavailableOverlay.tsx create mode 100644 src/sidebar/unavailableOverlay.module.scss diff --git a/src/sidebar/ConnectedSidebar.tsx b/src/sidebar/ConnectedSidebar.tsx index f46dc02a41..6942dd78c0 100644 --- a/src/sidebar/ConnectedSidebar.tsx +++ b/src/sidebar/ConnectedSidebar.tsx @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -import React, { useMemo } from "react"; +import React, { useCallback, useMemo } from "react"; import { addListener, removeListener, @@ -40,7 +40,10 @@ import DefaultPanel from "@/sidebar/DefaultPanel"; import { MOD_LAUNCHER } from "@/store/sidebar/constants"; import { ensureExtensionPointsInstalled } from "@/contentScript/messenger/api"; import { getReservedSidebarEntries } from "@/contentScript/messenger/strict/api"; -import { getConnectedTarget } from "@/sidebar/connectedTarget"; +import { + getConnectedTabIdMv3, + getConnectedTarget, +} from "@/sidebar/connectedTarget"; import useAsyncEffect from "use-async-effect"; import activateLinkClickHandler from "@/activation/activateLinkClickHandler"; import addFormPanel from "@/store/sidebar/thunks/addFormPanel"; @@ -48,6 +51,9 @@ import addTemporaryPanel from "@/store/sidebar/thunks/addTemporaryPanel"; import removeTemporaryPanel from "@/store/sidebar/thunks/removeTemporaryPanel"; import { type AsyncDispatch } from "@/sidebar/store"; import useEventListener from "@/hooks/useEventListener"; +import { WebNavigation } from "webextension-polyfill"; +import OnBeforeNavigateDetailsType = WebNavigation.OnBeforeNavigateDetailsType; +import { isMV3 } from "@/mv3/api"; /** * Listeners to update the Sidebar's Redux state upon receiving messages from the contentScript. @@ -98,7 +104,21 @@ const ConnectedSidebar: React.VFC = () => { const listener = useConnectedListener(); const sidebarIsEmpty = useSelector(selectIsSidebarEmpty); - // `useAsyncEffect` will run once on component mount since listener and formsRef don't change on renders. + const navigationListener = useCallback( + (details: OnBeforeNavigateDetailsType) => { + const { frameId, tabId } = details; + if (isMV3()) { + const connectedTabId = getConnectedTabIdMv3(); + if (tabId === connectedTabId && frameId === 0) { + console.log("navigationListener:connectedTabId", connectedTabId); + dispatch(sidebarSlice.actions.markTemporaryPanelsAsUnavailable()); + } + } + }, + [dispatch], + ); + + // `useAsyncEffect` will run once on component mount since listeners and formsRef don't change on renders. // 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 () => { @@ -132,10 +152,13 @@ const ConnectedSidebar: React.VFC = () => { // To avoid races with panel registration, listen after reserving the initial panels. addListener(listener); + browser.webNavigation.onBeforeNavigate.addListener(navigationListener); + return () => { // NOTE: we don't need to cancel any outstanding forms on unmount because the FormTransformer is set up to watch // for PANEL_HIDING_EVENT. (and the only time this SidebarApp would unmount is if the sidebar was closing) removeListener(listener); + browser.webNavigation.onBeforeNavigate.removeListener(navigationListener); }; // Excluding showModLauncher from deps. The flags detect shouldn't change after initial mount. And if they somehow do, // we don't want to attempt to change mod launcher panel visibility after initial mount. diff --git a/src/sidebar/Tabs.module.scss b/src/sidebar/Tabs.module.scss index a2758d42c5..55a6ef55cf 100644 --- a/src/sidebar/Tabs.module.scss +++ b/src/sidebar/Tabs.module.scss @@ -24,6 +24,7 @@ .tabContainer { flex-wrap: nowrap; + position: relative; } .tabWrapper { diff --git a/src/sidebar/Tabs.tsx b/src/sidebar/Tabs.tsx index 1c37f44fa9..d8639e03f2 100644 --- a/src/sidebar/Tabs.tsx +++ b/src/sidebar/Tabs.tsx @@ -63,6 +63,7 @@ import { useHideEmptySidebar } from "@/sidebar/useHideEmptySidebar"; import removeTemporaryPanel from "@/store/sidebar/thunks/removeTemporaryPanel"; import { type AsyncDispatch } from "@/sidebar/store"; import useOnMountOnly from "@/hooks/useOnMountOnly"; +import UnavailableOverlay from "@/sidebar/UnavailableOverlay"; const ActivateModPanel = lazy( async () => @@ -350,6 +351,7 @@ const Tabs: React.FC = () => { }); }} > + {form.isUnavailable && } diff --git a/src/sidebar/UnavailableOverlay.tsx b/src/sidebar/UnavailableOverlay.tsx new file mode 100644 index 0000000000..d043483234 --- /dev/null +++ b/src/sidebar/UnavailableOverlay.tsx @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 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 React from "react"; +import styles from "./unavailableOverlay.module.scss"; +import cx from "classnames"; +import { Button, Modal } from "react-bootstrap"; + +const UnavailableOverlay = () => ( +
+
+ + + Panel not available on this page + + + +

The browser navigated away from the page

+ +
+
+
+
+); + +export default UnavailableOverlay; diff --git a/src/sidebar/connectedTarget.tsx b/src/sidebar/connectedTarget.tsx index 530482ee95..a97d832a66 100644 --- a/src/sidebar/connectedTarget.tsx +++ b/src/sidebar/connectedTarget.tsx @@ -22,7 +22,7 @@ import { once } from "lodash"; import { type TopLevelFrame, getTopLevelFrame } from "webext-messenger"; import { getTabUrl } from "webext-tools"; -function getConnectedTabIdMv3(): number { +export function getConnectedTabIdMv3(): number { expectContext("sidebar"); const tabId = new URLSearchParams(window.location.search).get("tabId"); assertNotNullish( diff --git a/src/sidebar/unavailableOverlay.module.scss b/src/sidebar/unavailableOverlay.module.scss new file mode 100644 index 0000000000..d13adeb208 --- /dev/null +++ b/src/sidebar/unavailableOverlay.module.scss @@ -0,0 +1,57 @@ +/*! + * Copyright (C) 2024 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 . + */ + +.unavailable-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.5); + display: flex; + justify-content: center; + align-items: flex-start; + backdrop-filter: blur(2.5px); + pointer-events: all; +} + +.modal-dialog { + margin: 23px; + padding: 16px; + border-radius: 12px; + background: white; + border: 1px solid #cfcbd6; + > div { + border: 0; + } +} + +.modal-header { + display: block; + padding-bottom: 0; + background: white; + border: 0; +} + +.modal-body { + background: white; + border: 0; +} + +.modal-button { + border-radius: 4px; +} diff --git a/src/store/sidebar/sidebarSlice.ts b/src/store/sidebar/sidebarSlice.ts index abf183f3cd..c880800a1e 100644 --- a/src/store/sidebar/sidebarSlice.ts +++ b/src/store/sidebar/sidebarSlice.ts @@ -217,6 +217,15 @@ const sidebarSlice = createSlice({ fixActiveTabOnRemove(state, entry); }, + markTemporaryPanelsAsUnavailable(state) { + for (const form of state.forms) { + form.isUnavailable = true; + } + + for (const temporaryPanel of state.temporaryPanels) { + temporaryPanel.isUnavailable = true; + } + }, updateTemporaryPanel( state, action: PayloadAction<{ panel: TemporaryPanelEntry }>, diff --git a/src/testUtils/factories/sidebarEntryFactories.ts b/src/testUtils/factories/sidebarEntryFactories.ts index a242960b81..633373638c 100644 --- a/src/testUtils/factories/sidebarEntryFactories.ts +++ b/src/testUtils/factories/sidebarEntryFactories.ts @@ -17,9 +17,9 @@ import { define, type FactoryConfig } from "cooky-cutter"; import { - type ModActivationPanelEntry, type EntryType, type FormPanelEntry, + type ModActivationPanelEntry, type PanelEntry, type SidebarEntry, type StaticPanelEntry, @@ -38,11 +38,13 @@ const activateModPanelEntryFactory = define({ }, ], heading: (n: number) => `Activate Mods Test ${n}`, + isUnavailable: false, }); const staticPanelEntryFactory = define({ type: "staticPanel", heading: (n: number) => `Static Panel ${n}`, key: (n: number) => `static-panel-${n}`, + isUnavailable: false, }); const formDefinitionFactory = define({ schema: () => ({ @@ -60,6 +62,7 @@ export const formEntryFactory = define({ validateRegistryId(`@test/form-panel-recipe-test-${n}`), nonce: uuidSequence, form: formDefinitionFactory, + isUnavailable: false, }); const temporaryPanelEntryFactory = define({ type: "temporaryPanel", @@ -68,6 +71,7 @@ const temporaryPanelEntryFactory = define({ heading: (n: number) => `Temporary Panel Test ${n}`, payload: null, nonce: uuidSequence, + isUnavailable: false, }); const panelEntryFactory = define({ type: "panel", @@ -78,6 +82,7 @@ const panelEntryFactory = define({ payload: null, extensionPointId: (n: number) => validateRegistryId(`@test/panel-extension-point-test-${n}`), + isUnavailable: false, }); export function sidebarEntryFactory( diff --git a/src/types/sidebarTypes.ts b/src/types/sidebarTypes.ts index e833977c77..5b675076a8 100644 --- a/src/types/sidebarTypes.ts +++ b/src/types/sidebarTypes.ts @@ -111,6 +111,12 @@ type BasePanelEntry = { * The panel type. */ type: EntryType; + + /** + * Determines if the panel cannot be displayed for the current tab. Used + * to show an overlay over the panel to indicate it is unavailable. + */ + isUnavailable?: boolean; }; /** diff --git a/webpack.config.mjs b/webpack.config.mjs index 99f7e0e9ca..e3d83f079b 100644 --- a/webpack.config.mjs +++ b/webpack.config.mjs @@ -203,7 +203,7 @@ const createConfig = (env, options) => }), // Only notifies when watching. `zsh-notify` is suggested for the `build` script - options.watch && + !isProd(options) && process.env.DEV_NOTIFY !== "false" && new WebpackBuildNotifierPlugin({ title: "PB Extension",