tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 };