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,
/**
* WIDGET_REGISTRY — single source of truth for all New Tab widgets.
*
* WHY THIS EXISTS
* Previously, every widget was hardcoded in three places: the render loop in
* Widgets.jsx, the hideAllWidgets handler, and the toggleMaximize handler.
* Adding or removing a widget required edits in all three spots and was easy
* to get out of sync. This registry replaces those hardcoded lists so that
* Widgets.jsx, WidgetsSidebar.jsx, and any future consumers share one
* authoritative definition.
*
* HOW IT WORKS
* Each entry describes one widget's static metadata:
*
* id — unique string key used in prefs and the order pref
* telemetryName — the name sent in Glean events (snake_case; may differ from id)
* order — default render position (0-indexed); used when widgets.order is empty
* enabledPref — the user-facing pref that toggles this widget on/off
* sizePref — the pref that stores the user's chosen size (empty string = not set)
* defaultSize — size to use when sizePref is empty and no trainhop suggestion exists
* validSizes — the sizes this widget supports (drives size picker options)
* hasSidebar — when true, the widget renders in the sidebar instead of the
* widget row when its effective size equals "small". Size alone is not
* sufficient — this flag must be set explicitly so that future
* widgets that support "small" but stay in the row are not
* accidentally moved to the sidebar.
* systemEnabledPref — system/operator pref that gates this widget independent of the user pref
* trainhopEnabledKey — key in trainhopConfig.widgets.* for the enabled override
* trainhopSizeKey — key in trainhopConfig.widgets.* for the size default suggestion
* (only applies when the user has not explicitly set sizePref)
* trainhopSidebarKey — key in trainhopConfig.widgets.* for the hasSidebar override;
* null means the sidebar placement is not overridable via trainhop
*
* SIZE PRIORITY
* sizePref defaults to "" (empty string) in PREFS_CONFIG. An empty value
* means the user has not explicitly chosen a size; resolveWidgetSize() falls
* through to a trainhop suggestion and then to widget.defaultSize. Once the
* user resizes a widget via the UI the pref is written with a real value and
* trainhop can no longer override it. resolveWidgetSize() applies these in order:
* 1. User-set pref (sizePref is non-empty) — always wins
* 2. trainhopConfig suggestion (trainhopSizeKey) — acts as default, not override
* 3. widget.defaultSize — final fallback
*
* Note: widgets.weather.size uses getValue: getWeatherWidgetSize in
* ActivityStream.sys.mjs rather than value: "" because it has a Nova migration
* path that infers the correct initial size from the user's previous weather
* configuration. After migration the stored value is non-empty and the sentinel
* logic above applies normally.
*
* ADDING A NEW WIDGET
* 1. Add a new entry to WIDGET_REGISTRY below with the next `order` integer.
* Set telemetryName to the snake_case Glean name for this widget.
* 2. Export its pref key constants from this file.
* 3. Register both prefs (enabled + size) in lib/ActivityStream.sys.mjs.
* 4. Add the component to WIDGET_ROW_COMPONENTS in WidgetsComponentRegistry.jsx.
* 5. If it has a sidebar variant, set hasSidebar: true and add its component
* to WIDGET_SIDEBAR_COMPONENTS in WidgetsComponentRegistry.jsx.
*
* ADDING A NEW PER-WIDGET DIMENSION (e.g. "scale")
* 1. Add scalePref and trainhopScaleKey fields to each registry entry.
* 2. Export a resolveWidgetScale(widget, prefs) helper following the same
* user-pref-wins pattern as resolveWidgetSize().
* 3. Update components to call the helper instead of reading the pref directly.
*
* The widgets.order pref (CSV of widget IDs) persists user-defined order.
* It is only written when the user explicitly reorders widgets — never on
* enable/disable. Disabled widgets keep their slot so they reappear in the
* same position when re-enabled. See resolveWidgetOrder() below.
*/
export const PREF_WIDGETS_LISTS_ENABLED = "widgets.lists.enabled";
export const PREF_WIDGETS_TIMER_ENABLED = "widgets.focusTimer.enabled";
export const PREF_WIDGETS_WEATHER_ENABLED = "widgets.weather.enabled";
export const PREF_LISTS_SIZE = "widgets.lists.size";
export const PREF_FOCUS_TIMER_SIZE = "widgets.focusTimer.size";
export const PREF_WEATHER_SIZE = "widgets.weather.size";
export const PREF_WIDGETS_ORDER = "widgets.order";
export const PREF_WIDGETS_SYSTEM_LISTS_ENABLED = "widgets.system.lists.enabled";
export const PREF_WIDGETS_SYSTEM_TIMER_ENABLED =
"widgets.system.focusTimer.enabled";
export const PREF_WIDGETS_SYSTEM_WEATHER_ENABLED =
"widgets.system.weather.enabled";
export const PREF_WIDGETS_SPORTS_WIDGET_ENABLED =
"widgets.sportsWidget.enabled";
export const PREF_SPORTS_WIDGET_SIZE = "widgets.sportsWidget.size";
export const PREF_WIDGETS_SYSTEM_SPORTS_WIDGET_ENABLED =
"widgets.system.sportsWidget.enabled";
export const PREF_WIDGETS_CLOCKS_ENABLED = "widgets.clocks.enabled";
export const PREF_CLOCKS_SIZE = "widgets.clocks.size";
export const PREF_WIDGETS_SYSTEM_CLOCKS_ENABLED =
"widgets.system.clocks.enabled";
/**
* @typedef {object} WidgetRegistryEntry
* @property {string} id - Unique key used in prefs and the order pref.
* @property {string} telemetryName - Snake_case name sent in Glean events. May differ from id (e.g. "focus_timer" for id "focusTimer").
* @property {number} order - Default render position (0-indexed).
* @property {string} enabledPref - User-facing pref that toggles this widget on/off.
* @property {string} sizePref - Pref that stores the user's chosen size ("" = not yet set).
* @property {string} defaultSize - Fallback size when sizePref is empty and no trainhop suggestion exists.
* @property {string[]} validSizes - Sizes this widget supports.
* @property {boolean} hasSidebar - When true, the widget moves to the sidebar at size "small".
* @property {string} systemEnabledPref - Operator pref that gates the widget independently of the user pref.
* @property {string} trainhopEnabledKey - Key in trainhopConfig.widgets.* for the enabled override.
* @property {string|null} trainhopSizeKey - Key in trainhopConfig.widgets.* for the size default suggestion.
* @property {string|null} trainhopSidebarKey - Key in trainhopConfig.widgets.* for the hasSidebar override.
*/
/** @type {WidgetRegistryEntry[]} */
export const WIDGET_REGISTRY = [
{
id: "sportsWidget",
telemetryName: "sports",
order: 0,
enabledPref: PREF_WIDGETS_SPORTS_WIDGET_ENABLED,
sizePref: PREF_SPORTS_WIDGET_SIZE,
defaultSize: "medium",
validSizes: ["medium", "large"],
hasSidebar: false,
systemEnabledPref: PREF_WIDGETS_SYSTEM_SPORTS_WIDGET_ENABLED,
trainhopEnabledKey: "sportsWidgetEnabled",
trainhopSizeKey: "sportsWidgetSize",
trainhopSidebarKey: null,
},
{
id: "clocks",
telemetryName: "clocks",
order: 1,
enabledPref: PREF_WIDGETS_CLOCKS_ENABLED,
sizePref: PREF_CLOCKS_SIZE,
defaultSize: "medium",
validSizes: ["small", "medium", "large"],
hasSidebar: false,
systemEnabledPref: PREF_WIDGETS_SYSTEM_CLOCKS_ENABLED,
trainhopEnabledKey: "clocksEnabled",
trainhopSizeKey: "clocksSize",
trainhopSidebarKey: null,
},
{
id: "lists",
telemetryName: "lists",
order: 2,
enabledPref: PREF_WIDGETS_LISTS_ENABLED,
sizePref: PREF_LISTS_SIZE,
defaultSize: "medium",
validSizes: ["small", "medium", "large"],
hasSidebar: false,
systemEnabledPref: PREF_WIDGETS_SYSTEM_LISTS_ENABLED,
trainhopEnabledKey: "listsEnabled",
trainhopSizeKey: "listsSize",
trainhopSidebarKey: null,
},
{
id: "focusTimer",
telemetryName: "focus_timer",
order: 3,
enabledPref: PREF_WIDGETS_TIMER_ENABLED,
sizePref: PREF_FOCUS_TIMER_SIZE,
defaultSize: "medium",
validSizes: ["small", "medium", "large"],
hasSidebar: false,
systemEnabledPref: PREF_WIDGETS_SYSTEM_TIMER_ENABLED,
trainhopEnabledKey: "timerEnabled",
trainhopSizeKey: "timerSize",
trainhopSidebarKey: null,
},
{
id: "weather",
telemetryName: "weather",
order: 4,
enabledPref: PREF_WIDGETS_WEATHER_ENABLED,
sizePref: PREF_WEATHER_SIZE,
defaultSize: "small",
validSizes: ["small", "medium", "large"],
hasSidebar: true,
systemEnabledPref: PREF_WIDGETS_SYSTEM_WEATHER_ENABLED,
trainhopEnabledKey: "weatherEnabled",
trainhopSizeKey: "weatherSize",
trainhopSidebarKey: "weatherSidebar",
},
];
/**
* Returns an ordered list of all widget IDs (including disabled ones).
* Saved order is respected; any widget IDs not in the saved pref are appended
* in registry-default order. Unknown IDs in the saved pref are dropped.
*
* @param {string} orderPref - value of the widgets.order pref (CSV string)
*/
export function getWidgetOrder(orderPref) {
const registryIds = WIDGET_REGISTRY.map(w => w.id);
if (!orderPref) {
return registryIds;
}
const seen = new Set();
const saved = orderPref
.split(",")
.filter(id => registryIds.includes(id) && !seen.has(id) && seen.add(id));
const appended = registryIds.filter(id => !seen.has(id));
return [...saved, ...appended];
}
/**
* Returns the effective widget render order. The user's saved order wins;
* a trainhop suggestion applies only when no user order is saved.
*
* @param {object} prefs - current pref values from the Redux store
* @returns {string[]} ordered array of widget IDs
*/
export function resolveWidgetOrder(prefs) {
const userOrder = prefs[PREF_WIDGETS_ORDER];
if (userOrder) {
return getWidgetOrder(userOrder);
}
const trainhopOrder = prefs.trainhopConfig?.widgets?.order;
if (trainhopOrder) {
return getWidgetOrder(trainhopOrder);
}
return getWidgetOrder(null);
}
/**
* Returns true if the widget is available to the user, based on the
* trainhop/system gate. Does not consider whether the user has turned the
* widget on, or whether the widgets container is enabled.
*
* @param {object} widget - a WIDGET_REGISTRY entry
* @param {object} prefs - current pref values from the Redux store
* @returns {boolean}
*/
export function isWidgetAddable(widget, prefs) {
return Boolean(
prefs.trainhopConfig?.widgets?.[widget.trainhopEnabledKey] ||
prefs[widget.systemEnabledPref]
);
}
/**
* Returns true if the widget is currently enabled: the widgets container is
* on, the widget is addable, and the user's enabled pref is set.
*
* @param {object} widget - a WIDGET_REGISTRY entry
* @param {object} prefs - current pref values from the Redux store
* @param {boolean} widgetsEnabled - value of the widgets.enabled container pref
* @returns {boolean}
*/
export function isWidgetEnabled(widget, prefs, widgetsEnabled) {
return Boolean(
widgetsEnabled &&
isWidgetAddable(widget, prefs) &&
prefs[widget.enabledPref]
);
}
/**
* Returns the effective size for a widget, applying priority:
* user-set pref > trainhop suggestion > registry defaultSize
*
* A sizePref value of "" means the user has not explicitly chosen a size,
* so trainhop and defaultSize are consulted. Any non-empty value was written
* by a user action (size picker, maximize/minimize button) and always wins.
*
* @param {object} widget - a WIDGET_REGISTRY entry
* @param {object} prefs - current pref values from the Redux store
* @returns {string}
*/
export function resolveWidgetSize(widget, prefs) {
const userPref = prefs[widget.sizePref];
if (userPref) {
return userPref;
}
const trainhopSize = widget.trainhopSizeKey
? prefs.trainhopConfig?.widgets?.[widget.trainhopSizeKey]
: null;
return trainhopSize || widget.defaultSize;
}
/**
* Returns whether the widget should be placed in the sidebar.
* A trainhop override (trainhopSidebarKey) takes precedence over the
* static registry hasSidebar flag when present.
*
* @param {object} widget - a WIDGET_REGISTRY entry
* @param {object} prefs - current pref values from the Redux store
* @returns {boolean}
*/
export function resolveWidgetHasSidebar(widget, prefs) {
if (widget.trainhopSidebarKey) {
const override = prefs.trainhopConfig?.widgets?.[widget.trainhopSidebarKey];
if (override !== undefined) {
return override;
}
}
return widget.hasSidebar;
}
/**
* Returns the list of widgets to disable when "hide all" is triggered.
* A widget is included if it has no sidebar variant OR if it is currently
* in the row (not the sidebar). Each entry carries the pref to disable,
* the telemetry name, and whether it was active (for telemetry filtering).
*
* @param {object} prefs - current pref values from the Redux store
* @param {object} widgetEnabledMap - map of widget id → boolean (currently active in row)
* @returns {{ enabledPref: string, telemetryName: string, active: boolean }[]}
*/
export function getHideAllTargets(prefs, widgetEnabledMap) {
return WIDGET_REGISTRY.filter(
w => !resolveWidgetHasSidebar(w, prefs) || widgetEnabledMap[w.id]
).map(w => ({
enabledPref: w.enabledPref,
telemetryName: w.telemetryName,
active: !!widgetEnabledMap[w.id],
}));
}