summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/content-src/components/Base/Base.jsx
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/content-src/components/Base/Base.jsx')
-rw-r--r--browser/components/newtab/content-src/components/Base/Base.jsx273
1 files changed, 273 insertions, 0 deletions
diff --git a/browser/components/newtab/content-src/components/Base/Base.jsx b/browser/components/newtab/content-src/components/Base/Base.jsx
new file mode 100644
index 0000000000..8845ad87cd
--- /dev/null
+++ b/browser/components/newtab/content-src/components/Base/Base.jsx
@@ -0,0 +1,273 @@
+/* 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/. */
+
+import {
+ actionCreators as ac,
+ actionTypes as at,
+} from "common/Actions.sys.mjs";
+import { ASRouterAdmin } from "content-src/components/ASRouterAdmin/ASRouterAdmin";
+import { ASRouterUISurface } from "../../asrouter/asrouter-content";
+import { ConfirmDialog } from "content-src/components/ConfirmDialog/ConfirmDialog";
+import { connect } from "react-redux";
+import { DiscoveryStreamBase } from "content-src/components/DiscoveryStreamBase/DiscoveryStreamBase";
+import { ErrorBoundary } from "content-src/components/ErrorBoundary/ErrorBoundary";
+import { CustomizeMenu } from "content-src/components/CustomizeMenu/CustomizeMenu";
+import React from "react";
+import { Search } from "content-src/components/Search/Search";
+import { Sections } from "content-src/components/Sections/Sections";
+
+export const PrefsButton = ({ onClick, icon }) => (
+ <div className="prefs-button">
+ <button
+ className={`icon ${icon || "icon-settings"}`}
+ onClick={onClick}
+ data-l10n-id="newtab-settings-button"
+ />
+ </div>
+);
+
+// Returns a function will not be continuously triggered when called. The
+// function will be triggered if called again after `wait` milliseconds.
+function debounce(func, wait) {
+ let timer;
+ return (...args) => {
+ if (timer) {
+ return;
+ }
+
+ let wakeUp = () => {
+ timer = null;
+ };
+
+ timer = setTimeout(wakeUp, wait);
+ func.apply(this, args);
+ };
+}
+
+export class _Base extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ message: {},
+ };
+ this.notifyContent = this.notifyContent.bind(this);
+ }
+
+ notifyContent(state) {
+ this.setState(state);
+ }
+
+ componentWillUnmount() {
+ this.updateTheme();
+ }
+
+ componentWillUpdate() {
+ this.updateTheme();
+ }
+
+ updateTheme() {
+ const bodyClassName = [
+ "activity-stream",
+ // If we skipped the about:welcome overlay and removed the CSS classes
+ // we don't want to add them back to the Activity Stream view
+ document.body.classList.contains("inline-onboarding")
+ ? "inline-onboarding"
+ : "",
+ ]
+ .filter(v => v)
+ .join(" ");
+ global.document.body.className = bodyClassName;
+ }
+
+ render() {
+ const { props } = this;
+ const { App } = props;
+ const isDevtoolsEnabled = props.Prefs.values["asrouter.devtoolsEnabled"];
+
+ if (!App.initialized) {
+ return null;
+ }
+
+ return (
+ <ErrorBoundary className="base-content-fallback">
+ <React.Fragment>
+ <BaseContent {...this.props} adminContent={this.state} />
+ {isDevtoolsEnabled ? (
+ <ASRouterAdmin notifyContent={this.notifyContent} />
+ ) : null}
+ </React.Fragment>
+ </ErrorBoundary>
+ );
+ }
+}
+
+export class BaseContent extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.openPreferences = this.openPreferences.bind(this);
+ this.openCustomizationMenu = this.openCustomizationMenu.bind(this);
+ this.closeCustomizationMenu = this.closeCustomizationMenu.bind(this);
+ this.handleOnKeyDown = this.handleOnKeyDown.bind(this);
+ this.onWindowScroll = debounce(this.onWindowScroll.bind(this), 5);
+ this.setPref = this.setPref.bind(this);
+ this.state = { fixedSearch: false };
+ }
+
+ componentDidMount() {
+ global.addEventListener("scroll", this.onWindowScroll);
+ global.addEventListener("keydown", this.handleOnKeyDown);
+ }
+
+ componentWillUnmount() {
+ global.removeEventListener("scroll", this.onWindowScroll);
+ global.removeEventListener("keydown", this.handleOnKeyDown);
+ }
+
+ onWindowScroll() {
+ const prefs = this.props.Prefs.values;
+ const SCROLL_THRESHOLD = prefs["logowordmark.alwaysVisible"] ? 179 : 34;
+ if (global.scrollY > SCROLL_THRESHOLD && !this.state.fixedSearch) {
+ this.setState({ fixedSearch: true });
+ } else if (global.scrollY <= SCROLL_THRESHOLD && this.state.fixedSearch) {
+ this.setState({ fixedSearch: false });
+ }
+ }
+
+ openPreferences() {
+ this.props.dispatch(ac.OnlyToMain({ type: at.SETTINGS_OPEN }));
+ this.props.dispatch(ac.UserEvent({ event: "OPEN_NEWTAB_PREFS" }));
+ }
+
+ openCustomizationMenu() {
+ this.props.dispatch({ type: at.SHOW_PERSONALIZE });
+ this.props.dispatch(ac.UserEvent({ event: "SHOW_PERSONALIZE" }));
+ }
+
+ closeCustomizationMenu() {
+ if (this.props.App.customizeMenuVisible) {
+ this.props.dispatch({ type: at.HIDE_PERSONALIZE });
+ this.props.dispatch(ac.UserEvent({ event: "HIDE_PERSONALIZE" }));
+ }
+ }
+
+ handleOnKeyDown(e) {
+ if (e.key === "Escape") {
+ this.closeCustomizationMenu();
+ }
+ }
+
+ setPref(pref, value) {
+ this.props.dispatch(ac.SetPref(pref, value));
+ }
+
+ render() {
+ const { props } = this;
+ const { App } = props;
+ const { initialized, customizeMenuVisible } = App;
+ const prefs = props.Prefs.values;
+
+ const isDiscoveryStream =
+ props.DiscoveryStream.config && props.DiscoveryStream.config.enabled;
+ let filteredSections = props.Sections.filter(
+ section => section.id !== "topstories"
+ );
+
+ const pocketEnabled =
+ prefs["feeds.section.topstories"] && prefs["feeds.system.topstories"];
+ const noSectionsEnabled =
+ !prefs["feeds.topsites"] &&
+ !pocketEnabled &&
+ filteredSections.filter(section => section.enabled).length === 0;
+ const searchHandoffEnabled = prefs["improvesearch.handoffToAwesomebar"];
+ const enabledSections = {
+ topSitesEnabled: prefs["feeds.topsites"],
+ pocketEnabled: prefs["feeds.section.topstories"],
+ highlightsEnabled: prefs["feeds.section.highlights"],
+ showSponsoredTopSitesEnabled: prefs.showSponsoredTopSites,
+ showSponsoredPocketEnabled: prefs.showSponsored,
+ showRecentSavesEnabled: prefs.showRecentSaves,
+ topSitesRowsCount: prefs.topSitesRows,
+ };
+
+ const pocketRegion = prefs["feeds.system.topstories"];
+ const { mayHaveSponsoredTopSites } = prefs;
+
+ const outerClassName = [
+ "outer-wrapper",
+ isDiscoveryStream && pocketEnabled && "ds-outer-wrapper-search-alignment",
+ isDiscoveryStream && "ds-outer-wrapper-breakpoint-override",
+ prefs.showSearch &&
+ this.state.fixedSearch &&
+ !noSectionsEnabled &&
+ "fixed-search",
+ prefs.showSearch && noSectionsEnabled && "only-search",
+ prefs["logowordmark.alwaysVisible"] && "visible-logo",
+ ]
+ .filter(v => v)
+ .join(" ");
+
+ const hasSnippet =
+ prefs["feeds.snippets"] &&
+ this.props.adminContent &&
+ this.props.adminContent.message &&
+ this.props.adminContent.message.id;
+
+ return (
+ <div>
+ <CustomizeMenu
+ onClose={this.closeCustomizationMenu}
+ onOpen={this.openCustomizationMenu}
+ openPreferences={this.openPreferences}
+ setPref={this.setPref}
+ enabledSections={enabledSections}
+ pocketRegion={pocketRegion}
+ mayHaveSponsoredTopSites={mayHaveSponsoredTopSites}
+ showing={customizeMenuVisible}
+ />
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions*/}
+ <div className={outerClassName} onClick={this.closeCustomizationMenu}>
+ <main className={hasSnippet ? "has-snippet" : ""}>
+ {prefs.showSearch && (
+ <div className="non-collapsible-section">
+ <ErrorBoundary>
+ <Search
+ showLogo={
+ noSectionsEnabled || prefs["logowordmark.alwaysVisible"]
+ }
+ handoffEnabled={searchHandoffEnabled}
+ {...props.Search}
+ />
+ </ErrorBoundary>
+ </div>
+ )}
+ <ASRouterUISurface
+ adminContent={this.props.adminContent}
+ appUpdateChannel={this.props.Prefs.values.appUpdateChannel}
+ fxaEndpoint={this.props.Prefs.values.fxa_endpoint}
+ dispatch={this.props.dispatch}
+ />
+ <div className={`body-wrapper${initialized ? " on" : ""}`}>
+ {isDiscoveryStream ? (
+ <ErrorBoundary className="borderless-error">
+ <DiscoveryStreamBase locale={props.App.locale} />
+ </ErrorBoundary>
+ ) : (
+ <Sections />
+ )}
+ </div>
+ <ConfirmDialog />
+ </main>
+ </div>
+ </div>
+ );
+ }
+}
+
+export const Base = connect(state => ({
+ App: state.App,
+ Prefs: state.Prefs,
+ Sections: state.Sections,
+ DiscoveryStream: state.DiscoveryStream,
+ Search: state.Search,
+}))(_Base);