Compare commits

...

105 Commits

Author SHA1 Message Date
luke crouch e3ed4582d2 Merge pull request #707 from jonathanKingston/icon-reset-fix
Fix on update icons being reset to central defaults. Fixes #703
2017-08-03 16:05:54 +00:00
Jonathan Kingston 38c098edb6 Fix on update icons being reset to central defaults. Fixes #703 2017-08-03 11:26:00 +01:00
Jonathan Kingston ee63f72f3e Merge pull request #698 from mozilla/bump-version-to-2.4.1
bump version to 2.4.1
2017-07-31 19:51:15 +01:00
groovecoder 4cbcfac0f4 bump version to 2.4.1 2017-07-31 11:40:18 -05:00
luke crouch 641c95e64e Merge pull request #681 from jonathanKingston/set-default-background
Set default background to fix dark linux themes. Fixes #677
2017-07-18 18:53:01 +00:00
Jonathan Kingston b646a9183c Set default background to fix dark linux themes. Fixes #677 2017-07-17 15:22:45 +01:00
luke crouch 6e2ed6393e Merge pull request #669 from jonathanKingston/assignment-index
Fix assignment index loading at the end of the tab strip.
2017-07-14 10:53:10 -05:00
Jonathan Kingston af98174a19 Fix assignment index loading at the end of the tab strip. Fixes #672. 2017-07-14 10:44:53 +01:00
luke crouch 1c8530ef02 Merge pull request #668 from jonathanKingston/mac-keybord-shortcut
Adding a keyboard shortcut that will work on MacOS. Ctrl+.
2017-07-13 16:12:56 -05:00
Jonathan Kingston 366f9ec047 Adding a keyboard shortcut that will work on MacOS. Ctrl+. 2017-07-13 21:42:07 +01:00
luke crouch 63343f18eb Merge pull request #664 from mozilla/add-surveyurl-for-shield
add surveyUrl for shield study participants
2017-07-11 11:39:50 -05:00
groovecoder 080e9dd22d add surveyUrl for shield study participants 2017-07-11 11:08:38 -05:00
luke crouch 268da1350a Merge pull request #660 from mozilla/new-tab-long-press-onboarding
add onboarding panel for long-press
2017-07-06 14:47:02 -05:00
Jonathan Kingston e0abaa67e2 Using browser.storage.local for onboarding stage 2017-07-05 16:09:59 -07:00
groovecoder 69f06f96cc add onboarding panel for long-press 2017-07-05 16:00:19 -05:00
luke crouch 62d479a3f3 Merge pull request #659 from jonathanKingston/fix-non-nightly-menu
Fix non nightly long press menu. Fixes #658
2017-07-05 15:17:14 -05:00
Jonathan Kingston 315c75f2ac Fix non nightly long press menu. Fixes #658 2017-07-05 13:08:33 -07:00
luke crouch 9fcf822140 Merge pull request #643 from jonathanKingston/pre-shield
Pre shield
2017-06-29 17:06:32 -07:00
Jonathan Kingston e7ac72a6a2 Add new telemetry to the old plus button menu 2017-06-29 13:47:20 -07:00
Jonathan Kingston da39d18ce0 Increasing notification timeout 2017-06-29 10:00:54 -07:00
Jonathan Kingston 63025ab3d6 Removal of hover menu for plus menu 2017-06-29 09:59:53 -07:00
Jonathan Kingston 1c38e09dcc Changing the notification colour to be less alien 2017-06-29 09:42:41 -07:00
luke crouch 2178e26220 Merge pull request #637 from jonathanKingston/cleanup-exemption-code
Cleanup exemption code. On assignment set/remove clear exemptions and…
2017-06-27 15:54:59 -07:00
luke crouch 310e2fa503 Merge pull request #638 from jonathanKingston/tidy-bits
Add a code of conduct and contributing file.
2017-06-27 15:37:17 -07:00
Jonathan Kingston 30c55e093c Add a code of conduct and contributing file. 2017-06-23 14:22:26 +01:00
Jonathan Kingston d0037d1377 Cleanup exemption code. On assignment set/remove clear exemptions and set based on existing tabs. Fixes #635 2017-06-23 12:50:47 +01:00
luke crouch 2a3aa296c0 Merge pull request #633 from jonathanKingston/different-display-of-notice-b
Aligning items to center for container notification.
2017-06-22 16:28:27 -05:00
Jonathan Kingston 10e83d3795 Aligning items to center for container notification. 2017-06-22 22:22:45 +01:00
luke crouch 29b9590878 Merge pull request #631 from jonathanKingston/different-display-of-notice
Fix different display in content warning notice. Fixes #630
2017-06-22 16:09:13 -05:00
Jonathan Kingston a86bcf7983 Fix different display in content warning notice. Fixes #630 2017-06-22 22:05:05 +01:00
groovecoder e28b25e04c fix #626: cast message userContextId to string 2017-06-22 22:04:00 +01:00
luke crouch 1cc3ab83b9 Merge pull request #627 from jonathanKingston/correct-assignment-toggle
Fixing the assignment toggle for when a different container is open. …
2017-06-22 10:11:44 -05:00
Jonathan Kingston 0566c9f962 Fixing the assignment toggle for when a different container is open. Fixes #611 2017-06-22 16:09:10 +01:00
luke crouch 8c92d8ef5d Merge pull request #629 from jonathanKingston/assignment-confirm-alignment
Fixing alignment of checkbox on confirm screen. Fixes #607
2017-06-22 09:49:49 -05:00
Jonathan Kingston a8cac47125 Fixing alignment of checkbox on confirm screen. Fixes #607 2017-06-22 15:43:56 +01:00
groovecoder e191255c47 fix #608: re-"render" badge on window focus change 2017-06-22 15:35:55 +01:00
groovecoder 7eb752c2f7 fix #614: only call thisStudy.shutdown in shield 2017-06-22 14:50:31 +01:00
luke crouch 40f2f1af5e Merge pull request #620 from jonathanKingston/tooltips
Adding tooltips. Fixes #615. Fixes #609.  Fixes #112
2017-06-22 08:43:16 -05:00
Jonathan Kingston 214a83deda Adding tooltips. Fixes #615. Fixes #609. Fixes #112 2017-06-22 14:40:01 +01:00
luke crouch 51b804f96d Merge pull request #621 from jonathanKingston/close-correct-assignment
Close correct assignment window on confirmation page. Fixes #606
2017-06-22 08:16:03 -05:00
Jonathan Kingston 83e8340a70 Close correct assignment window on confirmation page. Fixes #606 2017-06-22 13:45:31 +01:00
groovecoder 6292d9b25d fix #498: final copy for security onboarding panels 2017-06-22 11:38:23 +01:00
groovecoder 2f5e195c91 for #498: use local node_modules/.bin/json 2017-06-22 11:38:23 +01:00
groovecoder af966d6d29 for #498 add onboarding-3-security.png asset
remove tab.create orientation
2017-06-22 11:38:23 +01:00
groovecoder 4ed136299b replace a .then() with await Promise.all() 2017-06-22 11:38:23 +01:00
groovecoder 5237e67fa6 for #498: security onboarding panels and logic 2017-06-22 11:38:23 +01:00
luke crouch 13cd601212 Merge pull request #602 from jonathanKingston/edit-restyle
Edit restyle
2017-06-20 10:18:46 -05:00
Jonathan Kingston bc847b53f5 Making create screen have buttons again 2017-06-19 15:51:29 +01:00
Jonathan Kingston 4e0180d521 Improve assignment styles part of #561 2017-06-19 14:33:57 +01:00
Jonathan Kingston 13e4b4e7f7 WIP styles improving radio styles 2017-06-19 14:33:57 +01:00
Jonathan Kingston 2278498b06 WIP edit-containers restyle 2017-06-19 14:33:57 +01:00
luke crouch 6533c74d0a Merge pull request #597 from jonathanKingston/sheild-study-fixes
Fix icons in shield study. Fixes #586
2017-06-16 10:30:17 -05:00
Jonathan Kingston 9b0fe826de Fix icons in shield study. Fixes #586 2017-06-16 15:14:21 +01:00
groovecoder 5d75d4525d document "alltabs-menu" as an open tab source 2017-06-15 20:35:55 +01:00
luke crouch 59f2b8a764 Merge pull request #593 from jonathanKingston/current-tab-window
Fix for current tab showing the wrong window. Fixes #592
2017-06-15 10:30:06 -05:00
Jonathan Kingston 68c21624e2 Reset context menu when assignment changes. Fixes #589 2017-06-15 13:39:22 +01:00
Jonathan Kingston bfc6f68978 Fix for current tab showing the wrong window. Fixes #592 2017-06-14 23:59:54 +01:00
groovecoder 78ef2e8304 update README for shield per @kjozwiak 2017-06-14 23:10:50 +01:00
luke crouch 5b85fc1690 Merge pull request #584 from jonathanKingston/assignment-context-issues
Fixing showing of assignment menu. Fixes #579
2017-06-14 11:39:13 -05:00
luke crouch be8f6bbe7c Merge pull request #581 from jonathanKingston/keyboard-focus
Fixing keyboard focus issues to new layout
2017-06-14 11:27:37 -05:00
Jonathan Kingston dfd420d1a5 Fix which assignment is being changed. Fixes #580 2017-06-14 17:01:14 +01:00
luke crouch 4030b6eeec Merge pull request #582 from jonathanKingston/fix-reload-telemetry
Fix reload telemetry payload.
2017-06-14 10:38:55 -05:00
Jonathan Kingston d2b4d972e1 Fixing showing of assignment menu. Fixes #579 2017-06-14 13:49:13 +01:00
Jonathan Kingston 06d381b931 Fix reload telemetry payload. 2017-06-14 12:09:18 +01:00
Jonathan Kingston c2ed5420a4 Fixing keyboard focus issues to new layout 2017-06-14 11:41:28 +01:00
luke crouch fc789a49ac Merge pull request #576 from jonathanKingston/shield-study
Shield study work
2017-06-13 11:41:28 -05:00
Jonathan Kingston bf75f52a52 Fix linting 2017-06-13 16:36:00 +01:00
Jonathan Kingston 090ae1f139 Fixing popup to use tabId for messaging assignment change 2017-06-13 15:02:47 +01:00
Jonathan Kingston cd2e110c17 Merge branch 'ux-fiddles' into shield-study 2017-06-13 14:09:27 +01:00
Jonathan Kingston d63e887ef7 Merge branch 'assignment-manage' into shield-study 2017-06-13 14:07:21 +01:00
Jonathan Kingston ea0c9d4306 Merge branch 'comma-shortcut' into shield-study 2017-06-13 14:05:29 +01:00
Jonathan Kingston e5a87ab535 Merge branch 'assign-menu-for-wrong-tab' into shield-study 2017-06-13 14:05:10 +01:00
Jonathan Kingston 3b9da05e67 Merge branch 'bakulf-colors' 2017-06-12 15:04:13 +01:00
baku 5c5cf02249 Reset color and icon when disabled - issue #398 2017-06-12 15:03:49 +01:00
Jonathan Kingston 8503e9c9c5 Adding manage assignment from edit container panel. Fixes #501 2017-06-06 16:48:01 +01:00
Jonathan Kingston 7f37ed906f Exchanging Ctrl+Y for Ctrl+Period due to text field collisions. Fixes #546 2017-06-06 14:22:48 +01:00
Jonathan Kingston 15477dc384 Start fixing styles 2017-06-05 17:48:18 +01:00
Jonathan Kingston 06d35e65ce Adding in content notification to look more browser like. 2017-06-05 15:37:01 +01:00
Jonathan Kingston 49e8afaf9a Fixing truncation for info screen tabs. 2017-06-05 15:37:01 +01:00
Jonathan Kingston 9903e811c2 Fix first focus issue on opening browser action. Fixes #564 2017-06-05 15:37:01 +01:00
Jonathan Kingston e467988a71 Fixing favicon loading for all icons. Part of #561 2017-06-05 15:37:01 +01:00
Jonathan Kingston 094a0e2391 Update README.md to bump Firefox version number 2017-06-04 22:22:42 +01:00
Jonathan Kingston df8bf4e5e4 Force removal of assign context menu entries before anything async happens to prevent the wrong tabs assign preference showing. Fixes #539 2017-05-31 11:10:45 +01:00
groovecoder 45f34a586a only start study for @shield-study-privacy 2017-05-30 21:24:29 +01:00
Jonathan Kingston ab2b9a48c7 Changing current tab truncation to prevent container overflowing. Fixes #552 2017-05-30 21:24:18 +01:00
luke crouch 82c9cac34c Merge pull request #540 from jonathanKingston/current-tab-layout
Cleaning up layout issues for current tab panel.
2017-05-30 12:37:15 -05:00
Jonathan Kingston 5cd2ac0187 Cleaning up layout issues for current tab panel. 2017-05-26 16:42:58 +01:00
luke crouch 0f9dd77687 Merge pull request #535 from jonathanKingston/assignment-controls_reject-assignment
Assignment controls reject assignment
2017-05-25 11:48:45 -05:00
Jonathan Kingston fb845cce12 Fixing linting errors 2017-05-25 17:16:46 +01:00
Jonathan Kingston a29fae0893 Fixing exemption to be stored in memory rather than storage (prevents exemption from being remembered on restart). 2017-05-25 17:02:16 +01:00
Jonathan Kingston bd72b4e759 Adding new exemption pings to metrics.md 2017-05-25 17:02:16 +01:00
Jonathan Kingston 4f6e91336f WIP assignment controls. Fixes #499 2017-05-25 17:02:16 +01:00
Jonathan Kingston 69d497bacd Adding container assignment exemption on confirm prompt. Fixes #500 2017-05-25 17:02:16 +01:00
luke crouch d3413c7afc Merge pull request #478 from mozilla/shield
Add lib/shield to enable shield study
2017-05-25 09:16:15 -05:00
Jonathan Kingston 08ba094748 add npm run build-shield command 2017-05-19 14:31:58 -05:00
groovecoder 5916bd2871 only use our experimentPing outside of Test Pilot 2017-05-19 13:38:04 -05:00
groovecoder 3700e6f461 experiment.js from testpilot addon
Remove variants functionality and javascript-flow

