commit 69b8d3553d21796db96c0913c5747f8724b9b662 Author: Kathy Brade brade@pearlcrescent.com Date: Fri Aug 24 14:47:31 2018 -0400
Bug 26962 - implement new features onboarding (part 1).
Add an "Explore" button to the "Circuit Display" panel within new user onboarding which opens the DuckDuckGo .onion and then guides users through a short circuit display tutorial.
Allow a few additional UITour actions while limiting as much as possible how it can be used.
Tweak the UITour styles to match the Tor Browser branding.
All user interface strings are retrieved from Torbutton's browserOnboarding.properties file. --- browser/app/permissions | 2 + browser/components/uitour/UITour.jsm | 51 +++- browser/components/uitour/content-UITour.js | 2 +- browser/extensions/onboarding/bootstrap.js | 16 ++ .../content/onboarding-tor-circuit-display.js | 283 +++++++++++++++++++++ .../onboarding/content/onboarding-tour-agent.js | 3 - .../extensions/onboarding/content/onboarding.js | 6 +- browser/extensions/onboarding/jar.mn | 1 + browser/themes/shared/UITour.inc.css | 30 +-- 9 files changed, 366 insertions(+), 28 deletions(-)
diff --git a/browser/app/permissions b/browser/app/permissions index b4b166c755ae..ac3464afd41c 100644 --- a/browser/app/permissions +++ b/browser/app/permissions @@ -7,6 +7,8 @@ # See nsPermissionManager.cpp for more...
# UITour +# DuckDuckGo .onion (used for circuit display onboarding). +origin uitour 1 https://3g2upl4pq6kufc4m.onion origin uitour 1 about:home origin uitour 1 about:newtab origin uitour 1 about:tor diff --git a/browser/components/uitour/UITour.jsm b/browser/components/uitour/UITour.jsm index b282d5c2d885..ce3e20fda662 100644 --- a/browser/components/uitour/UITour.jsm +++ b/browser/components/uitour/UITour.jsm @@ -42,9 +42,23 @@ const PREF_LOG_LEVEL = "browser.uitour.loglevel"; const PREF_SEENPAGEIDS = "browser.uitour.seenPageIDs";
const TOR_BROWSER_PAGE_ACTIONS_ALLOWED = new Set([ + "showInfo", // restricted to TOR_BROWSER_TARGETS_ALLOWED + "showMenu", // restricted to TOR_BROWSER_MENUS_ALLOWED + "hideMenu", // restricted to TOR_BROWSER_MENUS_ALLOWED + "closeTab", "torBrowserOpenSecuritySettings", ]);
+const TOR_BROWSER_TARGETS_ALLOWED = new Set([ + "torBrowser-circuitDisplay", + "torBrowser-circuitDisplay-diagram", + "torBrowser-circuitDisplay-newCircuitButton", +]); + +const TOR_BROWSER_MENUS_ALLOWED = new Set([ + "controlCenter", +]); + const BACKGROUND_PAGE_ACTIONS_ALLOWED = new Set([ "forceShowReaderIcon", "getConfiguration", @@ -103,6 +117,14 @@ var UITour = {
highlightEffects: ["random", "wobble", "zoom", "color"], targets: new Map([ + ["torBrowser-circuitDisplay", { + query: "#connection-icon", + }], + ["torBrowser-circuitDisplay-diagram", + torBrowserCircuitDisplayTarget("circuit-display-nodes")], + ["torBrowser-circuitDisplay-newCircuitButton", + torBrowserCircuitDisplayTarget("circuit-reload-button")], + ["accountStatus", { query: (aDocument) => { // If the user is logged in, use the avatar element. @@ -945,7 +967,7 @@ var UITour = {
// This function is copied to UITourListener. isSafeScheme(aURI) { - let allowedSchemes = new Set(["about"]); + let allowedSchemes = new Set(["about", "https"]);
if (!allowedSchemes.has(aURI.scheme)) { log.error("Unsafe scheme:", aURI.scheme); @@ -988,7 +1010,10 @@ var UITour = { return Promise.reject("Invalid target name specified"); }
- let targetObject = this.targets.get(aTargetName); + let targetObject; + if (TOR_BROWSER_TARGETS_ALLOWED.has(aTargetName)) { + targetObject = this.targets.get(aTargetName); + } if (!targetObject) { log.warn("getTarget: The specified target name is not in the allowed set"); return Promise.reject("The specified target name is not in the allowed set"); @@ -1389,6 +1414,10 @@ var UITour = { },
showMenu(aWindow, aMenuName, aOpenCallback = null) { + if (!TOR_BROWSER_MENUS_ALLOWED.has(aMenuName)) { + return; + } + log.debug("showMenu:", aMenuName); function openMenuButton(aMenuBtn) { if (!aMenuBtn || !aMenuBtn.boxObject || aMenuBtn.open) { @@ -1485,6 +1514,10 @@ var UITour = { },
hideMenu(aWindow, aMenuName) { + if (!TOR_BROWSER_MENUS_ALLOWED.has(aMenuName)) { + return; + } + log.debug("hideMenu:", aMenuName); function closeMenuButton(aMenuBtn) { if (aMenuBtn && aMenuBtn.boxObject) @@ -1872,6 +1905,20 @@ function controlCenterTrackingToggleTarget(aUnblock) { }; }
+function torBrowserCircuitDisplayTarget(aElemID) { + return { + infoPanelPosition: "rightcenter topleft", + query(aDocument) { + let popup = aDocument.defaultView.gIdentityHandler._identityPopup; + if (popup.state != "open") { + return null; + } + let element = aDocument.getElementById(aElemID); + return UITour.isElementVisible(element) ? element : null; + }, + }; +} + this.UITour.init();
/** diff --git a/browser/components/uitour/content-UITour.js b/browser/components/uitour/content-UITour.js index 88d300c91419..8cd7be0c456b 100644 --- a/browser/components/uitour/content-UITour.js +++ b/browser/components/uitour/content-UITour.js @@ -28,7 +28,7 @@ var UITourListener = {
// This function is copied from UITour.jsm. isSafeScheme(aURI) { - let allowedSchemes = new Set(["about"]); + let allowedSchemes = new Set(["about", "https"]);
if (!allowedSchemes.has(aURI.scheme)) return false; diff --git a/browser/extensions/onboarding/bootstrap.js b/browser/extensions/onboarding/bootstrap.js index c553abe39759..4bc6d468dce9 100644 --- a/browser/extensions/onboarding/bootstrap.js +++ b/browser/extensions/onboarding/bootstrap.js @@ -87,6 +87,19 @@ function setPrefs(prefs) { }); }
+function openTorCircuitDisplayPage() { + let kFrameScript = "resource://onboarding/onboarding-tor-circuit-display.js"; + const kOnionURL = "https://3g2upl4pq6kufc4m.onion/"; // DuckDuckGo + let win = Services.wm.getMostRecentWindow('navigator:browser'); + if (win) { + let tabBrowser = win.gBrowser; + let tab = tabBrowser.addTab(kOnionURL); + tabBrowser.selectedTab = tab; + let b = tabBrowser.getBrowserForTab(tab); + b.messageManager.loadFrameScript(kFrameScript, true); + } +} + /** * syncTourChecker listens to and maintains the login status inside, and can be * queried at any time once initialized. @@ -160,6 +173,9 @@ function initContentMessageListener() { isLoggedIn: syncTourChecker.isLoggedIn() }); break; + case "tor-open-circuit-display-page": + openTorCircuitDisplayPage(); + break; #if 0 // No telemetry in Tor Browser. case "ping-centre": diff --git a/browser/extensions/onboarding/content/onboarding-tor-circuit-display.js b/browser/extensions/onboarding/content/onboarding-tor-circuit-display.js new file mode 100644 index 000000000000..de4b23c84c2a --- /dev/null +++ b/browser/extensions/onboarding/content/onboarding-tor-circuit-display.js @@ -0,0 +1,283 @@ +// Copyright (c) 2018, The Tor Project, Inc. +// vim: set sw=2 sts=2 ts=8 et syntax=javascript: + +let gStringBundle; + +let domLoadedListener = (aEvent) => { + let doc = aEvent.originalTarget; + if (doc.nodeName == "#document") { + removeEventListener("DOMContentLoaded", domLoadedListener); + beginCircuitDisplayOnboarding(); + } +}; + +addEventListener("DOMContentLoaded", domLoadedListener, false); + +function beginCircuitDisplayOnboarding() { + // 1 of 3: Show the introductory "How do circuits work?" info panel. + let target = "torBrowser-circuitDisplay"; + let title = getStringFromName("intro.title"); + let msg = getStringFromName("intro.msg"); + let button1Label = getStringFromName("one-of-three"); + let button2Label = getStringFromName("next"); + let buttons = []; + buttons.push({label: button1Label, style: "text"}); + buttons.push({label: button2Label, style: "primary", callback: function() { + showCircuitDiagram(); }}); + let options = {closeButtonCallback: function() { cleanUp(); }}; + Mozilla.UITour.showInfo(target, title, msg, undefined, buttons, options); +} + +function showCircuitDiagram() { + // 2 of 3: Open the control center and show the circuit diagram info panel. + Mozilla.UITour.showMenu("controlCenter", function() { + let target = "torBrowser-circuitDisplay-diagram"; + let title = getStringFromName("diagram.title"); + let msg = getStringFromName("diagram.msg"); + let button1Label = getStringFromName("two-of-three"); + let button2Label = getStringFromName("next"); + let buttons = []; + buttons.push({label: button1Label, style: "text"}); + buttons.push({label: button2Label, style: "primary", callback: function() { + showNewCircuitButton(); }}); + let options = {closeButtonCallback: function() { cleanUp(); }}; + Mozilla.UITour.showInfo(target, title, msg, undefined, buttons, options); + }); +} + +function showNewCircuitButton() { + // 3 of 3: Show the New Circuit button info panel. + let target = "torBrowser-circuitDisplay-newCircuitButton"; + let title = getStringFromName("new-circuit.title"); + let msg = getStringFromName("new-circuit.msg"); + let button1Label = getStringFromName("three-of-three"); + let button2Label = getStringFromName("done"); + let buttons = []; + buttons.push({label: button1Label, style: "text"}); + buttons.push({label: button2Label, style: "primary", callback: function() { + cleanUp(); }}); + let options = {closeButtonCallback: function() { cleanUp(); }}; + Mozilla.UITour.showInfo(target, title, msg, undefined, buttons, options); +} + +function cleanUp() { + Mozilla.UITour.hideMenu("controlCenter"); + Mozilla.UITour.closeTab(); +} + +function getStringFromName(aName) { + const TORBUTTON_BUNDLE_URI = "chrome://torbutton/locale/browserOnboarding.properties"; + const PREFIX = "onboarding.tor-circuit-display."; + + if (!gStringBundle) { + gStringBundle = Services.strings.createBundle(TORBUTTON_BUNDLE_URI) + } + + let result; + try { + result = gStringBundle.GetStringFromName(PREFIX + aName); + } catch (e) { + result = aName; + } + return result; +} + + +// The remainder of the code in this file was adapted from +// browser/components/uitour/UITour-lib.js (unfortunately, we cannot use that +// code here because it directly accesses 'document' and it assumes that the +// content window is the global JavaScript object), + +/* 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/. */ + +// create namespace +if (typeof Mozilla == "undefined") { + var Mozilla = {}; +} + +(function($) { + "use strict"; + + // create namespace + if (typeof Mozilla.UITour == "undefined") { + /** + * Library that exposes an event-based Web API for communicating with the + * desktop browser chrome. It can be used for tasks such as opening menu + * panels and highlighting the position of buttons in the toolbar. + * + * <p>For security/privacy reasons `Mozilla.UITour` will only work on a list of allowed + * secure origins. The list of allowed origins can be found in + * {@link https://dxr.mozilla.org/mozilla-central/source/browser/app/permissions%7C + * browser/app/permissions}.</p> + * + * @since 29 + * @namespace + */ + Mozilla.UITour = {}; + } + + function _sendEvent(action, data) { + var event = new content.CustomEvent("mozUITour", { + bubbles: true, + detail: { + action, + data: data || {} + } + }); + + content.document.dispatchEvent(event); + } + + function _generateCallbackID() { + return Math.random().toString(36).replace(/[^a-z]+/g, ""); + } + + function _waitForCallback(callback) { + var id = _generateCallbackID(); + + function listener(event) { + if (typeof event.detail != "object") + return; + if (event.detail.callbackID != id) + return; + + content.document.removeEventListener("mozUITourResponse", listener); + callback(event.detail.data); + } + content.document.addEventListener("mozUITourResponse", listener); + + return id; + } + + /** + * Show an arrow panel with optional images and buttons anchored at a specific UI target. + * + * @see Mozilla.UITour.hideInfo + * + * @param {Mozilla.UITour.Target} target - Identifier of the UI widget to anchor the panel at. + * @param {String} title - Title text to be shown as the heading of the panel. + * @param {String} text - Body text of the panel. + * @param {String} [icon=null] - URL of a 48x48px (96px @ 2dppx) image (which will be resolved + * relative to the tab's URI) to display in the panel. + * @param {Object[]} [buttons=[]] - Array of objects describing buttons. + * @param {String} buttons[].label - Button label + * @param {String} buttons[].icon - Button icon URL + * @param {String} buttons[].style - Button style ("primary" or "link") + * @param {Function} buttons[].callback - Called when the button is clicked + * @param {Object} [options={}] - Advanced options + * @param {Function} options.closeButtonCallback - Called when the panel's close button is clicked. + * + * @example + * var buttons = [ + * { + * label: 'Cancel', + * style: 'link', + * callback: cancelBtnCallback + * }, + * { + * label: 'Confirm', + * style: 'primary', + * callback: confirmBtnCallback + * } + * ]; + * + * var icon = '//mozorg.cdn.mozilla.net/media/img/firefox/australis/logo.png'; + * + * var options = { + * closeButtonCallback: closeBtnCallback + * }; + * + * Mozilla.UITour.showInfo('appMenu', 'my title', 'my text', icon, buttons, options); + */ + Mozilla.UITour.showInfo = function(target, title, text, icon, buttons, options) { + var buttonData = []; + if (Array.isArray(buttons)) { + for (var i = 0; i < buttons.length; i++) { + buttonData.push({ + label: buttons[i].label, + icon: buttons[i].icon, + style: buttons[i].style, + callbackID: _waitForCallback(buttons[i].callback) + }); + } + } + + var closeButtonCallbackID, targetCallbackID; + if (options && options.closeButtonCallback) + closeButtonCallbackID = _waitForCallback(options.closeButtonCallback); + if (options && options.targetCallback) + targetCallbackID = _waitForCallback(options.targetCallback); + + _sendEvent("showInfo", { + target, + title, + text, + icon, + buttons: buttonData, + closeButtonCallbackID, + targetCallbackID + }); + }; + + /** + * Hide any visible info panels. + * @see Mozilla.UITour.showInfo + */ + Mozilla.UITour.hideInfo = function() { + _sendEvent("hideInfo"); + }; + + /** + * Open the named application menu. + * + * @see Mozilla.UITour.hideMenu + * + * @param {Mozilla.UITour.MenuName} name - Menu name + * @param {Function} [callback] - Callback to be called with no arguments when + * the menu opens. + * + * @example + * Mozilla.UITour.showMenu('appMenu', function() { + * console.log('menu was opened'); + * }); + */ + Mozilla.UITour.showMenu = function(name, callback) { + var showCallbackID; + if (callback) + showCallbackID = _waitForCallback(callback); + + _sendEvent("showMenu", { + name, + showCallbackID, + }); + }; + + /** + * Close the named application menu. + * + * @see Mozilla.UITour.showMenu + * + * @param {Mozilla.UITour.MenuName} name - Menu name + */ + Mozilla.UITour.hideMenu = function(name) { + _sendEvent("hideMenu", { + name + }); + }; + + /** + * @summary Closes the tab where this code is running. As usual, if the tab is in the + * foreground, the tab that was displayed before is selected. + * + * @description The last tab in the current window will never be closed, in which case + * this call will have no effect. The calling code is expected to take an + * action after a small timeout in order to handle this case, for example by + * displaying a goodbye message or a button to restart the tour. + * @since 46 + */ + Mozilla.UITour.closeTab = function() { + _sendEvent("closeTab"); + }; +})(); diff --git a/browser/extensions/onboarding/content/onboarding-tour-agent.js b/browser/extensions/onboarding/content/onboarding-tour-agent.js index af93f7220730..b373c5e0ef01 100644 --- a/browser/extensions/onboarding/content/onboarding-tour-agent.js +++ b/browser/extensions/onboarding/content/onboarding-tour-agent.js @@ -18,9 +18,6 @@ let onCanSetDefaultBrowserInBackground = () => {
let onClick = evt => { switch (evt.target.id) { - case "onboarding-tour-tor-circuit-display-button": - // TODO: open circuit display onboarding - break; case "onboarding-tour-tor-security-button": Mozilla.UITour.torBrowserOpenSecuritySettings(); break; diff --git a/browser/extensions/onboarding/content/onboarding.js b/browser/extensions/onboarding/content/onboarding.js index 0cfc763e4f5e..de382ac34890 100644 --- a/browser/extensions/onboarding/content/onboarding.js +++ b/browser/extensions/onboarding/content/onboarding.js @@ -159,17 +159,14 @@ var onboardingTourset = { "circuit-display": { id: "onboarding-tour-tor-circuit-display", tourNameId: "onboarding.tour-tor-circuit-display", - instantComplete: true, getPage(win) { let div = win.document.createElement("div");
createOnboardingTourDescription(div, "onboarding.tour-tor-circuit-display.title", "onboarding.tour-tor-circuit-display.description"); createOnboardingTourContent(div, "resource://onboarding/img/figure_tor-circuit-display.png"); -/* TODO: Circuit display onboarding will be implemented in bug 26962. createOnboardingTourButton(div, "onboarding-tour-tor-circuit-display-button", "onboarding.tour-tor-circuit-display.button"); -*/
return div; }, @@ -916,6 +913,9 @@ class Onboarding { this.gotoPage("onboarding-tour-tor-circuit-display"); handledTourActionClick = true; break; + case "onboarding-tour-tor-circuit-display-button": + sendMessageToChrome("tor-open-circuit-display-page"); + break; } if (classList.contains("onboarding-tour-item")) { telemetry({ diff --git a/browser/extensions/onboarding/jar.mn b/browser/extensions/onboarding/jar.mn index f7fb13d033ce..8263aa14ebcd 100644 --- a/browser/extensions/onboarding/jar.mn +++ b/browser/extensions/onboarding/jar.mn @@ -10,6 +10,7 @@ content/img/ (content/img/*) * content/onboarding-tour-agent.js (content/onboarding-tour-agent.js) * content/onboarding.js (content/onboarding.js) + content/onboarding-tor-circuit-display.js (content/onboarding-tor-circuit-display.js) # Package UITour-lib.js in here rather than under # /browser/components/uitour to avoid "unreferenced files" error when # Onboarding extension is not built. diff --git a/browser/themes/shared/UITour.inc.css b/browser/themes/shared/UITour.inc.css index 1e4298afb82d..ca46be282b51 100644 --- a/browser/themes/shared/UITour.inc.css +++ b/browser/themes/shared/UITour.inc.css @@ -49,7 +49,8 @@ }
#UITourTooltipTitle { - font-size: 1.45rem; + color: #420C5D; + font-size: 16px; font-weight: bold; margin: 0; } @@ -57,7 +58,8 @@ #UITourTooltipDescription { margin-inline-start: 0; margin-inline-end: 0; - font-size: 1.15rem; + color: #4A4A4A; + font-size: 13px; line-height: 1.8rem; margin-bottom: 0; /* Override global.css */ } @@ -79,7 +81,6 @@ #UITourTooltipButtons { -moz-box-pack: end; background-color: hsla(210,4%,10%,.07); - border-top: 1px solid hsla(210,4%,10%,.14); margin: 10px -16px -16px; padding: 16px; } @@ -113,28 +114,20 @@ #UITourTooltipButtons > button:not(.button-link) { -moz-appearance: none; background-color: rgb(251,251,251); - border-radius: 3px; - border: 1px solid; - border-color: rgb(192,192,192); + border-radius: 2px; color: rgb(71,71,71); - padding: 4px 30px; + padding: 6px 30px; transition-property: background-color, border-color; transition-duration: 150ms; }
-#UITourTooltipButtons > button:not(.button-link):not(:active):hover { - background-color: hsla(210,4%,10%,.15); - border-color: hsla(210,4%,10%,.15); - box-shadow: 0 1px 0 0 hsla(210,4%,10%,.05) inset; -} - #UITourTooltipButtons > label, #UITourTooltipButtons > button.button-link { -moz-appearance: none; background: transparent; border: none; box-shadow: none; - color: rgba(0,0,0,0.35); + color: #4A4A4A; padding-left: 10px; padding-right: 10px; } @@ -143,14 +136,13 @@ color: black; }
-/* The primary button gets the same color as the customize button. */ #UITourTooltipButtons > button.button-primary { - background-color: rgb(116,191,67); + background-color: #420C5D; color: white; - padding-left: 30px; - padding-right: 30px; + padding-left: 28px; + padding-right: 28px; }
#UITourTooltipButtons > button.button-primary:not(:active):hover { - background-color: rgb(105,173,61); + background-color: #410A4E; }