Source code

Revision control

Copy as Markdown

Other Tools

/* 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 https://mozilla.org/MPL/2.0/. */
// eslint-disable-next-line no-unused-vars
import React, { useCallback, useRef } from "react";
import { useSelector, batch } from "react-redux";
import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { useIntersectionObserver } from "../../../lib/utils";
import { WIDGET_REGISTRY, resolveWidgetSize } from "common/WidgetsRegistry.mjs";
import { WidgetMenuFooter } from "../WidgetMenuFooter";
import { SizeSubmenu } from "../SizeSubmenu";
import { StockTicker } from "./StockTicker";
const USER_ACTION_TYPES = {
CHANGE_SIZE: "change_size",
SEARCH_TICKERS: "search_tickers",
LEARN_MORE: "learn_more",
};
const STOCKS_ENTRY = WIDGET_REGISTRY.find(w => w.id === "stocks");
const STOCKS_PLACEHOLDER_COUNT = 4;
function Stocks({ dispatch, widgetsMayBeMaximized, widgetEnabledMap }) {
const prefs = useSelector(state => state.Prefs.values);
const { tickers } = useSelector(state => state.Stocks);
// Resolve size through the registry helper, not the pref, so trainhop and the
// default can apply.
const widgetSize = resolveWidgetSize(STOCKS_ENTRY, prefs);
const impressionFired = useRef(false);
const handleIntersection = useCallback(() => {
if (impressionFired.current) {
return;
}
impressionFired.current = true;
dispatch(
ac.AlsoToMain({
type: at.WIDGETS_IMPRESSION,
data: {
widget_name: "stocks",
widget_size: widgetSize,
},
})
);
}, [dispatch, widgetSize]);
const widgetRef = useIntersectionObserver(handleIntersection);
const handleChangeSize = useCallback(
size => {
batch(() => {
dispatch(
ac.OnlyToMain({
type: at.SET_PREF,
data: { name: STOCKS_ENTRY.sizePref, value: size },
})
);
dispatch(
ac.OnlyToMain({
type: at.WIDGETS_USER_EVENT,
data: {
widget_name: "stocks",
widget_source: "context_menu",
user_action: USER_ACTION_TYPES.CHANGE_SIZE,
action_value: size,
widget_size: size,
},
})
);
});
},
[dispatch]
);
// Placeholder: a real ticker search will replace this telemetry-only stub in
// a follow-up.
function handleSearchTickers() {
dispatch(
ac.OnlyToMain({
type: at.WIDGETS_USER_EVENT,
data: {
widget_name: "stocks",
widget_source: "context_menu",
user_action: USER_ACTION_TYPES.SEARCH_TICKERS,
widget_size: widgetSize,
},
})
);
}
// The shared footer opens the support link; here we only record the click.
function handleLearnMore() {
dispatch(
ac.OnlyToMain({
type: at.WIDGETS_USER_EVENT,
data: {
widget_name: "stocks",
widget_source: "context_menu",
user_action: USER_ACTION_TYPES.LEARN_MORE,
widget_size: widgetSize,
},
})
);
}
return (
<article
className={`stocks widget col-4 ${widgetSize}-widget`}
ref={el => {
widgetRef.current = [el];
}}
>
<div className="stocks-title-wrapper">
<span
className="stocks-title"
data-l10n-id="newtab-stocks-widget-title"
></span>
<div className="stocks-context-menu-wrapper">
<moz-button
className="stocks-context-menu-button"
iconSrc="chrome://global/skin/icons/more.svg"
menuId="stocks-context-menu"
type="icon ghost"
size="small"
data-l10n-id="newtab-stocks-widget-menu-button"
/>
<panel-list id="stocks-context-menu">
<panel-item
data-l10n-id="newtab-stocks-menu-search"
onClick={handleSearchTickers}
/>
<WidgetMenuFooter
dispatch={dispatch}
widgetId="stocks"
widgetEnabledMap={widgetEnabledMap}
widgetName="stocks"
enabledPref={STOCKS_ENTRY.enabledPref}
widgetSize={widgetSize}
learnMoreL10nId="newtab-stocks-menu-learn-more"
onLearnMore={handleLearnMore}
sizeSubmenu={
widgetsMayBeMaximized ? (
<SizeSubmenu
submenuId="stocks-size-submenu"
sizes={["medium", "large"]}
checkedSize={widgetSize}
onChangeSize={handleChangeSize}
/>
) : null
}
/>
</panel-list>
</div>
</div>
<div className="stocks-body">
{widgetSize === "medium" && (
<ul
className={`stocks-grid${tickers.length ? "" : " stocks-grid--loading"}`}
>
{tickers.length
? tickers.map(t => (
<StockTicker
key={t.ticker}
name={t.name}
ticker={t.ticker}
price={t.last_price}
changePercent={t.todays_change_perc}
/>
))
: Array.from({ length: STOCKS_PLACEHOLDER_COUNT }).map((_, i) => (
<StockTicker key={i} loading={true} />
))}
</ul>
)}
{widgetSize === "large" && (
<ul
className={`stocks-list${tickers.length ? "" : " stocks-list--loading"}`}
>
{tickers.length
? tickers.map(t => (
<StockTicker
key={t.ticker}
size="large"
name={t.name}
ticker={t.ticker}
price={t.last_price}
changePercent={t.todays_change_perc}
/>
))
: Array.from({ length: STOCKS_PLACEHOLDER_COUNT }).map((_, i) => (
<StockTicker key={i} size="large" loading={true} />
))}
</ul>
)}
</div>
</article>
);
}
export { Stocks };