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
 - 
48110609
by Henry Wilkes at 2024-01-31T09:28:03+00:00
 
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:
| ... | ... | @@ -1321,7 +1321,17 @@ const gLoxStatus = { | 
| 1321 | 1321 |      );
 | 
| 1322 | 1322 | |
| 1323 | 1323 |      this._invitesButton.addEventListener("click", () => {
 | 
| 1324 | -      // TODO: Show invites.
 | 
|
| 1324 | +      gSubDialog.open(
 | 
|
| 1325 | +        "chrome://browser/content/torpreferences/loxInviteDialog.xhtml",
 | 
|
| 1326 | +        {
 | 
|
| 1327 | +          features: "resizable=yes",
 | 
|
| 1328 | +          closedCallback: () => {
 | 
|
| 1329 | +            // TODO: Listen for events from Lox, rather than call _updateInvites
 | 
|
| 1330 | +            // directly.
 | 
|
| 1331 | +            this._updateInvites();
 | 
|
| 1332 | +          },
 | 
|
| 1333 | +        }
 | 
|
| 1334 | +      );
 | 
|
| 1325 | 1335 |      });
 | 
| 1326 | 1336 |      this._unlockAlertButton.addEventListener("click", () => {
 | 
| 1327 | 1337 |        // TODO: Have a way to ensure that the cleared event data matches the
 | 
| 1 | +"use strict";
 | 
|
| 2 | +  | 
|
| 3 | +const { TorSettings, TorSettingsTopics, TorBridgeSource } =
 | 
|
| 4 | +  ChromeUtils.importESModule("resource://gre/modules/TorSettings.sys.mjs");
 | 
|
| 5 | +  | 
|
| 6 | +const { Lox, LoxErrors } = ChromeUtils.importESModule(
 | 
|
| 7 | +  "resource://gre/modules/Lox.sys.mjs"
 | 
|
| 8 | +);
 | 
|
| 9 | +  | 
|
| 10 | +/**
 | 
|
| 11 | + * Fake Lox module
 | 
|
| 12 | +  | 
|
| 13 | +const LoxErrors = {
 | 
|
| 14 | +  LoxServerUnreachable: "LoxServerUnreachable",
 | 
|
| 15 | +  Other: "Other",
 | 
|
| 16 | +};
 | 
|
| 17 | +  | 
|
| 18 | +const Lox = {
 | 
|
| 19 | +  remainingInvites: 5,
 | 
|
| 20 | +  getRemainingInviteCount() {
 | 
|
| 21 | +    return this.remainingInvites;
 | 
|
| 22 | +  },
 | 
|
| 23 | +  invites: [
 | 
|
| 24 | +    '{"invite": [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22]}',
 | 
|
| 25 | +    '{"invite": [9,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22]}',
 | 
|
| 26 | +  ],
 | 
|
| 27 | +  getInvites() {
 | 
|
| 28 | +    return this.invites;
 | 
|
| 29 | +  },
 | 
|
| 30 | +  failError: null,
 | 
|
| 31 | +  generateInvite() {
 | 
|
| 32 | +    return new Promise((res, rej) => {
 | 
|
| 33 | +      setTimeout(() => {
 | 
|
| 34 | +        if (this.failError) {
 | 
|
| 35 | +          rej({ type: this.failError });
 | 
|
| 36 | +          return;
 | 
|
| 37 | +        }
 | 
|
| 38 | +        if (!this.remainingInvites) {
 | 
|
| 39 | +          rej({ type: LoxErrors.Other });
 | 
|
| 40 | +          return;
 | 
|
| 41 | +        }
 | 
|
| 42 | +        const invite = JSON.stringify({
 | 
|
| 43 | +          invite: Array.from({ length: 100 }, () =>
 | 
|
| 44 | +            Math.floor(Math.random() * 265)
 | 
|
| 45 | +          ),
 | 
|
| 46 | +        });
 | 
|
| 47 | +        this.invites.push(invite);
 | 
|
| 48 | +        this.remainingInvites--;
 | 
|
| 49 | +        res(invite);
 | 
|
| 50 | +      }, 4000);
 | 
|
| 51 | +    });
 | 
|
| 52 | +  },
 | 
|
| 53 | +};
 | 
|
| 54 | +*/
 | 
|
| 55 | +  | 
|
| 56 | +const gLoxInvites = {
 | 
|
| 57 | +  /**
 | 
|
| 58 | +   * Initialize the dialog.
 | 
|
| 59 | +   */
 | 
|
| 60 | +  init() {
 | 
|
| 61 | +    this._dialog = document.getElementById("lox-invite-dialog");
 | 
|
| 62 | +    this._remainingInvitesEl = document.getElementById(
 | 
|
| 63 | +      "lox-invite-dialog-remaining"
 | 
|
| 64 | +    );
 | 
|
| 65 | +    this._generateButton = document.getElementById(
 | 
|
| 66 | +      "lox-invite-dialog-generate-button"
 | 
|
| 67 | +    );
 | 
|
| 68 | +    this._connectingEl = document.getElementById(
 | 
|
| 69 | +      "lox-invite-dialog-connecting"
 | 
|
| 70 | +    );
 | 
|
| 71 | +    this._errorEl = document.getElementById("lox-invite-dialog-error-message");
 | 
|
| 72 | +    this._inviteListEl = document.getElementById("lox-invite-dialog-list");
 | 
|
| 73 | +  | 
|
| 74 | +    this._generateButton.addEventListener("click", () => {
 | 
|
| 75 | +      this._generateNewInvite();
 | 
|
| 76 | +    });
 | 
|
| 77 | +  | 
|
| 78 | +    const menu = document.getElementById("lox-invite-dialog-item-menu");
 | 
|
| 79 | +    this._inviteListEl.addEventListener("contextmenu", event => {
 | 
|
| 80 | +      if (!this._inviteListEl.selectedItem) {
 | 
|
| 81 | +        return;
 | 
|
| 82 | +      }
 | 
|
| 83 | +      menu.openPopupAtScreen(event.screenX, event.screenY, true);
 | 
|
| 84 | +    });
 | 
|
| 85 | +    menu.addEventListener("popuphidden", () => {
 | 
|
| 86 | +      menu.setAttribute("aria-hidden", "true");
 | 
|
| 87 | +    });
 | 
|
| 88 | +    menu.addEventListener("popupshowing", () => {
 | 
|
| 89 | +      menu.removeAttribute("aria-hidden");
 | 
|
| 90 | +    });
 | 
|
| 91 | +    document
 | 
|
| 92 | +      .getElementById("lox-invite-dialog-copy-menu-item")
 | 
|
| 93 | +      .addEventListener("command", () => {
 | 
|
| 94 | +        const selected = this._inviteListEl.selectedItem;
 | 
|
| 95 | +        if (!selected) {
 | 
|
| 96 | +          return;
 | 
|
| 97 | +        }
 | 
|
| 98 | +        const clipboard = Cc[
 | 
|
| 99 | +          "@mozilla.org/widget/clipboardhelper;1"
 | 
|
| 100 | +        ].getService(Ci.nsIClipboardHelper);
 | 
|
| 101 | +        clipboard.copyString(selected.textContent);
 | 
|
| 102 | +      });
 | 
|
| 103 | +  | 
|
| 104 | +    // NOTE: TorSettings should already be initialized when this dialog is
 | 
|
| 105 | +    // opened.
 | 
|
| 106 | +    Services.obs.addObserver(this, TorSettingsTopics.SettingsChanged);
 | 
|
| 107 | +    // TODO: Listen for new invites from Lox, when supported.
 | 
|
| 108 | +  | 
|
| 109 | +    // Set initial _loxId value. Can close this dialog.
 | 
|
| 110 | +    this._updateLoxId();
 | 
|
| 111 | +  | 
|
| 112 | +    this._updateRemainingInvites();
 | 
|
| 113 | +    this._updateExistingInvites();
 | 
|
| 114 | +  },
 | 
|
| 115 | +  | 
|
| 116 | +  /**
 | 
|
| 117 | +   * Un-initialize the dialog.
 | 
|
| 118 | +   */
 | 
|
| 119 | +  uninit() {
 | 
|
| 120 | +    Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged);
 | 
|
| 121 | +  },
 | 
|
| 122 | +  | 
|
| 123 | +  observe(subject, topic, data) {
 | 
|
| 124 | +    switch (topic) {
 | 
|
| 125 | +      case TorSettingsTopics.SettingsChanged:
 | 
|
| 126 | +        const { changes } = subject.wrappedJSObject;
 | 
|
| 127 | +        if (
 | 
|
| 128 | +          changes.includes("bridges.source") ||
 | 
|
| 129 | +          changes.includes("bridges.lox_id")
 | 
|
| 130 | +        ) {
 | 
|
| 131 | +          this._updateLoxId();
 | 
|
| 132 | +        }
 | 
|
| 133 | +        break;
 | 
|
| 134 | +    }
 | 
|
| 135 | +  },
 | 
|
| 136 | +  | 
|
| 137 | +  /**
 | 
|
| 138 | +   * The loxId this dialog is shown for. null if uninitailized.
 | 
|
| 139 | +   *
 | 
|
| 140 | +   * @type {string?}
 | 
|
| 141 | +   */
 | 
|
| 142 | +  _loxId: null,
 | 
|
| 143 | +  /**
 | 
|
| 144 | +   * Update the _loxId value. Will close the dialog if it changes after
 | 
|
| 145 | +   * initialization.
 | 
|
| 146 | +   */
 | 
|
| 147 | +  _updateLoxId() {
 | 
|
| 148 | +    const loxId =
 | 
|
| 149 | +      TorSettings.bridges.source === TorBridgeSource.Lox
 | 
|
| 150 | +        ? TorSettings.bridges.lox_id
 | 
|
| 151 | +        : "";
 | 
|
| 152 | +    if (!loxId || (this._loxId !== null && loxId !== this._loxId)) {
 | 
|
| 153 | +      // No lox id, or it changed. Close this dialog.
 | 
|
| 154 | +      this._dialog.cancelDialog();
 | 
|
| 155 | +    }
 | 
|
| 156 | +    this._loxId = loxId;
 | 
|
| 157 | +  },
 | 
|
| 158 | +  | 
|
| 159 | +  /**
 | 
|
| 160 | +   * The invites that are already shown.
 | 
|
| 161 | +   *
 | 
|
| 162 | +   * @type {Set<string>}
 | 
|
| 163 | +   */
 | 
|
| 164 | +  _shownInvites: new Set(),
 | 
|
| 165 | +  | 
|
| 166 | +  /**
 | 
|
| 167 | +   * Add a new invite at the start of the list.
 | 
|
| 168 | +   *
 | 
|
| 169 | +   * @param {string} invite - The invite to add.
 | 
|
| 170 | +   */
 | 
|
| 171 | +  _addInvite(invite) {
 | 
|
| 172 | +    if (this._shownInvites.has(invite)) {
 | 
|
| 173 | +      return;
 | 
|
| 174 | +    }
 | 
|
| 175 | +    const newInvite = document.createXULElement("richlistitem");
 | 
|
| 176 | +    newInvite.classList.add("lox-invite-dialog-list-item");
 | 
|
| 177 | +    newInvite.textContent = invite;
 | 
|
| 178 | +  | 
|
| 179 | +    this._inviteListEl.prepend(newInvite);
 | 
|
| 180 | +    this._shownInvites.add(invite);
 | 
|
| 181 | +  },
 | 
|
| 182 | +  | 
|
| 183 | +  /**
 | 
|
| 184 | +   * Update the display of the existing invites.
 | 
|
| 185 | +   */
 | 
|
| 186 | +  _updateExistingInvites() {
 | 
|
| 187 | +    // Add new invites.
 | 
|
| 188 | +  | 
|
| 189 | +    // NOTE: we only expect invites to be appended, so we won't re-order any.
 | 
|
| 190 | +    // NOTE: invites are ordered with the oldest first.
 | 
|
| 191 | +    for (const invite of Lox.getInvites()) {
 | 
|
| 192 | +      this._addInvite(invite);
 | 
|
| 193 | +    }
 | 
|
| 194 | +  },
 | 
|
| 195 | +  | 
|
| 196 | +  /**
 | 
|
| 197 | +   * The shown number or remaining invites we have.
 | 
|
| 198 | +   *
 | 
|
| 199 | +   * @type {integer}
 | 
|
| 200 | +   */
 | 
|
| 201 | +  _remainingInvites: 0,
 | 
|
| 202 | +  | 
|
| 203 | +  /**
 | 
|
| 204 | +   * Update the display of the remaining invites.
 | 
|
| 205 | +   */
 | 
|
| 206 | +  _updateRemainingInvites() {
 | 
|
| 207 | +    this._remainingInvites = Lox.getRemainingInviteCount();
 | 
|
| 208 | +  | 
|
| 209 | +    document.l10n.setAttributes(
 | 
|
| 210 | +      this._remainingInvitesEl,
 | 
|
| 211 | +      "tor-bridges-lox-remaining-invites",
 | 
|
| 212 | +      { numInvites: this._remainingInvites }
 | 
|
| 213 | +    );
 | 
|
| 214 | +    this._updateGenerateButtonState();
 | 
|
| 215 | +  },
 | 
|
| 216 | +  | 
|
| 217 | +  /**
 | 
|
| 218 | +   * Whether we are currently generating an invite.
 | 
|
| 219 | +   *
 | 
|
| 220 | +   * @type {boolean}
 | 
|
| 221 | +   */
 | 
|
| 222 | +  _generating: false,
 | 
|
| 223 | +  /**
 | 
|
| 224 | +   * Set whether we are generating an invite.
 | 
|
| 225 | +   *
 | 
|
| 226 | +   * @param {boolean} isGenerating - Whether we are generating.
 | 
|
| 227 | +   */
 | 
|
| 228 | +  _setGenerating(isGenerating) {
 | 
|
| 229 | +    this._generating = isGenerating;
 | 
|
| 230 | +    this._updateGenerateButtonState();
 | 
|
| 231 | +    this._connectingEl.classList.toggle("show-connecting", isGenerating);
 | 
|
| 232 | +  },
 | 
|
| 233 | +  | 
|
| 234 | +  /**
 | 
|
| 235 | +   * Update the state of the generate button.
 | 
|
| 236 | +   */
 | 
|
| 237 | +  _updateGenerateButtonState() {
 | 
|
| 238 | +    this._generateButton.disabled = this._generating || !this._remainingInvites;
 | 
|
| 239 | +  },
 | 
|
| 240 | +  | 
|
| 241 | +  /**
 | 
|
| 242 | +   * Start generating a new invite.
 | 
|
| 243 | +   */
 | 
|
| 244 | +  _generateNewInvite() {
 | 
|
| 245 | +    if (this._generating) {
 | 
|
| 246 | +      console.error("Already generating an invite");
 | 
|
| 247 | +      return;
 | 
|
| 248 | +    }
 | 
|
| 249 | +    this._setGenerating(true);
 | 
|
| 250 | +    // Clear the previous error.
 | 
|
| 251 | +    this._updateGenerateError(null);
 | 
|
| 252 | +    // Move focus from the button to the connecting element, since button is
 | 
|
| 253 | +    // now disabled.
 | 
|
| 254 | +    this._connectingEl.focus();
 | 
|
| 255 | +  | 
|
| 256 | +    let lostFocus = false;
 | 
|
| 257 | +    Lox.generateInvite()
 | 
|
| 258 | +      .finally(() => {
 | 
|
| 259 | +        // Fetch whether the connecting label still has focus before we hide it.
 | 
|
| 260 | +        lostFocus = this._connectingEl.contains(document.activeElement);
 | 
|
| 261 | +        this._setGenerating(false);
 | 
|
| 262 | +      })
 | 
|
| 263 | +      .then(
 | 
|
| 264 | +        invite => {
 | 
|
| 265 | +          this._addInvite(invite);
 | 
|
| 266 | +  | 
|
| 267 | +          if (!this._inviteListEl.contains(document.activeElement)) {
 | 
|
| 268 | +            // Does not have focus, change the selected item to be the new
 | 
|
| 269 | +            // invite (at index 0).
 | 
|
| 270 | +            this._inviteListEl.selectedIndex = 0;
 | 
|
| 271 | +          }
 | 
|
| 272 | +  | 
|
| 273 | +          if (lostFocus) {
 | 
|
| 274 | +            // Move focus to the new invite before we hide the "Connecting"
 | 
|
| 275 | +            // message.
 | 
|
| 276 | +            this._inviteListEl.focus();
 | 
|
| 277 | +          }
 | 
|
| 278 | +  | 
|
| 279 | +          // TODO: When Lox sends out notifications, let the observer handle the
 | 
|
| 280 | +          // change rather than calling _updateRemainingInvites directly.
 | 
|
| 281 | +          this._updateRemainingInvites();
 | 
|
| 282 | +        },
 | 
|
| 283 | +        loxError => {
 | 
|
| 284 | +          console.error("Failed to generate an invite", loxError);
 | 
|
| 285 | +          switch (loxError.type) {
 | 
|
| 286 | +            case LoxErrors.LoxServerUnreachable:
 | 
|
| 287 | +              this._updateGenerateError("no-server");
 | 
|
| 288 | +              break;
 | 
|
| 289 | +            default:
 | 
|
| 290 | +              this._updateGenerateError("generic");
 | 
|
| 291 | +              break;
 | 
|
| 292 | +          }
 | 
|
| 293 | +  | 
|
| 294 | +          if (lostFocus) {
 | 
|
| 295 | +            // Move focus back to the button before we hide the "Connecting"
 | 
|
| 296 | +            // message.
 | 
|
| 297 | +            this._generateButton.focus();
 | 
|
| 298 | +          }
 | 
|
| 299 | +        }
 | 
|
| 300 | +      );
 | 
|
| 301 | +  },
 | 
|
| 302 | +  | 
|
| 303 | +  /**
 | 
|
| 304 | +   * Update the shown generation error.
 | 
|
| 305 | +   *
 | 
|
| 306 | +   * @param {string?} type - The error type, or null if no error should be
 | 
|
| 307 | +   *   shown.
 | 
|
| 308 | +   */
 | 
|
| 309 | +  _updateGenerateError(type) {
 | 
|
| 310 | +    // First clear the existing error.
 | 
|
| 311 | +    this._errorEl.removeAttribute("data-l10n-id");
 | 
|
| 312 | +    this._errorEl.textContent = "";
 | 
|
| 313 | +    this._errorEl.classList.toggle("show-error", !!type);
 | 
|
| 314 | +  | 
|
| 315 | +    if (!type) {
 | 
|
| 316 | +      return;
 | 
|
| 317 | +    }
 | 
|
| 318 | +  | 
|
| 319 | +    let errorId;
 | 
|
| 320 | +    switch (type) {
 | 
|
| 321 | +      case "no-server":
 | 
|
| 322 | +        errorId = "lox-invite-dialog-no-server-error";
 | 
|
| 323 | +        break;
 | 
|
| 324 | +      case "generic":
 | 
|
| 325 | +        // Generic error.
 | 
|
| 326 | +        errorId = "lox-invite-dialog-generic-invite-error";
 | 
|
| 327 | +        break;
 | 
|
| 328 | +    }
 | 
|
| 329 | +  | 
|
| 330 | +    document.l10n.setAttributes(this._errorEl, errorId);
 | 
|
| 331 | +  },
 | 
|
| 332 | +};
 | 
|
| 333 | +  | 
|
| 334 | +window.addEventListener(
 | 
|
| 335 | +  "DOMContentLoaded",
 | 
|
| 336 | +  () => {
 | 
|
| 337 | +    gLoxInvites.init();
 | 
|
| 338 | +    window.addEventListener(
 | 
|
| 339 | +      "unload",
 | 
|
| 340 | +      () => {
 | 
|
| 341 | +        gLoxInvites.uninit();
 | 
|
| 342 | +      },
 | 
|
| 343 | +      { once: true }
 | 
|
| 344 | +    );
 | 
|
| 345 | +  },
 | 
|
| 346 | +  { once: true }
 | 
|
| 347 | +); | 
| 1 | +<?xml version="1.0" encoding="UTF-8"?>
 | 
|
| 2 | +<?xml-stylesheet href="" type="text/css"?>
 | 
|
| 3 | +<?xml-stylesheet href=""?>
 | 
|
| 4 | +<?xml-stylesheet href=""?>
 | 
|
| 5 | +  | 
|
| 6 | +<window
 | 
|
| 7 | +  type="child"
 | 
|
| 8 | +  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
 | 
|
| 9 | +  xmlns:html="http://www.w3.org/1999/xhtml"
 | 
|
| 10 | +  data-l10n-id="lox-invite-dialog-title"
 | 
|
| 11 | +>
 | 
|
| 12 | +  <!-- Context menu, aria-hidden whilst not shown so it does not appear in the
 | 
|
| 13 | +     - document content. -->
 | 
|
| 14 | +  <menupopup id="lox-invite-dialog-item-menu" aria-hidden="true">
 | 
|
| 15 | +    <menuitem
 | 
|
| 16 | +      id="lox-invite-dialog-copy-menu-item"
 | 
|
| 17 | +      data-l10n-id="lox-invite-dialog-menu-item-copy-invite"
 | 
|
| 18 | +    />
 | 
|
| 19 | +  </menupopup>
 | 
|
| 20 | +  <dialog id="lox-invite-dialog" buttons="accept">
 | 
|
| 21 | +    <linkset>
 | 
|
| 22 | +      <html:link rel="localization" href="">"browser/tor-browser.ftl" />
 | 
|
| 23 | +    </linkset>
 | 
|
| 24 | +  | 
|
| 25 | +    <script src="">"chrome://browser/content/torpreferences/loxInviteDialog.js" />
 | 
|
| 26 | +  | 
|
| 27 | +    <description data-l10n-id="lox-invite-dialog-description"></description>
 | 
|
| 28 | +    <html:div id="lox-invite-dialog-generate-area">
 | 
|
| 29 | +      <html:span id="lox-invite-dialog-remaining"></html:span>
 | 
|
| 30 | +      <html:button
 | 
|
| 31 | +        id="lox-invite-dialog-generate-button"
 | 
|
| 32 | +        data-l10n-id="lox-invite-dialog-request-button"
 | 
|
| 33 | +      ></html:button>
 | 
|
| 34 | +      <html:div id="lox-invite-dialog-message-area">
 | 
|
| 35 | +        <html:span
 | 
|
| 36 | +          id="lox-invite-dialog-error-message"
 | 
|
| 37 | +          role="alert"
 | 
|
| 38 | +        ></html:span>
 | 
|
| 39 | +        <html:span
 | 
|
| 40 | +          id="lox-invite-dialog-connecting"
 | 
|
| 41 | +          role="alert"
 | 
|
| 42 | +          tabindex="0"
 | 
|
| 43 | +          data-l10n-id="lox-invite-dialog-connecting"
 | 
|
| 44 | +        ></html:span>
 | 
|
| 45 | +      </html:div>
 | 
|
| 46 | +    </html:div>
 | 
|
| 47 | +    <html:div
 | 
|
| 48 | +      id="lox-invite-dialog-list-label"
 | 
|
| 49 | +      data-l10n-id="lox-invite-dialog-invites-label"
 | 
|
| 50 | +    ></html:div>
 | 
|
| 51 | +    <richlistbox
 | 
|
| 52 | +      id="lox-invite-dialog-list"
 | 
|
| 53 | +      aria-labelledby="lox-invite-dialog-list-label"
 | 
|
| 54 | +    ></richlistbox>
 | 
|
| 55 | +  </dialog>
 | 
|
| 56 | +</window> | 
| ... | ... | @@ -820,6 +820,75 @@ dialog#torPreferences-requestBridge-dialog > hbox { | 
| 820 | 820 |    background: var(--qr-one);
 | 
| 821 | 821 |  }
 | 
