Compare commits
135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee63f72f3e | |||
| 4cbcfac0f4 | |||
| 641c95e64e | |||
| b646a9183c | |||
| 6e2ed6393e | |||
| af98174a19 | |||
| 1c8530ef02 | |||
| 366f9ec047 | |||
| 63343f18eb | |||
| 080e9dd22d | |||
| 268da1350a | |||
| e0abaa67e2 | |||
| 69f06f96cc | |||
| 62d479a3f3 | |||
| 315c75f2ac | |||
| 9fcf822140 | |||
| e7ac72a6a2 | |||
| da39d18ce0 | |||
| 63025ab3d6 | |||
| 1c38e09dcc | |||
| 2178e26220 | |||
| 310e2fa503 | |||
| 30c55e093c | |||
| d0037d1377 | |||
| 2a3aa296c0 | |||
| 10e83d3795 | |||
| 29b9590878 | |||
| a86bcf7983 | |||
| e28b25e04c | |||
| 1cc3ab83b9 | |||
| 0566c9f962 | |||
| 8c92d8ef5d | |||
| a8cac47125 | |||
| e191255c47 | |||
| 7eb752c2f7 | |||
| 40f2f1af5e | |||
| 214a83deda | |||
| 51b804f96d | |||
| 83e8340a70 | |||
| 6292d9b25d | |||
| 2f5e195c91 | |||
| af966d6d29 | |||
| 4ed136299b | |||
| 5237e67fa6 | |||
| 13cd601212 | |||
| bc847b53f5 | |||
| 4e0180d521 | |||
| 13e4b4e7f7 | |||
| 2278498b06 | |||
| 6533c74d0a | |||
| 9b0fe826de | |||
| 5d75d4525d | |||
| 59f2b8a764 | |||
| 68c21624e2 | |||
| bfc6f68978 | |||
| 78ef2e8304 | |||
| 5b85fc1690 | |||
| be8f6bbe7c | |||
| dfd420d1a5 | |||
| 4030b6eeec | |||
| d2b4d972e1 | |||
| 06d381b931 | |||
| c2ed5420a4 | |||
| fc789a49ac | |||
| bf75f52a52 | |||
| 090ae1f139 | |||
| cd2e110c17 | |||
| d63e887ef7 | |||
| ea0c9d4306 | |||
| e5a87ab535 | |||
| 3b9da05e67 | |||
| 5c5cf02249 | |||
| 8503e9c9c5 | |||
| 7f37ed906f | |||
| 15477dc384 | |||
| 06d35e65ce | |||
| 49e8afaf9a | |||
| 9903e811c2 | |||
| e467988a71 | |||
| 094a0e2391 | |||
| df8bf4e5e4 | |||
| 45f34a586a | |||
| ab2b9a48c7 | |||
| 82c9cac34c | |||
| 5cd2ac0187 | |||
| 0f9dd77687 | |||
| fb845cce12 | |||
| a29fae0893 | |||
| bd72b4e759 | |||
| 4f6e91336f | |||
| 69d497bacd | |||
| d3413c7afc | |||
| bb96ab9c37 | |||
| 6a10c1c970 | |||
| 1d385d2aaa | |||
| fd918408f4 | |||
| 08ba094748 | |||
| 5916bd2871 | |||
| 3700e6f461 | |||
| a99cd9c8ef | |||
| dad3214986 | |||
| 099d07bf1f | |||
| 93b6378b22 | |||
| 84dd73bff5 | |||
| b0c53063d2 | |||
| 1819e6cde9 | |||
| 54c598e22e | |||
| e499ff5711 | |||
| cd03ea7a59 | |||
| 46b155c90f | |||
| 31f3a76a1c | |||
| 4ffb587d9e | |||
| 4d61fa190c | |||
| 4a97e07d43 | |||
| 41df7a10dc | |||
| 5089091617 | |||
| 54ccf5b9ec | |||
| ee98034572 | |||
| d8fd47a353 | |||
| 02300630f6 | |||
| 3805f12e17 | |||
| 4a48a7debb | |||
| 26e6efd611 | |||
| 92ab56448c | |||
| ac02c45e81 | |||
| 1ec86c7fd2 | |||
| 8f80b527f5 | |||
| 337dee2061 | |||
| f2ddc7fd84 | |||
| f4597eae84 | |||
| 5887f991b3 | |||
| 1de3f42385 | |||
| 91ec0c4a6d | |||
| 0b1d6e9087 | |||
| 2ee9682950 |
@@ -1 +1,3 @@
|
||||
testpilot-metrics.js
|
||||
lib/shield/*.js
|
||||
lib/testpilot/*.js
|
||||
|
||||
+20
-6
@@ -1,14 +1,15 @@
|
||||
module.exports = {
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 8
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true,
|
||||
"webextensions": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
],
|
||||
"globals": {
|
||||
"Utils": true,
|
||||
"CustomizableUI": true,
|
||||
"CustomizableWidgets": true,
|
||||
"SessionStore": true,
|
||||
@@ -16,7 +17,10 @@ module.exports = {
|
||||
},
|
||||
"plugins": [
|
||||
"promise",
|
||||
"no-unescaped"
|
||||
"no-unsanitized"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
],
|
||||
"root": true,
|
||||
"rules": {
|
||||
@@ -29,8 +33,18 @@ module.exports = {
|
||||
"promise/no-promise-in-callback": "warn",
|
||||
"promise/no-return-wrap": "error",
|
||||
"promise/param-names": "error",
|
||||
"no-unescaped/no-key-assignment": "error",
|
||||
"no-unescaped/enforce": "error",
|
||||
|
||||
"no-unsanitized/method": [
|
||||
"error"
|
||||
],
|
||||
"no-unsanitized/property": [
|
||||
"error",
|
||||
{
|
||||
"escape": {
|
||||
"taggedTemplates": ["escaped"]
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"eqeqeq": "error",
|
||||
"indent": ["error", 2],
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
.DS_Store
|
||||
package-lock.json
|
||||
node_modules
|
||||
README.html
|
||||
*.xpi
|
||||
*.swp
|
||||
*.swo
|
||||
.vimrc
|
||||
.env
|
||||
addon.env
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
"declaration-block-no-duplicate-properties": true,
|
||||
"order/declaration-block-properties-alphabetical-order": true,
|
||||
"property-blacklist": [
|
||||
"/height/",
|
||||
"/(min[-]|max[-])height/",
|
||||
"/width/",
|
||||
"/top/",
|
||||
"/bottom/",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Code Of Conduct
|
||||
|
||||
This add-on follows the [Mozilla Community Participation Guidelines](https://www.mozilla.org/en-US/about/governance/policies/participation/) for our code of conduct.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Contributing
|
||||
|
||||
Everyone is welcome to contribute to containers. Reach out to team members if you have questions:
|
||||
|
||||
- IRC: #containers on irc.mozilla.org
|
||||
- Email: containers@mozilla.com
|
||||
|
||||
## Filing bugs
|
||||
|
||||
If you find a bug with containers, please file a issue.
|
||||
|
||||
Check first if the bug might already exist: https://github.com/mozilla/testpilot-containers/issues
|
||||
|
||||
[Open an issue](https://github.com/mozilla/testpilot-containers/issues/new)
|
||||
|
||||
1. Visit about:support
|
||||
2. Click "Copy raw data to clipboard" and paste into the bug. Alternatively copy the following sections into the issue:
|
||||
- Application Basics
|
||||
- Nightly Features (if you are in nightly)
|
||||
- Extensions
|
||||
- Experimental Features
|
||||
3. Include clear steps to reproduce the issue you have experienced.
|
||||
4. Include screenshots if possible.
|
||||
|
||||
## Sending Pull Requests
|
||||
|
||||
Patches should be submitted as pull requests. When submitting patches as PRs:
|
||||
|
||||
- You agree to license your code under the project's open source license (MPL 2.0).
|
||||
- Base your branch off the current master (see below for an example workflow).
|
||||
- Add both your code and new tests if relevant.
|
||||
- Run npm test to make sure all tests still pass.
|
||||
- Please do not include merge commits in pull requests; include only commits with the new relevant code.
|
||||
|
||||
See the main [README](./README.md) for information on prerequisites, installing, running and testing.
|
||||
@@ -1,25 +1,22 @@
|
||||
# Containers: Test Pilot Experiment
|
||||
# Containers Add-on
|
||||
|
||||
[](https://testpilot.firefox.com/experiments/containers)
|
||||
|
||||
[Embedded Web Extension](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Embedded_WebExtensions) to experiment with [Containers](https://blog.mozilla.org/tanvi/2016/06/16/contextual-identities-on-the-web/) in [Firefox Test Pilot](https://testpilot.firefox.com/) to learn:
|
||||
[Embedded Web Extension](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Embedded_WebExtensions) to build [Containers](https://blog.mozilla.org/tanvi/2016/06/16/contextual-identities-on-the-web/) as a Firefox [Test Pilot](https://testpilot.firefox.com/) Experiment and [Shield Study](https://wiki.mozilla.org/Firefox/Shield/Shield_Studies) to learn:
|
||||
|
||||
* Will a general Firefox audience understand the Containers feature?
|
||||
* Is the UI as currently implemented in Nightly clear or discoverable?
|
||||
|
||||
See [the Product Hypothesis Document for more
|
||||
details](https://docs.google.com/document/d/1WQdHTVXROk7dYkSFluc6_hS44tqZjIrG9I-uPyzevE8/edit?ts=5824ba12#).
|
||||
For more info, see:
|
||||
|
||||
* [Test Pilot Product Hypothesis Document](https://docs.google.com/document/d/1WQdHTVXROk7dYkSFluc6_hS44tqZjIrG9I-uPyzevE8/edit#)
|
||||
* [Shield Product Hypothesis Document](https://docs.google.com/document/d/1vMD-fH_5hGDDqNvpRZk12_RhCN2WAe4_yaBamaNdtik/edit#)
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
* node 7+ (for jpm)
|
||||
* Firefox 51+
|
||||
|
||||
|
||||
## Run it
|
||||
|
||||
See Development
|
||||
* Firefox 53+
|
||||
|
||||
|
||||
## Development
|
||||
@@ -27,28 +24,23 @@ See Development
|
||||
|
||||
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.
|
||||
|
||||
|
||||
1. Make a new profile by running `/path/to/firefox -P`, which launches the profile editor. "Create Profile" -- name it whatever you wish (e.g. 'addon_dev') and store it in the default location. It's probably best to deselect the option to "Use without asking," since you probably don't want to use this as your default profile.
|
||||
|
||||
2. Once you've created your profile, click "Start Firefox". A new instance of Firefox should launch. Go to Tools->Add-ons and search for "DevPrefs". Install it. Quit Firefox.
|
||||
|
||||
3. Now you have a new, vanilla Firefox profile with the DevPrefs add-on installed. You can use your new profile with the code in _this_ repository like so:
|
||||
|
||||
**Beta building**
|
||||
#### Run the `.xpi` file in an unbranded build
|
||||
Release & Beta channels do not allow un-signed add-ons, even with the DevPrefs. So, you must run the add-on in an [unbranded build](https://wiki.mozilla.org/Add-ons/Extension_Signing#Unbranded_Builds):
|
||||
|
||||
To build this for 51 beta just using the downloaded version of beta will not work as XPI signature checking is disabled fully.
|
||||
1. Download and install an un-branded build of Firefox
|
||||
2. Download the latest `.xpi` from this repository's releases
|
||||
3. Run the un-branded build of Firefox with your DevPrefs profile
|
||||
4. Go to `about:addons`
|
||||
5. Click the gear, and select "Install Add-on From File..."
|
||||
6. Select the `.xpi` file
|
||||
|
||||
The only way to run the experiment is using an [unbranded version build](https://wiki.mozilla.org/Add-ons/Extension_Signing#Unbranded_Builds) or to build beta yourself:
|
||||
|
||||
1. [Download the mozilla-beta repo](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Source_Code/Mercurial#mozilla-beta_(prerelease_development_tree))
|
||||
2. [Create a mozconfig file](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Build_Instructions/Configuring_Build_Options) - probably optional
|
||||
3. `cd <reponame>`
|
||||
3. `./mach bootstrap`
|
||||
4. `./mach build`
|
||||
5. Follow the above instructions by creating the new profile via: `~/<reponame>/obj-x86_64-pc-linux-gnu/dist/bin/firefox -P` (Where "obj-x86_64-pc-linux-gnu" may be different depending on platform obj-...)
|
||||
|
||||
|
||||
### Run with jpm
|
||||
#### Run the TxP experiment with `jpm`
|
||||
|
||||
1. `git clone git@github.com:mozilla/testpilot-containers.git`
|
||||
2. `cd testpilot-containers`
|
||||
@@ -57,11 +49,22 @@ The only way to run the experiment is using an [unbranded version build](https:/
|
||||
|
||||
Check out the [Browser Toolbox](https://developer.mozilla.org/en-US/docs/Tools/Browser_Toolbox) for more information about debugging add-on code.
|
||||
|
||||
#### Run the shield study with `shield`
|
||||
|
||||
1. `git clone git@github.com:mozilla/testpilot-containers.git`
|
||||
2. `cd testpilot-containers`
|
||||
3. `npm install`
|
||||
4. `npm install -g shield-study-cli`
|
||||
5. `shield run . -- --binary Nightly`
|
||||
|
||||
### Building .xpi
|
||||
|
||||
To build a local .xpi, use the plain [`jpm
|
||||
xpi`](https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/jpm#jpm_xpi) command.
|
||||
To build a local testpilot-containers.xpi, use the plain [`jpm
|
||||
xpi`](https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/jpm#jpm_xpi) command,
|
||||
or run `npm run build`.
|
||||
|
||||
#### Building a shield .xpi
|
||||
To build a local shield-study-containers.xpi, run `npm run build-shield`.
|
||||
|
||||
### Signing an .xpi
|
||||
|
||||
@@ -75,6 +78,11 @@ add-on](https://addons.mozilla.org/en-US/developers/addon/containers-experiment/
|
||||
### Testing
|
||||
TBD
|
||||
|
||||
|
||||
### Distributing
|
||||
TBD
|
||||
|
||||
### Links
|
||||
|
||||
- [Licence](./LICENSE.txt)
|
||||
- [Contributing](./CONTRIBUTING.md)
|
||||
- [Code Of Conduct](./CODE_OF_CONDUCT.md)
|
||||
|
||||
+16
-16
@@ -52,55 +52,55 @@ value, or chrome url path as an alternate selector mitiages this bug.*/
|
||||
|
||||
[data-identity-icon="fingerprint"],
|
||||
[data-identity-icon="chrome://browser/skin/usercontext/personal.svg"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#fingerprint");
|
||||
--identity-icon: url("/data/usercontext.svg#fingerprint");
|
||||
}
|
||||
|
||||
[data-identity-icon="briefcase"],
|
||||
[data-identity-icon="chrome://browser/skin/usercontext/work.svg"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#briefcase");
|
||||
--identity-icon: url("/data/usercontext.svg#briefcase");
|
||||
}
|
||||
|
||||
[data-identity-icon="dollar"],
|
||||
[data-identity-icon="chrome://browser/skin/usercontext/banking.svg"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#dollar");
|
||||
--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("resource://testpilot-containers/data/usercontext.svg#cart");
|
||||
--identity-icon: url("/data/usercontext.svg#cart");
|
||||
}
|
||||
|
||||
[data-identity-icon="circle"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#circle");
|
||||
--identity-icon: url("/data/usercontext.svg#circle");
|
||||
}
|
||||
|
||||
[data-identity-icon="gift"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#gift");
|
||||
--identity-icon: url("/data/usercontext.svg#gift");
|
||||
}
|
||||
|
||||
[data-identity-icon="vacation"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#vacation");
|
||||
--identity-icon: url("/data/usercontext.svg#vacation");
|
||||
}
|
||||
|
||||
[data-identity-icon="food"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#food");
|
||||
--identity-icon: url("/data/usercontext.svg#food");
|
||||
}
|
||||
|
||||
[data-identity-icon="fruit"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#fruit");
|
||||
--identity-icon: url("/data/usercontext.svg#fruit");
|
||||
}
|
||||
|
||||
[data-identity-icon="pet"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#pet");
|
||||
--identity-icon: url("/data/usercontext.svg#pet");
|
||||
}
|
||||
|
||||
[data-identity-icon="tree"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#tree");
|
||||
--identity-icon: url("/data/usercontext.svg#tree");
|
||||
}
|
||||
|
||||
[data-identity-icon="chill"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#chill");
|
||||
--identity-icon: url("/data/usercontext.svg#chill");
|
||||
}
|
||||
|
||||
#userContext-indicator {
|
||||
@@ -139,7 +139,7 @@ value, or chrome url path as an alternate selector mitiages this bug.*/
|
||||
background-size: contain;
|
||||
fill: var(--identity-icon-color) !important;
|
||||
filter: url(/img/filters.svg#fill);
|
||||
filter: url(resource://testpilot-containers/data/filters.svg#fill);
|
||||
filter: url(/data/filters.svg#fill);
|
||||
}
|
||||
|
||||
/* containers experiment */
|
||||
@@ -200,7 +200,7 @@ special cases are addressed below */
|
||||
}
|
||||
|
||||
#new-tab-overlay {
|
||||
--icon-size: 26px;
|
||||
--icon-size: 16px;
|
||||
-moz-appearance: none;
|
||||
background: transparent;
|
||||
font-style: -moz-use-system-font;
|
||||
@@ -252,8 +252,8 @@ special cases are addressed below */
|
||||
}
|
||||
|
||||
#new-tab-overlay .menuitem-iconic[data-usercontextid] > .menu-iconic-left > .menu-iconic-icon {
|
||||
block-height: var(--icon-size);
|
||||
block-width: var(--icon-size);
|
||||
block-size: var(--icon-size);
|
||||
inline-size: var(--icon-size);
|
||||
}
|
||||
|
||||
.menuitem-iconic[data-usercontextid] > .menu-iconic-left {
|
||||
|
||||
+22
-3
@@ -68,6 +68,16 @@ 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
|
||||
@@ -76,7 +86,7 @@ of a `testpilottest` telemetry ping for each scenario.
|
||||
"userContextId": <userContextId>,
|
||||
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
|
||||
"event": "open-tab",
|
||||
"eventSource": ["tab-bar"|"pop-up"|"file-menu"]
|
||||
"eventSource": ["tab-bar"|"pop-up"|"file-menu"|"alltabs-menu"|"plus-button"]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -220,7 +230,7 @@ of a `testpilottest` telemetry ping for each scenario.
|
||||
}
|
||||
```
|
||||
|
||||
* The user clicks "Take me there" to reload a site into a container after the user picked "Always Open in this Container".
|
||||
* The user clicks "Open in *assigned* container" to reload a site into a container after the user picked "Always Open in this Container".
|
||||
|
||||
```js
|
||||
{
|
||||
@@ -229,6 +239,15 @@ of a `testpilottest` telemetry ping for each scenario.
|
||||
}
|
||||
```
|
||||
|
||||
* The user clicks "Open in *Current* container" to reload a site into a container after the user picked "Always Open in this Container".
|
||||
|
||||
```js
|
||||
{
|
||||
"uuid": <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
|
||||
@@ -260,7 +279,7 @@ local schema = {
|
||||
|
||||
### Valid data should be enforced on the server side:
|
||||
|
||||
* `eventSource` should be one of `tab-bar`, `pop-up`, or `file-menu`.
|
||||
* `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.
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Drop-in replacement for {@link external:sdk/event/target.EventTarget} for use
|
||||
* with es6 classes.
|
||||
* @module event-target
|
||||
* @author Martin Giger
|
||||
* @license MPL-2.0
|
||||
*/
|
||||
/**
|
||||
* An SDK class that add event reqistration methods
|
||||
* @external sdk/event/target
|
||||
* @requires sdk/event/target
|
||||
*/
|
||||
/**
|
||||
* @class EventTarget
|
||||
* @memberof external:sdk/event/target
|
||||
* @see {@link https://developer.mozilla.org/en-US/Add-ons/SDK/Low-Level_APIs/event_target#EventTarget}
|
||||
*/
|
||||
|
||||
// slightly modified from: https://raw.githubusercontent.com/freaktechnik/justintv-stream-notifications/master/lib/event-target.js
|
||||
|
||||
"use strict";
|
||||
|
||||
const { on, once, off, setListeners } = require("sdk/event/core");
|
||||
|
||||
/* istanbul ignore next */
|
||||
/**
|
||||
* @class
|
||||
*/
|
||||
class EventTarget {
|
||||
constructor(options) {
|
||||
setListeners(this, options);
|
||||
}
|
||||
|
||||
on(...args) {
|
||||
on(this, ...args);
|
||||
return this;
|
||||
}
|
||||
|
||||
once(...args) {
|
||||
once(this, ...args);
|
||||
return this;
|
||||
}
|
||||
|
||||
off(...args) {
|
||||
off(this, ...args);
|
||||
return this;
|
||||
}
|
||||
|
||||
removeListener(...args) {
|
||||
off(this, ...args);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
exports.EventTarget = EventTarget;
|
||||
@@ -0,0 +1,428 @@
|
||||
"use strict";
|
||||
|
||||
// Chrome privileged
|
||||
const {Cu} = require("chrome");
|
||||
const { Services } = Cu.import("resource://gre/modules/Services.jsm");
|
||||
const { TelemetryController } = Cu.import("resource://gre/modules/TelemetryController.jsm");
|
||||
const CID = Cu.import("resource://gre/modules/ClientID.jsm");
|
||||
|
||||
// sdk
|
||||
const { merge } = require("sdk/util/object");
|
||||
const querystring = require("sdk/querystring");
|
||||
const { prefs } = require("sdk/simple-prefs");
|
||||
const prefSvc = require("sdk/preferences/service");
|
||||
const { setInterval } = require("sdk/timers");
|
||||
const tabs = require("sdk/tabs");
|
||||
const { URL } = require("sdk/url");
|
||||
|
||||
const { EventTarget } = require("./event-target");
|
||||
const { emit } = require("sdk/event/core");
|
||||
const self = require("sdk/self");
|
||||
|
||||
const DAY = 86400*1000;
|
||||
|
||||
// ongoing within-addon fuses / timers
|
||||
let lastDailyPing = Date.now();
|
||||
|
||||
/* Functional, self-contained utils */
|
||||
|
||||
// equal probability choices from a list "choices"
|
||||
function chooseVariation(choices,rng=Math.random()) {
|
||||
let l = choices.length;
|
||||
return choices[Math.floor(l*Math.random())];
|
||||
}
|
||||
|
||||
function dateToUTC(date) {
|
||||
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds());
|
||||
}
|
||||
|
||||
function generateTelemetryIdIfNeeded() {
|
||||
let id = TelemetryController.clientID;
|
||||
/* istanbul ignore next */
|
||||
if (id == undefined) {
|
||||
return CID.ClientIDImpl._doLoadClientID()
|
||||
} else {
|
||||
return Promise.resolve(id)
|
||||
}
|
||||
}
|
||||
|
||||
function userId () {
|
||||
return prefSvc.get("toolkit.telemetry.cachedClientID","unknown");
|
||||
}
|
||||
|
||||
var Reporter = new EventTarget().on("report",
|
||||
(d) => prefSvc.get('shield.debug') && console.log("report",d)
|
||||
);
|
||||
|
||||
function report(data, src="addon", bucket="shield-study") {
|
||||
data = merge({}, data , {
|
||||
study_version: self.version,
|
||||
about: {
|
||||
_src: src,
|
||||
_v: 2
|
||||
}
|
||||
});
|
||||
if (prefSvc.get('shield.testing')) data.testing = true
|
||||
|
||||
emit(Reporter, "report", data);
|
||||
let telOptions = {addClientId: true, addEnvironment: true}
|
||||
return TelemetryController.submitExternalPing(bucket, data, telOptions);
|
||||
}
|
||||
|
||||
function survey (url, queryArgs={}) {
|
||||
if (! url) return
|
||||
|
||||
let U = new URL(url);
|
||||
let q = U.search;
|
||||
if (q) {
|
||||
url = U.href.split(q)[0];
|
||||
q = querystring.parse(querystring.unescape(q.slice(1)));
|
||||
} else {
|
||||
q = {};
|
||||
}
|
||||
// get user info.
|
||||
let newArgs = merge({},
|
||||
q,
|
||||
queryArgs
|
||||
);
|
||||
let searchstring = querystring.stringify(newArgs);
|
||||
url = url + "?" + searchstring;
|
||||
return url;
|
||||
}
|
||||
|
||||
|
||||
function setOrGetFirstrun () {
|
||||
let firstrun = prefs["shield.firstrun"];
|
||||
if (firstrun === undefined) {
|
||||
firstrun = prefs["shield.firstrun"] = String(dateToUTC(new Date())) // in utc, user set
|
||||
}
|
||||
return Number(firstrun)
|
||||
}
|
||||
|
||||
function reuseVariation (choices) {
|
||||
return prefs["shield.variation"];
|
||||
}
|
||||
|
||||
function setVariation (choice) {
|
||||
prefs["shield.variation"] = choice
|
||||
return choice
|
||||
}
|
||||
|
||||
function die (addonId=self.id) {
|
||||
/* istanbul ignore else */
|
||||
if (prefSvc.get("shield.fakedie")) return;
|
||||
/* istanbul ignore next */
|
||||
require("sdk/addon/installer").uninstall(addonId);
|
||||
}
|
||||
|
||||
// TODO: GRL vulnerable to clock time issues #1
|
||||
function expired (xconfig, now = Date.now() ) {
|
||||
return ((now - Number(xconfig.firstrun))/ DAY) > xconfig.days;
|
||||
}
|
||||
|
||||
function resetShieldPrefs () {
|
||||
delete prefs['shield.firstrun'];
|
||||
delete prefs['shield.variation'];
|
||||
}
|
||||
|
||||
function cleanup () {
|
||||
prefSvc.keys(`extensions.${self.preferencesBranch}`).forEach (
|
||||
(p) => {
|
||||
delete prefs[p];
|
||||
})
|
||||
}
|
||||
|
||||
function telemetrySubset (xconfig) {
|
||||
return {
|
||||
study_name: xconfig.name,
|
||||
branch: xconfig.variation,
|
||||
}
|
||||
}
|
||||
|
||||
class Study extends EventTarget {
|
||||
constructor (config) {
|
||||
super();
|
||||
this.config = merge({
|
||||
name: self.addonId,
|
||||
variations: {'observe-only': () => {}},
|
||||
surveyUrls: {},
|
||||
days: 7
|
||||
},config);
|
||||
|
||||
this.config.firstrun = setOrGetFirstrun();
|
||||
|
||||
let variation = reuseVariation();
|
||||
if (variation === undefined) {
|
||||
variation = this.decideVariation();
|
||||
if (!(variation in this.config.variations)) {
|
||||
// chaijs doesn't think this is an instanceof Error
|
||||
// freaktechnik and gregglind debugged for a while.
|
||||
// sdk errors might not be 'Errors' or chai is wack, who knows.
|
||||
// https://dxr.mozilla.org/mozilla-central/search?q=regexp%3AError%5Cs%3F(%3A%7C%3D)+path%3Aaddon-sdk%2Fsource%2F&redirect=false would list
|
||||
throw new Error("Study Error: chosen variation must be in config.variations")
|
||||
}
|
||||
setVariation(variation);
|
||||
}
|
||||
this.config.variation = variation;
|
||||
|
||||
this.flags = {
|
||||
ineligibleDie: undefined
|
||||
};
|
||||
this.states = [];
|
||||
// all these work, but could be cleaner. I hate the `bind` stuff.
|
||||
this.on(
|
||||
"change", (function (newstate) {
|
||||
prefSvc.get('shield.debug') && console.log(newstate, this.states);
|
||||
this.states.push(newstate);
|
||||
emit(this, newstate); // could have checks here.
|
||||
}).bind(this)
|
||||
)
|
||||
this.on(
|
||||
"starting", (function () {
|
||||
this.changeState("modifying");
|
||||
}).bind(this)
|
||||
)
|
||||
this.on(
|
||||
"maybe-installing", (function () {
|
||||
if (!this.isEligible()) {
|
||||
this.changeState("ineligible-die");
|
||||
} else {
|
||||
this.changeState("installed")
|
||||
}
|
||||
}).bind(this)
|
||||
)
|
||||
this.on(
|
||||
"ineligible-die", (function () {
|
||||
try {this.whenIneligible()} catch (err) {/*ok*/} finally { /*ok*/ }
|
||||
this.flags.ineligibleDie = true;
|
||||
this.report(merge({}, telemetrySubset(this.config), {study_state: "ineligible"}), "shield");
|
||||
this.final();
|
||||
die();
|
||||
}).bind(this)
|
||||
)
|
||||
this.on(
|
||||
"installed", (function () {
|
||||
try {this.whenInstalled()} catch (err) {/*ok*/} finally { /*ok*/ }
|
||||
this.report(merge({}, telemetrySubset(this.config), {study_state: "install"}), "shield");
|
||||
this.changeState("modifying");
|
||||
}).bind(this)
|
||||
)
|
||||
this.on(
|
||||
"modifying", (function () {
|
||||
var mybranchname = this.variation;
|
||||
this.config.variations[mybranchname](); // do the effect
|
||||
this.changeState("running");
|
||||
}).bind(this)
|
||||
)
|
||||
this.on( // the one 'many'
|
||||
"running", (function () {
|
||||
// report success
|
||||
this.report(merge({}, telemetrySubset(this.config), {study_state: "running"}), "shield");
|
||||
this.final();
|
||||
}).bind(this)
|
||||
)
|
||||
this.on(
|
||||
"normal-shutdown", (function () {
|
||||
this.flags.dying = true;
|
||||
this.report(merge({}, telemetrySubset(this.config), {study_state: "shutdown"}), "shield");
|
||||
this.final();
|
||||
}).bind(this)
|
||||
)
|
||||
this.on(
|
||||
"end-of-study", (function () {
|
||||
if (this.flags.expired) { // safe to call multiple times
|
||||
this.final();
|
||||
return;
|
||||
} else {
|
||||
// first time seen.
|
||||
this.flags.expired = true;
|
||||
try {this.whenComplete()} catch (err) { /*ok*/ } finally { /*ok*/ }
|
||||
this.report(merge({}, telemetrySubset(this.config) ,{study_state: "end-of-study"}), "shield");
|
||||
// survey for end of study
|
||||
let that = this;
|
||||
generateTelemetryIdIfNeeded().then(()=>that.showSurvey("end-of-study"));
|
||||
try {this.cleanup()} catch (err) {/*ok*/} finally { /*ok*/ }
|
||||
this.final();
|
||||
die();
|
||||
}
|
||||
}).bind(this)
|
||||
)
|
||||
this.on(
|
||||
"user-uninstall-disable", (function () {
|
||||
if (this.flags.dying) {
|
||||
this.final();
|
||||
return;
|
||||
}
|
||||
this.flags.dying = true;
|
||||
this.report(merge({}, telemetrySubset(this.config), {study_state: "user-ended-study"}), "shield");
|
||||
let that = this;
|
||||
generateTelemetryIdIfNeeded().then(()=>that.showSurvey("user-ended-study"));
|
||||
try {this.cleanup()} catch (err) {/*ok*/} finally { /*ok*/ }
|
||||
this.final();
|
||||
die();
|
||||
}).bind(this)
|
||||
)
|
||||
}
|
||||
|
||||
get state () {
|
||||
let n = this.states.length;
|
||||
return n ? this.states[n-1] : undefined
|
||||
}
|
||||
|
||||
get variation () {
|
||||
return this.config.variation;
|
||||
}
|
||||
|
||||
get firstrun () {
|
||||
return this.config.firstrun;
|
||||
}
|
||||
|
||||
dieIfExpired () {
|
||||
let xconfig = this.config;
|
||||
if (expired(xconfig)) {
|
||||
emit(this, "change", "end-of-study");
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
alivenessPulse (last=lastDailyPing) {
|
||||
// check for new day, phone home if true.
|
||||
let t = Date.now();
|
||||
if ((t - last) >= DAY) {
|
||||
lastDailyPing = t;
|
||||
// phone home
|
||||
emit(this,"change","running");
|
||||
}
|
||||
// check expiration, and die with report if needed
|
||||
return this.dieIfExpired();
|
||||
}
|
||||
|
||||
changeState (newstate) {
|
||||
emit(this,'change', newstate);
|
||||
}
|
||||
|
||||
final () {
|
||||
emit(this,'final', {});
|
||||
}
|
||||
|
||||
startup (reason) {
|
||||
// https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Listening_for_load_and_unload
|
||||
|
||||
// check expiry first, before anything, quit and die if so
|
||||
|
||||
// check once, right away, short circuit both install and startup
|
||||
// to prevent modifications from happening.
|
||||
if (this.dieIfExpired()) return this
|
||||
|
||||
switch (reason) {
|
||||
case "install":
|
||||
emit(this, "change", "maybe-installing");
|
||||
break;
|
||||
|
||||
case "enable":
|
||||
case "startup":
|
||||
case "upgrade":
|
||||
case "downgrade":
|
||||
emit(this, "change", "starting");
|
||||
}
|
||||
|
||||
if (! this._pulseTimer) this._pulseTimer = setInterval(this.alivenessPulse.bind(this), 5*60*1000 /*5 minutes */)
|
||||
return this;
|
||||
}
|
||||
|
||||
shutdown (reason) {
|
||||
// https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Listening_for_load_and_unload
|
||||
if (this.flags.ineligibleDie ||
|
||||
this.flags.expired ||
|
||||
this.flags.dying
|
||||
) { return this } // special cases.
|
||||
|
||||
switch (reason) {
|
||||
case "uninstall":
|
||||
case "disable":
|
||||
emit(this, "change", "user-uninstall-disable");
|
||||
break;
|
||||
|
||||
// 5. usual end of session.
|
||||
case "shutdown":
|
||||
case "upgrade":
|
||||
case "downgrade":
|
||||
emit(this, "change", "normal-shutdown")
|
||||
break;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
cleanup () {
|
||||
// do the simple prefs and simplestorage cleanup
|
||||
// extend by extension
|
||||
resetShieldPrefs();
|
||||
cleanup();
|
||||
}
|
||||
|
||||
isEligible () {
|
||||
return true;
|
||||
}
|
||||
|
||||
whenIneligible () {
|
||||
// empty function unless overrided
|
||||
}
|
||||
|
||||
whenInstalled () {
|
||||
// empty unless overrided
|
||||
}
|
||||
|
||||
whenComplete () {
|
||||
// when the study expires
|
||||
}
|
||||
|
||||
/**
|
||||
* equal choice from varations, by default. override to get unequal
|
||||
*/
|
||||
decideVariation (rng=Math.random()) {
|
||||
return chooseVariation(Object.keys(this.config.variations), rng);
|
||||
}
|
||||
|
||||
get surveyQueryArgs () {
|
||||
return {
|
||||
variation: this.variation,
|
||||
xname: this.config.name,
|
||||
who: userId(),
|
||||
updateChannel: Services.appinfo.defaultUpdateChannel,
|
||||
fxVersion: Services.appinfo.version,
|
||||
}
|
||||
}
|
||||
|
||||
showSurvey(reason) {
|
||||
let partial = this.config.surveyUrls[reason];
|
||||
|
||||
let queryArgs = this.surveyQueryArgs;
|
||||
queryArgs.reason = reason;
|
||||
if (partial) {
|
||||
let url = survey(partial, queryArgs);
|
||||
tabs.open(url);
|
||||
return url
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
report () { // convenience only
|
||||
return report.apply(null, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
chooseVariation: chooseVariation,
|
||||
die: die,
|
||||
expired: expired,
|
||||
generateTelemetryIdIfNeeded: generateTelemetryIdIfNeeded,
|
||||
report: report,
|
||||
Reporter: Reporter,
|
||||
resetShieldPrefs: resetShieldPrefs,
|
||||
Study: Study,
|
||||
cleanup: cleanup,
|
||||
survey: survey
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
const { AddonManager } = require('resource://gre/modules/AddonManager.jsm');
|
||||
const { ClientID } = require('resource://gre/modules/ClientID.jsm');
|
||||
const Events = require('sdk/system/events');
|
||||
const { Services } = require('resource://gre/modules/Services.jsm');
|
||||
const { storage } = require('sdk/simple-storage');
|
||||
const {
|
||||
TelemetryController
|
||||
} = require('resource://gre/modules/TelemetryController.jsm');
|
||||
const { Request } = require('sdk/request');
|
||||
|
||||
|
||||
const EVENT_SEND_METRIC = 'testpilot::send-metric';
|
||||
const startTime = (Services.startup.getStartupInfo().process);
|
||||
|
||||
function makeTimestamp(timestamp) {
|
||||
return Math.round((timestamp - startTime) / 1000);
|
||||
}
|
||||
|
||||
function experimentPing(event) {
|
||||
const timestamp = new Date();
|
||||
const { subject, data } = event;
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(data);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
return console.error(`Dropping bad metrics packet: ${err}`);
|
||||
}
|
||||
|
||||
AddonManager.getAddonByID(subject, addon => {
|
||||
const payload = {
|
||||
test: subject,
|
||||
version: addon.version,
|
||||
timestamp: makeTimestamp(timestamp),
|
||||
variants: storage.experimentVariants &&
|
||||
subject in storage.experimentVariants
|
||||
? storage.experimentVariants[subject]
|
||||
: null,
|
||||
payload: parsed
|
||||
};
|
||||
TelemetryController.submitExternalPing('testpilottest', payload, {
|
||||
addClientId: true,
|
||||
addEnvironment: true
|
||||
});
|
||||
|
||||
// TODO: DRY up this ping centre code here and in lib/Telemetry.
|
||||
const pcPing = TelemetryController.getCurrentPingData();
|
||||
pcPing.type = 'testpilot';
|
||||
pcPing.payload = payload;
|
||||
const pcPayload = {
|
||||
// 'method' is used by testpilot-metrics library.
|
||||
// 'event' was used before that library existed.
|
||||
event_type: parsed.event || parsed.method,
|
||||
client_time: makeTimestamp(parsed.timestamp || timestamp),
|
||||
addon_id: subject,
|
||||
addon_version: addon.version,
|
||||
firefox_version: pcPing.environment.build.version,
|
||||
os_name: pcPing.environment.system.os.name,
|
||||
os_version: pcPing.environment.system.os.version,
|
||||
locale: pcPing.environment.settings.locale,
|
||||
// Note: these two keys are normally inserted by the ping-centre client.
|
||||
client_id: ClientID.getCachedClientID(),
|
||||
topic: 'testpilot'
|
||||
};
|
||||
// Add any other extra top-level keys = require(the payload, possibly including
|
||||
// 'object' or 'category', among others.
|
||||
Object.keys(parsed).forEach(f => {
|
||||
// Ignore the keys we've already added to `pcPayload`.
|
||||
const ignored = ['event', 'method', 'timestamp'];
|
||||
if (!ignored.includes(f)) {
|
||||
pcPayload[f] = parsed[f];
|
||||
}
|
||||
});
|
||||
|
||||
const req = new Request({
|
||||
url: 'https://tiles.services.mozilla.com/v3/links/ping-centre',
|
||||
contentType: 'application/json',
|
||||
content: JSON.stringify(pcPayload)
|
||||
});
|
||||
req.post();
|
||||
});
|
||||
}
|
||||
|
||||
function Experiment() {
|
||||
// If the user has @testpilot-addon, it already bound
|
||||
// experimentPing to testpilot::send-metric,
|
||||
// so we don't need to bind this one
|
||||
AddonManager.getAddonByID('@testpilot-addon', addon => {
|
||||
if (!addon) {
|
||||
Events.on(EVENT_SEND_METRIC, experimentPing);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = Experiment;
|
||||
+6
-2
@@ -2,7 +2,7 @@
|
||||
"name": "testpilot-containers",
|
||||
"title": "Containers Experiment",
|
||||
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
|
||||
"version": "2.2.0",
|
||||
"version": "2.4.1",
|
||||
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
|
||||
"bugs": {
|
||||
"url": "https://github.com/mozilla/testpilot-containers/issues"
|
||||
@@ -12,11 +12,13 @@
|
||||
"addons-linter": "^0.15.14",
|
||||
"deploy-txp": "^1.0.7",
|
||||
"eslint": "^3.17.1",
|
||||
"eslint-plugin-no-unescaped": "^1.1.0",
|
||||
"eslint-plugin-no-unsanitized": "^2.0.0",
|
||||
"eslint-plugin-promise": "^3.4.0",
|
||||
"htmllint-cli": "^0.0.5",
|
||||
"jpm": "^1.2.2",
|
||||
"json": "^9.0.6",
|
||||
"npm-run-all": "^4.0.0",
|
||||
"shield-studies-addon-utils": "^2.0.0",
|
||||
"stylelint": "^7.9.0",
|
||||
"stylelint-config-standard": "^16.0.0",
|
||||
"stylelint-order": "^0.3.0",
|
||||
@@ -41,6 +43,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm test && jpm xpi",
|
||||
"build-shield": "npm test && npm run package-shield",
|
||||
"deploy": "deploy-txp",
|
||||
"lint": "npm-run-all lint:*",
|
||||
"lint:addon": "addons-linter webextension --self-hosted",
|
||||
@@ -48,6 +51,7 @@
|
||||
"lint:html": "htmllint webextension/*.html",
|
||||
"lint:js": "eslint .",
|
||||
"package": "npm run build && mv testpilot-containers.xpi addon.xpi",
|
||||
"package-shield": "./node_modules/.bin/json -I -f package.json -e 'this.name=\"shield-study-containers\"' && jpm xpi && ./node_modules/.bin/json -I -f package.json -e 'this.name=\"testpilot-containers\"'",
|
||||
"test": "npm run lint"
|
||||
},
|
||||
"updateURL": "https://testpilot.firefox.com/files/@testpilot-containers/updates.json"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const self = require("sdk/self");
|
||||
const { when: unload } = require("sdk/system/unload");
|
||||
|
||||
const shield = require("./lib/shield/index");
|
||||
|
||||
const surveyUrl = "https://www.surveygizmo.com/s3/3621810/shield-txp-containers";
|
||||
|
||||
const studyConfig = {
|
||||
name: self.addonId,
|
||||
days: 28,
|
||||
surveyUrls: {
|
||||
"end-of-study": surveyUrl,
|
||||
"user-ended-study": surveyUrl,
|
||||
ineligible: null,
|
||||
},
|
||||
variations: {
|
||||
"control": () => {},
|
||||
"securityOnboarding": () => {}
|
||||
}
|
||||
};
|
||||
|
||||
class ContainersStudy extends shield.Study {
|
||||
isEligible () {
|
||||
// If the user already has testpilot-containers extension, they are in the
|
||||
// Test Pilot experiment, so exclude them.
|
||||
return super.isEligible();
|
||||
}
|
||||
}
|
||||
|
||||
const thisStudy = new ContainersStudy(studyConfig);
|
||||
|
||||
if (self.id === "@shield-study-containers") {
|
||||
unload((reason) => thisStudy.shutdown(reason));
|
||||
}
|
||||
|
||||
exports.study = thisStudy;
|
||||
@@ -1,6 +1,9 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
const Experiment = require('./lib/testpilot/experiment');
|
||||
|
||||
const experiment = new Experiment();
|
||||
|
||||
/**
|
||||
* Class that represents a metrics event broker. Events are sent to Google
|
||||
|
||||
+379
-106
@@ -1,14 +1,12 @@
|
||||
const MAJOR_VERSIONS = ["2.3.0", "2.4.0"];
|
||||
const LOOKUP_KEY = "$ref";
|
||||
|
||||
const assignManager = {
|
||||
CLOSEABLE_WINDOWS: new Set([
|
||||
"about:startpage",
|
||||
"about:newtab",
|
||||
"about:home",
|
||||
"about:blank"
|
||||
]),
|
||||
MENU_ASSIGN_ID: "open-in-this-container",
|
||||
MENU_REMOVE_ID: "remove-open-in-this-container",
|
||||
storageArea: {
|
||||
area: browser.storage.local,
|
||||
exemptedTabs: {},
|
||||
|
||||
getSiteStoreKey(pageUrl) {
|
||||
const url = new window.URL(pageUrl);
|
||||
@@ -16,6 +14,27 @@ const assignManager = {
|
||||
return `${storagePrefix}${url.hostname}`;
|
||||
},
|
||||
|
||||
setExempted(pageUrl, tabId) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
if (!(siteStoreKey in this.exemptedTabs)) {
|
||||
this.exemptedTabs[siteStoreKey] = [];
|
||||
}
|
||||
this.exemptedTabs[siteStoreKey].push(tabId);
|
||||
},
|
||||
|
||||
removeExempted(pageUrl) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
this.exemptedTabs[siteStoreKey] = [];
|
||||
},
|
||||
|
||||
isExempted(pageUrl, tabId) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
if (!(siteStoreKey in this.exemptedTabs)) {
|
||||
return false;
|
||||
}
|
||||
return this.exemptedTabs[siteStoreKey].includes(tabId);
|
||||
},
|
||||
|
||||
get(pageUrl) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -30,8 +49,13 @@ const assignManager = {
|
||||
});
|
||||
},
|
||||
|
||||
set(pageUrl, data) {
|
||||
set(pageUrl, data, exemptedTabIds) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
if (exemptedTabIds) {
|
||||
exemptedTabIds.forEach((tabId) => {
|
||||
this.setExempted(pageUrl, tabId);
|
||||
});
|
||||
}
|
||||
return this.area.set({
|
||||
[siteStoreKey]: data
|
||||
});
|
||||
@@ -39,74 +63,58 @@ const assignManager = {
|
||||
|
||||
remove(pageUrl) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
// When we remove an assignment we should clear all the exemptions
|
||||
this.removeExempted(pageUrl);
|
||||
return this.area.remove([siteStoreKey]);
|
||||
},
|
||||
|
||||
deleteContainer(userContextId) {
|
||||
const removeKeys = [];
|
||||
this.area.get().then((siteConfigs) => {
|
||||
Object.keys(siteConfigs).forEach((key) => {
|
||||
// For some reason this is stored as string... lets check them both as that
|
||||
if (String(siteConfigs[key].userContextId) === String(userContextId)) {
|
||||
removeKeys.push(key);
|
||||
}
|
||||
});
|
||||
this.area.remove(removeKeys);
|
||||
async deleteContainer(userContextId) {
|
||||
const sitesByContainer = await this.getByContainer(userContextId);
|
||||
this.area.remove(Object.keys(sitesByContainer));
|
||||
},
|
||||
|
||||
async getByContainer(userContextId) {
|
||||
const sites = {};
|
||||
const siteConfigs = await this.area.get();
|
||||
Object.keys(siteConfigs).forEach((key) => {
|
||||
// For some reason this is stored as string... lets check them both as that
|
||||
if (String(siteConfigs[key].userContextId) === String(userContextId)) {
|
||||
const site = siteConfigs[key];
|
||||
// In hindsight we should have stored this
|
||||
// TODO file a follow up to clean the storage onLoad
|
||||
site.hostname = key.replace(/^siteContainerMap@@_/, "");
|
||||
sites[key] = site;
|
||||
}
|
||||
});
|
||||
return sites;
|
||||
}
|
||||
},
|
||||
|
||||
_neverAsk(m) {
|
||||
const pageUrl = m.pageUrl;
|
||||
if (m.neverAsk === true) {
|
||||
// If we have existing data and for some reason it hasn't been deleted etc lets update it
|
||||
this.storageArea.get(pageUrl).then((siteSettings) => {
|
||||
if (siteSettings) {
|
||||
siteSettings.neverAsk = true;
|
||||
this.storageArea.set(pageUrl, siteSettings);
|
||||
}
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
init() {
|
||||
browser.runtime.onMessage.addListener((neverAskMessage) => {
|
||||
const pageUrl = neverAskMessage.pageUrl;
|
||||
if (neverAskMessage.neverAsk === true) {
|
||||
// If we have existing data and for some reason it hasn't been deleted etc lets update it
|
||||
this.storageArea.get(pageUrl).then((siteSettings) => {
|
||||
if (siteSettings) {
|
||||
siteSettings.neverAsk = true;
|
||||
this.storageArea.set(pageUrl, siteSettings);
|
||||
}
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
});
|
||||
// We return here so the confirm page can load the tab when exempted
|
||||
async _exemptTab(m) {
|
||||
const pageUrl = m.pageUrl;
|
||||
this.storageArea.setExempted(pageUrl, m.tabId);
|
||||
return true;
|
||||
},
|
||||
|
||||
init() {
|
||||
browser.contextMenus.onClicked.addListener((info, tab) => {
|
||||
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
||||
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
|
||||
if (userContextId) {
|
||||
let actionName;
|
||||
let storageAction;
|
||||
if (info.menuItemId === this.MENU_ASSIGN_ID) {
|
||||
actionName = "added";
|
||||
storageAction = this.storageArea.set(info.pageUrl, {
|
||||
userContextId,
|
||||
neverAsk: false
|
||||
});
|
||||
} else {
|
||||
actionName = "removed";
|
||||
storageAction = this.storageArea.remove(info.pageUrl);
|
||||
}
|
||||
storageAction.then(() => {
|
||||
browser.notifications.create({
|
||||
type: "basic",
|
||||
title: "Containers",
|
||||
message: `Successfully ${actionName} site to always open in this container`,
|
||||
iconUrl: browser.extension.getURL("/img/onboarding-1.png")
|
||||
});
|
||||
browser.runtime.sendMessage({
|
||||
method: "sendTelemetryPayload",
|
||||
event: `${actionName}-container-assignment`,
|
||||
userContextId: userContextId,
|
||||
});
|
||||
this.calculateContextMenu(tab);
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
this._onClickedHandler(info, tab);
|
||||
});
|
||||
|
||||
// Before a request is handled by the browser we decide if we should route through a different container
|
||||
@@ -114,6 +122,7 @@ const assignManager = {
|
||||
if (options.frameId !== 0 || options.tabId === -1) {
|
||||
return {};
|
||||
}
|
||||
this.removeContextMenu();
|
||||
return Promise.all([
|
||||
browser.tabs.get(options.tabId),
|
||||
this.storageArea.get(options.url)
|
||||
@@ -121,25 +130,26 @@ const assignManager = {
|
||||
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
||||
if (!siteSettings
|
||||
|| userContextId === siteSettings.userContextId
|
||||
|| tab.incognito) {
|
||||
|| tab.incognito
|
||||
|| this.storageArea.isExempted(options.url, tab.id)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
this.reloadPageInContainer(options.url, siteSettings.userContextId, tab.index + 1, siteSettings.neverAsk);
|
||||
this.reloadPageInContainer(options.url, userContextId, siteSettings.userContextId, tab.index + 1, siteSettings.neverAsk);
|
||||
this.calculateContextMenu(tab);
|
||||
|
||||
/* Removal of existing tabs:
|
||||
We aim to open the new assigned container tab / warning prompt in it's own tab:
|
||||
- As the history won't span from one container to another it seems most sane to not try and reopen a tab on history.back()
|
||||
- When users open a new tab themselves we want to make sure we don't end up with three tabs as per: https://github.com/mozilla/testpilot-containers/issues/421
|
||||
If we are coming from an internal url that are used for the new tab page (CLOSEABLE_WINDOWS), we can safely close as user is unlikely losing history
|
||||
If we are coming from an internal url that are used for the new tab page (NEW_TAB_PAGES), we can safely close as user is unlikely losing history
|
||||
Detecting redirects on "new tab" opening actions is pretty hard as we don't get tab history:
|
||||
- Redirects happen from Short URLs and tracking links that act as a gateway
|
||||
- Extensions don't provide a way to history crawl for tabs, we could inject content scripts to do this
|
||||
however they don't run on about:blank so this would likely be just as hacky.
|
||||
We capture the time the tab was created and close if it was within the timeout to try to capture pages which haven't had user interaction or history.
|
||||
*/
|
||||
if (this.CLOSEABLE_WINDOWS.has(tab.url)
|
||||
if (backgroundLogic.NEW_TAB_PAGES.has(tab.url)
|
||||
|| (messageHandler.lastCreatedTab
|
||||
&& messageHandler.lastCreatedTab.id === tab.id)) {
|
||||
browser.tabs.remove(tab.id);
|
||||
@@ -153,6 +163,21 @@ const assignManager = {
|
||||
},{urls: ["<all_urls>"], types: ["main_frame"]}, ["blocking"]);
|
||||
},
|
||||
|
||||
async _onClickedHandler(info, tab) {
|
||||
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
||||
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
|
||||
if (userContextId) {
|
||||
// let actionName;
|
||||
let remove;
|
||||
if (info.menuItemId === this.MENU_ASSIGN_ID) {
|
||||
remove = false;
|
||||
} else {
|
||||
remove = true;
|
||||
}
|
||||
await this._setOrRemoveAssignment(tab.id, info.pageUrl, userContextId, remove);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
deleteContainer(userContextId) {
|
||||
this.storageArea.deleteContainer(userContextId);
|
||||
@@ -175,62 +200,132 @@ const assignManager = {
|
||||
// Ensure we are not in incognito mode
|
||||
const url = new URL(tab.url);
|
||||
if (url.protocol === "about:"
|
||||
|| url.protocol === "moz-extension:"
|
||||
|| tab.incognito) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
calculateContextMenu(tab) {
|
||||
async _setOrRemoveAssignment(tabId, pageUrl, userContextId, remove) {
|
||||
let actionName;
|
||||
|
||||
// https://github.com/mozilla/testpilot-containers/issues/626
|
||||
// Context menu has stored context IDs as strings, so we need to coerce
|
||||
// the value to a string for accurate checking
|
||||
userContextId = String(userContextId);
|
||||
|
||||
if (!remove) {
|
||||
const tabs = await browser.tabs.query({});
|
||||
const assignmentStoreKey = this.storageArea.getSiteStoreKey(pageUrl);
|
||||
const exemptedTabIds = tabs.filter((tab) => {
|
||||
const tabStoreKey = this.storageArea.getSiteStoreKey(tab.url);
|
||||
/* Auto exempt all tabs that exist for this hostname that are not in the same container */
|
||||
if (tabStoreKey === assignmentStoreKey &&
|
||||
this.getUserContextIdFromCookieStore(tab) !== userContextId) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}).map((tab) => {
|
||||
return tab.id;
|
||||
});
|
||||
|
||||
await this.storageArea.set(pageUrl, {
|
||||
userContextId,
|
||||
neverAsk: false
|
||||
}, exemptedTabIds);
|
||||
actionName = "added";
|
||||
} else {
|
||||
await this.storageArea.remove(pageUrl);
|
||||
actionName = "removed";
|
||||
}
|
||||
browser.tabs.sendMessage(tabId, {
|
||||
text: `Successfully ${actionName} site to always open in this container`
|
||||
});
|
||||
backgroundLogic.sendTelemetryPayload({
|
||||
event: `${actionName}-container-assignment`,
|
||||
userContextId: userContextId,
|
||||
});
|
||||
const tab = await browser.tabs.get(tabId);
|
||||
this.calculateContextMenu(tab);
|
||||
},
|
||||
|
||||
async _getAssignment(tab) {
|
||||
const cookieStore = this.getUserContextIdFromCookieStore(tab);
|
||||
// Ensure we have a cookieStore to assign to
|
||||
if (cookieStore
|
||||
&& this.isTabPermittedAssign(tab)) {
|
||||
return await this.storageArea.get(tab.url);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
_getByContainer(userContextId) {
|
||||
return this.storageArea.getByContainer(userContextId);
|
||||
},
|
||||
|
||||
removeContextMenu() {
|
||||
// There is a focus issue in this menu where if you change window with a context menu click
|
||||
// you get the wrong menu display because of async
|
||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1215376#c16
|
||||
// We also can't change for always private mode
|
||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1352102
|
||||
const cookieStore = this.getUserContextIdFromCookieStore(tab);
|
||||
browser.contextMenus.remove(this.MENU_ASSIGN_ID);
|
||||
browser.contextMenus.remove(this.MENU_REMOVE_ID);
|
||||
// Ensure we have a cookieStore to assign to
|
||||
if (cookieStore
|
||||
&& this.isTabPermittedAssign(tab)) {
|
||||
this.storageArea.get(tab.url).then((siteSettings) => {
|
||||
// ✓ This is to mitigate https://bugzilla.mozilla.org/show_bug.cgi?id=1351418
|
||||
let prefix = " "; // Alignment of non breaking space, unknown why this requires so many spaces to align with the tick
|
||||
let menuId = this.MENU_ASSIGN_ID;
|
||||
if (siteSettings) {
|
||||
prefix = "✓";
|
||||
menuId = this.MENU_REMOVE_ID;
|
||||
}
|
||||
browser.contextMenus.create({
|
||||
id: menuId,
|
||||
title: `${prefix} Always Open in This Container`,
|
||||
checked: true,
|
||||
contexts: ["all"],
|
||||
});
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
reloadPageInContainer(url, userContextId, index, neverAsk = false) {
|
||||
async calculateContextMenu(tab) {
|
||||
this.removeContextMenu();
|
||||
const siteSettings = await this._getAssignment(tab);
|
||||
// Return early and not add an item if we have false
|
||||
// False represents assignment is not permitted
|
||||
if (siteSettings === false) {
|
||||
return false;
|
||||
}
|
||||
// ✓ This is to mitigate https://bugzilla.mozilla.org/show_bug.cgi?id=1351418
|
||||
let prefix = " "; // Alignment of non breaking space, unknown why this requires so many spaces to align with the tick
|
||||
let menuId = this.MENU_ASSIGN_ID;
|
||||
const tabUserContextId = this.getUserContextIdFromCookieStore(tab);
|
||||
if (siteSettings &&
|
||||
Number(siteSettings.userContextId) === Number(tabUserContextId)) {
|
||||
prefix = "✓";
|
||||
menuId = this.MENU_REMOVE_ID;
|
||||
}
|
||||
browser.contextMenus.create({
|
||||
id: menuId,
|
||||
title: `${prefix} Always Open in This Container`,
|
||||
checked: true,
|
||||
contexts: ["all"],
|
||||
});
|
||||
},
|
||||
|
||||
reloadPageInContainer(url, currentUserContextId, userContextId, index, neverAsk = false) {
|
||||
const cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
|
||||
const loadPage = browser.extension.getURL("confirm-page.html");
|
||||
// False represents assignment is not permitted
|
||||
// If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there
|
||||
if (neverAsk) {
|
||||
browser.tabs.create({url, cookieStoreId: `firefox-container-${userContextId}`, index});
|
||||
browser.runtime.sendMessage({
|
||||
method: "sendTelemetryPayload",
|
||||
browser.tabs.create({url, cookieStoreId, index});
|
||||
backgroundLogic.sendTelemetryPayload({
|
||||
event: "auto-reload-page-in-container",
|
||||
userContextId: userContextId,
|
||||
});
|
||||
} else {
|
||||
browser.runtime.sendMessage({
|
||||
method: "sendTelemetryPayload",
|
||||
backgroundLogic.sendTelemetryPayload({
|
||||
event: "prompt-to-reload-page-in-container",
|
||||
userContextId: userContextId,
|
||||
});
|
||||
const confirmUrl = `${loadPage}?url=${url}`;
|
||||
browser.tabs.create({url: confirmUrl, cookieStoreId: `firefox-container-${userContextId}`, index}).then(() => {
|
||||
let confirmUrl = `${loadPage}?url=${encodeURIComponent(url)}&cookieStoreId=${cookieStoreId}`;
|
||||
let currentCookieStoreId;
|
||||
if (currentUserContextId) {
|
||||
currentCookieStoreId = backgroundLogic.cookieStoreId(currentUserContextId);
|
||||
confirmUrl += `¤tCookieStoreId=${currentCookieStoreId}`;
|
||||
}
|
||||
browser.tabs.create({
|
||||
url: confirmUrl,
|
||||
cookieStoreId: currentCookieStoreId,
|
||||
index
|
||||
}).then(() => {
|
||||
// We don't want to sync this URL ever nor clutter the users history
|
||||
browser.history.deleteUrl({url: confirmUrl});
|
||||
}).catch((e) => {
|
||||
@@ -240,6 +335,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 !== "new") {
|
||||
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 = {
|
||||
// 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
|
||||
@@ -247,15 +455,55 @@ const messageHandler = {
|
||||
LAST_CREATED_TAB_TIMER: 2000,
|
||||
|
||||
init() {
|
||||
// Handles messages from index.js
|
||||
// Handles messages from webextension code
|
||||
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;
|
||||
case "getAssignment":
|
||||
response = browser.tabs.get(m.tabId).then((tab) => {
|
||||
return assignManager._getAssignment(tab);
|
||||
});
|
||||
break;
|
||||
case "getAssignmentObjectByContainer":
|
||||
response = assignManager._getByContainer(m.message.userContextId);
|
||||
break;
|
||||
case "setOrRemoveAssignment":
|
||||
// m.tabId is used for where to place the in content message
|
||||
// m.url is the assignment to be removed/added
|
||||
response = browser.tabs.get(m.tabId).then((tab) => {
|
||||
return assignManager._setOrRemoveAssignment(tab.id, m.url, m.userContextId, m.value);
|
||||
});
|
||||
break;
|
||||
case "exemptContainerAssignment":
|
||||
response = assignManager._exemptTab(m);
|
||||
break;
|
||||
}
|
||||
return response;
|
||||
});
|
||||
|
||||
// Handles messages from sdk code
|
||||
const port = browser.runtime.connect();
|
||||
port.onMessage.addListener(m => {
|
||||
switch (m.type) {
|
||||
case "lightweight-theme-changed":
|
||||
themeManager.update(m.message);
|
||||
break;
|
||||
case "delete-container":
|
||||
assignManager.deleteContainer(m.message.userContextId);
|
||||
case "open-tab":
|
||||
backgroundLogic.openTab(m.message);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled message type: ${m.message}`);
|
||||
@@ -279,6 +527,7 @@ const messageHandler = {
|
||||
});
|
||||
|
||||
browser.tabs.onActivated.addListener((info) => {
|
||||
assignManager.removeContextMenu();
|
||||
browser.tabs.get(info.tabId).then((tab) => {
|
||||
tabPageCounter.initTabCounter(tab);
|
||||
assignManager.calculateContextMenu(tab);
|
||||
@@ -288,6 +537,12 @@ const messageHandler = {
|
||||
});
|
||||
|
||||
browser.windows.onFocusChanged.addListener((windowId) => {
|
||||
assignManager.removeContextMenu();
|
||||
// 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
|
||||
displayBrowserActionBadge();
|
||||
browser.tabs.query({active: true, windowId}).then((tabs) => {
|
||||
if (tabs && tabs[0]) {
|
||||
tabPageCounter.initTabCounter(tabs[0]);
|
||||
@@ -316,6 +571,7 @@ const messageHandler = {
|
||||
if (details.frameId !== 0 || details.tabId === -1) {
|
||||
return {};
|
||||
}
|
||||
assignManager.removeContextMenu();
|
||||
|
||||
browser.tabs.get(details.tabId).then((tab) => {
|
||||
tabPageCounter.incrementTabCount(tab);
|
||||
@@ -408,8 +664,7 @@ const tabPageCounter = {
|
||||
return;
|
||||
}
|
||||
if (why === "user-closed-tab" && this.counters[tabId].tab) {
|
||||
browser.runtime.sendMessage({
|
||||
method: "sendTelemetryPayload",
|
||||
backgroundLogic.sendTelemetryPayload({
|
||||
event: "page-requests-completed-per-tab",
|
||||
userContextId: this.counters[tabId].tab.cookieStoreId,
|
||||
pageRequestCount: this.counters[tabId].tab.pageRequests
|
||||
@@ -418,8 +673,7 @@ const tabPageCounter = {
|
||||
// delete both the 'tab' and 'activity' counters
|
||||
delete this.counters[tabId];
|
||||
} else if (why === "user-went-idle" && this.counters[tabId].activity) {
|
||||
browser.runtime.sendMessage({
|
||||
method: "sendTelemetryPayload",
|
||||
backgroundLogic.sendTelemetryPayload({
|
||||
event: "page-requests-completed-per-activity",
|
||||
userContextId: this.counters[tabId].activity.cookieStoreId,
|
||||
pageRequestCount: this.counters[tabId].activity.pageRequests
|
||||
@@ -471,3 +725,22 @@ function disableAddon(tabId) {
|
||||
browser.browserAction.disable(tabId);
|
||||
browser.browserAction.setTitle({ tabId, title: "Containers disabled in Private Browsing Mode" });
|
||||
}
|
||||
|
||||
async function getExtensionInfo() {
|
||||
const manifestPath = browser.extension.getURL("manifest.json");
|
||||
const response = await fetch(manifestPath);
|
||||
const extensionInfo = await response.json();
|
||||
return extensionInfo;
|
||||
}
|
||||
|
||||
async function displayBrowserActionBadge() {
|
||||
const extensionInfo = await getExtensionInfo();
|
||||
const storage = await browser.storage.local.get({browserActionBadgesClicked: []});
|
||||
|
||||
if (MAJOR_VERSIONS.indexOf(extensionInfo.version) > -1 &&
|
||||
storage.browserActionBadgesClicked.indexOf(extensionInfo.version) < 0) {
|
||||
browser.browserAction.setBadgeBackgroundColor({color: "rgba(0,217,0,255)"});
|
||||
browser.browserAction.setBadgeText({text: "NEW"});
|
||||
}
|
||||
}
|
||||
displayBrowserActionBadge();
|
||||
|
||||
@@ -8,26 +8,29 @@
|
||||
<body>
|
||||
<main>
|
||||
<div class="title">
|
||||
<h1 class="title-text">Should we open this in your container?</h1>
|
||||
<h1 class="title-text">Open this site in your assigned container?</h1>
|
||||
</div>
|
||||
<form id="redirect-form">
|
||||
<p>
|
||||
Looks like you requested:
|
||||
You asked <dfn id="browser-name" title="Thanks for trying out Containers. Sorry we may have got your browser name wrong. #FxNightly" >Firefox</dfn> to always open <dfn class="container-name"></dfn> for this site:<br />
|
||||
</p>
|
||||
<div id="redirect-url"></div>
|
||||
<p>
|
||||
You asked <dfn id="browser-name" title="Thanks for trying out Containers. Sorry we may have got your browser name wrong. #FxNightly" >Firefox</dfn> to always open <dfn id="redirect-site"></dfn> in <dfn>this</dfn> type of container. Would you like to proceed?<br />
|
||||
</p>
|
||||
<p>Would you still like to open in this current container?</p>
|
||||
<br />
|
||||
<br />
|
||||
<input id="never-ask" type="checkbox" /><label for="never-ask">Remember my decision for this site</label>
|
||||
<label for="never-ask" class="check-label">
|
||||
<input id="never-ask" type="checkbox" />
|
||||
Remember my decision for this site
|
||||
</label>
|
||||
<br />
|
||||
<div class="button-container">
|
||||
<button id="confirm" class="button primary" autofocus>Take me there</button>
|
||||
<button id="deny" class="button">Open in <dfn id="current-container-name">Current</dfn> Container</button>
|
||||
<button id="confirm" class="button primary" autofocus>Open in <dfn class="container-name"></dfn> Container</button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<script src="js/utils.js"></script>
|
||||
<script src="js/confirm-page.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,11 +4,21 @@
|
||||
}
|
||||
|
||||
main {
|
||||
background: url(/img/onboarding-1.png) no-repeat;
|
||||
background: url(/img/onboarding-4.png) no-repeat;
|
||||
background-position: -10px -15px;
|
||||
background-size: 285px;
|
||||
margin-inline-start: -285px;
|
||||
padding-inline-start: 285px;
|
||||
background-size: 300px;
|
||||
margin-inline-start: -350px;
|
||||
padding-inline-start: 350px;
|
||||
}
|
||||
|
||||
.container-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
button .container-name,
|
||||
#current-container-name {
|
||||
font-weight: bold;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1300px) {
|
||||
@@ -36,6 +46,33 @@ html {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
#redirect-url {
|
||||
background: #efefef;
|
||||
border-radius: 2px;
|
||||
line-height: 1.5;
|
||||
padding-block-end: 0.5rem;
|
||||
padding-block-start: 0.5rem;
|
||||
padding-inline-end: 0.5rem;
|
||||
padding-inline-start: 0.5rem;
|
||||
}
|
||||
|
||||
#redirect-url img {
|
||||
block-size: 16px;
|
||||
inline-size: 16px;
|
||||
margin-inline-end: 6px;
|
||||
offset-block-start: 3px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
dfn {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.button-container > button {
|
||||
min-inline-size: 240px;
|
||||
}
|
||||
|
||||
.check-label {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
.container-notification {
|
||||
align-items: center;
|
||||
background: #efefef;
|
||||
color: #003f07;
|
||||
display: flex;
|
||||
font: 12px sans-serif;
|
||||
inline-size: 100vw;
|
||||
justify-content: start;
|
||||
offset-block-start: 0;
|
||||
offset-inline-start: 0;
|
||||
padding-block-end: 8px;
|
||||
padding-block-start: 8px;
|
||||
padding-inline-end: 8px;
|
||||
padding-inline-start: 8px;
|
||||
position: fixed;
|
||||
text-align: start;
|
||||
transform: translateY(-100%);
|
||||
transition: transform 0.3s cubic-bezier(0.07, 0.95, 0, 1) 0.3s;
|
||||
z-index: 999999999999;
|
||||
}
|
||||
|
||||
.container-notification img {
|
||||
block-size: 16px;
|
||||
display: inline-block;
|
||||
inline-size: 16px;
|
||||
margin-inline-end: 3px;
|
||||
}
|
||||
+364
-68
@@ -1,11 +1,56 @@
|
||||
/* General Rules and Resets */
|
||||
* {
|
||||
font-size: inherit;
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: 0;
|
||||
padding-block-end: 0;
|
||||
padding-block-start: 0;
|
||||
padding-inline-end: 0;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: #fefefe;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Roboto, Noto, "San Francisco", Ubuntu, "Segoe UI", "Fira Sans", message-box, Arial, sans-serif;
|
||||
inline-size: 300px;
|
||||
max-inline-size: 300px;
|
||||
}
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
:root {
|
||||
--primary-action-color: #248aeb;
|
||||
--title-text-color: #000;
|
||||
--text-normal-color: #4a4a4a;
|
||||
--text-heading-color: #000;
|
||||
|
||||
/* calculated from 12px */
|
||||
--font-size-heading: 1.33rem; /* 16px */
|
||||
--block-line-space-size: 0.5rem; /* 6px */
|
||||
--inline-item-space-size: 0.5rem; /* 6px */
|
||||
--block-line-separation-size: 0.33rem; /* 10px */
|
||||
--inline-icon-space-size: 0.833rem; /* 10px */
|
||||
|
||||
/* Use for url and icon size */
|
||||
--block-url-label-size: 2rem; /* 24px */
|
||||
--inline-start-size: 1.66rem; /* 20px */
|
||||
--inline-button-size: 5.833rem; /* 70px */
|
||||
--icon-size: 1.166rem; /* 14px */
|
||||
|
||||
--small-text-size: 0.833rem; /* 10px */
|
||||
--small-radius: 3px;
|
||||
--icon-button-size: calc(calc(var(--block-line-separation-size) * 2) + 1.66rem); /* 20px */
|
||||
}
|
||||
|
||||
@media (min-resolution: 1dppx) {
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
@@ -14,6 +59,13 @@ html {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
form {
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
border: 0;
|
||||
border-spacing: 0;
|
||||
@@ -30,11 +82,27 @@ table {
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
border-block-start: 1px solid #f1f1f1;
|
||||
inline-size: 100%;
|
||||
max-block-size: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.offpage {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Effect borrowed from tabs in Firefox, ensure that the element flexes to the full width */
|
||||
.truncate-text {
|
||||
mask-image: linear-gradient(to left, transparent, black 1em);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Color and icon helpers */
|
||||
[data-identity-color="blue"] {
|
||||
--identity-tab-color: #37adff;
|
||||
@@ -51,6 +119,11 @@ table {
|
||||
--identity-icon-color: #51cd00;
|
||||
}
|
||||
|
||||
[data-identity-color="grey"] {
|
||||
/* Only used for the edit panel */
|
||||
--identity-icon-color: #616161;
|
||||
}
|
||||
|
||||
[data-identity-color="yellow"] {
|
||||
--identity-tab-color: #ffcb00;
|
||||
--identity-icon-color: #ffcb00;
|
||||
@@ -124,20 +197,43 @@ table {
|
||||
--identity-icon: url("/img/usercontext.svg#chill");
|
||||
}
|
||||
|
||||
#current-tab [data-identity-icon="default-tab"] {
|
||||
background: center center no-repeat url("/img/blank-tab.svg");
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.button {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.button.primary {
|
||||
background-color: #0996f8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button.primary:hover {
|
||||
.button.primary:hover,
|
||||
.button.primary:focus {
|
||||
background-color: #0675d3;
|
||||
}
|
||||
|
||||
.button.secondary:hover {
|
||||
.button.secondary:hover,
|
||||
.button.secondary:focus {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Text links with actions */
|
||||
|
||||
.action-link:link {
|
||||
color: var(--primary-action-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.action-link:active,
|
||||
.action-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Panels keep everything togethert */
|
||||
.panel {
|
||||
display: flex;
|
||||
@@ -181,7 +277,7 @@ table {
|
||||
.column-panel-content .button,
|
||||
.panel-footer .button {
|
||||
align-items: center;
|
||||
block-size: 54px;
|
||||
block-size: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
@@ -198,7 +294,8 @@ table {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.panel-back-arrow:hover {
|
||||
.panel-back-arrow:hover,
|
||||
.panel-back-arrow:focus {
|
||||
background: #dedede;
|
||||
}
|
||||
|
||||
@@ -220,7 +317,7 @@ table {
|
||||
|
||||
.onboarding-title {
|
||||
color: #43484e;
|
||||
font-size: 16px;
|
||||
font-size: var(--font-size-heading);
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
margin-inline-end: 0;
|
||||
@@ -229,7 +326,7 @@ table {
|
||||
}
|
||||
|
||||
.onboarding p {
|
||||
color: #4a4a4a;
|
||||
color: var(--text-normal-color);
|
||||
font-size: 14px;
|
||||
margin-block-end: 16px;
|
||||
max-inline-size: 84%;
|
||||
@@ -249,7 +346,8 @@ table {
|
||||
transition: background-color 75ms;
|
||||
}
|
||||
|
||||
.onboarding-button:hover {
|
||||
.onboarding-button:hover,
|
||||
.onboarding-button:active {
|
||||
background-color: #0675d3;
|
||||
}
|
||||
|
||||
@@ -257,25 +355,38 @@ table {
|
||||
manage things like container crud */
|
||||
.pop-button {
|
||||
align-items: center;
|
||||
block-size: 48px;
|
||||
block-size: var(--icon-button-size);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex: 0 0 48px;
|
||||
flex: 0 0 var(--icon-button-size);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pop-button:hover,
|
||||
.pop-button:focus,
|
||||
.panel-footer-secondary:focus,
|
||||
.panel-footer-secondary:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.pop-button:active,
|
||||
.panel-footer-secondary:active {
|
||||
.pop-button:focus,
|
||||
.panel-footer-secondary:focus {
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.pop-button a,
|
||||
.panel-footer a,
|
||||
.panel-footer-secondary a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.pop-button-image {
|
||||
block-size: 20px;
|
||||
flex: 0 0 20px;
|
||||
margin-block-end: auto;
|
||||
margin-block-start: auto;
|
||||
margin-inline-end: auto;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.pop-button-image-small {
|
||||
@@ -287,20 +398,23 @@ manage things like container crud */
|
||||
.panel-header {
|
||||
align-items: center;
|
||||
block-size: 48px;
|
||||
border-block-end: 1px solid #ebebeb;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.panel-header .usercontext-icon {
|
||||
inline-size: var(--icon-button-size);
|
||||
}
|
||||
|
||||
.column-panel-content .panel-header {
|
||||
flex: 0 0 48px;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.panel-header-text {
|
||||
color: #4a4a4a;
|
||||
color: var(--text-normal-color);
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
font-size: var(--font-size-heading);
|
||||
font-weight: normal;
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
@@ -312,6 +426,47 @@ manage things like container crud */
|
||||
padding-inline-start: 16px;
|
||||
}
|
||||
|
||||
#container-panel .panel-header {
|
||||
background-color: #efefef;
|
||||
block-size: 26px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#container-panel .panel-header-text {
|
||||
color: #727272;
|
||||
font-size: 14px;
|
||||
padding-block-end: 0;
|
||||
padding-block-start: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.container-panel-controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-block-end: var(--block-line-space-size);
|
||||
margin-block-start: var(--block-line-space-size);
|
||||
margin-inline-end: var(--inline-item-space-size);
|
||||
margin-inline-start: var(--inline-item-space-size);
|
||||
}
|
||||
|
||||
#container-panel #sort-containers-link {
|
||||
align-items: center;
|
||||
block-size: var(--block-url-label-size);
|
||||
border: 1px solid #d8d8d8;
|
||||
border-radius: var(--small-radius);
|
||||
color: var(--title-text-color);
|
||||
display: flex;
|
||||
font-size: var(--small-text-size);
|
||||
inline-size: var(--inline-button-size);
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#container-panel #sort-containers-link:hover,
|
||||
#container-panel #sort-containers-link:focus {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
span ~ .panel-header-text {
|
||||
padding-block-end: 0;
|
||||
padding-block-start: 0;
|
||||
@@ -319,11 +474,92 @@ span ~ .panel-header-text {
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
#current-tab {
|
||||
align-items: center;
|
||||
color: var(--text-normal-color);
|
||||
display: grid;
|
||||
font-size: var(--small-text-size);
|
||||
grid-column-gap: var(--inline-item-space-size);
|
||||
grid-row-gap: var(--block-line-space-size);
|
||||
grid-template-columns: var(--icon-size) var(--icon-size) 1fr;
|
||||
margin-block-end: var(--block-line-space-size);
|
||||
margin-block-start: var(--block-line-separation-size);
|
||||
margin-inline-end: var(--inline-start-size);
|
||||
margin-inline-start: var(--inline-start-size);
|
||||
max-inline-size: 100%;
|
||||
}
|
||||
|
||||
#current-tab img {
|
||||
max-block-size: var(--icon-size);
|
||||
}
|
||||
|
||||
#current-tab > h3 {
|
||||
color: var(--text-heading-color);
|
||||
font-weight: normal;
|
||||
grid-column: span 3;
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
#current-page {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
#current-tab .page-title {
|
||||
font-size: var(--font-size-heading);
|
||||
grid-column: 2 / 4;
|
||||
}
|
||||
|
||||
#current-tab > label {
|
||||
display: contents;
|
||||
font-size: var(--small-text-size);
|
||||
}
|
||||
|
||||
#current-tab > label > input {
|
||||
-moz-appearance: none;
|
||||
block-size: var(--icon-size);
|
||||
border: 1px solid #d8d8d8;
|
||||
border-radius: var(--small-radius);
|
||||
display: block;
|
||||
grid-column-start: 2;
|
||||
inline-size: var(--icon-size);
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
#current-tab > label > input[disabled] {
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
#current-tab > label > input:checked {
|
||||
background-image: url("chrome://global/skin/in-content/check.svg#check-native");
|
||||
background-position: -1px -1px;
|
||||
background-size: var(--icon-size);
|
||||
}
|
||||
|
||||
#current-container {
|
||||
color: var(--identity-tab-color);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#current-tab > label > .usercontext-icon {
|
||||
background-size: 16px;
|
||||
block-size: 16px;
|
||||
display: block;
|
||||
flex: 0 0 20px;
|
||||
inline-size: 20px;
|
||||
margin-inline-end: 3px;
|
||||
margin-inline-start: 3px;
|
||||
}
|
||||
|
||||
/* Rows used when iterating over panels */
|
||||
.container-panel-row {
|
||||
align-items: center;
|
||||
background-color: #fefefe !important;
|
||||
block-size: 48px;
|
||||
border-block-end: 1px solid #f1f1f1;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
@@ -331,12 +567,10 @@ span ~ .panel-header-text {
|
||||
}
|
||||
|
||||
.container-panel-row .container-name {
|
||||
flex: 1;
|
||||
max-inline-size: 160px;
|
||||
overflow: hidden;
|
||||
padding-inline-end: 4px;
|
||||
padding-inline-start: 4px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.edit-containers-panel .userContext-wrapper {
|
||||
@@ -350,40 +584,46 @@ span ~ .panel-header-text {
|
||||
transition: background-color 75ms;
|
||||
}
|
||||
|
||||
.clickable.userContext-wrapper:hover {
|
||||
.container-panel-row:hover .clickable.userContext-wrapper,
|
||||
.container-panel-row:focus .clickable.userContext-wrapper {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.userContext-icon-wrapper {
|
||||
block-size: 48px;
|
||||
flex: 0 0 48px;
|
||||
block-size: var(--icon-button-size);
|
||||
flex: 0 0 var(--icon-button-size);
|
||||
margin-inline-start: var(--inline-icon-space-size);
|
||||
}
|
||||
|
||||
/* .userContext-icon is used natively, Bug 1333811 was raised to fix */
|
||||
.userContext-icon,
|
||||
.usercontext-icon {
|
||||
background-image: var(--identity-icon);
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 20px 20px;
|
||||
block-size: 48px;
|
||||
block-size: 100%;
|
||||
fill: var(--identity-icon-color);
|
||||
filter: url('/img/filters.svg#fill');
|
||||
flex: 0 0 48px;
|
||||
}
|
||||
|
||||
.clickable:hover .userContext-icon,
|
||||
.clickable:hover .usercontext-icon {
|
||||
.container-panel-row:hover .clickable .usercontext-icon,
|
||||
.container-panel-row:focus .clickable .usercontext-icon,
|
||||
.container-panel-row .clickable:focus .usercontext-icon {
|
||||
background-image: url('/img/container-newtab.svg');
|
||||
fill: 'gray';
|
||||
fill: #979797;
|
||||
filter: url('/img/filters.svg#fill');
|
||||
}
|
||||
|
||||
.container-panel-row .clickable:hover .usercontext-icon,
|
||||
.container-panel-row .clickable:focus .usercontext-icon {
|
||||
fill: #0094fb;
|
||||
}
|
||||
|
||||
/* Panel Footer */
|
||||
.panel-footer {
|
||||
align-items: center;
|
||||
background: #efefef;
|
||||
block-size: 54px;
|
||||
block-size: var(--icon-button-size);
|
||||
border-block-end: 1px solid #d8d8d8;
|
||||
color: #000;
|
||||
display: flex;
|
||||
@@ -392,14 +632,9 @@ span ~ .panel-header-text {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.panel-footer .pop-button {
|
||||
block-size: 54px;
|
||||
flex: 0 0 54px;
|
||||
}
|
||||
|
||||
.edit-containers-text {
|
||||
align-items: center;
|
||||
block-size: 54px;
|
||||
block-size: 100%;
|
||||
border-inline-end: solid 1px #d8d8d8;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@@ -408,7 +643,7 @@ span ~ .panel-header-text {
|
||||
|
||||
.edit-containers-text a {
|
||||
align-items: center;
|
||||
block-size: 54px;
|
||||
block-size: 100%;
|
||||
color: #0a0a0a;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@@ -416,11 +651,8 @@ span ~ .panel-header-text {
|
||||
}
|
||||
|
||||
/* Container info list */
|
||||
#container-info-name {
|
||||
margin-inline-end: 0.5rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
.container-info-tab-title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#container-info-hideorshow {
|
||||
@@ -465,30 +697,31 @@ span ~ .panel-header-text {
|
||||
|
||||
.container-info-tab-row td {
|
||||
max-inline-size: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.container-info-list {
|
||||
border-block-start: 1px solid #ebebeb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-block-start: 4px;
|
||||
padding-block-start: 4px;
|
||||
}
|
||||
|
||||
.container-info-list tbody {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clickable:hover {
|
||||
.clickable:hover,
|
||||
.clickable:focus {
|
||||
background-color: #ebebeb;
|
||||
}
|
||||
|
||||
.edit-containers-exit-text {
|
||||
align-items: center;
|
||||
background: #248aeb;
|
||||
background: var(--primary-action-color);
|
||||
block-size: 100%;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
@@ -515,7 +748,7 @@ span ~ .panel-header-text {
|
||||
|
||||
.delete-container-confirm-title {
|
||||
color: #000;
|
||||
font-size: 16px;
|
||||
font-size: var(--font-size-heading);
|
||||
}
|
||||
|
||||
/* Form info */
|
||||
@@ -527,36 +760,95 @@ span ~ .panel-header-text {
|
||||
padding-inline-start: 16px;
|
||||
}
|
||||
|
||||
.column-panel-content form span {
|
||||
align-items: center;
|
||||
block-size: 44px;
|
||||
display: flex;
|
||||
flex: 0 0 25%;
|
||||
justify-content: center;
|
||||
#edit-sites-assigned {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.edit-container-panel label {
|
||||
#edit-sites-assigned h3 {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
padding-block-end: 6px;
|
||||
padding-block-start: 6px;
|
||||
padding-inline-end: 16px;
|
||||
padding-inline-start: 16px;
|
||||
}
|
||||
|
||||
.assigned-sites-list > div {
|
||||
display: flex;
|
||||
padding-block-end: 6px;
|
||||
padding-block-start: 6px;
|
||||
}
|
||||
|
||||
.assigned-sites-list > div > .icon {
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
|
||||
.assigned-sites-list > div > .delete-assignment {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.assigned-sites-list > div:hover > .delete-assignment {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.assigned-sites-list > div > .hostname {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.radio-choice > .radio-container {
|
||||
align-items: center;
|
||||
block-size: 29px;
|
||||
display: flex;
|
||||
flex: 0 0 calc(100% / 8);
|
||||
}
|
||||
|
||||
.radio-choice > .radio-container > label {
|
||||
background: none;
|
||||
block-size: 23px;
|
||||
border: 0;
|
||||
filter: none;
|
||||
inline-size: 23px;
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: 0;
|
||||
padding-block-end: 0;
|
||||
padding-block-start: 0;
|
||||
padding-inline-end: 0;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
.radio-choice > .radio-container > label::before {
|
||||
background-color: unset;
|
||||
background-image: var(--identity-icon);
|
||||
background-size: 26px 26px;
|
||||
block-size: 34px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 16px;
|
||||
block-size: 23px;
|
||||
border: none;
|
||||
content: "";
|
||||
display: block;
|
||||
fill: var(--identity-icon-color);
|
||||
filter: url('/img/filters.svg#fill');
|
||||
flex: 0 0 34px;
|
||||
inline-size: 23px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.edit-container-panel label::before {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
.edit-container-panel [type="radio"] {
|
||||
.radio-choice > .radio-container > [type="radio"] {
|
||||
-moz-appearance: none;
|
||||
display: inline;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.edit-container-panel [type="radio"]:checked + label {
|
||||
outline: 2px solid grey;
|
||||
-moz-outline-radius: 50px;
|
||||
.radio-choice > .radio-container > [type="radio"]:checked + label {
|
||||
background: #d3d3d3;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
/* When focusing the element add a thin blue highlight to match input fields. This gives a distinction to other selected radio items */
|
||||
.radio-choice > .radio-container > [type="radio"]:focus + label {
|
||||
outline: 1px solid #1f9ffc;
|
||||
-moz-outline-radius: 100%;
|
||||
}
|
||||
|
||||
.edit-container-panel fieldset {
|
||||
@@ -575,6 +867,10 @@ span ~ .panel-header-text {
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
.edit-container-panel fieldset:last-of-type {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.edit-container-panel input[type="text"] {
|
||||
block-size: 36px;
|
||||
border-radius: 3px;
|
||||
@@ -589,5 +885,5 @@ span ~ .panel-header-text {
|
||||
.edit-container-panel legend {
|
||||
flex: 1 0;
|
||||
font-size: 14px !important;
|
||||
padding-block-end: 5px;
|
||||
padding-block-end: 6px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
|
||||
<path d="M17,12v2a1,1,0,0,1-1,1H2a1,1,0,0,1-1-1V12a1,1,0,0,1,1-1H1.142c2.3,0,2.536-1.773,2.874-4,0.351-2.316.083-4,3.13-4h3.707C13.917,3,13.647,4.684,14,7c0.34,2.228.582,4,2.89,4H16A1,1,0,0,1,17,12Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 307 B |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
@@ -1,30 +1,92 @@
|
||||
const redirectUrl = new URL(window.location).searchParams.get("url");
|
||||
document.getElementById("redirect-url").textContent = redirectUrl;
|
||||
const redirectSite = new URL(redirectUrl).hostname;
|
||||
document.getElementById("redirect-site").textContent = redirectSite;
|
||||
async function load() {
|
||||
const searchParams = new URL(window.location).searchParams;
|
||||
const redirectUrl = decodeURIComponent(searchParams.get("url"));
|
||||
const cookieStoreId = searchParams.get("cookieStoreId");
|
||||
const currentCookieStoreId = searchParams.get("currentCookieStoreId");
|
||||
const redirectUrlElement = document.getElementById("redirect-url");
|
||||
redirectUrlElement.textContent = redirectUrl;
|
||||
appendFavicon(redirectUrl, redirectUrlElement);
|
||||
|
||||
document.getElementById("redirect-form").addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
const container = await browser.contextualIdentities.get(cookieStoreId);
|
||||
[...document.querySelectorAll(".container-name")].forEach((containerNameElement) => {
|
||||
containerNameElement.textContent = container.name;
|
||||
});
|
||||
|
||||
// If default container, button will default to normal HTML content
|
||||
if (currentCookieStoreId) {
|
||||
const currentContainer = await browser.contextualIdentities.get(currentCookieStoreId);
|
||||
document.getElementById("current-container-name").textContent = currentContainer.name;
|
||||
}
|
||||
|
||||
document.getElementById("redirect-form").addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
const buttonTarget = e.explicitOriginalTarget;
|
||||
switch (buttonTarget.id) {
|
||||
case "confirm":
|
||||
confirmSubmit(redirectUrl, cookieStoreId);
|
||||
break;
|
||||
case "deny":
|
||||
denySubmit(redirectUrl);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function appendFavicon(pageUrl, redirectUrlElement) {
|
||||
const origin = new URL(pageUrl).origin;
|
||||
const favIconElement = Utils.createFavIconElement(`${origin}/favicon.ico`);
|
||||
|
||||
redirectUrlElement.prepend(favIconElement);
|
||||
}
|
||||
|
||||
function confirmSubmit(redirectUrl, cookieStoreId) {
|
||||
const neverAsk = document.getElementById("never-ask").checked;
|
||||
// Sending neverAsk message to background to store for next time we see this process
|
||||
if (neverAsk) {
|
||||
browser.runtime.sendMessage({
|
||||
method: "neverAsk",
|
||||
neverAsk: true,
|
||||
pageUrl: redirectUrl
|
||||
}).then(() => {
|
||||
redirect();
|
||||
}).catch(() => {
|
||||
// Can't really do much here user will have to click it again
|
||||
});
|
||||
}
|
||||
browser.runtime.sendMessage({
|
||||
method: "sendTelemetryPayload",
|
||||
event: "click-to-reload-page-in-container",
|
||||
});
|
||||
redirect();
|
||||
});
|
||||
openInContainer(redirectUrl, cookieStoreId);
|
||||
}
|
||||
|
||||
function redirect() {
|
||||
const redirectUrl = document.getElementById("redirect-url").textContent;
|
||||
function getCurrentTab() {
|
||||
return browser.tabs.query({
|
||||
active: true,
|
||||
windowId: browser.windows.WINDOW_ID_CURRENT
|
||||
});
|
||||
}
|
||||
|
||||
async function denySubmit(redirectUrl) {
|
||||
const tab = await getCurrentTab();
|
||||
await browser.runtime.sendMessage({
|
||||
method: "exemptContainerAssignment",
|
||||
tabId: tab[0].id,
|
||||
pageUrl: redirectUrl
|
||||
});
|
||||
browser.runtime.sendMessage({
|
||||
method: "sendTelemetryPayload",
|
||||
event: "click-to-reload-page-in-same-container",
|
||||
});
|
||||
document.location.replace(redirectUrl);
|
||||
}
|
||||
|
||||
load();
|
||||
|
||||
async function openInContainer(redirectUrl, cookieStoreId) {
|
||||
const tab = await getCurrentTab();
|
||||
await browser.tabs.create({
|
||||
index: tab[0].index + 1,
|
||||
cookieStoreId,
|
||||
url: redirectUrl
|
||||
});
|
||||
if (tab.length > 0) {
|
||||
browser.tabs.remove(tab[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
async function delayAnimation(delay = 350) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, delay);
|
||||
});
|
||||
}
|
||||
|
||||
async function doAnimation(element, property, value) {
|
||||
return new Promise((resolve) => {
|
||||
const handler = () => {
|
||||
resolve();
|
||||
element.removeEventListener("transitionend", handler);
|
||||
};
|
||||
element.addEventListener("transitionend", handler);
|
||||
window.requestAnimationFrame(() => {
|
||||
element.style[property] = value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function addMessage(message) {
|
||||
const divElement = document.createElement("div");
|
||||
divElement.classList.add("container-notification");
|
||||
// For the eager eyed, this is an experiment. It is however likely that a website will know it is "contained" anyway
|
||||
divElement.innerText = message.text;
|
||||
|
||||
const imageElement = document.createElement("img");
|
||||
imageElement.src = browser.extension.getURL("/img/container-site-d-24.png");
|
||||
divElement.prepend(imageElement);
|
||||
|
||||
document.body.appendChild(divElement);
|
||||
|
||||
await delayAnimation(100);
|
||||
await doAnimation(divElement, "transform", "translateY(0)");
|
||||
await delayAnimation(3000);
|
||||
await doAnimation(divElement, "transform", "translateY(-100%)");
|
||||
|
||||
divElement.remove();
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.addListener((message) => {
|
||||
addMessage(message);
|
||||
});
|
||||
+495
-93
@@ -7,11 +7,16 @@ const CONTAINER_UNHIDE_SRC = "/img/container-unhide.svg";
|
||||
|
||||
const DEFAULT_COLOR = "blue";
|
||||
const DEFAULT_ICON = "circle";
|
||||
const NEW_CONTAINER_ID = "new";
|
||||
|
||||
const ONBOARDING_STORAGE_KEY = "onboarding-stage";
|
||||
|
||||
// List of panels
|
||||
const P_ONBOARDING_1 = "onboarding1";
|
||||
const P_ONBOARDING_2 = "onboarding2";
|
||||
const P_ONBOARDING_3 = "onboarding3";
|
||||
const P_ONBOARDING_4 = "onboarding4";
|
||||
const P_ONBOARDING_5 = "onboarding5";
|
||||
const P_CONTAINERS_LIST = "containersList";
|
||||
const P_CONTAINERS_EDIT = "containersEdit";
|
||||
const P_CONTAINER_INFO = "containerInfo";
|
||||
@@ -54,6 +59,13 @@ function escaped(strings, ...values) {
|
||||
return result.join("");
|
||||
}
|
||||
|
||||
async function getExtensionInfo() {
|
||||
const manifestPath = browser.extension.getURL("manifest.json");
|
||||
const response = await fetch(manifestPath);
|
||||
const extensionInfo = await response.json();
|
||||
return extensionInfo;
|
||||
}
|
||||
|
||||
// This object controls all the panels, identities and many other things.
|
||||
const Logic = {
|
||||
_identities: [],
|
||||
@@ -61,40 +73,144 @@ const Logic = {
|
||||
_currentPanel: null,
|
||||
_previousPanel: null,
|
||||
_panels: {},
|
||||
_onboardingVariation: null,
|
||||
|
||||
async init() {
|
||||
// Remove browserAction "upgraded" badge when opening panel
|
||||
this.clearBrowserActionBadge();
|
||||
|
||||
init() {
|
||||
// Retrieve the list of identities.
|
||||
this.refreshIdentities()
|
||||
const identitiesPromise = this.refreshIdentities();
|
||||
// Get the onboarding variation
|
||||
const variationPromise = this.getShieldStudyVariation();
|
||||
|
||||
try {
|
||||
await Promise.all([identitiesPromise, variationPromise]);
|
||||
} catch(e) {
|
||||
throw new Error("Failed to retrieve the identities or variation. We cannot continue. ", e.message);
|
||||
}
|
||||
|
||||
// Routing to the correct panel.
|
||||
.then(() => {
|
||||
// If localStorage is disabled, we don't show the onboarding.
|
||||
if (!localStorage || localStorage.getItem("onboarded3")) {
|
||||
this.showPanel(P_CONTAINERS_LIST);
|
||||
// If localStorage is disabled, we don't show the onboarding.
|
||||
const data = await browser.storage.local.get([ONBOARDING_STORAGE_KEY]);
|
||||
let onboarded = data[ONBOARDING_STORAGE_KEY];
|
||||
if (!onboarded) {
|
||||
// Legacy local storage used before panel 5
|
||||
if (localStorage.getItem("onboarded4")) {
|
||||
onboarded = 4;
|
||||
} else if (localStorage.getItem("onboarded3")) {
|
||||
onboarded = 3;
|
||||
} else if (localStorage.getItem("onboarded2")) {
|
||||
this.showPanel(P_ONBOARDING_3);
|
||||
onboarded = 2;
|
||||
} else if (localStorage.getItem("onboarded1")) {
|
||||
this.showPanel(P_ONBOARDING_2);
|
||||
onboarded = 1;
|
||||
} else {
|
||||
this.showPanel(P_ONBOARDING_1);
|
||||
onboarded = 0;
|
||||
}
|
||||
})
|
||||
this.setOnboardingStage(onboarded);
|
||||
}
|
||||
|
||||
.catch(() => {
|
||||
throw new Error("Failed to retrieve the identities. We cannot continue.");
|
||||
switch (onboarded) {
|
||||
case 5:
|
||||
this.showPanel(P_CONTAINERS_LIST);
|
||||
break;
|
||||
case 4:
|
||||
this.showPanel(P_ONBOARDING_5);
|
||||
break;
|
||||
case 3:
|
||||
this.showPanel(P_ONBOARDING_4);
|
||||
break;
|
||||
case 2:
|
||||
this.showPanel(P_ONBOARDING_3);
|
||||
break;
|
||||
case 1:
|
||||
this.showPanel(P_ONBOARDING_2);
|
||||
break;
|
||||
case 0:
|
||||
default:
|
||||
this.showPanel(P_ONBOARDING_1);
|
||||
break;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
setOnboardingStage(stage) {
|
||||
return browser.storage.local.set({
|
||||
[ONBOARDING_STORAGE_KEY]: stage
|
||||
});
|
||||
},
|
||||
|
||||
async clearBrowserActionBadge() {
|
||||
const extensionInfo = await getExtensionInfo();
|
||||
const storage = await browser.storage.local.get({browserActionBadgesClicked: []});
|
||||
browser.browserAction.setBadgeBackgroundColor({color: ""});
|
||||
browser.browserAction.setBadgeText({text: ""});
|
||||
storage.browserActionBadgesClicked.push(extensionInfo.version);
|
||||
browser.storage.local.set({browserActionBadgesClicked: storage.browserActionBadgesClicked});
|
||||
},
|
||||
|
||||
async identity(cookieStoreId) {
|
||||
const identity = await browser.contextualIdentities.get(cookieStoreId);
|
||||
return identity || {
|
||||
name: "Default",
|
||||
cookieStoreId,
|
||||
icon: "default-tab",
|
||||
color: "default-tab"
|
||||
};
|
||||
},
|
||||
|
||||
addEnterHandler(element, handler) {
|
||||
element.addEventListener("click", (e) => {
|
||||
handler(e);
|
||||
});
|
||||
element.addEventListener("keydown", (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
handler(e);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
userContextId(cookieStoreId = "") {
|
||||
const userContextId = cookieStoreId.replace("firefox-container-", "");
|
||||
return (userContextId !== cookieStoreId) ? Number(userContextId) : false;
|
||||
},
|
||||
|
||||
async currentTab() {
|
||||
const activeTabs = await browser.tabs.query({active: true, windowId: browser.windows.WINDOW_ID_CURRENT});
|
||||
if (activeTabs.length > 0) {
|
||||
return activeTabs[0];
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
refreshIdentities() {
|
||||
return browser.runtime.sendMessage({
|
||||
method: "queryIdentities"
|
||||
})
|
||||
.then(identities => {
|
||||
this._identities = identities;
|
||||
});
|
||||
return Promise.all([
|
||||
browser.contextualIdentities.query({}),
|
||||
browser.runtime.sendMessage({
|
||||
method: "queryIdentitiesState"
|
||||
})
|
||||
]).then(([identities, state]) => {
|
||||
this._identities = identities.map((identity) => {
|
||||
const stateObject = state[Logic.userContextId(identity.cookieStoreId)];
|
||||
if (stateObject) {
|
||||
identity.hasOpenTabs = stateObject.hasOpenTabs;
|
||||
identity.hasHiddenTabs = stateObject.hasHiddenTabs;
|
||||
}
|
||||
return identity;
|
||||
});
|
||||
}).catch((e) => {throw e;});
|
||||
},
|
||||
|
||||
showPanel(panel, currentIdentity = null) {
|
||||
getPanelSelector(panel) {
|
||||
if (this._onboardingVariation === "securityOnboarding" &&
|
||||
panel.hasOwnProperty("securityPanelSelector")) {
|
||||
return panel.securityPanelSelector;
|
||||
} else {
|
||||
return panel.panelSelector;
|
||||
}
|
||||
},
|
||||
|
||||
async showPanel(panel, currentIdentity = null) {
|
||||
// Invalid panel... ?!?
|
||||
if (!(panel in this._panels)) {
|
||||
throw new Error("Something really bad happened. Unknown panel: " + panel);
|
||||
@@ -106,15 +222,18 @@ const Logic = {
|
||||
this._currentIdentity = currentIdentity;
|
||||
|
||||
// Initialize the panel before showing it.
|
||||
this._panels[panel].prepare().then(() => {
|
||||
for (let panelElement of document.querySelectorAll(".panel")) { // eslint-disable-line prefer-const
|
||||
await this._panels[panel].prepare();
|
||||
Object.keys(this._panels).forEach((panelKey) => {
|
||||
const panelItem = this._panels[panelKey];
|
||||
const panelElement = document.querySelector(this.getPanelSelector(panelItem));
|
||||
if (!panelElement.classList.contains("hide")) {
|
||||
panelElement.classList.add("hide");
|
||||
if ("unregister" in panelItem) {
|
||||
panelItem.unregister();
|
||||
}
|
||||
}
|
||||
document.querySelector(this._panels[panel].panelSelector).classList.remove("hide");
|
||||
})
|
||||
.catch(() => {
|
||||
throw new Error("Failed to show panel " + panel);
|
||||
});
|
||||
document.querySelector(this.getPanelSelector(this._panels[panel])).classList.remove("hide");
|
||||
},
|
||||
|
||||
showPreviousPanel() {
|
||||
@@ -141,6 +260,62 @@ const Logic = {
|
||||
return this._currentIdentity;
|
||||
},
|
||||
|
||||
currentUserContextId() {
|
||||
const identity = Logic.currentIdentity();
|
||||
return Logic.userContextId(identity.cookieStoreId);
|
||||
},
|
||||
|
||||
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}
|
||||
});
|
||||
},
|
||||
|
||||
getAssignment(tab) {
|
||||
return browser.runtime.sendMessage({
|
||||
method: "getAssignment",
|
||||
tabId: tab.id
|
||||
});
|
||||
},
|
||||
|
||||
getAssignmentObjectByContainer(userContextId) {
|
||||
return browser.runtime.sendMessage({
|
||||
method: "getAssignmentObjectByContainer",
|
||||
message: {userContextId}
|
||||
});
|
||||
},
|
||||
|
||||
setOrRemoveAssignment(tabId, url, userContextId, value) {
|
||||
return browser.runtime.sendMessage({
|
||||
method: "setOrRemoveAssignment",
|
||||
tabId,
|
||||
url,
|
||||
userContextId,
|
||||
value
|
||||
});
|
||||
},
|
||||
|
||||
getShieldStudyVariation() {
|
||||
return browser.runtime.sendMessage({
|
||||
method: "getShieldStudyVariation"
|
||||
}).then(variation => {
|
||||
this._onboardingVariation = variation;
|
||||
});
|
||||
},
|
||||
|
||||
generateIdentityName() {
|
||||
const defaultName = "Container #";
|
||||
const ids = [];
|
||||
@@ -169,13 +344,16 @@ const Logic = {
|
||||
|
||||
Logic.registerPanel(P_ONBOARDING_1, {
|
||||
panelSelector: ".onboarding-panel-1",
|
||||
securityPanelSelector: ".security-onboarding-panel-1",
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
// Let's move to the next panel.
|
||||
document.querySelector("#onboarding-start-button").addEventListener("click", () => {
|
||||
localStorage.setItem("onboarded1", true);
|
||||
Logic.showPanel(P_ONBOARDING_2);
|
||||
[...document.querySelectorAll(".onboarding-start-button")].forEach(startElement => {
|
||||
Logic.addEnterHandler(startElement, async function () {
|
||||
await Logic.setOnboardingStage(1);
|
||||
Logic.showPanel(P_ONBOARDING_2);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@@ -190,13 +368,16 @@ Logic.registerPanel(P_ONBOARDING_1, {
|
||||
|
||||
Logic.registerPanel(P_ONBOARDING_2, {
|
||||
panelSelector: ".onboarding-panel-2",
|
||||
securityPanelSelector: ".security-onboarding-panel-2",
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
// Let's move to the containers list panel.
|
||||
document.querySelector("#onboarding-next-button").addEventListener("click", () => {
|
||||
localStorage.setItem("onboarded2", true);
|
||||
Logic.showPanel(P_ONBOARDING_3);
|
||||
[...document.querySelectorAll(".onboarding-next-button")].forEach(nextElement => {
|
||||
Logic.addEnterHandler(nextElement, async function () {
|
||||
await Logic.setOnboardingStage(2);
|
||||
Logic.showPanel(P_ONBOARDING_3);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@@ -211,12 +392,57 @@ Logic.registerPanel(P_ONBOARDING_2, {
|
||||
|
||||
Logic.registerPanel(P_ONBOARDING_3, {
|
||||
panelSelector: ".onboarding-panel-3",
|
||||
securityPanelSelector: ".security-onboarding-panel-3",
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
// Let's move to the containers list panel.
|
||||
document.querySelector("#onboarding-done-button").addEventListener("click", () => {
|
||||
localStorage.setItem("onboarded3", true);
|
||||
[...document.querySelectorAll(".onboarding-almost-done-button")].forEach(almostElement => {
|
||||
Logic.addEnterHandler(almostElement, async function () {
|
||||
await Logic.setOnboardingStage(3);
|
||||
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.
|
||||
Logic.addEnterHandler(document.querySelector("#onboarding-done-button"), async function () {
|
||||
await Logic.setOnboardingStage(4);
|
||||
Logic.showPanel(P_ONBOARDING_5);
|
||||
});
|
||||
},
|
||||
|
||||
// This method is called when the panel is shown.
|
||||
prepare() {
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
});
|
||||
|
||||
// P_ONBOARDING_5: Fifth page for Onboarding: new tab long-press behavior
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
Logic.registerPanel(P_ONBOARDING_5, {
|
||||
panelSelector: ".onboarding-panel-5",
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
// Let's move to the containers list panel.
|
||||
Logic.addEnterHandler(document.querySelector("#onboarding-longpress-button"), async function () {
|
||||
await Logic.setOnboardingStage(5);
|
||||
Logic.showPanel(P_CONTAINERS_LIST);
|
||||
});
|
||||
},
|
||||
@@ -235,19 +461,18 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
document.querySelector("#container-add-link").addEventListener("click", () => {
|
||||
Logic.addEnterHandler(document.querySelector("#container-add-link"), () => {
|
||||
Logic.showPanel(P_CONTAINER_EDIT, { name: Logic.generateIdentityName() });
|
||||
});
|
||||
|
||||
document.querySelector("#edit-containers-link").addEventListener("click", () => {
|
||||
browser.runtime.sendMessage({
|
||||
method: "sendTelemetryPayload",
|
||||
Logic.addEnterHandler(document.querySelector("#edit-containers-link"), () => {
|
||||
Logic.sendTelemetryPayload({
|
||||
event: "edit-containers"
|
||||
});
|
||||
Logic.showPanel(P_CONTAINERS_EDIT);
|
||||
});
|
||||
|
||||
document.querySelector("#sort-containers-link").addEventListener("click", () => {
|
||||
Logic.addEnterHandler(document.querySelector("#sort-containers-link"), () => {
|
||||
browser.runtime.sendMessage({
|
||||
method: "sortTabs"
|
||||
}).then(() => {
|
||||
@@ -256,12 +481,96 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||
window.close();
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
const element = document.activeElement;
|
||||
function next() {
|
||||
const nextElement = element.nextElementSibling;
|
||||
if (nextElement) {
|
||||
nextElement.querySelector("td[tabindex=0]").focus();
|
||||
}
|
||||
}
|
||||
function previous() {
|
||||
const previousElement = element.previousElementSibling;
|
||||
if (previousElement) {
|
||||
previousElement.querySelector("td[tabindex=0]").focus();
|
||||
}
|
||||
}
|
||||
switch (e.keyCode) {
|
||||
case 40:
|
||||
next();
|
||||
break;
|
||||
case 38:
|
||||
previous();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// When the popup is open sometimes the tab will still be updating it's state
|
||||
this.tabUpdateHandler = (tabId, changeInfo) => {
|
||||
const propertiesToUpdate = ["title", "favIconUrl"];
|
||||
const hasChanged = Object.keys(changeInfo).find((changeInfoKey) => {
|
||||
if (propertiesToUpdate.includes(changeInfoKey)) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (hasChanged) {
|
||||
this.prepareCurrentTabHeader();
|
||||
}
|
||||
};
|
||||
browser.tabs.onUpdated.addListener(this.tabUpdateHandler);
|
||||
},
|
||||
|
||||
unregister() {
|
||||
browser.tabs.onUpdated.removeListener(this.tabUpdateHandler);
|
||||
},
|
||||
|
||||
setupAssignmentCheckbox(siteSettings, currentUserContextId) {
|
||||
const assignmentCheckboxElement = document.getElementById("container-page-assigned");
|
||||
let checked = false;
|
||||
if (siteSettings && Number(siteSettings.userContextId) === currentUserContextId) {
|
||||
checked = true;
|
||||
}
|
||||
assignmentCheckboxElement.checked = checked;
|
||||
let disabled = false;
|
||||
if (siteSettings === false) {
|
||||
disabled = true;
|
||||
}
|
||||
assignmentCheckboxElement.disabled = disabled;
|
||||
},
|
||||
|
||||
async prepareCurrentTabHeader() {
|
||||
const currentTab = await Logic.currentTab();
|
||||
const currentTabElement = document.getElementById("current-tab");
|
||||
const assignmentCheckboxElement = document.getElementById("container-page-assigned");
|
||||
const currentTabUserContextId = Logic.userContextId(currentTab.cookieStoreId);
|
||||
assignmentCheckboxElement.addEventListener("change", () => {
|
||||
Logic.setOrRemoveAssignment(currentTab.id, currentTab.url, currentTabUserContextId, !assignmentCheckboxElement.checked);
|
||||
});
|
||||
currentTabElement.hidden = !currentTab;
|
||||
this.setupAssignmentCheckbox(false, currentTabUserContextId);
|
||||
if (currentTab) {
|
||||
const identity = await Logic.identity(currentTab.cookieStoreId);
|
||||
const siteSettings = await Logic.getAssignment(currentTab);
|
||||
this.setupAssignmentCheckbox(siteSettings, currentTabUserContextId);
|
||||
const currentPage = document.getElementById("current-page");
|
||||
currentPage.innerHTML = escaped`<span class="page-title truncate-text">${currentTab.title}</span>`;
|
||||
const favIconElement = Utils.createFavIconElement(currentTab.favIconUrl || "");
|
||||
currentPage.prepend(favIconElement);
|
||||
|
||||
const currentContainer = document.getElementById("current-container");
|
||||
currentContainer.innerText = identity.name;
|
||||
|
||||
currentContainer.setAttribute("data-identity-color", identity.color);
|
||||
}
|
||||
},
|
||||
|
||||
// This method is called when the panel is shown.
|
||||
prepare() {
|
||||
async prepare() {
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
this.prepareCurrentTabHeader();
|
||||
|
||||
Logic.identities().forEach(identity => {
|
||||
const hasTabs = (identity.hasHiddenTabs || identity.hasOpenTabs);
|
||||
const tr = document.createElement("tr");
|
||||
@@ -269,16 +578,20 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||
const manage = document.createElement("td");
|
||||
|
||||
tr.classList.add("container-panel-row");
|
||||
|
||||
context.classList.add("userContext-wrapper", "open-newtab", "clickable");
|
||||
manage.classList.add("show-tabs", "pop-button");
|
||||
manage.title = escaped`View ${identity.name} container`;
|
||||
context.setAttribute("tabindex", "0");
|
||||
context.title = escaped`Create ${identity.name} tab`;
|
||||
context.innerHTML = escaped`
|
||||
<div class="userContext-icon-wrapper open-newtab">
|
||||
<div class="userContext-icon"
|
||||
data-identity-icon="${identity.image}"
|
||||
<div class="usercontext-icon"
|
||||
data-identity-icon="${identity.icon}"
|
||||
data-identity-color="${identity.color}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-name"></div>`;
|
||||
<div class="container-name truncate-text"></div>`;
|
||||
context.querySelector(".container-name").textContent = identity.name;
|
||||
manage.innerHTML = "<img src='/img/container-arrow.svg' class='show-tabs pop-button-image-small' />";
|
||||
|
||||
@@ -290,12 +603,16 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||
tr.appendChild(manage);
|
||||
}
|
||||
|
||||
tr.addEventListener("click", e => {
|
||||
if (e.target.matches(".open-newtab") || e.target.parentNode.matches(".open-newtab")) {
|
||||
Logic.addEnterHandler(tr, e => {
|
||||
if (e.target.matches(".open-newtab")
|
||||
|| e.target.parentNode.matches(".open-newtab")
|
||||
|| e.type === "keydown") {
|
||||
browser.runtime.sendMessage({
|
||||
method: "openTab",
|
||||
userContextId: identity.userContextId,
|
||||
source: "pop-up"
|
||||
message: {
|
||||
userContextId: Logic.userContextId(identity.cookieStoreId),
|
||||
source: "pop-up"
|
||||
}
|
||||
}).then(() => {
|
||||
window.close();
|
||||
}).catch(() => {
|
||||
@@ -307,10 +624,22 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||
});
|
||||
});
|
||||
|
||||
const list = document.querySelector(".identities-list");
|
||||
const list = document.querySelector(".identities-list tbody");
|
||||
|
||||
list.innerHTML = "";
|
||||
list.appendChild(fragment);
|
||||
/* Not sure why extensions require a focus for the doorhanger,
|
||||
however it allows us to have a tabindex before the first selected item
|
||||
*/
|
||||
const focusHandler = () => {
|
||||
list.querySelector("tr .clickable").focus();
|
||||
document.removeEventListener("focus", focusHandler);
|
||||
};
|
||||
document.addEventListener("focus", focusHandler);
|
||||
/* If the user mousedown's first then remove the focus handler */
|
||||
document.addEventListener("mousedown", () => {
|
||||
document.removeEventListener("focus", focusHandler);
|
||||
});
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
@@ -324,15 +653,15 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
document.querySelector("#close-container-info-panel").addEventListener("click", () => {
|
||||
Logic.addEnterHandler(document.querySelector("#close-container-info-panel"), () => {
|
||||
Logic.showPreviousPanel();
|
||||
});
|
||||
|
||||
document.querySelector("#container-info-hideorshow").addEventListener("click", () => {
|
||||
Logic.addEnterHandler(document.querySelector("#container-info-hideorshow"), () => {
|
||||
const identity = Logic.currentIdentity();
|
||||
browser.runtime.sendMessage({
|
||||
method: identity.hasHiddenTabs ? "showTabs" : "hideTabs",
|
||||
userContextId: identity.userContextId
|
||||
userContextId: Logic.currentUserContextId()
|
||||
}).then(() => {
|
||||
window.close();
|
||||
}).catch(() => {
|
||||
@@ -359,13 +688,13 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
||||
|
||||
moveTabsEl.parentNode.insertBefore(fragment, moveTabsEl.nextSibling);
|
||||
} else {
|
||||
moveTabsEl.addEventListener("click", () => {
|
||||
return browser.runtime.sendMessage({
|
||||
Logic.addEnterHandler(moveTabsEl, () => {
|
||||
browser.runtime.sendMessage({
|
||||
method: "moveTabsToWindow",
|
||||
userContextId: Logic.currentIdentity().userContextId,
|
||||
userContextId: Logic.userContextId(Logic.currentIdentity().cookieStoreId),
|
||||
}).then(() => {
|
||||
window.close();
|
||||
});
|
||||
}).catch((e) => { throw e; });
|
||||
});
|
||||
}
|
||||
}).catch(() => {
|
||||
@@ -381,7 +710,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
||||
document.getElementById("container-info-name").textContent = identity.name;
|
||||
|
||||
const icon = document.getElementById("container-info-icon");
|
||||
icon.setAttribute("data-identity-icon", identity.image);
|
||||
icon.setAttribute("data-identity-icon", identity.icon);
|
||||
icon.setAttribute("data-identity-color", identity.color);
|
||||
|
||||
// Show or not the has-tabs section.
|
||||
@@ -404,7 +733,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
||||
// Let's retrieve the list of tabs.
|
||||
return browser.runtime.sendMessage({
|
||||
method: "getTabs",
|
||||
userContextId: identity.userContextId,
|
||||
userContextId: Logic.currentUserContextId(),
|
||||
}).then(this.buildInfoTable);
|
||||
},
|
||||
|
||||
@@ -416,13 +745,14 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
||||
fragment.appendChild(tr);
|
||||
tr.classList.add("container-info-tab-row");
|
||||
tr.innerHTML = escaped`
|
||||
<td><img class="icon" src="${tab.favicon}" /></td>
|
||||
<td class="container-info-tab-title">${tab.title}</td>`;
|
||||
<td></td>
|
||||
<td class="container-info-tab-title truncate-text" title="${tab.url}" >${tab.title}</td>`;
|
||||
tr.querySelector("td").appendChild(Utils.createFavIconElement(tab.favicon));
|
||||
|
||||
// On click, we activate this tab. But only if this tab is active.
|
||||
if (tab.active) {
|
||||
tr.classList.add("clickable");
|
||||
tr.addEventListener("click", () => {
|
||||
Logic.addEnterHandler(tr, () => {
|
||||
browser.runtime.sendMessage({
|
||||
method: "showTab",
|
||||
tabId: tab.id,
|
||||
@@ -447,7 +777,7 @@ Logic.registerPanel(P_CONTAINERS_EDIT, {
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
document.querySelector("#exit-edit-mode-link").addEventListener("click", () => {
|
||||
Logic.addEnterHandler(document.querySelector("#exit-edit-mode-link"), () => {
|
||||
Logic.showPanel(P_CONTAINERS_LIST);
|
||||
});
|
||||
},
|
||||
@@ -462,30 +792,30 @@ Logic.registerPanel(P_CONTAINERS_EDIT, {
|
||||
tr.innerHTML = escaped`
|
||||
<td class="userContext-wrapper">
|
||||
<div class="userContext-icon-wrapper">
|
||||
<div class="userContext-icon"
|
||||
data-identity-icon="${identity.image}"
|
||||
<div class="usercontext-icon"
|
||||
data-identity-icon="${identity.icon}"
|
||||
data-identity-color="${identity.color}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-name"></div>
|
||||
<div class="container-name truncate-text"></div>
|
||||
</td>
|
||||
<td class="edit-container pop-button edit-container-icon">
|
||||
<img
|
||||
src="/img/container-edit.svg"
|
||||
class="pop-button-image" />
|
||||
</td>
|
||||
<td class="remove-container pop-button delete-container-icon" >
|
||||
<td class="remove-container pop-button delete-container-icon">
|
||||
<img
|
||||
class="pop-button-image"
|
||||
src="/img/container-delete.svg"
|
||||
/>
|
||||
</td>`;
|
||||
tr.querySelector(".container-name").textContent = identity.name;
|
||||
tr.querySelector(".edit-container .pop-button-image").setAttribute("title", `Edit ${identity.name} container`);
|
||||
tr.querySelector(".remove-container .pop-button-image").setAttribute("title", `Edit ${identity.name} container`);
|
||||
tr.querySelector(".edit-container").setAttribute("title", `Edit ${identity.name} container`);
|
||||
tr.querySelector(".remove-container").setAttribute("title", `Delete ${identity.name} container`);
|
||||
|
||||
|
||||
tr.addEventListener("click", e => {
|
||||
Logic.addEnterHandler(tr, e => {
|
||||
if (e.target.matches(".edit-container-icon") || e.target.parentNode.matches(".edit-container-icon")) {
|
||||
Logic.showPanel(P_CONTAINER_EDIT, identity);
|
||||
} else if (e.target.matches(".delete-container-icon") || e.target.parentNode.matches(".delete-container-icon")) {
|
||||
@@ -513,30 +843,46 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||
initialize() {
|
||||
this.initializeRadioButtons();
|
||||
|
||||
document.querySelector("#edit-container-panel-back-arrow").addEventListener("click", () => {
|
||||
Logic.showPreviousPanel();
|
||||
Logic.addEnterHandler(document.querySelector("#edit-container-panel-back-arrow"), () => {
|
||||
const formValues = new FormData(this._editForm);
|
||||
if (formValues.get("container-id") !== NEW_CONTAINER_ID) {
|
||||
this._submitForm();
|
||||
} else {
|
||||
Logic.showPreviousPanel();
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector("#edit-container-cancel-link").addEventListener("click", () => {
|
||||
Logic.addEnterHandler(document.querySelector("#edit-container-cancel-link"), () => {
|
||||
Logic.showPreviousPanel();
|
||||
});
|
||||
|
||||
this._editForm = document.getElementById("edit-container-panel-form");
|
||||
const editLink = document.querySelector("#edit-container-ok-link");
|
||||
editLink.addEventListener("click", this._submitForm.bind(this));
|
||||
editLink.addEventListener("submit", this._submitForm.bind(this));
|
||||
this._editForm.addEventListener("submit", this._submitForm.bind(this));
|
||||
Logic.addEnterHandler(editLink, () => {
|
||||
this._submitForm();
|
||||
});
|
||||
editLink.addEventListener("submit", () => {
|
||||
this._submitForm();
|
||||
});
|
||||
this._editForm.addEventListener("submit", () => {
|
||||
this._submitForm();
|
||||
});
|
||||
|
||||
|
||||
},
|
||||
|
||||
_submitForm() {
|
||||
const identity = Logic.currentIdentity();
|
||||
const formValues = new FormData(this._editForm);
|
||||
browser.runtime.sendMessage({
|
||||
method: identity.userContextId ? "updateIdentity" : "createIdentity",
|
||||
userContextId: identity.userContextId || 0,
|
||||
name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(),
|
||||
icon: formValues.get("container-icon") || DEFAULT_ICON,
|
||||
color: formValues.get("container-color") || DEFAULT_COLOR,
|
||||
return browser.runtime.sendMessage({
|
||||
method: "createOrUpdateContainer",
|
||||
message: {
|
||||
userContextId: formValues.get("container-id") || NEW_CONTAINER_ID,
|
||||
params: {
|
||||
name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(),
|
||||
icon: formValues.get("container-icon") || DEFAULT_ICON,
|
||||
color: formValues.get("container-color") || DEFAULT_COLOR,
|
||||
}
|
||||
}
|
||||
}).then(() => {
|
||||
return Logic.refreshIdentities();
|
||||
}).then(() => {
|
||||
@@ -546,6 +892,51 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||
});
|
||||
},
|
||||
|
||||
showAssignedContainers(assignments) {
|
||||
const assignmentPanel = document.getElementById("edit-sites-assigned");
|
||||
const assignmentKeys = Object.keys(assignments);
|
||||
assignmentPanel.hidden = !(assignmentKeys.length > 0);
|
||||
if (assignments) {
|
||||
const tableElement = assignmentPanel.querySelector(".assigned-sites-list");
|
||||
/* Remove previous assignment list,
|
||||
after removing one we rerender the list */
|
||||
while (tableElement.firstChild) {
|
||||
tableElement.firstChild.remove();
|
||||
}
|
||||
assignmentKeys.forEach((siteKey) => {
|
||||
const site = assignments[siteKey];
|
||||
const trElement = document.createElement("div");
|
||||
/* As we don't have the full or correct path the best we can assume is the path is HTTPS and then replace with a broken icon later if it doesn't load.
|
||||
This is pending a better solution for favicons from web extensions */
|
||||
const assumedUrl = `https://${site.hostname}`;
|
||||
trElement.innerHTML = escaped`
|
||||
<img class="icon" src="${assumedUrl}/favicon.ico">
|
||||
<div title="${site.hostname}" class="truncate-text hostname">
|
||||
${site.hostname}
|
||||
</div>
|
||||
<img
|
||||
class="pop-button-image delete-assignment"
|
||||
src="/img/container-delete.svg"
|
||||
/>`;
|
||||
const deleteButton = trElement.querySelector(".delete-assignment");
|
||||
Logic.addEnterHandler(deleteButton, () => {
|
||||
const userContextId = Logic.currentUserContextId();
|
||||
// Lets show the message to the current tab
|
||||
// TODO remove then when firefox supports arrow fn async
|
||||
Logic.currentTab().then((currentTab) => {
|
||||
Logic.setOrRemoveAssignment(currentTab.id, assumedUrl, userContextId, true);
|
||||
delete assignments[siteKey];
|
||||
this.showAssignedContainers(assignments);
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
trElement.classList.add("container-info-tab-row", "clickable");
|
||||
tableElement.appendChild(trElement);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
initializeRadioButtons() {
|
||||
const colorRadioTemplate = (containerColor) => {
|
||||
return escaped`<input type="radio" value="${containerColor}" name="container-color" id="edit-container-panel-choose-color-${containerColor}" />
|
||||
@@ -554,8 +945,9 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||
const colors = ["blue", "turquoise", "green", "yellow", "orange", "red", "pink", "purple" ];
|
||||
const colorRadioFieldset = document.getElementById("edit-container-panel-choose-color");
|
||||
colors.forEach((containerColor) => {
|
||||
const templateInstance = document.createElement("span");
|
||||
// eslint-disable-next-line no-unescaped/enforce
|
||||
const templateInstance = document.createElement("div");
|
||||
templateInstance.classList.add("radio-container");
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
templateInstance.innerHTML = colorRadioTemplate(containerColor);
|
||||
colorRadioFieldset.appendChild(templateInstance);
|
||||
});
|
||||
@@ -567,22 +959,30 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||
const icons = ["fingerprint", "briefcase", "dollar", "cart", "vacation", "gift", "food", "fruit", "pet", "tree", "chill", "circle"];
|
||||
const iconRadioFieldset = document.getElementById("edit-container-panel-choose-icon");
|
||||
icons.forEach((containerIcon) => {
|
||||
const templateInstance = document.createElement("span");
|
||||
// eslint-disable-next-line no-unescaped/enforce
|
||||
const templateInstance = document.createElement("div");
|
||||
templateInstance.classList.add("radio-container");
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
templateInstance.innerHTML = iconRadioTemplate(containerIcon);
|
||||
iconRadioFieldset.appendChild(templateInstance);
|
||||
});
|
||||
},
|
||||
|
||||
// This method is called when the panel is shown.
|
||||
prepare() {
|
||||
async prepare() {
|
||||
const identity = Logic.currentIdentity();
|
||||
|
||||
const userContextId = Logic.currentUserContextId();
|
||||
const assignments = await Logic.getAssignmentObjectByContainer(userContextId);
|
||||
this.showAssignedContainers(assignments);
|
||||
document.querySelector("#edit-container-panel .panel-footer").hidden = !!userContextId;
|
||||
|
||||
document.querySelector("#edit-container-panel-name-input").value = identity.name || "";
|
||||
document.querySelector("#edit-container-panel-usercontext-input").value = userContextId || NEW_CONTAINER_ID;
|
||||
[...document.querySelectorAll("[name='container-color']")].forEach(colorInput => {
|
||||
colorInput.checked = colorInput.value === identity.color;
|
||||
});
|
||||
[...document.querySelectorAll("[name='container-icon']")].forEach(iconInput => {
|
||||
iconInput.checked = iconInput.value === identity.image;
|
||||
iconInput.checked = iconInput.value === identity.icon;
|
||||
});
|
||||
|
||||
return Promise.resolve(null);
|
||||
@@ -598,15 +998,17 @@ Logic.registerPanel(P_CONTAINER_DELETE, {
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
document.querySelector("#delete-container-cancel-link").addEventListener("click", () => {
|
||||
Logic.addEnterHandler(document.querySelector("#delete-container-cancel-link"), () => {
|
||||
Logic.showPreviousPanel();
|
||||
});
|
||||
|
||||
document.querySelector("#delete-container-ok-link").addEventListener("click", () => {
|
||||
browser.runtime.sendMessage({
|
||||
method: "removeIdentity",
|
||||
userContextId: Logic.currentIdentity().userContextId,
|
||||
}).then(() => {
|
||||
Logic.addEnterHandler(document.querySelector("#delete-container-ok-link"), () => {
|
||||
/* This promise wont resolve if the last tab was removed from the window.
|
||||
as the message async callback stops listening, this isn't an issue for us however it might be in future
|
||||
if you want to do anything post delete do it in the background script.
|
||||
Browser console currently warns about not listening also.
|
||||
*/
|
||||
Logic.removeIdentity(Logic.userContextId(Logic.currentIdentity().cookieStoreId)).then(() => {
|
||||
return Logic.refreshIdentities();
|
||||
}).then(() => {
|
||||
Logic.showPreviousPanel();
|
||||
@@ -624,7 +1026,7 @@ Logic.registerPanel(P_CONTAINER_DELETE, {
|
||||
document.getElementById("delete-container-name").textContent = identity.name;
|
||||
|
||||
const icon = document.getElementById("delete-container-icon");
|
||||
icon.setAttribute("data-identity-icon", identity.image);
|
||||
icon.setAttribute("data-identity-icon", identity.icon);
|
||||
icon.setAttribute("data-identity-color", identity.color);
|
||||
|
||||
return Promise.resolve(null);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
const DEFAULT_FAVICON = "moz-icon://goat?size=16";
|
||||
|
||||
// TODO use export here instead of globals
|
||||
window.Utils = {
|
||||
|
||||
createFavIconElement(url) {
|
||||
const imageElement = document.createElement("img");
|
||||
imageElement.classList.add("icon", "offpage");
|
||||
imageElement.src = url;
|
||||
const loadListener = (e) => {
|
||||
e.target.classList.remove("offpage");
|
||||
e.target.removeEventListener("load", loadListener);
|
||||
e.target.removeEventListener("error", errorListener);
|
||||
};
|
||||
const errorListener = (e) => {
|
||||
e.target.src = DEFAULT_FAVICON;
|
||||
};
|
||||
imageElement.addEventListener("error", errorListener);
|
||||
imageElement.addEventListener("load", loadListener);
|
||||
return imageElement;
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Containers Experiment",
|
||||
"version": "2.2.0",
|
||||
"version": "2.4.1",
|
||||
|
||||
"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": {
|
||||
@@ -23,15 +23,25 @@
|
||||
"activeTab",
|
||||
"cookies",
|
||||
"contextMenus",
|
||||
"contextualIdentities",
|
||||
"history",
|
||||
"idle",
|
||||
"notifications",
|
||||
"storage",
|
||||
"tabs",
|
||||
"webRequestBlocking",
|
||||
"webRequest"
|
||||
],
|
||||
|
||||
"commands": {
|
||||
"_execute_browser_action": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Period",
|
||||
"mac": "MacCtrl+Period"
|
||||
},
|
||||
"description": "Open containers panel"
|
||||
}
|
||||
},
|
||||
|
||||
"browser_action": {
|
||||
"browser_style": true,
|
||||
"default_icon": {
|
||||
@@ -44,5 +54,18 @@
|
||||
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
}
|
||||
},
|
||||
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["js/content-script.js"],
|
||||
"css": ["css/content.css"],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
|
||||
"web_accessible_resources": [
|
||||
"/img/container-site-d-24.png"
|
||||
]
|
||||
}
|
||||
|
||||
+77
-24
@@ -3,55 +3,102 @@
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<title>Containers browserAction Popup</title>
|
||||
<link rel="stylesheet" href="/css/popup.css">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="hide panel onboarding onboarding-panel-1" id="onboarding-panel-1">
|
||||
<div class="hide panel onboarding onboarding-panel-1">
|
||||
<img class="onboarding-img" alt="Container Tabs Overview" src="/img/onboarding-1.png" />
|
||||
<h3 class="onboarding-title">A better way to manage all the things you do online</h3>
|
||||
<p>
|
||||
Use containers to organize tasks, manage accounts, and keep your focus where you want it.
|
||||
</p>
|
||||
<a href="#" id="onboarding-start-button" class="onboarding-button">Get Started</a>
|
||||
<a href="#" class="onboarding-button onboarding-start-button">Get Started</a>
|
||||
</div>
|
||||
|
||||
<div class="hide panel onboarding security-onboarding-panel-1">
|
||||
<img class="onboarding-img" alt="Container Tabs Overview" src="/img/onboarding-1.png" />
|
||||
<h3 class="onboarding-title">A simple and secure way to manage your online life</h3>
|
||||
<p>
|
||||
Use containers to organize tasks, manage accounts, and store sensitive data.
|
||||
</p>
|
||||
<a href="#" class="onboarding-button onboarding-start-button">Get Started</a>
|
||||
</div>
|
||||
|
||||
<div class="panel onboarding onboarding-panel-2 hide" id="onboarding-panel-2">
|
||||
<div class="panel onboarding onboarding-panel-2 hide">
|
||||
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-2.png" />
|
||||
<h3 class="onboarding-title">Put containers to work for you.</h3>
|
||||
<p>Features like color-coding and separate container tabs help you find things easily, focus your attention, and minimize distractions.</p>
|
||||
<a href="#" id="onboarding-next-button" class="onboarding-button">Next</a>
|
||||
<a href="#" class="onboarding-button onboarding-next-button">Next</a>
|
||||
</div>
|
||||
|
||||
<div class="panel onboarding onboarding-panel-3 hide" id="onboarding-panel-3">
|
||||
<div class="panel onboarding security-onboarding-panel-2 hide">
|
||||
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-2.png" />
|
||||
<h3 class="onboarding-title">Put containers to work for you.</h3>
|
||||
<p>Color-coding helps you categorize your online life, find things easily, and minimize distractions.</p>
|
||||
<a href="#" class="onboarding-button onboarding-next-button">Next</a>
|
||||
</div>
|
||||
|
||||
<div class="panel onboarding onboarding-panel-3 hide">
|
||||
<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>
|
||||
<p>Start with the containers we've created, or create your own.</p>
|
||||
<a href="#" id="onboarding-done-button" class="onboarding-button">Done</a>
|
||||
<a href="#" class="onboarding-button onboarding-almost-done-button">Next</a>
|
||||
</div>
|
||||
|
||||
<div class="panel onboarding security-onboarding-panel-3 hide">
|
||||
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-3-security.png" />
|
||||
<h3 class="onboarding-title">Set boundaries for your browsing.</h3>
|
||||
<p>Cookies are stored within a container, so you can segment sensitive data and browsing history to stay organized and to limit the impact of online trackers.</p>
|
||||
<a href="#" class="onboarding-button onboarding-almost-done-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">Next</a>
|
||||
</div>
|
||||
|
||||
<div class="panel onboarding onboarding-panel-5 hide" id="onboarding-panel-5">
|
||||
<img class="onboarding-img" alt="Long-press the New Tab button to create a new container tab." src="/img/onboarding-3.png" />
|
||||
<h3 class="onboarding-title">Container tabs when you need them.</h3>
|
||||
<p>Long-press the New Tab button to create a new container tab.</p>
|
||||
<a href="#" id="onboarding-longpress-button" class="onboarding-button">Done</a>
|
||||
</div>
|
||||
|
||||
<div class="panel container-panel hide" id="container-panel">
|
||||
<div class="panel-header">
|
||||
<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>
|
||||
<div id="current-tab">
|
||||
<h3>Current Tab</h3>
|
||||
<div id="current-page"></div>
|
||||
<label for="container-page-assigned">
|
||||
<input type="checkbox" id="container-page-assigned" />
|
||||
<span class="truncate-text">
|
||||
Always open in
|
||||
<span id="current-container"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="scrollable panel-content">
|
||||
<table>
|
||||
<tbody class="identities-list"></tbody>
|
||||
<div class="container-panel-controls">
|
||||
<a href="#" class="action-link" id="sort-containers-link" title="Sort tabs into container order">Sort Tabs</a>
|
||||
</div>
|
||||
<div class="scrollable panel-content" tabindex="-1">
|
||||
<table class="identities-list">
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel-footer edit-identities">
|
||||
<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>
|
||||
<a 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" />
|
||||
<a href="#" tabindex="0" class="add-container-link pop-button" id="container-add-link" title="Create new container">
|
||||
<img class="pop-button-image-small icon" alt="Create new container icon" src="/img/container-add.svg" />
|
||||
</a>
|
||||
</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="panel-back-arrow" id="close-container-info-panel">
|
||||
<img alt="Panel Back Arrow" src="/img/container-arrow.svg" class="back-arrow-img" />
|
||||
@@ -59,7 +106,7 @@
|
||||
<div class="column-panel-content">
|
||||
<div class="panel-header container-info-panel-header">
|
||||
<span class="usercontext-icon" id="container-info-icon"></span>
|
||||
<h3 id="container-info-name" class="panel-header-text container-name"></h3>
|
||||
<h3 id="container-info-name" class="panel-header-text container-name truncate-text"></h3>
|
||||
</div>
|
||||
<div class="select-row clickable container-info-panel-hide container-info-has-tabs" id="container-info-hideorshow">
|
||||
<img id="container-info-hideorshow-icon" alt="Hide Container icon" src="/img/container-hide.svg" class="icon container-info-panel-hideorshow-icon"/>
|
||||
@@ -85,7 +132,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@@ -97,19 +144,25 @@
|
||||
</div>
|
||||
<div class="column-panel-content">
|
||||
<form id="edit-container-panel-form">
|
||||
<input type="hidden" name="container-id" id="edit-container-panel-usercontext-input" />
|
||||
<fieldset>
|
||||
<legend>Name</legend>
|
||||
<input type="text" name="container-name" id="edit-container-panel-name-input" maxlength="25"/>
|
||||
</fieldset>
|
||||
<fieldset id="edit-container-panel-choose-color">
|
||||
<fieldset id="edit-container-panel-choose-color" class="radio-choice">
|
||||
<legend>Choose a color</legend>
|
||||
</fieldset>
|
||||
<fieldset id="edit-container-panel-choose-icon">
|
||||
<fieldset id="edit-container-panel-choose-icon" class="radio-choice">
|
||||
<legend>Choose an icon</legend>
|
||||
</fieldset>
|
||||
</form>
|
||||
<div id="edit-sites-assigned" class="scrollable" hidden>
|
||||
<h3>Sites assigned to this container</h3>
|
||||
<div class="assigned-sites-list">
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -126,12 +179,12 @@
|
||||
<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 class="panel-footer">
|
||||
<a 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 secondary footer-button cancel-button" id="delete-container-cancel-link">Cancel</a>
|
||||
<a href="#" class="button expanded primary footer-button" id="delete-container-ok-link">OK</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="js/utils.js"></script>
|
||||
<script src="js/popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user