Bug 1940566 - Add section to manage followed/blocked topics in Customize panel r=home-newtab-reviewers,fluent-reviewers,bolsson,amy

Differential Revision: https://phabricator.services.mozilla.com/D234219
This commit is contained in:
Maxx Crawford
2025-01-17 16:49:56 +00:00
parent 00d84f5344
commit 32ca8aa13e
17 changed files with 961 additions and 3 deletions

View File

@@ -108,6 +108,11 @@ function templateHTML(options) {
type="module"
src="chrome://global/content/elements/moz-button-group.mjs"
></script>
<script
async
type="module"
src="chrome://global/content/elements/moz-box-button.mjs"
></script>
</body>
</html>
`.trimLeft();

View File

@@ -178,6 +178,7 @@ for (const type of [
"TOP_SITES_UPDATED",
"TOTAL_BOOKMARKS_REQUEST",
"TOTAL_BOOKMARKS_RESPONSE",
"UNBLOCK_SECTION",
"UNFOLLOW_SECTION",
"UNINIT",
"UPDATE_PINNED_SEARCH_SHORTCUTS",

View File

@@ -629,6 +629,14 @@ export class BaseContent extends React.PureComponent {
prefs["discoverystream.thumbsUpDown.searchTopsitesCompact"];
const hasThumbsUpDown = prefs["discoverystream.thumbsUpDown.enabled"];
const sectionsEnabled = prefs["discoverystream.sections.enabled"];
const topicLabelsEnabled = prefs["discoverystream.topicLabels.enabled"];
const sectionsCustomizeMenuPanelEnabled =
prefs["discoverystream.sections.customizeMenuPanel.enabled"];
// Logic to show follow/block topic mgmt panel in Customize panel
const mayHaveTopicSections =
topicLabelsEnabled &&
sectionsEnabled &&
sectionsCustomizeMenuPanelEnabled;
const featureClassName = [
weatherEnabled && mayHaveWeather && "has-weather", // Show is weather is enabled/visible
@@ -678,6 +686,7 @@ export class BaseContent extends React.PureComponent {
wallpapersV2Enabled={wallpapersV2Enabled}
activeWallpaper={activeWallpaper}
pocketRegion={pocketRegion}
mayHaveTopicSections={mayHaveTopicSections}
mayHaveSponsoredTopSites={mayHaveSponsoredTopSites}
mayHaveSponsoredStories={mayHaveSponsoredStories}
mayHaveWeather={mayHaveWeather}

View File

@@ -4,6 +4,7 @@
import React from "react";
import { actionCreators as ac } from "common/Actions.mjs";
import { SectionsMgmtPanel } from "../SectionsMgmtPanel/SectionsMgmtPanel";
import { SafeAnchor } from "../../DiscoveryStreamComponents/SafeAnchor/SafeAnchor";
import { WallpapersSection } from "../../WallpapersSection/WallpapersSection";
import { WallpaperCategories } from "../../WallpapersSection/WallpaperCategories";
@@ -103,6 +104,8 @@ export class ContentSection extends React.PureComponent {
wallpapersV2Enabled,
activeWallpaper,
setPref,
mayHaveTopicSections,
exitEventFired,
} = this.props;
const {
topSitesEnabled,
@@ -242,6 +245,9 @@ export class ContentSection extends React.PureComponent {
/>
</div>
)}
{mayHaveTopicSections && (
<SectionsMgmtPanel exitEventFired={exitEventFired} />
)}
{mayHaveRecentSaves && (
<div className="check-wrapper" role="presentation">
<input

View File

@@ -13,15 +13,20 @@ export class _CustomizeMenu extends React.PureComponent {
super(props);
this.onEntered = this.onEntered.bind(this);
this.onExited = this.onExited.bind(this);
this.state = {
exitEventFired: false,
};
}
onEntered() {
this.setState({ exitEventFired: false });
if (this.closeButton) {
this.closeButton.focus();
}
}
onExited() {
this.setState({ exitEventFired: true });
if (this.openButton) {
this.openButton.focus();
}
@@ -77,12 +82,14 @@ export class _CustomizeMenu extends React.PureComponent {
wallpapersV2Enabled={this.props.wallpapersV2Enabled}
activeWallpaper={this.props.activeWallpaper}
pocketRegion={this.props.pocketRegion}
mayHaveTopicSections={this.props.mayHaveTopicSections}
mayHaveSponsoredTopSites={this.props.mayHaveSponsoredTopSites}
mayHaveSponsoredStories={this.props.mayHaveSponsoredStories}
mayHaveRecentSaves={this.props.DiscoveryStream.recentSavesEnabled}
mayHaveWeather={this.props.mayHaveWeather}
spocMessageVariant={this.props.spocMessageVariant}
dispatch={this.props.dispatch}
exitEventFired={this.state.exitEventFired}
/>
</div>
</CSSTransition>

View File

@@ -0,0 +1,314 @@
/* 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 } from "react";
import { useDispatch, useSelector } from "react-redux";
import { actionCreators as ac } from "common/Actions.mjs";
// eslint-disable-next-line no-shadow
import { CSSTransition } from "react-transition-group";
const PREF_FOLLOWED_SECTIONS = "discoverystream.sections.following";
const PREF_BLOCKED_SECTIONS = "discoverystream.sections.blocked";
/**
* Transforms a comma-separated string of topics in user preferences
* into a cleaned-up array.
*
* @param pref
* @returns string[]
*/
// TODO: DRY Issue: Import function from CardSections.jsx?
const getTopics = pref => {
return pref
.split(",")
.map(item => item.trim())
.filter(item => item);
};
function SectionsMgmtPanel({ exitEventFired }) {
const [showPanel, setShowPanel] = useState(false); // State management with useState
const prefs = useSelector(state => state.Prefs.values);
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 followedSectionsPref = prefs[PREF_FOLLOWED_SECTIONS] || "";
const blockedSectionsPref = prefs[PREF_BLOCKED_SECTIONS] || "";
const followedSections = getTopics(followedSectionsPref);
const blockedSections = getTopics(blockedSectionsPref);
const [followedSectionsState, setFollowedSectionsState] =
useState(followedSectionsPref); // State management with useState
const [blockedSectionsState, setBlockedSectionsState] =
useState(blockedSectionsPref); // State management with useState
let followedSectionsData = sectionsList.filter(item =>
followedSectionsState.includes(item.sectionKey)
);
let blockedSectionsData = sectionsList.filter(item =>
blockedSectionsState.includes(item.sectionKey)
);
function updateCachedData() {
// Reset cached followed/blocked list data while panel is open
setFollowedSectionsState(followedSectionsPref);
setBlockedSectionsState(blockedSectionsPref);
followedSectionsData = sectionsList.filter(item =>
followedSectionsState.includes(item.sectionKey)
);
blockedSectionsData = sectionsList.filter(item =>
blockedSectionsState.includes(item.sectionKey)
);
}
const onFollowClick = useCallback(
(sectionKey, receivedRank) => {
dispatch(
ac.SetPref(
PREF_FOLLOWED_SECTIONS,
[...followedSections, sectionKey].join(", ")
)
);
// Telemetry Event Dispatch
dispatch(
ac.OnlyToMain({
type: "FOLLOW_SECTION",
data: {
section: sectionKey,
section_position: receivedRank,
event_source: "CUSTOMIZE_PANEL",
},
})
);
},
[dispatch, followedSections]
);
const onBlockClick = useCallback(
(sectionKey, receivedRank) => {
dispatch(
ac.SetPref(
PREF_BLOCKED_SECTIONS,
[...blockedSections, sectionKey].join(", ")
)
);
// Telemetry Event Dispatch
dispatch(
ac.OnlyToMain({
type: "BLOCK_SECTION",
data: {
section: sectionKey,
section_position: receivedRank,
event_source: "CUSTOMIZE_PANEL",
},
})
);
},
[dispatch, blockedSections]
);
const onUnblockClick = useCallback(
(sectionKey, receivedRank) => {
dispatch(
ac.SetPref(
PREF_BLOCKED_SECTIONS,
[...blockedSections.filter(item => item !== sectionKey)].join(", ")
)
);
// Telemetry Event Dispatch
dispatch(
ac.OnlyToMain({
type: "UNBLOCK_SECTION",
data: {
section: sectionKey,
section_position: receivedRank,
event_source: "CUSTOMIZE_PANEL",
},
})
);
},
[dispatch, blockedSections]
);
const onUnfollowClick = useCallback(
(sectionKey, receivedRank) => {
dispatch(
ac.SetPref(
PREF_FOLLOWED_SECTIONS,
[...followedSections.filter(item => item !== sectionKey)].join(", ")
)
);
// Telemetry Event Dispatch
dispatch(
ac.OnlyToMain({
type: "UNFOLLOW_SECTION",
data: {
section: sectionKey,
section_position: receivedRank,
event_source: "CUSTOMIZE_PANEL",
},
})
);
},
[dispatch, followedSections]
);
// Close followed/blocked topic subpanel when parent menu is closed
useEffect(() => {
if (exitEventFired) {
setShowPanel(false);
}
}, [exitEventFired]);
const togglePanel = () => {
setShowPanel(prevShowPanel => !prevShowPanel);
// Fire when the panel is open
if (!showPanel) {
updateCachedData();
}
};
const followedSectionsList = followedSectionsData.map(
({ sectionKey, title, receivedRank }) => {
const following = followedSections.includes(sectionKey);
return (
<li key={sectionKey}>
<label htmlFor={`follow-topic-${sectionKey}`}>{title}</label>
<div
className={
following ? "section-follow following" : "section-follow"
}
>
<moz-button
onClick={() =>
following
? onUnfollowClick(sectionKey, receivedRank)
: onFollowClick(sectionKey, receivedRank)
}
type={following ? "destructive" : "default"}
index={receivedRank}
section={sectionKey}
id={`follow-topic-${sectionKey}`}
>
<span
className="section-button-follow-text"
data-l10n-id="newtab-section-follow-button"
/>
<span
className="section-button-following-text"
data-l10n-id="newtab-section-following-button"
/>
<span
className="section-button-unfollow-text"
data-l10n-id="newtab-section-unfollow-button"
/>
</moz-button>
</div>
</li>
);
}
);
const blockedSectionsList = blockedSectionsData.map(
({ sectionKey, title, receivedRank }) => {
const blocked = blockedSections.includes(sectionKey);
return (
<li key={sectionKey}>
<label htmlFor={`blocked-topic-${sectionKey}`}>{title}</label>
<div className={blocked ? "section-block blocked" : "section-block"}>
<moz-button
onClick={() =>
blocked
? onUnblockClick(sectionKey, receivedRank)
: onBlockClick(sectionKey, receivedRank)
}
type="default"
index={receivedRank}
section={sectionKey}
id={`blocked-topic-${sectionKey}`}
>
<span
className="section-button-block-text"
data-l10n-id="newtab-section-block-button"
/>
<span
className="section-button-blocked-text"
data-l10n-id="newtab-section-blocked-button"
/>
<span
className="section-button-unblock-text"
data-l10n-id="newtab-section-unblock-button"
/>
</moz-button>
</div>
</li>
);
}
);
return (
<div>
<moz-box-button
onClick={togglePanel}
data-l10n-id="newtab-section-mangage-topics-button"
></moz-box-button>
<CSSTransition
in={showPanel}
timeout={300}
classNames="sections-mgmt-panel"
unmountOnExit={true}
>
<div className="sections-mgmt-panel">
<button className="arrow-button" onClick={togglePanel}>
<h1 data-l10n-id="newtab-section-mangage-topics-title"></h1>
</button>
<h3 data-l10n-id="newtab-section-mangage-topics-followed-topics-subtitle"></h3>
{followedSectionsData.length ? (
<ul className="topic-list">{followedSectionsList}</ul>
) : (
<span
className="topic-list-empty-state"
data-l10n-id="newtab-section-mangage-topics-followed-topics-empty-state"
></span>
)}
<h3 data-l10n-id="newtab-section-mangage-topics-blocked-topics-subtitle"></h3>
{blockedSectionsData.length ? (
<ul className="topic-list">{blockedSectionsList}</ul>
) : (
<span
className="topic-list-empty-state"
data-l10n-id="newtab-section-mangage-topics-blocked-topics-empty-state"
></span>
)}
</div>
</CSSTransition>
</div>
);
}
export { SectionsMgmtPanel };

View File

@@ -252,6 +252,18 @@
}
}
.more-info-pocket-wrapper {
.more-information {
// Note: This is necessary so the follow/block topics panel can
// be positioned absolutely across the entire Customize menu
position: static;
}
.check-wrapper {
margin-block-end: var(--space-small);
}
}
.more-info-top-wrapper {
.check-wrapper {
margin-block-start: var(--space-large);
@@ -349,3 +361,157 @@
}
}
}
.sections-mgmt-panel {
/* XXXdholbert The 32px subtraction here is to account for our 16px of
* margin on top and bottom. Ideally this should change to use
* 'height: stretch' when bug 1789477 lands. */
height: calc(100% - var(--space-xxlarge));
// Width of panel minus the margins
inset-inline-start: var(--space-xlarge);
position: absolute;
top: 0;
width: 380px;
z-index: 2;
transform: translateX(100%);
margin-block: var(--space-large) 0;
padding: 0;
background-color: var(--newtab-background-color-secondary);
&:dir(rtl) {
transform: translateX(-100%);
}
@media (prefers-reduced-motion: no-preference) {
&.sections-mgmt-panel-enter,
&.sections-mgmt-panel-exit,
&.sections-mgmt-panel-enter-done,
&.sections-mgmt-panel-exit-done {
transition: transform 300ms;
}
}
&.sections-mgmt-panel-enter-done,
&.sections-mgmt-panel-enter-active {
transform: translateX(0);
}
// Typography
h3 {
margin-block: 0 var(--space-small);
}
// List
.topic-list {
@include wallpaper-contrast-fix;
list-style: none;
display: flex;
flex-direction: column;
gap: var(--space-small);
margin-block: 0 var(--space-xxlarge);
padding-inline: 0;
width: 100%;
li {
display: flex;
justify-content: space-between;
}
}
.topic-list-empty-state {
display: block;
margin-block-end: var(--space-xxlarge);
color: var(--text-color-deemphasized);
}
// Buttons
.arrow-button {
background: url('chrome://global/skin/icons/arrow-left.svg') no-repeat left center;
&:dir(rtl) {
background: url('chrome://global/skin/icons/arrow-right.svg') no-repeat right center;
}
border: none;
cursor: pointer;
font-size: var(--font-size-root);
margin-block-end: var(--space-xlarge);
padding-inline-start: var(--space-xlarge);
-moz-context-properties: fill;
fill: currentColor;
min-height: var(--space-large);
h1 {
font-size: var(--font-size-root);
margin-block: 0;
font-weight: var(--button-font-weight);
}
}
// Follow / Unfollow and Block / Unblock Buttons
.section-block,
.section-follow {
cursor: pointer;
.section-button-blocked-text,
.section-button-following-text {
display: none;
}
.section-button-unblock-text,
.section-button-unfollow-text {
display: none;
}
&.following {
.section-button-follow-text {
display: none;
}
.section-button-following-text {
display: block;
}
}
&.following:not(:hover) {
moz-button {
--button-background-color-destructive: var(--button-background-color);
--button-text-color-destructive: var(--button-text-color);
--button-border-color-destructive: var(--button-border-color);
}
}
&.following:hover {
.section-button-following-text {
display: none;
}
.section-button-unfollow-text {
display: block;
}
}
&.blocked {
.section-button-block-text {
display: none;
}
.section-button-blocked-text {
display: block;
}
}
&.blocked:hover {
.section-button-blocked-text {
display: none;
}
.section-button-unblock-text {
display: block;
}
}
}
}

View File

@@ -2209,6 +2209,12 @@ main section {
position: relative;
transition: margin-top 250ms cubic-bezier(0.82, 0.085, 0.395, 0.895);
}
.home-section .section .more-info-pocket-wrapper .more-information {
position: static;
}
.home-section .section .more-info-pocket-wrapper .check-wrapper {
margin-block-end: var(--space-small);
}
.home-section .section .more-info-top-wrapper .check-wrapper {
margin-block-start: var(--space-large);
}
@@ -2283,6 +2289,133 @@ main section {
outline: 0;
}
.sections-mgmt-panel {
/* XXXdholbert The 32px subtraction here is to account for our 16px of
* margin on top and bottom. Ideally this should change to use
* 'height: stretch' when bug 1789477 lands. */
height: calc(100% - var(--space-xxlarge));
inset-inline-start: var(--space-xlarge);
position: absolute;
top: 0;
width: 380px;
z-index: 2;
transform: translateX(100%);
margin-block: var(--space-large) 0;
padding: 0;
background-color: var(--newtab-background-color-secondary);
}
.sections-mgmt-panel:dir(rtl) {
transform: translateX(-100%);
}
@media (prefers-reduced-motion: no-preference) {
.sections-mgmt-panel.sections-mgmt-panel-enter, .sections-mgmt-panel.sections-mgmt-panel-exit, .sections-mgmt-panel.sections-mgmt-panel-enter-done, .sections-mgmt-panel.sections-mgmt-panel-exit-done {
transition: transform 300ms;
}
}
.sections-mgmt-panel.sections-mgmt-panel-enter-done, .sections-mgmt-panel.sections-mgmt-panel-enter-active {
transform: translateX(0);
}
.sections-mgmt-panel h3 {
margin-block: 0 var(--space-small);
}
.sections-mgmt-panel .topic-list {
list-style: none;
display: flex;
flex-direction: column;
gap: var(--space-small);
margin-block: 0 var(--space-xxlarge);
padding-inline: 0;
width: 100%;
}
.lightWallpaper .sections-mgmt-panel .topic-list {
color-scheme: light;
}
.darkWallpaper .sections-mgmt-panel .topic-list {
color-scheme: dark;
}
.sections-mgmt-panel .topic-list li {
display: flex;
justify-content: space-between;
}
.sections-mgmt-panel .topic-list-empty-state {
display: block;
margin-block-end: var(--space-xxlarge);
color: var(--text-color-deemphasized);
}
.sections-mgmt-panel .arrow-button {
background: url("chrome://global/skin/icons/arrow-left.svg") no-repeat left center;
border: none;
cursor: pointer;
font-size: var(--font-size-root);
margin-block-end: var(--space-xlarge);
padding-inline-start: var(--space-xlarge);
-moz-context-properties: fill;
fill: currentColor;
min-height: var(--space-large);
}
.sections-mgmt-panel .arrow-button:dir(rtl) {
background: url("chrome://global/skin/icons/arrow-right.svg") no-repeat right center;
}
.sections-mgmt-panel .arrow-button h1 {
font-size: var(--font-size-root);
margin-block: 0;
font-weight: var(--button-font-weight);
}
.sections-mgmt-panel .section-block,
.sections-mgmt-panel .section-follow {
cursor: pointer;
}
.sections-mgmt-panel .section-block .section-button-blocked-text,
.sections-mgmt-panel .section-block .section-button-following-text,
.sections-mgmt-panel .section-follow .section-button-blocked-text,
.sections-mgmt-panel .section-follow .section-button-following-text {
display: none;
}
.sections-mgmt-panel .section-block .section-button-unblock-text,
.sections-mgmt-panel .section-block .section-button-unfollow-text,
.sections-mgmt-panel .section-follow .section-button-unblock-text,
.sections-mgmt-panel .section-follow .section-button-unfollow-text {
display: none;
}
.sections-mgmt-panel .section-block.following .section-button-follow-text,
.sections-mgmt-panel .section-follow.following .section-button-follow-text {
display: none;
}
.sections-mgmt-panel .section-block.following .section-button-following-text,
.sections-mgmt-panel .section-follow.following .section-button-following-text {
display: block;
}
.sections-mgmt-panel .section-block.following:not(:hover) moz-button,
.sections-mgmt-panel .section-follow.following:not(:hover) moz-button {
--button-background-color-destructive: var(--button-background-color);
--button-text-color-destructive: var(--button-text-color);
--button-border-color-destructive: var(--button-border-color);
}
.sections-mgmt-panel .section-block.following:hover .section-button-following-text,
.sections-mgmt-panel .section-follow.following:hover .section-button-following-text {
display: none;
}
.sections-mgmt-panel .section-block.following:hover .section-button-unfollow-text,
.sections-mgmt-panel .section-follow.following:hover .section-button-unfollow-text {
display: block;
}
.sections-mgmt-panel .section-block.blocked .section-button-block-text,
.sections-mgmt-panel .section-follow.blocked .section-button-block-text {
display: none;
}
.sections-mgmt-panel .section-block.blocked .section-button-blocked-text,
.sections-mgmt-panel .section-follow.blocked .section-button-blocked-text {
display: block;
}
.sections-mgmt-panel .section-block.blocked:hover .section-button-blocked-text,
.sections-mgmt-panel .section-follow.blocked:hover .section-button-blocked-text {
display: none;
}
.sections-mgmt-panel .section-block.blocked:hover .section-button-unblock-text,
.sections-mgmt-panel .section-follow.blocked:hover .section-button-unblock-text {
display: block;
}
.category-list {
border: none;
display: grid;

View File

@@ -45,5 +45,6 @@
<script async type="module" src="chrome://global/content/elements/moz-toggle.mjs"></script>
<script async type="module" src="chrome://global/content/elements/moz-button.mjs"></script>
<script async type="module" src="chrome://global/content/elements/moz-button-group.mjs"></script>
<script async type="module" src="chrome://global/content/elements/moz-box-button.mjs"></script>
</body>
</html>

View File

@@ -251,6 +251,7 @@ for (const type of [
"TOP_SITES_UPDATED",
"TOTAL_BOOKMARKS_REQUEST",
"TOTAL_BOOKMARKS_RESPONSE",
"UNBLOCK_SECTION",
"UNFOLLOW_SECTION",
"UNINIT",
"UPDATE_PINNED_SEARCH_SHORTCUTS",
@@ -10695,6 +10696,219 @@ const DiscoveryStreamBase = (0,external_ReactRedux_namespaceObject.connect)(stat
document: globalThis.document,
App: state.App
}))(_DiscoveryStreamBase);
;// CONCATENATED MODULE: ./content-src/components/CustomizeMenu/SectionsMgmtPanel/SectionsMgmtPanel.jsx
/* 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/. */
// eslint-disable-next-line no-shadow
const SectionsMgmtPanel_PREF_FOLLOWED_SECTIONS = "discoverystream.sections.following";
const SectionsMgmtPanel_PREF_BLOCKED_SECTIONS = "discoverystream.sections.blocked";
/**
* Transforms a comma-separated string of topics in user preferences
* into a cleaned-up array.
*
* @param pref
* @returns string[]
*/
// TODO: DRY Issue: Import function from CardSections.jsx?
const SectionsMgmtPanel_getTopics = pref => {
return pref.split(",").map(item => item.trim()).filter(item => item);
};
function SectionsMgmtPanel({
exitEventFired
}) {
const [showPanel, setShowPanel] = (0,external_React_namespaceObject.useState)(false); // State management with useState
const prefs = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Prefs.values);
const layoutComponents = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.DiscoveryStream.layout[0].components);
const sections = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.DiscoveryStream.feeds.data);
const dispatch = (0,external_ReactRedux_namespaceObject.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 followedSectionsPref = prefs[SectionsMgmtPanel_PREF_FOLLOWED_SECTIONS] || "";
const blockedSectionsPref = prefs[SectionsMgmtPanel_PREF_BLOCKED_SECTIONS] || "";
const followedSections = SectionsMgmtPanel_getTopics(followedSectionsPref);
const blockedSections = SectionsMgmtPanel_getTopics(blockedSectionsPref);
const [followedSectionsState, setFollowedSectionsState] = (0,external_React_namespaceObject.useState)(followedSectionsPref); // State management with useState
const [blockedSectionsState, setBlockedSectionsState] = (0,external_React_namespaceObject.useState)(blockedSectionsPref); // State management with useState
let followedSectionsData = sectionsList.filter(item => followedSectionsState.includes(item.sectionKey));
let blockedSectionsData = sectionsList.filter(item => blockedSectionsState.includes(item.sectionKey));
function updateCachedData() {
// Reset cached followed/blocked list data while panel is open
setFollowedSectionsState(followedSectionsPref);
setBlockedSectionsState(blockedSectionsPref);
followedSectionsData = sectionsList.filter(item => followedSectionsState.includes(item.sectionKey));
blockedSectionsData = sectionsList.filter(item => blockedSectionsState.includes(item.sectionKey));
}
const onFollowClick = (0,external_React_namespaceObject.useCallback)((sectionKey, receivedRank) => {
dispatch(actionCreators.SetPref(SectionsMgmtPanel_PREF_FOLLOWED_SECTIONS, [...followedSections, sectionKey].join(", ")));
// Telemetry Event Dispatch
dispatch(actionCreators.OnlyToMain({
type: "FOLLOW_SECTION",
data: {
section: sectionKey,
section_position: receivedRank,
event_source: "CUSTOMIZE_PANEL"
}
}));
}, [dispatch, followedSections]);
const onBlockClick = (0,external_React_namespaceObject.useCallback)((sectionKey, receivedRank) => {
dispatch(actionCreators.SetPref(SectionsMgmtPanel_PREF_BLOCKED_SECTIONS, [...blockedSections, sectionKey].join(", ")));
// Telemetry Event Dispatch
dispatch(actionCreators.OnlyToMain({
type: "BLOCK_SECTION",
data: {
section: sectionKey,
section_position: receivedRank,
event_source: "CUSTOMIZE_PANEL"
}
}));
}, [dispatch, blockedSections]);
const onUnblockClick = (0,external_React_namespaceObject.useCallback)((sectionKey, receivedRank) => {
dispatch(actionCreators.SetPref(SectionsMgmtPanel_PREF_BLOCKED_SECTIONS, [...blockedSections.filter(item => item !== sectionKey)].join(", ")));
// Telemetry Event Dispatch
dispatch(actionCreators.OnlyToMain({
type: "UNBLOCK_SECTION",
data: {
section: sectionKey,
section_position: receivedRank,
event_source: "CUSTOMIZE_PANEL"
}
}));
}, [dispatch, blockedSections]);
const onUnfollowClick = (0,external_React_namespaceObject.useCallback)((sectionKey, receivedRank) => {
dispatch(actionCreators.SetPref(SectionsMgmtPanel_PREF_FOLLOWED_SECTIONS, [...followedSections.filter(item => item !== sectionKey)].join(", ")));
// Telemetry Event Dispatch
dispatch(actionCreators.OnlyToMain({
type: "UNFOLLOW_SECTION",
data: {
section: sectionKey,
section_position: receivedRank,
event_source: "CUSTOMIZE_PANEL"
}
}));
}, [dispatch, followedSections]);
// Close followed/blocked topic subpanel when parent menu is closed
(0,external_React_namespaceObject.useEffect)(() => {
if (exitEventFired) {
setShowPanel(false);
}
}, [exitEventFired]);
const togglePanel = () => {
setShowPanel(prevShowPanel => !prevShowPanel);
// Fire when the panel is open
if (!showPanel) {
updateCachedData();
}
};
const followedSectionsList = followedSectionsData.map(({
sectionKey,
title,
receivedRank
}) => {
const following = followedSections.includes(sectionKey);
return /*#__PURE__*/external_React_default().createElement("li", {
key: sectionKey
}, /*#__PURE__*/external_React_default().createElement("label", {
htmlFor: `follow-topic-${sectionKey}`
}, title), /*#__PURE__*/external_React_default().createElement("div", {
className: following ? "section-follow following" : "section-follow"
}, /*#__PURE__*/external_React_default().createElement("moz-button", {
onClick: () => following ? onUnfollowClick(sectionKey, receivedRank) : onFollowClick(sectionKey, receivedRank),
type: following ? "destructive" : "default",
index: receivedRank,
section: sectionKey,
id: `follow-topic-${sectionKey}`
}, /*#__PURE__*/external_React_default().createElement("span", {
className: "section-button-follow-text",
"data-l10n-id": "newtab-section-follow-button"
}), /*#__PURE__*/external_React_default().createElement("span", {
className: "section-button-following-text",
"data-l10n-id": "newtab-section-following-button"
}), /*#__PURE__*/external_React_default().createElement("span", {
className: "section-button-unfollow-text",
"data-l10n-id": "newtab-section-unfollow-button"
}))));
});
const blockedSectionsList = blockedSectionsData.map(({
sectionKey,
title,
receivedRank
}) => {
const blocked = blockedSections.includes(sectionKey);
return /*#__PURE__*/external_React_default().createElement("li", {
key: sectionKey
}, /*#__PURE__*/external_React_default().createElement("label", {
htmlFor: `blocked-topic-${sectionKey}`
}, title), /*#__PURE__*/external_React_default().createElement("div", {
className: blocked ? "section-block blocked" : "section-block"
}, /*#__PURE__*/external_React_default().createElement("moz-button", {
onClick: () => blocked ? onUnblockClick(sectionKey, receivedRank) : onBlockClick(sectionKey, receivedRank),
type: "default",
index: receivedRank,
section: sectionKey,
id: `blocked-topic-${sectionKey}`
}, /*#__PURE__*/external_React_default().createElement("span", {
className: "section-button-block-text",
"data-l10n-id": "newtab-section-block-button"
}), /*#__PURE__*/external_React_default().createElement("span", {
className: "section-button-blocked-text",
"data-l10n-id": "newtab-section-blocked-button"
}), /*#__PURE__*/external_React_default().createElement("span", {
className: "section-button-unblock-text",
"data-l10n-id": "newtab-section-unblock-button"
}))));
});
return /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement("moz-box-button", {
onClick: togglePanel,
"data-l10n-id": "newtab-section-mangage-topics-button"
}), /*#__PURE__*/external_React_default().createElement(external_ReactTransitionGroup_namespaceObject.CSSTransition, {
in: showPanel,
timeout: 300,
classNames: "sections-mgmt-panel",
unmountOnExit: true
}, /*#__PURE__*/external_React_default().createElement("div", {
className: "sections-mgmt-panel"
}, /*#__PURE__*/external_React_default().createElement("button", {
className: "arrow-button",
onClick: togglePanel
}, /*#__PURE__*/external_React_default().createElement("h1", {
"data-l10n-id": "newtab-section-mangage-topics-title"
})), /*#__PURE__*/external_React_default().createElement("h3", {
"data-l10n-id": "newtab-section-mangage-topics-followed-topics-subtitle"
}), followedSectionsData.length ? /*#__PURE__*/external_React_default().createElement("ul", {
className: "topic-list"
}, followedSectionsList) : /*#__PURE__*/external_React_default().createElement("span", {
className: "topic-list-empty-state",
"data-l10n-id": "newtab-section-mangage-topics-followed-topics-empty-state"
}), /*#__PURE__*/external_React_default().createElement("h3", {
"data-l10n-id": "newtab-section-mangage-topics-blocked-topics-subtitle"
}), blockedSectionsData.length ? /*#__PURE__*/external_React_default().createElement("ul", {
className: "topic-list"
}, blockedSectionsList) : /*#__PURE__*/external_React_default().createElement("span", {
className: "topic-list-empty-state",
"data-l10n-id": "newtab-section-mangage-topics-blocked-topics-empty-state"
}))));
}
;// CONCATENATED MODULE: ./content-src/components/WallpapersSection/WallpapersSection.jsx
/* 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,
@@ -11080,6 +11294,7 @@ const WallpaperCategories = (0,external_ReactRedux_namespaceObject.connect)(stat
class ContentSection extends (external_React_default()).PureComponent {
constructor(props) {
super(props);
@@ -11162,7 +11377,9 @@ class ContentSection extends (external_React_default()).PureComponent {
wallpapersEnabled,
wallpapersV2Enabled,
activeWallpaper,
setPref
setPref,
mayHaveTopicSections,
exitEventFired
} = this.props;
const {
topSitesEnabled,
@@ -11280,7 +11497,9 @@ class ContentSection extends (external_React_default()).PureComponent {
className: "sponsored",
htmlFor: "sponsored-pocket",
"data-l10n-id": "newtab-custom-pocket-sponsored"
})), mayHaveRecentSaves && /*#__PURE__*/external_React_default().createElement("div", {
})), mayHaveTopicSections && /*#__PURE__*/external_React_default().createElement(SectionsMgmtPanel, {
exitEventFired: exitEventFired
}), mayHaveRecentSaves && /*#__PURE__*/external_React_default().createElement("div", {
className: "check-wrapper",
role: "presentation"
}, /*#__PURE__*/external_React_default().createElement("input", {
@@ -11349,13 +11568,22 @@ class _CustomizeMenu extends (external_React_default()).PureComponent {
super(props);
this.onEntered = this.onEntered.bind(this);
this.onExited = this.onExited.bind(this);
this.state = {
exitEventFired: false
};
}
onEntered() {
this.setState({
exitEventFired: false
});
if (this.closeButton) {
this.closeButton.focus();
}
}
onExited() {
this.setState({
exitEventFired: true
});
if (this.openButton) {
this.openButton.focus();
}
@@ -11402,12 +11630,14 @@ class _CustomizeMenu extends (external_React_default()).PureComponent {
wallpapersV2Enabled: this.props.wallpapersV2Enabled,
activeWallpaper: this.props.activeWallpaper,
pocketRegion: this.props.pocketRegion,
mayHaveTopicSections: this.props.mayHaveTopicSections,
mayHaveSponsoredTopSites: this.props.mayHaveSponsoredTopSites,
mayHaveSponsoredStories: this.props.mayHaveSponsoredStories,
mayHaveRecentSaves: this.props.DiscoveryStream.recentSavesEnabled,
mayHaveWeather: this.props.mayHaveWeather,
spocMessageVariant: this.props.spocMessageVariant,
dispatch: this.props.dispatch
dispatch: this.props.dispatch,
exitEventFired: this.state.exitEventFired
}))));
}
}
@@ -13005,6 +13235,10 @@ class BaseContent extends (external_React_default()).PureComponent {
const hasThumbsUpDownLayout = prefs["discoverystream.thumbsUpDown.searchTopsitesCompact"];
const hasThumbsUpDown = prefs["discoverystream.thumbsUpDown.enabled"];
const sectionsEnabled = prefs["discoverystream.sections.enabled"];
const topicLabelsEnabled = prefs["discoverystream.topicLabels.enabled"];
const sectionsCustomizeMenuPanelEnabled = prefs["discoverystream.sections.customizeMenuPanel.enabled"];
// Logic to show follow/block topic mgmt panel in Customize panel
const mayHaveTopicSections = topicLabelsEnabled && sectionsEnabled && sectionsCustomizeMenuPanelEnabled;
const featureClassName = [weatherEnabled && mayHaveWeather && "has-weather",
// Show is weather is enabled/visible
prefs.showSearch ? "has-search" : "no-search", layoutsVariantAEnabled ? "layout-variant-a" : "",
@@ -13030,6 +13264,7 @@ class BaseContent extends (external_React_default()).PureComponent {
wallpapersV2Enabled: wallpapersV2Enabled,
activeWallpaper: activeWallpaper,
pocketRegion: pocketRegion,
mayHaveTopicSections: mayHaveTopicSections,
mayHaveSponsoredTopSites: mayHaveSponsoredTopSites,
mayHaveSponsoredStories: mayHaveSponsoredStories,
mayHaveWeather: mayHaveWeather,

View File

@@ -520,6 +520,14 @@ export const PREFS_CONFIG = new Map([
value: false,
},
],
[
"discoverystream.sections.customizeMenuPanel.enabled",
{
title:
"Boolean flag to enable the setions management panel in Customize menu",
value: false,
},
],
[
"discoverystream.sections.cards.enabled",
{

View File

@@ -1195,6 +1195,8 @@ export class TelemetryFeed {
// Intentional fall-through
case at.FOLLOW_SECTION:
// Intentional fall-through
case at.UNBLOCK_SECTION:
// Intentional fall-through
case at.UNFOLLOW_SECTION: {
this.handleCardSectionUserEvent(action);
break;
@@ -1238,6 +1240,14 @@ export class TelemetryFeed {
event_source,
});
break;
case "UNBLOCK_SECTION":
Glean.newtab.sectionsUnblockSection.record({
newtab_visit_id: session.session_id,
section,
section_position,
event_source,
});
break;
case "CARD_SECTION_IMPRESSION":
Glean.newtab.sectionsImpression.record({
newtab_visit_id: session.session_id,

View File

@@ -783,6 +783,36 @@ newtab:
send_in_pings:
- newtab
sections_unblock_section:
type: event
description: >
Recorded when a section is unblocked
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1940566
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1940566
data_sensitivity:
- interaction
notification_emails:
- mcrawford@mozilla.com
expires: never
extra_keys:
newtab_visit_id: *newtab_visit_id
section:
description: >
section that had unblock event
type: string
section_position:
description: >
position of section on newtab
type: string
event_source:
description: >
Where the source of the event originated ("button", "context menu", etc.)
type: string
send_in_pings:
- newtab
newtab.search:
enabled:
lifetime: application

View File

@@ -62,5 +62,10 @@
type="module"
src="chrome://global/content/elements/moz-button-group.mjs"
></script>
<script
async
type="module"
src="chrome://global/content/elements/moz-box-button.mjs"
></script>
</body>
</html>

View File

@@ -51,5 +51,10 @@
type="module"
src="chrome://global/content/elements/moz-button-group.mjs"
></script>
<script
async
type="module"
src="chrome://global/content/elements/moz-box-button.mjs"
></script>
</body>
</html>

View File

@@ -62,5 +62,10 @@
type="module"
src="chrome://global/content/elements/moz-button-group.mjs"
></script>
<script
async
type="module"
src="chrome://global/content/elements/moz-box-button.mjs"
></script>
</body>
</html>

View File

@@ -445,9 +445,27 @@ newtab-section-follow-button = Follow
newtab-section-following-button = Following
newtab-section-unfollow-button = Unfollow
## Button to block/unblock listed topics
## "Block", "unblocked", and "blocked" are social media terms that refer to hiding a section of stories.
## e.g. Blocked the politics section of stories.
newtab-section-block-button = Block
newtab-section-blocked-button = Blocked
newtab-section-unblock-button = Unblock
## Confirmation modal for blocking a section
newtab-section-confirm-block-section-p1 = Are you sure you want to block this section?
newtab-section-confirm-block-section-p2 = Blocked section will no longer appear in your feed.
newtab-section-block-section-button = Block this section
newtab-section-cancel-button = Not now
## Panel in the Customize menu section to manage followed and blocked topics
newtab-section-mangage-topics-title = Topics
newtab-section-mangage-topics-button =
.label = Followed and blocked topics
newtab-section-mangage-topics-followed-topics-subtitle = Followed Topics
newtab-section-mangage-topics-followed-topics-empty-state = You have not followed any topics yet.
newtab-section-mangage-topics-blocked-topics-subtitle = Blocked Topics
newtab-section-mangage-topics-blocked-topics-empty-state = You have not blocked any topics yet.