| 822 | 822 | |
| 823 | +/* Lox invite dialog */
 | 
|
| 824 | +  | 
|
| 825 | +#lox-invite-dialog-generate-area {
 | 
|
| 826 | +  flex: 0 0 auto;
 | 
|
| 827 | +  display: grid;
 | 
|
| 828 | +  grid-template:
 | 
|
| 829 | +    ". remaining button" min-content
 | 
|
| 830 | +    "message message message" auto
 | 
|
| 831 | +    / 1fr max-content max-content;
 | 
|
| 832 | +  gap: 8px;
 | 
|
| 833 | +  margin-block: 16px 8px;
 | 
|
| 834 | +  align-items: center;
 | 
|
| 835 | +}
 | 
|
| 836 | +  | 
|
| 837 | +#lox-invite-dialog-remaining {
 | 
|
| 838 | +  grid-area: remaining;
 | 
|
| 839 | +}
 | 
|
| 840 | +  | 
|
| 841 | +#lox-invite-dialog-generate-button {
 | 
|
| 842 | +  grid-area: button;
 | 
|
| 843 | +}
 | 
|
| 844 | +  | 
|
| 845 | +#lox-invite-dialog-message-area {
 | 
|
| 846 | +  grid-area: message;
 | 
|
| 847 | +  justify-self: end;
 | 
