tor-browser

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

MenuMessage.sys.mjs (10661B)


      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  AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs",
      9  ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
     10  PanelMultiView:
     11    "moz-src:///browser/components/customizableui/PanelMultiView.sys.mjs",
     12  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     13  RemoteL10n: "resource:///modules/asrouter/RemoteL10n.sys.mjs",
     14  SpecialMessageActions:
     15    "resource://messaging-system/lib/SpecialMessageActions.sys.mjs",
     16  UIState: "resource://services-sync/UIState.sys.mjs",
     17 });
     18 
     19 export const MenuMessage = {
     20  SOURCES: Object.freeze({
     21    APP_MENU: "app_menu",
     22    PXI_MENU: "pxi_menu",
     23  }),
     24 
     25  MESSAGE_TYPES: Object.freeze({
     26    FXA_CTA: "fxa_cta",
     27    DEFAULT_CTA: "default_cta",
     28  }),
     29 
     30  MESSAGE_TYPE_ALLOWED_BY_SOURCE: Object.freeze({
     31    app_menu: new Set(["fxa_cta", "default_cta"]),
     32    pxi_menu: new Set(["fxa_cta"]),
     33  }),
     34  SHOWING_FXA_MENU_MESSAGE_ATTR: "showing-fxa-menu-message",
     35  SHOWING_SET_TO_DEFAULT_MENU_MESSAGE_ATTR: "showing-default-cta-menu-message",
     36 
     37  async showMenuMessage(browser, message, trigger, force) {
     38    if (!browser) {
     39      return;
     40    }
     41 
     42    let win = browser.ownerGlobal;
     43 
     44    if (!win || lazy.PrivateBrowsingUtils.isWindowPrivate(win)) {
     45      return;
     46    }
     47 
     48    let source = trigger?.context?.source || message.testingTriggerContext;
     49 
     50    // Restrict message types to their allowed sources
     51    if (
     52      !this.MESSAGE_TYPE_ALLOWED_BY_SOURCE[source]?.has(
     53        message.content.messageType
     54      )
     55    ) {
     56      return;
     57    }
     58 
     59    switch (source) {
     60      case this.SOURCES.APP_MENU: {
     61        this.showAppMenuMessage(browser, message, force);
     62        break;
     63      }
     64 
     65      case this.SOURCES.PXI_MENU: {
     66        this.showPxiMenuMessage(browser, message, force);
     67        break;
     68      }
     69    }
     70  },
     71 
     72  /**
     73   * Whether the message should be suppressed for a signed-in user.
     74   *  - fxa_cta: suppress when signed in by default, unless
     75   *    content.allowWhenSignedIn is set to true.
     76   *  - default_cta: never suppress
     77   */
     78  shouldSuppressForSignedIn(message) {
     79    const type = message?.content?.messageType;
     80    const isSignedIn =
     81      lazy.UIState.get().status === lazy.UIState.STATUS_SIGNED_IN;
     82 
     83    // If not signed in, no need to suppress
     84    if (!isSignedIn) {
     85      return false;
     86    }
     87 
     88    //  Suppress fxa_cta messages by default, unless we explicitly allow it.
     89    if (type === this.MESSAGE_TYPES.FXA_CTA) {
     90      const allowWhenSignedIn = !!message.content?.allowWhenSignedIn;
     91      return !allowWhenSignedIn;
     92    }
     93 
     94    // For any other message, we don't suppress it.
     95    return false;
     96  },
     97 
     98  preparePrimaryAction(message, source) {
     99    const type = message?.content?.messageType;
    100    const primaryAction = message?.content?.primaryAction;
    101    if (!primaryAction) {
    102      return null;
    103    }
    104    const action = structuredClone(primaryAction);
    105 
    106    // For fxa_cta messages, depending on the source that showed the
    107    // message, we'll want to set a particular entrypoint in the data
    108    // payload in the event that we're  opening up the FxA sign-up page.
    109    if (type === this.MESSAGE_TYPES.FXA_CTA) {
    110      action.data = action.data || {};
    111      action.data.extraParams = action.data.extraParams || {};
    112 
    113      if (source === this.SOURCES.APP_MENU) {
    114        action.data.entrypoint = "fxa_app_menu";
    115        action.data.extraParams.utm_content = `${action.data.extraParams.utm_content}-app_menu`;
    116      } else if (source === this.SOURCES.PXI_MENU) {
    117        action.data.entrypoint = "fxa_avatar_menu";
    118        action.data.extraParams.utm_content = `${action.data.extraParams.utm_content}-avatar`;
    119      }
    120    }
    121    return action;
    122  },
    123 
    124  async showAppMenuMessage(browser, message, force) {
    125    const win = browser.ownerGlobal;
    126    const msgContainer = this.hideAppMenuMessage(browser);
    127    const type = message?.content?.messageType;
    128 
    129    // This version of the browser only supports the fxa_cta and
    130    // default_cta versions of this message in the AppMenu.
    131    // We also don't draw focus away from any existing AppMenuNotifications.
    132    if (!message || lazy.AppMenuNotifications.activeNotification) {
    133      return;
    134    }
    135 
    136    // Respect message type signed-in render rules (fxa_cta suppresses by default)
    137    if (this.shouldSuppressForSignedIn(message)) {
    138      return;
    139    }
    140 
    141    const menuMessageAttribute =
    142      type === this.MESSAGE_TYPES.DEFAULT_CTA
    143        ? MenuMessage.SHOWING_SET_TO_DEFAULT_MENU_MESSAGE_ATTR
    144        : MenuMessage.SHOWING_FXA_MENU_MESSAGE_ATTR;
    145 
    146    let msgElement = await this.constructMenuMessage(
    147      win,
    148      message,
    149      MenuMessage.SOURCES.APP_MENU
    150    );
    151 
    152    win.PanelUI.mainView.setAttribute(menuMessageAttribute, message.id);
    153 
    154    msgElement.addEventListener("MenuMessage:Close", () => {
    155      win.PanelUI.mainView.removeAttribute(menuMessageAttribute);
    156    });
    157 
    158    msgElement.addEventListener("MenuMessage:PrimaryButton", () => {
    159      win.PanelUI.hide();
    160    });
    161 
    162    msgContainer.appendChild(msgElement);
    163 
    164    if (force) {
    165      win.PanelUI.show();
    166    }
    167  },
    168 
    169  hideAppMenuMessage(browser) {
    170    const win = browser.ownerGlobal;
    171    const document = browser.ownerDocument;
    172    const msgContainer = lazy.PanelMultiView.getViewNode(
    173      document,
    174      "appMenu-menu-message"
    175    );
    176    msgContainer.innerHTML = "";
    177    win.PanelUI.mainView.removeAttribute(
    178      MenuMessage.SHOWING_FXA_MENU_MESSAGE_ATTR
    179    );
    180    win.PanelUI.mainView.removeAttribute(
    181      MenuMessage.SHOWING_SET_TO_DEFAULT_MENU_MESSAGE_ATTR
    182    );
    183 
    184    return msgContainer;
    185  },
    186 
    187  async showPxiMenuMessage(browser, message, force) {
    188    const win = browser.ownerGlobal;
    189    const { document } = win;
    190    const msgContainer = this.hidePxiMenuMessage(browser);
    191 
    192    // Respect message type signed-in render rules (fxa_cta suppresses by default)
    193    if (this.shouldSuppressForSignedIn(message)) {
    194      return;
    195    }
    196 
    197    let fxaPanelView = lazy.PanelMultiView.getViewNode(document, "PanelUI-fxa");
    198    fxaPanelView.setAttribute(
    199      MenuMessage.SHOWING_FXA_MENU_MESSAGE_ATTR,
    200      message.id
    201    );
    202 
    203    let msgElement = await this.constructMenuMessage(
    204      win,
    205      message,
    206      MenuMessage.SOURCES.PXI_MENU
    207    );
    208 
    209    msgElement.addEventListener("MenuMessage:Close", () => {
    210      fxaPanelView.removeAttribute(MenuMessage.SHOWING_FXA_MENU_MESSAGE_ATTR);
    211    });
    212 
    213    msgElement.addEventListener("MenuMessage:PrimaryButton", () => {
    214      let panelNode = fxaPanelView.closest("panel");
    215 
    216      if (panelNode) {
    217        lazy.PanelMultiView.hidePopup(panelNode);
    218      }
    219    });
    220 
    221    msgContainer.appendChild(msgElement);
    222 
    223    if (force) {
    224      await win.gSync.toggleAccountPanel(
    225        document.getElementById("fxa-toolbar-menu-button"),
    226        new MouseEvent("mousedown")
    227      );
    228    }
    229  },
    230 
    231  hidePxiMenuMessage(browser) {
    232    const document = browser.ownerDocument;
    233    const msgContainer = lazy.PanelMultiView.getViewNode(
    234      document,
    235      "PanelUI-fxa-menu-message"
    236    );
    237    msgContainer.innerHTML = "";
    238    let fxaPanelView = lazy.PanelMultiView.getViewNode(document, "PanelUI-fxa");
    239    fxaPanelView.removeAttribute(MenuMessage.SHOWING_FXA_MENU_MESSAGE_ATTR);
    240    return msgContainer;
    241  },
    242 
    243  async constructMenuMessage(win, message, source) {
    244    let { document, gBrowser } = win;
    245 
    246    win.MozXULElement.insertFTLIfNeeded("browser/newtab/asrouter.ftl");
    247 
    248    const msgElement = document.createElement("menu-message");
    249    msgElement.layout = message.content.layout ?? "column";
    250    msgElement.imageURL = message.content.imageURL;
    251    msgElement.logoURL = message.content.logoURL;
    252    msgElement.primaryButtonSize =
    253      message.content.primaryButtonSize ?? "default";
    254    msgElement.buttonText = await lazy.RemoteL10n.formatLocalizableText(
    255      message.content.primaryActionText
    256    );
    257    msgElement.primaryText = await lazy.RemoteL10n.formatLocalizableText(
    258      message.content.primaryText
    259    );
    260    // Simple layout does not support secondary text
    261    if (message.content.layout !== "simple" && message.content.secondaryText) {
    262      msgElement.secondaryText = await lazy.RemoteL10n.formatLocalizableText(
    263        message.content.secondaryText
    264      );
    265    }
    266    msgElement.dataset.navigableWithTabOnly = "true";
    267    if (message.content.imageWidth !== undefined) {
    268      msgElement.style.setProperty(
    269        "--image-width",
    270        `${message.content.imageWidth}px`
    271      );
    272    }
    273    if (message.content.logoWidth !== undefined) {
    274      msgElement.style.setProperty(
    275        "--logo-width",
    276        `${message.content.logoWidth}px`
    277      );
    278    }
    279    if (message.content.imageVerticalTopOffset !== undefined) {
    280      msgElement.style.setProperty(
    281        "--illustration-margin-block-start-offset",
    282        `${message.content.imageVerticalTopOffset}px`
    283      );
    284    }
    285    if (message.content.imageVerticalBottomOffset !== undefined) {
    286      msgElement.style.setProperty(
    287        "--illustration-margin-block-end-offset",
    288        `${message.content.imageVerticalBottomOffset}px`
    289      );
    290    }
    291    if (message.content.containerVerticalBottomOffset !== undefined) {
    292      msgElement.style.setProperty(
    293        "--container-margin-block-end-offset",
    294        `${message.content.containerVerticalBottomOffset}px`
    295      );
    296    }
    297    if (message.content.containerPaddingBottom !== undefined) {
    298      msgElement.style.setProperty(
    299        "--container-padding-block-end",
    300        `${message.content.containerPaddingBottom}px`
    301      );
    302    }
    303 
    304    msgElement.addEventListener("MenuMessage:Close", () => {
    305      msgElement.remove();
    306 
    307      this.recordMenuMessageTelemetry("DISMISS", source, message.id);
    308 
    309      lazy.SpecialMessageActions.handleAction(
    310        message.content.closeAction,
    311        gBrowser.selectedBrowser
    312      );
    313    });
    314 
    315    msgElement.addEventListener("MenuMessage:PrimaryButton", () => {
    316      this.recordMenuMessageTelemetry("CLICK", source, message.id);
    317 
    318      const primaryAction = this.preparePrimaryAction(message, source);
    319      if (primaryAction) {
    320        lazy.SpecialMessageActions.handleAction(
    321          primaryAction,
    322          gBrowser.selectedBrowser
    323        );
    324      }
    325    });
    326 
    327    return msgElement;
    328  },
    329 
    330  recordMenuMessageTelemetry(event, source, messageId) {
    331    let ping = {
    332      message_id: messageId,
    333      event,
    334      source,
    335    };
    336    lazy.ASRouter.dispatchCFRAction({
    337      type: "MENU_MESSAGE_TELEMETRY",
    338      data: { action: "menu_message_user_event", ...ping },
    339    });
    340  },
    341 };