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/.stylelintrc b/.stylelintrc index 656873f..cf506c8 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -11,7 +11,7 @@ "declaration-block-no-duplicate-properties": true, "order/declaration-block-properties-alphabetical-order": true, "property-blacklist": [ - "/height/", + "/(min[-]|max[-])height/", "/width/", "/top/", "/bottom/", diff --git a/docs/metrics.md b/docs/metrics.md index 135ed97..af1ae8d 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -220,7 +220,7 @@ of a `testpilottest` telemetry ping for each scenario. } ``` -* The user clicks "Take me there" to reload a site into a container after the user picked "Always Open in this Container". +* The user clicks "Open in *assigned* container" to reload a site into a container after the user picked "Always Open in this Container". ```js { @@ -229,6 +229,15 @@ of a `testpilottest` telemetry ping for each scenario. } ``` +* The user clicks "Open in *Current* container" to reload a site into a container after the user picked "Always Open in this Container". + +```js + { + "uuid": , + "event": "click-to-reload-page-in-same-container" + } +``` + * Firefox automatically reloads a site into a container after the user picked "Always Open in this Container". ```js diff --git a/webextension/background.js b/webextension/background.js index 8f4278c..ca01d89 100644 --- a/webextension/background.js +++ b/webextension/background.js @@ -6,6 +6,7 @@ const assignManager = { MENU_REMOVE_ID: "remove-open-in-this-container", storageArea: { area: browser.storage.local, + exemptedTabs: {}, getSiteStoreKey(pageUrl) { const url = new window.URL(pageUrl); @@ -13,6 +14,22 @@ const assignManager = { return `${storagePrefix}${url.hostname}`; }, + setExempted(pageUrl, tabId) { + if (!(tabId in this.exemptedTabs)) { + this.exemptedTabs[tabId] = []; + } + const siteStoreKey = this.getSiteStoreKey(pageUrl); + this.exemptedTabs[tabId].push(siteStoreKey); + }, + + isExempted(pageUrl, tabId) { + if (!(tabId in this.exemptedTabs)) { + return false; + } + const siteStoreKey = this.getSiteStoreKey(pageUrl); + return this.exemptedTabs[tabId].includes(siteStoreKey); + }, + get(pageUrl) { const siteStoreKey = this.getSiteStoreKey(pageUrl); return new Promise((resolve, reject) => { @@ -70,39 +87,16 @@ const assignManager = { } }, + // We return here so the confirm page can load the tab when exempted + async _exemptTab(m) { + const pageUrl = m.pageUrl; + this.storageArea.setExempted(pageUrl, m.tabId); + return true; + }, + 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 - }); - } 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 @@ -117,11 +111,12 @@ const assignManager = { const userContextId = this.getUserContextIdFromCookieStore(tab); if (!siteSettings || userContextId === siteSettings.userContextId - || tab.incognito) { + || tab.incognito + || this.storageArea.isExempted(options.url, tab.id)) { return {}; } - this.reloadPageInContainer(options.url, siteSettings.userContextId, tab.index + 1, siteSettings.neverAsk); + this.reloadPageInContainer(options.url, userContextId, siteSettings.userContextId, tab.index + 1, siteSettings.neverAsk); this.calculateContextMenu(tab); /* Removal of existing tabs: @@ -149,6 +144,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); @@ -177,43 +192,71 @@ const assignManager = { return true; }, - calculateContextMenu(tab) { + async _setOrRemoveAssignment(pageUrl, userContextId, remove) { + let actionName; + 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, userContextId, index, neverAsk = false) { + reloadPageInContainer(url, currentUserContextId, userContextId, index, neverAsk = false) { + const cookieStoreId = backgroundLogic.cookieStoreId(userContextId); const loadPage = browser.extension.getURL("confirm-page.html"); // If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there if (neverAsk) { - browser.tabs.create({url, cookieStoreId: backgroundLogic.cookieStoreId(userContextId), index}); + browser.tabs.create({url, cookieStoreId, index}); backgroundLogic.sendTelemetryPayload({ event: "auto-reload-page-in-container", userContextId: userContextId, @@ -223,8 +266,17 @@ const assignManager = { event: "prompt-to-reload-page-in-container", userContextId: userContextId, }); - const confirmUrl = `${loadPage}?url=${url}`; - browser.tabs.create({url: confirmUrl, cookieStoreId: backgroundLogic.cookieStoreId(userContextId), index}).then(() => { + let confirmUrl = `${loadPage}?url=${encodeURIComponent(url)}&cookieStoreId=${cookieStoreId}`; + let currentCookieStoreId; + if (currentUserContextId) { + currentCookieStoreId = backgroundLogic.cookieStoreId(currentUserContextId); + confirmUrl += `¤tCookieStoreId=${currentCookieStoreId}`; + } + browser.tabs.create({ + url: confirmUrl, + cookieStoreId: currentCookieStoreId, + index + }).then(() => { // We don't want to sync this URL ever nor clutter the users history browser.history.deleteUrl({url: confirmUrl}); }).catch((e) => { @@ -354,7 +406,7 @@ const messageHandler = { LAST_CREATED_TAB_TIMER: 2000, init() { - // Handles messages from webextension/js/popup.js + // Handles messages from webextension code browser.runtime.onMessage.addListener((m) => { let response; @@ -372,11 +424,25 @@ 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; } return response; }); - // Handles messages from index.js + // Handles messages from sdk code const port = browser.runtime.connect(); port.onMessage.addListener(m => { switch (m.type) { diff --git a/webextension/confirm-page.html b/webextension/confirm-page.html index 48e12f9..09d4852 100644 --- a/webextension/confirm-page.html +++ b/webextension/confirm-page.html @@ -8,22 +8,21 @@
-

Should we open this in your container?

+

Open this site in your assigned container?

- Looks like you requested: + You asked Firefox to always open for this site:

-

- You asked Firefox to always open in this type of container. Would you like to proceed?
-

+

Would you still like to open in this current container?




- + +
diff --git a/webextension/css/confirm-page.css b/webextension/css/confirm-page.css index a595e4c..24baa7c 100644 --- a/webextension/css/confirm-page.css +++ b/webextension/css/confirm-page.css @@ -4,11 +4,21 @@ } main { - background: url(/img/onboarding-1.png) no-repeat; + background: url(/img/onboarding-4.png) no-repeat; background-position: -10px -15px; - background-size: 285px; - margin-inline-start: -285px; - padding-inline-start: 285px; + background-size: 300px; + margin-inline-start: -350px; + padding-inline-start: 350px; +} + +.container-name { + font-weight: bold; +} + +button .container-name, +#current-container-name { + font-weight: bold; + text-transform: capitalize; } @media only screen and (max-width: 1300px) { @@ -36,6 +46,28 @@ html { word-break: break-all; } +#redirect-url { + background: #efefef; + border-radius: 2px; + line-height: 1.5; + padding-block-end: 0.5rem; + padding-block-start: 0.5rem; + padding-inline-end: 0.5rem; + padding-inline-start: 0.5rem; +} + +#redirect-url img { + block-size: 16px; + inline-size: 16px; + margin-inline-end: 6px; + offset-block-start: 3px; + position: relative; +} + dfn { font-style: normal; } + +.button-container > button { + min-inline-size: 240px; +} diff --git a/webextension/css/popup.css b/webextension/css/popup.css index d053693..b336b34 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 { + background-color: #efefef; + block-size: 26px; + font-size: 14px; +} + +#container-panel .panel-header-text { + color: #727272; + font-size: 14px; + padding-block-end: 0; + padding-block-start: 0; + text-transform: uppercase; +} + +#container-panel .sort-containers-link { + margin-inline-end: 16px; +} + span ~ .panel-header-text { padding-block-end: 0; padding-block-start: 0; @@ -331,6 +370,63 @@ 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 { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#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 { + background-size: 16px; + block-size: 16px; + display: block; + flex: 0 0 20px; + inline-size: 20px; +} + /* Rows used when iterating over panels */ .container-panel-row { align-items: center; @@ -501,7 +597,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 +624,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/confirm-page.js b/webextension/js/confirm-page.js index d54dd06..5c5038b 100644 --- a/webextension/js/confirm-page.js +++ b/webextension/js/confirm-page.js @@ -1,10 +1,46 @@ -const redirectUrl = new URL(window.location).searchParams.get("url"); -document.getElementById("redirect-url").textContent = redirectUrl; -const redirectSite = new URL(redirectUrl).hostname; -document.getElementById("redirect-site").textContent = redirectSite; +async function load() { + const searchParams = new URL(window.location).searchParams; + const redirectUrl = decodeURIComponent(searchParams.get("url")); + const cookieStoreId = searchParams.get("cookieStoreId"); + const currentCookieStoreId = searchParams.get("currentCookieStoreId"); + const redirectUrlElement = document.getElementById("redirect-url"); + redirectUrlElement.textContent = redirectUrl; + createFavicon(redirectUrl, redirectUrlElement); -document.getElementById("redirect-form").addEventListener("submit", (e) => { - e.preventDefault(); + const container = await browser.contextualIdentities.get(cookieStoreId); + [...document.querySelectorAll(".container-name")].forEach((containerNameElement) => { + containerNameElement.textContent = container.name; + }); + + // If default container, button will default to normal HTML content + if (currentCookieStoreId) { + const currentContainer = await browser.contextualIdentities.get(currentCookieStoreId); + document.getElementById("current-container-name").textContent = currentContainer.name; + } + + document.getElementById("redirect-form").addEventListener("submit", (e) => { + e.preventDefault(); + const buttonTarget = e.explicitOriginalTarget; + switch (buttonTarget.id) { + case "confirm": + confirmSubmit(redirectUrl, cookieStoreId); + break; + case "deny": + denySubmit(redirectUrl); + break; + } + }); +} + +function createFavicon(pageUrl, redirectUrlElement) { + const origin = new URL(pageUrl).origin; + const imageElement = document.createElement("img"); + imageElement.src = `${origin}/favicon.ico`; + + redirectUrlElement.prepend(imageElement); +} + +function confirmSubmit(redirectUrl, cookieStoreId) { const neverAsk = document.getElementById("never-ask").checked; // Sending neverAsk message to background to store for next time we see this process if (neverAsk) { @@ -12,20 +48,38 @@ document.getElementById("redirect-form").addEventListener("submit", (e) => { method: "neverAsk", neverAsk: true, pageUrl: redirectUrl - }).then(() => { - redirect(); - }).catch(() => { - // Can't really do much here user will have to click it again }); } browser.runtime.sendMessage({ method: "sendTelemetryPayload", event: "click-to-reload-page-in-container", }); - redirect(); -}); + openInContainer(redirectUrl, cookieStoreId); +} -function redirect() { - const redirectUrl = document.getElementById("redirect-url").textContent; +async function denySubmit(redirectUrl) { + const tab = await browser.tabs.query({active: true}); + await browser.runtime.sendMessage({ + method: "exemptContainerAssignment", + tabId: tab[0].id, + pageUrl: redirectUrl + }); document.location.replace(redirectUrl); } + +load(); + +async function openInContainer(redirectUrl, cookieStoreId) { + const tabs = await browser.tabs.query({active: true}); + browser.runtime.sendMessage({ + method: "sendTelemetryPayload", + event: "click-to-reload-page-in-same-container", + }); + await browser.tabs.create({ + cookieStoreId, + url: redirectUrl + }); + if (tabs.length > 0) { + browser.tabs.remove(tabs[0].id); + } +} diff --git a/webextension/js/popup.js b/webextension/js/popup.js index 26afa59..e14dc63 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