|
| 848 | +}
 | 
|
| 849 | +  | 
|
| 850 | +#lox-invite-dialog-message-area::after {
 | 
|
| 851 | +  /* Zero width space, to ensure we are always one line high. */
 | 
|
| 852 | +  content: "\200B";
 | 
|
| 853 | +}
 | 
|
| 854 | +  | 
|
| 855 | +#lox-invite-dialog-error-message {
 | 
|
| 856 | +  color: var(--in-content-error-text-color);
 | 
|
| 857 | +}
 | 
|
| 858 | +  | 
|
| 859 | +#lox-invite-dialog-error-message:not(.show-error) {
 | 
|
| 860 | +  display: none;
 | 
|
| 861 | +}
 | 
|
| 862 | +  | 
|
| 863 | +#lox-invite-dialog-connecting {
 | 
|
| 864 | +  color: var(--text-color-deemphasized);
 | 
|
| 865 | +  /* TODO: Add spinner ::before */
 | 
|
| 866 | +}
 | 
|
| 867 | +  | 
|
| 868 | +#lox-invite-dialog-connecting:not(.show-connecting) {
 | 
|
| 869 | +  display: none;
 | 
|
| 870 | +}
 | 
|
| 871 | +  | 
|
| 872 | +#lox-invite-dialog-list-label {
 | 
