/* 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 React, { useEffect, useRef } from "react"; import { useDispatch, useSelector, batch } from "react-redux"; import { Lists } from "./Lists/Lists"; import { FocusTimer } from "./FocusTimer/FocusTimer"; import { WeatherForecast } from "./WeatherForecast/WeatherForecast"; import { MessageWrapper } from "content-src/components/MessageWrapper/MessageWrapper"; import { WidgetsFeatureHighlight } from "../DiscoveryStreamComponents/FeatureHighlight/WidgetsFeatureHighlight"; import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; const CONTAINER_ACTION_TYPES = { HIDE_ALL: "hide_all", CHANGE_SIZE_ALL: "change_size_all", FEEDBACK: "feedback", }; const PREF_WIDGETS_ENABLED = "widgets.enabled"; const PREF_WIDGETS_LISTS_ENABLED = "widgets.lists.enabled"; const PREF_WIDGETS_SYSTEM_LISTS_ENABLED = "widgets.system.lists.enabled"; const PREF_WIDGETS_TIMER_ENABLED = "widgets.focusTimer.enabled"; const PREF_WIDGETS_SYSTEM_TIMER_ENABLED = "widgets.system.focusTimer.enabled"; const PREF_WIDGETS_SYSTEM_WEATHER_FORECAST_ENABLED = "widgets.system.weatherForecast.enabled"; const PREF_WIDGETS_MAXIMIZED = "widgets.maximized"; const PREF_WIDGETS_SYSTEM_MAXIMIZED = "widgets.system.maximized"; const PREF_WIDGETS_FEEDBACK_ENABLED = "widgets.feedback.enabled"; const PREF_WIDGETS_HIDE_ALL_TOAST_ENABLED = "widgets.hideAllToast.enabled"; const WIDGETS_FEEDBACK_URL = "https://connect.mozilla.org/t5/discussions/feedback-welcome-for-new-tab-widgets-now-available-via-firefox/td-p/108354"; // resets timer to default values (exported for testing) // In practice, this logic runs inside a useEffect when // the timer widget is disabled (after the pref flips from true to false). // Because Enzyme tests cannot reliably simulate that pref update or trigger // the related useEffect, we expose this helper to at least just test the reset behavior instead export function resetTimerToDefaults(dispatch, timerType) { const originalTime = timerType === "focus" ? 1500 : 300; // Reset both focus and break timers to their initial durations dispatch( ac.AlsoToMain({ type: at.WIDGETS_TIMER_RESET, data: { timerType, duration: originalTime, initialDuration: originalTime, }, }) ); // Set the timer type back to "focus" dispatch( ac.AlsoToMain({ type: at.WIDGETS_TIMER_SET_TYPE, data: { timerType: "focus", }, }) ); } function Widgets() { const prefs = useSelector(state => state.Prefs.values); const weatherData = useSelector(state => state.Weather); const { messageData } = useSelector(state => state.Messages); const timerType = useSelector(state => state.TimerWidget.timerType); const timerData = useSelector(state => state.TimerWidget); const isMaximized = prefs[PREF_WIDGETS_MAXIMIZED]; const widgetsMayBeMaximized = prefs[PREF_WIDGETS_SYSTEM_MAXIMIZED]; const dispatch = useDispatch(); const nimbusListsEnabled = prefs.widgetsConfig?.listsEnabled; const nimbusTimerEnabled = prefs.widgetsConfig?.timerEnabled; const nimbusListsTrainhopEnabled = prefs.trainhopConfig?.widgets?.listsEnabled; const nimbusTimerTrainhopEnabled = prefs.trainhopConfig?.widgets?.timerEnabled; const nimbusWeatherForecastTrainhopEnabled = prefs.trainhopConfig?.widgets?.weatherForecastEnabled; const nimbusMaximizedTrainhopEnabled = prefs.trainhopConfig?.widgets?.maximized; const feedbackEnabled = prefs.trainhopConfig?.widgets?.feedbackEnabled || prefs[PREF_WIDGETS_FEEDBACK_ENABLED]; const hideAllToastEnabled = prefs.trainhopConfig?.widgets?.hideAllToastEnabled || prefs[PREF_WIDGETS_HIDE_ALL_TOAST_ENABLED]; const feedbackUrl = prefs.trainhopConfig?.widgets?.feedbackUrl ?? WIDGETS_FEEDBACK_URL; const widgetsEnabled = prefs[PREF_WIDGETS_ENABLED]; const listsEnabled = widgetsEnabled && (nimbusListsTrainhopEnabled || nimbusListsEnabled || prefs[PREF_WIDGETS_SYSTEM_LISTS_ENABLED]) && prefs[PREF_WIDGETS_LISTS_ENABLED]; const timerEnabled = widgetsEnabled && (nimbusTimerTrainhopEnabled || nimbusTimerEnabled || prefs[PREF_WIDGETS_SYSTEM_TIMER_ENABLED]) && prefs[PREF_WIDGETS_TIMER_ENABLED]; // This weather forecast widget will only show when the following are true: // - The weather view is set to "detailed" (can be checked with the weather.display pref) // - Weather is displayed on New Tab (system.showWeather) // - The weather forecast widget is enabled (system.weatherForecast.enabled) // Note that if the view is set to "detailed" but the weather forecast widget is not enabled, // then the mini weather widget will display with the "detailed" view const weatherForecastSystemEnabled = nimbusWeatherForecastTrainhopEnabled || prefs[PREF_WIDGETS_SYSTEM_WEATHER_FORECAST_ENABLED]; const showDetailedView = prefs["weather.display"] === "detailed"; // Check if weather is enabled (browser.newtabpage.activity-stream.showWeather) const { showWeather } = prefs; const systemShowWeather = prefs["system.showWeather"]; const weatherExperimentEnabled = prefs.trainhopConfig?.weather?.enabled; const isWeatherEnabled = showWeather && (systemShowWeather || weatherExperimentEnabled); const weatherForecastEnabled = widgetsEnabled && weatherForecastSystemEnabled && showDetailedView && weatherData?.initialized && isWeatherEnabled; // Widget size is "small" only when maximize feature is enabled and widgets // are currently minimized. Otherwise defaults to "medium". const widgetSize = widgetsMayBeMaximized && !isMaximized ? "small" : "medium"; // track previous timerEnabled state to detect when it becomes disabled const prevTimerEnabledRef = useRef(timerEnabled); // Reset timer when it becomes disabled useEffect(() => { const wasTimerEnabled = prevTimerEnabledRef.current; const isTimerEnabled = timerEnabled; // Only reset if timer was enabled and is now disabled if (wasTimerEnabled && !isTimerEnabled && timerData) { resetTimerToDefaults(dispatch, timerType); } // Update the ref to track current state prevTimerEnabledRef.current = isTimerEnabled; }, [timerEnabled, timerData, dispatch, timerType]); // Bug 2013978 - Replace hardcoded widget list with programmatic registry function hideAllWidgets() { batch(() => { dispatch(ac.SetPref(PREF_WIDGETS_LISTS_ENABLED, false)); dispatch(ac.SetPref(PREF_WIDGETS_TIMER_ENABLED, false)); // If weather forecast widget is visible, turn off the weather if (weatherForecastEnabled) { dispatch(ac.SetPref("showWeather", false)); } const telemetryData = { action_type: CONTAINER_ACTION_TYPES.HIDE_ALL, widget_size: widgetSize, }; dispatch( ac.OnlyToMain({ type: at.WIDGETS_CONTAINER_ACTION, data: telemetryData, }) ); // Dispatch WIDGETS_ENABLED for each widget being hidden if (listsEnabled) { dispatch( ac.OnlyToMain({ type: at.WIDGETS_ENABLED, data: { widget_name: "lists", widget_source: "widget", enabled: false, widget_size: widgetSize, }, }) ); } if (timerEnabled) { dispatch( ac.OnlyToMain({ type: at.WIDGETS_ENABLED, data: { widget_name: "focus_timer", widget_source: "widget", enabled: false, widget_size: widgetSize, }, }) ); } // Send telemetry for weather widget if it was visible when hiding all widgets if (weatherForecastEnabled) { dispatch( ac.OnlyToMain({ type: at.WIDGETS_ENABLED, data: { widget_name: "weather", widget_source: "widget", enabled: false, widget_size: widgetSize, }, }) ); } if (hideAllToastEnabled) { dispatch( ac.OnlyToOneContent( { type: at.SHOW_TOAST_MESSAGE, data: { toastId: "hideWidgetsToast", showNotifications: true }, }, "ActivityStream:Content" ) ); } }); } function handleHideAllWidgetsClick(e) { e.preventDefault(); hideAllWidgets(); } function handleHideAllWidgetsKeyDown(e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); hideAllWidgets(); } } function toggleMaximize() { const newMaximizedState = !isMaximized; const newWidgetSize = widgetsMayBeMaximized && !newMaximizedState ? "small" : "medium"; batch(() => { dispatch(ac.SetPref(PREF_WIDGETS_MAXIMIZED, newMaximizedState)); const telemetryData = { action_type: CONTAINER_ACTION_TYPES.CHANGE_SIZE_ALL, action_value: newMaximizedState ? "maximize_widgets" : "minimize_widgets", widget_size: newWidgetSize, }; dispatch( ac.OnlyToMain({ type: at.WIDGETS_CONTAINER_ACTION, data: telemetryData, }) ); }); } function handleToggleMaximizeClick(e) { e.preventDefault(); toggleMaximize(); } function handleToggleMaximizeKeyDown(e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleMaximize(); } } function handleFeedbackClick(e) { e.preventDefault(); batch(() => { dispatch( ac.OnlyToMain({ type: at.OPEN_LINK, data: { url: feedbackUrl }, }) ); dispatch( ac.OnlyToMain({ type: at.WIDGETS_CONTAINER_ACTION, data: { action_type: CONTAINER_ACTION_TYPES.FEEDBACK, widget_size: widgetSize, }, }) ); }); } function handleUserInteraction(widgetName) { const prefName = `widgets.${widgetName}.interaction`; const hasInteracted = prefs[prefName]; // we want to make sure that the value is a strict false (and that the property exists) if (hasInteracted === false) { dispatch(ac.SetPref(prefName, true)); } } if (!(listsEnabled || timerEnabled || weatherForecastEnabled)) { return null; } return (
); } export { Widgets };