tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

loxInviteDialog.js (10027B)


      1 "use strict";
      2 
      3 const { TorSettings, TorSettingsTopics, TorBridgeSource } =
      4  ChromeUtils.importESModule("resource://gre/modules/TorSettings.sys.mjs");
      5 
      6 const { Lox, LoxError, LoxTopics } = ChromeUtils.importESModule(
      7  "resource://gre/modules/Lox.sys.mjs"
      8 );
      9 
     10 /**
     11 * Fake Lox module
     12 
     13 const LoxError = {
     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: LoxError.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._generateArea = document.getElementById(
     66      "lox-invite-dialog-generate-area"
     67    );
     68    this._generateButton = document.getElementById(
     69      "lox-invite-dialog-generate-button"
     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    Services.obs.addObserver(this, LoxTopics.UpdateActiveLoxId);
    108    Services.obs.addObserver(this, LoxTopics.UpdateRemainingInvites);
    109    Services.obs.addObserver(this, LoxTopics.NewInvite);
    110 
    111    // Set initial _loxId value. Can close this dialog.
    112    this._updateLoxId();
    113 
    114    this._updateRemainingInvites();
    115    this._updateExistingInvites();
    116  },
    117 
    118  /**
    119   * Un-initialize the dialog.
    120   */
    121  uninit() {
    122    Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged);
    123    Services.obs.removeObserver(this, LoxTopics.UpdateActiveLoxId);
    124    Services.obs.removeObserver(this, LoxTopics.UpdateRemainingInvites);
    125    Services.obs.removeObserver(this, LoxTopics.NewInvite);
    126  },
    127 
    128  observe(subject, topic) {
    129    switch (topic) {
    130      case TorSettingsTopics.SettingsChanged: {
    131        const { changes } = subject.wrappedJSObject;
    132        if (changes.includes("bridges.source")) {
    133          this._updateLoxId();
    134        }
    135        break;
    136      }
    137      case LoxTopics.UpdateActiveLoxId:
    138        this._updateLoxId();
    139        break;
    140      case LoxTopics.UpdateRemainingInvites:
    141        this._updateRemainingInvites();
    142        break;
    143      case LoxTopics.NewInvite:
    144        this._updateExistingInvites();
    145        break;
    146    }
    147  },
    148 
    149  /**
    150   * The loxId this dialog is shown for. null if uninitailized.
    151   *
    152   * @type {string?}
    153   */
    154  _loxId: null,
    155  /**
    156   * Update the _loxId value. Will close the dialog if it changes after
    157   * initialization.
    158   */
    159  _updateLoxId() {
    160    const loxId =
    161      TorSettings.bridges.source === TorBridgeSource.Lox ? Lox.activeLoxId : "";
    162    if (!loxId || (this._loxId !== null && loxId !== this._loxId)) {
    163      // No lox id, or it changed. Close this dialog.
    164      this._dialog.cancelDialog();
    165    }
    166    this._loxId = loxId;
    167  },
    168 
    169  /**
    170   * The invites that are already shown.
    171   *
    172   * @type {Set<string>}
    173   */
    174  _shownInvites: new Set(),
    175 
    176  /**
    177   * Add a new invite at the start of the list.
    178   *
    179   * @param {string} invite - The invite to add.
    180   */
    181  _addInvite(invite) {
    182    if (this._shownInvites.has(invite)) {
    183      return;
    184    }
    185    const newInvite = document.createXULElement("richlistitem");
    186    newInvite.classList.add("lox-invite-dialog-list-item");
    187    newInvite.textContent = invite;
    188 
    189    this._inviteListEl.prepend(newInvite);
    190    this._shownInvites.add(invite);
    191  },
    192 
    193  /**
    194   * Update the display of the existing invites.
    195   */
    196  _updateExistingInvites() {
    197    // Add new invites.
    198 
    199    // NOTE: we only expect invites to be appended, so we won't re-order any.
    200    // NOTE: invites are ordered with the oldest first.
    201    for (const invite of Lox.getInvites()) {
    202      this._addInvite(invite);
    203    }
    204  },
    205 
    206  /**
    207   * The shown number or remaining invites we have.
    208   *
    209   * @type {integer}
    210   */
    211  _remainingInvites: 0,
    212 
    213  /**
    214   * Update the display of the remaining invites.
    215   */
    216  _updateRemainingInvites() {
    217    this._remainingInvites = Lox.getRemainingInviteCount(this._loxId);
    218 
    219    document.l10n.setAttributes(
    220      this._remainingInvitesEl,
    221      "tor-bridges-lox-remaining-invites",
    222      { numInvites: this._remainingInvites }
    223    );
    224    this._updateGenerateButtonState();
    225  },
    226 
    227  /**
    228   * Whether we are currently generating an invite.
    229   *
    230   * @type {boolean}
    231   */
    232  _generating: false,
    233  /**
    234   * Set whether we are generating an invite.
    235   *
    236   * @param {boolean} isGenerating - Whether we are generating.
    237   */
    238  _setGenerating(isGenerating) {
    239    this._generating = isGenerating;
    240    this._updateGenerateButtonState();
    241    this._generateArea.classList.toggle("show-connecting", isGenerating);
    242  },
    243 
    244  /**
    245   * Whether the generate button is disabled.
    246   *
    247   * @type {boolean}
    248   */
    249  _generateDisabled: false,
    250  /**
    251   * Update the state of the generate button.
    252   */
    253  _updateGenerateButtonState() {
    254    const disabled = this._generating || !this._remainingInvites;
    255    this._generateDisabled = disabled;
    256    // When generating we use "aria-disabled" rather than the "disabled"
    257    // attribute so that the button can remain focusable whilst we generate
    258    // invites.
    259    // TODO: Replace with moz-button when it handles this for us. See
    260    // tor-browser#43275.
    261    this._generateButton.classList.toggle("spoof-button-disabled", disabled);
    262    this._generateButton.tabIndex = disabled ? -1 : 0;
    263    if (disabled) {
    264      this._generateButton.setAttribute("aria-disabled", "true");
    265    } else {
    266      this._generateButton.removeAttribute("aria-disabled");
    267    }
    268  },
    269 
    270  /**
    271   * Start generating a new invite.
    272   */
    273  _generateNewInvite() {
    274    if (this._generateDisabled) {
    275      return;
    276    }
    277    if (this._generating) {
    278      console.error("Already generating an invite");
    279      return;
    280    }
    281    this._setGenerating(true);
    282    // Clear the previous error.
    283    this._updateGenerateError(null);
    284 
    285    let moveFocus = false;
    286    Lox.generateInvite(this._loxId)
    287      .finally(() => {
    288        // Fetch whether the generate button has focus before we potentially
    289        // disable it.
    290        moveFocus = this._generateButton.contains(document.activeElement);
    291        this._setGenerating(false);
    292      })
    293      .then(
    294        invite => {
    295          this._addInvite(invite);
    296 
    297          if (!this._inviteListEl.contains(document.activeElement)) {
    298            // Does not have focus, change the selected item to be the new
    299            // invite (at index 0).
    300            this._inviteListEl.selectedIndex = 0;
    301          }
    302 
    303          if (moveFocus) {
    304            // Move focus to the new invite before we hide the "Connecting"
    305            // message.
    306            this._inviteListEl.focus();
    307          }
    308        },
    309        loxError => {
    310          console.error("Failed to generate an invite", loxError);
    311          switch (loxError instanceof LoxError ? loxError.code : null) {
    312            case LoxError.LoxServerUnreachable:
    313              this._updateGenerateError("no-server");
    314              break;
    315            default:
    316              this._updateGenerateError("generic");
    317              break;
    318          }
    319        }
    320      );
    321  },
    322 
    323  /**
    324   * Update the shown generation error.
    325   *
    326   * @param {string?} type - The error type, or null if no error should be
    327   *   shown.
    328   */
    329  _updateGenerateError(type) {
    330    // First clear the existing error.
    331    this._errorEl.removeAttribute("data-l10n-id");
    332    this._errorEl.textContent = "";
    333    this._generateArea.classList.toggle("show-error", !!type);
    334 
    335    if (!type) {
    336      return;
    337    }
    338 
    339    let errorId;
    340    switch (type) {
    341      case "no-server":
    342        errorId = "lox-invite-dialog-no-server-error";
    343        break;
    344      case "generic":
    345        // Generic error.
    346        errorId = "lox-invite-dialog-generic-invite-error";
    347        break;
    348    }
    349 
    350    document.l10n.setAttributes(this._errorEl, errorId);
    351  },
    352 };
    353 
    354 window.addEventListener(
    355  "DOMContentLoaded",
    356  () => {
    357    gLoxInvites.init();
    358    window.addEventListener(
    359      "unload",
    360      () => {
    361        gLoxInvites.uninit();
    362      },
    363      { once: true }
    364    );
    365  },
    366  { once: true }
    367 );