Skip to content

Commit a7c91d5

Browse files
committed
handle resetting the list when collapsed
1 parent ba0cb5c commit a7c91d5

File tree

8 files changed

+114
-72
lines changed

8 files changed

+114
-72
lines changed

special-pages/pages/new-tab/app/activity/ActivityProvider.js

Lines changed: 58 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -95,51 +95,49 @@ export function ActivityProvider(props) {
9595
* @return {NormalizedActivity}
9696
*/
9797
function normalizeItems(prev, data) {
98-
return {
99-
favorites: Object.fromEntries(
100-
data.activity.map((x) => {
101-
return [x.url, x.favorite];
102-
}),
103-
),
104-
items: Object.fromEntries(
105-
data.activity.map((x) => {
106-
/** @type {Item} */
107-
const next = {
108-
etldPlusOne: x.etldPlusOne,
109-
title: x.title,
110-
url: x.url,
111-
faviconMax: x.favicon?.maxAvailableSize ?? DDG_DEFAULT_ICON_SIZE,
112-
favoriteSrc: x.favicon?.src,
113-
trackersFound: x.trackersFound,
114-
};
115-
const differs = shallowDiffers(next, prev.items[x.url] || {});
116-
return [x.url, differs ? next : prev.items[x.url] || {}];
117-
}),
118-
),
119-
history: Object.fromEntries(
120-
data.activity.map((x) => {
121-
const differs = shallowDiffers(x.history, prev.history[x.url] || []);
122-
return [x.url, differs ? [...x.history] : prev.history[x.url] || []];
123-
}),
124-
),
125-
trackingStatus: Object.fromEntries(
126-
data.activity.map((x) => {
127-
const prevItem = prev.trackingStatus[x.url] || {
128-
totalCount: 0,
129-
trackerCompanies: [],
130-
};
131-
const differs = shallowDiffers(x.trackingStatus.trackerCompanies, prevItem.trackerCompanies);
132-
if (prevItem.totalCount !== x.trackingStatus.totalCount || differs) {
133-
const next = {
134-
totalCount: x.trackingStatus.totalCount,
135-
trackerCompanies: [...x.trackingStatus.trackerCompanies],
136-
};
137-
return [x.url, next];
138-
}
139-
return [x.url, prevItem];
140-
}),
141-
),
98+
/** @type {NormalizedActivity} */
99+
const output = {
100+
favorites: {},
101+
items: {},
102+
history: {},
103+
trackingStatus: {},
142104
};
105+
for (const item of data.activity) {
106+
const id = item.url;
107+
108+
output.favorites[id] = item.favorite;
109+
110+
/** @type {Item} */
111+
const next = {
112+
etldPlusOne: item.etldPlusOne,
113+
title: item.title,
114+
url: id,
115+
faviconMax: item.favicon?.maxAvailableSize ?? DDG_DEFAULT_ICON_SIZE,
116+
favoriteSrc: item.favicon?.src,
117+
trackersFound: item.trackersFound,
118+
};
119+
const differs = shallowDiffers(next, prev.items[id] || {});
120+
output.items[id] = differs ? next : prev.items[id] || {};
121+
122+
const historyDiff = shallowDiffers(item.history, prev.history[id] || []);
123+
output.history[id] = historyDiff ? [...item.history] : prev.history[id] || [];
124+
125+
const prevItem = prev.trackingStatus[id] || {
126+
totalCount: 0,
127+
trackerCompanies: [],
128+
};
129+
const trackersDiffer = shallowDiffers(item.trackingStatus.trackerCompanies, prevItem.trackerCompanies);
130+
if (prevItem.totalCount !== item.trackingStatus.totalCount || trackersDiffer) {
131+
const next = {
132+
totalCount: item.trackingStatus.totalCount,
133+
trackerCompanies: [...item.trackingStatus.trackerCompanies],
134+
};
135+
output.trackingStatus[id] = next;
136+
} else {
137+
output.trackingStatus[id] = prevItem;
138+
}
139+
}
140+
return output;
143141
}
144142

145143
/**
@@ -195,16 +193,16 @@ export function SignalStateProvider({ children }) {
195193
if (!target) return;
196194
if (!service) return;
197195
const anchor = /** @type {HTMLAnchorElement|null} */ (target.closest('a[href][data-url]'));
196+
const button = /** @type {HTMLButtonElement|null} */ (target.closest('button[value][data-action]'));
197+
const toggle = /** @type {HTMLButtonElement|null} */ (target.closest('button[data-toggle]'));
198198
if (anchor) {
199199
const url = anchor.dataset.url;
200200
if (!url) return;
201201
event.preventDefault();
202202
event.stopImmediatePropagation();
203203
const openTarget = eventToTarget(event, platformName);
204204
service.openUrl(url, openTarget);
205-
} else {
206-
const button = /** @type {HTMLButtonElement|null} */ (target.closest('button[value][data-action]'));
207-
if (!button) return;
205+
} else if (button) {
208206
event.preventDefault();
209207
event.stopImmediatePropagation();
210208

@@ -226,10 +224,15 @@ export function SignalStateProvider({ children }) {
226224
} else {
227225
console.warn('unhandled action:', action);
228226
}
227+
} else if (toggle) {
228+
if (state.config?.expansion === 'collapsed') {
229+
const next = urls.value.slice(0, Math.min(service.INITIAL, urls.value.length));
230+
setVisibleRange(next);
231+
}
229232
}
230233
}
231234

232-
const didClick = useCallback(didClick_, []);
235+
const didClick = useCallback(didClick_, [service, state.config.expansion]);
233236

234237
const keys = useSignal(
235238
normalizeKeys(
@@ -297,15 +300,19 @@ export function SignalStateProvider({ children }) {
297300
fillHoles();
298301
});
299302

300-
window.addEventListener('activity.next', showNextChunk);
301-
302303
return () => {
303304
unsub();
304305
unsubPatch();
305-
window.removeEventListener('activity.next', showNextChunk);
306306
};
307307
});
308308