|
| 873 | +  font-weight: 700;
 | 
|
| 874 | +}
 | 
|
| 875 | +  | 
|
| 876 | +#lox-invite-dialog-list {
 | 
|
| 877 | +  flex: 1 1 auto;
 | 
|
| 878 | +  /* basis height */
 | 
|
| 879 | +  height: 10em;
 | 
|
| 880 | +  margin-block: 8px;
 | 
|
| 881 | +}
 | 
|
| 882 | +  | 
|
| 883 | +.lox-invite-dialog-list-item {
 | 
|
| 884 | +  white-space: nowrap;
 | 
|
| 885 | +  overflow-x: hidden;
 | 
|
| 886 | +  /* FIXME: ellipsis does not show. */
 | 
|
| 887 | +  text-overflow: ellipsis;
 | 
|
| 888 | +  padding-block: 6px;
 | 
|
| 889 | +  padding-inline: 8px;
 | 
|
| 890 | +}
 | 
|
| 891 | +  | 
|
| 823 | 892 |  /* Builtin bridge dialog */
 | 
| 824 | 893 |  #torPreferences-builtinBridge-header {
 | 
| 825 | 894 |    margin: 8px 0 10px 0;
 | 
| ... | ... | @@ -9,6 +9,8 @@ browser.jar: | 
| 9 | 9 |      content/browser/torpreferences/lox-success.svg                   (content/lox-success.svg)
 | 
