Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da3fc2ede2 | |||
| 385c585888 | |||
| 734b97beb0 | |||
| 3aa311a3c1 | |||
| df7d7f9c38 | |||
| 65be77665a | |||
| 44548659db | |||
| 10c4395efd | |||
| 27b2a4b5f2 | |||
| 6f54e7ff7f | |||
| cb6726b667 | |||
| 0964311fa1 | |||
| 2831d019f5 | |||
| 1cc58cad9b | |||
| bc9660f76e | |||
| f526caca50 | |||
| b6a98fb83e | |||
| af2b4b79a9 | |||
| a762b5eca2 | |||
| 3cc40344af | |||
| 278cdb7f69 | |||
| 0be03ebeb7 | |||
| c69f37a2de | |||
| b20ac8169a | |||
| 9e98d35b45 | |||
| 17f2781e07 | |||
| 0acf9cc0e6 | |||
| 70573d0559 | |||
| da239237f7 |
@@ -1,4 +1,4 @@
|
|||||||
# Containers Add-on
|
# Firefox Multi-Account Containers
|
||||||
|
|
||||||
[](https://testpilot.firefox.com/experiments/containers)
|
[](https://testpilot.firefox.com/experiments/containers)
|
||||||
|
|
||||||
@@ -20,6 +20,19 @@ For more info, see:
|
|||||||
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
### Web Extension Development
|
||||||
|
|
||||||
|
Since Firefox 57, this extension can now be run without any of the legacy components that were previously needed.
|
||||||
|
|
||||||
|
1. Install web-ext with npm
|
||||||
|
2. cd webextension; web-ext run -f Nightly
|
||||||
|
|
||||||
|
This will work in other builds of Firefox however certain features won't work and you will need to manually flip preferences to enable containers. All other sections of this guide talk about using the legacy setup with jpm.
|
||||||
|
|
||||||
|
|
||||||
|
## Legacy Development
|
||||||
|
|
||||||
### Development Environment
|
### Development Environment
|
||||||
|
|
||||||
Add-on development is better with [a particular environment](https://developer.mozilla.org/en-US/Add-ons/Setting_up_extension_development_environment). One simple way to get that environment set up is to install the [DevPrefs add-on](https://addons.mozilla.org/en-US/firefox/addon/devprefs/). You can make a custom Firefox profile that includes the DevPrefs add-on, and use that profile when you run the code in this repository.
|
Add-on development is better with [a particular environment](https://developer.mozilla.org/en-US/Add-ons/Setting_up_extension_development_environment). One simple way to get that environment set up is to install the [DevPrefs add-on](https://addons.mozilla.org/en-US/firefox/addon/devprefs/). You can make a custom Firefox profile that includes the DevPrefs add-on, and use that profile when you run the code in this repository.
|
||||||
@@ -44,7 +57,6 @@ Release & Beta channels do not allow un-signed add-ons, even with the DevPrefs.
|
|||||||
|
|
||||||
Whilst this is still using legacy code to test you will need the following in your profile:
|
Whilst this is still using legacy code to test you will need the following in your profile:
|
||||||
|
|
||||||
|
|
||||||
Change the following prefs in about:config:
|
Change the following prefs in about:config:
|
||||||
|
|
||||||
- extensions.legacy.enabled = true
|
- extensions.legacy.enabled = true
|
||||||
|
|||||||
Vendored
+72
-22
@@ -4,26 +4,31 @@ const PREFS = [
|
|||||||
{
|
{
|
||||||
name: "privacy.userContext.enabled",
|
name: "privacy.userContext.enabled",
|
||||||
value: true,
|
value: true,
|
||||||
type: "bool"
|
type: "bool",
|
||||||
|
default: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "privacy.userContext.longPressBehavior",
|
name: "privacy.userContext.longPressBehavior",
|
||||||
value: 2,
|
value: 2,
|
||||||
type: "int"
|
type: "int",
|
||||||
|
default: 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "privacy.userContext.ui.enabled",
|
name: "privacy.userContext.ui.enabled",
|
||||||
value: true, // Post web ext we will be setting this true
|
value: true, // Post web ext we will be setting this true
|
||||||
type: "bool"
|
type: "bool",
|
||||||
|
default: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "privacy.usercontext.about_newtab_segregation.enabled",
|
name: "privacy.usercontext.about_newtab_segregation.enabled",
|
||||||
value: true,
|
value: true,
|
||||||
type: "bool"
|
type: "bool",
|
||||||
|
default: false
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const Ci = Components.interfaces;
|
const Ci = Components.interfaces;
|
||||||
const Cu = Components.utils;
|
const Cu = Components.utils;
|
||||||
|
const Cc = Components.classes;
|
||||||
Cu.import("resource://gre/modules/Services.jsm");
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
const { TextDecoder, TextEncoder } = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
|
const { TextDecoder, TextEncoder } = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
|
||||||
@@ -34,6 +39,28 @@ XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
|||||||
const JETPACK_DIR_BASENAME = "jetpack";
|
const JETPACK_DIR_BASENAME = "jetpack";
|
||||||
const EXTENSION_ID = "@testpilot-containers";
|
const EXTENSION_ID = "@testpilot-containers";
|
||||||
|
|
||||||
|
function loadStyles(resourceURI) {
|
||||||
|
const styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"]
|
||||||
|
.getService(Ci.nsIStyleSheetService);
|
||||||
|
const styleURI = styleSheet(resourceURI);
|
||||||
|
const sheetType = styleSheetService.AGENT_SHEET;
|
||||||
|
styleSheetService.loadAndRegisterSheet(styleURI, sheetType);
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleSheet(resourceURI) {
|
||||||
|
return Services.io.newURI("data/usercontext.css", null, resourceURI);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unloadStyles(resourceURI) {
|
||||||
|
const styleURI = styleSheet(resourceURI);
|
||||||
|
const styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"]
|
||||||
|
.getService(Ci.nsIStyleSheetService);
|
||||||
|
const sheetType = styleSheetService.AGENT_SHEET;
|
||||||
|
if (styleSheetService.sheetRegistered(styleURI, sheetType)) {
|
||||||
|
styleSheetService.unregisterSheet(styleURI, sheetType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function filename() {
|
function filename() {
|
||||||
const storeFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
|
const storeFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
|
||||||
storeFile.append(JETPACK_DIR_BASENAME);
|
storeFile.append(JETPACK_DIR_BASENAME);
|
||||||
@@ -43,14 +70,27 @@ function filename() {
|
|||||||
return storeFile.path;
|
return storeFile.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getConfig() {
|
async function makeFilepath() {
|
||||||
const bytes = await OS.File.read(filename());
|
const storeFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
|
||||||
const raw = new TextDecoder().decode(bytes) || "";
|
storeFile.append(JETPACK_DIR_BASENAME);
|
||||||
let savedConfig = {savedConfiguration: {}};
|
await OS.File.makeDir(storeFile.path, { ignoreExisting: true });
|
||||||
if (raw) {
|
storeFile.append(EXTENSION_ID);
|
||||||
savedConfig = JSON.parse(raw);
|
await OS.File.makeDir(storeFile.path, { ignoreExisting: true });
|
||||||
}
|
storeFile.append("simple-storage");
|
||||||
|
await OS.File.makeDir(storeFile.path, { ignoreExisting: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getConfig() {
|
||||||
|
let savedConfig = {savedConfiguration: {}};
|
||||||
|
try {
|
||||||
|
const bytes = await OS.File.read(filename());
|
||||||
|
const raw = new TextDecoder().decode(bytes) || "";
|
||||||
|
if (raw) {
|
||||||
|
savedConfig = JSON.parse(raw);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore file read errors, sometimes they happen and I'm not sure if we can fix
|
||||||
|
}
|
||||||
return savedConfig;
|
return savedConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,14 +101,15 @@ async function initConfig() {
|
|||||||
savedConfig.savedConfiguration.prefs = {};
|
savedConfig.savedConfiguration.prefs = {};
|
||||||
PREFS.forEach((pref) => {
|
PREFS.forEach((pref) => {
|
||||||
if ("int" === pref.type) {
|
if ("int" === pref.type) {
|
||||||
savedConfig.savedConfiguration.prefs[pref.name] = Services.prefs.getIntPref(pref.name, pref.name);
|
savedConfig.savedConfiguration.prefs[pref.name] = Services.prefs.getIntPref(pref.name);
|
||||||
} else {
|
} else {
|
||||||
savedConfig.savedConfiguration.prefs[pref.name] = Services.prefs.getBoolPref(pref.name, pref.value);
|
savedConfig.savedConfiguration.prefs[pref.name] = Services.prefs.getBoolPref(pref.name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const serialized = JSON.stringify(savedConfig);
|
const serialized = JSON.stringify(savedConfig);
|
||||||
const bytes = new TextEncoder().encode(serialized) || "";
|
const bytes = new TextEncoder().encode(serialized) || "";
|
||||||
|
await makeFilepath();
|
||||||
await OS.File.writeAtomic(filename(), bytes, { });
|
await OS.File.writeAtomic(filename(), bytes, { });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,28 +134,37 @@ async function uninstall(aData, aReason) {
|
|||||||
if (aReason === ADDON_UNINSTALL
|
if (aReason === ADDON_UNINSTALL
|
||||||
|| aReason === ADDON_DISABLE) {
|
|| aReason === ADDON_DISABLE) {
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
const storedPrefs = config.savedConfiguration.prefs;
|
const storedPrefs = config.savedConfiguration.prefs || {};
|
||||||
PREFS.forEach((pref) => {
|
PREFS.forEach((pref) => {
|
||||||
|
let value = pref.default;
|
||||||
if (pref.name in storedPrefs) {
|
if (pref.name in storedPrefs) {
|
||||||
if ("int" === pref.type) {
|
value = storedPrefs[pref.name];
|
||||||
Services.prefs.setIntPref(pref.name, storedPrefs[pref.name]);
|
}
|
||||||
} else {
|
if ("int" === pref.type) {
|
||||||
Services.prefs.setBoolPref(pref.name, storedPrefs[pref.name]);
|
Services.prefs.setIntPref(pref.name, value);
|
||||||
}
|
} else {
|
||||||
|
Services.prefs.setBoolPref(pref.name, value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
function startup({webExtension}) {
|
function startup({webExtension, resourceURI}) {
|
||||||
|
const version = Services.appinfo.version;
|
||||||
|
const versionMatch = version.match(/^([0-9]+)\./)[1];
|
||||||
|
if (versionMatch === "55"
|
||||||
|
|| versionMatch === "56") {
|
||||||
|
loadStyles(resourceURI);
|
||||||
|
}
|
||||||
// Reset prefs that may have changed, or are legacy
|
// Reset prefs that may have changed, or are legacy
|
||||||
setPrefs();
|
install();
|
||||||
// Start the embedded webextension.
|
// Start the embedded webextension.
|
||||||
webExtension.startup();
|
webExtension.startup();
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
function shutdown() {
|
function shutdown({resourceURI}) {
|
||||||
|
unloadStyles(resourceURI);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,108 +1,3 @@
|
|||||||
/* HACK: Custom Container vars do not propigate correctly
|
|
||||||
until the container tab is blurred and refocused,
|
|
||||||
adding the data-identity-color with the default hex
|
|
||||||
value, or chrome url path as an alternate selector mitiages this bug.*/
|
|
||||||
[data-identity-color="blue"],
|
|
||||||
[data-identity-color="#00a7e0"] {
|
|
||||||
--identity-tab-color: #37adff;
|
|
||||||
--identity-icon-color: #37adff;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="turquoise"],
|
|
||||||
[data-identity-color="#01bdad"] {
|
|
||||||
--identity-tab-color: #00c79a;
|
|
||||||
--identity-icon-color: #00c79a;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="green"],
|
|
||||||
[data-identity-color="#7dc14c"] {
|
|
||||||
--identity-tab-color: #51cd00;
|
|
||||||
--identity-icon-color: #51cd00;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="yellow"],
|
|
||||||
[data-identity-color="#ffcb00"] {
|
|
||||||
--identity-tab-color: #ffcb00;
|
|
||||||
--identity-icon-color: #ffcb00;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="orange"],
|
|
||||||
[data-identity-color="#f89c24"] {
|
|
||||||
--identity-tab-color: #ff9f00;
|
|
||||||
--identity-icon-color: #ff9f00;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="red"],
|
|
||||||
[data-identity-color="#d92215"] {
|
|
||||||
--identity-tab-color: #ff613d;
|
|
||||||
--identity-icon-color: #ff613d;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="pink"],
|
|
||||||
[data-identity-color="#ee5195"] {
|
|
||||||
--identity-tab-color: #ff4bda;
|
|
||||||
--identity-icon-color: #ff4bda;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="purple"],
|
|
||||||
[data-identity-color="#7a2f7a"] {
|
|
||||||
--identity-tab-color: #af51f5;
|
|
||||||
--identity-icon-color: #af51f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="fingerprint"],
|
|
||||||
[data-identity-icon="chrome://browser/skin/usercontext/personal.svg"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#fingerprint");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="briefcase"],
|
|
||||||
[data-identity-icon="chrome://browser/skin/usercontext/work.svg"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#briefcase");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="dollar"],
|
|
||||||
[data-identity-icon="chrome://browser/skin/usercontext/banking.svg"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#dollar");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="cart"],
|
|
||||||
[data-identity-icon="chrome://browser/skin/usercontext/cart.svg"],
|
|
||||||
[data-identity-icon="chrome://browser/skin/usercontext/shopping.svg"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#cart");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="circle"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#circle");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="gift"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#gift");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="vacation"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#vacation");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="food"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#food");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="fruit"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#fruit");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="pet"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#pet");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="tree"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#tree");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="chill"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#chill");
|
|
||||||
}
|
|
||||||
|
|
||||||
#userContext-indicator {
|
#userContext-indicator {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
list-style-image: none !important;
|
list-style-image: none !important;
|
||||||
@@ -129,19 +24,6 @@ value, or chrome url path as an alternate selector mitiages this bug.*/
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.userContext-icon,
|
|
||||||
.menuitem-iconic[data-usercontextid] > .menu-iconic-left > .menu-iconic-icon,
|
|
||||||
.subviewbutton[usercontextid] > .toolbarbutton-icon,
|
|
||||||
#userContext-indicator {
|
|
||||||
background-image: var(--identity-icon) !important;
|
|
||||||
background-position: center center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: contain;
|
|
||||||
fill: var(--identity-icon-color) !important;
|
|
||||||
filter: url(/img/filters.svg#fill);
|
|
||||||
filter: url(/data/filters.svg#fill);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* containers experiment */
|
/* containers experiment */
|
||||||
|
|
||||||
/* reset nightly containers */
|
/* reset nightly containers */
|
||||||
|
|||||||
-285
@@ -1,285 +0,0 @@
|
|||||||
# METRICS
|
|
||||||
|
|
||||||
## Data Analysis
|
|
||||||
The collected data will primarily be used to answer the following questions.
|
|
||||||
Images are used for visualization and are not composed of actual data.
|
|
||||||
|
|
||||||
### Do users install and run this?
|
|
||||||
|
|
||||||
What is the overall engagement of the Containers experiment?
|
|
||||||
**This is the standard Daily Active User (DAU) and Monthly Active User (MAU) analysis.**
|
|
||||||
|
|
||||||
This captures data from the users who have the add-on installed, regardless of
|
|
||||||
whether they are actively interacting with it.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Immediate Questions
|
|
||||||
|
|
||||||
* Do people use the containers feature & how do people create new container tabs?
|
|
||||||
* Click to create new container tab
|
|
||||||
* \+ `entry-point` value: "tab-bar" or "pop-up"
|
|
||||||
* Do people who use the containers feature continue to use it?
|
|
||||||
* Retention: opening a second container tab (second tab in the same container, or a tab in a second container?)
|
|
||||||
* What containers do people use?
|
|
||||||
* userContextId
|
|
||||||
* \+ Number of tabs in the container (when should we measure this? on every tab open?)
|
|
||||||
* Do people edit their containers?
|
|
||||||
* Click on "Edit Containers"
|
|
||||||
* Click to edit a single container
|
|
||||||
* Click "OK"
|
|
||||||
* Click to delete a single container
|
|
||||||
* Click "OK"
|
|
||||||
* Click to add a container
|
|
||||||
* Click "OK"
|
|
||||||
* Do people sort the tabs?
|
|
||||||
* Click sort
|
|
||||||
* \+ Number of tabs when clicked
|
|
||||||
* Average number of container tabs when sort was clicked
|
|
||||||
* Do users show and hide container tabs?
|
|
||||||
* Click hide
|
|
||||||
* \+ Number of tabs when clicked
|
|
||||||
* \+ Number of hidden containers when clicked
|
|
||||||
* Click show
|
|
||||||
* \+ Number of tabs when clicked
|
|
||||||
* \+ Number of shown containers when clicked
|
|
||||||
* Do users move container tabs to new windows?
|
|
||||||
* Click move
|
|
||||||
* \+ Number of tabs when clicked
|
|
||||||
* Average number of container tabs when new window was clicked
|
|
||||||
* How many containers do users have hidden at the same time? (when should we measure this? each time a container is hidden?)
|
|
||||||
* Do users pin container tabs? (do we have existing Telemetry for pinning?)
|
|
||||||
* Do users visit more pages in container tabs than non-container tabs?
|
|
||||||
|
|
||||||
### Follow-up Questions
|
|
||||||
|
|
||||||
What are some follow-up questions we anticipate we will ask based on any of the
|
|
||||||
above answers/data?
|
|
||||||
|
|
||||||
* What is the average lifespan of a container tab? Is that longer or shorter than a regular tab? (if we don't have data on the latter, the former probably isn't worth gathering data on since we will have nothing to compare it to).
|
|
||||||
|
|
||||||
## Data Collection
|
|
||||||
|
|
||||||
### Server Side
|
|
||||||
There is currently no server side component to Containers.
|
|
||||||
|
|
||||||
### Client Side
|
|
||||||
Containers will use Test Pilot Telemetry with no batching of data. Details
|
|
||||||
of when pings are sent are below, along with examples of the `payload` portion
|
|
||||||
of a `testpilottest` telemetry ping for each scenario.
|
|
||||||
|
|
||||||
* The user shows the new tab menu
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "show-plus-button-menu",
|
|
||||||
"eventSource": ["plus-button"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks on a container name to open a tab in that container
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
|
|
||||||
"event": "open-tab",
|
|
||||||
"eventSource": ["tab-bar"|"pop-up"|"file-menu"|"alltabs-menu"|"plus-button"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks "Edit Containers" in the pop-up
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "edit-containers"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks OK after clicking on a container edit icon in the pop-up
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "edit-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks OK after clicking on a container delete icon in the pop-up
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "delete-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks OK after clicking to add a container in the pop-up
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "add-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks the sort button/icon in the pop-up
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "sort-tabs",
|
|
||||||
"shownContainersCount": <number-of-containers-with-tabs-shown>,
|
|
||||||
"totalContainerTabsCount": <number-of-all-container-tabs>,
|
|
||||||
"totalNonContainerTabsCount": <number-of-all-non-container-tabs>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks "Hide these container tabs" in the popup
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
|
|
||||||
"event": "hide-tabs",
|
|
||||||
"hiddenContainersCount": <number-of-containers-with-tabs-hidden>,
|
|
||||||
"shownContainersCount": <number-of-containers-with-tabs-shown>,
|
|
||||||
"totalContainersCount": <number-of-containers-with-tabs-hidden-or-shown>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks "Show these container tabs" in the popup
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
|
|
||||||
"event": "show-tabs",
|
|
||||||
"hiddenContainersCount": <number-of-containers-with-tabs-hidden>,
|
|
||||||
"shownContainersCount": <number-of-containers-with-tabs-shown>,
|
|
||||||
"totalContainersCount": <number-of-containers-with-tabs-hidden-or-shown>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks "Move tabs to a new window" in the popup
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
|
|
||||||
"event": "move-tabs-to-window"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* When a user encounters the disabled "move" feature because of incompatible add-ons
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "incompatible-addons-detected"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user closes a tab
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "page-requests-completed-per-tab",
|
|
||||||
"pageRequestCount": <pageRequestCount>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user goes idle
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "page-requests-completed-per-activity",
|
|
||||||
"pageRequestCount": <pageRequestCount>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user chooses "Always Open in this Container" context menu option. (Note: We send two separate event names: one for assigning a site to a container, one for removing a site from a container.)
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "[added|removed]-container-assignment"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Firefox prompts the user to reload a site into a container after the user picked "Always Open in this Container".
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "prompt-reload-page-in-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks "Open in *assigned* container" to reload a site into a container after the user picked "Always Open in this Container".
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "click-to-reload-page-in-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks "Open in *Current* container" to reload a site into a container after the user picked "Always Open in this Container".
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "click-to-reload-page-in-same-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Firefox automatically reloads a site into a container after the user picked "Always Open in this Container".
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "auto-reload-page-in-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### A Redshift schema for the payload:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
local schema = {
|
|
||||||
-- column name field type length attributes field name
|
|
||||||
{"uuid", "VARCHAR", 255, nil, "Fields[payload.uuid]"},
|
|
||||||
{"userContextId", "INTEGER", 255, nil, "Fields[payload.userContextId]"},
|
|
||||||
{"clickedContainerTabCount", "INTEGER", 255, nil, "Fields[payload.clickedContainerTabCount]"},
|
|
||||||
{"eventSource", "VARCHAR", 255, nil, "Fields[payload.eventSource]"},
|
|
||||||
{"event", "VARCHAR", 255, nil, "Fields[payload.event]"},
|
|
||||||
{"pageRequestCount", "INTEGER", 255, nil, "Fields[payload.pageRequestCount]"}
|
|
||||||
{"hiddenContainersCount", "INTEGER", 255, nil, "Fields[payload.hiddenContainersCount]"},
|
|
||||||
{"shownContainersCount", "INTEGER", 255, nil, "Fields[payload.shownContainersCount]"},
|
|
||||||
{"totalContainersCount", "INTEGER", 255, nil, "Fields[payload.totalContainersCount]"},
|
|
||||||
{"totalContainerTabsCount", "INTEGER", 255, nil, "Fields[payload.totalContainerTabsCount]"},
|
|
||||||
{"totalNonContainerTabsCount", "INTEGER", 255, nil, "Fields[payload.totalNonContainerTabsCount]"}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Valid data should be enforced on the server side:
|
|
||||||
|
|
||||||
* `eventSource` should be one of `tab-bar`, `pop-up`, `file-menu`, "alltabs-nmenu" or "plus-button".
|
|
||||||
|
|
||||||
All Mozilla data is kept by default for 180 days and in accordance with our
|
|
||||||
privacy policies.
|
|
||||||
+3
-2
@@ -7,7 +7,8 @@
|
|||||||
<em:bootstrap>true</em:bootstrap>
|
<em:bootstrap>true</em:bootstrap>
|
||||||
<em:multiprocessCompatible>true</em:multiprocessCompatible>
|
<em:multiprocessCompatible>true</em:multiprocessCompatible>
|
||||||
<em:hasEmbeddedWebExtension>true</em:hasEmbeddedWebExtension>
|
<em:hasEmbeddedWebExtension>true</em:hasEmbeddedWebExtension>
|
||||||
<em:name>Testpilot containers</em:name>
|
<em:name>Firefox Multi-Account Containers</em:name>
|
||||||
|
<em:description>Containers helps you keep all the parts of your online life contained in different tabs. Custom labels and color-coded tabs help keep different activities — like online shopping, travel planning, or checking work email — separate.</em:description>
|
||||||
<em:targetApplication>
|
<em:targetApplication>
|
||||||
<Description>
|
<Description>
|
||||||
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!--Firefox-->
|
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!--Firefox-->
|
||||||
@@ -15,7 +16,7 @@
|
|||||||
<em:maxVersion>*</em:maxVersion>
|
<em:maxVersion>*</em:maxVersion>
|
||||||
</Description>
|
</Description>
|
||||||
</em:targetApplication>
|
</em:targetApplication>
|
||||||
<em:version>3.1.0</em:version>
|
<em:version>4.0.0</em:version>
|
||||||
<em:unpack>false</em:unpack>
|
<em:unpack>false</em:unpack>
|
||||||
</Description>
|
</Description>
|
||||||
</RDF>
|
</RDF>
|
||||||
|
|||||||
+4
-5
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "testpilot-containers",
|
"name": "testpilot-containers",
|
||||||
"title": "Containers Experiment",
|
"title": "Firefox Multi-Account Containers",
|
||||||
"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 helps you keep all the parts of your online life contained in different tabs. Custom labels and color-coded tabs help keep different activities — like online shopping, travel planning, or checking work email — separate.",
|
||||||
"version": "3.1.0",
|
"version": "4.0.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"
|
||||||
@@ -39,6 +39,5 @@
|
|||||||
"lint:js": "eslint .",
|
"lint:js": "eslint .",
|
||||||
"package": "npm run build && mv testpilot-containers.xpi addon.xpi",
|
"package": "npm run build && mv testpilot-containers.xpi addon.xpi",
|
||||||
"test": "npm run lint"
|
"test": "npm run lint"
|
||||||
},
|
}
|
||||||
"updateURL": "https://testpilot.firefox.com/files/@testpilot-containers/updates.json"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||||
<title>Containers confirm navigation</title>
|
<title>Firefox Multi-Account Containers Confirm Navigation</title>
|
||||||
<link xmlns="http://www.w3.org/1999/xhtml" rel="stylesheet" href="chrome://browser/skin/aboutNetError.css" type="text/css" media="all" />
|
<link xmlns="http://www.w3.org/1999/xhtml" rel="stylesheet" href="chrome://browser/skin/aboutNetError.css" type="text/css" media="all" />
|
||||||
<link rel="stylesheet" href="/css/confirm-page.css" />
|
<link rel="stylesheet" href="/css/confirm-page.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ module.exports = {
|
|||||||
"badge": true,
|
"badge": true,
|
||||||
"backgroundLogic": true,
|
"backgroundLogic": true,
|
||||||
"identityState": true,
|
"identityState": true,
|
||||||
"messageHandler": true,
|
"messageHandler": true
|
||||||
"tabPageCounter": true
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
const assignManager = {
|
const assignManager = {
|
||||||
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",
|
||||||
|
MENU_SEPARATOR_ID: "separator",
|
||||||
|
MENU_HIDE_ID: "hide-container",
|
||||||
|
MENU_MOVE_ID: "move-to-new-window-container",
|
||||||
|
|
||||||
storageArea: {
|
storageArea: {
|
||||||
area: browser.storage.local,
|
area: browser.storage.local,
|
||||||
exemptedTabs: {},
|
exemptedTabs: {},
|
||||||
@@ -163,15 +167,31 @@ const assignManager = {
|
|||||||
async _onClickedHandler(info, tab) {
|
async _onClickedHandler(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}
|
||||||
|
let remove;
|
||||||
if (userContextId) {
|
if (userContextId) {
|
||||||
// let actionName;
|
switch (info.menuItemId) {
|
||||||
let remove;
|
case this.MENU_ASSIGN_ID:
|
||||||
if (info.menuItemId === this.MENU_ASSIGN_ID) {
|
case this.MENU_REMOVE_ID:
|
||||||
remove = false;
|
if (info.menuItemId === this.MENU_ASSIGN_ID) {
|
||||||
} else {
|
remove = false;
|
||||||
remove = true;
|
} else {
|
||||||
|
remove = true;
|
||||||
|
}
|
||||||
|
await this._setOrRemoveAssignment(tab.id, info.pageUrl, userContextId, remove);
|
||||||
|
break;
|
||||||
|
case this.MENU_MOVE_ID:
|
||||||
|
backgroundLogic.moveTabsToWindow({
|
||||||
|
cookieStoreId: tab.cookieStoreId,
|
||||||
|
windowId: tab.windowId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case this.MENU_HIDE_ID:
|
||||||
|
backgroundLogic.hideTabs({
|
||||||
|
cookieStoreId: tab.cookieStoreId,
|
||||||
|
windowId: tab.windowId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
await this._setOrRemoveAssignment(tab.id, info.pageUrl, userContextId, remove);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -260,6 +280,9 @@ const assignManager = {
|
|||||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1352102
|
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1352102
|
||||||
browser.contextMenus.remove(this.MENU_ASSIGN_ID);
|
browser.contextMenus.remove(this.MENU_ASSIGN_ID);
|
||||||
browser.contextMenus.remove(this.MENU_REMOVE_ID);
|
browser.contextMenus.remove(this.MENU_REMOVE_ID);
|
||||||
|
browser.contextMenus.remove(this.MENU_SEPARATOR_ID);
|
||||||
|
browser.contextMenus.remove(this.MENU_HIDE_ID);
|
||||||
|
browser.contextMenus.remove(this.MENU_MOVE_ID);
|
||||||
},
|
},
|
||||||
|
|
||||||
async calculateContextMenu(tab) {
|
async calculateContextMenu(tab) {
|
||||||
@@ -270,19 +293,37 @@ const assignManager = {
|
|||||||
if (siteSettings === false) {
|
if (siteSettings === false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// ✓ This is to mitigate https://bugzilla.mozilla.org/show_bug.cgi?id=1351418
|
let checked = false;
|
||||||
let prefix = " "; // Alignment of non breaking space, unknown why this requires so many spaces to align with the tick
|
|
||||||
let menuId = this.MENU_ASSIGN_ID;
|
let menuId = this.MENU_ASSIGN_ID;
|
||||||
const tabUserContextId = this.getUserContextIdFromCookieStore(tab);
|
const tabUserContextId = this.getUserContextIdFromCookieStore(tab);
|
||||||
if (siteSettings &&
|
if (siteSettings &&
|
||||||
Number(siteSettings.userContextId) === Number(tabUserContextId)) {
|
Number(siteSettings.userContextId) === Number(tabUserContextId)) {
|
||||||
prefix = "✓";
|
checked = true;
|
||||||
menuId = this.MENU_REMOVE_ID;
|
menuId = this.MENU_REMOVE_ID;
|
||||||
}
|
}
|
||||||
browser.contextMenus.create({
|
browser.contextMenus.create({
|
||||||
id: menuId,
|
id: menuId,
|
||||||
title: `${prefix} Always Open in This Container`,
|
title: "Always Open in This Container",
|
||||||
checked: true,
|
checked,
|
||||||
|
type: "checkbox",
|
||||||
|
contexts: ["all"],
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: this.MENU_SEPARATOR_ID,
|
||||||
|
type: "separator",
|
||||||
|
contexts: ["all"],
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: this.MENU_HIDE_ID,
|
||||||
|
title: "Hide This Container",
|
||||||
|
contexts: ["all"],
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: this.MENU_MOVE_ID,
|
||||||
|
title: "Move Tabs to a New Window",
|
||||||
contexts: ["all"],
|
contexts: ["all"],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,13 +25,12 @@ const backgroundLogic = {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteContainer(userContextId) {
|
async deleteContainer(userContextId, removed = false) {
|
||||||
await this._closeTabs(userContextId);
|
await this._closeTabs(userContextId);
|
||||||
await browser.contextualIdentities.remove(this.cookieStoreId(userContextId));
|
if (!removed) {
|
||||||
|
await browser.contextualIdentities.remove(this.cookieStoreId(userContextId));
|
||||||
|
}
|
||||||
assignManager.deleteContainer(userContextId);
|
assignManager.deleteContainer(userContextId);
|
||||||
await browser.runtime.sendMessage({
|
|
||||||
method: "forgetIdentityAndRefresh"
|
|
||||||
});
|
|
||||||
return {done: true, userContextId};
|
return {done: true, userContextId};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -51,7 +50,7 @@ const backgroundLogic = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async openTab(options) {
|
async openNewTab(options) {
|
||||||
let url = options.url || undefined;
|
let url = options.url || undefined;
|
||||||
const userContextId = ("userContextId" in options) ? options.userContextId : 0;
|
const userContextId = ("userContextId" in options) ? options.userContextId : 0;
|
||||||
const active = ("nofocus" in options) ? options.nofocus : true;
|
const active = ("nofocus" in options) ? options.nofocus : true;
|
||||||
@@ -64,10 +63,11 @@ const backgroundLogic = {
|
|||||||
url = undefined;
|
url = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unhide all hidden tabs
|
// We can't open these we just have to throw them away
|
||||||
this.showTabs({
|
if (new URL(url).protocol === "about:") {
|
||||||
cookieStoreId
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
return browser.tabs.create({
|
return browser.tabs.create({
|
||||||
url,
|
url,
|
||||||
active,
|
active,
|
||||||
@@ -76,60 +76,61 @@ const backgroundLogic = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async getTabs(options) {
|
checkArgs(requiredArguments, options, methodName) {
|
||||||
if (!("cookieStoreId" in options)) {
|
requiredArguments.forEach((argument) => {
|
||||||
return new Error("getTabs must be called with cookieStoreId argument.");
|
if (!(argument in options)) {
|
||||||
}
|
return new Error(`${methodName} must be called with ${argument} argument.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId);
|
async getTabs(options) {
|
||||||
await identityState.remapTabsIfMissing(options.cookieStoreId);
|
const requiredArguments = ["cookieStoreId", "windowId"];
|
||||||
const isKnownContainer = await identityState._isKnownContainer(userContextId);
|
this.checkArgs(requiredArguments, options, "getTabs");
|
||||||
if (!isKnownContainer) {
|
const { cookieStoreId, windowId } = options;
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const list = [];
|
const list = [];
|
||||||
const tabs = await this._containerTabs(options.cookieStoreId);
|
const tabs = await browser.tabs.query({
|
||||||
|
cookieStoreId,
|
||||||
|
windowId
|
||||||
|
});
|
||||||
tabs.forEach((tab) => {
|
tabs.forEach((tab) => {
|
||||||
list.push(identityState._createTabObject(tab));
|
list.push(identityState._createTabObject(tab));
|
||||||
});
|
});
|
||||||
|
|
||||||
const containerState = await identityState.storageArea.get(options.cookieStoreId);
|
const containerState = await identityState.storageArea.get(cookieStoreId);
|
||||||
return list.concat(containerState.hiddenTabs);
|
return list.concat(containerState.hiddenTabs);
|
||||||
},
|
},
|
||||||
|
|
||||||
async moveTabsToWindow(options) {
|
async moveTabsToWindow(options) {
|
||||||
if (!("cookieStoreId" in options)) {
|
const requiredArguments = ["cookieStoreId", "windowId"];
|
||||||
return new Error("moveTabsToWindow must be called with cookieStoreId argument.");
|
this.checkArgs(requiredArguments, options, "moveTabsToWindow");
|
||||||
}
|
const { cookieStoreId, windowId } = options;
|
||||||
|
|
||||||
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId);
|
const list = await browser.tabs.query({
|
||||||
await identityState.remapTabsIfMissing(options.cookieStoreId);
|
cookieStoreId,
|
||||||
if (!identityState._isKnownContainer(userContextId)) {
|
windowId
|
||||||
return null;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const list = await identityState._matchTabsByContainer(options.cookieStoreId);
|
const containerState = await identityState.storageArea.get(cookieStoreId);
|
||||||
|
|
||||||
const containerState = await identityState.storageArea.get(options.cookieStoreId);
|
|
||||||
// Nothing to do
|
// Nothing to do
|
||||||
if (list.length === 0 &&
|
if (list.length === 0 &&
|
||||||
containerState.hiddenTabs.length === 0) {
|
containerState.hiddenTabs.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const window = await browser.windows.create({
|
const newWindowObj = await browser.windows.create({
|
||||||
tabId: list.shift().id
|
tabId: list.shift().id
|
||||||
});
|
});
|
||||||
browser.tabs.move(list, {
|
browser.tabs.move(list.map((tab) => tab.id), {
|
||||||
windowId: window.id,
|
windowId: newWindowObj.id,
|
||||||
index: -1
|
index: -1
|
||||||
});
|
});
|
||||||
|
|
||||||
// Let's show the hidden tabs.
|
// Let's show the hidden tabs.
|
||||||
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
|
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
|
||||||
browser.tabs.create(object.url || DEFAULT_TAB, {
|
browser.tabs.create(object.url || DEFAULT_TAB, {
|
||||||
windowId: window.id,
|
windowId: newWindowObj.id,
|
||||||
cookieStoreId: options.cookieStoreId
|
cookieStoreId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,31 +139,46 @@ const backgroundLogic = {
|
|||||||
// Let's close all the normal tab in the new window. In theory it
|
// Let's close all the normal tab in the new window. In theory it
|
||||||
// should be only the first tab, but maybe there are addons doing
|
// should be only the first tab, but maybe there are addons doing
|
||||||
// crazy stuff.
|
// crazy stuff.
|
||||||
const tabs = browser.tabs.query({windowId: window.id});
|
const tabs = browser.tabs.query({windowId: newWindowObj.id});
|
||||||
for (let tab of tabs) { // eslint-disable-line prefer-const
|
for (let tab of tabs) { // eslint-disable-line prefer-const
|
||||||
if (tabs.cookieStoreId !== options.cookieStoreId) {
|
if (tabs.cookieStoreId !== cookieStoreId) {
|
||||||
browser.tabs.remove(tab.id);
|
browser.tabs.remove(tab.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return await identityState.storageArea.set(options.cookieStoreId, containerState);
|
return await identityState.storageArea.set(cookieStoreId, containerState);
|
||||||
},
|
},
|
||||||
|
|
||||||
async _closeTabs(userContextId) {
|
async _closeTabs(userContextId, windowId = false) {
|
||||||
const cookieStoreId = this.cookieStoreId(userContextId);
|
const cookieStoreId = this.cookieStoreId(userContextId);
|
||||||
const tabs = await this._containerTabs(cookieStoreId);
|
let tabs;
|
||||||
|
/* if we have no windowId we are going to close all this container (used for deleting) */
|
||||||
|
if (windowId !== false) {
|
||||||
|
tabs = await browser.tabs.query({
|
||||||
|
cookieStoreId,
|
||||||
|
windowId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
tabs = await browser.tabs.query({
|
||||||
|
cookieStoreId
|
||||||
|
});
|
||||||
|
}
|
||||||
const tabIds = tabs.map((tab) => tab.id);
|
const tabIds = tabs.map((tab) => tab.id);
|
||||||
return browser.tabs.remove(tabIds);
|
return browser.tabs.remove(tabIds);
|
||||||
},
|
},
|
||||||
|
|
||||||
async queryIdentitiesState() {
|
async queryIdentitiesState(windowId) {
|
||||||
const identities = await browser.contextualIdentities.query({});
|
const identities = await browser.contextualIdentities.query({});
|
||||||
const identitiesOutput = {};
|
const identitiesOutput = {};
|
||||||
const identitiesPromise = identities.map(async function (identity) {
|
const identitiesPromise = identities.map(async function (identity) {
|
||||||
await identityState.remapTabsIfMissing(identity.cookieStoreId);
|
const { cookieStoreId } = identity;
|
||||||
const containerState = await identityState.storageArea.get(identity.cookieStoreId);
|
const containerState = await identityState.storageArea.get(cookieStoreId);
|
||||||
identitiesOutput[identity.cookieStoreId] = {
|
const openTabs = await browser.tabs.query({
|
||||||
|
cookieStoreId,
|
||||||
|
windowId
|
||||||
|
});
|
||||||
|
identitiesOutput[cookieStoreId] = {
|
||||||
hasHiddenTabs: !!containerState.hiddenTabs.length,
|
hasHiddenTabs: !!containerState.hiddenTabs.length,
|
||||||
hasOpenTabs: !!containerState.openTabs
|
hasOpenTabs: !!openTabs.length
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
@@ -172,15 +188,15 @@ const backgroundLogic = {
|
|||||||
|
|
||||||
async sortTabs() {
|
async sortTabs() {
|
||||||
const windows = await browser.windows.getAll();
|
const windows = await browser.windows.getAll();
|
||||||
for (let window of windows) { // eslint-disable-line prefer-const
|
for (let windowObj of windows) { // eslint-disable-line prefer-const
|
||||||
// First the pinned tabs, then the normal ones.
|
// First the pinned tabs, then the normal ones.
|
||||||
await this._sortTabsInternal(window, true);
|
await this._sortTabsInternal(windowObj, true);
|
||||||
await this._sortTabsInternal(window, false);
|
await this._sortTabsInternal(windowObj, false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async _sortTabsInternal(window, pinnedTabs) {
|
async _sortTabsInternal(windowObj, pinnedTabs) {
|
||||||
const tabs = await browser.tabs.query({windowId: window.id});
|
const tabs = await browser.tabs.query({windowId: windowObj.id});
|
||||||
let pos = 0;
|
let pos = 0;
|
||||||
|
|
||||||
// Let's collect UCIs/tabs for this window.
|
// Let's collect UCIs/tabs for this window.
|
||||||
@@ -212,28 +228,22 @@ const backgroundLogic = {
|
|||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
++pos;
|
++pos;
|
||||||
browser.tabs.move(tab.id, {
|
browser.tabs.move(tab.id, {
|
||||||
windowId: window.id,
|
windowId: windowObj.id,
|
||||||
index: pos
|
index: pos
|
||||||
});
|
});
|
||||||
//xulWindow.gBrowser.moveTabTo(tab, pos++);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async hideTabs(options) {
|
async hideTabs(options) {
|
||||||
if (!("cookieStoreId" in options)) {
|
const requiredArguments = ["cookieStoreId", "windowId"];
|
||||||
return new Error("hideTabs must be called with cookieStoreId option.");
|
this.checkArgs(requiredArguments, options, "hideTabs");
|
||||||
}
|
const { cookieStoreId, windowId } = options;
|
||||||
|
|
||||||
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId);
|
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(cookieStoreId);
|
||||||
await identityState.remapTabsIfMissing(options.cookieStoreId);
|
|
||||||
const isKnownContainer = await identityState._isKnownContainer(userContextId);
|
|
||||||
if (!isKnownContainer) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const containerState = await identityState.storeHidden(options.cookieStoreId);
|
const containerState = await identityState.storeHidden(cookieStoreId, windowId);
|
||||||
await this._closeTabs(userContextId);
|
await this._closeTabs(userContextId, windowId);
|
||||||
return containerState;
|
return containerState;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -243,17 +253,12 @@ const backgroundLogic = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId);
|
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId);
|
||||||
await identityState.remapTabsIfMissing(options.cookieStoreId);
|
|
||||||
if (!identityState._isKnownContainer(userContextId)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
|
||||||
const containerState = await identityState.storageArea.get(options.cookieStoreId);
|
const containerState = await identityState.storageArea.get(options.cookieStoreId);
|
||||||
|
|
||||||
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
|
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
|
||||||
promises.push(this.openTab({
|
promises.push(this.openNewTab({
|
||||||
userContextId: userContextId,
|
userContextId: userContextId,
|
||||||
url: object.url,
|
url: object.url,
|
||||||
nofocus: options.nofocus || false,
|
nofocus: options.nofocus || false,
|
||||||
@@ -269,12 +274,6 @@ const backgroundLogic = {
|
|||||||
|
|
||||||
cookieStoreId(userContextId) {
|
cookieStoreId(userContextId) {
|
||||||
return `firefox-container-${userContextId}`;
|
return `firefox-container-${userContextId}`;
|
||||||
},
|
}
|
||||||
|
|
||||||
_containerTabs(cookieStoreId) {
|
|
||||||
return browser.tabs.query({
|
|
||||||
cookieStoreId
|
|
||||||
}).catch((e) => {throw e;});
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
const MAJOR_VERSIONS = ["2.3.0", "2.4.0"];
|
const MAJOR_VERSIONS = ["2.3.0", "2.4.0"];
|
||||||
const badge = {
|
const badge = {
|
||||||
init() {
|
async init() {
|
||||||
this.displayBrowserActionBadge();
|
const currentWindow = await browser.windows.getCurrent();
|
||||||
|
this.displayBrowserActionBadge(currentWindow.incognito);
|
||||||
},
|
},
|
||||||
async displayBrowserActionBadge() {
|
async displayBrowserActionBadge(disable) {
|
||||||
|
if (disable) {
|
||||||
|
browser.browserAction.disable();
|
||||||
|
} else {
|
||||||
|
browser.browserAction.enable();
|
||||||
|
}
|
||||||
const extensionInfo = await backgroundLogic.getExtensionInfo();
|
const extensionInfo = await backgroundLogic.getExtensionInfo();
|
||||||
const storage = await browser.storage.local.get({browserActionBadgesClicked: []});
|
const storage = await browser.storage.local.get({browserActionBadgesClicked: []});
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ const identityState = {
|
|||||||
if (storageResponse && storeKey in storageResponse) {
|
if (storageResponse && storeKey in storageResponse) {
|
||||||
return storageResponse[storeKey];
|
return storageResponse[storeKey];
|
||||||
}
|
}
|
||||||
return null;
|
const defaultContainerState = identityState._createIdentityState();
|
||||||
|
await this.set(cookieStoreId, defaultContainerState);
|
||||||
|
|
||||||
|
return defaultContainerState;
|
||||||
},
|
},
|
||||||
|
|
||||||
set(cookieStoreId, data) {
|
set(cookieStoreId, data) {
|
||||||
@@ -29,19 +32,13 @@ const identityState = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async _isKnownContainer(userContextId) {
|
|
||||||
const cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
|
|
||||||
const state = await this.storageArea.get(cookieStoreId);
|
|
||||||
return !!state;
|
|
||||||
},
|
|
||||||
|
|
||||||
_createTabObject(tab) {
|
_createTabObject(tab) {
|
||||||
return Object.assign({}, tab);
|
return Object.assign({}, tab);
|
||||||
},
|
},
|
||||||
|
|
||||||
async storeHidden(cookieStoreId) {
|
async storeHidden(cookieStoreId, windowId) {
|
||||||
const containerState = await this.storageArea.get(cookieStoreId);
|
const containerState = await this.storageArea.get(cookieStoreId);
|
||||||
const tabsByContainer = await this._matchTabsByContainer(cookieStoreId);
|
const tabsByContainer = await browser.tabs.query({cookieStoreId, windowId});
|
||||||
tabsByContainer.forEach((tab) => {
|
tabsByContainer.forEach((tab) => {
|
||||||
const tabObject = this._createTabObject(tab);
|
const tabObject = this._createTabObject(tab);
|
||||||
// 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
|
||||||
@@ -54,89 +51,9 @@ const identityState = {
|
|||||||
return this.storageArea.set(cookieStoreId, containerState);
|
return this.storageArea.set(cookieStoreId, containerState);
|
||||||
},
|
},
|
||||||
|
|
||||||
async containersCounts() {
|
|
||||||
let containersCounts = { // eslint-disable-line prefer-const
|
|
||||||
"shown": 0,
|
|
||||||
"hidden": 0,
|
|
||||||
"total": 0
|
|
||||||
};
|
|
||||||
const containers = await browser.contextualIdentities.query({});
|
|
||||||
for (const id in containers) {
|
|
||||||
const container = containers[id];
|
|
||||||
await this.remapTabsIfMissing(container.cookieStoreId);
|
|
||||||
const containerState = await this.storageArea.get(container.cookieStoreId);
|
|
||||||
if (containerState.openTabs > 0) {
|
|
||||||
++containersCounts.shown;
|
|
||||||
++containersCounts.total;
|
|
||||||
continue;
|
|
||||||
} else if (containerState.hiddenTabs.length > 0) {
|
|
||||||
++containersCounts.hidden;
|
|
||||||
++containersCounts.total;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return containersCounts;
|
|
||||||
},
|
|
||||||
|
|
||||||
async containerTabCount(cookieStoreId) {
|
|
||||||
// Returns the total of open and hidden tabs with this userContextId
|
|
||||||
let containerTabsCount = 0;
|
|
||||||
await identityState.remapTabsIfMissing(cookieStoreId);
|
|
||||||
const containerState = await this.storageArea.get(cookieStoreId);
|
|
||||||
containerTabsCount += containerState.openTabs;
|
|
||||||
containerTabsCount += containerState.hiddenTabs.length;
|
|
||||||
return containerTabsCount;
|
|
||||||
},
|
|
||||||
|
|
||||||
async totalContainerTabsCount() {
|
|
||||||
// Returns the number of total open tabs across ALL containers
|
|
||||||
let totalContainerTabsCount = 0;
|
|
||||||
const containers = await browser.contextualIdentities.query({});
|
|
||||||
for (const id in containers) {
|
|
||||||
const container = containers[id];
|
|
||||||
const cookieStoreId = container.cookieStoreId;
|
|
||||||
await identityState.remapTabsIfMissing(cookieStoreId);
|
|
||||||
totalContainerTabsCount += await this.storageArea.get(cookieStoreId).openTabs;
|
|
||||||
}
|
|
||||||
return totalContainerTabsCount;
|
|
||||||
},
|
|
||||||
|
|
||||||
async totalNonContainerTabsCount() {
|
|
||||||
// Returns the number of open tabs NOT IN a container
|
|
||||||
let totalNonContainerTabsCount = 0;
|
|
||||||
const tabs = await browser.tabs.query({});
|
|
||||||
for (const tab of tabs) {
|
|
||||||
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(tab.cookieStoreId);
|
|
||||||
if (userContextId === 0) {
|
|
||||||
++totalNonContainerTabsCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return totalNonContainerTabsCount;
|
|
||||||
},
|
|
||||||
|
|
||||||
async remapTabsIfMissing(cookieStoreId) {
|
|
||||||
// We already know this cookieStoreId.
|
|
||||||
const containerState = await this.storageArea.get(cookieStoreId) || this._createIdentityState();
|
|
||||||
|
|
||||||
await this.storageArea.set(cookieStoreId, containerState);
|
|
||||||
await this.remapTabsFromUserContextId(cookieStoreId);
|
|
||||||
},
|
|
||||||
|
|
||||||
_matchTabsByContainer(cookieStoreId) {
|
|
||||||
return browser.tabs.query({cookieStoreId});
|
|
||||||
},
|
|
||||||
|
|
||||||
async remapTabsFromUserContextId(cookieStoreId) {
|
|
||||||
const tabsByContainer = await this._matchTabsByContainer(cookieStoreId);
|
|
||||||
const containerState = await this.storageArea.get(cookieStoreId);
|
|
||||||
containerState.openTabs = tabsByContainer.length;
|
|
||||||
await this.storageArea.set(cookieStoreId, containerState);
|
|
||||||
},
|
|
||||||
|
|
||||||
_createIdentityState() {
|
_createIdentityState() {
|
||||||
return {
|
return {
|
||||||
hiddenTabs: [],
|
hiddenTabs: []
|
||||||
openTabs: 0
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
"js/background/badge.js",
|
"js/background/badge.js",
|
||||||
"js/background/identityState.js",
|
"js/background/identityState.js",
|
||||||
"js/background/messageHandler.js",
|
"js/background/messageHandler.js",
|
||||||
"js/background/tabPageCounter.js",
|
|
||||||
"js/backdround/init.js"
|
"js/backdround/init.js"
|
||||||
]
|
]
|
||||||
-->
|
-->
|
||||||
@@ -20,7 +19,6 @@
|
|||||||
<script type="text/javascript" src="badge.js"></script>
|
<script type="text/javascript" src="badge.js"></script>
|
||||||
<script type="text/javascript" src="identityState.js"></script>
|
<script type="text/javascript" src="identityState.js"></script>
|
||||||
<script type="text/javascript" src="messageHandler.js"></script>
|
<script type="text/javascript" src="messageHandler.js"></script>
|
||||||
<script type="text/javascript" src="tabPageCounter.js"></script>
|
|
||||||
<script type="text/javascript" src="init.js"></script>
|
<script type="text/javascript" src="init.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const messageHandler = {
|
|||||||
// We use this to catch redirected tabs that have just opened
|
// We use this to catch redirected tabs that have just opened
|
||||||
// If this were in platform we would change how the tab opens based on "new tab" link navigations such as ctrl+click
|
// If this were in platform we would change how the tab opens based on "new tab" link navigations such as ctrl+click
|
||||||
LAST_CREATED_TAB_TIMER: 2000,
|
LAST_CREATED_TAB_TIMER: 2000,
|
||||||
|
unhideQueue: [],
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
// Handles messages from webextension code
|
// Handles messages from webextension code
|
||||||
@@ -16,10 +17,6 @@ const messageHandler = {
|
|||||||
case "createOrUpdateContainer":
|
case "createOrUpdateContainer":
|
||||||
response = backgroundLogic.createOrUpdateContainer(m.message);
|
response = backgroundLogic.createOrUpdateContainer(m.message);
|
||||||
break;
|
break;
|
||||||
case "openTab":
|
|
||||||
// Same as open-tab for index.js
|
|
||||||
response = backgroundLogic.openTab(m.message);
|
|
||||||
break;
|
|
||||||
case "neverAsk":
|
case "neverAsk":
|
||||||
assignManager._neverAsk(m);
|
assignManager._neverAsk(m);
|
||||||
break;
|
break;
|
||||||
@@ -45,23 +42,28 @@ const messageHandler = {
|
|||||||
backgroundLogic.showTabs({cookieStoreId: m.cookieStoreId});
|
backgroundLogic.showTabs({cookieStoreId: m.cookieStoreId});
|
||||||
break;
|
break;
|
||||||
case "hideTabs":
|
case "hideTabs":
|
||||||
backgroundLogic.hideTabs({cookieStoreId: m.cookieStoreId});
|
backgroundLogic.hideTabs({
|
||||||
|
cookieStoreId: m.cookieStoreId,
|
||||||
|
windowId: m.windowId
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "checkIncompatibleAddons":
|
case "checkIncompatibleAddons":
|
||||||
// TODO
|
// TODO
|
||||||
break;
|
break;
|
||||||
case "moveTabsToWindow":
|
case "moveTabsToWindow":
|
||||||
response = backgroundLogic.moveTabsToWindow({
|
response = backgroundLogic.moveTabsToWindow({
|
||||||
cookieStoreId: m.cookieStoreId
|
cookieStoreId: m.cookieStoreId,
|
||||||
|
windowId: m.windowId
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "getTabs":
|
case "getTabs":
|
||||||
response = backgroundLogic.getTabs({
|
response = backgroundLogic.getTabs({
|
||||||
cookieStoreId: m.cookieStoreId
|
cookieStoreId: m.cookieStoreId,
|
||||||
|
windowId: m.windowId
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "queryIdentitiesState":
|
case "queryIdentitiesState":
|
||||||
response = backgroundLogic.queryIdentitiesState();
|
response = backgroundLogic.queryIdentitiesState(m.message.windowId);
|
||||||
break;
|
break;
|
||||||
case "exemptContainerAssignment":
|
case "exemptContainerAssignment":
|
||||||
response = assignManager._exemptTab(m);
|
response = assignManager._exemptTab(m);
|
||||||
@@ -70,38 +72,16 @@ const messageHandler = {
|
|||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handles messages from sdk code
|
if (browser.contextualIdentities.onRemoved) {
|
||||||
const port = browser.runtime.connect();
|
browser.contextualIdentities.onRemoved.addListener(({contextualIdentity}) => {
|
||||||
port.onMessage.addListener(m => {
|
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(contextualIdentity.cookieStoreId);
|
||||||
switch (m.type) {
|
backgroundLogic.deleteContainer(userContextId, true);
|
||||||
case "open-tab":
|
});
|
||||||
backgroundLogic.openTab(m.message);
|
}
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unhandled message type: ${m.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
browser.tabs.onCreated.addListener((tab) => {
|
|
||||||
// This works at capturing the tabs as they are created
|
|
||||||
// However we need onFocusChanged and onActivated to capture the initial tab
|
|
||||||
if (tab.id === -1) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
tabPageCounter.initTabCounter(tab);
|
|
||||||
});
|
|
||||||
|
|
||||||
browser.tabs.onRemoved.addListener((tabId) => {
|
|
||||||
if (tabId === -1) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
tabPageCounter.sendTabCountAndDelete(tabId);
|
|
||||||
});
|
|
||||||
|
|
||||||
browser.tabs.onActivated.addListener((info) => {
|
browser.tabs.onActivated.addListener((info) => {
|
||||||
assignManager.removeContextMenu();
|
assignManager.removeContextMenu();
|
||||||
browser.tabs.get(info.tabId).then((tab) => {
|
browser.tabs.get(info.tabId).then((tab) => {
|
||||||
tabPageCounter.initTabCounter(tab);
|
|
||||||
assignManager.calculateContextMenu(tab);
|
assignManager.calculateContextMenu(tab);
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
throw e;
|
throw e;
|
||||||
@@ -109,34 +89,7 @@ const messageHandler = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
browser.windows.onFocusChanged.addListener((windowId) => {
|
browser.windows.onFocusChanged.addListener((windowId) => {
|
||||||
assignManager.removeContextMenu();
|
this.onFocusChangedCallback(windowId);
|
||||||
// browserAction loses background color in new windows ...
|
|
||||||
// https://bugzil.la/1314674
|
|
||||||
// https://github.com/mozilla/testpilot-containers/issues/608
|
|
||||||
// ... so re-call displayBrowserActionBadge on window changes
|
|
||||||
badge.displayBrowserActionBadge();
|
|
||||||
browser.tabs.query({active: true, windowId}).then((tabs) => {
|
|
||||||
if (tabs && tabs[0]) {
|
|
||||||
tabPageCounter.initTabCounter(tabs[0]);
|
|
||||||
assignManager.calculateContextMenu(tabs[0]);
|
|
||||||
}
|
|
||||||
}).catch((e) => {
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
browser.idle.onStateChanged.addListener((newState) => {
|
|
||||||
browser.tabs.query({}).then(tabs => {
|
|
||||||
for (let tab of tabs) { // eslint-disable-line prefer-const
|
|
||||||
if (newState === "idle") {
|
|
||||||
tabPageCounter.sendTabCountAndDelete(tab.id, "user-went-idle");
|
|
||||||
} else if (newState === "active" && tab.active) {
|
|
||||||
tabPageCounter.initTabCounter(tab);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).catch(e => {
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
browser.webRequest.onCompleted.addListener((details) => {
|
browser.webRequest.onCompleted.addListener((details) => {
|
||||||
@@ -146,21 +99,50 @@ const messageHandler = {
|
|||||||
assignManager.removeContextMenu();
|
assignManager.removeContextMenu();
|
||||||
|
|
||||||
browser.tabs.get(details.tabId).then((tab) => {
|
browser.tabs.get(details.tabId).then((tab) => {
|
||||||
tabPageCounter.incrementTabCount(tab);
|
|
||||||
assignManager.calculateContextMenu(tab);
|
assignManager.calculateContextMenu(tab);
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
}, {urls: ["<all_urls>"], types: ["main_frame"]});
|
}, {urls: ["<all_urls>"], types: ["main_frame"]});
|
||||||
|
|
||||||
// lets remember the last tab created so we can close it if it looks like a redirect
|
browser.tabs.onCreated.addListener((tab) => {
|
||||||
browser.tabs.onCreated.addListener((details) => {
|
// lets remember the last tab created so we can close it if it looks like a redirect
|
||||||
this.lastCreatedTab = details;
|
this.lastCreatedTab = tab;
|
||||||
|
if (tab.cookieStoreId) {
|
||||||
|
this.unhideContainer(tab.cookieStoreId);
|
||||||
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.lastCreatedTab = null;
|
this.lastCreatedTab = null;
|
||||||
}, this.LAST_CREATED_TAB_TIMER);
|
}, this.LAST_CREATED_TAB_TIMER);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async unhideContainer(cookieStoreId) {
|
||||||
|
if (!this.unhideQueue.includes(cookieStoreId)) {
|
||||||
|
this.unhideQueue.push(cookieStoreId);
|
||||||
|
// Unhide all hidden tabs
|
||||||
|
await backgroundLogic.showTabs({
|
||||||
|
cookieStoreId
|
||||||
|
});
|
||||||
|
this.unhideQueue.splice(this.unhideQueue.indexOf(cookieStoreId), 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async onFocusChangedCallback(windowId) {
|
||||||
|
assignManager.removeContextMenu();
|
||||||
|
const currentWindow = await browser.windows.getCurrent();
|
||||||
|
// browserAction loses background color in new windows ...
|
||||||
|
// https://bugzil.la/1314674
|
||||||
|
// https://github.com/mozilla/testpilot-containers/issues/608
|
||||||
|
// ... so re-call displayBrowserActionBadge on window changes
|
||||||
|
badge.displayBrowserActionBadge(currentWindow.incognito);
|
||||||
|
browser.tabs.query({active: true, windowId}).then((tabs) => {
|
||||||
|
if (tabs && tabs[0]) {
|
||||||
|
assignManager.calculateContextMenu(tabs[0]);
|
||||||
|
}
|
||||||
|
}).catch((e) => {
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const tabPageCounter = {
|
|
||||||
counters: {},
|
|
||||||
|
|
||||||
initTabCounter(tab) {
|
|
||||||
if (tab.id in this.counters) {
|
|
||||||
if (!("activity" in this.counters[tab.id])) {
|
|
||||||
this.counters[tab.id].activity = {
|
|
||||||
"cookieStoreId": tab.cookieStoreId,
|
|
||||||
"pageRequests": 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!("tab" in this.counters[tab.id])) {
|
|
||||||
this.counters[tab.id].tab = {
|
|
||||||
"cookieStoreId": tab.cookieStoreId,
|
|
||||||
"pageRequests": 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.counters[tab.id] = {};
|
|
||||||
this.counters[tab.id].tab = {
|
|
||||||
"cookieStoreId": tab.cookieStoreId,
|
|
||||||
"pageRequests": 0
|
|
||||||
};
|
|
||||||
this.counters[tab.id].activity = {
|
|
||||||
"cookieStoreId": tab.cookieStoreId,
|
|
||||||
"pageRequests": 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
sendTabCountAndDelete(tabId, why = "user-closed-tab") {
|
|
||||||
if (!(this.counters[tabId])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (why === "user-closed-tab" && this.counters[tabId].tab) {
|
|
||||||
// When we send the ping because the user closed the tab,
|
|
||||||
// delete both the 'tab' and 'activity' counters
|
|
||||||
delete this.counters[tabId];
|
|
||||||
} else if (why === "user-went-idle" && this.counters[tabId].activity) {
|
|
||||||
// When we send the ping because the user went idle,
|
|
||||||
// only reset the 'activity' counter
|
|
||||||
this.counters[tabId].activity = {
|
|
||||||
"cookieStoreId": this.counters[tabId].tab.cookieStoreId,
|
|
||||||
"pageRequests": 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
incrementTabCount(tab) {
|
|
||||||
this.initTabCounter(tab);
|
|
||||||
this.counters[tab.id].tab.pageRequests++;
|
|
||||||
this.counters[tab.id].activity.pageRequests++;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -190,7 +190,10 @@ const Logic = {
|
|||||||
const [identities, state] = await Promise.all([
|
const [identities, state] = await Promise.all([
|
||||||
browser.contextualIdentities.query({}),
|
browser.contextualIdentities.query({}),
|
||||||
browser.runtime.sendMessage({
|
browser.runtime.sendMessage({
|
||||||
method: "queryIdentitiesState"
|
method: "queryIdentitiesState",
|
||||||
|
message: {
|
||||||
|
windowId: browser.windows.WINDOW_ID_CURRENT
|
||||||
|
}
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
this._identities = identities.map((identity) => {
|
this._identities = identities.map((identity) => {
|
||||||
@@ -451,7 +454,7 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
|||||||
panelSelector: "#container-panel",
|
panelSelector: "#container-panel",
|
||||||
|
|
||||||
// This method is called when the object is registered.
|
// This method is called when the object is registered.
|
||||||
initialize() {
|
async initialize() {
|
||||||
Logic.addEnterHandler(document.querySelector("#container-add-link"), () => {
|
Logic.addEnterHandler(document.querySelector("#container-add-link"), () => {
|
||||||
Logic.showPanel(P_CONTAINER_EDIT, { name: Logic.generateIdentityName() });
|
Logic.showPanel(P_CONTAINER_EDIT, { name: Logic.generateIdentityName() });
|
||||||
});
|
});
|
||||||
@@ -599,12 +602,8 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
|||||||
|| e.target.parentNode.matches(".open-newtab")
|
|| e.target.parentNode.matches(".open-newtab")
|
||||||
|| e.type === "keydown") {
|
|| e.type === "keydown") {
|
||||||
try {
|
try {
|
||||||
await browser.runtime.sendMessage({
|
browser.tabs.create({
|
||||||
method: "openTab",
|
cookieStoreId: identity.cookieStoreId
|
||||||
message: {
|
|
||||||
userContextId: Logic.userContextId(identity.cookieStoreId),
|
|
||||||
source: "pop-up"
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
window.close();
|
window.close();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -654,6 +653,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
|||||||
try {
|
try {
|
||||||
browser.runtime.sendMessage({
|
browser.runtime.sendMessage({
|
||||||
method: identity.hasHiddenTabs ? "showTabs" : "hideTabs",
|
method: identity.hasHiddenTabs ? "showTabs" : "hideTabs",
|
||||||
|
windowId: browser.windows.WINDOW_ID_CURRENT,
|
||||||
cookieStoreId: Logic.currentCookieStoreId()
|
cookieStoreId: Logic.currentCookieStoreId()
|
||||||
});
|
});
|
||||||
window.close();
|
window.close();
|
||||||
@@ -685,6 +685,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
|||||||
Logic.addEnterHandler(moveTabsEl, async function () {
|
Logic.addEnterHandler(moveTabsEl, async function () {
|
||||||
await browser.runtime.sendMessage({
|
await browser.runtime.sendMessage({
|
||||||
method: "moveTabsToWindow",
|
method: "moveTabsToWindow",
|
||||||
|
windowId: browser.windows.WINDOW_ID_CURRENT,
|
||||||
cookieStoreId: Logic.currentIdentity().cookieStoreId,
|
cookieStoreId: Logic.currentIdentity().cookieStoreId,
|
||||||
});
|
});
|
||||||
window.close();
|
window.close();
|
||||||
@@ -726,6 +727,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
|||||||
// Let's retrieve the list of tabs.
|
// Let's retrieve the list of tabs.
|
||||||
const tabs = await browser.runtime.sendMessage({
|
const tabs = await browser.runtime.sendMessage({
|
||||||
method: "getTabs",
|
method: "getTabs",
|
||||||
|
windowId: browser.windows.WINDOW_ID_CURRENT,
|
||||||
cookieStoreId: Logic.currentIdentity().cookieStoreId
|
cookieStoreId: Logic.currentIdentity().cookieStoreId
|
||||||
});
|
});
|
||||||
return this.buildInfoTable(tabs);
|
return this.buildInfoTable(tabs);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "Containers Experiment",
|
"name": "Firefox Multi-Account Containers",
|
||||||
"version": "3.1.0",
|
"version": "4.0.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": "Firefox Multi-Account Containers helps you keep all the parts of your online life contained in different tabs. Custom labels and color-coded tabs help keep different activities — like online shopping, travel planning, or checking work email — separate.",
|
||||||
"icons": {
|
"icons": {
|
||||||
"48": "img/container-site-d-48.png",
|
"48": "img/container-site-d-48.png",
|
||||||
"96": "img/container-site-d-96.png"
|
"96": "img/container-site-d-96.png"
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"browser_action": {
|
"browser_action": {
|
||||||
"browser_style": true,
|
"browser_style": true,
|
||||||
"default_icon": "img/container-site.svg",
|
"default_icon": "img/container-site.svg",
|
||||||
"default_title": "Containers",
|
"default_title": "Firefox Multi-Account Containers",
|
||||||
"default_popup": "popup.html"
|
"default_popup": "popup.html"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||||
<title>Containers browserAction Popup</title>
|
<title>Firefox Multi-Account Containers</title>
|
||||||
<link rel="stylesheet" href="/css/popup.css">
|
<link rel="stylesheet" href="/css/popup.css">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
Reference in New Issue
Block a user