diff options
Diffstat (limited to 'browser/components/newtab/lib/ToolbarBadgeHub.jsm')
-rw-r--r-- | browser/components/newtab/lib/ToolbarBadgeHub.jsm | 318 |
1 files changed, 318 insertions, 0 deletions
diff --git a/browser/components/newtab/lib/ToolbarBadgeHub.jsm b/browser/components/newtab/lib/ToolbarBadgeHub.jsm new file mode 100644 index 0000000000..f403ca9186 --- /dev/null +++ b/browser/components/newtab/lib/ToolbarBadgeHub.jsm @@ -0,0 +1,318 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + clearTimeout: "resource://gre/modules/Timer.sys.mjs", + requestIdleCallback: "resource://gre/modules/Timer.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + EveryWindow: "resource:///modules/EveryWindow.jsm", + ToolbarPanelHub: "resource://activity-stream/lib/ToolbarPanelHub.jsm", +}); + +let notificationsByWindow = new WeakMap(); + +class _ToolbarBadgeHub { + constructor() { + this.id = "toolbar-badge-hub"; + this.state = {}; + this.prefs = { + WHATSNEW_TOOLBAR_PANEL: "browser.messaging-system.whatsNewPanel.enabled", + }; + this.removeAllNotifications = this.removeAllNotifications.bind(this); + this.removeToolbarNotification = this.removeToolbarNotification.bind(this); + this.addToolbarNotification = this.addToolbarNotification.bind(this); + this.registerBadgeToAllWindows = this.registerBadgeToAllWindows.bind(this); + this._sendPing = this._sendPing.bind(this); + this.sendUserEventTelemetry = this.sendUserEventTelemetry.bind(this); + + this._handleMessageRequest = null; + this._addImpression = null; + this._blockMessageById = null; + this._sendTelemetry = null; + this._initialized = false; + } + + async init( + waitForInitialized, + { + handleMessageRequest, + addImpression, + blockMessageById, + unblockMessageById, + sendTelemetry, + } + ) { + if (this._initialized) { + return; + } + + this._initialized = true; + this._handleMessageRequest = handleMessageRequest; + this._blockMessageById = blockMessageById; + this._unblockMessageById = unblockMessageById; + this._addImpression = addImpression; + this._sendTelemetry = sendTelemetry; + // Need to wait for ASRouter to initialize before trying to fetch messages + await waitForInitialized; + this.messageRequest({ + triggerId: "toolbarBadgeUpdate", + template: "toolbar_badge", + }); + // Listen for pref changes that could trigger new badges + Services.prefs.addObserver(this.prefs.WHATSNEW_TOOLBAR_PANEL, this); + } + + observe(aSubject, aTopic, aPrefName) { + switch (aPrefName) { + case this.prefs.WHATSNEW_TOOLBAR_PANEL: + this.messageRequest({ + triggerId: "toolbarBadgeUpdate", + template: "toolbar_badge", + }); + break; + } + } + + maybeInsertFTL(win) { + win.MozXULElement.insertFTLIfNeeded("browser/newtab/asrouter.ftl"); + } + + executeAction({ id, data, message_id }) { + switch (id) { + case "show-whatsnew-button": + lazy.ToolbarPanelHub.enableToolbarButton(); + lazy.ToolbarPanelHub.enableAppmenuButton(); + break; + } + } + + _clearBadgeTimeout() { + if (this.state.showBadgeTimeoutId) { + lazy.clearTimeout(this.state.showBadgeTimeoutId); + } + } + + removeAllNotifications(event) { + if (event) { + // ignore right clicks + if ( + (event.type === "mousedown" || event.type === "click") && + event.button !== 0 + ) { + return; + } + // ignore keyboard access that is not one of the usual accessor keys + if ( + event.type === "keypress" && + event.key !== " " && + event.key !== "Enter" + ) { + return; + } + + event.target.removeEventListener( + "mousedown", + this.removeAllNotifications + ); + event.target.removeEventListener("keypress", this.removeAllNotifications); + // If we have an event it means the user interacted with the badge + // we should send telemetry + if (this.state.notification) { + this.sendUserEventTelemetry("CLICK", this.state.notification); + } + } + // Will call uninit on every window + lazy.EveryWindow.unregisterCallback(this.id); + if (this.state.notification) { + this._blockMessageById(this.state.notification.id); + } + this._clearBadgeTimeout(); + this.state = {}; + } + + removeToolbarNotification(toolbarButton) { + // Remove it from the element that displays the badge + toolbarButton + .querySelector(".toolbarbutton-badge") + .classList.remove("feature-callout"); + toolbarButton.removeAttribute("badged"); + // Remove id used for for aria-label badge description + const notificationDescription = toolbarButton.querySelector( + "#toolbarbutton-notification-description" + ); + if (notificationDescription) { + notificationDescription.remove(); + toolbarButton.removeAttribute("aria-labelledby"); + toolbarButton.removeAttribute("aria-describedby"); + } + } + + addToolbarNotification(win, message) { + const document = win.browser.ownerDocument; + if (message.content.action) { + this.executeAction({ ...message.content.action, message_id: message.id }); + } + let toolbarbutton = document.getElementById(message.content.target); + if (toolbarbutton) { + const badge = toolbarbutton.querySelector(".toolbarbutton-badge"); + badge.classList.add("feature-callout"); + toolbarbutton.setAttribute("badged", true); + // If we have additional aria-label information for the notification + // we add this content to the hidden `toolbarbutton-text` node. + // We then use `aria-labelledby` to link this description to the button + // that received the notification badge. + if (message.content.badgeDescription) { + // Insert strings as soon as we know we're showing them + this.maybeInsertFTL(win); + toolbarbutton.setAttribute( + "aria-labelledby", + `toolbarbutton-notification-description ${message.content.target}` + ); + // Because tooltiptext is different to the label, it gets duplicated as + // the description. Setting `describedby` to the same value as + // `labelledby` will be detected by the a11y code and the description + // will be removed. + toolbarbutton.setAttribute( + "aria-describedby", + `toolbarbutton-notification-description ${message.content.target}` + ); + const descriptionEl = document.createElement("span"); + descriptionEl.setAttribute( + "id", + "toolbarbutton-notification-description" + ); + descriptionEl.hidden = true; + document.l10n.setAttributes( + descriptionEl, + message.content.badgeDescription.string_id + ); + toolbarbutton.appendChild(descriptionEl); + } + // `mousedown` event required because of the `onmousedown` defined on + // the button that prevents `click` events from firing + toolbarbutton.addEventListener("mousedown", this.removeAllNotifications); + // `keypress` event required for keyboard accessibility + toolbarbutton.addEventListener("keypress", this.removeAllNotifications); + this.state = { notification: { id: message.id } }; + + // Impression should be added when the badge becomes visible + this._addImpression(message); + // Send a telemetry ping when adding the notification badge + this.sendUserEventTelemetry("IMPRESSION", message); + + return toolbarbutton; + } + + return null; + } + + registerBadgeToAllWindows(message) { + if (message.template === "update_action") { + this.executeAction({ ...message.content.action, message_id: message.id }); + // No badge to set only an action to execute + return; + } + + lazy.EveryWindow.registerCallback( + this.id, + win => { + if (notificationsByWindow.has(win)) { + // nothing to do + return; + } + const el = this.addToolbarNotification(win, message); + notificationsByWindow.set(win, el); + }, + win => { + const el = notificationsByWindow.get(win); + if (el) { + this.removeToolbarNotification(el); + } + notificationsByWindow.delete(win); + } + ); + } + + registerBadgeNotificationListener(message, options = {}) { + // We need to clear any existing notifications and only show + // the one set by devtools + if (options.force) { + this.removeAllNotifications(); + // When debugging immediately show the badge + this.registerBadgeToAllWindows(message); + return; + } + + if (message.content.delay) { + this.state.showBadgeTimeoutId = lazy.setTimeout(() => { + lazy.requestIdleCallback(() => this.registerBadgeToAllWindows(message)); + }, message.content.delay); + } else { + this.registerBadgeToAllWindows(message); + } + } + + async messageRequest({ triggerId, template }) { + const telemetryObject = { triggerId }; + TelemetryStopwatch.start("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject); + const message = await this._handleMessageRequest({ + triggerId, + template, + }); + TelemetryStopwatch.finish("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject); + if (message) { + this.registerBadgeNotificationListener(message); + } + } + + _sendPing(ping) { + this._sendTelemetry({ + type: "TOOLBAR_BADGE_TELEMETRY", + data: { action: "badge_user_event", ...ping }, + }); + } + + sendUserEventTelemetry(event, message) { + const win = Services.wm.getMostRecentWindow("navigator:browser"); + // Only send pings for non private browsing windows + if ( + win && + !lazy.PrivateBrowsingUtils.isBrowserPrivate( + win.ownerGlobal.gBrowser.selectedBrowser + ) + ) { + this._sendPing({ + message_id: message.id, + event, + }); + } + } + + uninit() { + this._clearBadgeTimeout(); + this.state = {}; + this._initialized = false; + notificationsByWindow = new WeakMap(); + Services.prefs.removeObserver(this.prefs.WHATSNEW_TOOLBAR_PANEL, this); + } +} + +/** + * ToolbarBadgeHub - singleton instance of _ToolbarBadgeHub that can initiate + * message requests and render messages. + */ +const ToolbarBadgeHub = new _ToolbarBadgeHub(); + +const EXPORTED_SYMBOLS = ["ToolbarBadgeHub", "_ToolbarBadgeHub"]; |