| 10 | 10 |      content/browser/torpreferences/lox-complete-ring.svg             (content/lox-complete-ring.svg)
 | 
| 11 | 11 |      content/browser/torpreferences/lox-progress-ring.svg             (content/lox-progress-ring.svg)
 | 
| 12 | +    content/browser/torpreferences/loxInviteDialog.xhtml             (content/loxInviteDialog.xhtml)
 | 
|
| 13 | +    content/browser/torpreferences/loxInviteDialog.js                (content/loxInviteDialog.js)
 | 
|
| 12 | 14 |      content/browser/torpreferences/bridgeQrDialog.xhtml              (content/bridgeQrDialog.xhtml)
 | 
| 13 | 15 |      content/browser/torpreferences/bridgeQrDialog.js                 (content/bridgeQrDialog.js)
 | 
| 14 | 16 |      content/browser/torpreferences/builtinBridgeDialog.xhtml         (content/builtinBridgeDialog.xhtml)
 | 
| ... | ... | @@ -296,3 +296,17 @@ user-provide-bridge-dialog-result-invite = The following bridges were shared wit | 
| 296 | 296 |  user-provide-bridge-dialog-result-addresses = The following bridges were entered by you.
 | 
| 297 | 297 |  user-provide-bridge-dialog-next-button =
 | 
| 298 | 298 |      .label = Next
 | 
| 299 | +  | 
|
| 300 | +## Bridge pass invite dialog. Temporary.
 | 
|
| 301 | +  | 
|
| 302 | +lox-invite-dialog-title =
 | 
|
| 303 | +    .title = Bridge pass invites
 | 
|
| 304 | +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.
 | 
|
| 305 | +lox-invite-dialog-request-button = Request new invite
 | 
|
| 306 | +lox-invite-dialog-connecting = Connecting to bridge pass server…
 | 
|
| 307 | +lox-invite-dialog-no-server-error = Unable to connect to bridge pass server.
 | 
|
| 308 | +lox-invite-dialog-generic-invite-error = Failed to create a new invite.
 | 
|
| 309 | +lox-invite-dialog-invites-label = Created invites:
 | 
|
| 310 | +lox-invite-dialog-menu-item-copy-invite =
 | 
|
| 311 | +    .label = Copy invite
 | 
|
| 312 | +    .accesskey = C |