TabNotesController.sys.mjs (9036B)
1 /** 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 */ 6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 7 8 /** @import { CanonicalURLParent } from "./CanonicalURLParent.sys.mjs" */ 9 10 const lazy = {}; 11 ChromeUtils.defineESModuleGetters(lazy, { 12 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 13 TabNotes: "moz-src:///browser/components/tabnotes/TabNotes.sys.mjs", 14 }); 15 ChromeUtils.defineLazyGetter(lazy, "logConsole", function () { 16 return console.createInstance({ 17 prefix: "TabNotes", 18 maxLogLevel: Services.prefs.getBoolPref("browser.tabs.notes.debug", false) 19 ? "Debug" 20 : "Warn", 21 }); 22 }); 23 XPCOMUtils.defineLazyPreferenceGetter( 24 lazy, 25 "TAB_NOTES_ENABLED", 26 "browser.tabs.notes.enabled", 27 false 28 ); 29 30 const EVENTS = [ 31 "CanonicalURL:Identified", 32 "TabNote:Created", 33 "TabNote:Edited", 34 "TabNote:Removed", 35 ]; 36 37 /** 38 * Orchestrates the tab notes life cycle. 39 * 40 * Singleton class within Firefox that observes tab notes-related topics, 41 * listens for tab notes-related events, and ensures that the state of the 42 * tabbrowser stays in sync with the TabNotes repository. 43 * 44 * Registers with the category manager in order to initialize on Firefox 45 * startup and be notified when windows are opened/closed. 46 * 47 * @see https://firefox-source-docs.mozilla.org/browser/CategoryManagerIndirection.html 48 */ 49 class TabNotesControllerClass { 50 /** 51 * Registered with `browser-first-window-ready` to be notified of 52 * app startup. 53 * 54 * @see tabnotes.manifest 55 */ 56 init() { 57 if (lazy.TAB_NOTES_ENABLED) { 58 lazy.TabNotes.init(); 59 } else { 60 lazy.logConsole.info("Tab notes disabled"); 61 } 62 } 63 64 /** 65 * Registered with `browser-window-delayed-startup` to be notified of new 66 * windows. 67 * 68 * @param {Window} win 69 * @see tabnotes.manifest 70 */ 71 registerWindow(win) { 72 if (lazy.TAB_NOTES_ENABLED) { 73 EVENTS.forEach(eventName => win.addEventListener(eventName, this)); 74 win.gBrowser.addTabsProgressListener(this); 75 lazy.logConsole.debug("registerWindow", EVENTS, win); 76 } 77 } 78 79 /** 80 * Registered with `browser-window-unload` to be notified of unloaded windows. 81 * 82 * @param {Window} win 83 * @see tabnotes.manifest 84 */ 85 unregisterWindow(win) { 86 if (lazy.TAB_NOTES_ENABLED) { 87 EVENTS.forEach(eventName => win.removeEventListener(eventName, this)); 88 win.gBrowser.removeTabsProgressListener(this); 89 lazy.logConsole.debug("unregisterWindow", EVENTS, win); 90 } 91 } 92 93 /** 94 * Registered with `browser-quit-application-granted` to be notified of 95 * app shutdown. 96 * 97 * @see tabnotes.manifest 98 */ 99 quit() { 100 if (lazy.TAB_NOTES_ENABLED) { 101 lazy.TabNotes.deinit(); 102 } 103 } 104 105 /** 106 * @param {CanonicalURLIdentifiedEvent|TabNoteCreatedEvent|TabNoteEditedEvent|TabNoteRemovedEvent} event 107 */ 108 handleEvent(event) { 109 switch (event.type) { 110 case "CanonicalURL:Identified": 111 { 112 // A browser identified its canonical URL, so we can determine whether 113 // the tab has an associated note and should therefore display a tab 114 // notes icon. 115 const browser = event.target; 116 const { canonicalUrl } = event.detail; 117 const gBrowser = browser.getTabBrowser(); 118 const tab = gBrowser.getTabForBrowser(browser); 119 tab.canonicalUrl = canonicalUrl; 120 lazy.TabNotes.has(tab).then(hasTabNote => { 121 tab.hasTabNote = hasTabNote; 122 }); 123 124 lazy.logConsole.debug("CanonicalURL:Identified", tab, canonicalUrl); 125 } 126 break; 127 case "TabNote:Created": 128 { 129 const { telemetrySource } = event.detail; 130 if (telemetrySource) { 131 Glean.tabNotes.added.record({ 132 source: telemetrySource, 133 }); 134 } 135 // A new tab note was created for a specific canonical URL. Ensure that 136 // all tabs with the same canonical URL also indicate that there is a 137 // tab note. 138 const { canonicalUrl } = event.target; 139 for (const win of lazy.BrowserWindowTracker.orderedWindows) { 140 for (const tab of win.gBrowser.tabs) { 141 if (tab.canonicalUrl == canonicalUrl) { 142 tab.hasTabNote = true; 143 } 144 } 145 } 146 lazy.logConsole.debug("TabNote:Created", canonicalUrl); 147 } 148 break; 149 case "TabNote:Edited": 150 { 151 const { canonicalUrl } = event.target; 152 const { telemetrySource } = event.detail; 153 if (telemetrySource) { 154 Glean.tabNotes.edited.record({ 155 source: telemetrySource, 156 }); 157 } 158 lazy.logConsole.debug("TabNote:Edited", canonicalUrl); 159 } 160 break; 161 case "TabNote:Removed": 162 { 163 const { telemetrySource, note } = event.detail; 164 const now = Temporal.Now.instant(); 165 const noteAgeHours = Math.round( 166 now.since(note.created).total("hours") 167 ); 168 if (telemetrySource) { 169 Glean.tabNotes.deleted.record({ 170 source: telemetrySource, 171 note_age_hours: noteAgeHours, 172 }); 173 } 174 175 // A new tab note was removed from a specific canonical URL. Ensure that 176 // all tabs with the same canonical URL also indicate that there is no 177 // longer a tab note. 178 const { canonicalUrl } = event.target; 179 for (const win of lazy.BrowserWindowTracker.orderedWindows) { 180 for (const tab of win.gBrowser.tabs) { 181 if (tab.canonicalUrl == canonicalUrl) { 182 tab.hasTabNote = false; 183 } 184 } 185 } 186 lazy.logConsole.debug("TabNote:Removed", canonicalUrl); 187 } 188 break; 189 } 190 } 191 192 /** 193 * Invoked by Tabbrowser after we register with `Tabbrowser.addProgressListener`. 194 * 195 * Clears the tab note icon and canonical URL from a tab when navigation starts 196 * within a tab. 197 * 198 * The CanonicalURL actor running in this tab's browser will report the canonical 199 * URL of the new destination in the browser, which will determine whether the 200 * new destination has an associated tab note. 201 * 202 * @type {TabbrowserWebProgressListener<"onLocationChange">} 203 */ 204 onLocationChange(aBrowser, aWebProgress, aRequest, aLocation, aFlags) { 205 // Tab notes only apply to the top-level frames loaded in tabs. 206 if (!aWebProgress.isTopLevel) { 207 return; 208 } 209 210 if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) { 211 if ( 212 aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_RELOAD || 213 aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_HISTORY 214 ) { 215 // User is reloading/returning to the same document via history. We 216 // can count on CanonicalURLChild to listen for `pageshow` and tell us 217 // about the canonical URL at the new location. 218 lazy.logConsole.debug( 219 "reload/history navigation, waiting for pageshow", 220 aLocation.spec 221 ); 222 return; 223 } 224 225 if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_HASHCHANGE) { 226 // The web site modified the hash/fragment identifier part of the URL 227 // directly. TODO: determine how and whether to handle `hashchange`. 228 lazy.logConsole.debug("fragment identifier changed", aLocation.spec); 229 return; 230 } 231 232 if (aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE) { 233 // Web page is using `history.pushState()` to change URLs. There isn't 234 // a way for CanonicalURLChild to detect this in the content process, 235 // so we need to ask it to recalculate canonical URLs to see if they 236 // changed. 237 /** @type {CanonicalURLParent|undefined} */ 238 let parent = 239 aBrowser.browsingContext?.currentWindowGlobal.getExistingActor( 240 "CanonicalURL" 241 ); 242 243 if (parent) { 244 parent.sendAsyncMessage("CanonicalURL:Detect"); 245 lazy.logConsole.debug( 246 "requesting CanonicalURL:Detect due to history.pushState", 247 aLocation.spec 248 ); 249 } 250 251 return; 252 } 253 254 // General same document case: we are navigating in the same document, 255 // so the tab note indicator does not need to change. 256 return; 257 } 258 259 // General case: we are doing normal navigation to another URL, so we 260 // clear the canonical URL/tab note state on the tab and wait for 261 // `CanonicalURL:Identified` to tell us whether the new location has 262 // a tab note. 263 const tab = aBrowser.ownerGlobal.gBrowser.getTabForBrowser(aBrowser); 264 tab.canonicalUrl = undefined; 265 tab.hasTabNote = false; 266 lazy.logConsole.debug("clear tab note due to location change", tab); 267 } 268 } 269 270 export const TabNotesController = new TabNotesControllerClass();