tor-browser

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

SectionsMgmtPanel.jsx (9590B)


      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, { useState, useCallback, useEffect, useRef } from "react";
      6 import { useDispatch, useSelector } from "react-redux";
      7 import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
      8 // eslint-disable-next-line no-shadow
      9 import { CSSTransition } from "react-transition-group";
     10 
     11 function SectionsMgmtPanel({
     12  exitEventFired,
     13  pocketEnabled,
     14  onSubpanelToggle,
     15  togglePanel,
     16  showPanel,
     17 }) {
     18  const arrowButtonRef = useRef(null);
     19  const { sectionPersonalization } = useSelector(
     20    state => state.DiscoveryStream
     21  );
     22  const layoutComponents = useSelector(
     23    state => state.DiscoveryStream.layout[0].components
     24  );
     25  const sections = useSelector(state => state.DiscoveryStream.feeds.data);
     26  const dispatch = useDispatch();
     27 
     28  // TODO: Wrap sectionsFeedName -> sectionsList logic in try...catch?
     29  let sectionsFeedName;
     30 
     31  const cardGridEntry = layoutComponents.find(item => item.type === "CardGrid");
     32 
     33  if (cardGridEntry) {
     34    sectionsFeedName = cardGridEntry.feed.url;
     35  }
     36 
     37  let sectionsList;
     38 
     39  if (sectionsFeedName) {
     40    sectionsList = sections[sectionsFeedName].data.sections;
     41  }
     42 
     43  const [sectionsState, setSectionState] = useState(sectionPersonalization); // State management with useState
     44 
     45  let followedSectionsData = sectionsList.filter(
     46    item => sectionsState[item.sectionKey]?.isFollowed
     47  );
     48 
     49  let blockedSectionsData = sectionsList.filter(
     50    item => sectionsState[item.sectionKey]?.isBlocked
     51  );
     52 
     53  function updateCachedData() {
     54    // Reset cached followed/blocked list data while panel is open
     55    setSectionState(sectionPersonalization);
     56 
     57    followedSectionsData = sectionsList.filter(
     58      item => sectionsState[item.sectionKey]?.isFollowed
     59    );
     60 
     61    blockedSectionsData = sectionsList.filter(
     62      item => sectionsState[item.sectionKey]?.isBlocked
     63    );
     64  }
     65 
     66  const onFollowClick = useCallback(
     67    (sectionKey, receivedRank) => {
     68      dispatch(
     69        ac.AlsoToMain({
     70          type: at.SECTION_PERSONALIZATION_SET,
     71          data: {
     72            ...sectionPersonalization,
     73            [sectionKey]: {
     74              isFollowed: true,
     75              isBlocked: false,
     76              followedAt: new Date().toISOString(),
     77            },
     78          },
     79        })
     80      );
     81      // Telemetry Event Dispatch
     82      dispatch(
     83        ac.OnlyToMain({
     84          type: "FOLLOW_SECTION",
     85          data: {
     86            section: sectionKey,
     87            section_position: receivedRank,
     88            event_source: "CUSTOMIZE_PANEL",
     89          },
     90        })
     91      );
     92    },
     93    [dispatch, sectionPersonalization]
     94  );
     95 
     96  const onBlockClick = useCallback(
     97    (sectionKey, receivedRank) => {
     98      dispatch(
     99        ac.AlsoToMain({
    100          type: at.SECTION_PERSONALIZATION_SET,
    101          data: {
    102            ...sectionPersonalization,
    103            [sectionKey]: {
    104              isFollowed: false,
    105              isBlocked: true,
    106            },
    107          },
    108        })
    109      );
    110 
    111      // Telemetry Event Dispatch
    112      dispatch(
    113        ac.OnlyToMain({
    114          type: "BLOCK_SECTION",
    115          data: {
    116            section: sectionKey,
    117            section_position: receivedRank,
    118            event_source: "CUSTOMIZE_PANEL",
    119          },
    120        })
    121      );
    122    },
    123    [dispatch, sectionPersonalization]
    124  );
    125 
    126  const onUnblockClick = useCallback(
    127    (sectionKey, receivedRank) => {
    128      const updatedSectionData = { ...sectionPersonalization };
    129      delete updatedSectionData[sectionKey];
    130      dispatch(
    131        ac.AlsoToMain({
    132          type: at.SECTION_PERSONALIZATION_SET,
    133          data: updatedSectionData,
    134        })
    135      );
    136      // Telemetry Event Dispatch
    137      dispatch(
    138        ac.OnlyToMain({
    139          type: "UNBLOCK_SECTION",
    140          data: {
    141            section: sectionKey,
    142            section_position: receivedRank,
    143            event_source: "CUSTOMIZE_PANEL",
    144          },
    145        })
    146      );
    147    },
    148    [dispatch, sectionPersonalization]
    149  );
    150 
    151  const onUnfollowClick = useCallback(
    152    (sectionKey, receivedRank) => {
    153      const updatedSectionData = { ...sectionPersonalization };
    154      delete updatedSectionData[sectionKey];
    155      dispatch(
    156        ac.AlsoToMain({
    157          type: at.SECTION_PERSONALIZATION_SET,
    158          data: updatedSectionData,
    159        })
    160      );
    161      // Telemetry Event Dispatch
    162      dispatch(
    163        ac.OnlyToMain({
    164          type: "UNFOLLOW_SECTION",
    165          data: {
    166            section: sectionKey,
    167            section_position: receivedRank,
    168            event_source: "CUSTOMIZE_PANEL",
    169          },
    170        })
    171      );
    172    },
    173    [dispatch, sectionPersonalization]
    174  );
    175 
    176  // Close followed/blocked topic subpanel when parent menu is closed
    177  useEffect(() => {
    178    if (exitEventFired && showPanel) {
    179      togglePanel();
    180    }
    181  }, [exitEventFired, showPanel, togglePanel]);
    182 
    183  // Notify parent menu when subpanel opens/closes
    184  useEffect(() => {
    185    if (onSubpanelToggle) {
    186      onSubpanelToggle(showPanel);
    187    }
    188  }, [showPanel, onSubpanelToggle]);
    189 
    190  useEffect(() => {
    191    if (showPanel) {
    192      updateCachedData();
    193    }
    194    // eslint-disable-next-line react-hooks/exhaustive-deps
    195  }, [showPanel]);
    196 
    197  const handlePanelEntered = () => {
    198    arrowButtonRef.current?.focus();
    199  };
    200 
    201  const followedSectionsList = followedSectionsData.map(
    202    ({ sectionKey, title, receivedRank }) => {
    203      const following = sectionPersonalization[sectionKey]?.isFollowed;
    204 
    205      return (
    206        <li key={sectionKey}>
    207          <label htmlFor={`follow-topic-${sectionKey}`}>{title}</label>
    208          <div
    209            className={
    210              following ? "section-follow following" : "section-follow"
    211            }
    212          >
    213            <moz-button
    214              onClick={() =>
    215                following
    216                  ? onUnfollowClick(sectionKey, receivedRank)
    217                  : onFollowClick(sectionKey, receivedRank)
    218              }
    219              type={"default"}
    220              index={receivedRank}
    221              section={sectionKey}
    222              id={`follow-topic-${sectionKey}`}
    223            >
    224              <span
    225                className="section-button-follow-text"
    226                data-l10n-id="newtab-section-follow-button"
    227              />
    228              <span
    229                className="section-button-following-text"
    230                data-l10n-id="newtab-section-following-button"
    231              />
    232              <span
    233                className="section-button-unfollow-text"
    234                data-l10n-id="newtab-section-unfollow-button"
    235              />
    236            </moz-button>
    237          </div>
    238        </li>
    239      );
    240    }
    241  );
    242 
    243  const blockedSectionsList = blockedSectionsData.map(
    244    ({ sectionKey, title, receivedRank }) => {
    245      const blocked = sectionPersonalization[sectionKey]?.isBlocked;
    246 
    247      return (
    248        <li key={sectionKey}>
    249          <label htmlFor={`blocked-topic-${sectionKey}`}>{title}</label>
    250          <div className={blocked ? "section-block blocked" : "section-block"}>
    251            <moz-button
    252              onClick={() =>
    253                blocked
    254                  ? onUnblockClick(sectionKey, receivedRank)
    255                  : onBlockClick(sectionKey, receivedRank)
    256              }
    257              type="default"
    258              index={receivedRank}
    259              section={sectionKey}
    260              id={`blocked-topic-${sectionKey}`}
    261            >
    262              <span
    263                className="section-button-block-text"
    264                data-l10n-id="newtab-section-block-button"
    265              />
    266              <span
    267                className="section-button-blocked-text"
    268                data-l10n-id="newtab-section-blocked-button"
    269              />
    270              <span
    271                className="section-button-unblock-text"
    272                data-l10n-id="newtab-section-unblock-button"
    273              />
    274            </moz-button>
    275          </div>
    276        </li>
    277      );
    278    }
    279  );
    280 
    281  return (
    282    <div>
    283      <moz-box-button
    284        onClick={togglePanel}
    285        data-l10n-id="newtab-section-manage-topics-button-v2"
    286        {...(!pocketEnabled ? { disabled: true } : {})}
    287      ></moz-box-button>
    288      <CSSTransition
    289        in={showPanel}
    290        timeout={300}
    291        classNames="sections-mgmt-panel"
    292        unmountOnExit={true}
    293        onEntered={handlePanelEntered}
    294      >
    295        <div className="sections-mgmt-panel">
    296          <button
    297            ref={arrowButtonRef}
    298            className="arrow-button"
    299            onClick={togglePanel}
    300          >
    301            <h1 data-l10n-id="newtab-section-mangage-topics-title"></h1>
    302          </button>
    303          <h3 data-l10n-id="newtab-section-mangage-topics-followed-topics"></h3>
    304          {followedSectionsData.length ? (
    305            <ul className="topic-list">{followedSectionsList}</ul>
    306          ) : (
    307            <span
    308              className="topic-list-empty-state"
    309              data-l10n-id="newtab-section-mangage-topics-followed-topics-empty-state"
    310            ></span>
    311          )}
    312          <h3 data-l10n-id="newtab-section-mangage-topics-blocked-topics"></h3>
    313          {blockedSectionsData.length ? (
    314            <ul className="topic-list">{blockedSectionsList}</ul>
    315          ) : (
    316            <span
    317              className="topic-list-empty-state"
    318              data-l10n-id="newtab-section-mangage-topics-blocked-topics-empty-state"
    319            ></span>
    320          )}
    321        </div>
    322      </CSSTransition>
    323    </div>
    324  );
    325 }
    326 
    327 export { SectionsMgmtPanel };