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