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

Commits:

6 changed files:

Changes:

  • browser/components/torpreferences/content/connectionPane.js
    ... ... @@ -1321,7 +1321,17 @@ const gLoxStatus = {
    1321 1321
         );
    
    1322 1322
     
    
    1323 1323
         this._invitesButton.addEventListener("click", () => {
    
    1324
    -      // TODO: Show invites.
    
    1324
    +      gSubDialog.open(
    
    1325
    +        "chrome://browser/content/torpreferences/loxInviteDialog.xhtml",
    
    1326
    +        {
    
    1327
    +          features: "resizable=yes",
    
    1328
    +          closedCallback: () => {
    
    1329
    +            // TODO: Listen for events from Lox, rather than call _updateInvites
    
    1330
    +            // directly.
    
    1331
    +            this._updateInvites();
    
    1332
    +          },
    
    1333
    +        }
    
    1334
    +      );
    
    1325 1335
         });
    
    1326 1336
         this._unlockAlertButton.addEventListener("click", () => {
    
    1327 1337
           // TODO: Have a way to ensure that the cleared event data matches the
    

  • browser/components/torpreferences/content/loxInviteDialog.js
    1
    +"use strict";
    
    2
    +
    
    3
    +const { TorSettings, TorSettingsTopics, TorBridgeSource } =
    
    4
    +  ChromeUtils.importESModule("resource://gre/modules/TorSettings.sys.mjs");
    
    5
    +
    
    6
    +const { Lox, LoxErrors } = ChromeUtils.importESModule(
    
    7
    +  "resource://gre/modules/Lox.sys.mjs"
    
    8
    +);
    
    9
    +
    
    10
    +/**
    
    11
    + * Fake Lox module
    
    12
    +
    
    13
    +const LoxErrors = {
    
    14
    +  LoxServerUnreachable: "LoxServerUnreachable",
    
    15
    +  Other: "Other",
    
    16
    +};
    
    17
    +
    
    18
    +const Lox = {
    
    19
    +  remainingInvites: 5,
    
    20
    +  getRemainingInviteCount() {
    
    21
    +    return this.remainingInvites;
    
    22
    +  },
    
    23
    +  invites: [
    
    24
    +    '{"invite": [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22]}',
    
    25
    +    '{"invite": [9,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22]}',
    
    26
    +  ],
    
    27
    +  getInvites() {
    
    28
    +    return this.invites;
    
    29
    +  },
    
    30
    +  failError: null,
    
    31
    +  generateInvite() {
    
    32
    +    return new Promise((res, rej) => {
    
    33
    +      setTimeout(() => {
    
    34
    +        if (this.failError) {
    
    35
    +          rej({ type: this.failError });
    
    36
    +          return;
    
    37
    +        }
    
    38
    +        if (!this.remainingInvites) {
    
    39
    +          rej({ type: LoxErrors.Other });
    
    40
    +          return;
    
    41
    +        }
    
    42
    +        const invite = JSON.stringify({
    
    43
    +          invite: Array.from({ length: 100 }, () =>
    
    44
    +            Math.floor(Math.random() * 265)
    
    45
    +          ),
    
    46
    +        });
    
    47
    +        this.invites.push(invite);
    
    48
    +        this.remainingInvites--;
    
    49
    +        res(invite);
    
    50
    +      }, 4000);
    
    51
    +    });
    
    52
    +  },
    
    53
    +};
    
    54
    +*/
    
    55
    +
    
    56
    +const gLoxInvites = {
    
    57
    +  /**
    
    58
    +   * Initialize the dialog.
    
    59
    +   */
    
    60
    +  init() {
    
    61
    +    this._dialog = document.getElementById("lox-invite-dialog");
    
    62
    +    this._remainingInvitesEl = document.getElementById(
    
    63
    +      "lox-invite-dialog-remaining"
    
    64
    +    );
    
    65
    +    this._generateButton = document.getElementById(
    
    66
    +      "lox-invite-dialog-generate-button"
    
    67
    +    );
    
    68
    +    this._connectingEl = document.getElementById(
    
    69
    +      "lox-invite-dialog-connecting"
    
    70
    +    );
    
    71
    +    this._errorEl = document.getElementById("lox-invite-dialog-error-message");
    
    72
    +    this._inviteListEl = document.getElementById("lox-invite-dialog-list");
    
    73
    +
    
    74
    +    this._generateButton.addEventListener("click", () => {
    
    75
    +      this._generateNewInvite();
    
    76
    +    });
    
    77
    +
    
    78
    +    const menu = document.getElementById("lox-invite-dialog-item-menu");
    
    79
    +    this._inviteListEl.addEventListener("contextmenu", event => {
    
    80
    +      if (!this._inviteListEl.selectedItem) {
    
    81
    +        return;
    
    82
    +      }
    
    83
    +      menu.openPopupAtScreen(event.screenX, event.screenY, true);
    
    84
    +    });
    
    85
    +    menu.addEventListener("popuphidden", () => {
    
    86
    +      menu.setAttribute("aria-hidden", "true");
    
    87
    +    });
    
    88
    +    menu.addEventListener("popupshowing", () => {
    
    89
    +      menu.removeAttribute("aria-hidden");
    
    90
    +    });
    
    91
    +    document
    
    92
    +      .getElementById("lox-invite-dialog-copy-menu-item")
    
    93
    +      .addEventListener("command", () => {
    
    94
    +        const selected = this._inviteListEl.selectedItem;
    
    95
    +        if (!selected) {
    
    96
    +          return;
    
    97
    +        }
    
    98
    +        const clipboard = Cc[
    
    99
    +          "@mozilla.org/widget/clipboardhelper;1"
    
    100
    +        ].getService(Ci.nsIClipboardHelper);
    
    101
    +        clipboard.copyString(selected.textContent);
    
    102
    +      });
    
    103
    +
    
    104
    +    // NOTE: TorSettings should already be initialized when this dialog is
    
    105
    +    // opened.
    
    106
    +    Services.obs.addObserver(this, TorSettingsTopics.SettingsChanged);
    
    107
    +    // TODO: Listen for new invites from Lox, when supported.
    
    108
    +
    
    109
    +    // Set initial _loxId value. Can close this dialog.
    
    110
    +    this._updateLoxId();
    
    111
    +
    
    112
    +    this._updateRemainingInvites();
    
    113
    +    this._updateExistingInvites();
    
    114
    +  },
    
    115
    +
    
    116
    +  /**
    
    117
    +   * Un-initialize the dialog.
    
    118
    +   */
    
    119
    +  uninit() {
    
    120
    +    Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged);
    
    121
    +  },
    
    122
    +
    
    123
    +  observe(subject, topic, data) {
    
    124
    +    switch (topic) {
    
    125
    +      case TorSettingsTopics.SettingsChanged:
    
    126
    +        const { changes } = subject.wrappedJSObject;
    
    127
    +        if (
    
    128
    +          changes.includes("bridges.source") ||
    
    129
    +          changes.includes("bridges.lox_id")
    
    130
    +        ) {
    
    131
    +          this._updateLoxId();
    
    132
    +        }
    
    133
    +        break;
    
    134
    +    }
    
    135
    +  },
    
    136
    +
    
    137
    +  /**
    
    138
    +   * The loxId this dialog is shown for. null if uninitailized.
    
    139
    +   *
    
    140
    +   * @type {string?}
    
    141
    +   */
    
    142
    +  _loxId: null,
    
    143
    +  /**
    
    144
    +   * Update the _loxId value. Will close the dialog if it changes after
    
    145
    +   * initialization.
    
    146
    +   */
    
    147
    +  _updateLoxId() {
    
    148
    +    const loxId =
    
    149
    +      TorSettings.bridges.source === TorBridgeSource.Lox
    
    150
    +        ? TorSettings.bridges.lox_id
    
    151
    +        : "";
    
    152
    +    if (!loxId || (this._loxId !== null && loxId !== this._loxId)) {
    
    153
    +      // No lox id, or it changed. Close this dialog.
    
    154
    +      this._dialog.cancelDialog();
    
    155
    +    }
    
    156
    +    this._loxId = loxId;
    
    157
    +  },
    
    158
    +
    
    159
    +  /**
    
    160
    +   * The invites that are already shown.
    
    161
    +   *
    
    162
    +   * @type {Set<string>}
    
    163
    +   */
    
    164
    +  _shownInvites: new Set(),
    
    165
    +
    
    166
    +  /**
    
    167
    +   * Add a new invite at the start of the list.
    
    168
    +   *
    
    169
    +   * @param {string} invite - The invite to add.
    
    170
    +   */
    
    171
    +  _addInvite(invite) {
    
    172
    +    if (this._shownInvites.has(invite)) {
    
    173
    +      return;
    
    174
    +    }
    
    175
    +    const newInvite = document.createXULElement("richlistitem");
    
    176
    +    newInvite.classList.add("lox-invite-dialog-list-item");
    
    177
    +    newInvite.textContent = invite;
    
    178
    +
    
    179
    +    this._inviteListEl.prepend(newInvite);
    
    180
    +    this._shownInvites.add(invite);
    
    181
    +  },
    
    182
    +
    
    183
    +  /**
    
    184
    +   * Update the display of the existing invites.
    
    185
    +   */
    
    186
    +  _updateExistingInvites() {
    
    187
    +    // Add new invites.
    
    188
    +
    
    189
    +    // NOTE: we only expect invites to be appended, so we won't re-order any.
    
    190
    +    // NOTE: invites are ordered with the oldest first.
    
    191
    +    for (const invite of Lox.getInvites()) {
    
    192
    +      this._addInvite(invite);
    
    193
    +    }
    
    194
    +  },
    
    195
    +
    
    196
    +  /**
    
    197
    +   * The shown number or remaining invites we have.
    
    198
    +   *
    
    199
    +   * @type {integer}
    
    200
    +   */
    
    201
    +  _remainingInvites: 0,
    
    202
    +
    
    203
    +  /**
    
    204
    +   * Update the display of the remaining invites.
    
    205
    +   */
    
    206
    +  _updateRemainingInvites() {
    
    207
    +    this._remainingInvites = Lox.getRemainingInviteCount();
    
    208
    +
    
    209
    +    document.l10n.setAttributes(
    
    210
    +      this._remainingInvitesEl,
    
    211
    +      "tor-bridges-lox-remaining-invites",
    
    212
    +      { numInvites: this._remainingInvites }
    
    213
    +    );
    
    214
    +    this._updateGenerateButtonState();
    
    215
    +  },
    
    216
    +
    
    217
    +  /**
    
    218
    +   * Whether we are currently generating an invite.
    
    219
    +   *
    
    220
    +   * @type {boolean}
    
    221
    +   */
    
    222
    +  _generating: false,
    
    223
    +  /**
    
    224
    +   * Set whether we are generating an invite.
    
    225
    +   *
    
    226
    +   * @param {boolean} isGenerating - Whether we are generating.
    
    227
    +   */
    
    228
    +  _setGenerating(isGenerating) {
    
    229
    +    this._generating = isGenerating;
    
    230
    +    this._updateGenerateButtonState();
    
    231
    +    this._connectingEl.classList.toggle("show-connecting", isGenerating);
    
    232
    +  },
    
    233
    +
    
    234
    +  /**
    
    235
    +   * Update the state of the generate button.
    
    236
    +   */
    
    237
    +  _updateGenerateButtonState() {
    
    238
    +    this._generateButton.disabled = this._generating || !this._remainingInvites;
    
    239
    +  },
    
    240
    +
    
    241
    +  /**
    
    242
    +   * Start generating a new invite.
    
    243
    +   */
    
    244
    +  _generateNewInvite() {
    
    245
    +    if (this._generating) {
    
    246
    +      console.error("Already generating an invite");
    
    247
    +      return;
    
    248
    +    }
    
    249
    +    this._setGenerating(true);
    
    250
    +    // Clear the previous error.
    
    251
    +    this._updateGenerateError(null);
    
    252
    +    // Move focus from the button to the connecting element, since button is
    
    253
    +    // now disabled.
    
    254
    +    this._connectingEl.focus();
    
    255
    +
    
    256
    +    let lostFocus = false;
    
    257
    +    Lox.generateInvite()
    
    258
    +      .finally(() => {
    
    259
    +        // Fetch whether the connecting label still has focus before we hide it.
    
    260
    +        lostFocus = this._connectingEl.contains(document.activeElement);
    
    261
    +        this._setGenerating(false);
    
    262
    +      })
    
    263
    +      .then(
    
    264
    +        invite => {
    
    265
    +          this._addInvite(invite);
    
    266
    +
    
    267
    +          if (!this._inviteListEl.contains(document.activeElement)) {
    
    268
    +            // Does not have focus, change the selected item to be the new
    
    269
    +            // invite (at index 0).
    
    270
    +            this._inviteListEl.selectedIndex = 0;
    
    271
    +          }
    
    272
    +
    
    273
    +          if (lostFocus) {
    
    274
    +            // Move focus to the new invite before we hide the "Connecting"
    
    275
    +            // message.
    
    276
    +            this._inviteListEl.focus();
    
    277
    +          }
    
    278
    +
    
    279
    +          // TODO: When Lox sends out notifications, let the observer handle the
    
    280
    +          // change rather than calling _updateRemainingInvites directly.
    
    281
    +          this._updateRemainingInvites();
    
    282
    +        },
    
    283
    +        loxError => {
    
    284
    +          console.error("Failed to generate an invite", loxError);
    
    285
    +          switch (loxError.type) {
    
    286
    +            case LoxErrors.LoxServerUnreachable:
    
    287
    +              this._updateGenerateError("no-server");
    
    288
    +              break;
    
    289
    +            default:
    
    290
    +              this._updateGenerateError("generic");
    
    291
    +              break;
    
    292
    +          }
    
    293
    +
    
    294
    +          if (lostFocus) {
    
    295
    +            // Move focus back to the button before we hide the "Connecting"
    
    296
    +            // message.
    
    297
    +            this._generateButton.focus();
    
    298
    +          }
    
    299
    +        }
    
    300
    +      );
    
    301
    +  },
    
    302
    +
    
    303
    +  /**
    
    304
    +   * Update the shown generation error.
    
    305
    +   *
    
    306
    +   * @param {string?} type - The error type, or null if no error should be
    
    307
    +   *   shown.
    
    308
    +   */
    
    309
    +  _updateGenerateError(type) {
    
    310
    +    // First clear the existing error.
    
    311
    +    this._errorEl.removeAttribute("data-l10n-id");
    
    312
    +    this._errorEl.textContent = "";
    
    313
    +    this._errorEl.classList.toggle("show-error", !!type);
    
    314
    +
    
    315
    +    if (!type) {
    
    316
    +      return;
    
    317
    +    }
    
    318
    +
    
    319
    +    let errorId;
    
    320
    +    switch (type) {
    
    321
    +      case "no-server":
    
    322
    +        errorId = "lox-invite-dialog-no-server-error";
    
    323
    +        break;
    
    324
    +      case "generic":
    
    325
    +        // Generic error.
    
    326
    +        errorId = "lox-invite-dialog-generic-invite-error";
    
    327
    +        break;
    
    328
    +    }
    
    329
    +
    
    330
    +    document.l10n.setAttributes(this._errorEl, errorId);
    
    331
    +  },
    
    332
    +};
    
    333
    +
    
    334
    +window.addEventListener(
    
    335
    +  "DOMContentLoaded",
    
    336
    +  () => {
    
    337
    +    gLoxInvites.init();
    
    338
    +    window.addEventListener(
    
    339
    +      "unload",
    
    340
    +      () => {
    
    341
    +        gLoxInvites.uninit();
    
    342
    +      },
    
    343
    +      { once: true }
    
    344
    +    );
    
    345
    +  },
    
    346
    +  { once: true }
    
    347
    +);

  • browser/components/torpreferences/content/loxInviteDialog.xhtml
    1
    +<?xml version="1.0" encoding="UTF-8"?>
    
    2
    +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
    
    3
    +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
    
    4
    +<?xml-stylesheet href="chrome://browser/content/torpreferences/torPreferences.css"?>
    
    5
    +
    
    6
    +<window
    
    7
    +  type="child"
    
    8
    +  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
    
    9
    +  xmlns:html="http://www.w3.org/1999/xhtml"
    
    10
    +  data-l10n-id="lox-invite-dialog-title"
    
    11
    +>
    
    12
    +  <!-- Context menu, aria-hidden whilst not shown so it does not appear in the
    
    13
    +     - document content. -->
    
    14
    +  <menupopup id="lox-invite-dialog-item-menu" aria-hidden="true">
    
    15
    +    <menuitem
    
    16
    +      id="lox-invite-dialog-copy-menu-item"
    
    17
    +      data-l10n-id="lox-invite-dialog-menu-item-copy-invite"
    
    18
    +    />
    
    19
    +  </menupopup>
    
    20
    +  <dialog id="lox-invite-dialog" buttons="accept">
    
    21
    +    <linkset>
    
    22
    +      <html:link rel="localization" href="browser/tor-browser.ftl" />
    
    23
    +    </linkset>
    
    24
    +
    
    25
    +    <script src="chrome://browser/content/torpreferences/loxInviteDialog.js" />
    
    26
    +
    
    27
    +    <description data-l10n-id="lox-invite-dialog-description"></description>
    
    28
    +    <html:div id="lox-invite-dialog-generate-area">
    
    29
    +      <html:span id="lox-invite-dialog-remaining"></html:span>
    
    30
    +      <html:button
    
    31
    +        id="lox-invite-dialog-generate-button"
    
    32
    +        data-l10n-id="lox-invite-dialog-request-button"
    
    33
    +      ></html:button>
    
    34
    +      <html:div id="lox-invite-dialog-message-area">
    
    35
    +        <html:span
    
    36
    +          id="lox-invite-dialog-error-message"
    
    37
    +          role="alert"
    
    38
    +        ></html:span>
    
    39
    +        <html:span
    
    40
    +          id="lox-invite-dialog-connecting"
    
    41
    +          role="alert"
    
    42
    +          tabindex="0"
    
    43
    +          data-l10n-id="lox-invite-dialog-connecting"
    
    44
    +        ></html:span>
    
    45
    +      </html:div>
    
    46
    +    </html:div>
    
    47
    +    <html:div
    
    48
    +      id="lox-invite-dialog-list-label"
    
    49
    +      data-l10n-id="lox-invite-dialog-invites-label"
    
    50
    +    ></html:div>
    
    51
    +    <richlistbox
    
    52
    +      id="lox-invite-dialog-list"
    
    53
    +      aria-labelledby="lox-invite-dialog-list-label"
    
    54
    +    ></richlistbox>
    
    55
    +  </dialog>
    
    56
    +</window>

  • browser/components/torpreferences/content/torPreferences.css
    ... ... @@ -820,6 +820,75 @@ dialog#torPreferences-requestBridge-dialog > hbox {
    820 820
       background: var(--qr-one);
    
    821 821
     }
    
    822 822
     
    
    823
    +/* Lox invite dialog */
    
    824
    +
    
    825
    +#lox-invite-dialog-generate-area {
    
    826
    +  flex: 0 0 auto;
    
    827
    +  display: grid;
    
    828
    +  grid-template:
    
    829
    +    ". remaining button" min-content
    
    830
    +    "message message message" auto
    
    831
    +    / 1fr max-content max-content;
    
    832
    +  gap: 8px;
    
    833
    +  margin-block: 16px 8px;
    
    834
    +  align-items: center;
    
    835
    +}
    
    836
    +
    
    837
    +#lox-invite-dialog-remaining {
    
    838
    +  grid-area: remaining;
    
    839
    +}
    
    840
    +
    
    841
    +#lox-invite-dialog-generate-button {
    
    842
    +  grid-area: button;
    
    843
    +}
    
    844
    +
    
    845
    +#lox-invite-dialog-message-area {
    
    846
    +  grid-area: message;
    
    847
    +  justify-self: end;
    
    848
    +}
    
    849
    +
    
    850
    +#lox-invite-dialog-message-area::after {
    
    851
    +  /* Zero width space, to ensure we are always one line high. */
    
    852
    +  content: "\200B";
    
    853
    +}
    
    854
    +
    
    855
    +#lox-invite-dialog-error-message {
    
    856
    +  color: var(--in-content-error-text-color);
    
    857
    +}
    
    858
    +
    
    859
    +#lox-invite-dialog-error-message:not(.show-error) {
    
    860
    +  display: none;
    
    861
    +}
    
    862
    +
    
    863
    +#lox-invite-dialog-connecting {
    
    864
    +  color: var(--text-color-deemphasized);
    
    865
    +  /* TODO: Add spinner ::before */
    
    866
    +}
    
    867
    +
    
    868
    +#lox-invite-dialog-connecting:not(.show-connecting) {
    
    869
    +  display: none;
    
    870
    +}
    
    871
    +
    
    872
    +#lox-invite-dialog-list-label {
    
    873
    +  font-weight: 700;
    
    874
    +}
    
    875
    +
    
    876
    +#lox-invite-dialog-list {
    
    877
    +  flex: 1 1 auto;
    
    878
    +  /* basis height */
    
    879
    +  height: 10em;
    
    880
    +  margin-block: 8px;
    
    881
    +}
    
    882
    +
    
    883
    +.lox-invite-dialog-list-item {
    
    884
    +  white-space: nowrap;
    
    885
    +  overflow-x: hidden;
    
    886
    +  /* FIXME: ellipsis does not show. */
    
    887
    +  text-overflow: ellipsis;
    
    888
    +  padding-block: 6px;
    
    889
    +  padding-inline: 8px;
    
    890
    +}
    
    891
    +
    
    823 892
     /* Builtin bridge dialog */
    
    824 893
     #torPreferences-builtinBridge-header {
    
    825 894
       margin: 8px 0 10px 0;
    

  • browser/components/torpreferences/jar.mn
    ... ... @@ -9,6 +9,8 @@ browser.jar:
    9 9
         content/browser/torpreferences/lox-success.svg                   (content/lox-success.svg)
    
    10 10
         content/browser/torpreferences/lox-complete-ring.svg             (content/lox-complete-ring.svg)
    
    11 11
         content/browser/torpreferences/lox-progress-ring.svg             (content/lox-progress-ring.svg)
    
    12
    +    content/browser/torpreferences/loxInviteDialog.xhtml             (content/loxInviteDialog.xhtml)
    
    13
    +    content/browser/torpreferences/loxInviteDialog.js                (content/loxInviteDialog.js)
    
    12 14
         content/browser/torpreferences/bridgeQrDialog.xhtml              (content/bridgeQrDialog.xhtml)
    
    13 15
         content/browser/torpreferences/bridgeQrDialog.js                 (content/bridgeQrDialog.js)
    
    14 16
         content/browser/torpreferences/builtinBridgeDialog.xhtml         (content/builtinBridgeDialog.xhtml)
    

  • browser/locales/en-US/browser/tor-browser.ftl
    ... ... @@ -296,3 +296,17 @@ user-provide-bridge-dialog-result-invite = The following bridges were shared wit
    296 296
     user-provide-bridge-dialog-result-addresses = The following bridges were entered by you.
    
    297 297
     user-provide-bridge-dialog-next-button =
    
    298 298
         .label = Next
    
    299
    +
    
    300
    +## Bridge pass invite dialog. Temporary.
    
    301
    +
    
    302
    +lox-invite-dialog-title =
    
    303
    +    .title = Bridge pass invites
    
    304
    +lox-invite-dialog-description = You can ask the bridge bot to create a new invite, which you can share with a trusted contact to give them their own bridge pass. Each invite can only be redeemed once, but you will unlock access to more invites over time.
    
    305
    +lox-invite-dialog-request-button = Request new invite
    
    306
    +lox-invite-dialog-connecting = Connecting to bridge pass server…
    
    307
    +lox-invite-dialog-no-server-error = Unable to connect to bridge pass server.
    
    308
    +lox-invite-dialog-generic-invite-error = Failed to create a new invite.
    
    309
    +lox-invite-dialog-invites-label = Created invites:
    
    310
    +lox-invite-dialog-menu-item-copy-invite =
    
    311
    +    .label = Copy invite
    
    312
    +    .accesskey = C