diff --git a/package.json b/package.json index 746a84f..b57cc35 100644 --- a/package.json +++ b/package.json @@ -2,20 +2,20 @@ "name": "testpilot-containers", "title": "Multi-Account Containers", "description": "Containers helps you keep all the parts of your online life contained in different tabs. Custom labels and color-coded tabs help keep different activities — like online shopping, travel planning, or checking work email — separate.", - "version": "6.0.0", + "version": "6.0.1", "author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston", "bugs": { "url": "https://github.com/mozilla/multi-account-containers/issues" }, "dependencies": {}, "devDependencies": { - "addons-linter": "^0.15.14", + "ajv": "^6.6.2", + "addons-linter": "^1.3.2", "chai": "^4.1.2", - "deploy-txp": "^1.0.7", "eslint": "^3.17.1", "eslint-plugin-no-unsanitized": "^2.0.0", "eslint-plugin-promise": "^3.4.0", - "htmllint-cli": "^0.0.5", + "htmllint-cli": "0.0.7", "jsdom": "^11.6.2", "json": "^9.0.6", "mocha": "^5.0.0", @@ -36,7 +36,6 @@ }, "scripts": { "build": "npm test && cd src && web-ext build --overwrite-dest", - "deploy": "deploy-txp", "lint": "npm-run-all lint:*", "lint:addon": "addons-linter src --self-hosted", "lint:css": "stylelint src/css/*.css", diff --git a/src/css/popup.css b/src/css/popup.css index beede2c..f2423a3 100644 --- a/src/css/popup.css +++ b/src/css/popup.css @@ -46,6 +46,7 @@ body { --small-radius: 3px; --icon-button-size: calc(calc(var(--block-line-separation-size) * 2) + 1.66rem); /* 20px */ --column-panel-inline-size: 268px; + --inactive-opacity: 0.3; } @media (min-resolution: 1dppx) { @@ -538,7 +539,7 @@ span ~ .panel-header-text { } #current-tab > label > input:checked { - background-image: url("chrome://global/skin/in-content/check.svg#check-native"); + background-image: url("/img/check.svg"); background-position: -1px -1px; background-size: var(--icon-size); } @@ -579,6 +580,11 @@ span ~ .panel-header-text { max-inline-size: 204px; } +.disable-edit-containers { + opacity: var(--inactive-opacity); + pointer-events: none; +} + .userContext-wrapper { align-items: center; display: flex; diff --git a/src/img/blank-favicon.svg b/src/img/blank-favicon.svg new file mode 100644 index 0000000..c2393b6 --- /dev/null +++ b/src/img/blank-favicon.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/img/check.svg b/src/img/check.svg new file mode 100644 index 0000000..bcbcfc0 --- /dev/null +++ b/src/img/check.svg @@ -0,0 +1,6 @@ + + + + diff --git a/src/js/background/backgroundLogic.js b/src/js/background/backgroundLogic.js index 04906d8..89f9159 100644 --- a/src/js/background/backgroundLogic.js +++ b/src/js/background/backgroundLogic.js @@ -6,6 +6,7 @@ const backgroundLogic = { "about:home", "about:blank" ]), + unhideQueue: [], async getExtensionInfo() { const manifestPath = browser.extension.getURL("manifest.json"); @@ -112,6 +113,17 @@ const backgroundLogic = { return list.concat(containerState.hiddenTabs); }, + async unhideContainer(cookieStoreId) { + if (!this.unhideQueue.includes(cookieStoreId)) { + this.unhideQueue.push(cookieStoreId); + await this.showTabs({ + cookieStoreId + }); + this.unhideQueue.splice(this.unhideQueue.indexOf(cookieStoreId), 1); + } + }, + + async moveTabsToWindow(options) { const requiredArguments = ["cookieStoreId", "windowId"]; this.checkArgs(requiredArguments, options, "moveTabsToWindow"); @@ -123,6 +135,7 @@ const backgroundLogic = { }); const containerState = await identityState.storageArea.get(cookieStoreId); + // Nothing to do if (list.length === 0 && containerState.hiddenTabs.length === 0) { @@ -152,12 +165,15 @@ const backgroundLogic = { const showHiddenPromises = []; // Let's show the hidden tabs. - for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const - showHiddenPromises.push(browser.tabs.create({ - url: object.url || DEFAULT_TAB, - windowId: newWindowObj.id, - cookieStoreId - })); + if (!this.unhideQueue.includes(cookieStoreId)) { + this.unhideQueue.push(cookieStoreId); + for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const + showHiddenPromises.push(browser.tabs.create({ + url: object.url || DEFAULT_TAB, + windowId: newWindowObj.id, + cookieStoreId + })); + } } if (hiddenDefaultTabToClose) { @@ -176,7 +192,9 @@ const backgroundLogic = { browser.tabs.remove(tab.id); } } - return await identityState.storageArea.set(cookieStoreId, containerState); + const rv = await identityState.storageArea.set(cookieStoreId, containerState); + this.unhideQueue.splice(this.unhideQueue.indexOf(cookieStoreId), 1); + return rv; }, async _closeTabs(userContextId, windowId = false) { @@ -209,7 +227,9 @@ const backgroundLogic = { }); identitiesOutput[cookieStoreId] = { hasHiddenTabs: !!containerState.hiddenTabs.length, - hasOpenTabs: !!openTabs.length + hasOpenTabs: !!openTabs.length, + numberOfHiddenTabs: containerState.hiddenTabs.length, + numberOfOpenTabs: openTabs.length }; return; }); @@ -307,4 +327,3 @@ const backgroundLogic = { return `firefox-container-${userContextId}`; } }; - diff --git a/src/js/background/messageHandler.js b/src/js/background/messageHandler.js index 6e5fced..9fbe88e 100644 --- a/src/js/background/messageHandler.js +++ b/src/js/background/messageHandler.js @@ -3,7 +3,6 @@ const messageHandler = { // We use this to catch redirected tabs that have just opened // If this were in platform we would change how the tab opens based on "new tab" link navigations such as ctrl+click LAST_CREATED_TAB_TIMER: 2000, - unhideQueue: [], init() { // Handles messages from webextension code @@ -39,7 +38,7 @@ const messageHandler = { backgroundLogic.sortTabs(); break; case "showTabs": - this.unhideContainer(m.cookieStoreId); + backgroundLogic.unhideContainer(m.cookieStoreId); break; case "hideTabs": backgroundLogic.hideTabs({ @@ -156,7 +155,7 @@ const messageHandler = { this.incrementCountOfContainerTabsOpened(); } - this.unhideContainer(tab.cookieStoreId); + backgroundLogic.unhideContainer(tab.cookieStoreId); } setTimeout(() => { this.lastCreatedTab = null; @@ -182,17 +181,6 @@ const messageHandler = { } }, - async unhideContainer(cookieStoreId) { - if (!this.unhideQueue.includes(cookieStoreId)) { - this.unhideQueue.push(cookieStoreId); - // Unhide all hidden tabs - await backgroundLogic.showTabs({ - cookieStoreId - }); - this.unhideQueue.splice(this.unhideQueue.indexOf(cookieStoreId), 1); - } - }, - async onFocusChangedCallback(windowId) { assignManager.removeContextMenu(); // browserAction loses background color in new windows ... diff --git a/src/js/popup.js b/src/js/popup.js index f9690de..7593bea 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -162,7 +162,7 @@ const Logic = { async clearBrowserActionBadge() { const extensionInfo = await getExtensionInfo(); const storage = await browser.storage.local.get({browserActionBadgesClicked: []}); - browser.browserAction.setBadgeBackgroundColor({color: ""}); + browser.browserAction.setBadgeBackgroundColor({color: null}); browser.browserAction.setBadgeText({text: ""}); storage.browserActionBadgesClicked.push(extensionInfo.version); // use set and spread to create a unique array @@ -177,7 +177,9 @@ const Logic = { name: "Default", cookieStoreId, icon: "default-tab", - color: "default-tab" + color: "default-tab", + numberOfHiddenTabs: 0, + numberOfOpenTabs: 0 }; // Handle old style rejection with null and also Promise.reject new style try { @@ -212,6 +214,27 @@ const Logic = { return false; }, + async numTabs() { + const activeTabs = await browser.tabs.query({windowId: browser.windows.WINDOW_ID_CURRENT}); + return activeTabs.length; + }, + + _disableMoveTabs(message) { + const moveTabsEl = document.querySelector("#container-info-movetabs"); + const fragment = document.createDocumentFragment(); + const incompatEl = document.createElement("div"); + + moveTabsEl.classList.remove("clickable"); + moveTabsEl.setAttribute("title", message); + + fragment.appendChild(incompatEl); + incompatEl.setAttribute("id", "container-info-movetabs-incompat"); + incompatEl.textContent = message; + incompatEl.classList.add("container-info-tab-row"); + + moveTabsEl.parentNode.insertBefore(fragment, moveTabsEl.nextSibling); + }, + async refreshIdentities() { const [identities, state] = await Promise.all([ browser.contextualIdentities.query({}), @@ -227,6 +250,8 @@ const Logic = { if (stateObject) { identity.hasOpenTabs = stateObject.hasOpenTabs; identity.hasHiddenTabs = stateObject.hasHiddenTabs; + identity.numberOfHiddenTabs = stateObject.numberOfHiddenTabs; + identity.numberOfOpenTabs = stateObject.numberOfOpenTabs; } return identity; }); @@ -485,8 +510,10 @@ Logic.registerPanel(P_CONTAINERS_LIST, { Logic.showPanel(P_CONTAINER_EDIT, { name: Logic.generateIdentityName() }); }); - Logic.addEnterHandler(document.querySelector("#edit-containers-link"), () => { - Logic.showPanel(P_CONTAINERS_EDIT); + Logic.addEnterHandler(document.querySelector("#edit-containers-link"), (e) => { + if (!e.target.classList.contains("disable-edit-containers")){ + Logic.showPanel(P_CONTAINERS_EDIT); + } }); Logic.addEnterHandler(document.querySelector("#sort-containers-link"), async function () { @@ -666,6 +693,13 @@ Logic.registerPanel(P_CONTAINERS_LIST, { document.addEventListener("mousedown", () => { document.removeEventListener("focus", focusHandler); }); + /* If no container is present disable the Edit Containers button */ + const editContainer = document.querySelector("#edit-containers-link"); + if (Logic.identities().length === 0) { + editContainer.classList.add("disable-edit-containers"); + } else { + editContainer.classList.remove("disable-edit-containers"); + } return Promise.resolve(); }, @@ -698,37 +732,31 @@ Logic.registerPanel(P_CONTAINER_INFO, { }); // Check if the user has incompatible add-ons installed + let incompatible = false; try { - const incompatible = await browser.runtime.sendMessage({ + incompatible = await browser.runtime.sendMessage({ method: "checkIncompatibleAddons" }); - const moveTabsEl = document.querySelector("#container-info-movetabs"); - if (incompatible) { - const fragment = document.createDocumentFragment(); - const incompatEl = document.createElement("div"); - - moveTabsEl.classList.remove("clickable"); - moveTabsEl.setAttribute("title", "Moving container tabs is incompatible with Pulse, PageShot, and SnoozeTabs."); - - fragment.appendChild(incompatEl); - incompatEl.setAttribute("id", "container-info-movetabs-incompat"); - incompatEl.textContent = "Incompatible with other Experiments."; - incompatEl.classList.add("container-info-tab-row"); - - moveTabsEl.parentNode.insertBefore(fragment, moveTabsEl.nextSibling); - } else { - Logic.addEnterHandler(moveTabsEl, async function () { - await browser.runtime.sendMessage({ - method: "moveTabsToWindow", - windowId: browser.windows.WINDOW_ID_CURRENT, - cookieStoreId: Logic.currentIdentity().cookieStoreId, - }); - window.close(); - }); - } } catch (e) { throw new Error("Could not check for incompatible add-ons."); } + const moveTabsEl = document.querySelector("#container-info-movetabs"); + const numTabs = await Logic.numTabs(); + if (incompatible) { + Logic._disableMoveTabs("Moving container tabs is incompatible with Pulse, PageShot, and SnoozeTabs."); + return; + } else if (numTabs === 1) { + Logic._disableMoveTabs("Cannot move a tab from a single-tab window."); + return; + } + Logic.addEnterHandler(moveTabsEl, async function () { + await browser.runtime.sendMessage({ + method: "moveTabsToWindow", + windowId: browser.windows.WINDOW_ID_CURRENT, + cookieStoreId: Logic.currentIdentity().cookieStoreId, + }); + window.close(); + }); }, // This method is called when the panel is shown. @@ -1025,6 +1053,11 @@ Logic.registerPanel(P_CONTAINER_EDIT, { document.querySelector("#edit-container-panel-name-input").value = identity.name || ""; document.querySelector("#edit-container-panel-usercontext-input").value = userContextId || NEW_CONTAINER_ID; + const containerName = document.querySelector("#edit-container-panel-name-input"); + window.requestAnimationFrame(() => { + containerName.select(); + containerName.focus(); + }); [...document.querySelectorAll("[name='container-color']")].forEach(colorInput => { colorInput.checked = colorInput.value === identity.color; }); @@ -1069,9 +1102,17 @@ Logic.registerPanel(P_CONTAINER_DELETE, { prepare() { const identity = Logic.currentIdentity(); - // Populating the panel: name and icon + // Populating the panel: name, icon, and warning message document.getElementById("delete-container-name").textContent = identity.name; + const totalNumberOfTabs = identity.numberOfHiddenTabs + identity.numberOfOpenTabs; + let warningMessage = ""; + if (totalNumberOfTabs > 0) { + const grammaticalNumTabs = totalNumberOfTabs > 1 ? "tabs" : "tab"; + warningMessage = `If you remove this container now, ${totalNumberOfTabs} container ${grammaticalNumTabs} will be closed.`; + } + document.getElementById("delete-container-tab-warning").textContent = warningMessage; + const icon = document.getElementById("delete-container-icon"); icon.setAttribute("data-identity-icon", identity.icon); icon.setAttribute("data-identity-color", identity.color); diff --git a/src/js/utils.js b/src/js/utils.js index 5d5046b..804e9d9 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -1,4 +1,4 @@ -const DEFAULT_FAVICON = "moz-icon://goat?size=16"; +const DEFAULT_FAVICON = "/img/blank-favicon.svg"; // TODO use export here instead of globals window.Utils = { diff --git a/src/manifest.json b/src/manifest.json index f3fa049..d624d32 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Firefox Multi-Account Containers", - "version": "6.0.0", + "version": "6.0.1", "description": "Multi-Account Containers helps you keep all the parts of your online life contained in different tabs. Custom labels and color-coded tabs help keep different activities — like online shopping, travel planning, or checking work email — separate.", "icons": { diff --git a/src/popup.html b/src/popup.html index a28dd32..4cd8219 100644 --- a/src/popup.html +++ b/src/popup.html @@ -204,7 +204,7 @@

Remove This Container

-

If you remove this container now, container tabs will be closed. Are you sure you want to remove this Container?

+

Are you sure you want to remove this Container?