From 10404e6933c77055404600a1a7202b7e33eb4700 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Fri, 24 Oct 2025 10:49:56 -0400 Subject: [PATCH 01/12] 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 26cbd21f2aaa3cfeac78ea5187abf0e97549eaf7 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Fri, 24 Oct 2025 13:44:40 -0400 Subject: [PATCH 02/12] 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 602facd33cc236294da049a73b3e14728d703600 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Fri, 24 Oct 2025 18:22:26 -0400 Subject: [PATCH 03/12] 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 6c1fc5672f507210ff2c50254c08495fe99be025 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Fri, 24 Oct 2025 18:47:02 -0400 Subject: [PATCH 04/12] 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 46a03c6c0ba0031bb14656934d84a7f27b947382 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Mon, 27 Oct 2025 19:23:44 -0400 Subject: [PATCH 05/12] 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 f2cfcec12705a8599ac724dfb28adcf8a2d892d9 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Mon, 27 Oct 2025 19:27:24 -0400 Subject: [PATCH 06/12] 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 715b40155673804f1ef528e8b200d2cae4b0fbb9 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Mon, 27 Oct 2025 20:17:33 -0400 Subject: [PATCH 07/12] 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 ac43b372b84bba974af2919a77b68741b2f99703 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Tue, 28 Oct 2025 12:38:56 -0400 Subject: [PATCH 09/12] 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 c8ad930bf3f3e6358d330adaff8c5576d4864733 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Wed, 29 Oct 2025 14:49:13 -0400 Subject: [PATCH 10/12] 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 e596630a5d7d19555ec139ebb8afe3622ee21950 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Wed, 29 Oct 2025 20:34:01 -0400 Subject: [PATCH 11/12] 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 02438b39419200497479c3e3122c7fea60f1c306 Mon Sep 17 00:00:00 2001 From: Jason Ingram Date: Wed, 29 Oct 2025 20:53:15 -0400 Subject: [PATCH 12/12] 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 && }