.eslintignore experiment.js
2017-05-18 10:32:27 -05:00
groovecoder dad3214986 actually start the study 2017-05-17 14:41:04 -05:00
Jonathan Kingston 099d07bf1f Update shield install path 2017-05-17 12:37:24 -05:00
groovecoder 93b6378b22 fix npm test/lint failures 2017-05-17 12:37:24 -05:00
groovecoder 84dd73bff5 update README with shield run instructions 2017-05-17 12:37:24 -05:00
groovecoder b0c53063d2 start shield study AFTER SDK starts the webext 2017-05-17 12:37:24 -05:00
groovecoder 54c598e22e startup the study 2017-05-17 11:34:03 -05:00
groovecoder e499ff5711 include lib/shield to make it work 2017-05-17 11:34:03 -05:00
groovecoder cd03ea7a59 start study.js 2017-05-17 11:34:03 -05:00
29 changed files with 2007 additions and 421 deletions
+2
View File
@@ -1 +1,3 @@
testpilot-metrics.js
lib/shield/*.js
lib/testpilot/*.js
+1
View File
@@ -9,6 +9,7 @@ module.exports = {
"webextensions": true
},
"globals": {
"Utils": true,
"CustomizableUI": true,
"CustomizableWidgets": true,
"SessionStore": true,
+2
View File
@@ -1,8 +1,10 @@
.DS_Store
package-lock.json
node_modules
README.html
*.xpi
*.swp
*.swo
.vimrc
.env
addon.env
+1 -1
View File
@@ -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
View 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
View 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.
+35 -27
View File
@@ -1,25 +1,22 @@
# Containers: Test Pilot Experiment
# Containers Add-on
[![Available on Test Pilot](https://img.shields.io/badge/available_on-Test_Pilot-0996F8.svg)](https://testpilot.firefox.com/experiments/containers)
[Embedded Web Extension](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Embedded_WebExtensions) to experiment with [Containers](https://blog.mozilla.org/tanvi/2016/06/16/contextual-identities-on-the-web/) in [Firefox Test Pilot](https://testpilot.firefox.com/) to learn:
[Embedded Web Extension](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Embedded_WebExtensions) to build [Containers](https://blog.mozilla.org/tanvi/2016/06/16/contextual-identities-on-the-web/) as a Firefox [Test Pilot](https://testpilot.firefox.com/) Experiment and [Shield Study](https://wiki.mozilla.org/Firefox/Shield/Shield_Studies) to learn:
* Will a general Firefox audience understand the Containers feature?
* Is the UI as currently implemented in Nightly clear or discoverable?
See [the Product Hypothesis Document for more
details](https://docs.google.com/document/d/1WQdHTVXROk7dYkSFluc6_hS44tqZjIrG9I-uPyzevE8/edit?ts=5824ba12#).
For more info, see:
* [Test Pilot Product Hypothesis Document](https://docs.google.com/document/d/1WQdHTVXROk7dYkSFluc6_hS44tqZjIrG9I-uPyzevE8/edit#)
* [Shield Product Hypothesis Document](https://docs.google.com/document/d/1vMD-fH_5hGDDqNvpRZk12_RhCN2WAe4_yaBamaNdtik/edit#)
## Requirements
* node 7+ (for jpm)
* Firefox 51+
## Run it
See Development
* Firefox 53+
## Development
@@ -27,28 +24,23 @@ See Development
Add-on development is better with [a particular environment](https://developer.mozilla.org/en-US/Add-ons/Setting_up_extension_development_environment). One simple way to get that environment set up is to install the [DevPrefs add-on](https://addons.mozilla.org/en-US/firefox/addon/devprefs/). You can make a custom Firefox profile that includes the DevPrefs add-on, and use that profile when you run the code in this repository.
1. Make a new profile by running `/path/to/firefox -P`, which launches the profile editor. "Create Profile" -- name it whatever you wish (e.g. 'addon_dev') and store it in the default location. It's probably best to deselect the option to "Use without asking," since you probably don't want to use this as your default profile.
2. Once you've created your profile, click "Start Firefox". A new instance of Firefox should launch. Go to Tools->Add-ons and search for "DevPrefs". Install it. Quit Firefox.
3. Now you have a new, vanilla Firefox profile with the DevPrefs add-on installed. You can use your new profile with the code in _this_ repository like so:
**Beta building**
#### Run the `.xpi` file in an unbranded build
Release & Beta channels do not allow un-signed add-ons, even with the DevPrefs. So, you must run the add-on in an [unbranded build](https://wiki.mozilla.org/Add-ons/Extension_Signing#Unbranded_Builds):
To build this for 51 beta just using the downloaded version of beta will not work as XPI signature checking is disabled fully.
1. Download and install an un-branded build of Firefox
2. Download the latest `.xpi` from this repository's releases
3. Run the un-branded build of Firefox with your DevPrefs profile
4. Go to `about:addons`
5. Click the gear, and select "Install Add-on From File..."
6. Select the `.xpi` file
The only way to run the experiment is using an [unbranded version build](https://wiki.mozilla.org/Add-ons/Extension_Signing#Unbranded_Builds) or to build beta yourself:
1. [Download the mozilla-beta repo](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Source_Code/Mercurial#mozilla-beta_(prerelease_development_tree))
2. [Create a mozconfig file](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Build_Instructions/Configuring_Build_Options) - probably optional
3. `cd <reponame>`
3. `./mach bootstrap`
4. `./mach build`
5. Follow the above instructions by creating the new profile via: `~/<reponame>/obj-x86_64-pc-linux-gnu/dist/bin/firefox -P` (Where "obj-x86_64-pc-linux-gnu" may be different depending on platform obj-...)
### Run with jpm
#### Run the TxP experiment with `jpm`
1. `git clone git@github.com:mozilla/testpilot-containers.git`
2. `cd testpilot-containers`
@@ -57,11 +49,22 @@ The only way to run the experiment is using an [unbranded version build](https:/
Check out the [Browser Toolbox](https://developer.mozilla.org/en-US/docs/Tools/Browser_Toolbox) for more information about debugging add-on code.
#### Run the shield study with `shield`
1. `git clone git@github.com:mozilla/testpilot-containers.git`
2. `cd testpilot-containers`
3. `npm install`
4. `npm install -g shield-study-cli`
5. `shield run . -- --binary Nightly`
### Building .xpi
To build a local .xpi, use the plain [`jpm
xpi`](https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/jpm#jpm_xpi) command.
To build a local testpilot-containers.xpi, use the plain [`jpm
xpi`](https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/jpm#jpm_xpi) command,
or run `npm run build`.
#### Building a shield .xpi
To build a local shield-study-containers.xpi, run `npm run build-shield`.
### Signing an .xpi
@@ -75,6 +78,11 @@ add-on](https://addons.mozilla.org/en-US/developers/addon/containers-experiment/
### Testing
TBD
### Distributing
TBD
### Links
- [Licence](./LICENSE.txt)
- [Contributing](./CONTRIBUTING.md)
- [Code Of Conduct](./CODE_OF_CONDUCT.md)
+16 -16
View File
@@ -52,55 +52,55 @@ value, or chrome url path as an alternate selector mitiages this bug.*/
[data-identity-icon="fingerprint"],
[data-identity-icon="chrome://browser/skin/usercontext/personal.svg"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#fingerprint");
--identity-icon: url("/data/usercontext.svg#fingerprint");
}
[data-identity-icon="briefcase"],
[data-identity-icon="chrome://browser/skin/usercontext/work.svg"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#briefcase");
--identity-icon: url("/data/usercontext.svg#briefcase");
}
[data-identity-icon="dollar"],
[data-identity-icon="chrome://browser/skin/usercontext/banking.svg"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#dollar");
--identity-icon: url("/data/usercontext.svg#dollar");
}
[data-identity-icon="cart"],
[data-identity-icon="chrome://browser/skin/usercontext/cart.svg"],
[data-identity-icon="chrome://browser/skin/usercontext/shopping.svg"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#cart");
--identity-icon: url("/data/usercontext.svg#cart");
}
[data-identity-icon="circle"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#circle");
--identity-icon: url("/data/usercontext.svg#circle");
}
[data-identity-icon="gift"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#gift");
--identity-icon: url("/data/usercontext.svg#gift");
}
[data-identity-icon="vacation"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#vacation");
--identity-icon: url("/data/usercontext.svg#vacation");
}
[data-identity-icon="food"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#food");
--identity-icon: url("/data/usercontext.svg#food");
}
[data-identity-icon="fruit"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#fruit");
--identity-icon: url("/data/usercontext.svg#fruit");
}
[data-identity-icon="pet"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#pet");
--identity-icon: url("/data/usercontext.svg#pet");
}
[data-identity-icon="tree"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#tree");
--identity-icon: url("/data/usercontext.svg#tree");
}
[data-identity-icon="chill"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#chill");
--identity-icon: url("/data/usercontext.svg#chill");
}
#userContext-indicator {
@@ -139,7 +139,7 @@ value, or chrome url path as an alternate selector mitiages this bug.*/
background-size: contain;
fill: var(--identity-icon-color) !important;
filter: url(/img/filters.svg#fill);
filter: url(resource://testpilot-containers/data/filters.svg#fill);
filter: url(/data/filters.svg#fill);
}
/* containers experiment */
@@ -200,7 +200,7 @@ special cases are addressed below */
}
#new-tab-overlay {
--icon-size: 26px;
--icon-size: 16px;
-moz-appearance: none;
background: transparent;
font-style: -moz-use-system-font;
@@ -252,8 +252,8 @@ special cases are addressed below */
}
#new-tab-overlay .menuitem-iconic[data-usercontextid] > .menu-iconic-left > .menu-iconic-icon {
block-height: var(--icon-size);
block-width: var(--icon-size);
block-size: var(--icon-size);
inline-size: var(--icon-size);
}
.menuitem-iconic[data-usercontextid] > .menu-iconic-left {
+22 -3
View File
@@ -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.
+84 -133
View File
@@ -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);
@@ -1020,6 +1028,8 @@ const ContainerService = {
// Let's forget all the previous closed tabs.
this._forgetIdentity();
this._resetContainerToCentralIcons();
const preInstalledIdentities = data.preInstalledIdentities;
ContextualIdentityProxy.getIdentities().forEach(identity => {
if (!preInstalledIdentities.includes(identity.userContextId)) {
@@ -1086,22 +1096,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 +1152,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,16 +1321,10 @@ 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();
@@ -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;
},
_resetContainerToCentralIcons() {
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
View 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
View 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
View 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;
+5 -1
View File
@@ -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.5.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
View 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;
+3
View File
@@ -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
+204 -77
View File
@@ -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) => {
Object.keys(siteConfigs).forEach((key) => {
// For some reason this is stored as string... lets check them both as that
if (String(siteConfigs[key].userContextId) === String(userContextId)) {
removeKeys.push(key);
}
});
this.area.remove(removeKeys);
}).catch((e) => {
throw e;
async deleteContainer(userContextId) {
const sitesByContainer = await this.getByContainer(userContextId);
this.area.remove(Object.keys(sitesByContainer));
},
async getByContainer(userContextId) {
const sites = {};
const siteConfigs = await this.area.get();
Object.keys(siteConfigs).forEach((key) => {
// For some reason this is stored as string... lets check them both as that
if (String(siteConfigs[key].userContextId) === String(userContextId)) {
const site = siteConfigs[key];
// In hindsight we should have stored this
// TODO file a follow up to clean the storage onLoad
site.hostname = key.replace(/^siteContainerMap@@_/, "");
sites[key] = site;
}
});
return sites;
}
},
@@ -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,49 +200,112 @@ const assignManager = {
// Ensure we are not in incognito mode
const url = new URL(tab.url);
if (url.protocol === "about:"
|| url.protocol === "moz-extension:"
|| tab.incognito) {
return false;
}
return true;
},
calculateContextMenu(tab) {
async _setOrRemoveAssignment(tabId, pageUrl, userContextId, remove) {
let actionName;
// https://github.com/mozilla/testpilot-containers/issues/626
// Context menu has stored context IDs as strings, so we need to coerce
// the value to a string for accurate checking
userContextId = String(userContextId);
if (!remove) {
const tabs = await browser.tabs.query({});
const assignmentStoreKey = this.storageArea.getSiteStoreKey(pageUrl);
const exemptedTabIds = tabs.filter((tab) => {
const tabStoreKey = this.storageArea.getSiteStoreKey(tab.url);
/* Auto exempt all tabs that exist for this hostname that are not in the same container */
if (tabStoreKey === assignmentStoreKey &&
this.getUserContextIdFromCookieStore(tab) !== userContextId) {
return true;
}
return false;
}).map((tab) => {
return tab.id;
});
await this.storageArea.set(pageUrl, {
userContextId,
neverAsk: false
}, exemptedTabIds);
actionName = "added";
} else {
await this.storageArea.remove(pageUrl);
actionName = "removed";
}
browser.tabs.sendMessage(tabId, {
text: `Successfully ${actionName} site to always open in this container`
});
backgroundLogic.sendTelemetryPayload({
event: `${actionName}-container-assignment`,
userContextId: userContextId,
});
const tab = await browser.tabs.get(tabId);
this.calculateContextMenu(tab);
},
async _getAssignment(tab) {
const cookieStore = this.getUserContextIdFromCookieStore(tab);
// Ensure we have a cookieStore to assign to
if (cookieStore
&& this.isTabPermittedAssign(tab)) {
return await this.storageArea.get(tab.url);
}
return false;
},
_getByContainer(userContextId) {
return this.storageArea.getByContainer(userContextId);
},
removeContextMenu() {
// There is a focus issue in this menu where if you change window with a context menu click
// you get the wrong menu display because of async
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1215376#c16
// We also can't change for always private mode
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1352102
const cookieStore = this.getUserContextIdFromCookieStore(tab);
browser.contextMenus.remove(this.MENU_ASSIGN_ID);
browser.contextMenus.remove(this.MENU_REMOVE_ID);
// Ensure we have a cookieStore to assign to
if (cookieStore
&& this.isTabPermittedAssign(tab)) {
this.storageArea.get(tab.url).then((siteSettings) => {
// ✓ This is to mitigate https://bugzilla.mozilla.org/show_bug.cgi?id=1351418
let prefix = " "; // Alignment of non breaking space, unknown why this requires so many spaces to align with the tick
let menuId = this.MENU_ASSIGN_ID;
if (siteSettings) {
prefix = "✓";
menuId = this.MENU_REMOVE_ID;
}
browser.contextMenus.create({
id: menuId,
title: `${prefix} Always Open in This Container`,
checked: true,
contexts: ["all"],
});
}).catch((e) => {
throw e;
});
}
},
reloadPageInContainer(url, userContextId, index, neverAsk = false) {
async calculateContextMenu(tab) {
this.removeContextMenu();
const siteSettings = await this._getAssignment(tab);
// Return early and not add an item if we have false
// False represents assignment is not permitted
if (siteSettings === false) {
return false;
}
// ✓ This is to mitigate https://bugzilla.mozilla.org/show_bug.cgi?id=1351418
let prefix = " "; // Alignment of non breaking space, unknown why this requires so many spaces to align with the tick
let menuId = this.MENU_ASSIGN_ID;
const tabUserContextId = this.getUserContextIdFromCookieStore(tab);
if (siteSettings &&
Number(siteSettings.userContextId) === Number(tabUserContextId)) {
prefix = "✓";
menuId = this.MENU_REMOVE_ID;
}
browser.contextMenus.create({
id: menuId,
title: `${prefix} Always Open in This Container`,
checked: true,
contexts: ["all"],
});
},
reloadPageInContainer(url, currentUserContextId, userContextId, index, neverAsk = false) {
const cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
const loadPage = browser.extension.getURL("confirm-page.html");
// False represents assignment is not permitted
// If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there
if (neverAsk) {
browser.tabs.create({url, cookieStoreId: 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 += `&currentCookieStoreId=${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);
+10 -7
View File
@@ -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>
+41 -4
View File
@@ -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
View 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;
}
+341 -58
View File
@@ -1,11 +1,56 @@
/* General Rules and Resets */
* {
font-size: inherit;
margin-block-end: 0;
margin-block-start: 0;
margin-inline-end: 0;
margin-inline-start: 0;
padding-block-end: 0;
padding-block-start: 0;
padding-inline-end: 0;
padding-inline-start: 0;
}
html {
background-color: #fefefe;
box-sizing: border-box;
font-size: 12px;
}
body {
font-family: Roboto, Noto, "San Francisco", Ubuntu, "Segoe UI", "Fira Sans", message-box, Arial, sans-serif;
inline-size: 300px;
max-inline-size: 300px;
}
html {
box-sizing: border-box;
:root {
--primary-action-color: #248aeb;
--title-text-color: #000;
--text-normal-color: #4a4a4a;
--text-heading-color: #000;
/* calculated from 12px */
--font-size-heading: 1.33rem; /* 16px */
--block-line-space-size: 0.5rem; /* 6px */
--inline-item-space-size: 0.5rem; /* 6px */
--block-line-separation-size: 0.33rem; /* 10px */
--inline-icon-space-size: 0.833rem; /* 10px */
/* Use for url and icon size */
--block-url-label-size: 2rem; /* 24px */
--inline-start-size: 1.66rem; /* 20px */
--inline-button-size: 5.833rem; /* 70px */
--icon-size: 1.166rem; /* 14px */
--small-text-size: 0.833rem; /* 10px */
--small-radius: 3px;
--icon-button-size: calc(calc(var(--block-line-separation-size) * 2) + 1.66rem); /* 20px */
}
@media (min-resolution: 1dppx) {
html {
font-size: 14px;
}
}
*,
@@ -14,6 +59,13 @@ html {
box-sizing: inherit;
}
form {
margin-block-end: 0;
margin-block-start: 0;
margin-inline-end: 0;
margin-inline-start: 0;
}
table {
border: 0;
border-spacing: 0;
@@ -30,11 +82,27 @@ table {
}
.scrollable {
border-block-start: 1px solid #f1f1f1;
inline-size: 100%;
max-block-size: 400px;
overflow: auto;
}
.offpage {
opacity: 0;
}
[hidden] {
display: none !important;
}
/* Effect borrowed from tabs in Firefox, ensure that the element flexes to the full width */
.truncate-text {
mask-image: linear-gradient(to left, transparent, black 1em);
overflow: hidden;
white-space: nowrap;
}
/* Color and icon helpers */
[data-identity-color="blue"] {
--identity-tab-color: #37adff;
@@ -51,6 +119,11 @@ table {
--identity-icon-color: #51cd00;
}
[data-identity-color="grey"] {
/* Only used for the edit panel */
--identity-icon-color: #616161;
}
[data-identity-color="yellow"] {
--identity-tab-color: #ffcb00;
--identity-icon-color: #ffcb00;
@@ -124,7 +197,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 +222,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 +277,7 @@ table {
.column-panel-content .button,
.panel-footer .button {
align-items: center;
block-size: 54px;
block-size: 100%;
display: flex;
flex: 1;
justify-content: center;
@@ -223,7 +317,7 @@ table {
.onboarding-title {
color: #43484e;
font-size: 16px;
font-size: var(--font-size-heading);
margin-block-end: 0;
margin-block-start: 0;
margin-inline-end: 0;
@@ -232,7 +326,7 @@ table {
}
.onboarding p {
color: #4a4a4a;
color: var(--text-normal-color);
font-size: 14px;
margin-block-end: 16px;
max-inline-size: 84%;
@@ -261,9 +355,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 +383,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 +398,23 @@ manage things like container crud */
.panel-header {
align-items: center;
block-size: 48px;
border-block-end: 1px solid #ebebeb;
display: flex;
justify-content: space-between;
}
.panel-header .usercontext-icon {
inline-size: var(--icon-button-size);
}
.column-panel-content .panel-header {
flex: 0 0 48px;
inline-size: 100%;
}
.panel-header-text {
color: #4a4a4a;
color: var(--text-normal-color);
flex: 1;
font-size: 16px;
font-size: var(--font-size-heading);
font-weight: normal;
margin-block-end: 0;
margin-block-start: 0;
@@ -324,6 +426,47 @@ manage things like container crud */
padding-inline-start: 16px;
}
#container-panel .panel-header {
background-color: #efefef;
block-size: 26px;
font-size: 14px;
}
#container-panel .panel-header-text {
color: #727272;
font-size: 14px;
padding-block-end: 0;
padding-block-start: 0;
text-transform: uppercase;
}
.container-panel-controls {
display: flex;
justify-content: flex-end;
margin-block-end: var(--block-line-space-size);
margin-block-start: var(--block-line-space-size);
margin-inline-end: var(--inline-item-space-size);
margin-inline-start: var(--inline-item-space-size);
}
#container-panel #sort-containers-link {
align-items: center;
block-size: var(--block-url-label-size);
border: 1px solid #d8d8d8;
border-radius: var(--small-radius);
color: var(--title-text-color);
display: flex;
font-size: var(--small-text-size);
inline-size: var(--inline-button-size);
justify-content: center;
text-decoration: none;
}
#container-panel #sort-containers-link:hover,
#container-panel #sort-containers-link:focus {
background: #f2f2f2;
}
span ~ .panel-header-text {
padding-block-end: 0;
padding-block-start: 0;
@@ -331,11 +474,92 @@ span ~ .panel-header-text {
padding-inline-start: 0;
}
#current-tab {
align-items: center;
color: var(--text-normal-color);
display: grid;
font-size: var(--small-text-size);
grid-column-gap: var(--inline-item-space-size);
grid-row-gap: var(--block-line-space-size);
grid-template-columns: var(--icon-size) var(--icon-size) 1fr;
margin-block-end: var(--block-line-space-size);
margin-block-start: var(--block-line-separation-size);
margin-inline-end: var(--inline-start-size);
margin-inline-start: var(--inline-start-size);
max-inline-size: 100%;
}
#current-tab img {
max-block-size: var(--icon-size);
}
#current-tab > h3 {
color: var(--text-heading-color);
font-weight: normal;
grid-column: span 3;
margin-block-end: 0;
margin-block-start: 0;
margin-inline-end: 0;
margin-inline-start: 0;
}
#current-page {
display: contents;
}
#current-tab .page-title {
font-size: var(--font-size-heading);
grid-column: 2 / 4;
}
#current-tab > label {
display: contents;
font-size: var(--small-text-size);
}
#current-tab > label > input {
-moz-appearance: none;
block-size: var(--icon-size);
border: 1px solid #d8d8d8;
border-radius: var(--small-radius);
display: block;
grid-column-start: 2;
inline-size: var(--icon-size);
margin-block-end: 0;
margin-block-start: 0;
margin-inline-end: 0;
margin-inline-start: 0;
}
#current-tab > label > input[disabled] {
background-color: #efefef;
}
#current-tab > label > input:checked {
background-image: url("chrome://global/skin/in-content/check.svg#check-native");
background-position: -1px -1px;
background-size: var(--icon-size);
}
#current-container {
color: var(--identity-tab-color);
flex: 1;
}
#current-tab > label > .usercontext-icon {
background-size: 16px;
block-size: 16px;
display: block;
flex: 0 0 20px;
inline-size: 20px;
margin-inline-end: 3px;
margin-inline-start: 3px;
}
/* Rows used when iterating over panels */
.container-panel-row {
align-items: center;
background-color: #fefefe !important;
block-size: 48px;
border-block-end: 1px solid #f1f1f1;
box-sizing: border-box;
display: flex;
@@ -343,12 +567,10 @@ span ~ .panel-header-text {
}
.container-panel-row .container-name {
flex: 1;
max-inline-size: 160px;
overflow: hidden;
padding-inline-end: 4px;
padding-inline-start: 4px;
text-overflow: ellipsis;
white-space: nowrap;
}
.edit-containers-panel .userContext-wrapper {
@@ -368,8 +590,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 +601,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 +632,9 @@ span ~ .panel-header-text {
justify-content: space-between;
}
.panel-footer .pop-button {
block-size: 54px;
flex: 0 0 54px;
}
.edit-containers-text {
align-items: center;
block-size: 54px;
block-size: 100%;
border-inline-end: solid 1px #d8d8d8;
display: flex;
flex: 1;
@@ -420,7 +643,7 @@ span ~ .panel-header-text {
.edit-containers-text a {
align-items: center;
block-size: 54px;
block-size: 100%;
color: #0a0a0a;
display: flex;
flex: 1;
@@ -428,11 +651,8 @@ span ~ .panel-header-text {
}
/* Container info list */
#container-info-name {
margin-inline-end: 0.5rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.container-info-tab-title {
flex: 1;
}
#container-info-hideorshow {
@@ -477,19 +697,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 +721,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 +748,7 @@ span ~ .panel-header-text {
.delete-container-confirm-title {
color: #000;
font-size: 16px;
font-size: var(--font-size-heading);
}
/* Form info */
@@ -540,36 +760,95 @@ span ~ .panel-header-text {
padding-inline-start: 16px;
}
.column-panel-content form span {
align-items: center;
block-size: 44px;
display: flex;
flex: 0 0 25%;
justify-content: center;
#edit-sites-assigned {
flex: 1;
}
.edit-container-panel label {
#edit-sites-assigned h3 {
font-size: 14px;
font-weight: normal;
padding-block-end: 6px;
padding-block-start: 6px;
padding-inline-end: 16px;
padding-inline-start: 16px;
}
.assigned-sites-list > div {
display: flex;
padding-block-end: 6px;
padding-block-start: 6px;
}
.assigned-sites-list > div > .icon {
margin-inline-end: 10px;
}
.assigned-sites-list > div > .delete-assignment {
display: none;
}
.assigned-sites-list > div:hover > .delete-assignment {
display: block;
}
.assigned-sites-list > div > .hostname {
flex: 1;
}
.radio-choice > .radio-container {
align-items: center;
block-size: 29px;
display: flex;
flex: 0 0 calc(100% / 8);
}
.radio-choice > .radio-container > label {
background: none;
block-size: 23px;
border: 0;
filter: none;
inline-size: 23px;
margin-block-end: 0;
margin-block-start: 0;
margin-inline-end: 0;
margin-inline-start: 0;
padding-block-end: 0;
padding-block-start: 0;
padding-inline-end: 0;
padding-inline-start: 0;
}
.radio-choice > .radio-container > label::before {
background-color: unset;
background-image: var(--identity-icon);
background-size: 26px 26px;
block-size: 34px;
background-position: center;
background-repeat: no-repeat;
background-size: 16px;
block-size: 23px;
border: none;
content: "";
display: block;
fill: var(--identity-icon-color);
filter: url('/img/filters.svg#fill');
flex: 0 0 34px;
inline-size: 23px;
position: relative;
}
.edit-container-panel label::before {
opacity: 0 !important;
}
.edit-container-panel [type="radio"] {
.radio-choice > .radio-container > [type="radio"] {
-moz-appearance: none;
display: inline;
opacity: 0;
}
.edit-container-panel [type="radio"]:checked + label {
outline: 2px solid grey;
-moz-outline-radius: 50px;
.radio-choice > .radio-container > [type="radio"]:checked + label {
background: #d3d3d3;
border-radius: 100%;
}
/* When focusing the element add a thin blue highlight to match input fields. This gives a distinction to other selected radio items */
.radio-choice > .radio-container > [type="radio"]:focus + label {
outline: 1px solid #1f9ffc;
-moz-outline-radius: 100%;
}
.edit-container-panel fieldset {
@@ -588,6 +867,10 @@ span ~ .panel-header-text {
padding-inline-start: 0;
}
.edit-container-panel fieldset:last-of-type {
margin-block-end: 0;
}
.edit-container-panel input[type="text"] {
block-size: 36px;
border-radius: 3px;
@@ -602,5 +885,5 @@ span ~ .panel-header-text {
.edit-container-panel legend {
flex: 1 0;
font-size: 14px !important;
padding-block-end: 5px;
padding-block-end: 6px;
}
+3
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+75 -14
View File
@@ -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) => {
e.preventDefault();
const container = await browser.contextualIdentities.get(cookieStoreId);
[...document.querySelectorAll(".container-name")].forEach((containerNameElement) => {
containerNameElement.textContent = container.name;
});
// If default container, button will default to normal HTML content
if (currentCookieStoreId) {
const currentContainer = await browser.contextualIdentities.get(currentCookieStoreId);
document.getElementById("current-container-name").textContent = currentContainer.name;
}
document.getElementById("redirect-form").addEventListener("submit", (e) => {
e.preventDefault();
const buttonTarget = e.explicitOriginalTarget;
switch (buttonTarget.id) {
case "confirm":
confirmSubmit(redirectUrl, cookieStoreId);
break;
case "deny":
denySubmit(redirectUrl);
break;
}
});
}
function appendFavicon(pageUrl, redirectUrlElement) {
const origin = new URL(pageUrl).origin;
const favIconElement = Utils.createFavIconElement(`${origin}/favicon.ico`);
redirectUrlElement.prepend(favIconElement);
}
function confirmSubmit(redirectUrl, cookieStoreId) {
const neverAsk = document.getElementById("never-ask").checked;
// Sending neverAsk message to background to store for next time we see this process
if (neverAsk) {
@@ -12,20 +47,46 @@ 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({
index: tab[0].index + 1,
cookieStoreId,
url: redirectUrl
});
if (tab.length > 0) {
browser.tabs.remove(tab[0].id);
}
}
+42
View 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);
});
+333 -58
View File
@@ -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);
// If localStorage is disabled, we don't show the onboarding.
const data = await browser.storage.local.get([ONBOARDING_STORAGE_KEY]);
let onboarded = data[ONBOARDING_STORAGE_KEY];
if (!onboarded) {
// Legacy local storage used before panel 5
if (localStorage.getItem("onboarded4")) {
onboarded = 4;
} else if (localStorage.getItem("onboarded3")) {
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,13 +344,16 @@ const Logic = {
Logic.registerPanel(P_ONBOARDING_1, {
panelSelector: ".onboarding-panel-1",
securityPanelSelector: ".security-onboarding-panel-1",
// This method is called when the object is registered.
initialize() {
// Let's move to the next panel.
Logic.addEnterHandler(document.querySelector("#onboarding-start-button"), () => {
localStorage.setItem("onboarded1", true);
Logic.showPanel(P_ONBOARDING_2);
[...document.querySelectorAll(".onboarding-start-button")].forEach(startElement => {
Logic.addEnterHandler(startElement, async function () {
await Logic.setOnboardingStage(1);
Logic.showPanel(P_ONBOARDING_2);
});
});
},
@@ -254,13 +368,16 @@ Logic.registerPanel(P_ONBOARDING_1, {
Logic.registerPanel(P_ONBOARDING_2, {
panelSelector: ".onboarding-panel-2",
securityPanelSelector: ".security-onboarding-panel-2",
// This method is called when the object is registered.
initialize() {
// Let's move to the containers list panel.
Logic.addEnterHandler(document.querySelector("#onboarding-next-button"), () => {
localStorage.setItem("onboarded2", true);
Logic.showPanel(P_ONBOARDING_3);
[...document.querySelectorAll(".onboarding-next-button")].forEach(nextElement => {
Logic.addEnterHandler(nextElement, async function () {
await Logic.setOnboardingStage(2);
Logic.showPanel(P_ONBOARDING_3);
});
});
},
@@ -275,13 +392,16 @@ 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);
Logic.showPanel(P_ONBOARDING_4);
[...document.querySelectorAll(".onboarding-almost-done-button")].forEach(almostElement => {
Logic.addEnterHandler(almostElement, async function () {
await Logic.setOnboardingStage(3);
Logic.showPanel(P_ONBOARDING_4);
});
});
},
@@ -300,8 +420,29 @@ Logic.registerPanel(P_ONBOARDING_4, {
// This method is called when the object is registered.
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"), () => {
Logic.showPreviousPanel();
const formValues = new FormData(this._editForm);
if (formValues.get("container-id") !== NEW_CONTAINER_ID) {
this._submitForm();
} else {
Logic.showPreviousPanel();
}
});
Logic.addEnterHandler(document.querySelector("#edit-container-cancel-link"), () => {
@@ -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
View 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;
}
};
+17 -4
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Containers Experiment",
"version": "2.3.0",
"version": "2.5.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"
]
}
+64 -18
View File
@@ -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>