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 };