morgan pushed to branch tor-browser-153.0esr-16.0-1 at The Tor Project / Applications / Tor Browser

Commits:

13 changed files:

Changes:

  • browser/components/preferences/preferences.js
    ... ... @@ -271,7 +271,7 @@ const CONFIG_PANES = Object.freeze({
    271 271
       connection: {
    
    272 272
         l10nId: "tor-connection-settings-pane",
    
    273 273
         iconSrc: "chrome://browser/content/torconnect/tor-connect.svg",
    
    274
    -    groupIds: ["connectionStatus"],
    
    274
    +    groupIds: ["connectionStatus", "torBridges"],
    
    275 275
         module: "chrome://browser/content/torpreferences/config/connection.mjs",
    
    276 276
         visible: () => {
    
    277 277
           return TorConnect.enabled;
    

  • browser/components/preferences/preferences.xhtml
    ... ... @@ -108,6 +108,7 @@
    108 108
       <script type="module" src="chrome://browser/content/preferences/widgets/update-information.mjs"></script>
    
    109 109
       <script type="module" src="chrome://browser/content/preferences/widgets/update-state.mjs"></script>
    
    110 110
       <script type="module" src="chrome://browser/content/ipprotection/bandwidth-usage.mjs"></script>
    
    111
    +  <script type="module" src="chrome://browser/content/torpreferences/widgets/tor-bridges-display.mjs"></script>
    
    111 112
       <script type="module" src="chrome://browser/content/torpreferences/widgets/tor-connection-status.mjs"></script>
    
    112 113
       <script src="chrome://browser/content/torpreferences/bridgemoji/BridgeEmoji.js"/>
    
    113 114
     </head>
    

  • browser/components/torpreferences/config/connection.mjs
    ... ... @@ -4,11 +4,33 @@ import { Preferences } from "chrome://global/content/preferences/Preferences.mjs
    4 4
     const lazy = {};
    
    5 5
     ChromeUtils.defineESModuleGetters(lazy, {
    
    6 6
       InternetStatus: "moz-src:///toolkit/modules/TorConnect.sys.mjs",
    
    7
    +  openBridgeDialog:
    
    8
    +    "chrome://browser/content/torpreferences/config/helpers.mjs",
    
    9
    +  openUserProvideBridgeDialog:
    
    10
    +    "chrome://browser/content/torpreferences/config/helpers.mjs",
    
    11
    +  TorBridgeSource: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
    
    7 12
       TorConnect: "moz-src:///toolkit/modules/TorConnect.sys.mjs",
    
    8 13
       TorConnectStage: "moz-src:///toolkit/modules/TorConnect.sys.mjs",
    
    9 14
       TorConnectTopics: "moz-src:///toolkit/modules/TorConnect.sys.mjs",
    
    15
    +  TorProviderBuilder:
    
    16
    +    "moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs",
    
    17
    +  TorProviderState:
    
    18
    +    "moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs",
    
    19
    +  TorProviderTopics:
    
    20
    +    "moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs",
    
    21
    +  TorSettings: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
    
    22
    +  TorSettingsTopics: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
    
    10 23
     });
    
    11 24
     
    
    25
    +// TODO: Change to GetLoxBridges if Lox enabled, and the account is set up.
    
    26
    +const TELEGRAM_USER_NAME = "GetBridgesBot";
    
    27
    +const TELEGRAM_HREF = `https://t.me/${TELEGRAM_USER_NAME}`;
    
    28
    +
    
    29
    +const TOR_BRIDGES_URL_NAME = "bridges.torproject.org";
    
    30
    +const TOR_BRIDGES_HREF = "https://bridges.torproject.org";
    
    31
    +
    
    32
    +const TOR_BRIDGES_EMAIL = "bridges@torproject.org";
    
    33
    +
    
    12 34
     SettingGroupManager.registerGroups({
    
    13 35
       connectionStatus: {
    
    14 36
         inProgress: true,
    
    ... ... @@ -39,6 +61,123 @@ SettingGroupManager.registerGroups({
    39 61
           },
    
    40 62
         ],
    
    41 63
       },
    
    64
    +  torBridges: {
    
    65
    +    inProgress: true,
    
    66
    +    l10nId: "tor-bridges-group",
    
    67
    +    supportPage: "tor-manual:bridges",
    
    68
    +    headingLevel: 2,
    
    69
    +    controlAttrs: { "focusable-heading": true },
    
    70
    +    items: [
    
    71
    +      {
    
    72
    +        id: "bridgesEnabled",
    
    73
    +        l10nId: "tor-bridges-use-bridges",
    
    74
    +        control: "moz-toggle",
    
    75
    +      },
    
    76
    +      {
    
    77
    +        id: "torBridgesDisplay",
    
    78
    +        control: "tor-bridges-display",
    
    79
    +      },
    
    80
    +      {
    
    81
    +        id: "newBridgesGroup",
    
    82
    +        control: "moz-fieldset",
    
    83
    +        controlAttrs: {
    
    84
    +          headinglevel: 3,
    
    85
    +        },
    
    86
    +        options: [
    
    87
    +          {
    
    88
    +            control: "moz-box-group",
    
    89
    +            items: [
    
    90
    +              {
    
    91
    +                id: "builtinBridges",
    
    92
    +                l10nId: "tor-bridges-choose-built-in-button",
    
    93
    +                control: "moz-box-button",
    
    94
    +              },
    
    95
    +              {
    
    96
    +                id: "userProvidedBridges",
    
    97
    +                l10nId: "tor-bridges-enter-bridges-button",
    
    98
    +                control: "moz-box-button",
    
    99
    +              },
    
    100
    +            ],
    
    101
    +          },
    
    102
    +        ],
    
    103
    +      },
    
    104
    +      {
    
    105
    +        id: "findMoreBridgesGroup",
    
    106
    +        l10nId: "tor-bridges-find-more-group",
    
    107
    +        control: "moz-fieldset",
    
    108
    +        controlAttrs: {
    
    109
    +          headinglevel: 3,
    
    110
    +        },
    
    111
    +        options: [
    
    112
    +          {
    
    113
    +            control: "moz-box-item",
    
    114
    +            options: [
    
    115
    +              {
    
    116
    +                id: "torBridgesRequestBanner",
    
    117
    +                control: "article",
    
    118
    +                options: [
    
    119
    +                  {
    
    120
    +                    control: "img",
    
    121
    +                    controlAttrs: {
    
    122
    +                      alt: "",
    
    123
    +                      src: "chrome://browser/content/torpreferences/bridge-bot.svg",
    
    124
    +                    },
    
    125
    +                  },
    
    126
    +                  {
    
    127
    +                    control: "p",
    
    128
    +                    l10nId: "tor-bridges-request-from-browser2",
    
    129
    +                  },
    
    130
    +                  {
    
    131
    +                    // NOTE: We use the wrapping `div` to simply switch from the
    
    132
    +                    // `options` context to the `items` context, with the latter
    
    133
    +                    // wrapping the elements in a setting-control.
    
    134
    +                    control: "div",
    
    135
    +                    items: [
    
    136
    +                      {
    
    137
    +                        id: "requestBridges",
    
    138
    +                        l10nId: "tor-bridges-request-button2",
    
    139
    +                        control: "moz-button",
    
    140
    +                      },
    
    141
    +                    ],
    
    142
    +                  },
    
    143
    +                ],
    
    144
    +              },
    
    145
    +            ],
    
    146
    +          },
    
    147
    +          {
    
    148
    +            control: "moz-box-group",
    
    149
    +            options: [
    
    150
    +              {
    
    151
    +                l10nId: "tor-bridges-source-telegram-link",
    
    152
    +                l10nArgs: { telegramUserName: TELEGRAM_USER_NAME },
    
    153
    +                control: "moz-box-link",
    
    154
    +                iconSrc:
    
    155
    +                  "chrome://browser/content/torpreferences/telegram-logo.svg",
    
    156
    +                controlAttrs: {
    
    157
    +                  href: TELEGRAM_HREF,
    
    158
    +                },
    
    159
    +              },
    
    160
    +              {
    
    161
    +                l10nId: "tor-bridges-source-web-link",
    
    162
    +                l10nArgs: { url: TOR_BRIDGES_URL_NAME },
    
    163
    +                control: "moz-box-link",
    
    164
    +                iconSrc: "chrome://browser/content/torconnect/network.svg",
    
    165
    +                controlAttrs: {
    
    166
    +                  href: TOR_BRIDGES_HREF,
    
    167
    +                },
    
    168
    +              },
    
    169
    +              {
    
    170
    +                l10nId: "tor-bridges-source-email-link",
    
    171
    +                l10nArgs: { address: TOR_BRIDGES_EMAIL },
    
    172
    +                control: "moz-box-item",
    
    173
    +                iconSrc: "chrome://browser/content/torpreferences/mail.svg",
    
    174
    +              },
    
    175
    +            ],
    
    176
    +          },
    
    177
    +        ],
    
    178
    +      },
    
    179
    +    ],
    
    180
    +  },
    
    42 181
     });
    
    43 182
     
    
    44 183
     Preferences.addSetting({
    
    ... ... @@ -113,3 +252,277 @@ Preferences.addSetting({
    113 252
         lazy.TorConnect.quickstart = val;
    
    114 253
       },
    
    115 254
     });
    
    255
    +
    
    256
    +Preferences.addSetting({
    
    257
    +  id: "torSettingsReady",
    
    258
    +  _ready: false,
    
    259
    +  setup(emitChange) {
    
    260
    +    if (!lazy.TorSettings.enabled) {
    
    261
    +      // Remain in `false`.
    
    262
    +      return;
    
    263
    +    }
    
    264
    +    // Most likely, TorSettings will already be initialised.
    
    265
    +    if (lazy.TorSettings.initialized) {
    
    266
    +      this._ready = true;
    
    267
    +      return;
    
    268
    +    }
    
    269
    +    // Else, wait for it to be initialised.
    
    270
    +    lazy.TorSettings.initializedPromise.then(
    
    271
    +      () => {
    
    272
    +        this._ready = true;
    
    273
    +        emitChange();
    
    274
    +      },
    
    275
    +      error => {
    
    276
    +        // No change in state.
    
    277
    +        console.error("TorSettings failed to initialize.", error);
    
    278
    +      }
    
    279
    +    );
    
    280
    +  },
    
    281
    +  get() {
    
    282
    +    return this._ready;
    
    283
    +  },
    
    284
    +});
    
    285
    +
    
    286
    +Preferences.addSetting({
    
    287
    +  id: "torBridgesRaw",
    
    288
    +  deps: ["torSettingsReady"],
    
    289
    +  _value: null,
    
    290
    +  setup(emitChange) {
    
    291
    +    const observer = subject => {
    
    292
    +      const { changes } = subject.wrappedJSObject;
    
    293
    +      // NOTE: We do not include "bridges.lox_id" in the changes. Instead, any
    
    294
    +      // widgets should wait for LoxTopics.UpdateActiveLoxId to ensure that the
    
    295
    +      // Lox module has responded to the change in ID strictly *before* we do.
    
    296
    +      // In particular, we want to make sure the invites and event data has been
    
    297
    +      // cleared.
    
    298
    +      if (
    
    299
    +        changes.includes("bridges.source") ||
    
    300
    +        changes.includes("bridges.bridge_strings") ||
    
    301
    +        changes.includes("bridges.builtin_type")
    
    302
    +      ) {
    
    303
    +        // Reset.
    
    304
    +        this._value = null;
    
    305
    +        emitChange();
    
    306
    +      }
    
    307
    +    };
    
    308
    +    Services.obs.addObserver(observer, lazy.TorSettingsTopics.SettingsChanged);
    
    309
    +    return () => {
    
    310
    +      Services.obs.removeObserver(
    
    311
    +        observer,
    
    312
    +        lazy.TorSettingsTopics.SettingsChanged
    
    313
    +      );
    
    314
    +    };
    
    315
    +  },
    
    316
    +  get(_pref, { torSettingsReady }) {
    
    317
    +    if (this._value === null) {
    
    318
    +      if (!torSettingsReady.value) {
    
    319
    +        // TorSettings getter will throw.
    
    320
    +        return null;
    
    321
    +      }
    
    322
    +      const source = lazy.TorSettings.bridges.source;
    
    323
    +      // Cache a value.
    
    324
    +      this._value = {
    
    325
    +        haveBridges: source !== lazy.TorBridgeSource.Invalid,
    
    326
    +        source,
    
    327
    +        builtinType: lazy.TorSettings.bridges.builtin_type,
    
    328
    +        bridgeStrings: lazy.TorSettings.bridges.bridge_strings,
    
    329
    +      };
    
    330
    +    }
    
    331
    +    return this._value;
    
    332
    +  },
    
    333
    +});
    
    334
    +
    
    335
    +Preferences.addSetting({
    
    336
    +  id: "connectedBridgeId",
    
    337
    +  _value: null,
    
    338
    +  setup(emitChange) {
    
    339
    +    const observer = async () => {
    
    340
    +      // NOTE: It should be safe for this method to be called concurrently.
    
    341
    +      let bridge = null;
    
    342
    +      try {
    
    343
    +        if (
    
    344
    +          lazy.TorProviderBuilder.currentState() ===
    
    345
    +          lazy.TorProviderState.Running
    
    346
    +        ) {
    
    347
    +          bridge = (await lazy.TorProviderBuilder.build()).currentBridge;
    
    348
    +        }
    
    349
    +        // Else, bridge is `null` whilst the provider is not running.
    
    350
    +      } catch (e) {
    
    351
    +        console.warn("Could not get current bridge", e);
    
    352
    +      }
    
    353
    +      const prevVal = this._value;
    
    354
    +      this._value = bridge?.fingerprint ?? null;
    
    355
    +      if (prevVal !== this._value) {
    
    356
    +        emitChange();
    
    357
    +      }
    
    358
    +    };
    
    359
    +    Services.obs.addObserver(observer, lazy.TorProviderTopics.BridgeChanged);
    
    360
    +    // NOTE: BridgeChanged is only fired directly by the provider instances,
    
    361
    +    // rather than by TorProviderBuilder. In particular, it will not fire when
    
    362
    +    // the previous provider had a bridge and the new one does not, because
    
    363
    +    // neither provider saw a change in their own bridge. But from the user's
    
    364
    +    // point of view, the overall change would mean that the current bridge has
    
    365
    +    // changed.
    
    366
    +    // Moreover, we want to show no connected bridge whilst we are missing a
    
    367
    +    // provider. Therefore, we also need to listen for a change in provider and
    
    368
    +    // its state.
    
    369
    +    // TODO: Maybe this logic should be moved to TorProviderBuilder itself if it
    
    370
    +    // is ever needed by other parts of the UI.
    
    371
    +    Services.obs.addObserver(
    
    372
    +      observer,
    
    373
    +      lazy.TorProviderTopics.ProviderStateChanged
    
    374
    +    );
    
    375
    +    // Get the initial value.
    
    376
    +    observer();
    
    377
    +
    
    378
    +    return () => {
    
    379
    +      Services.obs.removeObserver(
    
    380
    +        observer,
    
    381
    +        lazy.TorProviderTopics.ProviderStateChanged
    
    382
    +      );
    
    383
    +      Services.obs.removeObserver(
    
    384
    +        observer,
    
    385
    +        lazy.TorProviderTopics.BridgeChanged
    
    386
    +      );
    
    387
    +    };
    
    388
    +  },
    
    389
    +  get() {
    
    390
    +    return this._value;
    
    391
    +  },
    
    392
    +});
    
    393
    +
    
    394
    +Preferences.addSetting({
    
    395
    +  id: "bridgesEnabled",
    
    396
    +  deps: ["torSettingsReady", "torBridgesRaw"],
    
    397
    +  setup(emitChange) {
    
    398
    +    const observer = subject => {
    
    399
    +      const { changes } = subject.wrappedJSObject;
    
    400
    +      if (changes.includes("bridges.enabled")) {
    
    401
    +        emitChange();
    
    402
    +      }
    
    403
    +    };
    
    404
    +    Services.obs.addObserver(observer, lazy.TorSettingsTopics.SettingsChanged);
    
    405
    +    return () => {
    
    406
    +      Services.obs.removeObserver(
    
    407
    +        observer,
    
    408
    +        lazy.TorSettingsTopics.SettingsChanged
    
    409
    +      );
    
    410
    +    };
    
    411
    +  },
    
    412
    +  get(_prefVal, { torSettingsReady }) {
    
    413
    +    if (!torSettingsReady.value) {
    
    414
    +      // TorSettings.bridges will throw before TorSettings has finished
    
    415
    +      // initialisation.
    
    416
    +      return false;
    
    417
    +    }
    
    418
    +    return lazy.TorSettings.bridges.enabled;
    
    419
    +  },
    
    420
    +  set(val) {
    
    421
    +    lazy.TorSettings.changeSettings({
    
    422
    +      bridges: { enabled: val },
    
    423
    +    });
    
    424
    +  },
    
    425
    +  visible({ torSettingsReady }) {
    
    426
    +    return torSettingsReady.value;
    
    427
    +  },
    
    428
    +  disabled({ torBridgesRaw }) {
    
    429
    +    return !torBridgesRaw.value?.haveBridges;
    
    430
    +  },
    
    431
    +});
    
    432
    +
    
    433
    +Preferences.addSetting({
    
    434
    +  id: "torBridgesDisplay",
    
    435
    +  deps: ["torSettingsReady", "torBridgesRaw", "connectedBridgeId"],
    
    436
    +  getControlConfig(config, { torBridgesRaw, connectedBridgeId }) {
    
    437
    +    config.controlAttrs = {
    
    438
    +      ...config.controlAttrs,
    
    439
    +      // Set the `bridges` and `connnectedBridgeId` object *properties* (rather
    
    440
    +      // than attributes) by using the `.` prefix.
    
    441
    +      ".bridges": torBridgesRaw.value,
    
    442
    +      ".connectedBridgeId": connectedBridgeId.value,
    
    443
    +    };
    
    444
    +    return config;
    
    445
    +  },
    
    446
    +  visible({ torSettingsReady }) {
    
    447
    +    return torSettingsReady.value;
    
    448
    +  },
    
    449
    +});
    
    450
    +
    
    451
    +Preferences.addSetting({
    
    452
    +  id: "newBridgesGroup",
    
    453
    +  deps: ["torSettingsReady", "torBridgesRaw"],
    
    454
    +  getControlConfig(config, { torBridgesRaw }) {
    
    455
    +    config.l10nId = torBridgesRaw.value?.haveBridges
    
    456
    +      ? "tor-bridges-replace-bridges-group"
    
    457
    +      : "tor-bridges-add-bridges-group";
    
    458
    +    return config;
    
    459
    +  },
    
    460
    +  visible({ torSettingsReady }) {
    
    461
    +    return torSettingsReady.value;
    
    462
    +  },
    
    463
    +});
    
    464
    +
    
    465
    +Preferences.addSetting({
    
    466
    +  id: "builtinBridges",
    
    467
    +  onUserClick() {
    
    468
    +    lazy.openBridgeDialog(
    
    469
    +      window,
    
    470
    +      "chrome://browser/content/torpreferences/builtinBridgeDialog.xhtml",
    
    471
    +      null,
    
    472
    +      result => {
    
    473
    +        if (!result.type) {
    
    474
    +          return null;
    
    475
    +        }
    
    476
    +        return lazy.TorSettings.changeSettings({
    
    477
    +          bridges: {
    
    478
    +            enabled: true,
    
    479
    +            source: lazy.TorBridgeSource.BuiltIn,
    
    480
    +            builtin_type: result.type,
    
    481
    +          },
    
    482
    +        });
    
    483
    +      }
    
    484
    +    );
    
    485
    +  },
    
    486
    +});
    
    487
    +
    
    488
    +Preferences.addSetting({
    
    489
    +  id: "userProvidedBridges",
    
    490
    +  deps: ["torBridgesRaw"],
    
    491
    +  onUserClick(_event, { torBridgesRaw }) {
    
    492
    +    lazy.openUserProvideBridgeDialog(
    
    493
    +      window,
    
    494
    +      torBridgesRaw.value?.haveBridges ? "replace" : "add"
    
    495
    +    );
    
    496
    +  },
    
    497
    +});
    
    498
    +
    
    499
    +Preferences.addSetting({
    
    500
    +  id: "findMoreBridgesGroup",
    
    501
    +  deps: ["torSettingsReady"],
    
    502
    +  visible({ torSettingsReady }) {
    
    503
    +    return torSettingsReady.value;
    
    504
    +  },
    
    505
    +});
    
    506
    +
    
    507
    +Preferences.addSetting({
    
    508
    +  id: "requestBridges",
    
    509
    +  onUserClick() {
    
    510
    +    lazy.openBridgeDialog(
    
    511
    +      window,
    
    512
    +      "chrome://browser/content/torpreferences/requestBridgeDialog.xhtml",
    
    513
    +      null,
    
    514
    +      result => {
    
    515
    +        if (!result.bridges?.length) {
    
    516
    +          return null;
    
    517
    +        }
    
    518
    +        return lazy.TorSettings.changeSettings({
    
    519
    +          bridges: {
    
    520
    +            enabled: true,
    
    521
    +            source: lazy.TorBridgeSource.BridgeDB,
    
    522
    +            bridge_strings: result.bridges,
    
    523
    +          },
    
    524
    +        });
    
    525
    +      }
    
    526
    +    );
    
    527
    +  },
    
    528
    +});

  • browser/components/torpreferences/config/helpers.mjs
    1
    +const lazy = {};
    
    2
    +ChromeUtils.defineESModuleGetters(lazy, {
    
    3
    +  TorBridgeSource: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
    
    4
    +  TorConnectParent:
    
    5
    +    "moz-src:///browser/components/torconnect/TorConnectParent.sys.mjs",
    
    6
    +  TorSettings: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
    
    7
    +});
    
    8
    +
    
    9
    +/**
    
    10
    + * Force the focus to move to the bridge heading.
    
    11
    + *
    
    12
    + * @param {Window} win - The preferences window.
    
    13
    + * @param {boolean} [forceTopHeading=false] - Force the focus to move to the
    
    14
    + *   top "Bridges" setting heading.
    
    15
    + */
    
    16
    +export function moveFocusToBridgeHeading(win, forceTopHeading = false) {
    
    17
    +  // Move focus to the start of the relevant section, which is a heading.
    
    18
    +  // They have tabindex="-1" so should be focusable, even though they are not
    
    19
    +  // part of the usual tab navigation.
    
    20
    +  // TODO: It might be better if we could use the # named anchor to
    
    21
    +  // re-orient the screen reader position instead of using tabIndex=-1, but
    
    22
    +  // about:preferences currently uses the anchor for showing categories
    
    23
    +  // only. See bugzilla bug 1799153.
    
    24
    +  if (
    
    25
    +    forceTopHeading ||
    
    26
    +    !win.document.getElementById("torBridgesDisplay").focusHeading()
    
    27
    +  ) {
    
    28
    +    win.document
    
    29
    +      .querySelector('setting-group[groupid="torBridges"] moz-fieldset')
    
    30
    +      .focusHeading();
    
    31
    +  }
    
    32
    +}
    
    33
    +
    
    34
    +/**
    
    35
    + * Open a bridge dialog that will change the users bridges.
    
    36
    + *
    
    37
    + * @param {Window} win - The preferences window.
    
    38
    + * @param {string} url - The url of the dialog to open.
    
    39
    + * @param {object?} inputData - The input data to send to the dialog window.
    
    40
    + * @param {Function} onAccept - The method to call if the bridge dialog was
    
    41
    + *   accepted by the user. This will be passed a "result" object containing
    
    42
    + *   data set by the dialog. This should return a promise that resolves once
    
    43
    + *   the bridge settings have been set, or null if the settings have not
    
    44
    + *   been applied.
    
    45
    + */
    
    46
    +export function openBridgeDialog(win, url, inputData, onAccept) {
    
    47
    +  const result = { accepted: false, connect: false };
    
    48
    +  let savedSettings = null;
    
    49
    +  win.gSubDialog.open(
    
    50
    +    url,
    
    51
    +    {
    
    52
    +      features: "resizable=yes",
    
    53
    +      closingCallback: () => {
    
    54
    +        if (!result.accepted) {
    
    55
    +          return;
    
    56
    +        }
    
    57
    +        savedSettings = onAccept(result);
    
    58
    +        if (!savedSettings) {
    
    59
    +          // No change in settings.
    
    60
    +          return;
    
    61
    +        }
    
    62
    +        if (!result.connect) {
    
    63
    +          // Do not open about:torconnect.
    
    64
    +          return;
    
    65
    +        }
    
    66
    +
    
    67
    +        // Wait until the settings are applied before bootstrapping.
    
    68
    +        // NOTE: Saving the settings should also cancel any existing bootstrap
    
    69
    +        // attempt first. See tor-browser#41921.
    
    70
    +        savedSettings.then(() => {
    
    71
    +          // The bridge dialog button is "connect" when Tor is not
    
    72
    +          // bootstrapped, so do the connect.
    
    73
    +
    
    74
    +          // Start Bootstrapping, which should use the configured bridges.
    
    75
    +          // NOTE: We do this regardless of any previous TorConnect Error.
    
    76
    +          lazy.TorConnectParent.open({ beginBootstrapping: "hard" });
    
    77
    +        });
    
    78
    +      },
    
    79
    +      // closedCallback should be called after gSubDialog has already
    
    80
    +      // re-assigned focus back to the document.
    
    81
    +      closedCallback: () => {
    
    82
    +        if (!savedSettings) {
    
    83
    +          return;
    
    84
    +        }
    
    85
    +        // Wait until the settings have changed, so that the UI could
    
    86
    +        // respond, then move focus.
    
    87
    +        savedSettings.then(() => {
    
    88
    +          moveFocusToBridgeHeading(win);
    
    89
    +        });
    
    90
    +      },
    
    91
    +    },
    
    92
    +    result,
    
    93
    +    inputData
    
    94
    +  );
    
    95
    +}
    
    96
    +
    
    97
    +/**
    
    98
    + * Open the user provide dialog.
    
    99
    + *
    
    100
    + * @param {Window} win - The preferences window.
    
    101
    + * @param {string} mode - The mode to open the dialog in: "add", "replace" or
    
    102
    + *   "edit".
    
    103
    + */
    
    104
    +export function openUserProvideBridgeDialog(win, mode) {
    
    105
    +  openBridgeDialog(
    
    106
    +    win,
    
    107
    +    "chrome://browser/content/torpreferences/provideBridgeDialog.xhtml",
    
    108
    +    { mode },
    
    109
    +    result => {
    
    110
    +      const loxId = result.loxId;
    
    111
    +      if (!loxId && !result.addresses?.length) {
    
    112
    +        return null;
    
    113
    +      }
    
    114
    +      const bridges = { enabled: true };
    
    115
    +      if (loxId) {
    
    116
    +        bridges.source = lazy.TorBridgeSource.Lox;
    
    117
    +        bridges.lox_id = loxId;
    
    118
    +      } else {
    
    119
    +        bridges.source = lazy.TorBridgeSource.UserProvided;
    
    120
    +        bridges.bridge_strings = result.addresses;
    
    121
    +      }
    
    122
    +      return lazy.TorSettings.changeSettings({ bridges });
    
    123
    +    }
    
    124
    +  );
    
    125
    +}

  • browser/components/torpreferences/content/connectionPane.inc.xhtml
    1
    +<html:template id="tor-bridges-display-template">
    
    2
    +  <html:div id="tor-bridges-none" hidden="hidden">
    
    3
    +    <html:img id="tor-bridges-none-icon" alt="" />
    
    4
    +    <html:p data-l10n-id="tor-bridges-none-added"></html:p>
    
    5
    +  </html:div>
    
    6
    +  <html:fieldset id="tor-bridges-current" hidden="hidden">
    
    7
    +    <html:legend>
    
    8
    +      <html:h4
    
    9
    +        id="tor-bridges-current-heading-non-search"
    
    10
    +        class="tor-bridges-current-heading tor-focusable-heading tor-small-heading"
    
    11
    +        tabindex="-1"
    
    12
    +        data-l10n-id="tor-bridges-your-bridges"
    
    13
    +      ></html:h4>
    
    14
    +    </html:legend>
    
    15
    +    <html:p
    
    16
    +      id="tor-bridges-user-label"
    
    17
    +      class="tor-bridges-source-label"
    
    18
    +      data-l10n-id="tor-bridges-source-user"
    
    19
    +    ></html:p>
    
    20
    +    <html:p
    
    21
    +      id="tor-bridges-built-in-label"
    
    22
    +      class="tor-bridges-source-label"
    
    23
    +      data-l10n-id="tor-bridges-source-built-in"
    
    24
    +    ></html:p>
    
    25
    +    <html:p
    
    26
    +      id="tor-bridges-requested-label"
    
    27
    +      class="tor-bridges-source-label"
    
    28
    +      data-l10n-id="tor-bridges-source-requested"
    
    29
    +    ></html:p>
    
    30
    +    <html:p id="tor-bridges-lox-label" class="tor-bridges-source-label">
    
    31
    +      <html:img id="tor-bridges-lox-label-icon" alt="" />
    
    32
    +      <html:span data-l10n-id="tor-bridges-source-lox"></html:span>
    
    33
    +    </html:p>
    
    34
    +    <html:button
    
    35
    +      id="tor-bridges-all-options-button"
    
    36
    +      class="tor-bridges-options-button"
    
    37
    +      aria-haspopup="menu"
    
    38
    +      aria-expanded="false"
    
    39
    +      aria-controls="tor-bridges-all-options-menu"
    
    40
    +      data-l10n-id="tor-bridges-options-button"
    
    41
    +    ></html:button>
    
    42
    +    <html:panel-list
    
    43
    +      id="tor-bridges-all-options-menu"
    
    44
    +      data-hidden-from-search="true"
    
    45
    +    >
    
    46
    +      <html:panel-item
    
    47
    +        id="tor-bridges-options-qr-all-menu-item"
    
    48
    +        data-l10n-attrs="accesskey"
    
    49
    +        data-l10n-id="tor-bridges-menu-item-qr-all-bridge-addresses"
    
    50
    +      ></html:panel-item>
    
    51
    +      <html:panel-item
    
    52
    +        id="tor-bridges-options-copy-all-menu-item"
    
    53
    +        data-l10n-attrs="accesskey"
    
    54
    +        data-l10n-id="tor-bridges-menu-item-copy-all-bridge-addresses"
    
    55
    +      ></html:panel-item>
    
    56
    +      <html:panel-item
    
    57
    +        id="tor-bridges-options-edit-all-menu-item"
    
    58
    +        data-l10n-attrs="accesskey"
    
    59
    +        data-l10n-id="tor-bridges-menu-item-edit-all-bridges"
    
    60
    +      ></html:panel-item>
    
    61
    +      <html:panel-item
    
    62
    +        id="tor-bridges-options-remove-all-menu-item"
    
    63
    +        data-l10n-attrs="accesskey"
    
    64
    +        data-l10n-id="tor-bridges-menu-item-remove-all-bridges"
    
    65
    +      ></html:panel-item>
    
    66
    +    </html:panel-list>
    
    67
    +    <html:div id="tor-bridges-built-in-display" hidden="hidden">
    
    68
    +      <html:p id="tor-bridges-built-in-type-name"></html:p>
    
    69
    +      <html:p id="tor-bridges-built-in-connected" class="bridge-status-badge">
    
    70
    +        <html:div class="bridge-status-icon"></html:div>
    
    71
    +        <html:span
    
    72
    +          data-l10n-id="tor-bridges-built-in-status-connected"
    
    73
    +        ></html:span>
    
    74
    +      </html:p>
    
    75
    +      <html:p id="tor-bridges-built-in-description"></html:p>
    
    76
    +    </html:div>
    
    77
    +    <html:div
    
    78
    +      id="tor-bridges-grid-display"
    
    79
    +      class="tor-bridges-grid"
    
    80
    +      role="grid"
    
    81
    +      aria-labelledby="tor-bridges-current-heading-non-search"
    
    82
    +      hidden="hidden"
    
    83
    +    ></html:div>
    
    84
    +    <html:template id="tor-bridges-grid-row-template">
    
    85
    +      <html:div class="tor-bridges-grid-row" role="row">
    
    86
    +        <!-- TODO: lox status cell for new bridges? -->
    
    87
    +        <html:span
    
    88
    +          class="tor-bridges-type-cell tor-bridges-grid-cell"
    
    89
    +          role="gridcell"
    
    90
    +        ></html:span>
    
    91
    +        <html:span class="tor-bridges-emojis-block" role="none"></html:span>
    
    92
    +        <html:span class="tor-bridges-grid-end-block" role="none">
    
    93
    +          <html:span
    
    94
    +            class="tor-bridges-address-cell tor-bridges-grid-cell"
    
    95
    +            role="gridcell"
    
    96
    +          >
    
    97
    +            <html:span class="tor-bridges-address-cell-text"></html:span>
    
    98
    +          </html:span>
    
    99
    +          <html:span
    
    100
    +            class="tor-bridges-status-cell tor-bridges-grid-cell"
    
    101
    +            role="gridcell"
    
    102
    +          >
    
    103
    +            <html:div class="bridge-status-badge">
    
    104
    +              <html:div class="bridge-status-icon"></html:div>
    
    105
    +              <html:span class="tor-bridges-status-cell-text"></html:span>
    
    106
    +            </html:div>
    
    107
    +          </html:span>
    
    108
    +          <html:span
    
    109
    +            class="tor-bridges-options-cell tor-bridges-grid-cell"
    
    110
    +            role="gridcell"
    
    111
    +          >
    
    112
    +            <html:button
    
    113
    +              class="tor-bridges-options-cell-button tor-bridges-options-button tor-bridges-grid-focus"
    
    114
    +              aria-haspopup="menu"
    
    115
    +              aria-expanded="false"
    
    116
    +              data-l10n-id="tor-bridges-individual-bridge-options-button"
    
    117
    +            ></html:button>
    
    118
    +            <html:panel-list
    
    119
    +              class="tor-bridges-individual-options-menu"
    
    120
    +              data-hidden-from-search="true"
    
    121
    +            >
    
    122
    +              <html:panel-item
    
    123
    +                class="tor-bridges-options-qr-one-menu-item"
    
    124
    +                data-l10n-attrs="accesskey"
    
    125
    +                data-l10n-id="tor-bridges-menu-item-qr-address"
    
    126
    +              ></html:panel-item>
    
    127
    +              <html:panel-item
    
    128
    +                class="tor-bridges-options-copy-one-menu-item"
    
    129
    +                data-l10n-attrs="accesskey"
    
    130
    +                data-l10n-id="tor-bridges-menu-item-copy-address"
    
    131
    +              ></html:panel-item>
    
    132
    +              <html:panel-item
    
    133
    +                class="tor-bridges-options-remove-one-menu-item"
    
    134
    +                data-l10n-attrs="accesskey"
    
    135
    +                data-l10n-id="tor-bridges-menu-item-remove-bridge"
    
    136
    +              ></html:panel-item>
    
    137
    +            </html:panel-list>
    
    138
    +          </html:span>
    
    139
    +        </html:span>
    
    140
    +      </html:div>
    
    141
    +    </html:template>
    
    142
    +    <html:div
    
    143
    +      id="tor-bridges-share"
    
    144
    +      class="tor-bridges-details-box"
    
    145
    +      hidden="hidden"
    
    146
    +    >
    
    147
    +      <html:h5
    
    148
    +        class="tor-bridges-share-heading tor-small-heading"
    
    149
    +        data-l10n-id="tor-bridges-share-heading"
    
    150
    +      ></html:h5>
    
    151
    +      <html:p
    
    152
    +        id="tor-bridges-share-description"
    
    153
    +        data-l10n-id="tor-bridges-share-description"
    
    154
    +      ></html:p>
    
    155
    +      <html:button
    
    156
    +        id="tor-bridges-copy-addresses-button"
    
    157
    +        data-l10n-id="tor-bridges-copy-addresses-button"
    
    158
    +      ></html:button>
    
    159
    +      <html:button
    
    160
    +        id="tor-bridges-qr-addresses-button"
    
    161
    +        data-l10n-id="tor-bridges-qr-addresses-button"
    
    162
    +      ></html:button>
    
    163
    +    </html:div>
    
    164
    +    <html:div id="tor-bridges-lox-status" hidden="hidden">
    
    165
    +      <html:div data-l10n-id="tor-bridges-lox-description"></html:div>
    
    166
    +      <html:div
    
    167
    +        id="tor-bridges-lox-details"
    
    168
    +        class="tor-bridges-details-box tor-bridges-lox-box"
    
    169
    +        hidden="hidden"
    
    170
    +      >
    
    171
    +        <html:img alt="" class="tor-bridges-lox-image-inner" />
    
    172
    +        <html:img alt="" class="tor-bridges-lox-image-outer" />
    
    173
    +        <html:h5
    
    174
    +          class="tor-bridges-lox-next-unlock-counter tor-small-heading tor-bridges-lox-intro tor-focusable-heading"
    
    175
    +          tabindex="-1"
    
    176
    +        ></html:h5>
    
    177
    +        <html:ul class="tor-bridges-lox-list">
    
    178
    +          <html:li
    
    179
    +            id="tor-bridges-lox-next-unlock-gain-bridges"
    
    180
    +            class="tor-bridges-lox-list-item tor-bridges-lox-list-item-bridge"
    
    181
    +            data-l10n-id="tor-bridges-lox-unlock-two-bridges"
    
    182
    +            hidden="hidden"
    
    183
    +          ></html:li>
    
    184
    +          <html:li
    
    185
    +            id="tor-bridges-lox-next-unlock-first-invites"
    
    186
    +            class="tor-bridges-lox-list-item tor-bridges-lox-list-item-invite"
    
    187
    +            data-l10n-id="tor-bridges-lox-unlock-first-invites"
    
    188
    +            hidden="hidden"
    
    189
    +          ></html:li>
    
    190
    +          <html:li
    
    191
    +            id="tor-bridges-lox-next-unlock-more-invites"
    
    192
    +            class="tor-bridges-lox-list-item tor-bridges-lox-list-item-invite"
    
    193
    +            data-l10n-id="tor-bridges-lox-unlock-more-invites"
    
    194
    +            hidden="hidden"
    
    195
    +          ></html:li>
    
    196
    +        </html:ul>
    
    197
    +        <html:div
    
    198
    +          id="tor-bridges-lox-remaining-invites"
    
    199
    +          hidden="hidden"
    
    200
    +        ></html:div>
    
    201
    +        <html:button
    
    202
    +          id="tor-bridges-lox-show-invites-button"
    
    203
    +          class="tor-bridges-lox-button"
    
    204
    +          data-l10n-id="tor-bridges-lox-show-invites-button"
    
    205
    +          hidden="hidden"
    
    206
    +        ></html:button>
    
    207
    +      </html:div>
    
    208
    +      <html:div
    
    209
    +        id="tor-bridges-lox-unlock-alert"
    
    210
    +        role="alert"
    
    211
    +        class="tor-bridges-details-box tor-bridges-lox-box"
    
    212
    +        hidden="hidden"
    
    213
    +      >
    
    214
    +        <html:img alt="" class="tor-bridges-lox-image-inner" />
    
    215
    +        <html:img alt="" class="tor-bridges-lox-image-outer" />
    
    216
    +        <html:div
    
    217
    +          id="tor-bridge-unlock-alert-title"
    
    218
    +          class="tor-small-heading tor-bridges-lox-intro"
    
    219
    +        ></html:div>
    
    220
    +        <html:ul class="tor-bridges-lox-list">
    
    221
    +          <html:li
    
    222
    +            id="tor-bridges-lox-unlock-alert-gain-bridges"
    
    223
    +            class="tor-bridges-lox-list-item tor-bridges-lox-list-item-bridge"
    
    224
    +            data-l10n-id="tor-bridges-lox-gained-two-bridges"
    
    225
    +            hidden="hidden"
    
    226
    +          ></html:li>
    
    227
    +          <html:li
    
    228
    +            id="tor-bridges-lox-unlock-alert-new-bridges"
    
    229
    +            class="tor-bridges-lox-list-item tor-bridges-lox-list-item-bridge"
    
    230
    +            data-l10n-id="tor-bridges-lox-new-bridges"
    
    231
    +            hidden="hidden"
    
    232
    +          ></html:li>
    
    233
    +          <html:li
    
    234
    +            id="tor-bridges-lox-unlock-alert-invites"
    
    235
    +            class="tor-bridges-lox-list-item tor-bridges-lox-list-item-invite"
    
    236
    +            hidden="hidden"
    
    237
    +          ></html:li>
    
    238
    +        </html:ul>
    
    239
    +        <html:button
    
    240
    +          id="tor-bridges-lox-unlock-alert-button"
    
    241
    +          class="tor-bridges-lox-button"
    
    242
    +          data-l10n-id="tor-bridges-lox-got-it-button"
    
    243
    +        ></html:button>
    
    244
    +      </html:div>
    
    245
    +    </html:div>
    
    246
    +  </html:fieldset>
    
    247
    +</html:template>
    
    248
    +
    
    1 249
     <!-- Tor panel -->
    
    2 250
     
    
    3 251
     <script
    

  • browser/components/torpreferences/content/torPreferences.css
    ... ... @@ -201,20 +201,27 @@ button.spoof-button-disabled {
    201 201
     
    
    202 202
     #tor-bridges-none,
    
    203 203
     #tor-bridges-current {
    
    204
    -  margin-inline: 0;
    
    205
    -  margin-block: var(--space-xxlarge);
    
    204
    +  @media not -moz-pref("browser.settings-redesign.enabled") {
    
    205
    +    margin-inline: 0;
    
    206
    +    margin-block: var(--space-xxlarge);
    
    207
    +  }
    
    206 208
     }
    
    207 209
     
    
    208 210
     #tor-bridges-none:not([hidden]) {
    
    209 211
       display: grid;
    
    210 212
       justify-items: center;
    
    211 213
       text-align: center;
    
    212
    -  padding-block: 64px;
    
    214
    +  padding-block: 54px;
    
    213 215
       padding-inline: var(--space-xxlarge);
    
    214
    -  gap: var(--space-large);
    
    216
    +  gap: var(--space-small);
    
    215 217
       border-radius: var(--border-radius-small);
    
    216 218
       color: var(--text-color-deemphasized);
    
    217 219
       border: 2px dashed var(--border-color-deemphasized);
    
    220
    +
    
    221
    +  @media not -moz-pref("browser.settings-redesign.enabled") {
    
    222
    +    padding-block: 64px;
    
    223
    +    gap: var(--space-large);
    
    224
    +  }
    
    218 225
     }
    
    219 226
     
    
    220 227
     #tor-bridges-none-icon {
    
    ... ... @@ -228,9 +235,13 @@ button.spoof-button-disabled {
    228 235
     .tor-bridges-box,
    
    229 236
     .tor-bridges-details-box {
    
    230 237
       padding: var(--space-large);
    
    231
    -  border-radius: var(--border-radius-small);
    
    232
    -  background: var(--background-color-box-info);
    
    238
    +  border-radius: var(--border-radius-medium);
    
    233 239
       border: var(--border-width) solid var(--border-color);
    
    240
    +
    
    241
    +  @media not -moz-pref("browser.settings-redesign.enabled") {
    
    242
    +    border-radius: var(--border-radius-small);
    
    243
    +    background: var(--background-color-box-info);
    
    244
    +  }
    
    234 245
     }
    
    235 246
     
    
    236 247
     @media not forced-colors {
    
    ... ... @@ -250,6 +261,56 @@ button.spoof-button-disabled {
    250 261
       white-space: nowrap;
    
    251 262
     }
    
    252 263
     
    
    264
    +tor-bridges-display {
    
    265
    +  display: block;
    
    266
    +  margin-block: var(--space-small);
    
    267
    +
    
    268
    +  &.has-tor-bridges {
    
    269
    +    display: grid;
    
    270
    +    grid-template:
    
    271
    +      "heading source button" min-content
    
    272
    +      "bridges bridges bridges" auto
    
    273
    +      "extra extra extra" auto
    
    274
    +      / max-content 1fr max-content;
    
    275
    +    align-items: center;
    
    276
    +    border: var(--card-border);
    
    277
    +    border-radius: var(--border-radius-medium);
    
    278
    +    background-color: var(--card-background-color);
    
    279
    +    padding: var(--space-large);
    
    280
    +  }
    
    281
    +
    
    282
    +  & p {
    
    283
    +    margin: 0;
    
    284
    +  }
    
    285
    +
    
    286
    +  & #tor-bridges-current:not([hidden]) {
    
    287
    +    /* Avoid the special display logic for <fieldset> and <legend>. */
    
    288
    +    display: contents;
    
    289
    +  }
    
    290
    +
    
    291
    +  & legend {
    
    292
    +    grid-area: heading;
    
    293
    +    white-space: nowrap;
    
    294
    +  }
    
    295
    +
    
    296
    +  & .tor-bridges-source-label {
    
    297
    +    white-space: nowrap;
    
    298
    +    padding-inline-end: var(--space-small);
    
    299
    +    border-inline-end: var(--border-width) solid var(--border-color);
    
    300
    +  }
    
    301
    +
    
    302
    +  & :is(#tor-bridges-built-in-display, #tor-bridges-grid-display) {
    
    303
    +    grid-area: bridges;
    
    304
    +    margin-block-start: var(--space-large);
    
    305
    +    border-block-start: var(--border-width) solid var(--border-color);
    
    306
    +    padding-block-start: var(--space-large);
    
    307
    +  }
    
    308
    +
    
    309
    +  & :is(#tor-bridges-share, #tor-bridges-lox-status) {
    
    310
    +    grid-area: extra;
    
    311
    +  }
    
    312
    +}
    
    313
    +
    
    253 314
     .tor-bridges-current-heading {
    
    254 315
       grid-area: heading;
    
    255 316
     }
    
    ... ... @@ -681,6 +742,24 @@ button.spoof-button-disabled {
    681 742
       grid-column: 2 / 3;
    
    682 743
     }
    
    683 744
     
    
    745
    +#torBridgesRequestBanner {
    
    746
    +  display: flex;
    
    747
    +  flex-direction: column;
    
    748
    +  align-items: center;
    
    749
    +  text-align: center;
    
    750
    +  text-wrap-style: balance;
    
    751
    +  gap: var(--space-large);
    
    752
    +
    
    753
    +  & > * {
    
    754
    +    margin: 0;
    
    755
    +    flex: 0 0 auto;
    
    756
    +  }
    
    757
    +
    
    758
    +  & > p {
    
    759
    +    max-width: var(--size-layout-medium);
    
    760
    +  }
    
    761
    +}
    
    762
    +
    
    684 763
     #tor-bridges-request-box {
    
    685 764
       /* Take up the full height in the container. */
    
    686 765
       align-self: stretch;
    

  • browser/components/torpreferences/jar.mn
    ... ... @@ -25,6 +25,8 @@ browser.jar:
    25 25
         content/browser/torpreferences/torLogDialog.js                   (content/torLogDialog.js)
    
    26 26
         content/browser/torpreferences/torLogDialog.xhtml                (content/torLogDialog.xhtml)
    
    27 27
         content/browser/torpreferences/config/connection.mjs             (config/connection.mjs)
    
    28
    +    content/browser/torpreferences/config/helpers.mjs                (config/helpers.mjs)
    
    29
    +    content/browser/torpreferences/widgets/tor-bridges-display.mjs   (widgets/tor-bridges-display.mjs)
    
    28 30
         content/browser/torpreferences/widgets/tor-connection-status.mjs (widgets/tor-connection-status.mjs)
    
    29 31
         content/browser/torpreferences/widgets/tor-connection-status.css (widgets/tor-connection-status.css)
    
    30 32
         content/browser/torpreferences/connectionPane.js                 (content/connectionPane.js)
    

  • browser/components/torpreferences/widgets/tor-bridges-display.mjs
    1
    +const lazy = {};
    
    2
    +ChromeUtils.defineESModuleGetters(lazy, {
    
    3
    +  Lox: "moz-src:///toolkit/components/lox/Lox.sys.mjs",
    
    4
    +  LoxTopics: "moz-src:///toolkit/components/lox/Lox.sys.mjs",
    
    5
    +  moveFocusToBridgeHeading:
    
    6
    +    "chrome://browser/content/torpreferences/config/helpers.mjs",
    
    7
    +  openUserProvideBridgeDialog:
    
    8
    +    "chrome://browser/content/torpreferences/config/helpers.mjs",
    
    9
    +  TorBridgeSource: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
    
    10
    +  TorParsers: "moz-src:///toolkit/components/tor-launcher/TorParsers.sys.mjs",
    
    11
    +  TorSettings: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
    
    12
    +});
    
    13
    +
    
    14
    +/**
    
    15
    + * Show the bridge QR to the user.
    
    16
    + *
    
    17
    + * @param {string} bridgeString - The string to use in the QR.
    
    18
    + */
    
    19
    +function showBridgeQr(bridgeString) {
    
    20
    +  window.gSubDialog.open(
    
    21
    +    "chrome://browser/content/torpreferences/bridgeQrDialog.xhtml",
    
    22
    +    { features: "resizable=yes" },
    
    23
    +    bridgeString
    
    24
    +  );
    
    25
    +}
    
    26
    +
    
    27
    +/**
    
    28
    + * Post a new notification, replacing any existing one.
    
    29
    + *
    
    30
    + * @param {string} type - The notification type.
    
    31
    + */
    
    32
    +async function postBridgeNotification(type) {
    
    33
    +  let updateId;
    
    34
    +  switch (type) {
    
    35
    +    case "removed-one":
    
    36
    +      updateId = "tor-bridges-update-removed-one-bridge";
    
    37
    +      break;
    
    38
    +    case "removed-all":
    
    39
    +      updateId = "tor-bridges-update-removed-all-bridges";
    
    40
    +      break;
    
    41
    +    case "changed":
    
    42
    +    default:
    
    43
    +      // Generic message for when bridges change.
    
    44
    +      updateId = "tor-bridges-update-changed-bridges";
    
    45
    +      break;
    
    46
    +  }
    
    47
    +  const bridgeDisplay = document.querySelector("tor-bridges-display");
    
    48
    +  const settingGroup = bridgeDisplay?.closest("setting-group");
    
    49
    +  if (!settingGroup) {
    
    50
    +    console.error("Missing a setting-group for a notification.");
    
    51
    +    return;
    
    52
    +  }
    
    53
    +  if (!settingGroup.checkVisibility()) {
    
    54
    +    // Only ping the user if the bridge settings are visible.
    
    55
    +    // NOTE: Most operations to change the bridges will occur within the
    
    56
    +    // connection settings. However, in principle the user could have multiple
    
    57
    +    // setting tabs open, or they may have the settings open whilst Connection
    
    58
    +    // Assist is setting their bridges.
    
    59
    +    return;
    
    60
    +  }
    
    61
    +  const [message] = await Promise.all([
    
    62
    +    document.l10n.formatValue(updateId),
    
    63
    +    // Wait at least a small amount of time to actually trigger ariaNotify.
    
    64
    +    // Otherwise Orca will ignore the notification when it almost coincides with
    
    65
    +    // a change in focus, which is normally the case.
    
    66
    +    new Promise(resolve => setTimeout(resolve, 500)),
    
    67
    +  ]);
    
    68
    +  bridgeDisplay.ariaNotify(message);
    
    69
    +}
    
    70
    +
    
    71
    +/**
    
    72
    + * Controls the bridge grid.
    
    73
    + */
    
    74
    +const gBridgeGrid = {
    
    75
    +  /**
    
    76
    +   * The grid element.
    
    77
    +   *
    
    78
    +   * @type {Element?}
    
    79
    +   */
    
    80
    +  _grid: null,
    
    81
    +  /**
    
    82
    +   * The template for creating new rows.
    
    83
    +   *
    
    84
    +   * @type {HTMLTemplateElement?}
    
    85
    +   */
    
    86
    +  _rowTemplate: null,
    
    87
    +
    
    88
    +  /**
    
    89
    +   * @typedef {object} BridgeGridRow
    
    90
    +   *
    
    91
    +   * @property {Element} element - The row element.
    
    92
    +   * @property {Element} optionsButton - The options button.
    
    93
    +   * @property {Element} menu - The options menupopup.
    
    94
    +   * @property {Element} statusEl - The bridge status element.
    
    95
    +   * @property {Element} statusText - The status text.
    
    96
    +   * @property {string} bridgeLine - The identifying bridge string for this row.
    
    97
    +   * @property {string?} bridgeId - The ID/fingerprint for the bridge, or null
    
    98
    +   *   if it doesn't have one.
    
    99
    +   * @property {integer} index - The index of the row in the grid.
    
    100
    +   * @property {boolean} connected - Whether we are connected to the bridge
    
    101
    +   *   (recently in use for a Tor circuit).
    
    102
    +   * @property {BridgeGridCell[]} cells - The cells that belong to the row,
    
    103
    +   *   ordered by their column.
    
    104
    +   */
    
    105
    +  /**
    
    106
    +   * @typedef {object} BridgeGridCell
    
    107
    +   *
    
    108
    +   * @property {Element} element - The cell element.
    
    109
    +   * @property {Element} focusEl - The element belonging to the cell that should
    
    110
    +   *   receive focus. Should be the cell element itself, or an interactive
    
    111
    +   *   focusable child.
    
    112
    +   * @property {integer} columnIndex - The index of the column this cell belongs
    
    113
    +   *   to.
    
    114
    +   * @property {BridgeGridRow} row - The row this cell belongs to.
    
    115
    +   */
    
    116
    +  /**
    
    117
    +   * The current rows in the grid.
    
    118
    +   *
    
    119
    +   * @type {BridgeGridRow[]}
    
    120
    +   */
    
    121
    +  _rows: [],
    
    122
    +  /**
    
    123
    +   * The cell that should be the focus target when the user moves focus into the
    
    124
    +   * grid, or null if the grid itself should be the target.
    
    125
    +   *
    
    126
    +   * @type {BridgeGridCell?}
    
    127
    +   */
    
    128
    +  _focusCell: null,
    
    129
    +
    
    130
    +  /**
    
    131
    +   * Initialize the bridge grid.
    
    132
    +   */
    
    133
    +  init() {
    
    134
    +    this._grid = document.getElementById("tor-bridges-grid-display");
    
    135
    +    // Initially, make only the grid itself part of the keyboard tab cycle.
    
    136
    +    // matches _focusCell = null.
    
    137
    +    this._grid.tabIndex = 0;
    
    138
    +
    
    139
    +    this._rowTemplate = document.getElementById(
    
    140
    +      "tor-bridges-grid-row-template"
    
    141
    +    );
    
    142
    +
    
    143
    +    this._grid.addEventListener("keydown", this);
    
    144
    +    this._grid.addEventListener("mousedown", this);
    
    145
    +    this._grid.addEventListener("focusin", this);
    
    146
    +
    
    147
    +    this._supportedSources = [
    
    148
    +      lazy.TorBridgeSource.BridgeDB,
    
    149
    +      lazy.TorBridgeSource.UserProvided,
    
    150
    +      lazy.TorBridgeSource.Lox,
    
    151
    +    ];
    
    152
    +  },
    
    153
    +
    
    154
    +  /**
    
    155
    +   * Whether the grid is visible and responsive.
    
    156
    +   *
    
    157
    +   * @type {boolean}
    
    158
    +   */
    
    159
    +  _active: false,
    
    160
    +
    
    161
    +  /**
    
    162
    +   * Activate and show the bridge grid.
    
    163
    +   */
    
    164
    +  activate() {
    
    165
    +    if (this._active) {
    
    166
    +      return;
    
    167
    +    }
    
    168
    +
    
    169
    +    this._active = true;
    
    170
    +
    
    171
    +    this._grid.hidden = false;
    
    172
    +  },
    
    173
    +
    
    174
    +  /**
    
    175
    +   * Deactivate and hide the bridge grid.
    
    176
    +   */
    
    177
    +  deactivate() {
    
    178
    +    if (!this._active) {
    
    179
    +      return;
    
    180
    +    }
    
    181
    +
    
    182
    +    this._active = false;
    
    183
    +
    
    184
    +    this._forceCloseRowMenus();
    
    185
    +
    
    186
    +    this._grid.hidden = true;
    
    187
    +  },
    
    188
    +
    
    189
    +  handleEvent(event) {
    
    190
    +    if (event.type === "keydown") {
    
    191
    +      if (event.altKey || event.shiftKey || event.metaKey || event.ctrlKey) {
    
    192
    +        // Don't interfere with these events.
    
    193
    +        return;
    
    194
    +      }
    
    195
    +
    
    196
    +      if (this._rows.some(row => row.menu.open)) {
    
    197
    +        // Have an open menu, let the menu handle the event instead.
    
    198
    +        return;
    
    199
    +      }
    
    200
    +
    
    201
    +      let numRows = this._rows.length;
    
    202
    +      if (!numRows) {
    
    203
    +        // Nowhere for focus to go.
    
    204
    +        return;
    
    205
    +      }
    
    206
    +
    
    207
    +      let moveRow = 0;
    
    208
    +      let moveColumn = 0;
    
    209
    +      const isLTR = this._grid.matches(":dir(ltr)");
    
    210
    +      switch (event.key) {
    
    211
    +        case "ArrowDown":
    
    212
    +          moveRow = 1;
    
    213
    +          break;
    
    214
    +        case "ArrowUp":
    
    215
    +          moveRow = -1;
    
    216
    +          break;
    
    217
    +        case "ArrowRight":
    
    218
    +          moveColumn = isLTR ? 1 : -1;
    
    219
    +          break;
    
    220
    +        case "ArrowLeft":
    
    221
    +          moveColumn = isLTR ? -1 : 1;
    
    222
    +          break;
    
    223
    +        default:
    
    224
    +          return;
    
    225
    +      }
    
    226
    +
    
    227
    +      // Prevent scrolling the nearest scroll container.
    
    228
    +      event.preventDefault();
    
    229
    +
    
    230
    +      const curCell = this._focusCell;
    
    231
    +      let row = curCell ? curCell.row.index + moveRow : 0;
    
    232
    +      let column = curCell ? curCell.columnIndex + moveColumn : 0;
    
    233
    +
    
    234
    +      // Clamp in bounds.
    
    235
    +      if (row < 0) {
    
    236
    +        row = 0;
    
    237
    +      } else if (row >= numRows) {
    
    238
    +        row = numRows - 1;
    
    239
    +      }
    
    240
    +
    
    241
    +      const numCells = this._rows[row].cells.length;
    
    242
    +      if (column < 0) {
    
    243
    +        column = 0;
    
    244
    +      } else if (column >= numCells) {
    
    245
    +        column = numCells - 1;
    
    246
    +      }
    
    247
    +
    
    248
    +      const newCell = this._rows[row].cells[column];
    
    249
    +
    
    250
    +      if (newCell !== curCell) {
    
    251
    +        this._setFocus(newCell);
    
    252
    +      }
    
    253
    +    } else if (event.type === "mousedown") {
    
    254
    +      if (event.button !== 0) {
    
    255
    +        return;
    
    256
    +      }
    
    257
    +      // Move focus index to the clicked target.
    
    258
    +      // NOTE: Since the cells and the grid have "tabindex=-1", they are still
    
    259
    +      // click-focusable. Therefore, the default mousedown handler will try to
    
    260
    +      // move focus to it.
    
    261
    +      // Rather than block this default handler, we instead re-direct the focus
    
    262
    +      // to the correct cell in the "focusin" listener.
    
    263
    +      const newCell = this._getCellFromTarget(event.target);
    
    264
    +      // NOTE: If newCell is null, then we do nothing here, but instead wait for
    
    265
    +      // the focusin handler to trigger.
    
    266
    +      if (newCell && newCell !== this._focusCell) {
    
    267
    +        this._setFocus(newCell);
    
    268
    +      }
    
    269
    +    } else if (event.type === "focusin") {
    
    270
    +      const focusCell = this._getCellFromTarget(event.target);
    
    271
    +      if (focusCell !== this._focusCell) {
    
    272
    +        // Focus is not where it is expected.
    
    273
    +        // E.g. the user has clicked the edge of the grid.
    
    274
    +        // Restore focus immediately back to the cell we expect.
    
    275
    +        this._setFocus(this._focusCell);
    
    276
    +      }
    
    277
    +    }
    
    278
    +  },
    
    279
    +
    
    280
    +  /**
    
    281
    +   * Return the cell that was the target of an event.
    
    282
    +   *
    
    283
    +   * @param {Element} element - The target of an event.
    
    284
    +   *
    
    285
    +   * @returns {BridgeGridCell?} - The cell that the element belongs to, or null
    
    286
    +   *   if it doesn't belong to any cell.
    
    287
    +   */
    
    288
    +  _getCellFromTarget(element) {
    
    289
    +    for (const row of this._rows) {
    
    290
    +      for (const cell of row.cells) {
    
    291
    +        if (cell.element.contains(element)) {
    
    292
    +          return cell;
    
    293
    +        }
    
    294
    +      }
    
    295
    +    }
    
    296
    +    return null;
    
    297
    +  },
    
    298
    +
    
    299
    +  /**
    
    300
    +   * Determine whether the document's active element (focus) is within the grid
    
    301
    +   * or not.
    
    302
    +   *
    
    303
    +   * @returns {boolean} - Whether focus is within this grid or not.
    
    304
    +   */
    
    305
    +  _focusWithin() {
    
    306
    +    return this._grid.contains(document.activeElement);
    
    307
    +  },
    
    308
    +
    
    309
    +  /**
    
    310
    +   * Set the cell that should be the focus target of the grid, possibly moving
    
    311
    +   * the document's focus as well.
    
    312
    +   *
    
    313
    +   * @param {BridgeGridCell?} cell - The cell to make the focus target, or null
    
    314
    +   *   if the grid itself should be the target.
    
    315
    +   * @param {boolean} [focusWithin] - Whether focus should be moved within the
    
    316
    +   *   grid. If undefined, this will move focus if the grid currently contains
    
    317
    +   *   the document's focus.
    
    318
    +   */
    
    319
    +  _setFocus(cell, focusWithin) {
    
    320
    +    if (focusWithin === undefined) {
    
    321
    +      focusWithin = this._focusWithin();
    
    322
    +    }
    
    323
    +    const prevFocusElement = this._focusCell
    
    324
    +      ? this._focusCell.focusEl
    
    325
    +      : this._grid;
    
    326
    +    const newFocusElement = cell ? cell.focusEl : this._grid;
    
    327
    +
    
    328
    +    if (prevFocusElement !== newFocusElement) {
    
    329
    +      prevFocusElement.tabIndex = -1;
    
    330
    +      newFocusElement.tabIndex = 0;
    
    331
    +    }
    
    332
    +    // Set _focusCell now, before we potentially call "focus", which can trigger
    
    333
    +    // the "focusin" handler.
    
    334
    +    this._focusCell = cell;
    
    335
    +
    
    336
    +    if (focusWithin) {
    
    337
    +      // Focus was within the grid, so we need to actively move it to the new
    
    338
    +      // element.
    
    339
    +      newFocusElement.focus({ preventScroll: true });
    
    340
    +      // Scroll to the whole cell into view, rather than just the focus element.
    
    341
    +      (cell?.element ?? newFocusElement).scrollIntoView({
    
    342
    +        block: "nearest",
    
    343
    +        inline: "nearest",
    
    344
    +      });
    
    345
    +    }
    
    346
    +  },
    
    347
    +
    
    348
    +  /**
    
    349
    +   * Reset the grids focus to be the first row's first cell, if any.
    
    350
    +   *
    
    351
    +   * @param {boolean} [focusWithin] - Whether focus should be moved within the
    
    352
    +   *   grid. If undefined, this will move focus if the grid currently contains
    
    353
    +   *   the document's focus.
    
    354
    +   */
    
    355
    +  _resetFocus(focusWithin) {
    
    356
    +    this._setFocus(
    
    357
    +      this._rows.length ? this._rows[0].cells[0] : null,
    
    358
    +      focusWithin
    
    359
    +    );
    
    360
    +  },
    
    361
    +
    
    362
    +  /**
    
    363
    +   * The bridge ID/fingerprint of the most recently used bridge (appearing in
    
    364
    +   * the latest Tor circuit). Roughly corresponds to the bridge we are currently
    
    365
    +   * connected to.
    
    366
    +   *
    
    367
    +   * null if there are no such bridges.
    
    368
    +   *
    
    369
    +   * @type {string?}
    
    370
    +   */
    
    371
    +  _connectedBridgeId: null,
    
    372
    +
    
    373
    +  set connectedBridgeId(bridgeId) {
    
    374
    +    if (bridgeId === this._connectedBridgeId) {
    
    375
    +      return;
    
    376
    +    }
    
    377
    +    this._connectedBridgeId = bridgeId;
    
    378
    +    for (const row of this._rows) {
    
    379
    +      this._updateRowStatus(row);
    
    380
    +    }
    
    381
    +  },
    
    382
    +
    
    383
    +  /**
    
    384
    +   * Update the status of a row.
    
    385
    +   *
    
    386
    +   * @param {BridgeGridRow} row - The row to update.
    
    387
    +   */
    
    388
    +  _updateRowStatus(row) {
    
    389
    +    const connected = row.bridgeId && this._connectedBridgeId === row.bridgeId;
    
    390
    +    // NOTE: row.connected is initially undefined, so won't match `connected`.
    
    391
    +    if (connected === row.connected) {
    
    392
    +      return;
    
    393
    +    }
    
    394
    +
    
    395
    +    row.connected = connected;
    
    396
    +
    
    397
    +    const noStatus = !connected;
    
    398
    +
    
    399
    +    row.element.classList.toggle("hide-status", noStatus);
    
    400
    +    row.statusEl.classList.toggle("bridge-status-none", noStatus);
    
    401
    +    row.statusEl.classList.toggle("bridge-status-connected", connected);
    
    402
    +
    
    403
    +    if (connected) {
    
    404
    +      document.l10n.setAttributes(
    
    405
    +        row.statusText,
    
    406
    +        "tor-bridges-status-connected"
    
    407
    +      );
    
    408
    +    } else {
    
    409
    +      document.l10n.setAttributes(row.statusText, "tor-bridges-status-none");
    
    410
    +    }
    
    411
    +  },
    
    412
    +
    
    413
    +  /**
    
    414
    +   * Create a new row for the grid.
    
    415
    +   *
    
    416
    +   * @param {string} bridgeLine - The bridge line for this row, which also acts
    
    417
    +   *   as its ID.
    
    418
    +   *
    
    419
    +   * @returns {BridgeGridRow} - A new row, with then "index" unset and the
    
    420
    +   *   "element" without a parent.
    
    421
    +   */
    
    422
    +  _createRow(bridgeLine) {
    
    423
    +    let details;
    
    424
    +    try {
    
    425
    +      details = lazy.TorParsers.parseBridgeLine(bridgeLine);
    
    426
    +    } catch (e) {
    
    427
    +      console.error(`Detected invalid bridge line: ${bridgeLine}`, e);
    
    428
    +    }
    
    429
    +    const row = {
    
    430
    +      element: this._rowTemplate.content.children[0].cloneNode(true),
    
    431
    +      bridgeLine,
    
    432
    +      bridgeId: details?.id ?? null,
    
    433
    +      cells: [],
    
    434
    +    };
    
    435
    +
    
    436
    +    const emojiBlock = row.element.querySelector(".tor-bridges-emojis-block");
    
    437
    +    const BridgeEmoji = customElements.get("tor-bridge-emoji");
    
    438
    +    for (const cell of BridgeEmoji.createForAddress(bridgeLine)) {
    
    439
    +      // Each emoji is its own cell, we rely on the fact that createForAddress
    
    440
    +      // always returns four elements.
    
    441
    +      cell.setAttribute("role", "gridcell");
    
    442
    +      cell.classList.add("tor-bridges-grid-cell", "tor-bridges-emoji-cell");
    
    443
    +      emojiBlock.append(cell);
    
    444
    +    }
    
    445
    +
    
    446
    +    for (const [columnIndex, element] of row.element
    
    447
    +      .querySelectorAll(".tor-bridges-grid-cell")
    
    448
    +      .entries()) {
    
    449
    +      const focusEl =
    
    450
    +        element.querySelector(".tor-bridges-grid-focus") ?? element;
    
    451
    +      // Set a negative tabIndex, this makes the element click-focusable but not
    
    452
    +      // part of the tab navigation sequence.
    
    453
    +      focusEl.tabIndex = -1;
    
    454
    +      row.cells.push({ element, focusEl, columnIndex, row });
    
    455
    +    }
    
    456
    +
    
    457
    +    const transport = details?.transport ?? "vanilla";
    
    458
    +    const typeCell = row.element.querySelector(".tor-bridges-type-cell");
    
    459
    +    if (transport === "vanilla") {
    
    460
    +      document.l10n.setAttributes(typeCell, "tor-bridges-type-prefix-generic");
    
    461
    +    } else {
    
    462
    +      document.l10n.setAttributes(typeCell, "tor-bridges-type-prefix", {
    
    463
    +        type: transport,
    
    464
    +      });
    
    465
    +    }
    
    466
    +
    
    467
    +    row.element.querySelector(".tor-bridges-address-cell-text").textContent =
    
    468
    +      bridgeLine;
    
    469
    +
    
    470
    +    row.statusEl = row.element.querySelector(
    
    471
    +      ".tor-bridges-status-cell .bridge-status-badge"
    
    472
    +    );
    
    473
    +    row.statusText = row.element.querySelector(".tor-bridges-status-cell-text");
    
    474
    +
    
    475
    +    this._initRowMenu(row);
    
    476
    +
    
    477
    +    this._updateRowStatus(row);
    
    478
    +    return row;
    
    479
    +  },
    
    480
    +
    
    481
    +  /**
    
    482
    +   * The row menu index used for generating new ids.
    
    483
    +   *
    
    484
    +   * @type {integer}
    
    485
    +   */
    
    486
    +  _rowMenuIndex: 0,
    
    487
    +  /**
    
    488
    +   * Generate a new id for the options menu.
    
    489
    +   *
    
    490
    +   * @returns {string} - The new id.
    
    491
    +   */
    
    492
    +  _generateRowMenuId() {
    
    493
    +    const id = `tor-bridges-individual-options-menu-${this._rowMenuIndex}`;
    
    494
    +    // Assume we won't run out of ids.
    
    495
    +    this._rowMenuIndex++;
    
    496
    +    return id;
    
    497
    +  },
    
    498
    +
    
    499
    +  /**
    
    500
    +   * Initialize the shared menu for a row.
    
    501
    +   *
    
    502
    +   * @param {BridgeGridRow} row - The row to initialize the menu of.
    
    503
    +   */
    
    504
    +  _initRowMenu(row) {
    
    505
    +    row.menu = row.element.querySelector(
    
    506
    +      ".tor-bridges-individual-options-menu"
    
    507
    +    );
    
    508
    +    row.optionsButton = row.element.querySelector(
    
    509
    +      ".tor-bridges-options-cell-button"
    
    510
    +    );
    
    511
    +
    
    512
    +    row.menu.id = this._generateRowMenuId();
    
    513
    +    row.optionsButton.setAttribute("aria-controls", row.menu.id);
    
    514
    +
    
    515
    +    row.optionsButton.addEventListener("click", event => {
    
    516
    +      row.menu.toggle(event);
    
    517
    +    });
    
    518
    +
    
    519
    +    row.menu.addEventListener("hidden", () => {
    
    520
    +      // Make sure the button receives focus again when the menu is hidden.
    
    521
    +      // Currently, panel-list.js only does this when the menu is opened with a
    
    522
    +      // keyboard, but this causes focus to be lost from the page if the user
    
    523
    +      // uses a mixture of keyboard and mouse.
    
    524
    +      row.optionsButton.focus();
    
    525
    +    });
    
    526
    +
    
    527
    +    const qrItem = row.menu.querySelector(
    
    528
    +      ".tor-bridges-options-qr-one-menu-item"
    
    529
    +    );
    
    530
    +    const removeItem = row.menu.querySelector(
    
    531
    +      ".tor-bridges-options-remove-one-menu-item"
    
    532
    +    );
    
    533
    +    row.menu.addEventListener("showing", () => {
    
    534
    +      const show =
    
    535
    +        this._bridgeSource === lazy.TorBridgeSource.UserProvided ||
    
    536
    +        this._bridgeSource === lazy.TorBridgeSource.BridgeDB;
    
    537
    +      qrItem.hidden = !show;
    
    538
    +      removeItem.hidden = !show;
    
    539
    +    });
    
    540
    +
    
    541
    +    qrItem.addEventListener("click", () => {
    
    542
    +      const bridgeLine = row.bridgeLine;
    
    543
    +      if (!bridgeLine) {
    
    544
    +        return;
    
    545
    +      }
    
    546
    +      showBridgeQr(bridgeLine);
    
    547
    +    });
    
    548
    +    row.menu
    
    549
    +      .querySelector(".tor-bridges-options-copy-one-menu-item")
    
    550
    +      .addEventListener("click", () => {
    
    551
    +        const clipboard = Cc[
    
    552
    +          "@mozilla.org/widget/clipboardhelper;1"
    
    553
    +        ].getService(Ci.nsIClipboardHelper);
    
    554
    +        clipboard.copyString(row.bridgeLine);
    
    555
    +      });
    
    556
    +    removeItem.addEventListener("click", () => {
    
    557
    +      const bridgeLine = row.bridgeLine;
    
    558
    +      const source = lazy.TorSettings.bridges.source;
    
    559
    +      if (source !== this._bridgesVal?.source) {
    
    560
    +        // Our value is stale, abort.
    
    561
    +        return;
    
    562
    +      }
    
    563
    +      const strings = lazy.TorSettings.bridges.bridge_strings;
    
    564
    +      const index = strings.indexOf(bridgeLine);
    
    565
    +      if (index === -1) {
    
    566
    +        return;
    
    567
    +      }
    
    568
    +      strings.splice(index, 1);
    
    569
    +
    
    570
    +      if (strings.length) {
    
    571
    +        lazy.TorSettings.changeSettings({
    
    572
    +          bridges: { source, bridge_strings: strings },
    
    573
    +        });
    
    574
    +      } else {
    
    575
    +        // Remove all bridges and disable.
    
    576
    +        lazy.TorSettings.changeSettings({
    
    577
    +          bridges: { source: lazy.TorBridgeSource.Invalid },
    
    578
    +        });
    
    579
    +      }
    
    580
    +    });
    
    581
    +  },
    
    582
    +
    
    583
    +  /**
    
    584
    +   * Force the row menu to close.
    
    585
    +   */
    
    586
    +  _forceCloseRowMenus() {
    
    587
    +    for (const row of this._rows) {
    
    588
    +      row.menu.hide(null, { force: true });
    
    589
    +    }
    
    590
    +  },
    
    591
    +
    
    592
    +  /**
    
    593
    +   * The known bridge source.
    
    594
    +   *
    
    595
    +   * Initially null to indicate that it is unset.
    
    596
    +   *
    
    597
    +   * @type {integer?}
    
    598
    +   */
    
    599
    +  _bridgeSource: null,
    
    600
    +  /**
    
    601
    +   * The bridge sources this is shown for.
    
    602
    +   *
    
    603
    +   * @type {string[]}
    
    604
    +   */
    
    605
    +  _supportedSources: [],
    
    606
    +
    
    607
    +  /**
    
    608
    +   * The bridges value, set by the setting-control element.
    
    609
    +   *
    
    610
    +   * @type {object}
    
    611
    +   */
    
    612
    +  _bridgesVal: null,
    
    613
    +
    
    614
    +  set bridges(val) {
    
    615
    +    if (val === null) {
    
    616
    +      // Ignore and wait for the initial.
    
    617
    +      return;
    
    618
    +    }
    
    619
    +    const initial = this._bridgesVal === null;
    
    620
    +    this._bridgesVal = val;
    
    621
    +    this._updateRows(initial);
    
    622
    +  },
    
    623
    +
    
    624
    +  /**
    
    625
    +   * Update the grid to show the latest bridge strings.
    
    626
    +   *
    
    627
    +   * @param {boolean} initializing - Whether this is being called as part of
    
    628
    +   *   initialization.
    
    629
    +   */
    
    630
    +  _updateRows(initializing) {
    
    631
    +    // Store whether we have focus within the grid, before removing or hiding
    
    632
    +    // DOM elements.
    
    633
    +    const focusWithin = this._focusWithin();
    
    634
    +
    
    635
    +    let lostAllBridges = false;
    
    636
    +    let newSource = false;
    
    637
    +    const bridgeSource = this._bridgesVal.source;
    
    638
    +    if (bridgeSource !== this._bridgeSource) {
    
    639
    +      newSource = true;
    
    640
    +
    
    641
    +      this._bridgeSource = bridgeSource;
    
    642
    +
    
    643
    +      if (this._supportedSources.includes(bridgeSource)) {
    
    644
    +        this.activate();
    
    645
    +      } else {
    
    646
    +        if (this._active && bridgeSource === lazy.TorBridgeSource.Invalid) {
    
    647
    +          lostAllBridges = true;
    
    648
    +        }
    
    649
    +        this.deactivate();
    
    650
    +      }
    
    651
    +    }
    
    652
    +
    
    653
    +    const ordered = this._active
    
    654
    +      ? this._bridgesVal.bridgeStrings.map(bridgeLine => {
    
    655
    +          const row = this._rows.find(r => r.bridgeLine === bridgeLine);
    
    656
    +          if (row) {
    
    657
    +            return row;
    
    658
    +          }
    
    659
    +          return this._createRow(bridgeLine);
    
    660
    +        })
    
    661
    +      : [];
    
    662
    +
    
    663
    +    // Whether we should reset the grid's focus.
    
    664
    +    // We always reset when we have a new bridge source.
    
    665
    +    // We reset the focus if no current Cell has focus. I.e. when adding a row
    
    666
    +    // to an empty grid, we want the focus to move to the first item.
    
    667
    +    // We also reset the focus if the current Cell is in a row that will be
    
    668
    +    // removed (including if all rows are removed).
    
    669
    +    // NOTE: In principle, if a row is removed, we could move the focus to the
    
    670
    +    // next or previous row (in the same cell column). However, most likely if
    
    671
    +    // the grid has the user focus, they are removing a single row using its
    
    672
    +    // options button. In this case, returning the user to some other row's
    
    673
    +    // options button might be more disorienting since it would not be simple
    
    674
    +    // for them to know *which* bridge they have landed on.
    
    675
    +    // NOTE: We do not reset the focus in other cases because we do not want the
    
    676
    +    // user to loose their place in the grid unnecessarily.
    
    677
    +    let resetFocus =
    
    678
    +      newSource || !this._focusCell || !ordered.includes(this._focusCell.row);
    
    679
    +
    
    680
    +    // Remove rows no longer needed from the DOM.
    
    681
    +    let numRowsRemoved = 0;
    
    682
    +    let rowAddedOrMoved = false;
    
    683
    +
    
    684
    +    for (const row of this._rows) {
    
    685
    +      if (!ordered.includes(row)) {
    
    686
    +        numRowsRemoved++;
    
    687
    +        // If the row menu was open, it will also be deleted.
    
    688
    +        // NOTE: Since the row menu is part of the row, focusWithin will be true
    
    689
    +        // if the menu had focus, so focus should be re-assigned.
    
    690
    +        row.element.remove();
    
    691
    +      }
    
    692
    +    }
    
    693
    +
    
    694
    +    // Go through all the rows to set their ".index" property and to ensure they
    
    695
    +    // are in the correct position in the DOM.
    
    696
    +    // NOTE: We could use replaceChildren to get the correct DOM structure, but
    
    697
    +    // we want to avoid rebuilding the entire tree when a single row is added or
    
    698
    +    // removed.
    
    699
    +    for (const [index, row] of ordered.entries()) {
    
    700
    +      row.index = index;
    
    701
    +      const element = row.element;
    
    702
    +      // Get the expected previous element, that should already be in the DOM
    
    703
    +      // from the previous loop.
    
    704
    +      const prevEl = index ? ordered[index - 1].element : null;
    
    705
    +
    
    706
    +      if (
    
    707
    +        element.parentElement === this._grid &&
    
    708
    +        prevEl === element.previousElementSibling
    
    709
    +      ) {
    
    710
    +        // Already in the correct position in the DOM.
    
    711
    +        continue;
    
    712
    +      }
    
    713
    +
    
    714
    +      rowAddedOrMoved = true;
    
    715
    +      // NOTE: Any elements already in the DOM, but not in the correct position
    
    716
    +      // will be removed and re-added by the below command.
    
    717
    +      // NOTE: if the row has document focus, then it should remain there.
    
    718
    +      if (prevEl) {
    
    719
    +        prevEl.after(element);
    
    720
    +      } else {
    
    721
    +        this._grid.prepend(element);
    
    722
    +      }
    
    723
    +    }
    
    724
    +    this._rows = ordered;
    
    725
    +
    
    726
    +    // Restore any lost focus.
    
    727
    +    if (resetFocus) {
    
    728
    +      // If we are not active (and therefore hidden), we will not try and move
    
    729
    +      // focus (activeElement), but may still change the *focusable* element for
    
    730
    +      // when we are shown again.
    
    731
    +      this._resetFocus(this._active && focusWithin);
    
    732
    +    }
    
    733
    +    // NOTE: In the case we were previously active and now inactive,
    
    734
    +    // tor-bridges-display will have already moved the focus out of this area.
    
    735
    +
    
    736
    +    // Notify the user if there was some change to the DOM.
    
    737
    +    // If we are initializing, we generate no notification since there has been
    
    738
    +    // no change in the setting.
    
    739
    +    if (!initializing) {
    
    740
    +      let notificationType;
    
    741
    +      if (lostAllBridges) {
    
    742
    +        // Just lost all bridges, and became de-active.
    
    743
    +        notificationType = "removed-all";
    
    744
    +      } else if (this._rows.length) {
    
    745
    +        // Otherwise, only generate a notification if we are still active, with
    
    746
    +        // at least one bridge.
    
    747
    +        // I.e. do not generate a message if the new source is "builtin".
    
    748
    +        if (newSource) {
    
    749
    +          // A change in source.
    
    750
    +          notificationType = "changed";
    
    751
    +        } else if (numRowsRemoved === 1 && !rowAddedOrMoved) {
    
    752
    +          // Only one bridge was removed. This is most likely in response to them
    
    753
    +          // manually removing a single bridge or using the bridge row's options
    
    754
    +          // menu.
    
    755
    +          notificationType = "removed-one";
    
    756
    +        } else if (numRowsRemoved || rowAddedOrMoved) {
    
    757
    +          // Some other change. This is most likely in response to a manual edit
    
    758
    +          // of the existing bridges.
    
    759
    +          notificationType = "changed";
    
    760
    +        }
    
    761
    +        // Else, there was no change.
    
    762
    +      }
    
    763
    +
    
    764
    +      if (notificationType) {
    
    765
    +        postBridgeNotification(notificationType);
    
    766
    +      }
    
    767
    +    }
    
    768
    +  },
    
    769
    +};
    
    770
    +
    
    771
    +/**
    
    772
    + * Controls the built-in bridges area.
    
    773
    + */
    
    774
    +const gBuiltinBridgesArea = {
    
    775
    +  /**
    
    776
    +   * The display area.
    
    777
    +   *
    
    778
    +   * @type {Element?}
    
    779
    +   */
    
    780
    +  _area: null,
    
    781
    +  /**
    
    782
    +   * The type name element.
    
    783
    +   *
    
    784
    +   * @type {Element?}
    
    785
    +   */
    
    786
    +  _nameEl: null,
    
    787
    +  /**
    
    788
    +   * The bridge type description element.
    
    789
    +   *
    
    790
    +   * @type {Element?}
    
    791
    +   */
    
    792
    +  _descriptionEl: null,
    
    793
    +  /**
    
    794
    +   * The connection status.
    
    795
    +   *
    
    796
    +   * @type {Element?}
    
    797
    +   */
    
    798
    +  _connectionStatusEl: null,
    
    799
    +
    
    800
    +  /**
    
    801
    +   * Initialize the built-in bridges area.
    
    802
    +   */
    
    803
    +  init() {
    
    804
    +    this._area = document.getElementById("tor-bridges-built-in-display");
    
    805
    +    this._nameEl = document.getElementById("tor-bridges-built-in-type-name");
    
    806
    +    this._descriptionEl = document.getElementById(
    
    807
    +      "tor-bridges-built-in-description"
    
    808
    +    );
    
    809
    +    this._connectionStatusEl = document.getElementById(
    
    810
    +      "tor-bridges-built-in-connected"
    
    811
    +    );
    
    812
    +  },
    
    813
    +
    
    814
    +  /**
    
    815
    +   * Whether the built-in area is visible and responsive.
    
    816
    +   *
    
    817
    +   * @type {boolean}
    
    818
    +   */
    
    819
    +  _active: false,
    
    820
    +
    
    821
    +  /**
    
    822
    +   * Activate and show the built-in bridge area.
    
    823
    +   */
    
    824
    +  activate() {
    
    825
    +    if (this._active) {
    
    826
    +      return;
    
    827
    +    }
    
    828
    +    this._active = true;
    
    829
    +
    
    830
    +    this._area.hidden = false;
    
    831
    +  },
    
    832
    +
    
    833
    +  /**
    
    834
    +   * Deactivate and hide built-in bridge area.
    
    835
    +   */
    
    836
    +  deactivate() {
    
    837
    +    if (!this._active) {
    
    838
    +      return;
    
    839
    +    }
    
    840
    +    this._active = false;
    
    841
    +
    
    842
    +    this._area.hidden = true;
    
    843
    +  },
    
    844
    +
    
    845
    +  /**
    
    846
    +   * Updates the shown connected state.
    
    847
    +   */
    
    848
    +  _updateConnectedState() {
    
    849
    +    this._connectionStatusEl.classList.toggle(
    
    850
    +      "bridge-status-connected",
    
    851
    +      this._bridgeType &&
    
    852
    +        this._connectedBridgeId &&
    
    853
    +        this._bridgeIds.includes(this._connectedBridgeId)
    
    854
    +    );
    
    855
    +  },
    
    856
    +
    
    857
    +  /**
    
    858
    +   * The bridges value, set by the setting-control element.
    
    859
    +   *
    
    860
    +   * @type {object}
    
    861
    +   */
    
    862
    +  _bridgesVal: null,
    
    863
    +  set bridges(val) {
    
    864
    +    if (val === null) {
    
    865
    +      // Ignore and wait for the initial.
    
    866
    +      return;
    
    867
    +    }
    
    868
    +    const initial = this._bridgesVal === null;
    
    869
    +    this._bridgesVal = val;
    
    870
    +    this._updateBridgeType(initial);
    
    871
    +    this._updateBridgeIds();
    
    872
    +  },
    
    873
    +
    
    874
    +  /**
    
    875
    +   * The currently shown bridge type. Empty if deactivated, and null if
    
    876
    +   * uninitialized.
    
    877
    +   *
    
    878
    +   * @type {string?}
    
    879
    +   */
    
    880
    +  _bridgeType: null,
    
    881
    +  /**
    
    882
    +   * The strings for each known bridge type.
    
    883
    +   *
    
    884
    +   * @type {{[key: string]: {[key: string]: string}}}
    
    885
    +   */
    
    886
    +  _bridgeTypeStrings: {
    
    887
    +    obfs4: {
    
    888
    +      name: "tor-bridges-built-in-obfs4-name",
    
    889
    +      description: "tor-bridges-built-in-obfs4-description",
    
    890
    +    },
    
    891
    +    snowflake: {
    
    892
    +      name: "tor-bridges-built-in-snowflake-name",
    
    893
    +      description: "tor-bridges-built-in-snowflake-description",
    
    894
    +    },
    
    895
    +    meek: {
    
    896
    +      name: "tor-bridges-built-in-meek-name",
    
    897
    +      description: "tor-bridges-built-in-meek-description",
    
    898
    +    },
    
    899
    +  },
    
    900
    +
    
    901
    +  /**
    
    902
    +   * The known bridge source.
    
    903
    +   *
    
    904
    +   * Initially null to indicate that it is unset.
    
    905
    +   *
    
    906
    +   * @type {integer?}
    
    907
    +   */
    
    908
    +  _bridgeSource: null,
    
    909
    +
    
    910
    +  /**
    
    911
    +   * Update the shown bridge type.
    
    912
    +   *
    
    913
    +   * @param {boolean} initializing - Whether this is being called as part of
    
    914
    +   *   initialization.
    
    915
    +   */
    
    916
    +  async _updateBridgeType(initializing) {
    
    917
    +    let lostAllBridges = false;
    
    918
    +    let newSource = false;
    
    919
    +    const bridgeSource = this._bridgesVal.source;
    
    920
    +    if (bridgeSource !== this._bridgeSource) {
    
    921
    +      newSource = true;
    
    922
    +
    
    923
    +      this._bridgeSource = bridgeSource;
    
    924
    +
    
    925
    +      if (bridgeSource === lazy.TorBridgeSource.BuiltIn) {
    
    926
    +        this.activate();
    
    927
    +      } else {
    
    928
    +        if (this._active && bridgeSource === lazy.TorBridgeSource.Invalid) {
    
    929
    +          lostAllBridges = true;
    
    930
    +        }
    
    931
    +        this.deactivate();
    
    932
    +        // NOTE: In the case we were previously active, tor-bridges-display will
    
    933
    +        // have already moved the focus out of this area.
    
    934
    +      }
    
    935
    +    }
    
    936
    +
    
    937
    +    const bridgeType = this._active ? this._bridgesVal.builtinType : "";
    
    938
    +
    
    939
    +    let newType = false;
    
    940
    +    if (bridgeType !== this._bridgeType) {
    
    941
    +      newType = true;
    
    942
    +
    
    943
    +      this._bridgeType = bridgeType;
    
    944
    +
    
    945
    +      const bridgeStrings = this._bridgeTypeStrings[bridgeType];
    
    946
    +      if (bridgeStrings) {
    
    947
    +        document.l10n.setAttributes(this._nameEl, bridgeStrings.name);
    
    948
    +        document.l10n.setAttributes(
    
    949
    +          this._descriptionEl,
    
    950
    +          bridgeStrings.description
    
    951
    +        );
    
    952
    +      } else {
    
    953
    +        // Unknown type, or no type.
    
    954
    +        this._nameEl.removeAttribute("data-l10n-id");
    
    955
    +        this._nameEl.textContent = bridgeType;
    
    956
    +        this._descriptionEl.removeAttribute("data-l10n-id");
    
    957
    +        this._descriptionEl.textContent = "";
    
    958
    +      }
    
    959
    +
    
    960
    +      this._updateConnectedState();
    
    961
    +    }
    
    962
    +
    
    963
    +    // Notify the user if there was some change to the type.
    
    964
    +    // If we are initializing, we generate no notification since there has been
    
    965
    +    // no change in the setting.
    
    966
    +    if (!initializing) {
    
    967
    +      let notificationType;
    
    968
    +      if (lostAllBridges) {
    
    969
    +        // Just lost all bridges, and became de-active.
    
    970
    +        notificationType = "removed-all";
    
    971
    +      } else if (this._active && (newSource || newType)) {
    
    972
    +        // Otherwise, only generate a notification if we are still active, with
    
    973
    +        // a bridge type.
    
    974
    +        // I.e. do not generate a message if the new source is not "builtin".
    
    975
    +        notificationType = "changed";
    
    976
    +      }
    
    977
    +
    
    978
    +      if (notificationType) {
    
    979
    +        postBridgeNotification(notificationType);
    
    980
    +      }
    
    981
    +    }
    
    982
    +  },
    
    983
    +
    
    984
    +  /**
    
    985
    +   * The bridge IDs/fingerprints for the built-in bridges.
    
    986
    +   *
    
    987
    +   * @type {Array<string>}
    
    988
    +   */
    
    989
    +  _bridgeIds: [],
    
    990
    +  /**
    
    991
    +   * Update _bridgeIds
    
    992
    +   */
    
    993
    +  _updateBridgeIds() {
    
    994
    +    this._bridgeIds = [];
    
    995
    +    for (const bridgeLine of this._bridgesVal.bridgeStrings) {
    
    996
    +      try {
    
    997
    +        this._bridgeIds.push(lazy.TorParsers.parseBridgeLine(bridgeLine).id);
    
    998
    +      } catch (e) {
    
    999
    +        console.error(`Detected invalid bridge line: ${bridgeLine}`, e);
    
    1000
    +      }
    
    1001
    +    }
    
    1002
    +
    
    1003
    +    this._updateConnectedState();
    
    1004
    +  },
    
    1005
    +
    
    1006
    +  /**
    
    1007
    +   * The bridge ID/fingerprint of the most recently used bridge (appearing in
    
    1008
    +   * the latest Tor circuit). Roughly corresponds to the bridge we are currently
    
    1009
    +   * connected to.
    
    1010
    +   *
    
    1011
    +   * @type {string?}
    
    1012
    +   */
    
    1013
    +  _connectedBridgeId: null,
    
    1014
    +
    
    1015
    +  set connectedBridgeId(val) {
    
    1016
    +    this._connectedBridgeId = val;
    
    1017
    +    this._updateConnectedState();
    
    1018
    +  },
    
    1019
    +};
    
    1020
    +
    
    1021
    +/**
    
    1022
    + * Controls the bridge pass area.
    
    1023
    + */
    
    1024
    +const gLoxStatus = {
    
    1025
    +  /**
    
    1026
    +   * The status area.
    
    1027
    +   *
    
    1028
    +   * @type {Element?}
    
    1029
    +   */
    
    1030
    +  _area: null,
    
    1031
    +  /**
    
    1032
    +   * The area for showing the next unlock and invites.
    
    1033
    +   *
    
    1034
    +   * @type {Element?}
    
    1035
    +   */
    
    1036
    +  _detailsArea: null,
    
    1037
    +  /**
    
    1038
    +   * The list items showing the next unlocks.
    
    1039
    +   *
    
    1040
    +   * @type {?{[key: string]: Element}}
    
    1041
    +   */
    
    1042
    +  _nextUnlockItems: null,
    
    1043
    +  /**
    
    1044
    +   * The day counter headings for the next unlock.
    
    1045
    +   *
    
    1046
    +   * One heading is shown during a search, the other is shown otherwise.
    
    1047
    +   *
    
    1048
    +   * @type {?Element[]}
    
    1049
    +   */
    
    1050
    +  _nextUnlockCounterEls: null,
    
    1051
    +  /**
    
    1052
    +   * Shows the number of remaining invites.
    
    1053
    +   *
    
    1054
    +   * @type {Element?}
    
    1055
    +   */
    
    1056
    +  _remainingInvitesEl: null,
    
    1057
    +  /**
    
    1058
    +   * The button to show the invites.
    
    1059
    +   *
    
    1060
    +   * @type {Element?}
    
    1061
    +   */
    
    1062
    +  _invitesButton: null,
    
    1063
    +  /**
    
    1064
    +   * The alert for new unlocks.
    
    1065
    +   *
    
    1066
    +   * @type {Element?}
    
    1067
    +   */
    
    1068
    +  _unlockAlert: null,
    
    1069
    +  /**
    
    1070
    +   * The list items showing the unlocks.
    
    1071
    +   *
    
    1072
    +   * @type {?{[key: string]: Element}}
    
    1073
    +   */
    
    1074
    +  _unlockItems: null,
    
    1075
    +  /**
    
    1076
    +   * The alert title.
    
    1077
    +   *
    
    1078
    +   * @type {Element?}
    
    1079
    +   */
    
    1080
    +  _unlockAlertTitle: null,
    
    1081
    +  /**
    
    1082
    +   * The alert invites item.
    
    1083
    +   *
    
    1084
    +   * @type {Element?}
    
    1085
    +   */
    
    1086
    +  _unlockAlertInvitesItem: null,
    
    1087
    +  /**
    
    1088
    +   * Button for the user to dismiss the alert.
    
    1089
    +   *
    
    1090
    +   * @type {Element?}
    
    1091
    +   */
    
    1092
    +  _unlockAlertButton: null,
    
    1093
    +
    
    1094
    +  _enabled: false,
    
    1095
    +
    
    1096
    +  /**
    
    1097
    +   * Initialize the bridge pass area.
    
    1098
    +   */
    
    1099
    +  init() {
    
    1100
    +    if (!lazy.Lox.enabled) {
    
    1101
    +      // Area should remain inactive and hidden.
    
    1102
    +      return;
    
    1103
    +    }
    
    1104
    +
    
    1105
    +    this._enabled = true;
    
    1106
    +    this._area = document.getElementById("tor-bridges-lox-status");
    
    1107
    +    this._detailsArea = document.getElementById("tor-bridges-lox-details");
    
    1108
    +    this._nextUnlockItems = {
    
    1109
    +      gainBridges: document.getElementById(
    
    1110
    +        "tor-bridges-lox-next-unlock-gain-bridges"
    
    1111
    +      ),
    
    1112
    +      firstInvites: document.getElementById(
    
    1113
    +        "tor-bridges-lox-next-unlock-first-invites"
    
    1114
    +      ),
    
    1115
    +      moreInvites: document.getElementById(
    
    1116
    +        "tor-bridges-lox-next-unlock-more-invites"
    
    1117
    +      ),
    
    1118
    +    };
    
    1119
    +    this._nextUnlockCounterEls = Array.from(
    
    1120
    +      document.querySelectorAll(".tor-bridges-lox-next-unlock-counter")
    
    1121
    +    );
    
    1122
    +    this._remainingInvitesEl = document.getElementById(
    
    1123
    +      "tor-bridges-lox-remaining-invites"
    
    1124
    +    );
    
    1125
    +    this._invitesButton = document.getElementById(
    
    1126
    +      "tor-bridges-lox-show-invites-button"
    
    1127
    +    );
    
    1128
    +    this._unlockAlert = document.getElementById("tor-bridges-lox-unlock-alert");
    
    1129
    +    this._unlockItems = {
    
    1130
    +      gainBridges: document.getElementById(
    
    1131
    +        "tor-bridges-lox-unlock-alert-gain-bridges"
    
    1132
    +      ),
    
    1133
    +      newBridges: document.getElementById(
    
    1134
    +        "tor-bridges-lox-unlock-alert-new-bridges"
    
    1135
    +      ),
    
    1136
    +      invites: document.getElementById("tor-bridges-lox-unlock-alert-invites"),
    
    1137
    +    };
    
    1138
    +    this._unlockAlertTitle = document.getElementById(
    
    1139
    +      "tor-bridge-unlock-alert-title"
    
    1140
    +    );
    
    1141
    +    this._unlockAlertInviteItem = document.getElementById(
    
    1142
    +      "tor-bridges-lox-unlock-alert-invites"
    
    1143
    +    );
    
    1144
    +    this._unlockAlertButton = document.getElementById(
    
    1145
    +      "tor-bridges-lox-unlock-alert-button"
    
    1146
    +    );
    
    1147
    +
    
    1148
    +    this._invitesButton.addEventListener("click", () => {
    
    1149
    +      window.gSubDialog.open(
    
    1150
    +        "chrome://browser/content/torpreferences/loxInviteDialog.xhtml",
    
    1151
    +        { features: "resizable=yes" }
    
    1152
    +      );
    
    1153
    +    });
    
    1154
    +    this._unlockAlertButton.addEventListener("click", () => {
    
    1155
    +      lazy.Lox.clearEventData(this._loxId);
    
    1156
    +    });
    
    1157
    +
    
    1158
    +    Services.obs.addObserver(this, lazy.LoxTopics.UpdateActiveLoxId);
    
    1159
    +    Services.obs.addObserver(this, lazy.LoxTopics.UpdateEvents);
    
    1160
    +    Services.obs.addObserver(this, lazy.LoxTopics.UpdateNextUnlock);
    
    1161
    +    Services.obs.addObserver(this, lazy.LoxTopics.UpdateRemainingInvites);
    
    1162
    +    Services.obs.addObserver(this, lazy.LoxTopics.NewInvite);
    
    1163
    +
    
    1164
    +    window.addEventListener(
    
    1165
    +      "unload",
    
    1166
    +      () => {
    
    1167
    +        Services.obs.removeObserver(this, lazy.LoxTopics.UpdateActiveLoxId);
    
    1168
    +        Services.obs.removeObserver(this, lazy.LoxTopics.UpdateEvents);
    
    1169
    +        Services.obs.removeObserver(this, lazy.LoxTopics.UpdateNextUnlock);
    
    1170
    +        Services.obs.removeObserver(
    
    1171
    +          this,
    
    1172
    +          lazy.LoxTopics.UpdateRemainingInvites
    
    1173
    +        );
    
    1174
    +        Services.obs.removeObserver(this, lazy.LoxTopics.NewInvite);
    
    1175
    +      },
    
    1176
    +      { once: true }
    
    1177
    +    );
    
    1178
    +  },
    
    1179
    +
    
    1180
    +  observe(subject, topic) {
    
    1181
    +    switch (topic) {
    
    1182
    +      case lazy.LoxTopics.UpdateActiveLoxId:
    
    1183
    +        this._updateLoxId();
    
    1184
    +        break;
    
    1185
    +      case lazy.LoxTopics.UpdateNextUnlock:
    
    1186
    +        this._updateNextUnlock();
    
    1187
    +        break;
    
    1188
    +      case lazy.LoxTopics.UpdateEvents:
    
    1189
    +        this._updatePendingEvents();
    
    1190
    +        break;
    
    1191
    +      case lazy.LoxTopics.UpdateRemainingInvites:
    
    1192
    +        this._updateRemainingInvites();
    
    1193
    +        break;
    
    1194
    +      case lazy.LoxTopics.NewInvite:
    
    1195
    +        this._updateHaveExistingInvites();
    
    1196
    +        break;
    
    1197
    +    }
    
    1198
    +  },
    
    1199
    +
    
    1200
    +  /**
    
    1201
    +   * The bridges value, set by the setting-control element.
    
    1202
    +   *
    
    1203
    +   * @type {object}
    
    1204
    +   */
    
    1205
    +  _bridgesVal: null,
    
    1206
    +  set bridges(val) {
    
    1207
    +    if (val === null) {
    
    1208
    +      // Ignore and wait for initial.
    
    1209
    +      return;
    
    1210
    +    }
    
    1211
    +    if (!this._enabled) {
    
    1212
    +      // Area should remain inactive and hidden.
    
    1213
    +      return;
    
    1214
    +    }
    
    1215
    +    this._bridgesVal = val;
    
    1216
    +    this._updateLoxId();
    
    1217
    +  },
    
    1218
    +
    
    1219
    +  /**
    
    1220
    +   * The Lox id currently shown. Empty if deactivated, and null if
    
    1221
    +   * uninitialized.
    
    1222
    +   *
    
    1223
    +   * @type {string?}
    
    1224
    +   */
    
    1225
    +  _loxId: null,
    
    1226
    +
    
    1227
    +  /**
    
    1228
    +   * Update the shown bridge pass.
    
    1229
    +   */
    
    1230
    +  async _updateLoxId() {
    
    1231
    +    let loxId =
    
    1232
    +      this._bridgesVal?.source === lazy.TorBridgeSource.Lox
    
    1233
    +        ? lazy.Lox.activeLoxId
    
    1234
    +        : "";
    
    1235
    +    if (loxId === this._loxId) {
    
    1236
    +      return;
    
    1237
    +    }
    
    1238
    +    this._loxId = loxId;
    
    1239
    +    this._area.hidden = !loxId;
    
    1240
    +    // We unset _nextUnlock to ensure the areas no longer use the old value for
    
    1241
    +    // the new loxId.
    
    1242
    +    this._updateNextUnlock(true);
    
    1243
    +    this._updateRemainingInvites();
    
    1244
    +    this._updateHaveExistingInvites();
    
    1245
    +    this._updatePendingEvents();
    
    1246
    +  },
    
    1247
    +
    
    1248
    +  /**
    
    1249
    +   * The remaining invites shown, or null if uninitialized or no loxId.
    
    1250
    +   *
    
    1251
    +   * @type {integer?}
    
    1252
    +   */
    
    1253
    +  _remainingInvites: null,
    
    1254
    +  /**
    
    1255
    +   * Update the shown value.
    
    1256
    +   */
    
    1257
    +  _updateRemainingInvites() {
    
    1258
    +    const numInvites = this._loxId
    
    1259
    +      ? lazy.Lox.getRemainingInviteCount(this._loxId)
    
    1260
    +      : null;
    
    1261
    +    if (numInvites === this._remainingInvites) {
    
    1262
    +      return;
    
    1263
    +    }
    
    1264
    +    this._remainingInvites = numInvites;
    
    1265
    +    this._updateUnlockArea();
    
    1266
    +    this._updateInvitesArea();
    
    1267
    +  },
    
    1268
    +  /**
    
    1269
    +   * Whether we have existing invites, or null if uninitialized or no loxId.
    
    1270
    +   *
    
    1271
    +   * @type {boolean?}
    
    1272
    +   */
    
    1273
    +  _haveExistingInvites: null,
    
    1274
    +  /**
    
    1275
    +   * Update the shown value.
    
    1276
    +   */
    
    1277
    +  _updateHaveExistingInvites() {
    
    1278
    +    const haveInvites = this._loxId ? !!lazy.Lox.getInvites().length : null;
    
    1279
    +    if (haveInvites === this._haveExistingInvites) {
    
    1280
    +      return;
    
    1281
    +    }
    
    1282
    +    this._haveExistingInvites = haveInvites;
    
    1283
    +    this._updateInvitesArea();
    
    1284
    +  },
    
    1285
    +  /**
    
    1286
    +   * Details about the next unlock, or null if uninitialized or no loxId.
    
    1287
    +   *
    
    1288
    +   * @type {UnlockData?}
    
    1289
    +   */
    
    1290
    +  _nextUnlock: null,
    
    1291
    +  /**
    
    1292
    +   * Tracker id to ensure that the results from later calls to _updateNextUnlock
    
    1293
    +   * take priority over earlier calls.
    
    1294
    +   *
    
    1295
    +   * @type {integer}
    
    1296
    +   */
    
    1297
    +  _nextUnlockCallId: 0,
    
    1298
    +  /**
    
    1299
    +   * Update the shown value asynchronously.
    
    1300
    +   *
    
    1301
    +   * @param {boolean} [unset=false] - Whether to set the _nextUnlock value to
    
    1302
    +   *   null before waiting for the new value. I.e. ensure that the current value
    
    1303
    +   *   will not be used.
    
    1304
    +   */
    
    1305
    +  async _updateNextUnlock(unset = false) {
    
    1306
    +    // NOTE: We do not expect the integer to exceed the maximum integer.
    
    1307
    +    this._nextUnlockCallId++;
    
    1308
    +    const callId = this._nextUnlockCallId;
    
    1309
    +    if (unset) {
    
    1310
    +      this._nextUnlock = null;
    
    1311
    +    }
    
    1312
    +    const nextUnlock = this._loxId
    
    1313
    +      ? await lazy.Lox.getNextUnlock(this._loxId)
    
    1314
    +      : null;
    
    1315
    +    if (callId !== this._nextUnlockCallId) {
    
    1316
    +      // Replaced by another update.
    
    1317
    +      // E.g. if the _loxId changed. Or if getNextUnlock triggered
    
    1318
    +      // LoxTopics.UpdateNextUnlock.
    
    1319
    +      return;
    
    1320
    +    }
    
    1321
    +    // Should be safe to trigger the update, even when the value hasn't changed.
    
    1322
    +    this._nextUnlock = nextUnlock;
    
    1323
    +    this._updateUnlockArea();
    
    1324
    +  },
    
    1325
    +  /**
    
    1326
    +   * The list of events the user has not yet cleared, or null if uninitialized
    
    1327
    +   * or no loxId.
    
    1328
    +   *
    
    1329
    +   * @type {EventData[]?}
    
    1330
    +   */
    
    1331
    +  _pendingEvents: null,
    
    1332
    +  /**
    
    1333
    +   * Update the shown value.
    
    1334
    +   */
    
    1335
    +  _updatePendingEvents() {
    
    1336
    +    // Should be safe to trigger the update, even when the value hasn't changed.
    
    1337
    +    this._pendingEvents = this._loxId
    
    1338
    +      ? lazy.Lox.getEventData(this._loxId)
    
    1339
    +      : null;
    
    1340
    +    this._updateUnlockArea();
    
    1341
    +  },
    
    1342
    +
    
    1343
    +  /**
    
    1344
    +   * Update the display of the current or next unlock.
    
    1345
    +   */
    
    1346
    +  _updateUnlockArea() {
    
    1347
    +    if (
    
    1348
    +      !this._loxId ||
    
    1349
    +      this._pendingEvents === null ||
    
    1350
    +      this._remainingInvites === null ||
    
    1351
    +      this._nextUnlock === null
    
    1352
    +    ) {
    
    1353
    +      // Uninitialized or no Lox source.
    
    1354
    +      // NOTE: This area may already be hidden by the change in Lox source,
    
    1355
    +      // but we clean up for the next non-empty id.
    
    1356
    +      this._unlockAlert.hidden = true;
    
    1357
    +      this._detailsArea.hidden = true;
    
    1358
    +      return;
    
    1359
    +    }
    
    1360
    +
    
    1361
    +    // Grab focus state before changing visibility.
    
    1362
    +    const alertHadFocus = this._unlockAlert.contains(document.activeElement);
    
    1363
    +    const detailsHadFocus = this._detailsArea.contains(document.activeElement);
    
    1364
    +
    
    1365
    +    const pendingEvents = this._pendingEvents;
    
    1366
    +    const showAlert = !!pendingEvents.length;
    
    1367
    +    this._unlockAlert.hidden = !showAlert;
    
    1368
    +    this._detailsArea.hidden = showAlert;
    
    1369
    +
    
    1370
    +    if (showAlert) {
    
    1371
    +      // At level 0 and level 1, we do not have any invites.
    
    1372
    +      // If the user starts and ends on level 0 or 1, then overall they would
    
    1373
    +      // have had no change in their invites. So we do not want to show their
    
    1374
    +      // latest updates.
    
    1375
    +      // NOTE: If the user starts at level > 1 and ends with level 1 (levelling
    
    1376
    +      // down to level 0 should not be possible), then we *do* want to show the
    
    1377
    +      // user that they now have "0" invites.
    
    1378
    +      // NOTE: pendingEvents are time-ordered, with the most recent event
    
    1379
    +      // *last*.
    
    1380
    +      const firstEvent = pendingEvents[0];
    
    1381
    +      // NOTE: We cannot get a blockage event when the user starts at level 1 or
    
    1382
    +      // 0.
    
    1383
    +      const startingAtLowLevel =
    
    1384
    +        firstEvent.type === "levelup" && firstEvent.newLevel <= 2;
    
    1385
    +      const lastEvent = pendingEvents[pendingEvents.length - 1];
    
    1386
    +      const endingAtLowLevel = lastEvent.newLevel <= 1;
    
    1387
    +
    
    1388
    +      const showInvites = !(startingAtLowLevel && endingAtLowLevel);
    
    1389
    +
    
    1390
    +      let blockage = false;
    
    1391
    +      let levelUp = false;
    
    1392
    +      let bridgeGain = false;
    
    1393
    +      // Go through events, in the order that they occurred.
    
    1394
    +      for (const loxEvent of pendingEvents) {
    
    1395
    +        if (loxEvent.type === "levelup") {
    
    1396
    +          levelUp = true;
    
    1397
    +          if (loxEvent.newLevel === 1) {
    
    1398
    +            // Gain 2 bridges from level 0 to 1.
    
    1399
    +            bridgeGain = true;
    
    1400
    +          }
    
    1401
    +        } else {
    
    1402
    +          blockage = true;
    
    1403
    +        }
    
    1404
    +      }
    
    1405
    +
    
    1406
    +      let alertTitleId;
    
    1407
    +      if (levelUp && !blockage) {
    
    1408
    +        alertTitleId = "tor-bridges-lox-upgrade";
    
    1409
    +      } else {
    
    1410
    +        // Show as blocked bridges replaced.
    
    1411
    +        // Even if we have a mixture of level ups as well.
    
    1412
    +        alertTitleId = "tor-bridges-lox-blocked";
    
    1413
    +      }
    
    1414
    +      document.l10n.setAttributes(this._unlockAlertTitle, alertTitleId);
    
    1415
    +      document.l10n.setAttributes(
    
    1416
    +        this._unlockAlertInviteItem,
    
    1417
    +        "tor-bridges-lox-new-invites",
    
    1418
    +        { numInvites: this._remainingInvites }
    
    1419
    +      );
    
    1420
    +      this._unlockAlert.classList.toggle(
    
    1421
    +        "lox-unlock-upgrade",
    
    1422
    +        levelUp && !blockage
    
    1423
    +      );
    
    1424
    +      this._unlockItems.gainBridges.hidden = !bridgeGain;
    
    1425
    +      this._unlockItems.newBridges.hidden = !blockage;
    
    1426
    +      this._unlockItems.invites.hidden = !showInvites;
    
    1427
    +    } else {
    
    1428
    +      // Show next unlock.
    
    1429
    +      // Number of days until the next unlock, rounded up.
    
    1430
    +      const numDays = Math.max(
    
    1431
    +        1,
    
    1432
    +        Math.ceil(
    
    1433
    +          (new Date(this._nextUnlock.date).getTime() - Date.now()) /
    
    1434
    +            (24 * 60 * 60 * 1000)
    
    1435
    +        )
    
    1436
    +      );
    
    1437
    +      for (const counterEl of this._nextUnlockCounterEls) {
    
    1438
    +        document.l10n.setAttributes(
    
    1439
    +          counterEl,
    
    1440
    +          "tor-bridges-lox-days-until-unlock",
    
    1441
    +          { numDays }
    
    1442
    +        );
    
    1443
    +      }
    
    1444
    +
    
    1445
    +      // Gain 2 bridges from level 0 to 1. After that gain invites.
    
    1446
    +      this._nextUnlockItems.gainBridges.hidden =
    
    1447
    +        this._nextUnlock.nextLevel !== 1;
    
    1448
    +      this._nextUnlockItems.firstInvites.hidden =
    
    1449
    +        this._nextUnlock.nextLevel !== 2;
    
    1450
    +      this._nextUnlockItems.moreInvites.hidden =
    
    1451
    +        this._nextUnlock.nextLevel <= 2;
    
    1452
    +    }
    
    1453
    +
    
    1454
    +    if (alertHadFocus && !showAlert) {
    
    1455
    +      // Alert has become hidden, move focus back up to the now revealed details
    
    1456
    +      // area.
    
    1457
    +      // NOTE: We have two headings: one shown during a search and one shown
    
    1458
    +      // otherwise. We focus the heading that is currently visible.
    
    1459
    +      // See tor-browser#43320.
    
    1460
    +      // TODO: It might be better if we could use the # named anchor to
    
    1461
    +      // re-orient the screen reader position instead of using tabIndex=-1, but
    
    1462
    +      // about:preferences currently uses the anchor for showing categories
    
    1463
    +      // only. See bugzilla bug 1799153.
    
    1464
    +      if (
    
    1465
    +        this._nextUnlockCounterEls[0].checkVisibility({
    
    1466
    +          visibilityProperty: true,
    
    1467
    +        })
    
    1468
    +      ) {
    
    1469
    +        this._nextUnlockCounterEls[0].focus();
    
    1470
    +      } else {
    
    1471
    +        this._nextUnlockCounterEls[1].focus();
    
    1472
    +      }
    
    1473
    +    } else if (detailsHadFocus && showAlert) {
    
    1474
    +      this._unlockAlertButton.focus();
    
    1475
    +    }
    
    1476
    +  },
    
    1477
    +
    
    1478
    +  /**
    
    1479
    +   * Update the invites area.
    
    1480
    +   */
    
    1481
    +  _updateInvitesArea() {
    
    1482
    +    let hasInvites;
    
    1483
    +    if (
    
    1484
    +      !this._loxId ||
    
    1485
    +      this._remainingInvites === null ||
    
    1486
    +      this._haveExistingInvites === null
    
    1487
    +    ) {
    
    1488
    +      // Not initialized yet.
    
    1489
    +      hasInvites = false;
    
    1490
    +    } else {
    
    1491
    +      hasInvites = this._haveExistingInvites || !!this._remainingInvites;
    
    1492
    +    }
    
    1493
    +
    
    1494
    +    if (
    
    1495
    +      !hasInvites &&
    
    1496
    +      (this._remainingInvitesEl.contains(document.activeElement) ||
    
    1497
    +        this._invitesButton.contains(document.activeElement))
    
    1498
    +    ) {
    
    1499
    +      // About to loose focus.
    
    1500
    +      // Unexpected for the lox level to loose all invites.
    
    1501
    +      // Move to the top of the details area, which should be visible if we
    
    1502
    +      // just had focus.
    
    1503
    +      this._nextUnlockCounterEl.focus();
    
    1504
    +    }
    
    1505
    +    // Hide the invite elements if we have no historic invites or a way of
    
    1506
    +    // creating new ones.
    
    1507
    +    this._remainingInvitesEl.hidden = !hasInvites;
    
    1508
    +    this._invitesButton.hidden = !hasInvites;
    
    1509
    +
    
    1510
    +    if (hasInvites) {
    
    1511
    +      document.l10n.setAttributes(
    
    1512
    +        this._remainingInvitesEl,
    
    1513
    +        "tor-bridges-lox-remaining-invites",
    
    1514
    +        { numInvites: this._remainingInvites }
    
    1515
    +      );
    
    1516
    +    }
    
    1517
    +  },
    
    1518
    +};
    
    1519
    +
    
    1520
    +/**
    
    1521
    + * Controls the bridge settings.
    
    1522
    + */
    
    1523
    +const gBridgeSettings = {
    
    1524
    +  /**
    
    1525
    +   * The display area.
    
    1526
    +   *
    
    1527
    +   * @type {Element?}
    
    1528
    +   */
    
    1529
    +  _displayEl: null,
    
    1530
    +  /**
    
    1531
    +   * The area for showing current bridges.
    
    1532
    +   *
    
    1533
    +   * @type {Element?}
    
    1534
    +   */
    
    1535
    +  _bridgesEl: null,
    
    1536
    +  /**
    
    1537
    +   * The area for sharing bridge addresses.
    
    1538
    +   *
    
    1539
    +   * @type {Element?}
    
    1540
    +   */
    
    1541
    +  _shareEl: null,
    
    1542
    +  /**
    
    1543
    +   * The area for showing no bridges.
    
    1544
    +   *
    
    1545
    +   * @type {Element?}
    
    1546
    +   */
    
    1547
    +  _noBridgesEl: null,
    
    1548
    +  /**
    
    1549
    +   * A map from the bridge source to its corresponding label.
    
    1550
    +   *
    
    1551
    +   * @type {?Map<number, Element>}
    
    1552
    +   */
    
    1553
    +  _sourceLabels: null,
    
    1554
    +
    
    1555
    +  /**
    
    1556
    +   * Whether we have been initialized.
    
    1557
    +   *
    
    1558
    +   * @type {boolean}
    
    1559
    +   */
    
    1560
    +  _initialized: false,
    
    1561
    +
    
    1562
    +  /**
    
    1563
    +   * Initialize the bridge settings.
    
    1564
    +   *
    
    1565
    +   * @param {Element} displayEl - The widget element we are controlling.
    
    1566
    +   */
    
    1567
    +  init(displayEl) {
    
    1568
    +    if (this._initialized) {
    
    1569
    +      return;
    
    1570
    +    }
    
    1571
    +
    
    1572
    +    this._displayEl = displayEl;
    
    1573
    +    this._bridgesEl = document.getElementById("tor-bridges-current");
    
    1574
    +    this._noBridgesEl = document.getElementById("tor-bridges-none");
    
    1575
    +
    
    1576
    +    this._sourceLabels = new Map([
    
    1577
    +      [
    
    1578
    +        lazy.TorBridgeSource.BuiltIn,
    
    1579
    +        document.getElementById("tor-bridges-built-in-label"),
    
    1580
    +      ],
    
    1581
    +      [
    
    1582
    +        lazy.TorBridgeSource.UserProvided,
    
    1583
    +        document.getElementById("tor-bridges-user-label"),
    
    1584
    +      ],
    
    1585
    +      [
    
    1586
    +        lazy.TorBridgeSource.BridgeDB,
    
    1587
    +        document.getElementById("tor-bridges-requested-label"),
    
    1588
    +      ],
    
    1589
    +      [
    
    1590
    +        lazy.TorBridgeSource.Lox,
    
    1591
    +        document.getElementById("tor-bridges-lox-label"),
    
    1592
    +      ],
    
    1593
    +    ]);
    
    1594
    +    this._shareEl = document.getElementById("tor-bridges-share");
    
    1595
    +
    
    1596
    +    this._initBridgesMenu();
    
    1597
    +    this._initShareArea();
    
    1598
    +
    
    1599
    +    gBridgeGrid.init();
    
    1600
    +    gBuiltinBridgesArea.init();
    
    1601
    +    gLoxStatus.init();
    
    1602
    +    this._initialized = true;
    
    1603
    +    // Re-trigger our current bridges value to pass on to any descendants.
    
    1604
    +    this.bridges = this._bridgesVal;
    
    1605
    +    this.connectedBridgeId = this._connectedBridgeId;
    
    1606
    +  },
    
    1607
    +
    
    1608
    +  /**
    
    1609
    +   * The bridges value, set by the setting-control element.
    
    1610
    +   *
    
    1611
    +   * @type {object}
    
    1612
    +   */
    
    1613
    +  _bridgesVal: null,
    
    1614
    +
    
    1615
    +  set bridges(val) {
    
    1616
    +    if (val === null) {
    
    1617
    +      // Corresponds to pending TorSettings initialization, wait for a non-null
    
    1618
    +      // value.
    
    1619
    +      return;
    
    1620
    +    }
    
    1621
    +    this._bridgesVal = val;
    
    1622
    +    if (!this._initialized) {
    
    1623
    +      return;
    
    1624
    +    }
    
    1625
    +    this._updateSource();
    
    1626
    +    this._updateBridgeStrings();
    
    1627
    +    // Pass on to descendants.
    
    1628
    +    gBridgeGrid.bridges = val;
    
    1629
    +    gBuiltinBridgesArea.bridges = val;
    
    1630
    +    gLoxStatus.bridges = val;
    
    1631
    +  },
    
    1632
    +
    
    1633
    +  /**
    
    1634
    +   * The ID of the currently connected bridge, or `null` if there is none.
    
    1635
    +   *
    
    1636
    +   * @type {string?}
    
    1637
    +   */
    
    1638
    +  _connectedBridgeId: null,
    
    1639
    +
    
    1640
    +  set connectedBridgeId(val) {
    
    1641
    +    this._connectedBridgeId = val;
    
    1642
    +    if (!this._initialized) {
    
    1643
    +      return;
    
    1644
    +    }
    
    1645
    +    // NOTE: This should be safe to call, even when _bridgesVal is still null.
    
    1646
    +    gBridgeGrid.connectedBridgeId = val;
    
    1647
    +    gBuiltinBridgesArea.connectedBridgeId = val;
    
    1648
    +  },
    
    1649
    +
    
    1650
    +  /**
    
    1651
    +   * The shown bridge source.
    
    1652
    +   *
    
    1653
    +   * Initially null to indicate that it is unset for the first call to
    
    1654
    +   * _updateSource.
    
    1655
    +   *
    
    1656
    +   * @type {integer?}
    
    1657
    +   */
    
    1658
    +  _bridgeSource: null,
    
    1659
    +  /**
    
    1660
    +   * Whether the user is encouraged to share their bridge addresses.
    
    1661
    +   *
    
    1662
    +   * @type {boolean}
    
    1663
    +   */
    
    1664
    +  _canShare: false,
    
    1665
    +
    
    1666
    +  /**
    
    1667
    +   * Update _bridgeSource.
    
    1668
    +   */
    
    1669
    +  _updateSource() {
    
    1670
    +    // NOTE: This should only ever be called after TorSettings is already
    
    1671
    +    // initialized.
    
    1672
    +    const bridgeSource = this._bridgesVal.source;
    
    1673
    +    if (bridgeSource === this._bridgeSource) {
    
    1674
    +      // Avoid re-activating an area if the source has not changed.
    
    1675
    +      return;
    
    1676
    +    }
    
    1677
    +
    
    1678
    +    this._bridgeSource = bridgeSource;
    
    1679
    +
    
    1680
    +    // Before hiding elements, we determine whether our region contained the
    
    1681
    +    // user focus.
    
    1682
    +    const hadFocus =
    
    1683
    +      this._bridgesEl.contains(document.activeElement) ||
    
    1684
    +      this._noBridgesEl.contains(document.activeElement);
    
    1685
    +
    
    1686
    +    for (const [source, labelEl] of this._sourceLabels.entries()) {
    
    1687
    +      labelEl.hidden = source !== bridgeSource;
    
    1688
    +    }
    
    1689
    +
    
    1690
    +    this._canShare =
    
    1691
    +      bridgeSource === lazy.TorBridgeSource.UserProvided ||
    
    1692
    +      bridgeSource === lazy.TorBridgeSource.BridgeDB;
    
    1693
    +
    
    1694
    +    this._shareEl.hidden = !this._canShare;
    
    1695
    +
    
    1696
    +    // Force the menu to close whenever the source changes.
    
    1697
    +    // NOTE: If the menu had focus then hadFocus will be true, and focus will be
    
    1698
    +    // re-assigned.
    
    1699
    +    this._forceCloseBridgesMenu();
    
    1700
    +
    
    1701
    +    // Update whether we have bridges.
    
    1702
    +    this._updateHaveBridges();
    
    1703
    +
    
    1704
    +    if (hadFocus) {
    
    1705
    +      // Always reset the focus to the start of the area whenever the source
    
    1706
    +      // changes.
    
    1707
    +      lazy.moveFocusToBridgeHeading(window);
    
    1708
    +    }
    
    1709
    +  },
    
    1710
    +
    
    1711
    +  /**
    
    1712
    +   * Whether we have bridges or not, or null if it is unknown.
    
    1713
    +   *
    
    1714
    +   * @type {boolean?}
    
    1715
    +   */
    
    1716
    +  _haveBridges: null,
    
    1717
    +
    
    1718
    +  /**
    
    1719
    +   * Update the _haveBridges value.
    
    1720
    +   */
    
    1721
    +  _updateHaveBridges() {
    
    1722
    +    const haveBridges = this._bridgesVal.haveBridges;
    
    1723
    +
    
    1724
    +    if (haveBridges === this._haveBridges) {
    
    1725
    +      return;
    
    1726
    +    }
    
    1727
    +
    
    1728
    +    this._haveBridges = haveBridges;
    
    1729
    +
    
    1730
    +    // Add classes to show or hide the "no bridges" and "Your bridges" sections.
    
    1731
    +    this._bridgesEl.hidden = !haveBridges;
    
    1732
    +    this._noBridgesEl.hidden = haveBridges;
    
    1733
    +
    
    1734
    +    this._displayEl.classList.toggle("has-tor-bridges", haveBridges);
    
    1735
    +  },
    
    1736
    +
    
    1737
    +  /**
    
    1738
    +   * The bridge strings in a copy-able form.
    
    1739
    +   *
    
    1740
    +   * @type {string}
    
    1741
    +   */
    
    1742
    +  _bridgeStrings: "",
    
    1743
    +  /**
    
    1744
    +   * Whether the bridge strings should be shown as a QR code.
    
    1745
    +   *
    
    1746
    +   * @type {boolean}
    
    1747
    +   */
    
    1748
    +  _canQRBridges: false,
    
    1749
    +
    
    1750
    +  /**
    
    1751
    +   * Update the stored bridge strings.
    
    1752
    +   */
    
    1753
    +  _updateBridgeStrings() {
    
    1754
    +    const bridges = this._bridgesVal.bridgeStrings;
    
    1755
    +
    
    1756
    +    this._bridgeStrings = bridges.join("\n");
    
    1757
    +    // TODO: Determine what logic we want.
    
    1758
    +    this._canQRBridges = bridges.length <= 3;
    
    1759
    +
    
    1760
    +    this._qrButton.disabled = !this._canQRBridges;
    
    1761
    +  },
    
    1762
    +
    
    1763
    +  /**
    
    1764
    +   * Copy all the bridge addresses to the clipboard.
    
    1765
    +   */
    
    1766
    +  _copyBridges() {
    
    1767
    +    const clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
    
    1768
    +      Ci.nsIClipboardHelper
    
    1769
    +    );
    
    1770
    +    clipboard.copyString(this._bridgeStrings);
    
    1771
    +  },
    
    1772
    +
    
    1773
    +  /**
    
    1774
    +   * Open the QR code dialog encoding all the bridge addresses.
    
    1775
    +   */
    
    1776
    +  _openQR() {
    
    1777
    +    if (!this._canQRBridges) {
    
    1778
    +      return;
    
    1779
    +    }
    
    1780
    +    showBridgeQr(this._bridgeStrings);
    
    1781
    +  },
    
    1782
    +
    
    1783
    +  /**
    
    1784
    +   * The QR button for copying all QR codes.
    
    1785
    +   *
    
    1786
    +   * @type {Element?}
    
    1787
    +   */
    
    1788
    +  _qrButton: null,
    
    1789
    +
    
    1790
    +  _initShareArea() {
    
    1791
    +    document
    
    1792
    +      .getElementById("tor-bridges-copy-addresses-button")
    
    1793
    +      .addEventListener("click", () => {
    
    1794
    +        this._copyBridges();
    
    1795
    +      });
    
    1796
    +
    
    1797
    +    this._qrButton = document.getElementById("tor-bridges-qr-addresses-button");
    
    1798
    +    this._qrButton.addEventListener("click", () => {
    
    1799
    +      this._openQR();
    
    1800
    +    });
    
    1801
    +  },
    
    1802
    +
    
    1803
    +  /**
    
    1804
    +   * The menu for all bridges.
    
    1805
    +   *
    
    1806
    +   * @type {Element?}
    
    1807
    +   */
    
    1808
    +  _bridgesMenu: null,
    
    1809
    +
    
    1810
    +  /**
    
    1811
    +   * Initialize the menu for all bridges.
    
    1812
    +   */
    
    1813
    +  _initBridgesMenu() {
    
    1814
    +    this._bridgesMenu = document.getElementById("tor-bridges-all-options-menu");
    
    1815
    +
    
    1816
    +    // NOTE: We generally assume that once the bridge menu is opened the
    
    1817
    +    // this._bridgeStrings value will not change.
    
    1818
    +    const qrItem = document.getElementById(
    
    1819
    +      "tor-bridges-options-qr-all-menu-item"
    
    1820
    +    );
    
    1821
    +    qrItem.addEventListener("click", () => {
    
    1822
    +      this._openQR();
    
    1823
    +    });
    
    1824
    +
    
    1825
    +    const copyItem = document.getElementById(
    
    1826
    +      "tor-bridges-options-copy-all-menu-item"
    
    1827
    +    );
    
    1828
    +    copyItem.addEventListener("click", () => {
    
    1829
    +      this._copyBridges();
    
    1830
    +    });
    
    1831
    +
    
    1832
    +    const editItem = document.getElementById(
    
    1833
    +      "tor-bridges-options-edit-all-menu-item"
    
    1834
    +    );
    
    1835
    +    editItem.addEventListener("click", () => {
    
    1836
    +      lazy.openUserProvideBridgeDialog(window, "edit");
    
    1837
    +    });
    
    1838
    +
    
    1839
    +    // TODO: Do we want a different item for built-in bridges, rather than
    
    1840
    +    // "Remove all bridges"?
    
    1841
    +    document
    
    1842
    +      .getElementById("tor-bridges-options-remove-all-menu-item")
    
    1843
    +      .addEventListener("click", async () => {
    
    1844
    +        // TODO: Should we only have a warning when not built-in?
    
    1845
    +        const parentWindow =
    
    1846
    +          Services.wm.getMostRecentWindow("navigator:browser");
    
    1847
    +        const flags =
    
    1848
    +          Services.prompt.BUTTON_POS_0 *
    
    1849
    +            Services.prompt.BUTTON_TITLE_IS_STRING +
    
    1850
    +          Services.prompt.BUTTON_POS_0_DEFAULT +
    
    1851
    +          Services.prompt.BUTTON_DEFAULT_IS_DESTRUCTIVE +
    
    1852
    +          Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL;
    
    1853
    +
    
    1854
    +        const [titleString, bodyString, removeString] =
    
    1855
    +          await document.l10n.formatValues([
    
    1856
    +            { id: "remove-all-bridges-warning-title" },
    
    1857
    +            { id: "remove-all-bridges-warning-description" },
    
    1858
    +            { id: "remove-all-bridges-warning-remove-button" },
    
    1859
    +          ]);
    
    1860
    +
    
    1861
    +        // TODO: Update the text, and remove old strings.
    
    1862
    +        const buttonIndex = Services.prompt.confirmEx(
    
    1863
    +          parentWindow,
    
    1864
    +          titleString,
    
    1865
    +          bodyString,
    
    1866
    +          flags,
    
    1867
    +          removeString,
    
    1868
    +          null,
    
    1869
    +          null,
    
    1870
    +          null,
    
    1871
    +          {}
    
    1872
    +        );
    
    1873
    +
    
    1874
    +        if (buttonIndex !== 0) {
    
    1875
    +          return;
    
    1876
    +        }
    
    1877
    +
    
    1878
    +        lazy.TorSettings.changeSettings({
    
    1879
    +          // This should always have the side effect of disabling bridges as
    
    1880
    +          // well.
    
    1881
    +          bridges: { source: lazy.TorBridgeSource.Invalid },
    
    1882
    +        });
    
    1883
    +      });
    
    1884
    +
    
    1885
    +    this._bridgesMenu.addEventListener("showing", () => {
    
    1886
    +      qrItem.hidden = !this._canShare || !this._canQRBridges;
    
    1887
    +      editItem.hidden =
    
    1888
    +        this._bridgeSource !== lazy.TorBridgeSource.UserProvided;
    
    1889
    +    });
    
    1890
    +
    
    1891
    +    const bridgesMenuButton = document.getElementById(
    
    1892
    +      "tor-bridges-all-options-button"
    
    1893
    +    );
    
    1894
    +    bridgesMenuButton.addEventListener("click", event => {
    
    1895
    +      this._bridgesMenu.toggle(event, bridgesMenuButton);
    
    1896
    +    });
    
    1897
    +
    
    1898
    +    this._bridgesMenu.addEventListener("hidden", () => {
    
    1899
    +      // Make sure the button receives focus again when the menu is hidden.
    
    1900
    +      // Currently, panel-list.js only does this when the menu is opened with a
    
    1901
    +      // keyboard, but this causes focus to be lost from the page if the user
    
    1902
    +      // uses a mixture of keyboard and mouse.
    
    1903
    +      bridgesMenuButton.focus();
    
    1904
    +    });
    
    1905
    +  },
    
    1906
    +
    
    1907
    +  /**
    
    1908
    +   * Force the bridges menu to close.
    
    1909
    +   */
    
    1910
    +  _forceCloseBridgesMenu() {
    
    1911
    +    this._bridgesMenu.hide(null, { force: true });
    
    1912
    +  },
    
    1913
    +};
    
    1914
    +
    
    1915
    +// TODO: Replace gBridgeSettings and #tor-bridges-display-template with proper
    
    1916
    +// widgets (using MozLitElement).
    
    1917
    +/**
    
    1918
    + * Show the current bridges.
    
    1919
    + */
    
    1920
    +class TorBridgesDisplay extends HTMLElement {
    
    1921
    +  connectedCallback() {
    
    1922
    +    if (this.children.length) {
    
    1923
    +      return;
    
    1924
    +    }
    
    1925
    +    // Take the template children since we only expect one instance of this.
    
    1926
    +    this.replaceChildren(
    
    1927
    +      ...document.getElementById("tor-bridges-display-template").content
    
    1928
    +        .childNodes
    
    1929
    +    );
    
    1930
    +    gBridgeSettings.init(this);
    
    1931
    +  }
    
    1932
    +
    
    1933
    +  set connectedBridgeId(val) {
    
    1934
    +    gBridgeSettings.connectedBridgeId = val;
    
    1935
    +  }
    
    1936
    +
    
    1937
    +  set bridges(val) {
    
    1938
    +    gBridgeSettings.bridges = val;
    
    1939
    +  }
    
    1940
    +
    
    1941
    +  /**
    
    1942
    +   * Focus the "Your bridges" heading, if it is visible.
    
    1943
    +   *
    
    1944
    +   * @returns {boolean} - `true` if the heading was visible and focused.
    
    1945
    +   */
    
    1946
    +  focusHeading() {
    
    1947
    +    if (!gBridgeSettings._haveBridges) {
    
    1948
    +      // Heading is hidden.
    
    1949
    +      return false;
    
    1950
    +    }
    
    1951
    +    document.getElementById("tor-bridges-current-heading-non-search").focus();
    
    1952
    +    return true;
    
    1953
    +  }
    
    1954
    +}
    
    1955
    +customElements.define("tor-bridges-display", TorBridgesDisplay);

  • toolkit/content/widgets/moz-fieldset/moz-fieldset.css
    ... ... @@ -46,6 +46,10 @@ h4,
    46 46
     h5,
    
    47 47
     h6 {
    
    48 48
       margin: 0;
    
    49
    +
    
    50
    +  &[tabindex]:focus-visible {
    
    51
    +    outline-offset: var(--focus-outline-offset);
    
    52
    +  }
    
    49 53
     }
    
    50 54
     
    
    51 55
     .description {
    

  • toolkit/content/widgets/moz-fieldset/moz-fieldset.mjs
    ... ... @@ -11,12 +11,24 @@ import { MozLitElement } from "../lit-utils.mjs";
    11 11
      * @type {Record<number, (label: string) => ReturnType<typeof html>>}
    
    12 12
      */
    
    13 13
     const HEADING_LEVEL_TEMPLATES = {
    
    14
    -  1: label => html`<h1 class="text-box-trim-start">${label}</h1>`,
    
    15
    -  2: label => html`<h2 class="text-box-trim-start">${label}</h2>`,
    
    16
    -  3: label => html`<h3 class="text-box-trim-start">${label}</h3>`,
    
    17
    -  4: label => html`<h4>${label}</h4>`,
    
    18
    -  5: label => html`<h5>${label}</h5>`,
    
    19
    -  6: label => html`<h6>${label}</h6>`,
    
    14
    +  1: (label, tabindex) =>
    
    15
    +    html`<h1 class="text-box-trim-start" tabindex=${ifDefined(tabindex)}>
    
    16
    +      ${label}
    
    17
    +    </h1>`,
    
    18
    +  2: (label, tabindex) =>
    
    19
    +    html`<h2 class="text-box-trim-start" tabindex=${ifDefined(tabindex)}>
    
    20
    +      ${label}
    
    21
    +    </h2>`,
    
    22
    +  3: (label, tabindex) =>
    
    23
    +    html`<h3 class="text-box-trim-start" tabindex=${ifDefined(tabindex)}>
    
    24
    +      ${label}
    
    25
    +    </h3>`,
    
    26
    +  4: (label, tabindex) =>
    
    27
    +    html`<h4 tabindex=${ifDefined(tabindex)}>${label}</h4>`,
    
    28
    +  5: (label, tabindex) =>
    
    29
    +    html`<h5 tabindex=${ifDefined(tabindex)}>${label}</h5>`,
    
    30
    +  6: (label, tabindex) =>
    
    31
    +    html`<h6 tabindex=${ifDefined(tabindex)}>${label}</h6>`,
    
    20 32
     };
    
    21 33
     
    
    22 34
     /**
    
    ... ... @@ -42,6 +54,9 @@ export default class MozFieldset extends MozLitElement {
    42 54
         disabled: { type: Boolean, reflect: true },
    
    43 55
         iconSrc: { type: String },
    
    44 56
         badge: { type: String },
    
    57
    +    // Allows the heading to be focusable, but not part of the Tab focus cycle.
    
    58
    +    // See tor-browser#45143.
    
    59
    +    focusableHeading: { type: Boolean, attribute: "focusable-heading" },
    
    45 60
       };
    
    46 61
     
    
    47 62
       constructor() {
    
    ... ... @@ -132,12 +147,25 @@ export default class MozFieldset extends MozLitElement {
    132 147
     
    
    133 148
       legendTemplate() {
    
    134 149
         let label =
    
    135
    -      HEADING_LEVEL_TEMPLATES[this.headingLevel]?.(this.label) || this.label;
    
    150
    +      HEADING_LEVEL_TEMPLATES[this.headingLevel]?.(
    
    151
    +        this.label,
    
    152
    +        this.focusableHeading ? "-1" : undefined
    
    153
    +      ) || this.label;
    
    136 154
         return html`<legend part="label">
    
    137 155
           ${this.iconTemplate()}${label}${this.badgeTemplate()}
    
    138 156
         </legend>`;
    
    139 157
       }
    
    140 158
     
    
    159
    +  /**
    
    160
    +   * Move the user's focus to the heading, if it is focusable.
    
    161
    +   */
    
    162
    +  focusHeading() {
    
    163
    +    if (!this.focusableHeading) {
    
    164
    +      return;
    
    165
    +    }
    
    166
    +    this.shadowRoot.querySelector("h1,h2,h3,h4,h5,h6")?.focus();
    
    167
    +  }
    
    168
    +
    
    141 169
       iconTemplate() {
    
    142 170
         if (!this.iconSrc) {
    
    143 171
           return "";
    

  • toolkit/locales/en-US/toolkit/global/tor-browser.ftl
    ... ... @@ -152,6 +152,9 @@ tor-connection-status-connect-button =
    152 152
     tor-bridges-heading = Bridges
    
    153 153
     tor-bridges-overview = Bridges help you securely access the Tor network in places where Tor is blocked. Depending on where you are, one bridge may work better than another.
    
    154 154
     tor-bridges-learn-more-link = Learn more
    
    155
    +tor-bridges-group =
    
    156
    +    .label = Bridges
    
    157
    +    .description = Bridges help you securely access the Tor network in places where Tor is blocked. Depending on where you are, one bridge may work better than another.
    
    155 158
     
    
    156 159
     # Toggle button for enabling and disabling the use of bridges.
    
    157 160
     tor-bridges-use-bridges =
    
    ... ... @@ -310,12 +313,23 @@ tor-bridges-lox-got-it-button = Got it
    310 313
     tor-bridges-add-bridges-heading = Add bridges
    
    311 314
     # Shown as a heading when the user has existing bridges that can be replaced.
    
    312 315
     tor-bridges-replace-bridges-heading = Replace your bridges
    
    316
    +# Shown as a heading when the user has no current bridges.
    
    317
    +tor-bridges-add-bridges-group =
    
    318
    +    .label = Add bridges
    
    319
    +# Shown as a heading when the user has existing bridges that can be replaced.
    
    320
    +tor-bridges-replace-bridges-group =
    
    321
    +    .label = Replace your bridges
    
    313 322
     
    
    314 323
     # -brand-short-name refers to 'Tor Browser', localized.
    
    315 324
     tor-bridges-select-built-in-description = Choose from one of { -brand-short-name }’s built-in bridges
    
    325
    +# -brand-short-name refers to 'Tor Browser', localized.
    
    326
    +tor-bridges-choose-built-in-button =
    
    327
    +    .label = Choose from one of { -brand-short-name }’s built-in bridges
    
    316 328
     tor-bridges-select-built-in-button = Select a built-in bridge…
    
    317 329
     
    
    318 330
     tor-bridges-add-addresses-description = Enter bridge addresses you already know
    
    331
    +tor-bridges-enter-bridges-button =
    
    332
    +    .label = Enter bridge addresses you already know
    
    319 333
     # Shown when the user has no current bridges.
    
    320 334
     # Opens a dialog where the user can provide a new bridge address or share code.
    
    321 335
     tor-bridges-add-new-button = Add new bridges…
    
    ... ... @@ -326,6 +340,10 @@ tor-bridges-replace-button = Replace bridges…
    326 340
     tor-bridges-find-more-heading = Find more bridges
    
    327 341
     # "Tor Project" is the organisation name.
    
    328 342
     tor-bridges-find-more-description = Since many bridge addresses aren’t public, you may need to request some from the Tor Project.
    
    343
    +# "Tor Project" is the organisation name.
    
    344
    +tor-bridges-find-more-group =
    
    345
    +    .label = Find more bridges
    
    346
    +    .description = Since many bridge addresses aren’t public, you may need to request some from the Tor Project.
    
    329 347
     
    
    330 348
     # "Telegram" is the common brand name of the Telegram Messenger application
    
    331 349
     tor-bridges-provider-telegram-name = Telegram
    
    ... ... @@ -333,6 +351,12 @@ tor-bridges-provider-telegram-name = Telegram
    333 351
     # $telegramUserName (String) - The Telegram Messenger user name that should receive messages. Should be wrapped in '<a data-l10n-name="user">' and '</a>'.
    
    334 352
     # E.g. in English, "Message GetBridgesBot".
    
    335 353
     tor-bridges-provider-telegram-instruction = Message <a data-l10n-name="user">{ $telegramUserName }</a>
    
    354
    +# "Telegram" is the common brand name of the Telegram Messenger application.
    
    355
    +# Here "Message" is a verb, short for "Send a message to". This is an instruction to send a message to the given Telegram Messenger user to receive a new bridge.
    
    356
    +# $telegramUserName (String) - The Telegram Messenger user name that should receive messages.
    
    357
    +tor-bridges-source-telegram-link =
    
    358
    +    .label = Telegram
    
    359
    +    .description = Message { $telegramUserName }
    
    336 360
     
    
    337 361
     # "Web" is the proper noun for the "World Wide Web".
    
    338 362
     tor-bridges-provider-web-name = Web
    
    ... ... @@ -340,15 +364,31 @@ tor-bridges-provider-web-name = Web
    340 364
     # $url (String) - The URL for Tor Project bridges. Should be wrapped in '<a data-l10n-name"url">' and '</a>'.
    
    341 365
     tor-bridges-provider-web-instruction = Visit <a data-l10n-name="url">{ $url }</a>
    
    342 366
     
    
    367
    +# "Web" is the proper noun for the "World Wide Web".
    
    368
    +# $url (String) - The URL for Tor Project bridges.
    
    369
    +tor-bridges-source-web-link =
    
    370
    +    .label = Web
    
    371
    +    .description = Visit { $url }
    
    372
    +
    
    343 373
     # "Gmail" is the Google brand name. "Riseup" refers to the Riseup organisation at riseup.net.
    
    344 374
     tor-bridges-provider-email-name = Gmail or Riseup
    
    345 375
     # Here "Email" is a verb, short for "Send an email to". This is an instruction to send an email to the given address to receive a new bridge.
    
    346 376
     # $address (String) - The email address that should receive the email.
    
    347 377
     # E.g. in English, "Email bridges@torproject.org".
    
    348 378
     tor-bridges-provider-email-instruction = Email { $address }
    
    379
    +# "Gmail" is the Google brand name. "Riseup" refers to the Riseup organisation at riseup.net.
    
    380
    +# Here "Email" is a verb, short for "Send an email to". This is an instruction to send an email to the given address to receive a new bridge.
    
    381
    +# $address (String) - The email address that should receive the email.
    
    382
    +# E.g. in English, "Email bridges@torproject.org".
    
    383
    +tor-bridges-source-email-link =
    
    384
    +    .label = Gmail or Riseup
    
    385
    +    .description = Email { $address }
    
    349 386
     
    
    350 387
     tor-bridges-request-from-browser = You can also get bridges from the bridge bot without leaving { -brand-short-name }.
    
    351 388
     tor-bridges-request-button = Request bridges…
    
    389
    +tor-bridges-request-from-browser2 = Get bridges from the bridge bot without leaving { -brand-short-name }.
    
    390
    +tor-bridges-request-button2 =
    
    391
    +    .label = Request bridges…
    
    352 392
     
    
    353 393
     ## Warning dialog when removing all bridges.
    
    354 394
     
    

  • toolkit/modules/TorSettings.sys.mjs
    ... ... @@ -358,6 +358,12 @@ class TorSettingsImpl {
    358 358
        */
    
    359 359
       #initialized = false;
    
    360 360
     
    
    361
    +  /**
    
    362
    +   * Whether init has been called.
    
    363
    +   *
    
    364
    +   * @type {boolean}
    
    365
    +   */
    
    366
    +  #initCalled = false;
    
    361 367
       /**
    
    362 368
        * Whether uninit cleanup has been called.
    
    363 369
        *
    
    ... ... @@ -510,11 +516,12 @@ class TorSettingsImpl {
    510 516
        * Load or init our settings.
    
    511 517
        */
    
    512 518
       async init() {
    
    513
    -    if (this.#initialized) {
    
    519
    +    if (this.#initCalled) {
    
    514 520
           lazy.logger.warn("Called init twice.");
    
    515 521
           await this.#initializedPromise;
    
    516 522
           return;
    
    517 523
         }
    
    524
    +    this.#initCalled = true;
    
    518 525
         try {
    
    519 526
           await this.#initInternal();
    
    520 527
           this.#initialized = true;
    

  • tools/torbrowser/l10n/migrations/bug-45058-bridge-settings.py
    1
    +import re
    
    2
    +
    
    3
    +import fluent.syntax.ast as FTL
    
    4
    +from fluent.migrate.helpers import transforms_from
    
    5
    +from fluent.migrate.transforms import COPY_PATTERN, FluentSource
    
    6
    +from fluent.syntax.visitor import Visitor
    
    7
    +
    
    8
    +
    
    9
    +class RemoveAnchorVisitor(Visitor):
    
    10
    +    """Class to remove <a> and </a> wrappers from a Fluent TextElement."""
    
    11
    +
    
    12
    +    def __init__(self):
    
    13
    +        # Good enough regex for our needs that will match starting and ending
    
    14
    +        # tags.
    
    15
    +        self._anchor_regex = re.compile(r"<\/?[aA](| [^>]*)>")
    
    16
    +        super().__init__()
    
    17
    +
    
    18
    +    def visit_TextElement(self, node):
    
    19
    +        node.value = self._anchor_regex.sub("", node.value)
    
    20
    +
    
    21
    +
    
    22
    +class RemoveAnchorTransform(FluentSource):
    
    23
    +    """Class to remove <a> and </a> wrappers from a Fluent source."""
    
    24
    +
    
    25
    +    def __call__(self, ctx):
    
    26
    +        pattern = ctx.get_fluent_source_pattern(self.path, self.key).clone()
    
    27
    +        # Visit every node in the pattern, replacing each TextElement's content.
    
    28
    +        RemoveAnchorVisitor().visit(pattern)
    
    29
    +        return pattern
    
    30
    +
    
    31
    +
    
    32
    +def migrate(ctx):
    
    33
    +    ctx.add_transforms(
    
    34
    +        "tor-browser.ftl",
    
    35
    +        "tor-browser.ftl",
    
    36
    +        transforms_from(
    
    37
    +            """
    
    38
    +tor-bridges-group =
    
    39
    +    .label = { COPY_PATTERN(path, "tor-bridges-heading") }
    
    40
    +    .description = { COPY_PATTERN(path, "tor-bridges-overview") }
    
    41
    +tor-bridges-add-bridges-group =
    
    42
    +    .label = { COPY_PATTERN(path, "tor-bridges-add-bridges-heading") }
    
    43
    +tor-bridges-replace-bridges-group =
    
    44
    +    .label = { COPY_PATTERN(path, "tor-bridges-replace-bridges-heading") }
    
    45
    +tor-bridges-choose-built-in-button =
    
    46
    +    .label = { COPY_PATTERN(path, "tor-bridges-select-built-in-description") }
    
    47
    +tor-bridges-enter-bridges-button =
    
    48
    +    .label = { COPY_PATTERN(path, "tor-bridges-add-addresses-description") }
    
    49
    +tor-bridges-find-more-group =
    
    50
    +    .label = { COPY_PATTERN(path, "tor-bridges-find-more-heading") }
    
    51
    +    .description = { COPY_PATTERN(path, "tor-bridges-find-more-description") }
    
    52
    +tor-bridges-request-button2 =
    
    53
    +    .label = { COPY_PATTERN(path, "tor-bridges-request-button") }
    
    54
    +tor-bridges-source-email-link =
    
    55
    +    .label = { COPY_PATTERN(path, "tor-bridges-provider-email-name") }
    
    56
    +    .description = { COPY_PATTERN(path, "tor-bridges-provider-email-instruction") }
    
    57
    +""",
    
    58
    +            path="tor-browser.ftl",
    
    59
    +        )
    
    60
    +        + [
    
    61
    +            FTL.Message(
    
    62
    +                id=FTL.Identifier("tor-bridges-source-telegram-link"),
    
    63
    +                value=None,
    
    64
    +                attributes=[
    
    65
    +                    FTL.Attribute(
    
    66
    +                        id=FTL.Identifier("label"),
    
    67
    +                        value=COPY_PATTERN(
    
    68
    +                            "tor-browser.ftl",
    
    69
    +                            "tor-bridges-provider-telegram-name",
    
    70
    +                        ),
    
    71
    +                    ),
    
    72
    +                    FTL.Attribute(
    
    73
    +                        id=FTL.Identifier("description"),
    
    74
    +                        value=RemoveAnchorTransform(
    
    75
    +                            "tor-browser.ftl",
    
    76
    +                            "tor-bridges-provider-telegram-instruction",
    
    77
    +                        ),
    
    78
    +                    ),
    
    79
    +                ],
    
    80
    +            ),
    
    81
    +            FTL.Message(
    
    82
    +                id=FTL.Identifier("tor-bridges-source-web-link"),
    
    83
    +                value=None,
    
    84
    +                attributes=[
    
    85
    +                    FTL.Attribute(
    
    86
    +                        id=FTL.Identifier("label"),
    
    87
    +                        value=COPY_PATTERN(
    
    88
    +                            "tor-browser.ftl",
    
    89
    +                            "tor-bridges-provider-web-name",
    
    90
    +                        ),
    
    91
    +                    ),
    
    92
    +                    FTL.Attribute(
    
    93
    +                        id=FTL.Identifier("description"),
    
    94
    +                        value=RemoveAnchorTransform(
    
    95
    +                            "tor-browser.ftl",
    
    96
    +                            "tor-bridges-provider-web-instruction",
    
    97
    +                        ),
    
    98
    +                    ),
    
    99
    +                ],
    
    100
    +            ),
    
    101
    +        ],
    
    102
    +    )