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 }