AboutPreferences.sys.mjs (11877B)
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 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { 6 actionTypes as at, 7 actionCreators as ac, 8 } from "resource://newtab/common/Actions.mjs"; 9 10 export const PREFERENCES_LOADED_EVENT = "home-pane-loaded"; 11 12 // These "section" objects are formatted in a way to be similar to the ones from 13 // SectionsManager to construct the preferences view. 14 const PREFS_FOR_SETTINGS = () => [ 15 { 16 id: "web-search", 17 pref: { 18 feed: "showSearch", 19 titleString: "home-prefs-search-header", 20 }, 21 }, 22 { 23 id: "weather", 24 pref: { 25 feed: "showWeather", 26 titleString: "home-prefs-weather-header", 27 descString: "home-prefs-weather-description", 28 learnMore: { 29 link: { 30 href: "https://support.mozilla.org/kb/customize-items-on-firefox-new-tab-page", 31 id: "home-prefs-weather-learn-more-link", 32 }, 33 }, 34 }, 35 eventSource: "WEATHER", 36 shouldHidePref: !Services.prefs.getBoolPref( 37 "browser.newtabpage.activity-stream.system.showWeather", 38 false 39 ), 40 }, 41 { 42 id: "topsites", 43 pref: { 44 feed: "feeds.topsites", 45 titleString: "home-prefs-shortcuts-header", 46 descString: "home-prefs-shortcuts-description", 47 }, 48 maxRows: 4, 49 rowsPref: "topSitesRows", 50 eventSource: "TOP_SITES", 51 }, 52 { 53 id: "topstories", 54 pref: { 55 feed: "feeds.section.topstories", 56 titleString: { 57 id: "home-prefs-recommended-by-header-generic", 58 }, 59 descString: { 60 id: "home-prefs-recommended-by-description-generic", 61 }, 62 }, 63 shouldHidePref: !Services.prefs.getBoolPref( 64 "browser.newtabpage.activity-stream.feeds.system.topstories", 65 true 66 ), 67 eventSource: "TOP_STORIES", 68 }, 69 { 70 id: "support-firefox", 71 pref: { 72 feed: "showSponsoredCheckboxes", 73 titleString: "home-prefs-support-firefox-header", 74 nestedPrefs: [ 75 { 76 name: "showSponsoredTopSites", 77 titleString: "home-prefs-shortcuts-by-option-sponsored", 78 eventSource: "SPONSORED_TOP_SITES", 79 }, 80 { 81 name: "showSponsored", 82 titleString: "home-prefs-recommended-by-option-sponsored-stories", 83 eventSource: "POCKET_SPOCS", 84 shouldHidePref: !Services.prefs.getBoolPref( 85 "browser.newtabpage.activity-stream.feeds.system.topstories", 86 true 87 ), 88 shouldDisablePref: !Services.prefs.getBoolPref( 89 "browser.newtabpage.activity-stream.feeds.section.topstories", 90 true 91 ), 92 }, 93 ], 94 }, 95 }, 96 ]; 97 98 export class AboutPreferences { 99 init() { 100 Services.obs.addObserver(this, PREFERENCES_LOADED_EVENT); 101 } 102 103 uninit() { 104 Services.obs.removeObserver(this, PREFERENCES_LOADED_EVENT); 105 } 106 107 onAction(action) { 108 switch (action.type) { 109 case at.INIT: 110 this.init(); 111 break; 112 case at.UNINIT: 113 this.uninit(); 114 break; 115 case at.SETTINGS_OPEN: 116 action._target.browser.ownerGlobal.openPreferences("paneHome"); 117 break; 118 // This is used to open the web extension settings page for an extension 119 case at.OPEN_WEBEXT_SETTINGS: 120 action._target.browser.ownerGlobal.BrowserAddonUI.openAddonsMgr( 121 `addons://detail/${encodeURIComponent(action.data)}` 122 ); 123 break; 124 } 125 } 126 127 setupUserEvent(element, eventSource) { 128 element.addEventListener("command", e => { 129 const { checked } = e.target; 130 if (typeof checked === "boolean") { 131 this.store.dispatch( 132 ac.UserEvent({ 133 event: "PREF_CHANGED", 134 source: eventSource, 135 value: { status: checked, menu_source: "ABOUT_PREFERENCES" }, 136 }) 137 ); 138 } 139 }); 140 } 141 142 observe(window) { 143 const { document, Preferences } = window; 144 145 // Extract just the "Recent activity" pref info from SectionsManager as we have everything else already 146 const highlights = this.store 147 .getState() 148 .Sections.find(el => el.id === "highlights"); 149 150 const allSections = [...PREFS_FOR_SETTINGS(), highlights]; 151 152 // Render the preferences 153 allSections.forEach(pref => { 154 this.renderPreferenceSection(pref, document, Preferences); 155 }); 156 157 // Update the visibility of the Restore Defaults button based on checked prefs 158 this.toggleRestoreDefaults(window.gHomePane); 159 } 160 161 /** 162 * Render a single preference with all the details, e.g. description, links, 163 * more granular preferences. 164 * 165 * @param sectionData 166 * @param document 167 * @param Preferences 168 */ 169 170 /** 171 * We can remove this eslint exception once the Settings redesign is complete. 172 * In fact, we can probably remove this entire method. 173 */ 174 // eslint-disable-next-line max-statements 175 renderPreferenceSection(sectionData, document, Preferences) { 176 /* Do not render old-style settings if new settings UI is enabled - this is needed to avoid 177 * registering prefs twice and ensuing errors */ 178 if (Services.prefs.getBoolPref("browser.settings-redesign.enabled")) { 179 return; 180 } 181 182 const { 183 id, 184 pref: prefData, 185 maxRows, 186 rowsPref, 187 shouldHidePref, 188 eventSource, 189 } = sectionData; 190 const { 191 feed: name, 192 titleString = {}, 193 descString, 194 nestedPrefs = [], 195 } = prefData || {}; 196 197 // Helper to link a UI element to a preference for updating 198 const linkPref = (element, prefName, type) => { 199 const fullPref = `browser.newtabpage.activity-stream.${prefName}`; 200 element.setAttribute("preference", fullPref); 201 Preferences.add({ id: fullPref, type }); 202 203 // Prevent changing the UI if the preference can't be changed 204 element.disabled = Preferences.get(fullPref).locked; 205 }; 206 207 // Don't show any sections that we don't want to expose in preferences UI 208 if (shouldHidePref) { 209 return; 210 } 211 212 // Add the main preference for turning on/off a section 213 const sectionVbox = document.getElementById(id); 214 sectionVbox.setAttribute("data-subcategory", id); 215 const checkbox = this.createAppend(document, "checkbox", sectionVbox); 216 checkbox.classList.add("section-checkbox"); 217 // Set up a user event if we have an event source for this pref. 218 if (eventSource) { 219 this.setupUserEvent(checkbox, eventSource); 220 } 221 document.l10n.setAttributes( 222 checkbox, 223 this.getString(titleString), 224 titleString.values 225 ); 226 227 linkPref(checkbox, name, "bool"); 228 229 // Specially add a link for Weather 230 if (id === "weather") { 231 const hboxWithLink = this.createAppend(document, "hbox", sectionVbox); 232 hboxWithLink.appendChild(checkbox); 233 checkbox.classList.add("tail-with-learn-more"); 234 235 const link = this.createAppend(document, "label", hboxWithLink, { 236 is: "text-link", 237 }); 238 link.setAttribute("href", sectionData.pref.learnMore.link.href); 239 document.l10n.setAttributes(link, sectionData.pref.learnMore.link.id); 240 } 241 242 // Add more details for the section (e.g., description, more prefs) 243 const detailVbox = this.createAppend(document, "vbox", sectionVbox); 244 detailVbox.classList.add("indent"); 245 if (descString) { 246 const description = this.createAppend( 247 document, 248 "description", 249 detailVbox 250 ); 251 description.classList.add("text-deemphasized"); 252 document.l10n.setAttributes( 253 description, 254 this.getString(descString), 255 descString.values 256 ); 257 258 // Add a rows dropdown if we have a pref to control and a maximum 259 if (rowsPref && maxRows) { 260 const detailHbox = this.createAppend(document, "hbox", detailVbox); 261 detailHbox.setAttribute("align", "center"); 262 description.setAttribute("flex", 1); 263 detailHbox.appendChild(description); 264 265 // Add box so the search tooltip is positioned correctly 266 const tooltipBox = this.createAppend(document, "hbox", detailHbox); 267 268 // Add appropriate number of localized entries to the dropdown 269 const menulist = this.createAppend(document, "menulist", tooltipBox); 270 menulist.setAttribute("crop", "none"); 271 const menupopup = this.createAppend(document, "menupopup", menulist); 272 for (let num = 1; num <= maxRows; num++) { 273 const item = this.createAppend(document, "menuitem", menupopup); 274 document.l10n.setAttributes(item, "home-prefs-sections-rows-option", { 275 num, 276 }); 277 item.setAttribute("value", num); 278 } 279 linkPref(menulist, rowsPref, "int"); 280 } 281 } 282 283 const subChecks = []; 284 const fullName = `browser.newtabpage.activity-stream.${sectionData.pref.feed}`; 285 const pref = Preferences.get(fullName); 286 287 // Add a checkbox pref for any nested preferences 288 nestedPrefs.forEach(nested => { 289 if (nested.shouldHidePref !== true) { 290 const subcheck = this.createAppend(document, "checkbox", detailVbox); 291 // Set up a user event if we have an event source for this pref. 292 if (nested.eventSource) { 293 this.setupUserEvent(subcheck, nested.eventSource); 294 } 295 document.l10n.setAttributes(subcheck, nested.titleString); 296 297 linkPref(subcheck, nested.name, "bool"); 298 299 subChecks.push(subcheck); 300 subcheck.disabled = !pref._value; 301 if (nested.shouldDisablePref) { 302 subcheck.disabled = nested.shouldDisablePref; 303 } 304 subcheck.hidden = nested.hidden; 305 } 306 }); 307 308 // Special cases to like the nested prefs with another pref, 309 // so we can disable it real time. 310 if (id === "support-firefox") { 311 function setupSupportFirefoxSubCheck(triggerPref, subPref) { 312 const subCheckFullName = `browser.newtabpage.activity-stream.${triggerPref}`; 313 const subCheckPref = Preferences.get(subCheckFullName); 314 315 subCheckPref?.on("change", () => { 316 const showSponsoredFullName = `browser.newtabpage.activity-stream.${subPref}`; 317 const showSponsoredSubcheck = subChecks.find( 318 subcheck => 319 subcheck.getAttribute("preference") === showSponsoredFullName 320 ); 321 if (showSponsoredSubcheck) { 322 showSponsoredSubcheck.disabled = !Services.prefs.getBoolPref( 323 subCheckFullName, 324 true 325 ); 326 } 327 }); 328 } 329 330 setupSupportFirefoxSubCheck("feeds.section.topstories", "showSponsored"); 331 setupSupportFirefoxSubCheck("feeds.topsites", "showSponsoredTopSites"); 332 } 333 334 pref.on("change", () => { 335 subChecks.forEach(subcheck => { 336 // Update child preferences for the "Support Firefox" checkbox group 337 // so that they're turned on and off at the same time. 338 if (id === "support-firefox") { 339 const subPref = Preferences.get(subcheck.getAttribute("preference")); 340 subPref.value = pref.value; 341 } 342 343 // Disable any nested checkboxes if the parent pref is not enabled. 344 subcheck.disabled = !pref._value; 345 }); 346 }); 347 } 348 349 /** 350 * Update the visibility of the Restore Defaults button based on checked prefs. 351 * 352 * @param gHomePane 353 */ 354 toggleRestoreDefaults(gHomePane) { 355 gHomePane.toggleRestoreDefaultsBtn(); 356 } 357 358 /** 359 * A helper function to append XUL elements on the page. 360 * 361 * @param document 362 * @param tag 363 * @param parent 364 * @param options 365 */ 366 createAppend(document, tag, parent, options = {}) { 367 return parent.appendChild(document.createXULElement(tag, options)); 368 } 369 370 /** 371 * Helper to get fluentIDs sometimes encase in an object 372 * 373 * @param message 374 * @returns string 375 */ 376 getString(message) { 377 return typeof message !== "object" ? message : message.id; 378 } 379 }