tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ToolbarBadgeHub.sys.mjs (8952B)


      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 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  EveryWindow: "resource:///modules/EveryWindow.sys.mjs",
      9  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     10  clearTimeout: "resource://gre/modules/Timer.sys.mjs",
     11  requestIdleCallback: "resource://gre/modules/Timer.sys.mjs",
     12  setTimeout: "resource://gre/modules/Timer.sys.mjs",
     13 });
     14 
     15 let notificationsByWindow = new WeakMap();
     16 
     17 export class _ToolbarBadgeHub {
     18  constructor() {
     19    this.id = "toolbar-badge-hub";
     20    this.state = {};
     21    this.removeAllNotifications = this.removeAllNotifications.bind(this);
     22    this.removeToolbarNotification = this.removeToolbarNotification.bind(this);
     23    this.addToolbarNotification = this.addToolbarNotification.bind(this);
     24    this.registerBadgeToAllWindows = this.registerBadgeToAllWindows.bind(this);
     25    this._sendPing = this._sendPing.bind(this);
     26    this.sendUserEventTelemetry = this.sendUserEventTelemetry.bind(this);
     27 
     28    this._handleMessageRequest = null;
     29    this._addImpression = null;
     30    this._blockMessageById = null;
     31    this._sendTelemetry = null;
     32    this._initialized = false;
     33  }
     34 
     35  async init(
     36    waitForInitialized,
     37    {
     38      handleMessageRequest,
     39      addImpression,
     40      blockMessageById,
     41      unblockMessageById,
     42      sendTelemetry,
     43    }
     44  ) {
     45    if (this._initialized) {
     46      return;
     47    }
     48 
     49    this._initialized = true;
     50    this._handleMessageRequest = handleMessageRequest;
     51    this._blockMessageById = blockMessageById;
     52    this._unblockMessageById = unblockMessageById;
     53    this._addImpression = addImpression;
     54    this._sendTelemetry = sendTelemetry;
     55    // Need to wait for ASRouter to initialize before trying to fetch messages
     56    await waitForInitialized;
     57    this.messageRequest({
     58      triggerId: "toolbarBadgeUpdate",
     59      template: "toolbar_badge",
     60    });
     61  }
     62 
     63  maybeInsertFTL(win) {
     64    win.MozXULElement.insertFTLIfNeeded("browser/newtab/asrouter.ftl");
     65  }
     66 
     67  _clearBadgeTimeout() {
     68    if (this.state.showBadgeTimeoutId) {
     69      lazy.clearTimeout(this.state.showBadgeTimeoutId);
     70    }
     71  }
     72 
     73  removeAllNotifications(event) {
     74    if (event) {
     75      // ignore right clicks
     76      if (
     77        (event.type === "mousedown" || event.type === "click") &&
     78        event.button !== 0
     79      ) {
     80        return;
     81      }
     82      // ignore keyboard access that is not one of the usual accessor keys
     83      if (
     84        event.type === "keypress" &&
     85        event.key !== " " &&
     86        event.key !== "Enter"
     87      ) {
     88        return;
     89      }
     90 
     91      event.target.removeEventListener(
     92        "mousedown",
     93        this.removeAllNotifications
     94      );
     95      event.target.removeEventListener("keypress", this.removeAllNotifications);
     96      // If we have an event it means the user interacted with the badge
     97      // we should send telemetry
     98      if (this.state.notification) {
     99        this.sendUserEventTelemetry("CLICK", this.state.notification);
    100      }
    101    }
    102    // Will call uninit on every window
    103    lazy.EveryWindow.unregisterCallback(this.id);
    104    if (this.state.notification) {
    105      this._blockMessageById(this.state.notification.id);
    106    }
    107    this._clearBadgeTimeout();
    108    this.state = {};
    109  }
    110 
    111  removeToolbarNotification(toolbarButton) {
    112    // Remove it from the element that displays the badge
    113    toolbarButton
    114      .querySelector(".toolbarbutton-badge")
    115      .classList.remove("feature-callout");
    116    toolbarButton.removeAttribute("badged");
    117    toolbarButton.removeAttribute("showing-callout");
    118    // Remove id used for for aria-label badge description
    119    const notificationDescription = toolbarButton.querySelector(
    120      "#toolbarbutton-notification-description"
    121    );
    122    if (notificationDescription) {
    123      notificationDescription.remove();
    124      toolbarButton.removeAttribute("aria-labelledby");
    125      toolbarButton.removeAttribute("aria-describedby");
    126    }
    127  }
    128 
    129  addToolbarNotification(win, message) {
    130    const document = win.browser.ownerDocument;
    131    let toolbarbutton = document.getElementById(message.content.target);
    132    if (toolbarbutton) {
    133      const badge = toolbarbutton.querySelector(".toolbarbutton-badge");
    134      badge.classList.add("feature-callout");
    135      toolbarbutton.setAttribute("badged", true);
    136      toolbarbutton.setAttribute("showing-callout", true);
    137      // If we have additional aria-label information for the notification
    138      // we add this content to the hidden `toolbarbutton-text` node.
    139      // We then use `aria-labelledby` to link this description to the button
    140      // that received the notification badge.
    141      if (message.content.badgeDescription) {
    142        // Insert strings as soon as we know we're showing them
    143        this.maybeInsertFTL(win);
    144        toolbarbutton.setAttribute(
    145          "aria-labelledby",
    146          `toolbarbutton-notification-description ${message.content.target}`
    147        );
    148        // Because tooltiptext is different to the label, it gets duplicated as
    149        // the description. Setting `describedby` to the same value as
    150        // `labelledby` will be detected by the a11y code and the description
    151        // will be removed.
    152        toolbarbutton.setAttribute(
    153          "aria-describedby",
    154          `toolbarbutton-notification-description ${message.content.target}`
    155        );
    156        const descriptionEl = document.createElement("span");
    157        descriptionEl.setAttribute(
    158          "id",
    159          "toolbarbutton-notification-description"
    160        );
    161        descriptionEl.hidden = true;
    162        document.l10n.setAttributes(
    163          descriptionEl,
    164          message.content.badgeDescription.string_id
    165        );
    166        toolbarbutton.appendChild(descriptionEl);
    167      }
    168      // `mousedown` event required because of the `onmousedown` defined on
    169      // the button that prevents `click` events from firing
    170      toolbarbutton.addEventListener("mousedown", this.removeAllNotifications);
    171      // `keypress` event required for keyboard accessibility
    172      toolbarbutton.addEventListener("keypress", this.removeAllNotifications);
    173      this.state = { notification: { id: message.id } };
    174 
    175      // Impression should be added when the badge becomes visible
    176      this._addImpression(message);
    177      // Send a telemetry ping when adding the notification badge
    178      this.sendUserEventTelemetry("IMPRESSION", message);
    179 
    180      return toolbarbutton;
    181    }
    182 
    183    return null;
    184  }
    185 
    186  registerBadgeToAllWindows(message) {
    187    lazy.EveryWindow.registerCallback(
    188      this.id,
    189      win => {
    190        if (notificationsByWindow.has(win)) {
    191          // nothing to do
    192          return;
    193        }
    194        const el = this.addToolbarNotification(win, message);
    195        notificationsByWindow.set(win, el);
    196      },
    197      win => {
    198        const el = notificationsByWindow.get(win);
    199        if (el) {
    200          this.removeToolbarNotification(el);
    201        }
    202        notificationsByWindow.delete(win);
    203      }
    204    );
    205  }
    206 
    207  registerBadgeNotificationListener(message, options = {}) {
    208    // We need to clear any existing notifications and only show
    209    // the one set by devtools
    210    if (options.force) {
    211      this.removeAllNotifications();
    212      // When debugging immediately show the badge
    213      this.registerBadgeToAllWindows(message);
    214      return;
    215    }
    216 
    217    if (message.content.delay) {
    218      this.state.showBadgeTimeoutId = lazy.setTimeout(() => {
    219        lazy.requestIdleCallback(() => this.registerBadgeToAllWindows(message));
    220      }, message.content.delay);
    221    } else {
    222      this.registerBadgeToAllWindows(message);
    223    }
    224  }
    225 
    226  async messageRequest({ triggerId, template }) {
    227    const timerId = Glean.messagingSystem.messageRequestTime.start();
    228    const message = await this._handleMessageRequest({
    229      triggerId,
    230      template,
    231    });
    232    Glean.messagingSystem.messageRequestTime.stopAndAccumulate(timerId);
    233    if (message) {
    234      this.registerBadgeNotificationListener(message);
    235    }
    236  }
    237 
    238  _sendPing(ping) {
    239    this._sendTelemetry({
    240      type: "TOOLBAR_BADGE_TELEMETRY",
    241      data: { action: "badge_user_event", ...ping },
    242    });
    243  }
    244 
    245  sendUserEventTelemetry(event, message) {
    246    const win = Services.wm.getMostRecentWindow("navigator:browser");
    247    // Only send pings for non private browsing windows
    248    if (
    249      win &&
    250      !lazy.PrivateBrowsingUtils.isBrowserPrivate(
    251        win.ownerGlobal.gBrowser.selectedBrowser
    252      )
    253    ) {
    254      this._sendPing({
    255        message_id: message.id,
    256        event,
    257      });
    258    }
    259  }
    260 
    261  uninit() {
    262    this._clearBadgeTimeout();
    263    this.state = {};
    264    this._initialized = false;
    265    notificationsByWindow = new WeakMap();
    266  }
    267 }
    268 
    269 /**
    270 * ToolbarBadgeHub - singleton instance of _ToolbarBadgeHub that can initiate
    271 * message requests and render messages.
    272 */
    273 export const ToolbarBadgeHub = new _ToolbarBadgeHub();