Pier Angelo Vendrame pushed to branch tor-browser-128.8.0esr-14.5-1 at The Tor Project / Applications / Tor Browser
Commits: e54a67ba by Henry Wilkes at 2025-03-27T09:59:37+00:00 fixup! TB 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection
TB 43563: Use TorConnect.getRegionNames rather than TorConnect.countryNames.
Also, add a note regarding changing app locales.
- - - - - c71487dc by Henry Wilkes at 2025-03-27T09:59:37+00:00 fixup! TB 27476: Implement about:torconnect captive portal within Tor Browser
TB 43563: Re-fill the list of region names when the app locale changes.
We also clean up some of the corresponding code.
- - - - - 31ac7a09 by Henry Wilkes at 2025-03-27T09:59:37+00:00 fixup! TB 40597: Implement TorSettings module
TB 43563: Rebuild TorConnect region names when the app locale changes.
Also rename TorConnect.countryNames to TorConnect.getRegionNames.
- - - - - d24a4c8c by Henry Wilkes at 2025-03-27T09:59:37+00:00 fixup! TB 42247: Android helpers for the TorProvider
TB 43563: Use TorConnect.getRegionNames rather than TorConnect.countryNames.
- - - - -
7 changed files:
- browser/components/torpreferences/content/connectionPane.js - toolkit/components/torconnect/TorConnectParent.sys.mjs - toolkit/components/torconnect/content/aboutTorConnect.html - toolkit/components/torconnect/content/aboutTorConnect.js - toolkit/modules/RemotePageAccessManager.sys.mjs - toolkit/modules/TorAndroidIntegration.sys.mjs - toolkit/modules/TorConnect.sys.mjs
Changes:
===================================== browser/components/torpreferences/content/connectionPane.js ===================================== @@ -2545,17 +2545,14 @@ const gConnectionPane = (function () { } return item; }; + + // TODO: Re-fetch when intl:app-locales-changed is fired, if we keep + // this after tor-browser#42477. + const regionNames = TorConnect.getRegionNames(); const addLocations = codes => { const items = []; for (const code of codes) { - items.push( - createItem( - code, - TorConnect.countryNames[code] - ? TorConnect.countryNames[code] - : code - ) - ); + items.push(createItem(code, regionNames[code] || code)); } items.sort((left, right) => left.label.localeCompare(right.label)); locationEntries.append(...items); @@ -2573,7 +2570,7 @@ const gConnectionPane = (function () { locationEntries.append( createItem("", TorStrings.settings.bridgeLocationOther, true) ); - addLocations(Object.keys(TorConnect.countryNames)); + addLocations(Object.keys(regionNames)); }); this._showAutoconfiguration = () => { locationGroup.hidden =
===================================== toolkit/components/torconnect/TorConnectParent.sys.mjs ===================================== @@ -53,6 +53,9 @@ export class TorConnectParent extends JSWindowActorParent { TorConnect.quickstart ); break; + case TorConnectTopics.RegionNamesChange: + self.sendAsyncMessage("torconnect:region-names-change"); + break; } }, }; @@ -69,6 +72,10 @@ export class TorConnectParent extends JSWindowActorParent { this.torConnectObserver, TorConnectTopics.QuickstartChange ); + Services.obs.addObserver( + this.torConnectObserver, + TorConnectTopics.RegionNamesChange + ); }
didDestroy() { @@ -84,6 +91,10 @@ export class TorConnectParent extends JSWindowActorParent { this.torConnectObserver, TorConnectTopics.QuickstartChange ); + Services.obs.removeObserver( + this.torConnectObserver, + TorConnectTopics.RegionNamesChange + ); }
async receiveMessage(message) { @@ -134,7 +145,6 @@ export class TorConnectParent extends JSWindowActorParent { return { TorStrings, Direction: Services.locale.isAppLocaleRTL ? "rtl" : "ltr", - CountryNames: TorConnect.countryNames, stage: TorConnect.stage, userHasEverClickedConnect: Services.prefs.getBoolPref( userHasEverClickedConnectPref, @@ -142,8 +152,10 @@ export class TorConnectParent extends JSWindowActorParent { ), quickstartEnabled: TorConnect.quickstart, }; - case "torconnect:get-frequent-regions": - return TorConnect.getFrequentRegions(); + case "torconnect:get-regions": + return TorConnect.getFrequentRegions().then(frequent => { + return { names: TorConnect.getRegionNames(), frequent }; + }); } return undefined; }
===================================== toolkit/components/torconnect/content/aboutTorConnect.html ===================================== @@ -68,7 +68,11 @@ <div class="button-container"> <label id="locationDropdownLabel" for="countries"></label> <form id="locationDropdown" hidden="true"> - <select id="countries"></select> + <select id="regions-select"> + <option id="first-region-option"></option> + <optgroup id="frequent-regions-option-group"></optgroup> + <optgroup id="full-regions-option-group"></optgroup> + </select> </form> <span id="buttonPadding"></span> <span id="connectButtonContainer">
===================================== toolkit/components/torconnect/content/aboutTorConnect.js ===================================== @@ -70,7 +70,7 @@ class AboutTorConnect { tryBridge: "button#tryBridgeButton", locationDropdownLabel: "#locationDropdownLabel", locationDropdown: "#locationDropdown", - locationDropdownSelect: "#locationDropdown select", + locationDropdownSelect: "#regions-select", }, });
@@ -129,13 +129,38 @@ class AboutTorConnect { locationDropdownSelect: document.querySelector( this.selectors.buttons.locationDropdownSelect ), + firstRegionOption: document.getElementById("first-region-option"), + frequentRegionsOptionGroup: document.getElementById( + "frequent-regions-option-group" + ), + fullRegionsOptionGroup: document.getElementById( + "full-regions-option-group" + ), tryBridgeButton: document.querySelector(this.selectors.buttons.tryBridge), });
- selectedLocation; + /** + * The currently shown stage, or `null` if the page in uninitialised. + * + * @type {?string} + */ shownStage = null;
- locations = {}; + /** + * A promise that resolves to a list of region names and frequent regions, or + * `null` if this needs to be re-fetched from the TorConnectParent. + * + * @type {?Promise<object>} + */ + regions = null; + + /** + * The option value that *should* be selected when the list of regions is + * populated. + * + * @type {string} + */ + selectedRegion = "";
/** * Whether the user requested a cancellation of the bootstrap from *this* @@ -201,89 +226,6 @@ class AboutTorConnect { this.hide(this.elements.tryBridgeButton); }
- populateLocations() { - const selectCountryRegion = document.createElement("option"); - selectCountryRegion.textContent = TorStrings.torConnect.selectCountryRegion; - selectCountryRegion.value = ""; - - // get all codes and names from TorStrings - const locationNodes = []; - for (const [code, name] of Object.entries(this.locations)) { - let option = document.createElement("option"); - option.value = code; - option.textContent = name; - locationNodes.push(option); - } - // locale sort by name - locationNodes.sort((left, right) => - left.textContent.localeCompare(right.textContent) - ); - this.elements.locationDropdownSelect.append( - selectCountryRegion, - ...locationNodes - ); - } - - populateFrequentLocations(locations) { - this.removeFrequentLocations(); - if (!locations || !locations.length) { - return; - } - - const locationNodes = []; - for (const code of locations) { - const option = document.createElement("option"); - option.value = code; - option.className = "frequent-location"; - // codes (partially) come from rdsys service, so make sure we have a - // string defined for it - let name = this.locations[code]; - if (!name) { - name = code; - } - option.textContent = name; - locationNodes.push(option); - } - // locale sort by name - locationNodes.sort((left, right) => - left.textContent.localeCompare(right.textContent) - ); - - const frequentGroup = document.createElement("optgroup"); - frequentGroup.setAttribute( - "label", - TorStrings.torConnect.frequentLocations - ); - frequentGroup.className = "frequent-location"; - const locationGroup = document.createElement("optgroup"); - locationGroup.setAttribute("label", TorStrings.torConnect.otherLocations); - locationGroup.className = "frequent-location"; - // options[0] is either "Select Country or Region" or "Automatic" - this.elements.locationDropdownSelect.options[0].after( - frequentGroup, - ...locationNodes, - locationGroup - ); - } - - removeFrequentLocations() { - const select = this.elements.locationDropdownSelect; - for (const option of select.querySelectorAll(".frequent-location")) { - option.remove(); - } - } - - validateLocation() { - const selectedIndex = this.elements.locationDropdownSelect.selectedIndex; - const selectedOption = - this.elements.locationDropdownSelect.options[selectedIndex]; - if (!selectedOption.value) { - this.elements.tryBridgeButton.setAttribute("disabled", "disabled"); - } else { - this.elements.tryBridgeButton.removeAttribute("disabled"); - } - } - setTitle(title, className) { this.elements.heading.textContent = title; this.elements.title.className = "title"; @@ -407,7 +349,9 @@ class AboutTorConnect {
const prevStage = this.shownStage; this.shownStage = stage.name; - this.selectedLocation = stage.defaultRegion; + // Make a request to change the selected region in the next call to + // selectRegionOption. + this.selectedRegion = stage.defaultRegion;
// By default we want to reset the focus to the top of the page when // changing the displayed page since we want a user to read the new page @@ -708,24 +652,105 @@ class AboutTorConnect { } }
+ /** + * Try and select the region specified in `selectedRegion`. + */ + selectRegionOption() { + // NOTE: If the region appears in both the frequent list and the full list, + // then this will select the region option in + // frequentRegionsOptionGroup, even if the user had prior selected the + // option from fullRegionsOptionGroup. But the overall value should be the + // same. + this.elements.locationDropdownSelect.value = this.selectedRegion; + if (this.elements.locationDropdownSelect.selectedIndex === -1) { + // Select the first, as a fallback. E.g. in RegionNotFound the + // selectedRegion may still be "automatic", but this is no longer + // available. + this.elements.locationDropdownSelect.selectedIndex = 0; + } + this.validateRegion(); + } + + /** + * Ensure that the current selected region is valid for the shown stage. + */ + validateRegion() { + this.elements.tryBridgeButton.toggleAttribute( + "disabled", + !this.elements.locationDropdownSelect.value + ); + } + + /** + * Populate the full list of regions, if necessary. + */ + async populateDelayedRegionOptions() { + if (this.regions) { + // Already populated, or about to populate. + return; + } + + this.regions = RPMSendQuery("torconnect:get-regions"); + const regions = this.regions; + const { names, frequent } = await regions; + + if (regions !== this.regions) { + // Replaced by a new call. + return; + } + + this.setRegionOptions( + this.elements.frequentRegionsOptionGroup, + frequent.map(code => [code, names[code]]) + ); + + this.setRegionOptions( + this.elements.fullRegionsOptionGroup, + Object.entries(names) + ); + + // Now that the list has been re-populated we want to re-select the + // requested region. + this.selectRegionOption(); + } + + /** + * Set the shown region options. + * + * @param {HTMLOptGroupElement} group - The group to set the children of. + * @param {[string, string|undefined][]} regions - The list of region + * key-value entries to fill the group with. The key is the region code and + * the value is the region's localised name. + */ + setRegionOptions(group, regions) { + const regionNodes = regions + .sort(([_code1, name1], [_code2, name2]) => name1.localeCompare(name2)) + .map(([code, name]) => { + const option = document.createElement("option"); + option.value = code; + // If the name is unexpectedly empty or undefined we use the code + // instead. + option.textContent = name || code; + return option; + }); + group.replaceChildren(...regionNodes); + } + showLocationForm(isChoose, buttonLabel) { this.hideButtons(); - RPMSendQuery("torconnect:get-frequent-regions").then(codes => { - if (codes && codes.length) { - this.populateFrequentLocations(codes); - this.setLocation(); - } - }); - let firstOpt = this.elements.locationDropdownSelect.options[0]; - if (isChoose) { - firstOpt.value = "automatic"; - firstOpt.textContent = TorStrings.torConnect.automatic; - } else { - firstOpt.value = ""; - firstOpt.textContent = TorStrings.torConnect.selectCountryRegion; - } - this.setLocation(); - this.validateLocation(); + + this.elements.firstRegionOption.textContent = isChoose + ? TorStrings.torConnect.automatic + : TorStrings.torConnect.selectCountryRegion; + this.elements.firstRegionOption.value = isChoose ? "automatic" : ""; + + // Try and select the region now, prior to waiting for + // populateDelayedRegionOptions. + this.selectRegionOption(); + + // Async fill the rest of the region options, if needed. + this.populateDelayedRegionOptions(); + this.show(this.elements.locationDropdownLabel); this.show(this.elements.locationDropdown); this.elements.locationDropdownLabel.classList.toggle("error", !isChoose); @@ -735,29 +760,6 @@ class AboutTorConnect { } }
- getLocation() { - const selectedIndex = this.elements.locationDropdownSelect.selectedIndex; - return this.elements.locationDropdownSelect.options[selectedIndex].value; - } - - setLocation() { - const code = this.selectedLocation; - if (this.getLocation() === code) { - return; - } - const options = this.elements.locationDropdownSelect.options; - // We need to do this way, because we have repeated values that break - // the .value way to select (which would however require the label, - // rather than the code)... - for (let i = 0; i < options.length; i++) { - if (options[i].value === code) { - this.elements.locationDropdownSelect.selectedIndex = i; - break; - } - } - this.validateLocation(); - } - initElements(direction) { const isAndroid = navigator.userAgent.includes("Android"); document.body.classList.toggle("android", isAndroid); @@ -826,17 +828,32 @@ class AboutTorConnect { this.beginBootstrapping(this.shownStage === "Start"); });
- this.populateLocations(); this.elements.locationDropdownSelect.addEventListener("change", () => { - this.validateLocation(); + // Overwrite the stage requested selectedRegion. + // NOTE: This should not fire in response to a programmatic change in + // value. + // E.g. if the user selects a region, then changes locale, we want the + // same region to be re-selected after the option list is rebuilt. + this.selectedRegion = this.elements.locationDropdownSelect.value; + + this.validateRegion(); });
this.elements.locationDropdownLabel.textContent = TorStrings.torConnect.unblockInternetIn;
+ this.elements.frequentRegionsOptionGroup.setAttribute( + "label", + TorStrings.torConnect.frequentLocations + ); + this.elements.fullRegionsOptionGroup.setAttribute( + "label", + TorStrings.torConnect.otherLocations + ); + this.elements.tryBridgeButton.textContent = TorStrings.torConnect.tryBridge; this.elements.tryBridgeButton.addEventListener("click", () => { - const value = this.getLocation(); + const value = this.elements.locationDropdownSelect.value; if (value) { this.beginAutoBootstrapping(value); } @@ -879,6 +896,15 @@ class AboutTorConnect { RPMAddMessageListener("torconnect:quickstart-change", ({ data }) => { this.updateQuickstart(data); }); + RPMAddMessageListener("torconnect:region-names-change", () => { + // Reset the regions list. + this.regions = null; + if (!this.elements.locationDropdown.hidden) { + // Re-populate immediately. + this.populateDelayedRegionOptions(); + } + // Else, wait until we show the region select to re-populate. + }); }
initKeyboardShortcuts() { @@ -897,7 +923,6 @@ class AboutTorConnect {
// various constants TorStrings = Object.freeze(args.TorStrings); - this.locations = args.CountryNames;
this.initElements(args.Direction); this.initObservers();
===================================== toolkit/modules/RemotePageAccessManager.sys.mjs ===================================== @@ -242,6 +242,7 @@ export let RemotePageAccessManager = { "torconnect:stage-change", "torconnect:bootstrap-progress", "torconnect:quickstart-change", + "torconnect:region-names-change", ], RPMSendAsyncMessage: [ "torconnect:open-tor-preferences", @@ -253,10 +254,7 @@ export let RemotePageAccessManager = { "torconnect:start-again", "torconnect:choose-region", ], - RPMSendQuery: [ - "torconnect:get-init-args", - "torconnect:get-frequent-regions", - ], + RPMSendQuery: ["torconnect:get-init-args", "torconnect:get-regions"], }, "about:welcome": { RPMSendAsyncMessage: ["ActivityStream:ContentToMain"],
===================================== toolkit/modules/TorAndroidIntegration.sys.mjs ===================================== @@ -115,6 +115,10 @@ class TorAndroidIntegrationImpl { stage: subj.wrappedJSObject ?? "", }); break; + case lazy.TorConnectTopics.RegionNamesChange: + // TODO: Respond to change in region names if we are showing a + // TorConnectStage that uses them. + break; case lazy.TorConnectTopics.BootstrapProgress: lazy.EventDispatcher.instance.sendRequest({ type: EmittedEvents.bootstrapProgress, @@ -202,7 +206,7 @@ class TorAndroidIntegrationImpl { lazy.TorConnect.quickstart = data.enabled; break; case ListenedEvents.countryNamesGet: - callback?.onSuccess(lazy.TorConnect.countryNames); + callback?.onSuccess(lazy.TorConnect.getRegionNames()); return; } callback?.onSuccess();
===================================== toolkit/modules/TorConnect.sys.mjs ===================================== @@ -89,6 +89,7 @@ export const TorConnectTopics = Object.freeze({ StateChange: "torconnect:state-change", QuickstartChange: "torconnect:quickstart-change", InternetStatusChange: "torconnect:internet-status-change", + RegionNamesChange: "torconnect:region-names-change", BootstrapProgress: "torconnect:bootstrap-progress", BootstrapComplete: "torconnect:bootstrap-complete", // TODO: Remove torconnect:error when pages have switched to stage. @@ -842,17 +843,13 @@ export const TorConnect = { */ _moatRegionsPromise: null,
- _countryNames: Object.freeze( - (() => { - const codes = Services.intl.getAvailableLocaleDisplayNames("region"); - const names = Services.intl.getRegionDisplayNames(undefined, codes); - let codesNames = {}; - for (let i = 0; i < codes.length; i++) { - codesNames[codes[i]] = names[i]; - } - return codesNames; - })() - ), + /** + * The map of all regions and their localized names. Or `null` when + * uninitialized. + * + * @type {?object} + */ + _regionNames: null,
/** * The status of the most recent bootstrap attempt. @@ -937,6 +934,7 @@ export const TorConnect = { observeTopic(lazy.TorProviderTopics.HasWarnOrErr); observeTopic(lazy.TorSettingsTopics.SettingsChanged); observeTopic(NETWORK_LINK_TOPIC); + observeTopic("intl:app-locales-changed");
this._updateInternetStatus();
@@ -1003,6 +1001,12 @@ export const TorConnect = { case NETWORK_LINK_TOPIC: this._updateInternetStatus(); break; + case "intl:app-locales-changed": + // Unset the regionNames to use the new app locale when requested. + this._regionNames = null; + // Let consumers know that the list of names has changed. + Services.obs.notifyObservers(null, TorConnectTopics.RegionNamesChange); + break; } },
@@ -1148,8 +1152,22 @@ export const TorConnect = { return null; },
- get countryNames() { - return this._countryNames; + /** + * Get a map of all region codes and their localized names. + * + * @returns {object} - The region names in the current locale. + */ + getRegionNames() { + if (!this._regionNames) { + this._regionNames = {}; + const codes = Services.intl.getAvailableLocaleDisplayNames("region"); + // Get the localised names, for the current app locale. + const names = Services.intl.getRegionDisplayNames(undefined, codes); + for (let i = 0; i < codes.length; i++) { + this._regionNames[codes[i]] = names[i]; + } + } + return structuredClone(this._regionNames); },
/**
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/375a8c3...
tbb-commits@lists.torproject.org