Bug 1543121 - Add buttons for toggling FxA message (in bookmark panel) r=k88hudson
authorDanny Coates <dannycoates@gmail.com>
Thu, 25 Apr 2019 14:47:24 +0000
changeset 530139 a47ffeaa2467cf4a927fc0bc225b2b7f2f6911f4
parent 530138 635e8833ed5fe4aba5d86721cdd57251f5e471cc
child 530140 24740ab9a7266f84283b2beeebe665edf903f09f
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersk88hudson
bugs1543121
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1543121 - Add buttons for toggling FxA message (in bookmark panel) r=k88hudson Differential Revision: https://phabricator.services.mozilla.com/D28165
browser/base/content/browser-places.js
browser/base/content/browser.xul
browser/components/newtab/lib/ASRouter.jsm
browser/components/newtab/lib/BookmarkPanelHub.jsm
browser/themes/shared/places/editBookmarkPanel.inc.css
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -3,16 +3,19 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // This file is loaded into the browser window scope.
 /* eslint-env mozilla/browser-window */
 
 XPCOMUtils.defineLazyScriptGetter(this, ["PlacesToolbar", "PlacesMenu",
                                          "PlacesPanelview", "PlacesPanelMenuView"],
                                   "chrome://browser/content/places/browserPlacesViews.js");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  BookmarkPanelHub: "resource://activity-stream/lib/BookmarkPanelHub.jsm",
+});
 
 var StarUI = {
   _itemGuids: null,
   _batching: false,
   _isNewBookmark: false,
   _isComposing: false,
   _autoCloseTimer: 0,
   // The autoclose timer is diasbled if the user interacts with the
@@ -177,16 +180,31 @@ var StarUI = {
             }
           }, delay);
           this._autoCloseTimerEnabled = true;
         }
         break;
     }
   },
 
