/* 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, { useState, useCallback, useEffect, useRef } from "react"; import { useDispatch, useSelector } from "react-redux"; import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; // eslint-disable-next-line no-shadow import { CSSTransition } from "react-transition-group"; function SectionsMgmtPanel({ pocketEnabled, onSubpanelToggle, togglePanel, showPanel, novaEnabled, }) { const arrowButtonRef = useRef(null); const panelRef = useRef(null); const { sectionPersonalization } = useSelector( state => state.DiscoveryStream ); const layoutComponents = useSelector( state => state.DiscoveryStream.layout[0].components ); const sections = useSelector(state => state.DiscoveryStream.feeds.data); const dispatch = useDispatch(); // TODO: Wrap sectionsFeedName -> sectionsList logic in try...catch? let sectionsFeedName; const cardGridEntry = layoutComponents.find(item => item.type === "CardGrid"); if (cardGridEntry) { sectionsFeedName = cardGridEntry.feed.url; } let sectionsList = []; if (sectionsFeedName) { sectionsList = sections[sectionsFeedName]?.data?.sections ?? []; } const [sectionsState, setSectionState] = useState(sectionPersonalization); let followedSectionsData = sectionsList.filter( item => sectionsState[item.sectionKey]?.isFollowed ); // Keys of sections currently returned by the feed . const sectionListKeys = new Set(sectionsList.map(s => s.sectionKey)); // Blocked sections still present in the feed (normal case, cache not yet expired). const blockedFromFeed = sectionsList.filter( item => sectionsState[item.sectionKey]?.isBlocked ); // Blocked sections absent from the feed (Sections not returned from merino). // Reconstructed from persisted personalization data using the title // stored at block-time. const blockedFromPersonalization = Object.entries(sectionsState) .filter( ([key, val]) => val?.isBlocked && val.title && !sectionListKeys.has(key) ) .map(([key, val]) => ({ sectionKey: key, title: val.title, })); let blockedSectionsData = [...blockedFromFeed, ...blockedFromPersonalization]; function updateCachedData() { setSectionState(sectionPersonalization); } const onFollowClick = useCallback( (sectionKey, receivedRank) => { dispatch( ac.AlsoToMain({ type: at.SECTION_PERSONALIZATION_SET, data: { ...sectionPersonalization, [sectionKey]: { isFollowed: true, isBlocked: false, followedAt: new Date().toISOString(), }, }, }) ); // Telemetry Event Dispatch dispatch( ac.OnlyToMain({ type: "FOLLOW_SECTION", data: { section: sectionKey, section_position: receivedRank, event_source: "CUSTOMIZE_PANEL", }, }) ); }, [dispatch, sectionPersonalization] ); const onBlockClick = useCallback( (sectionKey, receivedRank, title) => { dispatch( ac.AlsoToMain({ type: at.SECTION_PERSONALIZATION_SET, data: { ...sectionPersonalization, [sectionKey]: { isFollowed: false, isBlocked: true, title, }, }, }) ); // Telemetry Event Dispatch dispatch( ac.OnlyToMain({ type: "BLOCK_SECTION", data: { section: sectionKey, section_position: receivedRank, event_source: "CUSTOMIZE_PANEL", }, }) ); }, [dispatch, sectionPersonalization] ); const onUnblockClick = useCallback( (sectionKey, receivedRank) => { const updatedSectionData = { ...sectionPersonalization }; delete updatedSectionData[sectionKey]; dispatch( ac.AlsoToMain({ type: at.SECTION_PERSONALIZATION_SET, data: updatedSectionData, }) ); // Telemetry Event Dispatch dispatch( ac.OnlyToMain({ type: "UNBLOCK_SECTION", data: { section: sectionKey, section_position: receivedRank, event_source: "CUSTOMIZE_PANEL", }, }) ); }, [dispatch, sectionPersonalization] ); const onUnfollowClick = useCallback( (sectionKey, receivedRank) => { const updatedSectionData = { ...sectionPersonalization }; delete updatedSectionData[sectionKey]; dispatch( ac.AlsoToMain({ type: at.SECTION_PERSONALIZATION_SET, data: updatedSectionData, }) ); // Telemetry Event Dispatch dispatch( ac.OnlyToMain({ type: "UNFOLLOW_SECTION", data: { section: sectionKey, section_position: receivedRank, event_source: "CUSTOMIZE_PANEL", }, }) ); }, [dispatch, sectionPersonalization] ); // Notify parent menu when subpanel opens/closes useEffect(() => { if (onSubpanelToggle) { onSubpanelToggle(showPanel); } }, [showPanel, onSubpanelToggle]); useEffect(() => { if (showPanel) { updateCachedData(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [showPanel]); const handlePanelEntered = () => { arrowButtonRef.current?.focus(); }; const followedSectionsList = followedSectionsData.map( ({ sectionKey, title, receivedRank }) => { const following = sectionPersonalization[sectionKey]?.isFollowed; return (