richard pushed to branch tor-browser-115.7.0esr-13.5-1 at The Tor Project / Applications / Tor Browser
Commits: d66eecaa by Henry Wilkes at 2024-01-31T09:28:02+00:00 fixup! Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection
Bug 42385: Add lox invite dialog.
- - - - - 48110609 by Henry Wilkes at 2024-01-31T09:28:03+00:00 fixup! Tor Browser strings
Bug 42385: Add lox invite dialog strings.
- - - - -
6 changed files:
- browser/components/torpreferences/content/connectionPane.js - + browser/components/torpreferences/content/loxInviteDialog.js - + browser/components/torpreferences/content/loxInviteDialog.xhtml - browser/components/torpreferences/content/torPreferences.css - browser/components/torpreferences/jar.mn - browser/locales/en-US/browser/tor-browser.ftl
Changes:
===================================== browser/components/torpreferences/content/connectionPane.js ===================================== @@ -1321,7 +1321,17 @@ const gLoxStatus = { );
this._invitesButton.addEventListener("click", () => { - // TODO: Show invites. + gSubDialog.open( + "chrome://browser/content/torpreferences/loxInviteDialog.xhtml", + { + features: "resizable=yes", + closedCallback: () => { + // TODO: Listen for events from Lox, rather than call _updateInvites + // directly. + this._updateInvites(); + }, + } + ); }); this._unlockAlertButton.addEventListener("click", () => { // TODO: Have a way to ensure that the cleared event data matches the
===================================== browser/components/torpreferences/content/loxInviteDialog.js ===================================== @@ -0,0 +1,347 @@ +"use strict"; + +const { TorSettings, TorSettingsTopics, TorBridgeSource } = + ChromeUtils.importESModule("resource://gre/modules/TorSettings.sys.mjs"); + +const { Lox, LoxErrors } = ChromeUtils.importESModule( + "resource://gre/modules/Lox.sys.mjs" +); + +/** + * Fake Lox module + +const LoxErrors = { + LoxServerUnreachable: "LoxServerUnreachable", + Other: "Other", +}; + +const Lox = { + remainingInvites: 5, + getRemainingInviteCount() { + return this.remainingInvites; + }, + invites: [ + '{"invite": [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22]}', + '{"invite": [9,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22]}', + ], + getInvites() { + return this.invites; + }, + failError: null, + generateInvite() { + return new Promise((res, rej) => { + setTimeout(() => { + if (this.failError) { + rej({ type: this.failError }); + return; + } + if (!this.remainingInvites) { + rej({ type: LoxErrors.Other }); + return; + } + const invite = JSON.stringify({ + invite: Array.from({ length: 100 }, () => + Math.floor(Math.random() * 265) + ), + }); + this.invites.push(invite); + this.remainingInvites--; + res(invite); + }, 4000); + }); + }, +}; +*/ + +const gLoxInvites = { + /** + * Initialize the dialog. + */ + init() { + this._dialog = document.getElementById("lox-invite-dialog"); + this._remainingInvitesEl = document.getElementById( + "lox-invite-dialog-remaining" + ); + this._generateButton = document.getElementById( + "lox-invite-dialog-generate-button" + ); + this._connectingEl = document.getElementById( + "lox-invite-dialog-connecting" + ); + this._errorEl = document.getElementById("lox-invite-dialog-error-message"); + this._inviteListEl = document.getElementById("lox-invite-dialog-list"); + + this._generateButton.addEventListener("click", () => { + this._generateNewInvite(); + }); + + const menu = document.getElementById("lox-invite-dialog-item-menu"); + this._inviteListEl.addEventListener("contextmenu", event => { + if (!this._inviteListEl.selectedItem) { + return; + } + menu.openPopupAtScreen(event.screenX, event.screenY, true); + }); + menu.addEventListener("popuphidden", () => { + menu.setAttribute("aria-hidden", "true"); + }); + menu.addEventListener("popupshowing", () => { + menu.removeAttribute("aria-hidden"); + }); + document + .getElementById("lox-invite-dialog-copy-menu-item") + .addEventListener("command", () => { + const selected = this._inviteListEl.selectedItem; + if (!selected) { + return; + } + const clipboard = Cc[ + "@mozilla.org/widget/clipboardhelper;1" + ].getService(Ci.nsIClipboardHelper); + clipboard.copyString(selected.textContent); + }); + + // NOTE: TorSettings should already be initialized when this dialog is + // opened. + Services.obs.addObserver(this, TorSettingsTopics.SettingsChanged); + // TODO: Listen for new invites from Lox, when supported. + + // Set initial _loxId value. Can close this dialog. + this._updateLoxId(); + + this._updateRemainingInvites(); + this._updateExistingInvites(); + }, + + /** + * Un-initialize the dialog. + */ + uninit() { + Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged); + }, + + observe(subject, topic, data) { + switch (topic) { + case TorSettingsTopics.SettingsChanged: + const { changes } = subject.wrappedJSObject; + if ( + changes.includes("bridges.source") || + changes.includes("bridges.lox_id") + ) { + this._updateLoxId(); + } + break; + } + }, + + /** + * The loxId this dialog is shown for. null if uninitailized. + * + * @type {string?} + */ + _loxId: null, + /** + * Update the _loxId value. Will close the dialog if it changes after + * initialization. + */ + _updateLoxId() { + const loxId = + TorSettings.bridges.source === TorBridgeSource.Lox + ? TorSettings.bridges.lox_id + : ""; + if (!loxId || (this._loxId !== null && loxId !== this._loxId)) { + // No lox id, or it changed. Close this dialog. + this._dialog.cancelDialog(); + } + this._loxId = loxId; + }, + + /** + * The invites that are already shown. + * + * @type {Set<string>} + */ + _shownInvites: new Set(), + + /** + * Add a new invite at the start of the list. + * + * @param {string} invite - The invite to add. + */ + _addInvite(invite) { + if (this._shownInvites.has(invite)) { + return; + } + const newInvite = document.createXULElement("richlistitem"); + newInvite.classList.add("lox-invite-dialog-list-item"); + newInvite.textContent = invite; + + this._inviteListEl.prepend(newInvite); + this._shownInvites.add(invite); + }, + + /** + * Update the display of the existing invites. + */ + _updateExistingInvites() { + // Add new invites. + + // NOTE: we only expect invites to be appended, so we won't re-order any. + // NOTE: invites are ordered with the oldest first. + for (const invite of Lox.getInvites()) { + this._addInvite(invite); + } + }, + + /** + * The shown number or remaining invites we have. + * + * @type {integer} + */ + _remainingInvites: 0, + + /** + * Update the display of the remaining invites. + */ + _updateRemainingInvites() { + this._remainingInvites = Lox.getRemainingInviteCount(); + + document.l10n.setAttributes( + this._remainingInvitesEl, + "tor-bridges-lox-remaining-invites", + { numInvites: this._remainingInvites } + ); + this._updateGenerateButtonState(); + }, + + /** + * Whether we are currently generating an invite. + * + * @type {boolean} + */ + _generating: false, + /** + * Set whether we are generating an invite. + * + * @param {boolean} isGenerating - Whether we are generating. + */ + _setGenerating(isGenerating) { + this._generating = isGenerating; + this._updateGenerateButtonState(); + this._connectingEl.classList.toggle("show-connecting", isGenerating); + }, + + /** + * Update the state of the generate button. + */ + _updateGenerateButtonState() { + this._generateButton.disabled = this._generating || !this._remainingInvites; + }, + + /** + * Start generating a new invite. + */ + _generateNewInvite() { + if (this._generating) { + console.error("Already generating an invite"); + return; + } + this._setGenerating(true); + // Clear the previous error. + this._updateGenerateError(null); + // Move focus from the button to the connecting element, since button is + // now disabled. + this._connectingEl.focus(); + + let lostFocus = false; + Lox.generateInvite() + .finally(() => { + // Fetch whether the connecting label still has focus before we hide it. + lostFocus = this._connectingEl.contains(document.activeElement); + this._setGenerating(false); + }) + .then( + invite => { + this._addInvite(invite); + + if (!this._inviteListEl.contains(document.activeElement)) { + // Does not have focus, change the selected item to be the new + // invite (at index 0). + this._inviteListEl.selectedIndex = 0; + } + + if (lostFocus) { + // Move focus to the new invite before we hide the "Connecting" + // message. + this._inviteListEl.focus(); + } + + // TODO: When Lox sends out notifications, let the observer handle the + // change rather than calling _updateRemainingInvites directly. + this._updateRemainingInvites(); + }, + loxError => { + console.error("Failed to generate an invite", loxError); + switch (loxError.type) { + case LoxErrors.LoxServerUnreachable: + this._updateGenerateError("no-server"); + break; + default: + this._updateGenerateError("generic"); + break; + } + + if (lostFocus) { + // Move focus back to the button before we hide the "Connecting" + // message. + this._generateButton.focus(); + } + } + ); + }, + + /** + * Update the shown generation error. + * + * @param {string?} type - The error type, or null if no error should be + * shown. + */ + _updateGenerateError(type) { + // First clear the existing error. + this._errorEl.removeAttribute("data-l10n-id"); + this._errorEl.textContent = ""; + this._errorEl.classList.toggle("show-error", !!type); + + if (!type) { + return; + } + + let errorId; + switch (type) { + case "no-server": + errorId = "lox-invite-dialog-no-server-error"; + break; + case "generic": + // Generic error. + errorId = "lox-invite-dialog-generic-invite-error"; + break; + } + + document.l10n.setAttributes(this._errorEl, errorId); + }, +}; + +window.addEventListener( + "DOMContentLoaded", + () => { + gLoxInvites.init(); + window.addEventListener( + "unload", + () => { + gLoxInvites.uninit(); + }, + { once: true } + ); + }, + { once: true } +);
===================================== browser/components/torpreferences/content/loxInviteDialog.xhtml ===================================== @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?> +<?xml-stylesheet href="chrome://browser/content/torpreferences/torPreferences.css"?> + +<window + type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="lox-invite-dialog-title" +> + <!-- Context menu, aria-hidden whilst not shown so it does not appear in the + - document content. --> + <menupopup id="lox-invite-dialog-item-menu" aria-hidden="true"> + <menuitem + id="lox-invite-dialog-copy-menu-item" + data-l10n-id="lox-invite-dialog-menu-item-copy-invite" + /> + </menupopup> + <dialog id="lox-invite-dialog" buttons="accept"> + <linkset> + <html:link rel="localization" href="browser/tor-browser.ftl" /> + </linkset> + + <script src="chrome://browser/content/torpreferences/loxInviteDialog.js" /> + + <description data-l10n-id="lox-invite-dialog-description"></description> + <html:div id="lox-invite-dialog-generate-area"> + <html:span id="lox-invite-dialog-remaining"></html:span> + <html:button + id="lox-invite-dialog-generate-button" + data-l10n-id="lox-invite-dialog-request-button" + ></html:button> + <html:div id="lox-invite-dialog-message-area"> + <html:span + id="lox-invite-dialog-error-message" + role="alert" + ></html:span> + <html:span + id="lox-invite-dialog-connecting" + role="alert" + tabindex="0" + data-l10n-id="lox-invite-dialog-connecting" + ></html:span> + </html:div> + </html:div> + <html:div + id="lox-invite-dialog-list-label" + data-l10n-id="lox-invite-dialog-invites-label" + ></html:div> + <richlistbox + id="lox-invite-dialog-list" + aria-labelledby="lox-invite-dialog-list-label" + ></richlistbox> + </dialog> +</window>
===================================== browser/components/torpreferences/content/torPreferences.css ===================================== @@ -820,6 +820,75 @@ dialog#torPreferences-requestBridge-dialog > hbox { background: var(--qr-one); }
+/* Lox invite dialog */ + +#lox-invite-dialog-generate-area { + flex: 0 0 auto; + display: grid; + grid-template: + ". remaining button" min-content + "message message message" auto + / 1fr max-content max-content; + gap: 8px; + margin-block: 16px 8px; + align-items: center; +} + +#lox-invite-dialog-remaining { + grid-area: remaining; +} + +#lox-invite-dialog-generate-button { + grid-area: button; +} + +#lox-invite-dialog-message-area { + grid-area: message; + justify-self: end; +} + +#lox-invite-dialog-message-area::after { + /* Zero width space, to ensure we are always one line high. */ + content: "\200B"; +} + +#lox-invite-dialog-error-message { + color: var(--in-content-error-text-color); +} + +#lox-invite-dialog-error-message:not(.show-error) { + display: none; +} + +#lox-invite-dialog-connecting { + color: var(--text-color-deemphasized); + /* TODO: Add spinner ::before */ +} + +#lox-invite-dialog-connecting:not(.show-connecting) { + display: none; +} + +#lox-invite-dialog-list-label { + font-weight: 700; +} + +#lox-invite-dialog-list { + flex: 1 1 auto; + /* basis height */ + height: 10em; + margin-block: 8px; +} + +.lox-invite-dialog-list-item { + white-space: nowrap; + overflow-x: hidden; + /* FIXME: ellipsis does not show. */ + text-overflow: ellipsis; + padding-block: 6px; + padding-inline: 8px; +} + /* Builtin bridge dialog */ #torPreferences-builtinBridge-header { margin: 8px 0 10px 0;
===================================== browser/components/torpreferences/jar.mn ===================================== @@ -9,6 +9,8 @@ browser.jar: content/browser/torpreferences/lox-success.svg (content/lox-success.svg) content/browser/torpreferences/lox-complete-ring.svg (content/lox-complete-ring.svg) content/browser/torpreferences/lox-progress-ring.svg (content/lox-progress-ring.svg) + content/browser/torpreferences/loxInviteDialog.xhtml (content/loxInviteDialog.xhtml) + content/browser/torpreferences/loxInviteDialog.js (content/loxInviteDialog.js) content/browser/torpreferences/bridgeQrDialog.xhtml (content/bridgeQrDialog.xhtml) content/browser/torpreferences/bridgeQrDialog.js (content/bridgeQrDialog.js) content/browser/torpreferences/builtinBridgeDialog.xhtml (content/builtinBridgeDialog.xhtml)
===================================== browser/locales/en-US/browser/tor-browser.ftl ===================================== @@ -296,3 +296,17 @@ user-provide-bridge-dialog-result-invite = The following bridges were shared wit user-provide-bridge-dialog-result-addresses = The following bridges were entered by you. user-provide-bridge-dialog-next-button = .label = Next + +## Bridge pass invite dialog. Temporary. + +lox-invite-dialog-title = + .title = Bridge pass invites +lox-invite-dialog-description = You can ask the bridge bot to create a new invite, which you can share with a trusted contact to give them their own bridge pass. Each invite can only be redeemed once, but you will unlock access to more invites over time. +lox-invite-dialog-request-button = Request new invite +lox-invite-dialog-connecting = Connecting to bridge pass server… +lox-invite-dialog-no-server-error = Unable to connect to bridge pass server. +lox-invite-dialog-generic-invite-error = Failed to create a new invite. +lox-invite-dialog-invites-label = Created invites: +lox-invite-dialog-menu-item-copy-invite = + .label = Copy invite + .accesskey = C
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/8069e4e...
tor-commits@lists.torproject.org