+  getRecommendation(data) {
+    return BookmarkPanelHub.messageRequest(data, window);
+  },
+
+  toggleRecommendation(visible) {
+    const info = this._element("editBookmarkPanelInfoButton");
+    info.checked = visible !== undefined ? !!visible : !info.checked;
+    const recommendation = this._element("editBookmarkPanelRecommendation");
+    if (info.checked) {
+      recommendation.removeAttribute("disabled");
+    } else {
+      recommendation.setAttribute("disabled", "disabled");
+    }
+  },
+
   async showEditBookmarkPopup(aNode, aIsNewBookmark, aUrl) {
     // Slow double-clicks (not true double-clicks) shouldn't
     // cause the panel to flicker.
     if (this.panel.state != "closed") {
       return;
     }
 
     this._isNewBookmark = aIsNewBookmark;
@@ -218,16 +236,31 @@ var StarUI = {
                             .replace("#1", bookmarksCount);
       removeButton.label = label;
       removeButton.setAttribute("accesskey",
         gNavigatorBundle.getString("editBookmark.removeBookmarks.accesskey"));
     }
 
     this._setIconAndPreviewImage();
 
+    const showRecommendation = await this.getRecommendation({
+      container: this._element("editBookmarkPanelRecommendation"),
+      createElement: elem => document.createElementNS("http://www.w3.org/1999/xhtml", elem),
+      url: aUrl.href,
+      close: e => {
+        e.stopPropagation();
+        this.toggleRecommendation(false);
+      },
+      hidePopup: () => {
+        this.panel.hidePopup();
+      },
+    });
+    this._element("editBookmarkPanelInfoButton").disabled = !showRecommendation;
+    this.toggleRecommendation(showRecommendation);
+
     this.beginBatch();
 
     this._anchorElement = BookmarkingUI.anchor;
     this._anchorElement.setAttribute("open", "true");
 
     let onPanelReady = fn => {
       let target = this.panel;
       if (target.parentNode) {
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -268,21 +268,27 @@
            type="arrow"
            orient="vertical"
            ignorekeys="true"
            hidden="true"
            tabspecific="true"
            aria-labelledby="editBookmarkPanelTitle">
       <box class="panel-header">
         <label id="editBookmarkPanelTitle"/>
+        <toolbarbutton id="editBookmarkPanelInfoButton" oncommand="StarUI.toggleRecommendation();" >
+          <image/>
+        </toolbarbutton>
       </box>
-      <html:div id="editBookmarkPanelFaviconContainer">
-        <html:img id="editBookmarkPanelFavicon"/>
+      <html:div id="editBookmarkPanelInfoArea">
+        <html:div id="editBookmarkPanelRecommendation"></html:div>
+        <html:div id="editBookmarkPanelFaviconContainer">
+          <html:img id="editBookmarkPanelFavicon"/>
+        </html:div>
+        <html:div id="editBookmarkPanelImage"></html:div>
       </html:div>
-      <box id="editBookmarkPanelImage"/>
 #include ../../components/places/content/editBookmarkPanel.inc.xul
       <vbox id="editBookmarkPanelBottomContent"
             flex="1">
         <checkbox id="editBookmarkPanel_showForNewBookmarks"
                   label="&editBookmark.showForNewBookmarks.label;"
                   accesskey="&editBookmark.showForNewBookmarks.accesskey;"
                   oncommand="StarUI.onShowForNewBookmarksCheckboxCommand();"/>
       </vbox>
--- a/browser/components/newtab/lib/ASRouter.jsm
+++ b/browser/components/newtab/lib/ASRouter.jsm
@@ -7,16 +7,17 @@ const {Services} = ChromeUtils.import("r
 const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
 XPCOMUtils.defineLazyModuleGetters(this, {
   AddonManager: "resource://gre/modules/AddonManager.jsm",
   UITour: "resource:///modules/UITour.jsm",
   FxAccounts: "resource://gre/modules/FxAccounts.jsm",
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   OS: "resource://gre/modules/osfile.jsm",
+  BookmarkPanelHub: "resource://activity-stream/lib/BookmarkPanelHub.jsm",
 });
 const {ASRouterActions: ra, actionTypes: at, actionCreators: ac} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm");
 const {CFRMessageProvider} = ChromeUtils.import("resource://activity-stream/lib/CFRMessageProvider.jsm");
 const {OnboardingMessageProvider} = ChromeUtils.import("resource://activity-stream/lib/OnboardingMessageProvider.jsm");
 const {SnippetsTestMessageProvider} = ChromeUtils.import("resource://activity-stream/lib/SnippetsTestMessageProvider.jsm");
 const {RemoteSettings} = ChromeUtils.import("resource://services-settings/remote-settings.js");
 const {CFRPageActions} = ChromeUtils.import("resource://activity-stream/lib/CFRPageActions.jsm");
 
@@ -514,16 +515,17 @@ class _ASRouter {
     this.messageChannel.addMessageListener(INCOMING_MESSAGE_NAME, this.onMessage);
     this._storage = storage;
     this.WHITELIST_HOSTS = this._loadSnippetsWhitelistHosts();
     this.dispatchToAS = dispatchToAS;
     this.dispatch = this.dispatch.bind(this);
 
     ASRouterPreferences.init();
     ASRouterPreferences.addListener(this.onPrefChange);
+    BookmarkPanelHub.init(this.dispatch);
 
     const messageBlockList = await this._storage.get("messageBlockList") || [];
     const providerBlockList = await this._storage.get("providerBlockList") || [];
     const messageImpressions = await this._storage.get("messageImpressions") || {};
     const providerImpressions = await this._storage.get("providerImpressions") || {};
     const previousSessionEnd = await this._storage.get("previousSessionEnd") || 0;
     await this.setState({messageBlockList, providerBlockList, messageImpressions, providerImpressions, previousSessionEnd});
     this._updateMessageProviders();
@@ -542,16 +544,17 @@ class _ASRouter {
 
     this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_ALL"});
     this.messageChannel.removeMessageListener(INCOMING_MESSAGE_NAME, this.onMessage);
     this.messageChannel = null;
     this.dispatchToAS = null;
 
     ASRouterPreferences.removeListener(this.onPrefChange);
     ASRouterPreferences.uninit();
+    BookmarkPanelHub.uninit();
 
     // Uninitialise all trigger listeners
     for (const listener of ASRouterTriggerListeners.values()) {
       listener.uninit();
     }
     // If we added any CFR recommendations, they need to be removed
     CFRPageActions.clearRecommendations();
     this._resetInitialization();
@@ -896,16 +899,24 @@ class _ASRouter {
         messages: state.messages.filter(m => m.id !== message.id),
       }));
     } else {
       await this.setState({lastMessageId: message ? message.id : null});
     }
     await this._sendMessageToTarget(message, target, trigger);
   }
 
+  async handleMessageRequest(trigger, target) {
+    const msgs = this._getUnblockedMessages();
+    const message = await this._findMessage(
+      msgs.filter(m => m.trigger && m.trigger.id === trigger.id),
+      trigger);
+    target.sendMessage({message});
+  }
+
   async setMessageById(id, target, force = true, action = {}) {
     await this.setState({lastMessageId: id});
     const newMessage = this.getMessageById(id);
 
     await this._sendMessageToTarget(newMessage, target, action.data, force);
   }
 
   async blockMessageById(idOrIds) {
@@ -1094,19 +1105,28 @@ class _ASRouter {
         await MessageLoaderUtils.installAddonFromURL(target.browser, action.data.url);
         break;
       case ra.PIN_CURRENT_TAB:
         let tab = target.browser.ownerGlobal.gBrowser.selectedTab;
         target.browser.ownerGlobal.gBrowser.pinTab(tab);
         target.browser.ownerGlobal.ConfirmationHint.show(tab, "pinTab", {showDescription: true});
         break;
       case ra.SHOW_FIREFOX_ACCOUNTS:
-        const url = await FxAccounts.config.promiseSignUpURI("snippets");
-        // We want to replace the current tab.
-        target.browser.ownerGlobal.openLinkIn(url, "current", {
+        let url;
+        const entrypoint = action.entrypoint || "snippets";
+        switch (action.method) {
+          case "emailFirst":
+            url = await FxAccounts.config.promiseEmailFirstURI(entrypoint);
+            break;
+          default:
+            url = await FxAccounts.config.promiseSignUpURI(entrypoint);
+            break;
+        }
+        const where = action.where || "current";
+        target.browser.ownerGlobal.openLinkIn(url, where, {
           private: false,
           triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({}),
           csp: null,
         });
         break;
     }
   }
 
@@ -1217,16 +1237,23 @@ class _ASRouter {
       case "SET_PROVIDER_USER_PREF":
         ASRouterPreferences.setUserPreference(action.data.id, action.data.value);
         break;
       case "EVALUATE_JEXL_EXPRESSION":
         this.evaluateExpression(target, action.data);
         break;
       case "FORCE_ATTRIBUTION":
         this.forceAttribution(action.data);
+        break;
+      case "MESSAGE_REQUEST":
+        this.handleMessageRequest(action.data.trigger, target);
+        break;
+      default:
+        Cu.reportError("Unknown message received");
+        break;
     }
   }
 }
 this._ASRouter = _ASRouter;
 
 /**
  * ASRouter - singleton instance of _ASRouter that controls all messages
  * in the new tab page.
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/lib/BookmarkPanelHub.jsm
@@ -0,0 +1,145 @@
+/* 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";
+
+ChromeUtils.defineModuleGetter(this, "DOMLocalization",
+  "resource://gre/modules/DOMLocalization.jsm");
+
+class _BookmarkPanelHub {
+  constructor() {
+    this._id = "BookmarkPanelHub";
+    this._dispatchToASR = null;
+    this._initalized = false;
+    this._response = null;
+    this._l10n = null;
+
+    this.dispatch = this.dispatch.bind(this);
+    this.messageRequest = this.messageRequest.bind(this);
+  }
+
+  init(dispatch) {
+    this._dispatchToASR = dispatch;
+    this._l10n = new DOMLocalization([
+      "browser/newtab/asrouter.ftl",
+    ]);
+    this._initalized = true;
+  }
+
+  uninit() {
+    this._l10n = null;
+    this._dispatchToASR = null;
+    this._initalized = false;
+  }
+
+  dispatch(message, target) {
+    return this._dispatchToASR(message, target);
+  }
+
+  /**
+   * Checks if a similar cached requests exists before forwarding the request
+   * to ASRouter. Caches only 1 request, unique identifier is `request.url`.
+   * Caching ensures we don't duplicate requests and telemetry pings.
+   */
+  async messageRequest(target, win) {
+    if (this._request && this._message.url === target.url) {
+      this.onResponse(this._response, target);
+      return !!this._response;
+    }
+
+    const waitForASRMessage = new Promise((resolve, reject) => {
+      this.dispatch({
+        type: "MESSAGE_REQUEST",
+        source: this._id,
+        data: {trigger: {id: "bookmark-panel"}},
+      }, {sendMessage: resolve});
+    });
+
+    const response = await waitForASRMessage;
+    this.onResponse(response, target, win);
+    return !!this._response;
+  }
+
+  /**
+   * If the response contains a message render it and send an impression.
+   * Otherwise we remove the message from the container.
+   */
+  onResponse(response, target, win) {
+    if (response && response.message) {
+      this._response = {
+        ...response,
+        url: target.url,
+      };
+
+      this.showMessage(response.message.content, target, win);
+      this.sendImpression();
+    } else {
+      // If we didn't get a message response we need to remove and clear
+      // any existing messages
+      this._response = null;
+      this.hideMessage(target);
+    }
+  }
+
+  showMessage(message, target, win) {
+    const {createElement} = target;
+    if (!target.container.querySelector("#cfrMessageContainer")) {
+      const recommendation = createElement("div");
+      recommendation.setAttribute("id", "cfrMessageContainer");
+      recommendation.addEventListener("click", e => {
+        target.hidePopup();
+        this._dispatchToASR(
+          {type: "USER_ACTION", data: {
+            type: "SHOW_FIREFOX_ACCOUNTS",
+            entrypoint: "bookmark",
+            method: "emailFirst",
+            where: "tabshifted"
+          }},
+          {browser: win.gBrowser.selectedBrowser});
+      });
+      recommendation.style.color = message.color;
+      recommendation.style.background = `-moz-linear-gradient(-45deg, ${message.background_color_1} 0%, ${message.background_color_2} 70%)`;
+      const close = createElement("a");
+      close.setAttribute("id", "cfrClose");
+      close.setAttribute("aria-label", "close"); // TODO localize
+      close.addEventListener("click", target.close);
+      const title = createElement("h1");
+      title.setAttribute("id", "editBookmarkPanelRecommendationTitle");
+      title.textContent = "Sync your bookmarks everywhere.";
+      this._l10n.setAttributes(title, message.title);
+      const content = createElement("p");
+      content.setAttribute("id", "editBookmarkPanelRecommendationContent");
+      content.textContent = "Great find! Now don’t be left without this bookmark on your mobile devices. Get started with a Firefox Account.";
+      this._l10n.setAttributes(content, message.text);
+      const cta = createElement("button");
+      cta.setAttribute("id", "editBookmarkPanelRecommendationCta");
+      cta.textContent = "Sync my bookmarks …";
+      this._l10n.setAttributes(cta, message.cta);
+      recommendation.appendChild(close);
+      recommendation.appendChild(title);
+      recommendation.appendChild(content);
+      recommendation.appendChild(cta);
+      // this._l10n.translateElements([...recommendation.children]);
+      target.container.appendChild(recommendation);
+    }
+  }
+
+  hideMessage(target) {
+    const container = target.container.querySelector("#cfrMessageContainer");
+    if (container) {
+      container.remove();
+    }
+  }
+
+  sendImpression() {
+    this.dispatch({
+      type: "IMPRESSION",
+      source: this._id,
+      data: {},
+    });
+  }
+}
+
+this.BookmarkPanelHub = new _BookmarkPanelHub();
+
+const EXPORTED_SYMBOLS = ["BookmarkPanelHub"];
--- a/browser/themes/shared/places/editBookmarkPanel.inc.css
+++ b/browser/themes/shared/places/editBookmarkPanel.inc.css
@@ -3,16 +3,125 @@
  * 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/. */
 %endif
 
 #editBookmarkPanel > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 0;
 }
 
