tor-browser

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

BridgeEmoji.js (5575B)


      1 "use strict";
      2 
      3 {
      4  /**
      5   * Element to display a single bridge emoji, with a localized name.
      6   */
      7  class BridgeEmoji extends HTMLElement {
      8    static #activeInstances = new Set();
      9    static #observer(subject, topic) {
     10      if (topic === "intl:app-locales-changed") {
     11        BridgeEmoji.#updateEmojiLangCode();
     12      }
     13    }
     14 
     15    static #addActiveInstance(inst) {
     16      if (this.#activeInstances.size === 0) {
     17        Services.obs.addObserver(this.#observer, "intl:app-locales-changed");
     18        this.#updateEmojiLangCode();
     19      }
     20      this.#activeInstances.add(inst);
     21    }
     22 
     23    static #removeActiveInstance(inst) {
     24      this.#activeInstances.delete(inst);
     25      if (this.#activeInstances.size === 0) {
     26        Services.obs.removeObserver(this.#observer, "intl:app-locales-changed");
     27      }
     28    }
     29 
     30    /**
     31     * The language code for emoji annotations.
     32     *
     33     * null if unset.
     34     *
     35     * @type {string?}
     36     */
     37    static #emojiLangCode = null;
     38    /**
     39     * A promise that resolves to two JSON structures for bridge-emojis.json and
     40     * annotations.json, respectively.
     41     *
     42     * @type {Promise}
     43     */
     44    static #emojiPromise = Promise.all([
     45      fetch(
     46        "chrome://browser/content/torpreferences/bridgemoji/bridge-emojis.json"
     47      ).then(response => response.json()),
     48      fetch(
     49        "chrome://browser/content/torpreferences/bridgemoji/annotations.json"
     50      ).then(response => response.json()),
     51    ]);
     52 
     53    static #unknownStringPromise = null;
     54 
     55    /**
     56     * Update #emojiLangCode.
     57     */
     58    static async #updateEmojiLangCode() {
     59      let langCode;
     60      const emojiAnnotations = (await BridgeEmoji.#emojiPromise)[1];
     61      // Find the first desired locale we have annotations for.
     62      // Add "en" as a fallback.
     63      for (const bcp47 of [...Services.locale.appLocalesAsBCP47, "en"]) {
     64        langCode = bcp47;
     65        if (langCode in emojiAnnotations) {
     66          break;
     67        }
     68        // Remove everything after the dash, if there is one.
     69        langCode = bcp47.replace(/-.*/, "");
     70        if (langCode in emojiAnnotations) {
     71          break;
     72        }
     73      }
     74      if (langCode !== this.#emojiLangCode) {
     75        this.#emojiLangCode = langCode;
     76        this.#unknownStringPromise = document.l10n.formatValue(
     77          "tor-bridges-emoji-unknown"
     78        );
     79        for (const inst of this.#activeInstances) {
     80          inst.update();
     81        }
     82      }
     83    }
     84 
     85    /**
     86     * Update the bridge emoji to show their corresponding emoji with an
     87     * annotation that matches the current locale.
     88     */
     89    async update() {
     90      if (!this.#active) {
     91        return;
     92      }
     93 
     94      if (!BridgeEmoji.#emojiLangCode) {
     95        // No lang code yet, wait until it is updated.
     96        return;
     97      }
     98 
     99      const doc = this.ownerDocument;
    100      const [unknownString, [emojiList, emojiAnnotations]] = await Promise.all([
    101        BridgeEmoji.#unknownStringPromise,
    102        BridgeEmoji.#emojiPromise,
    103      ]);
    104 
    105      const emoji = emojiList[this.#index];
    106      let emojiName;
    107      if (!emoji) {
    108        // Unexpected.
    109        this.#img.removeAttribute("src");
    110      } else {
    111        const cp = emoji.codePointAt(0).toString(16);
    112        this.#img.setAttribute(
    113          "src",
    114          `chrome://browser/content/torpreferences/bridgemoji/svgs/${cp}.svg`
    115        );
    116        emojiName = emojiAnnotations[BridgeEmoji.#emojiLangCode][cp];
    117      }
    118      if (!emojiName) {
    119        doc.defaultView.console.error(`No emoji for index ${this.#index}`);
    120        emojiName = unknownString;
    121      }
    122      doc.l10n.setAttributes(this.#img, "tor-bridges-emoji-image", {
    123        emojiName,
    124      });
    125    }
    126 
    127    /**
    128     * The index for this bridge emoji.
    129     *
    130     * @type {integer?}
    131     */
    132    #index = null;
    133    /**
    134     * Whether we are active (i.e. in the DOM).
    135     *
    136     * @type {boolean}
    137     */
    138    #active = false;
    139    /**
    140     * The image element.
    141     *
    142     * @type {HTMLImgElement?}
    143     */
    144    #img = null;
    145 
    146    constructor(index) {
    147      super();
    148      this.#index = index;
    149    }
    150 
    151    connectedCallback() {
    152      if (!this.#img) {
    153        this.#img = this.ownerDocument.createElement("img");
    154        this.#img.classList.add("tor-bridges-emoji-icon");
    155        this.#img.setAttribute("alt", "");
    156        this.appendChild(this.#img);
    157      }
    158 
    159      this.#active = true;
    160      BridgeEmoji.#addActiveInstance(this);
    161      this.update();
    162    }
    163 
    164    disconnectedCallback() {
    165      this.#active = false;
    166      BridgeEmoji.#removeActiveInstance(this);
    167    }
    168 
    169    /**
    170     * Create four bridge emojis for the given address.
    171     *
    172     * @param {string} bridgeLine - The bridge address.
    173     *
    174     * @returns {BridgeEmoji[4]} - The bridge emoji elements.
    175     */
    176    static createForAddress(bridgeLine) {
    177      // JS uses UTF-16. While most of these emojis are surrogate pairs, a few
    178      // ones fit one UTF-16 character. So we could not use neither indices,
    179      // nor substr, nor some function to split the string.
    180      // FNV-1a implementation that is compatible with other languages
    181      const prime = 0x01000193;
    182      const offset = 0x811c9dc5;
    183      let hash = offset;
    184      const encoder = new TextEncoder();
    185      for (const byte of encoder.encode(bridgeLine)) {
    186        hash = Math.imul(hash ^ byte, prime);
    187      }
    188 
    189      return [
    190        ((hash & 0x7f000000) >> 24) | (hash < 0 ? 0x80 : 0),
    191        (hash & 0x00ff0000) >> 16,
    192        (hash & 0x0000ff00) >> 8,
    193        hash & 0x000000ff,
    194      ].map(index => new BridgeEmoji(index));
    195    }
    196  }
    197 
    198  customElements.define("tor-bridge-emoji", BridgeEmoji);
    199 }