| ... |
... |
@@ -70,7 +70,7 @@ class AboutTorConnect { |
|
70
|
70
|
tryBridge: "button#tryBridgeButton",
|
|
71
|
71
|
locationDropdownLabel: "#locationDropdownLabel",
|
|
72
|
72
|
locationDropdown: "#locationDropdown",
|
|
73
|
|
- locationDropdownSelect: "#locationDropdown select",
|
|
|
73
|
+ locationDropdownSelect: "#regions-select",
|
|
74
|
74
|
},
|
|
75
|
75
|
});
|
|
76
|
76
|
|
| ... |
... |
@@ -129,13 +129,38 @@ class AboutTorConnect { |
|
129
|
129
|
locationDropdownSelect: document.querySelector(
|
|
130
|
130
|
this.selectors.buttons.locationDropdownSelect
|
|
131
|
131
|
),
|
|
|
132
|
+ firstRegionOption: document.getElementById("first-region-option"),
|
|
|
133
|
+ frequentRegionsOptionGroup: document.getElementById(
|
|
|
134
|
+ "frequent-regions-option-group"
|
|
|
135
|
+ ),
|
|
|
136
|
+ fullRegionsOptionGroup: document.getElementById(
|
|
|
137
|
+ "full-regions-option-group"
|
|
|
138
|
+ ),
|
|
132
|
139
|
tryBridgeButton: document.querySelector(this.selectors.buttons.tryBridge),
|
|
133
|
140
|
});
|
|
134
|
141
|
|
|
135
|
|
- selectedLocation;
|
|
|
142
|
+ /**
|
|
|
143
|
+ * The currently shown stage, or `null` if the page in uninitialised.
|
|
|
144
|
+ *
|
|
|
145
|
+ * @type {?string}
|
|
|
146
|
+ */
|
|
136
|
147
|
shownStage = null;
|
|
137
|
148
|
|
|
138
|
|
- locations = {};
|
|
|
149
|
+ /**
|
|
|
150
|
+ * A promise that resolves to a list of region names and frequent regions, or
|
|
|
151
|
+ * `null` if this needs to be re-fetched from the TorConnectParent.
|
|
|
152
|
+ *
|
|
|
153
|
+ * @type {?Promise<object>}
|
|
|
154
|
+ */
|
|
|
155
|
+ regions = null;
|
|
|
156
|
+
|
|
|
157
|
+ /**
|
|
|
158
|
+ * The option value that *should* be selected when the list of regions is
|
|
|
159
|
+ * populated.
|
|
|
160
|
+ *
|
|
|
161
|
+ * @type {string}
|
|
|
162
|
+ */
|
|
|
163
|
+ selectedRegion = "";
|
|
139
|
164
|
|
|
140
|
165
|
/**
|
|
141
|
166
|
* Whether the user requested a cancellation of the bootstrap from *this*
|
| ... |
... |
@@ -201,89 +226,6 @@ class AboutTorConnect { |
|
201
|
226
|
this.hide(this.elements.tryBridgeButton);
|
|
202
|
227
|
}
|
|
203
|
228
|
|
|
204
|
|
- populateLocations() {
|
|
205
|
|
- const selectCountryRegion = document.createElement("option");
|
|
206
|
|
- selectCountryRegion.textContent = TorStrings.torConnect.selectCountryRegion;
|
|
207
|
|
- selectCountryRegion.value = "";
|
|
208
|
|
-
|
|
209
|
|
- // get all codes and names from TorStrings
|
|
210
|
|
- const locationNodes = [];
|
|
211
|
|
- for (const [code, name] of Object.entries(this.locations)) {
|
|
212
|
|
- let option = document.createElement("option");
|
|
213
|
|
- option.value = code;
|
|
214
|
|
- option.textContent = name;
|
|
215
|
|
- locationNodes.push(option);
|
|
216
|
|
- }
|
|
217
|
|
- // locale sort by name
|
|
218
|
|
- locationNodes.sort((left, right) =>
|
|
219
|
|
- left.textContent.localeCompare(right.textContent)
|
|
220
|
|
- );
|
|
221
|
|
- this.elements.locationDropdownSelect.append(
|
|
222
|
|
- selectCountryRegion,
|
|
223
|
|
- ...locationNodes
|
|
224
|
|
- );
|
|
225
|
|
- }
|
|
226
|
|
-
|
|
227
|
|
- populateFrequentLocations(locations) {
|
|
228
|
|
- this.removeFrequentLocations();
|
|
229
|
|
- if (!locations || !locations.length) {
|
|
230
|
|
- return;
|
|
231
|
|
- }
|
|
232
|
|
-
|
|
233
|
|
- const locationNodes = [];
|
|
234
|
|
- for (const code of locations) {
|
|
235
|
|
- const option = document.createElement("option");
|
|
236
|
|
- option.value = code;
|
|
237
|
|
- option.className = "frequent-location";
|
|
238
|
|
- // codes (partially) come from rdsys service, so make sure we have a
|
|
239
|
|
- // string defined for it
|
|
240
|
|
- let name = this.locations[code];
|
|
241
|
|
- if (!name) {
|
|
242
|
|
- name = code;
|
|
243
|
|
- }
|
|
244
|
|
- option.textContent = name;
|
|
245
|
|
- locationNodes.push(option);
|
|
246
|
|
- }
|
|
247
|
|
- // locale sort by name
|
|
248
|
|
- locationNodes.sort((left, right) =>
|
|
249
|
|
- left.textContent.localeCompare(right.textContent)
|
|
250
|
|
- );
|
|
251
|
|
-
|
|
252
|
|
- const frequentGroup = document.createElement("optgroup");
|
|
253
|
|
- frequentGroup.setAttribute(
|
|
254
|
|
- "label",
|
|
255
|
|
- TorStrings.torConnect.frequentLocations
|
|
256
|
|
- );
|
|
257
|
|
- frequentGroup.className = "frequent-location";
|
|
258
|
|
- const locationGroup = document.createElement("optgroup");
|
|
259
|
|
- locationGroup.setAttribute("label", TorStrings.torConnect.otherLocations);
|
|
260
|
|
- locationGroup.className = "frequent-location";
|
|
261
|
|
- // options[0] is either "Select Country or Region" or "Automatic"
|
|
262
|
|
- this.elements.locationDropdownSelect.options[0].after(
|
|
263
|
|
- frequentGroup,
|
|
264
|
|
- ...locationNodes,
|
|
265
|
|
- locationGroup
|
|
266
|
|
- );
|
|
267
|
|
- }
|
|
268
|
|
-
|
|
269
|
|
- removeFrequentLocations() {
|
|
270
|
|
- const select = this.elements.locationDropdownSelect;
|
|
271
|
|
- for (const option of select.querySelectorAll(".frequent-location")) {
|
|
272
|
|
- option.remove();
|
|
273
|
|
- }
|
|
274
|
|
- }
|
|
275
|
|
-
|
|
276
|
|
- validateLocation() {
|
|
277
|
|
- const selectedIndex = this.elements.locationDropdownSelect.selectedIndex;
|
|
278
|
|
- const selectedOption =
|
|
279
|
|
- this.elements.locationDropdownSelect.options[selectedIndex];
|
|
280
|
|
- if (!selectedOption.value) {
|
|
281
|
|
- this.elements.tryBridgeButton.setAttribute("disabled", "disabled");
|
|
282
|
|
- } else {
|
|
283
|
|
- this.elements.tryBridgeButton.removeAttribute("disabled");
|
|
284
|
|
- }
|
|
285
|
|
- }
|
|
286
|
|
-
|
|
287
|
229
|
setTitle(title, className) {
|
|
288
|
230
|
this.elements.heading.textContent = title;
|
|
289
|
231
|
this.elements.title.className = "title";
|
| ... |
... |
@@ -407,7 +349,9 @@ class AboutTorConnect { |
|
407
|
349
|
|
|
408
|
350
|
const prevStage = this.shownStage;
|
|
409
|
351
|
this.shownStage = stage.name;
|
|
410
|
|
- this.selectedLocation = stage.defaultRegion;
|
|
|
352
|
+ // Make a request to change the selected region in the next call to
|
|
|
353
|
+ // selectRegionOption.
|
|
|
354
|
+ this.selectedRegion = stage.defaultRegion;
|
|
411
|
355
|
|
|
412
|
356
|
// By default we want to reset the focus to the top of the page when
|
|
413
|
357
|
// changing the displayed page since we want a user to read the new page
|
| ... |
... |
@@ -708,24 +652,105 @@ class AboutTorConnect { |
|
708
|
652
|
}
|
|
709
|
653
|
}
|
|
710
|
654
|
|
|
|
655
|
+ /**
|
|
|
656
|
+ * Try and select the region specified in `selectedRegion`.
|
|
|
657
|
+ */
|
|
|
658
|
+ selectRegionOption() {
|
|
|
659
|
+ // NOTE: If the region appears in both the frequent list and the full list,
|
|
|
660
|
+ // then this will select the region option in
|
|
|
661
|
+ // frequentRegionsOptionGroup, even if the user had prior selected the
|
|
|
662
|
+ // option from fullRegionsOptionGroup. But the overall value should be the
|
|
|
663
|
+ // same.
|
|
|
664
|
+ this.elements.locationDropdownSelect.value = this.selectedRegion;
|
|
|
665
|
+ if (this.elements.locationDropdownSelect.selectedIndex === -1) {
|
|
|
666
|
+ // Select the first, as a fallback. E.g. in RegionNotFound the
|
|
|
667
|
+ // selectedRegion may still be "automatic", but this is no longer
|
|
|
668
|
+ // available.
|
|
|
669
|
+ this.elements.locationDropdownSelect.selectedIndex = 0;
|
|
|
670
|
+ }
|
|
|
671
|
+ this.validateRegion();
|
|
|
672
|
+ }
|
|
|
673
|
+
|
|
|
674
|
+ /**
|
|
|
675
|
+ * Ensure that the current selected region is valid for the shown stage.
|
|
|
676
|
+ */
|
|
|
677
|
+ validateRegion() {
|
|
|
678
|
+ this.elements.tryBridgeButton.toggleAttribute(
|
|
|
679
|
+ "disabled",
|
|
|
680
|
+ !this.elements.locationDropdownSelect.value
|
|
|
681
|
+ );
|
|
|
682
|
+ }
|
|
|
683
|
+
|
|
|
684
|
+ /**
|
|
|
685
|
+ * Populate the full list of regions, if necessary.
|
|
|
686
|
+ */
|
|
|
687
|
+ async populateDelayedRegionOptions() {
|
|
|
688
|
+ if (this.regions) {
|
|
|
689
|
+ // Already populated, or about to populate.
|
|
|
690
|
+ return;
|
|
|
691
|
+ }
|
|
|
692
|
+
|
|
|
693
|
+ this.regions = RPMSendQuery("torconnect:get-regions");
|
|
|
694
|
+ const regions = this.regions;
|
|
|
695
|
+ const { names, frequent } = await regions;
|
|
|
696
|
+
|
|
|
697
|
+ if (regions !== this.regions) {
|
|
|
698
|
+ // Replaced by a new call.
|
|
|
699
|
+ return;
|
|
|
700
|
+ }
|
|
|
701
|
+
|
|
|
702
|
+ this.setRegionOptions(
|
|
|
703
|
+ this.elements.frequentRegionsOptionGroup,
|
|
|
704
|
+ frequent.map(code => [code, names[code]])
|
|
|
705
|
+ );
|
|
|
706
|
+
|
|
|
707
|
+ this.setRegionOptions(
|
|
|
708
|
+ this.elements.fullRegionsOptionGroup,
|
|
|
709
|
+ Object.entries(names)
|
|
|
710
|
+ );
|
|
|
711
|
+
|
|
|
712
|
+ // Now that the list has been re-populated we want to re-select the
|
|
|
713
|
+ // requested region.
|
|
|
714
|
+ this.selectRegionOption();
|
|
|
715
|
+ }
|
|
|
716
|
+
|
|
|
717
|
+ /**
|
|
|
718
|
+ * Set the shown region options.
|
|
|
719
|
+ *
|
|
|
720
|
+ * @param {HTMLOptGroupElement} group - The group to set the children of.
|
|
|
721
|
+ * @param {[string, string|undefined][]} regions - The list of region
|
|
|
722
|
+ * key-value entries to fill the group with. The key is the region code and
|
|
|
723
|
+ * the value is the region's localised name.
|
|
|
724
|
+ */
|
|
|
725
|
+ setRegionOptions(group, regions) {
|
|
|
726
|
+ const regionNodes = regions
|
|
|
727
|
+ .sort(([_code1, name1], [_code2, name2]) => name1.localeCompare(name2))
|
|
|
728
|
+ .map(([code, name]) => {
|
|
|
729
|
+ const option = document.createElement("option");
|
|
|
730
|
+ option.value = code;
|
|
|
731
|
+ // If the name is unexpectedly empty or undefined we use the code
|
|
|
732
|
+ // instead.
|
|
|
733
|
+ option.textContent = name || code;
|
|
|
734
|
+ return option;
|
|
|
735
|
+ });
|
|
|
736
|
+ group.replaceChildren(...regionNodes);
|
|
|
737
|
+ }
|
|
|
738
|
+
|
|
711
|
739
|
showLocationForm(isChoose, buttonLabel) {
|
|
712
|
740
|
this.hideButtons();
|
|
713
|
|
- RPMSendQuery("torconnect:get-frequent-regions").then(codes => {
|
|
714
|
|
- if (codes && codes.length) {
|
|
715
|
|
- this.populateFrequentLocations(codes);
|
|
716
|
|
- this.setLocation();
|
|
717
|
|
- }
|
|
718
|
|
- });
|
|
719
|
|
- let firstOpt = this.elements.locationDropdownSelect.options[0];
|
|
720
|
|
- if (isChoose) {
|
|
721
|
|
- firstOpt.value = "automatic";
|
|
722
|
|
- firstOpt.textContent = TorStrings.torConnect.automatic;
|
|
723
|
|
- } else {
|
|
724
|
|
- firstOpt.value = "";
|
|
725
|
|
- firstOpt.textContent = TorStrings.torConnect.selectCountryRegion;
|
|
726
|
|
- }
|
|
727
|
|
- this.setLocation();
|
|
728
|
|
- this.validateLocation();
|
|
|
741
|
+
|
|
|
742
|
+ this.elements.firstRegionOption.textContent = isChoose
|
|
|
743
|
+ ? TorStrings.torConnect.automatic
|
|
|
744
|
+ : TorStrings.torConnect.selectCountryRegion;
|
|
|
745
|
+ this.elements.firstRegionOption.value = isChoose ? "automatic" : "";
|
|
|
746
|
+
|
|
|
747
|
+ // Try and select the region now, prior to waiting for
|
|
|
748
|
+ // populateDelayedRegionOptions.
|
|
|
749
|
+ this.selectRegionOption();
|
|
|
750
|
+
|
|
|
751
|
+ // Async fill the rest of the region options, if needed.
|
|
|
752
|
+ this.populateDelayedRegionOptions();
|
|
|
753
|
+
|
|
729
|
754
|
this.show(this.elements.locationDropdownLabel);
|
|
730
|
755
|
this.show(this.elements.locationDropdown);
|
|
731
|
756
|
this.elements.locationDropdownLabel.classList.toggle("error", !isChoose);
|
| ... |
... |
@@ -735,29 +760,6 @@ class AboutTorConnect { |
|
735
|
760
|
}
|
|
736
|
761
|
}
|
|
737
|
762
|
|
|
738
|
|
- getLocation() {
|
|
739
|
|
- const selectedIndex = this.elements.locationDropdownSelect.selectedIndex;
|
|
740
|
|
- return this.elements.locationDropdownSelect.options[selectedIndex].value;
|
|
741
|
|
- }
|
|
742
|
|
-
|
|
743
|
|
- setLocation() {
|
|
744
|
|
- const code = this.selectedLocation;
|
|
745
|
|
- if (this.getLocation() === code) {
|
|
746
|
|
- return;
|
|
747
|
|
- }
|
|
748
|
|
- const options = this.elements.locationDropdownSelect.options;
|
|
749
|
|
- // We need to do this way, because we have repeated values that break
|
|
750
|
|
- // the .value way to select (which would however require the label,
|
|
751
|
|
- // rather than the code)...
|
|
752
|
|
- for (let i = 0; i < options.length; i++) {
|
|
753
|
|
- if (options[i].value === code) {
|
|
754
|
|
- this.elements.locationDropdownSelect.selectedIndex = i;
|
|
755
|
|
- break;
|
|
756
|
|
- }
|
|
757
|
|
- }
|
|
758
|
|
- this.validateLocation();
|
|
759
|
|
- }
|
|
760
|
|
-
|
|
761
|
763
|
initElements(direction) {
|
|
762
|
764
|
const isAndroid = navigator.userAgent.includes("Android");
|
|
763
|
765
|
document.body.classList.toggle("android", isAndroid);
|
| ... |
... |
@@ -826,17 +828,32 @@ class AboutTorConnect { |
|
826
|
828
|
this.beginBootstrapping(this.shownStage === "Start");
|
|
827
|
829
|
});
|
|
828
|
830
|
|
|
829
|
|
- this.populateLocations();
|
|
830
|
831
|
this.elements.locationDropdownSelect.addEventListener("change", () => {
|
|
831
|
|
- this.validateLocation();
|
|
|
832
|
+ // Overwrite the stage requested selectedRegion.
|
|
|
833
|
+ // NOTE: This should not fire in response to a programmatic change in
|
|
|
834
|
+ // value.
|
|
|
835
|
+ // E.g. if the user selects a region, then changes locale, we want the
|
|
|
836
|
+ // same region to be re-selected after the option list is rebuilt.
|
|
|
837
|
+ this.selectedRegion = this.elements.locationDropdownSelect.value;
|
|
|
838
|
+
|
|
|
839
|
+ this.validateRegion();
|
|
832
|
840
|
});
|
|
833
|
841
|
|
|
834
|
842
|
this.elements.locationDropdownLabel.textContent =
|
|
835
|
843
|
TorStrings.torConnect.unblockInternetIn;
|
|
836
|
844
|
|
|
|
845
|
+ this.elements.frequentRegionsOptionGroup.setAttribute(
|
|
|
846
|
+ "label",
|
|
|
847
|
+ TorStrings.torConnect.frequentLocations
|
|
|
848
|
+ );
|
|
|
849
|
+ this.elements.fullRegionsOptionGroup.setAttribute(
|
|
|
850
|
+ "label",
|
|
|
851
|
+ TorStrings.torConnect.otherLocations
|
|
|
852
|
+ );
|
|
|
853
|
+
|
|
837
|
854
|
this.elements.tryBridgeButton.textContent = TorStrings.torConnect.tryBridge;
|
|
838
|
855
|
this.elements.tryBridgeButton.addEventListener("click", () => {
|
|
839
|
|
- const value = this.getLocation();
|
|
|
856
|
+ const value = this.elements.locationDropdownSelect.value;
|
|
840
|
857
|
if (value) {
|
|
841
|
858
|
this.beginAutoBootstrapping(value);
|
|
842
|
859
|
}
|
| ... |
... |
@@ -879,6 +896,15 @@ class AboutTorConnect { |
|
879
|
896
|
RPMAddMessageListener("torconnect:quickstart-change", ({ data }) => {
|
|
880
|
897
|
this.updateQuickstart(data);
|
|
881
|
898
|
});
|
|
|
899
|
+ RPMAddMessageListener("torconnect:region-names-change", () => {
|
|
|
900
|
+ // Reset the regions list.
|
|
|
901
|
+ this.regions = null;
|
|
|
902
|
+ if (!this.elements.locationDropdown.hidden) {
|
|
|
903
|
+ // Re-populate immediately.
|
|
|
904
|
+ this.populateDelayedRegionOptions();
|
|
|
905
|
+ }
|
|
|
906
|
+ // Else, wait until we show the region select to re-populate.
|
|
|
907
|
+ });
|
|
882
|
908
|
}
|
|
883
|
909
|
|
|
884
|
910
|
initKeyboardShortcuts() {
|
| ... |
... |
@@ -897,7 +923,6 @@ class AboutTorConnect { |
|
897
|
923
|
|
|
898
|
924
|
// various constants
|
|
899
|
925
|
TorStrings = Object.freeze(args.TorStrings);
|
|
900
|
|
- this.locations = args.CountryNames;
|
|
901
|
926
|
|
|
902
|
927
|
this.initElements(args.Direction);
|
|
903
|
928
|
this.initObservers();
|