From d94b7db90e1653bf20022fb2716739f3829122f8 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Fri, 24 Oct 2025 10:49:56 -0400 Subject: [PATCH 01/17] Initial setup, adjust styles and prep for new data --- .../pages/new-tab/app/components/Icons.js | 33 +++++++++ .../components/PrivacyStats.module.css | 37 ++++++++-- .../new-tab/app/privacy-stats/strings.json | 16 +++-- .../app/protections/components/Protections.js | 4 +- .../components/Protections.module.css | 2 +- .../components/ProtectionsConsumer.js | 7 +- .../components/ProtectionsHeading.js | 70 +++++++++++-------- .../components/ProtectionsProvider.js | 17 +++++ .../protections/mocks/protections.mocks.js | 3 + .../new-tab/app/protections/protections.md | 3 +- .../pages/new-tab/app/styles/ntp-theme.css | 4 ++ .../messages/types/protections-data.json | 4 ++ .../new-tab/public/locales/en/new-tab.json | 24 +++++-- special-pages/pages/new-tab/types/new-tab.ts | 6 +- special-pages/shared/styles/variables.css | 2 + 15 files changed, 182 insertions(+), 50 deletions(-) diff --git a/special-pages/pages/new-tab/app/components/Icons.js b/special-pages/pages/new-tab/app/components/Icons.js index 791d1bd889..b1ed526ed0 100644 --- a/special-pages/pages/new-tab/app/components/Icons.js +++ b/special-pages/pages/new-tab/app/components/Icons.js @@ -601,3 +601,36 @@ export function CloseSmallIcon(props) { ); } + +/** + * @param {import('preact').JSX.SVGAttributes} props + */ +export function NewBadgeIcon(props) { + return ( + + + + + + + ); +} + +/** + * @param {import('preact').JSX.SVGAttributes} props + */ +export function InfoIcon(props) { + return ( + + + + + + ); +} diff --git a/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStats.module.css b/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStats.module.css index de482e1342..252399abf9 100644 --- a/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStats.module.css +++ b/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStats.module.css @@ -2,9 +2,13 @@ display: flex; align-items: center; height: 24px; - margin-bottom: 16px; + margin-bottom: 36px; position: relative; - gap: 8px; + gap: 2px; + + &.noTrackers { + margin-bottom: 12px; + } } .headingIcon { @@ -13,7 +17,7 @@ position: relative; display: flex; align-items: center; - justify-content: center; + justify-content: left; padding-top: 0.5px; img { @@ -26,13 +30,18 @@ font-size: var(--title-3-em-font-size); font-weight: var(--title-3-em-font-weight); line-height: var(--title-3-em-line-height); - flex: 1; + flex: 0 0 auto; + margin-right: 6px; } .widgetExpander { position: relative; + flex: 1; & [aria-controls] { + background-color: var(--ntp-widget-expander-bg); + width: 24px; + height: 24px; position: absolute; top: 50%; transform: translateY(-50%); @@ -46,17 +55,31 @@ } } +.counterContainer { + display: flex; + gap: 24px; +} + .counter { display: flex; flex-direction: column; gap: 4px; + padding-right: 38px; } .title { + color: var(--ntp-text-muted); grid-area: title; - font-size: var(--title-2-font-size); - font-weight: var(--title-2-font-weight); - line-height: var(--title-2-line-height); + font-size: var(--title-3-em-font-size); + font-weight: 400; + line-height: 28px; + + & span { + color: var(--ntp-text-primary); + display: block; + font-size: 40px; + padding-bottom: 6px; + } } .subtitle { diff --git a/special-pages/pages/new-tab/app/privacy-stats/strings.json b/special-pages/pages/new-tab/app/privacy-stats/strings.json index 2b5ed4ba65..6b3063e8dc 100644 --- a/special-pages/pages/new-tab/app/privacy-stats/strings.json +++ b/special-pages/pages/new-tab/app/privacy-stats/strings.json @@ -16,11 +16,11 @@ "note": "Placeholder to indicate that no tracking activity was blocked in the last 7 days" }, "stats_countBlockedSingular": { - "title": "1 tracking attempt blocked", + "title": "Tracking attempt blocked", "note": "The main headline indicating that a single tracker was blocked" }, "stats_countBlockedPlural": { - "title": "{count} tracking attempts blocked", + "title": "Tracking attempts blocked", "note": "The main headline indicating that more than 1 attempt has been blocked. Eg: '2 tracking attempts blocked'" }, "stats_noActivityAdsAndTrackers": { @@ -32,13 +32,21 @@ "note": "Placeholder to indicate that no ads or tracking activity was blocked in the last 7 days" }, "stats_countBlockedAdsAndTrackersSingular": { - "title": "1 advertising & tracking attempt blocked", + "title": "advertising & tracking attempt blocked", "note": "The main headline indicating that a single ad or tracking attempt was blocked" }, "stats_countBlockedAdsAndTrackersPlural": { - "title": "{count} advertising & tracking attempts blocked", + "title": "advertising & tracking attempts blocked", "note": "The main headline indicating that more than 1 ad or tracking attempt has been blocked. Eg: '2 advertising & tracking attempts blocked" }, + "stats_totalCookiePopUpsBlockedSingular": { + "title": "Cookie pop-up blocked", + "note": "The heading indicating that a single cookie pop-up was handled by the CPM" + }, + "stats_totalCookiePopUpsBlockedPlural": { + "title": "Cookie pop-ups blocked", + "note": "The heading indicating multiple cookie pop-ups were handled by the CPM" + }, "stats_feedCountBlockedSingular": { "title": "1 attempt blocked by DuckDuckGo in the last 7 days", "note": "A summary description of how many tracking attempts where blocked, when only one exists." diff --git a/special-pages/pages/new-tab/app/protections/components/Protections.js b/special-pages/pages/new-tab/app/protections/components/Protections.js index 32e5c584c6..aa7f326f7b 100644 --- a/special-pages/pages/new-tab/app/protections/components/Protections.js +++ b/special-pages/pages/new-tab/app/protections/components/Protections.js @@ -22,8 +22,9 @@ import { useTypedTranslationWith } from '../../types.js'; * @param {(feed: ProtectionsConfig['feed']) => void} props.setFeed * @param {import("preact").ComponentChild} [props.children] * @param {()=>void} props.toggle + * @param {import("@preact/signals").Signal} props.totalCookiePopUpsBlockedSignal */ -export function Protections({ expansion = 'expanded', children, blockedCountSignal, feed, toggle, setFeed }) { +export function Protections({ expansion = 'expanded', children, blockedCountSignal, feed, toggle, setFeed, totalCookiePopUpsBlockedSignal }) { const WIDGET_ID = useId(); const TOGGLE_ID = useId(); @@ -42,6 +43,7 @@ export function Protections({ expansion = 'expanded', children, blockedCountSign expansion={expansion} canExpand={true} buttonAttrs={attrs} + totalCookiePopUpsBlockedSignal={totalCookiePopUpsBlockedSignal} /> {children} diff --git a/special-pages/pages/new-tab/app/protections/components/Protections.module.css b/special-pages/pages/new-tab/app/protections/components/Protections.module.css index 6e6ff274d2..e7d90a4242 100644 --- a/special-pages/pages/new-tab/app/protections/components/Protections.module.css +++ b/special-pages/pages/new-tab/app/protections/components/Protections.module.css @@ -44,7 +44,7 @@ } .block { - margin-top: 24px; + margin-top: 32px; } .empty { color: var(--ntp-text-muted); diff --git a/special-pages/pages/new-tab/app/protections/components/ProtectionsConsumer.js b/special-pages/pages/new-tab/app/protections/components/ProtectionsConsumer.js index 07255bebdd..1ed400e7a7 100644 --- a/special-pages/pages/new-tab/app/protections/components/ProtectionsConsumer.js +++ b/special-pages/pages/new-tab/app/protections/components/ProtectionsConsumer.js @@ -1,5 +1,5 @@ import { useContext } from 'preact/hooks'; -import { ProtectionsContext, useBlockedCount } from './ProtectionsProvider.js'; +import { ProtectionsContext, useBlockedCount, useCookiePopUpsBlockedCount } from './ProtectionsProvider.js'; import { h } from 'preact'; import { Protections } from './Protections.js'; import { ActivityProvider } from '../../activity/ActivityProvider.js'; @@ -40,6 +40,10 @@ export function ProtectionsConsumer() { function ProtectionsReadyState({ data, config }) { const { toggle, setFeed } = useContext(ProtectionsContext); const blockedCountSignal = useBlockedCount(data.totalCount); + const totalCookiePopUpsBlockedSignal = useCookiePopUpsBlockedCount( + data.totalCookiePopUpsBlocked + ); + return ( {config.feed === 'activity' && ( diff --git a/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.js b/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.js index f05f8ce8d5..717f21b72c 100644 --- a/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.js +++ b/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.js @@ -1,13 +1,9 @@ import { useTypedTranslationWith } from '../../types.js'; -import { useState } from 'preact/hooks'; import styles from '../../privacy-stats/components/PrivacyStats.module.css'; import { ShowHideButtonCircle } from '../../components/ShowHideButton.jsx'; import cn from 'classnames'; import { h } from 'preact'; -import { useAdBlocking } from '../../settings.provider.js'; -import { Trans } from '../../../../../shared/components/TranslationsProvider.js'; -import { getLocalizedNumberFormatter } from '../../../../../shared/utils.js'; -import { useLocale } from '../../../../../shared/components/EnvironmentProvider.js'; +import { InfoIcon, NewBadgeIcon } from '../../components/Icons.js'; /** * @import enStrings from "../strings.json" @@ -20,33 +16,33 @@ import { useLocale } from '../../../../../shared/components/EnvironmentProvider. * @param {boolean} props.canExpand * @param {() => void} props.onToggle * @param {import('preact').ComponentProps<'button'>} [props.buttonAttrs] + * @param {import("@preact/signals").Signal} props.totalCookiePopUpsBlockedSignal */ -export function ProtectionsHeading({ expansion, canExpand, blockedCountSignal, onToggle, buttonAttrs = {} }) { +export function ProtectionsHeading({ expansion, canExpand, blockedCountSignal, onToggle, buttonAttrs = {}, totalCookiePopUpsBlockedSignal }) { const { t } = useTypedTranslationWith(/** @type {Strings} */ ({})); - const locale = useLocale(); - const [formatter] = useState(() => getLocalizedNumberFormatter(locale)); - const adBlocking = useAdBlocking(); - const blockedCount = blockedCountSignal.value; - const none = blockedCount === 0; - const some = blockedCount > 0; - const alltime = formatter.format(blockedCount); + const totalTrackersBlocked = blockedCountSignal.value; + const totalCookiePopUpsBlocked = totalCookiePopUpsBlockedSignal.value; - let alltimeTitle; - if (blockedCount === 1) { - alltimeTitle = adBlocking ? t('stats_countBlockedAdsAndTrackersSingular') : t('stats_countBlockedSingular'); - } else { - alltimeTitle = adBlocking - ? t('stats_countBlockedAdsAndTrackersPlural', { count: alltime }) - : t('stats_countBlockedPlural', { count: alltime }); - } + // @todo jingram get these values from native + const isCpmEnabled = true; // Is Cookie pop-up protection in app + const shouldShowCookiePopUpsBlocked = true; // from ProtectionsConfig + + const trackersBlockedHeading = totalTrackersBlocked === 1 + ? t('stats_countBlockedSingular') + : t('stats_countBlockedPlural') + + const cookiePopUpsBlockedHeading = totalCookiePopUpsBlocked === 1 + ? t('stats_totalCookiePopUpsBlockedSingular') + : t('stats_totalCookiePopUpsBlockedPlural') return (
-
+
Privacy Shield

{t('protections_menuTitle')}

+ {canExpand && ( )}
-
- {none &&

{t('protections_noRecent')}

} - {some && ( +
+ {/* Total Trackers Blocked */} +
+ {totalTrackersBlocked === 0 && ( +

{t('protections_noRecent')}

+ )} + {totalTrackersBlocked > 0 && (

- {' '} - + {totalTrackersBlocked} + {trackersBlockedHeading}

+ )} +
+ + {/* Total Cookie Pop-Ups Blocked */} + {/* Rules: Display CPM stats when Cookie Pop-Up Protection is + enabled AND both `totalTrackersBlocked` and + `totalCookiePopUpsBlocked` are at least 1 */} + {(shouldShowCookiePopUpsBlocked && isCpmEnabled && totalTrackersBlocked > 0 && totalCookiePopUpsBlocked > 0) && ( +
+

+ {totalCookiePopUpsBlocked} + {cookiePopUpsBlockedHeading} +

+ +
)} -

{t('stats_feedCountBlockedPeriod')}

); diff --git a/special-pages/pages/new-tab/app/protections/components/ProtectionsProvider.js b/special-pages/pages/new-tab/app/protections/components/ProtectionsProvider.js index 1569ebfec3..1d13ebd06e 100644 --- a/special-pages/pages/new-tab/app/protections/components/ProtectionsProvider.js +++ b/special-pages/pages/new-tab/app/protections/components/ProtectionsProvider.js @@ -104,3 +104,20 @@ export function useBlockedCount(initial) { }); return signal; } + +/** + * @param {number} initial + * @return {import("@preact/signals").Signal} + */ +export function useCookiePopUpsBlockedCount(initial) { + const service = useService(); + const signal = useSignal(initial); + + useSignalEffect(() => { + return service.current?.onData((evt) => { + signal.value = evt.data.totalCookiePopUpsBlocked; + }); + }); + + return signal; +} diff --git a/special-pages/pages/new-tab/app/protections/mocks/protections.mocks.js b/special-pages/pages/new-tab/app/protections/mocks/protections.mocks.js index 19ebdfde9e..5b503ad379 100644 --- a/special-pages/pages/new-tab/app/protections/mocks/protections.mocks.js +++ b/special-pages/pages/new-tab/app/protections/mocks/protections.mocks.js @@ -5,11 +5,14 @@ export const protectionsMocks = { empty: { totalCount: 0, + totalCookiePopUpsBlocked: 0, }, few: { totalCount: 86, + totalCookiePopUpsBlocked: 23, }, many: { totalCount: 1_000_020, + totalCookiePopUpsBlocked: 5_432, }, }; diff --git a/special-pages/pages/new-tab/app/protections/protections.md b/special-pages/pages/new-tab/app/protections/protections.md index d542032611..b1a5d70863 100644 --- a/special-pages/pages/new-tab/app/protections/protections.md +++ b/special-pages/pages/new-tab/app/protections/protections.md @@ -29,7 +29,8 @@ title: Protections Report - returns {@link "NewTab Messages".ProtectionsData} ```json { - "totalCount": 84 + "totalCount": 84, + "totalCookiePopUpsBlocked": 23 } ``` diff --git a/special-pages/pages/new-tab/app/styles/ntp-theme.css b/special-pages/pages/new-tab/app/styles/ntp-theme.css index 02b89807d4..42d98c5ec2 100644 --- a/special-pages/pages/new-tab/app/styles/ntp-theme.css +++ b/special-pages/pages/new-tab/app/styles/ntp-theme.css @@ -49,8 +49,10 @@ body { --ntp-surface-border-color: var(--color-black-at-9); --ntp-text-normal: var(--color-black-at-84); --ntp-text-muted: var(--color-black-at-60); + --ntp-protections-text-muted: var(--color-black-at-66); --ntp-text-on-primary: var(--color-white-at-84); --ntp-color-primary: var(--ddg-color-primary); + --ntp-widget-expander-bg: rgba(31, 31, 31, 0.09); --ntp-focus-outline-color: black; --focus-ring: 0px 0px 0px 1px var(--color-white), 0px 0px 0px 3px var(--ntp-focus-outline-color); --focus-ring-thin: 0px 0px 0px 1px var(--ntp-focus-outline-color), 0px 0px 0px 1px var(--color-white); @@ -86,8 +88,10 @@ body { --ntp-surface-border-color: var(--color-white-at-12); --ntp-text-normal: var(--color-white-at-84); --ntp-text-muted: var(--color-white-at-60); + --ntp-protections-text-muted: var(--color-white-at-66); --ntp-color-primary: var(--color-blue-30); --ntp-text-on-primary: var(--color-black-at-84); + --ntp-widget-expander-bg: rgba(249, 249, 249, 0.12); --ntp-focus-outline-color: white; --focus-ring: 0px 0px 0px 1px var(--ntp-focus-outline-color), 0px 0px 0px 3px var(--color-white); --focus-ring-thin: 0px 0px 0px 1px var(--color-white), 0px 0px 0px 1px var(--ntp-focus-outline-color); diff --git a/special-pages/pages/new-tab/messages/types/protections-data.json b/special-pages/pages/new-tab/messages/types/protections-data.json index 171858df36..aea1b7087e 100644 --- a/special-pages/pages/new-tab/messages/types/protections-data.json +++ b/special-pages/pages/new-tab/messages/types/protections-data.json @@ -10,6 +10,10 @@ "totalCount": { "description": "Total number of trackers or ads blocked since install", "type": "number" + }, + "totalCookiePopUpsBlocked": { + "description": "Total number of cookie pop-ups blocked since install", + "type": "number" } } } diff --git a/special-pages/pages/new-tab/public/locales/en/new-tab.json b/special-pages/pages/new-tab/public/locales/en/new-tab.json index 88ea78a70b..34e6e5ba96 100644 --- a/special-pages/pages/new-tab/public/locales/en/new-tab.json +++ b/special-pages/pages/new-tab/public/locales/en/new-tab.json @@ -78,11 +78,11 @@ "note": "Placeholder to indicate that no tracking activity was blocked in the last 7 days" }, "stats_countBlockedSingular": { - "title": "1 tracking attempt blocked", + "title": "Tracking attempt blocked", "note": "The main headline indicating that a single tracker was blocked" }, "stats_countBlockedPlural": { - "title": "{count} tracking attempts blocked", + "title": "Tracking attempts blocked", "note": "The main headline indicating that more than 1 attempt has been blocked. Eg: '2 tracking attempts blocked'" }, "stats_noActivityAdsAndTrackers": { @@ -94,13 +94,21 @@ "note": "Placeholder to indicate that no ads or tracking activity was blocked in the last 7 days" }, "stats_countBlockedAdsAndTrackersSingular": { - "title": "1 advertising & tracking attempt blocked", + "title": "advertising & tracking attempt blocked", "note": "The main headline indicating that a single ad or tracking attempt was blocked" }, "stats_countBlockedAdsAndTrackersPlural": { - "title": "{count} advertising & tracking attempts blocked", + "title": "advertising & tracking attempts blocked", "note": "The main headline indicating that more than 1 ad or tracking attempt has been blocked. Eg: '2 advertising & tracking attempts blocked" }, + "stats_totalCookiePopUpsBlockedSingular": { + "title": "Cookie pop-up blocked", + "note": "The heading indicating that a single cookie pop-up was handled by the CPM" + }, + "stats_totalCookiePopUpsBlockedPlural": { + "title": "Cookie pop-ups blocked", + "note": "The heading indicating multiple cookie pop-ups were handled by the CPM" + }, "stats_feedCountBlockedSingular": { "title": "1 attempt blocked by DuckDuckGo in the last 7 days", "note": "A summary description of how many tracking attempts where blocked, when only one exists." @@ -410,8 +418,12 @@ "note": "Placeholder message indicating that no trackers are blocked" }, "activity_countBlockedPlural": { - "title": "{count} tracking attempts blocked", - "note": "The main headline indicating that more than 1 attempt has been blocked. Eg: '2 tracking attempts blocked'" + "title": "{tickMark} {count} Tracking attempts blocked", + "note": "The main headline indicating that more than 1 attempt has been blocked. Eg: '2 Tracking attempts blocked'" + }, + "activity_countBlockedSingular": { + "title": "{tickMark} {count} Tracking attempt blocked", + "note": "The main headline indicating that 1 attempt has been blocked. Eg: '1 Tracking attempt blocked'" }, "activity_noRecentAdsAndTrackers_subtitle": { "title": "Recently visited sites will appear here. Keep browsing to see how many ads and trackers we block.", diff --git a/special-pages/pages/new-tab/types/new-tab.ts b/special-pages/pages/new-tab/types/new-tab.ts index 560fc8bd02..8f8a9d4deb 100644 --- a/special-pages/pages/new-tab/types/new-tab.ts +++ b/special-pages/pages/new-tab/types/new-tab.ts @@ -968,6 +968,10 @@ export interface ProtectionsData { * Total number of trackers or ads blocked since install */ totalCount: number; + /** + * Total number of cookie pop-ups blocked since install + */ + totalCookiePopUpsBlocked: number; } /** * Generated from @see "../messages/rmf_getData.request.json" @@ -1230,4 +1234,4 @@ declare module "../src/index.js" { request: import("@duckduckgo/messaging/lib/shared-types").MessagingBase['request'], subscribe: import("@duckduckgo/messaging/lib/shared-types").MessagingBase['subscribe'] } -} \ No newline at end of file +} diff --git a/special-pages/shared/styles/variables.css b/special-pages/shared/styles/variables.css index da3a306280..62d5ebe2d4 100644 --- a/special-pages/shared/styles/variables.css +++ b/special-pages/shared/styles/variables.css @@ -90,6 +90,7 @@ --color-black-at-48: rgba(0, 0, 0, 0.48); --color-black-at-50: rgba(0, 0, 0, 0.5); --color-black-at-60: rgba(0, 0, 0, 0.6); + --color-black-at-66: rgba(0, 0, 0, 0.66); --color-black-at-72: rgba(0, 0, 0, 0.72); --color-black-at-80: rgba(0, 0, 0, 0.8); --color-black-at-84: rgba(0, 0, 0, 0.84); @@ -110,6 +111,7 @@ --color-white-at-42: rgba(255, 255, 255, 0.42); --color-white-at-50: rgba(255, 255, 255, 0.5); --color-white-at-60: rgba(255, 255, 255, 0.6); + --color-white-at-66: rgba(255, 255, 255, 0.66); --color-white-at-70: rgba(255, 255, 255, 0.7); --color-white-at-80: rgba(255, 255, 255, 0.8); --color-white-at-84: rgba(255, 255, 255, 0.84); From 46e532150ca84de9a155954c4e3abf8588cdc3ce Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Fri, 24 Oct 2025 13:44:40 -0400 Subject: [PATCH 02/17] WIP on activity details @todo wire up real data --- .../app/activity/components/Activity.js | 34 +++++++------- .../activity/components/Activity.module.css | 18 +++----- .../pages/new-tab/app/activity/strings.json | 12 ++++- .../pages/new-tab/app/components/Icons.js | 17 +++++++ .../app/components/TickPill/TickPill.js | 24 ++++++++++ .../components/TickPill/TickPill.module.css | 45 +++++++++++++++++++ .../new-tab/public/locales/en/new-tab.json | 12 +++-- 7 files changed, 128 insertions(+), 34 deletions(-) create mode 100644 special-pages/pages/new-tab/app/components/TickPill/TickPill.js create mode 100644 special-pages/pages/new-tab/app/components/TickPill/TickPill.module.css diff --git a/special-pages/pages/new-tab/app/activity/components/Activity.js b/special-pages/pages/new-tab/app/activity/components/Activity.js index 060090dfd4..6bcb38f9fa 100644 --- a/special-pages/pages/new-tab/app/activity/components/Activity.js +++ b/special-pages/pages/new-tab/app/activity/components/Activity.js @@ -6,7 +6,6 @@ import { ActivityContext, ActivityServiceContext } from '../ActivityProvider.js' import { useTypedTranslationWith } from '../../types.js'; import { useOnMiddleClick } from '../../utils.js'; import { useAdBlocking, useBatchedActivityApi, usePlatformName } from '../../settings.provider.js'; -import { CompanyIcon } from '../../components/CompanyIcon.js'; import { Trans } from '../../../../../shared/components/TranslationsProvider.js'; import { ActivityItem } from './ActivityItem.js'; import { ActivityBurningSignalContext, BurnProvider } from '../../burning/BurnProvider.js'; @@ -18,6 +17,7 @@ import { HistoryItems } from './HistoryItems.js'; import { NormalizedDataContext, SignalStateProvider } from '../NormalizeDataProvider.js'; import { ActivityInteractionsContext } from '../../burning/ActivityInteractionsContext.js'; import { ProtectionsEmpty } from '../../protections/components/Protections.js'; +import { TickPill } from '../../components/TickPill/TickPill'; /** * @import enStrings from "../strings.json" @@ -181,13 +181,12 @@ function TrackerStatus({ id, trackersFound }) { const { t } = useTypedTranslationWith(/** @type {enStrings} */ ({})); const { activity } = useContext(NormalizedDataContext); const status = useComputed(() => activity.value.trackingStatus[id]); - const other = status.value.trackerCompanies.slice(DDG_MAX_TRACKER_ICONS - 1); - const companyIconsMax = other.length === 0 ? DDG_MAX_TRACKER_ICONS : DDG_MAX_TRACKER_ICONS - 1; + // @todo jingram add `cookiePopUpBlocked` + const {totalCount, trackerCompanies} = status.value; + const cookiePopUpBlocked = true; + const other = trackerCompanies.slice(DDG_MAX_TRACKER_ICONS - 1); const adBlocking = useAdBlocking(); - const icons = status.value.trackerCompanies.slice(0, companyIconsMax).map((item, _index) => { - return ; - }); let otherIcon = null; if (other.length > 0) { @@ -199,7 +198,7 @@ function TrackerStatus({ id, trackersFound }) { ); } - if (status.value.totalCount === 0) { + if (totalCount === 0) { let text; if (trackersFound) { text = adBlocking ? t('activity_no_adsAndTrackers_blocked') : t('activity_no_trackers_blocked'); @@ -208,23 +207,26 @@ function TrackerStatus({ id, trackersFound }) { } return (

- {text} +

); } return (
-
- {icons} - {otherIcon} -
- {adBlocking ? ( - - ) : ( - + {totalCount > 0 && ( + )} + {cookiePopUpBlocked && }
); diff --git a/special-pages/pages/new-tab/app/activity/components/Activity.module.css b/special-pages/pages/new-tab/app/activity/components/Activity.module.css index efe08990e0..c01d39a291 100644 --- a/special-pages/pages/new-tab/app/activity/components/Activity.module.css +++ b/special-pages/pages/new-tab/app/activity/components/Activity.module.css @@ -77,9 +77,7 @@ background: var(--color-black-at-12); transition: transform .2s; - border: 0.5px solid rgba(0, 0, 0, 0.09); background: rgba(255, 255, 255, 0.30); - box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.12), 0px 0px 1.5px 0px rgba(0, 0, 0, 0.16); [data-theme="dark"] & { border: 0.5px solid rgba(255, 255, 255, 0.09); @@ -198,18 +196,14 @@ padding-left: 1px; /* visual alignment */ } -.companiesIcons { - display: flex; - gap: 3px; - > * { - flex-shrink: 0; - min-width: 0; - } +.companiesText { + & div:first-of-type { + margin-bottom: 6px; + } } -.companiesText {} .history { - margin-top: 10px; + margin-top: 8px; } .historyItem { display: flex; @@ -293,4 +287,4 @@ transform: rotate(180deg) } } -} \ No newline at end of file +} diff --git a/special-pages/pages/new-tab/app/activity/strings.json b/special-pages/pages/new-tab/app/activity/strings.json index c8a78a5d81..fb603297ea 100644 --- a/special-pages/pages/new-tab/app/activity/strings.json +++ b/special-pages/pages/new-tab/app/activity/strings.json @@ -16,8 +16,16 @@ "note": "Placeholder message indicating that no trackers are blocked" }, "activity_countBlockedPlural": { - "title": "{count} tracking attempts blocked", - "note": "The main headline indicating that more than 1 attempt has been blocked. Eg: '2 tracking attempts blocked'" + "title": "{count} Tracking attempts blocked", + "note": "Pill text indicating that more than 1 attempt has been blocked. Eg: '2 Tracking attempts blocked'" + }, + "activity_countBlockedSingular": { + "title": "{count} Tracking attempt blocked", + "note": "Pill text indicating that 1 attempt has been blocked. Eg: '1 Tracking attempt blocked'" + }, + "activity_cookiePopUpBlocked": { + "title": "Cookie pop-up blocked", + "note": "Pill text indicating that we have blocked cookie pop-ups" }, "activity_noRecentAdsAndTrackers_subtitle": { "title": "Recently visited sites will appear here. Keep browsing to see how many ads and trackers we block.", diff --git a/special-pages/pages/new-tab/app/components/Icons.js b/special-pages/pages/new-tab/app/components/Icons.js index b1ed526ed0..b59eff99f3 100644 --- a/special-pages/pages/new-tab/app/components/Icons.js +++ b/special-pages/pages/new-tab/app/components/Icons.js @@ -634,3 +634,20 @@ export function InfoIcon(props) { ); } + +/** + * From https://dub.duckduckgo.com/duckduckgo/Icons/blob/Main/Glyphs/16px/Check-16.svg + * @param {import('preact').JSX.SVGAttributes} props + */ +export function Check(props) { + return ( + + + + ); +} diff --git a/special-pages/pages/new-tab/app/components/TickPill/TickPill.js b/special-pages/pages/new-tab/app/components/TickPill/TickPill.js new file mode 100644 index 0000000000..5c62012a00 --- /dev/null +++ b/special-pages/pages/new-tab/app/components/TickPill/TickPill.js @@ -0,0 +1,24 @@ +import { h } from 'preact'; +import { Check } from '../Icons.js'; +import cn from 'classnames'; +import styles from './TickPill.module.css'; + +/** + * A pill-shaped component displaying a checkmark with text + * @param {Object} props + * @param {string} props.text - The text to display next to the checkmark + * @param {string} [props.className] - Additional CSS classes + * @param {bool} [props.displayTick] - Display the tick or not + */ +export function TickPill({ text, className, displayTick = true }) { + return ( +
+ {displayTick && ( + + + + )} + {text} +
+ ); +} diff --git a/special-pages/pages/new-tab/app/components/TickPill/TickPill.module.css b/special-pages/pages/new-tab/app/components/TickPill/TickPill.module.css new file mode 100644 index 0000000000..1ef1dd7490 --- /dev/null +++ b/special-pages/pages/new-tab/app/components/TickPill/TickPill.module.css @@ -0,0 +1,45 @@ +.tickPill { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 10px; + border-radius: 100px; + background-color: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.12); + height: 20px; + width: fit-content; +} + +.iconWrapper { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.iconWrapper svg { + width: 12px; + height: 12px; +} + +.text { + font-size: 11px; + font-weight: 400; + line-height: 16px; + color: rgba(255, 255, 255, 0.84); + white-space: nowrap; +} + +/* Light mode styles */ +[data-theme="light"] .tickPill { + background-color: rgba(0, 0, 0, 0.04); + border: 1px solid rgba(0, 0, 0, 0.12); +} + +[data-theme="light"] .text { + color: rgba(0, 0, 0, 0.84); +} + +[data-theme="light"] .iconWrapper svg path { + fill: rgba(0, 0, 0, 0.84); +} diff --git a/special-pages/pages/new-tab/public/locales/en/new-tab.json b/special-pages/pages/new-tab/public/locales/en/new-tab.json index 34e6e5ba96..de720ee4d0 100644 --- a/special-pages/pages/new-tab/public/locales/en/new-tab.json +++ b/special-pages/pages/new-tab/public/locales/en/new-tab.json @@ -418,12 +418,16 @@ "note": "Placeholder message indicating that no trackers are blocked" }, "activity_countBlockedPlural": { - "title": "{tickMark} {count} Tracking attempts blocked", - "note": "The main headline indicating that more than 1 attempt has been blocked. Eg: '2 Tracking attempts blocked'" + "title": "{count} Tracking attempts blocked", + "note": "Pill text indicating that more than 1 attempt has been blocked. Eg: '2 Tracking attempts blocked'" }, "activity_countBlockedSingular": { - "title": "{tickMark} {count} Tracking attempt blocked", - "note": "The main headline indicating that 1 attempt has been blocked. Eg: '1 Tracking attempt blocked'" + "title": "{count} Tracking attempt blocked", + "note": "Pill text indicating that 1 attempt has been blocked. Eg: '1 Tracking attempt blocked'" + }, + "activity_cookiePopUpBlocked": { + "title": "Cookie pop-up blocked", + "note": "Pill text indicating that we have blocked cookie pop-ups" }, "activity_noRecentAdsAndTrackers_subtitle": { "title": "Recently visited sites will appear here. Keep browsing to see how many ads and trackers we block.", From 75cac2c9f0ffe406ebf6ac1d764d8f25a4e28a7f Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Fri, 24 Oct 2025 18:22:26 -0400 Subject: [PATCH 03/17] Add cookiePopUpBlocked to TrackingStatus --- .../pages/new-tab/app/activity/components/Activity.js | 6 +----- .../new-tab/app/activity/components/Activity.module.css | 2 +- special-pages/pages/new-tab/messages/types/activity.json | 6 +++++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/special-pages/pages/new-tab/app/activity/components/Activity.js b/special-pages/pages/new-tab/app/activity/components/Activity.js index 6bcb38f9fa..6728c358d9 100644 --- a/special-pages/pages/new-tab/app/activity/components/Activity.js +++ b/special-pages/pages/new-tab/app/activity/components/Activity.js @@ -6,7 +6,6 @@ import { ActivityContext, ActivityServiceContext } from '../ActivityProvider.js' import { useTypedTranslationWith } from '../../types.js'; import { useOnMiddleClick } from '../../utils.js'; import { useAdBlocking, useBatchedActivityApi, usePlatformName } from '../../settings.provider.js'; -import { Trans } from '../../../../../shared/components/TranslationsProvider.js'; import { ActivityItem } from './ActivityItem.js'; import { ActivityBurningSignalContext, BurnProvider } from '../../burning/BurnProvider.js'; import { useEnv } from '../../../../../shared/components/EnvironmentProvider.js'; @@ -181,13 +180,10 @@ function TrackerStatus({ id, trackersFound }) { const { t } = useTypedTranslationWith(/** @type {enStrings} */ ({})); const { activity } = useContext(NormalizedDataContext); const status = useComputed(() => activity.value.trackingStatus[id]); - // @todo jingram add `cookiePopUpBlocked` - const {totalCount, trackerCompanies} = status.value; - const cookiePopUpBlocked = true; + const {totalCount, trackerCompanies, cookiePopUpBlocked} = status.value; const other = trackerCompanies.slice(DDG_MAX_TRACKER_ICONS - 1); const adBlocking = useAdBlocking(); - let otherIcon = null; if (other.length > 0) { const title = other.map((item) => item.displayName).join('\n'); diff --git a/special-pages/pages/new-tab/app/activity/components/Activity.module.css b/special-pages/pages/new-tab/app/activity/components/Activity.module.css index c01d39a291..ea2336cc26 100644 --- a/special-pages/pages/new-tab/app/activity/components/Activity.module.css +++ b/special-pages/pages/new-tab/app/activity/components/Activity.module.css @@ -197,7 +197,7 @@ } .companiesText { - & div:first-of-type { + & div:first-of-type:not(:only-child) { margin-bottom: 6px; } } diff --git a/special-pages/pages/new-tab/messages/types/activity.json b/special-pages/pages/new-tab/messages/types/activity.json index 34382b3eb8..2953c0d558 100644 --- a/special-pages/pages/new-tab/messages/types/activity.json +++ b/special-pages/pages/new-tab/messages/types/activity.json @@ -45,6 +45,10 @@ }, "favorite": { "type": "boolean" + }, + "cookiePopUpBlocked": { + "type": ["boolean", "null"], + "description": "A cookie pop-up has been blocked for the specific domain" } }, "required": ["etldPlusOne", "title", "url", "trackingStatus", "trackersFound", "history", "favorite", "favicon"] @@ -91,4 +95,4 @@ "required": ["title", "url", "relativeTime"] } } -} \ No newline at end of file +} From ffe9f6429574292b43738a7f0442dc602a0eafa6 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Fri, 24 Oct 2025 18:47:02 -0400 Subject: [PATCH 04/17] Implement tooltip Adjust animation --- .../pages/new-tab/app/components/Icons.js | 9 ++-- .../new-tab/app/components/Icons.module.css | 9 ++++ .../new-tab/app/components/Tooltip/Tooltip.js | 28 ++++++++++++ .../app/components/Tooltip/Tooltip.module.css | 44 +++++++++++++++++++ .../new-tab/app/privacy-stats/strings.json | 4 ++ .../components/ProtectionsHeading.js | 18 +++++--- .../new-tab/public/locales/en/new-tab.json | 4 ++ 7 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 special-pages/pages/new-tab/app/components/Tooltip/Tooltip.js create mode 100644 special-pages/pages/new-tab/app/components/Tooltip/Tooltip.module.css diff --git a/special-pages/pages/new-tab/app/components/Icons.js b/special-pages/pages/new-tab/app/components/Icons.js index b59eff99f3..225de53e82 100644 --- a/special-pages/pages/new-tab/app/components/Icons.js +++ b/special-pages/pages/new-tab/app/components/Icons.js @@ -621,15 +621,14 @@ export function NewBadgeIcon(props) { */ export function InfoIcon(props) { return ( - - - + + + ); diff --git a/special-pages/pages/new-tab/app/components/Icons.module.css b/special-pages/pages/new-tab/app/components/Icons.module.css index 1bbb2bb812..208defdf44 100644 --- a/special-pages/pages/new-tab/app/components/Icons.module.css +++ b/special-pages/pages/new-tab/app/components/Icons.module.css @@ -18,4 +18,13 @@ } } +/* InfoIcon styles */ +:global(.info-icon-fill) { + fill: black; + fill-opacity: 0.36; +} +[data-theme=dark] :global(.info-icon-fill) { + fill: white; + fill-opacity: 0.24; +} diff --git a/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.js b/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.js new file mode 100644 index 0000000000..b28eaf3899 --- /dev/null +++ b/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.js @@ -0,0 +1,28 @@ +import { h } from 'preact'; +import { useState } from 'preact/hooks'; +import styles from './Tooltip.module.css'; +import cn from 'classnames'; + +/** + * A tooltip component that appears on hover + * @param {Object} props + * @param {import('preact').ComponentChildren} props.children - The element that triggers the tooltip + * @param {string} props.content - The tooltip content text + * @param {string} [props.className] - Additional CSS classes for the trigger element + */ +export function Tooltip({ children, content, className }) { + const [isVisible, setIsVisible] = useState(false); + + return ( +
setIsVisible(true)} + onMouseLeave={() => setIsVisible(false)} + > + {children} + {isVisible && ( + + ); +} diff --git a/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.module.css b/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.module.css new file mode 100644 index 0000000000..32607e0a31 --- /dev/null +++ b/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.module.css @@ -0,0 +1,44 @@ +.tooltipContainer { + position: relative; + display: inline-flex; + align-items: center; +} + +.tooltip { + position: absolute; + bottom: -20px; + left: calc(100% + 8px); + padding: 8px 16px; + border-radius: 12px; + background-color: rgba(255, 255, 255, 0.98); + box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.20); + font-size: 12px; + line-height: 15px; + color: var(--color-black-at-96); + white-space: normal; + width: 236px; + z-index: 1000; + animation: tooltipFadeIn 0.7s ease-out; + + & span { + display: block; + margin-top: 22px; + } +} + +/* Dark mode styles */ +[data-theme="dark"] .tooltip { + background-color: rgb(71, 71, 71); + color: var(--color-white-at-96); +} + +@keyframes tooltipFadeIn { + from { + opacity: 0; + transform: translateX(-10px); + } + to { + opacity: 1; + transform: translateX(0); + } +} diff --git a/special-pages/pages/new-tab/app/privacy-stats/strings.json b/special-pages/pages/new-tab/app/privacy-stats/strings.json index 6b3063e8dc..ce8f3d00c5 100644 --- a/special-pages/pages/new-tab/app/privacy-stats/strings.json +++ b/special-pages/pages/new-tab/app/privacy-stats/strings.json @@ -47,6 +47,10 @@ "title": "Cookie pop-ups blocked", "note": "The heading indicating multiple cookie pop-ups were handled by the CPM" }, + "stats_protectionsReportInfo": { + "title": "Displays tracking attempts blocked in the last 7 days and the number of cookie pop-ups blocked since you started using the browser. You can reset these stats using the Fire Button.", + "note": "Text explaining how to reset the protections stats" + }, "stats_feedCountBlockedSingular": { "title": "1 attempt blocked by DuckDuckGo in the last 7 days", "note": "A summary description of how many tracking attempts where blocked, when only one exists." diff --git a/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.js b/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.js index 717f21b72c..a888ad64b6 100644 --- a/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.js +++ b/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.js @@ -4,6 +4,7 @@ import { ShowHideButtonCircle } from '../../components/ShowHideButton.jsx'; import cn from 'classnames'; import { h } from 'preact'; import { InfoIcon, NewBadgeIcon } from '../../components/Icons.js'; +import { Tooltip } from '../../components/Tooltip/Tooltip.js'; /** * @import enStrings from "../strings.json" @@ -21,11 +22,12 @@ import { InfoIcon, NewBadgeIcon } from '../../components/Icons.js'; export function ProtectionsHeading({ expansion, canExpand, blockedCountSignal, onToggle, buttonAttrs = {}, totalCookiePopUpsBlockedSignal }) { const { t } = useTypedTranslationWith(/** @type {Strings} */ ({})); const totalTrackersBlocked = blockedCountSignal.value; - const totalCookiePopUpsBlocked = totalCookiePopUpsBlockedSignal.value; + const totalCookiePopUpsBlocked = totalCookiePopUpsBlockedSignal.value ?? 0; - // @todo jingram get these values from native - const isCpmEnabled = true; // Is Cookie pop-up protection in app - const shouldShowCookiePopUpsBlocked = true; // from ProtectionsConfig + // Native does not tell the FE if cookie pop up protection is enabled but + // we can derive this from the value of `totalCookiePopUpsBlocked` in the + // `ProtectionsService` + const isCpmEnabled = totalCookiePopUpsBlockedSignal.value !== null; const trackersBlockedHeading = totalTrackersBlocked === 1 ? t('stats_countBlockedSingular') @@ -42,7 +44,11 @@ export function ProtectionsHeading({ expansion, canExpand, blockedCountSignal, o Privacy Shield

{t('protections_menuTitle')}

- + + + + + {canExpand && ( 0 && totalCookiePopUpsBlocked > 0) && ( + {(isCpmEnabled && totalTrackersBlocked > 0 && totalCookiePopUpsBlocked > 0) && (

{totalCookiePopUpsBlocked} diff --git a/special-pages/pages/new-tab/public/locales/en/new-tab.json b/special-pages/pages/new-tab/public/locales/en/new-tab.json index de720ee4d0..184911bc9d 100644 --- a/special-pages/pages/new-tab/public/locales/en/new-tab.json +++ b/special-pages/pages/new-tab/public/locales/en/new-tab.json @@ -109,6 +109,10 @@ "title": "Cookie pop-ups blocked", "note": "The heading indicating multiple cookie pop-ups were handled by the CPM" }, + "stats_protectionsReportInfo": { + "title": "Displays tracking attempts blocked in the last 7 days and the number of cookie pop-ups blocked since you started using the browser. You can reset these stats using the Fire Button.", + "note": "Text explaining how to reset the protections stats" + }, "stats_feedCountBlockedSingular": { "title": "1 attempt blocked by DuckDuckGo in the last 7 days", "note": "A summary description of how many tracking attempts where blocked, when only one exists." From 14769e63426b3f7513e3b1c3dab6617ae4d39e07 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Mon, 27 Oct 2025 19:23:44 -0400 Subject: [PATCH 05/17] Update types --- .../pages/new-tab/messages/types/activity.json | 11 +++++++++-- .../new-tab/messages/types/protections-data.json | 9 ++++++++- special-pages/pages/new-tab/types/new-tab.ts | 8 ++++++-- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/special-pages/pages/new-tab/messages/types/activity.json b/special-pages/pages/new-tab/messages/types/activity.json index 2953c0d558..6b34a14aa2 100644 --- a/special-pages/pages/new-tab/messages/types/activity.json +++ b/special-pages/pages/new-tab/messages/types/activity.json @@ -47,8 +47,15 @@ "type": "boolean" }, "cookiePopUpBlocked": { - "type": ["boolean", "null"], - "description": "A cookie pop-up has been blocked for the specific domain" + "description": "A cookie pop-up has been blocked for the specific domain", + "oneOf": [ + { + "type": "null" + }, + { + "type": "boolean" + } + ] } }, "required": ["etldPlusOne", "title", "url", "trackingStatus", "trackersFound", "history", "favorite", "favicon"] diff --git a/special-pages/pages/new-tab/messages/types/protections-data.json b/special-pages/pages/new-tab/messages/types/protections-data.json index aea1b7087e..c1f7b4010d 100644 --- a/special-pages/pages/new-tab/messages/types/protections-data.json +++ b/special-pages/pages/new-tab/messages/types/protections-data.json @@ -13,7 +13,14 @@ }, "totalCookiePopUpsBlocked": { "description": "Total number of cookie pop-ups blocked since install", - "type": "number" + "oneOf": [ + { + "type": "null" + }, + { + "type": "number" + } + ] } } } diff --git a/special-pages/pages/new-tab/types/new-tab.ts b/special-pages/pages/new-tab/types/new-tab.ts index 8f8a9d4deb..1755ba711b 100644 --- a/special-pages/pages/new-tab/types/new-tab.ts +++ b/special-pages/pages/new-tab/types/new-tab.ts @@ -745,6 +745,10 @@ export interface DomainActivity { trackersFound: boolean; history: HistoryEntry[]; favorite: boolean; + /** + * A cookie pop-up has been blocked for the specific domain + */ + cookiePopUpBlocked?: null | boolean; } export interface TrackingStatus { trackerCompanies: { @@ -971,7 +975,7 @@ export interface ProtectionsData { /** * Total number of cookie pop-ups blocked since install */ - totalCookiePopUpsBlocked: number; + totalCookiePopUpsBlocked?: null | number; } /** * Generated from @see "../messages/rmf_getData.request.json" @@ -1234,4 +1238,4 @@ declare module "../src/index.js" { request: import("@duckduckgo/messaging/lib/shared-types").MessagingBase['request'], subscribe: import("@duckduckgo/messaging/lib/shared-types").MessagingBase['subscribe'] } -} +} \ No newline at end of file From af54d6fd303e9005873e9c100e8acda1127164ee Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Mon, 27 Oct 2025 19:27:24 -0400 Subject: [PATCH 06/17] Wire up mock data and provide fallback for platforms not ready for update --- .../app/activity/NormalizeDataProvider.js | 6 + .../pages/new-tab/app/activity/activity.md | 5 +- .../app/activity/components/Activity.js | 189 +++++++++-- .../app/activity/components/ActivityItem.js | 42 +++ .../components/ActivityLegacy.module.css | 296 ++++++++++++++++++ .../activity/mocks/activity.mock-transport.js | 2 + .../app/activity/mocks/activity.mocks.js | 6 + .../pages/new-tab/app/activity/strings.json | 4 + .../components/PrivacyStatsLegacy.module.css | 168 ++++++++++ .../new-tab/app/privacy-stats/strings.json | 16 + .../app/protections/components/Protections.js | 33 +- .../components/ProtectionsConsumer.js | 5 +- .../components/ProtectionsHeading.js | 2 +- .../components/ProtectionsHeadingLegacy.js | 76 +++++ .../components/ProtectionsProvider.js | 5 +- .../mocks/protections.mock-transport.js | 9 + .../protections/mocks/protections.mocks.js | 2 +- .../new-tab/public/locales/en/new-tab.json | 20 ++ 18 files changed, 847 insertions(+), 39 deletions(-) create mode 100644 special-pages/pages/new-tab/app/activity/components/ActivityLegacy.module.css create mode 100644 special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStatsLegacy.module.css create mode 100644 special-pages/pages/new-tab/app/protections/components/ProtectionsHeadingLegacy.js diff --git a/special-pages/pages/new-tab/app/activity/NormalizeDataProvider.js b/special-pages/pages/new-tab/app/activity/NormalizeDataProvider.js index 80c516c43e..4bbc4508cb 100644 --- a/special-pages/pages/new-tab/app/activity/NormalizeDataProvider.js +++ b/special-pages/pages/new-tab/app/activity/NormalizeDataProvider.js @@ -36,6 +36,7 @@ import { ACTION_BURN } from '../burning/BurnProvider.js'; * @property {Record} favorites * @property {string[]} urls * @property {number} totalTrackers + * @property {DomainActivity['cookiePopUpBlocked']} cookiePopUpBlocked */ /** @@ -52,6 +53,7 @@ export function normalizeData(prev, incoming) { trackingStatus: {}, urls: [], totalTrackers: incoming.totalTrackers, + cookiePopUpBlocked: null, }; if (shallowDiffers(prev.urls, incoming.urls)) { @@ -64,6 +66,7 @@ export function normalizeData(prev, incoming) { const id = item.url; output.favorites[id] = item.favorite; + output.cookiePopUpBlocked = item.cookiePopUpBlocked; /** @type {Item} */ const next = { @@ -73,6 +76,7 @@ export function normalizeData(prev, incoming) { faviconMax: item.favicon?.maxAvailableSize ?? DDG_DEFAULT_ICON_SIZE, favoriteSrc: item.favicon?.src, trackersFound: item.trackersFound, + // cookiePopUpBlocked: item.cookiePopUpBlocked, }; const differs = shallowDiffers(next, prev.items[id] || {}); output.items[id] = differs ? next : prev.items[id] || {}; @@ -83,12 +87,14 @@ export function normalizeData(prev, incoming) { const prevItem = prev.trackingStatus[id] || { totalCount: 0, trackerCompanies: [], + cookiePopUpBlocked: null, }; const trackersDiffer = shallowDiffers(item.trackingStatus.trackerCompanies, prevItem.trackerCompanies); if (prevItem.totalCount !== item.trackingStatus.totalCount || trackersDiffer) { const next = { totalCount: item.trackingStatus.totalCount, trackerCompanies: [...item.trackingStatus.trackerCompanies], + cookiePopUpBlocked: item.cookiePopUpBlocked, }; output.trackingStatus[id] = next; } else { diff --git a/special-pages/pages/new-tab/app/activity/activity.md b/special-pages/pages/new-tab/app/activity/activity.md index 366d6ddad1..70a6198759 100644 --- a/special-pages/pages/new-tab/app/activity/activity.md +++ b/special-pages/pages/new-tab/app/activity/activity.md @@ -48,7 +48,8 @@ title: Activity "url": "https://youtube.com/watch?v=abc", "relativeTime": "Just now" } - ] + ], + "cookiePopUpBlocked": true, } ] } @@ -178,4 +179,4 @@ example payload without id (for example, on history items) ``` ### `activity_burnAnimationComplete` -- Sent when the burn animation completes \ No newline at end of file +- Sent when the burn animation completes diff --git a/special-pages/pages/new-tab/app/activity/components/Activity.js b/special-pages/pages/new-tab/app/activity/components/Activity.js index 6728c358d9..a0abb2f3b6 100644 --- a/special-pages/pages/new-tab/app/activity/components/Activity.js +++ b/special-pages/pages/new-tab/app/activity/components/Activity.js @@ -1,12 +1,17 @@ -import { h } from 'preact'; +import { h, Fragment } from 'preact'; import styles from './Activity.module.css'; +// @todo legacyProtections: `stylesLegacy` can be removed once all platforms +// are ready for the new Protections Report +import stylesLegacy from './ActivityLegacy.module.css'; import { useContext, useEffect, useRef } from 'preact/hooks'; import { memo } from 'preact/compat'; import { ActivityContext, ActivityServiceContext } from '../ActivityProvider.js'; import { useTypedTranslationWith } from '../../types.js'; import { useOnMiddleClick } from '../../utils.js'; import { useAdBlocking, useBatchedActivityApi, usePlatformName } from '../../settings.provider.js'; -import { ActivityItem } from './ActivityItem.js'; +import { CompanyIcon } from '../../components/CompanyIcon.js'; +import { Trans } from '../../../../../shared/components/TranslationsProvider.js'; +import { ActivityItem, ActivityItemLegacy } from './ActivityItem.js'; import { ActivityBurningSignalContext, BurnProvider } from '../../burning/BurnProvider.js'; import { useEnv } from '../../../../../shared/components/EnvironmentProvider.js'; import { useComputed } from '@preact/signals'; @@ -54,8 +59,9 @@ export function ActivityEmptyState() { * @param {object} props * @param {boolean} props.canBurn * @param {DocumentVisibilityState} props.visibility + * @param {boolean} props.shouldDisplayLegacyActivity */ -export function ActivityBody({ canBurn, visibility }) { +export function ActivityBody({ canBurn, visibility, shouldDisplayLegacyActivity }) { const { isReducedMotion } = useEnv(); const { keys } = useContext(NormalizedDataContext); const { burning, exiting } = useContext(ActivityBurningSignalContext); @@ -70,8 +76,33 @@ export function ActivityBody({ canBurn, visibility }) { return (
    {keys.value.map((id, _index) => { - if (canBurn && !isReducedMotion) return ; - return ; + if (canBurn && !isReducedMotion) { + return ( + + ); + } + + return ( + + ); })}
); @@ -110,16 +141,25 @@ const BurnableItem = memo( * @param {object} props * @param {string} props.id * @param {'visible' | 'hidden'} props.documentVisibility + * @param {boolean} props.shouldDisplayLegacyActivity */ - function BurnableItem({ id, documentVisibility }) { + function BurnableItem({ id, documentVisibility, shouldDisplayLegacyActivity }) { const { activity } = useContext(NormalizedDataContext); const item = useComputed(() => activity.value.items[id]); + if (!item.value) { return null; } + + // @todo legacyProtections: Once all platforms are ready for the new + // protections report we can use `ActivityItem` + const ActivityItemComponent = shouldDisplayLegacyActivity + ? ActivityItemLegacy + : ActivityItem; + return ( - - + {shouldDisplayLegacyActivity ? ( + // @todo legacyProtections: `TrackerStatusLegacy` and + // supporting prop can be removed once all platforms are + // ready for the new protections report + + ) : ( + + )} - + ); }, @@ -142,8 +195,9 @@ const RemovableItem = memo( * @param {string} props.id * @param {boolean} props.canBurn * @param {"visible" | "hidden"} props.documentVisibility + * @param {boolean} props.shouldDisplayLegacyActivity */ - function RemovableItem({ id, canBurn, documentVisibility }) { + function RemovableItem({ id, canBurn, documentVisibility, shouldDisplayLegacyActivity }) { const { activity } = useContext(NormalizedDataContext); const item = useComputed(() => activity.value.items[id]); if (!item.value) { @@ -153,8 +207,15 @@ const RemovableItem = memo(

); } + + // @todo legacyProtections: Once all platforms are ready for the new + // protections report we can use `ActivityItem` + const ActivityItemComponent = shouldDisplayLegacyActivity + ? ActivityItemLegacy + : ActivityItem; + return ( - - + {shouldDisplayLegacyActivity ? ( + // @todo legacyProtections: `TrackerStatusLegacy` and + // supporting prop can be removed once all platforms are + // ready for the new protections report + + ) : ( + + )} - + ); }, ); @@ -180,7 +254,8 @@ function TrackerStatus({ id, trackersFound }) { const { t } = useTypedTranslationWith(/** @type {enStrings} */ ({})); const { activity } = useContext(NormalizedDataContext); const status = useComputed(() => activity.value.trackingStatus[id]); - const {totalCount, trackerCompanies, cookiePopUpBlocked} = status.value; + const cookiePopUpBlocked = useComputed(() => activity.value.cookiePopUpBlocked); + const {totalCount, trackerCompanies} = status.value; const other = trackerCompanies.slice(DDG_MAX_TRACKER_ICONS - 1); const adBlocking = useAdBlocking(); @@ -195,12 +270,10 @@ function TrackerStatus({ id, trackersFound }) { } if (totalCount === 0) { - let text; - if (trackersFound) { - text = adBlocking ? t('activity_no_adsAndTrackers_blocked') : t('activity_no_trackers_blocked'); - } else { - text = adBlocking ? t('activity_no_adsAndTrackers') : t('activity_no_trackers'); - } + const text = trackersFound + ? t('activity_no_trackers_blocked') + : t('activity_no_trackers') + return (

@@ -228,6 +301,67 @@ function TrackerStatus({ id, trackersFound }) { ); } +// @todo legacyProtections: `TrackerStatusLegacy` can be removed once all +// platforms are ready for the new protections report + +/** + * @param {object} props + * @param {string} props.id + * @param {boolean} props.trackersFound + */ +function TrackerStatusLegacy({ id, trackersFound }) { + const { t } = useTypedTranslationWith(/** @type {enStrings} */ ({})); + const { activity } = useContext(NormalizedDataContext); + const status = useComputed(() => activity.value.trackingStatus[id]); + const other = status.value.trackerCompanies.slice(DDG_MAX_TRACKER_ICONS - 1); + const companyIconsMax = other.length === 0 ? DDG_MAX_TRACKER_ICONS : DDG_MAX_TRACKER_ICONS - 1; + const adBlocking = useAdBlocking(); + + const icons = status.value.trackerCompanies.slice(0, companyIconsMax).map((item, _index) => { + return ; + }); + + let otherIcon = null; + if (other.length > 0) { + const title = other.map((item) => item.displayName).join('\n'); + otherIcon = ( + + +{other.length} + + ); + } + + if (status.value.totalCount === 0) { + let text; + if (trackersFound) { + text = adBlocking ? t('activity_no_adsAndTrackers_blocked') : t('activity_no_trackers_blocked'); + } else { + text = adBlocking ? t('activity_no_adsAndTrackers') : t('activity_no_trackers'); + } + return ( +

+ {text} +

+ ); + } + + return ( +
+
+ {icons} + {otherIcon} +
+
+ {adBlocking ? ( + + ) : ( + + )} +
+
+ ); +} + /** * @param {object} props * @param {import("preact").ComponentChild} props.children @@ -261,8 +395,9 @@ export function ActivityConfigured({ children }) { * ``` * @param {object} props * @param {boolean} props.showBurnAnimation + * @param {boolean} props.shouldDisplayLegacyActivity */ -export function ActivityConsumer({ showBurnAnimation }) { +export function ActivityConsumer({ showBurnAnimation, shouldDisplayLegacyActivity }) { const { state } = useContext(ActivityContext); const service = useContext(ActivityServiceContext); const platformName = usePlatformName(); @@ -272,7 +407,11 @@ export function ActivityConsumer({ showBurnAnimation }) { return ( - + ); @@ -281,7 +420,11 @@ export function ActivityConsumer({ showBurnAnimation }) { - + diff --git a/special-pages/pages/new-tab/app/activity/components/ActivityItem.js b/special-pages/pages/new-tab/app/activity/components/ActivityItem.js index 34ba648d16..d95cd3fba7 100644 --- a/special-pages/pages/new-tab/app/activity/components/ActivityItem.js +++ b/special-pages/pages/new-tab/app/activity/components/ActivityItem.js @@ -2,6 +2,7 @@ import { h } from 'preact'; import { useTypedTranslationWith } from '../../types.js'; import cn from 'classnames'; import styles from './Activity.module.css'; +import stylesLegacy from './ActivityLegacy.module.css'; import { FaviconWithState } from '../../../../../shared/components/FaviconWithState.js'; import { ACTION_ADD_FAVORITE, ACTION_REMOVE, ACTION_REMOVE_FAVORITE } from '../constants.js'; import { Star, StarFilled } from '../../components/icons/Star.js'; @@ -55,6 +56,47 @@ export const ActivityItem = memo( }, ); +export const ActivityItemLegacy = memo( + /** + * @param {object} props + * @param {boolean} props.canBurn + * @param {"visible"|"hidden"} props.documentVisibility + * @param {import("preact").ComponentChild} props.children + * @param {string} props.title + * @param {string} props.url + * @param {string|null|undefined} props.favoriteSrc + * @param {number} props.faviconMax + * @param {string} props.etldPlusOne + */ + function ActivityItem({ canBurn, documentVisibility, title, url, favoriteSrc, faviconMax, etldPlusOne, children }) { + return ( +
  • + +
    {children}
    +
  • + ); + }, +); + /** * Renders a set of control buttons that handle actions related to favorites and burn/removal features. * diff --git a/special-pages/pages/new-tab/app/activity/components/ActivityLegacy.module.css b/special-pages/pages/new-tab/app/activity/components/ActivityLegacy.module.css new file mode 100644 index 0000000000..efe08990e0 --- /dev/null +++ b/special-pages/pages/new-tab/app/activity/components/ActivityLegacy.module.css @@ -0,0 +1,296 @@ +.root { + display: grid; +} + +.activity { + --favicon-width: 32px; + --heading-gap: 8px; + + + overflow: hidden; + width: calc(100% + 12px); + margin-left: -6px; + + &:not(:empty) { + margin-top: 24px; + } +} + +.block { + margin-top: 24px; +} + +.loader { + height: 10px; + border: 1px dotted black; + border-radius: 5px; + opacity: 0; +} + +.anim { + position: relative; + overflow: hidden; + border-radius: var(--border-radius-lg); + + [data-lottie-player] { + width: 100%; + height: auto; + object-fit: cover; + position: absolute; + bottom: -50%; + left: 0; + } +} + +.item { + padding-top: 8px; + padding-bottom: 8px; + padding-left: 6px; + padding-right: 6px; +} + +.burning { + > *:not([data-lottie-player]) { + transition: opacity .2s; + transition-delay: .3s; + opacity: 0; + } +} + +.heading { + display: flex; + gap: var(--heading-gap); + width: 100%; +} + +.favicon { + width: 32px; + height: 32px; + /* adding a margin to prevent needing an extra dom node for spacing */ + margin: 3px; + display: block; + backdrop-filter: blur(24px); + border-radius: var(--border-radius-sm); + flex-shrink: 0; + text-decoration: none; + position: relative; + background: var(--color-black-at-12); + transition: transform .2s; + + border: 0.5px solid rgba(0, 0, 0, 0.09); + background: rgba(255, 255, 255, 0.30); + box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.12), 0px 0px 1.5px 0px rgba(0, 0, 0, 0.16); + + [data-theme="dark"] & { + border: 0.5px solid rgba(255, 255, 255, 0.09); + background: rgba(0, 0, 0, 0.18); + box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.12), 0px 0px 1.5px 0px rgba(0, 0, 0, 0.16); + backdrop-filter: blur(24px); + } + + > *:first-child { + position: absolute; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); + } +} + +.title { + font-size: var(--title-3-em-font-size); + font-weight: var(--title-3-em-font-weight); + text-decoration: none; + color: var(--ntp-text-normal); + height: 35px; + display: flex; + align-items: center; + line-height: 1; + + /* Note: This is not a 1:1 value from figma, I reduced it for perfect visual alignment */ + gap: 4px; + min-width: 0; + + &:hover, &:focus-visible { + color: var(--ntp-color-primary); + .favicon { + transform: scale(1.08) + } + } +} + +.controls { + display: flex; + margin-left: auto; + flex-shrink: 0; + position: relative; + gap: 4px; + top: 4px; +} + +.icon { + width: 24px; + height: 24px; + position: relative; + border: none; + background: transparent; + padding: 0; + margin: 0; + color: var(--ntp-text-normal); + svg { + position: absolute; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); + } +} + +.controlIcon { + border-radius: 50%; + background-color: var(--color-black-at-3); + &:hover { + background-color: var(--color-black-at-6); + } + + [data-theme="dark"] & { + background-color: var(--color-white-at-6); + } + [data-theme="dark"] &:hover { + background-color: var(--color-white-at-9); + } + svg { + fill-opacity: 0.6; + } +} + +.disableWhenBusy { + [data-busy="true"] & { + cursor: not-allowed; + } +} + +.body { + padding-left: calc(var(--favicon-width) + var(--heading-gap)); + padding-right: calc(var(--favicon-width) + var(--heading-gap) * 2); + position: relative; +} + +.otherIcon { + width: 16px; + height: 16px; + border-radius: 50%; + font-weight: bold; + font-size: 0.5rem; + line-height: 16px; + color: var(--color-black-at-60); + background: var(--color-black-at-6); + text-align: center; + + [data-theme="dark"] & { + color: var(--color-white-at-50); + background: var(--color-white-at-9); + } +} + +.companiesIconRow { + display: flex; + align-items: center; + gap: 6px; + padding-left: 1px; /* visual alignment */ +} + +.companiesIcons { + display: flex; + gap: 3px; + > * { + flex-shrink: 0; + min-width: 0; + } +} +.companiesText {} + +.history { + margin-top: 10px; +} +.historyItem { + display: flex; + align-items: center; + width: 100%; + height: 16px; + + .historyItem { + margin-top: 5px; + } +} +.historyLink { + font-size: var(--small-label-font-size); + font-weight: var(--small-label-font-weight); + line-height: var(--small-label-line-height); + color: var(--ntp-text-normal); + text-decoration: none; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &:hover, &:focus-visible { + color: var(--ntp-color-primary) + } + + &:hover .time { + text-decoration: none; + display: inline-block; + } +} + +.time { + flex-shrink: 0; + margin-left: 8px; + color: var(--ntp-text-muted); + opacity: 0.6; + font-size: var(--small-label-font-size); + font-weight: var(--small-label-font-weight); + line-height: var(--small-label-line-height); +} + +.historyBtn { + width: 16px; + height: 16px; + flex-shrink: 0; + border: 0; + border-radius: 4px; + position: relative; + text-align: center; + padding: 0; + margin: 0; + margin-left: 8px; + background: transparent; + display: flex; + align-items: center; + justify-content: center; + color: var(--color-black-at-60); + + &:hover { + background-color: var(--color-black-at-6); + } + + [data-theme="dark"] & { + color: var(--color-white-at-60); + &:hover { + background-color: var(--color-white-at-6); + } + } + + svg { + display: inline-block; + width: 16px; + height: 16px; + position: relative; + top: 1px; + transform: rotate(0); + } + + &[data-action="hide"] { + svg { + transform: rotate(180deg) + } + } +} \ No newline at end of file diff --git a/special-pages/pages/new-tab/app/activity/mocks/activity.mock-transport.js b/special-pages/pages/new-tab/app/activity/mocks/activity.mock-transport.js index 2bf90c018f..b09ee22f2c 100644 --- a/special-pages/pages/new-tab/app/activity/mocks/activity.mock-transport.js +++ b/special-pages/pages/new-tab/app/activity/mocks/activity.mock-transport.js @@ -87,6 +87,7 @@ export function activityMockTransport() { trackersFound: false, trackingStatus: { trackerCompanies: [], totalCount: 0 }, title: 'example.com', + cookiePopUpBlocked: true, }); count += 1; console.log('sent', dataset); @@ -339,6 +340,7 @@ export function generateSampleData(count) { trackerCompanies, totalCount: trackerCompanies.length === 0 ? 0 : Math.round(trackerCompanies.length * 1.5), }, + cookiePopUpBlocked: true, }); } return data; diff --git a/special-pages/pages/new-tab/app/activity/mocks/activity.mocks.js b/special-pages/pages/new-tab/app/activity/mocks/activity.mocks.js index 27f41cec49..f164be54ac 100644 --- a/special-pages/pages/new-tab/app/activity/mocks/activity.mocks.js +++ b/special-pages/pages/new-tab/app/activity/mocks/activity.mocks.js @@ -77,6 +77,7 @@ export const activityMocks = { totalCount: 56, }, history: [], + cookiePopUpBlocked: true, }, ], }, @@ -115,6 +116,7 @@ export const activityMocks = { relativeTime: '1 day ago', }, ], + cookiePopUpBlocked: true, }, { favicon: { src: 'youtube-icon.png' }, @@ -139,6 +141,7 @@ export const activityMocks = { relativeTime: '3 days ago', }, ], + cookiePopUpBlocked: false, }, { favicon: { src: 'amazon-icon.png' }, @@ -158,6 +161,7 @@ export const activityMocks = { relativeTime: '1 day ago', }, ], + cookiePopUpBlocked: true, }, { favicon: { src: 'twitter-icon.png' }, @@ -177,6 +181,7 @@ export const activityMocks = { relativeTime: '2 days ago', }, ], + cookiePopUpBlocked: true, }, { favicon: { src: 'linkedin-icon.png' }, @@ -196,6 +201,7 @@ export const activityMocks = { relativeTime: '2 hrs ago', }, ], + cookiePopUpBlocked: false, }, ], }, diff --git a/special-pages/pages/new-tab/app/activity/strings.json b/special-pages/pages/new-tab/app/activity/strings.json index fb603297ea..b789f047f1 100644 --- a/special-pages/pages/new-tab/app/activity/strings.json +++ b/special-pages/pages/new-tab/app/activity/strings.json @@ -19,6 +19,10 @@ "title": "{count} Tracking attempts blocked", "note": "Pill text indicating that more than 1 attempt has been blocked. Eg: '2 Tracking attempts blocked'" }, + "activity_countBlockedPluralLegacy": { + "title": "{count} tracking attempts blocked", + "note": "The main headline indicating that more than 1 attempt has been blocked. Eg: '2 tracking attempts blocked'" + }, "activity_countBlockedSingular": { "title": "{count} Tracking attempt blocked", "note": "Pill text indicating that 1 attempt has been blocked. Eg: '1 Tracking attempt blocked'" diff --git a/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStatsLegacy.module.css b/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStatsLegacy.module.css new file mode 100644 index 0000000000..de482e1342 --- /dev/null +++ b/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStatsLegacy.module.css @@ -0,0 +1,168 @@ +.control { + display: flex; + align-items: center; + height: 24px; + margin-bottom: 16px; + position: relative; + gap: 8px; +} + +.headingIcon { + width: 24px; + height: 24px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding-top: 0.5px; + + img { + max-width: 1rem; + max-height: 1rem; + } +} + +.caption { + font-size: var(--title-3-em-font-size); + font-weight: var(--title-3-em-font-weight); + line-height: var(--title-3-em-line-height); + flex: 1; +} + +.widgetExpander { + position: relative; + + & [aria-controls] { + position: absolute; + top: 50%; + transform: translateY(-50%); + opacity: 1; + /** + * NOTE: This is just for visual alignment. The grid in which this sits is correct, + * but to preserve the larger tap-area for the button, we're opting to shift this over + * manually to solve this specific layout case. + */ + right: -4px + } +} + +.counter { + display: flex; + flex-direction: column; + gap: 4px; +} + +.title { + grid-area: title; + font-size: var(--title-2-font-size); + font-weight: var(--title-2-font-weight); + line-height: var(--title-2-line-height); +} + +.subtitle { + grid-area: label; + color: var(--ntp-text-muted); + line-height: var(--body-line-height); + text-transform: uppercase; + + &.indented { + padding-left: 2px; + } +} + +.body { + display: grid; + grid-row-gap: var(--sp-3); + +} + +.list { + display: grid; + grid-template-columns: auto; + grid-row-gap: calc(6 * var(--px-in-rem)); + transition: opacity ease-in-out 0.3s, visibility ease-in-out 0.3s; + + &:not(:empty) { + margin-top: 24px; + } +} + +.row { + min-height: 2rem; + display: grid; + grid-gap: var(--sp-2); + grid-template-columns: auto auto 40%; + grid-template-areas: 'company count bar'; + align-items: center; + + @media screen and (min-width: 500px) { + grid-template-columns: 35% 10% calc(55% - 1rem); /* - 1rem accounts for the grid gaps */ + } +} + +.listFooter { + display: flex; + .otherTrackersRow + .listExpander { + margin-left: auto; + } +} + +.otherTrackersRow { + padding-left: var(--sp-1); + color: var(--ntp-text-muted); + display: flex; + align-items: center; + +} + +.company { + grid-area: company; + display: flex; + align-items: center; + gap: var(--sp-2); + padding-left: var(--sp-1); + overflow: hidden; +} + +.name { + font-size: var(--title-3-em-font-size); + font-weight: var(--title-3-em-font-weight); + line-height: var(--title-3-em-line-height); + text-overflow: ellipsis; + display: block; + overflow: hidden; + white-space: nowrap; + position: relative; + top: -1px; +} + +.count { + grid-area: count; + text-align: right; + color: var(--ntp-text-normal); + line-height: 1; +} + +.bar { + grid-area: bar; + width: 100%; + height: 1rem; + border-radius: calc(20 * var(--px-in-rem)); + + background: var(--color-black-at-3); + + [data-theme=dark] & { + background: var(--color-white-at-6); + } +} + +.fill { + grid-area: bar; + height: 1rem; + border-radius: calc(20 * var(--px-in-rem)); + background: var(--color-black-at-6); + + [data-theme=dark] & { + background: var(--color-white-at-9); + } +} diff --git a/special-pages/pages/new-tab/app/privacy-stats/strings.json b/special-pages/pages/new-tab/app/privacy-stats/strings.json index ce8f3d00c5..01dfb479e6 100644 --- a/special-pages/pages/new-tab/app/privacy-stats/strings.json +++ b/special-pages/pages/new-tab/app/privacy-stats/strings.json @@ -19,10 +19,18 @@ "title": "Tracking attempt blocked", "note": "The main headline indicating that a single tracker was blocked" }, + "stats_countBlockedSingularLegacy": { + "title": "1 tracking attempt blocked", + "note": "The main headline indicating that a single tracker was blocked" + }, "stats_countBlockedPlural": { "title": "Tracking attempts blocked", "note": "The main headline indicating that more than 1 attempt has been blocked. Eg: '2 tracking attempts blocked'" }, + "stats_countBlockedPluralLegacy": { + "title": "{count} tracking attempts blocked", + "note": "The main headline indicating that more than 1 attempt has been blocked. Eg: '2 tracking attempts blocked'" + }, "stats_noActivityAdsAndTrackers": { "title": "DuckDuckGo blocks ads and tracking attempts as you browse. Visit a few sites to see how many we block!", "note": "Placeholder for when we cannot report any blocked ads and trackers yet" @@ -35,10 +43,18 @@ "title": "advertising & tracking attempt blocked", "note": "The main headline indicating that a single ad or tracking attempt was blocked" }, + "stats_countBlockedAdsAndTrackersSingularLegacy": { + "title": "1 advertising & tracking attempt blocked", + "note": "The main headline indicating that a single ad or tracking attempt was blocked" + }, "stats_countBlockedAdsAndTrackersPlural": { "title": "advertising & tracking attempts blocked", "note": "The main headline indicating that more than 1 ad or tracking attempt has been blocked. Eg: '2 advertising & tracking attempts blocked" }, + "stats_countBlockedAdsAndTrackersPluralLegacy": { + "title": "{count} advertising & tracking attempts blocked", + "note": "The main headline indicating that more than 1 ad or tracking attempt has been blocked. Eg: '2 advertising & tracking attempts blocked" + }, "stats_totalCookiePopUpsBlockedSingular": { "title": "Cookie pop-up blocked", "note": "The heading indicating that a single cookie pop-up was handled by the CPM" diff --git a/special-pages/pages/new-tab/app/protections/components/Protections.js b/special-pages/pages/new-tab/app/protections/components/Protections.js index aa7f326f7b..2b06b242f9 100644 --- a/special-pages/pages/new-tab/app/protections/components/Protections.js +++ b/special-pages/pages/new-tab/app/protections/components/Protections.js @@ -4,6 +4,7 @@ import cn from 'classnames'; import styles from './Protections.module.css'; import { ProtectionsHeading } from './ProtectionsHeading.js'; import { useTypedTranslationWith } from '../../types.js'; +import { ProtectionsHeadingLegacy } from './ProtectionsHeadingLegacy'; /** * @import enStrings from "../strings.json" @@ -22,11 +23,12 @@ import { useTypedTranslationWith } from '../../types.js'; * @param {(feed: ProtectionsConfig['feed']) => void} props.setFeed * @param {import("preact").ComponentChild} [props.children] * @param {()=>void} props.toggle - * @param {import("@preact/signals").Signal} props.totalCookiePopUpsBlockedSignal + * @param {import("@preact/signals").Signal} props.totalCookiePopUpsBlockedSignal */ export function Protections({ expansion = 'expanded', children, blockedCountSignal, feed, toggle, setFeed, totalCookiePopUpsBlockedSignal }) { const WIDGET_ID = useId(); const TOGGLE_ID = useId(); + const totalCookiePopUpsBlocked = totalCookiePopUpsBlockedSignal.value; const attrs = useMemo(() => { return { @@ -37,14 +39,27 @@ export function Protections({ expansion = 'expanded', children, blockedCountSign return (
    - + {/* If `totalCookiePopUpsBlocked` is `undefined`, it means the + native side is not sending this property and we can assume it's not + yet been implemented */} + {totalCookiePopUpsBlocked === undefined ? ( + + ) : ( + + )} {children} diff --git a/special-pages/pages/new-tab/app/protections/components/ProtectionsConsumer.js b/special-pages/pages/new-tab/app/protections/components/ProtectionsConsumer.js index 1ed400e7a7..0ce00ea7a6 100644 --- a/special-pages/pages/new-tab/app/protections/components/ProtectionsConsumer.js +++ b/special-pages/pages/new-tab/app/protections/components/ProtectionsConsumer.js @@ -55,7 +55,10 @@ function ProtectionsReadyState({ data, config }) { > {config.feed === 'activity' && ( - + )} {config.feed === 'privacy-stats' && ( diff --git a/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.js b/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.js index a888ad64b6..79373d52f4 100644 --- a/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.js +++ b/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.js @@ -17,7 +17,7 @@ import { Tooltip } from '../../components/Tooltip/Tooltip.js'; * @param {boolean} props.canExpand * @param {() => void} props.onToggle * @param {import('preact').ComponentProps<'button'>} [props.buttonAttrs] - * @param {import("@preact/signals").Signal} props.totalCookiePopUpsBlockedSignal + * @param {import("@preact/signals").Signal} props.totalCookiePopUpsBlockedSignal */ export function ProtectionsHeading({ expansion, canExpand, blockedCountSignal, onToggle, buttonAttrs = {}, totalCookiePopUpsBlockedSignal }) { const { t } = useTypedTranslationWith(/** @type {Strings} */ ({})); diff --git a/special-pages/pages/new-tab/app/protections/components/ProtectionsHeadingLegacy.js b/special-pages/pages/new-tab/app/protections/components/ProtectionsHeadingLegacy.js new file mode 100644 index 0000000000..c615a68f35 --- /dev/null +++ b/special-pages/pages/new-tab/app/protections/components/ProtectionsHeadingLegacy.js @@ -0,0 +1,76 @@ +import { useTypedTranslationWith } from '../../types.js'; +import { useState } from 'preact/hooks'; +import styles from '../../privacy-stats/components/PrivacyStatsLegacy.module.css'; +import { ShowHideButtonCircle } from '../../components/ShowHideButton.jsx'; +import cn from 'classnames'; +import { h } from 'preact'; +import { useAdBlocking } from '../../settings.provider.js'; +import { Trans } from '../../../../../shared/components/TranslationsProvider.js'; +import { getLocalizedNumberFormatter } from '../../../../../shared/utils.js'; +import { useLocale } from '../../../../../shared/components/EnvironmentProvider.js'; + +/** + * @import enStrings from "../strings.json" + * @import statsStrings from "../../privacy-stats/strings.json" + * @import activityStrings from "../../activity/strings.json" + * @typedef {enStrings & statsStrings & activityStrings} Strings + * @param {object} props + * @param {import('../../../types/new-tab.ts').Expansion} props.expansion + * @param {import("@preact/signals").Signal} props.blockedCountSignal + * @param {boolean} props.canExpand + * @param {() => void} props.onToggle + * @param {import('preact').ComponentProps<'button'>} [props.buttonAttrs] + */ +export function ProtectionsHeadingLegacy({ expansion, canExpand, blockedCountSignal, onToggle, buttonAttrs = {} }) { + const { t } = useTypedTranslationWith(/** @type {Strings} */ ({})); + const locale = useLocale(); + const [formatter] = useState(() => getLocalizedNumberFormatter(locale)); + const adBlocking = useAdBlocking(); + const blockedCount = blockedCountSignal.value; + const none = blockedCount === 0; + const some = blockedCount > 0; + const alltime = formatter.format(blockedCount); + + let alltimeTitle; + if (blockedCount === 1) { + alltimeTitle = adBlocking ? t('stats_countBlockedAdsAndTrackersSingularLegacy') : t('stats_countBlockedSingularLegacy'); + } else { + alltimeTitle = adBlocking + ? t('stats_countBlockedAdsAndTrackersPluralLegacy', { count: alltime }) + : t('stats_countBlockedPlural', { count: alltime }); + } + + return ( +
    +
    + + Privacy Shield + +

    {t('protections_menuTitle')}

    + {canExpand && ( + + + + )} +
    +
    + {none &&

    {t('protections_noRecent')}

    } + {some && ( +

    + {' '} + +

    + )} +

    {t('stats_feedCountBlockedPeriod')}

    +
    +
    + ); +} diff --git a/special-pages/pages/new-tab/app/protections/components/ProtectionsProvider.js b/special-pages/pages/new-tab/app/protections/components/ProtectionsProvider.js index 1d13ebd06e..9daaf414f2 100644 --- a/special-pages/pages/new-tab/app/protections/components/ProtectionsProvider.js +++ b/special-pages/pages/new-tab/app/protections/components/ProtectionsProvider.js @@ -97,6 +97,7 @@ export function useService() { export function useBlockedCount(initial) { const service = useService(); const signal = useSignal(initial); + {/* @todo jingram possibly refactor to include full object */} useSignalEffect(() => { return service.current?.onData((evt) => { signal.value = evt.data.totalCount; @@ -106,8 +107,8 @@ export function useBlockedCount(initial) { } /** - * @param {number} initial - * @return {import("@preact/signals").Signal} + * @param {number | null | undefined} initial + * @return {import("@preact/signals").Signal} */ export function useCookiePopUpsBlockedCount(initial) { const service = useService(); diff --git a/special-pages/pages/new-tab/app/protections/mocks/protections.mock-transport.js b/special-pages/pages/new-tab/app/protections/mocks/protections.mock-transport.js index c92573c58c..3ae81677c3 100644 --- a/special-pages/pages/new-tab/app/protections/mocks/protections.mock-transport.js +++ b/special-pages/pages/new-tab/app/protections/mocks/protections.mock-transport.js @@ -86,6 +86,15 @@ export function protectionsMockTransport() { if (url.searchParams.get('activity') === 'empty') { dataset.totalCount = 0; } + if (url.searchParams.get('cpm') === 'true') { + dataset.totalCookiePopUpsBlocked = 22; + } + // Setting cpm=undefined allows us to see the legacy + // protections report. Useful until all platforms adopt the + // new schema + if (url.searchParams.get('cpm') === 'undefined') { + dataset.totalCookiePopUpsBlocked = undefined; + } return Promise.resolve(dataset); case 'protections_getConfig': { if (url.searchParams.get('protections.feed') === 'activity') { diff --git a/special-pages/pages/new-tab/app/protections/mocks/protections.mocks.js b/special-pages/pages/new-tab/app/protections/mocks/protections.mocks.js index 5b503ad379..3480bedfb0 100644 --- a/special-pages/pages/new-tab/app/protections/mocks/protections.mocks.js +++ b/special-pages/pages/new-tab/app/protections/mocks/protections.mocks.js @@ -9,7 +9,7 @@ export const protectionsMocks = { }, few: { totalCount: 86, - totalCookiePopUpsBlocked: 23, + totalCookiePopUpsBlocked: 21, }, many: { totalCount: 1_000_020, diff --git a/special-pages/pages/new-tab/public/locales/en/new-tab.json b/special-pages/pages/new-tab/public/locales/en/new-tab.json index 184911bc9d..420cb0ae7e 100644 --- a/special-pages/pages/new-tab/public/locales/en/new-tab.json +++ b/special-pages/pages/new-tab/public/locales/en/new-tab.json @@ -81,10 +81,18 @@ "title": "Tracking attempt blocked", "note": "The main headline indicating that a single tracker was blocked" }, + "stats_countBlockedSingularLegacy": { + "title": "1 tracking attempt blocked", + "note": "The main headline indicating that a single tracker was blocked" + }, "stats_countBlockedPlural": { "title": "Tracking attempts blocked", "note": "The main headline indicating that more than 1 attempt has been blocked. Eg: '2 tracking attempts blocked'" }, + "stats_countBlockedPluralLegacy": { + "title": "{count} tracking attempts blocked", + "note": "The main headline indicating that more than 1 attempt has been blocked. Eg: '2 tracking attempts blocked'" + }, "stats_noActivityAdsAndTrackers": { "title": "DuckDuckGo blocks ads and tracking attempts as you browse. Visit a few sites to see how many we block!", "note": "Placeholder for when we cannot report any blocked ads and trackers yet" @@ -97,10 +105,18 @@ "title": "advertising & tracking attempt blocked", "note": "The main headline indicating that a single ad or tracking attempt was blocked" }, + "stats_countBlockedAdsAndTrackersSingularLegacy": { + "title": "1 advertising & tracking attempt blocked", + "note": "The main headline indicating that a single ad or tracking attempt was blocked" + }, "stats_countBlockedAdsAndTrackersPlural": { "title": "advertising & tracking attempts blocked", "note": "The main headline indicating that more than 1 ad or tracking attempt has been blocked. Eg: '2 advertising & tracking attempts blocked" }, + "stats_countBlockedAdsAndTrackersPluralLegacy": { + "title": "{count} advertising & tracking attempts blocked", + "note": "The main headline indicating that more than 1 ad or tracking attempt has been blocked. Eg: '2 advertising & tracking attempts blocked" + }, "stats_totalCookiePopUpsBlockedSingular": { "title": "Cookie pop-up blocked", "note": "The heading indicating that a single cookie pop-up was handled by the CPM" @@ -425,6 +441,10 @@ "title": "{count} Tracking attempts blocked", "note": "Pill text indicating that more than 1 attempt has been blocked. Eg: '2 Tracking attempts blocked'" }, + "activity_countBlockedPluralLegacy": { + "title": "{count} tracking attempts blocked", + "note": "The main headline indicating that more than 1 attempt has been blocked. Eg: '2 tracking attempts blocked'" + }, "activity_countBlockedSingular": { "title": "{count} Tracking attempt blocked", "note": "Pill text indicating that 1 attempt has been blocked. Eg: '1 Tracking attempt blocked'" From 98fa067339da9511c54cc96b7e4f847715bfe297 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Mon, 27 Oct 2025 20:17:33 -0400 Subject: [PATCH 07/17] Fix errors from lint-fix Balance of lint-fix fixes --- .../app/activity/NormalizeDataProvider.js | 1 + .../activity/components/Activity.examples.js | 7 +- .../app/activity/components/Activity.js | 80 ++++------------- .../pages/new-tab/app/components/Icons.js | 73 +++++++++------ .../app/components/TickPill/TickPill.js | 2 +- .../new-tab/app/components/Tooltip/Tooltip.js | 4 +- .../app/protections/components/Protections.js | 40 +++++---- .../components/ProtectionsConsumer.js | 4 +- .../components/ProtectionsHeading.examples.js | 88 +++++++++++++++++-- .../components/ProtectionsHeading.js | 52 +++++------ .../components/ProtectionsProvider.js | 2 +- 11 files changed, 206 insertions(+), 147 deletions(-) diff --git a/special-pages/pages/new-tab/app/activity/NormalizeDataProvider.js b/special-pages/pages/new-tab/app/activity/NormalizeDataProvider.js index 4bbc4508cb..ab619b7bae 100644 --- a/special-pages/pages/new-tab/app/activity/NormalizeDataProvider.js +++ b/special-pages/pages/new-tab/app/activity/NormalizeDataProvider.js @@ -193,6 +193,7 @@ export function SignalStateProvider({ children }) { favorites: {}, urls: [], totalTrackers: 0, + cookiePopUpBlocked: null, }, { activity: state.data.activity, urls: state.data.urls, totalTrackers: state.data.totalTrackers }, ), diff --git a/special-pages/pages/new-tab/app/activity/components/Activity.examples.js b/special-pages/pages/new-tab/app/activity/components/Activity.examples.js index d2c4791524..323e782fdb 100644 --- a/special-pages/pages/new-tab/app/activity/components/Activity.examples.js +++ b/special-pages/pages/new-tab/app/activity/components/Activity.examples.js @@ -16,7 +16,7 @@ export const activityExamples = { factory: () => ( - + ), @@ -25,7 +25,7 @@ export const activityExamples = { factory: () => ( - + ), @@ -34,7 +34,7 @@ export const activityExamples = { factory: () => ( - + ), @@ -58,6 +58,7 @@ function Mock({ children, size }) { favorites: {}, urls: [], totalTrackers: 0, + cookiePopUpBlocked: null, }, { activity: mocks, urls: mocks.map((x) => x.url), totalTrackers: 0 }, ); diff --git a/special-pages/pages/new-tab/app/activity/components/Activity.js b/special-pages/pages/new-tab/app/activity/components/Activity.js index a0abb2f3b6..e3aac14096 100644 --- a/special-pages/pages/new-tab/app/activity/components/Activity.js +++ b/special-pages/pages/new-tab/app/activity/components/Activity.js @@ -1,4 +1,4 @@ -import { h, Fragment } from 'preact'; +import { h } from 'preact'; import styles from './Activity.module.css'; // @todo legacyProtections: `stylesLegacy` can be removed once all platforms // are ready for the new Protections Report @@ -152,10 +152,8 @@ const BurnableItem = memo( } // @todo legacyProtections: Once all platforms are ready for the new - // protections report we can use `ActivityItem` - const ActivityItemComponent = shouldDisplayLegacyActivity - ? ActivityItemLegacy - : ActivityItem; + // protections report we can use `ActivityItem` + const ActivityItemComponent = shouldDisplayLegacyActivity ? ActivityItemLegacy : ActivityItem; return ( @@ -172,15 +170,9 @@ const BurnableItem = memo( // @todo legacyProtections: `TrackerStatusLegacy` and // supporting prop can be removed once all platforms are // ready for the new protections report - + ) : ( - + )} @@ -209,10 +201,8 @@ const RemovableItem = memo( } // @todo legacyProtections: Once all platforms are ready for the new - // protections report we can use `ActivityItem` - const ActivityItemComponent = shouldDisplayLegacyActivity - ? ActivityItemLegacy - : ActivityItem; + // protections report we can use `ActivityItem` + const ActivityItemComponent = shouldDisplayLegacyActivity ? ActivityItemLegacy : ActivityItem; return ( + ) : ( - + )} @@ -255,28 +239,14 @@ function TrackerStatus({ id, trackersFound }) { const { activity } = useContext(NormalizedDataContext); const status = useComputed(() => activity.value.trackingStatus[id]); const cookiePopUpBlocked = useComputed(() => activity.value.cookiePopUpBlocked); - const {totalCount, trackerCompanies} = status.value; - const other = trackerCompanies.slice(DDG_MAX_TRACKER_ICONS - 1); - const adBlocking = useAdBlocking(); - - let otherIcon = null; - if (other.length > 0) { - const title = other.map((item) => item.displayName).join('\n'); - otherIcon = ( - - +{other.length} - - ); - } + const { totalCount } = status.value; if (totalCount === 0) { - const text = trackersFound - ? t('activity_no_trackers_blocked') - : t('activity_no_trackers') + const text = trackersFound ? t('activity_no_trackers_blocked') : t('activity_no_trackers'); return (

    - +

    ); } @@ -285,15 +255,11 @@ function TrackerStatus({ id, trackersFound }) {
    {totalCount > 0 && ( - + )} {cookiePopUpBlocked && }
    @@ -407,11 +373,7 @@ export function ActivityConsumer({ showBurnAnimation, shouldDisplayLegacyActivit return ( - + ); @@ -420,11 +382,7 @@ export function ActivityConsumer({ showBurnAnimation, shouldDisplayLegacyActivit - + diff --git a/special-pages/pages/new-tab/app/components/Icons.js b/special-pages/pages/new-tab/app/components/Icons.js index 225de53e82..533d6e1a0c 100644 --- a/special-pages/pages/new-tab/app/components/Icons.js +++ b/special-pages/pages/new-tab/app/components/Icons.js @@ -606,32 +606,53 @@ export function CloseSmallIcon(props) { * @param {import('preact').JSX.SVGAttributes} props */ export function NewBadgeIcon(props) { - return ( - - - - - - - ); + return ( + + + + + + + ); } /** * @param {import('preact').JSX.SVGAttributes} props */ export function InfoIcon(props) { - return ( - - - - - - ); + return ( + + + + + + ); } /** @@ -641,12 +662,12 @@ export function InfoIcon(props) { export function Check(props) { return ( - + ); } diff --git a/special-pages/pages/new-tab/app/components/TickPill/TickPill.js b/special-pages/pages/new-tab/app/components/TickPill/TickPill.js index 5c62012a00..39e49e8d70 100644 --- a/special-pages/pages/new-tab/app/components/TickPill/TickPill.js +++ b/special-pages/pages/new-tab/app/components/TickPill/TickPill.js @@ -8,7 +8,7 @@ import styles from './TickPill.module.css'; * @param {Object} props * @param {string} props.text - The text to display next to the checkmark * @param {string} [props.className] - Additional CSS classes - * @param {bool} [props.displayTick] - Display the tick or not + * @param {boolean} [props.displayTick] - Display the tick or not */ export function TickPill({ text, className, displayTick = true }) { return ( diff --git a/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.js b/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.js index b28eaf3899..c6f684d11b 100644 --- a/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.js +++ b/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.js @@ -20,9 +20,7 @@ export function Tooltip({ children, content, className }) { onMouseLeave={() => setIsVisible(false)} > {children} - {isVisible && ( -

    + {/* @todo `NewBadgeIcon` will be manually removed in + a future iteration */}
    )} From aa42a91fbb7d1048f2cecc4eb44053535d693924 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Tue, 28 Oct 2025 12:38:56 -0400 Subject: [PATCH 09/17] Add testing states --- .../protections/mocks/protections.mock-transport.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/special-pages/pages/new-tab/app/protections/mocks/protections.mock-transport.js b/special-pages/pages/new-tab/app/protections/mocks/protections.mock-transport.js index 3ae81677c3..a9f52fe8c3 100644 --- a/special-pages/pages/new-tab/app/protections/mocks/protections.mock-transport.js +++ b/special-pages/pages/new-tab/app/protections/mocks/protections.mock-transport.js @@ -80,6 +80,8 @@ export function protectionsMockTransport() { const msg = /** @type {any} */ (_msg); switch (msg.method) { case 'protections_getData': + // No data. Setting `stats=none` (totalCount = 0) also + // hides CPM stats if (url.searchParams.get('stats') === 'none') { dataset.totalCount = 0; } @@ -89,6 +91,14 @@ export function protectionsMockTransport() { if (url.searchParams.get('cpm') === 'true') { dataset.totalCookiePopUpsBlocked = 22; } + // CPM = 0 state + if (url.searchParams.get('cpm') === 'none') { + dataset.totalCookiePopUpsBlocked = 0; + } + // CPM disabled state + if (url.searchParams.get('cpm') === 'null') { + dataset.totalCookiePopUpsBlocked = null; + } // Setting cpm=undefined allows us to see the legacy // protections report. Useful until all platforms adopt the // new schema From 544b3c0e03a735736e9d606aa224be835a9e51dc Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Wed, 29 Oct 2025 14:49:13 -0400 Subject: [PATCH 10/17] Update test cases --- .../integration-tests/activity.page.js | 18 +++++++ .../integration-tests/activity.spec.js | 16 ++++++ .../integrations-tests/protections.page.js | 53 +++++++++++++++++++ .../integrations-tests/protections.spec.js | 40 ++++++++++++++ 4 files changed, 127 insertions(+) diff --git a/special-pages/pages/new-tab/app/activity/integration-tests/activity.page.js b/special-pages/pages/new-tab/app/activity/integration-tests/activity.page.js index 2daa6d595b..1442c0ba6e 100644 --- a/special-pages/pages/new-tab/app/activity/integration-tests/activity.page.js +++ b/special-pages/pages/new-tab/app/activity/integration-tests/activity.page.js @@ -441,4 +441,22 @@ export class ActivityPage { - paragraph: Past 7 days `); } + + /** + * Test that cookie popup blocked indicator is shown for items with cookiePopUpBlocked: true + */ + async showsCookiePopupBlockedIndicator() { + // First item in 'few' mock has cookiePopUpBlocked: true + const firstItem = this.context().getByTestId('ActivityItem').nth(0); + await expect(firstItem.getByText(/cookie pop-up/i)).toBeVisible(); + } + + /** + * Test that cookie popup blocked indicator is NOT shown for items with cookiePopUpBlocked: false + */ + async hidesCookiePopupIndicatorWhenNotBlocked() { + // Second item in 'few' mock (youtube) has cookiePopUpBlocked: false + const secondItem = this.context().getByTestId('ActivityItem').nth(1); + await expect(secondItem.getByText(/cookie pop-up/i)).not.toBeVisible(); + } } diff --git a/special-pages/pages/new-tab/app/activity/integration-tests/activity.spec.js b/special-pages/pages/new-tab/app/activity/integration-tests/activity.spec.js index f4aad82f5b..6faca16187 100644 --- a/special-pages/pages/new-tab/app/activity/integration-tests/activity.spec.js +++ b/special-pages/pages/new-tab/app/activity/integration-tests/activity.spec.js @@ -100,6 +100,22 @@ test.describe('activity widget', () => { await ap.didRender(); await ap.showsAdsAndTrackersTrackerStates(); }); + test('shows cookie popup blocked indicator', async ({ page }, workerInfo) => { + const ntp = NewtabPage.create(page, workerInfo); + const ap = new ActivityPage(page, ntp); + await ntp.reducedMotion(); + await ntp.openPage({ additional: { ...defaultPageParams } }); + await ap.didRender(); + await ap.showsCookiePopupBlockedIndicator(); + }); + test('hides cookie popup indicator when not blocked', async ({ page }, workerInfo) => { + const ntp = NewtabPage.create(page, workerInfo); + const ap = new ActivityPage(page, ntp); + await ntp.reducedMotion(); + await ntp.openPage({ additional: { ...defaultPageParams } }); + await ap.didRender(); + await ap.hidesCookiePopupIndicatorWhenNotBlocked(); + }); test('after rendering and navigating to a new tab, data is re-requested on return', async ({ page }, workerInfo) => { const ntp = NewtabPage.create(page, workerInfo); const ap = new ActivityPage(page, ntp); diff --git a/special-pages/pages/new-tab/app/protections/integrations-tests/protections.page.js b/special-pages/pages/new-tab/app/protections/integrations-tests/protections.page.js index f6f6958afe..510be68bcc 100644 --- a/special-pages/pages/new-tab/app/protections/integrations-tests/protections.page.js +++ b/special-pages/pages/new-tab/app/protections/integrations-tests/protections.page.js @@ -40,6 +40,7 @@ export class ProtectionsPage { /** @type {ProtectionsData} */ const data = { totalCount: count, + totalCookiePopUpsBlocked: null, // null means CPM is not enabled }; await this.ntp.mocks.simulateSubscriptionMessage(named.subscription('protections_onDataUpdate'), data); await expect(this.context().getByRole('heading', { level: 3 })).toContainText(`${count} tracking attempts blocked`); @@ -56,4 +57,56 @@ export class ProtectionsPage { - paragraph: Ostatnie 7 dni `); } + + /** + * Test that cookie popup blocking stats are displayed when both trackers and cookie popups are > 0 + */ + async displaysCookiePopupStats() { + /** @type {ProtectionsData} */ + const data = { + totalCount: 100, + totalCookiePopUpsBlocked: 25, + }; + await this.ntp.mocks.simulateSubscriptionMessage(named.subscription('protections_onDataUpdate'), data); + await expect(this.context().getByRole('heading', { level: 3 }).first()).toContainText('100 tracking attempts blocked'); + // Cookie popup stats should be visible + await expect(this.context().getByText(/cookie pop-ups?/i)).toBeVisible(); + } + + /** + * Test that cookie popup stats are NOT displayed when totalCookiePopUpsBlocked is null (CPM disabled) + */ + async hidesCookiePopupStatsWhenDisabled() { + /** @type {ProtectionsData} */ + const data = { + totalCount: 100, + totalCookiePopUpsBlocked: null, + }; + await this.ntp.mocks.simulateSubscriptionMessage(named.subscription('protections_onDataUpdate'), data); + // Cookie popup stats should not be visible + await expect(this.context().getByText(/cookie pop-ups?/i)).not.toBeVisible(); + } + + /** + * Test that cookie popup stats are NOT displayed when totalCookiePopUpsBlocked is 0 + */ + async hidesCookiePopupStatsWhenZero() { + /** @type {ProtectionsData} */ + const data = { + totalCount: 100, + totalCookiePopUpsBlocked: 0, + }; + await this.ntp.mocks.simulateSubscriptionMessage(named.subscription('protections_onDataUpdate'), data); + // Cookie popup stats should not be visible when count is 0 + await expect(this.context().getByText(/cookie pop-ups?/i)).not.toBeVisible(); + } + + /** + * Test that the info tooltip is displayed + */ + async hasInfoTooltip() { + const heading = this.context().getByTestId('ProtectionsHeading'); + // The InfoIcon should be present + await expect(heading.locator('[class*="infoIcon"]')).toBeVisible(); + } } diff --git a/special-pages/pages/new-tab/app/protections/integrations-tests/protections.spec.js b/special-pages/pages/new-tab/app/protections/integrations-tests/protections.spec.js index aa05b645bb..ba5e891e3a 100644 --- a/special-pages/pages/new-tab/app/protections/integrations-tests/protections.spec.js +++ b/special-pages/pages/new-tab/app/protections/integrations-tests/protections.spec.js @@ -56,4 +56,44 @@ test.describe('protections report', () => { await protections.ready(); await protections.hasPolishText(); }); + + test('displays cookie popup blocking stats when enabled and counts > 0', async ({ page }, workerInfo) => { + const ntp = NewtabPage.create(page, workerInfo); + await ntp.reducedMotion(); + await ntp.openPage({ additional: { 'protections.feed': 'activity' } }); + + const protections = new ProtectionsPage(ntp); + await protections.ready(); + await protections.displaysCookiePopupStats(); + }); + + test('hides cookie popup stats when CPM is disabled (null)', async ({ page }, workerInfo) => { + const ntp = NewtabPage.create(page, workerInfo); + await ntp.reducedMotion(); + await ntp.openPage({ additional: { 'protections.feed': 'activity' } }); + + const protections = new ProtectionsPage(ntp); + await protections.ready(); + await protections.hidesCookiePopupStatsWhenDisabled(); + }); + + test('hides cookie popup stats when count is 0', async ({ page }, workerInfo) => { + const ntp = NewtabPage.create(page, workerInfo); + await ntp.reducedMotion(); + await ntp.openPage({ additional: { 'protections.feed': 'activity' } }); + + const protections = new ProtectionsPage(ntp); + await protections.ready(); + await protections.hidesCookiePopupStatsWhenZero(); + }); + + test('displays info tooltip', async ({ page }, workerInfo) => { + const ntp = NewtabPage.create(page, workerInfo); + await ntp.reducedMotion(); + await ntp.openPage({ additional: { 'protections.feed': 'activity' } }); + + const protections = new ProtectionsPage(ntp); + await protections.ready(); + await protections.hasInfoTooltip(); + }); }); From c6493d0e37cbab280c99d9a026e6dcb9ce7881c1 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Wed, 29 Oct 2025 20:34:01 -0400 Subject: [PATCH 11/17] Update fire icon in Activity details and animation on info icon hover Include missing styles for infoIcon --- .../new-tab/app/activity/components/ActivityItem.js | 4 ++-- special-pages/pages/new-tab/app/components/Icons.js | 12 ++++++++++++ .../pages/new-tab/app/components/Icons.module.css | 5 +++++ .../app/components/Tooltip/Tooltip.module.css | 2 +- .../privacy-stats/components/PrivacyStats.module.css | 7 +++++++ 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/special-pages/pages/new-tab/app/activity/components/ActivityItem.js b/special-pages/pages/new-tab/app/activity/components/ActivityItem.js index d95cd3fba7..ec4a4f9840 100644 --- a/special-pages/pages/new-tab/app/activity/components/ActivityItem.js +++ b/special-pages/pages/new-tab/app/activity/components/ActivityItem.js @@ -7,7 +7,7 @@ import { FaviconWithState } from '../../../../../shared/components/FaviconWithSt import { ACTION_ADD_FAVORITE, ACTION_REMOVE, ACTION_REMOVE_FAVORITE } from '../constants.js'; import { Star, StarFilled } from '../../components/icons/Star.js'; import { Fire } from '../../components/icons/Fire.js'; -import { Cross } from '../../components/Icons.js'; +import { Cross, FireIcon } from '../../components/Icons.js'; import { useContext } from 'preact/hooks'; import { memo } from 'preact/compat'; import { useComputed } from '@preact/signals'; @@ -139,7 +139,7 @@ function Controls({ canBurn, url, title }) { value={url} type="button" > - {canBurn ? : } + {canBurn ? : }
    ); diff --git a/special-pages/pages/new-tab/app/components/Icons.js b/special-pages/pages/new-tab/app/components/Icons.js index 533d6e1a0c..e0aab7291a 100644 --- a/special-pages/pages/new-tab/app/components/Icons.js +++ b/special-pages/pages/new-tab/app/components/Icons.js @@ -671,3 +671,15 @@ export function Check(props) { ); } + +/** + * From https://dub.duckduckgo.com/duckduckgo/Icons/blob/Main/Glyphs/16px/Fire-Solid-16.svg + * @param {import('preact').JSX.SVGAttributes} props + */ +export function FireIcon(props) { + return ( + + + + ); +} diff --git a/special-pages/pages/new-tab/app/components/Icons.module.css b/special-pages/pages/new-tab/app/components/Icons.module.css index 208defdf44..8989bac0ca 100644 --- a/special-pages/pages/new-tab/app/components/Icons.module.css +++ b/special-pages/pages/new-tab/app/components/Icons.module.css @@ -28,3 +28,8 @@ fill: white; fill-opacity: 0.24; } + +/* FireIcon styles */ +.fireIcon { + color: currentColor; +} diff --git a/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.module.css b/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.module.css index 32607e0a31..bb3c136461 100644 --- a/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.module.css +++ b/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.module.css @@ -18,7 +18,7 @@ white-space: normal; width: 236px; z-index: 1000; - animation: tooltipFadeIn 0.7s ease-out; + animation: tooltipFadeIn 300ms ease; & span { display: block; diff --git a/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStats.module.css b/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStats.module.css index 252399abf9..3626b1c34d 100644 --- a/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStats.module.css +++ b/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStats.module.css @@ -34,6 +34,13 @@ margin-right: 6px; } +.infoIcon { + width: 16px; + height: 16px; + display: inline-block; + vertical-align: middle; +} + .widgetExpander { position: relative; flex: 1; From 0b0b4d0dfc82d2113675fd836562dd6d758f9952 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Wed, 29 Oct 2025 20:53:15 -0400 Subject: [PATCH 12/17] Ensure cookie pill is visible on details even if no trackers are found --- .../app/activity/NormalizeDataProvider.js | 10 +++--- .../app/activity/components/Activity.js | 31 +++++++------------ 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/special-pages/pages/new-tab/app/activity/NormalizeDataProvider.js b/special-pages/pages/new-tab/app/activity/NormalizeDataProvider.js index ab619b7bae..b222a6c4d2 100644 --- a/special-pages/pages/new-tab/app/activity/NormalizeDataProvider.js +++ b/special-pages/pages/new-tab/app/activity/NormalizeDataProvider.js @@ -36,7 +36,7 @@ import { ACTION_BURN } from '../burning/BurnProvider.js'; * @property {Record} favorites * @property {string[]} urls * @property {number} totalTrackers - * @property {DomainActivity['cookiePopUpBlocked']} cookiePopUpBlocked + * @property {Record} cookiePopUpBlocked */ /** @@ -53,7 +53,7 @@ export function normalizeData(prev, incoming) { trackingStatus: {}, urls: [], totalTrackers: incoming.totalTrackers, - cookiePopUpBlocked: null, + cookiePopUpBlocked: {}, }; if (shallowDiffers(prev.urls, incoming.urls)) { @@ -66,7 +66,7 @@ export function normalizeData(prev, incoming) { const id = item.url; output.favorites[id] = item.favorite; - output.cookiePopUpBlocked = item.cookiePopUpBlocked; + output.cookiePopUpBlocked[id] = item.cookiePopUpBlocked; /** @type {Item} */ const next = { @@ -87,14 +87,12 @@ export function normalizeData(prev, incoming) { const prevItem = prev.trackingStatus[id] || { totalCount: 0, trackerCompanies: [], - cookiePopUpBlocked: null, }; const trackersDiffer = shallowDiffers(item.trackingStatus.trackerCompanies, prevItem.trackerCompanies); if (prevItem.totalCount !== item.trackingStatus.totalCount || trackersDiffer) { const next = { totalCount: item.trackingStatus.totalCount, trackerCompanies: [...item.trackingStatus.trackerCompanies], - cookiePopUpBlocked: item.cookiePopUpBlocked, }; output.trackingStatus[id] = next; } else { @@ -193,7 +191,7 @@ export function SignalStateProvider({ children }) { favorites: {}, urls: [], totalTrackers: 0, - cookiePopUpBlocked: null, + cookiePopUpBlocked: {}, }, { activity: state.data.activity, urls: state.data.urls, totalTrackers: state.data.totalTrackers }, ), diff --git a/special-pages/pages/new-tab/app/activity/components/Activity.js b/special-pages/pages/new-tab/app/activity/components/Activity.js index e3aac14096..f346baa8d9 100644 --- a/special-pages/pages/new-tab/app/activity/components/Activity.js +++ b/special-pages/pages/new-tab/app/activity/components/Activity.js @@ -238,29 +238,22 @@ function TrackerStatus({ id, trackersFound }) { const { t } = useTypedTranslationWith(/** @type {enStrings} */ ({})); const { activity } = useContext(NormalizedDataContext); const status = useComputed(() => activity.value.trackingStatus[id]); - const cookiePopUpBlocked = useComputed(() => activity.value.cookiePopUpBlocked); - const { totalCount } = status.value; - - if (totalCount === 0) { - const text = trackersFound ? t('activity_no_trackers_blocked') : t('activity_no_trackers'); - - return ( -

    - -

    - ); - } + const cookiePopUpBlocked = useComputed(() => activity.value.cookiePopUpBlocked?.[id]).value; + const { totalCount: totalTrackersBlocked } = status.value; + + const totalTrackersPillText = + totalTrackersBlocked === 0 + ? trackersFound + ? t('activity_no_trackers_blocked') + : t('activity_no_trackers') + : t(totalTrackersBlocked === 1 ? 'activity_countBlockedSingular' : 'activity_countBlockedPlural', { + count: String(totalTrackersBlocked), + }); return (
    - {totalCount > 0 && ( - - )} + {totalTrackersBlocked > 0 && } {cookiePopUpBlocked && }
    From 1f29ada860f4824c4b682ebb9f8a14e8b0e8a009 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Mon, 17 Nov 2025 13:38:42 -0500 Subject: [PATCH 13/17] Fix for duplicate protections_onDataUpdate --- .../app/protections/components/ProtectionsProvider.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/special-pages/pages/new-tab/app/protections/components/ProtectionsProvider.js b/special-pages/pages/new-tab/app/protections/components/ProtectionsProvider.js index 85e57edb8b..4b2b74ade1 100644 --- a/special-pages/pages/new-tab/app/protections/components/ProtectionsProvider.js +++ b/special-pages/pages/new-tab/app/protections/components/ProtectionsProvider.js @@ -1,5 +1,5 @@ import { createContext, h } from 'preact'; -import { useCallback, useEffect, useReducer, useRef } from 'preact/hooks'; +import { useCallback, useContext, useEffect, useReducer, useRef } from 'preact/hooks'; import { useMessaging } from '../../types.js'; import { reducer, useConfigSubscription, useInitialDataAndConfig } from '../../service.hooks.js'; import { ProtectionsService } from '../protections.service.js'; @@ -95,11 +95,11 @@ export function useService() { * @return {import("@preact/signals").Signal} */ export function useBlockedCount(initial) { - const service = useService(); + const service = useContext(ProtectionsServiceContext); const signal = useSignal(initial); // @todo jingram possibly refactor to include full object useSignalEffect(() => { - return service.current?.onData((evt) => { + return service?.onData((evt) => { signal.value = evt.data.totalCount; }); }); @@ -111,11 +111,11 @@ export function useBlockedCount(initial) { * @return {import("@preact/signals").Signal} */ export function useCookiePopUpsBlockedCount(initial) { - const service = useService(); + const service = useContext(ProtectionsServiceContext); const signal = useSignal(initial); useSignalEffect(() => { - return service.current?.onData((evt) => { + return service?.onData((evt) => { signal.value = evt.data.totalCookiePopUpsBlocked; }); }); From d147de45ebbb8067bdd3ecd8bbbead9600bf49b7 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Mon, 17 Nov 2025 16:34:33 -0500 Subject: [PATCH 14/17] Address PR comments --- .../app/activity/NormalizeDataProvider.js | 1 - .../app/activity/components/ActivityItem.js | 1 - .../app/components/TickPill/TickPill.module.css | 16 ++++++++-------- .../components/ProtectionsHeadingLegacy.js | 2 +- special-pages/shared/styles/variables.css | 1 + 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/special-pages/pages/new-tab/app/activity/NormalizeDataProvider.js b/special-pages/pages/new-tab/app/activity/NormalizeDataProvider.js index b222a6c4d2..5c49dab128 100644 --- a/special-pages/pages/new-tab/app/activity/NormalizeDataProvider.js +++ b/special-pages/pages/new-tab/app/activity/NormalizeDataProvider.js @@ -76,7 +76,6 @@ export function normalizeData(prev, incoming) { faviconMax: item.favicon?.maxAvailableSize ?? DDG_DEFAULT_ICON_SIZE, favoriteSrc: item.favicon?.src, trackersFound: item.trackersFound, - // cookiePopUpBlocked: item.cookiePopUpBlocked, }; const differs = shallowDiffers(next, prev.items[id] || {}); output.items[id] = differs ? next : prev.items[id] || {}; diff --git a/special-pages/pages/new-tab/app/activity/components/ActivityItem.js b/special-pages/pages/new-tab/app/activity/components/ActivityItem.js index ec4a4f9840..1ee97f6eec 100644 --- a/special-pages/pages/new-tab/app/activity/components/ActivityItem.js +++ b/special-pages/pages/new-tab/app/activity/components/ActivityItem.js @@ -6,7 +6,6 @@ import stylesLegacy from './ActivityLegacy.module.css'; import { FaviconWithState } from '../../../../../shared/components/FaviconWithState.js'; import { ACTION_ADD_FAVORITE, ACTION_REMOVE, ACTION_REMOVE_FAVORITE } from '../constants.js'; import { Star, StarFilled } from '../../components/icons/Star.js'; -import { Fire } from '../../components/icons/Fire.js'; import { Cross, FireIcon } from '../../components/Icons.js'; import { useContext } from 'preact/hooks'; import { memo } from 'preact/compat'; diff --git a/special-pages/pages/new-tab/app/components/TickPill/TickPill.module.css b/special-pages/pages/new-tab/app/components/TickPill/TickPill.module.css index 1ef1dd7490..2e4619a7d1 100644 --- a/special-pages/pages/new-tab/app/components/TickPill/TickPill.module.css +++ b/special-pages/pages/new-tab/app/components/TickPill/TickPill.module.css @@ -4,8 +4,8 @@ gap: 6px; padding: 8px 10px; border-radius: 100px; - background-color: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.12); + background-color: var(--color-white-at-3); + border: 1px solid var(--color-white-at-12); height: 20px; width: fit-content; } @@ -23,23 +23,23 @@ } .text { - font-size: 11px; + font-size: var(--small-label-font-size); font-weight: 400; line-height: 16px; - color: rgba(255, 255, 255, 0.84); + color: var(--color-white-at-84); white-space: nowrap; } /* Light mode styles */ [data-theme="light"] .tickPill { - background-color: rgba(0, 0, 0, 0.04); - border: 1px solid rgba(0, 0, 0, 0.12); + background-color: var(--color-black-at-4); + border: 1px solid var(--color-black-at-12); } [data-theme="light"] .text { - color: rgba(0, 0, 0, 0.84); + color: var(--color-black-at-84); } [data-theme="light"] .iconWrapper svg path { - fill: rgba(0, 0, 0, 0.84); + fill: var(--color-black-at-84); } diff --git a/special-pages/pages/new-tab/app/protections/components/ProtectionsHeadingLegacy.js b/special-pages/pages/new-tab/app/protections/components/ProtectionsHeadingLegacy.js index c615a68f35..3990ce8ff0 100644 --- a/special-pages/pages/new-tab/app/protections/components/ProtectionsHeadingLegacy.js +++ b/special-pages/pages/new-tab/app/protections/components/ProtectionsHeadingLegacy.js @@ -37,7 +37,7 @@ export function ProtectionsHeadingLegacy({ expansion, canExpand, blockedCountSig } else { alltimeTitle = adBlocking ? t('stats_countBlockedAdsAndTrackersPluralLegacy', { count: alltime }) - : t('stats_countBlockedPlural', { count: alltime }); + : t('stats_countBlockedPluralLegacy', { count: alltime }); } return ( diff --git a/special-pages/shared/styles/variables.css b/special-pages/shared/styles/variables.css index 62d5ebe2d4..0a80d9b4bd 100644 --- a/special-pages/shared/styles/variables.css +++ b/special-pages/shared/styles/variables.css @@ -79,6 +79,7 @@ --color-black-at-1: rgba(0, 0, 0, 0.01); --color-black-at-3: rgba(0, 0, 0, 0.03); + --color-black-at-4: rgba(0, 0, 0, 0.04); --color-black-at-6: rgba(0, 0, 0, 0.06); --color-black-at-9: rgba(0, 0, 0, 0.09); --color-black-at-12: rgba(0, 0, 0, 0.12); From 66366e3b0525c50a7e4738111cd8089b69eeb2ad Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Mon, 17 Nov 2025 18:42:55 -0500 Subject: [PATCH 15/17] Update test cases --- .../activity/components/Activity.examples.js | 2 +- .../integration-tests/activity.page.js | 2 ++ .../new-tab/app/privacy-stats/strings.json | 2 +- .../components/ProtectionsHeading.js | 4 +-- .../integrations-tests/protections.page.js | 25 +++++++++++-------- .../new-tab/public/locales/en/new-tab.json | 2 +- 6 files changed, 22 insertions(+), 15 deletions(-) diff --git a/special-pages/pages/new-tab/app/activity/components/Activity.examples.js b/special-pages/pages/new-tab/app/activity/components/Activity.examples.js index 323e782fdb..77b206dbcf 100644 --- a/special-pages/pages/new-tab/app/activity/components/Activity.examples.js +++ b/special-pages/pages/new-tab/app/activity/components/Activity.examples.js @@ -58,7 +58,7 @@ function Mock({ children, size }) { favorites: {}, urls: [], totalTrackers: 0, - cookiePopUpBlocked: null, + cookiePopUpBlocked: {}, }, { activity: mocks, urls: mocks.map((x) => x.url), totalTrackers: 0 }, ); diff --git a/special-pages/pages/new-tab/app/activity/integration-tests/activity.page.js b/special-pages/pages/new-tab/app/activity/integration-tests/activity.page.js index 1442c0ba6e..384f907ff8 100644 --- a/special-pages/pages/new-tab/app/activity/integration-tests/activity.page.js +++ b/special-pages/pages/new-tab/app/activity/integration-tests/activity.page.js @@ -308,6 +308,7 @@ export class ActivityPage { - button "Clear browsing history and data for example.com": - img - text: +1 56 tracking attempts blocked + - text: Cookie pop-up blocked - list: - listitem: - link "/bathrooms/toilets" @@ -355,6 +356,7 @@ export class ActivityPage { - button "Clear browsing history and data for example.com": - img - text: +1 56 ads + tracking attempts blocked + - text: Cookie pop-up blocked - list: - listitem: - link "/bathrooms/toilets" diff --git a/special-pages/pages/new-tab/app/privacy-stats/strings.json b/special-pages/pages/new-tab/app/privacy-stats/strings.json index 01dfb479e6..87d754dbf8 100644 --- a/special-pages/pages/new-tab/app/privacy-stats/strings.json +++ b/special-pages/pages/new-tab/app/privacy-stats/strings.json @@ -64,7 +64,7 @@ "note": "The heading indicating multiple cookie pop-ups were handled by the CPM" }, "stats_protectionsReportInfo": { - "title": "Displays tracking attempts blocked in the last 7 days and the number of cookie pop-ups blocked since you started using the browser. You can reset these stats using the Fire Button.", + "title": "Displays tracking attempts blocked in the last 7 days, and the number of cookie pop-ups blocked since you started using the browser. You can reset these stats using the Fire Button.", "note": "Text explaining how to reset the protections stats" }, "stats_feedCountBlockedSingular": { diff --git a/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.js b/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.js index 7e033c968e..eb80061831 100644 --- a/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.js +++ b/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.js @@ -74,7 +74,7 @@ export function ProtectionsHeading({ {totalTrackersBlocked === 0 &&

    {t('protections_noRecent')}

    } {totalTrackersBlocked > 0 && (

    - {totalTrackersBlocked} + {totalTrackersBlocked}{' '} {trackersBlockedHeading}

    )} @@ -87,7 +87,7 @@ export function ProtectionsHeading({ {isCpmEnabled && totalTrackersBlocked > 0 && totalCookiePopUpsBlocked > 0 && (

    - {totalCookiePopUpsBlocked} + {totalCookiePopUpsBlocked}{' '} {cookiePopUpsBlockedHeading}

    {/* @todo `NewBadgeIcon` will be manually removed in diff --git a/special-pages/pages/new-tab/app/protections/integrations-tests/protections.page.js b/special-pages/pages/new-tab/app/protections/integrations-tests/protections.page.js index 510be68bcc..82df4b2bfb 100644 --- a/special-pages/pages/new-tab/app/protections/integrations-tests/protections.page.js +++ b/special-pages/pages/new-tab/app/protections/integrations-tests/protections.page.js @@ -43,7 +43,7 @@ export class ProtectionsPage { totalCookiePopUpsBlocked: null, // null means CPM is not enabled }; await this.ntp.mocks.simulateSubscriptionMessage(named.subscription('protections_onDataUpdate'), data); - await expect(this.context().getByRole('heading', { level: 3 })).toContainText(`${count} tracking attempts blocked`); + await expect(this.context().getByRole('heading', { level: 3 })).toContainText(`${count} Tracking attempts blocked`); } async hasPolishText() { @@ -51,10 +51,12 @@ export class ProtectionsPage { await expect(heading).toMatchAriaSnapshot(` - img "Privacy Shield" - heading "Raport ochrony" [level=2] + - img - button "Ukryj ostatnią aktywność" [expanded] [pressed]: - img - - heading /\\d+ – tyle prób śledzenia zablokowano/ [level=3] - - paragraph: Ostatnie 7 dni + - heading /\\d+ {count} – tyle prób śledzenia zablokowano/ [level=3] + - heading /\\d+ Cookie pop-ups blocked/ [level=3] + - img `); } @@ -68,9 +70,10 @@ export class ProtectionsPage { totalCookiePopUpsBlocked: 25, }; await this.ntp.mocks.simulateSubscriptionMessage(named.subscription('protections_onDataUpdate'), data); - await expect(this.context().getByRole('heading', { level: 3 }).first()).toContainText('100 tracking attempts blocked'); - // Cookie popup stats should be visible - await expect(this.context().getByText(/cookie pop-ups?/i)).toBeVisible(); + await expect(this.context().getByRole('heading', { level: 3 }).first()).toContainText('100 Tracking attempts blocked'); + // Cookie popup stats should be visible in the ProtectionsHeading + const heading = this.context().getByTestId('ProtectionsHeading'); + await expect(heading.getByText(/cookie pop-up/i)).toBeVisible(); } /** @@ -83,8 +86,9 @@ export class ProtectionsPage { totalCookiePopUpsBlocked: null, }; await this.ntp.mocks.simulateSubscriptionMessage(named.subscription('protections_onDataUpdate'), data); - // Cookie popup stats should not be visible - await expect(this.context().getByText(/cookie pop-ups?/i)).not.toBeVisible(); + // Cookie popup stats should not be visible in the ProtectionsHeading + const heading = this.context().getByTestId('ProtectionsHeading'); + await expect(heading.getByText(/cookie pop-up/i)).not.toBeVisible(); } /** @@ -97,8 +101,9 @@ export class ProtectionsPage { totalCookiePopUpsBlocked: 0, }; await this.ntp.mocks.simulateSubscriptionMessage(named.subscription('protections_onDataUpdate'), data); - // Cookie popup stats should not be visible when count is 0 - await expect(this.context().getByText(/cookie pop-ups?/i)).not.toBeVisible(); + // Cookie popup stats should not be visible when count is 0 in the ProtectionsHeading + const heading = this.context().getByTestId('ProtectionsHeading'); + await expect(heading.getByText(/cookie pop-up/i)).not.toBeVisible(); } /** diff --git a/special-pages/pages/new-tab/public/locales/en/new-tab.json b/special-pages/pages/new-tab/public/locales/en/new-tab.json index 420cb0ae7e..9c2c8e9561 100644 --- a/special-pages/pages/new-tab/public/locales/en/new-tab.json +++ b/special-pages/pages/new-tab/public/locales/en/new-tab.json @@ -126,7 +126,7 @@ "note": "The heading indicating multiple cookie pop-ups were handled by the CPM" }, "stats_protectionsReportInfo": { - "title": "Displays tracking attempts blocked in the last 7 days and the number of cookie pop-ups blocked since you started using the browser. You can reset these stats using the Fire Button.", + "title": "Displays tracking attempts blocked in the last 7 days, and the number of cookie pop-ups blocked since you started using the browser. You can reset these stats using the Fire Button.", "note": "Text explaining how to reset the protections stats" }, "stats_feedCountBlockedSingular": { From ee3a4ae1150f5702ceb07fdcfbe232a452e6fd14 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Tue, 18 Nov 2025 11:16:37 -0500 Subject: [PATCH 16/17] Fix no message for zero trackers Bug: Tracker Status UI: No Message for Zero Trackers. The TrackerStatus function computes totalTrackersPillText for the zero-trackers case but never displays it. When totalTrackersBlocked === 0, the condition {totalTrackersBlocked > 0 && } prevents rendering any tracker status message. The legacy TrackerStatusLegacy displays "No trackers blocked" or "No trackers found" in this scenario, but the new implementation shows nothing, breaking the expected UI behavior for sites with no blocked trackers. https://github.com/duckduckgo/content-scope-scripts/pull/2039#discussion_r2535841464 --- .../app/activity/components/Activity.js | 2 +- .../integration-tests/activity.page.js | 35 +++++++++++++++++++ .../integration-tests/activity.spec.js | 8 +++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/special-pages/pages/new-tab/app/activity/components/Activity.js b/special-pages/pages/new-tab/app/activity/components/Activity.js index f346baa8d9..9a40ad54c2 100644 --- a/special-pages/pages/new-tab/app/activity/components/Activity.js +++ b/special-pages/pages/new-tab/app/activity/components/Activity.js @@ -253,7 +253,7 @@ function TrackerStatus({ id, trackersFound }) { return (
    - {totalTrackersBlocked > 0 && } + {cookiePopUpBlocked && }
    diff --git a/special-pages/pages/new-tab/app/activity/integration-tests/activity.page.js b/special-pages/pages/new-tab/app/activity/integration-tests/activity.page.js index 384f907ff8..a6f0ecf132 100644 --- a/special-pages/pages/new-tab/app/activity/integration-tests/activity.page.js +++ b/special-pages/pages/new-tab/app/activity/integration-tests/activity.page.js @@ -395,6 +395,41 @@ export class ActivityPage { `); } + /** + * Test that the new TrackerStatus component displays zero-tracker messages when CPM is enabled + */ + async showsZeroTrackerMessagesWithCpm() { + // Twitter has trackersFound: true, totalCount: 0 → should show "No trackers blocked" + await expect(this.context().getByTestId('ActivityItem').nth(3)).toMatchAriaSnapshot(` + - listitem: + - link "twitter.com" + - button "Add twitter.com to favorites": + - img + - button "Clear browsing history and data for twitter.com": + - img + - text: No trackers blocked + - list: + - listitem: + - link "Trending Topics" + - text: 2 days ago + `); + + // LinkedIn has trackersFound: false, totalCount: 0 → should show "No trackers found" + await expect(this.context().getByTestId('ActivityItem').nth(4)).toMatchAriaSnapshot(` + - listitem: + - link "app.linkedin.com" + - button "Add app.linkedin.com to favorites": + - img + - button "Clear browsing history and data for app.linkedin.com": + - img + - text: No trackers found + - list: + - listitem: + - link "Profile Page" + - text: 2 hrs ago + `); + } + async hasEmptyTrackersOnlyTitle() { const { page } = this; await expect(page.getByTestId('ActivityHeading')).toMatchAriaSnapshot(` diff --git a/special-pages/pages/new-tab/app/activity/integration-tests/activity.spec.js b/special-pages/pages/new-tab/app/activity/integration-tests/activity.spec.js index 6faca16187..86fc6be5d2 100644 --- a/special-pages/pages/new-tab/app/activity/integration-tests/activity.spec.js +++ b/special-pages/pages/new-tab/app/activity/integration-tests/activity.spec.js @@ -116,6 +116,14 @@ test.describe('activity widget', () => { await ap.didRender(); await ap.hidesCookiePopupIndicatorWhenNotBlocked(); }); + test('shows zero-tracker messages with new component when CPM enabled', async ({ page }, workerInfo) => { + const ntp = NewtabPage.create(page, workerInfo); + const ap = new ActivityPage(page, ntp); + await ntp.reducedMotion(); + await ntp.openPage({ additional: { ...defaultPageParams, cpm: 'true' } }); + await ap.didRender(); + await ap.showsZeroTrackerMessagesWithCpm(); + }); test('after rendering and navigating to a new tab, data is re-requested on return', async ({ page }, workerInfo) => { const ntp = NewtabPage.create(page, workerInfo); const ap = new ActivityPage(page, ntp); From 3507782eabe7f31603e90d252d283c39820136f9 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Tue, 18 Nov 2025 13:24:21 -0500 Subject: [PATCH 17/17] Make tooltip accessible and adjust tests --- .../new-tab/app/components/Tooltip/Tooltip.js | 33 ++++++++++++--- .../app/components/Tooltip/Tooltip.module.css | 7 ++++ .../components/ProtectionsHeading.js | 1 - .../integrations-tests/protections.page.js | 41 ++++++++++++++++++- 4 files changed, 74 insertions(+), 8 deletions(-) diff --git a/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.js b/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.js index c6f684d11b..42e909a79c 100644 --- a/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.js +++ b/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.js @@ -1,10 +1,10 @@ import { h } from 'preact'; -import { useState } from 'preact/hooks'; +import { useState, useId } from 'preact/hooks'; import styles from './Tooltip.module.css'; import cn from 'classnames'; /** - * A tooltip component that appears on hover + * A tooltip component that appears on hover and keyboard focus * @param {Object} props * @param {import('preact').ComponentChildren} props.children - The element that triggers the tooltip * @param {string} props.content - The tooltip content text @@ -12,15 +12,38 @@ import cn from 'classnames'; */ export function Tooltip({ children, content, className }) { const [isVisible, setIsVisible] = useState(false); + const tooltipId = useId(); + + const showTooltip = () => setIsVisible(true); + const hideTooltip = () => setIsVisible(false); + + const handleKeyDown = (e) => { + // Show/hide tooltip on Enter or Space key + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setIsVisible((prev) => !prev); + } + + // Hide tooltip on Escape key + if (e.key === 'Escape') { + hideTooltip(); + } + }; return (
    setIsVisible(true)} - onMouseLeave={() => setIsVisible(false)} + role="button" + tabIndex={0} + aria-describedby={isVisible ? tooltipId : undefined} + onMouseEnter={showTooltip} + onMouseLeave={hideTooltip} + onFocus={showTooltip} + onBlur={hideTooltip} + onKeyDown={handleKeyDown} > {children} - {isVisible &&