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 http://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
TemporaryMerinoClientShim:
});
import {
actionTypes as at,
actionCreators as ac,
const PREF_SPORTS_ENABLED = "widgets.sportsWidget.enabled";
const PREF_SYSTEM_SPORTS_ENABLED = "widgets.system.sportsWidget.enabled";
const FOLLOW_STATE = "sports-follow-state";
const CACHE_KEY = "sports_feed";
const MERINO_CLIENT_KEY = "HNT_SPORTS_FEED";
/**
* Manages persistent state for the Sports widget (selected teams and widget
* state), syncing with PersistentCache so state survives page refreshes.
* Also fetches teams and match data from the Merino WCS endpoints.
*/
export class SportsFeed {
constructor() {
this.initialized = false;
this.cache = this.PersistentCache(CACHE_KEY, true);
this.merino = this.MerinoClient(MERINO_CLIENT_KEY);
}
get enabled() {
const prefs = this.store.getState()?.Prefs.values;
const userValue = !!prefs?.[PREF_SPORTS_ENABLED];
const systemValue = !!prefs?.[PREF_SYSTEM_SPORTS_ENABLED];
const experimentValue = !!prefs?.trainhopConfig?.sports?.enabled;
return userValue && (systemValue || experimentValue);
}
async init() {
this.initialized = true;
await this.syncState();
await this.fetchSportsData();
}
// On startup, read whatever was saved to disk and send it to the UI.
async syncState() {
const cachedData = (await this.cache.get()) || {};
const { widgetState, selectedTeams, sportsData, matchesTab } = cachedData;
const { teams, matches } = sportsData || {};
if (widgetState) {
this.store.dispatch(
ac.BroadcastToContent({
type: at.WIDGETS_SPORTS_SET_WIDGET_STATE,
data: widgetState,
})
);
}
if (selectedTeams) {
this.store.dispatch(
ac.BroadcastToContent({
type: at.WIDGETS_SPORTS_SET_SELECTED_TEAMS,
data: selectedTeams,
})
);
}
if (matchesTab) {
this.store.dispatch(
ac.BroadcastToContent({
type: at.WIDGETS_SPORTS_SET_MATCHES_TAB,
data: matchesTab,
})
);
}
if (teams || matches) {
this.store.dispatch(
ac.BroadcastToContent({
type: at.WIDGETS_SPORTS_WIDGET_SET,
data: { teams: teams ?? [], matches: matches ?? [] },
})
);
}
}
async fetchSportsData() {
const prefs = this.store.getState()?.Prefs.values;
const teamsEndpoint =
prefs?.trainhopConfig?.sports?.teamsEndpoint ||
prefs?.["sports.worldCup.teamsEndpoint"];
const matchesEndpoint =
prefs?.trainhopConfig?.sports?.matchesEndpoint ||
prefs?.["sports.worldCup.matchesEndpoint"];
const allowedEndpoints = (prefs?.["discoverystream.endpoints"] ?? "")
.split(",")
.map(item => item.trim())
.filter(item => item);
if (
teamsEndpoint &&
!allowedEndpoints.some(prefix => teamsEndpoint.startsWith(prefix))
) {
console.error(`Sports teams endpoint not in allowlist: ${teamsEndpoint}`);
return;
}
if (
matchesEndpoint &&
!allowedEndpoints.some(prefix => matchesEndpoint.startsWith(prefix))
) {
console.error(
`Sports matches endpoint not in allowlist: ${matchesEndpoint}`
);
return;
}
const [teams, matches] = await Promise.all([
this.merino.fetchSportsTeams({
source: "newtab",
endpointUrl: teamsEndpoint,
}),
this.merino.fetchSportsMatches({
source: "newtab",
endpointUrl: matchesEndpoint,
}),
]);
if (teams?.teams || matches) {
await this.cache.set("sportsData", {
teams: teams?.teams,
matches,
});
}
this.store.dispatch(
ac.BroadcastToContent({
type: at.WIDGETS_SPORTS_WIDGET_SET,
data: { teams: teams?.teams ?? [], matches: matches ?? [] },
})
);
}
async onPrefChangedAction(action) {
if (
(action.data.name === PREF_SPORTS_ENABLED ||
action.data.name === PREF_SYSTEM_SPORTS_ENABLED ||
action.data.name === "trainhopConfig") &&
this.enabled &&
!this.initialized
) {
await this.init();
}
}
async onAction(action) {
switch (action.type) {
case at.INIT:
if (this.enabled) {
await this.init();
}
break;
case at.PREF_CHANGED:
await this.onPrefChangedAction(action);
break;
// User changed the widget state — save it and send the updated state to the UI.
case at.WIDGETS_SPORTS_CHANGE_WIDGET_STATE:
if (action.data !== FOLLOW_STATE) {
await this.cache.set("widgetState", action.data);
}
this.store.dispatch(
ac.BroadcastToContent({
type: at.WIDGETS_SPORTS_SET_WIDGET_STATE,
data: action.data,
})
);
break;
// User changed their team selection — save it and send the updated list to the UI.
case at.WIDGETS_SPORTS_CHANGE_SELECTED_TEAMS:
await this.cache.set("selectedTeams", action.data);
this.store.dispatch(
ac.BroadcastToContent({
type: at.WIDGETS_SPORTS_SET_SELECTED_TEAMS,
data: action.data,
})
);
break;
// User changed the matches tab — save it and broadcast to the UI.
case at.WIDGETS_SPORTS_CHANGE_MATCHES_TAB:
await this.cache.set("matchesTab", action.data);
this.store.dispatch(
ac.BroadcastToContent({
type: at.WIDGETS_SPORTS_SET_MATCHES_TAB,
data: action.data,
})
);
break;
}
}
}
SportsFeed.prototype.PersistentCache = (...args) => {
return new lazy.PersistentCache(...args);
};
SportsFeed.prototype.MerinoClient = name => {
return new lazy.TemporaryMerinoClientShim(name);
};