From 2ee9682950c2d6f2ce4c2c3c636e8455d247c26d Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Wed, 12 Apr 2017 17:57:23 +0100 Subject: [PATCH 01/21] Adding in context menu to tab context menu. Fixes #424. --- webextension/background.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webextension/background.js b/webextension/background.js index 57230b4..1274026 100644 --- a/webextension/background.js +++ b/webextension/background.js @@ -205,7 +205,7 @@ const assignManager = { id: menuId, title: `${prefix} Always Open in This Container`, checked: true, - contexts: ["all"], + contexts: ["all", "tab"], }); }).catch((e) => { throw e; From 91ec0c4a6d5516b1a74aed7ad12425b60440fc82 Mon Sep 17 00:00:00 2001 From: baku Date: Mon, 24 Apr 2017 11:37:40 +0200 Subject: [PATCH 02/21] Pinned tabs should be stored as such - issue #456 --- index.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index ba86ad3..d1f005d 100644 --- a/index.js +++ b/index.js @@ -457,7 +457,13 @@ const ContainerService = { }, _createTabObject(tab) { - return { title: tab.title, url: tab.url, id: tab.id, active: true }; + return { + title: tab.title, + url: tab.url, + id: tab.id, + active: true, + pinned: tabsUtils.isPinned(viewFor(tab)) + }; }, _containerTabIterator(userContextId, cb) { @@ -648,6 +654,7 @@ const ContainerService = { url: object.url, nofocus: args.nofocus || false, window: args.window || null, + pinned: object.pinned, })); } @@ -869,6 +876,10 @@ const ContainerService = { browserWin.gBrowser.selectedTab = tab; browserWin.focusAndSelectUrlBar(); } + + if (args.pinned) { + browserWin.gBrowser.pinTab(tab); + } return true; }); }).catch(() => false); From 1de3f42385890bff2138a7f9aad1830ef9b8e8ab Mon Sep 17 00:00:00 2001 From: baku Date: Fri, 28 Apr 2017 16:56:55 +0200 Subject: [PATCH 03/21] Recovering after a crash - issue #463 --- index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index d1f005d..ba8dacb 100644 --- a/index.js +++ b/index.js @@ -124,7 +124,8 @@ const ContainerService = { // uninstallation. This object contains also a version number, in case we // need to implement a migration in the future. // In 1.1.1 and less we deleted savedConfiguration on upgrade so we need to rebuild - if (installation && (reason !== "upgrade" || !ss.storage.savedConfiguration)) { + if (!ss.storage.savedConfiguration || + (installation && reason !== "upgrade")) { let preInstalledIdentities = []; // eslint-disable-line prefer-const ContextualIdentityProxy.getIdentities().forEach(identity => { preInstalledIdentities.push(identity.userContextId); From f4597eae844852ae9f3675b11e1840bb5848819a Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Fri, 28 Apr 2017 14:15:59 +0100 Subject: [PATCH 04/21] Moving bulk of removeIdentity code into popup.js --- index.js | 33 ++++--------------- webextension/background.js | 32 ++++++++++-------- webextension/js/confirm-page.js | 1 + webextension/js/popup.js | 57 ++++++++++++++++++++++++++++++--- webextension/manifest.json | 1 + 5 files changed, 80 insertions(+), 44 deletions(-) diff --git a/index.js b/index.js index ba8dacb..ea50364 100644 --- a/index.js +++ b/index.js @@ -197,11 +197,11 @@ const ContainerService = { "queryIdentities", "getIdentity", "createIdentity", - "removeIdentity", "updateIdentity", "getPreference", "sendTelemetryPayload", "getTheme", + "forgetIdentityAndRefresh", "checkIncompatibleAddons" ]; @@ -309,7 +309,7 @@ const ContainerService = { }, registerBackgroundConnection(api) { - // This is only used for theme and container deletion notifications + // This is only used for theme notifications api.browser.runtime.onConnect.addListener((port) => { this._onBackgroundConnectCallback = (message, topic) => { port.postMessage({ @@ -967,30 +967,6 @@ const ContainerService = { }); }, - removeIdentity(args) { - const eventName = "delete-container"; - if (!("userContextId" in args)) { - return Promise.reject("removeIdentity must be called with userContextId argument."); - } - - this.sendTelemetryPayload({ - "event": eventName, - "userContextId": args.userContextId - }); - - const tabsToClose = []; - this._containerTabIterator(args.userContextId, tab => { - tabsToClose.push(tab); - }); - - return this._closeTabs(tabsToClose).then(() => { - const removed = ContextualIdentityProxy.remove(args.userContextId); - this.triggerBackgroundCallback({userContextId: args.userContextId}, eventName); - this._forgetIdentity(args.userContextId); - return this._refreshNeeded().then(() => removed ); - }); - }, - // Preferences getPreference(args) { @@ -1169,6 +1145,11 @@ const ContainerService = { // End-Of-Hack }, + forgetIdentityAndRefresh(args) { + this._forgetIdentity(args.userContextId); + return this._refreshNeeded(); + }, + _forgetIdentity(userContextId = 0) { for (let window of windows.browserWindows) { // eslint-disable-line prefer-const window = viewFor(window); diff --git a/webextension/background.js b/webextension/background.js index a3eac44..5c541d9 100644 --- a/webextension/background.js +++ b/webextension/background.js @@ -59,18 +59,25 @@ const assignManager = { }, init() { - browser.runtime.onMessage.addListener((neverAskMessage) => { - const pageUrl = neverAskMessage.pageUrl; - if (neverAskMessage.neverAsk === true) { - // If we have existing data and for some reason it hasn't been deleted etc lets update it - this.storageArea.get(pageUrl).then((siteSettings) => { - if (siteSettings) { - siteSettings.neverAsk = true; - this.storageArea.set(pageUrl, siteSettings); + browser.runtime.onMessage.addListener((m) => { + switch (m.type) { + case "delete-container": + assignManager.deleteContainer(m.message.userContextId); + break; + case "never-ask": + const pageUrl = m.pageUrl; + if (m.neverAsk === true) { + // If we have existing data and for some reason it hasn't been deleted etc lets update it + this.storageArea.get(pageUrl).then((siteSettings) => { + if (siteSettings) { + siteSettings.neverAsk = true; + this.storageArea.set(pageUrl, siteSettings); + } + }).catch((e) => { + throw e; + }); } - }).catch((e) => { - throw e; - }); + break; } }); @@ -254,9 +261,6 @@ const messageHandler = { case "lightweight-theme-changed": themeManager.update(m.message); break; - case "delete-container": - assignManager.deleteContainer(m.message.userContextId); - break; default: throw new Error(`Unhandled message type: ${m.message}`); } diff --git a/webextension/js/confirm-page.js b/webextension/js/confirm-page.js index feefba1..319afec 100644 --- a/webextension/js/confirm-page.js +++ b/webextension/js/confirm-page.js @@ -9,6 +9,7 @@ document.getElementById("redirect-form").addEventListener("submit", (e) => { // Sending neverAsk message to background to store for next time we see this process if (neverAsk) { browser.runtime.sendMessage({ + type: "never-ask", neverAsk: true, pageUrl: redirectUrl }).then(() => { diff --git a/webextension/js/popup.js b/webextension/js/popup.js index 7ed2b50..1be3406 100644 --- a/webextension/js/popup.js +++ b/webextension/js/popup.js @@ -141,6 +141,58 @@ const Logic = { return this._currentIdentity; }, + cookieStoreId(userContextId) { + return `firefox-container-${userContextId}`; + }, + + _containerTabIterator(userContextId, cb) { + browser.tabs.query({ + cookieStoreId: Logic.cookieStoreId(userContextId) + }).then((tabs) => { + tabs.forEach((tab) => { + cb(tab); + }); + }); + }, + + _containers(userContextId) { + return browser.tabs.query({ + cookieStoreId: Logic.cookieStoreId(userContextId) + }); + }, + + removeIdentity(userContextId) { + const eventName = "delete-container"; + if (!userContextId) { + return Promise.reject("removeIdentity must be called with userContextId argument."); + } + + browser.runtime.sendMessage({ + method: "sendTelemetryPayload", + event: eventName, + userContextId + }); + + const removeTabsPromise = Logic._containers(userContextId).then((tabs) => { + const tabIds = tabs.map((tab) => tab.id); + return browser.tabs.remove(tabIds); + }); + + return removeTabsPromise.then(() => { + const removed = browser.contextualIdentities.remove(Logic.cookieStoreId(userContextId)); + // Send delete event to webextension/background.js + browser.runtime.sendMessage({ + type: eventName, + message: {userContextId} + }); + browser.runtime.sendMessage({ + method: "forgetIdentityAndRefresh" + }).then(() => { + return removed; + }); + }); + }, + generateIdentityName() { const defaultName = "Container #"; const ids = []; @@ -603,10 +655,7 @@ Logic.registerPanel(P_CONTAINER_DELETE, { }); document.querySelector("#delete-container-ok-link").addEventListener("click", () => { - browser.runtime.sendMessage({ - method: "removeIdentity", - userContextId: Logic.currentIdentity().userContextId, - }).then(() => { + Logic.removeIdentity(Logic.currentIdentity().userContextId).then(() => { return Logic.refreshIdentities(); }).then(() => { Logic.showPreviousPanel(); diff --git a/webextension/manifest.json b/webextension/manifest.json index d8e094a..a9582a7 100644 --- a/webextension/manifest.json +++ b/webextension/manifest.json @@ -23,6 +23,7 @@ "activeTab", "cookies", "contextMenus", + "contextualIdentities", "history", "idle", "notifications", From f2ddc7fd847107854bd5cd876036d8df871fd7f1 Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Fri, 28 Apr 2017 15:39:05 +0100 Subject: [PATCH 05/21] Moving create and update containers into popup.js --- index.js | 63 ++------------------------------------ webextension/background.js | 38 +++++++++++++---------- webextension/js/popup.js | 61 +++++++++++++++++++++++++++--------- 3 files changed, 70 insertions(+), 92 deletions(-) diff --git a/index.js b/index.js index ea50364..2bc59e1 100644 --- a/index.js +++ b/index.js @@ -196,11 +196,10 @@ const ContainerService = { "moveTabsToWindow", "queryIdentities", "getIdentity", - "createIdentity", - "updateIdentity", "getPreference", "sendTelemetryPayload", "getTheme", + "refreshNeeded", "forgetIdentityAndRefresh", "checkIncompatibleAddons" ]; @@ -911,62 +910,6 @@ const ContainerService = { return Promise.resolve(identity ? this._convert(identity) : null); }, - createIdentity(args) { - this.sendTelemetryPayload({ - "event": "add-container", - }); - - for (let arg of [ "name", "color", "icon"]) { // eslint-disable-line prefer-const - if (!(arg in args)) { - return Promise.reject("createIdentity must be called with " + arg + " argument."); - } - } - - const color = this._fromNameToColor(args.color); - const icon = this._fromNameToIcon(args.icon); - - const identity = ContextualIdentityProxy.create(args.name, icon, color); - - this._identitiesState[identity.userContextId] = this._createIdentityState(); - - this._refreshNeeded().then(() => { - return this._convert(identity); - }).catch(() => { - return this._convert(identity); - }); - }, - - updateIdentity(args) { - if (!("userContextId" in args)) { - return Promise.reject("updateIdentity must be called with userContextId argument."); - } - - this.sendTelemetryPayload({ - "event": "edit-container", - "userContextId": args.userContextId - }); - - const identity = ContextualIdentityProxy.getIdentityFromId(args.userContextId); - for (let arg of [ "name", "color", "icon"]) { // eslint-disable-line prefer-const - if ((arg in args)) { - identity[arg] = args[arg]; - } - } - - const color = this._fromNameToColor(identity.color); - const icon = this._fromNameToIcon(identity.icon); - - const updated = ContextualIdentityProxy.update(args.userContextId, - identity.name, - icon, color); - - this._refreshNeeded().then(() => { - return updated; - }).catch(() => { - return updated; - }); - }, - // Preferences getPreference(args) { @@ -1015,7 +958,7 @@ const ContainerService = { return this._windowMap.get(window); }, - _refreshNeeded() { + refreshNeeded() { return this._configureWindows(); }, @@ -1147,7 +1090,7 @@ const ContainerService = { forgetIdentityAndRefresh(args) { this._forgetIdentity(args.userContextId); - return this._refreshNeeded(); + return this.refreshNeeded(); }, _forgetIdentity(userContextId = 0) { diff --git a/webextension/background.js b/webextension/background.js index 5c541d9..6a85cbc 100644 --- a/webextension/background.js +++ b/webextension/background.js @@ -58,26 +58,30 @@ const assignManager = { } }, + _neverAsk(m) { + const pageUrl = m.pageUrl; + if (m.neverAsk === true) { + // If we have existing data and for some reason it hasn't been deleted etc lets update it + this.storageArea.get(pageUrl).then((siteSettings) => { + if (siteSettings) { + siteSettings.neverAsk = true; + this.storageArea.set(pageUrl, siteSettings); + } + }).catch((e) => { + throw e; + }); + } + }, + init() { browser.runtime.onMessage.addListener((m) => { switch (m.type) { - case "delete-container": - assignManager.deleteContainer(m.message.userContextId); - break; - case "never-ask": - const pageUrl = m.pageUrl; - if (m.neverAsk === true) { - // If we have existing data and for some reason it hasn't been deleted etc lets update it - this.storageArea.get(pageUrl).then((siteSettings) => { - if (siteSettings) { - siteSettings.neverAsk = true; - this.storageArea.set(pageUrl, siteSettings); - } - }).catch((e) => { - throw e; - }); - } - break; + case "delete-container": + assignManager.deleteContainer(m.message.userContextId); + break; + case "never-ask": + this._neverAsk(m); + break; } }); diff --git a/webextension/js/popup.js b/webextension/js/popup.js index 1be3406..c3a23c4 100644 --- a/webextension/js/popup.js +++ b/webextension/js/popup.js @@ -152,7 +152,7 @@ const Logic = { tabs.forEach((tab) => { cb(tab); }); - }); + }).catch((e) => {throw e;}); }, _containers(userContextId) { @@ -161,14 +161,21 @@ const Logic = { }); }, + sendTelemetryPayload(message = {}) { + if (!message.event) { + throw new Error("Missing event name for telemetry"); + } + message.method = "sendTelemetryPayload"; + browser.runtime.sendMessage(message); + }, + removeIdentity(userContextId) { const eventName = "delete-container"; if (!userContextId) { return Promise.reject("removeIdentity must be called with userContextId argument."); } - browser.runtime.sendMessage({ - method: "sendTelemetryPayload", + this.sendTelemetryPayload({ event: eventName, userContextId }); @@ -189,7 +196,7 @@ const Logic = { method: "forgetIdentityAndRefresh" }).then(() => { return removed; - }); + }).catch((e) => {throw e;}); }); }, @@ -292,8 +299,7 @@ Logic.registerPanel(P_CONTAINERS_LIST, { }); document.querySelector("#edit-containers-link").addEventListener("click", () => { - browser.runtime.sendMessage({ - method: "sendTelemetryPayload", + Logic.sendTelemetryPayload({ event: "edit-containers" }); Logic.showPanel(P_CONTAINERS_EDIT); @@ -412,12 +418,12 @@ Logic.registerPanel(P_CONTAINER_INFO, { moveTabsEl.parentNode.insertBefore(fragment, moveTabsEl.nextSibling); } else { moveTabsEl.addEventListener("click", () => { - return browser.runtime.sendMessage({ + browser.runtime.sendMessage({ method: "moveTabsToWindow", userContextId: Logic.currentIdentity().userContextId, }).then(() => { window.close(); - }); + }).catch((e) => { throw e; }); }); } }).catch(() => { @@ -583,13 +589,14 @@ Logic.registerPanel(P_CONTAINER_EDIT, { _submitForm() { const identity = Logic.currentIdentity(); const formValues = new FormData(this._editForm); - browser.runtime.sendMessage({ - method: identity.userContextId ? "updateIdentity" : "createIdentity", - userContextId: identity.userContextId || 0, - name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(), - icon: formValues.get("container-icon") || DEFAULT_ICON, - color: formValues.get("container-color") || DEFAULT_COLOR, - }).then(() => { + this._createOrUpdateIdentity( + { + name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(), + icon: formValues.get("container-icon") || DEFAULT_ICON, + color: formValues.get("container-color") || DEFAULT_COLOR, + }, + identity.userContextId || false + ).then(() => { return Logic.refreshIdentities(); }).then(() => { Logic.showPreviousPanel(); @@ -598,6 +605,30 @@ Logic.registerPanel(P_CONTAINER_EDIT, { }); }, + _createOrUpdateIdentity(params, userContextId) { + let donePromise; + if (userContextId) { + donePromise = browser.contextualIdentities.update( + Logic.cookieStoreId(userContextId), + params + ); + Logic.sendTelemetryPayload({ + event: "edit-container", + userContextId + }); + } else { + donePromise = browser.contextualIdentities.create(params); + Logic.sendTelemetryPayload({ + event: "add-container" + }); + } + return donePromise.then(() => { + browser.runtime.sendMessage({ + method: "refreshNeeded" + }); + }); + }, + initializeRadioButtons() { const colorRadioTemplate = (containerColor) => { return escaped` From 337dee20612eb62e3a17bae1a890c82d33b2c45f Mon Sep 17 00:00:00 2001 From: groovecoder Date: Thu, 4 May 2017 09:31:25 -0500 Subject: [PATCH 06/21] more descriptive _containerTabs method name --- webextension/js/popup.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webextension/js/popup.js b/webextension/js/popup.js index c3a23c4..4dbc0c0 100644 --- a/webextension/js/popup.js +++ b/webextension/js/popup.js @@ -155,7 +155,7 @@ const Logic = { }).catch((e) => {throw e;}); }, - _containers(userContextId) { + _containerTabs(userContextId) { return browser.tabs.query({ cookieStoreId: Logic.cookieStoreId(userContextId) }); @@ -180,7 +180,7 @@ const Logic = { userContextId }); - const removeTabsPromise = Logic._containers(userContextId).then((tabs) => { + const removeTabsPromise = Logic._containerTabs(userContextId).then((tabs) => { const tabIds = tabs.map((tab) => tab.id); return browser.tabs.remove(tabIds); }); From 8f80b527f5883adbdc8c3b70183a84754f1e151a Mon Sep 17 00:00:00 2001 From: groovecoder Date: Fri, 5 May 2017 13:46:58 -0500 Subject: [PATCH 07/21] start onboarding-panel-4 for site assignments --- webextension/js/popup.js | 28 ++++++++++++++++++++++++++-- webextension/popup.html | 7 +++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/webextension/js/popup.js b/webextension/js/popup.js index 7ed2b50..71a123f 100644 --- a/webextension/js/popup.js +++ b/webextension/js/popup.js @@ -12,6 +12,7 @@ const DEFAULT_ICON = "circle"; const P_ONBOARDING_1 = "onboarding1"; const P_ONBOARDING_2 = "onboarding2"; const P_ONBOARDING_3 = "onboarding3"; +const P_ONBOARDING_4 = "onboarding4"; const P_CONTAINERS_LIST = "containersList"; const P_CONTAINERS_EDIT = "containersEdit"; const P_CONTAINER_INFO = "containerInfo"; @@ -69,8 +70,10 @@ const Logic = { // Routing to the correct panel. .then(() => { // If localStorage is disabled, we don't show the onboarding. - if (!localStorage || localStorage.getItem("onboarded3")) { + if (!localStorage || localStorage.getItem("onboarded4")) { this.showPanel(P_CONTAINERS_LIST); + } else if (localStorage.getItem("onboarded3")) { + this.showPanel(P_ONBOARDING_4); } else if (localStorage.getItem("onboarded2")) { this.showPanel(P_ONBOARDING_3); } else if (localStorage.getItem("onboarded1")) { @@ -215,8 +218,29 @@ Logic.registerPanel(P_ONBOARDING_3, { // This method is called when the object is registered. initialize() { // Let's move to the containers list panel. - document.querySelector("#onboarding-done-button").addEventListener("click", () => { + document.querySelector("#onboarding-almost-done-button").addEventListener("click", () => { localStorage.setItem("onboarded3", true); + Logic.showPanel(P_ONBOARDING_4); + }); + }, + + // This method is called when the panel is shown. + prepare() { + return Promise.resolve(null); + }, +}); + +// P_ONBOARDING_4: Fourth page for Onboarding. +// ---------------------------------------------------------------------------- + +Logic.registerPanel(P_ONBOARDING_4, { + panelSelector: ".onboarding-panel-4", + + // This method is called when the object is registered. + initialize() { + // Let's move to the containers list panel. + document.querySelector("#onboarding-done-button").addEventListener("click", () => { + localStorage.setItem("onboarded4", true); Logic.showPanel(P_CONTAINERS_LIST); }); }, diff --git a/webextension/popup.html b/webextension/popup.html index fafc66d..5264b11 100644 --- a/webextension/popup.html +++ b/webextension/popup.html @@ -27,6 +27,13 @@ How Containers Work

