diff --git a/.gitignore b/.gitignore index f5198a3..6ff00a9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules README.html *.xpi *.swp +*.swo .vimrc .env addon.env diff --git a/webextension/background.js b/webextension/background.js index 979c164..a1a3817 100644 --- a/webextension/background.js +++ b/webextension/background.js @@ -84,38 +84,7 @@ const assignManager = { init() { browser.contextMenus.onClicked.addListener((info, tab) => { - const userContextId = this.getUserContextIdFromCookieStore(tab); - // Mapping ${URL(info.pageUrl).hostname} to ${userContextId} - if (userContextId) { - let actionName; - let storageAction; - if (info.menuItemId === this.MENU_ASSIGN_ID) { - actionName = "added"; - storageAction = this.storageArea.set(info.pageUrl, { - userContextId, - neverAsk: false, - exempted: [] - }); - } else { - actionName = "removed"; - storageAction = this.storageArea.remove(info.pageUrl); - } - storageAction.then(() => { - browser.notifications.create({ - type: "basic", - title: "Containers", - message: `Successfully ${actionName} site to always open in this container`, - iconUrl: browser.extension.getURL("/img/onboarding-1.png") - }); - backgroundLogic.sendTelemetryPayload({ - event: `${actionName}-container-assignment`, - userContextId: userContextId, - }); - this.calculateContextMenu(tab); - }).catch((e) => { - throw e; - }); - } + this._onClickedHandler(info, tab); }); // Before a request is handled by the browser we decide if we should route through a different container @@ -163,6 +132,26 @@ const assignManager = { },{urls: [""], types: ["main_frame"]}, ["blocking"]); }, + async _onClickedHandler(info, tab) { + const userContextId = this.getUserContextIdFromCookieStore(tab); + // Mapping ${URL(info.pageUrl).hostname} to ${userContextId} + if (userContextId) { + // let actionName; + let remove; + if (info.menuItemId === this.MENU_ASSIGN_ID) { + //actionName = "added"; + // storageAction = this._setAssignment(info.pageUrl, userContextId, setOrRemove); + remove = false; + } else { + // actionName = "removed"; + //storageAction = this.storageArea.remove(info.pageUrl); + remove = true; + } + await this._setOrRemoveAssignment(info.pageUrl, userContextId, remove); + this.calculateContextMenu(tab); + } + }, + deleteContainer(userContextId) { this.storageArea.deleteContainer(userContextId); @@ -191,36 +180,63 @@ const assignManager = { return true; }, - calculateContextMenu(tab) { + async _setOrRemoveAssignment(pageUrl, userContextId, remove) { + let storageAction; + if (!remove) { + await this.storageArea.set(pageUrl, { + userContextId, + neverAsk: false, + exempted: [] + }); + actionName = "added"; + } else { + await this.storageArea.remove(pageUrl); + actionName = "removed"; + } + browser.notifications.create({ + type: "basic", + title: "Containers", + message: `Successfully ${actionName} site to always open in this container`, + iconUrl: browser.extension.getURL("/img/onboarding-1.png") + }); + backgroundLogic.sendTelemetryPayload({ + event: `${actionName}-container-assignment`, + userContextId: userContextId, + }); + }, + + async _getAssignment(tab) { + const cookieStore = this.getUserContextIdFromCookieStore(tab); + // Ensure we have a cookieStore to assign to + if (cookieStore + && this.isTabPermittedAssign(tab)) { + return await this.storageArea.get(tab.url); + } + return false; + }, + + async calculateContextMenu(tab) { // There is a focus issue in this menu where if you change window with a context menu click // you get the wrong menu display because of async // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1215376#c16 // We also can't change for always private mode // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1352102 - const cookieStore = this.getUserContextIdFromCookieStore(tab); browser.contextMenus.remove(this.MENU_ASSIGN_ID); browser.contextMenus.remove(this.MENU_REMOVE_ID); - // Ensure we have a cookieStore to assign to - if (cookieStore - && this.isTabPermittedAssign(tab)) { - this.storageArea.get(tab.url).then((siteSettings) => { - // ✓ This is to mitigate https://bugzilla.mozilla.org/show_bug.cgi?id=1351418 - let prefix = " "; // Alignment of non breaking space, unknown why this requires so many spaces to align with the tick - let menuId = this.MENU_ASSIGN_ID; - if (siteSettings) { - prefix = "✓"; - menuId = this.MENU_REMOVE_ID; - } - browser.contextMenus.create({ - id: menuId, - title: `${prefix} Always Open in This Container`, - checked: true, - contexts: ["all"], - }); - }).catch((e) => { - throw e; - }); + const siteSettings = await this._getAssignment(tab); + // ✓ This is to mitigate https://bugzilla.mozilla.org/show_bug.cgi?id=1351418 + let prefix = " "; // Alignment of non breaking space, unknown why this requires so many spaces to align with the tick + let menuId = this.MENU_ASSIGN_ID; + if (siteSettings) { + prefix = "✓"; + menuId = this.MENU_REMOVE_ID; } + browser.contextMenus.create({ + id: menuId, + title: `${prefix} Always Open in This Container`, + checked: true, + contexts: ["all"], + }); }, reloadPageInContainer(url, currentUserContextId, userContextId, index, neverAsk = false) { @@ -396,6 +412,17 @@ const messageHandler = { case "neverAsk": assignManager._neverAsk(m); break; + case "getAssignment": + response = browser.tabs.get(m.tabId).then((tab) => { + return assignManager._getAssignment(tab); + }); + break; + case "setOrRemoveAssignment": + response = browser.tabs.get(m.tabId).then((tab) => { + const userContextId = assignManager.getUserContextIdFromCookieStore(tab); + return assignManager._setOrRemoveAssignment(tab.url, userContextId, m.value); + }); + break; case "exemptContainerAssignment": response = assignManager._exemptTab(m); break; diff --git a/webextension/css/popup.css b/webextension/css/popup.css index d053693..c01a194 100644 --- a/webextension/css/popup.css +++ b/webextension/css/popup.css @@ -8,6 +8,11 @@ html { box-sizing: border-box; } +:root { + --font-size-heading: 16px; + --primary-action-color: #248aeb; +} + *, *::before, *::after { @@ -35,6 +40,10 @@ table { overflow: auto; } +.offpage { + opacity: 0; +} + /* Color and icon helpers */ [data-identity-color="blue"] { --identity-tab-color: #37adff; @@ -140,6 +149,18 @@ table { background-color: rgba(0, 0, 0, 0.05); } +/* Text links with actions */ + +.action-link:link { + color: var(--primary-action-color); + text-decoration: none; +} + +.action-link:active, +.action-link:hover { + text-decoration: underline; +} + /* Panels keep everything togethert */ .panel { display: flex; @@ -223,7 +244,7 @@ table { .onboarding-title { color: #43484e; - font-size: 16px; + font-size: var(--font-size-heading); margin-block-end: 0; margin-block-start: 0; margin-inline-end: 0; @@ -312,7 +333,7 @@ manage things like container crud */ .panel-header-text { color: #4a4a4a; flex: 1; - font-size: 16px; + font-size: var(--font-size-heading); font-weight: normal; margin-block-end: 0; margin-block-start: 0; @@ -324,6 +345,24 @@ manage things like container crud */ padding-inline-start: 16px; } +#container-panel .panel-header { + block-size: 26px; + background-color: #efefef; + font-size: 14px; +} + +#container-panel .panel-header-text { + font-size: 14px; + text-transform: uppercase; + color: #727272; + padding-block-end: 0; + padding-block-start: 0; +} + +#container-panel .sort-containers-link { + margin-inline-end: 16px; +} + span ~ .panel-header-text { padding-block-end: 0; padding-block-start: 0; @@ -331,6 +370,62 @@ span ~ .panel-header-text { padding-inline-start: 0; } +#current-tab { + max-inline-size: 100%; + min-block-size: 94px; + padding-block-end: 16px; + padding-block-start: 16px; + padding-inline-end: 16px; + padding-inline-start: 16px; +} + +#current-tab > h3 { + color: #4a4a4a; + font-size: var(--font-size-heading); + font-weight: normal; + margin-block-end: 0; + margin-block-start: 0; +} + +#current-page { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +#current-page > img { + block-size: 16px; + inline-size: 16px; +} + +#current-tab > label { + align-items: center; + display: flex; + margin-inline-start: 17px; +} + +#current-tab > label > input { + display: inline; +} +#current-tab > label > img { + block-size: 12px; + display: inline-block; + inline-size: 12px; +} + +#current-container { + display: contents; + text-transform: lowercase; +} + +#current-container > .usercontext-icon { + block-size: 16px; + display: block; + flex: 0 0 20px; + inline-size: 20px; + background-size: 16px; +} + /* Rows used when iterating over panels */ .container-panel-row { align-items: center; @@ -501,7 +596,7 @@ span ~ .panel-header-text { .edit-containers-exit-text { align-items: center; - background: #248aeb; + background: var(--primary-action-color); block-size: 100%; color: #fff; display: flex; @@ -528,7 +623,7 @@ span ~ .panel-header-text { .delete-container-confirm-title { color: #000; - font-size: 16px; + font-size: var(--font-size-heading); } /* Form info */ diff --git a/webextension/js/popup.js b/webextension/js/popup.js index 26afa59..70188e6 100644 --- a/webextension/js/popup.js +++ b/webextension/js/popup.js @@ -18,6 +18,7 @@ const P_CONTAINERS_EDIT = "containersEdit"; const P_CONTAINER_INFO = "containerInfo"; const P_CONTAINER_EDIT = "containerEdit"; const P_CONTAINER_DELETE = "containerDelete"; +const DEFAULT_FAVICON = "moz-icon://goat?size=16"; /** * Escapes any occurances of &, ", <, > or / with XML entities. @@ -107,6 +108,16 @@ const Logic = { browser.storage.local.set({browserActionBadgesClicked: storage.browserActionBadgesClicked}); }, + async identity(cookieStoreId) { + const identity = await browser.contextualIdentities.get(cookieStoreId); + return identity || { + name: "Default", + cookieStoreId, + icon: "circle", + color: "black" + }; + }, + addEnterHandler(element, handler) { element.addEventListener("click", handler); element.addEventListener("keydown", (e) => { @@ -121,6 +132,14 @@ const Logic = { return (userContextId !== cookieStoreId) ? Number(userContextId) : false; }, + async currentTab() { + const activeTabs = await browser.tabs.query({active: true}); + if (activeTabs.length > 0) { + return activeTabs[0] + } + return false; + }, + refreshIdentities() { return Promise.all([ browser.contextualIdentities.query({}), @@ -139,7 +158,7 @@ const Logic = { }).catch((e) => {throw e;}); }, - showPanel(panel, currentIdentity = null) { + async showPanel(panel, currentIdentity = null) { // Invalid panel... ?!? if (!(panel in this._panels)) { throw new Error("Something really bad happened. Unknown panel: " + panel); @@ -151,15 +170,18 @@ const Logic = { this._currentIdentity = currentIdentity; // Initialize the panel before showing it. - this._panels[panel].prepare().then(() => { - for (let panelElement of document.querySelectorAll(".panel")) { // eslint-disable-line prefer-const + await this._panels[panel].prepare(); + Object.keys(this._panels).forEach((panelKey) => { + const panelItem = this._panels[panelKey]; + const panelElement = document.querySelector(panelItem.panelSelector); + if (!panelElement.classList.contains("hide")) { panelElement.classList.add("hide"); + if ("unregister" in panelItem) { + panelItem.unregister(); + } } - document.querySelector(this._panels[panel].panelSelector).classList.remove("hide"); - }) - .catch(() => { - throw new Error("Failed to show panel " + panel); }); + document.querySelector(this._panels[panel].panelSelector).classList.remove("hide"); }, showPreviousPanel() { @@ -205,6 +227,21 @@ const Logic = { }); }, + getAssignment(tab) { + return browser.runtime.sendMessage({ + method: "getAssignment", + tabId: tab.id + }); + }, + + setOrRemoveAssignment(tab, value) { + return browser.runtime.sendMessage({ + method: "setOrRemoveAssignment", + tabId: tab.id, + value + }); + }, + generateIdentityName() { const defaultName = "Container #"; const ids = []; @@ -364,12 +401,86 @@ Logic.registerPanel(P_CONTAINERS_LIST, { break; } }); + + // When the popup is open sometimes the tab will still be updating it's state + this.tabUpdateHandler = (tabId, changeInfo) => { + const propertiesToUpdate = ["title", "favIconUrl"]; + const hasChanged = Object.keys(changeInfo).find((changeInfoKey) => { + if (propertiesToUpdate.includes(changeInfoKey)) { + return true; + } + }); + if (hasChanged) { + this.prepareCurrentTabHeader(); + } + }; + browser.tabs.onUpdated.addListener(this.tabUpdateHandler); + }, + + unregister() { + browser.tabs.onUpdated.removeListener(this.tabUpdateHandler); + }, + + setupAssignmentCheckbox(siteSettings) { + const assignmentCheckboxElement = document.getElementById("container-page-assigned"); + // Cater for null and false + assignmentCheckboxElement.checked = !!siteSettings; + let disabled = false; + if (siteSettings === false) { + disabled = true; + } + assignmentCheckboxElement.disabled = disabled; + }, + + async prepareCurrentTabHeader() { + const currentTab = await Logic.currentTab(); + const currentTabElement = document.getElementById("current-tab"); + const assignmentCheckboxElement = document.getElementById("container-page-assigned"); + assignmentCheckboxElement.addEventListener("change", () => { + Logic.setOrRemoveAssignment(currentTab, !assignmentCheckboxElement.checked); + }); + currentTabElement.hidden = !currentTab; + this.setupAssignmentCheckbox(false); + if (currentTab) { + const identity = await Logic.identity(currentTab.cookieStoreId); + const siteSettings = await Logic.getAssignment(currentTab); + this.setupAssignmentCheckbox(siteSettings); + const currentPage = document.getElementById("current-page"); + const favIconUrl = currentTab.favIconUrl || ""; + currentPage.innerHTML = escaped` + ${currentTab.title} + `; + + const imageElement = currentPage.querySelector("img"); + const loadListener = (e) => { + e.target.classList.remove("offpage"); + e.target.removeEventListener("load", loadListener); + e.target.removeEventListener("error", errorListener); + }; + const errorListener = (e) => { + e.target.src = DEFAULT_FAVICON; + }; + imageElement.addEventListener("error", errorListener); + imageElement.addEventListener("load", loadListener); + + const currentContainer = document.getElementById("current-container"); + currentContainer.innerHTML = escaped` +
+
+ ${identity.name} + `; + } }, // This method is called when the panel is shown. - prepare() { + async prepare() { const fragment = document.createDocumentFragment(); + this.prepareCurrentTabHeader(); + Logic.identities().forEach(identity => { const hasTabs = (identity.hasHiddenTabs || identity.hasOpenTabs); const tr = document.createElement("tr"); diff --git a/webextension/popup.html b/webextension/popup.html index 794c9b0..82ea19e 100644 --- a/webextension/popup.html +++ b/webextension/popup.html @@ -38,9 +38,17 @@
+
+

Current Tab

+
+ +

Containers

- Sort Containers + Sort Tabs