richard pushed to branch tor-browser-115.7.0esr-13.5-1 at The Tor Project / Applications / Tor Browser

Commits:

12 changed files:

Changes:

  • browser/components/torpreferences/content/bridge-qr.svg
    1
    +<svg width="24" height="24" viewBox="0 0 24 24" fill="context-fill" xmlns="http://www.w3.org/2000/svg">
    
    2
    +  <path d="M5.5 3C4.83696 3 4.20107 3.26339 3.73223 3.73223C3.26339 4.20107 3 4.83696 3 5.5V9H4.75V5.5C4.75 5.30109 4.82902 5.11032 4.96967 4.96967C5.11032 4.82902 5.30109 4.75 5.5 4.75H9V3H5.5ZM11 10.25C11 10.4489 10.921 10.6397 10.7803 10.7803C10.6397 10.921 10.4489 11 10.25 11H7.75C7.55109 11 7.36032 10.921 7.21967 10.7803C7.07902 10.6397 7 10.4489 7 10.25V7.75C7 7.55109 7.07902 7.36032 7.21967 7.21967C7.36032 7.07902 7.55109 7 7.75 7H10.25C10.4489 7 10.6397 7.07902 10.7803 7.21967C10.921 7.36032 11 7.55109 11 7.75V10.25ZM17 15H15V13H17V11H15V9H17V7H15V9H13V11H15V13H13V15H11V13H9V15H7V17H9V15H11V17H13V15H15V17H17V15ZM3 18.5V15H4.75V18.5C4.75 18.914 5.086 19.25 5.5 19.25H9V21H5.5C4.83696 21 4.20107 20.7366 3.73223 20.2678C3.26339 19.7989 3 19.163 3 18.5ZM15 3H18.5C19.163 3 19.7989 3.26339 20.2678 3.73223C20.7366 4.20107 21 4.83696 21 5.5V9H19.25V5.5C19.25 5.30109 19.171 5.11032 19.0303 4.96967C18.8897 4.82902 18.6989 4.75 18.5 4.75H15V3ZM21 18.5V15H19.25V18.5C19.25 18.6989 19.171 18.8897 19.0303 19.0303C18.8897 19.171 18.6989 19.25 18.5 19.25H15V21H18.5C19.163 21 19.7989 20.7366 20.2678 20.2678C20.7366 19.7989 21 19.163 21 18.5Z"/>
    
    3
    +</svg>

  • browser/components/torpreferences/content/bridge.svg
    1
    +<svg width="16" height="16" viewBox="0 0 16 16" fill="context-fill" xmlns="http://www.w3.org/2000/svg">
    
    2
    +  <path d="M15.5 11.5C15.5 7.35786 12.1421 4 8 4C3.85786 4 0.5 7.35786 0.5 11.5V12.7461H1.67188V11.5C1.67188 8.00507 4.50507 5.17188 8 5.17188C11.4949 5.17188 14.3281 8.00507 14.3281 11.5V12.7461H15.5V11.5Z"/>
    
    3
    +  <path d="M13.25 11.5C13.25 8.6005 10.8995 6.24999 7.99999 6.24999C5.1005 6.24999 2.74999 8.6005 2.74999 11.5L2.74989 12.7461H3.92177L3.92187 11.5C3.92187 9.24771 5.74771 7.42187 7.99999 7.42187C10.2523 7.42187 12.0781 9.24771 12.0781 11.5L12.0782 12.7461H13.2501L13.25 11.5Z"/>
    
    4
    +  <path d="M8 8.5C9.65686 8.5 11 9.84315 11 11.5L11.0002 12.7461H9.82836L9.82813 11.5C9.82813 10.4904 9.00965 9.67188 8 9.67188C6.99036 9.67188 6.17188 10.4904 6.17188 11.5L6.17164 12.7461H4.99977V11.5C4.99977 9.84315 6.34315 8.5 8 8.5Z"/>
    
    5
    +</svg>

  • browser/components/torpreferences/content/builtinBridgeDialog.mjs
    ... ... @@ -69,10 +69,12 @@ export class BuiltinBridgeDialog {
    69 69
           optionEl.querySelector(
    
    70 70
             ".torPreferences-current-bridge-label"
    
    71 71
           ).textContent = TorStrings.settings.currentBridge;
    
    72
    -      optionEl.classList.toggle(
    
    73
    -        "current-builtin-bridge-type",
    
    74
    -        type === currentBuiltinType
    
    75
    -      );
    
    72
    +      optionEl
    
    73
    +        .querySelector(".bridge-status-badge")
    
    74
    +        .classList.toggle(
    
    75
    +          "bridge-status-current-built-in",
    
    76
    +          type === currentBuiltinType
    
    77
    +        );
    
    76 78
         }
    
    77 79
     
    
    78 80
         if (currentBuiltinType) {
    

  • browser/components/torpreferences/content/builtinBridgeDialog.xhtml
    ... ... @@ -20,8 +20,8 @@
    20 20
                 aria-describedby="obfs-bridges-current obfs-bridges-description"
    
    21 21
                 value="obfs4"
    
    22 22
               />
    
    23
    -          <html:span class="torPreferences-current-bridge-badge">
    
    24
    -            <image class="torPreferences-current-bridge-icon" />
    
    23
    +          <html:span class="bridge-status-badge">
    
    24
    +            <html:div class="bridge-status-icon"></html:div>
    
    25 25
                 <html:span
    
    26 26
                   id="obfs-bridges-current"
    
    27 27
                   class="torPreferences-current-bridge-label"
    
    ... ... @@ -41,8 +41,8 @@
    41 41
                 aria-describedby="snowflake-bridges-current snowflake-bridges-description"
    
    42 42
                 value="snowflake"
    
    43 43
               />
    
    44
    -          <html:span class="torPreferences-current-bridge-badge">
    
    45
    -            <image class="torPreferences-current-bridge-icon" />
    
    44
    +          <html:span class="bridge-status-badge">
    
    45
    +            <html:div class="bridge-status-icon"></html:div>
    
    46 46
                 <html:span
    
    47 47
                   id="snowflake-bridges-current"
    
    48 48
                   class="torPreferences-current-bridge-label"
    
    ... ... @@ -62,8 +62,8 @@
    62 62
                 aria-describedby="meek-bridges-current meek-bridges-description"
    
    63 63
                 value="meek-azure"
    
    64 64
               />
    
    65
    -          <html:span class="torPreferences-current-bridge-badge">
    
    66
    -            <image class="torPreferences-current-bridge-icon" />
    
    65
    +          <html:span class="bridge-status-badge">
    
    66
    +            <html:div class="bridge-status-icon"></html:div>
    
    67 67
                 <html:span
    
    68 68
                   id="meek-bridges-current"
    
    69 69
                   class="torPreferences-current-bridge-label"
    

  • browser/components/torpreferences/content/connectionPane.js
    ... ... @@ -66,6 +66,1642 @@ const InternetStatus = Object.freeze({
    66 66
       Offline: -1,
    
    67 67
     });
    
    68 68
     
    
    69
    +/**
    
    70
    + * Make changes to TorSettings and save them.
    
    71
    + *
    
    72
    + * Bulk changes will be frozen together.
    
    73
    + *
    
    74
    + * @param {Function} changes - Method to apply changes to TorSettings.
    
    75
    + */
    
    76
    +async function setTorSettings(changes) {
    
    77
    +  if (!TorSettings.initialized) {
    
    78
    +    console.warning("Ignoring changes to uninitialized TorSettings");
    
    79
    +    return;
    
    80
    +  }
    
    81
    +  TorSettings.freezeNotifications();
    
    82
    +  try {
    
    83
    +    changes();
    
    84
    +    // This will trigger TorSettings.#cleanupSettings()
    
    85
    +    TorSettings.saveToPrefs();
    
    86
    +    try {
    
    87
    +      // May throw.
    
    88
    +      await TorSettings.applySettings();
    
    89
    +    } catch (e) {
    
    90
    +      console.error("Failed to save Tor settings", e);
    
    91
    +    }
    
    92
    +  } finally {
    
    93
    +    TorSettings.thawNotifications();
    
    94
    +  }
    
    95
    +}
    
    96
    +
    
    97
    +/**
    
    98
    + * Get the ID/fingerprint of the bridge used in the most recent Tor circuit.
    
    99
    + *
    
    100
    + * @returns {string?} - The bridge ID or null if a bridge with an id was not
    
    101
    + *   used in the last circuit.
    
    102
    + */
    
    103
    +async function getConnectedBridgeId() {
    
    104
    +  // TODO: PieroV: We could make sure TorSettings is in sync by monitoring also
    
    105
    +  // changes of settings. At that point, we could query it, instead of doing a
    
    106
    +  // query over the control port.
    
    107
    +  let bridge = null;
    
    108
    +  try {
    
    109
    +    const provider = await TorProviderBuilder.build();
    
    110
    +    bridge = provider.currentBridge;
    
    111
    +  } catch (e) {
    
    112
    +    console.warn("Could not get current bridge", e);
    
    113
    +  }
    
    114
    +  return bridge?.fingerprint ?? null;
    
    115
    +}
    
    116
    +
    
    117
    +// TODO: Instead of aria-live in the DOM, use the proposed ariaNotify
    
    118
    +// API if it gets accepted into firefox and works with screen readers.
    
    119
    +// See https://github.com/WICG/proposals/issues/112
    
    120
    +/**
    
    121
    + * Notification for screen reader users.
    
    122
    + */
    
    123
    +const gBridgesNotification = {
    
    124
    +  /**
    
    125
    +   * The screen reader area that shows updates.
    
    126
    +   *
    
    127
    +   * @type {Element?}
    
    128
    +   */
    
    129
    +  _updateArea: null,
    
    130
    +  /**
    
    131
    +   * The text for the screen reader update.
    
    132
    +   *
    
    133
    +   * @type {Element?}
    
    134
    +   */
    
    135
    +  _textEl: null,
    
    136
    +  /**
    
    137
    +   * A timeout for hiding the update.
    
    138
    +   *
    
    139
    +   * @type {integer?}
    
    140
    +   */
    
    141
    +  _hideUpdateTimeout: null,
    
    142
    +
    
    143
    +  /**
    
    144
    +   * Initialize the area for notifications.
    
    145
    +   */
    
    146
    +  init() {
    
    147
    +    this._updateArea = document.getElementById("tor-bridges-update-area");
    
    148
    +    this._textEl = document.getElementById("tor-bridges-update-area-text");
    
    149
    +  },
    
    150
    +
    
    151
    +  /**
    
    152
    +   * Post a new notification, replacing any existing one.
    
    153
    +   *
    
    154
    +   * @param {string} type - The notification type.
    
    155
    +   */
    
    156
    +  post(type) {
    
    157
    +    this._updateArea.hidden = false;
    
    158
    +    // First we clear the update area to reset the text to be empty.
    
    159
    +    this._textEl.removeAttribute("data-l10n-id");
    
    160
    +    this._textEl.textContent = "";
    
    161
    +    if (this._hideUpdateTimeout !== null) {
    
    162
    +      clearTimeout(this._hideUpdateTimeout);
    
    163
    +      this._hideUpdateTimeout = null;
    
    164
    +    }
    
    165
    +
    
    166
    +    let updateId;
    
    167
    +    switch (type) {
    
    168
    +      case "removed-one":
    
    169
    +        updateId = "tor-bridges-update-removed-one-bridge";
    
    170
    +        break;
    
    171
    +      case "removed-all":
    
    172
    +        updateId = "tor-bridges-update-removed-all-bridges";
    
    173
    +        break;
    
    174
    +      case "changed":
    
    175
    +      default:
    
    176
    +        // Generic message for when bridges change.
    
    177
    +        updateId = "tor-bridges-update-changed-bridges";
    
    178
    +        break;
    
    179
    +    }
    
    180
    +
    
    181
    +    // Hide the area after 5 minutes, when the update is not "recent" any
    
    182
    +    // more.
    
    183
    +    this._hideUpdateTimeout = setTimeout(() => {
    
    184
    +      this._updateArea.hidden = true;
    
    185
    +    }, 300000);
    
    186
    +
    
    187
    +    // Wait a small amount of time to actually set the textContent. Otherwise
    
    188
    +    // the screen reader (tested with Orca) may not pick up on the change in
    
    189
    +    // text.
    
    190
    +    setTimeout(() => {
    
    191
    +      document.l10n.setAttributes(this._textEl, updateId);
    
    192
    +    }, 500);
    
    193
    +  },
    
    194
    +};
    
    195
    +
    
    196
    +/**
    
    197
    + * Controls the bridge grid.
    
    198
    + */
    
    199
    +const gBridgeGrid = {
    
    200
    +  /**
    
    201
    +   * The grid element.
    
    202
    +   *
    
    203
    +   * @type {Element?}
    
    204
    +   */
    
    205
    +  _grid: null,
    
    206
    +  /**
    
    207
    +   * The template for creating new rows.
    
    208
    +   *
    
    209
    +   * @type {HTMLTemplateElement?}
    
    210
    +   */
    
    211
    +  _rowTemplate: null,
    
    212
    +
    
    213
    +  /**
    
    214
    +   * @typedef {object} EmojiCell
    
    215
    +   *
    
    216
    +   * @property {Element} cell - The grid cell element.
    
    217
    +   * @property {Element} img - The grid cell icon.
    
    218
    +   * @property {Element} index - The emoji index.
    
    219
    +   */
    
    220
    +  /**
    
    221
    +   * @typedef {object} BridgeGridRow
    
    222
    +   *
    
    223
    +   * @property {Element} element - The row element.
    
    224
    +   * @property {Element} optionsButton - The options button.
    
    225
    +   * @property {EmojiCell[]} emojis - The emoji cells.
    
    226
    +   * @property {Element} menu - The options menupopup.
    
    227
    +   * @property {Element} statusEl - The bridge status element.
    
    228
    +   * @property {Element} statusText - The status text.
    
    229
    +   * @property {string} bridgeLine - The identifying bridge string for this row.
    
    230
    +   * @property {string?} bridgeId - The ID/fingerprint for the bridge, or null
    
    231
    +   *   if it doesn't have one.
    
    232
    +   * @property {integer} index - The index of the row in the grid.
    
    233
    +   * @property {boolean} connected - Whether we are connected to the bridge
    
    234
    +   *   (recently in use for a Tor circuit).
    
    235
    +   * @property {BridgeGridCell[]} cells - The cells that belong to the row,
    
    236
    +   *   ordered by their column.
    
    237
    +   */
    
    238
    +  /**
    
    239
    +   * @typedef {object} BridgeGridCell
    
    240
    +   *
    
    241
    +   * @property {Element} element - The cell element.
    
    242
    +   * @property {Element} focusEl - The element belonging to the cell that should
    
    243
    +   *   receive focus. Should be the cell element itself, or an interactive
    
    244
    +   *   focusable child.
    
    245
    +   * @property {integer} columnIndex - The index of the column this cell belongs
    
    246
    +   *   to.
    
    247
    +   * @property {BridgeGridRow} row - The row this cell belongs to.
    
    248
    +   */
    
    249
    +  /**
    
    250
    +   * The current rows in the grid.
    
    251
    +   *
    
    252
    +   * @type {BridgeGridRow[]}
    
    253
    +   */
    
    254
    +  _rows: [],
    
    255
    +  /**
    
    256
    +   * The cell that should be the focus target when the user moves focus into the
    
    257
    +   * grid, or null if the grid itself should be the target.
    
    258
    +   *
    
    259
    +   * @type {BridgeGridCell?}
    
    260
    +   */
    
    261
    +  _focusCell: null,
    
    262
    +
    
    263
    +  /**
    
    264
    +   * Initialize the bridge grid.
    
    265
    +   */
    
    266
    +  init() {
    
    267
    +    this._grid = document.getElementById("tor-bridges-grid-display");
    
    268
    +    // Initially, make only the grid itself part of the keyboard tab cycle.
    
    269
    +    // matches _focusCell = null.
    
    270
    +    this._grid.tabIndex = 0;
    
    271
    +
    
    272
    +    this._rowTemplate = document.getElementById(
    
    273
    +      "tor-bridges-grid-row-template"
    
    274
    +    );
    
    275
    +
    
    276
    +    this._grid.addEventListener("keydown", this);
    
    277
    +    this._grid.addEventListener("mousedown", this);
    
    278
    +    this._grid.addEventListener("focusin", this);
    
    279
    +
    
    280
    +    Services.obs.addObserver(this, TorSettingsTopics.SettingsChanged);
    
    281
    +
    
    282
    +    // NOTE: Before initializedPromise completes, this area is hidden.
    
    283
    +    TorSettings.initializedPromise.then(() => {
    
    284
    +      this._updateRows(true);
    
    285
    +    });
    
    286
    +  },
    
    287
    +
    
    288
    +  /**
    
    289
    +   * Uninitialize the bridge grid.
    
    290
    +   */
    
    291
    +  uninit() {
    
    292
    +    Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged);
    
    293
    +    this.deactivate();
    
    294
    +  },
    
    295
    +
    
    296
    +  /**
    
    297
    +   * Whether the grid is visible and responsive.
    
    298
    +   *
    
    299
    +   * @type {boolean}
    
    300
    +   */
    
    301
    +  _active: false,
    
    302
    +
    
    303
    +  /**
    
    304
    +   * Activate and show the bridge grid.
    
    305
    +   */
    
    306
    +  activate() {
    
    307
    +    if (this._active) {
    
    308
    +      return;
    
    309
    +    }
    
    310
    +
    
    311
    +    this._active = true;
    
    312
    +
    
    313
    +    Services.obs.addObserver(this, "intl:app-locales-changed");
    
    314
    +    Services.obs.addObserver(this, TorProviderTopics.BridgeChanged);
    
    315
    +
    
    316
    +    this._grid.classList.add("grid-active");
    
    317
    +
    
    318
    +    this._updateEmojiLangCode();
    
    319
    +    this._updateConnectedBridge();
    
    320
    +  },
    
    321
    +
    
    322
    +  /**
    
    323
    +   * Deactivate and hide the bridge grid.
    
    324
    +   */
    
    325
    +  deactivate() {
    
    326
    +    if (!this._active) {
    
    327
    +      return;
    
    328
    +    }
    
    329
    +
    
    330
    +    this._active = false;
    
    331
    +
    
    332
    +    this._forceCloseRowMenus();
    
    333
    +
    
    334
    +    this._grid.classList.remove("grid-active");
    
    335
    +
    
    336
    +    Services.obs.removeObserver(this, "intl:app-locales-changed");
    
    337
    +    Services.obs.removeObserver(this, TorProviderTopics.BridgeChanged);
    
    338
    +  },
    
    339
    +
    
    340
    +  observe(subject, topic, data) {
    
    341
    +    switch (topic) {
    
    342
    +      case TorSettingsTopics.SettingsChanged:
    
    343
    +        const { changes } = subject.wrappedJSObject;
    
    344
    +        if (
    
    345
    +          changes.includes("bridges.source") ||
    
    346
    +          changes.includes("bridges.bridge_strings")
    
    347
    +        ) {
    
    348
    +          this._updateRows();
    
    349
    +        }
    
    350
    +        break;
    
    351
    +      case "intl:app-locales-changed":
    
    352
    +        this._updateEmojiLangCode();
    
    353
    +        break;
    
    354
    +      case TorProviderTopics.BridgeChanged:
    
    355
    +        this._updateConnectedBridge();
    
    356
    +        break;
    
    357
    +    }
    
    358
    +  },
    
    359
    +
    
    360
    +  handleEvent(event) {
    
    361
    +    if (event.type === "keydown") {
    
    362
    +      if (event.altKey || event.shiftKey || event.metaKey || event.ctrlKey) {
    
    363
    +        // Don't interfere with these events.
    
    364
    +        return;
    
    365
    +      }
    
    366
    +
    
    367
    +      if (this._rows.some(row => row.menu.open)) {
    
    368
    +        // Have an open menu, let the menu handle the event instead.
    
    369
    +        return;
    
    370
    +      }
    
    371
    +
    
    372
    +      let numRows = this._rows.length;
    
    373
    +      if (!numRows) {
    
    374
    +        // Nowhere for focus to go.
    
    375
    +        return;
    
    376
    +      }
    
    377
    +
    
    378
    +      let moveRow = 0;
    
    379
    +      let moveColumn = 0;
    
    380
    +      const isLTR = this._grid.matches(":dir(ltr)");
    
    381
    +      switch (event.key) {
    
    382
    +        case "ArrowDown":
    
    383
    +          moveRow = 1;
    
    384
    +          break;
    
    385
    +        case "ArrowUp":
    
    386
    +          moveRow = -1;
    
    387
    +          break;
    
    388
    +        case "ArrowRight":
    
    389
    +          moveColumn = isLTR ? 1 : -1;
    
    390
    +          break;
    
    391
    +        case "ArrowLeft":
    
    392
    +          moveColumn = isLTR ? -1 : 1;
    
    393
    +          break;
    
    394
    +        default:
    
    395
    +          return;
    
    396
    +      }
    
    397
    +
    
    398
    +      // Prevent scrolling the nearest scroll container.
    
    399
    +      event.preventDefault();
    
    400
    +
    
    401
    +      const curCell = this._focusCell;
    
    402
    +      let row = curCell ? curCell.row.index + moveRow : 0;
    
    403
    +      let column = curCell ? curCell.columnIndex + moveColumn : 0;
    
    404
    +
    
    405
    +      // Clamp in bounds.
    
    406
    +      if (row < 0) {
    
    407
    +        row = 0;
    
    408
    +      } else if (row >= numRows) {
    
    409
    +        row = numRows - 1;
    
    410
    +      }
    
    411
    +
    
    412
    +      const numCells = this._rows[row].cells.length;
    
    413
    +      if (column < 0) {
    
    414
    +        column = 0;
    
    415
    +      } else if (column >= numCells) {
    
    416
    +        column = numCells - 1;
    
    417
    +      }
    
    418
    +
    
    419
    +      const newCell = this._rows[row].cells[column];
    
    420
    +
    
    421
    +      if (newCell !== curCell) {
    
    422
    +        this._setFocus(newCell);
    
    423
    +      }
    
    424
    +    } else if (event.type === "mousedown") {
    
    425
    +      if (event.button !== 0) {
    
    426
    +        return;
    
    427
    +      }
    
    428
    +      // Move focus index to the clicked target.
    
    429
    +      // NOTE: Since the cells and the grid have "tabindex=-1", they are still
    
    430
    +      // click-focusable. Therefore, the default mousedown handler will try to
    
    431
    +      // move focus to it.
    
    432
    +      // Rather than block this default handler, we instead re-direct the focus
    
    433
    +      // to the correct cell in the "focusin" listener.
    
    434
    +      const newCell = this._getCellFromTarget(event.target);
    
    435
    +      // NOTE: If newCell is null, then we do nothing here, but instead wait for
    
    436
    +      // the focusin handler to trigger.
    
    437
    +      if (newCell && newCell !== this._focusCell) {
    
    438
    +        this._setFocus(newCell);
    
    439
    +      }
    
    440
    +    } else if (event.type === "focusin") {
    
    441
    +      const focusCell = this._getCellFromTarget(event.target);
    
    442
    +      if (focusCell !== this._focusCell) {
    
    443
    +        // Focus is not where it is expected.
    
    444
    +        // E.g. the user has clicked the edge of the grid.
    
    445
    +        // Restore focus immediately back to the cell we expect.
    
    446
    +        this._setFocus(this._focusCell);
    
    447
    +      }
    
    448
    +    }
    
    449
    +  },
    
    450
    +
    
    451
    +  /**
    
    452
    +   * Return the cell that was the target of an event.
    
    453
    +   *
    
    454
    +   * @param {Element} element - The target of an event.
    
    455
    +   *
    
    456
    +   * @returns {BridgeGridCell?} - The cell that the element belongs to, or null
    
    457
    +   *   if it doesn't belong to any cell.
    
    458
    +   */
    
    459
    +  _getCellFromTarget(element) {
    
    460
    +    for (const row of this._rows) {
    
    461
    +      for (const cell of row.cells) {
    
    462
    +        if (cell.element.contains(element)) {
    
    463
    +          return cell;
    
    464
    +        }
    
    465
    +      }
    
    466
    +    }
    
    467
    +    return null;
    
    468
    +  },
    
    469
    +
    
    470
    +  /**
    
    471
    +   * Determine whether the document's active element (focus) is within the grid
    
    472
    +   * or not.
    
    473
    +   *
    
    474
    +   * @returns {boolean} - Whether focus is within this grid or not.
    
    475
    +   */
    
    476
    +  _focusWithin() {
    
    477
    +    return this._grid.contains(document.activeElement);
    
    478
    +  },
    
    479
    +
    
    480
    +  /**
    
    481
    +   * Set the cell that should be the focus target of the grid, possibly moving
    
    482
    +   * the document's focus as well.
    
    483
    +   *
    
    484
    +   * @param {BridgeGridCell?} cell - The cell to make the focus target, or null
    
    485
    +   *   if the grid itself should be the target.
    
    486
    +   * @param {boolean} [focusWithin] - Whether focus should be moved within the
    
    487
    +   *   grid. If undefined, this will move focus if the grid currently contains
    
    488
    +   *   the document's focus.
    
    489
    +   */
    
    490
    +  _setFocus(cell, focusWithin) {
    
    491
    +    if (focusWithin === undefined) {
    
    492
    +      focusWithin = this._focusWithin();
    
    493
    +    }
    
    494
    +    const prevFocusElement = this._focusCell
    
    495
    +      ? this._focusCell.focusEl
    
    496
    +      : this._grid;
    
    497
    +    const newFocusElement = cell ? cell.focusEl : this._grid;
    
    498
    +
    
    499
    +    if (prevFocusElement !== newFocusElement) {
    
    500
    +      prevFocusElement.tabIndex = -1;
    
    501
    +      newFocusElement.tabIndex = 0;
    
    502
    +    }
    
    503
    +    // Set _focusCell now, before we potentially call "focus", which can trigger
    
    504
    +    // the "focusin" handler.
    
    505
    +    this._focusCell = cell;
    
    506
    +
    
    507
    +    if (focusWithin) {
    
    508
    +      // Focus was within the grid, so we need to actively move it to the new
    
    509
    +      // element.
    
    510
    +      newFocusElement.focus({ preventScroll: true });
    
    511
    +      // Scroll to the whole cell into view, rather than just the focus element.
    
    512
    +      (cell?.element ?? newFocusElement).scrollIntoView({
    
    513
    +        block: "nearest",
    
    514
    +        inline: "nearest",
    
    515
    +      });
    
    516
    +    }
    
    517
    +  },
    
    518
    +
    
    519
    +  /**
    
    520
    +   * Reset the grids focus to be the first row's first cell, if any.
    
    521
    +   *
    
    522
    +   * @param {boolean} [focusWithin] - Whether focus should be moved within the
    
    523
    +   *   grid. If undefined, this will move focus if the grid currently contains
    
    524
    +   *   the document's focus.
    
    525
    +   */
    
    526
    +  _resetFocus(focusWithin) {
    
    527
    +    this._setFocus(
    
    528
    +      this._rows.length ? this._rows[0].cells[0] : null,
    
    529
    +      focusWithin
    
    530
    +    );
    
    531
    +  },
    
    532
    +
    
    533
    +  /**
    
    534
    +   * The bridge ID/fingerprint of the most recently used bridge (appearing in
    
    535
    +   * the latest Tor circuit). Roughly corresponds to the bridge we are currently
    
    536
    +   * connected to.
    
    537
    +   *
    
    538
    +   * null if there are no such bridges.
    
    539
    +   *
    
    540
    +   * @type {string?}
    
    541
    +   */
    
    542
    +  _connectedBridgeId: null,
    
    543
    +  /**
    
    544
    +   * Update _connectedBridgeId.
    
    545
    +   */
    
    546
    +  async _updateConnectedBridge() {
    
    547
    +    const bridgeId = await getConnectedBridgeId();
    
    548
    +    if (bridgeId === this._connectedBridgeId) {
    
    549
    +      return;
    
    550
    +    }
    
    551
    +    this._connectedBridgeId = bridgeId;
    
    552
    +    for (const row of this._rows) {
    
    553
    +      this._updateRowStatus(row);
    
    554
    +    }
    
    555
    +  },
    
    556
    +
    
    557
    +  /**
    
    558
    +   * Update the status of a row.
    
    559
    +   *
    
    560
    +   * @param {BridgeGridRow} row - The row to update.
    
    561
    +   */
    
    562
    +  _updateRowStatus(row) {
    
    563
    +    const connected = row.bridgeId && this._connectedBridgeId === row.bridgeId;
    
    564
    +    // NOTE: row.connected is initially undefined, so won't match `connected`.
    
    565
    +    if (connected === row.connected) {
    
    566
    +      return;
    
    567
    +    }
    
    568
    +
    
    569
    +    row.connected = connected;
    
    570
    +
    
    571
    +    const noStatus = !connected;
    
    572
    +
    
    573
    +    row.element.classList.toggle("hide-status", noStatus);
    
    574
    +    row.statusEl.classList.toggle("bridge-status-none", noStatus);
    
    575
    +    row.statusEl.classList.toggle("bridge-status-connected", connected);
    
    576
    +
    
    577
    +    if (connected) {
    
    578
    +      document.l10n.setAttributes(
    
    579
    +        row.statusText,
    
    580
    +        "tor-bridges-status-connected"
    
    581
    +      );
    
    582
    +    } else {
    
    583
    +      document.l10n.setAttributes(row.statusText, "tor-bridges-status-none");
    
    584
    +    }
    
    585
    +  },
    
    586
    +
    
    587
    +  /**
    
    588
    +   * The language code for emoji annotations.
    
    589
    +   *
    
    590
    +   * null if unset.
    
    591
    +   *
    
    592
    +   * @type {string?}
    
    593
    +   */
    
    594
    +  _emojiLangCode: null,
    
    595
    +  /**
    
    596
    +   * A promise that resolves to two JSON structures for bridge-emojis.json and
    
    597
    +   * annotations.json, respectively.
    
    598
    +   *
    
    599
    +   * @type {Promise}
    
    600
    +   */
    
    601
    +  _emojiPromise: Promise.all([
    
    602
    +    fetch(
    
    603
    +      "chrome://browser/content/torpreferences/bridgemoji/bridge-emojis.json"
    
    604
    +    ).then(response => response.json()),
    
    605
    +    fetch(
    
    606
    +      "chrome://browser/content/torpreferences/bridgemoji/annotations.json"
    
    607
    +    ).then(response => response.json()),
    
    608
    +  ]),
    
    609
    +
    
    610
    +  /**
    
    611
    +   * Update _emojiLangCode.
    
    612
    +   */
    
    613
    +  async _updateEmojiLangCode() {
    
    614
    +    let langCode;
    
    615
    +    const emojiAnnotations = (await this._emojiPromise)[1];
    
    616
    +    // Find the first desired locale we have annotations for.
    
    617
    +    // Add "en" as a fallback.
    
    618
    +    for (const bcp47 of [...Services.locale.appLocalesAsBCP47, "en"]) {
    
    619
    +      langCode = bcp47;
    
    620
    +      if (langCode in emojiAnnotations) {
    
    621
    +        break;
    
    622
    +      }
    
    623
    +      // Remove everything after the dash, if there is one.
    
    624
    +      langCode = bcp47.replace(/-.*/, "");
    
    625
    +      if (langCode in emojiAnnotations) {
    
    626
    +        break;
    
    627
    +      }
    
    628
    +    }
    
    629
    +    if (langCode !== this._emojiLangCode) {
    
    630
    +      this._emojiLangCode = langCode;
    
    631
    +      for (const row of this._rows) {
    
    632
    +        this._updateRowEmojis(row);
    
    633
    +      }
    
    634
    +    }
    
    635
    +  },
    
    636
    +
    
    637
    +  /**
    
    638
    +   * Update the bridge emojis to show their corresponding emoji with an
    
    639
    +   * annotation that matches the current locale.
    
    640
    +   *
    
    641
    +   * @param {BridgeGridRow} row - The row to update the emojis of.
    
    642
    +   */
    
    643
    +  async _updateRowEmojis(row) {
    
    644
    +    if (!this._emojiLangCode) {
    
    645
    +      // No lang code yet, wait until it is updated.
    
    646
    +      return;
    
    647
    +    }
    
    648
    +
    
    649
    +    const [emojiList, emojiAnnotations] = await this._emojiPromise;
    
    650
    +    const unknownString = await document.l10n.formatValue(
    
    651
    +      "tor-bridges-emoji-unknown"
    
    652
    +    );
    
    653
    +
    
    654
    +    for (const { cell, img, index } of row.emojis) {
    
    655
    +      const emoji = emojiList[index];
    
    656
    +      let emojiName;
    
    657
    +      if (!emoji) {
    
    658
    +        // Unexpected.
    
    659
    +        img.removeAttribute("src");
    
    660
    +      } else {
    
    661
    +        const cp = emoji.codePointAt(0).toString(16);
    
    662
    +        img.setAttribute(
    
    663
    +          "src",
    
    664
    +          `chrome://browser/content/torpreferences/bridgemoji/svgs/${cp}.svg`
    
    665
    +        );
    
    666
    +        emojiName = emojiAnnotations[this._emojiLangCode][cp];
    
    667
    +      }
    
    668
    +      if (!emojiName) {
    
    669
    +        console.error(`No emoji for index ${index}`);
    
    670
    +        emojiName = unknownString;
    
    671
    +      }
    
    672
    +      document.l10n.setAttributes(cell, "tor-bridges-emoji-cell", {
    
    673
    +        emojiName,
    
    674
    +      });
    
    675
    +    }
    
    676
    +  },
    
    677
    +
    
    678
    +  /**
    
    679
    +   * Create a new row for the grid.
    
    680
    +   *
    
    681
    +   * @param {string} bridgeLine - The bridge line for this row, which also acts
    
    682
    +   *   as its ID.
    
    683
    +   *
    
    684
    +   * @returns {BridgeGridRow} - A new row, with then "index" unset and the
    
    685
    +   *   "element" without a parent.
    
    686
    +   */
    
    687
    +  _createRow(bridgeLine) {
    
    688
    +    let details;
    
    689
    +    try {
    
    690
    +      details = TorParsers.parseBridgeLine(bridgeLine);
    
    691
    +    } catch (e) {
    
    692
    +      console.error(`Detected invalid bridge line: ${bridgeLine}`, e);
    
    693
    +    }
    
    694
    +    const row = {
    
    695
    +      element: this._rowTemplate.content.children[0].cloneNode(true),
    
    696
    +      bridgeLine,
    
    697
    +      bridgeId: details?.id ?? null,
    
    698
    +      cells: [],
    
    699
    +    };
    
    700
    +
    
    701
    +    const emojiBlock = row.element.querySelector(".tor-bridges-emojis-block");
    
    702
    +    row.emojis = makeBridgeId(bridgeLine).map(index => {
    
    703
    +      const cell = document.createElement("span");
    
    704
    +      // Each emoji is its own cell, we rely on the fact that makeBridgeId
    
    705
    +      // always returns four indices.
    
    706
    +      cell.setAttribute("role", "gridcell");
    
    707
    +      cell.classList.add("tor-bridges-grid-cell", "tor-bridges-emoji-cell");
    
    708
    +
    
    709
    +      const img = document.createElement("img");
    
    710
    +      img.classList.add("tor-bridges-emoji-icon");
    
    711
    +      // Accessible name will be set on the cell itself.
    
    712
    +      img.setAttribute("alt", "");
    
    713
    +
    
    714
    +      cell.appendChild(img);
    
    715
    +      emojiBlock.appendChild(cell);
    
    716
    +      // Image and text is set in _updateRowEmojis.
    
    717
    +      return { cell, img, index };
    
    718
    +    });
    
    719
    +
    
    720
    +    for (const [columnIndex, element] of row.element
    
    721
    +      .querySelectorAll(".tor-bridges-grid-cell")
    
    722
    +      .entries()) {
    
    723
    +      const focusEl =
    
    724
    +        element.querySelector(".tor-bridges-grid-focus") ?? element;
    
    725
    +      // Set a negative tabIndex, this makes the element click-focusable but not
    
    726
    +      // part of the tab navigation sequence.
    
    727
    +      focusEl.tabIndex = -1;
    
    728
    +      row.cells.push({ element, focusEl, columnIndex, row });
    
    729
    +    }
    
    730
    +
    
    731
    +    // TODO: properly handle "vanilla" bridges?
    
    732
    +    document.l10n.setAttributes(
    
    733
    +      row.element.querySelector(".tor-bridges-type-cell"),
    
    734
    +      "tor-bridges-type-prefix",
    
    735
    +      { type: details?.transport ?? "vanilla" }
    
    736
    +    );
    
    737
    +
    
    738
    +    row.element.querySelector(".tor-bridges-address-cell").textContent =
    
    739
    +      bridgeLine;
    
    740
    +
    
    741
    +    row.statusEl = row.element.querySelector(
    
    742
    +      ".tor-bridges-status-cell .bridge-status-badge"
    
    743
    +    );
    
    744
    +    row.statusText = row.element.querySelector(".tor-bridges-status-cell-text");
    
    745
    +
    
    746
    +    this._initRowMenu(row);
    
    747
    +
    
    748
    +    this._updateRowStatus(row);
    
    749
    +    this._updateRowEmojis(row);
    
    750
    +    return row;
    
    751
    +  },
    
    752
    +
    
    753
    +  /**
    
    754
    +   * The row menu index used for generating new ids.
    
    755
    +   *
    
    756
    +   * @type {integer}
    
    757
    +   */
    
    758
    +  _rowMenuIndex: 0,
    
    759
    +  /**
    
    760
    +   * Generate a new id for the options menu.
    
    761
    +   *
    
    762
    +   * @returns {string} - The new id.
    
    763
    +   */
    
    764
    +  _generateRowMenuId() {
    
    765
    +    const id = `tor-bridges-individual-options-menu-${this._rowMenuIndex}`;
    
    766
    +    // Assume we won't run out of ids.
    
    767
    +    this._rowMenuIndex++;
    
    768
    +    return id;
    
    769
    +  },
    
    770
    +
    
    771
    +  /**
    
    772
    +   * Initialize the shared menu for a row.
    
    773
    +   *
    
    774
    +   * @param {BridgeGridRow} row - The row to initialize the menu of.
    
    775
    +   */
    
    776
    +  _initRowMenu(row) {
    
    777
    +    row.menu = row.element.querySelector(
    
    778
    +      ".tor-bridges-individual-options-menu"
    
    779
    +    );
    
    780
    +    row.optionsButton = row.element.querySelector(
    
    781
    +      ".tor-bridges-options-cell-button"
    
    782
    +    );
    
    783
    +
    
    784
    +    row.menu.id = this._generateRowMenuId();
    
    785
    +    row.optionsButton.setAttribute("aria-controls", row.menu.id);
    
    786
    +
    
    787
    +    row.optionsButton.addEventListener("click", event => {
    
    788
    +      row.menu.toggle(event);
    
    789
    +    });
    
    790
    +
    
    791
    +    row.menu.addEventListener("hidden", () => {
    
    792
    +      // Make sure the button receives focus again when the menu is hidden.
    
    793
    +      // Currently, panel-list.js only does this when the menu is opened with a
    
    794
    +      // keyboard, but this causes focus to be lost from the page if the user
    
    795
    +      // uses a mixture of keyboard and mouse.
    
    796
    +      row.optionsButton.focus();
    
    797
    +    });
    
    798
    +
    
    799
    +    row.menu
    
    800
    +      .querySelector(".tor-bridges-options-qr-one-menu-item")
    
    801
    +      .addEventListener("click", () => {
    
    802
    +        const bridgeLine = row.bridgeLine;
    
    803
    +        if (!bridgeLine) {
    
    804
    +          return;
    
    805
    +        }
    
    806
    +        const dialog = new BridgeQrDialog();
    
    807
    +        dialog.openDialog(gSubDialog, bridgeLine);
    
    808
    +      });
    
    809
    +    row.menu
    
    810
    +      .querySelector(".tor-bridges-options-copy-one-menu-item")
    
    811
    +      .addEventListener("click", () => {
    
    812
    +        const clipboard = Cc[
    
    813
    +          "@mozilla.org/widget/clipboardhelper;1"
    
    814
    +        ].getService(Ci.nsIClipboardHelper);
    
    815
    +        clipboard.copyString(row.bridgeLine);
    
    816
    +      });
    
    817
    +    row.menu
    
    818
    +      .querySelector(".tor-bridges-options-remove-one-menu-item")
    
    819
    +      .addEventListener("click", () => {
    
    820
    +        const bridgeLine = row.bridgeLine;
    
    821
    +        const strings = TorSettings.bridges.bridge_strings;
    
    822
    +        const index = strings.indexOf(bridgeLine);
    
    823
    +        if (index === -1) {
    
    824
    +          return;
    
    825
    +        }
    
    826
    +        strings.splice(index, 1);
    
    827
    +
    
    828
    +        setTorSettings(() => {
    
    829
    +          TorSettings.bridges.bridge_strings = strings;
    
    830
    +        });
    
    831
    +      });
    
    832
    +  },
    
    833
    +
    
    834
    +  /**
    
    835
    +   * Force the row menu to close.
    
    836
    +   */
    
    837
    +  _forceCloseRowMenus() {
    
    838
    +    for (const row of this._rows) {
    
    839
    +      row.menu.hide(null, { force: true });
    
    840
    +    }
    
    841
    +  },
    
    842
    +
    
    843
    +  /**
    
    844
    +   * The known bridge source.
    
    845
    +   *
    
    846
    +   * Initially null to indicate that it is unset.
    
    847
    +   *
    
    848
    +   * @type {integer?}
    
    849
    +   */
    
    850
    +  _bridgeSource: null,
    
    851
    +  /**
    
    852
    +   * The bridge sources this is shown for.
    
    853
    +   *
    
    854
    +   * @type {string[]}
    
    855
    +   */
    
    856
    +  _supportedSources: [TorBridgeSource.BridgeDB, TorBridgeSource.UserProvided],
    
    857
    +
    
    858
    +  /**
    
    859
    +   * Update the grid to show the latest bridge strings.
    
    860
    +   *
    
    861
    +   * @param {boolean} [initializing=false] - Whether this is being called as
    
    862
    +   *   part of initialization.
    
    863
    +   */
    
    864
    +  _updateRows(initializing = false) {
    
    865
    +    // Store whether we have focus within the grid, before removing or hiding
    
    866
    +    // DOM elements.
    
    867
    +    const focusWithin = this._focusWithin();
    
    868
    +
    
    869
    +    let lostAllBridges = false;
    
    870
    +    let newSource = false;
    
    871
    +    const bridgeSource = TorSettings.bridges.source;
    
    872
    +    if (bridgeSource !== this._bridgeSource) {
    
    873
    +      newSource = true;
    
    874
    +
    
    875
    +      this._bridgeSource = bridgeSource;
    
    876
    +
    
    877
    +      if (this._supportedSources.includes(bridgeSource)) {
    
    878
    +        this.activate();
    
    879
    +      } else {
    
    880
    +        if (this._active && bridgeSource === TorBridgeSource.Invalid) {
    
    881
    +          lostAllBridges = true;
    
    882
    +        }
    
    883
    +        this.deactivate();
    
    884
    +      }
    
    885
    +    }
    
    886
    +
    
    887
    +    const ordered = this._active
    
    888
    +      ? TorSettings.bridges.bridge_strings.map(bridgeLine => {
    
    889
    +          const row = this._rows.find(r => r.bridgeLine === bridgeLine);
    
    890
    +          if (row) {
    
    891
    +            return row;
    
    892
    +          }
    
    893
    +          return this._createRow(bridgeLine);
    
    894
    +        })
    
    895
    +      : [];
    
    896
    +
    
    897
    +    // Whether we should reset the grid's focus.
    
    898
    +    // We always reset when we have a new bridge source.
    
    899
    +    // We reset the focus if no current Cell has focus. I.e. when adding a row
    
    900
    +    // to an empty grid, we want the focus to move to the first item.
    
    901
    +    // We also reset the focus if the current Cell is in a row that will be
    
    902
    +    // removed (including if all rows are removed).
    
    903
    +    // NOTE: In principle, if a row is removed, we could move the focus to the
    
    904
    +    // next or previous row (in the same cell column). However, most likely if
    
    905
    +    // the grid has the user focus, they are removing a single row using its
    
    906
    +    // options button. In this case, returning the user to some other row's
    
    907
    +    // options button might be more disorienting since it would not be simple
    
    908
    +    // for them to know *which* bridge they have landed on.
    
    909
    +    // NOTE: We do not reset the focus in other cases because we do not want the
    
    910
    +    // user to loose their place in the grid unnecessarily.
    
    911
    +    let resetFocus =
    
    912
    +      newSource || !this._focusCell || !ordered.includes(this._focusCell.row);
    
    913
    +
    
    914
    +    // Remove rows no longer needed from the DOM.
    
    915
    +    let numRowsRemoved = 0;
    
    916
    +    let rowAddedOrMoved = false;
    
    917
    +
    
    918
    +    for (const row of this._rows) {
    
    919
    +      if (!ordered.includes(row)) {
    
    920
    +        numRowsRemoved++;
    
    921
    +        // If the row menu was open, it will also be deleted.
    
    922
    +        // NOTE: Since the row menu is part of the row, focusWithin will be true
    
    923
    +        // if the menu had focus, so focus should be re-assigned.
    
    924
    +        row.element.remove();
    
    925
    +      }
    
    926
    +    }
    
    927
    +
    
    928
    +    // Go through all the rows to set their ".index" property and to ensure they
    
    929
    +    // are in the correct position in the DOM.
    
    930
    +    // NOTE: We could use replaceChildren to get the correct DOM structure, but
    
    931
    +    // we want to avoid rebuilding the entire tree when a single row is added or
    
    932
    +    // removed.
    
    933
    +    for (const [index, row] of ordered.entries()) {
    
    934
    +      row.index = index;
    
    935
    +      const element = row.element;
    
    936
    +      // Get the expected previous element, that should already be in the DOM
    
    937
    +      // from the previous loop.
    
    938
    +      const prevEl = index ? ordered[index - 1].element : null;
    
    939
    +
    
    940
    +      if (
    
    941
    +        element.parentElement === this._grid &&
    
    942
    +        prevEl === element.previousElementSibling
    
    943
    +      ) {
    
    944
    +        // Already in the correct position in the DOM.
    
    945
    +        continue;
    
    946
    +      }
    
    947
    +
    
    948
    +      rowAddedOrMoved = true;
    
    949
    +      // NOTE: Any elements already in the DOM, but not in the correct position
    
    950
    +      // will be removed and re-added by the below command.
    
    951
    +      // NOTE: if the row has document focus, then it should remain there.
    
    952
    +      if (prevEl) {
    
    953
    +        prevEl.after(element);
    
    954
    +      } else {
    
    955
    +        this._grid.prepend(element);
    
    956
    +      }
    
    957
    +    }
    
    958
    +    this._rows = ordered;
    
    959
    +
    
    960
    +    // Restore any lost focus.
    
    961
    +    if (resetFocus) {
    
    962
    +      // If we are not active (and therefore hidden), we will not try and move
    
    963
    +      // focus (activeElement), but may still change the *focusable* element for
    
    964
    +      // when we are shown again.
    
    965
    +      this._resetFocus(this._active && focusWithin);
    
    966
    +    }
    
    967
    +    if (!this._active && focusWithin) {
    
    968
    +      // Move focus out of this element, which has been hidden.
    
    969
    +      gBridgeSettings.takeFocus();
    
    970
    +    }
    
    971
    +
    
    972
    +    // Notify the user if there was some change to the DOM.
    
    973
    +    // If we are initializing, we generate no notification since there has been
    
    974
    +    // no change in the setting.
    
    975
    +    if (!initializing) {
    
    976
    +      let notificationType;
    
    977
    +      if (lostAllBridges) {
    
    978
    +        // Just lost all bridges, and became de-active.
    
    979
    +        notificationType = "removed-all";
    
    980
    +      } else if (this._rows.length) {
    
    981
    +        // Otherwise, only generate a notification if we are still active, with
    
    982
    +        // at least one bridge.
    
    983
    +        // I.e. do not generate a message if the new source is "builtin".
    
    984
    +        if (newSource) {
    
    985
    +          // A change in source.
    
    986
    +          notificationType = "changed";
    
    987
    +        } else if (numRowsRemoved === 1 && !rowAddedOrMoved) {
    
    988
    +          // Only one bridge was removed. This is most likely in response to them
    
    989
    +          // manually removing a single bridge or using the bridge row's options
    
    990
    +          // menu.
    
    991
    +          notificationType = "removed-one";
    
    992
    +        } else if (numRowsRemoved || rowAddedOrMoved) {
    
    993
    +          // Some other change. This is most likely in response to a manual edit
    
    994
    +          // of the existing bridges.
    
    995
    +          notificationType = "changed";
    
    996
    +        }
    
    997
    +        // Else, there was no change.
    
    998
    +      }
    
    999
    +
    
    1000
    +      if (notificationType) {
    
    1001
    +        gBridgesNotification.post(notificationType);
    
    1002
    +      }
    
    1003
    +    }
    
    1004
    +  },
    
    1005
    +};
    
    1006
    +
    
    1007
    +/**
    
    1008
    + * Controls the built-in bridges area.
    
    1009
    + */
    
    1010
    +const gBuiltinBridgesArea = {
    
    1011
    +  /**
    
    1012
    +   * The display area.
    
    1013
    +   *
    
    1014
    +   * @type {Element?}
    
    1015
    +   */
    
    1016
    +  _area: null,
    
    1017
    +  /**
    
    1018
    +   * The type name element.
    
    1019
    +   *
    
    1020
    +   * @type {Element?}
    
    1021
    +   */
    
    1022
    +  _nameEl: null,
    
    1023
    +  /**
    
    1024
    +   * The bridge type description element.
    
    1025
    +   *
    
    1026
    +   * @type {Element?}
    
    1027
    +   */
    
    1028
    +  _descriptionEl: null,
    
    1029
    +  /**
    
    1030
    +   * The connection status.
    
    1031
    +   *
    
    1032
    +   * @type {Element?}
    
    1033
    +   */
    
    1034
    +  _connectionStatusEl: null,
    
    1035
    +
    
    1036
    +  /**
    
    1037
    +   * Initialize the built-in bridges area.
    
    1038
    +   */
    
    1039
    +  init() {
    
    1040
    +    this._area = document.getElementById("tor-bridges-built-in-display");
    
    1041
    +    this._nameEl = document.getElementById("tor-bridges-built-in-type-name");
    
    1042
    +    this._descriptionEl = document.getElementById(
    
    1043
    +      "tor-bridges-built-in-description"
    
    1044
    +    );
    
    1045
    +    this._connectionStatusEl = document.getElementById(
    
    1046
    +      "tor-bridges-built-in-connected"
    
    1047
    +    );
    
    1048
    +
    
    1049
    +    Services.obs.addObserver(this, TorSettingsTopics.SettingsChanged);
    
    1050
    +
    
    1051
    +    // NOTE: Before initializedPromise completes, this area is hidden.
    
    1052
    +    TorSettings.initializedPromise.then(() => {
    
    1053
    +      this._updateBridgeType(true);
    
    1054
    +    });
    
    1055
    +  },
    
    1056
    +
    
    1057
    +  /**
    
    1058
    +   * Uninitialize the built-in bridges area.
    
    1059
    +   */
    
    1060
    +  uninit() {
    
    1061
    +    Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged);
    
    1062
    +    this.deactivate();
    
    1063
    +  },
    
    1064
    +
    
    1065
    +  /**
    
    1066
    +   * Whether the built-in area is visible and responsive.
    
    1067
    +   *
    
    1068
    +   * @type {boolean}
    
    1069
    +   */
    
    1070
    +  _active: false,
    
    1071
    +
    
    1072
    +  /**
    
    1073
    +   * Activate and show the built-in bridge area.
    
    1074
    +   */
    
    1075
    +  activate() {
    
    1076
    +    if (this._active) {
    
    1077
    +      return;
    
    1078
    +    }
    
    1079
    +    this._active = true;
    
    1080
    +
    
    1081
    +    Services.obs.addObserver(this, TorProviderTopics.BridgeChanged);
    
    1082
    +
    
    1083
    +    this._area.classList.add("built-in-active");
    
    1084
    +
    
    1085
    +    this._updateBridgeIds();
    
    1086
    +    this._updateConnectedBridge();
    
    1087
    +  },
    
    1088
    +
    
    1089
    +  /**
    
    1090
    +   * Deactivate and hide built-in bridge area.
    
    1091
    +   */
    
    1092
    +  deactivate() {
    
    1093
    +    if (!this._active) {
    
    1094
    +      return;
    
    1095
    +    }
    
    1096
    +    this._active = false;
    
    1097
    +
    
    1098
    +    this._area.classList.remove("built-in-active");
    
    1099
    +
    
    1100
    +    Services.obs.removeObserver(this, TorProviderTopics.BridgeChanged);
    
    1101
    +  },
    
    1102
    +
    
    1103
    +  observe(subject, topic, data) {
    
    1104
    +    switch (topic) {
    
    1105
    +      case TorSettingsTopics.SettingsChanged:
    
    1106
    +        const { changes } = subject.wrappedJSObject;
    
    1107
    +        if (
    
    1108
    +          changes.includes("bridges.source") ||
    
    1109
    +          changes.includes("bridges.builtin_type")
    
    1110
    +        ) {
    
    1111
    +          this._updateBridgeType();
    
    1112
    +        }
    
    1113
    +        if (changes.includes("bridges.bridge_strings")) {
    
    1114
    +          this._updateBridgeIds();
    
    1115
    +        }
    
    1116
    +        break;
    
    1117
    +      case TorProviderTopics.BridgeChanged:
    
    1118
    +        this._updateConnectedBridge();
    
    1119
    +        break;
    
    1120
    +    }
    
    1121
    +  },
    
    1122
    +
    
    1123
    +  /**
    
    1124
    +   * Updates the shown connected state.
    
    1125
    +   */
    
    1126
    +  _updateConnectedState() {
    
    1127
    +    this._connectionStatusEl.classList.toggle(
    
    1128
    +      "bridge-status-connected",
    
    1129
    +      this._bridgeType &&
    
    1130
    +        this._connectedBridgeId &&
    
    1131
    +        this._bridgeIds.includes(this._connectedBridgeId)
    
    1132
    +    );
    
    1133
    +  },
    
    1134
    +
    
    1135
    +  /**
    
    1136
    +   * The currently shown bridge type. Empty if deactivated, and null if
    
    1137
    +   * uninitialized.
    
    1138
    +   *
    
    1139
    +   * @type {string?}
    
    1140
    +   */
    
    1141
    +  _bridgeType: null,
    
    1142
    +  /**
    
    1143
    +   * The strings for each known bridge type.
    
    1144
    +   *
    
    1145
    +   * @type {Object<string,object>}
    
    1146
    +   */
    
    1147
    +  _bridgeTypeStrings: {
    
    1148
    +    // TODO: Change to Fluent ids.
    
    1149
    +    obfs4: {
    
    1150
    +      name: TorStrings.settings.builtinBridgeObfs4Title,
    
    1151
    +      description: TorStrings.settings.builtinBridgeObfs4Description2,
    
    1152
    +    },
    
    1153
    +    snowflake: {
    
    1154
    +      name: TorStrings.settings.builtinBridgeSnowflake,
    
    1155
    +      description: TorStrings.settings.builtinBridgeSnowflakeDescription2,
    
    1156
    +    },
    
    1157
    +    "meek-azure": {
    
    1158
    +      name: TorStrings.settings.builtinBridgeMeekAzure,
    
    1159
    +      description: TorStrings.settings.builtinBridgeMeekAzureDescription2,
    
    1160
    +    },
    
    1161
    +  },
    
    1162
    +
    
    1163
    +  /**
    
    1164
    +   * The known bridge source.
    
    1165
    +   *
    
    1166
    +   * Initially null to indicate that it is unset.
    
    1167
    +   *
    
    1168
    +   * @type {integer?}
    
    1169
    +   */
    
    1170
    +  _bridgeSource: null,
    
    1171
    +
    
    1172
    +  /**
    
    1173
    +   * Update the shown bridge type.
    
    1174
    +   *
    
    1175
    +   * @param {boolean} [initializing=false] - Whether this is being called as
    
    1176
    +   *   part of initialization.
    
    1177
    +   */
    
    1178
    +  async _updateBridgeType(initializing = false) {
    
    1179
    +    let lostAllBridges = false;
    
    1180
    +    let newSource = false;
    
    1181
    +    const bridgeSource = TorSettings.bridges.source;
    
    1182
    +    if (bridgeSource !== this._bridgeSource) {
    
    1183
    +      newSource = true;
    
    1184
    +
    
    1185
    +      this._bridgeSource = bridgeSource;
    
    1186
    +
    
    1187
    +      if (bridgeSource === TorBridgeSource.BuiltIn) {
    
    1188
    +        this.activate();
    
    1189
    +      } else {
    
    1190
    +        if (this._active && bridgeSource === TorBridgeSource.Invalid) {
    
    1191
    +          lostAllBridges = true;
    
    1192
    +        }
    
    1193
    +        const hadFocus = this._area.contains(document.activeElement);
    
    1194
    +        this.deactivate();
    
    1195
    +        if (hadFocus) {
    
    1196
    +          gBridgeSettings.takeFocus();
    
    1197
    +        }
    
    1198
    +      }
    
    1199
    +    }
    
    1200
    +
    
    1201
    +    const bridgeType = this._active ? TorSettings.bridges.builtin_type : "";
    
    1202
    +
    
    1203
    +    let newType = false;
    
    1204
    +    if (bridgeType !== this._bridgeType) {
    
    1205
    +      newType = true;
    
    1206
    +
    
    1207
    +      this._bridgeType = bridgeType;
    
    1208
    +
    
    1209
    +      const bridgeStrings = this._bridgeTypeStrings[bridgeType];
    
    1210
    +      if (bridgeStrings) {
    
    1211
    +        /*
    
    1212
    +        document.l10n.setAttributes(this._nameEl, bridgeStrings.name);
    
    1213
    +        document.l10n.setAttributes(
    
    1214
    +          this._descriptionEl,
    
    1215
    +          bridgeStrings.description
    
    1216
    +        );
    
    1217
    +        */
    
    1218
    +        this._nameEl.textContent = bridgeStrings.name;
    
    1219
    +        this._descriptionEl.textContent = bridgeStrings.description;
    
    1220
    +      } else {
    
    1221
    +        // Unknown type, or no type.
    
    1222
    +        this._nameEl.removeAttribute("data-l10n-id");
    
    1223
    +        this._nameEl.textContent = bridgeType;
    
    1224
    +        this._descriptionEl.removeAttribute("data-l10n-id");
    
    1225
    +        this._descriptionEl.textContent = "";
    
    1226
    +      }
    
    1227
    +
    
    1228
    +      this._updateConnectedState();
    
    1229
    +    }
    
    1230
    +
    
    1231
    +    // Notify the user if there was some change to the type.
    
    1232
    +    // If we are initializing, we generate no notification since there has been
    
    1233
    +    // no change in the setting.
    
    1234
    +    if (!initializing) {
    
    1235
    +      let notificationType;
    
    1236
    +      if (lostAllBridges) {
    
    1237
    +        // Just lost all bridges, and became de-active.
    
    1238
    +        notificationType = "removed-all";
    
    1239
    +      } else if (this._active && (newSource || newType)) {
    
    1240
    +        // Otherwise, only generate a notification if we are still active, with
    
    1241
    +        // a bridge type.
    
    1242
    +        // I.e. do not generate a message if the new source is not "builtin".
    
    1243
    +        notificationType = "changed";
    
    1244
    +      }
    
    1245
    +
    
    1246
    +      if (notificationType) {
    
    1247
    +        gBridgesNotification.post(notificationType);
    
    1248
    +      }
    
    1249
    +    }
    
    1250
    +  },
    
    1251
    +
    
    1252
    +  /**
    
    1253
    +   * The bridge IDs/fingerprints for the built-in bridges.
    
    1254
    +   *
    
    1255
    +   * @type {Array<string>}
    
    1256
    +   */
    
    1257
    +  _bridgeIds: [],
    
    1258
    +  /**
    
    1259
    +   * Update _bridgeIds
    
    1260
    +   */
    
    1261
    +  _updateBridgeIds() {
    
    1262
    +    this._bridgeIds = [];
    
    1263
    +    for (const bridgeLine of TorSettings.bridges.bridge_strings) {
    
    1264
    +      try {
    
    1265
    +        this._bridgeIds.push(TorParsers.parseBridgeLine(bridgeLine).id);
    
    1266
    +      } catch (e) {
    
    1267
    +        console.error(`Detected invalid bridge line: ${bridgeLine}`, e);
    
    1268
    +      }
    
    1269
    +    }
    
    1270
    +
    
    1271
    +    this._updateConnectedState();
    
    1272
    +  },
    
    1273
    +
    
    1274
    +  /**
    
    1275
    +   * The bridge ID/fingerprint of the most recently used bridge (appearing in
    
    1276
    +   * the latest Tor circuit). Roughly corresponds to the bridge we are currently
    
    1277
    +   * connected to.
    
    1278
    +   *
    
    1279
    +   * @type {string?}
    
    1280
    +   */
    
    1281
    +  _connectedBridgeId: null,
    
    1282
    +  /**
    
    1283
    +   * Update _connectedBridgeId.
    
    1284
    +   */
    
    1285
    +  async _updateConnectedBridge() {
    
    1286
    +    this._connectedBridgeId = await getConnectedBridgeId();
    
    1287
    +    this._updateConnectedState();
    
    1288
    +  },
    
    1289
    +};
    
    1290
    +
    
    1291
    +/**
    
    1292
    + * Controls the bridge settings.
    
    1293
    + */
    
    1294
    +const gBridgeSettings = {
    
    1295
    +  /**
    
    1296
    +   * The preferences <groupbox> for bridges
    
    1297
    +   *
    
    1298
    +   * @type {Element?}
    
    1299
    +   */
    
    1300
    +  _groupEl: null,
    
    1301
    +  /**
    
    1302
    +   * The button for controlling whether bridges are enabled.
    
    1303
    +   *
    
    1304
    +   * @type {Element?}
    
    1305
    +   */
    
    1306
    +  _toggleButton: null,
    
    1307
    +  /**
    
    1308
    +   * The area for showing current bridges.
    
    1309
    +   *
    
    1310
    +   * @type {Element?}
    
    1311
    +   */
    
    1312
    +  _bridgesEl: null,
    
    1313
    +  /**
    
    1314
    +   * The heading for the bridge settings.
    
    1315
    +   *
    
    1316
    +   * @type {Element?}
    
    1317
    +   */
    
    1318
    +  _bridgesSettingsHeading: null,
    
    1319
    +  /**
    
    1320
    +   * The current bridges heading, at the start of the area.
    
    1321
    +   *
    
    1322
    +   * @type {Element?}
    
    1323
    +   */
    
    1324
    +  _currentBridgesHeading: null,
    
    1325
    +  /**
    
    1326
    +   * The area for showing no bridges.
    
    1327
    +   *
    
    1328
    +   * @type {Element?}
    
    1329
    +   */
    
    1330
    +  _noBridgesEl: null,
    
    1331
    +
    
    1332
    +  /**
    
    1333
    +   * Initialize the bridge settings.
    
    1334
    +   */
    
    1335
    +  init() {
    
    1336
    +    gBridgesNotification.init();
    
    1337
    +
    
    1338
    +    this._bridgesSettingsHeading = document.getElementById(
    
    1339
    +      "torPreferences-bridges-header"
    
    1340
    +    );
    
    1341
    +    this._currentBridgesHeading = document.getElementById(
    
    1342
    +      "tor-bridges-current-heading"
    
    1343
    +    );
    
    1344
    +    this._bridgesEl = document.getElementById("tor-bridges-current");
    
    1345
    +    this._noBridgesEl = document.getElementById("tor-bridges-none");
    
    1346
    +    this._groupEl = document.getElementById("torPreferences-bridges-group");
    
    1347
    +    this._toggleButton = document.getElementById("tor-bridges-enabled-toggle");
    
    1348
    +    // Initially disabled whilst TorSettings may not be initialized.
    
    1349
    +    this._toggleButton.disabled = true;
    
    1350
    +
    
    1351
    +    this._toggleButton.addEventListener("toggle", () => {
    
    1352
    +      if (!this._haveBridges) {
    
    1353
    +        return;
    
    1354
    +      }
    
    1355
    +      setTorSettings(() => {
    
    1356
    +        TorSettings.bridges.enabled = this._toggleButton.pressed;
    
    1357
    +      });
    
    1358
    +    });
    
    1359
    +
    
    1360
    +    Services.obs.addObserver(this, TorSettingsTopics.SettingsChanged);
    
    1361
    +
    
    1362
    +    gBridgeGrid.init();
    
    1363
    +    gBuiltinBridgesArea.init();
    
    1364
    +
    
    1365
    +    this._initBridgesMenu();
    
    1366
    +    this._initShareArea();
    
    1367
    +
    
    1368
    +    // NOTE: Before initializedPromise completes, the current bridges sections
    
    1369
    +    // should be hidden.
    
    1370
    +    // And gBridgeGrid and gBuiltinBridgesArea are not active.
    
    1371
    +    TorSettings.initializedPromise.then(() => {
    
    1372
    +      this._updateEnabled();
    
    1373
    +      this._updateBridgeStrings();
    
    1374
    +      this._updateSource();
    
    1375
    +    });
    
    1376
    +  },
    
    1377
    +
    
    1378
    +  /**
    
    1379
    +   * Un-initialize the bridge settings.
    
    1380
    +   */
    
    1381
    +  uninit() {
    
    1382
    +    gBridgeGrid.uninit();
    
    1383
    +    gBuiltinBridgesArea.uninit();
    
    1384
    +
    
    1385
    +    Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged);
    
    1386
    +  },
    
    1387
    +
    
    1388
    +  observe(subject, topic, data) {
    
    1389
    +    switch (topic) {
    
    1390
    +      case TorSettingsTopics.SettingsChanged:
    
    1391
    +        const { changes } = subject.wrappedJSObject;
    
    1392
    +        if (changes.includes("bridges.enabled")) {
    
    1393
    +          this._updateEnabled();
    
    1394
    +        }
    
    1395
    +        if (changes.includes("bridges.source")) {
    
    1396
    +          this._updateSource();
    
    1397
    +        }
    
    1398
    +        if (changes.includes("bridges.bridge_strings")) {
    
    1399
    +          this._updateBridgeStrings();
    
    1400
    +        }
    
    1401
    +        break;
    
    1402
    +    }
    
    1403
    +  },
    
    1404
    +
    
    1405
    +  /**
    
    1406
    +   * Update whether the bridges should be shown as enabled.
    
    1407
    +   */
    
    1408
    +  _updateEnabled() {
    
    1409
    +    // Changing the pressed property on moz-toggle should not trigger its
    
    1410
    +    // "toggle" event.
    
    1411
    +    this._toggleButton.pressed = TorSettings.bridges.enabled;
    
    1412
    +  },
    
    1413
    +
    
    1414
    +  /**
    
    1415
    +   * The shown bridge source.
    
    1416
    +   *
    
    1417
    +   * Initially null to indicate that it is unset for the first call to
    
    1418
    +   * _updateSource.
    
    1419
    +   *
    
    1420
    +   * @type {integer?}
    
    1421
    +   */
    
    1422
    +  _bridgeSource: null,
    
    1423
    +
    
    1424
    +  /**
    
    1425
    +   * Update _bridgeSource.
    
    1426
    +   */
    
    1427
    +  _updateSource() {
    
    1428
    +    // NOTE: This should only ever be called after TorSettings is already
    
    1429
    +    // initialized.
    
    1430
    +    const bridgeSource = TorSettings.bridges.source;
    
    1431
    +    if (bridgeSource === this._bridgeSource) {
    
    1432
    +      // Avoid re-activating an area if the source has not changed.
    
    1433
    +      return;
    
    1434
    +    }
    
    1435
    +
    
    1436
    +    this._bridgeSource = bridgeSource;
    
    1437
    +
    
    1438
    +    // Before hiding elements, we determine whether our region contained the
    
    1439
    +    // user focus.
    
    1440
    +    const hadFocus =
    
    1441
    +      this._bridgesEl.contains(document.activeElement) ||
    
    1442
    +      this._noBridgesEl.contains(document.activeElement);
    
    1443
    +
    
    1444
    +    this._bridgesEl.classList.toggle(
    
    1445
    +      "source-built-in",
    
    1446
    +      bridgeSource === TorBridgeSource.BuiltIn
    
    1447
    +    );
    
    1448
    +    this._bridgesEl.classList.toggle(
    
    1449
    +      "source-user",
    
    1450
    +      bridgeSource === TorBridgeSource.UserProvided
    
    1451
    +    );
    
    1452
    +    this._bridgesEl.classList.toggle(
    
    1453
    +      "source-requested",
    
    1454
    +      bridgeSource === TorBridgeSource.BridgeDB
    
    1455
    +    );
    
    1456
    +
    
    1457
    +    // Force the menu to close whenever the source changes.
    
    1458
    +    // NOTE: If the menu had focus then hadFocus will be true, and focus will be
    
    1459
    +    // re-assigned.
    
    1460
    +    this._forceCloseBridgesMenu();
    
    1461
    +
    
    1462
    +    // Update whether we have bridges.
    
    1463
    +    this._updateHaveBridges();
    
    1464
    +
    
    1465
    +    if (hadFocus) {
    
    1466
    +      // Always reset the focus to the start of the area whenever the source
    
    1467
    +      // changes.
    
    1468
    +      // NOTE: gBuiltinBridges._updateBridgeType and gBridgeGrid._updateRows
    
    1469
    +      // may have already called takeFocus in response to them being
    
    1470
    +      // de-activated. The re-call should be safe.
    
    1471
    +      this.takeFocus();
    
    1472
    +    }
    
    1473
    +  },
    
    1474
    +
    
    1475
    +  /**
    
    1476
    +   * Whether we have bridges or not, or null if it is unknown.
    
    1477
    +   *
    
    1478
    +   * @type {boolean?}
    
    1479
    +   */
    
    1480
    +  _haveBridges: null,
    
    1481
    +
    
    1482
    +  /**
    
    1483
    +   * Update the _haveBridges value.
    
    1484
    +   */
    
    1485
    +  _updateHaveBridges() {
    
    1486
    +    // NOTE: We use the TorSettings.bridges.source value, rather than
    
    1487
    +    // this._bridgeSource because _updateHaveBridges can be called just before
    
    1488
    +    // _updateSource (via takeFocus).
    
    1489
    +    const haveBridges = TorSettings.bridges.source !== TorBridgeSource.Invalid;
    
    1490
    +
    
    1491
    +    if (haveBridges === this._haveBridges) {
    
    1492
    +      return;
    
    1493
    +    }
    
    1494
    +
    
    1495
    +    this._haveBridges = haveBridges;
    
    1496
    +
    
    1497
    +    this._toggleButton.disabled = !haveBridges;
    
    1498
    +    // Add classes to show or hide the "no bridges" and "Your bridges" sections.
    
    1499
    +    // NOTE: Before haveBridges is set, neither class is added, so both sections
    
    1500
    +    // and hidden.
    
    1501
    +    this._groupEl.classList.toggle("no-bridges", !haveBridges);
    
    1502
    +    this._groupEl.classList.toggle("have-bridges", haveBridges);
    
    1503
    +  },
    
    1504
    +
    
    1505
    +  /**
    
    1506
    +   * Force the focus to move to the bridge area.
    
    1507
    +   */
    
    1508
    +  takeFocus() {
    
    1509
    +    if (this._haveBridges === null) {
    
    1510
    +      // The bridges area has not been initialized yet, which means that
    
    1511
    +      // TorSettings may not be initialized.
    
    1512
    +      // Unexpected to receive a call before then, so just return early.
    
    1513
    +      return;
    
    1514
    +    }
    
    1515
    +
    
    1516
    +    // Make sure we have the latest value for _haveBridges.
    
    1517
    +    // We also ensure that the _currentBridgesHeading element is visible before
    
    1518
    +    // we focus it.
    
    1519
    +    this._updateHaveBridges();
    
    1520
    +    if (this._haveBridges) {
    
    1521
    +      // Move focus to the start of the area, which is the heading.
    
    1522
    +      // It has tabindex="-1" so should be focusable, even though it is not part
    
    1523
    +      // of the usual tab navigation.
    
    1524
    +      this._currentBridgesHeading.focus();
    
    1525
    +    } else {
    
    1526
    +      // Move focus to the top of the bridge settings.
    
    1527
    +      this._bridgesSettingsHeading.focus();
    
    1528
    +    }
    
    1529
    +  },
    
    1530
    +
    
    1531
    +  /**
    
    1532
    +   * The bridge strings in a copy-able form.
    
    1533
    +   *
    
    1534
    +   * @type {string}
    
    1535
    +   */
    
    1536
    +  _bridgeStrings: "",
    
    1537
    +  /**
    
    1538
    +   * Whether the bridge strings should be shown as a QR code.
    
    1539
    +   *
    
    1540
    +   * @type {boolean}
    
    1541
    +   */
    
    1542
    +  _canQRBridges: false,
    
    1543
    +
    
    1544
    +  /**
    
    1545
    +   * Update the stored bridge strings.
    
    1546
    +   */
    
    1547
    +  _updateBridgeStrings() {
    
    1548
    +    const bridges = TorSettings.bridges.bridge_strings;
    
    1549
    +
    
    1550
    +    this._bridgeStrings = bridges.join("\n");
    
    1551
    +    // TODO: Determine what logic we want.
    
    1552
    +    this._canQRBridges = bridges.length <= 3;
    
    1553
    +
    
    1554
    +    this._qrButton.disabled = !this._canQRBridges;
    
    1555
    +  },
    
    1556
    +
    
    1557
    +  /**
    
    1558
    +   * Copy all the bridge addresses to the clipboard.
    
    1559
    +   */
    
    1560
    +  _copyBridges() {
    
    1561
    +    const clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
    
    1562
    +      Ci.nsIClipboardHelper
    
    1563
    +    );
    
    1564
    +    clipboard.copyString(this._bridgeStrings);
    
    1565
    +  },
    
    1566
    +
    
    1567
    +  /**
    
    1568
    +   * Open the QR code dialog encoding all the bridge addresses.
    
    1569
    +   */
    
    1570
    +  _openQR() {
    
    1571
    +    if (!this._canQRBridges) {
    
    1572
    +      return;
    
    1573
    +    }
    
    1574
    +    const dialog = new BridgeQrDialog();
    
    1575
    +    dialog.openDialog(gSubDialog, this._bridgeStrings);
    
    1576
    +  },
    
    1577
    +
    
    1578
    +  /**
    
    1579
    +   * The QR button for copying all QR codes.
    
    1580
    +   *
    
    1581
    +   * @type {Element?}
    
    1582
    +   */
    
    1583
    +  _qrButton: null,
    
    1584
    +
    
    1585
    +  _initShareArea() {
    
    1586
    +    document
    
    1587
    +      .getElementById("tor-bridges-copy-addresses-button")
    
    1588
    +      .addEventListener("click", () => {
    
    1589
    +        this._copyBridges();
    
    1590
    +      });
    
    1591
    +
    
    1592
    +    this._qrButton = document.getElementById("tor-bridges-qr-addresses-button");
    
    1593
    +    this._qrButton.addEventListener("click", () => {
    
    1594
    +      this._openQR();
    
    1595
    +    });
    
    1596
    +  },
    
    1597
    +
    
    1598
    +  /**
    
    1599
    +   * The menu for all bridges.
    
    1600
    +   *
    
    1601
    +   * @type {Element?}
    
    1602
    +   */
    
    1603
    +  _bridgesMenu: null,
    
    1604
    +
    
    1605
    +  /**
    
    1606
    +   * Initialize the menu for all bridges.
    
    1607
    +   */
    
    1608
    +  _initBridgesMenu() {
    
    1609
    +    this._bridgesMenu = document.getElementById("tor-bridges-all-options-menu");
    
    1610
    +
    
    1611
    +    // NOTE: We generally assume that once the bridge menu is opened the
    
    1612
    +    // this._bridgeStrings value will not change.
    
    1613
    +    const qrItem = document.getElementById(
    
    1614
    +      "tor-bridges-options-qr-all-menu-item"
    
    1615
    +    );
    
    1616
    +    qrItem.addEventListener("click", () => {
    
    1617
    +      this._openQR();
    
    1618
    +    });
    
    1619
    +
    
    1620
    +    const copyItem = document.getElementById(
    
    1621
    +      "tor-bridges-options-copy-all-menu-item"
    
    1622
    +    );
    
    1623
    +    copyItem.addEventListener("click", () => {
    
    1624
    +      this._copyBridges();
    
    1625
    +    });
    
    1626
    +
    
    1627
    +    const editItem = document.getElementById(
    
    1628
    +      "tor-bridges-options-edit-all-menu-item"
    
    1629
    +    );
    
    1630
    +    editItem.addEventListener("click", () => {
    
    1631
    +      // TODO: move to gBridgeSettings.
    
    1632
    +      // TODO: Change dialog title. Do not allow Lox invite.
    
    1633
    +      gConnectionPane.onAddBridgeManually();
    
    1634
    +    });
    
    1635
    +
    
    1636
    +    // TODO: Do we want a different item for built-in bridges, rather than
    
    1637
    +    // "Remove all bridges"?
    
    1638
    +    document
    
    1639
    +      .getElementById("tor-bridges-options-remove-all-menu-item")
    
    1640
    +      .addEventListener("click", () => {
    
    1641
    +        // TODO: Should we only have a warning when not built-in?
    
    1642
    +        const parentWindow =
    
    1643
    +          Services.wm.getMostRecentWindow("navigator:browser");
    
    1644
    +        const flags =
    
    1645
    +          Services.prompt.BUTTON_POS_0 *
    
    1646
    +            Services.prompt.BUTTON_TITLE_IS_STRING +
    
    1647
    +          Services.prompt.BUTTON_POS_0_DEFAULT +
    
    1648
    +          Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL;
    
    1649
    +
    
    1650
    +        // TODO: Update the text, and remove old strings.
    
    1651
    +        const buttonIndex = Services.prompt.confirmEx(
    
    1652
    +          parentWindow,
    
    1653
    +          TorStrings.settings.bridgeRemoveAllDialogTitle,
    
    1654
    +          TorStrings.settings.bridgeRemoveAllDialogDescription,
    
    1655
    +          flags,
    
    1656
    +          TorStrings.settings.remove,
    
    1657
    +          null,
    
    1658
    +          null,
    
    1659
    +          null,
    
    1660
    +          {}
    
    1661
    +        );
    
    1662
    +
    
    1663
    +        if (buttonIndex !== 0) {
    
    1664
    +          return;
    
    1665
    +        }
    
    1666
    +
    
    1667
    +        setTorSettings(() => {
    
    1668
    +          // This should always have the side effect of disabling bridges as
    
    1669
    +          // well.
    
    1670
    +          TorSettings.bridges.bridge_strings = [];
    
    1671
    +        });
    
    1672
    +      });
    
    1673
    +
    
    1674
    +    this._bridgesMenu.addEventListener("showing", () => {
    
    1675
    +      const canCopy = this._bridgeSource !== TorBridgeSource.BuiltIn;
    
    1676
    +      qrItem.hidden = !this._canQRBridges || !canCopy;
    
    1677
    +      copyItem.hidden = !canCopy;
    
    1678
    +      editItem.hidden = this._bridgeSource !== TorBridgeSource.UserProvided;
    
    1679
    +    });
    
    1680
    +
    
    1681
    +    const bridgesMenuButton = document.getElementById(
    
    1682
    +      "tor-bridges-all-options-button"
    
    1683
    +    );
    
    1684
    +    bridgesMenuButton.addEventListener("click", event => {
    
    1685
    +      this._bridgesMenu.toggle(event, bridgesMenuButton);
    
    1686
    +    });
    
    1687
    +
    
    1688
    +    this._bridgesMenu.addEventListener("hidden", () => {
    
    1689
    +      // Make sure the button receives focus again when the menu is hidden.
    
    1690
    +      // Currently, panel-list.js only does this when the menu is opened with a
    
    1691
    +      // keyboard, but this causes focus to be lost from the page if the user
    
    1692
    +      // uses a mixture of keyboard and mouse.
    
    1693
    +      bridgesMenuButton.focus();
    
    1694
    +    });
    
    1695
    +  },
    
    1696
    +
    
    1697
    +  /**
    
    1698
    +   * Force the bridges menu to close.
    
    1699
    +   */
    
    1700
    +  _forceCloseBridgesMenu() {
    
    1701
    +    this._bridgesMenu.hide(null, { force: true });
    
    1702
    +  },
    
    1703
    +};
    
    1704
    +
    
    69 1705
     /*
    
    70 1706
       Connection Pane
    
    71 1707
     
    
    ... ... @@ -96,29 +1732,6 @@ const gConnectionPane = (function () {
    96 1732
           location: "#torPreferences-bridges-location",
    
    97 1733
           locationEntries: "#torPreferences-bridges-locationEntries",
    
    98 1734
           chooseForMe: "#torPreferences-bridges-buttonChooseBridgeForMe",
    
    99
    -      currentHeader: "#torPreferences-currentBridges-header",
    
    100
    -      currentDescription: "#torPreferences-currentBridges-description",
    
    101
    -      currentDescriptionText: "#torPreferences-currentBridges-descriptionText",
    
    102
    -      controls: "#torPreferences-currentBridges-controls",
    
    103
    -      switch: "#torPreferences-currentBridges-switch",
    
    104
    -      cards: "#torPreferences-currentBridges-cards",
    
    105
    -      cardTemplate: "#torPreferences-bridgeCard-template",
    
    106
    -      card: ".torPreferences-bridgeCard",
    
    107
    -      cardId: ".torPreferences-bridgeCard-id",
    
    108
    -      cardHeadingManualLink: ".torPreferences-bridgeCard-manualLink",
    
    109
    -      cardHeadingAddr: ".torPreferences-bridgeCard-headingAddr",
    
    110
    -      cardConnectedLabel: ".torPreferences-current-bridge-label",
    
    111
    -      cardOptions: ".torPreferences-bridgeCard-options",
    
    112
    -      cardMenu: "#torPreferences-bridgeCard-menu",
    
    113
    -      cardQrGrid: ".torPreferences-bridgeCard-grid",
    
    114
    -      cardQrContainer: ".torPreferences-bridgeCard-qr",
    
    115
    -      cardQr: ".torPreferences-bridgeCard-qrCode",
    
    116
    -      cardShare: ".torPreferences-bridgeCard-share",
    
    117
    -      cardAddr: ".torPreferences-bridgeCard-addr",
    
    118
    -      cardLearnMore: ".torPreferences-bridgeCard-learnMore",
    
    119
    -      cardCopy: ".torPreferences-bridgeCard-copyButton",
    
    120
    -      showAll: "#torPreferences-currentBridges-showAll",
    
    121
    -      removeAll: "#torPreferences-currentBridges-removeAll",
    
    122 1735
           addHeader: "#torPreferences-addBridge-header",
    
    123 1736
           addBuiltinLabel: "#torPreferences-addBridge-labelBuiltinBridge",
    
    124 1737
           addBuiltinButton: "#torPreferences-addBridge-buttonBuiltinBridge",
    
    ... ... @@ -142,8 +1755,6 @@ const gConnectionPane = (function () {
    142 1755
     
    
    143 1756
         _internetStatus: InternetStatus.Unknown,
    
    144 1757
     
    
    145
    -    _currentBridgeId: null,
    
    146
    -
    
    147 1758
         // populate xul with strings and cache the relevant elements
    
    148 1759
         _populateXUL() {
    
    149 1760
           // saves tor settings to disk when navigate away from about:preferences
    
    ... ... @@ -387,390 +1998,6 @@ const gConnectionPane = (function () {
    387 1998
             this._showAutoconfiguration();
    
    388 1999
           }
    
    389 2000
     
    
    390
    -      // Bridge cards
    
    391
    -      const bridgeHeader = prefpane.querySelector(
    
    392
    -        selectors.bridges.currentHeader
    
    393
    -      );
    
    394
    -      bridgeHeader.textContent = TorStrings.settings.bridgeCurrent;
    
    395
    -      const bridgeControls = prefpane.querySelector(selectors.bridges.controls);
    
    396
    -      const bridgeSwitch = prefpane.querySelector(selectors.bridges.switch);
    
    397
    -      bridgeSwitch.setAttribute("label", TorStrings.settings.allBridgesEnabled);
    
    398
    -      bridgeSwitch.addEventListener("toggle", () => {
    
    399
    -        TorSettings.bridges.enabled = bridgeSwitch.pressed;
    
    400
    -        TorSettings.saveToPrefs();
    
    401
    -        TorSettings.applySettings().finally(() => {
    
    402
    -          this._populateBridgeCards();
    
    403
    -        });
    
    404
    -      });
    
    405
    -      const bridgeDescription = prefpane.querySelector(
    
    406
    -        selectors.bridges.currentDescription
    
    407
    -      );
    
    408
    -      bridgeDescription.querySelector(
    
    409
    -        selectors.bridges.currentDescriptionText
    
    410
    -      ).textContent = TorStrings.settings.bridgeCurrentDescription;
    
    411
    -      const bridgeTemplate = prefpane.querySelector(
    
    412
    -        selectors.bridges.cardTemplate
    
    413
    -      );
    
    414
    -      {
    
    415
    -        const learnMore = bridgeTemplate.querySelector(
    
    416
    -          selectors.bridges.cardLearnMore
    
    417
    -        );
    
    418
    -        learnMore.setAttribute("value", TorStrings.settings.learnMore);
    
    419
    -        learnMore.setAttribute(
    
    420
    -          "href",
    
    421
    -          TorStrings.settings.learnMoreBridgesCardURL
    
    422
    -        );
    
    423
    -        if (TorStrings.settings.learnMoreBridgesCardURL.startsWith("about:")) {
    
    424
    -          learnMore.setAttribute("useoriginprincipal", "true");
    
    425
    -        }
    
    426
    -      }
    
    427
    -      {
    
    428
    -        const manualLink = bridgeTemplate.querySelector(
    
    429
    -          selectors.bridges.cardHeadingManualLink
    
    430
    -        );
    
    431
    -        manualLink.setAttribute("value", TorStrings.settings.whatAreThese);
    
    432
    -        manualLink.setAttribute(
    
    433
    -          "href",
    
    434
    -          TorStrings.settings.learnMoreBridgesCardURL
    
    435
    -        );
    
    436
    -        if (TorStrings.settings.learnMoreBridgesCardURL.startsWith("about:")) {
    
    437
    -          manualLink.setAttribute("useoriginprincipal", "true");
    
    438
    -        }
    
    439
    -      }
    
    440
    -      bridgeTemplate.querySelector(
    
    441
    -        selectors.bridges.cardConnectedLabel
    
    442
    -      ).textContent = TorStrings.settings.connectedBridge;
    
    443
    -      bridgeTemplate
    
    444
    -        .querySelector(selectors.bridges.cardCopy)
    
    445
    -        .setAttribute("label", TorStrings.settings.bridgeCopy);
    
    446
    -      bridgeTemplate.querySelector(selectors.bridges.cardShare).textContent =
    
    447
    -        TorStrings.settings.bridgeShare;
    
    448
    -      const bridgeCards = prefpane.querySelector(selectors.bridges.cards);
    
    449
    -      const bridgeMenu = prefpane.querySelector(selectors.bridges.cardMenu);
    
    450
    -
    
    451
    -      this._addBridgeCard = bridgeString => {
    
    452
    -        const card = bridgeTemplate.cloneNode(true);
    
    453
    -        card.removeAttribute("id");
    
    454
    -        const grid = card.querySelector(selectors.bridges.cardQrGrid);
    
    455
    -        card.addEventListener("click", e => {
    
    456
    -          if (
    
    457
    -            card.classList.contains("currently-connected") ||
    
    458
    -            bridgeCards.classList.contains("single-card")
    
    459
    -          ) {
    
    460
    -            return;
    
    461
    -          }
    
    462
    -          let target = e.target;
    
    463
    -          let apply = true;
    
    464
    -          while (target !== null && target !== card && apply) {
    
    465
    -            // Deal with mixture of "command" and "click" events
    
    466
    -            apply = !target.classList?.contains("stop-click");
    
    467
    -            target = target.parentElement;
    
    468
    -          }
    
    469
    -          if (apply) {
    
    470
    -            if (card.classList.toggle("expanded")) {
    
    471
    -              grid.classList.add("to-animate");
    
    472
    -              grid.style.height = `${grid.scrollHeight}px`;
    
    473
    -            } else {
    
    474
    -              // Be sure we still have the to-animate class
    
    475
    -              grid.classList.add("to-animate");
    
    476
    -              grid.style.height = "";
    
    477
    -            }
    
    478
    -          }
    
    479
    -        });
    
    480
    -        const emojis = makeBridgeId(bridgeString).map(emojiIndex => {
    
    481
    -          const img = document.createElement("img");
    
    482
    -          img.classList.add("emoji");
    
    483
    -          // Image is set in _updateBridgeEmojis.
    
    484
    -          img.dataset.emojiIndex = emojiIndex;
    
    485
    -          return img;
    
    486
    -        });
    
    487
    -        const idString = TorStrings.settings.bridgeId;
    
    488
    -        const id = card.querySelector(selectors.bridges.cardId);
    
    489
    -        let details;
    
    490
    -        try {
    
    491
    -          details = TorParsers.parseBridgeLine(bridgeString);
    
    492
    -        } catch (e) {
    
    493
    -          console.error(`Detected invalid bridge line: ${bridgeString}`, e);
    
    494
    -        }
    
    495
    -        if (details && details.id !== undefined) {
    
    496
    -          card.setAttribute("data-bridge-id", details.id);
    
    497
    -        }
    
    498
    -        // TODO: properly handle "vanilla" bridges?
    
    499
    -        const type =
    
    500
    -          details && details.transport !== undefined
    
    501
    -            ? details.transport
    
    502
    -            : "vanilla";
    
    503
    -        for (const piece of idString.split(/(%[12]\$S)/)) {
    
    504
    -          if (piece == "%1$S") {
    
    505
    -            id.append(type);
    
    506
    -          } else if (piece == "%2$S") {
    
    507
    -            id.append(...emojis);
    
    508
    -          } else {
    
    509
    -            id.append(piece);
    
    510
    -          }
    
    511
    -        }
    
    512
    -        card.querySelector(selectors.bridges.cardHeadingAddr).textContent =
    
    513
    -          bridgeString;
    
    514
    -        const optionsButton = card.querySelector(selectors.bridges.cardOptions);
    
    515
    -        if (TorSettings.bridges.source === TorBridgeSource.BuiltIn) {
    
    516
    -          optionsButton.setAttribute("hidden", "true");
    
    517
    -        } else {
    
    518
    -          // Cloning the menupopup element does not work as expected.
    
    519
    -          // Therefore, we use only one, and just before opening it, we remove
    
    520
    -          // its previous items, and add the ones relative to the bridge whose
    
    521
    -          // button has been pressed.
    
    522
    -          optionsButton.addEventListener("click", () => {
    
    523
    -            const menuItem = document.createXULElement("menuitem");
    
    524
    -            menuItem.setAttribute("label", TorStrings.settings.remove);
    
    525
    -            menuItem.classList.add("menuitem-iconic");
    
    526
    -            menuItem.image = "chrome://global/skin/icons/delete.svg";
    
    527
    -            menuItem.addEventListener("command", e => {
    
    528
    -              const strings = TorSettings.bridges.bridge_strings;
    
    529
    -              const index = strings.indexOf(bridgeString);
    
    530
    -              if (index !== -1) {
    
    531
    -                strings.splice(index, 1);
    
    532
    -              }
    
    533
    -              TorSettings.bridges.enabled =
    
    534
    -                bridgeSwitch.pressed && !!strings.length;
    
    535
    -              TorSettings.bridges.bridge_strings = strings.join("\n");
    
    536
    -              TorSettings.saveToPrefs();
    
    537
    -              TorSettings.applySettings().finally(() => {
    
    538
    -                this._populateBridgeCards();
    
    539
    -              });
    
    540
    -            });
    
    541
    -            if (bridgeMenu.firstChild) {
    
    542
    -              bridgeMenu.firstChild.remove();
    
    543
    -            }
    
    544
    -            bridgeMenu.append(menuItem);
    
    545
    -            bridgeMenu.openPopup(optionsButton, {
    
    546
    -              position: "bottomleft topleft",
    
    547
    -            });
    
    548
    -          });
    
    549
    -        }
    
    550
    -        const bridgeAddr = card.querySelector(selectors.bridges.cardAddr);
    
    551
    -        bridgeAddr.setAttribute("value", bridgeString);
    
    552
    -        const bridgeCopy = card.querySelector(selectors.bridges.cardCopy);
    
    553
    -        let restoreTimeout = null;
    
    554
    -        bridgeCopy.addEventListener("command", e => {
    
    555
    -          this.onCopyBridgeAddress(bridgeAddr);
    
    556
    -          const label = bridgeCopy.querySelector("label");
    
    557
    -          label.setAttribute("value", TorStrings.settings.copied);
    
    558
    -          bridgeCopy.classList.add("primary");
    
    559
    -
    
    560
    -          const RESTORE_TIME = 1200;
    
    561
    -          if (restoreTimeout !== null) {
    
    562
    -            clearTimeout(restoreTimeout);
    
    563
    -          }
    
    564
    -          restoreTimeout = setTimeout(() => {
    
    565
    -            label.setAttribute("value", TorStrings.settings.bridgeCopy);
    
    566
    -            bridgeCopy.classList.remove("primary");
    
    567
    -            restoreTimeout = null;
    
    568
    -          }, RESTORE_TIME);
    
    569
    -        });
    
    570
    -        if (details?.id && details.id === this._currentBridgeId) {
    
    571
    -          card.classList.add("currently-connected");
    
    572
    -          bridgeCards.prepend(card);
    
    573
    -        } else {
    
    574
    -          bridgeCards.append(card);
    
    575
    -        }
    
    576
    -        // Add the QR only after appending the card, to have the computed style
    
    577
    -        try {
    
    578
    -          const container = card.querySelector(selectors.bridges.cardQr);
    
    579
    -          const style = getComputedStyle(container);
    
    580
    -          const width = style.width.substring(0, style.width.length - 2);
    
    581
    -          const height = style.height.substring(0, style.height.length - 2);
    
    582
    -          new QRCode(container, {
    
    583
    -            text: bridgeString,
    
    584
    -            width,
    
    585
    -            height,
    
    586
    -            colorDark: style.color,
    
    587
    -            colorLight: style.backgroundColor,
    
    588
    -            document,
    
    589
    -          });
    
    590
    -          container.parentElement.addEventListener("click", () => {
    
    591
    -            this.onShowQr(bridgeString);
    
    592
    -          });
    
    593
    -        } catch (err) {
    
    594
    -          // TODO: Add a generic image in case of errors such as code overflow.
    
    595
    -          // It should never happen with correct codes, but after all this
    
    596
    -          // content can be generated by users...
    
    597
    -          console.error("Could not generate the QR code for the bridge:", err);
    
    598
    -        }
    
    599
    -      };
    
    600
    -      this._checkBridgeCardsHeight = () => {
    
    601
    -        for (const card of bridgeCards.children) {
    
    602
    -          // Expanded cards have the height set manually to their details for
    
    603
    -          // the CSS animation. However, when resizing the window, we may need
    
    604
    -          // to adjust their height.
    
    605
    -          if (
    
    606
    -            card.classList.contains("expanded") ||
    
    607
    -            card.classList.contains("currently-connected")
    
    608
    -          ) {
    
    609
    -            const grid = card.querySelector(selectors.bridges.cardQrGrid);
    
    610
    -            // Reset it first, to avoid having a height that is higher than
    
    611
    -            // strictly needed. Also, remove the to-animate class, because the
    
    612
    -            // animation interferes with this process!
    
    613
    -            grid.classList.remove("to-animate");
    
    614
    -            grid.style.height = "";
    
    615
    -            grid.style.height = `${grid.scrollHeight}px`;
    
    616
    -          }
    
    617
    -        }
    
    618
    -      };
    
    619
    -      this._currentBridgesExpanded = false;
    
    620
    -      const showAll = prefpane.querySelector(selectors.bridges.showAll);
    
    621
    -      showAll.setAttribute("label", TorStrings.settings.bridgeShowAll);
    
    622
    -      showAll.addEventListener("command", () => {
    
    623
    -        this._currentBridgesExpanded = !this._currentBridgesExpanded;
    
    624
    -        this._populateBridgeCards();
    
    625
    -        if (!this._currentBridgesExpanded) {
    
    626
    -          bridgeSwitch.scrollIntoView({ behavior: "smooth" });
    
    627
    -        }
    
    628
    -      });
    
    629
    -      const removeAll = prefpane.querySelector(selectors.bridges.removeAll);
    
    630
    -      removeAll.setAttribute("label", TorStrings.settings.bridgeRemoveAll);
    
    631
    -      removeAll.addEventListener("command", () => {
    
    632
    -        this._confirmBridgeRemoval();
    
    633
    -      });
    
    634
    -      this._populateBridgeCards = () => {
    
    635
    -        const collapseThreshold = 4;
    
    636
    -
    
    637
    -        const newStrings = new Set(TorSettings.bridges.bridge_strings);
    
    638
    -        const numBridges = newStrings.size;
    
    639
    -        const noBridges = !numBridges;
    
    640
    -        bridgeHeader.hidden = noBridges;
    
    641
    -        bridgeDescription.hidden = noBridges;
    
    642
    -        bridgeControls.hidden = noBridges;
    
    643
    -        bridgeCards.hidden = noBridges;
    
    644
    -        if (noBridges) {
    
    645
    -          showAll.hidden = true;
    
    646
    -          removeAll.hidden = true;
    
    647
    -          bridgeCards.textContent = "";
    
    648
    -          return;
    
    649
    -        }
    
    650
    -        // Changing the pressed property on moz-toggle should not trigger its
    
    651
    -        // "toggle" event.
    
    652
    -        bridgeSwitch.pressed = TorSettings.bridges.enabled;
    
    653
    -        bridgeCards.classList.toggle("disabled", !TorSettings.bridges.enabled);
    
    654
    -        bridgeCards.classList.toggle("single-card", numBridges === 1);
    
    655
    -
    
    656
    -        let shownCards = 0;
    
    657
    -        const toShow = this._currentBridgesExpanded
    
    658
    -          ? numBridges
    
    659
    -          : collapseThreshold;
    
    660
    -
    
    661
    -        // Do not remove all the old cards, because it makes scrollbar "jump"
    
    662
    -        const currentCards = bridgeCards.querySelectorAll(
    
    663
    -          selectors.bridges.card
    
    664
    -        );
    
    665
    -        for (const card of currentCards) {
    
    666
    -          const string = card.querySelector(selectors.bridges.cardAddr).value;
    
    667
    -          const hadString = newStrings.delete(string);
    
    668
    -          if (!hadString || shownCards == toShow) {
    
    669
    -            card.remove();
    
    670
    -          } else {
    
    671
    -            shownCards++;
    
    672
    -          }
    
    673
    -        }
    
    674
    -
    
    675
    -        // Add only the new strings that remained in the set
    
    676
    -        for (const bridge of newStrings) {
    
    677
    -          if (shownCards >= toShow) {
    
    678
    -            if (!this._currentBridgeId) {
    
    679
    -              break;
    
    680
    -            } else if (!bridge.includes(this._currentBridgeId)) {
    
    681
    -              continue;
    
    682
    -            }
    
    683
    -          }
    
    684
    -          this._addBridgeCard(bridge);
    
    685
    -          shownCards++;
    
    686
    -        }
    
    687
    -
    
    688
    -        // If we know the connected bridge, we may have added more than the ones
    
    689
    -        // we should actually show (but the connected ones have been prepended,
    
    690
    -        // if needed). So, remove any exceeding ones.
    
    691
    -        while (shownCards > toShow) {
    
    692
    -          bridgeCards.lastElementChild.remove();
    
    693
    -          shownCards--;
    
    694
    -        }
    
    695
    -
    
    696
    -        // Newly added emojis.
    
    697
    -        this._updateBridgeEmojis();
    
    698
    -
    
    699
    -        // And finally update the buttons
    
    700
    -        removeAll.hidden = false;
    
    701
    -        showAll.classList.toggle("primary", TorSettings.bridges.enabled);
    
    702
    -        if (numBridges > collapseThreshold) {
    
    703
    -          showAll.hidden = false;
    
    704
    -          showAll.setAttribute(
    
    705
    -            "aria-expanded",
    
    706
    -            // Boolean value gets converted to string "true" or "false".
    
    707
    -            this._currentBridgesExpanded
    
    708
    -          );
    
    709
    -          showAll.setAttribute(
    
    710
    -            "label",
    
    711
    -            this._currentBridgesExpanded
    
    712
    -              ? TorStrings.settings.bridgeShowFewer
    
    713
    -              : TorStrings.settings.bridgeShowAll
    
    714
    -          );
    
    715
    -          // We do not want both collapsed and disabled at the same time,
    
    716
    -          // because we use collapsed only to display a gradient on the list.
    
    717
    -          bridgeCards.classList.toggle(
    
    718
    -            "list-collapsed",
    
    719
    -            !this._currentBridgesExpanded && TorSettings.bridges.enabled
    
    720
    -          );
    
    721
    -        } else {
    
    722
    -          // NOTE: We do not expect the showAll button to have focus when we
    
    723
    -          // hide it since we do not expect `numBridges` to decrease whilst
    
    724
    -          // this button is focused.
    
    725
    -          showAll.hidden = true;
    
    726
    -          bridgeCards.classList.remove("list-collapsed");
    
    727
    -        }
    
    728
    -      };
    
    729
    -      this._populateBridgeCards();
    
    730
    -      this._updateConnectedBridges = () => {
    
    731
    -        for (const card of bridgeCards.querySelectorAll(
    
    732
    -          ".currently-connected"
    
    733
    -        )) {
    
    734
    -          card.classList.remove("currently-connected");
    
    735
    -          card.querySelector(selectors.bridges.cardQrGrid).style.height = "";
    
    736
    -        }
    
    737
    -        if (!this._currentBridgeId) {
    
    738
    -          return;
    
    739
    -        }
    
    740
    -        // Make sure we have the connected bridge in the list
    
    741
    -        this._populateBridgeCards();
    
    742
    -        // At the moment, IDs do not have to be unique (and it is a concrete
    
    743
    -        // case also with built-in bridges!). E.g., one line for the IPv4
    
    744
    -        // address and one for the IPv6 address, so use querySelectorAll
    
    745
    -        const cards = bridgeCards.querySelectorAll(
    
    746
    -          `[data-bridge-id="${this._currentBridgeId}"]`
    
    747
    -        );
    
    748
    -        for (const card of cards) {
    
    749
    -          card.classList.add("currently-connected");
    
    750
    -        }
    
    751
    -        const placeholder = document.createElement("span");
    
    752
    -        bridgeCards.prepend(placeholder);
    
    753
    -        placeholder.replaceWith(...cards);
    
    754
    -        this._checkBridgeCardsHeight();
    
    755
    -      };
    
    756
    -      this._checkConnectedBridge = async () => {
    
    757
    -        // TODO: We could make sure TorSettings is in sync by monitoring also
    
    758
    -        // changes of settings. At that point, we could query it, instead of
    
    759
    -        // doing a query over the control port.
    
    760
    -        let bridge = null;
    
    761
    -        try {
    
    762
    -          const provider = await TorProviderBuilder.build();
    
    763
    -          bridge = provider.currentBridge;
    
    764
    -        } catch (e) {
    
    765
    -          console.warn("Could not get current bridge", e);
    
    766
    -        }
    
    767
    -        if (bridge?.fingerprint !== this._currentBridgeId) {
    
    768
    -          this._currentBridgeId = bridge?.fingerprint ?? null;
    
    769
    -          this._updateConnectedBridges();
    
    770
    -        }
    
    771
    -      };
    
    772
    -      this._checkConnectedBridge();
    
    773
    -
    
    774 2001
           // Add a new bridge
    
    775 2002
           prefpane.querySelector(selectors.bridges.addHeader).textContent =
    
    776 2003
             TorStrings.settings.bridgeAdd;
    
    ... ... @@ -804,34 +2031,6 @@ const gConnectionPane = (function () {
    804 2031
             });
    
    805 2032
           }
    
    806 2033
     
    
    807
    -      this._confirmBridgeRemoval = () => {
    
    808
    -        const aParentWindow =
    
    809
    -          Services.wm.getMostRecentWindow("navigator:browser");
    
    810
    -
    
    811
    -        const ps = Services.prompt;
    
    812
    -        const btnFlags =
    
    813
    -          ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING +
    
    814
    -          ps.BUTTON_POS_0_DEFAULT +
    
    815
    -          ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL;
    
    816
    -
    
    817
    -        const notUsed = { value: false };
    
    818
    -        const btnIndex = ps.confirmEx(
    
    819
    -          aParentWindow,
    
    820
    -          TorStrings.settings.bridgeRemoveAllDialogTitle,
    
    821
    -          TorStrings.settings.bridgeRemoveAllDialogDescription,
    
    822
    -          btnFlags,
    
    823
    -          TorStrings.settings.remove,
    
    824
    -          null,
    
    825
    -          null,
    
    826
    -          null,
    
    827
    -          notUsed
    
    828
    -        );
    
    829
    -
    
    830
    -        if (btnIndex === 0) {
    
    831
    -          this.onRemoveAllBridges();
    
    832
    -        }
    
    833
    -      };
    
    834
    -
    
    835 2034
           // Advanced setup
    
    836 2035
           prefpane.querySelector(selectors.advanced.header).innerText =
    
    837 2036
             TorStrings.settings.advancedHeading;
    
    ... ... @@ -862,11 +2061,11 @@ const gConnectionPane = (function () {
    862 2061
           });
    
    863 2062
     
    
    864 2063
           Services.obs.addObserver(this, TorConnectTopics.StateChange);
    
    865
    -      Services.obs.addObserver(this, TorProviderTopics.BridgeChanged);
    
    866
    -      Services.obs.addObserver(this, "intl:app-locales-changed");
    
    867 2064
         },
    
    868 2065
     
    
    869 2066
         init() {
    
    2067
    +      gBridgeSettings.init();
    
    2068
    +
    
    870 2069
           TorSettings.initializedPromise.then(() => this._populateXUL());
    
    871 2070
     
    
    872 2071
           const onUnload = () => {
    
    ... ... @@ -874,21 +2073,14 @@ const gConnectionPane = (function () {
    874 2073
             gConnectionPane.uninit();
    
    875 2074
           };
    
    876 2075
           window.addEventListener("unload", onUnload);
    
    877
    -
    
    878
    -      window.addEventListener("resize", () => {
    
    879
    -        this._checkBridgeCardsHeight();
    
    880
    -      });
    
    881
    -      window.addEventListener("hashchange", () => {
    
    882
    -        this._checkBridgeCardsHeight();
    
    883
    -      });
    
    884 2076
         },
    
    885 2077
     
    
    886 2078
         uninit() {
    
    2079
    +      gBridgeSettings.uninit();
    
    2080
    +
    
    887 2081
           // unregister our observer topics
    
    888 2082
           Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged);
    
    889 2083
           Services.obs.removeObserver(this, TorConnectTopics.StateChange);
    
    890
    -      Services.obs.removeObserver(this, TorProviderTopics.BridgeChanged);
    
    891
    -      Services.obs.removeObserver(this, "intl:app-locales-changed");
    
    892 2084
         },
    
    893 2085
     
    
    894 2086
         // whether the page should be present in about:preferences
    
    ... ... @@ -916,64 +2108,6 @@ const gConnectionPane = (function () {
    916 2108
               this.onStateChange();
    
    917 2109
               break;
    
    918 2110
             }
    
    919
    -        case TorProviderTopics.BridgeChanged: {
    
    920
    -          this._checkConnectedBridge();
    
    921
    -          break;
    
    922
    -        }
    
    923
    -        case "intl:app-locales-changed": {
    
    924
    -          this._updateBridgeEmojis();
    
    925
    -          break;
    
    926
    -        }
    
    927
    -      }
    
    928
    -    },
    
    929
    -
    
    930
    -    /**
    
    931
    -     * Update the bridge emojis to show their corresponding emoji with an
    
    932
    -     * annotation that matches the current locale.
    
    933
    -     */
    
    934
    -    async _updateBridgeEmojis() {
    
    935
    -      if (!this._emojiPromise) {
    
    936
    -        this._emojiPromise = Promise.all([
    
    937
    -          fetch(
    
    938
    -            "chrome://browser/content/torpreferences/bridgemoji/bridge-emojis.json"
    
    939
    -          ).then(response => response.json()),
    
    940
    -          fetch(
    
    941
    -            "chrome://browser/content/torpreferences/bridgemoji/annotations.json"
    
    942
    -          ).then(response => response.json()),
    
    943
    -        ]);
    
    944
    -      }
    
    945
    -      const [emojiList, emojiAnnotations] = await this._emojiPromise;
    
    946
    -      let langCode;
    
    947
    -      // Find the first desired locale we have annotations for.
    
    948
    -      // Add "en" as a fallback.
    
    949
    -      for (const bcp47 of [...Services.locale.appLocalesAsBCP47, "en"]) {
    
    950
    -        langCode = bcp47;
    
    951
    -        if (langCode in emojiAnnotations) {
    
    952
    -          break;
    
    953
    -        }
    
    954
    -        // Remove everything after the dash, if there is one.
    
    955
    -        langCode = bcp47.replace(/-.*/, "");
    
    956
    -        if (langCode in emojiAnnotations) {
    
    957
    -          break;
    
    958
    -        }
    
    959
    -      }
    
    960
    -      for (const img of document.querySelectorAll(".emoji[data-emoji-index]")) {
    
    961
    -        const emoji = emojiList[img.dataset.emojiIndex];
    
    962
    -        if (!emoji) {
    
    963
    -          // Unexpected.
    
    964
    -          console.error(`No emoji for index ${img.dataset.emojiIndex}`);
    
    965
    -          img.removeAttribute("src");
    
    966
    -          img.removeAttribute("alt");
    
    967
    -          img.removeAttribute("title");
    
    968
    -          continue;
    
    969
    -        }
    
    970
    -        const cp = emoji.codePointAt(0).toString(16);
    
    971
    -        img.setAttribute(
    
    972
    -          "src",
    
    973
    -          `chrome://browser/content/torpreferences/bridgemoji/svgs/${cp}.svg`
    
    974
    -        );
    
    975
    -        img.setAttribute("alt", emoji);
    
    976
    -        img.setAttribute("title", emojiAnnotations[langCode][cp]);
    
    977 2111
           }
    
    978 2112
         },
    
    979 2113
     
    
    ... ... @@ -999,51 +2133,21 @@ const gConnectionPane = (function () {
    999 2133
         onStateChange() {
    
    1000 2134
           this._populateStatus();
    
    1001 2135
           this._showAutoconfiguration();
    
    1002
    -      this._populateBridgeCards();
    
    1003
    -    },
    
    1004
    -
    
    1005
    -    onShowQr(bridgeString) {
    
    1006
    -      const dialog = new BridgeQrDialog();
    
    1007
    -      dialog.openDialog(gSubDialog, bridgeString);
    
    1008
    -    },
    
    1009
    -
    
    1010
    -    onCopyBridgeAddress(addressElem) {
    
    1011
    -      const clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
    
    1012
    -        Ci.nsIClipboardHelper
    
    1013
    -      );
    
    1014
    -      clipboard.copyString(addressElem.value);
    
    1015
    -    },
    
    1016
    -
    
    1017
    -    onRemoveAllBridges() {
    
    1018
    -      TorSettings.bridges.enabled = false;
    
    1019
    -      TorSettings.bridges.bridge_strings = "";
    
    1020
    -      if (TorSettings.bridges.source === TorBridgeSource.BuiltIn) {
    
    1021
    -        TorSettings.bridges.builtin_type = "";
    
    1022
    -      }
    
    1023
    -      TorSettings.saveToPrefs();
    
    1024
    -      TorSettings.applySettings().finally(() => {
    
    1025
    -        this._populateBridgeCards();
    
    1026
    -      });
    
    1027 2136
         },
    
    1028 2137
     
    
    1029 2138
         /**
    
    1030 2139
          * Save and apply settings, then optionally open about:torconnect and start
    
    1031 2140
          * bootstrapping.
    
    1032 2141
          *
    
    2142
    +     * @param {fucntion} changes - The changes to make.
    
    1033 2143
          * @param {boolean} connect - Whether to open about:torconnect and start
    
    1034 2144
          *   bootstrapping if possible.
    
    1035 2145
          */
    
    1036
    -    async saveBridgeSettings(connect) {
    
    1037
    -      TorSettings.saveToPrefs();
    
    1038
    -      // FIXME: This can throw if the user adds a bridge manually with invalid
    
    1039
    -      // content. Should be addressed by tor-browser#41913.
    
    1040
    -      try {
    
    1041
    -        await TorSettings.applySettings();
    
    1042
    -      } catch (e) {
    
    1043
    -        console.error("Applying settings failed", e);
    
    1044
    -      }
    
    1045
    -
    
    1046
    -      this._populateBridgeCards();
    
    2146
    +    async saveBridgeSettings(changes, connect) {
    
    2147
    +      // TODO: Move focus into the bridge area.
    
    2148
    +      // dialog.ownerGlobal.addEventListener("unload", () => gCurrentBridgesArea.takeFocus(), { once: true });
    
    2149
    +      // or use closedCallback in gSubDialog.open()
    
    2150
    +      setTorSettings(changes);
    
    1047 2151
     
    
    1048 2152
           if (!connect) {
    
    1049 2153
             return;
    
    ... ... @@ -1071,11 +2175,11 @@ const gConnectionPane = (function () {
    1071 2175
         onAddBuiltinBridge() {
    
    1072 2176
           const builtinBridgeDialog = new BuiltinBridgeDialog(
    
    1073 2177
             (bridgeType, connect) => {
    
    1074
    -          TorSettings.bridges.enabled = true;
    
    1075
    -          TorSettings.bridges.source = TorBridgeSource.BuiltIn;
    
    1076
    -          TorSettings.bridges.builtin_type = bridgeType;
    
    1077
    -
    
    1078
    -          this.saveBridgeSettings(connect);
    
    2178
    +          this.saveBridgeSettings(() => {
    
    2179
    +            TorSettings.bridges.enabled = true;
    
    2180
    +            TorSettings.bridges.source = TorBridgeSource.BuiltIn;
    
    2181
    +            TorSettings.bridges.builtin_type = bridgeType;
    
    2182
    +          }, connect);
    
    1079 2183
             }
    
    1080 2184
           );
    
    1081 2185
           builtinBridgeDialog.openDialog(gSubDialog);
    
    ... ... @@ -1088,12 +2192,14 @@ const gConnectionPane = (function () {
    1088 2192
               if (!aBridges.length) {
    
    1089 2193
                 return;
    
    1090 2194
               }
    
    2195
    +
    
    1091 2196
               const bridgeStrings = aBridges.join("\n");
    
    1092
    -          TorSettings.bridges.enabled = true;
    
    1093
    -          TorSettings.bridges.source = TorBridgeSource.BridgeDB;
    
    1094
    -          TorSettings.bridges.bridge_strings = bridgeStrings;
    
    1095 2197
     
    
    1096
    -          this.saveBridgeSettings(connect);
    
    2198
    +          this.saveBridgeSettings(() => {
    
    2199
    +            TorSettings.bridges.enabled = true;
    
    2200
    +            TorSettings.bridges.source = TorBridgeSource.BridgeDB;
    
    2201
    +            TorSettings.bridges.bridge_strings = bridgeStrings;
    
    2202
    +          }, connect);
    
    1097 2203
             }
    
    1098 2204
           );
    
    1099 2205
           requestBridgeDialog.openDialog(gSubDialog);
    
    ... ... @@ -1102,11 +2208,11 @@ const gConnectionPane = (function () {
    1102 2208
         onAddBridgeManually() {
    
    1103 2209
           const provideBridgeDialog = new ProvideBridgeDialog(
    
    1104 2210
             (aBridgeString, connect) => {
    
    1105
    -          TorSettings.bridges.enabled = true;
    
    1106
    -          TorSettings.bridges.source = TorBridgeSource.UserProvided;
    
    1107
    -          TorSettings.bridges.bridge_strings = aBridgeString;
    
    1108
    -
    
    1109
    -          this.saveBridgeSettings(connect);
    
    2211
    +          this.saveBridgeSettings(() => {
    
    2212
    +            TorSettings.bridges.enabled = true;
    
    2213
    +            TorSettings.bridges.source = TorBridgeSource.UserProvided;
    
    2214
    +            TorSettings.bridges.bridge_strings = aBridgeString;
    
    2215
    +          }, connect);
    
    1110 2216
             }
    
    1111 2217
           );
    
    1112 2218
           provideBridgeDialog.openDialog(gSubDialog);
    

  • browser/components/torpreferences/content/connectionPane.xhtml
    ... ... @@ -67,7 +67,7 @@
    67 67
     
    
    68 68
       <!-- Bridges -->
    
    69 69
       <hbox class="subcategory" data-category="paneConnection" hidden="true">
    
    70
    -    <html:h1 id="torPreferences-bridges-header" />
    
    70
    +    <html:h1 id="torPreferences-bridges-header" tabindex="-1" />
    
    71 71
       </hbox>
    
    72 72
       <groupbox
    
    73 73
         id="torPreferences-bridges-group"
    
    ... ... @@ -103,73 +103,196 @@
    103 103
             class="primary"
    
    104 104
           />
    
    105 105
         </hbox>
    
    106
    -    <html:h2 id="torPreferences-currentBridges-header"> </html:h2>
    
    107
    -    <description flex="1" id="torPreferences-currentBridges-description">
    
    108
    -      <html:span id="torPreferences-currentBridges-descriptionText" />
    
    109
    -    </description>
    
    110
    -    <hbox align="center" id="torPreferences-currentBridges-controls">
    
    111
    -      <html:moz-toggle
    
    112
    -        id="torPreferences-currentBridges-switch"
    
    113
    -        label-align-after=""
    
    114
    -      />
    
    115
    -      <spacer flex="1" />
    
    116
    -      <button id="torPreferences-currentBridges-removeAll" />
    
    117
    -    </hbox>
    
    118
    -    <menupopup id="torPreferences-bridgeCard-menu" />
    
    119
    -    <vbox
    
    120
    -      id="torPreferences-bridgeCard-template"
    
    121
    -      class="torPreferences-bridgeCard"
    
    122
    -    >
    
    123
    -      <hbox class="torPreferences-bridgeCard-heading">
    
    124
    -        <html:div class="torPreferences-bridgeCard-id" />
    
    125
    -        <label
    
    126
    -          class="torPreferences-bridgeCard-manualLink learnMore text-link stop-click"
    
    127
    -          is="text-link"
    
    128
    -        />
    
    129
    -        <html:div class="torPreferences-bridgeCard-headingAddr" />
    
    130
    -        <html:div class="torPreferences-bridgeCard-buttons">
    
    131
    -          <html:span class="torPreferences-current-bridge-badge">
    
    132
    -            <image class="torPreferences-current-bridge-icon" />
    
    133
    -            <html:span class="torPreferences-current-bridge-label"></html:span>
    
    106
    +    <html:moz-toggle
    
    107
    +      id="tor-bridges-enabled-toggle"
    
    108
    +      label-align-after=""
    
    109
    +      data-l10n-id="tor-bridges-use-bridges"
    
    110
    +      data-l10n-attrs="label"
    
    111
    +    />
    
    112
    +    <!-- Add an aria-live area where we can post notifications to screen
    
    113
    +       - reader users about changes to their list of bridges. This is to give
    
    114
    +       - these users some feedback for when the remove a bridge or change
    
    115
    +       - their bridges in other ways. I.e. whenever tor-bridges-grid-display
    
    116
    +       - changes its rows.
    
    117
    +       -
    
    118
    +       - If we change the text in #tor-bridges-update-area-text, a screen
    
    119
    +       - reader should speak out the text to the user, even when this area
    
    120
    +       - does not have focus.
    
    121
    +       -
    
    122
    +       - In fact, we don't really want the user to navigate to this element
    
    123
    +       - directly. But currently using an aria-live region in the DOM is the
    
    124
    +       - only way to effectively pass a notification to a screen reader user.
    
    125
    +       - Since it must be somewhere in the DOM, we logically place it just
    
    126
    +       - before the grid, where it is hopefully least confusing to stumble
    
    127
    +       - across.
    
    128
    +       -
    
    129
    +       - TODO: Instead of aria-live in the DOM, use the proposed ariaNotify
    
    130
    +       - API if it gets accepted into firefox and works with screen readers.
    
    131
    +       - See https://github.com/WICG/proposals/issues/112
    
    132
    +       -->
    
    133
    +    <!-- NOTE: This area is hidden by default, and is only shown temporarily
    
    134
    +       - when a notification is added. -->
    
    135
    +    <html:div id="tor-bridges-update-area" hidden="hidden">
    
    136
    +      <!-- NOTE: This first span's text content will *not* be read out as part
    
    137
    +         - of the notification because it does not have an aria-live
    
    138
    +         - attribute. Instead it is just here to give context to the following
    
    139
    +         - text in #tor-bridges-update-area-text if the user navigates to
    
    140
    +         - #tor-bridges-update-area manually whilst it is not hidden.
    
    141
    +         - I.e. it is just here to make it less confusing if a screen reader
    
    142
    +         - user stumbles across this.
    
    143
    +         -->
    
    144
    +      <html:span data-l10n-id="tor-bridges-update-area-intro"></html:span>
    
    145
    +      <!-- Whitespace between spans. -->
    
    146
    +      <!-- This second span is the area to place notification text in. -->
    
    147
    +      <html:span
    
    148
    +        id="tor-bridges-update-area-text"
    
    149
    +        aria-live="polite"
    
    150
    +      ></html:span>
    
    151
    +    </html:div>
    
    152
    +    <html:div id="tor-bridges-none">
    
    153
    +      <html:img id="tor-bridges-none-icon" alt="" />
    
    154
    +      <html:div data-l10n-id="tor-bridges-none-added"></html:div>
    
    155
    +    </html:div>
    
    156
    +    <html:div id="tor-bridges-current">
    
    157
    +      <html:div id="tor-bridges-current-header-bar">
    
    158
    +        <html:h2
    
    159
    +          id="tor-bridges-current-heading"
    
    160
    +          tabindex="-1"
    
    161
    +          data-l10n-id="tor-bridges-your-bridges"
    
    162
    +        ></html:h2>
    
    163
    +        <html:span
    
    164
    +          id="tor-bridges-user-label"
    
    165
    +          data-l10n-id="tor-bridges-source-user"
    
    166
    +        ></html:span>
    
    167
    +        <html:span
    
    168
    +          id="tor-bridges-built-in-label"
    
    169
    +          data-l10n-id="tor-bridges-source-built-in"
    
    170
    +        ></html:span>
    
    171
    +        <html:span
    
    172
    +          id="tor-bridges-requested-label"
    
    173
    +          data-l10n-id="tor-bridges-source-requested"
    
    174
    +        ></html:span>
    
    175
    +        <html:button
    
    176
    +          id="tor-bridges-all-options-button"
    
    177
    +          class="tor-bridges-options-button"
    
    178
    +          aria-haspopup="menu"
    
    179
    +          aria-expanded="false"
    
    180
    +          aria-controls="tor-bridges-all-options-menu"
    
    181
    +          data-l10n-id="tor-bridges-options-button"
    
    182
    +        ></html:button>
    
    183
    +        <html:panel-list id="tor-bridges-all-options-menu">
    
    184
    +          <html:panel-item
    
    185
    +            id="tor-bridges-options-qr-all-menu-item"
    
    186
    +            data-l10n-attrs="accesskey"
    
    187
    +            data-l10n-id="tor-bridges-menu-item-qr-all-bridge-addresses"
    
    188
    +          ></html:panel-item>
    
    189
    +          <html:panel-item
    
    190
    +            id="tor-bridges-options-copy-all-menu-item"
    
    191
    +            data-l10n-attrs="accesskey"
    
    192
    +            data-l10n-id="tor-bridges-menu-item-copy-all-bridge-addresses"
    
    193
    +          ></html:panel-item>
    
    194
    +          <html:panel-item
    
    195
    +            id="tor-bridges-options-edit-all-menu-item"
    
    196
    +            data-l10n-attrs="accesskey"
    
    197
    +            data-l10n-id="tor-bridges-menu-item-edit-all-bridges"
    
    198
    +          ></html:panel-item>
    
    199
    +          <html:panel-item
    
    200
    +            id="tor-bridges-options-remove-all-menu-item"
    
    201
    +            data-l10n-attrs="accesskey"
    
    202
    +            data-l10n-id="tor-bridges-menu-item-remove-all-bridges"
    
    203
    +          ></html:panel-item>
    
    204
    +        </html:panel-list>
    
    205
    +      </html:div>
    
    206
    +      <html:div id="tor-bridges-built-in-display">
    
    207
    +        <html:div id="tor-bridges-built-in-type-name"></html:div>
    
    208
    +        <html:div
    
    209
    +          id="tor-bridges-built-in-connected"
    
    210
    +          class="bridge-status-badge"
    
    211
    +        >
    
    212
    +          <html:div class="bridge-status-icon"></html:div>
    
    213
    +          <html:span
    
    214
    +            data-l10n-id="tor-bridges-built-in-status-connected"
    
    215
    +          ></html:span>
    
    216
    +        </html:div>
    
    217
    +        <html:div id="tor-bridges-built-in-description"></html:div>
    
    218
    +      </html:div>
    
    219
    +      <html:div
    
    220
    +        id="tor-bridges-grid-display"
    
    221
    +        role="grid"
    
    222
    +        aria-labelledby="tor-bridges-current-heading"
    
    223
    +      ></html:div>
    
    224
    +      <html:template id="tor-bridges-grid-row-template">
    
    225
    +        <html:div class="tor-bridges-grid-row" role="row">
    
    226
    +          <!-- TODO: lox status cell for new bridges? -->
    
    227
    +          <html:span
    
    228
    +            class="tor-bridges-type-cell tor-bridges-grid-cell"
    
    229
    +            role="gridcell"
    
    230
    +          ></html:span>
    
    231
    +          <html:span class="tor-bridges-emojis-block" role="none"></html:span>
    
    232
    +          <html:span class="tor-bridges-grid-end-block" role="none">
    
    233
    +            <html:span
    
    234
    +              class="tor-bridges-address-cell tor-bridges-grid-cell"
    
    235
    +              role="gridcell"
    
    236
    +            ></html:span>
    
    237
    +            <html:span
    
    238
    +              class="tor-bridges-status-cell tor-bridges-grid-cell"
    
    239
    +              role="gridcell"
    
    240
    +            >
    
    241
    +              <html:div class="bridge-status-badge">
    
    242
    +                <html:div class="bridge-status-icon"></html:div>
    
    243
    +                <html:span class="tor-bridges-status-cell-text"></html:span>
    
    244
    +              </html:div>
    
    245
    +            </html:span>
    
    246
    +            <html:span
    
    247
    +              class="tor-bridges-options-cell tor-bridges-grid-cell"
    
    248
    +              role="gridcell"
    
    249
    +            >
    
    250
    +              <html:button
    
    251
    +                class="tor-bridges-options-cell-button tor-bridges-options-button tor-bridges-grid-focus"
    
    252
    +                aria-haspopup="menu"
    
    253
    +                aria-expanded="false"
    
    254
    +                data-l10n-id="tor-bridges-individual-bridge-options-button"
    
    255
    +              ></html:button>
    
    256
    +              <html:panel-list class="tor-bridges-individual-options-menu">
    
    257
    +                <html:panel-item
    
    258
    +                  class="tor-bridges-options-qr-one-menu-item"
    
    259
    +                  data-l10n-attrs="accesskey"
    
    260
    +                  data-l10n-id="tor-bridges-menu-item-qr-address"
    
    261
    +                ></html:panel-item>
    
    262
    +                <html:panel-item
    
    263
    +                  class="tor-bridges-options-copy-one-menu-item"
    
    264
    +                  data-l10n-attrs="accesskey"
    
    265
    +                  data-l10n-id="tor-bridges-menu-item-copy-address"
    
    266
    +                ></html:panel-item>
    
    267
    +                <html:panel-item
    
    268
    +                  class="tor-bridges-options-remove-one-menu-item"
    
    269
    +                  data-l10n-attrs="accesskey"
    
    270
    +                  data-l10n-id="tor-bridges-menu-item-remove-bridge"
    
    271
    +                ></html:panel-item>
    
    272
    +              </html:panel-list>
    
    273
    +            </html:span>
    
    134 274
               </html:span>
    
    135
    -          <html:button class="torPreferences-bridgeCard-options stop-click" />
    
    136 275
             </html:div>
    
    137
    -      </hbox>
    
    138
    -      <box class="torPreferences-bridgeCard-grid">
    
    139
    -        <box class="torPreferences-bridgeCard-qrWrapper">
    
    140
    -          <html:div class="torPreferences-bridgeCard-qr stop-click">
    
    141
    -            <html:div class="torPreferences-bridgeCard-qrCode" />
    
    142
    -            <html:div class="torPreferences-bridgeCard-qrOnionBox" />
    
    143
    -            <html:div class="torPreferences-bridgeCard-qrOnion" />
    
    144
    -          </html:div>
    
    145
    -        </box>
    
    146
    -        <description class="torPreferences-bridgeCard-share"></description>
    
    147
    -        <hbox class="torPreferences-bridgeCard-addrBox">
    
    148
    -          <html:input
    
    149
    -            class="torPreferences-bridgeCard-addr stop-click"
    
    150
    -            type="text"
    
    151
    -            readonly="readonly"
    
    152
    -          />
    
    153
    -        </hbox>
    
    154
    -        <!-- tor-browser#41977 disable this learn more link until we have an alternate manual entry -->
    
    155
    -        <hbox class="torPreferences-bridgeCard-learnMoreBox" align="center" hidden="true">
    
    156
    -          <label
    
    157
    -            class="torPreferences-bridgeCard-learnMore learnMore text-link stop-click"
    
    158
    -            is="text-link"
    
    159
    -          />
    
    160
    -        </hbox>
    
    161
    -        <hbox class="torPreferences-bridgeCard-copy" align="center">
    
    162
    -          <button class="torPreferences-bridgeCard-copyButton stop-click" />
    
    163
    -        </hbox>
    
    164
    -      </box>
    
    165
    -    </vbox>
    
    166
    -    <vbox id="torPreferences-currentBridges-cards"></vbox>
    
    167
    -    <vbox align="center">
    
    168
    -      <button
    
    169
    -        id="torPreferences-currentBridges-showAll"
    
    170
    -        aria-controls="torPreferences-currentBridges-cards"
    
    171
    -      />
    
    172
    -    </vbox>
    
    276
    +      </html:template>
    
    277
    +      <html:div id="tor-bridges-share">
    
    278
    +        <html:h3
    
    279
    +          id="tor-bridges-share-heading"
    
    280
    +          data-l10n-id="tor-bridges-share-heading"
    
    281
    +        ></html:h3>
    
    282
    +        <html:span
    
    283
    +          id="tor-bridges-share-description"
    
    284
    +          data-l10n-id="tor-bridges-share-description"
    
    285
    +        ></html:span>
    
    286
    +        <html:button
    
    287
    +          id="tor-bridges-copy-addresses-button"
    
    288
    +          data-l10n-id="tor-bridges-copy-addresses-button"
    
    289
    +        ></html:button>
    
    290
    +        <html:button
    
    291
    +          id="tor-bridges-qr-addresses-button"
    
    292
    +          data-l10n-id="tor-bridges-qr-addresses-button"
    
    293
    +        ></html:button>
    
    294
    +      </html:div>
    
    295
    +    </html:div>
    
    173 296
         <html:h2 id="torPreferences-addBridge-header"></html:h2>
    
    174 297
         <hbox align="center">
    
    175 298
           <label id="torPreferences-addBridge-labelBuiltinBridge" flex="1" />
    

  • browser/components/torpreferences/content/torPreferences.css
    ... ... @@ -69,282 +69,391 @@
    69 69
     }
    
    70 70
     
    
    71 71
     /* Bridge settings */
    
    72
    -#torPreferences-bridges-location {
    
    73
    -  width: 280px;
    
    74
    -}
    
    75 72
     
    
    76
    -#torPreferences-bridges-location menuitem[disabled="true"] {
    
    77
    -  color: var(--in-content-button-text-color, inherit);
    
    78
    -  font-weight: 700;
    
    73
    +.bridge-status-badge {
    
    74
    +  display: flex;
    
    75
    +  min-width: max-content;
    
    76
    +  align-items: center;
    
    77
    +  gap: 0.5em;
    
    78
    +  font-size: 0.85em;
    
    79 79
     }
    
    80 80
     
    
    81
    -/* Bridge cards */
    
    82
    -:root {
    
    83
    -  --bridgeCard-animation-time: 0.25s;
    
    81
    +.bridge-status-badge:not(
    
    82
    +  .bridge-status-connected,
    
    83
    +  .bridge-status-none,
    
    84
    +  .bridge-status-current-built-in
    
    85
    +) {
    
    86
    +  display: none;
    
    84 87
     }
    
    85 88
     
    
    86
    -#torPreferences-currentBridges-cards {
    
    87
    -  /* The padding is needed because the mask-image creates an unexpected result
    
    88
    -  otherwise... */
    
    89
    -  padding: 24px 4px;
    
    89
    +.bridge-status-badge.bridge-status-connected {
    
    90
    +  color: var(--purple-60);
    
    90 91
     }
    
    91 92
     
    
    92
    -#torPreferences-currentBridges-cards.list-collapsed {
    
    93
    -  mask-image: linear-gradient(rgb(0, 0, 0) 0% 75%, rgba(0, 0, 0, 0.1));
    
    93
    +@media (prefers-color-scheme: dark) {
    
    94
    +  .bridge-status-badge.bridge-status-connected {
    
    95
    +    color: var(--purple-30);
    
    96
    +  }
    
    94 97
     }
    
    95 98
     
    
    96
    -#torPreferences-currentBridges-cards.disabled {
    
    97
    -  opacity: 0.4;
    
    99
    +.bridge-status-badge.bridge-status-current-built-in {
    
    100
    +  color: var(--in-content-accent-color);
    
    98 101
     }
    
    99 102
     
    
    100
    -.torPreferences-bridgeCard {
    
    101
    -  padding: 16px 12px;
    
    102
    -  /* define border-radius here because of the transition */
    
    103
    -  border-radius: 4px;
    
    104
    -  transition: margin var(--bridgeCard-animation-time), box-shadow 150ms;
    
    105
    -  cursor: pointer;
    
    103
    +.bridge-status-badge > * {
    
    104
    +  flex: 0 0 auto;
    
    106 105
     }
    
    107 106
     
    
    108
    -.torPreferences-bridgeCard.expanded,
    
    109
    -.torPreferences-bridgeCard.currently-connected,
    
    110
    -.single-card .torPreferences-bridgeCard {
    
    111
    -  margin: 12px 0;
    
    112
    -  background: var(--in-content-box-background);
    
    113
    -  box-shadow: var(--card-shadow);
    
    107
    +.bridge-status-icon {
    
    108
    +  width: 16px;
    
    109
    +  height: 16px;
    
    110
    +  background-repeat: no-repeat;
    
    111
    +  background-position: center center;
    
    112
    +  -moz-context-properties: fill;
    
    113
    +  fill: currentColor;
    
    114 114
     }
    
    115 115
     
    
    116
    -.torPreferences-bridgeCard:hover {
    
    117
    -  background: var(--in-content-box-background);
    
    118
    -  box-shadow: var(--card-shadow-hover);
    
    116
    +.bridge-status-badge:is(
    
    117
    +  .bridge-status-connected,
    
    118
    +  .bridge-status-current-built-in
    
    119
    +) .bridge-status-icon {
    
    120
    +  background-image: url("chrome://global/skin/icons/check.svg");
    
    119 121
     }
    
    120 122
     
    
    121
    -.single-card .torPreferences-bridgeCard,
    
    122
    -.torPreferences-bridgeCard.currently-connected {
    
    123
    -  cursor: default;
    
    123
    +.bridge-status-badge.bridge-status-none .bridge-status-icon {
    
    124
    +  /* Hide the icon. */
    
    125
    +  display: none;
    
    124 126
     }
    
    125 127
     
    
    126
    -.torPreferences-bridgeCard-heading {
    
    127
    -  display: flex;
    
    128
    -  align-items: center;
    
    128
    +#tor-bridges-enabled-toggle {
    
    129
    +  margin-block: 16px;
    
    130
    +  width: max-content;
    
    129 131
     }
    
    130 132
     
    
    131
    -.torPreferences-bridgeCard-id {
    
    132
    -  display: flex;
    
    133
    -  align-items: center;
    
    134
    -  font-weight: 700;
    
    133
    +#tor-bridges-update-area {
    
    134
    +  /* Still accessible to screen reader, but not visual. */
    
    135
    +  position: absolute;
    
    136
    +  clip-path: inset(50%);
    
    135 137
     }
    
    136 138
     
    
    137
    -.torPreferences-bridgeCard-id .emoji {
    
    138
    -  width: 20px;
    
    139
    -  height: 20px;
    
    140
    -  margin-inline-start: 4px;
    
    141
    -  padding: 4px;
    
    142
    -  font-size: 20px;
    
    143
    -  border-radius: 4px;
    
    144
    -  background: var(--in-content-box-background-odd);
    
    139
    +#torPreferences-bridges-group:not(.have-bridges, .no-bridges) {
    
    140
    +  /* Hide bridge settings whilst not initialized. */
    
    141
    +  display: none;
    
    145 142
     }
    
    146 143
     
    
    147
    -#torPreferences-currentBridges-cards:not(
    
    148
    -  .single-card
    
    149
    -) .torPreferences-bridgeCard:not(
    
    150
    -  .expanded,
    
    151
    -  .currently-connected
    
    152
    -) .torPreferences-bridgeCard-manualLink {
    
    144
    +#torPreferences-bridges-group:not(.have-bridges) #tor-bridges-current {
    
    153 145
       display: none;
    
    154 146
     }
    
    155 147
     
    
    156
    -.torPreferences-bridgeCard-manualLink {
    
    157
    -  margin: 0 8px;
    
    148
    +#torPreferences-bridges-group:not(.no-bridges) #tor-bridges-none {
    
    149
    +  display: none;
    
    158 150
     }
    
    159 151
     
    
    160
    -.torPreferences-bridgeCard-headingAddr {
    
    161
    -  /* flex extends the element when needed, but without setting a width (any) the
    
    162
    -  overflow + ellipses does not work. */
    
    163
    -  width: 20px;
    
    164
    -  flex: 1;
    
    165
    -  margin: 0 8px;
    
    166
    -  overflow: hidden;
    
    167
    -  color: var(--text-color-deemphasized);
    
    168
    -  white-space: nowrap;
    
    169
    -  text-overflow: ellipsis;
    
    152
    +#tor-bridges-current:not(.source-built-in) #tor-bridges-built-in-label {
    
    153
    +  display: none;
    
    170 154
     }
    
    171 155
     
    
    172
    -.expanded .torPreferences-bridgeCard-headingAddr,
    
    173
    -.currently-connected .torPreferences-bridgeCard-headingAddr,
    
    174
    -.single-card .torPreferences-bridgeCard-headingAddr {
    
    156
    +#tor-bridges-current:not(.source-user) #tor-bridges-user-label {
    
    175 157
       display: none;
    
    176 158
     }
    
    177 159
     
    
    178
    -.torPreferences-bridgeCard-buttons {
    
    179
    -  display: flex;
    
    180
    -  align-items: center;
    
    181
    -  margin-inline-start: auto;
    
    182
    -  align-self: center;
    
    160
    +#tor-bridges-current:not(.source-requested) #tor-bridges-requested-label {
    
    161
    +  display: none;
    
    183 162
     }
    
    184 163
     
    
    185
    -.torPreferences-current-bridge-badge {
    
    186
    -  /* Hidden by default, otherwise display is "flex". */
    
    164
    +#tor-bridges-current:not(
    
    165
    +  .source-user,
    
    166
    +  .source-requested
    
    167
    +) #tor-bridges-share {
    
    187 168
       display: none;
    
    188
    -  align-items: center;
    
    189
    -  font-size: 0.85em;
    
    190 169
     }
    
    191 170
     
    
    192
    -:is(
    
    193
    -  .builtin-bridges-option.current-builtin-bridge-type,
    
    194
    -  .torPreferences-bridgeCard.currently-connected
    
    195
    -) .torPreferences-current-bridge-badge {
    
    196
    -  display: flex;
    
    171
    +#tor-bridges-none,
    
    172
    +#tor-bridges-current {
    
    173
    +  margin-inline: 0;
    
    174
    +  margin-block: 32px;
    
    175
    +  line-height: 1.8;
    
    197 176
     }
    
    198 177
     
    
    199
    -.torPreferences-current-bridge-icon {
    
    200
    -  margin-inline-start: 1px;
    
    201
    -  margin-inline-end: 7px;
    
    202
    -  list-style-image: url("chrome://global/skin/icons/check.svg");
    
    178
    +#tor-bridges-none {
    
    179
    +  display: grid;
    
    180
    +  justify-items: center;
    
    181
    +  text-align: center;
    
    182
    +  padding-block: 64px;
    
    183
    +  padding-inline: 32px;
    
    184
    +  gap: 16px;
    
    185
    +  border-radius: 4px;
    
    186
    +  color: var(--text-color-deemphasized);
    
    187
    +  border: 2px dashed color-mix(in srgb, currentColor 20%, transparent);
    
    188
    +}
    
    189
    +
    
    190
    +#tor-bridges-none-icon {
    
    191
    +  width: 20px;
    
    192
    +  height: 20px;
    
    193
    +  content: url("chrome://browser/content/torpreferences/bridge.svg");
    
    203 194
       -moz-context-properties: fill;
    
    204 195
       fill: currentColor;
    
    205
    -  flex: 0 0 auto;
    
    206 196
     }
    
    207 197
     
    
    208
    -.torPreferences-bridgeCard .torPreferences-current-bridge-badge {
    
    209
    -  color: var(--purple-60);
    
    210
    -  margin-inline-end: 12px;
    
    198
    +#tor-bridges-current {
    
    199
    +  padding: 16px;
    
    200
    +  border-radius: 4px;
    
    201
    +  background: var(--in-content-box-info-background);
    
    211 202
     }
    
    212 203
     
    
    213
    -@media (prefers-color-scheme: dark) {
    
    214
    -  .torPreferences-bridgeCard .torPreferences-current-bridge-badge {
    
    215
    -    color: var(--purple-30);
    
    216
    -  }
    
    204
    +#tor-bridges-current-header-bar {
    
    205
    +  display: flex;
    
    206
    +  min-width: max-content;
    
    207
    +  align-items: center;
    
    208
    +  border-block-end: 1px solid var(--in-content-border-color);
    
    209
    +  padding-block-end: 16px;
    
    210
    +  margin-block-end: 16px;
    
    217 211
     }
    
    218 212
     
    
    219
    -.torPreferences-bridgeCard-options {
    
    220
    -  width: 24px;
    
    221
    -  min-width: 0;
    
    222
    -  height: 24px;
    
    223
    -  min-height: 0;
    
    224
    -  margin-inline-start: 8px;
    
    225
    -  padding: 1px;
    
    213
    +#tor-bridges-current-header-bar > * {
    
    214
    +  flex: 0 0 auto;
    
    215
    +}
    
    216
    +
    
    217
    +#tor-bridges-current-heading {
    
    218
    +  margin: 0;
    
    219
    +  margin-inline-end: 2em;
    
    220
    +  font-size: inherit;
    
    221
    +  flex: 1 0 auto;
    
    222
    +}
    
    223
    +
    
    224
    +.tor-bridges-options-button {
    
    225
    +  padding: 3px;
    
    226
    +  margin: 0;
    
    227
    +  min-height: auto;
    
    228
    +  min-width: auto;
    
    229
    +  box-sizing: content-box;
    
    230
    +  width: 16px;
    
    231
    +  height: 16px;
    
    226 232
       background-image: url("chrome://global/skin/icons/more.svg");
    
    227 233
       background-repeat: no-repeat;
    
    228 234
       background-position: center center;
    
    229
    -  fill: currentColor;
    
    235
    +  background-origin: content-box;
    
    236
    +  background-size: contain;
    
    230 237
       -moz-context-properties: fill;
    
    238
    +  fill: currentColor;
    
    231 239
     }
    
    232 240
     
    
    233
    -#torPreferences-bridgeCard-menu menuitem {
    
    234
    -  fill: currentColor;
    
    235
    -  -moz-context-properties: fill;
    
    241
    +#tor-bridges-all-options-button {
    
    242
    +  margin-inline-start: 8px;
    
    236 243
     }
    
    237 244
     
    
    238
    -.torPreferences-bridgeCard-qrWrapper {
    
    239
    -  grid-area: bridge-qr;
    
    240
    -  display: block; /* So it doesn't stretch the child vertically. */
    
    241
    -  margin-inline-end: 14px;
    
    245
    +#tor-bridges-built-in-display {
    
    246
    +  display: grid;
    
    247
    +  grid-template:
    
    248
    +    "type status" min-content
    
    249
    +    "description description" auto
    
    250
    +    / max-content 1fr;
    
    251
    +  gap: 4px 1.5em;
    
    252
    +  margin-block-end: 16px;
    
    242 253
     }
    
    243 254
     
    
    244
    -.torPreferences-bridgeCard-qr {
    
    245
    -  --qr-one: black;
    
    246
    -  --qr-zero: white;
    
    247
    -  background: var(--qr-zero);
    
    248
    -  position: relative;
    
    249
    -  padding: 4px;
    
    250
    -  border-radius: 2px;
    
    255
    +#tor-bridges-built-in-display:not(.built-in-active) {
    
    256
    +  display: none;
    
    251 257
     }
    
    252 258
     
    
    253
    -.torPreferences-bridgeCard-qrCode {
    
    254
    -  width: 112px;
    
    255
    -  height: 112px;
    
    256
    -  /* Define these colors, as they will be passed to the QR code library */
    
    257
    -  background: var(--qr-zero);
    
    258
    -  color: var(--qr-one);
    
    259
    +#tor-bridges-built-in-type-name {
    
    260
    +  font-weight: 700;
    
    261
    +  grid-area: type;
    
    259 262
     }
    
    260 263
     
    
    261
    -.torPreferences-bridgeCard-qrOnionBox {
    
    262
    -  width: 28px;
    
    263
    -  height: 28px;
    
    264
    -  position: absolute;
    
    265
    -  top: calc(50% - 14px);
    
    266
    -  inset-inline-start: calc(50% - 14px);
    
    267
    -  background: var(--qr-zero);
    
    264
    +#tor-bridges-built-in-connected {
    
    265
    +  grid-area: status;
    
    266
    +  justify-self: end;
    
    268 267
     }
    
    269 268
     
    
    270
    -.torPreferences-bridgeCard-qrOnion {
    
    271
    -  width: 16px;
    
    272
    -  height: 16px;
    
    273
    -  position: absolute;
    
    274
    -  top: calc(50% - 8px);
    
    275
    -  inset-inline-start: calc(50% - 8px);
    
    269
    +#tor-bridges-built-in-description {
    
    270
    +  grid-area: description;
    
    271
    +}
    
    276 272
     
    
    277
    -  mask: url("chrome://browser/content/torpreferences/bridge-qr-onion-mask.svg");
    
    278
    -  mask-repeat: no-repeat;
    
    279
    -  mask-size: 16px;
    
    280
    -  background: var(--qr-one);
    
    273
    +#tor-bridges-grid-display {
    
    274
    +  display: grid;
    
    275
    +  grid-template-columns: max-content repeat(4, max-content) 1fr;
    
    276
    +  --tor-bridges-grid-column-gap: 8px;
    
    277
    +  --tor-bridges-grid-column-short-gap: 4px;
    
    281 278
     }
    
    282 279
     
    
    283
    -.torPreferences-bridgeCard-qr:hover .torPreferences-bridgeCard-qrOnionBox {
    
    284
    -  background: var(--qr-one);
    
    280
    +#tor-bridges-grid-display:not(.grid-active) {
    
    281
    +  display: none;
    
    285 282
     }
    
    286 283
     
    
    287
    -.torPreferences-bridgeCard-qr:hover .torPreferences-bridgeCard-qrOnion {
    
    288
    -  mask: url("chrome://global/skin/icons/search-glass.svg");
    
    289
    -  background: var(--qr-zero);
    
    284
    +.tor-bridges-grid-row {
    
    285
    +  /* We want each row to act as a row of three items in the
    
    286
    +   * #tor-bridges-grid-display grid layout.
    
    287
    +   * We also want a 16px spacing between rows, and 8px spacing between columns,
    
    288
    +   * which are outside the .tor-bridges-grid-cell's border area. So that
    
    289
    +   * clicking these gaps will not focus any item, and their focus outlines do
    
    290
    +   * not overlap.
    
    291
    +   * Moreover, we also want each row to show its .tor-bridges-options-cell when
    
    292
    +   * the .tor-bridges-grid-row has :hover.
    
    293
    +   *
    
    294
    +   * We could use "display: contents" on the row and set a "gap: 16px 8px" on
    
    295
    +   * the parent so that its items fall into the parent layout. However, the gap
    
    296
    +   * between the items would mean there are places where no row has :hover. So
    
    297
    +   * if the user glided across the grid, the options button would visibly
    
    298
    +   * disappear any time the pointer entered a gap, causing the display to feel
    
    299
    +   * "jumpy".
    
    300
    +   *
    
    301
    +   * Instead, we use a "subgrid" layout for each .tor-bridges-grid-row, and
    
    302
    +   * using padding, rather than a gap, for the vertical spacing. Therefore,
    
    303
    +   * every part of the grid is covered by a row, so moving the pointer over the
    
    304
    +   * grid will always have one row with :hover, so one of the options cell will
    
    305
    +   * always be visible.
    
    306
    +   */
    
    307
    +  display: grid;
    
    308
    +  grid-column: 1 / -1;
    
    309
    +  grid-template-columns: subgrid;
    
    310
    +  /* Add 16px gap between rows, plus 8px at the start and end of the grid. */
    
    311
    +  padding-block: 8px;
    
    290 312
     }
    
    291 313
     
    
    292
    -.torPreferences-bridgeCard-grid {
    
    293
    -  height: 0; /* We will set it in JS when expanding it! */
    
    314
    +.tor-bridges-grid-cell:focus-visible {
    
    315
    +  outline: var(--in-content-focus-outline);
    
    316
    +  outline-offset: var(--in-content-focus-outline-offset);
    
    317
    +}
    
    318
    +
    
    319
    +.tor-bridges-grid-cell {
    
    320
    +  /* The cell is stretched to the height of the row, so that each focus outline
    
    321
    +   * shares the same height, but we want to center-align the content within,
    
    322
    +   * which is either a single Element or a TextNode. */
    
    294 323
       display: grid;
    
    295
    -  grid-template-rows: auto 1fr;
    
    296
    -  grid-template-columns: auto 1fr auto;
    
    297
    -  grid-template-areas:
    
    298
    -  'bridge-qr bridge-share bridge-share'
    
    299
    -  'bridge-qr bridge-address bridge-address'
    
    300
    -  'bridge-qr bridge-learn-more bridge-copy';
    
    301
    -  visibility: hidden;
    
    324
    +  align-content: center;
    
    302 325
     }
    
    303 326
     
    
    304
    -.expanded .torPreferences-bridgeCard-grid,
    
    305
    -.currently-connected .torPreferences-bridgeCard-grid,
    
    306
    -.single-card .torPreferences-bridgeCard-grid {
    
    307
    -  padding-top: 12px;
    
    308
    -  visibility: visible;
    
    327
    +.tor-bridges-type-cell {
    
    328
    +  margin-inline-end: var(--tor-bridges-grid-column-gap);
    
    309 329
     }
    
    310 330
     
    
    311
    -.currently-connected .torPreferences-bridgeCard-grid,
    
    312
    -.single-card .torPreferences-bridgeCard-grid {
    
    313
    -  height: auto;
    
    331
    +.tor-bridges-emojis-block {
    
    332
    +  /* Emoji block occupies four columns, but with a smaller gap. */
    
    333
    +  display: contents;
    
    314 334
     }
    
    315 335
     
    
    316
    -.torPreferences-bridgeCard-grid.to-animate {
    
    317
    -  transition: height var(--bridgeCard-animation-time) ease-out, visibility var(--bridgeCard-animation-time);
    
    318
    -  overflow: hidden;
    
    336
    +.tor-bridges-emoji-cell:not(:last-child) {
    
    337
    +  margin-inline-end: var(--tor-bridges-grid-column-short-gap);
    
    319 338
     }
    
    320 339
     
    
    321
    -.torPreferences-bridgeCard-share {
    
    322
    -  grid-area: bridge-share;
    
    340
    +.tor-bridges-emoji-icon {
    
    341
    +  display: block;
    
    342
    +  box-sizing: content-box;
    
    343
    +  width: 16px;
    
    344
    +  height: 16px;
    
    345
    +  background: var(--in-content-button-background);
    
    346
    +  border-radius: 4px;
    
    347
    +  padding: 8px;
    
    323 348
     }
    
    324 349
     
    
    325
    -.torPreferences-bridgeCard-addrBox {
    
    326
    -  grid-area: bridge-address;
    
    350
    +.tor-bridges-grid-end-block {
    
    351
    +  /* The last three cells all share a single grid item slot in the
    
    352
    +   * #tor-bridges-grid-display layout.
    
    353
    +   * This is because we do not want to align its cells between rows. */
    
    354
    +  min-width: max-content;
    
    327 355
       display: flex;
    
    328
    -  align-items: center;
    
    329
    -  justify-content: center;
    
    330
    -  margin: 8px 0;
    
    356
    +  /* Choose "stretch" instead of "center" so that focus outline is a consistent
    
    357
    +   * height between cells. */
    
    358
    +  align-items: stretch;
    
    359
    +  margin-inline-start: var(--tor-bridges-grid-column-gap);
    
    360
    +  gap: var(--tor-bridges-grid-column-gap);
    
    331 361
     }
    
    332 362
     
    
    333
    -input.torPreferences-bridgeCard-addr {
    
    334
    -  width: 100%;
    
    363
    +.tor-bridges-address-cell {
    
    364
    +  /* base size */
    
    365
    +  width: 10em;
    
    366
    +  flex: 1 0 auto;
    
    367
    +  white-space: nowrap;
    
    368
    +  overflow: hidden;
    
    369
    +  text-overflow: ellipsis;
    
    335 370
       color: var(--text-color-deemphasized);
    
    336 371
     }
    
    337 372
     
    
    338
    -.torPreferences-bridgeCard-leranMoreBox {
    
    339
    -  grid-area: bridge-learn-more;
    
    373
    +.tor-bridges-status-cell,
    
    374
    +.tor-bridges-options-cell {
    
    375
    +  flex: 0 0 auto;
    
    340 376
     }
    
    341 377
     
    
    342
    -.torPreferences-bridgeCard-copy {
    
    343
    -  grid-area: bridge-copy;
    
    378
    +/* Hide the options button if the row does not have hover or focus. */
    
    379
    +.tor-bridges-grid-row:not(
    
    380
    +  :hover,
    
    381
    +  :focus-within
    
    382
    +) .tor-bridges-options-cell,
    
    383
    +/* Hide the status cell when it shows "No status" if the cell does not have
    
    384
    + * focus. */
    
    385
    +.tor-bridges-grid-row.hide-status .tor-bridges-status-cell:not(:focus) {
    
    386
    +  /* Still accessible to screen reader, but not visual and does not contribute
    
    387
    +   * to the parent flex layout. */
    
    388
    +  /* NOTE: We assume that the height of these cell's content is equal to or less
    
    389
    +   * than the other cells, so there won't be a jump in row height when they
    
    390
    +   * become visual again and contribute to the layout. */
    
    391
    +  position: absolute;
    
    392
    +  clip-path: inset(50%);
    
    344 393
     }
    
    345 394
     
    
    346
    -#torPreferences-bridgeCard-template {
    
    347
    -  display: none;
    
    395
    +#tor-bridges-share {
    
    396
    +  margin-block-start: 24px;
    
    397
    +  border-radius: 4px;
    
    398
    +  border: 1px solid var(--in-content-border-color);
    
    399
    +  padding: 16px;
    
    400
    +  display: grid;
    
    401
    +  grid-template:
    
    402
    +    "heading heading heading" min-content
    
    403
    +    /* If the description spans one line, it will be center-aligned with the
    
    404
    +     * buttons, otherwise it will start to expand upwards. */
    
    405
    +    "description . ." 1fr
    
    406
    +    "description copy qr" min-content
    
    407
    +    / 1fr max-content max-content;
    
    408
    +  gap: 0 8px;
    
    409
    +  align-items: center;
    
    410
    +}
    
    411
    +
    
    412
    +#tor-bridges-share-heading {
    
    413
    +  grid-area: heading;
    
    414
    +  font-size: inherit;
    
    415
    +  margin: 0;
    
    416
    +  font-weight: 700;
    
    417
    +}
    
    418
    +
    
    419
    +#tor-bridges-share-description {
    
    420
    +  grid-area: description;
    
    421
    +}
    
    422
    +
    
    423
    +#tor-bridges-copy-addresses-button {
    
    424
    +  grid-area: copy;
    
    425
    +  margin: 0;
    
    426
    +  /* Match the QR height if it is higher than ours. */
    
    427
    +  min-height: auto;
    
    428
    +  line-height: 1;
    
    429
    +  align-self: stretch;
    
    430
    +}
    
    431
    +
    
    432
    +#tor-bridges-qr-addresses-button {
    
    433
    +  grid-area: qr;
    
    434
    +  padding: 5px;
    
    435
    +  margin: 0;
    
    436
    +  min-height: auto;
    
    437
    +  min-width: auto;
    
    438
    +  box-sizing: content-box;
    
    439
    +  width: 24px;
    
    440
    +  height: 24px;
    
    441
    +  background-image: url("chrome://browser/content/torpreferences/bridge-qr.svg");
    
    442
    +  background-repeat: no-repeat;
    
    443
    +  background-position: center center;
    
    444
    +  background-origin: content-box;
    
    445
    +  background-size: contain;
    
    446
    +  -moz-context-properties: fill;
    
    447
    +  fill: currentColor;
    
    448
    +}
    
    449
    +
    
    450
    +#torPreferences-bridges-location {
    
    451
    +  width: 280px;
    
    452
    +}
    
    453
    +
    
    454
    +#torPreferences-bridges-location menuitem[disabled="true"] {
    
    455
    +  color: var(--in-content-button-text-color, inherit);
    
    456
    +  font-weight: 700;
    
    348 457
     }
    
    349 458
     
    
    350 459
     /* Advanced Settings */
    
    ... ... @@ -446,10 +555,6 @@ dialog#torPreferences-requestBridge-dialog > hbox {
    446 555
       font-weight: 700;
    
    447 556
     }
    
    448 557
     
    
    449
    -.builtin-bridges-option .torPreferences-current-bridge-badge {
    
    450
    -  color: var(--in-content-accent-color);
    
    451
    -}
    
    452
    -
    
    453 558
     /* Request bridge dialog */
    
    454 559
     /*
    
    455 560
       This hbox is hidden by css here by default so that the
    

  • browser/components/torpreferences/jar.mn
    1 1
     browser.jar:
    
    2
    +    content/browser/torpreferences/bridge.svg                        (content/bridge.svg)
    
    3
    +    content/browser/torpreferences/bridge-qr.svg                     (content/bridge-qr.svg)
    
    2 4
         content/browser/torpreferences/bridgeQrDialog.xhtml              (content/bridgeQrDialog.xhtml)
    
    3 5
         content/browser/torpreferences/bridgeQrDialog.mjs                (content/bridgeQrDialog.mjs)
    
    4 6
         content/browser/torpreferences/builtinBridgeDialog.xhtml         (content/builtinBridgeDialog.xhtml)
    

  • browser/locales/en-US/browser/tor-browser.ftl
    ... ... @@ -44,3 +44,80 @@ tor-browser-home-message-testing = This is an unstable version of Tor Browser fo
    44 44
     # Shown in Home settings, corresponds to the default about:tor home page.
    
    45 45
     home-mode-choice-tor =
    
    46 46
         .label = Tor Browser Home
    
    47
    +
    
    48
    +## Tor Bridges Settings
    
    49
    +
    
    50
    +# Toggle button for enabling and disabling the use of bridges.
    
    51
    +tor-bridges-use-bridges =
    
    52
    +    .label = Use bridges
    
    53
    +
    
    54
    +tor-bridges-none-added = No bridges added
    
    55
    +tor-bridges-your-bridges = Your bridges
    
    56
    +tor-bridges-source-user = Added by you
    
    57
    +tor-bridges-source-built-in = Built-in
    
    58
    +tor-bridges-source-requested = Requested from Tor
    
    59
    +# The "..." menu button for all current bridges.
    
    60
    +tor-bridges-options-button =
    
    61
    +    .title = All bridges
    
    62
    +# Shown in the "..." menu for all bridges when the user can generate a QR code for all of their bridges.
    
    63
    +tor-bridges-menu-item-qr-all-bridge-addresses = Show QR code
    
    64
    +    .accesskey = Q
    
    65
    +# Shown in the "..." menu for all bridges when the user can copy all of their bridges.
    
    66
    +tor-bridges-menu-item-copy-all-bridge-addresses = Copy bridge addresses
    
    67
    +    .accesskey = C
    
    68
    +# Only shown in the "..." menu for bridges added by the user.
    
    69
    +tor-bridges-menu-item-edit-all-bridges = Edit bridges
    
    70
    +    .accesskey = E
    
    71
    +# Shown in the "..." menu for all current bridges.
    
    72
    +tor-bridges-menu-item-remove-all-bridges = Remove all bridges
    
    73
    +    .accesskey = R
    
    74
    +
    
    75
    +# Shown when one of the built-in bridges is in use.
    
    76
    +tor-bridges-built-in-status-connected = Connected
    
    77
    +
    
    78
    +# Shown at the start of a Tor bridge line.
    
    79
    +# $type (String) - The Tor bridge type ("snowflake", "obfs4", "meek-azure").
    
    80
    +tor-bridges-type-prefix = { $type } bridge:
    
    81
    +# The name and accessible description for a bridge emoji cell. Each bridge address can be hashed into four emojis shown to the user (bridgemoji feature). This cell corresponds to a *single* such emoji. The "title" should just be emojiName. The "aria-description" should give screen reader users enough of a hint that the cell contains a single emoji.
    
    82
    +# $emojiName (String) - The name of the emoji, already localized.
    
    83
    +# E.g. with Orca screen reader in en-US this would read "unicorn. Row 2 Column 2. Emoji".
    
    84
    +tor-bridges-emoji-cell =
    
    85
    +    .title = { $emojiName }
    
    86
    +    .aria-description = Emoji
    
    87
    +# The emoji name to show on hover when a bridge emoji's name is unknown.
    
    88
    +tor-bridges-emoji-unknown = Unknown
    
    89
    +# Shown when the bridge has been used for the most recent Tor circuit, i.e. the most recent bridge we have connected to.
    
    90
    +tor-bridges-status-connected = Connected
    
    91
    +# Used when the bridge has no status, i.e. the *absence* of a status to report to the user. This is only visibly shown when the status cell has keyboard focus.
    
    92
    +tor-bridges-status-none = No status
    
    93
    +# The "..." menu button for an individual bridge row.
    
    94
    +tor-bridges-individual-bridge-options-button =
    
    95
    +    .title = Bridge options
    
    96
    +# Shown in the "..." menu for an individual bridge. Shows the QR code for this one bridge.
    
    97
    +tor-bridges-menu-item-qr-address = Show QR code
    
    98
    +    .accesskey = Q
    
    99
    +# Shown in the "..." menu for an individual bridge. Copies the single bridge address to clipboard.
    
    100
    +tor-bridges-menu-item-copy-address = Copy bridge address
    
    101
    +    .accesskey = C
    
    102
    +# Shown in the "..." menu for an individual bridge. Removes this one bridge.
    
    103
    +tor-bridges-menu-item-remove-bridge = Remove bridge
    
    104
    +    .accesskey = R
    
    105
    +
    
    106
    +# Text shown just before a description of the most recent change to the list of user's bridges. Some white space will separate this text from the change description.
    
    107
    +# This text is not visible, but is instead used for screen reader users.
    
    108
    +# E.g. in English this could be "Recent update: One of your Tor bridges has been removed."
    
    109
    +tor-bridges-update-area-intro = Recent update:
    
    110
    +# Update text for screen reader users when only one of their bridges has been removed.
    
    111
    +tor-bridges-update-removed-one-bridge = One of your Tor bridges has been removed.
    
    112
    +# Update text for screen reader users when all of their bridges have been removed.
    
    113
    +tor-bridges-update-removed-all-bridges = All of your Tor bridges have been removed.
    
    114
    +# Update text for screen reader users when their bridges have changed in some arbitrary way.
    
    115
    +tor-bridges-update-changed-bridges = Your Tor bridges have changed.
    
    116
    +
    
    117
    +# Shown for requested bridges and bridges added by the user.
    
    118
    +tor-bridges-share-heading = Help others connect
    
    119
    +#
    
    120
    +tor-bridges-share-description = Share your bridges with trusted contacts.
    
    121
    +tor-bridges-copy-addresses-button = Copy addresses
    
    122
    +tor-bridges-qr-addresses-button =
    
    123
    +    .title = Show QR code

  • toolkit/modules/TorSettings.sys.mjs
    ... ... @@ -175,6 +175,14 @@ class TorSettingsImpl {
    175 175
           allowed_ports: [],
    
    176 176
         },
    
    177 177
       };
    
    178
    +  /**
    
    179
    +   * Accumulated errors from trying to set settings.
    
    180
    +   *
    
    181
    +   * Only added to if not null.
    
    182
    +   *
    
    183
    +   * @type {Array<Error>?}
    
    184
    +   */
    
    185
    +  #settingErrors = null;
    
    178 186
     
    
    179 187
       /**
    
    180 188
        * The recommended pluggable transport.
    
    ... ... @@ -224,16 +232,33 @@ class TorSettingsImpl {
    224 232
           enabled: {},
    
    225 233
         });
    
    226 234
         this.#addProperties("bridges", {
    
    235
    +      /**
    
    236
    +       * Whether the bridges are enabled or not.
    
    237
    +       *
    
    238
    +       * @type {boolean}
    
    239
    +       */
    
    227 240
           enabled: {},
    
    241
    +      /**
    
    242
    +       * The current bridge source.
    
    243
    +       *
    
    244
    +       * @type {integer}
    
    245
    +       */
    
    228 246
           source: {
    
    229
    -        transform: val => {
    
    247
    +        transform: (val, addError) => {
    
    230 248
               if (Object.values(TorBridgeSource).includes(val)) {
    
    231 249
                 return val;
    
    232 250
               }
    
    233
    -          lazy.logger.error(`Not a valid bridge source: "${val}"`);
    
    251
    +          addError(`Not a valid bridge source: "${val}"`);
    
    234 252
               return TorBridgeSource.Invalid;
    
    235 253
             },
    
    236 254
           },
    
    255
    +      /**
    
    256
    +       * The current bridge strings.
    
    257
    +       *
    
    258
    +       * Can only be non-empty if the "source" is not Invalid.
    
    259
    +       *
    
    260
    +       * @type {Array<string>}
    
    261
    +       */
    
    237 262
           bridge_strings: {
    
    238 263
             transform: val => {
    
    239 264
               if (Array.isArray(val)) {
    
    ... ... @@ -244,13 +269,15 @@ class TorSettingsImpl {
    244 269
             copy: val => [...val],
    
    245 270
             equal: (val1, val2) => this.#arrayEqual(val1, val2),
    
    246 271
           },
    
    272
    +      /**
    
    273
    +       * The built-in type to use when using the BuiltIn "source", or empty when
    
    274
    +       * using any other source.
    
    275
    +       *
    
    276
    +       * @type {string}
    
    277
    +       */
    
    247 278
           builtin_type: {
    
    248
    -        callback: val => {
    
    279
    +        callback: (val, addError) => {
    
    249 280
               if (!val) {
    
    250
    -            // Make sure that the source is not BuiltIn
    
    251
    -            if (this.bridges.source === TorBridgeSource.BuiltIn) {
    
    252
    -              this.bridges.source = TorBridgeSource.Invalid;
    
    253
    -            }
    
    254 281
                 return;
    
    255 282
               }
    
    256 283
               const bridgeStrings = this.#getBuiltinBridges(val);
    
    ... ... @@ -258,39 +285,28 @@ class TorSettingsImpl {
    258 285
                 this.bridges.bridge_strings = bridgeStrings;
    
    259 286
                 return;
    
    260 287
               }
    
    261
    -          lazy.logger.error(`No built-in ${val} bridges found`);
    
    262
    -          // Change to be empty, this will trigger this callback again,
    
    263
    -          // but with val as "".
    
    264
    -          this.bridges.builtin_type == "";
    
    288
    +
    
    289
    +          addError(`No built-in ${val} bridges found`);
    
    290
    +          // Set as invalid, which will make the builtin_type "" and set the
    
    291
    +          // bridge_strings to be empty at the next #cleanupSettings.
    
    292
    +          this.bridges.source = TorBridgeSource.Invalid;
    
    265 293
             },
    
    266 294
           },
    
    267 295
         });
    
    268 296
         this.#addProperties("proxy", {
    
    269
    -      enabled: {
    
    270
    -        callback: val => {
    
    271
    -          if (val) {
    
    272
    -            return;
    
    273
    -          }
    
    274
    -          // Reset proxy settings.
    
    275
    -          this.proxy.type = TorProxyType.Invalid;
    
    276
    -          this.proxy.address = "";
    
    277
    -          this.proxy.port = 0;
    
    278
    -          this.proxy.username = "";
    
    279
    -          this.proxy.password = "";
    
    280
    -        },
    
    281
    -      },
    
    297
    +      enabled: {},
    
    282 298
           type: {
    
    283
    -        transform: val => {
    
    299
    +        transform: (val, addError) => {
    
    284 300
               if (Object.values(TorProxyType).includes(val)) {
    
    285 301
                 return val;
    
    286 302
               }
    
    287
    -          lazy.logger.error(`Not a valid proxy type: "${val}"`);
    
    303
    +          addError(`Not a valid proxy type: "${val}"`);
    
    288 304
               return TorProxyType.Invalid;
    
    289 305
             },
    
    290 306
           },
    
    291 307
           address: {},
    
    292 308
           port: {
    
    293
    -        transform: val => {
    
    309
    +        transform: (val, addError) => {
    
    294 310
               if (val === 0) {
    
    295 311
                 // This is a valid value that "unsets" the port.
    
    296 312
                 // Keep this value without giving a warning.
    
    ... ... @@ -298,15 +314,11 @@ class TorSettingsImpl {
    298 314
                 return 0;
    
    299 315
               }
    
    300 316
               // Unset to 0 if invalid null is returned.
    
    301
    -          return this.#parsePort(val, false) ?? 0;
    
    317
    +          return this.#parsePort(val, false, addError) ?? 0;
    
    302 318
             },
    
    303 319
           },
    
    304
    -      username: {
    
    305
    -        transform: val => val ?? "",
    
    306
    -      },
    
    307
    -      password: {
    
    308
    -        transform: val => val ?? "",
    
    309
    -      },
    
    320
    +      username: {},
    
    321
    +      password: {},
    
    310 322
           uri: {
    
    311 323
             getter: () => {
    
    312 324
               const { type, address, port, username, password } = this.proxy;
    
    ... ... @@ -329,20 +341,16 @@ class TorSettingsImpl {
    329 341
           },
    
    330 342
         });
    
    331 343
         this.#addProperties("firewall", {
    
    332
    -      enabled: {
    
    333
    -        callback: val => {
    
    334
    -          if (!val) {
    
    335
    -            this.firewall.allowed_ports = "";
    
    336
    -          }
    
    337
    -        },
    
    338
    -      },
    
    344
    +      enabled: {},
    
    339 345
           allowed_ports: {
    
    340
    -        transform: val => {
    
    346
    +        transform: (val, addError) => {
    
    341 347
               if (!Array.isArray(val)) {
    
    342 348
                 val = val === "" ? [] : val.split(",");
    
    343 349
               }
    
    344 350
               // parse and remove duplicates
    
    345
    -          const portSet = new Set(val.map(p => this.#parsePort(p, true)));
    
    351
    +          const portSet = new Set(
    
    352
    +            val.map(p => this.#parsePort(p, true, addError))
    
    353
    +          );
    
    346 354
               // parsePort returns null for failed parses, so remove it.
    
    347 355
               portSet.delete(null);
    
    348 356
               return [...portSet];
    
    ... ... @@ -353,6 +361,39 @@ class TorSettingsImpl {
    353 361
         });
    
    354 362
       }
    
    355 363
     
    
    364
    +  /**
    
    365
    +   * Clean the setting values after making some changes, so that the values do
    
    366
    +   * not contradict each other.
    
    367
    +   */
    
    368
    +  #cleanupSettings() {
    
    369
    +    this.freezeNotifications();
    
    370
    +    try {
    
    371
    +      if (this.bridges.source === TorBridgeSource.Invalid) {
    
    372
    +        this.bridges.enabled = false;
    
    373
    +        this.bridges.bridge_strings = [];
    
    374
    +      }
    
    375
    +      if (!this.bridges.bridge_strings.length) {
    
    376
    +        this.bridges.enabled = false;
    
    377
    +        this.bridges.source = TorBridgeSource.Invalid;
    
    378
    +      }
    
    379
    +      if (this.bridges.source !== TorBridgeSource.BuiltIn) {
    
    380
    +        this.bridges.builtin_type = "";
    
    381
    +      }
    
    382
    +      if (!this.proxy.enabled) {
    
    383
    +        this.proxy.type = TorProxyType.Invalid;
    
    384
    +        this.proxy.address = "";
    
    385
    +        this.proxy.port = 0;
    
    386
    +        this.proxy.username = "";
    
    387
    +        this.proxy.password = "";
    
    388
    +      }
    
    389
    +      if (!this.firewall.enabled) {
    
    390
    +        this.firewall.allowed_ports = [];
    
    391
    +      }
    
    392
    +    } finally {
    
    393
    +      this.thawNotifications();
    
    394
    +    }
    
    395
    +  }
    
    396
    +
    
    356 397
       /**
    
    357 398
        * The current number of freezes applied to the notifications.
    
    358 399
        *
    
    ... ... @@ -435,6 +476,13 @@ class TorSettingsImpl {
    435 476
         const group = {};
    
    436 477
         for (const name in propParams) {
    
    437 478
           const { getter, transform, callback, copy, equal } = propParams[name];
    
    479
    +      // Method for adding setting errors.
    
    480
    +      const addError = message => {
    
    481
    +        message = `TorSettings.${groupname}.${name}: ${message}`;
    
    482
    +        lazy.logger.error(message);
    
    483
    +        // Only add to #settingErrors if it is not null.
    
    484
    +        this.#settingErrors?.push(message);
    
    485
    +      };
    
    438 486
           Object.defineProperty(group, name, {
    
    439 487
             get: getter
    
    440 488
               ? () => {
    
    ... ... @@ -467,16 +515,20 @@ class TorSettingsImpl {
    467 515
                   this.freezeNotifications();
    
    468 516
                   try {
    
    469 517
                     if (transform) {
    
    470
    -                  val = transform(val);
    
    518
    +                  val = transform(val, addError);
    
    471 519
                     }
    
    472 520
                     const isEqual = equal ? equal(val, prevVal) : val === prevVal;
    
    473 521
                     if (!isEqual) {
    
    474
    -                  if (callback) {
    
    475
    -                    callback(val);
    
    476
    -                  }
    
    522
    +                  // Set before the callback.
    
    477 523
                       this.#settings[groupname][name] = val;
    
    478 524
                       this.#notificationQueue.add(`${groupname}.${name}`);
    
    525
    +
    
    526
    +                  if (callback) {
    
    527
    +                    callback(val, addError);
    
    528
    +                  }
    
    479 529
                     }
    
    530
    +              } catch (e) {
    
    531
    +                addError(e.message);
    
    480 532
                   } finally {
    
    481 533
                     this.thawNotifications();
    
    482 534
                   }
    
    ... ... @@ -503,11 +555,12 @@ class TorSettingsImpl {
    503 555
        * @param {string|integer} val - The value to parse.
    
    504 556
        * @param {boolean} trim - Whether a string value can be stripped of
    
    505 557
        *   whitespace before parsing.
    
    558
    +   * @param {function} addError - Callback to add error messages to.
    
    506 559
        *
    
    507 560
        * @return {integer?} - The port number, or null if the given value was not
    
    508 561
        *   valid.
    
    509 562
        */
    
    510
    -  #parsePort(val, trim) {
    
    563
    +  #parsePort(val, trim, addError) {
    
    511 564
         if (typeof val === "string") {
    
    512 565
           if (trim) {
    
    513 566
             val = val.trim();
    
    ... ... @@ -516,12 +569,12 @@ class TorSettingsImpl {
    516 569
           if (this.#portRegex.test(val)) {
    
    517 570
             val = Number.parseInt(val, 10);
    
    518 571
           } else {
    
    519
    -        lazy.logger.error(`Invalid port string "${val}"`);
    
    572
    +        addError(`Invalid port string "${val}"`);
    
    520 573
             return null;
    
    521 574
           }
    
    522 575
         }
    
    523 576
         if (!Number.isInteger(val) || val < 1 || val > 65535) {
    
    524
    -      lazy.logger.error(`Port out of range: ${val}`);
    
    577
    +      addError(`Port out of range: ${val}`);
    
    525 578
           return null;
    
    526 579
         }
    
    527 580
         return val;
    
    ... ... @@ -739,6 +792,8 @@ class TorSettingsImpl {
    739 792
             ""
    
    740 793
           );
    
    741 794
         }
    
    795
    +
    
    796
    +    this.#cleanupSettings();
    
    742 797
       }
    
    743 798
     
    
    744 799
       /**
    
    ... ... @@ -748,6 +803,7 @@ class TorSettingsImpl {
    748 803
         lazy.logger.debug("saveToPrefs()");
    
    749 804
     
    
    750 805
         this.#checkIfInitialized();
    
    806
    +    this.#cleanupSettings();
    
    751 807
     
    
    752 808
         /* Quickstart */
    
    753 809
         Services.prefs.setBoolPref(
    
    ... ... @@ -847,6 +903,8 @@ class TorSettingsImpl {
    847 903
       async #applySettings(allowUninitialized) {
    
    848 904
         lazy.logger.debug("#applySettings()");
    
    849 905
     
    
    906
    +    this.#cleanupSettings();
    
    907
    +
    
    850 908
         const settingsMap = new Map();
    
    851 909
     
    
    852 910
         // #applySettings can be called only when #allowUninitialized is false
    
    ... ... @@ -928,6 +986,8 @@ class TorSettingsImpl {
    928 986
     
    
    929 987
         const backup = this.getSettings();
    
    930 988
         const backupNotifications = [...this.#notificationQueue];
    
    989
    +    // Start collecting errors.
    
    990
    +    this.#settingErrors = [];
    
    931 991
     
    
    932 992
         // Hold off on lots of notifications until all settings are changed.
    
    933 993
         this.freezeNotifications();
    
    ... ... @@ -946,25 +1006,11 @@ class TorSettingsImpl {
    946 1006
               case TorBridgeSource.UserProvided:
    
    947 1007
                 this.bridges.bridge_strings = settings.bridges.bridge_strings;
    
    948 1008
                 break;
    
    949
    -          case TorBridgeSource.BuiltIn: {
    
    1009
    +          case TorBridgeSource.BuiltIn:
    
    950 1010
                 this.bridges.builtin_type = settings.bridges.builtin_type;
    
    951
    -            if (!this.bridges.bridge_strings.length) {
    
    952
    -              // No bridges were found when setting the builtin_type.
    
    953
    -              throw new Error(
    
    954
    -                `No available builtin bridges of type ${settings.bridges.builtin_type}`
    
    955
    -              );
    
    956
    -            }
    
    957 1011
                 break;
    
    958
    -          }
    
    959 1012
               case TorBridgeSource.Invalid:
    
    960 1013
                 break;
    
    961
    -          default:
    
    962
    -            if (settings.bridges.enabled) {
    
    963
    -              throw new Error(
    
    964
    -                `Bridge source '${settings.source}' is not a valid source`
    
    965
    -              );
    
    966
    -            }
    
    967
    -            break;
    
    968 1014
             }
    
    969 1015
           }
    
    970 1016
     
    
    ... ... @@ -985,6 +1031,12 @@ class TorSettingsImpl {
    985 1031
               this.firewall.allowed_ports = settings.firewall.allowed_ports;
    
    986 1032
             }
    
    987 1033
           }
    
    1034
    +
    
    1035
    +      this.#cleanupSettings();
    
    1036
    +
    
    1037
    +      if (this.#settingErrors.length) {
    
    1038
    +        throw Error(this.#settingErrors.join("; "));
    
    1039
    +      }
    
    988 1040
         } catch (ex) {
    
    989 1041
           // Restore the old settings without any new notifications generated from
    
    990 1042
           // the above code.
    
    ... ... @@ -1001,6 +1053,8 @@ class TorSettingsImpl {
    1001 1053
           lazy.logger.error("setSettings failed", ex);
    
    1002 1054
         } finally {
    
    1003 1055
           this.thawNotifications();
    
    1056
    +      // Stop collecting errors.
    
    1057
    +      this.#settingErrors = null;
    
    1004 1058
         }
    
    1005 1059
     
    
    1006 1060
         lazy.logger.debug("setSettings result", this.#settings);
    

  • toolkit/modules/TorStrings.sys.mjs
    ... ... @@ -3,8 +3,6 @@
    3 3
     // License, v. 2.0. If a copy of the MPL was not distributed with this
    
    4 4
     // file, You can obtain one at http://mozilla.org/MPL/2.0/.
    
    5 5
     
    
    6
    -"use strict";
    
    7
    -
    
    8 6
     const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
    
    9 7
     const { AppConstants } = ChromeUtils.import(
    
    10 8
       "resource://gre/modules/AppConstants.jsm"
    
    ... ... @@ -86,7 +84,6 @@ const Loader = {
    86 84
           statusTorNotConnected: "Not Connected",
    
    87 85
           statusTorBlocked: "Potentially Blocked",
    
    88 86
           learnMore: "Learn more",
    
    89
    -      whatAreThese: "What are these?",
    
    90 87
           // Quickstart
    
    91 88
           quickstartHeading: "Quickstart",
    
    92 89
           quickstartDescription:
    
    ... ... @@ -101,22 +98,10 @@ const Loader = {
    101 98
           bridgeLocationFrequent: "Frequently selected locations",
    
    102 99
           bridgeLocationOther: "Other locations",
    
    103 100
           bridgeChooseForMe: "Choose a Bridge For Me…",
    
    104
    -      bridgeCurrent: "Your Current Bridges",
    
    105
    -      bridgeCurrentDescription:
    
    106
    -        "You can keep one or more bridges saved, and Tor will choose which one to use when you connect. Tor will automatically switch to use another bridge when needed.",
    
    107
    -      bridgeId: "%1$S bridge: %2$S",
    
    108 101
           currentBridge: "Current bridge",
    
    109
    -      connectedBridge: "Connected",
    
    110 102
           remove: "Remove",
    
    111 103
           bridgeDisableBuiltIn: "Disable built-in bridges",
    
    112
    -      bridgeShare:
    
    113
    -        "Share this bridge using the QR code or by copying its address:",
    
    114
    -      bridgeCopy: "Copy Bridge Address",
    
    115 104
           copied: "Copied!",
    
    116
    -      bridgeShowAll: "Show All Bridges",
    
    117
    -      bridgeShowFewer: "Show Fewer Bridges",
    
    118
    -      allBridgesEnabled: "Use current bridges",
    
    119
    -      bridgeRemoveAll: "Remove All Bridges",
    
    120 105
           bridgeRemoveAllDialogTitle: "Remove all bridges?",
    
    121 106
           bridgeRemoveAllDialogDescription:
    
    122 107
             "If these bridges were received from torproject.org or added manually, this action cannot be undone",
    
    ... ... @@ -199,8 +184,6 @@ const Loader = {
    199 184
           ...tsb.getStrings(strings),
    
    200 185
           learnMoreTorBrowserURL: "about:manual#about",
    
    201 186
           learnMoreBridgesURL: "about:manual#bridges",
    
    202
    -      learnMoreBridgesCardURL: "about:manual#bridges_bridge-moji",
    
    203
    -      learnMoreCircumventionURL: "about:manual#circumvention",
    
    204 187
         };
    
    205 188
       } /* Tor Network Settings Strings */,
    
    206 189
     
    

  • toolkit/torbutton/chrome/locale/en-US/settings.properties
    ... ... @@ -32,23 +32,11 @@ settings.bridgeLocationAutomatic=Automatic
    32 32
     settings.bridgeLocationFrequent=Frequently selected locations
    
    33 33
     settings.bridgeLocationOther=Other locations
    
    34 34
     settings.bridgeChooseForMe=Choose a Bridge For Me…
    
    35
    -settings.bridgeCurrent=Your Current Bridges
    
    36
    -settings.bridgeCurrentDescription=You can keep one or more bridges saved, and Tor will choose which one to use when you connect. Tor will automatically switch to use another bridge when needed.
    
    37 35
     
    
    38
    -# Translation note: %1$S = bridge type; %2$S = bridge emoji id
    
    39
    -settings.bridgeId=%1$S bridge: %2$S
    
    40
    -settings.connectedBridge=Connected
    
    41 36
     settings.currentBridge=Current bridge
    
    42 37
     settings.remove=Remove
    
    43 38
     settings.bridgeDisableBuiltIn=Disable built-in bridges
    
    44
    -settings.bridgeShare=Share this bridge using the QR code or by copying its address:
    
    45
    -settings.whatAreThese=What are these?
    
    46
    -settings.bridgeCopy=Copy Bridge Address
    
    47 39
     settings.copied=Copied!
    
    48
    -settings.bridgeShowAll=Show All Bridges
    
    49
    -settings.bridgeShowFewer=Show Fewer Bridges
    
    50
    -settings.allBridgesEnabled=Use current bridges
    
    51
    -settings.bridgeRemoveAll=Remove All Bridges
    
    52 40
     settings.bridgeRemoveAllDialogTitle=Remove all bridges?
    
    53 41
     settings.bridgeRemoveAllDialogDescription=If these bridges were received from torproject.org or added manually, this action cannot be undone
    
    54 42
     settings.bridgeAdd=Add a New Bridge
    
    ... ... @@ -121,3 +109,19 @@ settings.allowedPortsPlaceholder=Comma-separated values
    121 109
     # Log dialog
    
    122 110
     settings.torLogDialogTitle=Tor Logs
    
    123 111
     settings.copyLog=Copy Tor Log to Clipboard
    
    112
    +
    
    113
    +
    
    114
    +# TODO: Remove
    
    115
    +
    
    116
    +settings.bridgeCurrent=Your Current Bridges
    
    117
    +settings.bridgeCurrentDescription=You can keep one or more bridges saved, and Tor will choose which one to use when you connect. Tor will automatically switch to use another bridge when needed.
    
    118
    +# Translation note: %1$S = bridge type; %2$S = bridge emoji id
    
    119
    +settings.bridgeId=%1$S bridge: %2$S
    
    120
    +settings.connectedBridge=Connected
    
    121
    +settings.bridgeShare=Share this bridge using the QR code or by copying its address:
    
    122
    +settings.whatAreThese=What are these?
    
    123
    +settings.bridgeCopy=Copy Bridge Address
    
    124
    +settings.bridgeShowAll=Show All Bridges
    
    125
    +settings.bridgeShowFewer=Show Fewer Bridges
    
    126
    +settings.allBridgesEnabled=Use current bridges
    
    127
    +settings.bridgeRemoveAll=Remove All Bridges