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
|
||||
lib/shield/*.js
|
||||
lib/testpilot/*.js
|
||||
|
|
|
@ -9,6 +9,7 @@ module.exports = {
|
|||
"webextensions": true
|
||||
},
|
||||
"globals": {
|
||||
"Utils": true,
|
||||
"CustomizableUI": true,
|
||||
"CustomizableWidgets": true,
|
||||
"SessionStore": true,
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,8 +1,10 @@
|
|||
.DS_Store
|
||||
package-lock.json
|
||||
node_modules
|
||||
README.html
|
||||
*.xpi
|
||||
*.swp
|
||||
*.swo
|
||||
.vimrc
|
||||
.env
|
||||
addon.env
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
"declaration-block-no-duplicate-properties": true,
|
||||
"order/declaration-block-properties-alphabetical-order": true,
|
||||
"property-blacklist": [
|
||||
"/height/",
|
||||
"/(min[-]|max[-])height/",
|
||||
"/width/",
|
||||
"/top/",
|
||||
"/bottom/",
|
||||
|
|
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)
|
||||
|
||||
[Embedded Web Extension](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Embedded_WebExtensions) to experiment with [Containers](https://blog.mozilla.org/tanvi/2016/06/16/contextual-identities-on-the-web/) in [Firefox Test Pilot](https://testpilot.firefox.com/) to learn:
|
||||
[Embedded Web Extension](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Embedded_WebExtensions) to build [Containers](https://blog.mozilla.org/tanvi/2016/06/16/contextual-identities-on-the-web/) as a Firefox [Test Pilot](https://testpilot.firefox.com/) Experiment and [Shield Study](https://wiki.mozilla.org/Firefox/Shield/Shield_Studies) to learn:
|
||||
|
||||
* Will a general Firefox audience understand the Containers feature?
|
||||
* Is the UI as currently implemented in Nightly clear or discoverable?
|
||||
|
||||
See [the Product Hypothesis Document for more
|
||||
details](https://docs.google.com/document/d/1WQdHTVXROk7dYkSFluc6_hS44tqZjIrG9I-uPyzevE8/edit?ts=5824ba12#).
|
||||
For more info, see:
|
||||
|
||||
* [Test Pilot Product Hypothesis Document](https://docs.google.com/document/d/1WQdHTVXROk7dYkSFluc6_hS44tqZjIrG9I-uPyzevE8/edit#)
|
||||
* [Shield Product Hypothesis Document](https://docs.google.com/document/d/1vMD-fH_5hGDDqNvpRZk12_RhCN2WAe4_yaBamaNdtik/edit#)
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
* node 7+ (for jpm)
|
||||
* Firefox 51+
|
||||
|
||||
|
||||
## Run it
|
||||
|
||||
See Development
|
||||
* Firefox 53+
|
||||
|
||||
|
||||
## Development
|
||||
|
@ -27,28 +24,23 @@ See Development
|
|||
|
||||
Add-on development is better with [a particular environment](https://developer.mozilla.org/en-US/Add-ons/Setting_up_extension_development_environment). One simple way to get that environment set up is to install the [DevPrefs add-on](https://addons.mozilla.org/en-US/firefox/addon/devprefs/). You can make a custom Firefox profile that includes the DevPrefs add-on, and use that profile when you run the code in this repository.
|
||||
|
||||
|
||||
1. Make a new profile by running `/path/to/firefox -P`, which launches the profile editor. "Create Profile" -- name it whatever you wish (e.g. 'addon_dev') and store it in the default location. It's probably best to deselect the option to "Use without asking," since you probably don't want to use this as your default profile.
|
||||
|
||||
2. Once you've created your profile, click "Start Firefox". A new instance of Firefox should launch. Go to Tools->Add-ons and search for "DevPrefs". Install it. Quit Firefox.
|
||||
|
||||
3. Now you have a new, vanilla Firefox profile with the DevPrefs add-on installed. You can use your new profile with the code in _this_ repository like so:
|
||||
|
||||
**Beta building**
|
||||
#### Run the `.xpi` file in an unbranded build
|
||||
Release & Beta channels do not allow un-signed add-ons, even with the DevPrefs. So, you must run the add-on in an [unbranded build](https://wiki.mozilla.org/Add-ons/Extension_Signing#Unbranded_Builds):
|
||||
|
||||
To build this for 51 beta just using the downloaded version of beta will not work as XPI signature checking is disabled fully.
|
||||
1. Download and install an un-branded build of Firefox
|
||||
2. Download the latest `.xpi` from this repository's releases
|
||||
3. Run the un-branded build of Firefox with your DevPrefs profile
|
||||
4. Go to `about:addons`
|
||||
5. Click the gear, and select "Install Add-on From File..."
|
||||
6. Select the `.xpi` file
|
||||
|
||||
The only way to run the experiment is using an [unbranded version build](https://wiki.mozilla.org/Add-ons/Extension_Signing#Unbranded_Builds) or to build beta yourself:
|
||||
|
||||
1. [Download the mozilla-beta repo](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Source_Code/Mercurial#mozilla-beta_(prerelease_development_tree))
|
||||
2. [Create a mozconfig file](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Build_Instructions/Configuring_Build_Options) - probably optional
|
||||
3. `cd <reponame>`
|
||||
3. `./mach bootstrap`
|
||||
4. `./mach build`
|
||||
5. Follow the above instructions by creating the new profile via: `~/<reponame>/obj-x86_64-pc-linux-gnu/dist/bin/firefox -P` (Where "obj-x86_64-pc-linux-gnu" may be different depending on platform obj-...)
|
||||
|
||||
|
||||
### Run with jpm
|
||||
#### Run the TxP experiment with `jpm`
|
||||
|
||||
1. `git clone git@github.com:mozilla/testpilot-containers.git`
|
||||
2. `cd testpilot-containers`
|
||||
|
@ -57,11 +49,22 @@ The only way to run the experiment is using an [unbranded version build](https:/
|
|||
|
||||
Check out the [Browser Toolbox](https://developer.mozilla.org/en-US/docs/Tools/Browser_Toolbox) for more information about debugging add-on code.
|
||||
|
||||
#### Run the shield study with `shield`
|
||||
|
||||
1. `git clone git@github.com:mozilla/testpilot-containers.git`
|
||||
2. `cd testpilot-containers`
|
||||
3. `npm install`
|
||||
4. `npm install -g shield-study-cli`
|
||||
5. `shield run . -- --binary Nightly`
|
||||
|
||||
### Building .xpi
|
||||
|
||||
To build a local .xpi, use the plain [`jpm
|
||||
xpi`](https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/jpm#jpm_xpi) command.
|
||||
To build a local testpilot-containers.xpi, use the plain [`jpm
|
||||
xpi`](https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/jpm#jpm_xpi) command,
|
||||
or run `npm run build`.
|
||||
|
||||
#### Building a shield .xpi
|
||||
To build a local shield-study-containers.xpi, run `npm run build-shield`.
|
||||
|
||||
### Signing an .xpi
|
||||
|
||||
|
@ -75,6 +78,11 @@ add-on](https://addons.mozilla.org/en-US/developers/addon/containers-experiment/
|
|||
### Testing
|
||||
TBD
|
||||
|
||||
|
||||
### Distributing
|
||||
TBD
|
||||
|
||||
### Links
|
||||
|
||||
- [Licence](./LICENSE.txt)
|
||||
- [Contributing](./CONTRIBUTING.md)
|
||||
- [Code Of Conduct](./CODE_OF_CONDUCT.md)
|
||||
|
|
|
@ -52,55 +52,55 @@ value, or chrome url path as an alternate selector mitiages this bug.*/
|
|||
|
||||
[data-identity-icon="fingerprint"],
|
||||
[data-identity-icon="chrome://browser/skin/usercontext/personal.svg"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#fingerprint");
|
||||
--identity-icon: url("/data/usercontext.svg#fingerprint");
|
||||
}
|
||||
|
||||
[data-identity-icon="briefcase"],
|
||||
[data-identity-icon="chrome://browser/skin/usercontext/work.svg"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#briefcase");
|
||||
--identity-icon: url("/data/usercontext.svg#briefcase");
|
||||
}
|
||||
|
||||
[data-identity-icon="dollar"],
|
||||
[data-identity-icon="chrome://browser/skin/usercontext/banking.svg"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#dollar");
|
||||
--identity-icon: url("/data/usercontext.svg#dollar");
|
||||
}
|
||||
|
||||
[data-identity-icon="cart"],
|
||||
[data-identity-icon="chrome://browser/skin/usercontext/cart.svg"],
|
||||
[data-identity-icon="chrome://browser/skin/usercontext/shopping.svg"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#cart");
|
||||
--identity-icon: url("/data/usercontext.svg#cart");
|
||||
}
|
||||
|
||||
[data-identity-icon="circle"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#circle");
|
||||
--identity-icon: url("/data/usercontext.svg#circle");
|
||||
}
|
||||
|
||||
[data-identity-icon="gift"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#gift");
|
||||
--identity-icon: url("/data/usercontext.svg#gift");
|
||||
}
|
||||
|
||||
[data-identity-icon="vacation"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#vacation");
|
||||
--identity-icon: url("/data/usercontext.svg#vacation");
|
||||
}
|
||||
|
||||
[data-identity-icon="food"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#food");
|
||||
--identity-icon: url("/data/usercontext.svg#food");
|
||||
}
|
||||
|
||||
[data-identity-icon="fruit"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#fruit");
|
||||
--identity-icon: url("/data/usercontext.svg#fruit");
|
||||
}
|
||||
|
||||
[data-identity-icon="pet"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#pet");
|
||||
--identity-icon: url("/data/usercontext.svg#pet");
|
||||
}
|
||||
|
||||
[data-identity-icon="tree"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#tree");
|
||||
--identity-icon: url("/data/usercontext.svg#tree");
|
||||
}
|
||||
|
||||
[data-identity-icon="chill"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#chill");
|
||||
--identity-icon: url("/data/usercontext.svg#chill");
|
||||
}
|
||||
|
||||
#userContext-indicator {
|
||||
|
@ -139,7 +139,7 @@ value, or chrome url path as an alternate selector mitiages this bug.*/
|
|||
background-size: contain;
|
||||
fill: var(--identity-icon-color) !important;
|
||||
filter: url(/img/filters.svg#fill);
|
||||
filter: url(resource://testpilot-containers/data/filters.svg#fill);
|
||||
filter: url(/data/filters.svg#fill);
|
||||
}
|
||||
|
||||
/* containers experiment */
|
||||
|
@ -200,7 +200,7 @@ special cases are addressed below */
|
|||
}
|
||||
|
||||
#new-tab-overlay {
|
||||
--icon-size: 26px;
|
||||
--icon-size: 16px;
|
||||
-moz-appearance: none;
|
||||
background: transparent;
|
||||
font-style: -moz-use-system-font;
|
||||
|
@ -252,8 +252,8 @@ special cases are addressed below */
|
|||
}
|
||||
|
||||
#new-tab-overlay .menuitem-iconic[data-usercontextid] > .menu-iconic-left > .menu-iconic-icon {
|
||||
block-height: var(--icon-size);
|
||||
block-width: var(--icon-size);
|
||||
block-size: var(--icon-size);
|
||||
inline-size: var(--icon-size);
|
||||
}
|
||||
|
||||
.menuitem-iconic[data-usercontextid] > .menu-iconic-left {
|
||||
|
|
|
@ -68,6 +68,16 @@ Containers will use Test Pilot Telemetry with no batching of data. Details
|
|||
of when pings are sent are below, along with examples of the `payload` portion
|
||||
of a `testpilottest` telemetry ping for each scenario.
|
||||
|
||||
* The user shows the new tab menu
|
||||
|
||||
```js
|
||||
{
|
||||
"uuid": <uuid>,
|
||||
"event": "show-plus-button-menu",
|
||||
"eventSource": ["plus-button"]
|
||||
}
|
||||
```
|
||||
|
||||
* The user clicks on a container name to open a tab in that container
|
||||
|
||||
```js
|
||||
|
@ -76,7 +86,7 @@ of a `testpilottest` telemetry ping for each scenario.
|
|||
"userContextId": <userContextId>,
|
||||
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
|
||||
"event": "open-tab",
|
||||
"eventSource": ["tab-bar"|"pop-up"|"file-menu"]
|
||||
"eventSource": ["tab-bar"|"pop-up"|"file-menu"|"alltabs-menu"|"plus-button"]
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -220,7 +230,7 @@ of a `testpilottest` telemetry ping for each scenario.
|
|||
}
|
||||
```
|
||||
|
||||
* The user clicks "Take me there" to reload a site into a container after the user picked "Always Open in this Container".
|
||||
* The user clicks "Open in *assigned* container" to reload a site into a container after the user picked "Always Open in this Container".
|
||||
|
||||
```js
|
||||
{
|
||||
|
@ -229,6 +239,15 @@ of a `testpilottest` telemetry ping for each scenario.
|
|||
}
|
||||
```
|
||||
|
||||
* The user clicks "Open in *Current* container" to reload a site into a container after the user picked "Always Open in this Container".
|
||||
|
||||
```js
|
||||
{
|
||||
"uuid": <uuid>,
|
||||
"event": "click-to-reload-page-in-same-container"
|
||||
}
|
||||
```
|
||||
|
||||
* Firefox automatically reloads a site into a container after the user picked "Always Open in this Container".
|
||||
|
||||
```js
|
||||
|
@ -260,7 +279,7 @@ local schema = {
|
|||
|
||||
### Valid data should be enforced on the server side:
|
||||
|
||||
* `eventSource` should be one of `tab-bar`, `pop-up`, or `file-menu`.
|
||||
* `eventSource` should be one of `tab-bar`, `pop-up`, `file-menu`, "alltabs-nmenu" or "plus-button".
|
||||
|
||||
All Mozilla data is kept by default for 180 days and in accordance with our
|
||||
privacy policies.
|
||||
|
|
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 LOOKUP_KEY = "$ref";
|
||||
|
||||
const SHOW_MENU_TIMEOUT = 100;
|
||||
const HIDE_MENU_TIMEOUT = 300;
|
||||
|
||||
const INCOMPATIBLE_ADDON_IDS = [
|
||||
"pulse@mozilla.com",
|
||||
"snoozetabs@mozilla.com",
|
||||
|
@ -42,8 +39,17 @@ const IDENTITY_ICONS = [
|
|||
{ name: "circle", image: "circle" },
|
||||
];
|
||||
|
||||
const IDENTITY_COLORS_STANDARD = [
|
||||
"blue", "orange", "green", "pink",
|
||||
];
|
||||
|
||||
const IDENTITY_ICONS_STANDARD = [
|
||||
"fingerprint", "briefcase", "dollar", "cart",
|
||||
];
|
||||
|
||||
const PREFS = [
|
||||
[ "privacy.userContext.enabled", true ],
|
||||
[ "privacy.userContext.longPressBehavior", 2 ],
|
||||
[ "privacy.userContext.ui.enabled", false ],
|
||||
[ "privacy.usercontext.about_newtab_segregation.enabled", true ],
|
||||
];
|
||||
|
@ -60,6 +66,7 @@ const prefService = require("sdk/preferences/service");
|
|||
const self = require("sdk/self");
|
||||
const { Services } = require("resource://gre/modules/Services.jsm");
|
||||
const ss = require("sdk/simple-storage");
|
||||
const { study } = require("./study");
|
||||
const { Style } = require("sdk/stylesheet/style");
|
||||
const tabs = require("sdk/tabs");
|
||||
const tabsUtils = require("sdk/tabs/utils");
|
||||
|
@ -213,6 +220,7 @@ const ContainerService = {
|
|||
"getPreference",
|
||||
"sendTelemetryPayload",
|
||||
"getTheme",
|
||||
"getShieldStudyVariation",
|
||||
"refreshNeeded",
|
||||
"forgetIdentityAndRefresh",
|
||||
"checkIncompatibleAddons"
|
||||
|
@ -236,18 +244,15 @@ const ContainerService = {
|
|||
}
|
||||
|
||||
tabs.on("open", tab => {
|
||||
this._hideAllPanels();
|
||||
this._restyleTab(tab);
|
||||
this._remapTab(tab);
|
||||
});
|
||||
|
||||
tabs.on("close", tab => {
|
||||
this._hideAllPanels();
|
||||
this._remapTab(tab);
|
||||
});
|
||||
|
||||
tabs.on("activate", tab => {
|
||||
this._hideAllPanels();
|
||||
this._restyleActiveTab(tab).catch(() => {});
|
||||
this._configureActiveWindows();
|
||||
this._remapTab(tab);
|
||||
|
@ -320,6 +325,11 @@ const ContainerService = {
|
|||
// End-Of-Hack
|
||||
|
||||
Services.obs.addObserver(this, "lightweight-theme-changed", false);
|
||||
|
||||
if (self.id === "@shield-study-containers") {
|
||||
study.startup(reason);
|
||||
this.shieldStudyVariation = study.variation;
|
||||
}
|
||||
},
|
||||
|
||||
registerBackgroundConnection(api) {
|
||||
|
@ -362,6 +372,10 @@ const ContainerService = {
|
|||
});
|
||||
},
|
||||
|
||||
getShieldStudyVariation() {
|
||||
return this.shieldStudyVariation;
|
||||
},
|
||||
|
||||
// utility methods
|
||||
|
||||
_containerTabCount(userContextId) {
|
||||
|
@ -922,12 +936,6 @@ const ContainerService = {
|
|||
return this._configureWindows();
|
||||
},
|
||||
|
||||
_hideAllPanels() {
|
||||
for (let windowObject of this._windowMap.values()) { // eslint-disable-line prefer-const
|
||||
windowObject.hidePanel();
|
||||
}
|
||||
},
|
||||
|
||||
_restyleActiveTab(tab) {
|
||||
if (!tab) {
|
||||
return Promise.resolve(null);
|
||||
|
@ -1086,22 +1094,50 @@ ContainerWindow.prototype = {
|
|||
_timeoutStore: new Map(),
|
||||
_elementCache: new Map(),
|
||||
_tooltipCache: new Map(),
|
||||
_plusButton: null,
|
||||
_overflowPlusButton: null,
|
||||
_tabsElement: null,
|
||||
|
||||
_init(window) {
|
||||
this._window = window;
|
||||
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._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);
|
||||
},
|
||||
|
||||
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() {
|
||||
return Promise.all([
|
||||
this._configurePlusButtonMenu(),
|
||||
this._configureActiveTab(),
|
||||
this._configureFileMenu(),
|
||||
this._configureAllTabsMenu(),
|
||||
|
@ -1114,112 +1150,6 @@ ContainerWindow.prototype = {
|
|||
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() {
|
||||
const promises = [];
|
||||
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() {
|
||||
// CSS must be removed.
|
||||
detachFrom(this._style, this._window);
|
||||
|
||||
this._shutdownPlusButtonMenu();
|
||||
this._shutdownFileMenu();
|
||||
this._shutdownAllTabsMenu();
|
||||
this._shutdownContextMenu();
|
||||
|
||||
this._shutdownContainers();
|
||||
},
|
||||
|
||||
_shutDownPlusButtonMenuElement(buttonElement) {
|
||||
|
@ -1415,11 +1341,6 @@ ContainerWindow.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
_shutdownPlusButtonMenu() {
|
||||
this._shutDownPlusButtonMenuElement(this._plusButton);
|
||||
this._shutDownPlusButtonMenuElement(this._overflowPlusButton);
|
||||
},
|
||||
|
||||
_shutdownFileMenu() {
|
||||
this._shutdownMenu("menu_newUserContext");
|
||||
},
|
||||
|
@ -1468,6 +1389,36 @@ ContainerWindow.prototype = {
|
|||
|
||||
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 ---------------------------------------------------
|
||||
|
|
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",
|
||||
"title": "Containers Experiment",
|
||||
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
|
||||
"version": "2.3.0",
|
||||
"version": "2.4.0",
|
||||
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
|
||||
"bugs": {
|
||||
"url": "https://github.com/mozilla/testpilot-containers/issues"
|
||||
|
@ -16,7 +16,9 @@
|
|||
"eslint-plugin-promise": "^3.4.0",
|
||||
"htmllint-cli": "^0.0.5",
|
||||
"jpm": "^1.2.2",
|
||||
"json": "^9.0.6",
|
||||
"npm-run-all": "^4.0.0",
|
||||
"shield-studies-addon-utils": "^2.0.0",
|
||||
"stylelint": "^7.9.0",
|
||||
"stylelint-config-standard": "^16.0.0",
|
||||
"stylelint-order": "^0.3.0",
|
||||
|
@ -41,6 +43,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"build": "npm test && jpm xpi",
|
||||
"build-shield": "npm test && npm run package-shield",
|
||||
"deploy": "deploy-txp",
|
||||
"lint": "npm-run-all lint:*",
|
||||
"lint:addon": "addons-linter webextension --self-hosted",
|
||||
|
@ -48,6 +51,7 @@
|
|||
"lint:html": "htmllint webextension/*.html",
|
||||
"lint:js": "eslint .",
|
||||
"package": "npm run build && mv testpilot-containers.xpi addon.xpi",
|
||||
"package-shield": "./node_modules/.bin/json -I -f package.json -e 'this.name=\"shield-study-containers\"' && jpm xpi && ./node_modules/.bin/json -I -f package.json -e 'this.name=\"testpilot-containers\"'",
|
||||
"test": "npm run lint"
|
||||
},
|
||||
"updateURL": "https://testpilot.firefox.com/files/@testpilot-containers/updates.json"
|
||||
|
|
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
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
const Experiment = require('./lib/testpilot/experiment');
|
||||
|
||||
const experiment = new Experiment();
|
||||
|
||||
/**
|
||||
* Class that represents a metrics event broker. Events are sent to Google
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const MAJOR_VERSIONS = ["2.3.0"];
|
||||
const MAJOR_VERSIONS = ["2.3.0", "2.4.0"];
|
||||
const LOOKUP_KEY = "$ref";
|
||||
|
||||
const assignManager = {
|
||||
|
@ -6,6 +6,7 @@ const assignManager = {
|
|||
MENU_REMOVE_ID: "remove-open-in-this-container",
|
||||
storageArea: {
|
||||
area: browser.storage.local,
|
||||
exemptedTabs: {},
|
||||
|
||||
getSiteStoreKey(pageUrl) {
|
||||
const url = new window.URL(pageUrl);
|
||||
|
@ -13,6 +14,27 @@ const assignManager = {
|
|||
return `${storagePrefix}${url.hostname}`;
|
||||
},
|
||||
|
||||
setExempted(pageUrl, tabId) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
if (!(siteStoreKey in this.exemptedTabs)) {
|
||||
this.exemptedTabs[siteStoreKey] = [];
|
||||
}
|
||||
this.exemptedTabs[siteStoreKey].push(tabId);
|
||||
},
|
||||
|
||||
removeExempted(pageUrl) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
this.exemptedTabs[siteStoreKey] = [];
|
||||
},
|
||||
|
||||
isExempted(pageUrl, tabId) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
if (!(siteStoreKey in this.exemptedTabs)) {
|
||||
return false;
|
||||
}
|
||||
return this.exemptedTabs[siteStoreKey].includes(tabId);
|
||||
},
|
||||
|
||||
get(pageUrl) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -27,8 +49,13 @@ const assignManager = {
|
|||
});
|
||||
},
|
||||
|
||||
set(pageUrl, data) {
|
||||
set(pageUrl, data, exemptedTabIds) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
if (exemptedTabIds) {
|
||||
exemptedTabIds.forEach((tabId) => {
|
||||
this.setExempted(pageUrl, tabId);
|
||||
});
|
||||
}
|
||||
return this.area.set({
|
||||
[siteStoreKey]: data
|
||||
});
|
||||
|
@ -36,22 +63,30 @@ const assignManager = {
|
|||
|
||||
remove(pageUrl) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
// When we remove an assignment we should clear all the exemptions
|
||||
this.removeExempted(pageUrl);
|
||||
return this.area.remove([siteStoreKey]);
|
||||
},
|
||||
|
||||
deleteContainer(userContextId) {
|
||||
const removeKeys = [];
|
||||
this.area.get().then((siteConfigs) => {
|
||||
async deleteContainer(userContextId) {
|
||||
const sitesByContainer = await this.getByContainer(userContextId);
|
||||
this.area.remove(Object.keys(sitesByContainer));
|
||||
},
|
||||
|
||||
async getByContainer(userContextId) {
|
||||
const sites = {};
|
||||
const siteConfigs = await this.area.get();
|
||||
Object.keys(siteConfigs).forEach((key) => {
|
||||
// For some reason this is stored as string... lets check them both as that
|
||||
if (String(siteConfigs[key].userContextId) === String(userContextId)) {
|
||||
removeKeys.push(key);
|
||||
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;
|
||||
}
|
||||
});
|
||||
this.area.remove(removeKeys);
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
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() {
|
||||
browser.contextMenus.onClicked.addListener((info, tab) => {
|
||||
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
||||
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
|
||||
if (userContextId) {
|
||||
let actionName;
|
||||
let storageAction;
|
||||
if (info.menuItemId === this.MENU_ASSIGN_ID) {
|
||||
actionName = "added";
|
||||
storageAction = this.storageArea.set(info.pageUrl, {
|
||||
userContextId,
|
||||
neverAsk: false
|
||||
});
|
||||
} else {
|
||||
actionName = "removed";
|
||||
storageAction = this.storageArea.remove(info.pageUrl);
|
||||
}
|
||||
storageAction.then(() => {
|
||||
browser.notifications.create({
|
||||
type: "basic",
|
||||
title: "Containers",
|
||||
message: `Successfully ${actionName} site to always open in this container`,
|
||||
iconUrl: browser.extension.getURL("/img/onboarding-1.png")
|
||||
});
|
||||
backgroundLogic.sendTelemetryPayload({
|
||||
event: `${actionName}-container-assignment`,
|
||||
userContextId: userContextId,
|
||||
});
|
||||
this.calculateContextMenu(tab);
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
this._onClickedHandler(info, tab);
|
||||
});
|
||||
|
||||
// Before a request is handled by the browser we decide if we should route through a different container
|
||||
|
@ -110,6 +122,7 @@ const assignManager = {
|
|||
if (options.frameId !== 0 || options.tabId === -1) {
|
||||
return {};
|
||||
}
|
||||
this.removeContextMenu();
|
||||
return Promise.all([
|
||||
browser.tabs.get(options.tabId),
|
||||
this.storageArea.get(options.url)
|
||||
|
@ -117,11 +130,12 @@ const assignManager = {
|
|||
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
||||
if (!siteSettings
|
||||
|| userContextId === siteSettings.userContextId
|
||||
|| tab.incognito) {
|
||||
|| tab.incognito
|
||||
|| this.storageArea.isExempted(options.url, tab.id)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
this.reloadPageInContainer(options.url, siteSettings.userContextId, tab.index + 1, siteSettings.neverAsk);
|
||||
this.reloadPageInContainer(options.url, userContextId, siteSettings.userContextId, tab.index + 1, siteSettings.neverAsk);
|
||||
this.calculateContextMenu(tab);
|
||||
|
||||
/* Removal of existing tabs:
|
||||
|
@ -149,6 +163,21 @@ const assignManager = {
|
|||
},{urls: ["<all_urls>"], types: ["main_frame"]}, ["blocking"]);
|
||||
},
|
||||
|
||||
async _onClickedHandler(info, tab) {
|
||||
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
||||
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
|
||||
if (userContextId) {
|
||||
// let actionName;
|
||||
let remove;
|
||||
if (info.menuItemId === this.MENU_ASSIGN_ID) {
|
||||
remove = false;
|
||||
} else {
|
||||
remove = true;
|
||||
}
|
||||
await this._setOrRemoveAssignment(tab.id, info.pageUrl, userContextId, remove);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
deleteContainer(userContextId) {
|
||||
this.storageArea.deleteContainer(userContextId);
|
||||
|
@ -171,29 +200,94 @@ const assignManager = {
|
|||
// Ensure we are not in incognito mode
|
||||
const url = new URL(tab.url);
|
||||
if (url.protocol === "about:"
|
||||
|| url.protocol === "moz-extension:"
|
||||
|| tab.incognito) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
calculateContextMenu(tab) {
|
||||
async _setOrRemoveAssignment(tabId, pageUrl, userContextId, remove) {
|
||||
let actionName;
|
||||
|
||||
// https://github.com/mozilla/testpilot-containers/issues/626
|
||||
// Context menu has stored context IDs as strings, so we need to coerce
|
||||
// the value to a string for accurate checking
|
||||
userContextId = String(userContextId);
|
||||
|
||||
if (!remove) {
|
||||
const tabs = await browser.tabs.query({});
|
||||
const assignmentStoreKey = this.storageArea.getSiteStoreKey(pageUrl);
|
||||
const exemptedTabIds = tabs.filter((tab) => {
|
||||
const tabStoreKey = this.storageArea.getSiteStoreKey(tab.url);
|
||||
/* Auto exempt all tabs that exist for this hostname that are not in the same container */
|
||||
if (tabStoreKey === assignmentStoreKey &&
|
||||
this.getUserContextIdFromCookieStore(tab) !== userContextId) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}).map((tab) => {
|
||||
return tab.id;
|
||||
});
|
||||
|
||||
await this.storageArea.set(pageUrl, {
|
||||
userContextId,
|
||||
neverAsk: false
|
||||
}, exemptedTabIds);
|
||||
actionName = "added";
|
||||
} else {
|
||||
await this.storageArea.remove(pageUrl);
|
||||
actionName = "removed";
|
||||
}
|
||||
browser.tabs.sendMessage(tabId, {
|
||||
text: `Successfully ${actionName} site to always open in this container`
|
||||
});
|
||||
backgroundLogic.sendTelemetryPayload({
|
||||
event: `${actionName}-container-assignment`,
|
||||
userContextId: userContextId,
|
||||
});
|
||||
const tab = await browser.tabs.get(tabId);
|
||||
this.calculateContextMenu(tab);
|
||||
},
|
||||
|
||||
async _getAssignment(tab) {
|
||||
const cookieStore = this.getUserContextIdFromCookieStore(tab);
|
||||
// Ensure we have a cookieStore to assign to
|
||||
if (cookieStore
|
||||
&& this.isTabPermittedAssign(tab)) {
|
||||
return await this.storageArea.get(tab.url);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
_getByContainer(userContextId) {
|
||||
return this.storageArea.getByContainer(userContextId);
|
||||
},
|
||||
|
||||
removeContextMenu() {
|
||||
// There is a focus issue in this menu where if you change window with a context menu click
|
||||
// you get the wrong menu display because of async
|
||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1215376#c16
|
||||
// We also can't change for always private mode
|
||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1352102
|
||||
const cookieStore = this.getUserContextIdFromCookieStore(tab);
|
||||
browser.contextMenus.remove(this.MENU_ASSIGN_ID);
|
||||
browser.contextMenus.remove(this.MENU_REMOVE_ID);
|
||||
// Ensure we have a cookieStore to assign to
|
||||
if (cookieStore
|
||||
&& this.isTabPermittedAssign(tab)) {
|
||||
this.storageArea.get(tab.url).then((siteSettings) => {
|
||||
},
|
||||
|
||||
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;
|
||||
if (siteSettings) {
|
||||
const tabUserContextId = this.getUserContextIdFromCookieStore(tab);
|
||||
if (siteSettings &&
|
||||
Number(siteSettings.userContextId) === Number(tabUserContextId)) {
|
||||
prefix = "✓";
|
||||
menuId = this.MENU_REMOVE_ID;
|
||||
}
|
||||
|
@ -203,17 +297,15 @@ const assignManager = {
|
|||
checked: true,
|
||||
contexts: ["all"],
|
||||
});
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
reloadPageInContainer(url, userContextId, index, neverAsk = false) {
|
||||
reloadPageInContainer(url, currentUserContextId, userContextId, index, neverAsk = false) {
|
||||
const cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
|
||||
const loadPage = browser.extension.getURL("confirm-page.html");
|
||||
// False represents assignment is not permitted
|
||||
// If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there
|
||||
if (neverAsk) {
|
||||
browser.tabs.create({url, cookieStoreId: backgroundLogic.cookieStoreId(userContextId), index});
|
||||
browser.tabs.create({url, cookieStoreId, index});
|
||||
backgroundLogic.sendTelemetryPayload({
|
||||
event: "auto-reload-page-in-container",
|
||||
userContextId: userContextId,
|
||||
|
@ -223,8 +315,17 @@ const assignManager = {
|
|||
event: "prompt-to-reload-page-in-container",
|
||||
userContextId: userContextId,
|
||||
});
|
||||
const confirmUrl = `${loadPage}?url=${url}`;
|
||||
browser.tabs.create({url: confirmUrl, cookieStoreId: backgroundLogic.cookieStoreId(userContextId), index}).then(() => {
|
||||
let confirmUrl = `${loadPage}?url=${encodeURIComponent(url)}&cookieStoreId=${cookieStoreId}`;
|
||||
let currentCookieStoreId;
|
||||
if (currentUserContextId) {
|
||||
currentCookieStoreId = backgroundLogic.cookieStoreId(currentUserContextId);
|
||||
confirmUrl += `¤tCookieStoreId=${currentCookieStoreId}`;
|
||||
}
|
||||
browser.tabs.create({
|
||||
url: confirmUrl,
|
||||
cookieStoreId: currentCookieStoreId,
|
||||
index
|
||||
}).then(() => {
|
||||
// We don't want to sync this URL ever nor clutter the users history
|
||||
browser.history.deleteUrl({url: confirmUrl});
|
||||
}).catch((e) => {
|
||||
|
@ -271,7 +372,7 @@ const backgroundLogic = {
|
|||
|
||||
createOrUpdateContainer(options) {
|
||||
let donePromise;
|
||||
if (options.userContextId) {
|
||||
if (options.userContextId !== "new") {
|
||||
donePromise = browser.contextualIdentities.update(
|
||||
this.cookieStoreId(options.userContextId),
|
||||
options.params
|
||||
|
@ -354,7 +455,7 @@ const messageHandler = {
|
|||
LAST_CREATED_TAB_TIMER: 2000,
|
||||
|
||||
init() {
|
||||
// Handles messages from webextension/js/popup.js
|
||||
// Handles messages from webextension code
|
||||
browser.runtime.onMessage.addListener((m) => {
|
||||
let response;
|
||||
|
||||
|
@ -372,11 +473,29 @@ const messageHandler = {
|
|||
case "neverAsk":
|
||||
assignManager._neverAsk(m);
|
||||
break;
|
||||
case "getAssignment":
|
||||
response = browser.tabs.get(m.tabId).then((tab) => {
|
||||
return assignManager._getAssignment(tab);
|
||||
});
|
||||
break;
|
||||
case "getAssignmentObjectByContainer":
|
||||
response = assignManager._getByContainer(m.message.userContextId);
|
||||
break;
|
||||
case "setOrRemoveAssignment":
|
||||
// m.tabId is used for where to place the in content message
|
||||
// m.url is the assignment to be removed/added
|
||||
response = browser.tabs.get(m.tabId).then((tab) => {
|
||||
return assignManager._setOrRemoveAssignment(tab.id, m.url, m.userContextId, m.value);
|
||||
});
|
||||
break;
|
||||
case "exemptContainerAssignment":
|
||||
response = assignManager._exemptTab(m);
|
||||
break;
|
||||
}
|
||||
return response;
|
||||
});
|
||||
|
||||
// Handles messages from index.js
|
||||
// Handles messages from sdk code
|
||||
const port = browser.runtime.connect();
|
||||
port.onMessage.addListener(m => {
|
||||
switch (m.type) {
|
||||
|
@ -408,6 +527,7 @@ const messageHandler = {
|
|||
});
|
||||
|
||||
browser.tabs.onActivated.addListener((info) => {
|
||||
assignManager.removeContextMenu();
|
||||
browser.tabs.get(info.tabId).then((tab) => {
|
||||
tabPageCounter.initTabCounter(tab);
|
||||
assignManager.calculateContextMenu(tab);
|
||||
|
@ -417,6 +537,12 @@ const messageHandler = {
|
|||
});
|
||||
|
||||
browser.windows.onFocusChanged.addListener((windowId) => {
|
||||
assignManager.removeContextMenu();
|
||||
// browserAction loses background color in new windows ...
|
||||
// https://bugzil.la/1314674
|
||||
// https://github.com/mozilla/testpilot-containers/issues/608
|
||||
// ... so re-call displayBrowserActionBadge on window changes
|
||||
displayBrowserActionBadge();
|
||||
browser.tabs.query({active: true, windowId}).then((tabs) => {
|
||||
if (tabs && tabs[0]) {
|
||||
tabPageCounter.initTabCounter(tabs[0]);
|
||||
|
@ -445,6 +571,7 @@ const messageHandler = {
|
|||
if (details.frameId !== 0 || details.tabId === -1) {
|
||||
return {};
|
||||
}
|
||||
assignManager.removeContextMenu();
|
||||
|
||||
browser.tabs.get(details.tabId).then((tab) => {
|
||||
tabPageCounter.incrementTabCount(tab);
|
||||
|
|
|
@ -8,26 +8,29 @@
|
|||
<body>
|
||||
<main>
|
||||
<div class="title">
|
||||
<h1 class="title-text">Should we open this in your container?</h1>
|
||||
<h1 class="title-text">Open this site in your assigned container?</h1>
|
||||
</div>
|
||||
<form id="redirect-form">
|
||||
<p>
|
||||
Looks like you requested:
|
||||
You asked <dfn id="browser-name" title="Thanks for trying out Containers. Sorry we may have got your browser name wrong. #FxNightly" >Firefox</dfn> to always open <dfn class="container-name"></dfn> for this site:<br />
|
||||
</p>
|
||||
<div id="redirect-url"></div>
|
||||
<p>
|
||||
You asked <dfn id="browser-name" title="Thanks for trying out Containers. Sorry we may have got your browser name wrong. #FxNightly" >Firefox</dfn> to always open <dfn id="redirect-site"></dfn> in <dfn>this</dfn> type of container. Would you like to proceed?<br />
|
||||
</p>
|
||||
<p>Would you still like to open in this current container?</p>
|
||||
<br />
|
||||
<br />
|
||||
<input id="never-ask" type="checkbox" /><label for="never-ask">Remember my decision for this site</label>
|
||||
<label for="never-ask" class="check-label">
|
||||
<input id="never-ask" type="checkbox" />
|
||||
Remember my decision for this site
|
||||
</label>
|
||||
<br />
|
||||
<div class="button-container">
|
||||
<button id="confirm" class="button primary" autofocus>Take me there</button>
|
||||
<button id="deny" class="button">Open in <dfn id="current-container-name">Current</dfn> Container</button>
|
||||
<button id="confirm" class="button primary" autofocus>Open in <dfn class="container-name"></dfn> Container</button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<script src="js/utils.js"></script>
|
||||
<script src="js/confirm-page.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -4,11 +4,21 @@
|
|||
}
|
||||
|
||||
main {
|
||||
background: url(/img/onboarding-1.png) no-repeat;
|
||||
background: url(/img/onboarding-4.png) no-repeat;
|
||||
background-position: -10px -15px;
|
||||
background-size: 285px;
|
||||
margin-inline-start: -285px;
|
||||
padding-inline-start: 285px;
|
||||
background-size: 300px;
|
||||
margin-inline-start: -350px;
|
||||
padding-inline-start: 350px;
|
||||
}
|
||||
|
||||
.container-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
button .container-name,
|
||||
#current-container-name {
|
||||
font-weight: bold;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1300px) {
|
||||
|
@ -36,6 +46,33 @@ html {
|
|||
word-break: break-all;
|
||||
}
|
||||
|
||||
#redirect-url {
|
||||
background: #efefef;
|
||||
border-radius: 2px;
|
||||
line-height: 1.5;
|
||||
padding-block-end: 0.5rem;
|
||||
padding-block-start: 0.5rem;
|
||||
padding-inline-end: 0.5rem;
|
||||
padding-inline-start: 0.5rem;
|
||||
}
|
||||
|
||||
#redirect-url img {
|
||||
block-size: 16px;
|
||||
inline-size: 16px;
|
||||
margin-inline-end: 6px;
|
||||
offset-block-start: 3px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
dfn {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.button-container > button {
|
||||
min-inline-size: 240px;
|
||||
}
|
||||
|
||||
.check-label {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
|
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 */
|
||||
body {
|
||||
inline-size: 300px;
|
||||
max-inline-size: 300px;
|
||||
* {
|
||||
font-size: inherit;
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: 0;
|
||||
padding-block-end: 0;
|
||||
padding-block-start: 0;
|
||||
padding-inline-end: 0;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
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;
|
||||
}
|
||||
|
||||
form {
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
border: 0;
|
||||
border-spacing: 0;
|
||||
|
@ -30,11 +81,27 @@ table {
|
|||
}
|
||||
|
||||
.scrollable {
|
||||
border-block-start: 1px solid #f1f1f1;
|
||||
inline-size: 100%;
|
||||
max-block-size: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.offpage {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Effect borrowed from tabs in Firefox, ensure that the element flexes to the full width */
|
||||
.truncate-text {
|
||||
mask-image: linear-gradient(to left, transparent, black 1em);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Color and icon helpers */
|
||||
[data-identity-color="blue"] {
|
||||
--identity-tab-color: #37adff;
|
||||
|
@ -51,6 +118,11 @@ table {
|
|||
--identity-icon-color: #51cd00;
|
||||
}
|
||||
|
||||
[data-identity-color="grey"] {
|
||||
/* Only used for the edit panel */
|
||||
--identity-icon-color: #616161;
|
||||
}
|
||||
|
||||
[data-identity-color="yellow"] {
|
||||
--identity-tab-color: #ffcb00;
|
||||
--identity-icon-color: #ffcb00;
|
||||
|
@ -124,7 +196,16 @@ table {
|
|||
--identity-icon: url("/img/usercontext.svg#chill");
|
||||
}
|
||||
|
||||
#current-tab [data-identity-icon="default-tab"] {
|
||||
background: center center no-repeat url("/img/blank-tab.svg");
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.button {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.button.primary {
|
||||
background-color: #0996f8;
|
||||
color: white;
|
||||
|
@ -140,6 +221,18 @@ table {
|
|||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Text links with actions */
|
||||
|
||||
.action-link:link {
|
||||
color: var(--primary-action-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.action-link:active,
|
||||
.action-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Panels keep everything togethert */
|
||||
.panel {
|
||||
display: flex;
|
||||
|
@ -183,7 +276,7 @@ table {
|
|||
.column-panel-content .button,
|
||||
.panel-footer .button {
|
||||
align-items: center;
|
||||
block-size: 54px;
|
||||
block-size: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
|
@ -223,7 +316,7 @@ table {
|
|||
|
||||
.onboarding-title {
|
||||
color: #43484e;
|
||||
font-size: 16px;
|
||||
font-size: var(--font-size-heading);
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
margin-inline-end: 0;
|
||||
|
@ -232,7 +325,7 @@ table {
|
|||
}
|
||||
|
||||
.onboarding p {
|
||||
color: #4a4a4a;
|
||||
color: var(--text-normal-color);
|
||||
font-size: 14px;
|
||||
margin-block-end: 16px;
|
||||
max-inline-size: 84%;
|
||||
|
@ -261,9 +354,10 @@ table {
|
|||
manage things like container crud */
|
||||
.pop-button {
|
||||
align-items: center;
|
||||
block-size: 48px;
|
||||
block-size: var(--icon-button-size);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex: 0 0 48px;
|
||||
flex: 0 0 var(--icon-button-size);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
@ -288,6 +382,10 @@ manage things like container crud */
|
|||
.pop-button-image {
|
||||
block-size: 20px;
|
||||
flex: 0 0 20px;
|
||||
margin-block-end: auto;
|
||||
margin-block-start: auto;
|
||||
margin-inline-end: auto;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.pop-button-image-small {
|
||||
|
@ -299,20 +397,23 @@ manage things like container crud */
|
|||
.panel-header {
|
||||
align-items: center;
|
||||
block-size: 48px;
|
||||
border-block-end: 1px solid #ebebeb;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.panel-header .usercontext-icon {
|
||||
inline-size: var(--icon-button-size);
|
||||
}
|
||||
|
||||
.column-panel-content .panel-header {
|
||||
flex: 0 0 48px;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.panel-header-text {
|
||||
color: #4a4a4a;
|
||||
color: var(--text-normal-color);
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
font-size: var(--font-size-heading);
|
||||
font-weight: normal;
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
|
@ -324,6 +425,47 @@ manage things like container crud */
|
|||
padding-inline-start: 16px;
|
||||
}
|
||||
|
||||
#container-panel .panel-header {
|
||||
background-color: #efefef;
|
||||
block-size: 26px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#container-panel .panel-header-text {
|
||||
color: #727272;
|
||||
font-size: 14px;
|
||||
padding-block-end: 0;
|
||||
padding-block-start: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.container-panel-controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-block-end: var(--block-line-space-size);
|
||||
margin-block-start: var(--block-line-space-size);
|
||||
margin-inline-end: var(--inline-item-space-size);
|
||||
margin-inline-start: var(--inline-item-space-size);
|
||||
}
|
||||
|
||||
#container-panel #sort-containers-link {
|
||||
align-items: center;
|
||||
block-size: var(--block-url-label-size);
|
||||
border: 1px solid #d8d8d8;
|
||||
border-radius: var(--small-radius);
|
||||
color: var(--title-text-color);
|
||||
display: flex;
|
||||
font-size: var(--small-text-size);
|
||||
inline-size: var(--inline-button-size);
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#container-panel #sort-containers-link:hover,
|
||||
#container-panel #sort-containers-link:focus {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
span ~ .panel-header-text {
|
||||
padding-block-end: 0;
|
||||
padding-block-start: 0;
|
||||
|
@ -331,11 +473,92 @@ span ~ .panel-header-text {
|
|||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
#current-tab {
|
||||
align-items: center;
|
||||
color: var(--text-normal-color);
|
||||
display: grid;
|
||||
font-size: var(--small-text-size);
|
||||
grid-column-gap: var(--inline-item-space-size);
|
||||
grid-row-gap: var(--block-line-space-size);
|
||||
grid-template-columns: var(--icon-size) var(--icon-size) 1fr;
|
||||
margin-block-end: var(--block-line-space-size);
|
||||
margin-block-start: var(--block-line-separation-size);
|
||||
margin-inline-end: var(--inline-start-size);
|
||||
margin-inline-start: var(--inline-start-size);
|
||||
max-inline-size: 100%;
|
||||
}
|
||||
|
||||
#current-tab img {
|
||||
max-block-size: var(--icon-size);
|
||||
}
|
||||
|
||||
#current-tab > h3 {
|
||||
color: var(--text-heading-color);
|
||||
font-weight: normal;
|
||||
grid-column: span 3;
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
#current-page {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
#current-tab .page-title {
|
||||
font-size: var(--font-size-heading);
|
||||
grid-column: 2 / 4;
|
||||
}
|
||||
|
||||
#current-tab > label {
|
||||
display: contents;
|
||||
font-size: var(--small-text-size);
|
||||
}
|
||||
|
||||
#current-tab > label > input {
|
||||
-moz-appearance: none;
|
||||
block-size: var(--icon-size);
|
||||
border: 1px solid #d8d8d8;
|
||||
border-radius: var(--small-radius);
|
||||
display: block;
|
||||
grid-column-start: 2;
|
||||
inline-size: var(--icon-size);
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
#current-tab > label > input[disabled] {
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
#current-tab > label > input:checked {
|
||||
background-image: url("chrome://global/skin/in-content/check.svg#check-native");
|
||||
background-position: -1px -1px;
|
||||
background-size: var(--icon-size);
|
||||
}
|
||||
|
||||
#current-container {
|
||||
color: var(--identity-tab-color);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#current-tab > label > .usercontext-icon {
|
||||
background-size: 16px;
|
||||
block-size: 16px;
|
||||
display: block;
|
||||
flex: 0 0 20px;
|
||||
inline-size: 20px;
|
||||
margin-inline-end: 3px;
|
||||
margin-inline-start: 3px;
|
||||
}
|
||||
|
||||
/* Rows used when iterating over panels */
|
||||
.container-panel-row {
|
||||
align-items: center;
|
||||
background-color: #fefefe !important;
|
||||
block-size: 48px;
|
||||
border-block-end: 1px solid #f1f1f1;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
|
@ -343,12 +566,10 @@ span ~ .panel-header-text {
|
|||
}
|
||||
|
||||
.container-panel-row .container-name {
|
||||
flex: 1;
|
||||
max-inline-size: 160px;
|
||||
overflow: hidden;
|
||||
padding-inline-end: 4px;
|
||||
padding-inline-start: 4px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.edit-containers-panel .userContext-wrapper {
|
||||
|
@ -368,8 +589,9 @@ span ~ .panel-header-text {
|
|||
}
|
||||
|
||||
.userContext-icon-wrapper {
|
||||
block-size: 48px;
|
||||
flex: 0 0 48px;
|
||||
block-size: var(--icon-button-size);
|
||||
flex: 0 0 var(--icon-button-size);
|
||||
margin-inline-start: var(--inline-icon-space-size);
|
||||
}
|
||||
|
||||
/* .userContext-icon is used natively, Bug 1333811 was raised to fix */
|
||||
|
@ -378,24 +600,29 @@ span ~ .panel-header-text {
|
|||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 20px 20px;
|
||||
block-size: 48px;
|
||||
block-size: 100%;
|
||||
fill: var(--identity-icon-color);
|
||||
filter: url('/img/filters.svg#fill');
|
||||
flex: 0 0 48px;
|
||||
}
|
||||
|
||||
.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');
|
||||
fill: 'gray';
|
||||
fill: #979797;
|
||||
filter: url('/img/filters.svg#fill');
|
||||
}
|
||||
|
||||
.container-panel-row .clickable:hover .usercontext-icon,
|
||||
.container-panel-row .clickable:focus .usercontext-icon {
|
||||
fill: #0094fb;
|
||||
}
|
||||
|
||||
/* Panel Footer */
|
||||
.panel-footer {
|
||||
align-items: center;
|
||||
background: #efefef;
|
||||
block-size: 54px;
|
||||
block-size: var(--icon-button-size);
|
||||
border-block-end: 1px solid #d8d8d8;
|
||||
color: #000;
|
||||
display: flex;
|
||||
|
@ -404,14 +631,9 @@ span ~ .panel-header-text {
|
|||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.panel-footer .pop-button {
|
||||
block-size: 54px;
|
||||
flex: 0 0 54px;
|
||||
}
|
||||
|
||||
.edit-containers-text {
|
||||
align-items: center;
|
||||
block-size: 54px;
|
||||
block-size: 100%;
|
||||
border-inline-end: solid 1px #d8d8d8;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
@ -420,7 +642,7 @@ span ~ .panel-header-text {
|
|||
|
||||
.edit-containers-text a {
|
||||
align-items: center;
|
||||
block-size: 54px;
|
||||
block-size: 100%;
|
||||
color: #0a0a0a;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
@ -428,11 +650,8 @@ span ~ .panel-header-text {
|
|||
}
|
||||
|
||||
/* Container info list */
|
||||
#container-info-name {
|
||||
margin-inline-end: 0.5rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
.container-info-tab-title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#container-info-hideorshow {
|
||||
|
@ -477,19 +696,19 @@ span ~ .panel-header-text {
|
|||
|
||||
.container-info-tab-row td {
|
||||
max-inline-size: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.container-info-list {
|
||||
border-block-start: 1px solid #ebebeb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-block-start: 4px;
|
||||
padding-block-start: 4px;
|
||||
}
|
||||
|
||||
.container-info-list tbody {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -501,7 +720,7 @@ span ~ .panel-header-text {
|
|||
|
||||
.edit-containers-exit-text {
|
||||
align-items: center;
|
||||
background: #248aeb;
|
||||
background: var(--primary-action-color);
|
||||
block-size: 100%;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
|
@ -528,7 +747,7 @@ span ~ .panel-header-text {
|
|||
|
||||
.delete-container-confirm-title {
|
||||
color: #000;
|
||||
font-size: 16px;
|
||||
font-size: var(--font-size-heading);
|
||||
}
|
||||
|
||||
/* Form info */
|
||||
|
@ -540,36 +759,95 @@ span ~ .panel-header-text {
|
|||
padding-inline-start: 16px;
|
||||
}
|
||||
|
||||
.column-panel-content form span {
|
||||
align-items: center;
|
||||
block-size: 44px;
|
||||
display: flex;
|
||||
flex: 0 0 25%;
|
||||
justify-content: center;
|
||||
#edit-sites-assigned {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.edit-container-panel label {
|
||||
#edit-sites-assigned h3 {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
padding-block-end: 6px;
|
||||
padding-block-start: 6px;
|
||||
padding-inline-end: 16px;
|
||||
padding-inline-start: 16px;
|
||||
}
|
||||
|
||||
.assigned-sites-list > div {
|
||||
display: flex;
|
||||
padding-block-end: 6px;
|
||||
padding-block-start: 6px;
|
||||
}
|
||||
|
||||
.assigned-sites-list > div > .icon {
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
|
||||
.assigned-sites-list > div > .delete-assignment {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.assigned-sites-list > div:hover > .delete-assignment {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.assigned-sites-list > div > .hostname {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.radio-choice > .radio-container {
|
||||
align-items: center;
|
||||
block-size: 29px;
|
||||
display: flex;
|
||||
flex: 0 0 calc(100% / 8);
|
||||
}
|
||||
|
||||
.radio-choice > .radio-container > label {
|
||||
background: none;
|
||||
block-size: 23px;
|
||||
border: 0;
|
||||
filter: none;
|
||||
inline-size: 23px;
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: 0;
|
||||
padding-block-end: 0;
|
||||
padding-block-start: 0;
|
||||
padding-inline-end: 0;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
.radio-choice > .radio-container > label::before {
|
||||
background-color: unset;
|
||||
background-image: var(--identity-icon);
|
||||
background-size: 26px 26px;
|
||||
block-size: 34px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 16px;
|
||||
block-size: 23px;
|
||||
border: none;
|
||||
content: "";
|
||||
display: block;
|
||||
fill: var(--identity-icon-color);
|
||||
filter: url('/img/filters.svg#fill');
|
||||
flex: 0 0 34px;
|
||||
inline-size: 23px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.edit-container-panel label::before {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
.edit-container-panel [type="radio"] {
|
||||
.radio-choice > .radio-container > [type="radio"] {
|
||||
-moz-appearance: none;
|
||||
display: inline;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.edit-container-panel [type="radio"]:checked + label {
|
||||
outline: 2px solid grey;
|
||||
-moz-outline-radius: 50px;
|
||||
.radio-choice > .radio-container > [type="radio"]:checked + label {
|
||||
background: #d3d3d3;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
/* When focusing the element add a thin blue highlight to match input fields. This gives a distinction to other selected radio items */
|
||||
.radio-choice > .radio-container > [type="radio"]:focus + label {
|
||||
outline: 1px solid #1f9ffc;
|
||||
-moz-outline-radius: 100%;
|
||||
}
|
||||
|
||||
.edit-container-panel fieldset {
|
||||
|
@ -588,6 +866,10 @@ span ~ .panel-header-text {
|
|||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
.edit-container-panel fieldset:last-of-type {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.edit-container-panel input[type="text"] {
|
||||
block-size: 36px;
|
||||
border-radius: 3px;
|
||||
|
@ -602,5 +884,5 @@ span ~ .panel-header-text {
|
|||
.edit-container-panel legend {
|
||||
flex: 1 0;
|
||||
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");
|
||||
document.getElementById("redirect-url").textContent = redirectUrl;
|
||||
const redirectSite = new URL(redirectUrl).hostname;
|
||||
document.getElementById("redirect-site").textContent = redirectSite;
|
||||
async function load() {
|
||||
const searchParams = new URL(window.location).searchParams;
|
||||
const redirectUrl = decodeURIComponent(searchParams.get("url"));
|
||||
const cookieStoreId = searchParams.get("cookieStoreId");
|
||||
const currentCookieStoreId = searchParams.get("currentCookieStoreId");
|
||||
const redirectUrlElement = document.getElementById("redirect-url");
|
||||
redirectUrlElement.textContent = redirectUrl;
|
||||
appendFavicon(redirectUrl, redirectUrlElement);
|
||||
|
||||
document.getElementById("redirect-form").addEventListener("submit", (e) => {
|
||||
const container = await browser.contextualIdentities.get(cookieStoreId);
|
||||
[...document.querySelectorAll(".container-name")].forEach((containerNameElement) => {
|
||||
containerNameElement.textContent = container.name;
|
||||
});
|
||||
|
||||
// If default container, button will default to normal HTML content
|
||||
if (currentCookieStoreId) {
|
||||
const currentContainer = await browser.contextualIdentities.get(currentCookieStoreId);
|
||||
document.getElementById("current-container-name").textContent = currentContainer.name;
|
||||
}
|
||||
|
||||
document.getElementById("redirect-form").addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
const buttonTarget = e.explicitOriginalTarget;
|
||||
switch (buttonTarget.id) {
|
||||
case "confirm":
|
||||
confirmSubmit(redirectUrl, cookieStoreId);
|
||||
break;
|
||||
case "deny":
|
||||
denySubmit(redirectUrl);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function appendFavicon(pageUrl, redirectUrlElement) {
|
||||
const origin = new URL(pageUrl).origin;
|
||||
const favIconElement = Utils.createFavIconElement(`${origin}/favicon.ico`);
|
||||
|
||||
redirectUrlElement.prepend(favIconElement);
|
||||
}
|
||||
|
||||
function confirmSubmit(redirectUrl, cookieStoreId) {
|
||||
const neverAsk = document.getElementById("never-ask").checked;
|
||||
// Sending neverAsk message to background to store for next time we see this process
|
||||
if (neverAsk) {
|
||||
|
@ -12,20 +47,45 @@ document.getElementById("redirect-form").addEventListener("submit", (e) => {
|
|||
method: "neverAsk",
|
||||
neverAsk: true,
|
||||
pageUrl: redirectUrl
|
||||
}).then(() => {
|
||||
redirect();
|
||||
}).catch(() => {
|
||||
// Can't really do much here user will have to click it again
|
||||
});
|
||||
}
|
||||
browser.runtime.sendMessage({
|
||||
method: "sendTelemetryPayload",
|
||||
event: "click-to-reload-page-in-container",
|
||||
});
|
||||
redirect();
|
||||
});
|
||||
openInContainer(redirectUrl, cookieStoreId);
|
||||
}
|
||||
|
||||
function redirect() {
|
||||
const redirectUrl = document.getElementById("redirect-url").textContent;
|
||||
function getCurrentTab() {
|
||||
return browser.tabs.query({
|
||||
active: true,
|
||||
windowId: browser.windows.WINDOW_ID_CURRENT
|
||||
});
|
||||
}
|
||||
|
||||
async function denySubmit(redirectUrl) {
|
||||
const tab = await getCurrentTab();
|
||||
await browser.runtime.sendMessage({
|
||||
method: "exemptContainerAssignment",
|
||||
tabId: tab[0].id,
|
||||
pageUrl: redirectUrl
|
||||
});
|
||||
browser.runtime.sendMessage({
|
||||
method: "sendTelemetryPayload",
|
||||
event: "click-to-reload-page-in-same-container",
|
||||
});
|
||||
document.location.replace(redirectUrl);
|
||||
}
|
||||
|
||||
load();
|
||||
|
||||
async function openInContainer(redirectUrl, cookieStoreId) {
|
||||
const tab = await getCurrentTab();
|
||||
await browser.tabs.create({
|
||||
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_ICON = "circle";
|
||||
const NEW_CONTAINER_ID = "new";
|
||||
|
||||
const ONBOARDING_STORAGE_KEY = "onboarding-stage";
|
||||
|
||||
// List of panels
|
||||
const P_ONBOARDING_1 = "onboarding1";
|
||||
const P_ONBOARDING_2 = "onboarding2";
|
||||
const P_ONBOARDING_3 = "onboarding3";
|
||||
const P_ONBOARDING_4 = "onboarding4";
|
||||
const P_ONBOARDING_5 = "onboarding5";
|
||||
const P_CONTAINERS_LIST = "containersList";
|
||||
const P_CONTAINERS_EDIT = "containersEdit";
|
||||
const P_CONTAINER_INFO = "containerInfo";
|
||||
|
@ -69,32 +73,70 @@ const Logic = {
|
|||
_currentPanel: null,
|
||||
_previousPanel: null,
|
||||
_panels: {},
|
||||
_onboardingVariation: null,
|
||||
|
||||
init() {
|
||||
async init() {
|
||||
// Remove browserAction "upgraded" badge when opening panel
|
||||
this.clearBrowserActionBadge();
|
||||
|
||||
// Retrieve the list of identities.
|
||||
this.refreshIdentities()
|
||||
const identitiesPromise = this.refreshIdentities();
|
||||
// Get the onboarding variation
|
||||
const variationPromise = this.getShieldStudyVariation();
|
||||
|
||||
try {
|
||||
await Promise.all([identitiesPromise, variationPromise]);
|
||||
} catch(e) {
|
||||
throw new Error("Failed to retrieve the identities or variation. We cannot continue. ", e.message);
|
||||
}
|
||||
|
||||
// Routing to the correct panel.
|
||||
.then(() => {
|
||||
// If localStorage is disabled, we don't show the onboarding.
|
||||
if (!localStorage || localStorage.getItem("onboarded4")) {
|
||||
this.showPanel(P_CONTAINERS_LIST);
|
||||
const data = await browser.storage.local.get([ONBOARDING_STORAGE_KEY]);
|
||||
let onboarded = data[ONBOARDING_STORAGE_KEY];
|
||||
if (!onboarded) {
|
||||
// Legacy local storage used before panel 5
|
||||
if (localStorage.getItem("onboarded4")) {
|
||||
onboarded = 4;
|
||||
} else if (localStorage.getItem("onboarded3")) {
|
||||
this.showPanel(P_ONBOARDING_4);
|
||||
onboarded = 3;
|
||||
} else if (localStorage.getItem("onboarded2")) {
|
||||
this.showPanel(P_ONBOARDING_3);
|
||||
onboarded = 2;
|
||||
} else if (localStorage.getItem("onboarded1")) {
|
||||
this.showPanel(P_ONBOARDING_2);
|
||||
onboarded = 1;
|
||||
} else {
|
||||
this.showPanel(P_ONBOARDING_1);
|
||||
onboarded = 0;
|
||||
}
|
||||
this.setOnboardingStage(onboarded);
|
||||
}
|
||||
})
|
||||
|
||||
.catch(() => {
|
||||
throw new Error("Failed to retrieve the identities. We cannot continue.");
|
||||
switch (onboarded) {
|
||||
case 5:
|
||||
this.showPanel(P_CONTAINERS_LIST);
|
||||
break;
|
||||
case 4:
|
||||
this.showPanel(P_ONBOARDING_5);
|
||||
break;
|
||||
case 3:
|
||||
this.showPanel(P_ONBOARDING_4);
|
||||
break;
|
||||
case 2:
|
||||
this.showPanel(P_ONBOARDING_3);
|
||||
break;
|
||||
case 1:
|
||||
this.showPanel(P_ONBOARDING_2);
|
||||
break;
|
||||
case 0:
|
||||
default:
|
||||
this.showPanel(P_ONBOARDING_1);
|
||||
break;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
setOnboardingStage(stage) {
|
||||
return browser.storage.local.set({
|
||||
[ONBOARDING_STORAGE_KEY]: stage
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -107,8 +149,20 @@ const Logic = {
|
|||
browser.storage.local.set({browserActionBadgesClicked: storage.browserActionBadgesClicked});
|
||||
},
|
||||
|
||||
async identity(cookieStoreId) {
|
||||
const identity = await browser.contextualIdentities.get(cookieStoreId);
|
||||
return identity || {
|
||||
name: "Default",
|
||||
cookieStoreId,
|
||||
icon: "default-tab",
|
||||
color: "default-tab"
|
||||
};
|
||||
},
|
||||
|
||||
addEnterHandler(element, handler) {
|
||||
element.addEventListener("click", handler);
|
||||
element.addEventListener("click", (e) => {
|
||||
handler(e);
|
||||
});
|
||||
element.addEventListener("keydown", (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
handler(e);
|
||||
|
@ -121,6 +175,14 @@ const Logic = {
|
|||
return (userContextId !== cookieStoreId) ? Number(userContextId) : false;
|
||||
},
|
||||
|
||||
async currentTab() {
|
||||
const activeTabs = await browser.tabs.query({active: true, windowId: browser.windows.WINDOW_ID_CURRENT});
|
||||
if (activeTabs.length > 0) {
|
||||
return activeTabs[0];
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
refreshIdentities() {
|
||||
return Promise.all([
|
||||
browser.contextualIdentities.query({}),
|
||||
|
@ -139,7 +201,16 @@ const Logic = {
|
|||
}).catch((e) => {throw e;});
|
||||
},
|
||||
|
||||
showPanel(panel, currentIdentity = null) {
|
||||
getPanelSelector(panel) {
|
||||
if (this._onboardingVariation === "securityOnboarding" &&
|
||||
panel.hasOwnProperty("securityPanelSelector")) {
|
||||
return panel.securityPanelSelector;
|
||||
} else {
|
||||
return panel.panelSelector;
|
||||
}
|
||||
},
|
||||
|
||||
async showPanel(panel, currentIdentity = null) {
|
||||
// Invalid panel... ?!?
|
||||
if (!(panel in this._panels)) {
|
||||
throw new Error("Something really bad happened. Unknown panel: " + panel);
|
||||
|
@ -151,15 +222,18 @@ const Logic = {
|
|||
this._currentIdentity = currentIdentity;
|
||||
|
||||
// Initialize the panel before showing it.
|
||||
this._panels[panel].prepare().then(() => {
|
||||
for (let panelElement of document.querySelectorAll(".panel")) { // eslint-disable-line prefer-const
|
||||
await this._panels[panel].prepare();
|
||||
Object.keys(this._panels).forEach((panelKey) => {
|
||||
const panelItem = this._panels[panelKey];
|
||||
const panelElement = document.querySelector(this.getPanelSelector(panelItem));
|
||||
if (!panelElement.classList.contains("hide")) {
|
||||
panelElement.classList.add("hide");
|
||||
if ("unregister" in panelItem) {
|
||||
panelItem.unregister();
|
||||
}
|
||||
}
|
||||
document.querySelector(this._panels[panel].panelSelector).classList.remove("hide");
|
||||
})
|
||||
.catch(() => {
|
||||
throw new Error("Failed to show panel " + panel);
|
||||
});
|
||||
document.querySelector(this.getPanelSelector(this._panels[panel])).classList.remove("hide");
|
||||
},
|
||||
|
||||
showPreviousPanel() {
|
||||
|
@ -186,6 +260,11 @@ const Logic = {
|
|||
return this._currentIdentity;
|
||||
},
|
||||
|
||||
currentUserContextId() {
|
||||
const identity = Logic.currentIdentity();
|
||||
return Logic.userContextId(identity.cookieStoreId);
|
||||
},
|
||||
|
||||
sendTelemetryPayload(message = {}) {
|
||||
if (!message.event) {
|
||||
throw new Error("Missing event name for telemetry");
|
||||
|
@ -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() {
|
||||
const defaultName = "Container #";
|
||||
const ids = [];
|
||||
|
@ -233,14 +344,17 @@ const Logic = {
|
|||
|
||||
Logic.registerPanel(P_ONBOARDING_1, {
|
||||
panelSelector: ".onboarding-panel-1",
|
||||
securityPanelSelector: ".security-onboarding-panel-1",
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
// Let's move to the next panel.
|
||||
Logic.addEnterHandler(document.querySelector("#onboarding-start-button"), () => {
|
||||
localStorage.setItem("onboarded1", true);
|
||||
[...document.querySelectorAll(".onboarding-start-button")].forEach(startElement => {
|
||||
Logic.addEnterHandler(startElement, async function () {
|
||||
await Logic.setOnboardingStage(1);
|
||||
Logic.showPanel(P_ONBOARDING_2);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// This method is called when the panel is shown.
|
||||
|
@ -254,14 +368,17 @@ Logic.registerPanel(P_ONBOARDING_1, {
|
|||
|
||||
Logic.registerPanel(P_ONBOARDING_2, {
|
||||
panelSelector: ".onboarding-panel-2",
|
||||
securityPanelSelector: ".security-onboarding-panel-2",
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
// Let's move to the containers list panel.
|
||||
Logic.addEnterHandler(document.querySelector("#onboarding-next-button"), () => {
|
||||
localStorage.setItem("onboarded2", true);
|
||||
[...document.querySelectorAll(".onboarding-next-button")].forEach(nextElement => {
|
||||
Logic.addEnterHandler(nextElement, async function () {
|
||||
await Logic.setOnboardingStage(2);
|
||||
Logic.showPanel(P_ONBOARDING_3);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// This method is called when the panel is shown.
|
||||
|
@ -275,14 +392,17 @@ Logic.registerPanel(P_ONBOARDING_2, {
|
|||
|
||||
Logic.registerPanel(P_ONBOARDING_3, {
|
||||
panelSelector: ".onboarding-panel-3",
|
||||
securityPanelSelector: ".security-onboarding-panel-3",
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
// Let's move to the containers list panel.
|
||||
Logic.addEnterHandler(document.querySelector("#onboarding-almost-done-button"), () => {
|
||||
localStorage.setItem("onboarded3", true);
|
||||
[...document.querySelectorAll(".onboarding-almost-done-button")].forEach(almostElement => {
|
||||
Logic.addEnterHandler(almostElement, async function () {
|
||||
await Logic.setOnboardingStage(3);
|
||||
Logic.showPanel(P_ONBOARDING_4);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// This method is called when the panel is shown.
|
||||
|
@ -300,8 +420,29 @@ Logic.registerPanel(P_ONBOARDING_4, {
|
|||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
// Let's move to the containers list panel.
|
||||
document.querySelector("#onboarding-done-button").addEventListener("click", () => {
|
||||
localStorage.setItem("onboarded4", true);
|
||||
Logic.addEnterHandler(document.querySelector("#onboarding-done-button"), async function () {
|
||||
await Logic.setOnboardingStage(4);
|
||||
Logic.showPanel(P_ONBOARDING_5);
|
||||
});
|
||||
},
|
||||
|
||||
// This method is called when the panel is shown.
|
||||
prepare() {
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
});
|
||||
|
||||
// P_ONBOARDING_5: Fifth page for Onboarding: new tab long-press behavior
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
Logic.registerPanel(P_ONBOARDING_5, {
|
||||
panelSelector: ".onboarding-panel-5",
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
// Let's move to the containers list panel.
|
||||
Logic.addEnterHandler(document.querySelector("#onboarding-longpress-button"), async function () {
|
||||
await Logic.setOnboardingStage(5);
|
||||
Logic.showPanel(P_CONTAINERS_LIST);
|
||||
});
|
||||
},
|
||||
|
@ -346,13 +487,13 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
|||
function next() {
|
||||
const nextElement = element.nextElementSibling;
|
||||
if (nextElement) {
|
||||
nextElement.focus();
|
||||
nextElement.querySelector("td[tabindex=0]").focus();
|
||||
}
|
||||
}
|
||||
function previous() {
|
||||
const previousElement = element.previousElementSibling;
|
||||
if (previousElement) {
|
||||
previousElement.focus();
|
||||
previousElement.querySelector("td[tabindex=0]").focus();
|
||||
}
|
||||
}
|
||||
switch (e.keyCode) {
|
||||
|
@ -364,12 +505,72 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
|||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// When the popup is open sometimes the tab will still be updating it's state
|
||||
this.tabUpdateHandler = (tabId, changeInfo) => {
|
||||
const propertiesToUpdate = ["title", "favIconUrl"];
|
||||
const hasChanged = Object.keys(changeInfo).find((changeInfoKey) => {
|
||||
if (propertiesToUpdate.includes(changeInfoKey)) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (hasChanged) {
|
||||
this.prepareCurrentTabHeader();
|
||||
}
|
||||
};
|
||||
browser.tabs.onUpdated.addListener(this.tabUpdateHandler);
|
||||
},
|
||||
|
||||
unregister() {
|
||||
browser.tabs.onUpdated.removeListener(this.tabUpdateHandler);
|
||||
},
|
||||
|
||||
setupAssignmentCheckbox(siteSettings, currentUserContextId) {
|
||||
const assignmentCheckboxElement = document.getElementById("container-page-assigned");
|
||||
let checked = false;
|
||||
if (siteSettings && Number(siteSettings.userContextId) === currentUserContextId) {
|
||||
checked = true;
|
||||
}
|
||||
assignmentCheckboxElement.checked = checked;
|
||||
let disabled = false;
|
||||
if (siteSettings === false) {
|
||||
disabled = true;
|
||||
}
|
||||
assignmentCheckboxElement.disabled = disabled;
|
||||
},
|
||||
|
||||
async prepareCurrentTabHeader() {
|
||||
const currentTab = await Logic.currentTab();
|
||||
const currentTabElement = document.getElementById("current-tab");
|
||||
const assignmentCheckboxElement = document.getElementById("container-page-assigned");
|
||||
const currentTabUserContextId = Logic.userContextId(currentTab.cookieStoreId);
|
||||
assignmentCheckboxElement.addEventListener("change", () => {
|
||||
Logic.setOrRemoveAssignment(currentTab.id, currentTab.url, currentTabUserContextId, !assignmentCheckboxElement.checked);
|
||||
});
|
||||
currentTabElement.hidden = !currentTab;
|
||||
this.setupAssignmentCheckbox(false, currentTabUserContextId);
|
||||
if (currentTab) {
|
||||
const identity = await Logic.identity(currentTab.cookieStoreId);
|
||||
const siteSettings = await Logic.getAssignment(currentTab);
|
||||
this.setupAssignmentCheckbox(siteSettings, currentTabUserContextId);
|
||||
const currentPage = document.getElementById("current-page");
|
||||
currentPage.innerHTML = escaped`<span class="page-title truncate-text">${currentTab.title}</span>`;
|
||||
const favIconElement = Utils.createFavIconElement(currentTab.favIconUrl || "");
|
||||
currentPage.prepend(favIconElement);
|
||||
|
||||
const currentContainer = document.getElementById("current-container");
|
||||
currentContainer.innerText = identity.name;
|
||||
|
||||
currentContainer.setAttribute("data-identity-color", identity.color);
|
||||
}
|
||||
},
|
||||
|
||||
// This method is called when the panel is shown.
|
||||
prepare() {
|
||||
async prepare() {
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
this.prepareCurrentTabHeader();
|
||||
|
||||
Logic.identities().forEach(identity => {
|
||||
const hasTabs = (identity.hasHiddenTabs || identity.hasOpenTabs);
|
||||
const tr = document.createElement("tr");
|
||||
|
@ -378,10 +579,11 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
|||
|
||||
tr.classList.add("container-panel-row");
|
||||
|
||||
tr.setAttribute("tabindex", "0");
|
||||
|
||||
context.classList.add("userContext-wrapper", "open-newtab", "clickable");
|
||||
manage.classList.add("show-tabs", "pop-button");
|
||||
manage.title = escaped`View ${identity.name} container`;
|
||||
context.setAttribute("tabindex", "0");
|
||||
context.title = escaped`Create ${identity.name} tab`;
|
||||
context.innerHTML = escaped`
|
||||
<div class="userContext-icon-wrapper open-newtab">
|
||||
<div class="usercontext-icon"
|
||||
|
@ -389,7 +591,7 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
|||
data-identity-color="${identity.color}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-name"></div>`;
|
||||
<div class="container-name truncate-text"></div>`;
|
||||
context.querySelector(".container-name").textContent = identity.name;
|
||||
manage.innerHTML = "<img src='/img/container-arrow.svg' class='show-tabs pop-button-image-small' />";
|
||||
|
||||
|
@ -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.appendChild(fragment);
|
||||
/* Not sure why extensions require a focus for the doorhanger,
|
||||
however it allows us to have a tabindex before the first selected item
|
||||
*/
|
||||
document.addEventListener("focus", () => {
|
||||
list.querySelector("tr").focus();
|
||||
const focusHandler = () => {
|
||||
list.querySelector("tr .clickable").focus();
|
||||
document.removeEventListener("focus", focusHandler);
|
||||
};
|
||||
document.addEventListener("focus", focusHandler);
|
||||
/* If the user mousedown's first then remove the focus handler */
|
||||
document.addEventListener("mousedown", () => {
|
||||
document.removeEventListener("focus", focusHandler);
|
||||
});
|
||||
|
||||
return Promise.resolve();
|
||||
|
@ -453,7 +661,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
|||
const identity = Logic.currentIdentity();
|
||||
browser.runtime.sendMessage({
|
||||
method: identity.hasHiddenTabs ? "showTabs" : "hideTabs",
|
||||
userContextId: Logic.userContextId(identity.cookieStoreId)
|
||||
userContextId: Logic.currentUserContextId()
|
||||
}).then(() => {
|
||||
window.close();
|
||||
}).catch(() => {
|
||||
|
@ -525,7 +733,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
|||
// Let's retrieve the list of tabs.
|
||||
return browser.runtime.sendMessage({
|
||||
method: "getTabs",
|
||||
userContextId: Logic.userContextId(identity.cookieStoreId),
|
||||
userContextId: Logic.currentUserContextId(),
|
||||
}).then(this.buildInfoTable);
|
||||
},
|
||||
|
||||
|
@ -537,8 +745,9 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
|||
fragment.appendChild(tr);
|
||||
tr.classList.add("container-info-tab-row");
|
||||
tr.innerHTML = escaped`
|
||||
<td><img class="icon" src="${tab.favicon}" /></td>
|
||||
<td class="container-info-tab-title">${tab.title}</td>`;
|
||||
<td></td>
|
||||
<td class="container-info-tab-title truncate-text" title="${tab.url}" >${tab.title}</td>`;
|
||||
tr.querySelector("td").appendChild(Utils.createFavIconElement(tab.favicon));
|
||||
|
||||
// On click, we activate this tab. But only if this tab is active.
|
||||
if (tab.active) {
|
||||
|
@ -588,22 +797,22 @@ Logic.registerPanel(P_CONTAINERS_EDIT, {
|
|||
data-identity-color="${identity.color}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-name"></div>
|
||||
<div class="container-name truncate-text"></div>
|
||||
</td>
|
||||
<td class="edit-container pop-button edit-container-icon">
|
||||
<img
|
||||
src="/img/container-edit.svg"
|
||||
class="pop-button-image" />
|
||||
</td>
|
||||
<td class="remove-container pop-button delete-container-icon" >
|
||||
<td class="remove-container pop-button delete-container-icon">
|
||||
<img
|
||||
class="pop-button-image"
|
||||
src="/img/container-delete.svg"
|
||||
/>
|
||||
</td>`;
|
||||
tr.querySelector(".container-name").textContent = identity.name;
|
||||
tr.querySelector(".edit-container .pop-button-image").setAttribute("title", `Edit ${identity.name} container`);
|
||||
tr.querySelector(".remove-container .pop-button-image").setAttribute("title", `Edit ${identity.name} container`);
|
||||
tr.querySelector(".edit-container").setAttribute("title", `Edit ${identity.name} container`);
|
||||
tr.querySelector(".remove-container").setAttribute("title", `Delete ${identity.name} container`);
|
||||
|
||||
|
||||
Logic.addEnterHandler(tr, e => {
|
||||
|
@ -635,7 +844,12 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
|||
this.initializeRadioButtons();
|
||||
|
||||
Logic.addEnterHandler(document.querySelector("#edit-container-panel-back-arrow"), () => {
|
||||
const formValues = new FormData(this._editForm);
|
||||
if (formValues.get("container-id") !== NEW_CONTAINER_ID) {
|
||||
this._submitForm();
|
||||
} else {
|
||||
Logic.showPreviousPanel();
|
||||
}
|
||||
});
|
||||
|
||||
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");
|
||||
const editLink = document.querySelector("#edit-container-ok-link");
|
||||
Logic.addEnterHandler(editLink, this._submitForm.bind(this));
|
||||
editLink.addEventListener("submit", this._submitForm.bind(this));
|
||||
this._editForm.addEventListener("submit", this._submitForm.bind(this));
|
||||
Logic.addEnterHandler(editLink, () => {
|
||||
this._submitForm();
|
||||
});
|
||||
editLink.addEventListener("submit", () => {
|
||||
this._submitForm();
|
||||
});
|
||||
this._editForm.addEventListener("submit", () => {
|
||||
this._submitForm();
|
||||
});
|
||||
|
||||
|
||||
},
|
||||
|
||||
_submitForm() {
|
||||
const identity = Logic.currentIdentity();
|
||||
const formValues = new FormData(this._editForm);
|
||||
return browser.runtime.sendMessage({
|
||||
method: "createOrUpdateContainer",
|
||||
message: {
|
||||
userContextId: Logic.userContextId(identity.cookieStoreId) || false,
|
||||
userContextId: formValues.get("container-id") || NEW_CONTAINER_ID,
|
||||
params: {
|
||||
name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(),
|
||||
icon: formValues.get("container-icon") || DEFAULT_ICON,
|
||||
|
@ -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() {
|
||||
const colorRadioTemplate = (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 colorRadioFieldset = document.getElementById("edit-container-panel-choose-color");
|
||||
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
|
||||
templateInstance.innerHTML = colorRadioTemplate(containerColor);
|
||||
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 iconRadioFieldset = document.getElementById("edit-container-panel-choose-icon");
|
||||
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
|
||||
templateInstance.innerHTML = iconRadioTemplate(containerIcon);
|
||||
iconRadioFieldset.appendChild(templateInstance);
|
||||
|
@ -700,9 +968,16 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
|||
},
|
||||
|
||||
// This method is called when the panel is shown.
|
||||
prepare() {
|
||||
async prepare() {
|
||||
const identity = Logic.currentIdentity();
|
||||
|
||||
const userContextId = Logic.currentUserContextId();
|
||||
const assignments = await Logic.getAssignmentObjectByContainer(userContextId);
|
||||
this.showAssignedContainers(assignments);
|
||||
document.querySelector("#edit-container-panel .panel-footer").hidden = !!userContextId;
|
||||
|
||||
document.querySelector("#edit-container-panel-name-input").value = identity.name || "";
|
||||
document.querySelector("#edit-container-panel-usercontext-input").value = userContextId || NEW_CONTAINER_ID;
|
||||
[...document.querySelectorAll("[name='container-color']")].forEach(colorInput => {
|
||||
colorInput.checked = colorInput.value === identity.color;
|
||||
});
|
||||
|
|
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,
|
||||
"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.",
|
||||
"icons": {
|
||||
|
@ -26,7 +26,6 @@
|
|||
"contextualIdentities",
|
||||
"history",
|
||||
"idle",
|
||||
"notifications",
|
||||
"storage",
|
||||
"tabs",
|
||||
"webRequestBlocking",
|
||||
|
@ -36,7 +35,8 @@
|
|||
"commands": {
|
||||
"_execute_browser_action": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Y"
|
||||
"default": "Ctrl+Period",
|
||||
"mac": "MacCtrl+Period"
|
||||
},
|
||||
"description": "Open containers panel"
|
||||
}
|
||||
|
@ -54,5 +54,18 @@
|
|||
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["js/content-script.js"],
|
||||
"css": ["css/content.css"],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
|
||||
"web_accessible_resources": [
|
||||
"/img/container-site-d-24.png"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -3,56 +3,96 @@
|
|||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<title>Containers browserAction Popup</title>
|
||||
<link rel="stylesheet" href="/css/popup.css">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="hide panel onboarding onboarding-panel-1" id="onboarding-panel-1">
|
||||
<div class="hide panel onboarding onboarding-panel-1">
|
||||
<img class="onboarding-img" alt="Container Tabs Overview" src="/img/onboarding-1.png" />
|
||||
<h3 class="onboarding-title">A better way to manage all the things you do online</h3>
|
||||
<p>
|
||||
Use containers to organize tasks, manage accounts, and keep your focus where you want it.
|
||||
</p>
|
||||
<a href="#" id="onboarding-start-button" class="onboarding-button">Get Started</a>
|
||||
<a href="#" class="onboarding-button onboarding-start-button">Get Started</a>
|
||||
</div>
|
||||
|
||||
<div class="hide panel onboarding security-onboarding-panel-1">
|
||||
<img class="onboarding-img" alt="Container Tabs Overview" src="/img/onboarding-1.png" />
|
||||
<h3 class="onboarding-title">A simple and secure way to manage your online life</h3>
|
||||
<p>
|
||||
Use containers to organize tasks, manage accounts, and store sensitive data.
|
||||
</p>
|
||||
<a href="#" class="onboarding-button onboarding-start-button">Get Started</a>
|
||||
</div>
|
||||
|
||||
<div class="panel onboarding onboarding-panel-2 hide" id="onboarding-panel-2">
|
||||
<div class="panel onboarding onboarding-panel-2 hide">
|
||||
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-2.png" />
|
||||
<h3 class="onboarding-title">Put containers to work for you.</h3>
|
||||
<p>Features like color-coding and separate container tabs help you find things easily, focus your attention, and minimize distractions.</p>
|
||||
<a href="#" id="onboarding-next-button" class="onboarding-button">Next</a>
|
||||
<a href="#" class="onboarding-button onboarding-next-button">Next</a>
|
||||
</div>
|
||||
|
||||
<div class="panel onboarding onboarding-panel-3 hide" id="onboarding-panel-3">
|
||||
<div class="panel onboarding security-onboarding-panel-2 hide">
|
||||
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-2.png" />
|
||||
<h3 class="onboarding-title">Put containers to work for you.</h3>
|
||||
<p>Color-coding helps you categorize your online life, find things easily, and minimize distractions.</p>
|
||||
<a href="#" class="onboarding-button onboarding-next-button">Next</a>
|
||||
</div>
|
||||
|
||||
<div class="panel onboarding onboarding-panel-3 hide">
|
||||
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-3.png" />
|
||||
<h3 class="onboarding-title">A place for everything, and everything in its place.</h3>
|
||||
<p>Start with the containers we've created, or create your own.</p>
|
||||
<a href="#" id="onboarding-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 class="panel onboarding onboarding-panel-4 hide" id="onboarding-panel-4">
|
||||
<img class="onboarding-img" alt="How to assign sites to containers" src="/img/onboarding-4.png" />
|
||||
<h3 class="onboarding-title">Always open sites in the containers you want.</h3>
|
||||
<p>Right-click inside a container tab to assign the site to always open in the container.</p>
|
||||
<a href="#" id="onboarding-done-button" class="onboarding-button">Done</a>
|
||||
<a href="#" id="onboarding-done-button" class="onboarding-button">Next</a>
|
||||
</div>
|
||||
|
||||
<div class="panel onboarding onboarding-panel-5 hide" id="onboarding-panel-5">
|
||||
<img class="onboarding-img" alt="Long-press the New Tab button to create a new container tab." src="/img/onboarding-3.png" />
|
||||
<h3 class="onboarding-title">Container tabs when you need them.</h3>
|
||||
<p>Long-press the New Tab button to create a new container tab.</p>
|
||||
<a href="#" id="onboarding-longpress-button" class="onboarding-button">Done</a>
|
||||
</div>
|
||||
|
||||
<div class="panel container-panel hide" id="container-panel">
|
||||
<div class="panel-header">
|
||||
<h3 class="panel-header-text">Containers</h3>
|
||||
<a 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-tab">
|
||||
<h3>Current Tab</h3>
|
||||
<div id="current-page"></div>
|
||||
<label for="container-page-assigned">
|
||||
<input type="checkbox" id="container-page-assigned" />
|
||||
<span class="truncate-text">
|
||||
Always open in
|
||||
<span id="current-container"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="container-panel-controls">
|
||||
<a href="#" class="action-link" id="sort-containers-link" title="Sort tabs into container order">Sort Tabs</a>
|
||||
</div>
|
||||
<div class="scrollable panel-content" tabindex="-1">
|
||||
<table>
|
||||
<tbody class="identities-list"></tbody>
|
||||
<table class="identities-list">
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel-footer edit-identities">
|
||||
<div class="edit-containers-text panel-footer-secondary">
|
||||
<a href="#" tabindex="0" id="edit-containers-link">Edit Containers</a>
|
||||
</div>
|
||||
<a href="#" tabindex="0" class="add-container-link pop-button" id="container-add-link">
|
||||
<img class="pop-button-image-small icon" alt="Create new container icon" title="Create new container" src="/img/container-add.svg" />
|
||||
<a href="#" tabindex="0" class="add-container-link pop-button" id="container-add-link" title="Create new container">
|
||||
<img class="pop-button-image-small icon" alt="Create new container icon" src="/img/container-add.svg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -66,7 +106,7 @@
|
|||
<div class="column-panel-content">
|
||||
<div class="panel-header container-info-panel-header">
|
||||
<span class="usercontext-icon" id="container-info-icon"></span>
|
||||
<h3 id="container-info-name" class="panel-header-text container-name"></h3>
|
||||
<h3 id="container-info-name" class="panel-header-text container-name truncate-text"></h3>
|
||||
</div>
|
||||
<div class="select-row clickable container-info-panel-hide container-info-has-tabs" id="container-info-hideorshow">
|
||||
<img id="container-info-hideorshow-icon" alt="Hide Container icon" src="/img/container-hide.svg" class="icon container-info-panel-hideorshow-icon"/>
|
||||
|
@ -104,17 +144,23 @@
|
|||
</div>
|
||||
<div class="column-panel-content">
|
||||
<form id="edit-container-panel-form">
|
||||
<input type="hidden" name="container-id" id="edit-container-panel-usercontext-input" />
|
||||
<fieldset>
|
||||
<legend>Name</legend>
|
||||
<input type="text" name="container-name" id="edit-container-panel-name-input" maxlength="25"/>
|
||||
</fieldset>
|
||||
<fieldset id="edit-container-panel-choose-color">
|
||||
<fieldset id="edit-container-panel-choose-color" class="radio-choice">
|
||||
<legend>Choose a color</legend>
|
||||
</fieldset>
|
||||
<fieldset id="edit-container-panel-choose-icon">
|
||||
<fieldset id="edit-container-panel-choose-icon" class="radio-choice">
|
||||
<legend>Choose an icon</legend>
|
||||
</fieldset>
|
||||
</form>
|
||||
<div id="edit-sites-assigned" class="scrollable" hidden>
|
||||
<h3>Sites assigned to this container</h3>
|
||||
<div class="assigned-sites-list">
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<a 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>
|
||||
|
@ -138,7 +184,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="js/utils.js"></script>
|
||||
<script src="js/popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
Loading…
Add table
Reference in a new issue