123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368 |
- import {addLocaleData, IntlProvider} from "react-intl";
- import {actionCreators as ac} from "common/Actions.jsm";
- import {OUTGOING_MESSAGE_NAME as AS_GENERAL_OUTGOING_MESSAGE_NAME} from "content-src/lib/init-store";
- import {generateBundles} from "./rich-text-strings";
- import {ImpressionsWrapper} from "./components/ImpressionsWrapper/ImpressionsWrapper";
- import {LocalizationProvider} from "fluent-react";
- import {NEWTAB_DARK_THEME} from "content-src/lib/constants";
- import {OnboardingMessage} from "./templates/OnboardingMessage/OnboardingMessage";
- import React from "react";
- import ReactDOM from "react-dom";
- import {ReturnToAMO} from "./templates/ReturnToAMO/ReturnToAMO";
- import {SnippetsTemplates} from "./templates/template-manifest";
- import {StartupOverlay} from "./templates/StartupOverlay/StartupOverlay";
- import {Trailhead} from "./templates/Trailhead/Trailhead";
- const INCOMING_MESSAGE_NAME = "ASRouter:parent-to-child";
- const OUTGOING_MESSAGE_NAME = "ASRouter:child-to-parent";
- const TEMPLATES_ABOVE_PAGE = ["trailhead"];
- const TEMPLATES_BELOW_SEARCH = ["simple_below_search_snippet"];
- export const ASRouterUtils = {
- addListener(listener) {
- if (global.RPMAddMessageListener) {
- global.RPMAddMessageListener(INCOMING_MESSAGE_NAME, listener);
- }
- },
- removeListener(listener) {
- if (global.RPMRemoveMessageListener) {
- global.RPMRemoveMessageListener(INCOMING_MESSAGE_NAME, listener);
- }
- },
- sendMessage(action) {
- if (global.RPMSendAsyncMessage) {
- global.RPMSendAsyncMessage(OUTGOING_MESSAGE_NAME, action);
- }
- },
- blockById(id, options) {
- ASRouterUtils.sendMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id, ...options}});
- },
- dismissById(id) {
- ASRouterUtils.sendMessage({type: "DISMISS_MESSAGE_BY_ID", data: {id}});
- },
- dismissBundle(bundle) {
- ASRouterUtils.sendMessage({type: "DISMISS_BUNDLE", data: {bundle}});
- },
- executeAction(button_action) {
- ASRouterUtils.sendMessage({
- type: "USER_ACTION",
- data: button_action,
- });
- },
- unblockById(id) {
- ASRouterUtils.sendMessage({type: "UNBLOCK_MESSAGE_BY_ID", data: {id}});
- },
- unblockBundle(bundle) {
- ASRouterUtils.sendMessage({type: "UNBLOCK_BUNDLE", data: {bundle}});
- },
- overrideMessage(id) {
- ASRouterUtils.sendMessage({type: "OVERRIDE_MESSAGE", data: {id}});
- },
- sendTelemetry(ping) {
- if (global.RPMSendAsyncMessage) {
- const payload = ac.ASRouterUserEvent(ping);
- global.RPMSendAsyncMessage(AS_GENERAL_OUTGOING_MESSAGE_NAME, payload);
- }
- },
- getPreviewEndpoint() {
- if (global.location && global.location.href.includes("endpoint")) {
- const params = new URLSearchParams(global.location.href.slice(global.location.href.indexOf("endpoint")));
- try {
- const endpoint = new URL(params.get("endpoint"));
- return {
- url: endpoint.href,
- snippetId: params.get("snippetId"),
- theme: this.getPreviewTheme(),
- };
- } catch (e) {}
- }
- return null;
- },
- getPreviewTheme() {
- return new URLSearchParams(global.location.href.slice(global.location.href.indexOf("theme"))).get("theme");
- },
- };
- // Note: nextProps/prevProps refer to props passed to <ImpressionsWrapper />, not <ASRouterUISurface />
- function shouldSendImpressionOnUpdate(nextProps, prevProps) {
- return (nextProps.message.id && (!prevProps.message || prevProps.message.id !== nextProps.message.id));
- }
- export class ASRouterUISurface extends React.PureComponent {
- constructor(props) {
- super(props);
- this.onMessageFromParent = this.onMessageFromParent.bind(this);
- this.sendClick = this.sendClick.bind(this);
- this.sendImpression = this.sendImpression.bind(this);
- this.sendUserActionTelemetry = this.sendUserActionTelemetry.bind(this);
- this.state = {message: {}, bundle: {}};
- if (props.document) {
- this.headerPortal = props.document.getElementById("header-asrouter-container");
- this.footerPortal = props.document.getElementById("footer-asrouter-container");
- }
- }
- sendUserActionTelemetry(extraProps = {}) {
- const {message, bundle} = this.state;
- if (!message && !extraProps.message_id) {
- throw new Error(`You must provide a message_id for bundled messages`);
- }
- // snippets_user_event, onboarding_user_event
- const eventType = `${message.provider || bundle.provider}_user_event`;
- ASRouterUtils.sendTelemetry({
- message_id: message.id || extraProps.message_id,
- source: extraProps.id,
- action: eventType,
- ...extraProps,
- });
- }
- sendImpression(extraProps) {
- if (this.state.message.provider === "preview") {
- return;
- }
- ASRouterUtils.sendMessage({type: "IMPRESSION", data: this.state.message});
- this.sendUserActionTelemetry({event: "IMPRESSION", ...extraProps});
- }
- // If link has a `metric` data attribute send it as part of the `value`
- // telemetry field which can have arbitrary values.
- // Used for router messages with links as part of the content.
- sendClick(event) {
- const metric = {
- value: event.target.dataset.metric,
- // Used for the `source` of the event. Needed to differentiate
- // from other snippet or onboarding events that may occur.
- id: "NEWTAB_FOOTER_BAR_CONTENT",
- };
- const action = {
- type: event.target.dataset.action,
- data: {args: event.target.dataset.args},
- };
- if (action.type) {
- ASRouterUtils.executeAction(action);
- }
- if (!this.state.message.content.do_not_autoblock && !event.target.dataset.do_not_autoblock) {
- ASRouterUtils.blockById(this.state.message.id);
- }
- if (this.state.message.provider !== "preview") {
- this.sendUserActionTelemetry({event: "CLICK_BUTTON", ...metric});
- }
- }
- onBlockById(id) {
- return options => ASRouterUtils.blockById(id, options);
- }
- onDismissById(id) {
- return () => ASRouterUtils.dismissById(id);
- }
- dismissBundle(bundle) {
- return () => {
- ASRouterUtils.dismissBundle(bundle);
- this.sendUserActionTelemetry({
- event: "DISMISS",
- id: "onboarding-cards",
- message_id: bundle.map(m => m.id).join(","),
- // Passing the action because some bundles (Trailhead) don't have a provider set
- action: "onboarding_user_event",
- });
- };
- }
- triggerOnboarding() {
- ASRouterUtils.sendMessage({type: "TRIGGER", data: {trigger: {id: "showOnboarding"}}});
- }
- clearMessage(id) {
- if (id === this.state.message.id) {
- this.setState({message: {}});
- // Remove any styles related to the RTAMO message
- document.body.classList.remove("welcome", "hide-main", "amo");
- }
- }
- onMessageFromParent({data: action}) {
- switch (action.type) {
- case "SET_MESSAGE":
- this.setState({message: action.data});
- break;
- case "SET_BUNDLED_MESSAGES":
- this.setState({bundle: action.data});
- break;
- case "CLEAR_MESSAGE":
- this.clearMessage(action.data.id);
- break;
- case "CLEAR_PROVIDER":
- if (action.data.id === this.state.message.provider) {
- this.setState({message: {}});
- }
- break;
- case "CLEAR_BUNDLE":
- if (this.state.bundle.bundle) {
- this.setState({bundle: {}});
- }
- break;
- case "CLEAR_ALL":
- this.setState({message: {}, bundle: {}});
- break;
- case "AS_ROUTER_TARGETING_UPDATE":
- action.data.forEach(id => this.clearMessage(id));
- break;
- }
- }
- componentWillMount() {
- if (global.document) {
- // Add locale data for StartupOverlay because it uses react-intl
- addLocaleData(global.document.documentElement.lang);
- }
- const endpoint = ASRouterUtils.getPreviewEndpoint();
- if (endpoint && endpoint.theme === "dark") {
- global.window.dispatchEvent(new CustomEvent("LightweightTheme:Set", {detail: {data: NEWTAB_DARK_THEME}}));
- }
- ASRouterUtils.addListener(this.onMessageFromParent);
- // If we are loading about:welcome we want to trigger the onboarding messages
- if (this.props.document && this.props.document.location.href === "about:welcome") {
- ASRouterUtils.sendMessage({type: "TRIGGER", data: {trigger: {id: "firstRun"}}});
- } else {
- ASRouterUtils.sendMessage({type: "SNIPPETS_REQUEST", data: {endpoint}});
- }
- }
- componentWillUnmount() {
- ASRouterUtils.removeListener(this.onMessageFromParent);
- }
- renderSnippets() {
- if (this.state.bundle.template === "onboarding" ||
- this.state.message.template === "fxa_overlay" ||
- this.state.message.template === "return_to_amo_overlay" ||
- this.state.message.template === "trailhead") {
- return null;
- }
- const SnippetComponent = SnippetsTemplates[this.state.message.template];
- const {content} = this.state.message;
- return (
- <ImpressionsWrapper
- id="NEWTAB_FOOTER_BAR"
- message={this.state.message}
- sendImpression={this.sendImpression}
- shouldSendImpressionOnUpdate={shouldSendImpressionOnUpdate}
- // This helps with testing
- document={this.props.document}>
- <LocalizationProvider bundles={generateBundles(content)}>
- <SnippetComponent
- {...this.state.message}
- UISurface="NEWTAB_FOOTER_BAR"
- onBlock={this.onBlockById(this.state.message.id)}
- onDismiss={this.onDismissById(this.state.message.id)}
- onAction={ASRouterUtils.executeAction}
- sendClick={this.sendClick}
- sendUserActionTelemetry={this.sendUserActionTelemetry} />
- </LocalizationProvider>
- </ImpressionsWrapper>);
- }
- renderOnboarding() {
- if (this.state.bundle.template === "onboarding") {
- return (
- <OnboardingMessage
- {...this.state.bundle}
- UISurface="NEWTAB_OVERLAY"
- onAction={ASRouterUtils.executeAction}
- onDismissBundle={this.dismissBundle(this.state.bundle.bundle)}
- sendUserActionTelemetry={this.sendUserActionTelemetry} />);
- }
- return null;
- }
- renderFirstRunOverlay() {
- const {message} = this.state;
- if (message.template === "fxa_overlay") {
- global.document.body.classList.add("fxa");
- return (
- <IntlProvider locale={global.document.documentElement.lang} messages={global.gActivityStreamStrings}>
- <StartupOverlay
- onReady={this.triggerOnboarding}
- onBlock={this.onDismissById(message.id)}
- dispatch={this.props.dispatch} />
- </IntlProvider>
- );
- } else if (message.template === "return_to_amo_overlay") {
- global.document.body.classList.add("amo");
- return (
- <LocalizationProvider messages={generateBundles({"amo_html": message.content.text})}>
- <ReturnToAMO
- {...message}
- UISurface="NEWTAB_OVERLAY"
- onReady={this.triggerOnboarding}
- onBlock={this.onDismissById(message.id)}
- onAction={ASRouterUtils.executeAction}
- sendUserActionTelemetry={this.sendUserActionTelemetry} />
- </LocalizationProvider>
- );
- }
- return null;
- }
- renderTrailhead() {
- const {message} = this.state;
- if (message.template === "trailhead") {
- return (<Trailhead
- document={this.props.document}
- message={message}
- onAction={ASRouterUtils.executeAction}
- onDismissBundle={this.dismissBundle(this.state.message.bundle)}
- sendUserActionTelemetry={this.sendUserActionTelemetry}
- dispatch={this.props.dispatch}
- fxaEndpoint={this.props.fxaEndpoint} />);
- }
- return null;
- }
- renderPreviewBanner() {
- if (this.state.message.provider !== "preview") {
- return null;
- }
- return (
- <div className="snippets-preview-banner">
- <span className="icon icon-small-spacer icon-info" />
- <span>Preview Purposes Only</span>
- </div>
- );
- }
- render() {
- const {message, bundle} = this.state;
- if (!message.id && !bundle.template) { return null; }
- const shouldRenderBelowSearch = TEMPLATES_BELOW_SEARCH.includes(message.template);
- const shouldRenderInHeader = TEMPLATES_ABOVE_PAGE.includes(message.template);
- return shouldRenderBelowSearch ?
- // Render special below search snippets in place;
- <div className="below-search-snippet">{this.renderSnippets()}</div> :
- // For onboarding, regular snippets etc. we should render
- // everything in our footer container.
- ReactDOM.createPortal(
- <>
- {this.renderPreviewBanner()}
- {this.renderTrailhead()}
- {this.renderFirstRunOverlay()}
- {this.renderOnboarding()}
- {this.renderSnippets()}
- </>,
- shouldRenderInHeader ? this.headerPortal : this.footerPortal
- );
- }
- }
- ASRouterUISurface.defaultProps = {document: global.document};
|