+
+
{icons}
{otherIcon}
-
+
{adBlocking ? (
) : (
-
+
)}
@@ -263,8 +354,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();
@@ -274,7 +366,7 @@ export function ActivityConsumer({ showBurnAnimation }) {
return (
-
+
);
@@ -283,7 +375,7 @@ export function ActivityConsumer({ showBurnAnimation }) {
-
+
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..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
@@ -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:not(:only-child) {
+ 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/components/ActivityItem.js b/special-pages/pages/new-tab/app/activity/components/ActivityItem.js
index 34ba648d16..ec4a4f9840 100644
--- a/special-pages/pages/new-tab/app/activity/components/ActivityItem.js
+++ b/special-pages/pages/new-tab/app/activity/components/ActivityItem.js
@@ -2,11 +2,12 @@ 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';
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';
@@ -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.
*
@@ -97,7 +139,7 @@ function Controls({ canBurn, url, title }) {
value={url}
type="button"
>
- {canBurn ?
:
}
+ {canBurn ?
:
}
);
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/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/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 c8a78a5d81..b789f047f1 100644
--- a/special-pages/pages/new-tab/app/activity/strings.json
+++ b/special-pages/pages/new-tab/app/activity/strings.json
@@ -16,9 +16,21 @@
"note": "Placeholder message indicating that no trackers are blocked"
},
"activity_countBlockedPlural": {
+ "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'"
+ },
+ "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.",
"note": "Shown in the place a list of browsing history entries will be displayed."
diff --git a/special-pages/pages/new-tab/app/components/Icons.js b/special-pages/pages/new-tab/app/components/Icons.js
index 791d1bd889..e0aab7291a 100644
--- a/special-pages/pages/new-tab/app/components/Icons.js
+++ b/special-pages/pages/new-tab/app/components/Icons.js
@@ -601,3 +601,85 @@ 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 (
+
+
+
+
+
+ );
+}
+
+/**
+ * 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 (
+
+
+
+ );
+}
+
+/**
+ * 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 1bbb2bb812..8989bac0ca 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,18 @@
}
}
+/* 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;
+}
+/* FireIcon styles */
+.fireIcon {
+ color: currentColor;
+}
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..39e49e8d70
--- /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 {boolean} [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/app/components/Tooltip/Tooltip.js b/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.js
new file mode 100644
index 0000000000..c6f684d11b
--- /dev/null
+++ b/special-pages/pages/new-tab/app/components/Tooltip/Tooltip.js
@@ -0,0 +1,26 @@
+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 (
+
+ );
+}
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..bb3c136461
--- /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 300ms ease;
+
+ & 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/components/PrivacyStats.module.css b/special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStats.module.css
index de482e1342..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
@@ -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,25 @@
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;
+}
+
+.infoIcon {
+ width: 16px;
+ height: 16px;
+ display: inline-block;
+ vertical-align: middle;
}
.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 +62,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/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 2b5ed4ba65..01dfb479e6 100644
--- a/special-pages/pages/new-tab/app/privacy-stats/strings.json
+++ b/special-pages/pages/new-tab/app/privacy-stats/strings.json
@@ -16,10 +16,18 @@
"note": "Placeholder to indicate that no tracking activity was blocked in the last 7 days"
},
"stats_countBlockedSingular": {
+ "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'"
},
@@ -32,13 +40,33 @@
"note": "Placeholder to indicate that no ads or tracking activity was blocked in the last 7 days"
},
"stats_countBlockedAdsAndTrackersSingular": {
+ "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"
+ },
+ "stats_totalCookiePopUpsBlockedPlural": {
+ "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/Protections.js b/special-pages/pages/new-tab/app/protections/components/Protections.js
index 32e5c584c6..038b40b804 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,10 +23,20 @@ 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();
+ const totalCookiePopUpsBlocked = totalCookiePopUpsBlockedSignal.value;
const attrs = useMemo(() => {
return {
@@ -36,13 +47,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/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..e7f0e16702 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,8 @@ 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' && (
-
+
)}
{config.feed === 'privacy-stats' && (
diff --git a/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.examples.js b/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.examples.js
index d0cfb21218..93c0aeef71 100644
--- a/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.examples.js
+++ b/special-pages/pages/new-tab/app/protections/components/ProtectionsHeading.examples.js
@@ -21,7 +21,16 @@ export const protectionsHeadingExamples = {
- {(/** @type {Mock} */ { expansion, feed, setFeed, blockedCountSignal, toggle }) => {
+ {(
+ /** @type {Mock} */ {
+ expansion,
+ feed,
+ setFeed,
+ blockedCountSignal,
+ toggle,
+ totalCookiePopUpsBlockedSignal,
+ },
+ ) => {
return (
@@ -38,7 +48,16 @@ export const protectionsHeadingExamples = {
}}
- {(/** @type {Mock} */ { expansion, feed, setFeed, blockedCountSignal, toggle }) => {
+ {(
+ /** @type {Mock} */ {
+ expansion,
+ feed,
+ setFeed,
+ blockedCountSignal,
+ toggle,
+ totalCookiePopUpsBlockedSignal,
+ },
+ ) => {
return (
@@ -59,7 +79,16 @@ export const protectionsHeadingExamples = {
- {(/** @type {Mock} */ { expansion, feed, setFeed, blockedCountSignal, toggle }) => {
+ {(
+ /** @type {Mock} */ {
+ expansion,
+ feed,
+ setFeed,
+ blockedCountSignal,
+ toggle,
+ totalCookiePopUpsBlockedSignal,
+ },
+ ) => {
return (
@@ -74,7 +104,16 @@ export const protectionsHeadingExamples = {
}}
- {(/** @type {Mock} */ { expansion, feed, setFeed, blockedCountSignal, toggle }) => {
+ {(
+ /** @type {Mock} */ {
+ expansion,
+ feed,
+ setFeed,
+ blockedCountSignal,
+ toggle,
+ totalCookiePopUpsBlockedSignal,
+ },
+ ) => {
return (
@@ -89,7 +129,16 @@ export const protectionsHeadingExamples = {
}}
- {(/** @type {Mock} */ { expansion, feed, setFeed, blockedCountSignal, toggle }) => {
+ {(
+ /** @type {Mock} */ {
+ expansion,
+ feed,
+ setFeed,
+ blockedCountSignal,
+ toggle,
+ totalCookiePopUpsBlockedSignal,
+ },
+ ) => {
return (
@@ -104,7 +154,16 @@ export const protectionsHeadingExamples = {
}}
- {(/** @type {Mock} */ { expansion, feed, setFeed, blockedCountSignal, toggle }) => {
+ {(
+ /** @type {Mock} */ {
+ expansion,
+ feed,
+ setFeed,
+ blockedCountSignal,
+ toggle,
+ totalCookiePopUpsBlockedSignal,
+ },
+ ) => {
return (
@@ -119,7 +179,16 @@ export const protectionsHeadingExamples = {
}}
- {(/** @type {Mock} */ { expansion, feed, setFeed, blockedCountSignal, toggle }) => {
+ {(
+ /** @type {Mock} */ {
+ expansion,
+ feed,
+ setFeed,
+ blockedCountSignal,
+ toggle,
+ totalCookiePopUpsBlockedSignal,
+ },
+ ) => {
return (
@@ -162,6 +232,7 @@ function PrintState(props) {
* @property {() => void} toggle
* @property {import('../../../types/new-tab.js').Expansion} expansion
* @property {import('@preact/signals').Signal} blockedCountSignal
+ * @property {import('@preact/signals').Signal} totalCookiePopUpsBlockedSignal
* @property {FeedType} feed
* @property {(f: FeedType) => void} setFeed
*
@@ -175,6 +246,7 @@ const MockWithState = ({ children, initial = 0, feedType = 'privacy-stats', inte
const [feed, setFeed] = useState(feedType);
const [expansion, setExpansion] = useState(/** @type {import('../../../types/new-tab.js').Expansion} */ ('expanded'));
const signal = useSignal(initial);
+ const totalCookiePopUpsBlockedSignal = useSignal(/** @type {number | null | undefined} */ (null));
useEffect(() => {
if (interval === 0) return;
const int = setInterval(() => (signal.value += 1), interval);
@@ -183,5 +255,5 @@ const MockWithState = ({ children, initial = 0, feedType = 'privacy-stats', inte
const toggle = () => {
setExpansion((old) => (old === 'expanded' ? 'collapsed' : 'expanded'));
};
- return children({ toggle, expansion, feed, setFeed, blockedCountSignal: signal });
+ return children({ toggle, expansion, feed, setFeed, blockedCountSignal: signal, totalCookiePopUpsBlockedSignal });
};
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..7e033c968e 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,10 @@
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 { Tooltip } from '../../components/Tooltip/Tooltip.js';
/**
* @import enStrings from "../strings.json"
@@ -20,33 +17,43 @@ 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 ?? 0;
- 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 });
- }
+ // 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') : t('stats_countBlockedPlural');
+
+ const cookiePopUpsBlockedHeading =
+ totalCookiePopUpsBlocked === 1 ? t('stats_totalCookiePopUpsBlockedSingular') : t('stats_totalCookiePopUpsBlockedPlural');
return (
-
+
{t('protections_menuTitle')}
+
+ {/* @todo accessibility: move focus to content on hover? */}
+
+
+
+
{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 */}
+ {isCpmEnabled && totalTrackersBlocked > 0 && totalCookiePopUpsBlocked > 0 && (
+
+
+ {totalCookiePopUpsBlocked}
+ {cookiePopUpsBlockedHeading}
+
+ {/* @todo `NewBadgeIcon` will be manually removed in
+ a future iteration */}
+
+
)}
-
{t('stats_feedCountBlockedPeriod')}
);
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 (
+
+
+
+
+
+
{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 1569ebfec3..85e57edb8b 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;
@@ -104,3 +105,20 @@ export function useBlockedCount(initial) {
});
return signal;
}
+
+/**
+ * @param {number | null | undefined} 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/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();
+ });
});
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..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,12 +80,31 @@ 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;
}
if (url.searchParams.get('activity') === 'empty') {
dataset.totalCount = 0;
}
+ 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
+ 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 19ebdfde9e..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
@@ -5,11 +5,14 @@
export const protectionsMocks = {
empty: {
totalCount: 0,
+ totalCookiePopUpsBlocked: 0,
},
few: {
totalCount: 86,
+ totalCookiePopUpsBlocked: 21,
},
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/activity.json b/special-pages/pages/new-tab/messages/types/activity.json
index 34382b3eb8..6b34a14aa2 100644
--- a/special-pages/pages/new-tab/messages/types/activity.json
+++ b/special-pages/pages/new-tab/messages/types/activity.json
@@ -45,6 +45,17 @@
},
"favorite": {
"type": "boolean"
+ },
+ "cookiePopUpBlocked": {
+ "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"]
@@ -91,4 +102,4 @@
"required": ["title", "url", "relativeTime"]
}
}
-}
\ No newline at end of file
+}
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..c1f7b4010d 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,17 @@
"totalCount": {
"description": "Total number of trackers or ads blocked since install",
"type": "number"
+ },
+ "totalCookiePopUpsBlocked": {
+ "description": "Total number of cookie pop-ups blocked since install",
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "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..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
@@ -78,10 +78,18 @@
"note": "Placeholder to indicate that no tracking activity was blocked in the last 7 days"
},
"stats_countBlockedSingular": {
+ "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'"
},
@@ -94,13 +102,33 @@
"note": "Placeholder to indicate that no ads or tracking activity was blocked in the last 7 days"
},
"stats_countBlockedAdsAndTrackersSingular": {
+ "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"
+ },
+ "stats_totalCookiePopUpsBlockedPlural": {
+ "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."
@@ -410,9 +438,21 @@
"note": "Placeholder message indicating that no trackers are blocked"
},
"activity_countBlockedPlural": {
+ "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'"
+ },
+ "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.",
"note": "Shown in the place a list of browsing history entries will be displayed."
diff --git a/special-pages/pages/new-tab/types/new-tab.ts b/special-pages/pages/new-tab/types/new-tab.ts
index 560fc8bd02..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: {
@@ -968,6 +972,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?: null | number;
}
/**
* Generated from @see "../messages/rmf_getData.request.json"
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);