commit ddd3820f7adf05cda06dcac92f8bb2c0477c0acf parent 71bbc300d9e01d250af91993c8ef0caceda98243 Author: Cosmin Sabou <csabou@mozilla.com> Date: Thu, 11 Dec 2025 21:05:16 +0200 Revert "Bug 1998975 Remove thumbs up/down functionality from Discovery Stream cards r=nbarrett,home-newtab-reviewers,desktop-theme-reviewers,maxx,tgiles" for causing failures on browser_all_files_referenced. This reverts commit b7429b0fcf1014d7d930342d3348f64f11172726. Diffstat:
29 files changed, 1629 insertions(+), 99 deletions(-)
diff --git a/browser/components/newtab/metrics.yaml b/browser/components/newtab/metrics.yaml @@ -2108,6 +2108,59 @@ pocket: send_in_pings: - spoc + thumb_voting_interaction: + type: event + description: > + Recorded when a thumbs up/down on a tile is clicked. + Only happens on click. Not on middle-click. Not on "Open in new Tab"-like + options in the context menu. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1902099 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1937200 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1902099 + data_sensitivity: + - interaction + notification_emails: + - mcrawford@mozilla.com + expires: never + extra_keys: + newtab_visit_id: *newtab_visit_id + recommendation_id: *recommendation_id + tile_id: *pocket_tile_id + format: *format + position: *pocket_position + thumbs_up: + description: > + If the user clicked thumbs up. + type: boolean + thumbs_down: + description: > + If the user clicked thumbs down. + type: boolean + scheduled_corpus_item_id: *scheduled_corpus_item_id + corpus_item_id: *corpus_item_id + received_rank: *received_rank + recommended_at: *recommended_at + topic: + description: The topic of the recommendation. Like "entertainment". + type: string + section: + description: > + If event belongs in a section, the name of the section + type: string + section_position: + description: > + If event belongs in a section, the numeric position of the section + type: string + is_section_followed: *is_section_followed + content_redacted: + description: > + Are content details sent separately in the newtab_content ping + type: boolean + send_in_pings: + - newtab + newtab_content: experiment_name: @@ -2441,6 +2494,57 @@ newtab_content: send_in_pings: - newtab-content + thumb_voting_interaction: + type: event + description: > + Recorded when a thumbs up/down on a tile is clicked. + Only happens on click. Not on middle-click. Not on "Open in new Tab"-like + options in the context menu. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1902099 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1937200 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1902099 + data_sensitivity: + - interaction + notification_emails: + - mcrawford@mozilla.com + expires: never + extra_keys: + recommendation_id: *recommendation_id + tile_id: *pocket_tile_id + format: *format + position: *pocket_position + thumbs_up: + description: > + If the user clicked thumbs up. + type: boolean + thumbs_down: + description: > + If the user clicked thumbs down. + type: boolean + scheduled_corpus_item_id: *scheduled_corpus_item_id + corpus_item_id: *corpus_item_id + received_rank: *received_rank + recommended_at: *recommended_at + topic: + description: The topic of the recommendation. Like "entertainment". + type: string + section: + description: > + If event belongs in a section, the name of the section + type: string + section_position: + description: > + If event belongs in a section, the numberic position of the section + type: string + is_section_followed: + description: > + If event belongs in a section, if that section is followed + type: boolean + send_in_pings: + - newtab-content + sections_impression: type: event description: > diff --git a/browser/extensions/newtab/common/Actions.mjs b/browser/extensions/newtab/common/Actions.mjs @@ -128,6 +128,8 @@ for (const type of [ "PLACES_LINKS_DELETED", "PLACES_LINK_BLOCKED", "POCKET_CTA", + "POCKET_THUMBS_DOWN", + "POCKET_THUMBS_UP", "POCKET_WAITING_FOR_SPOC", "PREFS_INITIAL_VALUES", "PREF_CHANGED", diff --git a/browser/extensions/newtab/content-src/components/Base/Base.jsx b/browser/extensions/newtab/content-src/components/Base/Base.jsx @@ -723,6 +723,10 @@ export class BaseContent extends React.PureComponent { mayHaveWeather ? "is-tall" : ""; + + 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 = @@ -767,6 +771,7 @@ export class BaseContent extends React.PureComponent { "only-topsites", noSectionsEnabled && "no-sections", prefs["logowordmark.alwaysVisible"] && "visible-logo", + hasThumbsUpDownLayout && hasThumbsUpDown && "thumbs-ui-compact", ] .filter(v => v) .join(" "); diff --git a/browser/extensions/newtab/content-src/components/Base/_Base.scss b/browser/extensions/newtab/content-src/components/Base/_Base.scss @@ -62,11 +62,6 @@ main { } } -// Edge case for if search is turned off (compact layout) -.no-search main { - margin-block-start: var(--space-xxlarge); -} - .ds-outer-wrapper-search-alignment { main { // This override is to ensure while Discovery Stream loads, diff --git a/browser/extensions/newtab/content-src/components/CollapsibleSection/_CollapsibleSection.scss b/browser/extensions/newtab/content-src/components/CollapsibleSection/_CollapsibleSection.scss @@ -1,9 +1,7 @@ /* stylelint-disable max-nesting-depth */ .collapsible-section { - // Compact layout padding and spacing from thumbs UI overrides - padding: 0 var(--space-xlarge); - margin-block-end: var(--space-xxlarge); + padding: var(--space-small) var(--space-xlarge); .section-title-container { margin: 0; diff --git a/browser/extensions/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx b/browser/extensions/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx @@ -353,6 +353,9 @@ export class DiscoveryStreamAdminUI extends React.PureComponent { this.props.dispatch( ac.SetPref("discoverystream.sections.cards.enabled", pressed) ); + this.props.dispatch( + ac.SetPref("discoverystream.sections.cards.thumbsUpDown.enabled", pressed) + ); } sendConversionEvent() { diff --git a/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx b/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx @@ -10,6 +10,7 @@ import { FluentOrText } from "../../FluentOrText/FluentOrText.jsx"; import React, { useEffect, useRef } from "react"; import { connect } from "react-redux"; const PREF_SECTIONS_CARDS_ENABLED = "discoverystream.sections.cards.enabled"; +const PREF_THUMBS_UP_DOWN_ENABLED = "discoverystream.thumbsUpDown.enabled"; const PREF_TOPICS_ENABLED = "discoverystream.topicLabels.enabled"; const PREF_TOPICS_SELECTED = "discoverystream.topicSelection.selectedTopics"; const PREF_TOPICS_AVAILABLE = "discoverystream.topicSelection.topics"; @@ -124,6 +125,7 @@ export class _CardGrid extends React.PureComponent { const { topicsLoading } = DiscoveryStream; const mayHaveSectionsCards = prefs[PREF_SECTIONS_CARDS_ENABLED]; + const mayHaveThumbsUpDown = prefs[PREF_THUMBS_UP_DOWN_ENABLED]; const showTopics = prefs[PREF_TOPICS_ENABLED]; const selectedTopics = prefs[PREF_TOPICS_SELECTED]; const availableTopics = prefs[PREF_TOPICS_AVAILABLE]; @@ -186,6 +188,7 @@ export class _CardGrid extends React.PureComponent { ctaButtonVariant={ctaButtonVariant} recommendation_id={rec.recommendation_id} firstVisibleTimestamp={this.props.firstVisibleTimestamp} + mayHaveThumbsUpDown={mayHaveThumbsUpDown} mayHaveSectionsCards={mayHaveSectionsCards} corpus_item_id={rec.corpus_item_id} scheduled_corpus_item_id={rec.scheduled_corpus_item_id} diff --git a/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/CardSections/CardSections.jsx b/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/CardSections/CardSections.jsx @@ -22,11 +22,14 @@ import { Weather } from "../../Weather/Weather.jsx"; // Prefs const PREF_SECTIONS_CARDS_ENABLED = "discoverystream.sections.cards.enabled"; +const PREF_SECTIONS_CARDS_THUMBS_UP_DOWN_ENABLED = + "discoverystream.sections.cards.thumbsUpDown.enabled"; const PREF_SECTIONS_PERSONALIZATION_ENABLED = "discoverystream.sections.personalization.enabled"; const PREF_TOPICS_ENABLED = "discoverystream.topicLabels.enabled"; const PREF_TOPICS_SELECTED = "discoverystream.topicSelection.selectedTopics"; const PREF_TOPICS_AVAILABLE = "discoverystream.topicSelection.topics"; +const PREF_THUMBS_UP_DOWN_ENABLED = "discoverystream.thumbsUpDown.enabled"; const PREF_INTEREST_PICKER_ENABLED = "discoverystream.sections.interestPicker.enabled"; const PREF_VISIBLE_SECTIONS = @@ -199,6 +202,9 @@ function CardSection({ const showTopics = prefs[PREF_TOPICS_ENABLED]; const mayHaveSectionsCards = prefs[PREF_SECTIONS_CARDS_ENABLED]; + const mayHaveSectionsCardsThumbsUpDown = + prefs[PREF_SECTIONS_CARDS_THUMBS_UP_DOWN_ENABLED]; + const mayHaveThumbsUpDown = prefs[PREF_THUMBS_UP_DOWN_ENABLED]; const selectedTopics = prefs[PREF_TOPICS_SELECTED]; const availableTopics = prefs[PREF_TOPICS_AVAILABLE]; const refinedCardsLayout = prefs[PREF_REFINED_CARDS_ENABLED]; @@ -229,6 +235,10 @@ function CardSection({ // Ref to hold the section element const sectionRefs = useIntersectionObserver(handleIntersection); + // Only show thumbs up/down buttons if both default thumbs and sections thumbs prefs are enabled + const mayHaveCombinedThumbsUpDown = + mayHaveSectionsCardsThumbsUpDown && mayHaveThumbsUpDown; + const onFollowClick = useCallback(() => { const updatedSectionData = { ...sectionPersonalization, @@ -443,6 +453,7 @@ function CardSection({ received_rank={rec.received_rank} format={rec.format} alt_text={rec.alt_text} + mayHaveThumbsUpDown={mayHaveCombinedThumbsUpDown} mayHaveSectionsCards={mayHaveSectionsCards} showTopics={shouldShowLabels} selectedTopics={selectedTopics} diff --git a/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/CardSections/_CardSections.scss b/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/CardSections/_CardSections.scss @@ -60,10 +60,21 @@ width: 32px; } + .card-stp-thumbs-buttons-wrapper { + .card-stp-thumbs-buttons { + gap: var(--space-small); + } + } + + &:hover, &:active, &:focus-within, &.active { + .card-stp-thumbs-buttons-wrapper { + visibility: visible; + } + .meta { .source-wrapper .source { display: none; @@ -76,6 +87,10 @@ padding: unset; } + .card-stp-thumbs-buttons-wrapper { + visibility: hidden; + } + .ds-card-link { display: flex; flex-direction: row; @@ -155,6 +170,10 @@ } } + .card-stp-thumbs-buttons-wrapper { + visibility: visible; + } + &.refined-cards { .ds-card-link { padding: unset; @@ -222,10 +241,18 @@ background: transparent; } + .card-stp-thumbs-buttons-wrapper { + visibility: hidden; + } + &:hover, &:active, &:focus-within, &.active { + .card-stp-thumbs-buttons-wrapper { + visibility: visible; + } + .meta { .source-wrapper { max-width: 175px; @@ -248,6 +275,10 @@ background: transparent; } + .card-stp-thumbs-buttons-wrapper { + display: block; + } + .ds-card-link { display: flex; flex-direction: column; diff --git a/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx b/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx @@ -2,7 +2,7 @@ * 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 { actionCreators as ac } from "common/Actions.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { DSImage } from "../DSImage/DSImage.jsx"; import { DSLinkMenu } from "../DSLinkMenu/DSLinkMenu"; import { ImpressionStats } from "../../DiscoveryStreamImpressionStats/ImpressionStats"; @@ -14,8 +14,10 @@ import { SponsorLabel, DSMessageFooter, } from "../DSContextFooter/DSContextFooter.jsx"; +import { DSThumbsUpDownButtons } from "../DSThumbsUpDownButtons/DSThumbsUpDownButtons.jsx"; import { FluentOrText } from "../../FluentOrText/FluentOrText.jsx"; import { connect } from "react-redux"; +import { LinkMenuOptions } from "content-src/lib/link-menu-options"; const READING_WPM = 220; const PREF_OHTTP_MERINO = "discoverystream.merino-provider.ohttp.enabled"; const PREF_OHTTP_UNIFIED_ADS = "unifiedAds.ohttp.enabled"; @@ -101,14 +103,22 @@ export const DefaultMeta = ({ ctaButtonVariant, dispatch, mayHaveSectionsCards, + mayHaveThumbsUpDown, + onThumbsUpClick, + onThumbsDownClick, + state, format, topic, isSectionsCard, showTopics, icon_src, refinedCardsLayout, + tabIndex, }) => { - const shouldHaveFooterSection = isSectionsCard && showTopics; + const shouldHaveThumbs = + format !== "rectangle" && mayHaveSectionsCards && mayHaveThumbsUpDown; + const shouldHaveFooterSection = + isSectionsCard && (shouldHaveThumbs || showTopics); return ( <div className="meta"> @@ -137,6 +147,19 @@ export const DefaultMeta = ({ excerpt && <p className="excerpt clamp">{excerpt}</p> )} </div> + {format !== "rectangle" && + !mayHaveSectionsCards && + mayHaveThumbsUpDown && + !refinedCardsLayout && ( + <DSThumbsUpDownButtons + onThumbsDownClick={onThumbsDownClick} + onThumbsUpClick={onThumbsUpClick} + sponsor={sponsor} + isThumbsDownActive={state.isThumbsDownActive} + isThumbsUpActive={state.isThumbsUpActive} + tabIndex={tabIndex} + /> + )} {(shouldHaveFooterSection || refinedCardsLayout) && ( <div className="sections-card-footer"> {refinedCardsLayout && @@ -153,6 +176,17 @@ export const DefaultMeta = ({ refinedCardsLayout={refinedCardsLayout} /> )} + {(shouldHaveThumbs || refinedCardsLayout) && ( + <DSThumbsUpDownButtons + onThumbsDownClick={onThumbsDownClick} + onThumbsUpClick={onThumbsUpClick} + sponsor={sponsor} + isThumbsDownActive={state.isThumbsDownActive} + isThumbsUpActive={state.isThumbsUpActive} + refinedCardsLayout={refinedCardsLayout} + tabIndex={tabIndex} + /> + )} {showTopics && ( <span className="ds-card-topic" @@ -192,6 +226,8 @@ export class _DSCard extends React.PureComponent { this.doesLinkTopicMatchSelectedTopic.bind(this); this.onMenuUpdate = this.onMenuUpdate.bind(this); this.onMenuShow = this.onMenuShow.bind(this); + this.onThumbsUpClick = this.onThumbsUpClick.bind(this); + this.onThumbsDownClick = this.onThumbsDownClick.bind(this); const refinedCardsLayout = this.props.Prefs.values["discoverystream.refinedCardsLayout.enabled"]; @@ -204,6 +240,8 @@ export class _DSCard extends React.PureComponent { this.state = { isSeen: false, + isThumbsUpActive: false, + isThumbsDownActive: false, }; // If this is for the about:home startup cache, then we always want @@ -380,6 +418,166 @@ export class _DSCard extends React.PureComponent { } } + onThumbsUpClick(event) { + event.stopPropagation(); + event.preventDefault(); + + // Toggle active state for thumbs up button to show CSS animation + const currentState = this.state.isThumbsUpActive; + + // If thumbs up has been clicked already, do nothing. + if (currentState) { + return; + } + + this.setState({ isThumbsUpActive: !currentState }); + + // Record thumbs up telemetry event + this.props.dispatch( + ac.DiscoveryStreamUserEvent({ + event: "POCKET_THUMBS_UP", + source: "THUMBS_UI", + value: { + action_position: this.props.pos, + recommendation_id: this.props.recommendation_id, + tile_id: this.props.id, + corpus_item_id: this.props.corpus_item_id, + scheduled_corpus_item_id: this.props.scheduled_corpus_item_id, + recommended_at: this.props.recommended_at, + received_rank: this.props.received_rank, + thumbs_up: true, + thumbs_down: false, + topic: this.props.topic, + format: getActiveCardSize( + window.innerWidth, + this.props.sectionsClassNames, + this.props.section, + false // (thumbs up/down only exist on organic content) + ), + ...(this.props.section + ? { + section: this.props.section, + section_position: this.props.sectionPosition, + is_section_followed: this.props.sectionFollowed, + } + : {}), + }, + }) + ); + + // Show Toast + this.props.dispatch( + ac.OnlyToOneContent( + { + type: at.SHOW_TOAST_MESSAGE, + data: { + showNotifications: true, + toastId: "thumbsUpToast", + }, + }, + "ActivityStream:Content" + ) + ); + } + + onThumbsDownClick(event) { + event.stopPropagation(); + event.preventDefault(); + + // Toggle active state for thumbs down button to show CSS animation + const currentState = this.state.isThumbsDownActive; + this.setState({ isThumbsDownActive: !currentState }); + + // Run dismiss event after 0.5 second delay + if ( + this.props.dispatch && + this.props.type && + this.props.id && + this.props.url + ) { + const index = this.props.pos; + const source = this.props.type.toUpperCase(); + const spocData = { + url: this.props.url, + guid: this.props.id, + type: "CardGrid", + card_type: "organic", + recommendation_id: this.props.recommendation_id, + tile_id: this.props.id, + corpus_item_id: this.props.corpus_item_id, + scheduled_corpus_item_id: this.props.scheduled_corpus_item_id, + recommended_at: this.props.recommended_at, + received_rank: this.props.received_rank, + }; + const blockUrlOption = LinkMenuOptions.BlockUrl(spocData, index, source); + + const { action, impression, userEvent } = blockUrlOption; + + setTimeout(() => { + this.props.dispatch(action); + + this.props.dispatch( + ac.DiscoveryStreamUserEvent({ + event: userEvent, + source, + action_position: index, + }) + ); + }, 500); + + if (impression) { + this.props.dispatch(impression); + } + + // Record thumbs down telemetry event + this.props.dispatch( + ac.DiscoveryStreamUserEvent({ + event: "POCKET_THUMBS_DOWN", + source: "THUMBS_UI", + value: { + action_position: this.props.pos, + recommendation_id: this.props.recommendation_id, + tile_id: this.props.id, + corpus_item_id: this.props.corpus_item_id, + scheduled_corpus_item_id: this.props.scheduled_corpus_item_id, + recommended_at: this.props.recommended_at, + received_rank: this.props.received_rank, + thumbs_up: false, + thumbs_down: true, + topic: this.props.topic, + format: getActiveCardSize( + window.innerWidth, + this.props.sectionsClassNames, + this.props.section, + false // (thumbs up/down only exist on organic content) + ), + ...(this.props.section + ? { + section: this.props.section, + section_position: this.props.sectionPosition, + is_section_followed: this.props.sectionFollowed, + } + : {}), + }, + }) + ); + + // Show Toast + this.props.dispatch( + ac.OnlyToOneContent( + { + type: at.SHOW_TOAST_MESSAGE, + data: { + showNotifications: true, + toastId: "thumbsDownToast", + }, + }, + "ActivityStream:Content" + ) + ); + } + } + onMenuUpdate(showContextMenu) { if (!showContextMenu) { const dsLinkMenuHostDiv = this.contextMenuButtonHostElement; @@ -743,7 +941,10 @@ export class _DSCard extends React.PureComponent { sponsored_by_override={this.props.sponsored_by_override} ctaButtonVariant={ctaButtonVariant} dispatch={this.props.dispatch} + mayHaveThumbsUpDown={this.props.mayHaveThumbsUpDown} mayHaveSectionsCards={this.props.mayHaveSectionsCards} + onThumbsUpClick={this.onThumbsUpClick} + onThumbsDownClick={this.onThumbsDownClick} state={this.state} showTopics={!refinedCardsLayout && this.props.showTopics} isSectionsCard={this.props.mayHaveSectionsCards && this.props.topic} diff --git a/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss b/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss @@ -182,6 +182,7 @@ $ds-card-image-gradient-solid: rgba(0, 0, 0, 100%); } } + // Note: `.ds-card .active,:focus-within,:hover` is also utilized by DSThumbsUpDownButtons // The active class is added when the context menu is open. &.active, &:focus-within, @@ -647,6 +648,17 @@ $ds-card-image-gradient-solid: rgba(0, 0, 0, 100%); margin-inline-end: var(--space-xxsmall); max-width: 175px; } + + .card-stp-thumbs-buttons-wrapper { + position: static; + inset-block-start: 0; + transform: translateY(0); + margin-block-start: 0; + + .card-stp-thumbs-buttons { + pointer-events: auto; + } + } } &:has(.story-footer .story-sponsored-label) { diff --git a/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSThumbsUpDownButtons/DSThumbsUpDownButtons.jsx b/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSThumbsUpDownButtons/DSThumbsUpDownButtons.jsx @@ -0,0 +1,73 @@ +/* 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 from "react"; + +function DSThumbsUpDownButtons({ + sponsor, + onThumbsUpClick, + onThumbsDownClick, + isThumbsUpActive, + isThumbsDownActive, + refinedCardsLayout, + tabIndex, +}) { + let thumbsButtons = ( + <> + <button + onClick={onThumbsUpClick} + className={`card-stp-thumbs-button icon icon-thumbs-up ${ + isThumbsUpActive ? "is-active" : null + }`} + data-l10n-id="newtab-pocket-thumbs-up-tooltip" + tabIndex={tabIndex} + ></button> + <button + onClick={onThumbsDownClick} + className={`card-stp-thumbs-button icon icon-thumbs-down ${ + isThumbsDownActive ? "is-active" : null + }`} + data-l10n-id="newtab-pocket-thumbs-down-tooltip" + tabIndex={tabIndex} + ></button> + </> + ); + + if (refinedCardsLayout) { + thumbsButtons = ( + <> + <moz-button + iconsrc="chrome://global/skin/icons/thumbs-up-20.svg" + onClick={onThumbsUpClick} + className={`card-stp-thumbs-button icon icon-thumbs-up refined-layout ${ + isThumbsUpActive ? "is-active" : null + }`} + data-l10n-id="newtab-pocket-thumbs-up-tooltip" + type="icon ghost" + tabIndex={tabIndex} + ></moz-button> + <moz-button + iconsrc="chrome://global/skin/icons/thumbs-down-20.svg" + onClick={onThumbsDownClick} + className={`card-stp-thumbs-button icon icon-thumbs-down ${ + isThumbsDownActive ? "is-active" : null + }`} + data-l10n-id="newtab-pocket-thumbs-down-tooltip" + type="icon ghost" + tabIndex={tabIndex} + ></moz-button> + </> + ); + } + return ( + <div className="card-stp-thumbs-buttons-wrapper"> + {/* Only show to non-sponsored content */} + {!sponsor && ( + <div className="card-stp-thumbs-buttons">{thumbsButtons}</div> + )} + </div> + ); +} + +export { DSThumbsUpDownButtons }; diff --git a/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSThumbsUpDownButtons/_DSThumbsUpDownButtons.scss b/browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSThumbsUpDownButtons/_DSThumbsUpDownButtons.scss @@ -0,0 +1,144 @@ +.card-stp-thumbs-buttons-wrapper { + margin-block-start: var(--space-medium); + visibility: hidden; + opacity: 0; + + .sections-card-ui & { + margin-block-start: 0; + position: absolute; + inset-inline-end: 0; + inset-block-start: 50%; + transform: translateY(-50%); + } + + .card-stp-thumbs-buttons { + display: flex; + height: 100%; + align-items: center; + justify-content: end; + gap: var(--space-medium); + } + + .card-stp-thumbs-button { + pointer-events: auto; + opacity: 0.8; + -moz-context-properties: fill; + cursor: pointer; + + &:hover, + &:focus-visible { + opacity: 1; + } + } + + .icon-thumbs-up, + .icon-thumbs-down { + display: block; + border: 0; + background-color: transparent; + background-size: var(--icon-size); + background-position: center; + padding: var(--space-small); + fill: var(--icon-color); + transition: fill 0.3s ease-in-out; + + &:hover { + fill: var(--text-color-deemphasized); + } + + &:hover:active { + fill: var(--color-accent-primary); + } + + &.is-active { + animation: iconBounce 0.2s ease-in-out; + fill: var(--color-accent-primary); + } + } +} + +.ds-card { + &.active, + &:focus-within, + &:hover { + .card-stp-thumbs-buttons-wrapper { + visibility: visible; + opacity: 1; + } + } +} + +// Animations for thumbs up/down +@keyframes iconBounce { + 0%, + 100% { + transform: scale(1); + } + + 50% { + transform: scale(0.6); + } +} + +// Thumbs Up/Down UI Overrides across the entire page +.thumbs-ui-compact { + + // Base.scss + + // Edge case for if search is turned off + &.no-search main { + margin-block-start: var(--space-xxlarge); + } + + section { + margin-block-end: 0; + } + + // CollapsibleSection.scss + .collapsible-section { + padding: 0 var(--space-xlarge); + margin-block-end: var(--space-xxlarge); + } + + // _Search.scss + .search-wrapper { + padding: 0; + margin-block: var(--space-xxlarge); + + .logo-and-wordmark { + margin-block-end: var(--space-xxlarge); + } + + .logo-and-wordmark-wrapper { + margin-block-end: 0; + } + + @media (height <=700px) { + padding: 0; + margin-block-start: 0; + } + + @media (height > 700px) { + padding: 0; + } + } + + &.fixed-search { + .search-wrapper { + margin-block-start: 0; + + .logo-and-wordmark-wrapper { + margin-block-end: 0; + } + } + } + + // _TopSites.scss + .top-site-outer { + margin-block-end: var(--space-medium); + + .shortcuts-refresh & { + margin-block-end: 0; + } + } +} diff --git a/browser/extensions/newtab/content-src/components/Notifications/Notifications.jsx b/browser/extensions/newtab/content-src/components/Notifications/Notifications.jsx @@ -5,6 +5,7 @@ import React, { useCallback, useEffect } from "react"; import { useSelector } from "react-redux"; import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; +import { ThumbUpThumbDownToast } from "./Toasts/ThumbUpThumbDownToast"; import { ReportContentToast } from "./Toasts/ReportContentToast"; function Notifications({ dispatch }) { @@ -52,6 +53,15 @@ function Notifications({ dispatch }) { key={toastCounter} /> ); + case "thumbsUpToast": + case "thumbsDownToast": + return ( + <ThumbUpThumbDownToast + onDismissClick={syncHiddenToastData} + onAnimationEnd={syncHiddenToastData} + key={toastCounter} + /> + ); default: throw new Error(`Unexpected toast type: ${latestToastItem}`); } diff --git a/browser/extensions/newtab/content-src/components/Notifications/Toasts/ThumbUpThumbDownToast.jsx b/browser/extensions/newtab/content-src/components/Notifications/Toasts/ThumbUpThumbDownToast.jsx @@ -0,0 +1,41 @@ +/* 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, { useEffect, useRef } from "react"; + +function ThumbUpThumbDownToast({ onDismissClick, onAnimationEnd }) { + const mozMessageBarRef = useRef(null); + + useEffect(() => { + const { current: mozMessageBarElement } = mozMessageBarRef; + + mozMessageBarElement.addEventListener( + "message-bar:user-dismissed", + onDismissClick, + { + once: true, + } + ); + + return () => { + mozMessageBarElement.removeEventListener( + "message-bar:user-dismissed", + onDismissClick + ); + }; + }, [onDismissClick]); + + return ( + <moz-message-bar + type="success" + class="notification-feed-item" + dismissable={true} + data-l10n-id="newtab-toast-thumbs-up-or-down2" + ref={mozMessageBarRef} + onAnimationEnd={onAnimationEnd} + ></moz-message-bar> + ); +} + +export { ThumbUpThumbDownToast }; diff --git a/browser/extensions/newtab/content-src/components/Search/_Search.scss b/browser/extensions/newtab/content-src/components/Search/_Search.scss @@ -7,26 +7,13 @@ $search-button-width: var(--size-item-xlarge); $glyph-forward: url('chrome://browser/skin/forward.svg'); .search-wrapper { - // Compact layout defaults formerly under .thumbs-ui-compact - padding: 0; - margin-block: var(--space-xxlarge); - margin-block-start: 0; - - .logo-and-wordmark { - margin-block-end: var(--space-xxlarge); - } + padding: 0 0 var(--space-xxlarge); - .logo-and-wordmark-wrapper { - margin-block-end: 0; - } - - @media (height <=700px) { - padding: 0; - margin-block-start: 0; - } - @media (height > 700px) { - padding: 0; + // Edge case for users who have pocket stories turned off + .no-recommended-stories & { + // Bug 1967304 - Large number (40px) + padding-block-end: calc(var(--space-large) + var(--space-xlarge)); } // Edge case for users who have only search enabled @@ -65,22 +52,6 @@ $glyph-forward: url('chrome://browser/skin/forward.svg'); } - .search-inner-wrapper:has(.trending-searches-pill-wrapper) { - display: flex; - flex-direction: column; - - .search-handoff-button { - height: 52px; - } - - &.no-handoff { - #newtab-search-text, - .search-button { - height: 52px; - } - } - } - .search-handoff-button, input { background: var(--newtab-background-color-secondary) var(--newtab-search-icon) $search-icon-padding center no-repeat; @@ -176,6 +147,13 @@ $glyph-forward: url('chrome://browser/skin/forward.svg'); } } +.shortcuts-refresh.has-recommended-stories { + .outer-wrapper:not(.fixed-search) .search-wrapper { + margin-block-end: var(--space-xlarge); + padding: 0; + } +} + .has-recommended-stories { .outer-wrapper:not(.fixed-search) .search-wrapper { margin-block-start: var(--space-large); @@ -292,6 +270,12 @@ $glyph-forward: url('chrome://browser/skin/forward.svg'); } } +@media (height <= 700px) { + .search-wrapper { + padding: 0 0 var(--space-xxlarge); + } +} + .search-handoff-button { background: var(--newtab-background-color-secondary) var(--newtab-search-icon) $search-icon-padding center no-repeat; background-size: $search-icon-size; diff --git a/browser/extensions/newtab/content-src/components/TopSites/_TopSites.scss b/browser/extensions/newtab/content-src/components/TopSites/_TopSites.scss @@ -153,8 +153,7 @@ $calculated-max-width-twice-widest: $break-point-widest + 2 * $card-width; // container for drop zone .top-site-outer { width: 120px; - // Compact layout spacing migrated from .thumbs-ui-compact - margin-block-end: var(--space-medium); + margin-block-end: 0; border-radius: var(--border-radius-large); display: inline-block; @@ -445,13 +444,6 @@ $calculated-max-width-twice-widest: $break-point-widest + 2 * $card-width; } } -// Preserve shortcuts-refresh variant from compact thumbs UI -.shortcuts-refresh { - .top-site-outer { - margin-block-end: 0; - } -} - .edit-topsites-wrapper { .top-site-inner > .top-site-button > .tile { border: 1px solid var(--newtab-border-color); diff --git a/browser/extensions/newtab/content-src/styles/_icons.scss b/browser/extensions/newtab/content-src/styles/_icons.scss @@ -230,6 +230,14 @@ background-image: url('chrome://global/skin/icons/arrow-right-12.svg'); } + &.icon-thumbs-up { + background-image: url('chrome://global/skin/icons/thumbs-up-20.svg'); + } + + &.icon-thumbs-down { + background-image: url('chrome://global/skin/icons/thumbs-down-20.svg'); + } + &.icon-device-phone { background-image: url('chrome://browser/skin/device-phone.svg'); } diff --git a/browser/extensions/newtab/content-src/styles/activity-stream.scss b/browser/extensions/newtab/content-src/styles/activity-stream.scss @@ -186,6 +186,7 @@ input { @import '../components/DiscoveryStreamComponents/DSMessage/DSMessage'; @import '../components/DiscoveryStreamImpressionStats/ImpressionStats'; @import '../components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState'; +@import '../components/DiscoveryStreamComponents/DSThumbsUpDownButtons/DSThumbsUpDownButtons'; @import '../components/DiscoveryStreamComponents/PrivacyLink/PrivacyLink'; @import '../components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget'; @import '../components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight'; diff --git a/browser/extensions/newtab/css/activity-stream.css b/browser/extensions/newtab/css/activity-stream.css @@ -304,6 +304,12 @@ input { .icon.icon-arrow { background-image: url("chrome://global/skin/icons/arrow-right-12.svg"); } +.icon.icon-thumbs-up { + background-image: url("chrome://global/skin/icons/thumbs-up-20.svg"); +} +.icon.icon-thumbs-down { + background-image: url("chrome://global/skin/icons/thumbs-down-20.svg"); +} .icon.icon-device-phone { background-image: url("chrome://browser/skin/device-phone.svg"); } @@ -502,10 +508,6 @@ main section { } } -.no-search main { - margin-block-start: var(--space-xxlarge); -} - .ds-outer-wrapper-search-alignment main { margin: 0 auto; } @@ -850,7 +852,7 @@ main section { .top-site-outer { width: 120px; - margin-block-end: var(--space-medium); + margin-block-end: 0; border-radius: var(--border-radius-large); display: inline-block; } @@ -1084,10 +1086,6 @@ main section { visibility: hidden; } -.shortcuts-refresh .top-site-outer { - margin-block-end: 0; -} - .edit-topsites-wrapper .top-site-inner > .top-site-button > .tile { border: 1px solid var(--newtab-border-color); } @@ -1478,26 +1476,10 @@ main section { } } .search-wrapper { - padding: 0; - margin-block: var(--space-xxlarge); - margin-block-start: 0; -} -.search-wrapper .logo-and-wordmark { - margin-block-end: var(--space-xxlarge); -} -.search-wrapper .logo-and-wordmark-wrapper { - margin-block-end: 0; -} -@media (height <= 700px) { - .search-wrapper { - padding: 0; - margin-block-start: 0; - } + padding: 0 0 var(--space-xxlarge); } -@media (height > 700px) { - .search-wrapper { - padding: 0; - } +.no-recommended-stories .search-wrapper { + padding-block-end: calc(var(--space-large) + var(--space-xlarge)); } .only-search .search-wrapper { padding-block-end: calc(var(--space-large) + var(--space-xlarge)); @@ -1528,17 +1510,6 @@ main section { .search-wrapper .search-inner-wrapper.no-handoff input { padding-inline-end: calc(var(--size-item-xlarge) + var(--space-medium)); } -.search-wrapper .search-inner-wrapper:has(.trending-searches-pill-wrapper) { - display: flex; - flex-direction: column; -} -.search-wrapper .search-inner-wrapper:has(.trending-searches-pill-wrapper) .search-handoff-button { - height: 52px; -} -.search-wrapper .search-inner-wrapper:has(.trending-searches-pill-wrapper).no-handoff #newtab-search-text, -.search-wrapper .search-inner-wrapper:has(.trending-searches-pill-wrapper).no-handoff .search-button { - height: 52px; -} .search-wrapper .search-handoff-button, .search-wrapper input { background: var(--newtab-background-color-secondary) var(--newtab-search-icon) 16px center no-repeat; @@ -1613,6 +1584,11 @@ main section { fill: var(--newtab-wordmark-color); } +.shortcuts-refresh.has-recommended-stories .outer-wrapper:not(.fixed-search) .search-wrapper { + margin-block-end: var(--space-xlarge); + padding: 0; +} + .has-recommended-stories .outer-wrapper:not(.fixed-search) .search-wrapper { margin-block-start: var(--space-large); } @@ -1718,6 +1694,11 @@ main section { } } +@media (height <= 700px) { + .search-wrapper { + padding: 0 0 var(--space-xxlarge); + } +} .search-handoff-button { background: var(--newtab-background-color-secondary) var(--newtab-search-icon) 16px center no-repeat; background-size: 24px; @@ -3827,8 +3808,7 @@ dialog:dir(rtl)::after { /* stylelint-disable max-nesting-depth */ .collapsible-section { - padding: 0 var(--space-xlarge); - margin-block-end: var(--space-xxlarge); + padding: var(--space-small) var(--space-xlarge); } .collapsible-section .section-title-container { margin: 0; @@ -5416,12 +5396,21 @@ dialog:dir(rtl)::after { height: 32px; width: 32px; } + .ds-section-grid.ds-card-grid .col-1-small.refined-cards .card-stp-thumbs-buttons-wrapper .card-stp-thumbs-buttons { + gap: var(--space-small); + } + .ds-section-grid.ds-card-grid .col-1-small.refined-cards:hover .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-1-small.refined-cards:active .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-1-small.refined-cards:focus-within .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-1-small.refined-cards.active .card-stp-thumbs-buttons-wrapper { + visibility: visible; + } .ds-section-grid.ds-card-grid .col-1-small.refined-cards:hover .meta .source-wrapper .source, .ds-section-grid.ds-card-grid .col-1-small.refined-cards:active .meta .source-wrapper .source, .ds-section-grid.ds-card-grid .col-1-small.refined-cards:focus-within .meta .source-wrapper .source, .ds-section-grid.ds-card-grid .col-1-small.refined-cards.active .meta .source-wrapper .source { display: none; } .ds-section-grid.ds-card-grid .col-1-small.ds-card.sections-card-ui { padding: unset; } + .ds-section-grid.ds-card-grid .col-1-small .card-stp-thumbs-buttons-wrapper { + visibility: hidden; + } .ds-section-grid.ds-card-grid .col-1-small .ds-card-link { display: flex; flex-direction: row; @@ -5486,6 +5475,9 @@ dialog:dir(rtl)::after { .ds-section-grid.ds-card-grid .col-1-medium.refined-cards.ds-card.sections-card-ui:hover { box-shadow: var(--box-shadow-card-hover); } + .ds-section-grid.ds-card-grid .col-1-medium .card-stp-thumbs-buttons-wrapper { + visibility: visible; + } .ds-section-grid.ds-card-grid .col-1-medium.refined-cards .ds-card-link { padding: unset; flex-grow: 1; @@ -5533,6 +5525,12 @@ dialog:dir(rtl)::after { .ds-section-grid.ds-card-grid .col-1-medium.refined-cards .card-stp-button-hover-background { background: transparent; } + .ds-section-grid.ds-card-grid .col-1-medium.refined-cards .card-stp-thumbs-buttons-wrapper { + visibility: hidden; + } + .ds-section-grid.ds-card-grid .col-1-medium.refined-cards:hover .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-1-medium.refined-cards:active .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-1-medium.refined-cards:focus-within .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-1-medium.refined-cards.active .card-stp-thumbs-buttons-wrapper { + visibility: visible; + } .ds-section-grid.ds-card-grid .col-1-medium.refined-cards:hover .meta .source-wrapper, .ds-section-grid.ds-card-grid .col-1-medium.refined-cards:active .meta .source-wrapper, .ds-section-grid.ds-card-grid .col-1-medium.refined-cards:focus-within .meta .source-wrapper, .ds-section-grid.ds-card-grid .col-1-medium.refined-cards.active .meta .source-wrapper { max-width: 175px; } @@ -5546,6 +5544,9 @@ dialog:dir(rtl)::after { .ds-section-grid.ds-card-grid .col-1-medium .card-stp-button-hover-background { background: transparent; } + .ds-section-grid.ds-card-grid .col-1-medium .card-stp-thumbs-buttons-wrapper { + display: block; + } .ds-section-grid.ds-card-grid .col-1-medium .ds-card-link { display: flex; flex-direction: column; @@ -5776,12 +5777,21 @@ dialog:dir(rtl)::after { height: 32px; width: 32px; } + .ds-section-grid.ds-card-grid .col-2-small.refined-cards .card-stp-thumbs-buttons-wrapper .card-stp-thumbs-buttons { + gap: var(--space-small); + } + .ds-section-grid.ds-card-grid .col-2-small.refined-cards:hover .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-2-small.refined-cards:active .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-2-small.refined-cards:focus-within .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-2-small.refined-cards.active .card-stp-thumbs-buttons-wrapper { + visibility: visible; + } .ds-section-grid.ds-card-grid .col-2-small.refined-cards:hover .meta .source-wrapper .source, .ds-section-grid.ds-card-grid .col-2-small.refined-cards:active .meta .source-wrapper .source, .ds-section-grid.ds-card-grid .col-2-small.refined-cards:focus-within .meta .source-wrapper .source, .ds-section-grid.ds-card-grid .col-2-small.refined-cards.active .meta .source-wrapper .source { display: none; } .ds-section-grid.ds-card-grid .col-2-small.ds-card.sections-card-ui { padding: unset; } + .ds-section-grid.ds-card-grid .col-2-small .card-stp-thumbs-buttons-wrapper { + visibility: hidden; + } .ds-section-grid.ds-card-grid .col-2-small .ds-card-link { display: flex; flex-direction: row; @@ -5846,6 +5856,9 @@ dialog:dir(rtl)::after { .ds-section-grid.ds-card-grid .col-2-medium.refined-cards.ds-card.sections-card-ui:hover { box-shadow: var(--box-shadow-card-hover); } + .ds-section-grid.ds-card-grid .col-2-medium .card-stp-thumbs-buttons-wrapper { + visibility: visible; + } .ds-section-grid.ds-card-grid .col-2-medium.refined-cards .ds-card-link { padding: unset; flex-grow: 1; @@ -5893,6 +5906,12 @@ dialog:dir(rtl)::after { .ds-section-grid.ds-card-grid .col-2-medium.refined-cards .card-stp-button-hover-background { background: transparent; } + .ds-section-grid.ds-card-grid .col-2-medium.refined-cards .card-stp-thumbs-buttons-wrapper { + visibility: hidden; + } + .ds-section-grid.ds-card-grid .col-2-medium.refined-cards:hover .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-2-medium.refined-cards:active .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-2-medium.refined-cards:focus-within .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-2-medium.refined-cards.active .card-stp-thumbs-buttons-wrapper { + visibility: visible; + } .ds-section-grid.ds-card-grid .col-2-medium.refined-cards:hover .meta .source-wrapper, .ds-section-grid.ds-card-grid .col-2-medium.refined-cards:active .meta .source-wrapper, .ds-section-grid.ds-card-grid .col-2-medium.refined-cards:focus-within .meta .source-wrapper, .ds-section-grid.ds-card-grid .col-2-medium.refined-cards.active .meta .source-wrapper { max-width: 175px; } @@ -5906,6 +5925,9 @@ dialog:dir(rtl)::after { .ds-section-grid.ds-card-grid .col-2-medium .card-stp-button-hover-background { background: transparent; } + .ds-section-grid.ds-card-grid .col-2-medium .card-stp-thumbs-buttons-wrapper { + display: block; + } .ds-section-grid.ds-card-grid .col-2-medium .ds-card-link { display: flex; flex-direction: column; @@ -6145,12 +6167,21 @@ dialog:dir(rtl)::after { height: 32px; width: 32px; } + .ds-section-grid.ds-card-grid .col-3-small.refined-cards .card-stp-thumbs-buttons-wrapper .card-stp-thumbs-buttons { + gap: var(--space-small); + } + .ds-section-grid.ds-card-grid .col-3-small.refined-cards:hover .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-3-small.refined-cards:active .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-3-small.refined-cards:focus-within .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-3-small.refined-cards.active .card-stp-thumbs-buttons-wrapper { + visibility: visible; + } .ds-section-grid.ds-card-grid .col-3-small.refined-cards:hover .meta .source-wrapper .source, .ds-section-grid.ds-card-grid .col-3-small.refined-cards:active .meta .source-wrapper .source, .ds-section-grid.ds-card-grid .col-3-small.refined-cards:focus-within .meta .source-wrapper .source, .ds-section-grid.ds-card-grid .col-3-small.refined-cards.active .meta .source-wrapper .source { display: none; } .ds-section-grid.ds-card-grid .col-3-small.ds-card.sections-card-ui { padding: unset; } + .ds-section-grid.ds-card-grid .col-3-small .card-stp-thumbs-buttons-wrapper { + visibility: hidden; + } .ds-section-grid.ds-card-grid .col-3-small .ds-card-link { display: flex; flex-direction: row; @@ -6215,6 +6246,9 @@ dialog:dir(rtl)::after { .ds-section-grid.ds-card-grid .col-3-medium.refined-cards.ds-card.sections-card-ui:hover { box-shadow: var(--box-shadow-card-hover); } + .ds-section-grid.ds-card-grid .col-3-medium .card-stp-thumbs-buttons-wrapper { + visibility: visible; + } .ds-section-grid.ds-card-grid .col-3-medium.refined-cards .ds-card-link { padding: unset; flex-grow: 1; @@ -6262,6 +6296,12 @@ dialog:dir(rtl)::after { .ds-section-grid.ds-card-grid .col-3-medium.refined-cards .card-stp-button-hover-background { background: transparent; } + .ds-section-grid.ds-card-grid .col-3-medium.refined-cards .card-stp-thumbs-buttons-wrapper { + visibility: hidden; + } + .ds-section-grid.ds-card-grid .col-3-medium.refined-cards:hover .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-3-medium.refined-cards:active .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-3-medium.refined-cards:focus-within .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-3-medium.refined-cards.active .card-stp-thumbs-buttons-wrapper { + visibility: visible; + } .ds-section-grid.ds-card-grid .col-3-medium.refined-cards:hover .meta .source-wrapper, .ds-section-grid.ds-card-grid .col-3-medium.refined-cards:active .meta .source-wrapper, .ds-section-grid.ds-card-grid .col-3-medium.refined-cards:focus-within .meta .source-wrapper, .ds-section-grid.ds-card-grid .col-3-medium.refined-cards.active .meta .source-wrapper { max-width: 175px; } @@ -6275,6 +6315,9 @@ dialog:dir(rtl)::after { .ds-section-grid.ds-card-grid .col-3-medium .card-stp-button-hover-background { background: transparent; } + .ds-section-grid.ds-card-grid .col-3-medium .card-stp-thumbs-buttons-wrapper { + display: block; + } .ds-section-grid.ds-card-grid .col-3-medium .ds-card-link { display: flex; flex-direction: column; @@ -6513,12 +6556,21 @@ dialog:dir(rtl)::after { height: 32px; width: 32px; } + .ds-section-grid.ds-card-grid .col-4-small.refined-cards .card-stp-thumbs-buttons-wrapper .card-stp-thumbs-buttons { + gap: var(--space-small); + } + .ds-section-grid.ds-card-grid .col-4-small.refined-cards:hover .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-4-small.refined-cards:active .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-4-small.refined-cards:focus-within .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-4-small.refined-cards.active .card-stp-thumbs-buttons-wrapper { + visibility: visible; + } .ds-section-grid.ds-card-grid .col-4-small.refined-cards:hover .meta .source-wrapper .source, .ds-section-grid.ds-card-grid .col-4-small.refined-cards:active .meta .source-wrapper .source, .ds-section-grid.ds-card-grid .col-4-small.refined-cards:focus-within .meta .source-wrapper .source, .ds-section-grid.ds-card-grid .col-4-small.refined-cards.active .meta .source-wrapper .source { display: none; } .ds-section-grid.ds-card-grid .col-4-small.ds-card.sections-card-ui { padding: unset; } + .ds-section-grid.ds-card-grid .col-4-small .card-stp-thumbs-buttons-wrapper { + visibility: hidden; + } .ds-section-grid.ds-card-grid .col-4-small .ds-card-link { display: flex; flex-direction: row; @@ -6583,6 +6635,9 @@ dialog:dir(rtl)::after { .ds-section-grid.ds-card-grid .col-4-medium.refined-cards.ds-card.sections-card-ui:hover { box-shadow: var(--box-shadow-card-hover); } + .ds-section-grid.ds-card-grid .col-4-medium .card-stp-thumbs-buttons-wrapper { + visibility: visible; + } .ds-section-grid.ds-card-grid .col-4-medium.refined-cards .ds-card-link { padding: unset; flex-grow: 1; @@ -6630,6 +6685,12 @@ dialog:dir(rtl)::after { .ds-section-grid.ds-card-grid .col-4-medium.refined-cards .card-stp-button-hover-background { background: transparent; } + .ds-section-grid.ds-card-grid .col-4-medium.refined-cards .card-stp-thumbs-buttons-wrapper { + visibility: hidden; + } + .ds-section-grid.ds-card-grid .col-4-medium.refined-cards:hover .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-4-medium.refined-cards:active .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-4-medium.refined-cards:focus-within .card-stp-thumbs-buttons-wrapper, .ds-section-grid.ds-card-grid .col-4-medium.refined-cards.active .card-stp-thumbs-buttons-wrapper { + visibility: visible; + } .ds-section-grid.ds-card-grid .col-4-medium.refined-cards:hover .meta .source-wrapper, .ds-section-grid.ds-card-grid .col-4-medium.refined-cards:active .meta .source-wrapper, .ds-section-grid.ds-card-grid .col-4-medium.refined-cards:focus-within .meta .source-wrapper, .ds-section-grid.ds-card-grid .col-4-medium.refined-cards.active .meta .source-wrapper { max-width: 175px; } @@ -6643,6 +6704,9 @@ dialog:dir(rtl)::after { .ds-section-grid.ds-card-grid .col-4-medium .card-stp-button-hover-background { background: transparent; } + .ds-section-grid.ds-card-grid .col-4-medium .card-stp-thumbs-buttons-wrapper { + display: block; + } .ds-section-grid.ds-card-grid .col-4-medium .ds-card-link { display: flex; flex-direction: column; @@ -7679,6 +7743,15 @@ dialog:dir(rtl)::after { margin-inline-end: var(--space-xxsmall); max-width: 175px; } +.ds-card-grid .refined-cards .sections-card-footer .card-stp-thumbs-buttons-wrapper { + position: static; + inset-block-start: 0; + transform: translateY(0); + margin-block-start: 0; +} +.ds-card-grid .refined-cards .sections-card-footer .card-stp-thumbs-buttons-wrapper .card-stp-thumbs-buttons { + pointer-events: auto; +} .ds-card-grid .refined-cards:has(.story-footer .story-sponsored-label) .sections-card-footer { display: none; } @@ -7971,6 +8044,116 @@ dialog:dir(rtl)::after { transform: rotate(360deg); } } +.card-stp-thumbs-buttons-wrapper { + margin-block-start: var(--space-medium); + visibility: hidden; + opacity: 0; +} +.sections-card-ui .card-stp-thumbs-buttons-wrapper { + margin-block-start: 0; + position: absolute; + inset-inline-end: 0; + inset-block-start: 50%; + transform: translateY(-50%); +} +.card-stp-thumbs-buttons-wrapper .card-stp-thumbs-buttons { + display: flex; + height: 100%; + align-items: center; + justify-content: end; + gap: var(--space-medium); +} +.card-stp-thumbs-buttons-wrapper .card-stp-thumbs-button { + pointer-events: auto; + opacity: 0.8; + -moz-context-properties: fill; + cursor: pointer; +} +.card-stp-thumbs-buttons-wrapper .card-stp-thumbs-button:hover, .card-stp-thumbs-buttons-wrapper .card-stp-thumbs-button:focus-visible { + opacity: 1; +} +.card-stp-thumbs-buttons-wrapper .icon-thumbs-up, +.card-stp-thumbs-buttons-wrapper .icon-thumbs-down { + display: block; + border: 0; + background-color: transparent; + background-size: var(--icon-size); + background-position: center; + padding: var(--space-small); + fill: var(--icon-color); + transition: fill 0.3s ease-in-out; +} +.card-stp-thumbs-buttons-wrapper .icon-thumbs-up:hover, +.card-stp-thumbs-buttons-wrapper .icon-thumbs-down:hover { + fill: var(--text-color-deemphasized); +} +.card-stp-thumbs-buttons-wrapper .icon-thumbs-up:hover:active, +.card-stp-thumbs-buttons-wrapper .icon-thumbs-down:hover:active { + fill: var(--color-accent-primary); +} +.card-stp-thumbs-buttons-wrapper .icon-thumbs-up.is-active, +.card-stp-thumbs-buttons-wrapper .icon-thumbs-down.is-active { + animation: iconBounce 0.2s ease-in-out; + fill: var(--color-accent-primary); +} + +.ds-card.active .card-stp-thumbs-buttons-wrapper, .ds-card:focus-within .card-stp-thumbs-buttons-wrapper, .ds-card:hover .card-stp-thumbs-buttons-wrapper { + visibility: visible; + opacity: 1; +} + +@keyframes iconBounce { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(0.6); + } +} +.thumbs-ui-compact.no-search main { + margin-block-start: var(--space-xxlarge); +} +.thumbs-ui-compact section { + margin-block-end: 0; +} +.thumbs-ui-compact .collapsible-section { + padding: 0 var(--space-xlarge); + margin-block-end: var(--space-xxlarge); +} +.thumbs-ui-compact .search-wrapper { + padding: 0; + margin-block: var(--space-xxlarge); +} +.thumbs-ui-compact .search-wrapper .logo-and-wordmark { + margin-block-end: var(--space-xxlarge); +} +.thumbs-ui-compact .search-wrapper .logo-and-wordmark-wrapper { + margin-block-end: 0; +} +@media (height <= 700px) { + .thumbs-ui-compact .search-wrapper { + padding: 0; + margin-block-start: 0; + } +} +@media (height > 700px) { + .thumbs-ui-compact .search-wrapper { + padding: 0; + } +} +.thumbs-ui-compact.fixed-search .search-wrapper { + margin-block-start: 0; +} +.thumbs-ui-compact.fixed-search .search-wrapper .logo-and-wordmark-wrapper { + margin-block-end: 0; +} +.thumbs-ui-compact .top-site-outer { + margin-block-end: var(--space-medium); +} +.shortcuts-refresh .thumbs-ui-compact .top-site-outer { + margin-block-end: 0; +} + .ds-privacy-link { text-align: center; font-size: var(--font-size-small); diff --git a/browser/extensions/newtab/data/content/activity-stream.bundle.js b/browser/extensions/newtab/data/content/activity-stream.bundle.js @@ -201,6 +201,8 @@ for (const type of [ "PLACES_LINKS_DELETED", "PLACES_LINK_BLOCKED", "POCKET_CTA", + "POCKET_THUMBS_DOWN", + "POCKET_THUMBS_UP", "POCKET_WAITING_FOR_SPOC", "PREFS_INITIAL_VALUES", "PREF_CHANGED", @@ -882,6 +884,7 @@ class DiscoveryStreamAdminUI extends (external_React_default()).PureComponent { } = e.target; this.props.dispatch(actionCreators.SetPref(PREF_SECTIONS_ENABLED, pressed)); this.props.dispatch(actionCreators.SetPref("discoverystream.sections.cards.enabled", pressed)); + this.props.dispatch(actionCreators.SetPref("discoverystream.sections.cards.thumbsUpDown.enabled", pressed)); } sendConversionEvent() { const detail = { @@ -3366,6 +3369,56 @@ const DSMessageFooter = props => { className: "story-footer" }, dsMessageLabel); }; +;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSThumbsUpDownButtons/DSThumbsUpDownButtons.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/. */ + + +function DSThumbsUpDownButtons({ + sponsor, + onThumbsUpClick, + onThumbsDownClick, + isThumbsUpActive, + isThumbsDownActive, + refinedCardsLayout, + tabIndex +}) { + let thumbsButtons = /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("button", { + onClick: onThumbsUpClick, + className: `card-stp-thumbs-button icon icon-thumbs-up ${isThumbsUpActive ? "is-active" : null}`, + "data-l10n-id": "newtab-pocket-thumbs-up-tooltip", + tabIndex: tabIndex + }), /*#__PURE__*/external_React_default().createElement("button", { + onClick: onThumbsDownClick, + className: `card-stp-thumbs-button icon icon-thumbs-down ${isThumbsDownActive ? "is-active" : null}`, + "data-l10n-id": "newtab-pocket-thumbs-down-tooltip", + tabIndex: tabIndex + })); + if (refinedCardsLayout) { + thumbsButtons = /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("moz-button", { + iconsrc: "chrome://global/skin/icons/thumbs-up-20.svg", + onClick: onThumbsUpClick, + className: `card-stp-thumbs-button icon icon-thumbs-up refined-layout ${isThumbsUpActive ? "is-active" : null}`, + "data-l10n-id": "newtab-pocket-thumbs-up-tooltip", + type: "icon ghost", + tabIndex: tabIndex + }), /*#__PURE__*/external_React_default().createElement("moz-button", { + iconsrc: "chrome://global/skin/icons/thumbs-down-20.svg", + onClick: onThumbsDownClick, + className: `card-stp-thumbs-button icon icon-thumbs-down ${isThumbsDownActive ? "is-active" : null}`, + "data-l10n-id": "newtab-pocket-thumbs-down-tooltip", + type: "icon ghost", + tabIndex: tabIndex + })); + } + return /*#__PURE__*/external_React_default().createElement("div", { + className: "card-stp-thumbs-buttons-wrapper" + }, !sponsor && /*#__PURE__*/external_React_default().createElement("div", { + className: "card-stp-thumbs-buttons" + }, thumbsButtons)); +} + ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSCard/DSCard.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, @@ -3381,6 +3434,8 @@ const DSMessageFooter = props => { + + const READING_WPM = 220; const PREF_OHTTP_MERINO = "discoverystream.merino-provider.ohttp.enabled"; const PREF_OHTTP_UNIFIED_ADS = "unifiedAds.ohttp.enabled"; @@ -3465,14 +3520,20 @@ const DefaultMeta = ({ ctaButtonVariant, dispatch, mayHaveSectionsCards, + mayHaveThumbsUpDown, + onThumbsUpClick, + onThumbsDownClick, + state, format, topic, isSectionsCard, showTopics, icon_src, - refinedCardsLayout + refinedCardsLayout, + tabIndex }) => { - const shouldHaveFooterSection = isSectionsCard && showTopics; + const shouldHaveThumbs = format !== "rectangle" && mayHaveSectionsCards && mayHaveThumbsUpDown; + const shouldHaveFooterSection = isSectionsCard && (shouldHaveThumbs || showTopics); return /*#__PURE__*/external_React_default().createElement("div", { className: "meta" }, /*#__PURE__*/external_React_default().createElement("div", { @@ -3491,7 +3552,14 @@ const DefaultMeta = ({ className: "excerpt clamp" }, "Sponsored content supports our mission to build a better web.") : excerpt && /*#__PURE__*/external_React_default().createElement("p", { className: "excerpt clamp" - }, excerpt)), (shouldHaveFooterSection || refinedCardsLayout) && /*#__PURE__*/external_React_default().createElement("div", { + }, excerpt)), format !== "rectangle" && !mayHaveSectionsCards && mayHaveThumbsUpDown && !refinedCardsLayout && /*#__PURE__*/external_React_default().createElement(DSThumbsUpDownButtons, { + onThumbsDownClick: onThumbsDownClick, + onThumbsUpClick: onThumbsUpClick, + sponsor: sponsor, + isThumbsDownActive: state.isThumbsDownActive, + isThumbsUpActive: state.isThumbsUpActive, + tabIndex: tabIndex + }), (shouldHaveFooterSection || refinedCardsLayout) && /*#__PURE__*/external_React_default().createElement("div", { className: "sections-card-footer" }, refinedCardsLayout && format !== "rectangle" && format !== "spoc" && /*#__PURE__*/external_React_default().createElement(DSSource, { source: source, @@ -3502,6 +3570,14 @@ const DefaultMeta = ({ sponsored_by_override: sponsored_by_override, icon_src: icon_src, refinedCardsLayout: refinedCardsLayout + }), (shouldHaveThumbs || refinedCardsLayout) && /*#__PURE__*/external_React_default().createElement(DSThumbsUpDownButtons, { + onThumbsDownClick: onThumbsDownClick, + onThumbsUpClick: onThumbsUpClick, + sponsor: sponsor, + isThumbsDownActive: state.isThumbsDownActive, + isThumbsUpActive: state.isThumbsUpActive, + refinedCardsLayout: refinedCardsLayout, + tabIndex: tabIndex }), showTopics && /*#__PURE__*/external_React_default().createElement("span", { className: "ds-card-topic", "data-l10n-id": `newtab-topic-label-${topic}` @@ -3526,6 +3602,8 @@ class _DSCard extends (external_React_default()).PureComponent { this.doesLinkTopicMatchSelectedTopic = this.doesLinkTopicMatchSelectedTopic.bind(this); this.onMenuUpdate = this.onMenuUpdate.bind(this); this.onMenuShow = this.onMenuShow.bind(this); + this.onThumbsUpClick = this.onThumbsUpClick.bind(this); + this.onThumbsDownClick = this.onThumbsDownClick.bind(this); const refinedCardsLayout = this.props.Prefs.values["discoverystream.refinedCardsLayout.enabled"]; this.setContextMenuButtonHostRef = element => { this.contextMenuButtonHostElement = element; @@ -3534,7 +3612,9 @@ class _DSCard extends (external_React_default()).PureComponent { this.placeholderElement = element; }; this.state = { - isSeen: false + isSeen: false, + isThumbsUpActive: false, + isThumbsDownActive: false }; // If this is for the about:home startup cache, then we always want @@ -3676,6 +3756,134 @@ class _DSCard extends (external_React_default()).PureComponent { })); } } + onThumbsUpClick(event) { + event.stopPropagation(); + event.preventDefault(); + + // Toggle active state for thumbs up button to show CSS animation + const currentState = this.state.isThumbsUpActive; + + // If thumbs up has been clicked already, do nothing. + if (currentState) { + return; + } + this.setState({ + isThumbsUpActive: !currentState + }); + + // Record thumbs up telemetry event + this.props.dispatch(actionCreators.DiscoveryStreamUserEvent({ + event: "POCKET_THUMBS_UP", + source: "THUMBS_UI", + value: { + action_position: this.props.pos, + recommendation_id: this.props.recommendation_id, + tile_id: this.props.id, + corpus_item_id: this.props.corpus_item_id, + scheduled_corpus_item_id: this.props.scheduled_corpus_item_id, + recommended_at: this.props.recommended_at, + received_rank: this.props.received_rank, + thumbs_up: true, + thumbs_down: false, + topic: this.props.topic, + format: getActiveCardSize(window.innerWidth, this.props.sectionsClassNames, this.props.section, false // (thumbs up/down only exist on organic content) + ), + ...(this.props.section ? { + section: this.props.section, + section_position: this.props.sectionPosition, + is_section_followed: this.props.sectionFollowed + } : {}) + } + })); + + // Show Toast + this.props.dispatch(actionCreators.OnlyToOneContent({ + type: actionTypes.SHOW_TOAST_MESSAGE, + data: { + showNotifications: true, + toastId: "thumbsUpToast" + } + }, "ActivityStream:Content")); + } + onThumbsDownClick(event) { + event.stopPropagation(); + event.preventDefault(); + + // Toggle active state for thumbs down button to show CSS animation + const currentState = this.state.isThumbsDownActive; + this.setState({ + isThumbsDownActive: !currentState + }); + + // Run dismiss event after 0.5 second delay + if (this.props.dispatch && this.props.type && this.props.id && this.props.url) { + const index = this.props.pos; + const source = this.props.type.toUpperCase(); + const spocData = { + url: this.props.url, + guid: this.props.id, + type: "CardGrid", + card_type: "organic", + recommendation_id: this.props.recommendation_id, + tile_id: this.props.id, + corpus_item_id: this.props.corpus_item_id, + scheduled_corpus_item_id: this.props.scheduled_corpus_item_id, + recommended_at: this.props.recommended_at, + received_rank: this.props.received_rank + }; + const blockUrlOption = LinkMenuOptions.BlockUrl(spocData, index, source); + const { + action, + impression, + userEvent + } = blockUrlOption; + setTimeout(() => { + this.props.dispatch(action); + this.props.dispatch(actionCreators.DiscoveryStreamUserEvent({ + event: userEvent, + source, + action_position: index + })); + }, 500); + if (impression) { + this.props.dispatch(impression); + } + + // Record thumbs down telemetry event + this.props.dispatch(actionCreators.DiscoveryStreamUserEvent({ + event: "POCKET_THUMBS_DOWN", + source: "THUMBS_UI", + value: { + action_position: this.props.pos, + recommendation_id: this.props.recommendation_id, + tile_id: this.props.id, + corpus_item_id: this.props.corpus_item_id, + scheduled_corpus_item_id: this.props.scheduled_corpus_item_id, + recommended_at: this.props.recommended_at, + received_rank: this.props.received_rank, + thumbs_up: false, + thumbs_down: true, + topic: this.props.topic, + format: getActiveCardSize(window.innerWidth, this.props.sectionsClassNames, this.props.section, false // (thumbs up/down only exist on organic content) + ), + ...(this.props.section ? { + section: this.props.section, + section_position: this.props.sectionPosition, + is_section_followed: this.props.sectionFollowed + } : {}) + } + })); + + // Show Toast + this.props.dispatch(actionCreators.OnlyToOneContent({ + type: actionTypes.SHOW_TOAST_MESSAGE, + data: { + showNotifications: true, + toastId: "thumbsDownToast" + } + }, "ActivityStream:Content")); + } + } onMenuUpdate(showContextMenu) { if (!showContextMenu) { const dsLinkMenuHostDiv = this.contextMenuButtonHostElement; @@ -3980,7 +4188,10 @@ class _DSCard extends (external_React_default()).PureComponent { sponsored_by_override: this.props.sponsored_by_override, ctaButtonVariant: ctaButtonVariant, dispatch: this.props.dispatch, + mayHaveThumbsUpDown: this.props.mayHaveThumbsUpDown, mayHaveSectionsCards: this.props.mayHaveSectionsCards, + onThumbsUpClick: this.onThumbsUpClick, + onThumbsDownClick: this.onThumbsDownClick, state: this.state, showTopics: !refinedCardsLayout && this.props.showTopics, isSectionsCard: this.props.mayHaveSectionsCards && this.props.topic, @@ -4623,6 +4834,7 @@ const AdBanner = ({ const PREF_SECTIONS_CARDS_ENABLED = "discoverystream.sections.cards.enabled"; +const PREF_THUMBS_UP_DOWN_ENABLED = "discoverystream.thumbsUpDown.enabled"; const PREF_TOPICS_ENABLED = "discoverystream.topicLabels.enabled"; const PREF_TOPICS_SELECTED = "discoverystream.topicSelection.selectedTopics"; const PREF_TOPICS_AVAILABLE = "discoverystream.topicSelection.topics"; @@ -4728,6 +4940,7 @@ class _CardGrid extends (external_React_default()).PureComponent { topicsLoading } = DiscoveryStream; const mayHaveSectionsCards = prefs[PREF_SECTIONS_CARDS_ENABLED]; + const mayHaveThumbsUpDown = prefs[PREF_THUMBS_UP_DOWN_ENABLED]; const showTopics = prefs[PREF_TOPICS_ENABLED]; const selectedTopics = prefs[PREF_TOPICS_SELECTED]; const availableTopics = prefs[PREF_TOPICS_AVAILABLE]; @@ -4781,6 +4994,7 @@ class _CardGrid extends (external_React_default()).PureComponent { ctaButtonVariant: ctaButtonVariant, recommendation_id: rec.recommendation_id, firstVisibleTimestamp: this.props.firstVisibleTimestamp, + mayHaveThumbsUpDown: mayHaveThumbsUpDown, mayHaveSectionsCards: mayHaveSectionsCards, corpus_item_id: rec.corpus_item_id, scheduled_corpus_item_id: rec.scheduled_corpus_item_id, @@ -11088,10 +11302,12 @@ const Weather_Weather = (0,external_ReactRedux_namespaceObject.connect)(state => // Prefs const CardSections_PREF_SECTIONS_CARDS_ENABLED = "discoverystream.sections.cards.enabled"; +const PREF_SECTIONS_CARDS_THUMBS_UP_DOWN_ENABLED = "discoverystream.sections.cards.thumbsUpDown.enabled"; const PREF_SECTIONS_PERSONALIZATION_ENABLED = "discoverystream.sections.personalization.enabled"; const CardSections_PREF_TOPICS_ENABLED = "discoverystream.topicLabels.enabled"; const CardSections_PREF_TOPICS_SELECTED = "discoverystream.topicSelection.selectedTopics"; const CardSections_PREF_TOPICS_AVAILABLE = "discoverystream.topicSelection.topics"; +const CardSections_PREF_THUMBS_UP_DOWN_ENABLED = "discoverystream.thumbsUpDown.enabled"; const PREF_INTEREST_PICKER_ENABLED = "discoverystream.sections.interestPicker.enabled"; const CardSections_PREF_VISIBLE_SECTIONS = "discoverystream.sections.interestPicker.visibleSections"; const CardSections_PREF_BILLBOARD_ENABLED = "newtabAdSize.billboard"; @@ -11230,6 +11446,8 @@ function CardSection({ }; const showTopics = prefs[CardSections_PREF_TOPICS_ENABLED]; const mayHaveSectionsCards = prefs[CardSections_PREF_SECTIONS_CARDS_ENABLED]; + const mayHaveSectionsCardsThumbsUpDown = prefs[PREF_SECTIONS_CARDS_THUMBS_UP_DOWN_ENABLED]; + const mayHaveThumbsUpDown = prefs[CardSections_PREF_THUMBS_UP_DOWN_ENABLED]; const selectedTopics = prefs[CardSections_PREF_TOPICS_SELECTED]; const availableTopics = prefs[CardSections_PREF_TOPICS_AVAILABLE]; const refinedCardsLayout = prefs[PREF_REFINED_CARDS_ENABLED]; @@ -11259,6 +11477,9 @@ function CardSection({ // Ref to hold the section element const sectionRefs = useIntersectionObserver(handleIntersection); + + // Only show thumbs up/down buttons if both default thumbs and sections thumbs prefs are enabled + const mayHaveCombinedThumbsUpDown = mayHaveSectionsCardsThumbsUpDown && mayHaveThumbsUpDown; const onFollowClick = (0,external_React_namespaceObject.useCallback)(() => { const updatedSectionData = { ...sectionPersonalization, @@ -11429,6 +11650,7 @@ function CardSection({ received_rank: rec.received_rank, format: rec.format, alt_text: rec.alt_text, + mayHaveThumbsUpDown: mayHaveCombinedThumbsUpDown, mayHaveSectionsCards: mayHaveSectionsCards, showTopics: shouldShowLabels, selectedTopics: selectedTopics, @@ -14985,6 +15207,38 @@ function DownloadModalToggle({ })); } +;// CONCATENATED MODULE: ./content-src/components/Notifications/Toasts/ThumbUpThumbDownToast.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/. */ + + +function ThumbUpThumbDownToast({ + onDismissClick, + onAnimationEnd +}) { + const mozMessageBarRef = (0,external_React_namespaceObject.useRef)(null); + (0,external_React_namespaceObject.useEffect)(() => { + const { + current: mozMessageBarElement + } = mozMessageBarRef; + mozMessageBarElement.addEventListener("message-bar:user-dismissed", onDismissClick, { + once: true + }); + return () => { + mozMessageBarElement.removeEventListener("message-bar:user-dismissed", onDismissClick); + }; + }, [onDismissClick]); + return /*#__PURE__*/external_React_default().createElement("moz-message-bar", { + type: "success", + class: "notification-feed-item", + dismissable: true, + "data-l10n-id": "newtab-toast-thumbs-up-or-down2", + ref: mozMessageBarRef, + onAnimationEnd: onAnimationEnd + }); +} + ;// CONCATENATED MODULE: ./content-src/components/Notifications/Toasts/ReportContentToast.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, @@ -15026,6 +15280,7 @@ function ReportContentToast({ + function Notifications_Notifications({ dispatch }) { @@ -15063,6 +15318,13 @@ function Notifications_Notifications({ onAnimationEnd: syncHiddenToastData, key: toastCounter }); + case "thumbsUpToast": + case "thumbsDownToast": + return /*#__PURE__*/external_React_default().createElement(ThumbUpThumbDownToast, { + onDismissClick: syncHiddenToastData, + onAnimationEnd: syncHiddenToastData, + key: toastCounter + }); default: throw new Error(`Unexpected toast type: ${latestToastItem}`); } @@ -16115,6 +16377,8 @@ class BaseContent extends (external_React_default()).PureComponent { const mobileDownloadPromoVariantCEnabled = prefs["mobileDownloadModal.variant-c"]; const mobileDownloadPromoVariantABorC = mobileDownloadPromoVariantAEnabled || mobileDownloadPromoVariantBEnabled || mobileDownloadPromoVariantCEnabled; const mobileDownloadPromoWrapperHeightModifier = prefs["weather.display"] === "detailed" && weatherEnabled && shouldDisplayWeather && mayHaveWeather ? "is-tall" : ""; + 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"]; @@ -16130,7 +16394,7 @@ class BaseContent extends (external_React_default()).PureComponent { // layoutsVariantAEnabled ? "layout-variant-a" : "", // Layout experiment variant A // layoutsVariantBEnabled ? "layout-variant-b" : "", // Layout experiment variant B pocketEnabled ? "has-recommended-stories" : "no-recommended-stories", sectionsEnabled ? "has-sections-grid" : ""].filter(v => v).join(" "); - const outerClassName = ["outer-wrapper", isDiscoveryStream && pocketEnabled && "ds-outer-wrapper-search-alignment", isDiscoveryStream && "ds-outer-wrapper-breakpoint-override", prefs.showSearch && this.state.fixedSearch && !noSectionsEnabled && "fixed-search", prefs.showSearch && noSectionsEnabled && "only-search", prefs["feeds.topsites"] && !pocketEnabled && !prefs.showSearch && "only-topsites", noSectionsEnabled && "no-sections", prefs["logowordmark.alwaysVisible"] && "visible-logo"].filter(v => v).join(" "); + const outerClassName = ["outer-wrapper", isDiscoveryStream && pocketEnabled && "ds-outer-wrapper-search-alignment", isDiscoveryStream && "ds-outer-wrapper-breakpoint-override", prefs.showSearch && this.state.fixedSearch && !noSectionsEnabled && "fixed-search", prefs.showSearch && noSectionsEnabled && "only-search", prefs["feeds.topsites"] && !pocketEnabled && !prefs.showSearch && "only-topsites", noSectionsEnabled && "no-sections", prefs["logowordmark.alwaysVisible"] && "visible-logo", hasThumbsUpDownLayout && hasThumbsUpDown && "thumbs-ui-compact"].filter(v => v).join(" "); // If state.showDownloadHighlightOverride has value, let it override the logic // Otherwise, defer to OMC message display logic diff --git a/browser/extensions/newtab/karma.mc.config.js b/browser/extensions/newtab/karma.mc.config.js @@ -266,6 +266,12 @@ module.exports = function (config) { functions: 0, branches: 0, }, + "content-src/components/DiscoveryStreamComponents/DSThumbsUpDownButtons/DSThumbsUpDownButtons.jsx": + { + statements: 75, + lines: 75, + branches: 50, + }, "content-src/components/DiscoveryStreamComponents/**/*.jsx": { statements: 80.95, lines: 80.95, diff --git a/browser/extensions/newtab/lib/ActivityStream.sys.mjs b/browser/extensions/newtab/lib/ActivityStream.sys.mjs @@ -95,6 +95,11 @@ const LOCALE_TOPIC_LABEL_CONFIG = const REGION_BASIC_CONFIG = "browser.newtabpage.activity-stream.discoverystream.region-basic-config"; +const REGION_THUMBS_CONFIG = + "browser.newtabpage.activity-stream.discoverystream.thumbsUpDown.region-thumbs-config"; +const LOCALE_THUMBS_CONFIG = + "browser.newtabpage.activity-stream.discoverystream.thumbsUpDown.locale-thumbs-config"; + const REGION_CONTEXTUAL_AD_CONFIG = "browser.newtabpage.activity-stream.discoverystream.sections.contextualAds.region-config"; const LOCALE_CONTEXTUAL_AD_CONFIG = @@ -230,6 +235,13 @@ function showTopicLabels({ geo, locale }) { ); } +function showThumbsUpDown({ geo, locale }) { + return ( + csvPrefHasValue(REGION_THUMBS_CONFIG, geo) && + csvPrefHasValue(LOCALE_THUMBS_CONFIG, locale) + ); +} + function showSectionLayout({ geo, locale }) { return ( csvPrefHasValue(REGION_SECTIONS_CONFIG, geo) && @@ -818,6 +830,14 @@ export const PREFS_CONFIG = new Map([ }, ], [ + "discoverystream.sections.cards.thumbsUpDown.enabled", + { + title: + "Boolean flag to enable thumbs up/down buttons in the new card UI in recommended stories", + value: true, + }, + ], + [ "discoverystream.reportAds.enabled", { title: "Boolean flag to enable reporting ads from the context menu", @@ -1196,6 +1216,22 @@ export const PREFS_CONFIG = new Map([ }, ], [ + "discoverystream.thumbsUpDown.enabled", + { + title: "Allow users to give thumbs up/down on recommended stories", + // pref is dynamic + getValue: showThumbsUpDown, + }, + ], + [ + "discoverystream.thumbsUpDown.searchTopsitesCompact", + { + title: + "A compact layout of the search/topsites/stories sections to account for new height from thumbs up/down icons ", + value: false, + }, + ], + [ "discoverystream.region-basic-layout", { title: "Decision to use basic layout based on region.", diff --git a/browser/extensions/newtab/lib/TelemetryFeed.sys.mjs b/browser/extensions/newtab/lib/TelemetryFeed.sys.mjs @@ -1037,6 +1037,66 @@ export class TelemetryFeed { break; } + case "POCKET_THUMBS_DOWN": + case "POCKET_THUMBS_UP": { + const { + corpus_item_id, + format, + is_section_followed, + action_position, + received_rank, + recommendation_id, + recommended_at, + scheduled_corpus_item_id, + section_position, + section, + thumbs_down, + thumbs_up, + tile_id, + topic, + } = action.data.value ?? {}; + + const gleanData = { + tile_id, + position: action_position, + // We conditionally add in a few props. + ...(corpus_item_id ? { corpus_item_id } : {}), + ...(scheduled_corpus_item_id ? { scheduled_corpus_item_id } : {}), + ...(corpus_item_id || scheduled_corpus_item_id + ? { + received_rank, + recommended_at, + } + : { + recommendation_id, + }), + thumbs_up, + thumbs_down, + topic, + ...(format ? { format } : {}), + ...(section + ? { + section, + section_position, + ...(this.sectionsPersonalizationEnabled + ? { is_section_followed: !!is_section_followed } + : {}), + } + : {}), + }; + Glean.pocket.thumbVotingInteraction.record({ + ...this.redactNewTabPing(gleanData), + newtab_visit_id: session.session_id, + }); + if (this.privatePingEnabled) { + // eslint-disable-next-line no-unused-vars + this.newtabContentPing.recordEvent( + "thumbVotingInteraction", + gleanData + ); + } + break; + } // Bug 1969452 - Feature Highlight Telemetry Events case "FEATURE_HIGHLIGHT_DISMISS": case "FEATURE_HIGHLIGHT_IMPRESSION": diff --git a/browser/extensions/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx b/browser/extensions/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx @@ -10,6 +10,7 @@ import { StatusMessage, SponsorLabel, } from "content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter"; +import { DSThumbsUpDownButtons } from "content-src/components/DiscoveryStreamComponents/DSThumbsUpDownButtons/DSThumbsUpDownButtons"; import { actionCreators as ac } from "common/Actions.mjs"; import { DSLinkMenu } from "content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu"; import { DSImage } from "content-src/components/DiscoveryStreamComponents/DSImage/DSImage"; @@ -111,6 +112,70 @@ describe("<DSCard>", () => { assert.lengthOf(contextFooter.find(StatusMessage), 1); }); + it("should render thumbs up/down UI when not a spoc element ", () => { + const store = createStore(combineReducers(reducers), INITIAL_STATE); + wrapper = mount( + <Provider store={store}> + <DSCard mayHaveThumbsUpDown={true} {...DEFAULT_PROPS} /> + </Provider> + ); + + const dsCardInstance = wrapper.find(DSCard).instance(); + dsCardInstance.setState({ isSeen: true }); + wrapper.update(); + + const thumbs_up_down_buttons_component = wrapper.find( + DSThumbsUpDownButtons + ); + assert.ok(thumbs_up_down_buttons_component.exists()); + }); + + it("thumbs up button should have active class when isThumbsUpActive is true", () => { + const store = createStore(combineReducers(reducers), INITIAL_STATE); + wrapper = mount( + <Provider store={store}> + <DSCard mayHaveThumbsUpDown={true} {...DEFAULT_PROPS} /> + </Provider> + ); + + const dsCardInstance = wrapper.find(DSCard).instance(); + dsCardInstance.setState({ isSeen: true, isThumbsUpActive: true }); + wrapper.update(); + + const thumbs_up_down_buttons_component = wrapper.find( + DSThumbsUpDownButtons + ); + const thumbs_up_active_button = thumbs_up_down_buttons_component.find( + ".icon-thumbs-up.is-active" + ); + assert.ok(thumbs_up_active_button.exists()); + }); + + it("should NOT render thumbs up/down UI when a spoc element ", () => { + const store = createStore(combineReducers(reducers), INITIAL_STATE); + + wrapper = mount( + <Provider store={store}> + <DSCard + mayHaveThumbsUpDown={true} + sponsor="Mozilla" + {...DEFAULT_PROPS} + /> + </Provider> + ); + const dsCardInstance = wrapper.find(DSCard).instance(); + dsCardInstance.setState({ isSeen: true }); + wrapper.update(); + // Note: The wrapper is still rendered for DSCard height but the contents is not + const thumbs_up_down_buttons_component = wrapper.find( + DSThumbsUpDownButtons + ); + const thumbs_up_down_buttons = thumbs_up_down_buttons_component.find( + ".card-stp-thumbs-buttons" + ); + assert.ok(!thumbs_up_down_buttons.exists()); + }); + it("should render Sponsored Context for a spoc element", () => { // eslint-disable-next-line no-shadow const context = "Sponsored by Foo"; @@ -526,6 +591,100 @@ describe("<DSCard>", () => { }); }); + describe("DSCard onThumbsUpClick", () => { + it("should update state.onThumbsUpClick for onThumbsUpClick", () => { + wrapper.setState({ isThumbsUpActive: false }); + wrapper.instance().onThumbsUpClick({ + stopPropagation: () => {}, + preventDefault: () => {}, + }); + assert.isTrue(wrapper.instance().state.isThumbsUpActive); + }); + + it("should not fire telemetry for onThumbsUpClick is clicked twice", () => { + wrapper.setState({ isThumbsUpActive: true }); + wrapper.instance().onThumbsUpClick({ + stopPropagation: () => {}, + preventDefault: () => {}, + }); + + // state.isThumbsUpActive remains in active state + assert.isTrue(wrapper.instance().state.isThumbsUpActive); + assert.notCalled(dispatch); + }); + + it("should fire telemetry for onThumbsUpClick", () => { + wrapper.instance().onThumbsUpClick({ + stopPropagation: () => {}, + preventDefault: () => {}, + }); + + assert.calledTwice(dispatch); + + let [action] = dispatch.firstCall.args; + + assert.equal(action.type, "DISCOVERY_STREAM_USER_EVENT"); + assert.equal(action.data.event, "POCKET_THUMBS_UP"); + assert.equal(action.data.source, "THUMBS_UI"); + assert.deepEqual(action.data.value.thumbs_up, true); + assert.deepEqual(action.data.value.thumbs_down, false); + + [action] = dispatch.secondCall.args; + + assert.equal(action.type, "SHOW_TOAST_MESSAGE"); + assert.deepEqual(action.data.showNotifications, true); + assert.deepEqual(action.data.toastId, "thumbsUpToast"); + }); + }); + + describe("DSCard onThumbsDownClick", () => { + it("should fire telemetry for onThumbsDownClick", () => { + wrapper.setProps({ + id: "fooidx", + pos: 1, + type: "foo", + fetchTimestamp: undefined, + url: "about:robots", + dispatch, + }); + + wrapper.instance().onThumbsDownClick({ + stopPropagation: () => {}, + preventDefault: () => {}, + }); + + assert.calledThrice(dispatch); + + let [action] = dispatch.firstCall.args; + + assert.equal(action.type, "TELEMETRY_IMPRESSION_STATS"); + assert.equal(action.data.source, "FOO"); + + [action] = dispatch.secondCall.args; + + assert.equal(action.type, "DISCOVERY_STREAM_USER_EVENT"); + assert.equal(action.data.event, "POCKET_THUMBS_DOWN"); + assert.equal(action.data.source, "THUMBS_UI"); + assert.deepEqual(action.data.value.thumbs_up, false); + assert.deepEqual(action.data.value.thumbs_down, true); + + [action] = dispatch.thirdCall.args; + + assert.equal(action.type, "SHOW_TOAST_MESSAGE"); + assert.deepEqual(action.data.showNotifications, true); + assert.deepEqual(action.data.toastId, "thumbsDownToast"); + }); + + it("should update state.onThumbsDownClick for onThumbsDownClick", () => { + wrapper.setState({ isThumbsDownActive: false }); + wrapper.instance().onThumbsDownClick({ + stopPropagation: () => {}, + preventDefault: () => {}, + }); + assert.isTrue(wrapper.instance().state.isThumbsDownActive); + }); + }); + describe("DSCard menu open states", () => { let cardNode; let fakeDocument; diff --git a/browser/extensions/newtab/test/unit/lib/ActivityStream.test.js b/browser/extensions/newtab/test/unit/lib/ActivityStream.test.js @@ -504,6 +504,80 @@ describe("ActivityStream", () => { assert.isFalse(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value); }); }); + describe("showThumbsUpDown", () => { + let stub; + let getStringPrefStub; + const FEATURE_ENABLED_PREF = "discoverystream.thumbsUpDown.enabled"; + const REGION_THUMBS_CONFIG = + "browser.newtabpage.activity-stream.discoverystream.thumbsUpDown.region-thumbs-config"; + const LOCALE_THUMBS_CONFIG = + "browser.newtabpage.activity-stream.discoverystream.thumbsUpDown.locale-thumbs-config"; + + beforeEach(() => { + stub = sandbox.stub(global.Region, "home"); + + sandbox + .stub(global.Services.locale, "appLocaleAsBCP47") + .get(() => "en-US"); + + getStringPrefStub = sandbox.stub(global.Services.prefs, "getStringPref"); + + // Set default regions + getStringPrefStub.withArgs(REGION_THUMBS_CONFIG).returns("US, CA"); + + // Set default locales + getStringPrefStub + .withArgs(LOCALE_THUMBS_CONFIG) + .returns("en-US,en-GB,en-CA"); + }); + it("should turn off when region and locale are not set", () => { + stub.get(() => ""); + sandbox.stub(global.Services.locale, "appLocaleAsBCP47").get(() => ""); + as._updateDynamicPrefs(); + assert.isFalse(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value); + }); + it("should turn off when region is not set", () => { + stub.get(() => ""); + as._updateDynamicPrefs(); + assert.isFalse(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value); + }); + it("should turn on when region is supported", () => { + stub.get(() => "US"); + as._updateDynamicPrefs(); + assert.isTrue(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value); + }); + it("should turn off when region is not supported", () => { + stub.get(() => "JP"); + as._updateDynamicPrefs(); + assert.isFalse(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value); + }); + it("should turn off when locale is not set", () => { + stub.get(() => "US"); + sandbox.stub(global.Services.locale, "appLocaleAsBCP47").get(() => ""); + as._updateDynamicPrefs(); + assert.isFalse(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value); + }); + it("should turn on when locale is supported", () => { + stub.get(() => "US"); + sandbox + .stub(global.Services.locale, "appLocaleAsBCP47") + .get(() => "en-US"); + as._updateDynamicPrefs(); + assert.isTrue(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value); + }); + it("should turn off when locale is not supported", () => { + stub.get(() => "US"); + sandbox.stub(global.Services.locale, "appLocaleAsBCP47").get(() => "fr"); + as._updateDynamicPrefs(); + assert.isFalse(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value); + }); + it("should turn off when region and locale are both not supported", () => { + stub.get(() => "FR"); + sandbox.stub(global.Services.locale, "appLocaleAsBCP47").get(() => "fr"); + as._updateDynamicPrefs(); + assert.isFalse(PREFS_CONFIG.get(FEATURE_ENABLED_PREF).value); + }); + }); describe("discoverystream.region-basic-layout config", () => { let getStringPrefStub; beforeEach(() => { diff --git a/browser/extensions/newtab/test/xpcshell/test_TelemetryFeed.js b/browser/extensions/newtab/test/xpcshell/test_TelemetryFeed.js @@ -2439,6 +2439,88 @@ add_task( ); add_task( + async function test_handleDiscoveryStreamUserEvent_thumbs_down_event() { + info( + "TelemetryFeed.handleDiscoveryStreamUserEvent instruments a thumbs down" + + " event of an organic story" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + const ACTION_POSITION = 42; + let action = actionCreators.DiscoveryStreamUserEvent({ + event: "POCKET_THUMBS_DOWN", + action_position: ACTION_POSITION, + value: { + card_type: "organic", + recommendation_id: "decaf-c0ff33", + tile_id: 314623757745896, + thumbs_down: true, + thumbs_up: false, + }, + }); + + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + + instance.handleDiscoveryStreamUserEvent(action); + + let thumbVotes = Glean.pocket.thumbVotingInteraction.testGetValue(); + Assert.equal(thumbVotes.length, 1, "Recorded 1 thumbs down"); + Assert.deepEqual(thumbVotes[0].extra, { + newtab_visit_id: SESSION_ID, + recommendation_id: "decaf-c0ff33", + thumbs_down: String(true), + thumbs_up: String(false), + content_redacted: String(true), + }); + + sandbox.restore(); + } +); + +add_task(async function test_handleDiscoveryStreamUserEvent_thumbs_up_event() { + info( + "TelemetryFeed.handleDiscoveryStreamUserEvent instruments a thumbs up" + + " event of an organic story" + ); + + let sandbox = sinon.createSandbox(); + let instance = new TelemetryFeed(); + Services.fog.testResetFOG(); + const ACTION_POSITION = 42; + let action = actionCreators.DiscoveryStreamUserEvent({ + event: "POCKET_THUMBS_DOWN", + action_position: ACTION_POSITION, + value: { + card_type: "organic", + recommendation_id: "decaf-c0ff33", + tile_id: 314623757745896, + thumbs_down: false, + thumbs_up: true, + }, + }); + + const SESSION_ID = "decafc0ffee"; + sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); + + instance.handleDiscoveryStreamUserEvent(action); + + let thumbVotes = Glean.pocket.thumbVotingInteraction.testGetValue(); + Assert.equal(thumbVotes.length, 1, "Recorded 1 thumbs down"); + Assert.deepEqual(thumbVotes[0].extra, { + newtab_visit_id: SESSION_ID, + recommendation_id: "decaf-c0ff33", + thumbs_down: String(false), + thumbs_up: String(true), + content_redacted: String(true), + }); + + sandbox.restore(); +}); + +add_task( async function test_handleAboutSponsoredTopSites_record_showPrivacyClick() { info( "TelemetryFeed.handleAboutSponsoredTopSites should record a Glean " + diff --git a/browser/locales/en-US/browser/newtab/newtab.ftl b/browser/locales/en-US/browser/newtab/newtab.ftl @@ -211,6 +211,21 @@ newtab-discovery-empty-section-topstories-loading = Loading… # Displays when a layout in a section took too long to fetch articles. newtab-discovery-empty-section-topstories-timed-out = Oops! We almost loaded this section, but not quite. +## Thumbs up and down buttons that shows over a newtab stories card thumbnail on hover. + +# Clicking the thumbs up button for this story will result in more stories like this one being recommended +newtab-pocket-thumbs-up-tooltip = + .title = More like this +# Clicking the thumbs down button for this story informs us that the user does not feel like the story is interesting for them +newtab-pocket-thumbs-down-tooltip = + .title = Not for me +# Used to show the user a message upon clicking the thumbs up or down buttons +newtab-toast-thumbs-up-or-down2 = + .message = Thanks. Your feedback will help us improve your feed. +newtab-toast-dismiss-button = + .title = Dismiss + .aria-label = Dismiss + ## Error Fallback Content. ## This message and suggested action link are shown in each section of UI that fails to render. diff --git a/toolkit/components/nimbus/FeatureManifest.yaml b/toolkit/components/nimbus/FeatureManifest.yaml @@ -1310,6 +1310,14 @@ newTabSectionsExperiment: pref: browser.newtabpage.activity-stream.discoverystream.sections.enabled description: >- Enable the Section UI for recommended stories on newtab + cardRefreshThumbsUpDownEnabled: + type: boolean + setPref: + branch: user + pref: browser.newtabpage.activity-stream.discoverystream.sections.cards.thumbsUpDown.enabled + description: >- + Enable the thumbs up/down buttons in the revised pocket story + card UI in recommended stories on newtab cardRefreshEnabled: type: boolean setPref: @@ -1979,6 +1987,31 @@ pocketNewtab: pref: browser.newtabpage.activity-stream.discoverystream.merino-feed-experiment description: >- Should we pass the experiment branch and slug to the Merino feed request. + thumbsUpDown: + type: boolean + setPref: + branch: user + pref: browser.newtabpage.activity-stream.discoverystream.thumbsUpDown.enabled + description: Display thumbs up and down buttons on recommended stories + regionThumbsUpDownConfig: + description: A comma-separated list of regions that get thumbs up and down buttons by default. + type: string + setPref: + branch: user + pref: browser.newtabpage.activity-stream.discoverystream.thumbsUpDown.region-thumbs-config + localeThumbsUpDownConfig: + description: A comma-separated list of locales that get thumbs up and down buttons by default. + type: string + setPref: + branch: user + pref: browser.newtabpage.activity-stream.discoverystream.thumbsUpDown.locale-thumbs-config + thumbsUpDownCompactLayout: + type: boolean + setPref: + branch: user + pref: browser.newtabpage.activity-stream.discoverystream.thumbsUpDown.searchTopsitesCompact + description: >- + Change the layout of the sections to be more compact to account for the added card height with the thumbs up/down enabled. sessionRestore: description: Session restore feature