A place for everything, and everything in its place.

Start with the containers we've created, or create your own.

+ Next + + +
+ How to assign sites to containers +

Assign your favorite sites to containers..

+

Assign your favorite sites to automatically open in containers.

Done
From 1ec86c7fd2b308e55165d2376fb825a5a92c6950 Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Mon, 8 May 2017 14:32:35 +0100 Subject: [PATCH 08/21] Moving remove, add and update code into the web extension background for stability --- webextension/background.js | 114 ++++++++++++++++++++++++++------ webextension/js/confirm-page.js | 2 +- webextension/js/popup.js | 94 ++++++-------------------- 3 files changed, 113 insertions(+), 97 deletions(-) diff --git a/webextension/background.js b/webextension/background.js index 6a85cbc..93bdd78 100644 --- a/webextension/background.js +++ b/webextension/background.js @@ -74,17 +74,6 @@ const assignManager = { }, init() { - browser.runtime.onMessage.addListener((m) => { - switch (m.type) { - case "delete-container": - assignManager.deleteContainer(m.message.userContextId); - break; - case "never-ask": - this._neverAsk(m); - break; - } - }); - browser.contextMenus.onClicked.addListener((info, tab) => { const userContextId = this.getUserContextIdFromCookieStore(tab); // Mapping ${URL(info.pageUrl).hostname} to ${userContextId} @@ -108,8 +97,7 @@ const assignManager = { message: `Successfully ${actionName} site to always open in this container`, iconUrl: browser.extension.getURL("/img/onboarding-1.png") }); - browser.runtime.sendMessage({ - method: "sendTelemetryPayload", + backgroundLogic.sendTelemetryPayload({ event: `${actionName}-container-assignment`, userContextId: userContextId, }); @@ -229,14 +217,12 @@ const assignManager = { // 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: `firefox-container-${userContextId}`, index}); - browser.runtime.sendMessage({ - method: "sendTelemetryPayload", + backgroundLogic.sendTelemetryPayload({ event: "auto-reload-page-in-container", userContextId: userContextId, }); } else { - browser.runtime.sendMessage({ - method: "sendTelemetryPayload", + backgroundLogic.sendTelemetryPayload({ event: "prompt-to-reload-page-in-container", userContextId: userContextId, }); @@ -251,6 +237,77 @@ const assignManager = { } }; + +const backgroundLogic = { + deleteContainer(userContextId) { + this.sendTelemetryPayload({ + event: "delete-container", + userContextId + }); + + const removeTabsPromise = this._containerTabs(userContextId).then((tabs) => { + const tabIds = tabs.map((tab) => tab.id); + return browser.tabs.remove(tabIds); + }); + + return new Promise((resolve) => { + removeTabsPromise.then(() => { + const removed = browser.contextualIdentities.remove(this.cookieStoreId(userContextId)); + removed.then(() => { + assignManager.deleteContainer(userContextId); + browser.runtime.sendMessage({ + method: "forgetIdentityAndRefresh" + }).then(() => { + resolve({done: true, userContextId}); + }).catch((e) => {throw e;}); + }).catch((e) => {throw e;}); + }).catch((e) => {throw e;}); + }); + }, + + createOrUpdateContainer(options) { + let donePromise; + if (options.userContextId) { + donePromise = browser.contextualIdentities.update( + this.cookieStoreId(options.userContextId), + options.params + ); + this.sendTelemetryPayload({ + event: "edit-container", + userContextId: options.userContextId + }); + } else { + donePromise = browser.contextualIdentities.create(options.params); + this.sendTelemetryPayload({ + event: "add-container" + }); + } + return donePromise.then(() => { + browser.runtime.sendMessage({ + method: "refreshNeeded" + }); + }); + }, + + sendTelemetryPayload(message = {}) { + if (!message.event) { + throw new Error("Missing event name for telemetry"); + } + message.method = "sendTelemetryPayload"; + browser.runtime.sendMessage(message); + }, + + cookieStoreId(userContextId) { + return `firefox-container-${userContextId}`; + }, + + _containerTabs(userContextId) { + return browser.tabs.query({ + cookieStoreId: this.cookieStoreId(userContextId) + }).catch((e) => {throw e;}); + }, +}; + const messageHandler = { // After the timer completes we assume it's a tab the user meant to keep open // We use this to catch redirected tabs that have just opened @@ -258,6 +315,23 @@ const messageHandler = { LAST_CREATED_TAB_TIMER: 2000, init() { + browser.runtime.onMessage.addListener((m) => { + let response; + + switch (m.method) { + case "deleteContainer": + response = backgroundLogic.deleteContainer(m.message.userContextId); + break; + case "createOrUpdateContainer": + response = backgroundLogic.createOrUpdateContainer(m.message); + break; + case "neverAsk": + assignManager._neverAsk(m); + break; + } + return response; + }); + // Handles messages from index.js const port = browser.runtime.connect(); port.onMessage.addListener(m => { @@ -416,8 +490,7 @@ const tabPageCounter = { return; } if (why === "user-closed-tab" && this.counters[tabId].tab) { - browser.runtime.sendMessage({ - method: "sendTelemetryPayload", + backgroundLogic.sendTelemetryPayload({ event: "page-requests-completed-per-tab", userContextId: this.counters[tabId].tab.cookieStoreId, pageRequestCount: this.counters[tabId].tab.pageRequests @@ -426,8 +499,7 @@ const tabPageCounter = { // delete both the 'tab' and 'activity' counters delete this.counters[tabId]; } else if (why === "user-went-idle" && this.counters[tabId].activity) { - browser.runtime.sendMessage({ - method: "sendTelemetryPayload", + backgroundLogic.sendTelemetryPayload({ event: "page-requests-completed-per-activity", userContextId: this.counters[tabId].activity.cookieStoreId, pageRequestCount: this.counters[tabId].activity.pageRequests diff --git a/webextension/js/confirm-page.js b/webextension/js/confirm-page.js index 319afec..d54dd06 100644 --- a/webextension/js/confirm-page.js +++ b/webextension/js/confirm-page.js @@ -9,7 +9,7 @@ document.getElementById("redirect-form").addEventListener("submit", (e) => { // Sending neverAsk message to background to store for next time we see this process if (neverAsk) { browser.runtime.sendMessage({ - type: "never-ask", + method: "neverAsk", neverAsk: true, pageUrl: redirectUrl }).then(() => { diff --git a/webextension/js/popup.js b/webextension/js/popup.js index 4dbc0c0..68f5dc1 100644 --- a/webextension/js/popup.js +++ b/webextension/js/popup.js @@ -141,26 +141,6 @@ const Logic = { return this._currentIdentity; }, - cookieStoreId(userContextId) { - return `firefox-container-${userContextId}`; - }, - - _containerTabIterator(userContextId, cb) { - browser.tabs.query({ - cookieStoreId: Logic.cookieStoreId(userContextId) - }).then((tabs) => { - tabs.forEach((tab) => { - cb(tab); - }); - }).catch((e) => {throw e;}); - }, - - _containerTabs(userContextId) { - return browser.tabs.query({ - cookieStoreId: Logic.cookieStoreId(userContextId) - }); - }, - sendTelemetryPayload(message = {}) { if (!message.event) { throw new Error("Missing event name for telemetry"); @@ -170,33 +150,13 @@ const Logic = { }, removeIdentity(userContextId) { - const eventName = "delete-container"; if (!userContextId) { return Promise.reject("removeIdentity must be called with userContextId argument."); } - this.sendTelemetryPayload({ - event: eventName, - userContextId - }); - - const removeTabsPromise = Logic._containerTabs(userContextId).then((tabs) => { - const tabIds = tabs.map((tab) => tab.id); - return browser.tabs.remove(tabIds); - }); - - return removeTabsPromise.then(() => { - const removed = browser.contextualIdentities.remove(Logic.cookieStoreId(userContextId)); - // Send delete event to webextension/background.js - browser.runtime.sendMessage({ - type: eventName, - message: {userContextId} - }); - browser.runtime.sendMessage({ - method: "forgetIdentityAndRefresh" - }).then(() => { - return removed; - }).catch((e) => {throw e;}); + return browser.runtime.sendMessage({ + method: "deleteContainer", + message: {userContextId} }); }, @@ -589,14 +549,17 @@ Logic.registerPanel(P_CONTAINER_EDIT, { _submitForm() { const identity = Logic.currentIdentity(); const formValues = new FormData(this._editForm); - this._createOrUpdateIdentity( - { - name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(), - icon: formValues.get("container-icon") || DEFAULT_ICON, - color: formValues.get("container-color") || DEFAULT_COLOR, - }, - identity.userContextId || false - ).then(() => { + return browser.runtime.sendMessage({ + method: "createOrUpdateContainer", + message: { + userContextId: identity.userContextId || false, + params: { + name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(), + icon: formValues.get("container-icon") || DEFAULT_ICON, + color: formValues.get("container-color") || DEFAULT_COLOR, + } + } + }).then(() => { return Logic.refreshIdentities(); }).then(() => { Logic.showPreviousPanel(); @@ -605,30 +568,6 @@ Logic.registerPanel(P_CONTAINER_EDIT, { }); }, - _createOrUpdateIdentity(params, userContextId) { - let donePromise; - if (userContextId) { - donePromise = browser.contextualIdentities.update( - Logic.cookieStoreId(userContextId), - params - ); - Logic.sendTelemetryPayload({ - event: "edit-container", - userContextId - }); - } else { - donePromise = browser.contextualIdentities.create(params); - Logic.sendTelemetryPayload({ - event: "add-container" - }); - } - return donePromise.then(() => { - browser.runtime.sendMessage({ - method: "refreshNeeded" - }); - }); - }, - initializeRadioButtons() { const colorRadioTemplate = (containerColor) => { return escaped` @@ -686,6 +625,11 @@ Logic.registerPanel(P_CONTAINER_DELETE, { }); document.querySelector("#delete-container-ok-link").addEventListener("click", () => { + /* This promise wont resolve if the last tab was removed from the window. + as the message async callback stops listening, this isn't an issue for us however it might be in future + if you want to do anything post delete do it in the background script. + Browser console currently warns about not listening also. + */ Logic.removeIdentity(Logic.currentIdentity().userContextId).then(() => { return Logic.refreshIdentities(); }).then(() => { From 92ab56448c364e988d03fe44d1a640c46c4017ad Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Tue, 9 May 2017 13:29:08 +0100 Subject: [PATCH 09/21] Move to the Mozilla backed no-unsanitized ESLint plugin --- .eslintrc.js | 22 ++++++++++++++++------ package.json | 2 +- webextension/js/popup.js | 4 ++-- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index d44e921..68705ff 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,9 +5,6 @@ module.exports = { "node": true, "webextensions": true }, - "extends": [ - "eslint:recommended" - ], "globals": { "CustomizableUI": true, "CustomizableWidgets": true, @@ -16,7 +13,10 @@ module.exports = { }, "plugins": [ "promise", - "no-unescaped" + "no-unsanitized" + ], + "extends": [ + "eslint:recommended" ], "root": true, "rules": { @@ -29,8 +29,18 @@ module.exports = { "promise/no-promise-in-callback": "warn", "promise/no-return-wrap": "error", "promise/param-names": "error", - "no-unescaped/no-key-assignment": "error", - "no-unescaped/enforce": "error", + + "no-unsanitized/method": [ + "error" + ], + "no-unsanitized/property": [ + "error", + { + "escape": { + "taggedTemplates": ["escaped"] + } + } + ], "eqeqeq": "error", "indent": ["error", 2], diff --git a/package.json b/package.json index 8fa61ea..30d562f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "addons-linter": "^0.15.14", "deploy-txp": "^1.0.7", "eslint": "^3.17.1", - "eslint-plugin-no-unescaped": "^1.1.0", + "eslint-plugin-no-unsanitized": "^2.0.0", "eslint-plugin-promise": "^3.4.0", "htmllint-cli": "^0.0.5", "jpm": "^1.2.2", diff --git a/webextension/js/popup.js b/webextension/js/popup.js index 7ed2b50..ab347ed 100644 --- a/webextension/js/popup.js +++ b/webextension/js/popup.js @@ -555,7 +555,7 @@ Logic.registerPanel(P_CONTAINER_EDIT, { const colorRadioFieldset = document.getElementById("edit-container-panel-choose-color"); colors.forEach((containerColor) => { const templateInstance = document.createElement("span"); - // eslint-disable-next-line no-unescaped/enforce + // eslint-disable-next-line no-unsanitized/property templateInstance.innerHTML = colorRadioTemplate(containerColor); colorRadioFieldset.appendChild(templateInstance); }); @@ -568,7 +568,7 @@ Logic.registerPanel(P_CONTAINER_EDIT, { const iconRadioFieldset = document.getElementById("edit-container-panel-choose-icon"); icons.forEach((containerIcon) => { const templateInstance = document.createElement("span"); - // eslint-disable-next-line no-unescaped/enforce + // eslint-disable-next-line no-unsanitized/property templateInstance.innerHTML = iconRadioTemplate(containerIcon); iconRadioFieldset.appendChild(templateInstance); }); From 4a48a7debbc9f61e49c136bfd28776f7fa7492bf Mon Sep 17 00:00:00 2001 From: groovecoder Date: Thu, 11 May 2017 11:45:39 -0500 Subject: [PATCH 10/21] show 'NEW' icon badge for major upgrades --- index.js | 5 +++++ webextension/background.js | 7 +++++++ webextension/img/onboarding-4.png | Bin 0 -> 48741 bytes webextension/js/popup.js | 4 ++++ webextension/popup.html | 4 ++-- 5 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 webextension/img/onboarding-4.png diff --git a/index.js b/index.js index ba8dacb..43e1b13 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,8 @@ const DEFAULT_TAB = "about:newtab"; const SHOW_MENU_TIMEOUT = 100; const HIDE_MENU_TIMEOUT = 300; +const MAJOR_VERSIONS = ["2.2.1"]; + const INCOMPATIBLE_ADDON_IDS = [ "pulse@mozilla.com", "snoozetabs@mozilla.com", @@ -256,6 +258,9 @@ const ContainerService = { webExtension.startup().then(api => { api.browser.runtime.onMessage.addListener((message, sender, sendReply) => { + if (message.method === "checkForMajorUpgrade") { + sendReply(reason === "upgrade" && MAJOR_VERSIONS.indexOf(self.version) > -1); + } if ("method" in message && methods.indexOf(message.method) !== -1) { sendReply(this[message.method](message)); } diff --git a/webextension/background.js b/webextension/background.js index a3eac44..4f547c5 100644 --- a/webextension/background.js +++ b/webextension/background.js @@ -467,6 +467,13 @@ browser.runtime.sendMessage({ } }).catch(() => {}); +browser.runtime.sendMessage({method: "checkForMajorUpgrade"}).then(upgrading=> { + if (upgrading) { + browser.browserAction.setBadgeBackgroundColor({color: "rgba(0,217,0,255)"}); + browser.browserAction.setBadgeText({text: "NEW"}); + } +}).catch((e) => { throw e;}); + function disableAddon(tabId) { browser.browserAction.disable(tabId); browser.browserAction.setTitle({ tabId, title: "Containers disabled in Private Browsing Mode" }); diff --git a/webextension/img/onboarding-4.png b/webextension/img/onboarding-4.png new file mode 100644 index 0000000000000000000000000000000000000000..96e5068dc32a8f2bcb748a4618809a70c1f51c49 GIT binary patch literal 48741 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�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 How to assign sites to containers -

Assign your favorite sites to containers..

-

Assign your favorite sites to automatically open in containers.

+

Always open sites in the containers you want.

+

Right-click inside a container tab to assign the site to always open in the container.

Done From 3805f12e17ad00ff9e2c702eddd15703900a67ae Mon Sep 17 00:00:00 2001 From: groovecoder Date: Mon, 15 May 2017 16:22:35 -0500 Subject: [PATCH 11/21] Logic.clearBrowserActionBadge method --- webextension/js/popup.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/webextension/js/popup.js b/webextension/js/popup.js index c079c09..a8fcf99 100644 --- a/webextension/js/popup.js +++ b/webextension/js/popup.js @@ -65,8 +65,7 @@ const Logic = { init() { // Remove browserAction "upgraded" badge when opening panel - browser.browserAction.setBadgeBackgroundColor({color: ""}); - browser.browserAction.setBadgeText({text: ""}); + this.clearBrowserActionBadge(); // Retrieve the list of identities. this.refreshIdentities() @@ -92,6 +91,11 @@ const Logic = { }); }, + clearBrowserActionBadge() { + browser.browserAction.setBadgeBackgroundColor({color: ""}); + browser.browserAction.setBadgeText({text: ""}); + }, + refreshIdentities() { return browser.runtime.sendMessage({ method: "queryIdentities" From 02300630f6644f9277d213f747c6a11a6763ee27 Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Tue, 16 May 2017 11:04:23 +0100 Subject: [PATCH 12/21] Undefined storage area. Fixes #508 --- index.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/index.js b/index.js index 2bc59e1..b81402f 100644 --- a/index.js +++ b/index.js @@ -124,7 +124,8 @@ const ContainerService = { // uninstallation. This object contains also a version number, in case we // need to implement a migration in the future. // In 1.1.1 and less we deleted savedConfiguration on upgrade so we need to rebuild - if (!ss.storage.savedConfiguration || + if (!("savedConfiguration" in ss.storage) || + !("prefs" in ss.storage.savedConfiguration) || (installation && reason !== "upgrade")) { let preInstalledIdentities = []; // eslint-disable-line prefer-const ContextualIdentityProxy.getIdentities().forEach(identity => { @@ -144,8 +145,8 @@ const ContainerService = { ss.storage.savedConfiguration = object; - // Maybe rename the Banking container. if (prefService.get("privacy.userContext.enabled") !== true) { + // Maybe rename the Banking container. const identity = ContextualIdentityProxy.getIdentityFromId(3); if (identity && identity.l10nID === "userContextBanking.label") { ContextualIdentityProxy.update(identity.userContextId, @@ -153,17 +154,16 @@ const ContainerService = { identity.icon, identity.color); } - } - // Let's create the default containers in case there are none. - if (prefService.get("privacy.userContext.enabled") !== true && - ss.storage.savedConfiguration.preInstalledIdentities.length === 0) { - // Note: we have to create them in this way because there is no way to - // reuse the same ID and the localized strings. - ContextualIdentityService.create("Personal", "fingerprint", "blue"); - ContextualIdentityService.create("Work", "briefcase", "orange"); - ContextualIdentityService.create("Finance", "dollar", "green"); - ContextualIdentityService.create("Shopping", "cart", "pink"); + // Let's create the default containers in case there are none. + if (ss.storage.savedConfiguration.preInstalledIdentities.length === 0) { + // Note: we have to create them in this way because there is no way to + // reuse the same ID and the localized strings. + ContextualIdentityService.create("Personal", "fingerprint", "blue"); + ContextualIdentityService.create("Work", "briefcase", "orange"); + ContextualIdentityService.create("Finance", "dollar", "green"); + ContextualIdentityService.create("Shopping", "cart", "pink"); + } } } From d8fd47a353eaa62a05b8c0102c5ecb65fe149b55 Mon Sep 17 00:00:00 2001 From: groovecoder Date: Tue, 16 May 2017 12:20:10 -0500 Subject: [PATCH 13/21] move all badge logic into WebExtension code --- index.js | 5 ----- webextension/background.js | 28 +++++++++++++++++++++------- webextension/js/popup.js | 17 +++++++++++++++-- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/index.js b/index.js index 43e1b13..ba8dacb 100644 --- a/index.js +++ b/index.js @@ -8,8 +8,6 @@ const DEFAULT_TAB = "about:newtab"; const SHOW_MENU_TIMEOUT = 100; const HIDE_MENU_TIMEOUT = 300; -const MAJOR_VERSIONS = ["2.2.1"]; - const INCOMPATIBLE_ADDON_IDS = [ "pulse@mozilla.com", "snoozetabs@mozilla.com", @@ -258,9 +256,6 @@ const ContainerService = { webExtension.startup().then(api => { api.browser.runtime.onMessage.addListener((message, sender, sendReply) => { - if (message.method === "checkForMajorUpgrade") { - sendReply(reason === "upgrade" && MAJOR_VERSIONS.indexOf(self.version) > -1); - } if ("method" in message && methods.indexOf(message.method) !== -1) { sendReply(this[message.method](message)); } diff --git a/webextension/background.js b/webextension/background.js index 4f547c5..d1036c0 100644 --- a/webextension/background.js +++ b/webextension/background.js @@ -1,3 +1,5 @@ +const MAJOR_VERSIONS = ["2.3.0"]; + const assignManager = { CLOSEABLE_WINDOWS: new Set([ "about:startpage", @@ -467,14 +469,26 @@ browser.runtime.sendMessage({ } }).catch(() => {}); -browser.runtime.sendMessage({method: "checkForMajorUpgrade"}).then(upgrading=> { - if (upgrading) { - browser.browserAction.setBadgeBackgroundColor({color: "rgba(0,217,0,255)"}); - browser.browserAction.setBadgeText({text: "NEW"}); - } -}).catch((e) => { throw e;}); - function disableAddon(tabId) { browser.browserAction.disable(tabId); browser.browserAction.setTitle({ tabId, title: "Containers disabled in Private Browsing Mode" }); } + +async function getExtensionInfo() { + const manifestPath = browser.extension.getURL("manifest.json"); + const response = await fetch(manifestPath); + const extensionInfo = await response.json(); + return extensionInfo; +} + +async function displayBrowserActionBadge() { + const extensionInfo = await getExtensionInfo(); + const storage = await browser.storage.local.get({browserActionBadgesClicked: []}); + + if (MAJOR_VERSIONS.indexOf(extensionInfo.version) > -1 && + storage.browserActionBadgesClicked.indexOf(extensionInfo.version) < 0) { + browser.browserAction.setBadgeBackgroundColor({color: "rgba(0,217,0,255)"}); + browser.browserAction.setBadgeText({text: "NEW"}); + } +} +displayBrowserActionBadge(); diff --git a/webextension/js/popup.js b/webextension/js/popup.js index a8fcf99..11e90ed 100644 --- a/webextension/js/popup.js +++ b/webextension/js/popup.js @@ -55,6 +55,13 @@ function escaped(strings, ...values) { return result.join(""); } +async function getExtensionInfo() { + const manifestPath = browser.extension.getURL("manifest.json"); + const response = await fetch(manifestPath); + const extensionInfo = await response.json(); + return extensionInfo; +} + // This object controls all the panels, identities and many other things. const Logic = { _identities: [], @@ -92,8 +99,14 @@ const Logic = { }, clearBrowserActionBadge() { - browser.browserAction.setBadgeBackgroundColor({color: ""}); - browser.browserAction.setBadgeText({text: ""}); + getExtensionInfo().then(extensionInfo=>{ + const storage = browser.storage.local.get({browserActionBadgesClicked: []}).then(storage=>{ + browser.browserAction.setBadgeBackgroundColor({color: ""}); + browser.browserAction.setBadgeText({text: ""}); + storage.browserActionBadgesClicked.push(extensionInfo.version); + browser.storage.local.set({browserActionBadgesClicked: storage.browserActionBadgesClicked}); + }); + }); }, refreshIdentities() { From ee980345726eef3f03401c24a52835cdfc6c4dd2 Mon Sep 17 00:00:00 2001 From: groovecoder Date: Tue, 16 May 2017 12:29:45 -0500 Subject: [PATCH 14/21] ecmaVersion 8 for eslint fixes --- .eslintrc.js | 3 +++ webextension/js/popup.js | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index d44e921..9f89f70 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,7 @@ module.exports = { + "parserOptions": { + "ecmaVersion": 8 + }, "env": { "browser": true, "es6": true, diff --git a/webextension/js/popup.js b/webextension/js/popup.js index 11e90ed..6b5b68b 100644 --- a/webextension/js/popup.js +++ b/webextension/js/popup.js @@ -100,13 +100,13 @@ const Logic = { clearBrowserActionBadge() { getExtensionInfo().then(extensionInfo=>{ - const storage = browser.storage.local.get({browserActionBadgesClicked: []}).then(storage=>{ + browser.storage.local.get({browserActionBadgesClicked: []}).then(storage=>{ browser.browserAction.setBadgeBackgroundColor({color: ""}); browser.browserAction.setBadgeText({text: ""}); storage.browserActionBadgesClicked.push(extensionInfo.version); browser.storage.local.set({browserActionBadgesClicked: storage.browserActionBadgesClicked}); - }); - }); + }).catch(e=>{throw e;}); + }).catch(e=>{throw e;}); }, refreshIdentities() { From 54ccf5b9ec73a420adea1b5085d956a5ea95fd4a Mon Sep 17 00:00:00 2001 From: groovecoder Date: Tue, 16 May 2017 13:01:59 -0500 Subject: [PATCH 15/21] async/await in popup code too --- webextension/js/popup.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/webextension/js/popup.js b/webextension/js/popup.js index 6b5b68b..086e458 100644 --- a/webextension/js/popup.js +++ b/webextension/js/popup.js @@ -98,15 +98,13 @@ const Logic = { }); }, - clearBrowserActionBadge() { - getExtensionInfo().then(extensionInfo=>{ - browser.storage.local.get({browserActionBadgesClicked: []}).then(storage=>{ - browser.browserAction.setBadgeBackgroundColor({color: ""}); - browser.browserAction.setBadgeText({text: ""}); - storage.browserActionBadgesClicked.push(extensionInfo.version); - browser.storage.local.set({browserActionBadgesClicked: storage.browserActionBadgesClicked}); - }).catch(e=>{throw e;}); - }).catch(e=>{throw e;}); + async clearBrowserActionBadge() { + const extensionInfo = await getExtensionInfo(); + const storage = await browser.storage.local.get({browserActionBadgesClicked: []}); + browser.browserAction.setBadgeBackgroundColor({color: ""}); + browser.browserAction.setBadgeText({text: ""}); + storage.browserActionBadgesClicked.push(extensionInfo.version); + browser.storage.local.set({browserActionBadgesClicked: storage.browserActionBadgesClicked}); }, refreshIdentities() { From 4a97e07d43b5510a8408270cc48e77d678026542 Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Tue, 16 May 2017 04:28:55 +0100 Subject: [PATCH 16/21] Adding in shortcut and keyboard controls --- webextension/css/popup.css | 29 ++++++++----- webextension/js/popup.js | 86 ++++++++++++++++++++++++++++---------- webextension/manifest.json | 9 ++++ webextension/popup.html | 18 ++++---- 4 files changed, 101 insertions(+), 41 deletions(-) diff --git a/webextension/css/popup.css b/webextension/css/popup.css index dd460d3..607563c 100644 --- a/webextension/css/popup.css +++ b/webextension/css/popup.css @@ -130,11 +130,13 @@ table { color: white; } -.button.primary:hover { +.button.primary:hover, +.button.primary:focus { background-color: #0675d3; } -.button.secondary:hover { +.button.secondary:hover, +.button.secondary:focus { background-color: rgba(0, 0, 0, 0.05); } @@ -198,7 +200,8 @@ table { justify-content: center; } -.panel-back-arrow:hover { +.panel-back-arrow:hover, +.panel-back-arrow:focus { background: #dedede; } @@ -249,7 +252,8 @@ table { transition: background-color 75ms; } -.onboarding-button:hover { +.onboarding-button:hover, +.onboarding-button:active { background-color: #0675d3; } @@ -264,12 +268,14 @@ manage things like container crud */ } .pop-button:hover, +.pop-button:focus, +.panel-footer-secondary:focus, .panel-footer-secondary:hover { background-color: rgba(0, 0, 0, 0.05); } -.pop-button:active, -.panel-footer-secondary:active { +.pop-button:focus, +.panel-footer-secondary:focus { background-color: rgba(0, 0, 0, 0.08); } @@ -350,7 +356,8 @@ span ~ .panel-header-text { transition: background-color 75ms; } -.clickable.userContext-wrapper:hover { +.container-panel-row:hover .clickable.userContext-wrapper, +.container-panel-row:focus .clickable.userContext-wrapper { background: #f2f2f2; } @@ -360,7 +367,6 @@ span ~ .panel-header-text { } /* .userContext-icon is used natively, Bug 1333811 was raised to fix */ -.userContext-icon, .usercontext-icon { background-image: var(--identity-icon); background-position: center center; @@ -372,8 +378,8 @@ span ~ .panel-header-text { flex: 0 0 48px; } -.clickable:hover .userContext-icon, -.clickable:hover .usercontext-icon { +.container-panel-row:hover .clickable .usercontext-icon, +.container-panel-row:focus .clickable .usercontext-icon { background-image: url('/img/container-newtab.svg'); fill: 'gray'; filter: url('/img/filters.svg#fill'); @@ -482,7 +488,8 @@ span ~ .panel-header-text { cursor: pointer; } -.clickable:hover { +.clickable:hover, +.clickable:focus { background-color: #ebebeb; } diff --git a/webextension/js/popup.js b/webextension/js/popup.js index b1289ae..c1d14a3 100644 --- a/webextension/js/popup.js +++ b/webextension/js/popup.js @@ -107,6 +107,15 @@ const Logic = { browser.storage.local.set({browserActionBadgesClicked: storage.browserActionBadgesClicked}); }, + addEnterHandler(element, handler) { + element.addEventListener("click", handler); + element.addEventListener("keydown", (e) => { + if (e.keyCode === 13) { + handler(e); + } + }); + }, + refreshIdentities() { return browser.runtime.sendMessage({ method: "queryIdentities" @@ -214,7 +223,7 @@ Logic.registerPanel(P_ONBOARDING_1, { // This method is called when the object is registered. initialize() { // Let's move to the next panel. - document.querySelector("#onboarding-start-button").addEventListener("click", () => { + Logic.addEnterHandler(document.querySelector("#onboarding-start-button"), () => { localStorage.setItem("onboarded1", true); Logic.showPanel(P_ONBOARDING_2); }); @@ -235,7 +244,7 @@ Logic.registerPanel(P_ONBOARDING_2, { // This method is called when the object is registered. initialize() { // Let's move to the containers list panel. - document.querySelector("#onboarding-next-button").addEventListener("click", () => { + Logic.addEnterHandler(document.querySelector("#onboarding-next-button"), () => { localStorage.setItem("onboarded2", true); Logic.showPanel(P_ONBOARDING_3); }); @@ -256,7 +265,7 @@ Logic.registerPanel(P_ONBOARDING_3, { // This method is called when the object is registered. initialize() { // Let's move to the containers list panel. - document.querySelector("#onboarding-almost-done-button").addEventListener("click", () => { + Logic.addEnterHandler(document.querySelector("#onboarding-almost-done-button"), () => { localStorage.setItem("onboarded3", true); Logic.showPanel(P_ONBOARDING_4); }); @@ -297,18 +306,18 @@ Logic.registerPanel(P_CONTAINERS_LIST, { // This method is called when the object is registered. initialize() { - document.querySelector("#container-add-link").addEventListener("click", () => { + Logic.addEnterHandler(document.querySelector("#container-add-link"), () => { Logic.showPanel(P_CONTAINER_EDIT, { name: Logic.generateIdentityName() }); }); - document.querySelector("#edit-containers-link").addEventListener("click", () => { + Logic.addEnterHandler(document.querySelector("#edit-containers-link"), () => { Logic.sendTelemetryPayload({ event: "edit-containers" }); Logic.showPanel(P_CONTAINERS_EDIT); }); - document.querySelector("#sort-containers-link").addEventListener("click", () => { + Logic.addEnterHandler(document.querySelector("#sort-containers-link"), () => { browser.runtime.sendMessage({ method: "sortTabs" }).then(() => { @@ -317,6 +326,30 @@ Logic.registerPanel(P_CONTAINERS_LIST, { window.close(); }); }); + + document.addEventListener("keydown", (e) => { + const element = document.activeElement; + function next() { + const nextElement = element.nextElementSibling; + if (nextElement) { + nextElement.focus(); + } + } + function previous() { + const previousElement = element.previousElementSibling; + if (previousElement) { + previousElement.focus(); + } + } + switch (e.keyCode) { + case 40: + next(); + break; + case 38: + previous(); + break; + } + }); }, // This method is called when the panel is shown. @@ -330,11 +363,14 @@ Logic.registerPanel(P_CONTAINERS_LIST, { const manage = document.createElement("td"); tr.classList.add("container-panel-row"); + + tr.setAttribute("tabindex", "0"); + context.classList.add("userContext-wrapper", "open-newtab", "clickable"); manage.classList.add("show-tabs", "pop-button"); context.innerHTML = escaped`
-
@@ -351,8 +387,10 @@ Logic.registerPanel(P_CONTAINERS_LIST, { tr.appendChild(manage); } - tr.addEventListener("click", e => { - if (e.target.matches(".open-newtab") || e.target.parentNode.matches(".open-newtab")) { + Logic.addEnterHandler(tr, e => { + if (e.target.matches(".open-newtab") + || e.target.parentNode.matches(".open-newtab") + || e.type === "keydown") { browser.runtime.sendMessage({ method: "openTab", userContextId: identity.userContextId, @@ -372,6 +410,12 @@ Logic.registerPanel(P_CONTAINERS_LIST, { list.innerHTML = ""; list.appendChild(fragment); + /* Not sure why extensions require a focus for the doorhanger, + however it allows us to have a tabindex before the first selected item + */ + document.addEventListener("focus", () => { + list.querySelector("tr").focus(); + }); return Promise.resolve(); }, @@ -385,11 +429,11 @@ Logic.registerPanel(P_CONTAINER_INFO, { // This method is called when the object is registered. initialize() { - document.querySelector("#close-container-info-panel").addEventListener("click", () => { + Logic.addEnterHandler(document.querySelector("#close-container-info-panel"), () => { Logic.showPreviousPanel(); }); - document.querySelector("#container-info-hideorshow").addEventListener("click", () => { + Logic.addEnterHandler(document.querySelector("#container-info-hideorshow"), () => { const identity = Logic.currentIdentity(); browser.runtime.sendMessage({ method: identity.hasHiddenTabs ? "showTabs" : "hideTabs", @@ -420,7 +464,7 @@ Logic.registerPanel(P_CONTAINER_INFO, { moveTabsEl.parentNode.insertBefore(fragment, moveTabsEl.nextSibling); } else { - moveTabsEl.addEventListener("click", () => { + Logic.addEnterHandler(moveTabsEl, () => { browser.runtime.sendMessage({ method: "moveTabsToWindow", userContextId: Logic.currentIdentity().userContextId, @@ -483,7 +527,7 @@ Logic.registerPanel(P_CONTAINER_INFO, { // On click, we activate this tab. But only if this tab is active. if (tab.active) { tr.classList.add("clickable"); - tr.addEventListener("click", () => { + Logic.addEnterHandler(tr, () => { browser.runtime.sendMessage({ method: "showTab", tabId: tab.id, @@ -508,7 +552,7 @@ Logic.registerPanel(P_CONTAINERS_EDIT, { // This method is called when the object is registered. initialize() { - document.querySelector("#exit-edit-mode-link").addEventListener("click", () => { + Logic.addEnterHandler(document.querySelector("#exit-edit-mode-link"), () => { Logic.showPanel(P_CONTAINERS_LIST); }); }, @@ -523,7 +567,7 @@ Logic.registerPanel(P_CONTAINERS_EDIT, { tr.innerHTML = escaped`
-
@@ -546,7 +590,7 @@ Logic.registerPanel(P_CONTAINERS_EDIT, { tr.querySelector(".remove-container .pop-button-image").setAttribute("title", `Edit ${identity.name} container`); - tr.addEventListener("click", e => { + Logic.addEnterHandler(tr, e => { if (e.target.matches(".edit-container-icon") || e.target.parentNode.matches(".edit-container-icon")) { Logic.showPanel(P_CONTAINER_EDIT, identity); } else if (e.target.matches(".delete-container-icon") || e.target.parentNode.matches(".delete-container-icon")) { @@ -574,17 +618,17 @@ Logic.registerPanel(P_CONTAINER_EDIT, { initialize() { this.initializeRadioButtons(); - document.querySelector("#edit-container-panel-back-arrow").addEventListener("click", () => { + Logic.addEnterHandler(document.querySelector("#edit-container-panel-back-arrow"), () => { Logic.showPreviousPanel(); }); - document.querySelector("#edit-container-cancel-link").addEventListener("click", () => { + Logic.addEnterHandler(document.querySelector("#edit-container-cancel-link"), () => { Logic.showPreviousPanel(); }); this._editForm = document.getElementById("edit-container-panel-form"); const editLink = document.querySelector("#edit-container-ok-link"); - editLink.addEventListener("click", this._submitForm.bind(this)); + Logic.addEnterHandler(editLink, this._submitForm.bind(this)); editLink.addEventListener("submit", this._submitForm.bind(this)); this._editForm.addEventListener("submit", this._submitForm.bind(this)); }, @@ -663,11 +707,11 @@ Logic.registerPanel(P_CONTAINER_DELETE, { // This method is called when the object is registered. initialize() { - document.querySelector("#delete-container-cancel-link").addEventListener("click", () => { + Logic.addEnterHandler(document.querySelector("#delete-container-cancel-link"), () => { Logic.showPreviousPanel(); }); - document.querySelector("#delete-container-ok-link").addEventListener("click", () => { + Logic.addEnterHandler(document.querySelector("#delete-container-ok-link"), () => { /* This promise wont resolve if the last tab was removed from the window. as the message async callback stops listening, this isn't an issue for us however it might be in future if you want to do anything post delete do it in the background script. diff --git a/webextension/manifest.json b/webextension/manifest.json index a9582a7..910aa8e 100644 --- a/webextension/manifest.json +++ b/webextension/manifest.json @@ -33,6 +33,15 @@ "webRequest" ], + "commands": { + "_execute_browser_action": { + "suggested_key": { + "default": "Ctrl+Y" + }, + "description": "Open containers panel" + } + }, + "browser_action": { "browser_style": true, "default_icon": { diff --git a/webextension/popup.html b/webextension/popup.html index 26c00ce..794c9b0 100644 --- a/webextension/popup.html +++ b/webextension/popup.html @@ -40,25 +40,25 @@

Containers

- Sort Containers + Sort Containers
- -
+
Panel Back Arrow @@ -92,7 +92,7 @@
@@ -116,7 +116,7 @@
@@ -133,8 +133,8 @@

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

From 4d61fa190c0dce79dac070588c9e7780e888a8cc Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Mon, 8 May 2017 20:20:59 +0100 Subject: [PATCH 17/21] Removal of more SDK code --- index.js | 247 +++++++++++++++++-------------------- webextension/background.js | 65 ++++++++-- webextension/js/popup.js | 52 +++++--- 3 files changed, 201 insertions(+), 163 deletions(-) diff --git a/index.js b/index.js index b81402f..4f5f739 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; const DEFAULT_TAB = "about:newtab"; +const LOOKUP_KEY = "$ref"; const SHOW_MENU_TIMEOUT = 100; const HIDE_MENU_TIMEOUT = 300; @@ -73,41 +74,43 @@ Cu.import("resource:///modules/CustomizableWidgets.jsm"); Cu.import("resource:///modules/sessionstore/SessionStore.jsm"); Cu.import("resource://gre/modules/Services.jsm"); -// ---------------------------------------------------------------------------- // ContextualIdentityProxy const ContextualIdentityProxy = { getIdentities() { + let response; if ("getPublicIdentities" in ContextualIdentityService) { - return ContextualIdentityService.getPublicIdentities(); + response = ContextualIdentityService.getPublicIdentities(); + } else { + response = ContextualIdentityService.getIdentities(); } - return ContextualIdentityService.getIdentities(); - }, - - getUserContextLabel(userContextId) { - return ContextualIdentityService.getUserContextLabel(userContextId); + return response.map((identity) => { + return this._convert(identity); + }); }, getIdentityFromId(userContextId) { + let response; if ("getPublicIdentityFromId" in ContextualIdentityService) { - return ContextualIdentityService.getPublicIdentityFromId(userContextId); + response = ContextualIdentityService.getPublicIdentityFromId(userContextId); + } else { + response = ContextualIdentityService.getIdentityFromId(userContextId); } - - return ContextualIdentityService.getIdentityFromId(userContextId); + if (response) { + return this._convert(response); + } + return response; }, - create(name, icon, color) { - return ContextualIdentityService.create(name, icon, color); + _convert(identity) { + return { + name: ContextualIdentityService.getUserContextLabel(identity.userContextId), + icon: identity.icon, + color: identity.color, + userContextId: identity.userContextId, + }; }, - - update(userContextId, name, icon, color) { - return ContextualIdentityService.update(userContextId, name, icon, color); - }, - - remove(userContextId) { - return ContextualIdentityService.remove(userContextId); - } }; // ---------------------------------------------------------------------------- @@ -149,7 +152,7 @@ const ContainerService = { // Maybe rename the Banking container. const identity = ContextualIdentityProxy.getIdentityFromId(3); if (identity && identity.l10nID === "userContextBanking.label") { - ContextualIdentityProxy.update(identity.userContextId, + ContextualIdentityService.update(identity.userContextId, "Finance", identity.icon, identity.color); @@ -167,6 +170,18 @@ const ContainerService = { } } + // TOCHECK should this run on all code + ContextualIdentityProxy.getIdentities().forEach(identity => { + const newIcon = this._fromIconToName(identity.icon); + const newColor = this._fromColorToName(identity.color); + if (newIcon !== identity.icon || newColor !== identity.color) { + ContextualIdentityService.update(identity.userContextId, + ContextualIdentityService.getUserContextLabel(identity.userContextId), + newIcon, + newColor); + } + }); + // Let's see if containers were enabled before this addon. this._containerWasEnabled = ss.storage.savedConfiguration.prefs["privacy.userContext.enabled"]; @@ -192,9 +207,8 @@ const ContainerService = { "sortTabs", "getTabs", "showTab", - "openTab", "moveTabsToWindow", - "queryIdentities", + "queryIdentitiesState", "getIdentity", "getPreference", "sendTelemetryPayload", @@ -308,7 +322,7 @@ const ContainerService = { }, registerBackgroundConnection(api) { - // This is only used for theme notifications + // This is only used for theme notifications and new tab api.browser.runtime.onConnect.addListener((port) => { this._onBackgroundConnectCallback = (message, topic) => { port.postMessage({ @@ -396,18 +410,6 @@ const ContainerService = { return containersCounts; }, - _convert(identity) { - // Let's convert the known colors to their color names. - return { - name: ContextualIdentityProxy.getUserContextLabel(identity.userContextId), - image: this._fromIconToName(identity.icon), - color: this._fromColorToName(identity.color), - userContextId: identity.userContextId, - hasHiddenTabs: !!this._identitiesState[identity.userContextId].hiddenTabs.length, - hasOpenTabs: !!this._identitiesState[identity.userContextId].openTabs - }; - }, - // In FF 50-51, the icon is the full path, in 52 and following // releases, we have IDs to be used with a svg file. In this function // we map URLs to svg IDs. @@ -433,10 +435,6 @@ const ContainerService = { // Helper methods for converting icons to names and names to icons. - _fromNameToIcon(name) { - return this._fromNameOrIcon(name, "image", ""); - }, - _fromIconToName(icon) { return this._fromNameOrIcon(icon, "name", "circle"); }, @@ -559,6 +557,29 @@ const ContainerService = { }; Object.assign(payload, args); + /* This is to masage the data whilst it is still active in the SDK side */ + const containersCounts = this._containersCounts(); + Object.keys(payload).forEach((keyName) => { + let value = payload[keyName]; + if (value === LOOKUP_KEY) { + switch (keyName) { + case "clickedContainerTabCount": + value = this._containerTabCount(payload.userContextId); + break; + case "shownContainersCount": + value = containersCounts.shown; + break; + case "hiddenContainersCount": + value = containersCounts.hidden; + break; + case "totalContainersCount": + value = containersCounts.total; + break; + } + } + payload[keyName] = value; + }); + this._sendEvent(payload); }, @@ -593,10 +614,10 @@ const ContainerService = { this.sendTelemetryPayload({ "event": "hide-tabs", "userContextId": args.userContextId, - "clickedContainerTabCount": this._containerTabCount(args.userContextId), - "shownContainersCount": containersCounts.shown, - "hiddenContainersCount": containersCounts.hidden, - "totalContainersCount": containersCounts.total + "clickedContainerTabCount": LOOKUP_KEY, + "shownContainersCount": LOOKUP_KEY, + "hiddenContainersCount": LOOKUP_KEY, + "totalContainersCount": LOOKUP_KEY }); const tabsToClose = []; @@ -633,14 +654,13 @@ const ContainerService = { return Promise.resolve(null); } - const containersCounts = this._containersCounts(); this.sendTelemetryPayload({ "event": "show-tabs", "userContextId": args.userContextId, - "clickedContainerTabCount": this._containerTabCount(args.userContextId), - "shownContainersCount": containersCounts.shown, - "hiddenContainersCount": containersCounts.hidden, - "totalContainersCount": containersCounts.total + "clickedContainerTabCount": LOOKUP_KEY, + "shownContainersCount": LOOKUP_KEY, + "hiddenContainersCount": LOOKUP_KEY, + "totalContainersCount": LOOKUP_KEY }); const promises = []; @@ -653,7 +673,6 @@ const ContainerService = { userContextId: args.userContextId, url: object.url, nofocus: args.nofocus || false, - window: args.window || null, pinned: object.pinned, })); } @@ -840,74 +859,37 @@ const ContainerService = { }, openTab(args) { - return new Promise(resolve => { - if ("window" in args && args.window) { - resolve(args.window); - } else { - this._recentBrowserWindow().then(browserWin => { - resolve(browserWin); - }).catch(() => {}); - } - }).then(browserWin => { - const userContextId = ("userContextId" in args) ? args.userContextId : 0; - const source = ("source" in args) ? args.source : null; - const nofocus = ("nofocus" in args) ? args.nofocus : false; - - // Only send telemetry for tabs opened by UI - i.e., not via showTabs - if (source && userContextId) { - this.sendTelemetryPayload({ - "event": "open-tab", - "eventSource": source, - "userContextId": userContextId, - "clickedContainerTabCount": this._containerTabCount(userContextId) - }); - } - - let promise; - if (userContextId) { - promise = this.showTabs(args); - } else { - promise = Promise.resolve(null); - } - - return promise.then(() => { - const tab = browserWin.gBrowser.addTab(args.url || DEFAULT_TAB, { userContextId }); - if (!nofocus) { - browserWin.gBrowser.selectedTab = tab; - browserWin.focusAndSelectUrlBar(); - } - - if (args.pinned) { - browserWin.gBrowser.pinTab(tab); - } - return true; - }); - }).catch(() => false); + return this.triggerBackgroundCallback(args, "open-tab"); }, // Identities management - - queryIdentities() { + queryIdentitiesState() { return new Promise(resolve => { - const identities = []; + const identities = {}; ContextualIdentityProxy.getIdentities().forEach(identity => { this._remapTabsIfMissing(identity.userContextId); - const convertedIdentity = this._convert(identity); - identities.push(convertedIdentity); + const convertedIdentity = { + hasHiddenTabs: !!this._identitiesState[identity.userContextId].hiddenTabs.length, + hasOpenTabs: !!this._identitiesState[identity.userContextId].openTabs + }; + + identities[identity.userContextId] = convertedIdentity; }); resolve(identities); }); }, - getIdentity(args) { - if (!("userContextId" in args)) { - return Promise.reject("getIdentity must be called with userContextId argument."); - } + queryIdentities() { + return new Promise(resolve => { + const identities = ContextualIdentityProxy.getIdentities(); + identities.forEach(identity => { + this._remapTabsIfMissing(identity.userContextId); + }); - const identity = ContextualIdentityProxy.getIdentityFromId(args.userContextId); - return Promise.resolve(identity ? this._convert(identity) : null); + resolve(identities); + }); }, // Preferences @@ -974,26 +956,25 @@ const ContainerService = { } const userContextId = ContainerService._getUserContextIdFromTab(tab); - return ContainerService.getIdentity({userContextId}).then(identity => { - const hbox = viewFor(tab.window).document.getElementById("userContext-icons"); + const identity = ContextualIdentityProxy.getIdentityFromId(userContextId); + const hbox = viewFor(tab.window).document.getElementById("userContext-icons"); - if (!identity) { - hbox.setAttribute("data-identity-color", ""); - return; - } + if (!identity) { + hbox.setAttribute("data-identity-color", ""); + return Promise.resolve(null); + } - hbox.setAttribute("data-identity-color", identity.color); + hbox.setAttribute("data-identity-color", identity.color); - const label = viewFor(tab.window).document.getElementById("userContext-label"); - label.setAttribute("value", identity.name); - label.style.color = ContainerService._fromNameToColor(identity.color); + const label = viewFor(tab.window).document.getElementById("userContext-label"); + label.setAttribute("value", identity.name); + label.style.color = ContainerService._fromNameToColor(identity.color); - const indicator = viewFor(tab.window).document.getElementById("userContext-indicator"); - indicator.setAttribute("data-identity-icon", identity.image); - indicator.style.listStyleImage = ""; - }).then(() => { - return this._restyleTab(tab); - }); + const indicator = viewFor(tab.window).document.getElementById("userContext-indicator"); + indicator.setAttribute("data-identity-icon", identity.icon); + indicator.style.listStyleImage = ""; + + return this._restyleTab(tab); }, _restyleTab(tab) { @@ -1001,12 +982,11 @@ const ContainerService = { return Promise.resolve(null); } const userContextId = ContainerService._getUserContextIdFromTab(tab); - return ContainerService.getIdentity({userContextId}).then(identity => { - if (!identity) { - return; - } - viewFor(tab).setAttribute("data-identity-color", identity.color); - }); + const identity = ContextualIdentityProxy.getIdentityFromId(userContextId); + if (!identity) { + return Promise.resolve(null); + } + return Promise.resolve(viewFor(tab).setAttribute("data-identity-color", identity.color)); }, // Uninstallation @@ -1065,7 +1045,7 @@ const ContainerService = { const preInstalledIdentities = data.preInstalledIdentities; ContextualIdentityProxy.getIdentities().forEach(identity => { if (!preInstalledIdentities.includes(identity.userContextId)) { - ContextualIdentityProxy.remove(identity.userContextId); + ContextualIdentityService.remove(identity.userContextId); } else { // Let's cleanup all the cookies for this container. Services.obs.notifyObservers(null, "clear-origin-attributes-data", @@ -1237,14 +1217,13 @@ ContainerWindow.prototype = { menuItemElement.className = "menuitem-iconic"; menuItemElement.setAttribute("label", identity.name); menuItemElement.setAttribute("data-usercontextid", identity.userContextId); - menuItemElement.setAttribute("data-identity-icon", identity.image); + menuItemElement.setAttribute("data-identity-icon", identity.icon); menuItemElement.setAttribute("data-identity-color", identity.color); menuItemElement.addEventListener("command", (e) => { ContainerService.openTab({ userContextId: identity.userContextId, - source: "tab-bar", - window: this._window, + source: "tab-bar" }); e.stopPropagation(); }); @@ -1280,8 +1259,7 @@ ContainerWindow.prototype = { const userContextId = parseInt(e.target.getAttribute("data-usercontextid"), 10); ContainerService.openTab({ userContextId: userContextId, - source: "file-menu", - window: this._window, + source: "file-menu" }); }); }, @@ -1296,8 +1274,7 @@ ContainerWindow.prototype = { }).then(() => { return ContainerService.openTab({ userContextId, - source: "alltabs-menu", - window: this._window, + source: "alltabs-menu" }); }).catch(() => {}); }); @@ -1399,7 +1376,7 @@ ContainerWindow.prototype = { menuitem.classList.add("menuitem-iconic"); menuitem.setAttribute("data-usercontextid", identity.userContextId); menuitem.setAttribute("data-identity-color", identity.color); - menuitem.setAttribute("data-identity-icon", identity.image); + menuitem.setAttribute("data-identity-icon", identity.icon); fragment.appendChild(menuitem); }); diff --git a/webextension/background.js b/webextension/background.js index 72bd8b8..0b52532 100644 --- a/webextension/background.js +++ b/webextension/background.js @@ -1,12 +1,7 @@ const MAJOR_VERSIONS = ["2.3.0"]; +const LOOKUP_KEY = "$ref"; const assignManager = { - CLOSEABLE_WINDOWS: new Set([ - "about:startpage", - "about:newtab", - "about:home", - "about:blank" - ]), MENU_ASSIGN_ID: "open-in-this-container", MENU_REMOVE_ID: "remove-open-in-this-container", storageArea: { @@ -133,14 +128,14 @@ const assignManager = { We aim to open the new assigned container tab / warning prompt in it's own tab: - As the history won't span from one container to another it seems most sane to not try and reopen a tab on history.back() - When users open a new tab themselves we want to make sure we don't end up with three tabs as per: https://github.com/mozilla/testpilot-containers/issues/421 - If we are coming from an internal url that are used for the new tab page (CLOSEABLE_WINDOWS), we can safely close as user is unlikely losing history + If we are coming from an internal url that are used for the new tab page (NEW_TAB_PAGES), we can safely close as user is unlikely losing history Detecting redirects on "new tab" opening actions is pretty hard as we don't get tab history: - Redirects happen from Short URLs and tracking links that act as a gateway - Extensions don't provide a way to history crawl for tabs, we could inject content scripts to do this however they don't run on about:blank so this would likely be just as hacky. We capture the time the tab was created and close if it was within the timeout to try to capture pages which haven't had user interaction or history. */ - if (this.CLOSEABLE_WINDOWS.has(tab.url) + if (backgroundLogic.NEW_TAB_PAGES.has(tab.url) || (messageHandler.lastCreatedTab && messageHandler.lastCreatedTab.id === tab.id)) { browser.tabs.remove(tab.id); @@ -218,7 +213,7 @@ const assignManager = { 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: `firefox-container-${userContextId}`, index}); + browser.tabs.create({url, cookieStoreId: backgroundLogic.cookieStoreId(userContextId), index}); backgroundLogic.sendTelemetryPayload({ event: "auto-reload-page-in-container", userContextId: userContextId, @@ -229,7 +224,7 @@ const assignManager = { userContextId: userContextId, }); const confirmUrl = `${loadPage}?url=${url}`; - browser.tabs.create({url: confirmUrl, cookieStoreId: `firefox-container-${userContextId}`, index}).then(() => { + browser.tabs.create({url: confirmUrl, cookieStoreId: backgroundLogic.cookieStoreId(userContextId), index}).then(() => { // We don't want to sync this URL ever nor clutter the users history browser.history.deleteUrl({url: confirmUrl}); }).catch((e) => { @@ -241,6 +236,13 @@ const assignManager = { const backgroundLogic = { + NEW_TAB_PAGES: new Set([ + "about:startpage", + "about:newtab", + "about:home", + "about:blank" + ]), + deleteContainer(userContextId) { this.sendTelemetryPayload({ event: "delete-container", @@ -291,6 +293,41 @@ const backgroundLogic = { }); }, + openTab(options) { + let url = options.url || undefined; + const userContextId = ("userContextId" in options) ? options.userContextId : 0; + const active = ("nofocus" in options) ? options.nofocus : true; + const source = ("source" in options) ? options.source : null; + + // Only send telemetry for tabs opened by UI - i.e., not via showTabs + if (source && userContextId) { + this.sendTelemetryPayload({ + "event": "open-tab", + "eventSource": source, + "userContextId": userContextId, + "clickedContainerTabCount": LOOKUP_KEY + }); + } + // Autofocus url bar will happen in 54: https://bugzilla.mozilla.org/show_bug.cgi?id=1295072 + + // We can't open new tab pages, so open a blank tab. Used in tab un-hide + if (this.NEW_TAB_PAGES.has(url)) { + url = undefined; + } + + // Unhide all hidden tabs + browser.runtime.sendMessage({ + method: "showTabs", + userContextId: options.userContextId + }); + return browser.tabs.create({ + url, + active, + pinned: options.pinned || false, + cookieStoreId: backgroundLogic.cookieStoreId(options.userContextId) + }); + }, + sendTelemetryPayload(message = {}) { if (!message.event) { throw new Error("Missing event name for telemetry"); @@ -317,6 +354,7 @@ const messageHandler = { LAST_CREATED_TAB_TIMER: 2000, init() { + // Handles messages from webextension/js/popup.js browser.runtime.onMessage.addListener((m) => { let response; @@ -327,6 +365,10 @@ const messageHandler = { case "createOrUpdateContainer": response = backgroundLogic.createOrUpdateContainer(m.message); break; + case "openTab": + // Same as open-tab for index.js + response = backgroundLogic.openTab(m.message); + break; case "neverAsk": assignManager._neverAsk(m); break; @@ -341,6 +383,9 @@ const messageHandler = { case "lightweight-theme-changed": themeManager.update(m.message); break; + case "open-tab": + backgroundLogic.openTab(m.message); + break; default: throw new Error(`Unhandled message type: ${m.message}`); } diff --git a/webextension/js/popup.js b/webextension/js/popup.js index c1d14a3..26afa59 100644 --- a/webextension/js/popup.js +++ b/webextension/js/popup.js @@ -116,13 +116,27 @@ const Logic = { }); }, + userContextId(cookieStoreId = "") { + const userContextId = cookieStoreId.replace("firefox-container-", ""); + return (userContextId !== cookieStoreId) ? Number(userContextId) : false; + }, + refreshIdentities() { - return browser.runtime.sendMessage({ - method: "queryIdentities" - }) - .then(identities => { - this._identities = identities; - }); + return Promise.all([ + browser.contextualIdentities.query({}), + browser.runtime.sendMessage({ + method: "queryIdentitiesState" + }) + ]).then(([identities, state]) => { + this._identities = identities.map((identity) => { + const stateObject = state[Logic.userContextId(identity.cookieStoreId)]; + if (stateObject) { + identity.hasOpenTabs = stateObject.hasOpenTabs; + identity.hasHiddenTabs = stateObject.hasHiddenTabs; + } + return identity; + }); + }).catch((e) => {throw e;}); }, showPanel(panel, currentIdentity = null) { @@ -371,7 +385,7 @@ Logic.registerPanel(P_CONTAINERS_LIST, { context.innerHTML = escaped`
@@ -393,8 +407,10 @@ Logic.registerPanel(P_CONTAINERS_LIST, { || e.type === "keydown") { browser.runtime.sendMessage({ method: "openTab", - userContextId: identity.userContextId, - source: "pop-up" + message: { + userContextId: Logic.userContextId(identity.cookieStoreId), + source: "pop-up" + } }).then(() => { window.close(); }).catch(() => { @@ -437,7 +453,7 @@ Logic.registerPanel(P_CONTAINER_INFO, { const identity = Logic.currentIdentity(); browser.runtime.sendMessage({ method: identity.hasHiddenTabs ? "showTabs" : "hideTabs", - userContextId: identity.userContextId + userContextId: Logic.userContextId(identity.cookieStoreId) }).then(() => { window.close(); }).catch(() => { @@ -467,7 +483,7 @@ Logic.registerPanel(P_CONTAINER_INFO, { Logic.addEnterHandler(moveTabsEl, () => { browser.runtime.sendMessage({ method: "moveTabsToWindow", - userContextId: Logic.currentIdentity().userContextId, + userContextId: Logic.userContextId(Logic.currentIdentity().cookieStoreId), }).then(() => { window.close(); }).catch((e) => { throw e; }); @@ -486,7 +502,7 @@ Logic.registerPanel(P_CONTAINER_INFO, { document.getElementById("container-info-name").textContent = identity.name; const icon = document.getElementById("container-info-icon"); - icon.setAttribute("data-identity-icon", identity.image); + icon.setAttribute("data-identity-icon", identity.icon); icon.setAttribute("data-identity-color", identity.color); // Show or not the has-tabs section. @@ -509,7 +525,7 @@ Logic.registerPanel(P_CONTAINER_INFO, { // Let's retrieve the list of tabs. return browser.runtime.sendMessage({ method: "getTabs", - userContextId: identity.userContextId, + userContextId: Logic.userContextId(identity.cookieStoreId), }).then(this.buildInfoTable); }, @@ -568,7 +584,7 @@ Logic.registerPanel(P_CONTAINERS_EDIT, {
@@ -639,7 +655,7 @@ Logic.registerPanel(P_CONTAINER_EDIT, { return browser.runtime.sendMessage({ method: "createOrUpdateContainer", message: { - userContextId: identity.userContextId || false, + userContextId: Logic.userContextId(identity.cookieStoreId) || false, params: { name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(), icon: formValues.get("container-icon") || DEFAULT_ICON, @@ -691,7 +707,7 @@ Logic.registerPanel(P_CONTAINER_EDIT, { colorInput.checked = colorInput.value === identity.color; }); [...document.querySelectorAll("[name='container-icon']")].forEach(iconInput => { - iconInput.checked = iconInput.value === identity.image; + iconInput.checked = iconInput.value === identity.icon; }); return Promise.resolve(null); @@ -717,7 +733,7 @@ Logic.registerPanel(P_CONTAINER_DELETE, { if you want to do anything post delete do it in the background script. Browser console currently warns about not listening also. */ - Logic.removeIdentity(Logic.currentIdentity().userContextId).then(() => { + Logic.removeIdentity(Logic.userContextId(Logic.currentIdentity().cookieStoreId)).then(() => { return Logic.refreshIdentities(); }).then(() => { Logic.showPreviousPanel(); @@ -735,7 +751,7 @@ Logic.registerPanel(P_CONTAINER_DELETE, { document.getElementById("delete-container-name").textContent = identity.name; const icon = document.getElementById("delete-container-icon"); - icon.setAttribute("data-identity-icon", identity.image); + icon.setAttribute("data-identity-icon", identity.icon); icon.setAttribute("data-identity-color", identity.color); return Promise.resolve(null); From 4ffb587d9e65b0e1be8430b19ed80484fab3a0bc Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Mon, 8 May 2017 20:20:59 +0100 Subject: [PATCH 18/21] Removal of more SDK code --- index.js | 406 +++++++++++++++++-------------------- webextension/background.js | 65 +++++- webextension/js/popup.js | 52 +++-- 3 files changed, 270 insertions(+), 253 deletions(-) diff --git a/index.js b/index.js index b81402f..bf70a1c 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; const DEFAULT_TAB = "about:newtab"; +const LOOKUP_KEY = "$ref"; const SHOW_MENU_TIMEOUT = 100; const HIDE_MENU_TIMEOUT = 300; @@ -73,41 +74,43 @@ Cu.import("resource:///modules/CustomizableWidgets.jsm"); Cu.import("resource:///modules/sessionstore/SessionStore.jsm"); Cu.import("resource://gre/modules/Services.jsm"); -// ---------------------------------------------------------------------------- // ContextualIdentityProxy const ContextualIdentityProxy = { getIdentities() { + let response; if ("getPublicIdentities" in ContextualIdentityService) { - return ContextualIdentityService.getPublicIdentities(); + response = ContextualIdentityService.getPublicIdentities(); + } else { + response = ContextualIdentityService.getIdentities(); } - return ContextualIdentityService.getIdentities(); - }, - - getUserContextLabel(userContextId) { - return ContextualIdentityService.getUserContextLabel(userContextId); + return response.map((identity) => { + return this._convert(identity); + }); }, getIdentityFromId(userContextId) { + let response; if ("getPublicIdentityFromId" in ContextualIdentityService) { - return ContextualIdentityService.getPublicIdentityFromId(userContextId); + response = ContextualIdentityService.getPublicIdentityFromId(userContextId); + } else { + response = ContextualIdentityService.getIdentityFromId(userContextId); } - - return ContextualIdentityService.getIdentityFromId(userContextId); + if (response) { + return this._convert(response); + } + return response; }, - create(name, icon, color) { - return ContextualIdentityService.create(name, icon, color); + _convert(identity) { + return { + name: ContextualIdentityService.getUserContextLabel(identity.userContextId), + icon: identity.icon, + color: identity.color, + userContextId: identity.userContextId, + }; }, - - update(userContextId, name, icon, color) { - return ContextualIdentityService.update(userContextId, name, icon, color); - }, - - remove(userContextId) { - return ContextualIdentityService.remove(userContextId); - } }; // ---------------------------------------------------------------------------- @@ -119,7 +122,7 @@ const ContainerService = { _containerWasEnabled: false, _onBackgroundConnectCallback: null, - init(installation, reason) { + async init(installation, reason) { // If we are just been installed, we must store some information for the // uninstallation. This object contains also a version number, in case we // need to implement a migration in the future. @@ -149,7 +152,7 @@ const ContainerService = { // Maybe rename the Banking container. const identity = ContextualIdentityProxy.getIdentityFromId(3); if (identity && identity.l10nID === "userContextBanking.label") { - ContextualIdentityProxy.update(identity.userContextId, + ContextualIdentityService.update(identity.userContextId, "Finance", identity.icon, identity.color); @@ -167,6 +170,18 @@ const ContainerService = { } } + // TOCHECK should this run on all code + ContextualIdentityProxy.getIdentities().forEach(identity => { + const newIcon = this._fromIconToName(identity.icon); + const newColor = this._fromColorToName(identity.color); + if (newIcon !== identity.icon || newColor !== identity.color) { + ContextualIdentityService.update(identity.userContextId, + ContextualIdentityService.getUserContextLabel(identity.userContextId), + newIcon, + newColor); + } + }); + // Let's see if containers were enabled before this addon. this._containerWasEnabled = ss.storage.savedConfiguration.prefs["privacy.userContext.enabled"]; @@ -192,9 +207,8 @@ const ContainerService = { "sortTabs", "getTabs", "showTab", - "openTab", "moveTabsToWindow", - "queryIdentities", + "queryIdentitiesState", "getIdentity", "getPreference", "sendTelemetryPayload", @@ -253,7 +267,8 @@ const ContainerService = { // WebExtension startup - webExtension.startup().then(api => { + try { + const api = await webExtension.startup(); api.browser.runtime.onMessage.addListener((message, sender, sendReply) => { if ("method" in message && methods.indexOf(message.method) !== -1) { sendReply(this[message.method](message)); @@ -261,9 +276,9 @@ const ContainerService = { }); this.registerBackgroundConnection(api); - }).catch(() => { + } catch (e) { throw new Error("WebExtension startup failed. Unable to continue."); - }); + } this._sendEvent = new Metrics({ type: "sdk", @@ -308,7 +323,7 @@ const ContainerService = { }, registerBackgroundConnection(api) { - // This is only used for theme notifications + // This is only used for theme notifications and new tab api.browser.runtime.onConnect.addListener((port) => { this._onBackgroundConnectCallback = (message, topic) => { port.postMessage({ @@ -325,13 +340,14 @@ const ContainerService = { } }, - observe(subject, topic) { + async observe(subject, topic) { if (topic === "lightweight-theme-changed") { - this.getTheme().then((theme) => { + try { + const theme = await this.getTheme(); this.triggerBackgroundCallback(theme, topic); - }).catch(() => { + } catch (e) { throw new Error("Unable to get theme"); - }); + } } }, @@ -396,18 +412,6 @@ const ContainerService = { return containersCounts; }, - _convert(identity) { - // Let's convert the known colors to their color names. - return { - name: ContextualIdentityProxy.getUserContextLabel(identity.userContextId), - image: this._fromIconToName(identity.icon), - color: this._fromColorToName(identity.color), - userContextId: identity.userContextId, - hasHiddenTabs: !!this._identitiesState[identity.userContextId].hiddenTabs.length, - hasOpenTabs: !!this._identitiesState[identity.userContextId].openTabs - }; - }, - // In FF 50-51, the icon is the full path, in 52 and following // releases, we have IDs to be used with a svg file. In this function // we map URLs to svg IDs. @@ -433,10 +437,6 @@ const ContainerService = { // Helper methods for converting icons to names and names to icons. - _fromNameToIcon(name) { - return this._fromNameOrIcon(name, "image", ""); - }, - _fromIconToName(icon) { return this._fromNameOrIcon(icon, "name", "circle"); }, @@ -456,22 +456,31 @@ const ContainerService = { return parseInt(viewFor(tab).getAttribute("usercontextid") || 0, 10); }, - _createTabObject(tab) { + async _createTabObject(tab) { + let url; + try { + url = await getFavicon(tab.url); + } catch (e) { + url = ""; + } return { title: tab.title, url: tab.url, + favicon: url, id: tab.id, active: true, pinned: tabsUtils.isPinned(viewFor(tab)) }; }, - _containerTabIterator(userContextId, cb) { - for (let tab of tabs) { // eslint-disable-line prefer-const + _matchTabsByContainer(userContextId) { + const matchedTabs = []; + for (const tab of tabs) { if (userContextId === this._getUserContextIdFromTab(tab)) { - cb(tab); + matchedTabs.push(tab); } } + return matchedTabs; }, _createIdentityState() { @@ -492,10 +501,7 @@ const ContainerService = { }, _remapTabsFromUserContextId(userContextId) { - this._identitiesState[userContextId].openTabs = 0; - this._containerTabIterator(userContextId, () => { - ++this._identitiesState[userContextId].openTabs; - }); + this._identitiesState[userContextId].openTabs = this._matchTabsByContainer(userContextId).length; }, _remapTab(tab) { @@ -509,30 +515,25 @@ const ContainerService = { return userContextId in this._identitiesState; }, - _closeTabs(tabsToClose) { + async _closeTabs(tabsToClose) { // We create a new tab only if the current operation closes all the // existing ones. - let promise; - if (tabs.length !== tabsToClose.length) { - promise = Promise.resolve(null); - } else { - promise = this.openTab({}); + if (tabs.length === tabsToClose.length) { + await this.openTab({}); } - return promise.then(() => { - for (let tab of tabsToClose) { // eslint-disable-line prefer-const - // after .close() window is null. Let's take it now. - const window = viewFor(tab.window); + for (const tab of tabsToClose) { + // after .close() window is null. Let's take it now. + const window = viewFor(tab.window); - tab.close(); + tab.close(); - // forget about this tab. 0 is the index of the forgotten tab and 0 - // means the last one. - try { - SessionStore.forgetClosedTab(window, 0); - } catch(e) {} // eslint-disable-line no-empty - } - }).catch(() => null); + // forget about this tab. 0 is the index of the forgotten tab and 0 + // means the last one. + try { + SessionStore.forgetClosedTab(window, 0); + } catch (e) {} // eslint-disable-line no-empty + } }, _recentBrowserWindow() { @@ -559,6 +560,29 @@ const ContainerService = { }; Object.assign(payload, args); + /* This is to masage the data whilst it is still active in the SDK side */ + const containersCounts = this._containersCounts(); + Object.keys(payload).forEach((keyName) => { + let value = payload[keyName]; + if (value === LOOKUP_KEY) { + switch (keyName) { + case "clickedContainerTabCount": + value = this._containerTabCount(payload.userContextId); + break; + case "shownContainersCount": + value = containersCounts.shown; + break; + case "hiddenContainersCount": + value = containersCounts.hidden; + break; + case "totalContainersCount": + value = containersCounts.total; + break; + } + } + payload[keyName] = value; + }); + this._sendEvent(payload); }, @@ -579,51 +603,46 @@ const ContainerService = { // Tabs management - hideTabs(args) { + async hideTabs(args) { if (!("userContextId" in args)) { - return Promise.reject("hideTabs must be called with userContextId argument."); + return new Error("hideTabs must be called with userContextId argument."); } this._remapTabsIfMissing(args.userContextId); if (!this._isKnownContainer(args.userContextId)) { - return Promise.resolve(null); + return null; } - const containersCounts = this._containersCounts(); this.sendTelemetryPayload({ "event": "hide-tabs", "userContextId": args.userContextId, - "clickedContainerTabCount": this._containerTabCount(args.userContextId), - "shownContainersCount": containersCounts.shown, - "hiddenContainersCount": containersCounts.hidden, - "totalContainersCount": containersCounts.total + "clickedContainerTabCount": LOOKUP_KEY, + "shownContainersCount": LOOKUP_KEY, + "hiddenContainersCount": LOOKUP_KEY, + "totalContainersCount": LOOKUP_KEY }); const tabsToClose = []; - this._containerTabIterator(args.userContextId, tab => { - const object = this._createTabObject(tab); + const tabObjects = await Promise.all(this._matchTabsByContainer(args.userContextId).map((tab) => { + tabsToClose.push(tab); + return this._createTabObject(tab); + })); + tabObjects.forEach((object) => { // This tab is going to be closed. Let's mark this tabObject as // non-active. object.active = false; - getFavicon(object.url).then(url => { - object.favicon = url; - }).catch(() => { - object.favicon = ""; - }); - this._identitiesState[args.userContextId].hiddenTabs.push(object); - tabsToClose.push(tab); }); - return this._closeTabs(tabsToClose).then(() => { - return this._syncTabs(); - }); + await this._closeTabs(tabsToClose); + + return this._syncTabs(); }, - showTabs(args) { + async showTabs(args) { if (!("userContextId" in args)) { return Promise.reject("showTabs must be called with userContextId argument."); } @@ -633,14 +652,13 @@ const ContainerService = { return Promise.resolve(null); } - const containersCounts = this._containersCounts(); this.sendTelemetryPayload({ "event": "show-tabs", "userContextId": args.userContextId, - "clickedContainerTabCount": this._containerTabCount(args.userContextId), - "shownContainersCount": containersCounts.shown, - "hiddenContainersCount": containersCounts.hidden, - "totalContainersCount": containersCounts.total + "clickedContainerTabCount": LOOKUP_KEY, + "shownContainersCount": LOOKUP_KEY, + "hiddenContainersCount": LOOKUP_KEY, + "totalContainersCount": LOOKUP_KEY }); const promises = []; @@ -653,16 +671,14 @@ const ContainerService = { userContextId: args.userContextId, url: object.url, nofocus: args.nofocus || false, - window: args.window || null, pinned: object.pinned, })); } this._identitiesState[args.userContextId].hiddenTabs = []; - return Promise.all(promises).then(() => { - return this._syncTabs(); - }); + await Promise.all(promises); + return this._syncTabs(); }, sortTabs() { @@ -692,7 +708,7 @@ const ContainerService = { // Let's collect UCIs/tabs for this window. const map = new Map; - for (let tab of tabs) { // eslint-disable-line prefer-const + for (const tab of tabs) { if (pinnedTabs && !tabsUtils.isPinned(tab)) { // We don't have, or we already handled all the pinned tabs. break; @@ -716,44 +732,29 @@ const ContainerService = { // Let's move tabs. sortMap.forEach(tabs => { - for (let tab of tabs) { // eslint-disable-line prefer-const + for (const tab of tabs) { xulWindow.gBrowser.moveTabTo(tab, pos++); } }); }, - getTabs(args) { + async getTabs(args) { if (!("userContextId" in args)) { - return Promise.reject("getTabs must be called with userContextId argument."); + return new Error("getTabs must be called with userContextId argument."); } this._remapTabsIfMissing(args.userContextId); if (!this._isKnownContainer(args.userContextId)) { - return Promise.resolve([]); + return []; } - return new Promise((resolve, reject) => { - const list = []; - this._containerTabIterator(args.userContextId, tab => { - list.push(this._createTabObject(tab)); - }); - - const promises = []; - - for (let object of list) { // eslint-disable-line prefer-const - promises.push(getFavicon(object.url).then(url => { - object.favicon = url; - }).catch(() => { - object.favicon = ""; - })); - } - - Promise.all(promises).then(() => { - resolve(list.concat(this._identitiesState[args.userContextId].hiddenTabs)); - }).catch((e) => { - reject(e); - }); + const promises = []; + this._matchTabsByContainer(args.userContextId).forEach((tab) => { + promises.push(this._createTabObject(tab)); }); + + const list = await Promise.all(promises); + return list.concat(this._identitiesState[args.userContextId].hiddenTabs); }, showTab(args) { @@ -763,7 +764,7 @@ const ContainerService = { return; } - for (let tab of tabs) { // eslint-disable-line prefer-const + for (const tab of tabs) { if (tab.id === args.tabId) { tab.window.activate(); tab.activate(); @@ -793,11 +794,7 @@ const ContainerService = { "clickedContainerTabCount": this._containerTabCount(args.userContextId), }); - // Let's create a list of the tabs. - const list = []; - this._containerTabIterator(args.userContextId, tab => { - list.push(tab); - }); + const list = this._matchTabsByContainer(args.userContextId); // Nothing to do if (list.length === 0 && @@ -840,74 +837,37 @@ const ContainerService = { }, openTab(args) { - return new Promise(resolve => { - if ("window" in args && args.window) { - resolve(args.window); - } else { - this._recentBrowserWindow().then(browserWin => { - resolve(browserWin); - }).catch(() => {}); - } - }).then(browserWin => { - const userContextId = ("userContextId" in args) ? args.userContextId : 0; - const source = ("source" in args) ? args.source : null; - const nofocus = ("nofocus" in args) ? args.nofocus : false; - - // Only send telemetry for tabs opened by UI - i.e., not via showTabs - if (source && userContextId) { - this.sendTelemetryPayload({ - "event": "open-tab", - "eventSource": source, - "userContextId": userContextId, - "clickedContainerTabCount": this._containerTabCount(userContextId) - }); - } - - let promise; - if (userContextId) { - promise = this.showTabs(args); - } else { - promise = Promise.resolve(null); - } - - return promise.then(() => { - const tab = browserWin.gBrowser.addTab(args.url || DEFAULT_TAB, { userContextId }); - if (!nofocus) { - browserWin.gBrowser.selectedTab = tab; - browserWin.focusAndSelectUrlBar(); - } - - if (args.pinned) { - browserWin.gBrowser.pinTab(tab); - } - return true; - }); - }).catch(() => false); + return this.triggerBackgroundCallback(args, "open-tab"); }, // Identities management - - queryIdentities() { + queryIdentitiesState() { return new Promise(resolve => { - const identities = []; + const identities = {}; ContextualIdentityProxy.getIdentities().forEach(identity => { this._remapTabsIfMissing(identity.userContextId); - const convertedIdentity = this._convert(identity); - identities.push(convertedIdentity); + const convertedIdentity = { + hasHiddenTabs: !!this._identitiesState[identity.userContextId].hiddenTabs.length, + hasOpenTabs: !!this._identitiesState[identity.userContextId].openTabs + }; + + identities[identity.userContextId] = convertedIdentity; }); resolve(identities); }); }, - getIdentity(args) { - if (!("userContextId" in args)) { - return Promise.reject("getIdentity must be called with userContextId argument."); - } + queryIdentities() { + return new Promise(resolve => { + const identities = ContextualIdentityProxy.getIdentities(); + identities.forEach(identity => { + this._remapTabsIfMissing(identity.userContextId); + }); - const identity = ContextualIdentityProxy.getIdentityFromId(args.userContextId); - return Promise.resolve(identity ? this._convert(identity) : null); + resolve(identities); + }); }, // Preferences @@ -974,26 +934,25 @@ const ContainerService = { } const userContextId = ContainerService._getUserContextIdFromTab(tab); - return ContainerService.getIdentity({userContextId}).then(identity => { - const hbox = viewFor(tab.window).document.getElementById("userContext-icons"); + const identity = ContextualIdentityProxy.getIdentityFromId(userContextId); + const hbox = viewFor(tab.window).document.getElementById("userContext-icons"); - if (!identity) { - hbox.setAttribute("data-identity-color", ""); - return; - } + if (!identity) { + hbox.setAttribute("data-identity-color", ""); + return Promise.resolve(null); + } - hbox.setAttribute("data-identity-color", identity.color); + hbox.setAttribute("data-identity-color", identity.color); - const label = viewFor(tab.window).document.getElementById("userContext-label"); - label.setAttribute("value", identity.name); - label.style.color = ContainerService._fromNameToColor(identity.color); + const label = viewFor(tab.window).document.getElementById("userContext-label"); + label.setAttribute("value", identity.name); + label.style.color = ContainerService._fromNameToColor(identity.color); - const indicator = viewFor(tab.window).document.getElementById("userContext-indicator"); - indicator.setAttribute("data-identity-icon", identity.image); - indicator.style.listStyleImage = ""; - }).then(() => { - return this._restyleTab(tab); - }); + const indicator = viewFor(tab.window).document.getElementById("userContext-indicator"); + indicator.setAttribute("data-identity-icon", identity.icon); + indicator.style.listStyleImage = ""; + + return this._restyleTab(tab); }, _restyleTab(tab) { @@ -1001,12 +960,11 @@ const ContainerService = { return Promise.resolve(null); } const userContextId = ContainerService._getUserContextIdFromTab(tab); - return ContainerService.getIdentity({userContextId}).then(identity => { - if (!identity) { - return; - } - viewFor(tab).setAttribute("data-identity-color", identity.color); - }); + const identity = ContextualIdentityProxy.getIdentityFromId(userContextId); + if (!identity) { + return Promise.resolve(null); + } + return Promise.resolve(viewFor(tab).setAttribute("data-identity-color", identity.color)); }, // Uninstallation @@ -1065,7 +1023,7 @@ const ContainerService = { const preInstalledIdentities = data.preInstalledIdentities; ContextualIdentityProxy.getIdentities().forEach(identity => { if (!preInstalledIdentities.includes(identity.userContextId)) { - ContextualIdentityProxy.remove(identity.userContextId); + ContextualIdentityService.remove(identity.userContextId); } else { // Let's cleanup all the cookies for this container. Services.obs.notifyObservers(null, "clear-origin-attributes-data", @@ -1203,7 +1161,7 @@ ContainerWindow.prototype = { } }, - _configurePlusButtonMenu() { + async _configurePlusButtonMenu() { const mainPopupSetElement = this._window.document.getElementById("mainPopupSet"); // Let's remove all the previous panels. @@ -1230,21 +1188,21 @@ ContainerWindow.prototype = { this._cleanAllTimeouts(); }); - return ContainerService.queryIdentities().then(identities => { + try { + const identities = await ContainerService.queryIdentities(); identities.forEach(identity => { const menuItemElement = this._window.document.createElementNS(XUL_NS, "menuitem"); this._panelElement.appendChild(menuItemElement); menuItemElement.className = "menuitem-iconic"; menuItemElement.setAttribute("label", identity.name); menuItemElement.setAttribute("data-usercontextid", identity.userContextId); - menuItemElement.setAttribute("data-identity-icon", identity.image); + menuItemElement.setAttribute("data-identity-icon", identity.icon); menuItemElement.setAttribute("data-identity-color", identity.color); menuItemElement.addEventListener("command", (e) => { ContainerService.openTab({ userContextId: identity.userContextId, - source: "tab-bar", - window: this._window, + source: "tab-bar" }); e.stopPropagation(); }); @@ -1257,9 +1215,9 @@ ContainerWindow.prototype = { this._panelElement.appendChild(menuItemElement); }); - }).catch(() => { + } catch (e) { this.hidePanel(); - }); + } }, _configureTabStyle() { @@ -1280,8 +1238,7 @@ ContainerWindow.prototype = { const userContextId = parseInt(e.target.getAttribute("data-usercontextid"), 10); ContainerService.openTab({ userContextId: userContextId, - source: "file-menu", - window: this._window, + source: "file-menu" }); }); }, @@ -1296,8 +1253,7 @@ ContainerWindow.prototype = { }).then(() => { return ContainerService.openTab({ userContextId, - source: "alltabs-menu", - window: this._window, + source: "alltabs-menu" }); }).catch(() => {}); }); @@ -1399,7 +1355,7 @@ ContainerWindow.prototype = { menuitem.classList.add("menuitem-iconic"); menuitem.setAttribute("data-usercontextid", identity.userContextId); menuitem.setAttribute("data-identity-color", identity.color); - menuitem.setAttribute("data-identity-icon", identity.image); + menuitem.setAttribute("data-identity-icon", identity.icon); fragment.appendChild(menuitem); }); diff --git a/webextension/background.js b/webextension/background.js index 72bd8b8..0b52532 100644 --- a/webextension/background.js +++ b/webextension/background.js @@ -1,12 +1,7 @@ const MAJOR_VERSIONS = ["2.3.0"]; +const LOOKUP_KEY = "$ref"; const assignManager = { - CLOSEABLE_WINDOWS: new Set([ - "about:startpage", - "about:newtab", - "about:home", - "about:blank" - ]), MENU_ASSIGN_ID: "open-in-this-container", MENU_REMOVE_ID: "remove-open-in-this-container", storageArea: { @@ -133,14 +128,14 @@ const assignManager = { We aim to open the new assigned container tab / warning prompt in it's own tab: - As the history won't span from one container to another it seems most sane to not try and reopen a tab on history.back() - When users open a new tab themselves we want to make sure we don't end up with three tabs as per: https://github.com/mozilla/testpilot-containers/issues/421 - If we are coming from an internal url that are used for the new tab page (CLOSEABLE_WINDOWS), we can safely close as user is unlikely losing history + If we are coming from an internal url that are used for the new tab page (NEW_TAB_PAGES), we can safely close as user is unlikely losing history Detecting redirects on "new tab" opening actions is pretty hard as we don't get tab history: - Redirects happen from Short URLs and tracking links that act as a gateway - Extensions don't provide a way to history crawl for tabs, we could inject content scripts to do this however they don't run on about:blank so this would likely be just as hacky. We capture the time the tab was created and close if it was within the timeout to try to capture pages which haven't had user interaction or history. */ - if (this.CLOSEABLE_WINDOWS.has(tab.url) + if (backgroundLogic.NEW_TAB_PAGES.has(tab.url) || (messageHandler.lastCreatedTab && messageHandler.lastCreatedTab.id === tab.id)) { browser.tabs.remove(tab.id); @@ -218,7 +213,7 @@ const assignManager = { 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: `firefox-container-${userContextId}`, index}); + browser.tabs.create({url, cookieStoreId: backgroundLogic.cookieStoreId(userContextId), index}); backgroundLogic.sendTelemetryPayload({ event: "auto-reload-page-in-container", userContextId: userContextId, @@ -229,7 +224,7 @@ const assignManager = { userContextId: userContextId, }); const confirmUrl = `${loadPage}?url=${url}`; - browser.tabs.create({url: confirmUrl, cookieStoreId: `firefox-container-${userContextId}`, index}).then(() => { + browser.tabs.create({url: confirmUrl, cookieStoreId: backgroundLogic.cookieStoreId(userContextId), index}).then(() => { // We don't want to sync this URL ever nor clutter the users history browser.history.deleteUrl({url: confirmUrl}); }).catch((e) => { @@ -241,6 +236,13 @@ const assignManager = { const backgroundLogic = { + NEW_TAB_PAGES: new Set([ + "about:startpage", + "about:newtab", + "about:home", + "about:blank" + ]), + deleteContainer(userContextId) { this.sendTelemetryPayload({ event: "delete-container", @@ -291,6 +293,41 @@ const backgroundLogic = { }); }, + openTab(options) { + let url = options.url || undefined; + const userContextId = ("userContextId" in options) ? options.userContextId : 0; + const active = ("nofocus" in options) ? options.nofocus : true; + const source = ("source" in options) ? options.source : null; + + // Only send telemetry for tabs opened by UI - i.e., not via showTabs + if (source && userContextId) { + this.sendTelemetryPayload({ + "event": "open-tab", + "eventSource": source, + "userContextId": userContextId, + "clickedContainerTabCount": LOOKUP_KEY + }); + } + // Autofocus url bar will happen in 54: https://bugzilla.mozilla.org/show_bug.cgi?id=1295072 + + // We can't open new tab pages, so open a blank tab. Used in tab un-hide + if (this.NEW_TAB_PAGES.has(url)) { + url = undefined; + } + + // Unhide all hidden tabs + browser.runtime.sendMessage({ + method: "showTabs", + userContextId: options.userContextId + }); + return browser.tabs.create({ + url, + active, + pinned: options.pinned || false, + cookieStoreId: backgroundLogic.cookieStoreId(options.userContextId) + }); + }, + sendTelemetryPayload(message = {}) { if (!message.event) { throw new Error("Missing event name for telemetry"); @@ -317,6 +354,7 @@ const messageHandler = { LAST_CREATED_TAB_TIMER: 2000, init() { + // Handles messages from webextension/js/popup.js browser.runtime.onMessage.addListener((m) => { let response; @@ -327,6 +365,10 @@ const messageHandler = { case "createOrUpdateContainer": response = backgroundLogic.createOrUpdateContainer(m.message); break; + case "openTab": + // Same as open-tab for index.js + response = backgroundLogic.openTab(m.message); + break; case "neverAsk": assignManager._neverAsk(m); break; @@ -341,6 +383,9 @@ const messageHandler = { case "lightweight-theme-changed": themeManager.update(m.message); break; + case "open-tab": + backgroundLogic.openTab(m.message); + break; default: throw new Error(`Unhandled message type: ${m.message}`); } diff --git a/webextension/js/popup.js b/webextension/js/popup.js index c1d14a3..26afa59 100644 --- a/webextension/js/popup.js +++ b/webextension/js/popup.js @@ -116,13 +116,27 @@ const Logic = { }); }, + userContextId(cookieStoreId = "") { + const userContextId = cookieStoreId.replace("firefox-container-", ""); + return (userContextId !== cookieStoreId) ? Number(userContextId) : false; + }, + refreshIdentities() { - return browser.runtime.sendMessage({ - method: "queryIdentities" - }) - .then(identities => { - this._identities = identities; - }); + return Promise.all([ + browser.contextualIdentities.query({}), + browser.runtime.sendMessage({ + method: "queryIdentitiesState" + }) + ]).then(([identities, state]) => { + this._identities = identities.map((identity) => { + const stateObject = state[Logic.userContextId(identity.cookieStoreId)]; + if (stateObject) { + identity.hasOpenTabs = stateObject.hasOpenTabs; + identity.hasHiddenTabs = stateObject.hasHiddenTabs; + } + return identity; + }); + }).catch((e) => {throw e;}); }, showPanel(panel, currentIdentity = null) { @@ -371,7 +385,7 @@ Logic.registerPanel(P_CONTAINERS_LIST, { context.innerHTML = escaped`
@@ -393,8 +407,10 @@ Logic.registerPanel(P_CONTAINERS_LIST, { || e.type === "keydown") { browser.runtime.sendMessage({ method: "openTab", - userContextId: identity.userContextId, - source: "pop-up" + message: { + userContextId: Logic.userContextId(identity.cookieStoreId), + source: "pop-up" + } }).then(() => { window.close(); }).catch(() => { @@ -437,7 +453,7 @@ Logic.registerPanel(P_CONTAINER_INFO, { const identity = Logic.currentIdentity(); browser.runtime.sendMessage({ method: identity.hasHiddenTabs ? "showTabs" : "hideTabs", - userContextId: identity.userContextId + userContextId: Logic.userContextId(identity.cookieStoreId) }).then(() => { window.close(); }).catch(() => { @@ -467,7 +483,7 @@ Logic.registerPanel(P_CONTAINER_INFO, { Logic.addEnterHandler(moveTabsEl, () => { browser.runtime.sendMessage({ method: "moveTabsToWindow", - userContextId: Logic.currentIdentity().userContextId, + userContextId: Logic.userContextId(Logic.currentIdentity().cookieStoreId), }).then(() => { window.close(); }).catch((e) => { throw e; }); @@ -486,7 +502,7 @@ Logic.registerPanel(P_CONTAINER_INFO, { document.getElementById("container-info-name").textContent = identity.name; const icon = document.getElementById("container-info-icon"); - icon.setAttribute("data-identity-icon", identity.image); + icon.setAttribute("data-identity-icon", identity.icon); icon.setAttribute("data-identity-color", identity.color); // Show or not the has-tabs section. @@ -509,7 +525,7 @@ Logic.registerPanel(P_CONTAINER_INFO, { // Let's retrieve the list of tabs. return browser.runtime.sendMessage({ method: "getTabs", - userContextId: identity.userContextId, + userContextId: Logic.userContextId(identity.cookieStoreId), }).then(this.buildInfoTable); }, @@ -568,7 +584,7 @@ Logic.registerPanel(P_CONTAINERS_EDIT, {
@@ -639,7 +655,7 @@ Logic.registerPanel(P_CONTAINER_EDIT, { return browser.runtime.sendMessage({ method: "createOrUpdateContainer", message: { - userContextId: identity.userContextId || false, + userContextId: Logic.userContextId(identity.cookieStoreId) || false, params: { name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(), icon: formValues.get("container-icon") || DEFAULT_ICON, @@ -691,7 +707,7 @@ Logic.registerPanel(P_CONTAINER_EDIT, { colorInput.checked = colorInput.value === identity.color; }); [...document.querySelectorAll("[name='container-icon']")].forEach(iconInput => { - iconInput.checked = iconInput.value === identity.image; + iconInput.checked = iconInput.value === identity.icon; }); return Promise.resolve(null); @@ -717,7 +733,7 @@ Logic.registerPanel(P_CONTAINER_DELETE, { if you want to do anything post delete do it in the background script. Browser console currently warns about not listening also. */ - Logic.removeIdentity(Logic.currentIdentity().userContextId).then(() => { + Logic.removeIdentity(Logic.userContextId(Logic.currentIdentity().cookieStoreId)).then(() => { return Logic.refreshIdentities(); }).then(() => { Logic.showPreviousPanel(); @@ -735,7 +751,7 @@ Logic.registerPanel(P_CONTAINER_DELETE, { document.getElementById("delete-container-name").textContent = identity.name; const icon = document.getElementById("delete-container-icon"); - icon.setAttribute("data-identity-icon", identity.image); + icon.setAttribute("data-identity-icon", identity.icon); icon.setAttribute("data-identity-color", identity.color); return Promise.resolve(null); From 1819e6cde9a5dbd3076268eb4097bee001625654 Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Wed, 17 May 2017 17:38:55 +0100 Subject: [PATCH 19/21] Remove underline from buttons. Fixes: #514 --- webextension/css/popup.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/webextension/css/popup.css b/webextension/css/popup.css index 607563c..d053693 100644 --- a/webextension/css/popup.css +++ b/webextension/css/popup.css @@ -279,6 +279,12 @@ manage things like container crud */ background-color: rgba(0, 0, 0, 0.08); } +.pop-button a, +.panel-footer a, +.panel-footer-secondary a { + text-decoration: none; +} + .pop-button-image { block-size: 20px; flex: 0 0 20px; From fd918408f4c02b6f050a78285e7b2ceac9562ff6 Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Wed, 24 May 2017 02:35:48 +0100 Subject: [PATCH 20/21] Removal of tab state context menu as unable to detect tab assignment state. Fixes #520 --- webextension/background.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webextension/background.js b/webextension/background.js index 0b52532..8f4278c 100644 --- a/webextension/background.js +++ b/webextension/background.js @@ -201,7 +201,7 @@ const assignManager = { id: menuId, title: `${prefix} Always Open in This Container`, checked: true, - contexts: ["all", "tab"], + contexts: ["all"], }); }).catch((e) => { throw e; From 6a10c1c970cf7a70729b2d669a175ae007b35141 Mon Sep 17 00:00:00 2001 From: groovecoder Date: Wed, 24 May 2017 12:11:24 -0500 Subject: [PATCH 21/21] bump version to 2.3.0 --- package.json | 2 +- webextension/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 30d562f..020a9b4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "testpilot-containers", "title": "Containers Experiment", "description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.", - "version": "2.2.0", + "version": "2.3.0", "author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston", "bugs": { "url": "https://github.com/mozilla/testpilot-containers/issues" diff --git a/webextension/manifest.json b/webextension/manifest.json index 910aa8e..b99a5f3 100644 --- a/webextension/manifest.json +++ b/webextension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Containers Experiment", - "version": "2.2.0", + "version": "2.3.0", "description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.", "icons": {