Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -224,12 +224,25 @@ export function SignalStateProvider({ children }) {
if (!service) return console.warn('could not access service');
const src = /** @type {import("./batched-activity.service.js").BatchedActivityService} */ (service);
const unsub = src.onData((evt) => {
console.log('[NormalizeDataProvider] Received activity data update:', {
source: evt.source,
activityCount: evt.data.activity.length,
totalTrackers: evt.data.totalTrackers,
urls: evt.data.urls.slice(0, 5), // Log first 5 URLs
});
batch(() => {
const oldActivity = activity.value;
activity.value = normalizeData(activity.value, {
activity: evt.data.activity,
urls: evt.data.urls,
totalTrackers: evt.data.totalTrackers,
});
console.log('[NormalizeDataProvider] Activity data normalized:', {
oldTotalTrackers: oldActivity.totalTrackers,
newTotalTrackers: activity.value.totalTrackers,
oldItemCount: Object.keys(oldActivity.items).length,
newItemCount: Object.keys(activity.value.items).length,
});
const visible = keys.value;
const all = activity.value.urls;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ export class BatchedActivityService {
},
subscribe: (cb) => {
const sub1 = ntp.messaging.subscribe('activity_onDataUpdate', (params) => {
console.log('[BatchedActivity] activity_onDataUpdate received:', {
activityCount: params.activity.length,
totalTrackers: params.activity.reduce((acc, item) => acc + item.trackingStatus.totalCount, 0),
urls: params.activity.map((x) => x.url),
});
cb({
activity: params.activity,
urls: params.activity.map((x) => x.url),
Expand All @@ -63,6 +68,12 @@ export class BatchedActivityService {
});
const sub2 = ntp.messaging.subscribe('activity_onDataPatch', (params) => {
const totalTrackers = params.totalTrackersBlocked;
console.log('[BatchedActivity] activity_onDataPatch received:', {
hasPatch: 'patch' in params && params.patch !== null,
totalTrackers,
urls: params.urls,
patchUrl: params.patch?.url,
});
if ('patch' in params && params.patch !== null) {
cb({ activity: [/** @type {DomainActivity} */ (params.patch)], urls: params.urls, totalTrackers });
} else {
Expand Down Expand Up @@ -91,6 +102,7 @@ export class BatchedActivityService {
/** @type {EventTarget|null} */
this.burns = new EventTarget();
this.burnUnsub = this.ntp.messaging.subscribe('activity_onBurnComplete', () => {
console.log('[BatchedActivity] activity_onBurnComplete received from native');
this.burns?.dispatchEvent(new CustomEvent('activity_onBurnComplete'));
});
}
Expand Down Expand Up @@ -216,10 +228,12 @@ export class BatchedActivityService {
}

enableBroadcast() {
console.log('[BatchedActivity] enableBroadcast called');
this.dataService.enableBroadcast();
this.dataService.flush();
}
disableBroadcast() {
console.log('[BatchedActivity] disableBroadcast called');
this.dataService.disableBroadcast();
}
}
69 changes: 58 additions & 11 deletions special-pages/pages/new-tab/app/activity/components/Activity.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ 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 { useContext, useEffect, useRef, useState } from 'preact/hooks';
import { memo } from 'preact/compat';
import { ActivityContext, ActivityServiceContext } from '../ActivityProvider.js';
import { useTypedTranslationWith } from '../../types.js';
Expand All @@ -14,7 +14,7 @@ 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';
import { useComputed, useSignalEffect } from '@preact/signals';
import { ActivityItemAnimationWrapper } from './ActivityItemAnimationWrapper.js';
import { useDocumentVisibility } from '../../../../../shared/components/DocumentVisibility.js';
import { HistoryItems, HistoryItemsLegacy } from './HistoryItems.js';
Expand Down Expand Up @@ -252,24 +252,71 @@ const DDG_MAX_TRACKER_ICONS = 3;
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?.[id]).value;
const { totalCount: totalTrackersBlocked } = status.value;

const totalTrackersPillText =
totalTrackersBlocked === 0
// Track activity.value directly to ensure we react to any changes
// When normalizeData updates activity.value (line 228 in NormalizeDataProvider),
// this computed will re-evaluate because activity.value is a new object reference
const activityData = useComputed(() => activity.value);

// Use computed to reactively track trackingStatus changes
// Access activityData.value.trackingStatus[id] to ensure we track the nested property
const trackingStatus = useComputed(() => {
const status = activityData.value.trackingStatus[id];
// Provide default if trackingStatus hasn't been populated yet
// This handles the case where a site is first logged but trackingStatus hasn't been updated
return status || { totalCount: 0, trackerCompanies: [] };
});

// Make text computation reactive so it updates when trackingStatus changes
const totalTrackersBlocked = useComputed(() => trackingStatus.value.totalCount);
const totalTrackersPillText = useComputed(() => {
const count = totalTrackersBlocked.value;
return count === 0
? trackersFound
? t('activity_no_trackers_blocked')
: t('activity_no_trackers')
: t(totalTrackersBlocked === 1 ? 'activity_countBlockedSingular' : 'activity_countBlockedPlural', {
count: String(totalTrackersBlocked),
: t(count === 1 ? 'activity_countBlockedSingular' : 'activity_countBlockedPlural', {
count: String(count),
});
});
const cookiePopUpBlocked = useComputed(() => activityData.value.cookiePopUpBlocked?.[id]);

// Force re-render when activityData changes by tracking it with useSignalEffect
// This ensures TickPill updates when trackingStatus data arrives
// The key insight: activityData.value changes when normalizeData creates a new object,
// so tracking activityData ensures we react to any updates
const [, setRenderKey] = useState(0);

// Track activityData changes - this computed changes when activity.value changes
// which happens when normalizeData runs (line 228 in NormalizeDataProvider)
useSignalEffect(() => {
// Access activityData.value to ensure Preact Signals tracks it
// This will run whenever activity.value changes, even if trackingStatus[id]
// is updated within the same normalizeData call
const currentData = activityData.value;

// Access the specific properties we care about
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
currentData.trackingStatus[id];
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
currentData.cookiePopUpBlocked?.[id];

// Force re-render by updating state
// This ensures the component updates when trackingStatus data arrives
setRenderKey((prev) => prev + 1);
});

// Access computed values during render to ensure Preact Signals tracks them
// This is the key - accessing .value in JSX should trigger reactivity
const pillText = totalTrackersPillText.value;
const showTick = totalTrackersBlocked.value > 0;
const showCookiePill = cookiePopUpBlocked.value;

return (
<div class={styles.companiesIconRow} data-testid="TrackerStatus">
<div class={styles.companiesText}>
<TickPill text={totalTrackersPillText} displayTick={totalTrackersBlocked > 0} />
{cookiePopUpBlocked && <TickPill text={t('activity_cookiePopUpBlocked')} />}
<TickPill text={pillText} displayTick={showTick} />
{showCookiePill && <TickPill text={t('activity_cookiePopUpBlocked')} />}
</div>
</div>
);
Expand Down
9 changes: 8 additions & 1 deletion special-pages/pages/new-tab/app/burning/BurnProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,15 @@ export function BurnProvider({ children, service, showBurnAnimation = true }) {
if (burning.value.length > 0 || exiting.value.length > 0) return;

const value = button.value;
console.log('[BurnProvider] Starting burn for:', value);
const response = await service?.confirmBurn(value);
if (response && response.action === 'none') return;
if (response && response.action === 'none') {
console.log('[BurnProvider] Burn cancelled by user');
return;
}

// stop the service broadcasting any updates for a moment
console.log('[BurnProvider] Disabling broadcast and marking as burning');
service.disableBroadcast();

// mark this item as burning - this will prevent further events until we're done
Expand All @@ -65,9 +70,11 @@ export function BurnProvider({ children, service, showBurnAnimation = true }) {
// but don't wait any longer than 3 seconds
const withTimer = any(required, timer(3000));

console.log('[BurnProvider] Waiting for FE and native signals...');
// exec the chain
await toPromise(withTimer);

console.log('[BurnProvider] Burn complete, clearing state and re-enabling broadcast');
// when we get here, clear out all state
batch(() => {
exiting.value = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
align-items: center;
gap: 6px;
padding: 8px 10px;
border-radius: 100px;
border-radius: 1000px;
background-color: var(--color-white-at-3);
border: 1px solid var(--color-white-at-12);
height: 20px;
Expand Down Expand Up @@ -32,8 +32,8 @@

/* Light mode styles */
[data-theme="light"] .tickPill {
background-color: var(--color-black-at-4);
border: 1px solid var(--color-black-at-12);
background-color: var(--color-black-at-1);
border: 1px solid var(--color-black-at-9);
}

[data-theme="light"] .text {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
height: 16px;
display: inline-block;
vertical-align: middle;
margin-top: 2px;
}

.widgetExpander {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ProtectionsHeadingLegacy } from './ProtectionsHeadingLegacy';
* @param {object} props
* @param {Expansion} props.expansion
* @param {import("@preact/signals").Signal<number>} props.blockedCountSignal
* @param {import("@preact/signals").Signal<boolean>} [props.skipAnimationSignal]
* @param {ProtectionsConfig['feed']} props.feed
* @param {(feed: ProtectionsConfig['feed']) => void} props.setFeed
* @param {import("preact").ComponentChild} [props.children]
Expand All @@ -29,6 +30,7 @@ export function Protections({
expansion = 'expanded',
children,
blockedCountSignal,
skipAnimationSignal,
feed,
toggle,
setFeed,
Expand Down Expand Up @@ -64,6 +66,7 @@ export function Protections({
) : (
<ProtectionsHeading
blockedCountSignal={blockedCountSignal}
skipAnimationSignal={skipAnimationSignal}
onToggle={toggle}
expansion={expansion}
canExpand={true}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
}

.block {
margin-top: 32px;
margin-top: 16px;
}

/* @todo legacyProtections: Remove once all platforms support the new UI */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useContext } from 'preact/hooks';
import { ProtectionsContext, useBlockedCount, useCookiePopUpsBlockedCount } from './ProtectionsProvider.js';
import { useContext, useEffect } from 'preact/hooks';
import { ProtectionsContext, useBlockedCount, useCookiePopUpsBlockedCount, ProtectionsServiceContext } from './ProtectionsProvider.js';
import { h } from 'preact';
import { Protections } from './Protections.js';
import { ActivityProvider } from '../../activity/ActivityProvider.js';
import { ActivityProvider, ActivityServiceContext } from '../../activity/ActivityProvider.js';
import { ActivityConsumer } from '../../activity/components/Activity.js';
import { PrivacyStatsProvider } from '../../privacy-stats/components/PrivacyStatsProvider.js';
import { BodyExpanderProvider } from '../../privacy-stats/components/BodyExpansionProvider.js';
Expand Down Expand Up @@ -32,19 +32,102 @@ export function ProtectionsConsumer() {
return null;
}

/**
* Bridge component that connects burn complete events to protections data refresh.
* This must be inside ActivityProvider to have access to ActivityServiceContext.
*/
function BurnToProtectionsDataBridge() {
const activityService = useContext(ActivityServiceContext);
const protectionsService = useContext(ProtectionsServiceContext);

useEffect(() => {
if (!activityService || !protectionsService) {
console.log('[BurnToProtectionsDataBridge] Missing service:', {
hasActivityService: !!activityService,
hasProtectionsService: !!protectionsService,
});
return;
}

// WORKAROUND: Native does not update protections data after burn (confirmed via polling).
// Instead, listen for activity data updates (which ARE correct) and sync protections data to match.
console.log('[BurnToProtectionsDataBridge] Setting up activity data listener to sync protections');

let burnInProgress = false;

const handleBurnStart = () => {
burnInProgress = true;
console.log('[BurnToProtectionsDataBridge] Burn started, waiting for activity data update...');
};

const unsubscribeActivityData = activityService.onData((evt) => {
if (!burnInProgress) return;

// Activity data has been updated after burn
const activityTotalTrackers = evt.data.totalTrackers;

// Calculate total cookie pop-ups blocked from activity data
// Count how many domains have cookiePopUpBlocked === true
const activityTotalCookiePopUps = evt.data.activity.filter(
(domain) => domain.cookiePopUpBlocked === true
).length;

const protectionsTotalCount = protectionsService.dataService?.data?.totalCount;
const protectionsTotalCookiePopUps = protectionsService.dataService?.data?.totalCookiePopUpsBlocked;

console.log('[BurnToProtectionsDataBridge] Activity data updated after burn:', {
activityTotalTrackers,
activityTotalCookiePopUps,
protectionsTotalCount,
protectionsTotalCookiePopUps,
needsTrackerSync: activityTotalTrackers !== protectionsTotalCount,
needsCookieSync: activityTotalCookiePopUps !== protectionsTotalCookiePopUps,
});

// Sync both trackers and cookie pop-ups if needed
if (activityTotalTrackers !== protectionsTotalCount || activityTotalCookiePopUps !== protectionsTotalCookiePopUps) {
console.log('[BurnToProtectionsDataBridge] Syncing protections data from activity data');
protectionsService.dataService.update((old) => ({
...old,
totalCount: activityTotalTrackers,
totalCookiePopUpsBlocked: activityTotalCookiePopUps,
}));
}

burnInProgress = false;
});

if (activityService.burns) {
activityService.burns.addEventListener('activity_onBurnComplete', handleBurnStart);
}

console.log('[BurnToProtectionsDataBridge] Bridge initialized');

return () => {
unsubscribeActivityData();
if (activityService.burns) {
activityService.burns.removeEventListener('activity_onBurnComplete', handleBurnStart);
}
};
}, [activityService, protectionsService]);

return null; // This component doesn't render anything
}

/**
* @param {object} props
* @param {ProtectionsData} props.data
* @param {ProtectionsConfig} props.config
*/
function ProtectionsReadyState({ data, config }) {
const { toggle, setFeed } = useContext(ProtectionsContext);
const blockedCountSignal = useBlockedCount(data.totalCount);
const { signal: blockedCountSignal, skipAnimation: skipAnimationSignal } = useBlockedCount(data.totalCount);
const totalCookiePopUpsBlockedSignal = useCookiePopUpsBlockedCount(data.totalCookiePopUpsBlocked);

return (
<Protections
blockedCountSignal={blockedCountSignal}
skipAnimationSignal={skipAnimationSignal}
expansion={config.expansion}
toggle={toggle}
feed={config.feed}
Expand All @@ -53,6 +136,7 @@ function ProtectionsReadyState({ data, config }) {
>
{config.feed === 'activity' && (
<ActivityProvider>
<BurnToProtectionsDataBridge />
<ActivityConsumer
showBurnAnimation={config.showBurnAnimation ?? true}
shouldDisplayLegacyActivity={totalCookiePopUpsBlockedSignal.value === undefined}
Expand Down
Loading
Loading