Merge pull request #667 from mozilla/master
Deploy 2.4.0 to Test Pilot users
This commit is contained in:
commit
174c108cb7
29 changed files with 2006 additions and 422 deletions
|
@ -1 +1,3 @@
|
||||||
testpilot-metrics.js
|
testpilot-metrics.js
|
||||||
|
lib/shield/*.js
|
||||||
|
lib/testpilot/*.js
|
||||||
|
|
|
@ -9,6 +9,7 @@ module.exports = {
|
||||||
"webextensions": true
|
"webextensions": true
|
||||||
},
|
},
|
||||||
"globals": {
|
"globals": {
|
||||||
|
"Utils": true,
|
||||||
"CustomizableUI": true,
|
"CustomizableUI": true,
|
||||||
"CustomizableWidgets": true,
|
"CustomizableWidgets": true,
|
||||||
"SessionStore": true,
|
"SessionStore": true,
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,8 +1,10 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
package-lock.json
|
||||||
node_modules
|
node_modules
|
||||||
README.html
|
README.html
|
||||||
*.xpi
|
*.xpi
|
||||||
*.swp
|
*.swp
|
||||||
|
*.swo
|
||||||
.vimrc
|
.vimrc
|
||||||
.env
|
.env
|
||||||
addon.env
|
addon.env
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
"declaration-block-no-duplicate-properties": true,
|
"declaration-block-no-duplicate-properties": true,
|
||||||
"order/declaration-block-properties-alphabetical-order": true,
|
"order/declaration-block-properties-alphabetical-order": true,
|
||||||
"property-blacklist": [
|
"property-blacklist": [
|
||||||
"/height/",
|
"/(min[-]|max[-])height/",
|
||||||
"/width/",
|
"/width/",
|
||||||
"/top/",
|
"/top/",
|
||||||
"/bottom/",
|
"/bottom/",
|
||||||
|
|
3
CODE_OF_CONDUCT.md
Normal file
3
CODE_OF_CONDUCT.md
Normal file
|
@ -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.
|
35
CONTRIBUTING.md
Normal file
35
CONTRIBUTING.md
Normal file
|
@ -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.
|
62
README.md
62
README.md
|
@ -1,25 +1,22 @@
|
||||||
# Containers: Test Pilot Experiment
|
# Containers Add-on
|
||||||
|
|
||||||
[](https://testpilot.firefox.com/experiments/containers)
|
[](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?
|
* Will a general Firefox audience understand the Containers feature?
|
||||||
* Is the UI as currently implemented in Nightly clear or discoverable?
|
* Is the UI as currently implemented in Nightly clear or discoverable?
|
||||||
|
|
||||||
See [the Product Hypothesis Document for more
|
For more info, see:
|
||||||
details](https://docs.google.com/document/d/1WQdHTVXROk7dYkSFluc6_hS44tqZjIrG9I-uPyzevE8/edit?ts=5824ba12#).
|
|
||||||
|
* [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
|
## Requirements
|
||||||
|
|
||||||
* node 7+ (for jpm)
|
* node 7+ (for jpm)
|
||||||
* Firefox 51+
|
* Firefox 53+
|
||||||
|
|
||||||
|
|
||||||
## Run it
|
|
||||||
|
|
||||||
See Development
|
|
||||||
|
|
||||||
|
|
||||||
## Development
|
## 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.
|
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.
|
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.
|
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:
|
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:
|
#### Run the TxP experiment with `jpm`
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
1. `git clone git@github.com:mozilla/testpilot-containers.git`
|
1. `git clone git@github.com:mozilla/testpilot-containers.git`
|
||||||
2. `cd testpilot-containers`
|
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.
|
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
|
### Building .xpi
|
||||||
|
|
||||||
To build a local .xpi, use the plain [`jpm
|
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.
|
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
|
### Signing an .xpi
|
||||||
|
|
||||||
|
@ -75,6 +78,11 @@ add-on](https://addons.mozilla.org/en-US/developers/addon/containers-experiment/
|
||||||
### Testing
|
### Testing
|
||||||
TBD
|
TBD
|
||||||
|
|
||||||
|
|
||||||
### Distributing
|
### Distributing
|
||||||
TBD
|
TBD
|
||||||
|
|
||||||
|
### Links
|
||||||
|
|
||||||
|
- [Licence](./LICENSE.txt)
|
||||||
|
- [Contributing](./CONTRIBUTING.md)
|
||||||
|
- [Code Of Conduct](./CODE_OF_CONDUCT.md)
|
||||||
|
|
|
@ -52,55 +52,55 @@ value, or chrome url path as an alternate selector mitiages this bug.*/
|
||||||
|
|
||||||
[data-identity-icon="fingerprint"],
|
[data-identity-icon="fingerprint"],
|
||||||
[data-identity-icon="chrome://browser/skin/usercontext/personal.svg"] {
|
[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="briefcase"],
|
||||||
[data-identity-icon="chrome://browser/skin/usercontext/work.svg"] {
|
[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="dollar"],
|
||||||
[data-identity-icon="chrome://browser/skin/usercontext/banking.svg"] {
|
[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="cart"],
|
||||||
[data-identity-icon="chrome://browser/skin/usercontext/cart.svg"],
|
[data-identity-icon="chrome://browser/skin/usercontext/cart.svg"],
|
||||||
[data-identity-icon="chrome://browser/skin/usercontext/shopping.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"] {
|
[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"] {
|
[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"] {
|
[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"] {
|
[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"] {
|
[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"] {
|
[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"] {
|
[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"] {
|
[data-identity-icon="chill"] {
|
||||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#chill");
|
--identity-icon: url("/data/usercontext.svg#chill");
|
||||||
}
|
}
|
||||||
|
|
||||||
#userContext-indicator {
|
#userContext-indicator {
|
||||||
|
@ -139,7 +139,7 @@ value, or chrome url path as an alternate selector mitiages this bug.*/
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
fill: var(--identity-icon-color) !important;
|
fill: var(--identity-icon-color) !important;
|
||||||
filter: url(/img/filters.svg#fill);
|
filter: url(/img/filters.svg#fill);
|
||||||
filter: url(resource://testpilot-containers/data/filters.svg#fill);
|
filter: url(/data/filters.svg#fill);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* containers experiment */
|
/* containers experiment */
|
||||||
|
@ -200,7 +200,7 @@ special cases are addressed below */
|
||||||
}
|
}
|
||||||
|
|
||||||
#new-tab-overlay {
|
#new-tab-overlay {
|
||||||
--icon-size: 26px;
|
--icon-size: 16px;
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
font-style: -moz-use-system-font;
|
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 {
|
#new-tab-overlay .menuitem-iconic[data-usercontextid] > .menu-iconic-left > .menu-iconic-icon {
|
||||||
block-height: var(--icon-size);
|
block-size: var(--icon-size);
|
||||||
block-width: var(--icon-size);
|
inline-size: var(--icon-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
.menuitem-iconic[data-usercontextid] > .menu-iconic-left {
|
.menuitem-iconic[data-usercontextid] > .menu-iconic-left {
|
||||||
|
|
|
@ -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 when pings are sent are below, along with examples of the `payload` portion
|
||||||
of a `testpilottest` telemetry ping for each scenario.
|
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
|
* The user clicks on a container name to open a tab in that container
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
@ -76,7 +86,7 @@ of a `testpilottest` telemetry ping for each scenario.
|
||||||
"userContextId": <userContextId>,
|
"userContextId": <userContextId>,
|
||||||
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
|
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
|
||||||
"event": "open-tab",
|
"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
|
```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".
|
* Firefox automatically reloads a site into a container after the user picked "Always Open in this Container".
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
@ -260,7 +279,7 @@ local schema = {
|
||||||
|
|
||||||
### Valid data should be enforced on the server side:
|
### 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
|
All Mozilla data is kept by default for 180 days and in accordance with our
|
||||||
privacy policies.
|
privacy policies.
|
||||||
|
|
217
index.js
217
index.js
|
@ -6,9 +6,6 @@ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||||
const DEFAULT_TAB = "about:newtab";
|
const DEFAULT_TAB = "about:newtab";
|
||||||
const LOOKUP_KEY = "$ref";
|
const LOOKUP_KEY = "$ref";
|
||||||
|
|
||||||
const SHOW_MENU_TIMEOUT = 100;
|
|
||||||
const HIDE_MENU_TIMEOUT = 300;
|
|
||||||
|
|
||||||
const INCOMPATIBLE_ADDON_IDS = [
|
const INCOMPATIBLE_ADDON_IDS = [
|
||||||
"pulse@mozilla.com",
|
"pulse@mozilla.com",
|
||||||
"snoozetabs@mozilla.com",
|
"snoozetabs@mozilla.com",
|
||||||
|
@ -42,8 +39,17 @@ const IDENTITY_ICONS = [
|
||||||
{ name: "circle", image: "circle" },
|
{ name: "circle", image: "circle" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const IDENTITY_COLORS_STANDARD = [
|
||||||
|
"blue", "orange", "green", "pink",
|
||||||
|
];
|
||||||
|
|
||||||
|
const IDENTITY_ICONS_STANDARD = [
|
||||||
|
"fingerprint", "briefcase", "dollar", "cart",
|
||||||
|
];
|
||||||
|
|
||||||
const PREFS = [
|
const PREFS = [
|
||||||
[ "privacy.userContext.enabled", true ],
|
[ "privacy.userContext.enabled", true ],
|
||||||
|
[ "privacy.userContext.longPressBehavior", 2 ],
|
||||||
[ "privacy.userContext.ui.enabled", false ],
|
[ "privacy.userContext.ui.enabled", false ],
|
||||||
[ "privacy.usercontext.about_newtab_segregation.enabled", true ],
|
[ "privacy.usercontext.about_newtab_segregation.enabled", true ],
|
||||||
];
|
];
|
||||||
|
@ -60,6 +66,7 @@ const prefService = require("sdk/preferences/service");
|
||||||
const self = require("sdk/self");
|
const self = require("sdk/self");
|
||||||
const { Services } = require("resource://gre/modules/Services.jsm");
|
const { Services } = require("resource://gre/modules/Services.jsm");
|
||||||
const ss = require("sdk/simple-storage");
|
const ss = require("sdk/simple-storage");
|
||||||
|
const { study } = require("./study");
|
||||||
const { Style } = require("sdk/stylesheet/style");
|
const { Style } = require("sdk/stylesheet/style");
|
||||||
const tabs = require("sdk/tabs");
|
const tabs = require("sdk/tabs");
|
||||||
const tabsUtils = require("sdk/tabs/utils");
|
const tabsUtils = require("sdk/tabs/utils");
|
||||||
|
@ -213,6 +220,7 @@ const ContainerService = {
|
||||||
"getPreference",
|
"getPreference",
|
||||||
"sendTelemetryPayload",
|
"sendTelemetryPayload",
|
||||||
"getTheme",
|
"getTheme",
|
||||||
|
"getShieldStudyVariation",
|
||||||
"refreshNeeded",
|
"refreshNeeded",
|
||||||
"forgetIdentityAndRefresh",
|
"forgetIdentityAndRefresh",
|
||||||
"checkIncompatibleAddons"
|
"checkIncompatibleAddons"
|
||||||
|
@ -236,18 +244,15 @@ const ContainerService = {
|
||||||
}
|
}
|
||||||
|
|
||||||
tabs.on("open", tab => {
|
tabs.on("open", tab => {
|
||||||
this._hideAllPanels();
|
|
||||||
this._restyleTab(tab);
|
this._restyleTab(tab);
|
||||||
this._remapTab(tab);
|
this._remapTab(tab);
|
||||||
});
|
});
|
||||||
|
|
||||||
tabs.on("close", tab => {
|
tabs.on("close", tab => {
|
||||||
this._hideAllPanels();
|
|
||||||
this._remapTab(tab);
|
this._remapTab(tab);
|
||||||
});
|
});
|
||||||
|
|
||||||
tabs.on("activate", tab => {
|
tabs.on("activate", tab => {
|
||||||
this._hideAllPanels();
|
|
||||||
this._restyleActiveTab(tab).catch(() => {});
|
this._restyleActiveTab(tab).catch(() => {});
|
||||||
this._configureActiveWindows();
|
this._configureActiveWindows();
|
||||||
this._remapTab(tab);
|
this._remapTab(tab);
|
||||||
|
@ -320,6 +325,11 @@ const ContainerService = {
|
||||||
// End-Of-Hack
|
// End-Of-Hack
|
||||||
|
|
||||||
Services.obs.addObserver(this, "lightweight-theme-changed", false);
|
Services.obs.addObserver(this, "lightweight-theme-changed", false);
|
||||||
|
|
||||||
|
if (self.id === "@shield-study-containers") {
|
||||||
|
study.startup(reason);
|
||||||
|
this.shieldStudyVariation = study.variation;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
registerBackgroundConnection(api) {
|
registerBackgroundConnection(api) {
|
||||||
|
@ -362,6 +372,10 @@ const ContainerService = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getShieldStudyVariation() {
|
||||||
|
return this.shieldStudyVariation;
|
||||||
|
},
|
||||||
|
|
||||||
// utility methods
|
// utility methods
|
||||||
|
|
||||||
_containerTabCount(userContextId) {
|
_containerTabCount(userContextId) {
|
||||||
|
@ -922,12 +936,6 @@ const ContainerService = {
|
||||||
return this._configureWindows();
|
return this._configureWindows();
|
||||||
},
|
},
|
||||||
|
|
||||||
_hideAllPanels() {
|
|
||||||
for (let windowObject of this._windowMap.values()) { // eslint-disable-line prefer-const
|
|
||||||
windowObject.hidePanel();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_restyleActiveTab(tab) {
|
_restyleActiveTab(tab) {
|
||||||
if (!tab) {
|
if (!tab) {
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
|
@ -1086,22 +1094,50 @@ ContainerWindow.prototype = {
|
||||||
_timeoutStore: new Map(),
|
_timeoutStore: new Map(),
|
||||||
_elementCache: new Map(),
|
_elementCache: new Map(),
|
||||||
_tooltipCache: new Map(),
|
_tooltipCache: new Map(),
|
||||||
_plusButton: null,
|
|
||||||
_overflowPlusButton: null,
|
|
||||||
_tabsElement: null,
|
_tabsElement: null,
|
||||||
|
|
||||||
_init(window) {
|
_init(window) {
|
||||||
this._window = window;
|
this._window = window;
|
||||||
this._tabsElement = this._window.document.getElementById("tabbrowser-tabs");
|
this._tabsElement = this._window.document.getElementById("tabbrowser-tabs");
|
||||||
|
this._style = Style({ uri: self.data.url("usercontext.css") });
|
||||||
this._plusButton = this._window.document.getAnonymousElementByAttribute(this._tabsElement, "anonid", "tabs-newtab-button");
|
this._plusButton = this._window.document.getAnonymousElementByAttribute(this._tabsElement, "anonid", "tabs-newtab-button");
|
||||||
this._overflowPlusButton = this._window.document.getElementById("new-tab-button");
|
this._overflowPlusButton = this._window.document.getElementById("new-tab-button");
|
||||||
this._style = Style({ uri: self.data.url("usercontext.css") });
|
|
||||||
|
// Only hack the normal plus button as the alltabs is done elsewhere
|
||||||
|
this.attachMenuEvent("plus-button", this._plusButton);
|
||||||
|
|
||||||
attachTo(this._style, this._window);
|
attachTo(this._style, this._window);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
attachMenuEvent(source, button) {
|
||||||
|
const popup = button.querySelector(".new-tab-popup");
|
||||||
|
popup.addEventListener("popupshown", () => {
|
||||||
|
ContainerService.sendTelemetryPayload({
|
||||||
|
"event": "show-plus-button-menu",
|
||||||
|
"eventSource": source
|
||||||
|
});
|
||||||
|
popup.querySelector("menuseparator").remove();
|
||||||
|
const popupMenuItems = [...popup.querySelectorAll("menuitem")];
|
||||||
|
popupMenuItems.forEach((item) => {
|
||||||
|
const userContextId = item.getAttribute("data-usercontextid");
|
||||||
|
if (!userContextId) {
|
||||||
|
item.remove();
|
||||||
|
}
|
||||||
|
item.setAttribute("command", "");
|
||||||
|
item.addEventListener("command", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
ContainerService.openTab({
|
||||||
|
userContextId: userContextId,
|
||||||
|
source: source
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
configure() {
|
configure() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
this._configurePlusButtonMenu(),
|
|
||||||
this._configureActiveTab(),
|
this._configureActiveTab(),
|
||||||
this._configureFileMenu(),
|
this._configureFileMenu(),
|
||||||
this._configureAllTabsMenu(),
|
this._configureAllTabsMenu(),
|
||||||
|
@ -1114,112 +1150,6 @@ ContainerWindow.prototype = {
|
||||||
return this._configureContextMenu();
|
return this._configureContextMenu();
|
||||||
},
|
},
|
||||||
|
|
||||||
handleEvent(e) {
|
|
||||||
let el = e.target;
|
|
||||||
switch (e.type) {
|
|
||||||
case "mouseover":
|
|
||||||
this._createTimeout("show", () => {
|
|
||||||
this.showPopup(el);
|
|
||||||
}, SHOW_MENU_TIMEOUT);
|
|
||||||
break;
|
|
||||||
case "click":
|
|
||||||
this.hidePanel();
|
|
||||||
break;
|
|
||||||
case "mouseout":
|
|
||||||
while(el) {
|
|
||||||
if (el === this._panelElement ||
|
|
||||||
el === this._plusButton ||
|
|
||||||
el === this._overflowPlusButton) {
|
|
||||||
// Clear show timeout so we don't hide and reshow
|
|
||||||
this._cleanTimeout("show");
|
|
||||||
this._createTimeout("hidden", () => {
|
|
||||||
this.hidePanel();
|
|
||||||
}, HIDE_MENU_TIMEOUT);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
el = el.parentElement;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
showPopup(buttonElement) {
|
|
||||||
this._cleanAllTimeouts();
|
|
||||||
this._panelElement.openPopup(buttonElement);
|
|
||||||
},
|
|
||||||
|
|
||||||
_configurePlusButtonMenuElement(buttonElement) {
|
|
||||||
if (buttonElement) {
|
|
||||||
// Let's remove the tooltip because it can go over our panel.
|
|
||||||
this._tooltipCache.set(buttonElement, buttonElement.getAttribute("tooltip"));
|
|
||||||
buttonElement.setAttribute("tooltip", "");
|
|
||||||
this._disableElement(buttonElement);
|
|
||||||
|
|
||||||
buttonElement.addEventListener("mouseover", this);
|
|
||||||
buttonElement.addEventListener("click", this);
|
|
||||||
buttonElement.addEventListener("mouseout", this);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async _configurePlusButtonMenu() {
|
|
||||||
const mainPopupSetElement = this._window.document.getElementById("mainPopupSet");
|
|
||||||
|
|
||||||
// Let's remove all the previous panels.
|
|
||||||
if (this._panelElement) {
|
|
||||||
this._panelElement.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._panelElement = this._window.document.createElementNS(XUL_NS, "panel");
|
|
||||||
this._panelElement.setAttribute("id", "new-tab-overlay");
|
|
||||||
this._panelElement.setAttribute("position", "bottomcenter topleft");
|
|
||||||
this._panelElement.setAttribute("side", "top");
|
|
||||||
this._panelElement.setAttribute("flip", "side");
|
|
||||||
this._panelElement.setAttribute("type", "arrow");
|
|
||||||
this._panelElement.setAttribute("animate", "open");
|
|
||||||
this._panelElement.setAttribute("consumeoutsideclicks", "never");
|
|
||||||
mainPopupSetElement.appendChild(this._panelElement);
|
|
||||||
|
|
||||||
this._configurePlusButtonMenuElement(this._plusButton);
|
|
||||||
this._configurePlusButtonMenuElement(this._overflowPlusButton);
|
|
||||||
|
|
||||||
this._panelElement.addEventListener("mouseout", this);
|
|
||||||
|
|
||||||
this._panelElement.addEventListener("mouseover", () => {
|
|
||||||
this._cleanAllTimeouts();
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const identities = await ContainerService.queryIdentities();
|
|
||||||
identities.forEach(identity => {
|
|
||||||
const menuItemElement = this._window.document.createElementNS(XUL_NS, "menuitem");
|
|
||||||
this._panelElement.appendChild(menuItemElement);
|
|
||||||
menuItemElement.className = "menuitem-iconic";
|
|
||||||
menuItemElement.setAttribute("label", identity.name);
|
|
||||||
menuItemElement.setAttribute("data-usercontextid", identity.userContextId);
|
|
||||||
menuItemElement.setAttribute("data-identity-icon", identity.icon);
|
|
||||||
menuItemElement.setAttribute("data-identity-color", identity.color);
|
|
||||||
|
|
||||||
menuItemElement.addEventListener("command", (e) => {
|
|
||||||
ContainerService.openTab({
|
|
||||||
userContextId: identity.userContextId,
|
|
||||||
source: "tab-bar"
|
|
||||||
});
|
|
||||||
e.stopPropagation();
|
|
||||||
});
|
|
||||||
|
|
||||||
menuItemElement.addEventListener("mouseover", () => {
|
|
||||||
this._cleanAllTimeouts();
|
|
||||||
});
|
|
||||||
|
|
||||||
menuItemElement.addEventListener("mouseout", this);
|
|
||||||
|
|
||||||
this._panelElement.appendChild(menuItemElement);
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
this.hidePanel();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_configureTabStyle() {
|
_configureTabStyle() {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for (let tab of modelFor(this._window).tabs) { // eslint-disable-line prefer-const
|
for (let tab of modelFor(this._window).tabs) { // eslint-disable-line prefer-const
|
||||||
|
@ -1389,19 +1319,15 @@ ContainerWindow.prototype = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
hidePanel() {
|
|
||||||
this._cleanAllTimeouts();
|
|
||||||
this._panelElement.hidePopup();
|
|
||||||
},
|
|
||||||
|
|
||||||
shutdown() {
|
shutdown() {
|
||||||
// CSS must be removed.
|
// CSS must be removed.
|
||||||
detachFrom(this._style, this._window);
|
detachFrom(this._style, this._window);
|
||||||
|
|
||||||
this._shutdownPlusButtonMenu();
|
|
||||||
this._shutdownFileMenu();
|
this._shutdownFileMenu();
|
||||||
this._shutdownAllTabsMenu();
|
this._shutdownAllTabsMenu();
|
||||||
this._shutdownContextMenu();
|
this._shutdownContextMenu();
|
||||||
|
|
||||||
|
this._shutdownContainers();
|
||||||
},
|
},
|
||||||
|
|
||||||
_shutDownPlusButtonMenuElement(buttonElement) {
|
_shutDownPlusButtonMenuElement(buttonElement) {
|
||||||
|
@ -1415,11 +1341,6 @@ ContainerWindow.prototype = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_shutdownPlusButtonMenu() {
|
|
||||||
this._shutDownPlusButtonMenuElement(this._plusButton);
|
|
||||||
this._shutDownPlusButtonMenuElement(this._overflowPlusButton);
|
|
||||||
},
|
|
||||||
|
|
||||||
_shutdownFileMenu() {
|
_shutdownFileMenu() {
|
||||||
this._shutdownMenu("menu_newUserContext");
|
this._shutdownMenu("menu_newUserContext");
|
||||||
},
|
},
|
||||||
|
@ -1468,6 +1389,36 @@ ContainerWindow.prototype = {
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_shutdownContainers() {
|
||||||
|
ContextualIdentityProxy.getIdentities().forEach(identity => {
|
||||||
|
if (IDENTITY_ICONS_STANDARD.indexOf(identity.icon) !== -1 &&
|
||||||
|
IDENTITY_COLORS_STANDARD.indexOf(identity.color) !== -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IDENTITY_ICONS_STANDARD.indexOf(identity.icon) === -1) {
|
||||||
|
if (identity.userContextId <= IDENTITY_ICONS_STANDARD.length) {
|
||||||
|
identity.icon = IDENTITY_ICONS_STANDARD[identity.userContextId - 1];
|
||||||
|
} else {
|
||||||
|
identity.icon = IDENTITY_ICONS_STANDARD[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IDENTITY_COLORS_STANDARD.indexOf(identity.color) === -1) {
|
||||||
|
if (identity.userContextId <= IDENTITY_COLORS_STANDARD.length) {
|
||||||
|
identity.color = IDENTITY_COLORS_STANDARD[identity.userContextId - 1];
|
||||||
|
} else {
|
||||||
|
identity.color = IDENTITY_COLORS_STANDARD[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextualIdentityService.update(identity.userContextId,
|
||||||
|
identity.name,
|
||||||
|
identity.icon,
|
||||||
|
identity.color);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// uninstall/install events ---------------------------------------------------
|
// uninstall/install events ---------------------------------------------------
|
||||||
|
|
55
lib/shield/event-target.js
Normal file
55
lib/shield/event-target.js
Normal file
|
@ -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;
|
428
lib/shield/index.js
Normal file
428
lib/shield/index.js
Normal file
|
@ -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
|
||||||
|
}
|
95
lib/testpilot/experiment.js
Normal file
95
lib/testpilot/experiment.js
Normal file
|
@ -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;
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "testpilot-containers",
|
"name": "testpilot-containers",
|
||||||
"title": "Containers Experiment",
|
"title": "Containers Experiment",
|
||||||
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
|
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
|
||||||
"version": "2.3.0",
|
"version": "2.4.0",
|
||||||
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
|
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/mozilla/testpilot-containers/issues"
|
"url": "https://github.com/mozilla/testpilot-containers/issues"
|
||||||
|
@ -16,7 +16,9 @@
|
||||||
"eslint-plugin-promise": "^3.4.0",
|
"eslint-plugin-promise": "^3.4.0",
|
||||||
"htmllint-cli": "^0.0.5",
|
"htmllint-cli": "^0.0.5",
|
||||||
"jpm": "^1.2.2",
|
"jpm": "^1.2.2",
|
||||||
|
"json": "^9.0.6",
|
||||||
"npm-run-all": "^4.0.0",
|
"npm-run-all": "^4.0.0",
|
||||||
|
"shield-studies-addon-utils": "^2.0.0",
|
||||||
"stylelint": "^7.9.0",
|
"stylelint": "^7.9.0",
|
||||||
"stylelint-config-standard": "^16.0.0",
|
"stylelint-config-standard": "^16.0.0",
|
||||||
"stylelint-order": "^0.3.0",
|
"stylelint-order": "^0.3.0",
|
||||||
|
@ -41,6 +43,7 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm test && jpm xpi",
|
"build": "npm test && jpm xpi",
|
||||||
|
"build-shield": "npm test && npm run package-shield",
|
||||||
"deploy": "deploy-txp",
|
"deploy": "deploy-txp",
|
||||||
"lint": "npm-run-all lint:*",
|
"lint": "npm-run-all lint:*",
|
||||||
"lint:addon": "addons-linter webextension --self-hosted",
|
"lint:addon": "addons-linter webextension --self-hosted",
|
||||||
|
@ -48,6 +51,7 @@
|
||||||
"lint:html": "htmllint webextension/*.html",
|
"lint:html": "htmllint webextension/*.html",
|
||||||
"lint:js": "eslint .",
|
"lint:js": "eslint .",
|
||||||
"package": "npm run build && mv testpilot-containers.xpi addon.xpi",
|
"package": "npm run build && mv testpilot-containers.xpi addon.xpi",
|
||||||
|
"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"
|
"test": "npm run lint"
|
||||||
},
|
},
|
||||||
"updateURL": "https://testpilot.firefox.com/files/@testpilot-containers/updates.json"
|
"updateURL": "https://testpilot.firefox.com/files/@testpilot-containers/updates.json"
|
||||||
|
|
40
study.js
Normal file
40
study.js
Normal file
|
@ -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
|
// 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
|
// 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/.
|
// 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
|
* Class that represents a metrics event broker. Events are sent to Google
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const MAJOR_VERSIONS = ["2.3.0"];
|
const MAJOR_VERSIONS = ["2.3.0", "2.4.0"];
|
||||||
const LOOKUP_KEY = "$ref";
|
const LOOKUP_KEY = "$ref";
|
||||||
|
|
||||||
const assignManager = {
|
const assignManager = {
|
||||||
|
@ -6,6 +6,7 @@ const assignManager = {
|
||||||
MENU_REMOVE_ID: "remove-open-in-this-container",
|
MENU_REMOVE_ID: "remove-open-in-this-container",
|
||||||
storageArea: {
|
storageArea: {
|
||||||
area: browser.storage.local,
|
area: browser.storage.local,
|
||||||
|
exemptedTabs: {},
|
||||||
|
|
||||||
getSiteStoreKey(pageUrl) {
|
getSiteStoreKey(pageUrl) {
|
||||||
const url = new window.URL(pageUrl);
|
const url = new window.URL(pageUrl);
|
||||||
|
@ -13,6 +14,27 @@ const assignManager = {
|
||||||
return `${storagePrefix}${url.hostname}`;
|
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) {
|
get(pageUrl) {
|
||||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -27,8 +49,13 @@ const assignManager = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
set(pageUrl, data) {
|
set(pageUrl, data, exemptedTabIds) {
|
||||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||||
|
if (exemptedTabIds) {
|
||||||
|
exemptedTabIds.forEach((tabId) => {
|
||||||
|
this.setExempted(pageUrl, tabId);
|
||||||
|
});
|
||||||
|
}
|
||||||
return this.area.set({
|
return this.area.set({
|
||||||
[siteStoreKey]: data
|
[siteStoreKey]: data
|
||||||
});
|
});
|
||||||
|
@ -36,22 +63,30 @@ const assignManager = {
|
||||||
|
|
||||||
remove(pageUrl) {
|
remove(pageUrl) {
|
||||||
const siteStoreKey = this.getSiteStoreKey(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]);
|
return this.area.remove([siteStoreKey]);
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteContainer(userContextId) {
|
async deleteContainer(userContextId) {
|
||||||
const removeKeys = [];
|
const sitesByContainer = await this.getByContainer(userContextId);
|
||||||
this.area.get().then((siteConfigs) => {
|
this.area.remove(Object.keys(sitesByContainer));
|
||||||
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)) {
|
async getByContainer(userContextId) {
|
||||||
removeKeys.push(key);
|
const sites = {};
|
||||||
}
|
const siteConfigs = await this.area.get();
|
||||||
});
|
Object.keys(siteConfigs).forEach((key) => {
|
||||||
this.area.remove(removeKeys);
|
// For some reason this is stored as string... lets check them both as that
|
||||||
}).catch((e) => {
|
if (String(siteConfigs[key].userContextId) === String(userContextId)) {
|
||||||
throw e;
|
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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -70,39 +105,16 @@ const assignManager = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 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() {
|
init() {
|
||||||
browser.contextMenus.onClicked.addListener((info, tab) => {
|
browser.contextMenus.onClicked.addListener((info, tab) => {
|
||||||
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
this._onClickedHandler(info, 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")
|
|
||||||
});
|
|
||||||
backgroundLogic.sendTelemetryPayload({
|
|
||||||
event: `${actionName}-container-assignment`,
|
|
||||||
userContextId: userContextId,
|
|
||||||
});
|
|
||||||
this.calculateContextMenu(tab);
|
|
||||||
}).catch((e) => {
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Before a request is handled by the browser we decide if we should route through a different container
|
// Before a request is handled by the browser we decide if we should route through a different container
|
||||||
|
@ -110,6 +122,7 @@ const assignManager = {
|
||||||
if (options.frameId !== 0 || options.tabId === -1) {
|
if (options.frameId !== 0 || options.tabId === -1) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
this.removeContextMenu();
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
browser.tabs.get(options.tabId),
|
browser.tabs.get(options.tabId),
|
||||||
this.storageArea.get(options.url)
|
this.storageArea.get(options.url)
|
||||||
|
@ -117,11 +130,12 @@ const assignManager = {
|
||||||
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
||||||
if (!siteSettings
|
if (!siteSettings
|
||||||
|| userContextId === siteSettings.userContextId
|
|| userContextId === siteSettings.userContextId
|
||||||
|| tab.incognito) {
|
|| tab.incognito
|
||||||
|
|| this.storageArea.isExempted(options.url, tab.id)) {
|
||||||
return {};
|
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);
|
this.calculateContextMenu(tab);
|
||||||
|
|
||||||
/* Removal of existing tabs:
|
/* Removal of existing tabs:
|
||||||
|
@ -149,6 +163,21 @@ const assignManager = {
|
||||||
},{urls: ["<all_urls>"], types: ["main_frame"]}, ["blocking"]);
|
},{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) {
|
deleteContainer(userContextId) {
|
||||||
this.storageArea.deleteContainer(userContextId);
|
this.storageArea.deleteContainer(userContextId);
|
||||||
|
@ -171,49 +200,112 @@ const assignManager = {
|
||||||
// Ensure we are not in incognito mode
|
// Ensure we are not in incognito mode
|
||||||
const url = new URL(tab.url);
|
const url = new URL(tab.url);
|
||||||
if (url.protocol === "about:"
|
if (url.protocol === "about:"
|
||||||
|
|| url.protocol === "moz-extension:"
|
||||||
|| tab.incognito) {
|
|| tab.incognito) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
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
|
// 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
|
// you get the wrong menu display because of async
|
||||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1215376#c16
|
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1215376#c16
|
||||||
// We also can't change for always private mode
|
// We also can't change for always private mode
|
||||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1352102
|
// 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_ASSIGN_ID);
|
||||||
browser.contextMenus.remove(this.MENU_REMOVE_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");
|
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 the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there
|
||||||
if (neverAsk) {
|
if (neverAsk) {
|
||||||
browser.tabs.create({url, cookieStoreId: backgroundLogic.cookieStoreId(userContextId), index});
|
browser.tabs.create({url, cookieStoreId, index});
|
||||||
backgroundLogic.sendTelemetryPayload({
|
backgroundLogic.sendTelemetryPayload({
|
||||||
event: "auto-reload-page-in-container",
|
event: "auto-reload-page-in-container",
|
||||||
userContextId: userContextId,
|
userContextId: userContextId,
|
||||||
|
@ -223,8 +315,17 @@ const assignManager = {
|
||||||
event: "prompt-to-reload-page-in-container",
|
event: "prompt-to-reload-page-in-container",
|
||||||
userContextId: userContextId,
|
userContextId: userContextId,
|
||||||
});
|
});
|
||||||
const confirmUrl = `${loadPage}?url=${url}`;
|
let confirmUrl = `${loadPage}?url=${encodeURIComponent(url)}&cookieStoreId=${cookieStoreId}`;
|
||||||
browser.tabs.create({url: confirmUrl, cookieStoreId: backgroundLogic.cookieStoreId(userContextId), index}).then(() => {
|
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
|
// We don't want to sync this URL ever nor clutter the users history
|
||||||
browser.history.deleteUrl({url: confirmUrl});
|
browser.history.deleteUrl({url: confirmUrl});
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
|
@ -271,7 +372,7 @@ const backgroundLogic = {
|
||||||
|
|
||||||
createOrUpdateContainer(options) {
|
createOrUpdateContainer(options) {
|
||||||
let donePromise;
|
let donePromise;
|
||||||
if (options.userContextId) {
|
if (options.userContextId !== "new") {
|
||||||
donePromise = browser.contextualIdentities.update(
|
donePromise = browser.contextualIdentities.update(
|
||||||
this.cookieStoreId(options.userContextId),
|
this.cookieStoreId(options.userContextId),
|
||||||
options.params
|
options.params
|
||||||
|
@ -354,7 +455,7 @@ const messageHandler = {
|
||||||
LAST_CREATED_TAB_TIMER: 2000,
|
LAST_CREATED_TAB_TIMER: 2000,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
// Handles messages from webextension/js/popup.js
|
// Handles messages from webextension code
|
||||||
browser.runtime.onMessage.addListener((m) => {
|
browser.runtime.onMessage.addListener((m) => {
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
|
@ -372,11 +473,29 @@ const messageHandler = {
|
||||||
case "neverAsk":
|
case "neverAsk":
|
||||||
assignManager._neverAsk(m);
|
assignManager._neverAsk(m);
|
||||||
break;
|
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;
|
return response;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handles messages from index.js
|
// Handles messages from sdk code
|
||||||
const port = browser.runtime.connect();
|
const port = browser.runtime.connect();
|
||||||
port.onMessage.addListener(m => {
|
port.onMessage.addListener(m => {
|
||||||
switch (m.type) {
|
switch (m.type) {
|
||||||
|
@ -408,6 +527,7 @@ const messageHandler = {
|
||||||
});
|
});
|
||||||
|
|
||||||
browser.tabs.onActivated.addListener((info) => {
|
browser.tabs.onActivated.addListener((info) => {
|
||||||
|
assignManager.removeContextMenu();
|
||||||
browser.tabs.get(info.tabId).then((tab) => {
|
browser.tabs.get(info.tabId).then((tab) => {
|
||||||
tabPageCounter.initTabCounter(tab);
|
tabPageCounter.initTabCounter(tab);
|
||||||
assignManager.calculateContextMenu(tab);
|
assignManager.calculateContextMenu(tab);
|
||||||
|
@ -417,6 +537,12 @@ const messageHandler = {
|
||||||
});
|
});
|
||||||
|
|
||||||
browser.windows.onFocusChanged.addListener((windowId) => {
|
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) => {
|
browser.tabs.query({active: true, windowId}).then((tabs) => {
|
||||||
if (tabs && tabs[0]) {
|
if (tabs && tabs[0]) {
|
||||||
tabPageCounter.initTabCounter(tabs[0]);
|
tabPageCounter.initTabCounter(tabs[0]);
|
||||||
|
@ -445,6 +571,7 @@ const messageHandler = {
|
||||||
if (details.frameId !== 0 || details.tabId === -1) {
|
if (details.frameId !== 0 || details.tabId === -1) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
assignManager.removeContextMenu();
|
||||||
|
|
||||||
browser.tabs.get(details.tabId).then((tab) => {
|
browser.tabs.get(details.tabId).then((tab) => {
|
||||||
tabPageCounter.incrementTabCount(tab);
|
tabPageCounter.incrementTabCount(tab);
|
||||||
|
|
|
@ -8,26 +8,29 @@
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
<div class="title">
|
<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>
|
</div>
|
||||||
<form id="redirect-form">
|
<form id="redirect-form">
|
||||||
<p>
|
<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>
|
</p>
|
||||||
<div id="redirect-url"></div>
|
<div id="redirect-url"></div>
|
||||||
<p>
|
<p>Would you still like to open in this current container?</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>
|
|
||||||
<br />
|
<br />
|
||||||
<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 />
|
<br />
|
||||||
<div class="button-container">
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<script src="js/utils.js"></script>
|
||||||
<script src="js/confirm-page.js"></script>
|
<script src="js/confirm-page.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -4,11 +4,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
background: url(/img/onboarding-1.png) no-repeat;
|
background: url(/img/onboarding-4.png) no-repeat;
|
||||||
background-position: -10px -15px;
|
background-position: -10px -15px;
|
||||||
background-size: 285px;
|
background-size: 300px;
|
||||||
margin-inline-start: -285px;
|
margin-inline-start: -350px;
|
||||||
padding-inline-start: 285px;
|
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) {
|
@media only screen and (max-width: 1300px) {
|
||||||
|
@ -36,6 +46,33 @@ html {
|
||||||
word-break: break-all;
|
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 {
|
dfn {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-container > button {
|
||||||
|
min-inline-size: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-label {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
27
webextension/css/content.css
Normal file
27
webextension/css/content.css
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -1,11 +1,55 @@
|
||||||
/* General Rules and Resets */
|
/* General Rules and Resets */
|
||||||
body {
|
* {
|
||||||
inline-size: 300px;
|
font-size: inherit;
|
||||||
max-inline-size: 300px;
|
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 {
|
html {
|
||||||
box-sizing: border-box;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
: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 +58,13 @@ html {
|
||||||
box-sizing: inherit;
|
box-sizing: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin-block-end: 0;
|
||||||
|
margin-block-start: 0;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
margin-inline-start: 0;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
border: 0;
|
border: 0;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
|
@ -30,11 +81,27 @@ table {
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollable {
|
.scrollable {
|
||||||
|
border-block-start: 1px solid #f1f1f1;
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
max-block-size: 400px;
|
max-block-size: 400px;
|
||||||
overflow: auto;
|
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 */
|
/* Color and icon helpers */
|
||||||
[data-identity-color="blue"] {
|
[data-identity-color="blue"] {
|
||||||
--identity-tab-color: #37adff;
|
--identity-tab-color: #37adff;
|
||||||
|
@ -51,6 +118,11 @@ table {
|
||||||
--identity-icon-color: #51cd00;
|
--identity-icon-color: #51cd00;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-identity-color="grey"] {
|
||||||
|
/* Only used for the edit panel */
|
||||||
|
--identity-icon-color: #616161;
|
||||||
|
}
|
||||||
|
|
||||||
[data-identity-color="yellow"] {
|
[data-identity-color="yellow"] {
|
||||||
--identity-tab-color: #ffcb00;
|
--identity-tab-color: #ffcb00;
|
||||||
--identity-icon-color: #ffcb00;
|
--identity-icon-color: #ffcb00;
|
||||||
|
@ -124,7 +196,16 @@ table {
|
||||||
--identity-icon: url("/img/usercontext.svg#chill");
|
--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 */
|
/* Buttons */
|
||||||
|
.button {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
.button.primary {
|
.button.primary {
|
||||||
background-color: #0996f8;
|
background-color: #0996f8;
|
||||||
color: white;
|
color: white;
|
||||||
|
@ -140,6 +221,18 @@ table {
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
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 */
|
/* Panels keep everything togethert */
|
||||||
.panel {
|
.panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -183,7 +276,7 @@ table {
|
||||||
.column-panel-content .button,
|
.column-panel-content .button,
|
||||||
.panel-footer .button {
|
.panel-footer .button {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
block-size: 54px;
|
block-size: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -223,7 +316,7 @@ table {
|
||||||
|
|
||||||
.onboarding-title {
|
.onboarding-title {
|
||||||
color: #43484e;
|
color: #43484e;
|
||||||
font-size: 16px;
|
font-size: var(--font-size-heading);
|
||||||
margin-block-end: 0;
|
margin-block-end: 0;
|
||||||
margin-block-start: 0;
|
margin-block-start: 0;
|
||||||
margin-inline-end: 0;
|
margin-inline-end: 0;
|
||||||
|
@ -232,7 +325,7 @@ table {
|
||||||
}
|
}
|
||||||
|
|
||||||
.onboarding p {
|
.onboarding p {
|
||||||
color: #4a4a4a;
|
color: var(--text-normal-color);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin-block-end: 16px;
|
margin-block-end: 16px;
|
||||||
max-inline-size: 84%;
|
max-inline-size: 84%;
|
||||||
|
@ -261,9 +354,10 @@ table {
|
||||||
manage things like container crud */
|
manage things like container crud */
|
||||||
.pop-button {
|
.pop-button {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
block-size: 48px;
|
block-size: var(--icon-button-size);
|
||||||
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 0 0 48px;
|
flex: 0 0 var(--icon-button-size);
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -288,6 +382,10 @@ manage things like container crud */
|
||||||
.pop-button-image {
|
.pop-button-image {
|
||||||
block-size: 20px;
|
block-size: 20px;
|
||||||
flex: 0 0 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 {
|
.pop-button-image-small {
|
||||||
|
@ -299,20 +397,23 @@ manage things like container crud */
|
||||||
.panel-header {
|
.panel-header {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
block-size: 48px;
|
block-size: 48px;
|
||||||
border-block-end: 1px solid #ebebeb;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-header .usercontext-icon {
|
||||||
|
inline-size: var(--icon-button-size);
|
||||||
|
}
|
||||||
|
|
||||||
.column-panel-content .panel-header {
|
.column-panel-content .panel-header {
|
||||||
flex: 0 0 48px;
|
flex: 0 0 48px;
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-header-text {
|
.panel-header-text {
|
||||||
color: #4a4a4a;
|
color: var(--text-normal-color);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 16px;
|
font-size: var(--font-size-heading);
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin-block-end: 0;
|
margin-block-end: 0;
|
||||||
margin-block-start: 0;
|
margin-block-start: 0;
|
||||||
|
@ -324,6 +425,47 @@ manage things like container crud */
|
||||||
padding-inline-start: 16px;
|
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 {
|
span ~ .panel-header-text {
|
||||||
padding-block-end: 0;
|
padding-block-end: 0;
|
||||||
padding-block-start: 0;
|
padding-block-start: 0;
|
||||||
|
@ -331,11 +473,92 @@ span ~ .panel-header-text {
|
||||||
padding-inline-start: 0;
|
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 */
|
/* Rows used when iterating over panels */
|
||||||
.container-panel-row {
|
.container-panel-row {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #fefefe !important;
|
background-color: #fefefe !important;
|
||||||
block-size: 48px;
|
|
||||||
border-block-end: 1px solid #f1f1f1;
|
border-block-end: 1px solid #f1f1f1;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -343,12 +566,10 @@ span ~ .panel-header-text {
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-panel-row .container-name {
|
.container-panel-row .container-name {
|
||||||
|
flex: 1;
|
||||||
max-inline-size: 160px;
|
max-inline-size: 160px;
|
||||||
overflow: hidden;
|
|
||||||
padding-inline-end: 4px;
|
padding-inline-end: 4px;
|
||||||
padding-inline-start: 4px;
|
padding-inline-start: 4px;
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-containers-panel .userContext-wrapper {
|
.edit-containers-panel .userContext-wrapper {
|
||||||
|
@ -368,8 +589,9 @@ span ~ .panel-header-text {
|
||||||
}
|
}
|
||||||
|
|
||||||
.userContext-icon-wrapper {
|
.userContext-icon-wrapper {
|
||||||
block-size: 48px;
|
block-size: var(--icon-button-size);
|
||||||
flex: 0 0 48px;
|
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 is used natively, Bug 1333811 was raised to fix */
|
||||||
|
@ -378,24 +600,29 @@ span ~ .panel-header-text {
|
||||||
background-position: center center;
|
background-position: center center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: 20px 20px;
|
background-size: 20px 20px;
|
||||||
block-size: 48px;
|
block-size: 100%;
|
||||||
fill: var(--identity-icon-color);
|
fill: var(--identity-icon-color);
|
||||||
filter: url('/img/filters.svg#fill');
|
filter: url('/img/filters.svg#fill');
|
||||||
flex: 0 0 48px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-panel-row:hover .clickable .usercontext-icon,
|
.container-panel-row:hover .clickable .usercontext-icon,
|
||||||
.container-panel-row:focus .clickable .usercontext-icon {
|
.container-panel-row:focus .clickable .usercontext-icon,
|
||||||
|
.container-panel-row .clickable:focus .usercontext-icon {
|
||||||
background-image: url('/img/container-newtab.svg');
|
background-image: url('/img/container-newtab.svg');
|
||||||
fill: 'gray';
|
fill: #979797;
|
||||||
filter: url('/img/filters.svg#fill');
|
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 */
|
||||||
.panel-footer {
|
.panel-footer {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #efefef;
|
background: #efefef;
|
||||||
block-size: 54px;
|
block-size: var(--icon-button-size);
|
||||||
border-block-end: 1px solid #d8d8d8;
|
border-block-end: 1px solid #d8d8d8;
|
||||||
color: #000;
|
color: #000;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -404,14 +631,9 @@ span ~ .panel-header-text {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-footer .pop-button {
|
|
||||||
block-size: 54px;
|
|
||||||
flex: 0 0 54px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-containers-text {
|
.edit-containers-text {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
block-size: 54px;
|
block-size: 100%;
|
||||||
border-inline-end: solid 1px #d8d8d8;
|
border-inline-end: solid 1px #d8d8d8;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -420,7 +642,7 @@ span ~ .panel-header-text {
|
||||||
|
|
||||||
.edit-containers-text a {
|
.edit-containers-text a {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
block-size: 54px;
|
block-size: 100%;
|
||||||
color: #0a0a0a;
|
color: #0a0a0a;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -428,11 +650,8 @@ span ~ .panel-header-text {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Container info list */
|
/* Container info list */
|
||||||
#container-info-name {
|
.container-info-tab-title {
|
||||||
margin-inline-end: 0.5rem;
|
flex: 1;
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#container-info-hideorshow {
|
#container-info-hideorshow {
|
||||||
|
@ -477,19 +696,19 @@ span ~ .panel-header-text {
|
||||||
|
|
||||||
.container-info-tab-row td {
|
.container-info-tab-row td {
|
||||||
max-inline-size: 200px;
|
max-inline-size: 200px;
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-info-list {
|
.container-info-list {
|
||||||
border-block-start: 1px solid #ebebeb;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-block-start: 4px;
|
margin-block-start: 4px;
|
||||||
padding-block-start: 4px;
|
padding-block-start: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container-info-list tbody {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
.clickable {
|
.clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -501,7 +720,7 @@ span ~ .panel-header-text {
|
||||||
|
|
||||||
.edit-containers-exit-text {
|
.edit-containers-exit-text {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #248aeb;
|
background: var(--primary-action-color);
|
||||||
block-size: 100%;
|
block-size: 100%;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -528,7 +747,7 @@ span ~ .panel-header-text {
|
||||||
|
|
||||||
.delete-container-confirm-title {
|
.delete-container-confirm-title {
|
||||||
color: #000;
|
color: #000;
|
||||||
font-size: 16px;
|
font-size: var(--font-size-heading);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form info */
|
/* Form info */
|
||||||
|
@ -540,36 +759,95 @@ span ~ .panel-header-text {
|
||||||
padding-inline-start: 16px;
|
padding-inline-start: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-panel-content form span {
|
#edit-sites-assigned {
|
||||||
align-items: center;
|
flex: 1;
|
||||||
block-size: 44px;
|
|
||||||
display: flex;
|
|
||||||
flex: 0 0 25%;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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-image: var(--identity-icon);
|
||||||
background-size: 26px 26px;
|
background-position: center;
|
||||||
block-size: 34px;
|
background-repeat: no-repeat;
|
||||||
|
background-size: 16px;
|
||||||
|
block-size: 23px;
|
||||||
|
border: none;
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
fill: var(--identity-icon-color);
|
fill: var(--identity-icon-color);
|
||||||
filter: url('/img/filters.svg#fill');
|
filter: url('/img/filters.svg#fill');
|
||||||
flex: 0 0 34px;
|
inline-size: 23px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-container-panel label::before {
|
.radio-choice > .radio-container > [type="radio"] {
|
||||||
opacity: 0 !important;
|
-moz-appearance: none;
|
||||||
}
|
|
||||||
|
|
||||||
.edit-container-panel [type="radio"] {
|
|
||||||
display: inline;
|
display: inline;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-container-panel [type="radio"]:checked + label {
|
.radio-choice > .radio-container > [type="radio"]:checked + label {
|
||||||
outline: 2px solid grey;
|
background: #d3d3d3;
|
||||||
-moz-outline-radius: 50px;
|
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 {
|
.edit-container-panel fieldset {
|
||||||
|
@ -588,6 +866,10 @@ span ~ .panel-header-text {
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-container-panel fieldset:last-of-type {
|
||||||
|
margin-block-end: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.edit-container-panel input[type="text"] {
|
.edit-container-panel input[type="text"] {
|
||||||
block-size: 36px;
|
block-size: 36px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
@ -602,5 +884,5 @@ span ~ .panel-header-text {
|
||||||
.edit-container-panel legend {
|
.edit-container-panel legend {
|
||||||
flex: 1 0;
|
flex: 1 0;
|
||||||
font-size: 14px !important;
|
font-size: 14px !important;
|
||||||
padding-block-end: 5px;
|
padding-block-end: 6px;
|
||||||
}
|
}
|
||||||
|
|
3
webextension/img/blank-tab.svg
Normal file
3
webextension/img/blank-tab.svg
Normal file
|
@ -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 |
BIN
webextension/img/onboarding-3-security.png
Normal file
BIN
webextension/img/onboarding-3-security.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
|
@ -1,10 +1,45 @@
|
||||||
const redirectUrl = new URL(window.location).searchParams.get("url");
|
async function load() {
|
||||||
document.getElementById("redirect-url").textContent = redirectUrl;
|
const searchParams = new URL(window.location).searchParams;
|
||||||
const redirectSite = new URL(redirectUrl).hostname;
|
const redirectUrl = decodeURIComponent(searchParams.get("url"));
|
||||||
document.getElementById("redirect-site").textContent = redirectSite;
|
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) => {
|
const container = await browser.contextualIdentities.get(cookieStoreId);
|
||||||
e.preventDefault();
|
[...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;
|
const neverAsk = document.getElementById("never-ask").checked;
|
||||||
// Sending neverAsk message to background to store for next time we see this process
|
// Sending neverAsk message to background to store for next time we see this process
|
||||||
if (neverAsk) {
|
if (neverAsk) {
|
||||||
|
@ -12,20 +47,45 @@ document.getElementById("redirect-form").addEventListener("submit", (e) => {
|
||||||
method: "neverAsk",
|
method: "neverAsk",
|
||||||
neverAsk: true,
|
neverAsk: true,
|
||||||
pageUrl: redirectUrl
|
pageUrl: redirectUrl
|
||||||
}).then(() => {
|
|
||||||
redirect();
|
|
||||||
}).catch(() => {
|
|
||||||
// Can't really do much here user will have to click it again
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
browser.runtime.sendMessage({
|
browser.runtime.sendMessage({
|
||||||
method: "sendTelemetryPayload",
|
method: "sendTelemetryPayload",
|
||||||
event: "click-to-reload-page-in-container",
|
event: "click-to-reload-page-in-container",
|
||||||
});
|
});
|
||||||
redirect();
|
openInContainer(redirectUrl, cookieStoreId);
|
||||||
});
|
}
|
||||||
|
|
||||||
function redirect() {
|
function getCurrentTab() {
|
||||||
const redirectUrl = document.getElementById("redirect-url").textContent;
|
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);
|
document.location.replace(redirectUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
load();
|
||||||
|
|
||||||
|
async function openInContainer(redirectUrl, cookieStoreId) {
|
||||||
|
const tab = await getCurrentTab();
|
||||||
|
await browser.tabs.create({
|
||||||
|
cookieStoreId,
|
||||||
|
url: redirectUrl
|
||||||
|
});
|
||||||
|
if (tab.length > 0) {
|
||||||
|
browser.tabs.remove(tab[0].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
42
webextension/js/content-script.js
Normal file
42
webextension/js/content-script.js
Normal file
|
@ -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);
|
||||||
|
});
|
|
@ -7,12 +7,16 @@ const CONTAINER_UNHIDE_SRC = "/img/container-unhide.svg";
|
||||||
|
|
||||||
const DEFAULT_COLOR = "blue";
|
const DEFAULT_COLOR = "blue";
|
||||||
const DEFAULT_ICON = "circle";
|
const DEFAULT_ICON = "circle";
|
||||||
|
const NEW_CONTAINER_ID = "new";
|
||||||
|
|
||||||
|
const ONBOARDING_STORAGE_KEY = "onboarding-stage";
|
||||||
|
|
||||||
// List of panels
|
// List of panels
|
||||||
const P_ONBOARDING_1 = "onboarding1";
|
const P_ONBOARDING_1 = "onboarding1";
|
||||||
const P_ONBOARDING_2 = "onboarding2";
|
const P_ONBOARDING_2 = "onboarding2";
|
||||||
const P_ONBOARDING_3 = "onboarding3";
|
const P_ONBOARDING_3 = "onboarding3";
|
||||||
const P_ONBOARDING_4 = "onboarding4";
|
const P_ONBOARDING_4 = "onboarding4";
|
||||||
|
const P_ONBOARDING_5 = "onboarding5";
|
||||||
const P_CONTAINERS_LIST = "containersList";
|
const P_CONTAINERS_LIST = "containersList";
|
||||||
const P_CONTAINERS_EDIT = "containersEdit";
|
const P_CONTAINERS_EDIT = "containersEdit";
|
||||||
const P_CONTAINER_INFO = "containerInfo";
|
const P_CONTAINER_INFO = "containerInfo";
|
||||||
|
@ -69,32 +73,70 @@ const Logic = {
|
||||||
_currentPanel: null,
|
_currentPanel: null,
|
||||||
_previousPanel: null,
|
_previousPanel: null,
|
||||||
_panels: {},
|
_panels: {},
|
||||||
|
_onboardingVariation: null,
|
||||||
|
|
||||||
init() {
|
async init() {
|
||||||
// Remove browserAction "upgraded" badge when opening panel
|
// Remove browserAction "upgraded" badge when opening panel
|
||||||
this.clearBrowserActionBadge();
|
this.clearBrowserActionBadge();
|
||||||
|
|
||||||
// Retrieve the list of identities.
|
// 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.
|
// Routing to the correct panel.
|
||||||
.then(() => {
|
// If localStorage is disabled, we don't show the onboarding.
|
||||||
// If localStorage is disabled, we don't show the onboarding.
|
const data = await browser.storage.local.get([ONBOARDING_STORAGE_KEY]);
|
||||||
if (!localStorage || localStorage.getItem("onboarded4")) {
|
let onboarded = data[ONBOARDING_STORAGE_KEY];
|
||||||
this.showPanel(P_CONTAINERS_LIST);
|
if (!onboarded) {
|
||||||
|
// Legacy local storage used before panel 5
|
||||||
|
if (localStorage.getItem("onboarded4")) {
|
||||||
|
onboarded = 4;
|
||||||
} else if (localStorage.getItem("onboarded3")) {
|
} else if (localStorage.getItem("onboarded3")) {
|
||||||
this.showPanel(P_ONBOARDING_4);
|
onboarded = 3;
|
||||||
} else if (localStorage.getItem("onboarded2")) {
|
} else if (localStorage.getItem("onboarded2")) {
|
||||||
this.showPanel(P_ONBOARDING_3);
|
onboarded = 2;
|
||||||
} else if (localStorage.getItem("onboarded1")) {
|
} else if (localStorage.getItem("onboarded1")) {
|
||||||
this.showPanel(P_ONBOARDING_2);
|
onboarded = 1;
|
||||||
} else {
|
} else {
|
||||||
this.showPanel(P_ONBOARDING_1);
|
onboarded = 0;
|
||||||
}
|
}
|
||||||
})
|
this.setOnboardingStage(onboarded);
|
||||||
|
}
|
||||||
|
|
||||||
.catch(() => {
|
switch (onboarded) {
|
||||||
throw new Error("Failed to retrieve the identities. We cannot continue.");
|
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
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -107,8 +149,20 @@ const Logic = {
|
||||||
browser.storage.local.set({browserActionBadgesClicked: storage.browserActionBadgesClicked});
|
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) {
|
addEnterHandler(element, handler) {
|
||||||
element.addEventListener("click", handler);
|
element.addEventListener("click", (e) => {
|
||||||
|
handler(e);
|
||||||
|
});
|
||||||
element.addEventListener("keydown", (e) => {
|
element.addEventListener("keydown", (e) => {
|
||||||
if (e.keyCode === 13) {
|
if (e.keyCode === 13) {
|
||||||
handler(e);
|
handler(e);
|
||||||
|
@ -121,6 +175,14 @@ const Logic = {
|
||||||
return (userContextId !== cookieStoreId) ? Number(userContextId) : false;
|
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() {
|
refreshIdentities() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
browser.contextualIdentities.query({}),
|
browser.contextualIdentities.query({}),
|
||||||
|
@ -139,7 +201,16 @@ const Logic = {
|
||||||
}).catch((e) => {throw e;});
|
}).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... ?!?
|
// Invalid panel... ?!?
|
||||||
if (!(panel in this._panels)) {
|
if (!(panel in this._panels)) {
|
||||||
throw new Error("Something really bad happened. Unknown panel: " + panel);
|
throw new Error("Something really bad happened. Unknown panel: " + panel);
|
||||||
|
@ -151,15 +222,18 @@ const Logic = {
|
||||||
this._currentIdentity = currentIdentity;
|
this._currentIdentity = currentIdentity;
|
||||||
|
|
||||||
// Initialize the panel before showing it.
|
// Initialize the panel before showing it.
|
||||||
this._panels[panel].prepare().then(() => {
|
await this._panels[panel].prepare();
|
||||||
for (let panelElement of document.querySelectorAll(".panel")) { // eslint-disable-line prefer-const
|
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");
|
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() {
|
showPreviousPanel() {
|
||||||
|
@ -186,6 +260,11 @@ const Logic = {
|
||||||
return this._currentIdentity;
|
return this._currentIdentity;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
currentUserContextId() {
|
||||||
|
const identity = Logic.currentIdentity();
|
||||||
|
return Logic.userContextId(identity.cookieStoreId);
|
||||||
|
},
|
||||||
|
|
||||||
sendTelemetryPayload(message = {}) {
|
sendTelemetryPayload(message = {}) {
|
||||||
if (!message.event) {
|
if (!message.event) {
|
||||||
throw new Error("Missing event name for telemetry");
|
throw new Error("Missing event name for telemetry");
|
||||||
|
@ -205,6 +284,38 @@ const Logic = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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() {
|
generateIdentityName() {
|
||||||
const defaultName = "Container #";
|
const defaultName = "Container #";
|
||||||
const ids = [];
|
const ids = [];
|
||||||
|
@ -233,13 +344,16 @@ const Logic = {
|
||||||
|
|
||||||
Logic.registerPanel(P_ONBOARDING_1, {
|
Logic.registerPanel(P_ONBOARDING_1, {
|
||||||
panelSelector: ".onboarding-panel-1",
|
panelSelector: ".onboarding-panel-1",
|
||||||
|
securityPanelSelector: ".security-onboarding-panel-1",
|
||||||
|
|
||||||
// This method is called when the object is registered.
|
// This method is called when the object is registered.
|
||||||
initialize() {
|
initialize() {
|
||||||
// Let's move to the next panel.
|
// Let's move to the next panel.
|
||||||
Logic.addEnterHandler(document.querySelector("#onboarding-start-button"), () => {
|
[...document.querySelectorAll(".onboarding-start-button")].forEach(startElement => {
|
||||||
localStorage.setItem("onboarded1", true);
|
Logic.addEnterHandler(startElement, async function () {
|
||||||
Logic.showPanel(P_ONBOARDING_2);
|
await Logic.setOnboardingStage(1);
|
||||||
|
Logic.showPanel(P_ONBOARDING_2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -254,13 +368,16 @@ Logic.registerPanel(P_ONBOARDING_1, {
|
||||||
|
|
||||||
Logic.registerPanel(P_ONBOARDING_2, {
|
Logic.registerPanel(P_ONBOARDING_2, {
|
||||||
panelSelector: ".onboarding-panel-2",
|
panelSelector: ".onboarding-panel-2",
|
||||||
|
securityPanelSelector: ".security-onboarding-panel-2",
|
||||||
|
|
||||||
// This method is called when the object is registered.
|
// This method is called when the object is registered.
|
||||||
initialize() {
|
initialize() {
|
||||||
// Let's move to the containers list panel.
|
// Let's move to the containers list panel.
|
||||||
Logic.addEnterHandler(document.querySelector("#onboarding-next-button"), () => {
|
[...document.querySelectorAll(".onboarding-next-button")].forEach(nextElement => {
|
||||||
localStorage.setItem("onboarded2", true);
|
Logic.addEnterHandler(nextElement, async function () {
|
||||||
Logic.showPanel(P_ONBOARDING_3);
|
await Logic.setOnboardingStage(2);
|
||||||
|
Logic.showPanel(P_ONBOARDING_3);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -275,13 +392,16 @@ Logic.registerPanel(P_ONBOARDING_2, {
|
||||||
|
|
||||||
Logic.registerPanel(P_ONBOARDING_3, {
|
Logic.registerPanel(P_ONBOARDING_3, {
|
||||||
panelSelector: ".onboarding-panel-3",
|
panelSelector: ".onboarding-panel-3",
|
||||||
|
securityPanelSelector: ".security-onboarding-panel-3",
|
||||||
|
|
||||||
// This method is called when the object is registered.
|
// This method is called when the object is registered.
|
||||||
initialize() {
|
initialize() {
|
||||||
// Let's move to the containers list panel.
|
// Let's move to the containers list panel.
|
||||||
Logic.addEnterHandler(document.querySelector("#onboarding-almost-done-button"), () => {
|
[...document.querySelectorAll(".onboarding-almost-done-button")].forEach(almostElement => {
|
||||||
localStorage.setItem("onboarded3", true);
|
Logic.addEnterHandler(almostElement, async function () {
|
||||||
Logic.showPanel(P_ONBOARDING_4);
|
await Logic.setOnboardingStage(3);
|
||||||
|
Logic.showPanel(P_ONBOARDING_4);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -300,8 +420,29 @@ Logic.registerPanel(P_ONBOARDING_4, {
|
||||||
// This method is called when the object is registered.
|
// This method is called when the object is registered.
|
||||||
initialize() {
|
initialize() {
|
||||||
// Let's move to the containers list panel.
|
// Let's move to the containers list panel.
|
||||||
document.querySelector("#onboarding-done-button").addEventListener("click", () => {
|
Logic.addEnterHandler(document.querySelector("#onboarding-done-button"), async function () {
|
||||||
localStorage.setItem("onboarded4", true);
|
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);
|
Logic.showPanel(P_CONTAINERS_LIST);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -346,13 +487,13 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||||
function next() {
|
function next() {
|
||||||
const nextElement = element.nextElementSibling;
|
const nextElement = element.nextElementSibling;
|
||||||
if (nextElement) {
|
if (nextElement) {
|
||||||
nextElement.focus();
|
nextElement.querySelector("td[tabindex=0]").focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function previous() {
|
function previous() {
|
||||||
const previousElement = element.previousElementSibling;
|
const previousElement = element.previousElementSibling;
|
||||||
if (previousElement) {
|
if (previousElement) {
|
||||||
previousElement.focus();
|
previousElement.querySelector("td[tabindex=0]").focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch (e.keyCode) {
|
switch (e.keyCode) {
|
||||||
|
@ -364,12 +505,72 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||||
break;
|
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.
|
// This method is called when the panel is shown.
|
||||||
prepare() {
|
async prepare() {
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
|
|
||||||
|
this.prepareCurrentTabHeader();
|
||||||
|
|
||||||
Logic.identities().forEach(identity => {
|
Logic.identities().forEach(identity => {
|
||||||
const hasTabs = (identity.hasHiddenTabs || identity.hasOpenTabs);
|
const hasTabs = (identity.hasHiddenTabs || identity.hasOpenTabs);
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
|
@ -378,10 +579,11 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||||
|
|
||||||
tr.classList.add("container-panel-row");
|
tr.classList.add("container-panel-row");
|
||||||
|
|
||||||
tr.setAttribute("tabindex", "0");
|
|
||||||
|
|
||||||
context.classList.add("userContext-wrapper", "open-newtab", "clickable");
|
context.classList.add("userContext-wrapper", "open-newtab", "clickable");
|
||||||
manage.classList.add("show-tabs", "pop-button");
|
manage.classList.add("show-tabs", "pop-button");
|
||||||
|
manage.title = escaped`View ${identity.name} container`;
|
||||||
|
context.setAttribute("tabindex", "0");
|
||||||
|
context.title = escaped`Create ${identity.name} tab`;
|
||||||
context.innerHTML = escaped`
|
context.innerHTML = escaped`
|
||||||
<div class="userContext-icon-wrapper open-newtab">
|
<div class="userContext-icon-wrapper open-newtab">
|
||||||
<div class="usercontext-icon"
|
<div class="usercontext-icon"
|
||||||
|
@ -389,7 +591,7 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||||
data-identity-color="${identity.color}">
|
data-identity-color="${identity.color}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container-name"></div>`;
|
<div class="container-name truncate-text"></div>`;
|
||||||
context.querySelector(".container-name").textContent = identity.name;
|
context.querySelector(".container-name").textContent = identity.name;
|
||||||
manage.innerHTML = "<img src='/img/container-arrow.svg' class='show-tabs pop-button-image-small' />";
|
manage.innerHTML = "<img src='/img/container-arrow.svg' class='show-tabs pop-button-image-small' />";
|
||||||
|
|
||||||
|
@ -422,15 +624,21 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const list = document.querySelector(".identities-list");
|
const list = document.querySelector(".identities-list tbody");
|
||||||
|
|
||||||
list.innerHTML = "";
|
list.innerHTML = "";
|
||||||
list.appendChild(fragment);
|
list.appendChild(fragment);
|
||||||
/* Not sure why extensions require a focus for the doorhanger,
|
/* Not sure why extensions require a focus for the doorhanger,
|
||||||
however it allows us to have a tabindex before the first selected item
|
however it allows us to have a tabindex before the first selected item
|
||||||
*/
|
*/
|
||||||
document.addEventListener("focus", () => {
|
const focusHandler = () => {
|
||||||
list.querySelector("tr").focus();
|
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();
|
return Promise.resolve();
|
||||||
|
@ -453,7 +661,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
||||||
const identity = Logic.currentIdentity();
|
const identity = Logic.currentIdentity();
|
||||||
browser.runtime.sendMessage({
|
browser.runtime.sendMessage({
|
||||||
method: identity.hasHiddenTabs ? "showTabs" : "hideTabs",
|
method: identity.hasHiddenTabs ? "showTabs" : "hideTabs",
|
||||||
userContextId: Logic.userContextId(identity.cookieStoreId)
|
userContextId: Logic.currentUserContextId()
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
window.close();
|
window.close();
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
@ -525,7 +733,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
||||||
// Let's retrieve the list of tabs.
|
// Let's retrieve the list of tabs.
|
||||||
return browser.runtime.sendMessage({
|
return browser.runtime.sendMessage({
|
||||||
method: "getTabs",
|
method: "getTabs",
|
||||||
userContextId: Logic.userContextId(identity.cookieStoreId),
|
userContextId: Logic.currentUserContextId(),
|
||||||
}).then(this.buildInfoTable);
|
}).then(this.buildInfoTable);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -537,8 +745,9 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
||||||
fragment.appendChild(tr);
|
fragment.appendChild(tr);
|
||||||
tr.classList.add("container-info-tab-row");
|
tr.classList.add("container-info-tab-row");
|
||||||
tr.innerHTML = escaped`
|
tr.innerHTML = escaped`
|
||||||
<td><img class="icon" src="${tab.favicon}" /></td>
|
<td></td>
|
||||||
<td class="container-info-tab-title">${tab.title}</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.
|
// On click, we activate this tab. But only if this tab is active.
|
||||||
if (tab.active) {
|
if (tab.active) {
|
||||||
|
@ -588,22 +797,22 @@ Logic.registerPanel(P_CONTAINERS_EDIT, {
|
||||||
data-identity-color="${identity.color}">
|
data-identity-color="${identity.color}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container-name"></div>
|
<div class="container-name truncate-text"></div>
|
||||||
</td>
|
</td>
|
||||||
<td class="edit-container pop-button edit-container-icon">
|
<td class="edit-container pop-button edit-container-icon">
|
||||||
<img
|
<img
|
||||||
src="/img/container-edit.svg"
|
src="/img/container-edit.svg"
|
||||||
class="pop-button-image" />
|
class="pop-button-image" />
|
||||||
</td>
|
</td>
|
||||||
<td class="remove-container pop-button delete-container-icon" >
|
<td class="remove-container pop-button delete-container-icon">
|
||||||
<img
|
<img
|
||||||
class="pop-button-image"
|
class="pop-button-image"
|
||||||
src="/img/container-delete.svg"
|
src="/img/container-delete.svg"
|
||||||
/>
|
/>
|
||||||
</td>`;
|
</td>`;
|
||||||
tr.querySelector(".container-name").textContent = identity.name;
|
tr.querySelector(".container-name").textContent = identity.name;
|
||||||
tr.querySelector(".edit-container .pop-button-image").setAttribute("title", `Edit ${identity.name} container`);
|
tr.querySelector(".edit-container").setAttribute("title", `Edit ${identity.name} container`);
|
||||||
tr.querySelector(".remove-container .pop-button-image").setAttribute("title", `Edit ${identity.name} container`);
|
tr.querySelector(".remove-container").setAttribute("title", `Delete ${identity.name} container`);
|
||||||
|
|
||||||
|
|
||||||
Logic.addEnterHandler(tr, e => {
|
Logic.addEnterHandler(tr, e => {
|
||||||
|
@ -635,7 +844,12 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||||
this.initializeRadioButtons();
|
this.initializeRadioButtons();
|
||||||
|
|
||||||
Logic.addEnterHandler(document.querySelector("#edit-container-panel-back-arrow"), () => {
|
Logic.addEnterHandler(document.querySelector("#edit-container-panel-back-arrow"), () => {
|
||||||
Logic.showPreviousPanel();
|
const formValues = new FormData(this._editForm);
|
||||||
|
if (formValues.get("container-id") !== NEW_CONTAINER_ID) {
|
||||||
|
this._submitForm();
|
||||||
|
} else {
|
||||||
|
Logic.showPreviousPanel();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Logic.addEnterHandler(document.querySelector("#edit-container-cancel-link"), () => {
|
Logic.addEnterHandler(document.querySelector("#edit-container-cancel-link"), () => {
|
||||||
|
@ -644,18 +858,25 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||||
|
|
||||||
this._editForm = document.getElementById("edit-container-panel-form");
|
this._editForm = document.getElementById("edit-container-panel-form");
|
||||||
const editLink = document.querySelector("#edit-container-ok-link");
|
const editLink = document.querySelector("#edit-container-ok-link");
|
||||||
Logic.addEnterHandler(editLink, this._submitForm.bind(this));
|
Logic.addEnterHandler(editLink, () => {
|
||||||
editLink.addEventListener("submit", this._submitForm.bind(this));
|
this._submitForm();
|
||||||
this._editForm.addEventListener("submit", this._submitForm.bind(this));
|
});
|
||||||
|
editLink.addEventListener("submit", () => {
|
||||||
|
this._submitForm();
|
||||||
|
});
|
||||||
|
this._editForm.addEventListener("submit", () => {
|
||||||
|
this._submitForm();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_submitForm() {
|
_submitForm() {
|
||||||
const identity = Logic.currentIdentity();
|
|
||||||
const formValues = new FormData(this._editForm);
|
const formValues = new FormData(this._editForm);
|
||||||
return browser.runtime.sendMessage({
|
return browser.runtime.sendMessage({
|
||||||
method: "createOrUpdateContainer",
|
method: "createOrUpdateContainer",
|
||||||
message: {
|
message: {
|
||||||
userContextId: Logic.userContextId(identity.cookieStoreId) || false,
|
userContextId: formValues.get("container-id") || NEW_CONTAINER_ID,
|
||||||
params: {
|
params: {
|
||||||
name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(),
|
name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(),
|
||||||
icon: formValues.get("container-icon") || DEFAULT_ICON,
|
icon: formValues.get("container-icon") || DEFAULT_ICON,
|
||||||
|
@ -671,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() {
|
initializeRadioButtons() {
|
||||||
const colorRadioTemplate = (containerColor) => {
|
const colorRadioTemplate = (containerColor) => {
|
||||||
return escaped`<input type="radio" value="${containerColor}" name="container-color" id="edit-container-panel-choose-color-${containerColor}" />
|
return escaped`<input type="radio" value="${containerColor}" name="container-color" id="edit-container-panel-choose-color-${containerColor}" />
|
||||||
|
@ -679,7 +945,8 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||||
const colors = ["blue", "turquoise", "green", "yellow", "orange", "red", "pink", "purple" ];
|
const colors = ["blue", "turquoise", "green", "yellow", "orange", "red", "pink", "purple" ];
|
||||||
const colorRadioFieldset = document.getElementById("edit-container-panel-choose-color");
|
const colorRadioFieldset = document.getElementById("edit-container-panel-choose-color");
|
||||||
colors.forEach((containerColor) => {
|
colors.forEach((containerColor) => {
|
||||||
const templateInstance = document.createElement("span");
|
const templateInstance = document.createElement("div");
|
||||||
|
templateInstance.classList.add("radio-container");
|
||||||
// eslint-disable-next-line no-unsanitized/property
|
// eslint-disable-next-line no-unsanitized/property
|
||||||
templateInstance.innerHTML = colorRadioTemplate(containerColor);
|
templateInstance.innerHTML = colorRadioTemplate(containerColor);
|
||||||
colorRadioFieldset.appendChild(templateInstance);
|
colorRadioFieldset.appendChild(templateInstance);
|
||||||
|
@ -692,7 +959,8 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||||
const icons = ["fingerprint", "briefcase", "dollar", "cart", "vacation", "gift", "food", "fruit", "pet", "tree", "chill", "circle"];
|
const icons = ["fingerprint", "briefcase", "dollar", "cart", "vacation", "gift", "food", "fruit", "pet", "tree", "chill", "circle"];
|
||||||
const iconRadioFieldset = document.getElementById("edit-container-panel-choose-icon");
|
const iconRadioFieldset = document.getElementById("edit-container-panel-choose-icon");
|
||||||
icons.forEach((containerIcon) => {
|
icons.forEach((containerIcon) => {
|
||||||
const templateInstance = document.createElement("span");
|
const templateInstance = document.createElement("div");
|
||||||
|
templateInstance.classList.add("radio-container");
|
||||||
// eslint-disable-next-line no-unsanitized/property
|
// eslint-disable-next-line no-unsanitized/property
|
||||||
templateInstance.innerHTML = iconRadioTemplate(containerIcon);
|
templateInstance.innerHTML = iconRadioTemplate(containerIcon);
|
||||||
iconRadioFieldset.appendChild(templateInstance);
|
iconRadioFieldset.appendChild(templateInstance);
|
||||||
|
@ -700,9 +968,16 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||||
},
|
},
|
||||||
|
|
||||||
// This method is called when the panel is shown.
|
// This method is called when the panel is shown.
|
||||||
prepare() {
|
async prepare() {
|
||||||
const identity = Logic.currentIdentity();
|
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-name-input").value = identity.name || "";
|
||||||
|
document.querySelector("#edit-container-panel-usercontext-input").value = userContextId || NEW_CONTAINER_ID;
|
||||||
[...document.querySelectorAll("[name='container-color']")].forEach(colorInput => {
|
[...document.querySelectorAll("[name='container-color']")].forEach(colorInput => {
|
||||||
colorInput.checked = colorInput.value === identity.color;
|
colorInput.checked = colorInput.value === identity.color;
|
||||||
});
|
});
|
||||||
|
|
23
webextension/js/utils.js
Normal file
23
webextension/js/utils.js
Normal file
|
@ -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,
|
"manifest_version": 2,
|
||||||
"name": "Containers Experiment",
|
"name": "Containers Experiment",
|
||||||
"version": "2.3.0",
|
"version": "2.4.0",
|
||||||
|
|
||||||
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
|
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
|
||||||
"icons": {
|
"icons": {
|
||||||
|
@ -26,7 +26,6 @@
|
||||||
"contextualIdentities",
|
"contextualIdentities",
|
||||||
"history",
|
"history",
|
||||||
"idle",
|
"idle",
|
||||||
"notifications",
|
|
||||||
"storage",
|
"storage",
|
||||||
"tabs",
|
"tabs",
|
||||||
"webRequestBlocking",
|
"webRequestBlocking",
|
||||||
|
@ -36,7 +35,8 @@
|
||||||
"commands": {
|
"commands": {
|
||||||
"_execute_browser_action": {
|
"_execute_browser_action": {
|
||||||
"suggested_key": {
|
"suggested_key": {
|
||||||
"default": "Ctrl+Y"
|
"default": "Ctrl+Period",
|
||||||
|
"mac": "MacCtrl+Period"
|
||||||
},
|
},
|
||||||
"description": "Open containers panel"
|
"description": "Open containers panel"
|
||||||
}
|
}
|
||||||
|
@ -54,5 +54,18 @@
|
||||||
|
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": ["background.js"]
|
"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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,56 +3,96 @@
|
||||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||||
<title>Containers browserAction Popup</title>
|
<title>Containers browserAction Popup</title>
|
||||||
<link rel="stylesheet" href="/css/popup.css">
|
<link rel="stylesheet" href="/css/popup.css">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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" />
|
<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>
|
<h3 class="onboarding-title">A better way to manage all the things you do online</h3>
|
||||||
<p>
|
<p>
|
||||||
Use containers to organize tasks, manage accounts, and keep your focus where you want it.
|
Use containers to organize tasks, manage accounts, and keep your focus where you want it.
|
||||||
</p>
|
</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>
|
||||||
|
|
||||||
|
<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" />
|
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-2.png" />
|
||||||
<h3 class="onboarding-title">Put containers to work for you.</h3>
|
<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>
|
<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>
|
||||||
|
|
||||||
<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" />
|
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-3.png" />
|
||||||
<h3 class="onboarding-title">A place for everything, and everything in its place.</h3>
|
<h3 class="onboarding-title">A place for everything, and everything in its place.</h3>
|
||||||
<p>Start with the containers we've created, or create your own.</p>
|
<p>Start with the containers we've created, or create your own.</p>
|
||||||
<a href="#" id="onboarding-almost-done-button" class="onboarding-button">Next</a>
|
<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>
|
||||||
|
|
||||||
<div class="panel onboarding onboarding-panel-4 hide" id="onboarding-panel-4">
|
<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" />
|
<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>
|
<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>
|
<p>Right-click inside a container tab to assign the site to always open in the container.</p>
|
||||||
<a href="#" id="onboarding-done-button" class="onboarding-button">Done</a>
|
<a href="#" id="onboarding-done-button" class="onboarding-button">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>
|
||||||
|
|
||||||
<div class="panel container-panel hide" id="container-panel">
|
<div class="panel container-panel hide" id="container-panel">
|
||||||
<div class="panel-header">
|
<div id="current-tab">
|
||||||
<h3 class="panel-header-text">Containers</h3>
|
<h3>Current Tab</h3>
|
||||||
<a href="#" class="pop-button" id="sort-containers-link"><img class="pop-button-image" alt="Sort Containers" title="Sort Containers" src="/img/container-sort.svg"></a>
|
<div 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="container-panel-controls">
|
||||||
|
<a href="#" class="action-link" id="sort-containers-link" title="Sort tabs into container order">Sort Tabs</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="scrollable panel-content" tabindex="-1">
|
<div class="scrollable panel-content" tabindex="-1">
|
||||||
<table>
|
<table class="identities-list">
|
||||||
<tbody class="identities-list"></tbody>
|
<tbody></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-footer edit-identities">
|
<div class="panel-footer edit-identities">
|
||||||
<div class="edit-containers-text panel-footer-secondary">
|
<div class="edit-containers-text panel-footer-secondary">
|
||||||
<a href="#" tabindex="0" id="edit-containers-link">Edit Containers</a>
|
<a href="#" tabindex="0" id="edit-containers-link">Edit Containers</a>
|
||||||
</div>
|
</div>
|
||||||
<a href="#" tabindex="0" class="add-container-link pop-button" id="container-add-link">
|
<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" title="Create new container" src="/img/container-add.svg" />
|
<img class="pop-button-image-small icon" alt="Create new container icon" src="/img/container-add.svg" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -66,7 +106,7 @@
|
||||||
<div class="column-panel-content">
|
<div class="column-panel-content">
|
||||||
<div class="panel-header container-info-panel-header">
|
<div class="panel-header container-info-panel-header">
|
||||||
<span class="usercontext-icon" id="container-info-icon"></span>
|
<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>
|
||||||
<div class="select-row clickable container-info-panel-hide container-info-has-tabs" id="container-info-hideorshow">
|
<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"/>
|
<img id="container-info-hideorshow-icon" alt="Hide Container icon" src="/img/container-hide.svg" class="icon container-info-panel-hideorshow-icon"/>
|
||||||
|
@ -104,17 +144,23 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="column-panel-content">
|
<div class="column-panel-content">
|
||||||
<form id="edit-container-panel-form">
|
<form id="edit-container-panel-form">
|
||||||
|
<input type="hidden" name="container-id" id="edit-container-panel-usercontext-input" />
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Name</legend>
|
<legend>Name</legend>
|
||||||
<input type="text" name="container-name" id="edit-container-panel-name-input" maxlength="25"/>
|
<input type="text" name="container-name" id="edit-container-panel-name-input" maxlength="25"/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset id="edit-container-panel-choose-color">
|
<fieldset id="edit-container-panel-choose-color" class="radio-choice">
|
||||||
<legend>Choose a color</legend>
|
<legend>Choose a color</legend>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset id="edit-container-panel-choose-icon">
|
<fieldset id="edit-container-panel-choose-icon" class="radio-choice">
|
||||||
<legend>Choose an icon</legend>
|
<legend>Choose an icon</legend>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</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">
|
<div class="panel-footer">
|
||||||
<a href="#" class="button secondary expanded footer-button cancel-button" id="edit-container-cancel-link">Cancel</a>
|
<a href="#" class="button secondary expanded footer-button cancel-button" id="edit-container-cancel-link">Cancel</a>
|
||||||
<a class="button primary expanded footer-button" id="edit-container-ok-link">OK</a>
|
<a class="button primary expanded footer-button" id="edit-container-ok-link">OK</a>
|
||||||
|
@ -138,7 +184,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="js/utils.js"></script>
|
||||||
<script src="js/popup.js"></script>
|
<script src="js/popup.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Add table
Reference in a new issue