Merge pull request #532 from mozilla/master

Merge 2.3.0 to production
This commit is contained in:
luke crouch 2017-05-24 15:30:49 -05:00 committed by GitHub
commit 63847f4d2d
10 changed files with 646 additions and 437 deletions

View file

@ -1,13 +1,13 @@
module.exports = { module.exports = {
"parserOptions": {
"ecmaVersion": 8
},
"env": { "env": {
"browser": true, "browser": true,
"es6": true, "es6": true,
"node": true, "node": true,
"webextensions": true "webextensions": true
}, },
"extends": [
"eslint:recommended"
],
"globals": { "globals": {
"CustomizableUI": true, "CustomizableUI": true,
"CustomizableWidgets": true, "CustomizableWidgets": true,
@ -16,7 +16,10 @@ module.exports = {
}, },
"plugins": [ "plugins": [
"promise", "promise",
"no-unescaped" "no-unsanitized"
],
"extends": [
"eslint:recommended"
], ],
"root": true, "root": true,
"rules": { "rules": {
@ -29,8 +32,18 @@ module.exports = {
"promise/no-promise-in-callback": "warn", "promise/no-promise-in-callback": "warn",
"promise/no-return-wrap": "error", "promise/no-return-wrap": "error",
"promise/param-names": "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", "eqeqeq": "error",
"indent": ["error", 2], "indent": ["error", 2],

526
index.js
View file

@ -4,6 +4,7 @@
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const DEFAULT_TAB = "about:newtab"; const DEFAULT_TAB = "about:newtab";
const LOOKUP_KEY = "$ref";
const SHOW_MENU_TIMEOUT = 100; const SHOW_MENU_TIMEOUT = 100;
const HIDE_MENU_TIMEOUT = 300; 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:///modules/sessionstore/SessionStore.jsm");
Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Services.jsm");
// ----------------------------------------------------------------------------
// ContextualIdentityProxy // ContextualIdentityProxy
const ContextualIdentityProxy = { const ContextualIdentityProxy = {
getIdentities() { getIdentities() {
let response;
if ("getPublicIdentities" in ContextualIdentityService) { if ("getPublicIdentities" in ContextualIdentityService) {
return ContextualIdentityService.getPublicIdentities(); response = ContextualIdentityService.getPublicIdentities();
} else {
response = ContextualIdentityService.getIdentities();
} }
return ContextualIdentityService.getIdentities(); return response.map((identity) => {
}, return this._convert(identity);
});
getUserContextLabel(userContextId) {
return ContextualIdentityService.getUserContextLabel(userContextId);
}, },
getIdentityFromId(userContextId) { getIdentityFromId(userContextId) {
let response;
if ("getPublicIdentityFromId" in ContextualIdentityService) { if ("getPublicIdentityFromId" in ContextualIdentityService) {
return ContextualIdentityService.getPublicIdentityFromId(userContextId); response = ContextualIdentityService.getPublicIdentityFromId(userContextId);
} else {
response = ContextualIdentityService.getIdentityFromId(userContextId);
} }
if (response) {
return ContextualIdentityService.getIdentityFromId(userContextId); return this._convert(response);
}
return response;
}, },
create(name, icon, color) { _convert(identity) {
return ContextualIdentityService.create(name, icon, color); 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,12 +122,14 @@ const ContainerService = {
_containerWasEnabled: false, _containerWasEnabled: false,
_onBackgroundConnectCallback: null, _onBackgroundConnectCallback: null,
init(installation, reason) { async init(installation, reason) {
// If we are just been installed, we must store some information for the // If we are just been installed, we must store some information for the
// uninstallation. This object contains also a version number, in case we // uninstallation. This object contains also a version number, in case we
// need to implement a migration in the future. // need to implement a migration in the future.
// In 1.1.1 and less we deleted savedConfiguration on upgrade so we need to rebuild // In 1.1.1 and less we deleted savedConfiguration on upgrade so we need to rebuild
if (installation && (reason !== "upgrade" || !ss.storage.savedConfiguration)) { if (!("savedConfiguration" in ss.storage) ||
!("prefs" in ss.storage.savedConfiguration) ||
(installation && reason !== "upgrade")) {
let preInstalledIdentities = []; // eslint-disable-line prefer-const let preInstalledIdentities = []; // eslint-disable-line prefer-const
ContextualIdentityProxy.getIdentities().forEach(identity => { ContextualIdentityProxy.getIdentities().forEach(identity => {
preInstalledIdentities.push(identity.userContextId); preInstalledIdentities.push(identity.userContextId);
@ -143,29 +148,40 @@ const ContainerService = {
ss.storage.savedConfiguration = object; ss.storage.savedConfiguration = object;
// Maybe rename the Banking container.
if (prefService.get("privacy.userContext.enabled") !== true) { if (prefService.get("privacy.userContext.enabled") !== true) {
// Maybe rename the Banking container.
const identity = ContextualIdentityProxy.getIdentityFromId(3); const identity = ContextualIdentityProxy.getIdentityFromId(3);
if (identity && identity.l10nID === "userContextBanking.label") { if (identity && identity.l10nID === "userContextBanking.label") {
ContextualIdentityProxy.update(identity.userContextId, ContextualIdentityService.update(identity.userContextId,
"Finance", "Finance",
identity.icon, identity.icon,
identity.color); identity.color);
} }
}
// Let's create the default containers in case there are none. // Let's create the default containers in case there are none.
if (prefService.get("privacy.userContext.enabled") !== true && if (ss.storage.savedConfiguration.preInstalledIdentities.length === 0) {
ss.storage.savedConfiguration.preInstalledIdentities.length === 0) { // Note: we have to create them in this way because there is no way to
// Note: we have to create them in this way because there is no way to // reuse the same ID and the localized strings.
// reuse the same ID and the localized strings. ContextualIdentityService.create("Personal", "fingerprint", "blue");
ContextualIdentityService.create("Personal", "fingerprint", "blue"); ContextualIdentityService.create("Work", "briefcase", "orange");
ContextualIdentityService.create("Work", "briefcase", "orange"); ContextualIdentityService.create("Finance", "dollar", "green");
ContextualIdentityService.create("Finance", "dollar", "green"); ContextualIdentityService.create("Shopping", "cart", "pink");
ContextualIdentityService.create("Shopping", "cart", "pink"); }
} }
} }
// 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. // Let's see if containers were enabled before this addon.
this._containerWasEnabled = this._containerWasEnabled =
ss.storage.savedConfiguration.prefs["privacy.userContext.enabled"]; ss.storage.savedConfiguration.prefs["privacy.userContext.enabled"];
@ -191,16 +207,14 @@ const ContainerService = {
"sortTabs", "sortTabs",
"getTabs", "getTabs",
"showTab", "showTab",
"openTab",
"moveTabsToWindow", "moveTabsToWindow",
"queryIdentities", "queryIdentitiesState",
"getIdentity", "getIdentity",
"createIdentity",
"removeIdentity",
"updateIdentity",
"getPreference", "getPreference",
"sendTelemetryPayload", "sendTelemetryPayload",
"getTheme", "getTheme",
"refreshNeeded",
"forgetIdentityAndRefresh",
"checkIncompatibleAddons" "checkIncompatibleAddons"
]; ];
@ -253,7 +267,8 @@ const ContainerService = {
// WebExtension startup // WebExtension startup
webExtension.startup().then(api => { try {
const api = await webExtension.startup();
api.browser.runtime.onMessage.addListener((message, sender, sendReply) => { api.browser.runtime.onMessage.addListener((message, sender, sendReply) => {
if ("method" in message && methods.indexOf(message.method) !== -1) { if ("method" in message && methods.indexOf(message.method) !== -1) {
sendReply(this[message.method](message)); sendReply(this[message.method](message));
@ -261,9 +276,9 @@ const ContainerService = {
}); });
this.registerBackgroundConnection(api); this.registerBackgroundConnection(api);
}).catch(() => { } catch (e) {
throw new Error("WebExtension startup failed. Unable to continue."); throw new Error("WebExtension startup failed. Unable to continue.");
}); }
this._sendEvent = new Metrics({ this._sendEvent = new Metrics({
type: "sdk", type: "sdk",
@ -308,7 +323,7 @@ const ContainerService = {
}, },
registerBackgroundConnection(api) { registerBackgroundConnection(api) {
// This is only used for theme and container deletion notifications // This is only used for theme notifications and new tab
api.browser.runtime.onConnect.addListener((port) => { api.browser.runtime.onConnect.addListener((port) => {
this._onBackgroundConnectCallback = (message, topic) => { this._onBackgroundConnectCallback = (message, topic) => {
port.postMessage({ port.postMessage({
@ -325,13 +340,14 @@ const ContainerService = {
} }
}, },
observe(subject, topic) { async observe(subject, topic) {
if (topic === "lightweight-theme-changed") { if (topic === "lightweight-theme-changed") {
this.getTheme().then((theme) => { try {
const theme = await this.getTheme();
this.triggerBackgroundCallback(theme, topic); this.triggerBackgroundCallback(theme, topic);
}).catch(() => { } catch (e) {
throw new Error("Unable to get theme"); throw new Error("Unable to get theme");
}); }
} }
}, },
@ -396,18 +412,6 @@ const ContainerService = {
return containersCounts; 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 // 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 // releases, we have IDs to be used with a svg file. In this function
// we map URLs to svg IDs. // we map URLs to svg IDs.
@ -433,10 +437,6 @@ const ContainerService = {
// Helper methods for converting icons to names and names to icons. // Helper methods for converting icons to names and names to icons.
_fromNameToIcon(name) {
return this._fromNameOrIcon(name, "image", "");
},
_fromIconToName(icon) { _fromIconToName(icon) {
return this._fromNameOrIcon(icon, "name", "circle"); return this._fromNameOrIcon(icon, "name", "circle");
}, },
@ -456,16 +456,31 @@ const ContainerService = {
return parseInt(viewFor(tab).getAttribute("usercontextid") || 0, 10); return parseInt(viewFor(tab).getAttribute("usercontextid") || 0, 10);
}, },
_createTabObject(tab) { async _createTabObject(tab) {
return { title: tab.title, url: tab.url, id: tab.id, active: true }; 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) { _matchTabsByContainer(userContextId) {
for (let tab of tabs) { // eslint-disable-line prefer-const const matchedTabs = [];
for (const tab of tabs) {
if (userContextId === this._getUserContextIdFromTab(tab)) { if (userContextId === this._getUserContextIdFromTab(tab)) {
cb(tab); matchedTabs.push(tab);
} }
} }
return matchedTabs;
}, },
_createIdentityState() { _createIdentityState() {
@ -486,10 +501,7 @@ const ContainerService = {
}, },
_remapTabsFromUserContextId(userContextId) { _remapTabsFromUserContextId(userContextId) {
this._identitiesState[userContextId].openTabs = 0; this._identitiesState[userContextId].openTabs = this._matchTabsByContainer(userContextId).length;
this._containerTabIterator(userContextId, () => {
++this._identitiesState[userContextId].openTabs;
});
}, },
_remapTab(tab) { _remapTab(tab) {
@ -503,30 +515,25 @@ const ContainerService = {
return userContextId in this._identitiesState; return userContextId in this._identitiesState;
}, },
_closeTabs(tabsToClose) { async _closeTabs(tabsToClose) {
// We create a new tab only if the current operation closes all the // We create a new tab only if the current operation closes all the
// existing ones. // existing ones.
let promise; if (tabs.length === tabsToClose.length) {
if (tabs.length !== tabsToClose.length) { await this.openTab({});
promise = Promise.resolve(null);
} else {
promise = this.openTab({});
} }
return promise.then(() => { for (const tab of tabsToClose) {
for (let tab of tabsToClose) { // eslint-disable-line prefer-const // after .close() window is null. Let's take it now.
// after .close() window is null. Let's take it now. const window = viewFor(tab.window);
const window = viewFor(tab.window);
tab.close(); tab.close();
// forget about this tab. 0 is the index of the forgotten tab and 0 // forget about this tab. 0 is the index of the forgotten tab and 0
// means the last one. // means the last one.
try { try {
SessionStore.forgetClosedTab(window, 0); SessionStore.forgetClosedTab(window, 0);
} catch(e) {} // eslint-disable-line no-empty } catch (e) {} // eslint-disable-line no-empty
} }
}).catch(() => null);
}, },
_recentBrowserWindow() { _recentBrowserWindow() {
@ -553,6 +560,29 @@ const ContainerService = {
}; };
Object.assign(payload, args); 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); this._sendEvent(payload);
}, },
@ -573,51 +603,46 @@ const ContainerService = {
// Tabs management // Tabs management
hideTabs(args) { async hideTabs(args) {
if (!("userContextId" in 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); this._remapTabsIfMissing(args.userContextId);
if (!this._isKnownContainer(args.userContextId)) { if (!this._isKnownContainer(args.userContextId)) {
return Promise.resolve(null); return null;
} }
const containersCounts = this._containersCounts();
this.sendTelemetryPayload({ this.sendTelemetryPayload({
"event": "hide-tabs", "event": "hide-tabs",
"userContextId": args.userContextId, "userContextId": args.userContextId,
"clickedContainerTabCount": this._containerTabCount(args.userContextId), "clickedContainerTabCount": LOOKUP_KEY,
"shownContainersCount": containersCounts.shown, "shownContainersCount": LOOKUP_KEY,
"hiddenContainersCount": containersCounts.hidden, "hiddenContainersCount": LOOKUP_KEY,
"totalContainersCount": containersCounts.total "totalContainersCount": LOOKUP_KEY
}); });
const tabsToClose = []; const tabsToClose = [];
this._containerTabIterator(args.userContextId, tab => { const tabObjects = await Promise.all(this._matchTabsByContainer(args.userContextId).map((tab) => {
const object = this._createTabObject(tab); tabsToClose.push(tab);
return this._createTabObject(tab);
}));
tabObjects.forEach((object) => {
// This tab is going to be closed. Let's mark this tabObject as // This tab is going to be closed. Let's mark this tabObject as
// non-active. // non-active.
object.active = false; object.active = false;
getFavicon(object.url).then(url => {
object.favicon = url;
}).catch(() => {
object.favicon = "";
});
this._identitiesState[args.userContextId].hiddenTabs.push(object); this._identitiesState[args.userContextId].hiddenTabs.push(object);
tabsToClose.push(tab);
}); });
return this._closeTabs(tabsToClose).then(() => { await this._closeTabs(tabsToClose);
return this._syncTabs();
}); return this._syncTabs();
}, },
showTabs(args) { async showTabs(args) {
if (!("userContextId" in args)) { if (!("userContextId" in args)) {
return Promise.reject("showTabs must be called with userContextId argument."); return Promise.reject("showTabs must be called with userContextId argument.");
} }
@ -627,14 +652,13 @@ const ContainerService = {
return Promise.resolve(null); return Promise.resolve(null);
} }
const containersCounts = this._containersCounts();
this.sendTelemetryPayload({ this.sendTelemetryPayload({
"event": "show-tabs", "event": "show-tabs",
"userContextId": args.userContextId, "userContextId": args.userContextId,
"clickedContainerTabCount": this._containerTabCount(args.userContextId), "clickedContainerTabCount": LOOKUP_KEY,
"shownContainersCount": containersCounts.shown, "shownContainersCount": LOOKUP_KEY,
"hiddenContainersCount": containersCounts.hidden, "hiddenContainersCount": LOOKUP_KEY,
"totalContainersCount": containersCounts.total "totalContainersCount": LOOKUP_KEY
}); });
const promises = []; const promises = [];
@ -647,15 +671,14 @@ const ContainerService = {
userContextId: args.userContextId, userContextId: args.userContextId,
url: object.url, url: object.url,
nofocus: args.nofocus || false, nofocus: args.nofocus || false,
window: args.window || null, pinned: object.pinned,
})); }));
} }
this._identitiesState[args.userContextId].hiddenTabs = []; this._identitiesState[args.userContextId].hiddenTabs = [];
return Promise.all(promises).then(() => { await Promise.all(promises);
return this._syncTabs(); return this._syncTabs();
});
}, },
sortTabs() { sortTabs() {
@ -685,7 +708,7 @@ const ContainerService = {
// Let's collect UCIs/tabs for this window. // Let's collect UCIs/tabs for this window.
const map = new Map; const map = new Map;
for (let tab of tabs) { // eslint-disable-line prefer-const for (const tab of tabs) {
if (pinnedTabs && !tabsUtils.isPinned(tab)) { if (pinnedTabs && !tabsUtils.isPinned(tab)) {
// We don't have, or we already handled all the pinned tabs. // We don't have, or we already handled all the pinned tabs.
break; break;
@ -709,44 +732,29 @@ const ContainerService = {
// Let's move tabs. // Let's move tabs.
sortMap.forEach(tabs => { sortMap.forEach(tabs => {
for (let tab of tabs) { // eslint-disable-line prefer-const for (const tab of tabs) {
xulWindow.gBrowser.moveTabTo(tab, pos++); xulWindow.gBrowser.moveTabTo(tab, pos++);
} }
}); });
}, },
getTabs(args) { async getTabs(args) {
if (!("userContextId" in 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); this._remapTabsIfMissing(args.userContextId);
if (!this._isKnownContainer(args.userContextId)) { if (!this._isKnownContainer(args.userContextId)) {
return Promise.resolve([]); return [];
} }
return new Promise((resolve, reject) => { const promises = [];
const list = []; this._matchTabsByContainer(args.userContextId).forEach((tab) => {
this._containerTabIterator(args.userContextId, tab => { promises.push(this._createTabObject(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 list = await Promise.all(promises);
return list.concat(this._identitiesState[args.userContextId].hiddenTabs);
}, },
showTab(args) { showTab(args) {
@ -756,7 +764,7 @@ const ContainerService = {
return; return;
} }
for (let tab of tabs) { // eslint-disable-line prefer-const for (const tab of tabs) {
if (tab.id === args.tabId) { if (tab.id === args.tabId) {
tab.window.activate(); tab.window.activate();
tab.activate(); tab.activate();
@ -786,11 +794,7 @@ const ContainerService = {
"clickedContainerTabCount": this._containerTabCount(args.userContextId), "clickedContainerTabCount": this._containerTabCount(args.userContextId),
}); });
// Let's create a list of the tabs. const list = this._matchTabsByContainer(args.userContextId);
const list = [];
this._containerTabIterator(args.userContextId, tab => {
list.push(tab);
});
// Nothing to do // Nothing to do
if (list.length === 0 && if (list.length === 0 &&
@ -833,149 +837,36 @@ const ContainerService = {
}, },
openTab(args) { openTab(args) {
return new Promise(resolve => { return this.triggerBackgroundCallback(args, "open-tab");
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();
}
return true;
});
}).catch(() => false);
}, },
// Identities management // Identities management
queryIdentitiesState() {
queryIdentities() {
return new Promise(resolve => { return new Promise(resolve => {
const identities = []; const identities = {};
ContextualIdentityProxy.getIdentities().forEach(identity => { ContextualIdentityProxy.getIdentities().forEach(identity => {
this._remapTabsIfMissing(identity.userContextId); this._remapTabsIfMissing(identity.userContextId);
const convertedIdentity = this._convert(identity); const convertedIdentity = {
identities.push(convertedIdentity); hasHiddenTabs: !!this._identitiesState[identity.userContextId].hiddenTabs.length,
hasOpenTabs: !!this._identitiesState[identity.userContextId].openTabs
};
identities[identity.userContextId] = convertedIdentity;
}); });
resolve(identities); resolve(identities);
}); });
}, },
getIdentity(args) { queryIdentities() {
if (!("userContextId" in args)) { return new Promise(resolve => {
return Promise.reject("getIdentity must be called with userContextId argument."); const identities = ContextualIdentityProxy.getIdentities();
} identities.forEach(identity => {
this._remapTabsIfMissing(identity.userContextId);
});
const identity = ContextualIdentityProxy.getIdentityFromId(args.userContextId); resolve(identities);
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;
});
},
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 );
}); });
}, },
@ -1027,7 +918,7 @@ const ContainerService = {
return this._windowMap.get(window); return this._windowMap.get(window);
}, },
_refreshNeeded() { refreshNeeded() {
return this._configureWindows(); return this._configureWindows();
}, },
@ -1043,26 +934,25 @@ const ContainerService = {
} }
const userContextId = ContainerService._getUserContextIdFromTab(tab); const userContextId = ContainerService._getUserContextIdFromTab(tab);
return ContainerService.getIdentity({userContextId}).then(identity => { const identity = ContextualIdentityProxy.getIdentityFromId(userContextId);
const hbox = viewFor(tab.window).document.getElementById("userContext-icons"); const hbox = viewFor(tab.window).document.getElementById("userContext-icons");
if (!identity) { if (!identity) {
hbox.setAttribute("data-identity-color", ""); hbox.setAttribute("data-identity-color", "");
return; 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"); const label = viewFor(tab.window).document.getElementById("userContext-label");
label.setAttribute("value", identity.name); label.setAttribute("value", identity.name);
label.style.color = ContainerService._fromNameToColor(identity.color); label.style.color = ContainerService._fromNameToColor(identity.color);
const indicator = viewFor(tab.window).document.getElementById("userContext-indicator"); const indicator = viewFor(tab.window).document.getElementById("userContext-indicator");
indicator.setAttribute("data-identity-icon", identity.image); indicator.setAttribute("data-identity-icon", identity.icon);
indicator.style.listStyleImage = ""; indicator.style.listStyleImage = "";
}).then(() => {
return this._restyleTab(tab); return this._restyleTab(tab);
});
}, },
_restyleTab(tab) { _restyleTab(tab) {
@ -1070,12 +960,11 @@ const ContainerService = {
return Promise.resolve(null); return Promise.resolve(null);
} }
const userContextId = ContainerService._getUserContextIdFromTab(tab); const userContextId = ContainerService._getUserContextIdFromTab(tab);
return ContainerService.getIdentity({userContextId}).then(identity => { const identity = ContextualIdentityProxy.getIdentityFromId(userContextId);
if (!identity) { if (!identity) {
return; return Promise.resolve(null);
} }
viewFor(tab).setAttribute("data-identity-color", identity.color); return Promise.resolve(viewFor(tab).setAttribute("data-identity-color", identity.color));
});
}, },
// Uninstallation // Uninstallation
@ -1134,7 +1023,7 @@ const ContainerService = {
const preInstalledIdentities = data.preInstalledIdentities; const preInstalledIdentities = data.preInstalledIdentities;
ContextualIdentityProxy.getIdentities().forEach(identity => { ContextualIdentityProxy.getIdentities().forEach(identity => {
if (!preInstalledIdentities.includes(identity.userContextId)) { if (!preInstalledIdentities.includes(identity.userContextId)) {
ContextualIdentityProxy.remove(identity.userContextId); ContextualIdentityService.remove(identity.userContextId);
} else { } else {
// Let's cleanup all the cookies for this container. // Let's cleanup all the cookies for this container.
Services.obs.notifyObservers(null, "clear-origin-attributes-data", Services.obs.notifyObservers(null, "clear-origin-attributes-data",
@ -1157,6 +1046,11 @@ const ContainerService = {
// End-Of-Hack // End-Of-Hack
}, },
forgetIdentityAndRefresh(args) {
this._forgetIdentity(args.userContextId);
return this.refreshNeeded();
},
_forgetIdentity(userContextId = 0) { _forgetIdentity(userContextId = 0) {
for (let window of windows.browserWindows) { // eslint-disable-line prefer-const for (let window of windows.browserWindows) { // eslint-disable-line prefer-const
window = viewFor(window); window = viewFor(window);
@ -1267,7 +1161,7 @@ ContainerWindow.prototype = {
} }
}, },
_configurePlusButtonMenu() { async _configurePlusButtonMenu() {
const mainPopupSetElement = this._window.document.getElementById("mainPopupSet"); const mainPopupSetElement = this._window.document.getElementById("mainPopupSet");
// Let's remove all the previous panels. // Let's remove all the previous panels.
@ -1294,21 +1188,21 @@ ContainerWindow.prototype = {
this._cleanAllTimeouts(); this._cleanAllTimeouts();
}); });
return ContainerService.queryIdentities().then(identities => { try {
const identities = await ContainerService.queryIdentities();
identities.forEach(identity => { identities.forEach(identity => {
const menuItemElement = this._window.document.createElementNS(XUL_NS, "menuitem"); const menuItemElement = this._window.document.createElementNS(XUL_NS, "menuitem");
this._panelElement.appendChild(menuItemElement); this._panelElement.appendChild(menuItemElement);
menuItemElement.className = "menuitem-iconic"; menuItemElement.className = "menuitem-iconic";
menuItemElement.setAttribute("label", identity.name); menuItemElement.setAttribute("label", identity.name);
menuItemElement.setAttribute("data-usercontextid", identity.userContextId); 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.setAttribute("data-identity-color", identity.color);
menuItemElement.addEventListener("command", (e) => { menuItemElement.addEventListener("command", (e) => {
ContainerService.openTab({ ContainerService.openTab({
userContextId: identity.userContextId, userContextId: identity.userContextId,
source: "tab-bar", source: "tab-bar"
window: this._window,
}); });
e.stopPropagation(); e.stopPropagation();
}); });
@ -1321,9 +1215,9 @@ ContainerWindow.prototype = {
this._panelElement.appendChild(menuItemElement); this._panelElement.appendChild(menuItemElement);
}); });
}).catch(() => { } catch (e) {
this.hidePanel(); this.hidePanel();
}); }
}, },
_configureTabStyle() { _configureTabStyle() {
@ -1344,8 +1238,7 @@ ContainerWindow.prototype = {
const userContextId = parseInt(e.target.getAttribute("data-usercontextid"), 10); const userContextId = parseInt(e.target.getAttribute("data-usercontextid"), 10);
ContainerService.openTab({ ContainerService.openTab({
userContextId: userContextId, userContextId: userContextId,
source: "file-menu", source: "file-menu"
window: this._window,
}); });
}); });
}, },
@ -1360,8 +1253,7 @@ ContainerWindow.prototype = {
}).then(() => { }).then(() => {
return ContainerService.openTab({ return ContainerService.openTab({
userContextId, userContextId,
source: "alltabs-menu", source: "alltabs-menu"
window: this._window,
}); });
}).catch(() => {}); }).catch(() => {});
}); });
@ -1463,7 +1355,7 @@ ContainerWindow.prototype = {
menuitem.classList.add("menuitem-iconic"); menuitem.classList.add("menuitem-iconic");
menuitem.setAttribute("data-usercontextid", identity.userContextId); menuitem.setAttribute("data-usercontextid", identity.userContextId);
menuitem.setAttribute("data-identity-color", identity.color); menuitem.setAttribute("data-identity-color", identity.color);
menuitem.setAttribute("data-identity-icon", identity.image); menuitem.setAttribute("data-identity-icon", identity.icon);
fragment.appendChild(menuitem); fragment.appendChild(menuitem);
}); });

View file

@ -2,7 +2,7 @@
"name": "testpilot-containers", "name": "testpilot-containers",
"title": "Containers Experiment", "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.", "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", "author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
"bugs": { "bugs": {
"url": "https://github.com/mozilla/testpilot-containers/issues" "url": "https://github.com/mozilla/testpilot-containers/issues"
@ -12,7 +12,7 @@
"addons-linter": "^0.15.14", "addons-linter": "^0.15.14",
"deploy-txp": "^1.0.7", "deploy-txp": "^1.0.7",
"eslint": "^3.17.1", "eslint": "^3.17.1",
"eslint-plugin-no-unescaped": "^1.1.0", "eslint-plugin-no-unsanitized": "^2.0.0",
"eslint-plugin-promise": "^3.4.0", "eslint-plugin-promise": "^3.4.0",
"htmllint-cli": "^0.0.5", "htmllint-cli": "^0.0.5",
"jpm": "^1.2.2", "jpm": "^1.2.2",

View file

@ -1,10 +1,7 @@
const MAJOR_VERSIONS = ["2.3.0"];
const LOOKUP_KEY = "$ref";
const assignManager = { const assignManager = {
CLOSEABLE_WINDOWS: new Set([
"about:startpage",
"about:newtab",
"about:home",
"about:blank"
]),
MENU_ASSIGN_ID: "open-in-this-container", MENU_ASSIGN_ID: "open-in-this-container",
MENU_REMOVE_ID: "remove-open-in-this-container", MENU_REMOVE_ID: "remove-open-in-this-container",
storageArea: { storageArea: {
@ -58,22 +55,22 @@ const assignManager = {
} }
}, },
init() { _neverAsk(m) {
browser.runtime.onMessage.addListener((neverAskMessage) => { const pageUrl = m.pageUrl;
const pageUrl = neverAskMessage.pageUrl; if (m.neverAsk === true) {
if (neverAskMessage.neverAsk === true) { // If we have existing data and for some reason it hasn't been deleted etc lets update it
// If we have existing data and for some reason it hasn't been deleted etc lets update it this.storageArea.get(pageUrl).then((siteSettings) => {
this.storageArea.get(pageUrl).then((siteSettings) => { if (siteSettings) {
if (siteSettings) { siteSettings.neverAsk = true;
siteSettings.neverAsk = true; this.storageArea.set(pageUrl, siteSettings);
this.storageArea.set(pageUrl, siteSettings); }
} }).catch((e) => {
}).catch((e) => { throw e;
throw e; });
}); }
} },
});
init() {
browser.contextMenus.onClicked.addListener((info, tab) => { browser.contextMenus.onClicked.addListener((info, tab) => {
const userContextId = this.getUserContextIdFromCookieStore(tab); const userContextId = this.getUserContextIdFromCookieStore(tab);
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId} // Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
@ -97,8 +94,7 @@ const assignManager = {
message: `Successfully ${actionName} site to always open in this container`, message: `Successfully ${actionName} site to always open in this container`,
iconUrl: browser.extension.getURL("/img/onboarding-1.png") iconUrl: browser.extension.getURL("/img/onboarding-1.png")
}); });
browser.runtime.sendMessage({ backgroundLogic.sendTelemetryPayload({
method: "sendTelemetryPayload",
event: `${actionName}-container-assignment`, event: `${actionName}-container-assignment`,
userContextId: userContextId, userContextId: userContextId,
}); });
@ -132,14 +128,14 @@ const assignManager = {
We aim to open the new assigned container tab / warning prompt in it's own tab: 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() - 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 - 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: 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 - 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 - 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. 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. 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
&& messageHandler.lastCreatedTab.id === tab.id)) { && messageHandler.lastCreatedTab.id === tab.id)) {
browser.tabs.remove(tab.id); browser.tabs.remove(tab.id);
@ -217,20 +213,18 @@ const assignManager = {
const loadPage = browser.extension.getURL("confirm-page.html"); 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 the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there
if (neverAsk) { if (neverAsk) {
browser.tabs.create({url, cookieStoreId: `firefox-container-${userContextId}`, index}); browser.tabs.create({url, cookieStoreId: backgroundLogic.cookieStoreId(userContextId), index});
browser.runtime.sendMessage({ backgroundLogic.sendTelemetryPayload({
method: "sendTelemetryPayload",
event: "auto-reload-page-in-container", event: "auto-reload-page-in-container",
userContextId: userContextId, userContextId: userContextId,
}); });
} else { } else {
browser.runtime.sendMessage({ backgroundLogic.sendTelemetryPayload({
method: "sendTelemetryPayload",
event: "prompt-to-reload-page-in-container", event: "prompt-to-reload-page-in-container",
userContextId: userContextId, userContextId: userContextId,
}); });
const confirmUrl = `${loadPage}?url=${url}`; 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 // We don't want to sync this URL ever nor clutter the users history
browser.history.deleteUrl({url: confirmUrl}); browser.history.deleteUrl({url: confirmUrl});
}).catch((e) => { }).catch((e) => {
@ -240,6 +234,119 @@ const assignManager = {
} }
}; };
const backgroundLogic = {
NEW_TAB_PAGES: new Set([
"about:startpage",
"about:newtab",
"about:home",
"about:blank"
]),
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"
});
});
},
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");
}
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 = { const messageHandler = {
// After the timer completes we assume it's a tab the user meant to keep open // 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 // We use this to catch redirected tabs that have just opened
@ -247,6 +354,28 @@ const messageHandler = {
LAST_CREATED_TAB_TIMER: 2000, LAST_CREATED_TAB_TIMER: 2000,
init() { init() {
// Handles messages from webextension/js/popup.js
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 "openTab":
// Same as open-tab for index.js
response = backgroundLogic.openTab(m.message);
break;
case "neverAsk":
assignManager._neverAsk(m);
break;
}
return response;
});
// Handles messages from index.js // Handles messages from index.js
const port = browser.runtime.connect(); const port = browser.runtime.connect();
port.onMessage.addListener(m => { port.onMessage.addListener(m => {
@ -254,8 +383,8 @@ const messageHandler = {
case "lightweight-theme-changed": case "lightweight-theme-changed":
themeManager.update(m.message); themeManager.update(m.message);
break; break;
case "delete-container": case "open-tab":
assignManager.deleteContainer(m.message.userContextId); backgroundLogic.openTab(m.message);
break; break;
default: default:
throw new Error(`Unhandled message type: ${m.message}`); throw new Error(`Unhandled message type: ${m.message}`);
@ -408,8 +537,7 @@ const tabPageCounter = {
return; return;
} }
if (why === "user-closed-tab" && this.counters[tabId].tab) { if (why === "user-closed-tab" && this.counters[tabId].tab) {
browser.runtime.sendMessage({ backgroundLogic.sendTelemetryPayload({
method: "sendTelemetryPayload",
event: "page-requests-completed-per-tab", event: "page-requests-completed-per-tab",
userContextId: this.counters[tabId].tab.cookieStoreId, userContextId: this.counters[tabId].tab.cookieStoreId,
pageRequestCount: this.counters[tabId].tab.pageRequests pageRequestCount: this.counters[tabId].tab.pageRequests
@ -418,8 +546,7 @@ const tabPageCounter = {
// delete both the 'tab' and 'activity' counters // delete both the 'tab' and 'activity' counters
delete this.counters[tabId]; delete this.counters[tabId];
} else if (why === "user-went-idle" && this.counters[tabId].activity) { } else if (why === "user-went-idle" && this.counters[tabId].activity) {
browser.runtime.sendMessage({ backgroundLogic.sendTelemetryPayload({
method: "sendTelemetryPayload",
event: "page-requests-completed-per-activity", event: "page-requests-completed-per-activity",
userContextId: this.counters[tabId].activity.cookieStoreId, userContextId: this.counters[tabId].activity.cookieStoreId,
pageRequestCount: this.counters[tabId].activity.pageRequests pageRequestCount: this.counters[tabId].activity.pageRequests
@ -471,3 +598,22 @@ function disableAddon(tabId) {
browser.browserAction.disable(tabId); browser.browserAction.disable(tabId);
browser.browserAction.setTitle({ tabId, title: "Containers disabled in Private Browsing Mode" }); 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();

View file

@ -130,11 +130,13 @@ table {
color: white; color: white;
} }
.button.primary:hover { .button.primary:hover,
.button.primary:focus {
background-color: #0675d3; background-color: #0675d3;
} }
.button.secondary:hover { .button.secondary:hover,
.button.secondary:focus {
background-color: rgba(0, 0, 0, 0.05); background-color: rgba(0, 0, 0, 0.05);
} }
@ -198,7 +200,8 @@ table {
justify-content: center; justify-content: center;
} }
.panel-back-arrow:hover { .panel-back-arrow:hover,
.panel-back-arrow:focus {
background: #dedede; background: #dedede;
} }
@ -249,7 +252,8 @@ table {
transition: background-color 75ms; transition: background-color 75ms;
} }
.onboarding-button:hover { .onboarding-button:hover,
.onboarding-button:active {
background-color: #0675d3; background-color: #0675d3;
} }
@ -264,15 +268,23 @@ manage things like container crud */
} }
.pop-button:hover, .pop-button:hover,
.pop-button:focus,
.panel-footer-secondary:focus,
.panel-footer-secondary:hover { .panel-footer-secondary:hover {
background-color: rgba(0, 0, 0, 0.05); background-color: rgba(0, 0, 0, 0.05);
} }
.pop-button:active, .pop-button:focus,
.panel-footer-secondary:active { .panel-footer-secondary:focus {
background-color: rgba(0, 0, 0, 0.08); background-color: rgba(0, 0, 0, 0.08);
} }
.pop-button a,
.panel-footer a,
.panel-footer-secondary a {
text-decoration: none;
}
.pop-button-image { .pop-button-image {
block-size: 20px; block-size: 20px;
flex: 0 0 20px; flex: 0 0 20px;
@ -350,7 +362,8 @@ span ~ .panel-header-text {
transition: background-color 75ms; transition: background-color 75ms;
} }
.clickable.userContext-wrapper:hover { .container-panel-row:hover .clickable.userContext-wrapper,
.container-panel-row:focus .clickable.userContext-wrapper {
background: #f2f2f2; background: #f2f2f2;
} }
@ -360,7 +373,6 @@ span ~ .panel-header-text {
} }
/* .userContext-icon is used natively, Bug 1333811 was raised to fix */ /* .userContext-icon is used natively, Bug 1333811 was raised to fix */
.userContext-icon,
.usercontext-icon { .usercontext-icon {
background-image: var(--identity-icon); background-image: var(--identity-icon);
background-position: center center; background-position: center center;
@ -372,8 +384,8 @@ span ~ .panel-header-text {
flex: 0 0 48px; flex: 0 0 48px;
} }
.clickable:hover .userContext-icon, .container-panel-row:hover .clickable .usercontext-icon,
.clickable:hover .usercontext-icon { .container-panel-row:focus .clickable .usercontext-icon {
background-image: url('/img/container-newtab.svg'); background-image: url('/img/container-newtab.svg');
fill: 'gray'; fill: 'gray';
filter: url('/img/filters.svg#fill'); filter: url('/img/filters.svg#fill');
@ -482,7 +494,8 @@ span ~ .panel-header-text {
cursor: pointer; cursor: pointer;
} }
.clickable:hover { .clickable:hover,
.clickable:focus {
background-color: #ebebeb; background-color: #ebebeb;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View file

@ -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 // Sending neverAsk message to background to store for next time we see this process
if (neverAsk) { if (neverAsk) {
browser.runtime.sendMessage({ browser.runtime.sendMessage({
method: "neverAsk",
neverAsk: true, neverAsk: true,
pageUrl: redirectUrl pageUrl: redirectUrl
}).then(() => { }).then(() => {

View file

@ -12,6 +12,7 @@ const DEFAULT_ICON = "circle";
const P_ONBOARDING_1 = "onboarding1"; const P_ONBOARDING_1 = "onboarding1";
const P_ONBOARDING_2 = "onboarding2"; const P_ONBOARDING_2 = "onboarding2";
const P_ONBOARDING_3 = "onboarding3"; const P_ONBOARDING_3 = "onboarding3";
const P_ONBOARDING_4 = "onboarding4";
const P_CONTAINERS_LIST = "containersList"; const P_CONTAINERS_LIST = "containersList";
const P_CONTAINERS_EDIT = "containersEdit"; const P_CONTAINERS_EDIT = "containersEdit";
const P_CONTAINER_INFO = "containerInfo"; const P_CONTAINER_INFO = "containerInfo";
@ -54,6 +55,13 @@ function escaped(strings, ...values) {
return result.join(""); 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. // This object controls all the panels, identities and many other things.
const Logic = { const Logic = {
_identities: [], _identities: [],
@ -63,14 +71,19 @@ const Logic = {
_panels: {}, _panels: {},
init() { init() {
// Remove browserAction "upgraded" badge when opening panel
this.clearBrowserActionBadge();
// Retrieve the list of identities. // Retrieve the list of identities.
this.refreshIdentities() this.refreshIdentities()
// Routing to the correct panel. // Routing to the correct panel.
.then(() => { .then(() => {
// If localStorage is disabled, we don't show the onboarding. // 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); this.showPanel(P_CONTAINERS_LIST);
} else if (localStorage.getItem("onboarded3")) {
this.showPanel(P_ONBOARDING_4);
} else if (localStorage.getItem("onboarded2")) { } else if (localStorage.getItem("onboarded2")) {
this.showPanel(P_ONBOARDING_3); this.showPanel(P_ONBOARDING_3);
} else if (localStorage.getItem("onboarded1")) { } else if (localStorage.getItem("onboarded1")) {
@ -85,15 +98,47 @@ const Logic = {
}); });
}, },
refreshIdentities() { async clearBrowserActionBadge() {
return browser.runtime.sendMessage({ const extensionInfo = await getExtensionInfo();
method: "queryIdentities" const storage = await browser.storage.local.get({browserActionBadgesClicked: []});
}) browser.browserAction.setBadgeBackgroundColor({color: ""});
.then(identities => { browser.browserAction.setBadgeText({text: ""});
this._identities = identities; storage.browserActionBadgesClicked.push(extensionInfo.version);
browser.storage.local.set({browserActionBadgesClicked: storage.browserActionBadgesClicked});
},
addEnterHandler(element, handler) {
element.addEventListener("click", handler);
element.addEventListener("keydown", (e) => {
if (e.keyCode === 13) {
handler(e);
}
}); });
}, },
userContextId(cookieStoreId = "") {
const userContextId = cookieStoreId.replace("firefox-container-", "");
return (userContextId !== cookieStoreId) ? Number(userContextId) : false;
},
refreshIdentities() {
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) { showPanel(panel, currentIdentity = null) {
// Invalid panel... ?!? // Invalid panel... ?!?
if (!(panel in this._panels)) { if (!(panel in this._panels)) {
@ -141,6 +186,25 @@ const Logic = {
return this._currentIdentity; return this._currentIdentity;
}, },
sendTelemetryPayload(message = {}) {
if (!message.event) {
throw new Error("Missing event name for telemetry");
}
message.method = "sendTelemetryPayload";
browser.runtime.sendMessage(message);
},
removeIdentity(userContextId) {
if (!userContextId) {
return Promise.reject("removeIdentity must be called with userContextId argument.");
}
return browser.runtime.sendMessage({
method: "deleteContainer",
message: {userContextId}
});
},
generateIdentityName() { generateIdentityName() {
const defaultName = "Container #"; const defaultName = "Container #";
const ids = []; const ids = [];
@ -173,7 +237,7 @@ Logic.registerPanel(P_ONBOARDING_1, {
// This method is called when the object is registered. // This method is called when the object is registered.
initialize() { initialize() {
// Let's move to the next panel. // 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); localStorage.setItem("onboarded1", true);
Logic.showPanel(P_ONBOARDING_2); Logic.showPanel(P_ONBOARDING_2);
}); });
@ -194,7 +258,7 @@ Logic.registerPanel(P_ONBOARDING_2, {
// This method is called when the object is registered. // This method is called when the object is registered.
initialize() { initialize() {
// Let's move to the containers list panel. // 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); localStorage.setItem("onboarded2", true);
Logic.showPanel(P_ONBOARDING_3); Logic.showPanel(P_ONBOARDING_3);
}); });
@ -215,8 +279,29 @@ Logic.registerPanel(P_ONBOARDING_3, {
// This method is called when the object is registered. // This method is called when the object is registered.
initialize() { initialize() {
// Let's move to the containers list panel. // Let's move to the containers list panel.
document.querySelector("#onboarding-done-button").addEventListener("click", () => { Logic.addEnterHandler(document.querySelector("#onboarding-almost-done-button"), () => {
localStorage.setItem("onboarded3", true); 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); Logic.showPanel(P_CONTAINERS_LIST);
}); });
}, },
@ -235,19 +320,18 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
// This method is called when the object is registered. // This method is called when the object is registered.
initialize() { initialize() {
document.querySelector("#container-add-link").addEventListener("click", () => { Logic.addEnterHandler(document.querySelector("#container-add-link"), () => {
Logic.showPanel(P_CONTAINER_EDIT, { name: Logic.generateIdentityName() }); Logic.showPanel(P_CONTAINER_EDIT, { name: Logic.generateIdentityName() });
}); });
document.querySelector("#edit-containers-link").addEventListener("click", () => { Logic.addEnterHandler(document.querySelector("#edit-containers-link"), () => {
browser.runtime.sendMessage({ Logic.sendTelemetryPayload({
method: "sendTelemetryPayload",
event: "edit-containers" event: "edit-containers"
}); });
Logic.showPanel(P_CONTAINERS_EDIT); Logic.showPanel(P_CONTAINERS_EDIT);
}); });
document.querySelector("#sort-containers-link").addEventListener("click", () => { Logic.addEnterHandler(document.querySelector("#sort-containers-link"), () => {
browser.runtime.sendMessage({ browser.runtime.sendMessage({
method: "sortTabs" method: "sortTabs"
}).then(() => { }).then(() => {
@ -256,6 +340,30 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
window.close(); 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. // This method is called when the panel is shown.
@ -269,12 +377,15 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
const manage = document.createElement("td"); const manage = document.createElement("td");
tr.classList.add("container-panel-row"); tr.classList.add("container-panel-row");
tr.setAttribute("tabindex", "0");
context.classList.add("userContext-wrapper", "open-newtab", "clickable"); context.classList.add("userContext-wrapper", "open-newtab", "clickable");
manage.classList.add("show-tabs", "pop-button"); manage.classList.add("show-tabs", "pop-button");
context.innerHTML = escaped` context.innerHTML = escaped`
<div class="userContext-icon-wrapper open-newtab"> <div class="userContext-icon-wrapper open-newtab">
<div class="userContext-icon" <div class="usercontext-icon"
data-identity-icon="${identity.image}" data-identity-icon="${identity.icon}"
data-identity-color="${identity.color}"> data-identity-color="${identity.color}">
</div> </div>
</div> </div>
@ -290,12 +401,16 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
tr.appendChild(manage); tr.appendChild(manage);
} }
tr.addEventListener("click", e => { Logic.addEnterHandler(tr, e => {
if (e.target.matches(".open-newtab") || e.target.parentNode.matches(".open-newtab")) { if (e.target.matches(".open-newtab")
|| e.target.parentNode.matches(".open-newtab")
|| e.type === "keydown") {
browser.runtime.sendMessage({ browser.runtime.sendMessage({
method: "openTab", method: "openTab",
userContextId: identity.userContextId, message: {
source: "pop-up" userContextId: Logic.userContextId(identity.cookieStoreId),
source: "pop-up"
}
}).then(() => { }).then(() => {
window.close(); window.close();
}).catch(() => { }).catch(() => {
@ -311,6 +426,12 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
list.innerHTML = ""; list.innerHTML = "";
list.appendChild(fragment); 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(); return Promise.resolve();
}, },
@ -324,15 +445,15 @@ Logic.registerPanel(P_CONTAINER_INFO, {
// This method is called when the object is registered. // This method is called when the object is registered.
initialize() { initialize() {
document.querySelector("#close-container-info-panel").addEventListener("click", () => { Logic.addEnterHandler(document.querySelector("#close-container-info-panel"), () => {
Logic.showPreviousPanel(); Logic.showPreviousPanel();
}); });
document.querySelector("#container-info-hideorshow").addEventListener("click", () => { Logic.addEnterHandler(document.querySelector("#container-info-hideorshow"), () => {
const identity = Logic.currentIdentity(); const identity = Logic.currentIdentity();
browser.runtime.sendMessage({ browser.runtime.sendMessage({
method: identity.hasHiddenTabs ? "showTabs" : "hideTabs", method: identity.hasHiddenTabs ? "showTabs" : "hideTabs",
userContextId: identity.userContextId userContextId: Logic.userContextId(identity.cookieStoreId)
}).then(() => { }).then(() => {
window.close(); window.close();
}).catch(() => { }).catch(() => {
@ -359,13 +480,13 @@ Logic.registerPanel(P_CONTAINER_INFO, {
moveTabsEl.parentNode.insertBefore(fragment, moveTabsEl.nextSibling); moveTabsEl.parentNode.insertBefore(fragment, moveTabsEl.nextSibling);
} else { } else {
moveTabsEl.addEventListener("click", () => { Logic.addEnterHandler(moveTabsEl, () => {
return browser.runtime.sendMessage({ browser.runtime.sendMessage({
method: "moveTabsToWindow", method: "moveTabsToWindow",
userContextId: Logic.currentIdentity().userContextId, userContextId: Logic.userContextId(Logic.currentIdentity().cookieStoreId),
}).then(() => { }).then(() => {
window.close(); window.close();
}); }).catch((e) => { throw e; });
}); });
} }
}).catch(() => { }).catch(() => {
@ -381,7 +502,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
document.getElementById("container-info-name").textContent = identity.name; document.getElementById("container-info-name").textContent = identity.name;
const icon = document.getElementById("container-info-icon"); 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); icon.setAttribute("data-identity-color", identity.color);
// Show or not the has-tabs section. // Show or not the has-tabs section.
@ -404,7 +525,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
// Let's retrieve the list of tabs. // Let's retrieve the list of tabs.
return browser.runtime.sendMessage({ return browser.runtime.sendMessage({
method: "getTabs", method: "getTabs",
userContextId: identity.userContextId, userContextId: Logic.userContextId(identity.cookieStoreId),
}).then(this.buildInfoTable); }).then(this.buildInfoTable);
}, },
@ -422,7 +543,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
// On click, we activate this tab. But only if this tab is active. // On click, we activate this tab. But only if this tab is active.
if (tab.active) { if (tab.active) {
tr.classList.add("clickable"); tr.classList.add("clickable");
tr.addEventListener("click", () => { Logic.addEnterHandler(tr, () => {
browser.runtime.sendMessage({ browser.runtime.sendMessage({
method: "showTab", method: "showTab",
tabId: tab.id, tabId: tab.id,
@ -447,7 +568,7 @@ Logic.registerPanel(P_CONTAINERS_EDIT, {
// This method is called when the object is registered. // This method is called when the object is registered.
initialize() { initialize() {
document.querySelector("#exit-edit-mode-link").addEventListener("click", () => { Logic.addEnterHandler(document.querySelector("#exit-edit-mode-link"), () => {
Logic.showPanel(P_CONTAINERS_LIST); Logic.showPanel(P_CONTAINERS_LIST);
}); });
}, },
@ -462,8 +583,8 @@ Logic.registerPanel(P_CONTAINERS_EDIT, {
tr.innerHTML = escaped` tr.innerHTML = escaped`
<td class="userContext-wrapper"> <td class="userContext-wrapper">
<div class="userContext-icon-wrapper"> <div class="userContext-icon-wrapper">
<div class="userContext-icon" <div class="usercontext-icon"
data-identity-icon="${identity.image}" data-identity-icon="${identity.icon}"
data-identity-color="${identity.color}"> data-identity-color="${identity.color}">
</div> </div>
</div> </div>
@ -485,7 +606,7 @@ Logic.registerPanel(P_CONTAINERS_EDIT, {
tr.querySelector(".remove-container .pop-button-image").setAttribute("title", `Edit ${identity.name} container`); 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")) { if (e.target.matches(".edit-container-icon") || e.target.parentNode.matches(".edit-container-icon")) {
Logic.showPanel(P_CONTAINER_EDIT, identity); Logic.showPanel(P_CONTAINER_EDIT, identity);
} else if (e.target.matches(".delete-container-icon") || e.target.parentNode.matches(".delete-container-icon")) { } else if (e.target.matches(".delete-container-icon") || e.target.parentNode.matches(".delete-container-icon")) {
@ -513,17 +634,17 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
initialize() { initialize() {
this.initializeRadioButtons(); this.initializeRadioButtons();
document.querySelector("#edit-container-panel-back-arrow").addEventListener("click", () => { Logic.addEnterHandler(document.querySelector("#edit-container-panel-back-arrow"), () => {
Logic.showPreviousPanel(); Logic.showPreviousPanel();
}); });
document.querySelector("#edit-container-cancel-link").addEventListener("click", () => { Logic.addEnterHandler(document.querySelector("#edit-container-cancel-link"), () => {
Logic.showPreviousPanel(); Logic.showPreviousPanel();
}); });
this._editForm = document.getElementById("edit-container-panel-form"); this._editForm = document.getElementById("edit-container-panel-form");
const editLink = document.querySelector("#edit-container-ok-link"); 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)); editLink.addEventListener("submit", this._submitForm.bind(this));
this._editForm.addEventListener("submit", this._submitForm.bind(this)); this._editForm.addEventListener("submit", this._submitForm.bind(this));
}, },
@ -531,12 +652,16 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
_submitForm() { _submitForm() {
const identity = Logic.currentIdentity(); const identity = Logic.currentIdentity();
const formValues = new FormData(this._editForm); const formValues = new FormData(this._editForm);
browser.runtime.sendMessage({ return browser.runtime.sendMessage({
method: identity.userContextId ? "updateIdentity" : "createIdentity", method: "createOrUpdateContainer",
userContextId: identity.userContextId || 0, message: {
name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(), userContextId: Logic.userContextId(identity.cookieStoreId) || false,
icon: formValues.get("container-icon") || DEFAULT_ICON, params: {
color: formValues.get("container-color") || DEFAULT_COLOR, 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(() => { }).then(() => {
return Logic.refreshIdentities(); return Logic.refreshIdentities();
}).then(() => { }).then(() => {
@ -555,7 +680,7 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
const colorRadioFieldset = document.getElementById("edit-container-panel-choose-color"); const colorRadioFieldset = document.getElementById("edit-container-panel-choose-color");
colors.forEach((containerColor) => { colors.forEach((containerColor) => {
const templateInstance = document.createElement("span"); const templateInstance = document.createElement("span");
// eslint-disable-next-line no-unescaped/enforce // eslint-disable-next-line no-unsanitized/property
templateInstance.innerHTML = colorRadioTemplate(containerColor); templateInstance.innerHTML = colorRadioTemplate(containerColor);
colorRadioFieldset.appendChild(templateInstance); colorRadioFieldset.appendChild(templateInstance);
}); });
@ -568,7 +693,7 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
const iconRadioFieldset = document.getElementById("edit-container-panel-choose-icon"); const iconRadioFieldset = document.getElementById("edit-container-panel-choose-icon");
icons.forEach((containerIcon) => { icons.forEach((containerIcon) => {
const templateInstance = document.createElement("span"); const templateInstance = document.createElement("span");
// eslint-disable-next-line no-unescaped/enforce // eslint-disable-next-line no-unsanitized/property
templateInstance.innerHTML = iconRadioTemplate(containerIcon); templateInstance.innerHTML = iconRadioTemplate(containerIcon);
iconRadioFieldset.appendChild(templateInstance); iconRadioFieldset.appendChild(templateInstance);
}); });
@ -582,7 +707,7 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
colorInput.checked = colorInput.value === identity.color; colorInput.checked = colorInput.value === identity.color;
}); });
[...document.querySelectorAll("[name='container-icon']")].forEach(iconInput => { [...document.querySelectorAll("[name='container-icon']")].forEach(iconInput => {
iconInput.checked = iconInput.value === identity.image; iconInput.checked = iconInput.value === identity.icon;
}); });
return Promise.resolve(null); return Promise.resolve(null);
@ -598,15 +723,17 @@ Logic.registerPanel(P_CONTAINER_DELETE, {
// This method is called when the object is registered. // This method is called when the object is registered.
initialize() { initialize() {
document.querySelector("#delete-container-cancel-link").addEventListener("click", () => { Logic.addEnterHandler(document.querySelector("#delete-container-cancel-link"), () => {
Logic.showPreviousPanel(); Logic.showPreviousPanel();
}); });
document.querySelector("#delete-container-ok-link").addEventListener("click", () => { Logic.addEnterHandler(document.querySelector("#delete-container-ok-link"), () => {
browser.runtime.sendMessage({ /* This promise wont resolve if the last tab was removed from the window.
method: "removeIdentity", as the message async callback stops listening, this isn't an issue for us however it might be in future
userContextId: Logic.currentIdentity().userContextId, if you want to do anything post delete do it in the background script.
}).then(() => { Browser console currently warns about not listening also.
*/
Logic.removeIdentity(Logic.userContextId(Logic.currentIdentity().cookieStoreId)).then(() => {
return Logic.refreshIdentities(); return Logic.refreshIdentities();
}).then(() => { }).then(() => {
Logic.showPreviousPanel(); Logic.showPreviousPanel();
@ -624,7 +751,7 @@ Logic.registerPanel(P_CONTAINER_DELETE, {
document.getElementById("delete-container-name").textContent = identity.name; document.getElementById("delete-container-name").textContent = identity.name;
const icon = document.getElementById("delete-container-icon"); 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); icon.setAttribute("data-identity-color", identity.color);
return Promise.resolve(null); return Promise.resolve(null);

View file

@ -1,7 +1,7 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "Containers Experiment", "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.", "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": { "icons": {
@ -23,6 +23,7 @@
"activeTab", "activeTab",
"cookies", "cookies",
"contextMenus", "contextMenus",
"contextualIdentities",
"history", "history",
"idle", "idle",
"notifications", "notifications",
@ -32,6 +33,15 @@
"webRequest" "webRequest"
], ],
"commands": {
"_execute_browser_action": {
"suggested_key": {
"default": "Ctrl+Y"
},
"description": "Open containers panel"
}
},
"browser_action": { "browser_action": {
"browser_style": true, "browser_style": true,
"default_icon": { "default_icon": {

View file

@ -27,31 +27,38 @@
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-3.png" /> <img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-3.png" />
<h3 class="onboarding-title">A place for everything, and everything in its place.</h3> <h3 class="onboarding-title">A place for everything, and everything in its place.</h3>
<p>Start with the containers we've created, or create your own.</p> <p>Start with the containers we've created, or create your own.</p>
<a href="#" id="onboarding-almost-done-button" class="onboarding-button">Next</a>
</div>
<div class="panel onboarding onboarding-panel-4 hide" id="onboarding-panel-4">
<img class="onboarding-img" alt="How to assign sites to containers" src="/img/onboarding-4.png" />
<h3 class="onboarding-title">Always open sites in the containers you want.</h3>
<p>Right-click inside a container tab to assign the site to always open in the container.</p>
<a href="#" id="onboarding-done-button" class="onboarding-button">Done</a> <a href="#" id="onboarding-done-button" class="onboarding-button">Done</a>
</div> </div>
<div class="panel container-panel hide" id="container-panel"> <div class="panel container-panel hide" id="container-panel">
<div class="panel-header"> <div class="panel-header">
<h3 class="panel-header-text">Containers</h3> <h3 class="panel-header-text">Containers</h3>
<a class="pop-button" id="sort-containers-link"><img class="pop-button-image" alt="Sort Containers" title="Sort Containers" src="/img/container-sort.svg"></a> <a href="#" class="pop-button" id="sort-containers-link"><img class="pop-button-image" alt="Sort Containers" title="Sort Containers" src="/img/container-sort.svg"></a>
</div> </div>
<div class="scrollable panel-content"> <div class="scrollable panel-content" tabindex="-1">
<table> <table>
<tbody class="identities-list"></tbody> <tbody class="identities-list"></tbody>
</table> </table>
</div> </div>
<div class="panel-footer edit-identities"> <div class="panel-footer edit-identities">
<div class="edit-containers-text panel-footer-secondary"> <div class="edit-containers-text panel-footer-secondary">
<a id="edit-containers-link">Edit Containers</a> <a href="#" tabindex="0" id="edit-containers-link">Edit Containers</a>
</div> </div>
<a class="add-container-link pop-button" id="container-add-link"> <a href="#" tabindex="0" class="add-container-link pop-button" id="container-add-link">
<img class="pop-button-image-small icon" alt="Create new container icon" title="Create new container" src="/img/container-add.svg" /> <img class="pop-button-image-small icon" alt="Create new container icon" title="Create new container" src="/img/container-add.svg" />
</a> </a>
</div> </div>
</div> </div>
<div class="hide panel container-info-panel" id="container-info-panel"> <div class="hide panel container-info-panel" id="container-info-panel" tabindex="-1">
<div class="columns"> <div class="columns">
<div class="panel-back-arrow" id="close-container-info-panel"> <div class="panel-back-arrow" id="close-container-info-panel">
<img alt="Panel Back Arrow" src="/img/container-arrow.svg" class="back-arrow-img" /> <img alt="Panel Back Arrow" src="/img/container-arrow.svg" class="back-arrow-img" />
@ -85,7 +92,7 @@
</table> </table>
</div> </div>
<div class="panel-footer edit-containers-panel-footer"> <div class="panel-footer edit-containers-panel-footer">
<a id="exit-edit-mode-link" class="exit-edit-mode-link edit-containers-exit-text">Exit Edit Mode</a> <a href="#" id="exit-edit-mode-link" class="exit-edit-mode-link edit-containers-exit-text">Exit Edit Mode</a>
</div> </div>
</div> </div>
@ -109,7 +116,7 @@
</fieldset> </fieldset>
</form> </form>
<div class="panel-footer"> <div class="panel-footer">
<a class="button secondary expanded footer-button cancel-button" id="edit-container-cancel-link">Cancel</a> <a href="#" class="button secondary expanded footer-button cancel-button" id="edit-container-cancel-link">Cancel</a>
<a class="button primary expanded footer-button" id="edit-container-ok-link">OK</a> <a class="button primary expanded footer-button" id="edit-container-ok-link">OK</a>
</div> </div>
</div> </div>
@ -126,8 +133,8 @@
<p>If you remove this container now, <span id="delete-container-tab-count"></span> container tabs will be closed. Are you sure you want to remove this Container?</p> <p>If you remove this container now, <span id="delete-container-tab-count"></span> container tabs will be closed. Are you sure you want to remove this Container?</p>
</div> </div>
<div class="panel-footer"> <div class="panel-footer">
<a class="button expanded secondary footer-button cancel-button" id="delete-container-cancel-link">Cancel</a> <a href="#" class="button expanded secondary footer-button cancel-button" id="delete-container-cancel-link">Cancel</a>
<a class="button expanded primary footer-button" id="delete-container-ok-link">OK</a> <a href="#" class="button expanded primary footer-button" id="delete-container-ok-link">OK</a>
</div> </div>
</div> </div>