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 |