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 );