richard pushed to branch tor-browser-115.10.0esr-13.5-1 at The Tor Project / Applications / Tor Browser
Commits: e992fb9d by Henry Wilkes at 2024-04-17T20:39:07+00:00 Add purple tor version of the loading APNG.
- - - - - 3cd48ad5 by Henry Wilkes at 2024-04-17T20:39:07+00:00 fixup! Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection
Bug 40843: Add a "testing" state to the internet status.
Bug 41383: Improved accessibility of the network status areas by managing focus and making the status an aria-live area so that changes are announced without requiring focus.
Also re-arranged the markup to ensure the "Connection" overview and network status areas are not part of search results.
Also moved the "Tor network" status onto a new line:
1. This improved navigation with a screen reader. 2. Ensures that the "Tor network" status does not jump horizontally during a test. 3. Ensures that each status area does not take up too much horizontal space so that the minimum page width can reduce. E.g. the "Tor network" area regularly took up more than 50% of the available width, and these may be wider depending on the locale.
- - - - - d1c78659 by Henry Wilkes at 2024-04-17T20:39:07+00:00 fixup! Tor Browser strings
Bug 42457: Add a "testing" state to the internet status area.
- - - - -
10 changed files:
- browser/components/torpreferences/content/connectionPane.js - browser/components/torpreferences/content/connectionPane.xhtml - browser/components/torpreferences/content/torPreferences.css - browser/locales/en-US/browser/tor-browser.ftl - toolkit/themes/shared/desktop-jar.inc.mn - + toolkit/themes/shared/icons/tor-dark-loading.png - + toolkit/themes/shared/icons/tor-dark-loading@2x.png - + toolkit/themes/shared/icons/tor-light-loading.png - + toolkit/themes/shared/icons/tor-light-loading@2x.png - + tools/torbrowser/generate_tor_loading_png.py
Changes:
===================================== browser/components/torpreferences/content/connectionPane.js ===================================== @@ -5,7 +5,8 @@
"use strict";
-/* global Services, gSubDialog */ +/* import-globals-from /browser/components/preferences/preferences.js */ +/* import-globals-from /browser/components/preferences/search.js */
const { setTimeout, clearTimeout } = ChromeUtils.import( "resource://gre/modules/Timer.jsm" @@ -90,12 +91,6 @@ const Lox = { }; */
-const InternetStatus = Object.freeze({ - Unknown: 0, - Online: 1, - Offline: -1, -}); - /** * Make changes to TorSettings and save them. * @@ -2283,6 +2278,168 @@ const gBridgeSettings = { }, };
+/** + * Area to show the internet and tor network connection status. + */ +const gNetworkStatus = { + /** + * Initialize the area. + */ + init() { + this._internetAreaEl = document.getElementById( + "network-status-internet-area" + ); + this._internetResultEl = this._internetAreaEl.querySelector( + ".network-status-result" + ); + this._internetTestButton = document.getElementById( + "network-status-internet-test-button" + ); + this._internetTestButton.addEventListener("click", () => { + this._startInternetTest(); + }); + + this._torAreaEl = document.getElementById("network-status-tor-area"); + this._torResultEl = this._torAreaEl.querySelector(".network-status-result"); + this._torConnectButton = document.getElementById( + "network-status-tor-connect-button" + ); + this._torConnectButton.addEventListener("click", () => { + TorConnect.openTorConnect({ beginBootstrap: true }); + }); + + this._updateInternetStatus("unknown"); + this._updateTorConnectionStatus(); + + Services.obs.addObserver(this, TorConnectTopics.StateChange); + }, + + /** + * Un-initialize the area. + */ + deinint() { + Services.obs.removeObserver(this, TorConnectTopics.StateChange); + }, + + observe(subject, topic, data) { + switch (topic) { + // triggered when tor connect state changes and we may + // need to update the messagebox + case TorConnectTopics.StateChange: { + this._updateTorConnectionStatus(); + break; + } + } + }, + + /** + * Whether the test should be disabled. + * + * @type {boolean} + */ + _internetTestDisabled: false, + /** + * Start the internet test. + */ + async _startInternetTest() { + if (this._internetTestDisabled) { + return; + } + this._internetTestDisabled = true; + // We use "aria-disabled" rather than the "disabled" attribute so that the + // button can remain focusable during the test. + this._internetTestButton.setAttribute("aria-disabled", "true"); + this._internetTestButton.classList.add("spoof-button-disabled"); + try { + this._updateInternetStatus("testing"); + const mrpc = new MoatRPC(); + let status = null; + try { + await mrpc.init(); + status = await mrpc.testInternetConnection(); + } catch (err) { + console.log("Error while checking the Internet connection", err); + } finally { + mrpc.uninit(); + } + if (status) { + this._updateInternetStatus(status.successful ? "online" : "offline"); + } else { + this._updateInternetStatus("unknown"); + } + } finally { + this._internetTestButton.removeAttribute("aria-disabled"); + this._internetTestButton.classList.remove("spoof-button-disabled"); + this._internetTestDisabled = false; + } + }, + + /** + * Update the shown internet status. + * + * @param {string} stateName - The name of the state to show. + */ + _updateInternetStatus(stateName) { + let l10nId; + switch (stateName) { + case "testing": + l10nId = "tor-connection-internet-status-testing"; + break; + case "offline": + l10nId = "tor-connection-internet-status-offline"; + break; + case "online": + l10nId = "tor-connection-internet-status-online"; + break; + } + if (l10nId) { + this._internetResultEl.setAttribute("data-l10n-id", l10nId); + } else { + this._internetResultEl.removeAttribute("data-l10n-id"); + this._internetResultEl.textContent = ""; + } + + this._internetAreaEl.classList.toggle( + "status-loading", + stateName === "testing" + ); + this._internetAreaEl.classList.toggle( + "status-offline", + stateName === "offline" + ); + }, + + /** + * Update the shown Tor connection status. + */ + _updateTorConnectionStatus() { + const buttonHadFocus = this._torConnectButton.contains( + document.activeElement + ); + const isBootstrapped = TorConnect.state === TorConnectState.Bootstrapped; + const isBlocked = !isBootstrapped && TorConnect.potentiallyBlocked; + let l10nId; + if (isBootstrapped) { + l10nId = "tor-connection-network-status-connected"; + } else if (isBlocked) { + l10nId = "tor-connection-network-status-blocked"; + } else { + l10nId = "tor-connection-network-status-not-connected"; + } + + document.l10n.setAttributes(this._torResultEl, l10nId); + this._torAreaEl.classList.toggle("status-connected", isBootstrapped); + this._torAreaEl.classList.toggle("status-blocked", isBlocked); + if (isBootstrapped && buttonHadFocus) { + // Button has become hidden and will loose focus. Most likely this has + // happened because the user clicked the button to open about:torconnect. + // Since this is near the top of the page, we move focus to the search + // input (for when the user returns). + gSearchResultsPane.searchInput.focus(); + } + }, +}; + /* Connection Pane
@@ -2304,8 +2461,6 @@ const gConnectionPane = (function () { // cached frequently accessed DOM elements _enableQuickstartCheckbox: null,
- _internetStatus: InternetStatus.Unknown, - // populate xul with strings and cache the relevant elements _populateXUL() { // saves tor settings to disk when navigate away from about:preferences @@ -2321,80 +2476,6 @@ const gConnectionPane = (function () { } });
- // Internet and Tor status - const internetStatus = document.getElementById( - "torPreferences-status-internet" - ); - const internetResult = internetStatus.querySelector( - ".torPreferences-status-result" - ); - const internetTest = document.getElementById( - "torPreferences-status-internet-test" - ); - internetTest.addEventListener("click", () => { - this.onInternetTest(); - }); - - const torConnectStatus = document.getElementById( - "torPreferences-status-tor-connect" - ); - const torConnectResult = torConnectStatus.querySelector( - ".torPreferences-status-result" - ); - const torConnectButton = document.getElementById( - "torPreferences-status-tor-connect-button" - ); - torConnectButton.addEventListener("click", () => { - TorConnect.openTorConnect({ beginBootstrap: true }); - }); - - this._populateStatus = () => { - let internetId; - switch (this._internetStatus) { - case InternetStatus.Online: - internetStatus.classList.remove("offline"); - internetId = "tor-connection-internet-status-online"; - break; - case InternetStatus.Offline: - internetStatus.classList.add("offline"); - internetId = "tor-connection-internet-status-offline"; - break; - case InternetStatus.Unknown: - default: - internetStatus.classList.remove("offline"); - break; - } - if (internetId) { - document.l10n.setAttributes(internetResult, internetId); - internetResult.hidden = false; - } else { - internetResult.hidden = true; - } - - let connectId; - // FIXME: What about the TorConnectState.Disabled state? - if (TorConnect.state === TorConnectState.Bootstrapped) { - torConnectStatus.classList.add("connected"); - torConnectStatus.classList.remove("blocked"); - connectId = "tor-connection-network-status-connected"; - // NOTE: If the button is focused when we hide it, the focus may be - // lost. But we don't have an obvious place to put the focus instead. - torConnectButton.hidden = true; - } else { - torConnectStatus.classList.remove("connected"); - torConnectStatus.classList.toggle( - "blocked", - TorConnect.potentiallyBlocked - ); - connectId = TorConnect.potentiallyBlocked - ? "tor-connection-network-status-blocked" - : "tor-connection-network-status-not-connected"; - torConnectButton.hidden = false; - } - document.l10n.setAttributes(torConnectResult, connectId); - }; - this._populateStatus(); - // Quickstart this._enableQuickstartCheckbox = document.getElementById( "torPreferences-quickstart-toggle" @@ -2514,6 +2595,7 @@ const gConnectionPane = (function () {
init() { gBridgeSettings.init(); + gNetworkStatus.init();
TorSettings.initializedPromise.then(() => this._populateXUL());
@@ -2526,6 +2608,7 @@ const gConnectionPane = (function () {
uninit() { gBridgeSettings.uninit(); + gNetworkStatus.uninit();
// unregister our observer topics Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged); @@ -2554,36 +2637,12 @@ const gConnectionPane = (function () { // triggered when tor connect state changes and we may // need to update the messagebox case TorConnectTopics.StateChange: { - this.onStateChange(); + this._showAutoconfiguration(); break; } } },
- async onInternetTest() { - const mrpc = new MoatRPC(); - let status = null; - try { - await mrpc.init(); - status = await mrpc.testInternetConnection(); - } catch (err) { - console.log("Error while checking the Internet connection", err); - } finally { - mrpc.uninit(); - } - if (status) { - this._internetStatus = status.successful - ? InternetStatus.Online - : InternetStatus.Offline; - this._populateStatus(); - } - }, - - onStateChange() { - this._populateStatus(); - this._showAutoconfiguration(); - }, - onAdvancedSettings() { gSubDialog.open( "chrome://browser/content/torpreferences/connectionSettingsDialog.xhtml",
===================================== browser/components/torpreferences/content/connectionPane.xhtml ===================================== @@ -5,16 +5,13 @@ src="chrome://browser/content/torpreferences/connectionPane.js" /> <html:template id="template-paneConnection"> - <hbox + <vbox id="torPreferencesCategory" class="subcategory" data-category="paneConnection" hidden="true"
<html:h1 data-l10n-id="tor-connection-settings-heading"></html:h1> - </hbox> - - <groupbox data-category="paneConnection" hidden="true"> <description flex="1"> <html:span data-l10n-id="tor-connection-overview" @@ -28,46 +25,61 @@ data-l10n-id="tor-connection-browser-learn-more-link" /> </description> - </groupbox> - - <groupbox - id="torPreferences-status-group" - data-category="paneConnection" - hidden="true" - > - <hbox id="torPreferences-status-box"> - <hbox - id="torPreferences-status-internet" - class="torPreferences-status-grouping" + <!-- Keep within #torPreferencesCategory so this won't appear in search + - results. --> + <html:div + id="network-status-internet-area" + class="network-status-area" + role="group" + aria-labelledby="network-status-internet-area-label" + > + <html:img alt="" class="network-status-icon" /> + <!-- Use an aria-live area to announce "Internet: Online" or + - "Internet: Offline". We only expect this to change when triggered by + - the user clicking the "Test" button, so shouldn't occur unexpectedly. + --> + <html:div + class="network-status-live-area" + aria-live="polite" + aria-atomic="true" > - <image class="torPreferences-status-icon" /> <html:span - class="torPreferences-status-name" + id="network-status-internet-area-label" + class="network-status-label" data-l10n-id="tor-connection-internet-status-label" ></html:span> - <html:span class="torPreferences-status-result"></html:span> - <html:button - id="torPreferences-status-internet-test" - data-l10n-id="tor-connection-internet-status-test-button" - ></html:button> - </hbox> - <hbox - id="torPreferences-status-tor-connect" - class="torPreferences-status-grouping" - > - <image class="torPreferences-status-icon" /> - <html:span - class="torPreferences-status-name" - data-l10n-id="tor-connection-network-status-label" - ></html:span> - <html:span class="torPreferences-status-result"></html:span> - <html:button - id="torPreferences-status-tor-connect-button" - data-l10n-id="tor-connection-network-status-connect-button" - ></html:button> - </hbox> - </hbox> - </groupbox> + <img alt="" class="network-status-loading-icon" /> + <html:span class="network-status-result"></html:span> + </html:div> + <html:button + id="network-status-internet-test-button" + data-l10n-id="tor-connection-internet-status-test-button" + ></html:button> + </html:div> + <html:div + id="network-status-tor-area" + class="network-status-area" + role="group" + aria-labelledby="network-status-tor-area-label" + > + <html:img alt="" class="network-status-icon" /> + <!-- NOTE: Unlike #network-status-internet-area, we do not wrap the label + - and status ("Tor network: Not connected", etc) in an aria-live area. + - This is not likely to change whilst this page has focus. + - Moreover, the status is already present in the torconnect status bar + - in the window tab bar. --> + <html:span + id="network-status-tor-area-label" + class="network-status-label" + data-l10n-id="tor-connection-network-status-label" + ></html:span> + <html:span class="network-status-result"></html:span> + <html:button + id="network-status-tor-connect-button" + data-l10n-id="tor-connection-network-status-connect-button" + ></html:button> + </html:div> + </vbox>
<!-- Quickstart --> <groupbox data-category="paneConnection" hidden="true">
===================================== browser/components/torpreferences/content/torPreferences.css ===================================== @@ -5,25 +5,36 @@ list-style-image: url("chrome://global/content/torconnect/tor-connect.svg"); }
+/* Make a button appear disabled, whilst still allowing it to keep keyboard + * focus. */ +button.spoof-button-disabled { + /* Borrow the :disabled rule from common-shared.css */ + opacity: 0.4; + /* Also ensure it does not get hover or active styling. */ + pointer-events: none; +} + /* Status */
-#torPreferences-status-box { - display: flex; - align-items: center; - gap: 32px; +#network-status-internet-area { + margin-block: 16px; +} + +#network-status-tor-area { + margin-block: 0 32px; }
-.torPreferences-status-grouping { +.network-status-area { display: flex; align-items: center; white-space: nowrap; }
-.torPreferences-status-grouping > * { +.network-status-area > * { flex: 0 0 auto; }
-.torPreferences-status-icon { +.network-status-icon { width: 18px; height: 18px; margin-inline-end: 8px; @@ -32,42 +43,74 @@ stroke: currentColor; }
-#torPreferences-status-internet .torPreferences-status-icon { - list-style-image: url("chrome://browser/content/torpreferences/network.svg"); +#network-status-internet-area .network-status-icon { + content: url("chrome://browser/content/torpreferences/network.svg"); }
-#torPreferences-status-tor-connect .torPreferences-status-icon { - list-style-image: url("chrome://global/content/torconnect/tor-connect-broken.svg"); +#network-status-internet-area.status-offline .network-status-icon { + content: url("chrome://browser/content/torpreferences/network-broken.svg"); }
-.torPreferences-status-name { - font-weight: bold; - margin-inline-end: 0.75em; +#network-status-tor-area .network-status-icon { + content: url("chrome://global/content/torconnect/tor-connect.svg"); }
-.torPreferences-status-result { - margin-inline-end: 8px; +#network-status-tor-area:not(.status-connected) .network-status-icon { + content: url("chrome://global/content/torconnect/tor-connect-broken.svg"); }
-#torPreferences-status-internet.offline .torPreferences-status-icon { - list-style-image: url("chrome://browser/content/torpreferences/network-broken.svg"); +#network-status-tor-area.status-blocked .network-status-icon { + /* Same as .tor-connect-status-potentially-blocked. */ + stroke: #c50042; }
-#torPreferences-status-tor-connect.connected .torPreferences-status-icon { - list-style-image: url("chrome://global/content/torconnect/tor-connect.svg"); +@media (prefers-color-scheme: dark) { + #network-status-tor-area.status-blocked .network-status-icon { + stroke: #ff9aa2; + } }
-#torPreferences-status-tor-connect.blocked .torPreferences-status-icon { - /* Same as .tor-connect-status-potentially-blocked. */ - stroke: #c50042; +.network-status-live-area { + display: contents; +} + +.network-status-label { + font-weight: bold; + margin-inline-end: 0.75em; +} + +.network-status-loading-icon { + margin-inline-end: 0.5em; + width: 16px; + height: 16px; + content: image-set( + url("chrome://global/skin/icons/tor-light-loading.png"), + url("chrome://global/skin/icons/tor-light-loading@2x.png") 2x + ); }
@media (prefers-color-scheme: dark) { - #torPreferences-status-tor-connect.blocked .torPreferences-status-icon { - stroke: #ff9aa2; + .network-status-loading-icon { + content: image-set( + url("chrome://global/skin/icons/tor-dark-loading.png"), + url("chrome://global/skin/icons/tor-dark-loading@2x.png") 2x + ); } }
+#network-status-internet-area:not(.status-loading) .network-status-loading-icon { + display: none; +} + +.network-status-result:not(:empty) { + margin-inline-end: 0.75em; +} + +#network-status-tor-area.status-connected #network-status-tor-connect-button { + /* Hide button when already connected. */ + display: none; +} + /* Bridge settings */
.tor-focusable-heading:focus-visible {
===================================== browser/locales/en-US/browser/tor-browser.ftl ===================================== @@ -65,6 +65,9 @@ tor-connection-internet-status-label = Internet: # Here "Test" is a verb, as in "test the internet connection". # Uses sentence case in English (US). tor-connection-internet-status-test-button = Test +# Shown when testing the internet status. +# Uses sentence case in English (US). +tor-connection-internet-status-testing = Testing… # Shown when the user is connected to the internet. # Uses sentence case in English (US). tor-connection-internet-status-online = Online
===================================== toolkit/themes/shared/desktop-jar.inc.mn ===================================== @@ -86,6 +86,10 @@ skin/classic/global/icons/link.svg (../../shared/icons/link.svg) skin/classic/global/icons/loading.png (../../shared/icons/loading.png) skin/classic/global/icons/loading@2x.png (../../shared/icons/loading@2x.png) + skin/classic/global/icons/tor-light-loading.png (../../shared/icons/tor-light-loading.png) + skin/classic/global/icons/tor-light-loading@2x.png (../../shared/icons/tor-light-loading@2x.png) + skin/classic/global/icons/tor-dark-loading.png (../../shared/icons/tor-dark-loading.png) + skin/classic/global/icons/tor-dark-loading@2x.png (../../shared/icons/tor-dark-loading@2x.png) skin/classic/global/icons/more.svg (../../shared/icons/more.svg) skin/classic/global/icons/open-in-new.svg (../../shared/icons/open-in-new.svg) skin/classic/global/icons/page-portrait.svg (../../shared/icons/page-portrait.svg)
===================================== toolkit/themes/shared/icons/tor-dark-loading.png ===================================== Binary files /dev/null and b/toolkit/themes/shared/icons/tor-dark-loading.png differ
===================================== toolkit/themes/shared/icons/tor-dark-loading@2x.png ===================================== Binary files /dev/null and b/toolkit/themes/shared/icons/tor-dark-loading@2x.png differ
===================================== toolkit/themes/shared/icons/tor-light-loading.png ===================================== Binary files /dev/null and b/toolkit/themes/shared/icons/tor-light-loading.png differ
===================================== toolkit/themes/shared/icons/tor-light-loading@2x.png ===================================== Binary files /dev/null and b/toolkit/themes/shared/icons/tor-light-loading@2x.png differ
===================================== tools/torbrowser/generate_tor_loading_png.py ===================================== @@ -0,0 +1,74 @@ +""" +Script to convert the loading.png and loading@2x.png blue spinners to purple +spinners for Tor Browser, for both the light and dark themes. +""" + +import argparse +import colorsys +import os + +from PIL import ExifTags, Image, ImageFilter + +parser = argparse.ArgumentParser(description="Convert the loading APNG to be purple.") +parser.add_argument("loading_png", help="The loading png to convert") +parser.add_argument( + "--light", required=True, help="The name of the light-theme purple output image" +) +parser.add_argument( + "--dark", required=True, help="The name of the dark-theme purple output image" +) + +parsed_args = parser.parse_args() + +orig_im = Image.open(parsed_args.loading_png) + + +def filter_to_light_theme(r, g, b): + h, s, v = colorsys.rgb_to_hsv(r, g, b) + # Convert from HSV 0.58, 1.0, 255 (start of the circle) + # to --purple-60 #8000d7 HSV 0.766, 1.0, 215 + h = 0.766 + v = v * 215 / 255 + return colorsys.hsv_to_rgb(h, s, v) + + +def filter_to_dark_theme(r, g, b): + h, s, v = colorsys.rgb_to_hsv(r, g, b) + # Convert from HSV 0.58, 1.0, 255 (start of the circle) + # to --purple-30 #c069ff HSV 0.766, 0.59, 255 + h = 0.766 + s = s * 0.59 / 1.0 + return colorsys.hsv_to_rgb(h, s, v) + + +filt_light = ImageFilter.Color3DLUT.generate(65, filter_to_light_theme) +filt_dark = ImageFilter.Color3DLUT.generate(65, filter_to_dark_theme) + +transformed_light = [] +transformed_dark = [] +duration = orig_im.info["duration"] + +# Transform each APNG frame individually. +for frame in range(orig_im.n_frames): + orig_im.seek(frame) + transformed_light.append(orig_im.filter(filt_light)) + transformed_dark.append(orig_im.filter(filt_dark)) + +exif = Image.Exif() +exif[ExifTags.Base.ImageDescription] = f"Generated by {os.path.basename(__file__)}" + +transformed_light[0].save( + parsed_args.light, + save_all=True, + append_images=transformed_light[1:], + duration=duration, + exif=exif, +) + +transformed_dark[0].save( + parsed_args.dark, + save_all=True, + append_images=transformed_dark[1:], + duration=duration, + exif=exif, +)
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/3cabcf7...