BookmarkPanelHub.jsm 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  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. "use strict";
  5. ChromeUtils.defineModuleGetter(this, "DOMLocalization",
  6. "resource://gre/modules/DOMLocalization.jsm");
  7. ChromeUtils.defineModuleGetter(this, "FxAccounts",
  8. "resource://gre/modules/FxAccounts.jsm");
  9. ChromeUtils.defineModuleGetter(this, "Services",
  10. "resource://gre/modules/Services.jsm");
  11. ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
  12. "resource://gre/modules/PrivateBrowsingUtils.jsm");
  13. class _BookmarkPanelHub {
  14. constructor() {
  15. this._id = "BookmarkPanelHub";
  16. this._trigger = {id: "bookmark-panel"};
  17. this._handleMessageRequest = null;
  18. this._addImpression = null;
  19. this._dispatch = null;
  20. this._initialized = false;
  21. this._response = null;
  22. this._l10n = null;
  23. this.messageRequest = this.messageRequest.bind(this);
  24. this.toggleRecommendation = this.toggleRecommendation.bind(this);
  25. this.sendUserEventTelemetry = this.sendUserEventTelemetry.bind(this);
  26. this.collapseMessage = this.collapseMessage.bind(this);
  27. }
  28. /**
  29. * @param {function} handleMessageRequest
  30. * @param {function} addImpression
  31. * @param {function} dispatch - Used for sending user telemetry information
  32. */
  33. init(handleMessageRequest, addImpression, dispatch) {
  34. this._handleMessageRequest = handleMessageRequest;
  35. this._addImpression = addImpression;
  36. this._dispatch = dispatch;
  37. this._l10n = new DOMLocalization();
  38. this._initialized = true;
  39. }
  40. uninit() {
  41. this._l10n = null;
  42. this._initialized = false;
  43. this._handleMessageRequest = null;
  44. this._addImpression = null;
  45. this._dispatch = null;
  46. this._response = null;
  47. }
  48. /**
  49. * Checks if a similar cached requests exists before forwarding the request
  50. * to ASRouter. Caches only 1 request, unique identifier is `request.url`.
  51. * Caching ensures we don't duplicate requests and telemetry pings.
  52. * Return value is important for the caller to know if a message will be
  53. * shown.
  54. *
  55. * @returns {obj|null} response object or null if no messages matched
  56. */
  57. async messageRequest(target, win) {
  58. if (!this._initialized) {
  59. return false;
  60. }
  61. if (this._response && this._response.win === win && this._response.url === target.url && this._response.content) {
  62. this.showMessage(this._response.content, target, win);
  63. return true;
  64. }
  65. // If we didn't match on a previously cached request then make sure
  66. // the container is empty
  67. this._removeContainer(target);
  68. const response = await this._handleMessageRequest(this._trigger);
  69. return this.onResponse(response, target, win);
  70. }
  71. /**
  72. * If the response contains a message render it and send an impression.
  73. * Otherwise we remove the message from the container.
  74. */
  75. onResponse(response, target, win) {
  76. this._response = {
  77. ...response,
  78. collapsed: false,
  79. target,
  80. win,
  81. url: target.url,
  82. };
  83. if (response && response.content) {
  84. // Only insert localization files if we need to show a message
  85. win.MozXULElement.insertFTLIfNeeded("browser/newtab/asrouter.ftl");
  86. win.MozXULElement.insertFTLIfNeeded("browser/branding/sync-brand.ftl");
  87. this.showMessage(response.content, target, win);
  88. this.sendImpression();
  89. this.sendUserEventTelemetry("IMPRESSION", win);
  90. } else {
  91. this.hideMessage(target);
  92. }
  93. target.infoButton.disabled = !response;
  94. return !!response;
  95. }
  96. showMessage(message, target, win) {
  97. if (this._response && this._response.collapsed) {
  98. this.toggleRecommendation(false);
  99. return;
  100. }
  101. const createElement = elem => target.document.createElementNS("http://www.w3.org/1999/xhtml", elem);
  102. if (!target.container.querySelector("#cfrMessageContainer")) {
  103. const recommendation = createElement("div");
  104. recommendation.setAttribute("id", "cfrMessageContainer");
  105. recommendation.addEventListener("click", async e => {
  106. target.hidePopup();
  107. const url = await FxAccounts.config.promiseEmailFirstURI("bookmark");
  108. win.ownerGlobal.openLinkIn(url, "tabshifted", {
  109. private: false,
  110. triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({}),
  111. csp: null,
  112. });
  113. this.sendUserEventTelemetry("CLICK", win);
  114. });
  115. recommendation.style.color = message.color;
  116. recommendation.style.background = `-moz-linear-gradient(-45deg, ${message.background_color_1} 0%, ${message.background_color_2} 70%)`;
  117. const close = createElement("button");
  118. close.setAttribute("id", "cfrClose");
  119. close.setAttribute("aria-label", "close");
  120. close.style.color = message.color;
  121. close.addEventListener("click", e => {
  122. this.sendUserEventTelemetry("DISMISS", win);
  123. this.collapseMessage();
  124. target.close(e);
  125. });
  126. const title = createElement("h1");
  127. title.setAttribute("id", "editBookmarkPanelRecommendationTitle");
  128. const content = createElement("p");
  129. content.setAttribute("id", "editBookmarkPanelRecommendationContent");
  130. const cta = createElement("button");
  131. cta.setAttribute("id", "editBookmarkPanelRecommendationCta");
  132. // If `string_id` is present it means we are relying on fluent for translations
  133. if (message.text.string_id) {
  134. this._l10n.setAttributes(close, message.close_button.tooltiptext.string_id);
  135. this._l10n.setAttributes(title, message.title.string_id);
  136. this._l10n.setAttributes(content, message.text.string_id);
  137. this._l10n.setAttributes(cta, message.cta.string_id);
  138. } else {
  139. close.setAttribute("title", message.close_button.tooltiptext);
  140. title.textContent = message.title;
  141. content.textContent = message.text;
  142. cta.textContent = message.cta;
  143. }
  144. recommendation.appendChild(close);
  145. recommendation.appendChild(title);
  146. recommendation.appendChild(content);
  147. recommendation.appendChild(cta);
  148. target.container.appendChild(recommendation);
  149. }
  150. this.toggleRecommendation(true);
  151. }
  152. toggleRecommendation(visible) {
  153. if (!this._response) {
  154. return;
  155. }
  156. const {target} = this._response;
  157. if (visible === undefined) {
  158. // When called from the info button of the bookmark panel
  159. target.infoButton.checked = !target.infoButton.checked;
  160. } else {
  161. target.infoButton.checked = visible;
  162. }
  163. if (target.infoButton.checked) {
  164. // If it was ever collapsed we need to cancel the state
  165. this._response.collapsed = false;
  166. target.container.removeAttribute("disabled");
  167. } else {
  168. target.container.setAttribute("disabled", "disabled");
  169. }
  170. }
  171. collapseMessage() {
  172. this._response.collapsed = true;
  173. this.toggleRecommendation(false);
  174. }
  175. _removeContainer(target) {
  176. if (target || (this._response && this._response.target)) {
  177. const container = (target || this._response.target).container.querySelector("#cfrMessageContainer");
  178. if (container) {
  179. container.remove();
  180. }
  181. }
  182. }
  183. hideMessage(target) {
  184. this._removeContainer(target);
  185. this.toggleRecommendation(false);
  186. this._response = null;
  187. }
  188. _forceShowMessage(target, message) {
  189. const doc = target.browser.ownerGlobal.gBrowser.ownerDocument;
  190. const win = target.browser.ownerGlobal.window;
  191. const panelTarget = {
  192. container: doc.getElementById("editBookmarkPanelRecommendation"),
  193. infoButton: doc.getElementById("editBookmarkPanelInfoButton"),
  194. document: doc,
  195. close: e => {
  196. e.stopPropagation();
  197. this.toggleRecommendation(false);
  198. },
  199. };
  200. // Remove any existing message
  201. this.hideMessage(panelTarget);
  202. // Reset the reference to the panel elements
  203. this._response = {target: panelTarget};
  204. // Required if we want to preview messages that include fluent strings
  205. win.MozXULElement.insertFTLIfNeeded("browser/newtab/asrouter.ftl");
  206. win.MozXULElement.insertFTLIfNeeded("browser/branding/sync-brand.ftl");
  207. this.showMessage(message.content, panelTarget, win);
  208. }
  209. sendImpression() {
  210. this._addImpression(this._response);
  211. }
  212. sendUserEventTelemetry(event, win) {
  213. // Only send pings for non private browsing windows
  214. if (!PrivateBrowsingUtils.isBrowserPrivate(win.ownerGlobal.gBrowser.selectedBrowser)) {
  215. this._sendTelemetry({message_id: this._response.id, bucket_id: this._response.id, event});
  216. }
  217. }
  218. _sendTelemetry(ping) {
  219. this._dispatch({
  220. type: "DOORHANGER_TELEMETRY",
  221. data: {action: "cfr_user_event", source: "CFR", ...ping},
  222. });
  223. }
  224. }
  225. this._BookmarkPanelHub = _BookmarkPanelHub;
  226. /**
  227. * BookmarkPanelHub - singleton instance of _BookmarkPanelHub that can initiate
  228. * message requests and render messages.
  229. */
  230. this.BookmarkPanelHub = new _BookmarkPanelHub();
  231. const EXPORTED_SYMBOLS = ["BookmarkPanelHub", "_BookmarkPanelHub"];