experimental.js (5884B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /* import-globals-from preferences.js */ 6 7 ChromeUtils.defineESModuleGetters(this, { 8 ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", 9 FirefoxLabs: "resource://nimbus/FirefoxLabs.sys.mjs", 10 }); 11 12 const gExperimentalPane = { 13 inited: false, 14 _featureGatesContainer: null, 15 _firefoxLabs: null, 16 17 async init() { 18 if (this.inited) { 19 return; 20 } 21 22 this.inited = true; 23 this._featureGatesContainer = document.getElementById( 24 "pane-experimental-featureGates" 25 ); 26 27 this._onCheckboxChanged = this._onCheckboxChanged.bind(this); 28 this._onNimbusUpdate = this._onNimbusUpdate.bind(this); 29 this._resetAllFeatures = this._resetAllFeatures.bind(this); 30 31 setEventListener( 32 "experimentalCategory-reset", 33 "click", 34 this._resetAllFeatures 35 ); 36 37 window.addEventListener("unload", () => this._removeObservers()); 38 39 await this._maybeRenderLabsRecipes(); 40 }, 41 42 async _maybeRenderLabsRecipes() { 43 this._firefoxLabs = await FirefoxLabs.create(); 44 45 const shouldHide = this._firefoxLabs.count === 0; 46 this._setCategoryVisibility(shouldHide); 47 48 if (shouldHide) { 49 return; 50 } 51 52 const frag = document.createDocumentFragment(); 53 54 const groups = new Map(); 55 for (const optIn of this._firefoxLabs.all()) { 56 if (!groups.has(optIn.firefoxLabsGroup)) { 57 groups.set(optIn.firefoxLabsGroup, []); 58 } 59 60 groups.get(optIn.firefoxLabsGroup).push(optIn); 61 } 62 63 for (const [group, optIns] of groups) { 64 const card = document.createElement("moz-card"); 65 card.classList.add("featureGate"); 66 67 const fieldset = document.createElement("moz-fieldset"); 68 document.l10n.setAttributes(fieldset, group); 69 70 card.append(fieldset); 71 72 for (const optIn of optIns) { 73 const checkbox = document.createElement("moz-checkbox"); 74 checkbox.dataset.nimbusSlug = optIn.slug; 75 checkbox.dataset.nimbusBranchSlug = optIn.branches[0].slug; 76 const description = document.createElement("div"); 77 description.slot = "description"; 78 description.id = `${optIn.slug}-description`; 79 description.classList.add("featureGateDescription"); 80 81 for (const [key, value] of Object.entries( 82 optIn.firefoxLabsDescriptionLinks ?? {} 83 )) { 84 const link = document.createElement("a"); 85 link.setAttribute("data-l10n-name", key); 86 link.setAttribute("href", value); 87 link.setAttribute("target", "_blank"); 88 89 description.append(link); 90 } 91 92 document.l10n.setAttributes(description, optIn.firefoxLabsDescription); 93 checkbox.id = optIn.slug; 94 checkbox.setAttribute("aria-describedby", description.id); 95 document.l10n.setAttributes(checkbox, optIn.firefoxLabsTitle); 96 97 checkbox.checked = 98 ExperimentAPI.manager.store.get(optIn.slug)?.active ?? false; 99 checkbox.addEventListener("change", this._onCheckboxChanged); 100 101 checkbox.append(description); 102 fieldset.append(checkbox); 103 } 104 105 frag.append(card); 106 } 107 108 this._featureGatesContainer.appendChild(frag); 109 110 ExperimentAPI.manager.store.on("update", this._onNimbusUpdate); 111 112 Services.obs.notifyObservers(window, "experimental-pane-loaded"); 113 }, 114 115 _removeLabsRecipes() { 116 ExperimentAPI.manager.store.off("update", this._onNimbusUpdate); 117 118 this._featureGatesContainer 119 .querySelectorAll(".featureGate") 120 .forEach(el => el.remove()); 121 }, 122 123 async _onCheckboxChanged(event) { 124 const target = event.target; 125 126 const slug = target.dataset.nimbusSlug; 127 const branchSlug = target.dataset.nimbusBranchSlug; 128 129 const enrolling = !(ExperimentAPI.manager.store.get(slug)?.active ?? false); 130 131 let shouldRestart = false; 132 if (this._firefoxLabs.get(slug).requiresRestart) { 133 const buttonIndex = await confirmRestartPrompt(enrolling, 1, true, false); 134 shouldRestart = buttonIndex === CONFIRM_RESTART_PROMPT_RESTART_NOW; 135 136 if (!shouldRestart) { 137 // The user declined to restart, so we will not enroll in the opt-in. 138 target.checked = false; 139 return; 140 } 141 } 142 143 // Disable the checkbox so that the user cannot interact with it during enrollment. 144 target.disabled = true; 145 146 if (enrolling) { 147 await this._firefoxLabs.enroll(slug, branchSlug); 148 } else { 149 this._firefoxLabs.unenroll(slug); 150 } 151 152 target.disabled = false; 153 154 if (shouldRestart) { 155 Services.startup.quit( 156 Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart 157 ); 158 } 159 }, 160 161 _onNimbusUpdate(_event, { slug, active }) { 162 if (this._firefoxLabs.get(slug)) { 163 document.getElementById(slug).checked = active; 164 } 165 }, 166 167 _removeObservers() { 168 ExperimentAPI.manager.store.off("update", this._onNimbusUpdate); 169 }, 170 171 // Reset the features to their default values 172 async _resetAllFeatures() { 173 for (const optIn of this._firefoxLabs.all()) { 174 const enrolled = 175 (await ExperimentAPI.manager.store.get(optIn.slug)?.active) ?? false; 176 if (enrolled) { 177 this._firefoxLabs.unenroll(optIn.slug); 178 } 179 } 180 }, 181 182 _setCategoryVisibility(shouldHide) { 183 document.getElementById("category-experimental").hidden = shouldHide; 184 185 // Cache the visibility so we can show it quicker in subsequent loads. 186 Services.prefs.setBoolPref( 187 "browser.preferences.experimental.hidden", 188 shouldHide 189 ); 190 191 if ( 192 shouldHide && 193 document.getElementById("categories").selectedItem?.id == 194 "category-experimental" 195 ) { 196 // Leave the 'experimental' category if there are no available features 197 gotoPref("general"); 198 } 199 }, 200 };