Widgets.jsx (7907B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import React, { useEffect, useRef } from "react"; 6 import { useDispatch, useSelector, batch } from "react-redux"; 7 import { Lists } from "./Lists/Lists"; 8 import { FocusTimer } from "./FocusTimer/FocusTimer"; 9 import { WeatherForecast } from "./WeatherForecast/WeatherForecast"; 10 import { MessageWrapper } from "content-src/components/MessageWrapper/MessageWrapper"; 11 import { WidgetsFeatureHighlight } from "../DiscoveryStreamComponents/FeatureHighlight/WidgetsFeatureHighlight"; 12 import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; 13 14 const PREF_WIDGETS_LISTS_ENABLED = "widgets.lists.enabled"; 15 const PREF_WIDGETS_SYSTEM_LISTS_ENABLED = "widgets.system.lists.enabled"; 16 const PREF_WIDGETS_TIMER_ENABLED = "widgets.focusTimer.enabled"; 17 const PREF_WIDGETS_SYSTEM_TIMER_ENABLED = "widgets.system.focusTimer.enabled"; 18 const PREF_WIDGETS_WEATHER_FORECAST_ENABLED = "widgets.weatherForecast.enabled"; 19 const PREF_WIDGETS_SYSTEM_WEATHER_FORECAST_ENABLED = 20 "widgets.system.weatherForecast.enabled"; 21 const PREF_WIDGETS_MAXIMIZED = "widgets.maximized"; 22 const PREF_WIDGETS_SYSTEM_MAXIMIZED = "widgets.system.maximized"; 23 24 // resets timer to default values (exported for testing) 25 // In practice, this logic runs inside a useEffect when 26 // the timer widget is disabled (after the pref flips from true to false). 27 // Because Enzyme tests cannot reliably simulate that pref update or trigger 28 // the related useEffect, we expose this helper to at least just test the reset behavior instead 29 30 export function resetTimerToDefaults(dispatch, timerType) { 31 const originalTime = timerType === "focus" ? 1500 : 300; 32 33 // Reset both focus and break timers to their initial durations 34 dispatch( 35 ac.AlsoToMain({ 36 type: at.WIDGETS_TIMER_RESET, 37 data: { 38 timerType, 39 duration: originalTime, 40 initialDuration: originalTime, 41 }, 42 }) 43 ); 44 45 // Set the timer type back to "focus" 46 dispatch( 47 ac.AlsoToMain({ 48 type: at.WIDGETS_TIMER_SET_TYPE, 49 data: { 50 timerType: "focus", 51 }, 52 }) 53 ); 54 } 55 56 function Widgets() { 57 const prefs = useSelector(state => state.Prefs.values); 58 const { messageData } = useSelector(state => state.Messages); 59 const timerType = useSelector(state => state.TimerWidget.timerType); 60 const timerData = useSelector(state => state.TimerWidget); 61 const isMaximized = prefs[PREF_WIDGETS_MAXIMIZED]; 62 const dispatch = useDispatch(); 63 64 const nimbusListsEnabled = prefs.widgetsConfig?.listsEnabled; 65 const nimbusTimerEnabled = prefs.widgetsConfig?.timerEnabled; 66 const nimbusWeatherForecastEnabled = 67 prefs.widgetsConfig?.weatherForecastEnabled; 68 const nimbusListsTrainhopEnabled = 69 prefs.trainhopConfig?.widgets?.listsEnabled; 70 const nimbusTimerTrainhopEnabled = 71 prefs.trainhopConfig?.widgets?.timerEnabled; 72 const nimbusWeatherForecastTrainhopEnabled = 73 prefs.trainhopConfig?.widgets?.weatherForecastEnabled; 74 75 const listsEnabled = 76 (nimbusListsTrainhopEnabled || 77 nimbusListsEnabled || 78 prefs[PREF_WIDGETS_SYSTEM_LISTS_ENABLED]) && 79 prefs[PREF_WIDGETS_LISTS_ENABLED]; 80 81 const timerEnabled = 82 (nimbusTimerTrainhopEnabled || 83 nimbusTimerEnabled || 84 prefs[PREF_WIDGETS_SYSTEM_TIMER_ENABLED]) && 85 prefs[PREF_WIDGETS_TIMER_ENABLED]; 86 87 const weatherForecastEnabled = 88 (nimbusWeatherForecastTrainhopEnabled || 89 nimbusWeatherForecastEnabled || 90 prefs[PREF_WIDGETS_SYSTEM_WEATHER_FORECAST_ENABLED]) && 91 prefs[PREF_WIDGETS_WEATHER_FORECAST_ENABLED]; 92 93 // track previous timerEnabled state to detect when it becomes disabled 94 const prevTimerEnabledRef = useRef(timerEnabled); 95 96 // Reset timer when it becomes disabled 97 useEffect(() => { 98 const wasTimerEnabled = prevTimerEnabledRef.current; 99 const isTimerEnabled = timerEnabled; 100 101 // Only reset if timer was enabled and is now disabled 102 if (wasTimerEnabled && !isTimerEnabled && timerData) { 103 resetTimerToDefaults(dispatch, timerType); 104 } 105 106 // Update the ref to track current state 107 prevTimerEnabledRef.current = isTimerEnabled; 108 }, [timerEnabled, timerData, dispatch, timerType]); 109 110 // Sends a dispatch to disable all widgets 111 function handleHideAllWidgetsClick(e) { 112 e.preventDefault(); 113 batch(() => { 114 dispatch(ac.SetPref(PREF_WIDGETS_LISTS_ENABLED, false)); 115 dispatch(ac.SetPref(PREF_WIDGETS_TIMER_ENABLED, false)); 116 }); 117 } 118 119 function handleHideAllWidgetsKeyDown(e) { 120 if (e.key === "Enter" || e.key === " ") { 121 e.preventDefault(); 122 batch(() => { 123 dispatch(ac.SetPref(PREF_WIDGETS_LISTS_ENABLED, false)); 124 dispatch(ac.SetPref(PREF_WIDGETS_TIMER_ENABLED, false)); 125 }); 126 } 127 } 128 129 // Toggles the maximized state of widgets 130 function handleToggleMaximizeClick(e) { 131 e.preventDefault(); 132 dispatch(ac.SetPref(PREF_WIDGETS_MAXIMIZED, !isMaximized)); 133 } 134 135 function handleToggleMaximizeKeyDown(e) { 136 if (e.key === "Enter" || e.key === " ") { 137 e.preventDefault(); 138 dispatch(ac.SetPref(PREF_WIDGETS_MAXIMIZED, !isMaximized)); 139 } 140 } 141 142 function handleUserInteraction(widgetName) { 143 const prefName = `widgets.${widgetName}.interaction`; 144 const hasInteracted = prefs[prefName]; 145 // we want to make sure that the value is a strict false (and that the property exists) 146 if (hasInteracted === false) { 147 dispatch(ac.SetPref(prefName, true)); 148 } 149 } 150 151 if (!(listsEnabled || timerEnabled || weatherForecastEnabled)) { 152 return null; 153 } 154 155 return ( 156 <div className="widgets-wrapper"> 157 <div className="widgets-section-container"> 158 <div className="widgets-title-container"> 159 <h1 data-l10n-id="newtab-widget-section-title"></h1> 160 {prefs[PREF_WIDGETS_SYSTEM_MAXIMIZED] && ( 161 <moz-button 162 id="toggle-widgets-size-button" 163 type="icon ghost" 164 size="small" 165 // Toggle the icon and hover text 166 data-l10n-id={ 167 isMaximized 168 ? "newtab-widget-section-maximize" 169 : "newtab-widget-section-minimize" 170 } 171 iconsrc={`chrome://browser/skin/${isMaximized ? "fullscreen" : "fullscreen-exit"}.svg`} 172 onClick={handleToggleMaximizeClick} 173 onKeyDown={handleToggleMaximizeKeyDown} 174 /> 175 )} 176 <moz-button 177 id="hide-all-widgets-button" 178 type="icon ghost" 179 size="small" 180 data-l10n-id="newtab-widget-section-hide-all-button" 181 iconsrc="chrome://global/skin/icons/close.svg" 182 onClick={handleHideAllWidgetsClick} 183 onKeyDown={handleHideAllWidgetsKeyDown} 184 /> 185 </div> 186 <div 187 className={`widgets-container ${isMaximized ? "is-maximized" : ""}`} 188 > 189 {listsEnabled && ( 190 <Lists 191 dispatch={dispatch} 192 handleUserInteraction={handleUserInteraction} 193 isMaximized={isMaximized} 194 /> 195 )} 196 {timerEnabled && ( 197 <FocusTimer 198 dispatch={dispatch} 199 handleUserInteraction={handleUserInteraction} 200 isMaximized={isMaximized} 201 /> 202 )} 203 {weatherForecastEnabled && ( 204 <WeatherForecast 205 dispatch={dispatch} 206 handleUserInteraction={handleUserInteraction} 207 isMaximized={isMaximized} 208 /> 209 )} 210 </div> 211 </div> 212 {messageData?.content?.messageType === "WidgetMessage" && ( 213 <MessageWrapper dispatch={dispatch}> 214 <WidgetsFeatureHighlight dispatch={dispatch} /> 215 </MessageWrapper> 216 )} 217 </div> 218 ); 219 } 220 221 export { Widgets };