SidebarManager.sys.mjs (12526B)
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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 7 8 const BACKUP_STATE_PREF = "sidebar.backupState"; 9 const VISIBILITY_SETTING_PREF = "sidebar.visibility"; 10 const SIDEBAR_TOOLS = "sidebar.main.tools"; 11 const VERTICAL_TABS_PREF = "sidebar.verticalTabs"; 12 const INSTALLED_EXTENSIONS = "sidebar.installed.extensions"; 13 const PINNED_PROMO_PREF = "sidebar.verticalTabs.dragToPinPromo.dismissed"; 14 15 // New panels that are ready to be introduced to new sidebar users should be added to this list; 16 // ensure your feature flag is enabled at the same time you do this and that its the same value as 17 // what you added to . 18 const DEFAULT_LAUNCHER_TOOLS = "aichat,syncedtabs,history,bookmarks"; 19 const lazy = {}; 20 ChromeUtils.defineESModuleGetters(lazy, { 21 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 22 CustomizableUI: 23 "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs", 24 NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", 25 PrefUtils: "moz-src:///toolkit/modules/PrefUtils.sys.mjs", 26 SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", 27 SidebarState: "moz-src:///browser/components/sidebar/SidebarState.sys.mjs", 28 }); 29 XPCOMUtils.defineLazyPreferenceGetter(lazy, "sidebarNimbus", "sidebar.nimbus"); 30 31 XPCOMUtils.defineLazyPreferenceGetter( 32 lazy, 33 "sidebarBackupState", 34 BACKUP_STATE_PREF 35 ); 36 37 XPCOMUtils.defineLazyPreferenceGetter( 38 lazy, 39 "verticalTabsEnabled", 40 VERTICAL_TABS_PREF, 41 false, 42 (pref, oldVal, newVal) => { 43 sidebarManager.handleVerticalTabsPrefChange(newVal, true); 44 } 45 ); 46 47 XPCOMUtils.defineLazyPreferenceGetter( 48 lazy, 49 "sidebarRevampEnabled", 50 "sidebar.revamp", 51 false, 52 (pref, oldVal, newVal) => { 53 sidebarManager.updateDefaultTools(); 54 55 if (!newVal) { 56 // Disable vertical tabs if revamped sidebar is turned off 57 Services.prefs.setBoolPref("sidebar.verticalTabs", false); 58 } else if (newVal && !lazy.verticalTabsEnabled) { 59 // horizontal tabs with sidebar.revamp must have visibility of "hide-sidebar" 60 Services.prefs.setStringPref(VISIBILITY_SETTING_PREF, "hide-sidebar"); 61 } 62 } 63 ); 64 65 XPCOMUtils.defineLazyPreferenceGetter(lazy, "sidebarTools", SIDEBAR_TOOLS, ""); 66 XPCOMUtils.defineLazyPreferenceGetter( 67 lazy, 68 "sidebarExtensions", 69 INSTALLED_EXTENSIONS, 70 "" 71 ); 72 73 XPCOMUtils.defineLazyPreferenceGetter( 74 lazy, 75 "newSidebarHasBeenUsed", 76 "sidebar.new-sidebar.has-used", 77 false, 78 () => sidebarManager.updateDefaultTools() 79 ); 80 81 XPCOMUtils.defineLazyPreferenceGetter( 82 lazy, 83 "dragToPinPromoDismissed", 84 PINNED_PROMO_PREF, 85 false 86 ); 87 88 class SidebarManager extends EventTarget { 89 /** 90 * SidebarManager is a singleton that handles startup tasks like telemetry, 91 * adding listeners and updating sidebar-related preferences. 92 */ 93 constructor() { 94 super(); 95 this.checkForPinnedTabsComplete = false; 96 } 97 #initialized = false; 98 init() { 99 lazy.CustomizableUI.addListener(this); 100 101 Services.prefs.addObserver( 102 "sidebar.newTool.migration.", 103 this.updateDefaultTools.bind(this) 104 ); 105 this.updateDefaultTools(); 106 lazy.SessionStore.promiseAllWindowsRestored.then(() => { 107 this.checkForPinnedTabs(); 108 }); 109 110 // if there's no user visibility pref, we may need to update it to the default value for the tab orientation 111 const shouldResetVisibility = !Services.prefs.prefHasUserValue( 112 VISIBILITY_SETTING_PREF 113 ); 114 this.handleVerticalTabsPrefChange( 115 lazy.verticalTabsEnabled, 116 shouldResetVisibility 117 ); 118 119 // Handle nimbus feature pref setting updates on init and enrollment 120 lazy.NimbusFeatures.sidebar.onUpdate(() => { 121 if (this.#initialized) { 122 this.onNimbusFeatureUpdate(); 123 } else { 124 // Schedule handling the update after this module has finished initializing 125 Promise.resolve().then(() => this.onNimbusFeatureUpdate()); 126 } 127 }); 128 this.#initialized = true; 129 } 130 131 onNimbusFeatureUpdate() { 132 const featureId = "sidebar"; 133 // Set prefs only if we have an enrollment that's new 134 const enrollment = lazy.NimbusFeatures[featureId].getEnrollmentMetadata(); 135 if (!enrollment) { 136 return; 137 } 138 const slug = enrollment.slug + ":" + enrollment.branch; 139 if (slug == lazy.sidebarNimbus) { 140 return; 141 } 142 143 // Enforce minimum version by skipping pref changes until Firefox restarts 144 // with the appropriate version 145 if ( 146 Services.vc.compare( 147 // Support betas, e.g., 132.0b1, instead of MOZ_APP_VERSION 148 AppConstants.MOZ_APP_VERSION_DISPLAY, 149 // Check configured version or compare with unset handled as 0 150 lazy.NimbusFeatures[featureId].getVariable("minVersion") 151 ) < 0 152 ) { 153 return; 154 } 155 156 // Set/override user prefs to persist after experiment end 157 const setPref = (pref, value) => { 158 // Only set prefs with a value (so no clearing) 159 if (value != null) { 160 lazy.PrefUtils.setPref("sidebar." + pref, value); 161 } 162 }; 163 setPref("nimbus", slug); 164 ["revamp", "verticalTabs", "visibility"].forEach(pref => 165 setPref(pref, lazy.NimbusFeatures[featureId].getVariable(pref)) 166 ); 167 } 168 169 /** 170 * Ensure the drag-to-pin promo card is not displayed to existing users who already have pinned tabs. 171 */ 172 checkForPinnedTabs() { 173 if (!lazy.dragToPinPromoDismissed) { 174 for (let win of lazy.BrowserWindowTracker.getOrderedWindows()) { 175 if (win.gBrowser.pinnedTabCount > 0) { 176 Services.prefs.setBoolPref(PINNED_PROMO_PREF, true); 177 break; 178 } 179 } 180 } 181 this.checkForPinnedTabsComplete = true; 182 this.dispatchEvent(new CustomEvent("checkForPinnedTabsComplete")); 183 } 184 185 /** 186 * Called when any widget is removed. We're only interested in the sidebar 187 * button. Note that this is also invoked if the button is merely moved 188 * to another area. 189 * 190 * @param {string} aWidgetId 191 * The widget being removed. 192 */ 193 async onWidgetRemoved(aWidgetId) { 194 if (aWidgetId == "sidebar-button") { 195 // Wait for JS to run to completion. Once that has happened, we'll 196 // know if we were _really_ removed or just moved elsewhere. 197 await Promise.resolve(); 198 if (!lazy.CustomizableUI.getPlacementOfWidget(aWidgetId)) { 199 // Removing sidebar button should force horizontal tabs (Bug 1970015). 200 Services.prefs.setBoolPref(VERTICAL_TABS_PREF, false); 201 this.closeAllSidebars(); 202 } 203 } 204 } 205 206 /** 207 * Convenience method to tell all sidebars to close when the toolbar button 208 * is removed. 209 */ 210 closeAllSidebars() { 211 for (let w of lazy.BrowserWindowTracker.getOrderedWindows()) { 212 if (w.SidebarController.isOpen) { 213 w.SidebarController.hide(); 214 } 215 w.SidebarController._state.loadInitialState({ 216 ...lazy.SidebarState.defaultProperties, 217 }); 218 } 219 } 220 221 /** 222 * Adjust for a change to the verticalTabs pref. 223 */ 224 handleVerticalTabsPrefChange(isEnabled, resetVisibility = true) { 225 if (!isEnabled) { 226 // horizontal tabs can only have visibility of "hide-sidebar" 227 Services.prefs.setStringPref(VISIBILITY_SETTING_PREF, "hide-sidebar"); 228 } else if (resetVisibility) { 229 // only reset visibility pref when switching to vertical tabs and explictly indicated 230 Services.prefs.setStringPref(VISIBILITY_SETTING_PREF, "always-show"); 231 } 232 } 233 234 /** 235 * Has the new sidebar launcher already been visible and "used" in this profile? 236 */ 237 get hasSidebarLauncherBeenVisible() { 238 // Its possible sidebar.revamp was enabled previously, but we can effectively reset if its currently false 239 if (!lazy.sidebarRevampEnabled) { 240 return false; 241 } 242 if (lazy.verticalTabsEnabled) { 243 return true; 244 } 245 // this pref tells us a sidebar panel has been opened, so it implies the launcher has 246 // been visible, but can't reliably indicate that the launcher has *not* been visible. 247 if (Services.prefs.getBoolPref("sidebar.new-sidebar.has-used", false)) { 248 return true; 249 } 250 // check if the launcher has ever been visible (in this session) in any of our open windows, 251 for (let w of lazy.BrowserWindowTracker.getOrderedWindows()) { 252 if (w.SidebarController.launcherEverVisible) { 253 return true; 254 } 255 } 256 return false; 257 } 258 259 /** 260 * Prepopulates default tools for new sidebar users and appends any new tools defined 261 * on the sidebar.newTool.migration pref branch to the sidebar.main.tools pref. 262 */ 263 updateDefaultTools() { 264 if (!lazy.sidebarRevampEnabled) { 265 return; 266 } 267 let tools = lazy.sidebarTools; 268 269 // For new sidebar.revamp users, we pre-populate a set of default tools to show in the launcher. 270 if (!tools && !lazy.newSidebarHasBeenUsed) { 271 tools = DEFAULT_LAUNCHER_TOOLS; 272 } 273 274 for (const pref of Services.prefs.getChildList( 275 "sidebar.newTool.migration." 276 )) { 277 try { 278 let options = JSON.parse(Services.prefs.getStringPref(pref)); 279 let newTool = pref.split(".")[3]; 280 281 if (options?.alreadyShown) { 282 continue; 283 } 284 285 if (options?.visibilityPref) { 286 // Will only add the tool to the launcher if the panel governing a panels sidebar visibility 287 // is first enabled 288 let visibilityPrefValue = Services.prefs.getBoolPref( 289 options.visibilityPref 290 ); 291 if (!visibilityPrefValue) { 292 Services.prefs.addObserver( 293 options.visibilityPref, 294 this.updateDefaultTools.bind(this) 295 ); 296 continue; 297 } 298 } 299 // avoid adding a tool from the pref branch where it's already been added to the DEFAULT_LAUNCHER_TOOLS (for new users) 300 if (!tools.includes(newTool)) { 301 tools += "," + newTool; 302 } 303 options.alreadyShown = true; 304 Services.prefs.setStringPref(pref, JSON.stringify(options)); 305 } catch (ex) { 306 console.error("Failed to handle pref " + pref, ex); 307 } 308 } 309 if (tools.length > lazy.sidebarTools.length) { 310 Services.prefs.setStringPref(SIDEBAR_TOOLS, tools); 311 } 312 } 313 314 updateToolsPref(toolName, remove = null) { 315 const updatedTools = lazy.sidebarTools ? lazy.sidebarTools.split(",") : []; 316 const index = updatedTools.indexOf(toolName); 317 318 if ((remove && index == -1) || (!remove && index != -1)) { 319 return; 320 } 321 322 if (remove) { 323 updatedTools.splice(index, 1); 324 } else { 325 updatedTools.push(toolName); 326 } 327 328 Services.prefs.setStringPref(SIDEBAR_TOOLS, updatedTools.join()); 329 } 330 331 clearExtensionsPref(toolName) { 332 let installedExtensions = lazy.sidebarExtensions 333 ? lazy.sidebarExtensions.split(",") 334 : []; 335 const index = installedExtensions.indexOf(toolName); 336 if (index != -1) { 337 installedExtensions.splice(index, 1); 338 Services.prefs.setStringPref( 339 INSTALLED_EXTENSIONS, 340 installedExtensions.join() 341 ); 342 } 343 } 344 345 cleanupPrefs(id) { 346 this.clearExtensionsPref(id); 347 this.updateToolsPref(id, true); 348 } 349 350 /** 351 * Return a list of tool IDs that have registered a badge for notification. 352 * This reads all prefs under "sidebar.notification.badge." 353 * 354 * @returns {Array} 355 */ 356 getBadgeTools() { 357 const BADGE_PREF_BRANCH = "sidebar.notification.badge."; 358 const badgePrefs = Services.prefs.getChildList(BADGE_PREF_BRANCH); 359 360 return badgePrefs.map(pref => pref.slice(BADGE_PREF_BRANCH.length)); 361 } 362 363 /** 364 * Provide a system-level "backup" state to be stored for those using "Never 365 * remember history" or "Clear history when browser closes". 366 * 367 * If it doesn't exist or isn't parsable, return `null`. 368 * 369 * @returns {object} 370 */ 371 getBackupState() { 372 try { 373 return JSON.parse(lazy.sidebarBackupState); 374 } catch (e) { 375 Services.prefs.clearUserPref(BACKUP_STATE_PREF); 376 return null; 377 } 378 } 379 380 /** 381 * Set the backup state. 382 * 383 * @param {object} state 384 */ 385 setBackupState(state) { 386 if (!state) { 387 return; 388 } 389 Services.prefs.setStringPref(BACKUP_STATE_PREF, JSON.stringify(state)); 390 } 391 } 392 393 // Initialize on first import 394 const sidebarManager = new SidebarManager(); 395 sidebarManager.init(); 396 export { sidebarManager as SidebarManager };