+#editBookmarkPanelTitle {
+  margin-inline-start: 32px;
+}
+
+#editBookmarkPanelInfoButton {
+  border-radius: var(--toolbarbutton-border-radius);
+  margin-right: 4px;
+}
+
+#editBookmarkPanelInfoButton[disabled=true] {
+  visibility: hidden;
+}
+
+#editBookmarkPanelInfoButton[checked] {
+  background-color: var(--toolbarbutton-active-background);
+}
+
+#editBookmarkPanelInfoButton:hover {
+  background-color: var(--toolbarbutton-hover-background);
+}
+
+#editBookmarkPanelInfoButton > image {
+  list-style-image: url(chrome://browser/skin/identity-icon.svg);
+  -moz-context-properties: fill, fill-opacity;
+  fill: currentColor;
+  fill-opacity: var(--toolbarbutton-icon-fill-opacity);
+  padding: 2px;
+}
+
+#editBookmarkPanelInfoArea {
+  overflow: hidden;
+  position: relative;
+  margin: 6px 8px 0 8px;
+}
+
+#editBookmarkPanelRecommendation {
+  position: absolute;
+  height: 100%;
+  transition: all 0.25s cubic-bezier(0.07, 0.95, 0, 1);
+}
+
+#editBookmarkPanelRecommendation[disabled] {
+  transform: translateY(-100%);
+}
+
+#editBookmarkPanelRecommendation > div {
+  border-radius: 2px;
+  display: flex;
+  flex-direction: column;
+  text-align: left;
+  height: 150px;
+  cursor: pointer;
+  position: relative;
+  padding: 0 16px;
+}
+
+#editBookmarkPanelRecommendation > div::-moz-focus-inner {
+  border: none;
+}
+
+#editBookmarkPanelRecommendation > div * {
+  max-width: 220px;
+}
+
+#editBookmarkPanelRecommendationTitle {
+  font-size: 1.5rem;
+  font-weight: 400;
+  line-height: 1.25;
+  margin-bottom: 6px;
+  padding-top: 2px;
+}
+
+#editBookmarkPanelRecommendationContent {
+  font-size: 1rem;
+  line-height: 1.5;
+  margin: 0;
+}
+
+#editBookmarkPanelRecommendationCta {
+  -moz-appearance: none;
+  background: transparent;
+  border: none;
+  color: inherit;
+  cursor: pointer;
+  font-size: 1rem;
+  font-weight: 700;
+  margin: auto 0;
+  padding: 0;
+  text-align: start;
+}
+
+#editBookmarkPanelRecommendation #cfrClose {
+  position: absolute;
+  right: 18px;
+  top: 18px;
+  width: 12px;
+  height: 12px;
+  background-image: url(chrome://browser/skin/stop.svg);
+  background-size: 12px;
+  background-repeat: no-repeat;
+  -moz-context-properties: fill, fill-opacity;
+  fill: currentColor;
+  fill-opacity: 0.6;
+}
+
+#editBookmarkPanelRecommendation #cfrClose:hover {
+  fill-opacity: 1;
+}
+
 html|div#editBookmarkPanelFaviconContainer {
   display: flex;
 }
 
 html|img#editBookmarkPanelFavicon[src] {
   box-sizing: content-box;
   width: 32px;
   height: 32px;
@@ -21,21 +130,22 @@ html|img#editBookmarkPanelFavicon[src] {
   box-shadow: inset 0 0 0 1px rgba(0,0,0,.1);
   border-radius: 6px;
   margin-top: 10px;
   margin-inline-start: 10px;
   margin-bottom: -52px; /* margin-top + paddings + height */
 }
 
 #editBookmarkPanelImage {
-  border-bottom: 1px solid var(--panel-separator-color);
+  border-radius: 2px;
   height: 150px;
   background-image: -moz-element(#editBookmarkPanelImageCanvas);
   background-repeat: no-repeat;
   background-size: cover;
+  margin: 0 2px;
 }
 
 #editBookmarkPanelRows,
 #editBookmarkPanelBottomContent {
   padding: var(--arrowpanel-padding);
 }
 
 #editBookmarkPanelRows {