309+
useEffect(() => {
310+
window.addEventListener('activity.next', showNextChunk);
311+
return () => {
312+
window.removeEventListener('activity.next', showNextChunk);
313+
};
314+
}, []);
315+
309316
useEffect(() => {
310317
const handler = () => {
311318
if (document.visibilityState === 'visible') {

special-pages/pages/new-tab/app/activity/batched-activity.service.js

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* @typedef {import("../../types/new-tab.js").UrlInfo} UrlInfo
55
* @typedef {import("../../types/new-tab.js").PatchData} PatchData
66
* @typedef {import('../../types/new-tab.js').DomainActivity} DomainActivity
7+
* @typedef {import('../service.js').InvocationSource} InvocationSource
78
*/
89
import { Service } from '../service.js';
910

@@ -47,7 +48,10 @@ export class BatchedActivityService {
4748
}
4849
},
4950
subscribe: (cb) => ntp.messaging.subscribe('activity_onDataUpdate', cb),
50-
}).withUpdater((old, next) => {
51+
}).withUpdater((old, next, source) => {
52+
if (source === 'manual') {
53+
return next;
54+
}
5155
if (this.batched) {
5256
return { activity: old.activity.concat(next.activity) };
5357
}
@@ -115,7 +119,7 @@ export class BatchedActivityService {
115119
}
116120

117121
/**
118-
* @param {(evt: {data: UrlInfo & PatchData, source: 'manual' | 'subscription'}) => void} cb
122+
* @param {(evt: {data: UrlInfo & PatchData, source: InvocationSource}) => void} cb
119123
* @internal
120124
*/
121125
onUrlData(cb) {
@@ -136,7 +140,7 @@ export class BatchedActivityService {
136140
}
137141

138142
/**
139-
* @param {(evt: {data: ActivityData, source: 'manual' | 'subscription'}) => void} cb
143+
* @param {(evt: {data: ActivityData, source: InvocationSource}) => void} cb
140144
* @internal
141145
*/
142146
onData(cb) {
@@ -155,7 +159,7 @@ export class BatchedActivityService {
155159
}
156160

157161
/**
158-
* @param {(evt: {data: ActivityConfig, source: 'manual' | 'subscription'}) => void} cb
162+
* @param {(evt: {data: ActivityConfig, source: InvocationSource}) => void} cb
159163
* @internal
160164
*/
161165
onConfig(cb) {
@@ -232,12 +236,6 @@ export class BatchedActivityService {
232236
});
233237
this.ntp.messaging.notify('activity_removeItem', { url });
234238
}
235-
/**
236-
* @param {string} url
237-
*/
238-
removeOnly(url) {
239-
this.ntp.messaging.notify('activity_removeItem', { url });
240-
}
241239
/**
242240
* @param {string} url
243241
* @param {import('../../types/new-tab.js').OpenTarget} target

special-pages/pages/new-tab/app/activity/components/Activity.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ function ActivityConfigured({ expansion, toggle }) {
4040
const batched = useBatchedActivityApi();
4141
const expanded = expansion === 'expanded';
4242
const { activity } = useContext(SignalStateContext);
43+
const { didClick } = useContext(ActivityApiContext);
4344
const count = useComputed(() => {
4445
return Object.values(activity.value.trackingStatus).reduce((acc, item) => {
4546
return acc + item.totalCount;
@@ -56,7 +57,7 @@ function ActivityConfigured({ expansion, toggle }) {
5657

5758
return (
5859
<Fragment>
59-
<div class={styles.root}>
60+
<div class={styles.root} onClick={didClick}>
6061
<ActivityHeading
6162
trackerCount={count.value}
6263
itemCount={itemCount.value}
@@ -70,7 +71,7 @@ function ActivityConfigured({ expansion, toggle }) {
7071
/>
7172
{itemCount.value > 0 && expanded && <ActivityBody canBurn={canBurn} />}
7273
</div>
73-
{batched && <Loader />}
74+
{batched && itemCount.value > 0 && expanded && <Loader />}
7475
</Fragment>
7576
);
7677
}
@@ -80,15 +81,14 @@ function ActivityConfigured({ expansion, toggle }) {
8081
* @param {boolean} props.canBurn
8182
*/
8283
function ActivityBody({ canBurn }) {
83-
const { didClick } = useContext(ActivityApiContext);
8484
const documentVisibility = useDocumentVisibility();
8585
const { isReducedMotion } = useEnv();
8686
const { keys } = useContext(SignalStateContext);
8787
const { burning, exiting } = useContext(ActivityBurningSignalContext);
8888
const busy = useComputed(() => burning.value.length > 0 || exiting.value.length > 0);
8989

9090
return (
91-
<ul class={styles.activity} onClick={didClick} data-busy={busy}>
91+
<ul class={styles.activity} data-busy={busy}>
9292
{keys.value.map((id, index) => {
9393
if (canBurn && !isReducedMotion) return <BurnableItem id={id} key={id} documentVisibility={documentVisibility} />;
9494
return <RemovableItem id={id} key={id} canBurn={canBurn} documentVisibility={documentVisibility} />;

special-pages/pages/new-tab/app/activity/integration-tests/activity.page.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,15 @@ export class ActivityPage {
9999
await page.getByLabel('Show recent activity').click();
100100
}
101101

102+
async collapsesList() {
103+
const { page } = this;
104+
await page.getByLabel('Hide recent activity').click();
105+
}
106+
async expandsList() {
107+
const { page } = this;
108+
await page.getByLabel('Show recent activity').click();
109+
}
110+
102111
async addsFavorite() {
103112
await this.context().getByRole('button', { name: 'Add example.com to favorites' }).click();
104113
const result = await this.ntp.mocks.waitForCallCount({ method: 'activity_addFavorite', count: 1 });

special-pages/pages/new-tab/app/activity/integration-tests/activity.spec.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,5 +218,25 @@ test.describe('activity widget', () => {
218218

219219
await batching.itemsReorder();
220220
});
221+
test('resets on collapse', async ({ page }, workerInfo) => {
222+
const ntp = NewtabPage.create(page, workerInfo);
223+
await ntp.reducedMotion();
224+
225+
// 20 entries, plenty to be triggered
226+
const widget = new ActivityPage(page, ntp).withEntries(20);
227+
const batching = new BatchingPage(page, ntp, widget);
228+
229+
await ntp.openPage({
230+
additional: { feed: 'activity', 'activity.api': 'batched', platform: 'windows', activity: widget.entries },
231+
});
232+
233+
await batching.fetchedRows(5);
234+
await widget.hasRows(5);
235+
await batching.triggerNext();
236+
await widget.hasRows(15);
237+
await widget.collapsesList();
238+
await widget.expandsList();
239+
await widget.hasRows(5);
240+
});
221241
});
222242
});

special-pages/pages/new-tab/app/components/ShowHideButton.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export function ShowHideButton({ text, onClick, buttonAttrs = {}, shape = 'none'
1919
{...buttonAttrs}
2020
class={cn(styles.button, shape === 'round' && styles.round, !!showText && styles.withText)}
2121
aria-label={text}
22+
data-toggle="true"
2223
onClick={onClick}
2324
>
2425
{showText ? (

special-pages/pages/new-tab/app/service.hooks.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
/**
1414
* @typedef {'entering' | 'exiting' | 'entered'} Ui
15+
* @typedef {import('./service.js').InvocationSource} InvocationSource
1516
*/
1617

1718
/**
@@ -186,7 +187,7 @@ export function useDataSubscription({ dispatch, service }) {
186187
* @param {object} params
187188
* @param {import("preact/hooks").Dispatch<Events<any, Config>>} params.dispatch
188189
* @param {import("preact").RefObject<{
189-
* onConfig: (cb: (event: { data:Config, source: 'manual' | 'subscription'}) => void) => () => void;
190+
* onConfig: (cb: (event: { data: Config, source: InvocationSource}) => void) => () => void;
190191
* toggleExpansion: () => void;
191192
* }>} params.service
192193
*/

special-pages/pages/new-tab/app/service.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/**
2+
* @typedef {'initial' | 'subscription' | 'manual' | 'trigger-fetch'} InvocationSource
3+
*/
14
/**
25
* @template Data - the data format this service produces/stores
36
*
@@ -14,7 +17,7 @@ export class Service {
1417
eventTarget = new EventTarget();
1518
DEBOUNCE_TIME_MS = 200;
1619
_broadcast = true;
17-
/** @type {undefined|((old: Data, next: Data) => Data)} */
20+
/** @type {undefined|((old: Data, next: Data, trigger: InvocationSource) => Data)} */
1821
accept;
1922
/**
2023
* @param {object} props
@@ -34,6 +37,9 @@ export class Service {
3437
}
3538
}
3639

40+
/**
41+
* @param {(old: Data, next: Data, trigger: string) => Data} fn
42+
*/
3743
withUpdater(fn) {
3844
this.accept = fn;
3945
return this;
@@ -65,7 +71,7 @@ export class Service {
6571
* @param {Data} d
6672
*/
6773
publish(d) {
68-
this._accept(d, 'manual');
74+
this._accept(d, 'subscription');
6975
}
7076

7177
/**
@@ -76,14 +82,14 @@ export class Service {
7682
*
7783
* A function is returned, which can be used to remove the event listener
7884
*
79-
* @param {(evt: {data: Data, source: 'manual' | 'subscription'}) => void} cb
85+
* @param {(evt: {data: Data, source: InvocationSource}) => void} cb
8086
*/
8187
onData(cb) {
8288
this._setupSubscription();
8389
const controller = new AbortController();
8490
this.eventTarget.addEventListener(
8591
'data',
86-
(/** @type {CustomEvent<{data: Data, source: 'manual' | 'subscription'}>} */ evt) => {
92+
(/** @type {CustomEvent<{data: Data, source: InvocationSource}>} */ evt) => {
8793
cb(evt.detail);
8894
},
8995
{ signal: controller.signal },
@@ -140,12 +146,12 @@ export class Service {
140146
}
141147
/**
142148
* @param {Data} data
143-
* @param {'initial' | 'subscription' | 'manual' | 'trigger-fetch'} source
149+
* @param {InvocationSource} source
144150
* @private
145151
*/
146152
_accept(data, source) {
147153
if (this.accept && source !== 'initial') {
148-
this.data = /** @type {NonNullable<Data>} */ (this.accept(/** @type {NonNullable<Data>} */ (this.data), data));
154+
this.data = /** @type {NonNullable<Data>} */ (this.accept(/** @type {NonNullable<Data>} */ (this.data), data, source));
149155
} else {
150156
this.data = /** @type {NonNullable<Data>} */ (data);
151157
}

0 commit comments

Comments
 (0)