Compare commits

...

222 Commits

Author SHA1 Message Date
luke crouch 31ac365e6d Merge pull request #974 from mozilla/bump-version-to-4.1.0
bump version to 4.1.0
2017-11-16 09:50:59 -06:00
groovecoder df8471a4dd bump version to 4.1.0 2017-11-16 09:47:18 -06:00
groovecoder 18539f2540 ignore webextension/web-ext-artifacts 2017-11-16 15:38:34 +00:00
Jonathan Kingston 7c1105a2b7 Change (un)install changes to only work for >57. Fixes #858 and Fixes #900 2017-11-16 15:38:34 +00:00
groovecoder 31298146f3 when user reaches container tab count, show achievement panel 2017-11-16 15:30:02 +00:00
groovecoder 4e6eee220c start containerTabsOpened counter 2017-11-16 15:30:02 +00:00
baku a7be3c9935 TextEncoder/TextDecoder from Cu.importGlobalProperties - #949 2017-11-14 19:36:34 +00:00
luke crouch f512473986 Merge pull request #882 from mozilla/update-README-distribute-steps
add steps for signing, AMO, and GitHub to README
2017-09-29 14:21:22 -05:00
groovecoder 8166a37722 add steps for signing, AMO, and GitHub to README 2017-09-29 13:15:16 -05:00
groovecoder adadb98482 bump version to 4.0.3 2017-09-29 18:04:53 +01:00
luke crouch 25e760cd64 Merge pull request #879 from jonathanKingston/fix-disable-notice
Clean up disabled Private Mode notice. Fixes #878
2017-09-29 11:24:24 -05:00
luke crouch 0ff8e17005 Merge pull request #870 from jonathanKingston/delete-non-existent-container
Fix assignment of stale containers. Fixes #803
2017-09-29 11:06:03 -05:00
Jonathan Kingston c433c6b39e Clean up disabled Private Mode notice. Fixes #878 2017-09-29 16:59:51 +01:00
Jonathan Kingston 1c09c29104 Fix assignment of stale containers. Fixes #803 Fixes #827 2017-09-27 13:51:40 +01:00
luke crouch c1e9cc3c56 Merge pull request #859 from jonathanKingston/prevent-enter-handler-calling-click
Preventing default on enter handler as it seems to call click handler…
2017-09-26 10:46:53 -05:00
luke crouch 27296d24c5 Merge pull request #857 from jonathanKingston/change-wording
Change delete title to remove. Fixes #700
2017-09-26 10:38:57 -05:00
luke crouch 030e635417 Merge pull request #853 from jonathanKingston/bump-version-and-min-ff-version
Add strict min version and extension id and bump version to 4.0.2. Fixes
2017-09-26 10:37:21 -05:00
Jonathan Kingston 07711aaecc Preventing default on enter handler as it seems to call click handlers now. Fixes #856 2017-09-26 11:16:58 +01:00
Jonathan Kingston 16ed8992e2 Change delete title to remove. Fixes #700 2017-09-26 02:50:10 +01:00
Jonathan Kingston 88e6dc7a05 Add strict min version and extension id and bump version to 4.0.2. Fixes #692. Fixes #852 2017-09-23 23:36:24 +01:00
luke crouch 28e8d46743 Merge pull request #834 from jonathanKingston/storage-clean
Store only one of the current version opened. Fixes #833
2017-09-20 15:47:06 -05:00
Jonathan Kingston 77ba1b723f Store only one of the current version opened. Fixes #833 2017-09-20 02:26:50 +01:00
Jonathan Kingston b0cc6e7c2f Merge pull request #818 from jonathanKingston/update-readme-launch-notice
Minor README edit
2017-09-15 00:17:42 +01:00
Jonathan Kingston e84e482130 Minor README edit 2017-09-15 00:17:05 +01:00
Jonathan Kingston b5ae20b874 Merge pull request #817 from jonathanKingston/update-readme-launch-notice
Add launch notice in README
2017-09-14 23:16:55 +01:00
Jonathan Kingston 3ec81e3d1f Add launch notice in README 2017-09-14 23:15:01 +01:00
luke crouch fb5436c287 Merge pull request #815 from jonathanKingston/blob-image
Fix dumping UUID image into the page. Fixes #812
2017-09-13 20:04:05 -05:00
Jonathan Kingston 01a628822b Fix dumping UUID image into the page. Fixes #812 2017-09-14 01:48:32 +01:00
Jonathan Kingston 66e2c8e297 Merge pull request #811 from mozilla/bump-version-4.0.2
bump version to 4.0.2
2017-09-13 22:32:54 +01:00
groovecoder 80661d68f2 fix #809: use "Containers" for name for context menu 2017-09-13 16:16:43 -05:00
groovecoder ef8aa3be75 bump version to 4.0.2 2017-09-13 10:07:56 -05:00
luke crouch 6bc056e019 Merge pull request #794 from jonathanKingston/hide-button-in-queue
Add show button to use showing queue to prevent dupes. Fixes #793
2017-09-07 14:28:47 -05:00
Jonathan Kingston 75deab139b Fix a moving hidden tabs to a new window. Fixes #797 2017-09-07 12:03:28 -07:00
Jonathan Kingston ae79f0a303 Ignore non permissible urls when hiding as we can't open them which causes issues. Fixes #793 2017-09-07 10:12:25 -07:00
Jonathan Kingston 9b83068234 Add show button to use showing queue to prevent dupes. Fixes #791 2017-09-07 09:25:13 -07:00
luke crouch fec2be9429 Merge pull request #789 from jonathanKingston/encode-url-fix
Encode non conforming chars that break moz-extension urls. Fixes #787
2017-09-07 09:05:16 -05:00
luke crouch 9f1b06ddd3 Merge pull request #790 from jonathanKingston/jpm-ignore-more
Ignore more files with .jpmignore
2017-09-06 14:46:45 -05:00
Jonathan Kingston ad2198e8b5 Encode non conforming chars that break moz-extension urls. Fixes #787 2017-09-05 17:10:07 -07:00
Jonathan Kingston 1791fdf0ef Ignore more files with .jpmignore 2017-09-05 17:09:36 -07:00
luke crouch 4ab705081e Merge pull request #788 from mozilla/bump-version-to-4.0.0
bump version to 4 for AMO
2017-09-05 14:25:57 -05:00
groovecoder 15b9dce1a9 AMO needs another version bump 2017-09-05 14:23:33 -05:00
groovecoder da3fc2ede2 bump version to 4.0.0 2017-09-05 13:33:34 -05:00
luke crouch 385c585888 Merge pull request #784 from jonathanKingston/fix-new-tab
Simplify new tab creation in the popup fixing issues. Fixes #781
2017-09-05 13:30:34 -05:00
luke crouch 734b97beb0 Merge pull request #786 from jonathanKingston/name-change
Name change. Fixes #763
2017-09-05 10:15:53 -05:00
Jonathan Kingston 3aa311a3c1 Name change. Fixes #763 2017-09-01 15:16:56 -07:00
Jonathan Kingston df7d7f9c38 Simplify new tab creation in the popup fixing issues. Fixes #781 2017-09-01 12:27:40 -07:00
luke crouch 65be77665a Merge pull request #783 from jonathanKingston/reset-prefs-on-uninstall
Reset prefs on uninstall
2017-09-01 11:08:06 -05:00
luke crouch 44548659db Merge pull request #780 from jonathanKingston/apply-styles-to-legacy
Add styles for 55+56 versions of Firefox so everyone gets a consisten…
2017-09-01 11:01:02 -05:00
Jonathan Kingston 10c4395efd Reset prefs for new users on uninstall. Fixes #782 2017-08-31 15:00:40 -07:00
Jonathan Kingston 27b2a4b5f2 Add styles for 55+56 versions of Firefox so everyone gets a consistent underline. Fixes #779 2017-08-31 11:58:03 -07:00
luke crouch 6f54e7ff7f Merge pull request #776 from jonathanKingston/context-menu-neaten
Add move and hide to context menu and neaten using checkboxes. Fixes …
2017-08-30 15:48:38 -05:00
Jonathan Kingston cb6726b667 Add move and hide to context menu and neaten using checkboxes. Fixes #711 2017-08-30 13:20:57 -07:00
luke crouch 0964311fa1 Merge pull request #778 from jonathanKingston/web-ext-advice
Add web extension specific advice on building. Fixes #751
2017-08-30 15:13:44 -05:00
luke crouch 2831d019f5 Merge pull request #777 from jonathanKingston/container-removal-fixes
Handle removing containers to refresh menus and remove assignments. F…
2017-08-30 15:13:03 -05:00
luke crouch 1cc58cad9b Merge pull request #775 from jonathanKingston/remove-unusable-paths-on-show
Remove about: paths from showTabs as it prevents the tabs being creat…
2017-08-30 15:00:26 -05:00
luke crouch bc9660f76e Merge pull request #774 from jonathanKingston/hidden-tab-open
Add a tab observer to show hidden tabs as there are many tab creation…
2017-08-30 14:55:06 -05:00
Jonathan Kingston f526caca50 Add web extension specific advice on building. Fixes #751 2017-08-30 11:12:55 -07:00
Jonathan Kingston b6a98fb83e Handle removing containers to refresh menus and remove assignments. Fixes: #761, Fixes: #752 2017-08-28 21:55:29 -07:00
Jonathan Kingston af2b4b79a9 Remove about: paths from showTabs as it prevents the tabs being created. Fixes #773 2017-08-28 17:46:55 -07:00
Jonathan Kingston a762b5eca2 Add a tab observer to show hidden tabs as there are many tab creation routes. Uses a queue to prevent multiple triggers. Fixes #765 2017-08-28 17:07:46 -07:00
luke crouch 3cc40344af Merge pull request #760 from jonathanKingston/removalOfTabPageCounter
Removal of tab page counter code. Fixes #759
2017-08-25 11:44:51 -05:00
luke crouch 278cdb7f69 Merge pull request #762 from jonathanKingston/change-to-window-only
Remove tab counting code as also not needed, change tab counting to b…
2017-08-25 11:27:45 -05:00
Jonathan Kingston 0be03ebeb7 Remove tab counting code as also not needed, change tab counting to be per window. Prevent reopening of hidden tabs doesn't call itself. Fixes #750, Fixes #753, Fixes #756 2017-08-24 16:05:14 +01:00
Jonathan Kingston c69f37a2de Removal of tab page counter code. Fixes #759 2017-08-24 15:04:47 +01:00
Jonathan Kingston b20ac8169a Merge pull request #758 from mozilla/move-to-AMO-665
for #665: update title, desc, version for AMO
2017-08-24 15:02:43 +01:00
groovecoder 9e98d35b45 for #665: update title, desc, version for AMO 2017-08-23 12:53:23 -05:00
luke crouch 17f2781e07 Merge pull request #757 from jonathanKingston/remove-pb-mode
Extension shouldn't be enabled in pb mode as it's currently not worki…
2017-08-23 12:48:38 -05:00
luke crouch 0acf9cc0e6 Merge pull request #754 from jonathanKingston/move-containers-fix
Fix moving of more than one tab to a new window. Fixes #746
2017-08-23 12:40:11 -05:00
Jonathan Kingston 70573d0559 Extension shouldn't be enabled in pb mode as it's currently not working in many places. Fixes #756 2017-08-23 15:23:49 +01:00
Jonathan Kingston da239237f7 Fix moving of more than one tab to a new window. Fixes #746 2017-08-22 18:54:32 +01:00
luke crouch 29f078d2c9 Merge pull request #726 from jonathanKingston/bootstrapify
Bootstrapify
2017-08-15 16:31:13 -05:00
Jonathan Kingston 5704a21c97 Merge remote-tracking branch 'upstream/master' into bootstrapify 2017-08-15 22:24:32 +01:00
Jonathan Kingston 57a31f7f97 Remove legacy telemetry code as non functional now 2017-08-15 19:30:57 +01:00
Jonathan Kingston d685a58d74 Bootstrapify extension to work for Firefox Nightly 57 SDK removal. Fixes #725 2017-08-15 16:53:43 +01:00
Jonathan Kingston a44bf21582 Merge pull request #713 from jonathanKingston/svg-icon-context-change
Svg icon context change
2017-08-15 10:50:44 +01:00
luke crouch 8f8fc322eb Merge pull request #715 from jonathanKingston/remove-legacy-css
Removal of legacy CSS for builds after 2015-08-06. Fixes #714
2017-08-07 14:05:51 -05:00
Jonathan Kingston f03404ad9e Removal of legacy CSS for builds after 2015-08-06. Fixes #714 2017-08-07 19:50:37 +01:00
Jonathan Kingston 78b5de3b44 Adding in SVG icon for context-fill colours. Fixes #710 2017-08-05 10:28:27 +01:00
Jonathan Kingston e78f49bec5 Making icons sharper since hand crafting your own svg was cool. 2017-08-05 09:59:45 +01:00
luke crouch ee8c69b73e Merge pull request #605 from jonathanKingston/sdk-mamoth-breakage
Shirk my SDK code
2017-08-04 20:13:20 +00:00
Jonathan Kingston 4dfffb8c34 Merge remote-tracking branch 'upstream/master' into sdk-mamoth-breakage 2017-08-04 18:21:48 +01:00
Jonathan Kingston fe77c891cd Adding back in legacy code without message passing code. 2017-08-04 16:28:53 +01:00
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 12a6bb3b9b Break web extensions code 2017-08-02 14:20:59 +01:00
Jonathan Kingston 175cdc1a6b Break my SDK code 2017-08-02 14:20:59 +01:00
Jonathan Kingston 0ec7e4aee3 Use new theme_icons manifest for specifying light and dark icons. Fixes #603 2017-08-02 14:20:59 +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
luke crouch bb96ab9c37 Merge pull request #531 from mozilla/version-2.3.0
bump version to 2.3.0
2017-05-24 15:10:29 -05:00
groovecoder 6a10c1c970 bump version to 2.3.0 2017-05-24 12:11:24 -05:00
luke crouch 1d385d2aaa Merge pull request #530 from jonathanKingston/removal-of-tab-state-context
Removal of tab state context menu as unable to detect tab assignment …
2017-05-24 11:14:30 -05:00
Jonathan Kingston fd918408f4 Removal of tab state context menu as unable to detect tab assignment state. Fixes #520 2017-05-24 02:35:48 +01: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
luke crouch a99cd9c8ef Merge pull request #515 from jonathanKingston/fix-underline
Remove underline from buttons. Fixes: #514
2017-05-18 09:08:40 -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
Jonathan Kingston 1819e6cde9 Remove underline from buttons. Fixes: #514 2017-05-17 17:38:55 +01: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
luke crouch 46b155c90f Merge pull request #487 from jonathanKingston/remove-sdk-further
Interim WIP patch of more removal
2017-05-17 11:25:46 -05:00
Jonathan Kingston 31f3a76a1c Merge branch 'asyncify' into remove-sdk-further 2017-05-17 16:09:33 +01:00
Jonathan Kingston 4ffb587d9e Removal of more SDK code 2017-05-17 16:08:29 +01:00
Jonathan Kingston 4d61fa190c Removal of more SDK code 2017-05-17 14:24:50 +01:00
Jonathan Kingston 4a97e07d43 Adding in shortcut and keyboard controls 2017-05-17 11:24:17 +01:00
luke crouch 41df7a10dc Merge pull request #510 from jonathanKingston/change-check-for-storage
Undefined storage area. Fixes #508
2017-05-16 13:29:07 -05:00
luke crouch 5089091617 Merge pull request #506 from mozilla/assignment-onboarding
Assignment onboarding panel and icon badge
2017-05-16 13:07:48 -05:00
groovecoder 54ccf5b9ec async/await in popup code too 2017-05-16 13:01:59 -05:00
groovecoder ee98034572 ecmaVersion 8 for eslint fixes 2017-05-16 12:29:45 -05:00
groovecoder d8fd47a353 move all badge logic into WebExtension code 2017-05-16 12:20:10 -05:00
Jonathan Kingston 02300630f6 Undefined storage area. Fixes #508 2017-05-16 11:04:23 +01:00
groovecoder 3805f12e17 Logic.clearBrowserActionBadge method 2017-05-15 16:22:35 -05:00
groovecoder 4a48a7debb show 'NEW' icon badge for major upgrades 2017-05-11 11:56:25 -05:00
luke crouch 26e6efd611 Merge pull request #488 from jonathanKingston/move-to-no-unescaped-eslint
Move to the Mozilla backed no-unsanitized ESLint plugin
2017-05-09 11:07:40 -05:00
Jonathan Kingston 92ab56448c Move to the Mozilla backed no-unsanitized ESLint plugin 2017-05-09 13:29:08 +01:00
luke crouch ac02c45e81 Merge pull request #485 from jonathanKingston/remove-sdk
Remove sdk
2017-05-08 14:06:42 -05:00
Jonathan Kingston 1ec86c7fd2 Moving remove, add and update code into the web extension background for stability 2017-05-08 14:32:35 +01:00
groovecoder 8f80b527f5 start onboarding-panel-4 for site assignments 2017-05-05 13:46:58 -05:00
groovecoder 337dee2061 more descriptive _containerTabs method name 2017-05-04 09:44:49 -05:00
Jonathan Kingston f2ddc7fd84 Moving create and update containers into popup.js 2017-05-04 09:44:49 -05:00
Jonathan Kingston f4597eae84 Moving bulk of removeIdentity code into popup.js 2017-05-04 09:44:49 -05:00
luke crouch 5887f991b3 Merge pull request #470 from bakulf/crash
Recovering after a crash - issue #463
2017-05-04 09:24:13 -05:00
baku 1de3f42385 Recovering after a crash - issue #463 2017-04-28 16:56:55 +02:00
baku 91ec0c4a6d Pinned tabs should be stored as such - issue #456 2017-04-28 14:44:20 +01:00
luke crouch 0b1d6e9087 Merge pull request #447 from jonathanKingston/tab-menu-item
Adding in context menu to tab context menu. Fixes #424.
2017-04-24 15:45:57 -05:00
Jonathan Kingston 2ee9682950 Adding in context menu to tab context menu. Fixes #424. 2017-04-12 17:57:23 +01:00
40 changed files with 2756 additions and 3138 deletions
+1 -1
View File
@@ -1 +1 @@
testpilot-metrics.js
lib/testpilot/*.js
+26 -7
View File
@@ -1,22 +1,31 @@
module.exports = {
"parserOptions": {
"ecmaVersion": 8
},
"env": {
"browser": true,
"es6": true,
"node": true,
"webextensions": true
},
"extends": [
"eslint:recommended"
],
"globals": {
"Utils": true,
"CustomizableUI": true,
"CustomizableWidgets": true,
"SessionStore": true,
"Services": true
"Services": true,
"Components": true,
"XPCOMUtils": true,
"OS": true,
"ADDON_UNINSTALL": true,
"ADDON_DISABLE": true
},
"plugins": [
"promise",
"no-unescaped"
"no-unsanitized"
],
"extends": [
"eslint:recommended"
],
"root": true,
"rules": {
@@ -29,8 +38,18 @@ module.exports = {
"promise/no-promise-in-callback": "warn",
"promise/no-return-wrap": "error",
"promise/param-names": "error",
"no-unescaped/no-key-assignment": "error",
"no-unescaped/enforce": "error",
"no-unsanitized/method": [
"error"
],
"no-unsanitized/property": [
"error",
{
"escape": {
"taggedTemplates": ["escaped"]
}
}
],
"eqeqeq": "error",
"indent": ["error", 2],
+4
View File
@@ -1,8 +1,12 @@
.DS_Store
package-lock.json
node_modules
README.html
*.xpi
*.swp
*.swo
.vimrc
.env
addon.env
webextension/web-ext-artifacts/*
+2
View File
@@ -3,6 +3,7 @@ docs/
test/
.npm/
node_modules/
bin/
.env
.eslintrc.js
@@ -14,6 +15,7 @@ node_modules/
.stylelintrc
.travis.yml
*.xpi
*.md
.vimrc
.DS_Store
.gdb_history
+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.
+79 -38
View File
@@ -1,54 +1,69 @@
# Containers: Test Pilot Experiment
# Multi-Account Containers
[![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:
The Firefox Multi-Account Containers extension lets you carve out a separate box for each of your online lives no more opening a different browser just to check your work email! [Learn More Here](https://blog.mozilla.org/firefox/introducing-firefox-multi-account-containers/)
* Will a general Firefox audience understand the Containers feature?
* Is the UI as currently implemented in Nightly clear or discoverable?
[Available on addons.mozilla.org](https://addons.mozilla.org/en-GB/firefox/addon/multi-account-containers/)
See [the Product Hypothesis Document for more
details](https://docs.google.com/document/d/1WQdHTVXROk7dYkSFluc6_hS44tqZjIrG9I-uPyzevE8/edit?ts=5824ba12#).
**Note:** Firefox 57 + 58 users should Install from our [latest GitHub Release](https://github.com/mozilla/testpilot-containers/releases/latest)
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
### Web Extension Development
Since Firefox 57, this extension can now be run without any of the legacy components that were previously needed.
1. Install web-ext with npm
2. cd webextension; web-ext run -f Nightly
This will work in other builds of Firefox however certain features won't work and you will need to manually flip preferences to enable containers. All other sections of this guide talk about using the legacy setup with jpm.
## Legacy Development
### Development Environment
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:
#### Correct prefs
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-...)
Whilst this is still using legacy code to test you will need the following in your profile:
Change the following prefs in about:config:
- extensions.legacy.enabled = true
- xpinstall.signatures.required = false
### Run with jpm
#### Run the TxP experiment with `jpm`
1. `git clone git@github.com:mozilla/testpilot-containers.git`
2. `cd testpilot-containers`
@@ -57,24 +72,50 @@ 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.
### 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.
### Signing an .xpi
To sign an .xpi, use [`jpm
sign`](https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/jpm#jpm_sign)
command.
Note: You will need to be [an author on the AMO
add-on](https://addons.mozilla.org/en-US/developers/addon/containers-experiment/ownership).
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`.
### Testing
TBD
### Distributing
TBD
#### Make the new version
1. Bump the version number in `package.json`, `install.rdf`, and
`manifest.json`
2. Commit the version number bump
3. Create a git tag for the version: `git tag <version>`
4. Push the tag up to GitHub: `git push --tags`
#### Publish to AMO
While the add-on is an Embedded Web Extension, we have to use the [Mozilla
Internal Signing
Service](https://mana.mozilla.org/wiki/display/FIREFOX/Internal+Extension+Signing)
to sign it as a Mozilla extension exempt from AMO's Web Extension restrictions.
So, to distribute the add-on to AMO:
1. Use `jpm xpi` to build the `.xpi` file
2. [Submit the `.xpi` to the Internal Signing Service and download the signed `.xpi`](https://mana.mozilla.org/wiki/display/SVCOPS/Sign+a+Mozilla+Internal+Extension)
3. [Upload the signed `.xpi` file to
AMO](https://addons.mozilla.org/en-US/developers/addon/multi-account-containers/versions/submit/)
#### Publish to GitHub
Finally, we also publish the release to GitHub for those followers.
1. [Make the new release on
GitHub](https://github.com/mozilla/multi-account-containers/releases/new)
* Use the version number for "Tag version" and "Release title"
* Release notes: copy the output of `git log --no-merges --pretty=format:"%h %s" <previous-version>..<new-version>`
* Attach binaries: select the signed `.xpi` file
### Links
Facebook & Twitter icons CC-Attrib http://fairheadcreative.com.
- [Licence](./LICENSE.txt)
- [Contributing](./CONTRIBUTING.md)
- [Code Of Conduct](./CODE_OF_CONDUCT.md)
+187
View File
@@ -0,0 +1,187 @@
"use strict";
const PREFS = [
{
name: "privacy.userContext.enabled",
value: true,
type: "bool",
default: false
},
{
name: "privacy.userContext.longPressBehavior",
value: 2,
type: "int",
default: 0
},
{
name: "privacy.userContext.ui.enabled",
value: true, // Post web ext we will be setting this true
type: "bool",
default: true
},
{
name: "privacy.usercontext.about_newtab_segregation.enabled",
value: true,
type: "bool",
default: false
},
];
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cc = Components.classes;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.importGlobalProperties(["TextEncoder", "TextDecoder"]);
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
const JETPACK_DIR_BASENAME = "jetpack";
const EXTENSION_ID = "@testpilot-containers";
function loadStyles(resourceURI) {
const styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"]
.getService(Ci.nsIStyleSheetService);
const styleURI = styleSheet(resourceURI);
const sheetType = styleSheetService.AGENT_SHEET;
styleSheetService.loadAndRegisterSheet(styleURI, sheetType);
}
function styleSheet(resourceURI) {
return Services.io.newURI("data/usercontext.css", null, resourceURI);
}
function unloadStyles(resourceURI) {
const styleURI = styleSheet(resourceURI);
const styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"]
.getService(Ci.nsIStyleSheetService);
const sheetType = styleSheetService.AGENT_SHEET;
if (styleSheetService.sheetRegistered(styleURI, sheetType)) {
styleSheetService.unregisterSheet(styleURI, sheetType);
}
}
function filename() {
const storeFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
storeFile.append(JETPACK_DIR_BASENAME);
storeFile.append(EXTENSION_ID);
storeFile.append("simple-storage");
storeFile.append("store.json");
return storeFile.path;
}
async function makeFilepath() {
const storeFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
storeFile.append(JETPACK_DIR_BASENAME);
await OS.File.makeDir(storeFile.path, { ignoreExisting: true });
storeFile.append(EXTENSION_ID);
await OS.File.makeDir(storeFile.path, { ignoreExisting: true });
storeFile.append("simple-storage");
await OS.File.makeDir(storeFile.path, { ignoreExisting: true });
}
async function getConfig() {
let savedConfig = {savedConfiguration: {}};
try {
const bytes = await OS.File.read(filename());
const raw = new TextDecoder().decode(bytes) || "";
if (raw) {
savedConfig = JSON.parse(raw);
}
} catch (e) {
// ignore file read errors, sometimes they happen and I'm not sure if we can fix
}
return savedConfig;
}
async function initConfig() {
const savedConfig = await getConfig();
savedConfig.savedConfiguration.version = 2;
if (!("prefs" in savedConfig.savedConfiguration)) {
savedConfig.savedConfiguration.prefs = {};
PREFS.forEach((pref) => {
if ("int" === pref.type) {
savedConfig.savedConfiguration.prefs[pref.name] = Services.prefs.getIntPref(pref.name);
} else {
savedConfig.savedConfiguration.prefs[pref.name] = Services.prefs.getBoolPref(pref.name);
}
});
}
const serialized = JSON.stringify(savedConfig);
const bytes = new TextEncoder().encode(serialized) || "";
await makeFilepath();
await OS.File.writeAtomic(filename(), bytes, { });
}
function setPrefs() {
PREFS.forEach((pref) => {
if ("int" === pref.type) {
Services.prefs.setIntPref(pref.name, pref.value);
} else {
Services.prefs.setBoolPref(pref.name, pref.value);
}
});
}
// eslint-disable-next-line no-unused-vars
async function install() {
await initConfig();
setPrefs();
}
// eslint-disable-next-line no-unused-vars
async function uninstall({resourceURI}, aReason) {
if (checkLegacyFirefox()) {
if (aReason === ADDON_UNINSTALL) {
unloadStyles(resourceURI);
await removeChanges();
}
}
}
async function removeChanges() {
const config = await getConfig();
const storedPrefs = config.savedConfiguration.prefs || {};
PREFS.forEach((pref) => {
let value = pref.default;
if (pref.name in storedPrefs) {
value = storedPrefs[pref.name];
}
if ("int" === pref.type) {
Services.prefs.setIntPref(pref.name, value);
} else {
Services.prefs.setBoolPref(pref.name, value);
}
});
}
function checkLegacyFirefox() {
const version = Services.appinfo.version;
const versionMatch = version.match(/^([0-9]+)\./)[1];
if (Number(versionMatch) <= 56) {
return true;
}
return false;
}
// eslint-disable-next-line no-unused-vars
function startup({webExtension, resourceURI}) {
if (checkLegacyFirefox()) {
loadStyles(resourceURI);
// Reset prefs that may have changed, or are legacy
install();
}
// Start the embedded webextension.
webExtension.startup();
}
// eslint-disable-next-line no-unused-vars
function shutdown({resourceURI}, aReason) {
if (checkLegacyFirefox()) {
unloadStyles(resourceURI);
if (aReason === ADDON_DISABLE) {
removeChanges();
}
}
}
+3 -121
View File
@@ -1,108 +1,3 @@
/* HACK: Custom Container vars do not propigate correctly
until the container tab is blurred and refocused,
adding the data-identity-color with the default hex
value, or chrome url path as an alternate selector mitiages this bug.*/
[data-identity-color="blue"],
[data-identity-color="#00a7e0"] {
--identity-tab-color: #37adff;
--identity-icon-color: #37adff;
}
[data-identity-color="turquoise"],
[data-identity-color="#01bdad"] {
--identity-tab-color: #00c79a;
--identity-icon-color: #00c79a;
}
[data-identity-color="green"],
[data-identity-color="#7dc14c"] {
--identity-tab-color: #51cd00;
--identity-icon-color: #51cd00;
}
[data-identity-color="yellow"],
[data-identity-color="#ffcb00"] {
--identity-tab-color: #ffcb00;
--identity-icon-color: #ffcb00;
}
[data-identity-color="orange"],
[data-identity-color="#f89c24"] {
--identity-tab-color: #ff9f00;
--identity-icon-color: #ff9f00;
}
[data-identity-color="red"],
[data-identity-color="#d92215"] {
--identity-tab-color: #ff613d;
--identity-icon-color: #ff613d;
}
[data-identity-color="pink"],
[data-identity-color="#ee5195"] {
--identity-tab-color: #ff4bda;
--identity-icon-color: #ff4bda;
}
[data-identity-color="purple"],
[data-identity-color="#7a2f7a"] {
--identity-tab-color: #af51f5;
--identity-icon-color: #af51f5;
}
[data-identity-icon="fingerprint"],
[data-identity-icon="chrome://browser/skin/usercontext/personal.svg"] {
--identity-icon: url("resource://testpilot-containers/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");
}
[data-identity-icon="dollar"],
[data-identity-icon="chrome://browser/skin/usercontext/banking.svg"] {
--identity-icon: url("resource://testpilot-containers/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");
}
[data-identity-icon="circle"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#circle");
}
[data-identity-icon="gift"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#gift");
}
[data-identity-icon="vacation"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#vacation");
}
[data-identity-icon="food"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#food");
}
[data-identity-icon="fruit"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#fruit");
}
[data-identity-icon="pet"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#pet");
}
[data-identity-icon="tree"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#tree");
}
[data-identity-icon="chill"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#chill");
}
#userContext-indicator {
height: 16px;
list-style-image: none !important;
@@ -129,19 +24,6 @@ value, or chrome url path as an alternate selector mitiages this bug.*/
display: none;
}
.userContext-icon,
.menuitem-iconic[data-usercontextid] > .menu-iconic-left > .menu-iconic-icon,
.subviewbutton[usercontextid] > .toolbarbutton-icon,
#userContext-indicator {
background-image: var(--identity-icon) !important;
background-position: center center;
background-repeat: no-repeat;
background-size: contain;
fill: var(--identity-icon-color) !important;
filter: url(/img/filters.svg#fill);
filter: url(resource://testpilot-containers/data/filters.svg#fill);
}
/* containers experiment */
/* reset nightly containers */
@@ -200,7 +82,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 +134,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 {
-266
View File
@@ -1,266 +0,0 @@
# METRICS
## Data Analysis
The collected data will primarily be used to answer the following questions.
Images are used for visualization and are not composed of actual data.
### Do users install and run this?
What is the overall engagement of the Containers experiment?
**This is the standard Daily Active User (DAU) and Monthly Active User (MAU) analysis.**
This captures data from the users who have the add-on installed, regardless of
whether they are actively interacting with it.
![](kpi-1.png)
### Immediate Questions
* Do people use the containers feature & how do people create new container tabs?
* Click to create new container tab
* \+ `entry-point` value: "tab-bar" or "pop-up"
* Do people who use the containers feature continue to use it?
* Retention: opening a second container tab (second tab in the same container, or a tab in a second container?)
* What containers do people use?
* userContextId
* \+ Number of tabs in the container (when should we measure this? on every tab open?)
* Do people edit their containers?
* Click on "Edit Containers"
* Click to edit a single container
* Click "OK"
* Click to delete a single container
* Click "OK"
* Click to add a container
* Click "OK"
* Do people sort the tabs?
* Click sort
* \+ Number of tabs when clicked
* Average number of container tabs when sort was clicked
* Do users show and hide container tabs?
* Click hide
* \+ Number of tabs when clicked
* \+ Number of hidden containers when clicked
* Click show
* \+ Number of tabs when clicked
* \+ Number of shown containers when clicked
* Do users move container tabs to new windows?
* Click move
* \+ Number of tabs when clicked
* Average number of container tabs when new window was clicked
* How many containers do users have hidden at the same time? (when should we measure this? each time a container is hidden?)
* Do users pin container tabs? (do we have existing Telemetry for pinning?)
* Do users visit more pages in container tabs than non-container tabs?
### Follow-up Questions
What are some follow-up questions we anticipate we will ask based on any of the
above answers/data?
* What is the average lifespan of a container tab? Is that longer or shorter than a regular tab? (if we don't have data on the latter, the former probably isn't worth gathering data on since we will have nothing to compare it to).
## Data Collection
### Server Side
There is currently no server side component to Containers.
### Client Side
Containers will use Test Pilot Telemetry with no batching of data. Details
of when pings are sent are below, along with examples of the `payload` portion
of a `testpilottest` telemetry ping for each scenario.
* The user clicks on a container name to open a tab in that container
```js
{
"uuid": <uuid>,
"userContextId": <userContextId>,
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
"event": "open-tab",
"eventSource": ["tab-bar"|"pop-up"|"file-menu"]
}
```
* The user clicks "Edit Containers" in the pop-up
```js
{
"uuid": <uuid>,
"event": "edit-containers"
}
```
* The user clicks OK after clicking on a container edit icon in the pop-up
```js
{
"uuid": <uuid>,
"userContextId": <userContextId>,
"event": "edit-container"
}
```
* The user clicks OK after clicking on a container delete icon in the pop-up
```js
{
"uuid": <uuid>,
"userContextId": <userContextId>,
"event": "delete-container"
}
```
* The user clicks OK after clicking to add a container in the pop-up
```js
{
"uuid": <uuid>,
"event": "add-container"
}
```
* The user clicks the sort button/icon in the pop-up
```js
{
"uuid": <uuid>,
"event": "sort-tabs",
"shownContainersCount": <number-of-containers-with-tabs-shown>,
"totalContainerTabsCount": <number-of-all-container-tabs>,
"totalNonContainerTabsCount": <number-of-all-non-container-tabs>
}
```
* The user clicks "Hide these container tabs" in the popup
```js
{
"uuid": <uuid>,
"userContextId": <userContextId>,
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
"event": "hide-tabs",
"hiddenContainersCount": <number-of-containers-with-tabs-hidden>,
"shownContainersCount": <number-of-containers-with-tabs-shown>,
"totalContainersCount": <number-of-containers-with-tabs-hidden-or-shown>
}
```
* The user clicks "Show these container tabs" in the popup
```js
{
"uuid": <uuid>,
"userContextId": <userContextId>,
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
"event": "show-tabs",
"hiddenContainersCount": <number-of-containers-with-tabs-hidden>,
"shownContainersCount": <number-of-containers-with-tabs-shown>,
"totalContainersCount": <number-of-containers-with-tabs-hidden-or-shown>
}
```
* The user clicks "Move tabs to a new window" in the popup
```js
{
"uuid": <uuid>,
"userContextId": <userContextId>,
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
"event": "move-tabs-to-window"
}
```
* When a user encounters the disabled "move" feature because of incompatible add-ons
```js
{
"uuid": <uuid>,
"event": "incompatible-addons-detected"
}
```
* The user closes a tab
```js
{
"uuid": <uuid>,
"userContextId": <userContextId>,
"event": "page-requests-completed-per-tab",
"pageRequestCount": <pageRequestCount>
}
```
* The user goes idle
```js
{
"uuid": <uuid>,
"userContextId": <userContextId>,
"event": "page-requests-completed-per-activity",
"pageRequestCount": <pageRequestCount>
}
```
* The user chooses "Always Open in this Container" context menu option. (Note: We send two separate event names: one for assigning a site to a container, one for removing a site from a container.)
```js
{
"uuid": <uuid>,
"userContextId": <userContextId>,
"event": "[added|removed]-container-assignment"
}
```
* Firefox prompts the user to reload a site into a container after the user picked "Always Open in this Container".
```js
{
"uuid": <uuid>,
"userContextId": <userContextId>,
"event": "prompt-reload-page-in-container"
}
```
* The user clicks "Take me there" to reload a site into a container after the user picked "Always Open in this Container".
```js
{
"uuid": <uuid>,
"event": "click-to-reload-page-in-container"
}
```
* Firefox automatically reloads a site into a container after the user picked "Always Open in this Container".
```js
{
"uuid": <uuid>,
"userContextId": <userContextId>,
"event": "auto-reload-page-in-container"
}
```
### A Redshift schema for the payload:
```lua
local schema = {
-- column name field type length attributes field name
{"uuid", "VARCHAR", 255, nil, "Fields[payload.uuid]"},
{"userContextId", "INTEGER", 255, nil, "Fields[payload.userContextId]"},
{"clickedContainerTabCount", "INTEGER", 255, nil, "Fields[payload.clickedContainerTabCount]"},
{"eventSource", "VARCHAR", 255, nil, "Fields[payload.eventSource]"},
{"event", "VARCHAR", 255, nil, "Fields[payload.event]"},
{"pageRequestCount", "INTEGER", 255, nil, "Fields[payload.pageRequestCount]"}
{"hiddenContainersCount", "INTEGER", 255, nil, "Fields[payload.hiddenContainersCount]"},
{"shownContainersCount", "INTEGER", 255, nil, "Fields[payload.shownContainersCount]"},
{"totalContainersCount", "INTEGER", 255, nil, "Fields[payload.totalContainersCount]"},
{"totalContainerTabsCount", "INTEGER", 255, nil, "Fields[payload.totalContainerTabsCount]"},
{"totalNonContainerTabsCount", "INTEGER", 255, nil, "Fields[payload.totalNonContainerTabsCount]"}
}
```
### Valid data should be enforced on the server side:
* `eventSource` should be one of `tab-bar`, `pop-up`, or `file-menu`.
All Mozilla data is kept by default for 180 days and in accordance with our
privacy policies.
-1600
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>@testpilot-containers</em:id>
<em:type>2</em:type>
<em:bootstrap>true</em:bootstrap>
<em:multiprocessCompatible>true</em:multiprocessCompatible>
<em:hasEmbeddedWebExtension>true</em:hasEmbeddedWebExtension>
<em:name>Multi-Account Containers</em:name>
<em:description>Containers helps you keep all the parts of your online life contained in different tabs. Custom labels and color-coded tabs help keep different activities — like online shopping, travel planning, or checking work email — separate.</em:description>
<em:targetApplication>
<Description>
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!--Firefox-->
<em:minVersion>53.0</em:minVersion>
<em:maxVersion>*</em:maxVersion>
</Description>
</em:targetApplication>
<em:version>4.1.0</em:version>
<em:unpack>false</em:unpack>
</Description>
</RDF>
+7 -18
View File
@@ -1,8 +1,8 @@
{
"name": "testpilot-containers",
"title": "Containers Experiment",
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored Container Tabs. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
"version": "2.2.0",
"title": "Multi-Account Containers",
"description": "Containers helps you keep all the parts of your online life contained in different tabs. Custom labels and color-coded tabs help keep different activities — like online shopping, travel planning, or checking work email — separate.",
"version": "4.1.0",
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
"bugs": {
"url": "https://github.com/mozilla/testpilot-containers/issues"
@@ -12,27 +12,17 @@
"addons-linter": "^0.15.14",
"deploy-txp": "^1.0.7",
"eslint": "^3.17.1",
"eslint-plugin-no-unescaped": "^1.1.0",
"eslint-plugin-no-unsanitized": "^2.0.0",
"eslint-plugin-promise": "^3.4.0",
"htmllint-cli": "^0.0.5",
"jpm": "^1.2.2",
"json": "^9.0.6",
"npm-run-all": "^4.0.0",
"stylelint": "^7.9.0",
"stylelint-config-standard": "^16.0.0",
"stylelint-order": "^0.3.0",
"testpilot-metrics": "^2.1.0"
"stylelint-order": "^0.3.0"
},
"engines": {
"firefox": ">=51.0"
},
"permissions": {
"multiprocess": true
},
"hasEmbeddedWebExtension": true,
"homepage": "https://github.com/mozilla/testpilot-containers#readme",
"keywords": [
"jetpack"
],
"license": "MPL-2.0",
"main": "index.js",
"repository": {
@@ -49,6 +39,5 @@
"lint:js": "eslint .",
"package": "npm run build && mv testpilot-containers.xpi addon.xpi",
"test": "npm run lint"
},
"updateURL": "https://testpilot.firefox.com/files/@testpilot-containers/updates.json"
}
}
-333
View File
@@ -1,333 +0,0 @@
// 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/.
/**
* Class that represents a metrics event broker. Events are sent to Google
* Analytics if the `tid` parameter is set. Events are sent to Mozilla's
* data pipeline via the Test Pilot add-on. No metrics code changes are
* needed when the experiment is added to or removed from Test Pilot.
* @constructor
* @param {string} $0.id - addon ID, e.g. '@testpilot-addon'. See https://mdn.io/add_on_id.
* @param {string} $0.version - addon version, e.g. '1.0.2'.
* @param {string} [$0.uid] - unique identifier for a specific instance of an addon.
* Optional, but required to send events to Google Analytics. Sent to Google Analytics
* but not Mozilla services.
* @param {string} [$0.tid] - Google Analytics tracking ID. Optional, but required
* to send events to Google Analytics.
* @param {string} [$0.type=webextension] - addon type. one of: 'webextension',
* 'sdk', 'bootstrapped'.
* @param {boolean} [$0.debug=false] - if true, enables logging. Note that this
* value can be changed on a running instance, by modifying its `debug` property.
* @throws {SyntaxError} If the required properties are missing, or if the
* 'type' property is unrecognized.
* @throws {Error} if initializing the transports fails.
*/
function Metrics({id, version, uid, tid = null, type = 'webextension', debug = false}) {
if (!id) {
throw new SyntaxError(`'id' property is required.`);
} else if (!version) {
throw new SyntaxError(`'version' property is required.`);
} else if (tid && !uid) {
throw new SyntaxError(`'uid' property is required to send events to Google Analytics.`);
}
if (!['webextension', 'sdk', 'bootstrapped'].includes(type)) {
throw new SyntaxError(`'type' property must be one of: 'webextension', 'sdk', or 'bootstrapped'`);
}
Object.assign(this, {id, uid, version, tid, type, debug});
// The test pilot add-on uses its own nsIObserverService topic for sending
// pings to Telemetry. Otherwise, the topic is based on add-on type.
if (id === '@testpilot-addon') {
this.topic = 'testpilot';
} else if (type === 'webextension') {
this.topic = 'testpilot-telemetry';
} else {
this.topic = 'testpilottest';
}
// NOTE: order is important here. _initTransports uses console.log, which may
// not be available before _initConsole has run.
this._initConsole();
this._initTransports();
this.sendEvent = this.sendEvent.bind(this);
this._log(`Initialized topic to ${this.topic}`);
if (!tid) {
this._log(`Google Analytics disabled: 'tid' value not passed to constructor.`);
} else {
this._log(`Google Analytics enabled for Tracking ID ${tid}.`);
}
this._log('Constructor finished successfully.');
}
Metrics.prototype = {
/**
* Sends an event to the Mozilla data pipeline (and Google Analytics, if
* a `tid` was passed to the constructor). Note: to avoid breaking callers,
* if sending the event fails, no Errors will be thrown. Instead, the message
* will be silently dropped, and, if debug mode is enabled, an error will be
* logged to the Browser Console.
*
* If you want to pass extra fields to GA, or use a GA hit type other than
* `Event`, you can transform the output data object yourself using the
* `transform` parameter. You will need to add Custom Dimensions to GA for any
* extra fields: https://support.google.com/analytics/answer/2709828. Note
* that, by convention, the `variant` argument is mapped to the first Custom
* Dimension (`cd1`) when constructing the GA Event hit.
*
* Note: the data object format is currently different for each experiment,
* and should be defined based on the result of conversations with the Mozilla
* data team.
*
* A suggested default format is:
* @param {string} [$0.method] - What is happening? e.g. `click`
* @param {string} [$0.object] - What is being affected? e.g. `home-button-1`
* @param {string} [$0.category=interactions] - If you want to add a category
* for easy reporting later. e.g. `mainmenu`
* @param {string} [$0.variant=null] - An identifying string if you're running
* different variants. e.g. `cohort-A`
* @param {function} [transform] - Transform function used to alter the
* parameters sent to GA. The `transform` function signature is
* `transform(input, output)`, where `input` is the object passed to
* `sendEvent` (excluding `transform`), and `output` is the default GA
* object generated by the `_gaTransform` method. The `transform` function
* should return an object whose keys are GA Measurement Protocol parameters.
* The returned object will be form encoded and sent to GA.
*/
sendEvent: function(params = {}, transform) {
const args = this._clone(params);
args.object = params.object || null;
args.category = params.category || 'interactions';
args.variant = params.variant || null;
this._log(`sendEvent called with method = ${args.method}, object = ${args.object}, category = ${args.category}, variant = ${args.variant}.`);
const clientData = this._clone(args);
const gaData = this._clone(args);
if (!clientData) {
this._error(`Unable to process data object. Dropping packet.`);
return;
}
this._sendToClient(clientData);
if (this.tid && this.uid) {
const defaultEvent = this._gaTransform(gaData);
let userEvent;
if (transform) {
userEvent = transform.call(null, gaData, defaultEvent);
}
this._gaSend(userEvent || defaultEvent);
}
},
/**
* Clone a data object by serializing / deserializing it.
* @private
* @param {object} o - Object to be cloned.
* @returns A clone of the object, or `null` if cloning failed.
*/
_clone: function(o) {
let cloned;
try {
cloned = JSON.parse(JSON.stringify(o));
} catch (ex) {
this._error(`Unable to clone object: ${ex}.`);
return null;
}
return cloned;
},
/**
* Sends an event to the Mozilla data pipeline via the Test Pilot add-on.
* Uses BroadcastChannel for WebExtensions, and nsIObserverService for other
* add-on types.
* @private
* @param {object} params - Entire object sent to `sendEvent`.
*/
_sendToClient: function(params) {
if (this.type === 'webextension') {
this._channel.postMessage(params);
this._log(`Sent client message via postMessage: ${params}`);
} else {
let stringified;
try {
stringified = JSON.stringify(params);
} catch(ex) {
this._error(`Unable to serialize metrics event: ${ex}`);
return;
}
const subject = {
wrappedJSObject: {
observersModuleSubjectWrapper: true,
object: this.id
}
};
try {
Services.obs.notifyObservers(subject, 'testpilot::send-metric', stringified);
this._log(`Sent client message via nsIObserverService: ${stringified}`);
} catch (ex) {
this._error(`Failed to send nsIObserver client ping: ${ex}`);
return;
}
}
},
/**
* Transforms `sendEvent()` arguments into a Google Analytics `Event` hit.
* @private
* @param {string} method - see `sendEvent` docs
* @param {string} [object] - see `sendEvent` docs
* @param {string} category - see `sendEvent` docs. Note that `category` is
* required here, assuming the default value was filled in by `sendEvent()`.
* @param {string} variant - see `sendEvent` docs. Note that `variant` is
* required here, assuming the default value was filled in by `sendEvent()`.
*/
_gaTransform: function({method, object, category, variant}) {
const data = {
v: 1,
an: this.id,
av: this.version,
tid: this.tid,
uid: this.uid,
t: 'event',
ec: category,
ea: method
};
if (object) {
data.el = object;
}
if (variant) {
data.cd1 = variant;
}
return data;
},
/**
* Encodes and sends an event message to Google Analytics.
* @private
* @param {object} msg - An object whose keys correspond to parameters in the
* Google Analytics Measurement Protocol.
*/
_gaSend: function(msg) {
const encoded = this._formEncode(msg);
const GA_URL = 'https://ssl.google-analytics.com/collect';
if (this.type === 'webextension') {
navigator.sendBeacon(GA_URL, encoded);
} else {
// SDK and bootstrapped types might not have a window reference, so get
// the sendBeacon DOM API from the hidden window.
Services.appShell.hiddenDOMWindow.navigator.sendBeacon(GA_URL, encoded);
}
this._log(`Sent GA message: ${encoded}`);
},
/**
* URL encodes an object. Encodes spaces as '%20', not '+', following the
* GA docs.
*
* @example
* // returns 'a=b&foo=b%20ar'
* metrics._formEncode({a: 'b', foo: 'b ar'});
* @private
* @param {Object} obj - Any JS object
* @returns {string}
*/
_formEncode: function(obj) {
const params = [];
if (!obj) { return ''; }
Object.keys(obj).forEach(item => {
const encoded = encodeURIComponent(item) + '=' + encodeURIComponent(obj[item]);
params.push(encoded);
});
return params.join('&');
},
/**
* Initializes transports used for sending messages. For WebExtensions,
* creates a `BroadcastChannel` (transport for client pings). WebExtensions
* use navigator.sendBeacon for GA transport, and they always have access
* to DOM APIs, so there's no setup work required. For other types, loads
* `Services.jsm`, which exposes the nsIObserverService (transport for client
* pings), and exposes the navigator.sendBeacon API (GA transport) via the
* appShell service's hidden window.
* @private
* @throws {Error} if transport setup unexpectedly fails
*/
_initTransports: function() {
if (this.type === 'webextension') {
try {
this._channel = new BroadcastChannel(this.topic);
} catch(ex) {
throw new Error(`Unable to create BroadcastChannel: ${ex}`);
}
} else if (this.type === 'sdk') {
try {
const { Cu } = require('chrome');
Cu.import('resource://gre/modules/Services.jsm');
} catch(ex) {
throw new Error(`Unable to load Services.jsm: ${ex}`);
}
} else { /* this.type === 'bootstrapped' */
try {
Components.utils.import('resource://gre/modules/Services.jsm');
} catch(ex) {
throw new Error(`Unable to load Services.jsm: ${ex}`);
}
}
this._log('Successfully initialized transports.');
},
/**
* Initializes a console for 'bootstrapped' add-ons.
* @private
*/
_initConsole: function() {
if (this.type === 'bootstrapped') {
try {
Components.utils.import('resource://gre/modules/Console.jsm');
this._log('Successfully initialized console.');
} catch(ex) {
throw new Error(`Unable to initialize console: ${ex}`);
}
}
},
/**
* Logs messages to the console. Only enabled if `this.debug` is truthy.
* @private
* @param {string} msg - A message
*/
_log: function(msg) {
if (this.debug) {
console.log(msg); // eslint-disable-line no-console
}
},
/**
* Logs errors to the console. Only enabled if `this.debug` is truthy.
* @private
* @param {string} msg - An error message
*/
_error: function(msg) {
if (this.debug) {
console.error(msg); // eslint-disable-line no-console
}
}
};
// WebExtensions don't support CommonJS module style, so 'module' might not be
// defined.
if (typeof module !== 'undefined') {
module.exports = Metrics;
}
// Export the Metrics constructor in Gecko JSM style, for legacy addons
// that use the JSM loader. See also: https://mdn.io/jsm/using
const EXPORTED_SYMBOLS = ['Metrics']; // eslint-disable-line no-unused-vars
-473
View File
@@ -1,473 +0,0 @@
const assignManager = {
CLOSEABLE_WINDOWS: new Set([
"about:startpage",
"about:newtab",
"about:home",
"about:blank"
]),
MENU_ASSIGN_ID: "open-in-this-container",
MENU_REMOVE_ID: "remove-open-in-this-container",
storageArea: {
area: browser.storage.local,
getSiteStoreKey(pageUrl) {
const url = new window.URL(pageUrl);
const storagePrefix = "siteContainerMap@@_";
return `${storagePrefix}${url.hostname}`;
},
get(pageUrl) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
return new Promise((resolve, reject) => {
this.area.get([siteStoreKey]).then((storageResponse) => {
if (storageResponse && siteStoreKey in storageResponse) {
resolve(storageResponse[siteStoreKey]);
}
resolve(null);
}).catch((e) => {
reject(e);
});
});
},
set(pageUrl, data) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
return this.area.set({
[siteStoreKey]: data
});
},
remove(pageUrl) {
const siteStoreKey = this.getSiteStoreKey(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;
});
}
},
init() {
browser.runtime.onMessage.addListener((neverAskMessage) => {
const pageUrl = neverAskMessage.pageUrl;
if (neverAskMessage.neverAsk === true) {
// If we have existing data and for some reason it hasn't been deleted etc lets update it
this.storageArea.get(pageUrl).then((siteSettings) => {
if (siteSettings) {
siteSettings.neverAsk = true;
this.storageArea.set(pageUrl, siteSettings);
}
}).catch((e) => {
throw e;
});
}
});
browser.contextMenus.onClicked.addListener((info, tab) => {
const userContextId = this.getUserContextIdFromCookieStore(tab);
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
if (userContextId) {
let actionName;
let storageAction;
if (info.menuItemId === this.MENU_ASSIGN_ID) {
actionName = "added";
storageAction = this.storageArea.set(info.pageUrl, {
userContextId,
neverAsk: false
});
} else {
actionName = "removed";
storageAction = this.storageArea.remove(info.pageUrl);
}
storageAction.then(() => {
browser.notifications.create({
type: "basic",
title: "Containers",
message: `Successfully ${actionName} site to always open in this container`,
iconUrl: browser.extension.getURL("/img/onboarding-1.png")
});
browser.runtime.sendMessage({
method: "sendTelemetryPayload",
event: `${actionName}-container-assignment`,
userContextId: userContextId,
});
this.calculateContextMenu(tab);
}).catch((e) => {
throw e;
});
}
});
// Before a request is handled by the browser we decide if we should route through a different container
browser.webRequest.onBeforeRequest.addListener((options) => {
if (options.frameId !== 0 || options.tabId === -1) {
return {};
}
return Promise.all([
browser.tabs.get(options.tabId),
this.storageArea.get(options.url)
]).then(([tab, siteSettings]) => {
const userContextId = this.getUserContextIdFromCookieStore(tab);
if (!siteSettings
|| userContextId === siteSettings.userContextId
|| tab.incognito) {
return {};
}
this.reloadPageInContainer(options.url, siteSettings.userContextId, tab.index + 1, siteSettings.neverAsk);
this.calculateContextMenu(tab);
/* Removal of existing tabs:
We aim to open the new assigned container tab / warning prompt in it's own tab:
- As the history won't span from one container to another it seems most sane to not try and reopen a tab on history.back()
- When users open a new tab themselves we want to make sure we don't end up with three tabs as per: https://github.com/mozilla/testpilot-containers/issues/421
If we are coming from an internal url that are used for the new tab page (CLOSEABLE_WINDOWS), we can safely close as user is unlikely losing history
Detecting redirects on "new tab" opening actions is pretty hard as we don't get tab history:
- Redirects happen from Short URLs and tracking links that act as a gateway
- Extensions don't provide a way to history crawl for tabs, we could inject content scripts to do this
however they don't run on about:blank so this would likely be just as hacky.
We capture the time the tab was created and close if it was within the timeout to try to capture pages which haven't had user interaction or history.
*/
if (this.CLOSEABLE_WINDOWS.has(tab.url)
|| (messageHandler.lastCreatedTab
&& messageHandler.lastCreatedTab.id === tab.id)) {
browser.tabs.remove(tab.id);
}
return {
cancel: true,
};
}).catch((e) => {
throw e;
});
},{urls: ["<all_urls>"], types: ["main_frame"]}, ["blocking"]);
},
deleteContainer(userContextId) {
this.storageArea.deleteContainer(userContextId);
},
getUserContextIdFromCookieStore(tab) {
if (!("cookieStoreId" in tab)) {
return false;
}
const cookieStore = tab.cookieStoreId;
const container = cookieStore.replace("firefox-container-", "");
if (container !== cookieStore) {
return container;
}
return false;
},
isTabPermittedAssign(tab) {
// Ensure we are not an important about url
// Ensure we are not in incognito mode
const url = new URL(tab.url);
if (url.protocol === "about:"
|| tab.incognito) {
return false;
}
return true;
},
calculateContextMenu(tab) {
// 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) {
const loadPage = browser.extension.getURL("confirm-page.html");
// If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there
if (neverAsk) {
browser.tabs.create({url, cookieStoreId: `firefox-container-${userContextId}`, index});
browser.runtime.sendMessage({
method: "sendTelemetryPayload",
event: "auto-reload-page-in-container",
userContextId: userContextId,
});
} else {
browser.runtime.sendMessage({
method: "sendTelemetryPayload",
event: "prompt-to-reload-page-in-container",
userContextId: userContextId,
});
const confirmUrl = `${loadPage}?url=${url}`;
browser.tabs.create({url: confirmUrl, cookieStoreId: `firefox-container-${userContextId}`, index}).then(() => {
// We don't want to sync this URL ever nor clutter the users history
browser.history.deleteUrl({url: confirmUrl});
}).catch((e) => {
throw e;
});
}
}
};
const messageHandler = {
// After the timer completes we assume it's a tab the user meant to keep open
// We use this to catch redirected tabs that have just opened
// If this were in platform we would change how the tab opens based on "new tab" link navigations such as ctrl+click
LAST_CREATED_TAB_TIMER: 2000,
init() {
// Handles messages from index.js
const port = browser.runtime.connect();
port.onMessage.addListener(m => {
switch (m.type) {
case "lightweight-theme-changed":
themeManager.update(m.message);
break;
case "delete-container":
assignManager.deleteContainer(m.message.userContextId);
break;
default:
throw new Error(`Unhandled message type: ${m.message}`);
}
});
browser.tabs.onCreated.addListener((tab) => {
// This works at capturing the tabs as they are created
// However we need onFocusChanged and onActivated to capture the initial tab
if (tab.id === -1) {
return {};
}
tabPageCounter.initTabCounter(tab);
});
browser.tabs.onRemoved.addListener((tabId) => {
if (tabId === -1) {
return {};
}
tabPageCounter.sendTabCountAndDelete(tabId);
});
browser.tabs.onActivated.addListener((info) => {
browser.tabs.get(info.tabId).then((tab) => {
tabPageCounter.initTabCounter(tab);
assignManager.calculateContextMenu(tab);
}).catch((e) => {
throw e;
});
});
browser.windows.onFocusChanged.addListener((windowId) => {
browser.tabs.query({active: true, windowId}).then((tabs) => {
if (tabs && tabs[0]) {
tabPageCounter.initTabCounter(tabs[0]);
assignManager.calculateContextMenu(tabs[0]);
}
}).catch((e) => {
throw e;
});
});
browser.idle.onStateChanged.addListener((newState) => {
browser.tabs.query({}).then(tabs => {
for (let tab of tabs) { // eslint-disable-line prefer-const
if (newState === "idle") {
tabPageCounter.sendTabCountAndDelete(tab.id, "user-went-idle");
} else if (newState === "active" && tab.active) {
tabPageCounter.initTabCounter(tab);
}
}
}).catch(e => {
throw e;
});
});
browser.webRequest.onCompleted.addListener((details) => {
if (details.frameId !== 0 || details.tabId === -1) {
return {};
}
browser.tabs.get(details.tabId).then((tab) => {
tabPageCounter.incrementTabCount(tab);
assignManager.calculateContextMenu(tab);
}).catch((e) => {
throw e;
});
}, {urls: ["<all_urls>"], types: ["main_frame"]});
// lets remember the last tab created so we can close it if it looks like a redirect
browser.tabs.onCreated.addListener((details) => {
this.lastCreatedTab = details;
setTimeout(() => {
this.lastCreatedTab = null;
}, this.LAST_CREATED_TAB_TIMER);
});
}
};
const themeManager = {
existingTheme: null,
init() {
this.check();
},
setPopupIcon(theme) {
let icons = {
16: "img/container-site-d-24.png",
32: "img/container-site-d-48.png"
};
if (theme === "firefox-compact-dark@mozilla.org") {
icons = {
16: "img/container-site-w-24.png",
32: "img/container-site-w-48.png"
};
}
browser.browserAction.setIcon({
path: icons
});
},
check() {
browser.runtime.sendMessage({
method: "getTheme"
}).then((theme) => {
this.update(theme);
}).catch(() => {
throw new Error("Unable to get theme");
});
},
update(theme) {
if (this.existingTheme !== theme) {
this.setPopupIcon(theme);
this.existingTheme = theme;
}
}
};
const tabPageCounter = {
counters: {},
initTabCounter(tab) {
if (tab.id in this.counters) {
if (!("activity" in this.counters[tab.id])) {
this.counters[tab.id].activity = {
"cookieStoreId": tab.cookieStoreId,
"pageRequests": 0
};
}
if (!("tab" in this.counters[tab.id])) {
this.counters[tab.id].tab = {
"cookieStoreId": tab.cookieStoreId,
"pageRequests": 0
};
}
} else {
this.counters[tab.id] = {};
this.counters[tab.id].tab = {
"cookieStoreId": tab.cookieStoreId,
"pageRequests": 0
};
this.counters[tab.id].activity = {
"cookieStoreId": tab.cookieStoreId,
"pageRequests": 0
};
}
},
sendTabCountAndDelete(tabId, why = "user-closed-tab") {
if (!(this.counters[tabId])) {
return;
}
if (why === "user-closed-tab" && this.counters[tabId].tab) {
browser.runtime.sendMessage({
method: "sendTelemetryPayload",
event: "page-requests-completed-per-tab",
userContextId: this.counters[tabId].tab.cookieStoreId,
pageRequestCount: this.counters[tabId].tab.pageRequests
});
// When we send the ping because the user closed the tab,
// delete both the 'tab' and 'activity' counters
delete this.counters[tabId];
} else if (why === "user-went-idle" && this.counters[tabId].activity) {
browser.runtime.sendMessage({
method: "sendTelemetryPayload",
event: "page-requests-completed-per-activity",
userContextId: this.counters[tabId].activity.cookieStoreId,
pageRequestCount: this.counters[tabId].activity.pageRequests
});
// When we send the ping because the user went idle,
// only reset the 'activity' counter
this.counters[tabId].activity = {
"cookieStoreId": this.counters[tabId].tab.cookieStoreId,
"pageRequests": 0
};
}
},
incrementTabCount(tab) {
this.counters[tab.id].tab.pageRequests++;
this.counters[tab.id].activity.pageRequests++;
}
};
assignManager.init();
themeManager.init();
// Lets do this last as theme manager did a check before connecting before
messageHandler.init();
browser.runtime.sendMessage({
method: "getPreference",
pref: "browser.privatebrowsing.autostart"
}).then(pbAutoStart => {
// We don't want to disable the addon if we are in auto private-browsing.
if (!pbAutoStart) {
browser.tabs.onCreated.addListener(tab => {
if (tab.incognito) {
disableAddon(tab.id);
}
});
browser.tabs.query({}).then(tabs => {
for (let tab of tabs) { // eslint-disable-line prefer-const
if (tab.incognito) {
disableAddon(tab.id);
}
}
}).catch(() => {});
}
}).catch(() => {});
function disableAddon(tabId) {
browser.browserAction.disable(tabId);
browser.browserAction.setTitle({ tabId, title: "Containers disabled in Private Browsing Mode" });
}
+11 -8
View File
@@ -1,33 +1,36 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Containers confirm navigation</title>
<title>Multi-Account Containers Confirm Navigation</title>
<link xmlns="http://www.w3.org/1999/xhtml" rel="stylesheet" href="chrome://browser/skin/aboutNetError.css" type="text/css" media="all" />
<link rel="stylesheet" href="/css/confirm-page.css" />
</head>
<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;
}
+416 -69
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,20 +197,43 @@ table {
--identity-icon: url("/img/usercontext.svg#chill");
}
#current-tab [data-identity-icon="default-tab"] {
background: center center no-repeat url("/img/blank-tab.svg");
fill: currentColor;
}
/* Buttons */
.button {
color: black;
}
.button.primary {
background-color: #0996f8;
color: white;
}
.button.primary:hover {
.button.primary:hover,
.button.primary:focus {
background-color: #0675d3;
}
.button.secondary:hover {
.button.secondary:hover,
.button.secondary:focus {
background-color: rgba(0, 0, 0, 0.05);
}
/* Text links with actions */
.action-link:link {
color: var(--primary-action-color);
text-decoration: none;
}
.action-link:active,
.action-link:hover {
text-decoration: underline;
}
/* Panels keep everything togethert */
.panel {
display: flex;
@@ -146,7 +242,8 @@ table {
min-block-size: 400px;
}
.panel.onboarding {
.panel.onboarding,
.achievement-panel {
align-items: center;
block-size: 360px;
margin-block-end: 16px;
@@ -181,7 +278,7 @@ table {
.column-panel-content .button,
.panel-footer .button {
align-items: center;
block-size: 54px;
block-size: 100%;
display: flex;
flex: 1;
justify-content: center;
@@ -198,7 +295,8 @@ table {
justify-content: center;
}
.panel-back-arrow:hover {
.panel-back-arrow:hover,
.panel-back-arrow:focus {
background: #dedede;
}
@@ -220,7 +318,7 @@ table {
.onboarding-title {
color: #43484e;
font-size: 16px;
font-size: var(--font-size-heading);
margin-block-end: 0;
margin-block-start: 0;
margin-inline-end: 0;
@@ -229,7 +327,7 @@ table {
}
.onboarding p {
color: #4a4a4a;
color: var(--text-normal-color);
font-size: 14px;
margin-block-end: 16px;
max-inline-size: 84%;
@@ -249,7 +347,8 @@ table {
transition: background-color 75ms;
}
.onboarding-button:hover {
.onboarding-button:hover,
.onboarding-button:active {
background-color: #0675d3;
}
@@ -257,25 +356,38 @@ table {
manage things like container crud */
.pop-button {
align-items: center;
block-size: 48px;
block-size: var(--icon-button-size);
cursor: pointer;
display: flex;
flex: 0 0 48px;
flex: 0 0 var(--icon-button-size);
justify-content: center;
}
.pop-button:hover,
.pop-button:focus,
.panel-footer-secondary:focus,
.panel-footer-secondary:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.pop-button:active,
.panel-footer-secondary:active {
.pop-button:focus,
.panel-footer-secondary:focus {
background-color: rgba(0, 0, 0, 0.08);
}
.pop-button a,
.panel-footer a,
.panel-footer-secondary a {
text-decoration: none;
}
.pop-button-image {
block-size: 20px;
flex: 0 0 20px;
margin-block-end: auto;
margin-block-start: auto;
margin-inline-end: auto;
margin-inline-start: auto;
}
.pop-button-image-small {
@@ -287,20 +399,23 @@ manage things like container crud */
.panel-header {
align-items: center;
block-size: 48px;
border-block-end: 1px solid #ebebeb;
display: flex;
justify-content: space-between;
}
.panel-header .usercontext-icon {
inline-size: var(--icon-button-size);
}
.column-panel-content .panel-header {
flex: 0 0 48px;
inline-size: 100%;
}
.panel-header-text {
color: #4a4a4a;
color: var(--text-normal-color);
flex: 1;
font-size: 16px;
font-size: var(--font-size-heading);
font-weight: normal;
margin-block-end: 0;
margin-block-start: 0;
@@ -312,6 +427,47 @@ manage things like container crud */
padding-inline-start: 16px;
}
#container-panel .panel-header {
background-color: #efefef;
block-size: 26px;
font-size: 14px;
}
#container-panel .panel-header-text {
color: #727272;
font-size: 14px;
padding-block-end: 0;
padding-block-start: 0;
text-transform: uppercase;
}
.container-panel-controls {
display: flex;
justify-content: flex-end;
margin-block-end: var(--block-line-space-size);
margin-block-start: var(--block-line-space-size);
margin-inline-end: var(--inline-item-space-size);
margin-inline-start: var(--inline-item-space-size);
}
#container-panel #sort-containers-link {
align-items: center;
block-size: var(--block-url-label-size);
border: 1px solid #d8d8d8;
border-radius: var(--small-radius);
color: var(--title-text-color);
display: flex;
font-size: var(--small-text-size);
inline-size: var(--inline-button-size);
justify-content: center;
text-decoration: none;
}
#container-panel #sort-containers-link:hover,
#container-panel #sort-containers-link:focus {
background: #f2f2f2;
}
span ~ .panel-header-text {
padding-block-end: 0;
padding-block-start: 0;
@@ -319,11 +475,92 @@ span ~ .panel-header-text {
padding-inline-start: 0;
}
#current-tab {
align-items: center;
color: var(--text-normal-color);
display: grid;
font-size: var(--small-text-size);
grid-column-gap: var(--inline-item-space-size);
grid-row-gap: var(--block-line-space-size);
grid-template-columns: var(--icon-size) var(--icon-size) 1fr;
margin-block-end: var(--block-line-space-size);
margin-block-start: var(--block-line-separation-size);
margin-inline-end: var(--inline-start-size);
margin-inline-start: var(--inline-start-size);
max-inline-size: 100%;
}
#current-tab img {
max-block-size: var(--icon-size);
}
#current-tab > h3 {
color: var(--text-heading-color);
font-weight: normal;
grid-column: span 3;
margin-block-end: 0;
margin-block-start: 0;
margin-inline-end: 0;
margin-inline-start: 0;
}
#current-page {
display: contents;
}
#current-tab .page-title {
font-size: var(--font-size-heading);
grid-column: 2 / 4;
}
#current-tab > label {
display: contents;
font-size: var(--small-text-size);
}
#current-tab > label > input {
-moz-appearance: none;
block-size: var(--icon-size);
border: 1px solid #d8d8d8;
border-radius: var(--small-radius);
display: block;
grid-column-start: 2;
inline-size: var(--icon-size);
margin-block-end: 0;
margin-block-start: 0;
margin-inline-end: 0;
margin-inline-start: 0;
}
#current-tab > label > input[disabled] {
background-color: #efefef;
}
#current-tab > label > input:checked {
background-image: url("chrome://global/skin/in-content/check.svg#check-native");
background-position: -1px -1px;
background-size: var(--icon-size);
}
#current-container {
color: var(--identity-tab-color);
flex: 1;
}
#current-tab > label > .usercontext-icon {
background-size: 16px;
block-size: 16px;
display: block;
flex: 0 0 20px;
inline-size: 20px;
margin-inline-end: 3px;
margin-inline-start: 3px;
}
/* Rows used when iterating over panels */
.container-panel-row {
align-items: center;
background-color: #fefefe !important;
block-size: 48px;
border-block-end: 1px solid #f1f1f1;
box-sizing: border-box;
display: flex;
@@ -331,12 +568,10 @@ span ~ .panel-header-text {
}
.container-panel-row .container-name {
flex: 1;
max-inline-size: 160px;
overflow: hidden;
padding-inline-end: 4px;
padding-inline-start: 4px;
text-overflow: ellipsis;
white-space: nowrap;
}
.edit-containers-panel .userContext-wrapper {
@@ -350,40 +585,46 @@ span ~ .panel-header-text {
transition: background-color 75ms;
}
.clickable.userContext-wrapper:hover {
.container-panel-row:hover .clickable.userContext-wrapper,
.container-panel-row:focus .clickable.userContext-wrapper {
background: #f2f2f2;
}
.userContext-icon-wrapper {
block-size: 48px;
flex: 0 0 48px;
block-size: var(--icon-button-size);
flex: 0 0 var(--icon-button-size);
margin-inline-start: var(--inline-icon-space-size);
}
/* .userContext-icon is used natively, Bug 1333811 was raised to fix */
.userContext-icon,
.usercontext-icon {
background-image: var(--identity-icon);
background-position: center center;
background-repeat: no-repeat;
background-size: 20px 20px;
block-size: 48px;
block-size: 100%;
fill: var(--identity-icon-color);
filter: url('/img/filters.svg#fill');
flex: 0 0 48px;
}
.clickable:hover .userContext-icon,
.clickable:hover .usercontext-icon {
.container-panel-row:hover .clickable .usercontext-icon,
.container-panel-row:focus .clickable .usercontext-icon,
.container-panel-row .clickable:focus .usercontext-icon {
background-image: url('/img/container-newtab.svg');
fill: 'gray';
fill: #979797;
filter: url('/img/filters.svg#fill');
}
.container-panel-row .clickable:hover .usercontext-icon,
.container-panel-row .clickable:focus .usercontext-icon {
fill: #0094fb;
}
/* Panel Footer */
.panel-footer {
align-items: center;
background: #efefef;
block-size: 54px;
block-size: var(--icon-button-size);
border-block-end: 1px solid #d8d8d8;
color: #000;
display: flex;
@@ -392,14 +633,9 @@ span ~ .panel-header-text {
justify-content: space-between;
}
.panel-footer .pop-button {
block-size: 54px;
flex: 0 0 54px;
}
.edit-containers-text {
align-items: center;
block-size: 54px;
block-size: 100%;
border-inline-end: solid 1px #d8d8d8;
display: flex;
flex: 1;
@@ -408,7 +644,7 @@ span ~ .panel-header-text {
.edit-containers-text a {
align-items: center;
block-size: 54px;
block-size: 100%;
color: #0a0a0a;
display: flex;
flex: 1;
@@ -416,11 +652,8 @@ span ~ .panel-header-text {
}
/* Container info list */
#container-info-name {
margin-inline-end: 0.5rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.container-info-tab-title {
flex: 1;
}
#container-info-hideorshow {
@@ -465,30 +698,31 @@ span ~ .panel-header-text {
.container-info-tab-row td {
max-inline-size: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.container-info-list {
border-block-start: 1px solid #ebebeb;
display: flex;
flex-direction: column;
margin-block-start: 4px;
padding-block-start: 4px;
}
.container-info-list tbody {
display: contents;
}
.clickable {
cursor: pointer;
}
.clickable:hover {
.clickable:hover,
.clickable:focus {
background-color: #ebebeb;
}
.edit-containers-exit-text {
align-items: center;
background: #248aeb;
background: var(--primary-action-color);
block-size: 100%;
color: #fff;
display: flex;
@@ -515,7 +749,7 @@ span ~ .panel-header-text {
.delete-container-confirm-title {
color: #000;
font-size: 16px;
font-size: var(--font-size-heading);
}
/* Form info */
@@ -527,36 +761,95 @@ span ~ .panel-header-text {
padding-inline-start: 16px;
}
.column-panel-content form span {
align-items: center;
block-size: 44px;
display: flex;
flex: 0 0 25%;
justify-content: center;
#edit-sites-assigned {
flex: 1;
}
.edit-container-panel label {
#edit-sites-assigned h3 {
font-size: 14px;
font-weight: normal;
padding-block-end: 6px;
padding-block-start: 6px;
padding-inline-end: 16px;
padding-inline-start: 16px;
}
.assigned-sites-list > div {
display: flex;
padding-block-end: 6px;
padding-block-start: 6px;
}
.assigned-sites-list > div > .icon {
margin-inline-end: 10px;
}
.assigned-sites-list > div > .delete-assignment {
display: none;
}
.assigned-sites-list > div:hover > .delete-assignment {
display: block;
}
.assigned-sites-list > div > .hostname {
flex: 1;
}
.radio-choice > .radio-container {
align-items: center;
block-size: 29px;
display: flex;
flex: 0 0 calc(100% / 8);
}
.radio-choice > .radio-container > label {
background: none;
block-size: 23px;
border: 0;
filter: none;
inline-size: 23px;
margin-block-end: 0;
margin-block-start: 0;
margin-inline-end: 0;
margin-inline-start: 0;
padding-block-end: 0;
padding-block-start: 0;
padding-inline-end: 0;
padding-inline-start: 0;
}
.radio-choice > .radio-container > label::before {
background-color: unset;
background-image: var(--identity-icon);
background-size: 26px 26px;
block-size: 34px;
background-position: center;
background-repeat: no-repeat;
background-size: 16px;
block-size: 23px;
border: none;
content: "";
display: block;
fill: var(--identity-icon-color);
filter: url('/img/filters.svg#fill');
flex: 0 0 34px;
inline-size: 23px;
position: relative;
}
.edit-container-panel label::before {
opacity: 0 !important;
}
.edit-container-panel [type="radio"] {
.radio-choice > .radio-container > [type="radio"] {
-moz-appearance: none;
display: inline;
opacity: 0;
}
.edit-container-panel [type="radio"]:checked + label {
outline: 2px solid grey;
-moz-outline-radius: 50px;
.radio-choice > .radio-container > [type="radio"]:checked + label {
background: #d3d3d3;
border-radius: 100%;
}
/* When focusing the element add a thin blue highlight to match input fields. This gives a distinction to other selected radio items */
.radio-choice > .radio-container > [type="radio"]:focus + label {
outline: 1px solid #1f9ffc;
-moz-outline-radius: 100%;
}
.edit-container-panel fieldset {
@@ -575,6 +868,10 @@ span ~ .panel-header-text {
padding-inline-start: 0;
}
.edit-container-panel fieldset:last-of-type {
margin-block-end: 0;
}
.edit-container-panel input[type="text"] {
block-size: 36px;
border-radius: 3px;
@@ -589,5 +886,55 @@ span ~ .panel-header-text {
.edit-container-panel legend {
flex: 1 0;
font-size: 14px !important;
padding-block-end: 5px;
padding-block-end: 6px;
}
/* Achievement panel elements */
.share-ctas {
padding-block-end: 0.5em;
padding-block-start: 0.5em;
padding-inline-end: 0.5em;
padding-inline-start: 0.5em;
text-align: center;
}
.cta-link {
text-decoration: none;
}
.cta {
color: #fff;
font-size: 0.7em;
font-weight: bold;
margin-block-end: 0.4em;
margin-block-start: 0.4em;
margin-inline-end: 0.4em;
margin-inline-start: 0.4em;
padding-block-end: 0.5em;
padding-block-start: 0.5em;
padding-inline-end: 0.5em;
padding-inline-start: 0.5em;
text-transform: uppercase;
}
.cta-icon {
height: 18px;
padding-right: 0.5em;
vertical-align: middle;
}
.fb-share-cta {
background: #375496;
}
.fb-share-cta .cta-icon {
margin-block-start: -5px;
}
.tweet-cta {
background: #37bae7;
}
.amo-rate-cta {
background: #0f1126;
}
+1
View File
@@ -0,0 +1 @@
<svg width="32px" height="33px" viewBox="0 0 32 33" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch --> <desc>Created with Sketch.</desc> <defs> <linearGradient x1="74.0423237%" y1="18.5882821%" x2="0%" y2="100%" id="linearGradient-1"> <stop stop-color="#00FEFF" offset="0%"/> <stop stop-color="#3D85FF" offset="100%"/> </linearGradient> </defs> <g id="Specs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Header-Copy" transform="translate(-182.000000, -152.000000)" fill="url(#linearGradient-1)"> <path d="M205.58574,176.859518 L205.58574,169.287998 C205.58574,169.287998 205.800116,167.315137 207.086372,167.315137 C208.372629,167.315137 208.265441,169.394639 210.677171,169.394639 C211.909834,169.394639 214,168.754792 214,165.022352 C214,161.289912 211.909834,160.810027 210.677171,160.810027 C208.265441,160.810027 208.372629,162.782888 207.086372,162.782888 C205.800116,162.782888 205.58574,160.756707 205.58574,160.756707 L205.58574,157.664114 C205.58574,156.491061 204.621048,155.531291 203.44198,155.531291 L197.814608,155.531291 C197.814608,155.531291 195.992412,155.211368 195.992412,153.931674 C195.992412,152.65198 198.028985,152.545339 198.028985,150.145914 C198.028985,148.91954 197.332262,147 193.580682,147 C189.829101,147 189.293161,148.91954 189.293161,150.145914 C189.293161,152.545339 191.115357,152.65198 191.115357,153.931674 C191.115357,155.211368 189.293161,155.531291 189.293161,155.531291 L184.148135,155.531291 C182.969067,155.531291 182.004375,156.491061 182.004375,157.664114 L182.004375,161.823118 C182.004375,161.823118 181.789999,165.022352 184.362512,165.022352 C186.023926,165.022352 186.07752,162.836209 188.274874,162.836209 C189.346755,162.836209 190.418635,163.8493 190.418635,166.035443 C190.418635,168.274907 189.346755,169.394639 188.274874,169.394639 C186.131114,169.394639 186.023926,167.208496 184.362512,167.208496 C181.789999,167.208496 182.004375,170.301089 182.004375,170.301089 L182.004375,176.859518 C182.004375,178.032571 182.969067,178.992341 184.148135,178.992341 L191.115357,178.992341 C191.115357,178.992341 194.49178,179.205623 194.49178,176.646236 C194.49178,174.993299 192.348019,174.726696 192.348019,172.540552 C192.348019,171.474141 193.527088,170.141127 195.778036,170.141127 C198.028985,170.141127 199.315241,171.474141 199.315241,172.540552 C199.315241,174.673375 197.225074,174.993299 197.225074,176.646236 C197.225074,179.258944 200.601497,178.992341 200.601497,178.992341 L203.44198,178.992341 C204.621048,178.992341 205.58574,178.032571 205.58574,176.859518 Z" id="Shape-Copy-23" transform="translate(198.000000, 163.000000) rotate(-42.000000) translate(-198.000000, -163.000000) "/> </g> </g> </svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+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

+3
View File
@@ -0,0 +1,3 @@
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path fill="context-fill #4c4c4c" fill-opacity="context-fill-opacity" d="M12.9137931,3.0862069 L12.9137931,1.27586207 C12.9137931,0.84736528 12.5664278,0.5 12.137931,0.5 C11.7094342,0.5 11.362069,0.84736528 11.362069,1.27586207 L11.362069,1.27586207 L11.362069,3.0862069 L9.55172414,3.0862069 C9.12322735,3.0862069 8.77586207,3.43357218 8.77586207,3.86206897 C8.77586207,4.29056575 9.12322735,4.63793103 9.55172414,4.63793103 L11.362069,4.63793103 L11.362069,6.44827586 C11.362069,6.87677265 11.7094342,7.22413793 12.137931,7.22413793 L12.137931,7.22413793 C12.5664278,7.22413793 12.9137931,6.87677265 12.9137931,6.44827586 L12.9137931,6.44827586 L12.9137931,4.63793103 L14.7241379,4.63793103 C15.1526347,4.63793103 15.5,4.29056575 15.5,3.86206897 L15.5,3.86206897 C15.5,3.43357218 15.1526347,3.0862069 14.7241379,3.0862069 L14.7241379,3.0862069 L12.9137931,3.0862069 Z M0.5,9.76803178 C0.5,9.22007158 0.94118947,8.77586207 1.49216971,8.77586207 L6.23196822,8.77586207 C6.77992842,8.77586207 7.22413793,9.21705154 7.22413793,9.76803178 L7.22413793,14.5078303 C7.22413793,15.0557905 6.78294846,15.5 6.23196822,15.5 L1.49216971,15.5 C0.94420951,15.5 0.5,15.0588105 0.5,14.5078303 L0.5,9.76803178 Z M8.77586207,9.76803178 C8.77586207,9.22007158 9.21705154,8.77586207 9.76803178,8.77586207 L14.5078303,8.77586207 C15.0557905,8.77586207 15.5,9.21705154 15.5,9.76803178 L15.5,14.5078303 C15.5,15.0557905 15.0588105,15.5 14.5078303,15.5 L9.76803178,15.5 C9.22007158,15.5 8.77586207,15.0588105 8.77586207,14.5078303 L8.77586207,9.76803178 Z M0.5,1.49216971 C0.5,0.94420951 0.94118947,0.5 1.49216971,0.5 L6.23196822,0.5 C6.77992842,0.5 7.22413793,0.94118947 7.22413793,1.49216971 L7.22413793,6.23196822 C7.22413793,6.77992842 6.78294846,7.22413793 6.23196822,7.22413793 L1.49216971,7.22413793 C0.94420951,7.22413793 0.5,6.78294846 0.5,6.23196822 L0.5,1.49216971 Z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="-14 -14 48 48" enable-background="new -14 -14 48 48" xml:space="preserve">
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="90.0527" y1="-99.7603" x2="90.0527" y2="-106.3809" gradientTransform="matrix(7.2338 0 0 -7.2338 -641.4998 -735.5619)">
<stop offset="0" style="stop-color:#4B71B8"/>
<stop offset="1" style="stop-color:#293F7E"/>
</linearGradient>
<path fill="url(#SVGID_1_)" d="M33.931,27.993c0,3.304-2.689,5.983-6.002,5.983H-8.082c-3.315,0-6.001-2.683-6.001-5.983V-7.928
c0-3.308,2.687-5.988,6.001-5.988h36.011c3.312,0,6.002,2.681,6.002,5.988V27.993z"/>
<path fill="#FFFFFF" d="M25.613-4.557c0,0-3.707,0-6.166,0c-3.662,0-7.732,1.535-7.732,6.835c0.019,1.845,0,3.613,0,5.603H7.481
v6.728h4.366v19.37h8.021V14.48h5.295l0.479-6.618h-5.913c0,0,0.016-2.946,0-3.8c0-2.093,2.184-1.974,2.312-1.974
c1.042,0,3.059,0.003,3.578,0v-6.646H25.613z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="23.9995" y1="0" x2="23.9995" y2="48.0005">
<stop offset="0" style="stop-color:#4BD0EF"/>
<stop offset="1" style="stop-color:#29AAE1"/>
</linearGradient>
<path fill-rule="evenodd" clip-rule="evenodd" fill="url(#SVGID_1_)" d="M48,42c0,3.313-2.687,6-6,6H6c-3.313,0-6-2.687-6-6V6
c0-3.313,2.687-6,6-6h36c3.313,0,6,2.687,6,6V42z"/>
<path fill="#29AAE1" d="M40.231,13.413c-1.12,0.497-2.323,0.833-3.588,0.984c1.291-0.774,2.28-1.998,2.747-3.457
c-1.206,0.716-2.543,1.236-3.968,1.516c-1.139-1.214-2.763-1.972-4.56-1.972c-3.449,0-6.246,2.796-6.246,6.247
c0,0.49,0.055,0.966,0.161,1.424c-5.192-0.261-9.795-2.749-12.876-6.528c-0.538,0.923-0.846,1.996-0.846,3.141
c0,2.167,1.103,4.08,2.779,5.199c-1.024-0.032-1.987-0.313-2.83-0.781c0,0.026,0,0.053,0,0.079c0,3.026,2.153,5.551,5.011,6.125
c-0.525,0.143-1.076,0.219-1.646,0.219c-0.403,0-0.794-0.038-1.176-0.11c0.795,2.48,3.102,4.287,5.835,4.338
c-2.138,1.675-4.832,2.675-7.758,2.675c-0.504,0-1.002-0.03-1.491-0.089c2.765,1.773,6.048,2.808,9.576,2.808
c11.49,0,17.774-9.519,17.774-17.774c0-0.271-0.006-0.54-0.019-0.809C38.334,15.766,39.394,14.666,40.231,13.413z"/>
<path fill="#FFFFFF" d="M40.231,14.739c-1.12,0.497-2.323,0.833-3.588,0.984c1.291-0.773,2.28-1.998,2.747-3.456
c-1.206,0.716-2.543,1.236-3.968,1.516c-1.139-1.214-2.763-1.972-4.56-1.972c-3.449,0-6.246,2.796-6.246,6.247
c0,0.489,0.055,0.966,0.161,1.424c-5.192-0.261-9.795-2.748-12.876-6.527c-0.538,0.923-0.846,1.996-0.846,3.141
c0,2.167,1.103,4.079,2.779,5.199c-1.024-0.032-1.987-0.313-2.83-0.781c0,0.026,0,0.052,0,0.079c0,3.027,2.153,5.551,5.011,6.125
c-0.525,0.144-1.076,0.219-1.646,0.219c-0.403,0-0.794-0.038-1.176-0.11c0.795,2.481,3.102,4.287,5.835,4.338
c-2.138,1.676-4.832,2.675-7.758,2.675c-0.504,0-1.002-0.03-1.491-0.089c2.765,1.773,6.048,2.808,9.576,2.808
c11.49,0,17.774-9.519,17.774-17.774c0-0.271-0.006-0.54-0.019-0.808C38.334,17.092,39.394,15.992,40.231,14.739z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+12
View File
@@ -0,0 +1,12 @@
module.exports = {
"extends": [
"../../.eslintrc.js"
],
"globals": {
"assignManager": true,
"badge": true,
"backgroundLogic": true,
"identityState": true,
"messageHandler": true
}
};
+381
View File
@@ -0,0 +1,381 @@
const assignManager = {
MENU_ASSIGN_ID: "open-in-this-container",
MENU_REMOVE_ID: "remove-open-in-this-container",
MENU_SEPARATOR_ID: "separator",
MENU_HIDE_ID: "hide-container",
MENU_MOVE_ID: "move-to-new-window-container",
storageArea: {
area: browser.storage.local,
exemptedTabs: {},
getSiteStoreKey(pageUrl) {
const url = new window.URL(pageUrl);
const storagePrefix = "siteContainerMap@@_";
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) => {
this.area.get([siteStoreKey]).then((storageResponse) => {
if (storageResponse && siteStoreKey in storageResponse) {
resolve(storageResponse[siteStoreKey]);
}
resolve(null);
}).catch((e) => {
reject(e);
});
});
},
set(pageUrl, data, exemptedTabIds) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
if (exemptedTabIds) {
exemptedTabIds.forEach((tabId) => {
this.setExempted(pageUrl, tabId);
});
}
return this.area.set({
[siteStoreKey]: data
});
},
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]);
},
async deleteContainer(userContextId) {
const sitesByContainer = await this.getByContainer(userContextId);
this.area.remove(Object.keys(sitesByContainer));
},
async getByContainer(userContextId) {
const sites = {};
const siteConfigs = await this.area.get();
Object.keys(siteConfigs).forEach((key) => {
// For some reason this is stored as string... lets check them both as that
if (String(siteConfigs[key].userContextId) === String(userContextId)) {
const site = siteConfigs[key];
// In hindsight we should have stored this
// TODO file a follow up to clean the storage onLoad
site.hostname = key.replace(/^siteContainerMap@@_/, "");
sites[key] = site;
}
});
return sites;
}
},
_neverAsk(m) {
const pageUrl = m.pageUrl;
if (m.neverAsk === true) {
// If we have existing data and for some reason it hasn't been deleted etc lets update it
this.storageArea.get(pageUrl).then((siteSettings) => {
if (siteSettings) {
siteSettings.neverAsk = true;
this.storageArea.set(pageUrl, siteSettings);
}
}).catch((e) => {
throw e;
});
}
},
// 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;
},
// Before a request is handled by the browser we decide if we should route through a different container
async onBeforeRequest(options) {
if (options.frameId !== 0 || options.tabId === -1) {
return {};
}
this.removeContextMenu();
const [tab, siteSettings] = await Promise.all([
browser.tabs.get(options.tabId),
this.storageArea.get(options.url)
]);
let container;
try {
container = await browser.contextualIdentities.get(backgroundLogic.cookieStoreId(siteSettings.userContextId));
} catch (e) {
container = false;
}
// The container we have in the assignment map isn't present any more so lets remove it
// then continue the existing load
if (!container) {
this.deleteContainer(siteSettings.userContextId);
return {};
}
const userContextId = this.getUserContextIdFromCookieStore(tab);
if (!siteSettings
|| userContextId === siteSettings.userContextId
|| tab.incognito
|| this.storageArea.isExempted(options.url, tab.id)) {
return {};
}
this.reloadPageInContainer(options.url, userContextId, siteSettings.userContextId, tab.index + 1, siteSettings.neverAsk);
this.calculateContextMenu(tab);
/* Removal of existing tabs:
We aim to open the new assigned container tab / warning prompt in it's own tab:
- As the history won't span from one container to another it seems most sane to not try and reopen a tab on history.back()
- When users open a new tab themselves we want to make sure we don't end up with three tabs as per: https://github.com/mozilla/testpilot-containers/issues/421
If we are coming from an internal url that are used for the new tab page (NEW_TAB_PAGES), we can safely close as user is unlikely losing history
Detecting redirects on "new tab" opening actions is pretty hard as we don't get tab history:
- Redirects happen from Short URLs and tracking links that act as a gateway
- Extensions don't provide a way to history crawl for tabs, we could inject content scripts to do this
however they don't run on about:blank so this would likely be just as hacky.
We capture the time the tab was created and close if it was within the timeout to try to capture pages which haven't had user interaction or history.
*/
if (backgroundLogic.NEW_TAB_PAGES.has(tab.url)
|| (messageHandler.lastCreatedTab
&& messageHandler.lastCreatedTab.id === tab.id)) {
browser.tabs.remove(tab.id);
}
return {
cancel: true,
};
},
init() {
browser.contextMenus.onClicked.addListener((info, tab) => {
this._onClickedHandler(info, tab);
});
// Before a request is handled by the browser we decide if we should route through a different container
browser.webRequest.onBeforeRequest.addListener((options) => {
return this.onBeforeRequest(options);
},{urls: ["<all_urls>"], types: ["main_frame"]}, ["blocking"]);
},
async _onClickedHandler(info, tab) {
const userContextId = this.getUserContextIdFromCookieStore(tab);
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
let remove;
if (userContextId) {
switch (info.menuItemId) {
case this.MENU_ASSIGN_ID:
case this.MENU_REMOVE_ID:
if (info.menuItemId === this.MENU_ASSIGN_ID) {
remove = false;
} else {
remove = true;
}
await this._setOrRemoveAssignment(tab.id, info.pageUrl, userContextId, remove);
break;
case this.MENU_MOVE_ID:
backgroundLogic.moveTabsToWindow({
cookieStoreId: tab.cookieStoreId,
windowId: tab.windowId,
});
break;
case this.MENU_HIDE_ID:
backgroundLogic.hideTabs({
cookieStoreId: tab.cookieStoreId,
windowId: tab.windowId,
});
break;
}
}
},
deleteContainer(userContextId) {
this.storageArea.deleteContainer(userContextId);
},
getUserContextIdFromCookieStore(tab) {
if (!("cookieStoreId" in tab)) {
return false;
}
return backgroundLogic.getUserContextIdFromCookieStoreId(tab.cookieStoreId);
},
isTabPermittedAssign(tab) {
// Ensure we are not an important about url
// 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;
},
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`
});
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
browser.contextMenus.remove(this.MENU_ASSIGN_ID);
browser.contextMenus.remove(this.MENU_REMOVE_ID);
browser.contextMenus.remove(this.MENU_SEPARATOR_ID);
browser.contextMenus.remove(this.MENU_HIDE_ID);
browser.contextMenus.remove(this.MENU_MOVE_ID);
},
async calculateContextMenu(tab) {
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;
}
let checked = false;
let menuId = this.MENU_ASSIGN_ID;
const tabUserContextId = this.getUserContextIdFromCookieStore(tab);
if (siteSettings &&
Number(siteSettings.userContextId) === Number(tabUserContextId)) {
checked = true;
menuId = this.MENU_REMOVE_ID;
}
browser.contextMenus.create({
id: menuId,
title: "Always Open in This Container",
checked,
type: "checkbox",
contexts: ["all"],
});
browser.contextMenus.create({
id: this.MENU_SEPARATOR_ID,
type: "separator",
contexts: ["all"],
});
browser.contextMenus.create({
id: this.MENU_HIDE_ID,
title: "Hide This Container",
contexts: ["all"],
});
browser.contextMenus.create({
id: this.MENU_MOVE_ID,
title: "Move Tabs to a New Window",
contexts: ["all"],
});
},
encodeURLProperty(url) {
return encodeURIComponent(url).replace(/[!'()*]/g, (c) => {
const charCode = c.charCodeAt(0).toString(16);
return `%${charCode}`;
});
},
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, index});
} else {
let confirmUrl = `${loadPage}?url=${this.encodeURLProperty(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) => {
throw e;
});
}
}
};
assignManager.init();
@@ -0,0 +1,306 @@
const DEFAULT_TAB = "about:newtab";
const backgroundLogic = {
NEW_TAB_PAGES: new Set([
"about:startpage",
"about:newtab",
"about:home",
"about:blank"
]),
async getExtensionInfo() {
const manifestPath = browser.extension.getURL("manifest.json");
const response = await fetch(manifestPath);
const extensionInfo = await response.json();
return extensionInfo;
},
getUserContextIdFromCookieStoreId(cookieStoreId) {
if (!cookieStoreId) {
return false;
}
const container = cookieStoreId.replace("firefox-container-", "");
if (container !== cookieStoreId) {
return container;
}
return false;
},
async deleteContainer(userContextId, removed = false) {
await this._closeTabs(userContextId);
if (!removed) {
await browser.contextualIdentities.remove(this.cookieStoreId(userContextId));
}
assignManager.deleteContainer(userContextId);
return {done: true, userContextId};
},
async createOrUpdateContainer(options) {
let donePromise;
if (options.userContextId !== "new") {
donePromise = browser.contextualIdentities.update(
this.cookieStoreId(options.userContextId),
options.params
);
} else {
donePromise = browser.contextualIdentities.create(options.params);
}
await donePromise;
browser.runtime.sendMessage({
method: "refreshNeeded"
});
},
async openNewTab(options) {
let url = options.url || undefined;
const userContextId = ("userContextId" in options) ? options.userContextId : 0;
const active = ("nofocus" in options) ? options.nofocus : true;
const cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
// Autofocus url bar will happen in 54: https://bugzilla.mozilla.org/show_bug.cgi?id=1295072
// We can't open new tab pages, so open a blank tab. Used in tab un-hide
if (this.NEW_TAB_PAGES.has(url)) {
url = undefined;
}
if (!this.isPermissibleURL(url)) {
return;
}
return browser.tabs.create({
url,
active,
pinned: options.pinned || false,
cookieStoreId
});
},
isPermissibleURL(url) {
const protocol = new URL(url).protocol;
// We can't open these we just have to throw them away
if (protocol === "about:"
|| protocol === "chrome:"
|| protocol === "moz-extension:") {
return false;
}
return true;
},
checkArgs(requiredArguments, options, methodName) {
requiredArguments.forEach((argument) => {
if (!(argument in options)) {
return new Error(`${methodName} must be called with ${argument} argument.`);
}
});
},
async getTabs(options) {
const requiredArguments = ["cookieStoreId", "windowId"];
this.checkArgs(requiredArguments, options, "getTabs");
const { cookieStoreId, windowId } = options;
const list = [];
const tabs = await browser.tabs.query({
cookieStoreId,
windowId
});
tabs.forEach((tab) => {
list.push(identityState._createTabObject(tab));
});
const containerState = await identityState.storageArea.get(cookieStoreId);
return list.concat(containerState.hiddenTabs);
},
async moveTabsToWindow(options) {
const requiredArguments = ["cookieStoreId", "windowId"];
this.checkArgs(requiredArguments, options, "moveTabsToWindow");
const { cookieStoreId, windowId } = options;
const list = await browser.tabs.query({
cookieStoreId,
windowId
});
const containerState = await identityState.storageArea.get(cookieStoreId);
// Nothing to do
if (list.length === 0 &&
containerState.hiddenTabs.length === 0) {
return;
}
let newWindowObj;
let hiddenDefaultTabToClose;
if (list.length) {
newWindowObj = await browser.windows.create({
tabId: list.shift().id
});
browser.tabs.move(list.map((tab) => tab.id), {
windowId: newWindowObj.id,
index: -1
});
} else {
//As we get a blank tab here we will need to await the tabs creation
newWindowObj = await browser.windows.create({
});
hiddenDefaultTabToClose = true;
}
const showHiddenPromises = [];
// Let's show the hidden tabs.
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
showHiddenPromises.push(browser.tabs.create({
url: object.url || DEFAULT_TAB,
windowId: newWindowObj.id,
cookieStoreId
}));
}
if (hiddenDefaultTabToClose) {
// Lets wait for hidden tabs to show before closing the others
await showHiddenPromises;
}
containerState.hiddenTabs = [];
// Let's close all the normal tab in the new window. In theory it
// should be only the first tab, but maybe there are addons doing
// crazy stuff.
const tabs = await browser.tabs.query({windowId: newWindowObj.id});
for (let tab of tabs) { // eslint-disable-line prefer-const
if (tab.cookieStoreId !== cookieStoreId) {
browser.tabs.remove(tab.id);
}
}
return await identityState.storageArea.set(cookieStoreId, containerState);
},
async _closeTabs(userContextId, windowId = false) {
const cookieStoreId = this.cookieStoreId(userContextId);
let tabs;
/* if we have no windowId we are going to close all this container (used for deleting) */
if (windowId !== false) {
tabs = await browser.tabs.query({
cookieStoreId,
windowId
});
} else {
tabs = await browser.tabs.query({
cookieStoreId
});
}
const tabIds = tabs.map((tab) => tab.id);
return browser.tabs.remove(tabIds);
},
async queryIdentitiesState(windowId) {
const identities = await browser.contextualIdentities.query({});
const identitiesOutput = {};
const identitiesPromise = identities.map(async function (identity) {
const { cookieStoreId } = identity;
const containerState = await identityState.storageArea.get(cookieStoreId);
const openTabs = await browser.tabs.query({
cookieStoreId,
windowId
});
identitiesOutput[cookieStoreId] = {
hasHiddenTabs: !!containerState.hiddenTabs.length,
hasOpenTabs: !!openTabs.length
};
return;
});
await Promise.all(identitiesPromise);
return identitiesOutput;
},
async sortTabs() {
const windows = await browser.windows.getAll();
for (let windowObj of windows) { // eslint-disable-line prefer-const
// First the pinned tabs, then the normal ones.
await this._sortTabsInternal(windowObj, true);
await this._sortTabsInternal(windowObj, false);
}
},
async _sortTabsInternal(windowObj, pinnedTabs) {
const tabs = await browser.tabs.query({windowId: windowObj.id});
let pos = 0;
// Let's collect UCIs/tabs for this window.
const map = new Map;
for (const tab of tabs) {
if (pinnedTabs && !tab.pinned) {
// We don't have, or we already handled all the pinned tabs.
break;
}
if (!pinnedTabs && tab.pinned) {
// pinned tabs must be consider as taken positions.
++pos;
continue;
}
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(tab.cookieStoreId);
if (!map.has(userContextId)) {
map.set(userContextId, []);
}
map.get(userContextId).push(tab);
}
// Let's sort the map.
const sortMap = new Map([...map.entries()].sort((a, b) => a[0] > b[0]));
// Let's move tabs.
sortMap.forEach(tabs => {
for (const tab of tabs) {
++pos;
browser.tabs.move(tab.id, {
windowId: windowObj.id,
index: pos
});
}
});
},
async hideTabs(options) {
const requiredArguments = ["cookieStoreId", "windowId"];
this.checkArgs(requiredArguments, options, "hideTabs");
const { cookieStoreId, windowId } = options;
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(cookieStoreId);
const containerState = await identityState.storeHidden(cookieStoreId, windowId);
await this._closeTabs(userContextId, windowId);
return containerState;
},
async showTabs(options) {
if (!("cookieStoreId" in options)) {
return Promise.reject("showTabs must be called with cookieStoreId argument.");
}
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId);
const promises = [];
const containerState = await identityState.storageArea.get(options.cookieStoreId);
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
promises.push(this.openNewTab({
userContextId: userContextId,
url: object.url,
nofocus: options.nofocus || false,
pinned: object.pinned,
}));
}
containerState.hiddenTabs = [];
await Promise.all(promises);
return await identityState.storageArea.set(options.cookieStoreId, containerState);
},
cookieStoreId(userContextId) {
return `firefox-container-${userContextId}`;
}
};
+25
View File
@@ -0,0 +1,25 @@
const MAJOR_VERSIONS = ["2.3.0", "2.4.0"];
const badge = {
async init() {
const currentWindow = await browser.windows.getCurrent();
this.displayBrowserActionBadge(currentWindow.incognito);
},
disableAddon(tabId) {
browser.browserAction.disable(tabId);
browser.browserAction.setTitle({ tabId, title: "Containers disabled in Private Browsing Mode" });
},
async displayBrowserActionBadge() {
const extensionInfo = await backgroundLogic.getExtensionInfo();
const storage = await browser.storage.local.get({browserActionBadgesClicked: []});
if (MAJOR_VERSIONS.indexOf(extensionInfo.version) > -1 &&
storage.browserActionBadgesClicked.indexOf(extensionInfo.version) < 0) {
browser.browserAction.setBadgeBackgroundColor({color: "rgba(0,217,0,255)"});
browser.browserAction.setBadgeText({text: "NEW"});
}
}
};
badge.init();
@@ -0,0 +1,62 @@
const identityState = {
storageArea: {
area: browser.storage.local,
getContainerStoreKey(cookieStoreId) {
const storagePrefix = "identitiesState@@_";
return `${storagePrefix}${cookieStoreId}`;
},
async get(cookieStoreId) {
const storeKey = this.getContainerStoreKey(cookieStoreId);
const storageResponse = await this.area.get([storeKey]);
if (storageResponse && storeKey in storageResponse) {
return storageResponse[storeKey];
}
const defaultContainerState = identityState._createIdentityState();
await this.set(cookieStoreId, defaultContainerState);
return defaultContainerState;
},
set(cookieStoreId, data) {
const storeKey = this.getContainerStoreKey(cookieStoreId);
return this.area.set({
[storeKey]: data
});
},
remove(cookieStoreId) {
const storeKey = this.getContainerStoreKey(cookieStoreId);
return this.area.remove([storeKey]);
}
},
_createTabObject(tab) {
return Object.assign({}, tab);
},
async storeHidden(cookieStoreId, windowId) {
const containerState = await this.storageArea.get(cookieStoreId);
const tabsByContainer = await browser.tabs.query({cookieStoreId, windowId});
tabsByContainer.forEach((tab) => {
const tabObject = this._createTabObject(tab);
if (!backgroundLogic.isPermissibleURL(tab.url)) {
return;
}
// This tab is going to be closed. Let's mark this tabObject as
// non-active.
tabObject.active = false;
tabObject.hiddenState = true;
containerState.hiddenTabs.push(tabObject);
});
return this.storageArea.set(cookieStoreId, containerState);
},
_createIdentityState() {
return {
hiddenTabs: []
};
},
};
+22
View File
@@ -0,0 +1,22 @@
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<!--
This didn't work for debugging in the manifest.
"scripts": [
"js/background/backgroundLogic.js",
"js/background/assignManager.js",
"js/background/badge.js",
"js/background/identityState.js",
"js/background/messageHandler.js",
]
-->
<script type="text/javascript" src="backgroundLogic.js"></script>
<script type="text/javascript" src="assignManager.js"></script>
<script type="text/javascript" src="badge.js"></script>
<script type="text/javascript" src="identityState.js"></script>
<script type="text/javascript" src="messageHandler.js"></script>
</body>
</html>
@@ -0,0 +1,178 @@
const messageHandler = {
// After the timer completes we assume it's a tab the user meant to keep open
// We use this to catch redirected tabs that have just opened
// If this were in platform we would change how the tab opens based on "new tab" link navigations such as ctrl+click
LAST_CREATED_TAB_TIMER: 2000,
unhideQueue: [],
init() {
// Handles messages from webextension code
browser.runtime.onMessage.addListener((m) => {
let response;
switch (m.method) {
case "deleteContainer":
response = backgroundLogic.deleteContainer(m.message.userContextId);
break;
case "createOrUpdateContainer":
response = backgroundLogic.createOrUpdateContainer(m.message);
break;
case "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 "sortTabs":
backgroundLogic.sortTabs();
break;
case "showTabs":
this.unhideContainer(m.cookieStoreId);
break;
case "hideTabs":
backgroundLogic.hideTabs({
cookieStoreId: m.cookieStoreId,
windowId: m.windowId
});
break;
case "checkIncompatibleAddons":
// TODO
break;
case "moveTabsToWindow":
response = backgroundLogic.moveTabsToWindow({
cookieStoreId: m.cookieStoreId,
windowId: m.windowId
});
break;
case "getTabs":
response = backgroundLogic.getTabs({
cookieStoreId: m.cookieStoreId,
windowId: m.windowId
});
break;
case "queryIdentitiesState":
response = backgroundLogic.queryIdentitiesState(m.message.windowId);
break;
case "exemptContainerAssignment":
response = assignManager._exemptTab(m);
break;
}
return response;
});
if (browser.contextualIdentities.onRemoved) {
browser.contextualIdentities.onRemoved.addListener(({contextualIdentity}) => {
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(contextualIdentity.cookieStoreId);
backgroundLogic.deleteContainer(userContextId, true);
});
}
browser.tabs.onActivated.addListener((info) => {
assignManager.removeContextMenu();
browser.tabs.get(info.tabId).then((tab) => {
assignManager.calculateContextMenu(tab);
}).catch((e) => {
throw e;
});
});
browser.windows.onFocusChanged.addListener((windowId) => {
this.onFocusChangedCallback(windowId);
});
browser.webRequest.onCompleted.addListener((details) => {
if (details.frameId !== 0 || details.tabId === -1) {
return {};
}
assignManager.removeContextMenu();
browser.tabs.get(details.tabId).then((tab) => {
assignManager.calculateContextMenu(tab);
}).catch((e) => {
throw e;
});
}, {urls: ["<all_urls>"], types: ["main_frame"]});
browser.tabs.onCreated.addListener((tab) => {
if (tab.incognito) {
badge.disableAddon(tab.id);
}
// lets remember the last tab created so we can close it if it looks like a redirect
this.lastCreatedTab = tab;
if (tab.cookieStoreId) {
// Don't count firefox-default, firefox-private, nor our own confirm page loads
if (tab.cookieStoreId !== "firefox-default" &&
tab.cookieStoreId !== "firefox-private" &&
!tab.url.startsWith("moz-extension")) {
// increment the counter of container tabs opened
this.incrementCountOfContainerTabsOpened();
}
this.unhideContainer(tab.cookieStoreId);
}
setTimeout(() => {
this.lastCreatedTab = null;
}, this.LAST_CREATED_TAB_TIMER);
});
},
async incrementCountOfContainerTabsOpened() {
const key = "containerTabsOpened";
const count = await browser.storage.local.get({[key]: 0});
const countOfContainerTabsOpened = ++count[key];
browser.storage.local.set({[key]: countOfContainerTabsOpened});
// When the user opens their _ tab, give them the achievement
if (countOfContainerTabsOpened === 100) {
const storage = await browser.storage.local.get({achievements: []});
storage.achievements.push({"name": "manyContainersOpened", "done": false});
// use set and spread to create a unique array
const achievements = [...new Set(storage.achievements)];
browser.storage.local.set({achievements});
browser.browserAction.setBadgeBackgroundColor({color: "rgba(0,217,0,255)"});
browser.browserAction.setBadgeText({text: "NEW"});
}
},
async unhideContainer(cookieStoreId) {
if (!this.unhideQueue.includes(cookieStoreId)) {
this.unhideQueue.push(cookieStoreId);
// Unhide all hidden tabs
await backgroundLogic.showTabs({
cookieStoreId
});
this.unhideQueue.splice(this.unhideQueue.indexOf(cookieStoreId), 1);
}
},
async onFocusChangedCallback(windowId) {
assignManager.removeContextMenu();
// browserAction loses background color in new windows ...
// https://bugzil.la/1314674
// https://github.com/mozilla/testpilot-containers/issues/608
// ... so re-call displayBrowserActionBadge on window changes
badge.displayBrowserActionBadge();
browser.tabs.query({active: true, windowId}).then((tabs) => {
if (tabs && tabs[0]) {
assignManager.calculateContextMenu(tabs[0]);
}
}).catch((e) => {
throw e;
});
}
};
// Lets do this last as theme manager did a check before connecting before
messageHandler.init();
+72 -18
View File
@@ -1,30 +1,84 @@
const redirectUrl = new URL(window.location).searchParams.get("url");
document.getElementById("redirect-url").textContent = redirectUrl;
const redirectSite = new URL(redirectUrl).hostname;
document.getElementById("redirect-site").textContent = redirectSite;
async function load() {
const searchParams = new URL(window.location).searchParams;
const redirectUrl = decodeURIComponent(searchParams.get("url"));
const cookieStoreId = searchParams.get("cookieStoreId");
const currentCookieStoreId = searchParams.get("currentCookieStoreId");
const redirectUrlElement = document.getElementById("redirect-url");
redirectUrlElement.textContent = redirectUrl;
appendFavicon(redirectUrl, redirectUrlElement);
document.getElementById("redirect-form").addEventListener("submit", (e) => {
e.preventDefault();
const container = await browser.contextualIdentities.get(cookieStoreId);
[...document.querySelectorAll(".container-name")].forEach((containerNameElement) => {
containerNameElement.textContent = container.name;
});
// If default container, button will default to normal HTML content
if (currentCookieStoreId) {
const currentContainer = await browser.contextualIdentities.get(currentCookieStoreId);
document.getElementById("current-container-name").textContent = currentContainer.name;
}
document.getElementById("redirect-form").addEventListener("submit", (e) => {
e.preventDefault();
const buttonTarget = e.explicitOriginalTarget;
switch (buttonTarget.id) {
case "confirm":
confirmSubmit(redirectUrl, cookieStoreId);
break;
case "deny":
denySubmit(redirectUrl);
break;
}
});
}
function appendFavicon(pageUrl, redirectUrlElement) {
const origin = new URL(pageUrl).origin;
const favIconElement = Utils.createFavIconElement(`${origin}/favicon.ico`);
redirectUrlElement.prepend(favIconElement);
}
function confirmSubmit(redirectUrl, cookieStoreId) {
const neverAsk = document.getElementById("never-ask").checked;
// Sending neverAsk message to background to store for next time we see this process
if (neverAsk) {
browser.runtime.sendMessage({
method: "neverAsk",
neverAsk: true,
pageUrl: redirectUrl
}).then(() => {
redirect();
}).catch(() => {
// Can't really do much here user will have to click it again
});
}
browser.runtime.sendMessage({
method: "sendTelemetryPayload",
event: "click-to-reload-page-in-container",
});
redirect();
});
openInContainer(redirectUrl, cookieStoreId);
}
function redirect() {
const redirectUrl = document.getElementById("redirect-url").textContent;
function getCurrentTab() {
return browser.tabs.query({
active: true,
windowId: browser.windows.WINDOW_ID_CURRENT
});
}
async function denySubmit(redirectUrl) {
const tab = await getCurrentTab();
await browser.runtime.sendMessage({
method: "exemptContainerAssignment",
tabId: tab[0].id,
pageUrl: redirectUrl
});
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);
}
}
+46
View File
@@ -0,0 +1,46 @@
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");
// Ideally we would use https://bugzilla.mozilla.org/show_bug.cgi?id=1340930 when this is available
divElement.innerText = message.text;
const imageElement = document.createElement("img");
const imagePath = browser.extension.getURL("/img/container-site-d-24.png");
const response = await fetch(imagePath);
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
imageElement.src = objectUrl;
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);
});
+580 -143
View File
File diff suppressed because it is too large Load Diff
+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;
}
};
+32 -13
View File
@@ -1,9 +1,9 @@
{
"manifest_version": 2,
"name": "Containers Experiment",
"version": "2.2.0",
"name": "Firefox Multi-Account Containers",
"version": "4.1.0",
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored Container Tabs. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
"description": "Multi-Account Containers helps you keep all the parts of your online life contained in different tabs. Custom labels and color-coded tabs help keep different activities — like online shopping, travel planning, or checking work email — separate.",
"icons": {
"48": "img/container-site-d-48.png",
"96": "img/container-site-d-96.png"
@@ -11,8 +11,7 @@
"applications": {
"gecko": {
"strict_min_version": "51.0",
"update_url": "https://testpilot.firefox.com/files/@testpilot-containers/updates.json"
"strict_min_version": "53.0"
}
},
@@ -23,26 +22,46 @@
"activeTab",
"cookies",
"contextMenus",
"contextualIdentities",
"history",
"idle",
"notifications",
"storage",
"tabs",
"webRequestBlocking",
"webRequest"
],
"commands": {
"_execute_browser_action": {
"suggested_key": {
"default": "Ctrl+Period",
"mac": "MacCtrl+Period"
},
"description": "Open containers panel"
}
},
"browser_action": {
"browser_style": true,
"default_icon": {
"16": "img/container-site-d-24.png",
"32": "img/container-site-d-48.png"
},
"default_title": "Containers",
"default_icon": "img/container-site.svg",
"default_title": "Multi-Account Containers",
"default_popup": "popup.html"
},
"background": {
"scripts": ["background.js"]
}
"page": "js/background/index.html"
},
"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"
]
}
+106 -25
View File
@@ -1,57 +1,132 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Containers browserAction Popup</title>
<title>Multi-Account Containers</title>
<link rel="stylesheet" href="/css/popup.css">
</head>
<body>
<div class="hide panel onboarding onboarding-panel-1" id="onboarding-panel-1">
<div class="hide panel onboarding onboarding-panel-1">
<img class="onboarding-img" alt="Container Tabs Overview" src="/img/onboarding-1.png" />
<h3 class="onboarding-title">A better way to manage all the things you do online</h3>
<p>
Use containers to organize tasks, manage accounts, and keep your focus where you want it.
</p>
<a href="#" id="onboarding-start-button" class="onboarding-button">Get Started</a>
<a href="#" class="onboarding-button onboarding-start-button">Get Started</a>
</div>
<div class="hide panel onboarding security-onboarding-panel-1">
<img class="onboarding-img" alt="Container Tabs Overview" src="/img/onboarding-1.png" />
<h3 class="onboarding-title">A simple and secure way to manage your online life</h3>
<p>
Use containers to organize tasks, manage accounts, and store sensitive data.
</p>
<a href="#" class="onboarding-button onboarding-start-button">Get Started</a>
</div>
<div class="panel onboarding onboarding-panel-2 hide" id="onboarding-panel-2">
<div class="panel onboarding onboarding-panel-2 hide">
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-2.png" />
<h3 class="onboarding-title">Put containers to work for you.</h3>
<p>Features like color-coding and separate container tabs help you find things easily, focus your attention, and minimize distractions.</p>
<a href="#" id="onboarding-next-button" class="onboarding-button">Next</a>
<a href="#" class="onboarding-button onboarding-next-button">Next</a>
</div>
<div class="panel onboarding onboarding-panel-3 hide" id="onboarding-panel-3">
<div class="panel onboarding security-onboarding-panel-2 hide">
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-2.png" />
<h3 class="onboarding-title">Put containers to work for you.</h3>
<p>Color-coding helps you categorize your online life, find things easily, and minimize distractions.</p>
<a href="#" class="onboarding-button onboarding-next-button">Next</a>
</div>
<div class="panel onboarding onboarding-panel-3 hide">
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-3.png" />
<h3 class="onboarding-title">A place for everything, and everything in its place.</h3>
<p>Start with the containers we've created, or create your own.</p>
<a href="#" id="onboarding-done-button" class="onboarding-button">Done</a>
<a href="#" class="onboarding-button onboarding-almost-done-button">Next</a>
</div>
<div class="panel onboarding security-onboarding-panel-3 hide">
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-3-security.png" />
<h3 class="onboarding-title">Set boundaries for your browsing.</h3>
<p>Cookies are stored within a container, so you can segment sensitive data and browsing history to stay organized and to limit the impact of online trackers.</p>
<a href="#" class="onboarding-button onboarding-almost-done-button">Next</a>
</div>
<div class="panel onboarding onboarding-panel-4 hide" id="onboarding-panel-4">
<img class="onboarding-img" alt="How to assign sites to containers" src="/img/onboarding-4.png" />
<h3 class="onboarding-title">Always open sites in the containers you want.</h3>
<p>Right-click inside a container tab to assign the site to always open in the container.</p>
<a href="#" id="onboarding-done-button" class="onboarding-button">Next</a>
</div>
<div class="panel onboarding onboarding-panel-5 hide" id="onboarding-panel-5">
<img class="onboarding-img" alt="Long-press the New Tab button to create a new container tab." src="/img/onboarding-3.png" />
<h3 class="onboarding-title">Container tabs when you need them.</h3>
<p>Long-press the New Tab button to create a new container tab.</p>
<a href="#" id="onboarding-longpress-button" class="onboarding-button">Done</a>
</div>
<div class="panel achievement-panel hide" id="achievement-panel">
<img class="onboarding-img" alt="You achieved a Containers milestone!" src="/img/onboarding-3.png" />
<h3 class="onboarding-title">100 tabs!</h3>
<p>You've opened 100 Container tabs.</p>
<p>If you enjoy Containers, help us spread the word!</p>
<p class="share-ctas">
<a class="cta-link" href="https://mzl.la/2gJtIZ4" id="achievement-rate-button" target="_blank">
<span class="cta amo-rate-cta">
<img src="/img/amo-icon.svg" class="cta-icon" alt="addons.mozilla.org Icon">
Rate
</span>
</a>
<a class="cta-link" href="https://bit.ly/fb-share-mac-addon" target="_blank">
<span class="cta fb-share-cta">
<img src="/img/webicon-facebook.svg" class="cta-icon" alt="Facebook Icon">
Share
</span>
</a>
<a class="cta-link" href="http://bit.ly/tweet-100-tabs-mac-addon" target="_blank">
<span class="cta tweet-cta">
<img src="/img/webicon-twitter.svg" class="cta-icon" alt="Twitter Icon">
Tweet
</span>
</a>
</p>
<a href="#" id="achievement-done-button" class="onboarding-button">Done</a>
</div>
<div class="panel container-panel hide" id="container-panel">
<div class="panel-header">
<h3 class="panel-header-text">Containers</h3>
<a class="pop-button" id="sort-containers-link"><img class="pop-button-image" alt="Sort Containers" title="Sort Containers" src="/img/container-sort.svg"></a>
<div id="current-tab">
<h3>Current Tab</h3>
<div id="current-page"></div>
<label for="container-page-assigned">
<input type="checkbox" id="container-page-assigned" />
<span class="truncate-text">
Always open in
<span id="current-container"></span>
</span>
</label>
</div>
<div class="scrollable panel-content">
<table>
<tbody class="identities-list"></tbody>
<div class="container-panel-controls">
<a href="#" class="action-link" id="sort-containers-link" title="Sort tabs into container order">Sort Tabs</a>
</div>
<div class="scrollable panel-content" tabindex="-1">
<table class="identities-list">
<tbody></tbody>
</table>
</div>
<div class="panel-footer edit-identities">
<div class="edit-containers-text panel-footer-secondary">
<a id="edit-containers-link">Edit Containers</a>
<a href="#" tabindex="0" id="edit-containers-link">Edit Containers</a>
</div>
<a class="add-container-link pop-button" id="container-add-link">
<img class="pop-button-image-small icon" alt="Create new container icon" title="Create new container" src="/img/container-add.svg" />
<a href="#" tabindex="0" class="add-container-link pop-button" id="container-add-link" title="Create new container">
<img class="pop-button-image-small icon" alt="Create new container icon" src="/img/container-add.svg" />
</a>
</div>
</div>
<div class="hide panel container-info-panel" id="container-info-panel">
<div class="hide panel container-info-panel" id="container-info-panel" tabindex="-1">
<div class="columns">
<div class="panel-back-arrow" id="close-container-info-panel">
<img alt="Panel Back Arrow" src="/img/container-arrow.svg" class="back-arrow-img" />
@@ -59,7 +134,7 @@
<div class="column-panel-content">
<div class="panel-header container-info-panel-header">
<span class="usercontext-icon" id="container-info-icon"></span>
<h3 id="container-info-name" class="panel-header-text container-name"></h3>
<h3 id="container-info-name" class="panel-header-text container-name truncate-text"></h3>
</div>
<div class="select-row clickable container-info-panel-hide container-info-has-tabs" id="container-info-hideorshow">
<img id="container-info-hideorshow-icon" alt="Hide Container icon" src="/img/container-hide.svg" class="icon container-info-panel-hideorshow-icon"/>
@@ -85,7 +160,7 @@
</table>
</div>
<div class="panel-footer edit-containers-panel-footer">
<a id="exit-edit-mode-link" class="exit-edit-mode-link edit-containers-exit-text">Exit Edit Mode</a>
<a href="#" id="exit-edit-mode-link" class="exit-edit-mode-link edit-containers-exit-text">Exit Edit Mode</a>
</div>
</div>
@@ -97,19 +172,25 @@
</div>
<div class="column-panel-content">
<form id="edit-container-panel-form">
<input type="hidden" name="container-id" id="edit-container-panel-usercontext-input" />
<fieldset>
<legend>Name</legend>
<input type="text" name="container-name" id="edit-container-panel-name-input" maxlength="25"/>
</fieldset>
<fieldset id="edit-container-panel-choose-color">
<fieldset id="edit-container-panel-choose-color" class="radio-choice">
<legend>Choose a color</legend>
</fieldset>
<fieldset id="edit-container-panel-choose-icon">
<fieldset id="edit-container-panel-choose-icon" class="radio-choice">
<legend>Choose an icon</legend>
</fieldset>
</form>
<div id="edit-sites-assigned" class="scrollable" hidden>
<h3>Sites assigned to this container</h3>
<div class="assigned-sites-list">
</div>
</div>
<div class="panel-footer">
<a class="button secondary expanded footer-button cancel-button" id="edit-container-cancel-link">Cancel</a>
<a href="#" class="button secondary expanded footer-button cancel-button" id="edit-container-cancel-link">Cancel</a>
<a class="button primary expanded footer-button" id="edit-container-ok-link">OK</a>
</div>
</div>
@@ -126,12 +207,12 @@
<p>If you remove this container now, <span id="delete-container-tab-count"></span> container tabs will be closed. Are you sure you want to remove this Container?</p>
</div>
<div class="panel-footer">
<a class="button expanded secondary footer-button cancel-button" id="delete-container-cancel-link">Cancel</a>
<a class="button expanded primary footer-button" id="delete-container-ok-link">OK</a>
<a href="#" class="button expanded secondary footer-button cancel-button" id="delete-container-cancel-link">Cancel</a>
<a href="#" class="button expanded primary footer-button" id="delete-container-ok-link">OK</a>
</div>
</div>
<script src="js/utils.js"></script>
<script src="js/popup.js"></script>
</body>
</html>