Skip to content

Commit

Permalink
figuring out persistence
Browse files Browse the repository at this point in the history
  • Loading branch information
shakyShane committed Jan 20, 2025
1 parent 312e022 commit f0abecd
Show file tree
Hide file tree
Showing 11 changed files with 162 additions and 55 deletions.
8 changes: 6 additions & 2 deletions special-pages/pages/new-tab/app/mock-transport.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { freemiumPIRDataExamples } from './freemium-pir-banner/mocks/freemiumPIR
* @typedef {import('@duckduckgo/messaging/lib/test-utils.mjs').SubscriptionEvent} SubscriptionEvent
*/

const VERSION_PREFIX = '__ntp_29__.';
const VERSION_PREFIX = '__ntp_30__.';
const url = new URL(window.location.href);

export function mockTransport() {
Expand Down Expand Up @@ -385,7 +385,11 @@ export function mockTransport() {
}
case 'stats_getConfig': {
/** @type {StatsConfig} */
const defaultConfig = { expansion: 'expanded', animation: { kind: 'auto-animate' } };
const defaultConfig = {
expansion: 'expanded',
animation: { kind: 'auto-animate' },
onboarding: 'history',
};
const fromStorage = read('stats_config') || defaultConfig;
if (url.searchParams.get('animation') === 'none') {
fromStorage.animation = { kind: 'none' };
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createContext, h } from 'preact';
import { useEffect, useReducer, useRef } from 'preact/hooks';
import { useCallback, useEffect, useReducer, useRef } from 'preact/hooks';
import { useMessaging } from '../types.js';
import { PrivacyStatsService } from './privacy-stats.service.js';
import { reducer, useConfigSubscription, useDataSubscription, useInitialDataAndConfig } from '../service.hooks.js';
Expand All @@ -25,6 +25,11 @@ export const PrivacyStatsContext = createContext({

export const PrivacyStatsDispatchContext = createContext(/** @type {import("preact/hooks").Dispatch<Events>} */ ({}));

export const HistoryOnboardingContext = createContext({
openHistory: () => {},
dismiss: () => {},
});

/**
* A data provider that will use `PrivacyStatsService` to fetch data, subscribe
* to updates and modify state.
Expand Down Expand Up @@ -54,9 +59,19 @@ export function PrivacyStatsProvider(props) {
// subscribe to toggle + expose a fn for sync toggling
const { toggle } = useConfigSubscription({ dispatch, service });

const openHistory = useCallback(() => {
service.current?.openHistory();
}, [service]);

const dismiss = useCallback(() => {
service.current?.dismiss();
}, [service]);

return (
<PrivacyStatsContext.Provider value={{ state, toggle }}>
<PrivacyStatsDispatchContext.Provider value={dispatch}>{props.children}</PrivacyStatsDispatchContext.Provider>
<PrivacyStatsDispatchContext.Provider value={dispatch}>
<HistoryOnboardingContext.Provider value={{ openHistory, dismiss }}>{props.children}</HistoryOnboardingContext.Provider>
</PrivacyStatsDispatchContext.Provider>
</PrivacyStatsContext.Provider>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const privacyStatsExamples = {
},
'stats.few.collapsed': {
factory: () => (
<PrivacyStatsMockProvider config={{ expansion: 'collapsed' }}>
<PrivacyStatsMockProvider config={{ expansion: 'collapsed', onboarding: 'history' }}>
<PrivacyStatsConsumer />
</PrivacyStatsMockProvider>
),
Expand Down Expand Up @@ -54,6 +54,7 @@ export const otherPrivacyStatsExamples = {
config={{
expansion: 'expanded',
animation: { kind: 'none' },
onboarding: 'history',
}}
>
<PrivacyStatsConsumer />
Expand All @@ -67,6 +68,7 @@ export const otherPrivacyStatsExamples = {
config={{
expansion: 'expanded',
animation: { kind: 'view-transitions' },
onboarding: null,
}}
>
<PrivacyStatsConsumer />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import cn from 'classnames';
import styles from './PrivacyStats.module.css';
import { useMessaging, useTypedTranslationWith } from '../../types.js';
import { useContext, useState, useId, useCallback, useMemo } from 'preact/hooks';
import { PrivacyStatsContext, PrivacyStatsProvider } from '../PrivacyStatsProvider.js';
import { HistoryOnboardingContext, PrivacyStatsContext, PrivacyStatsProvider } from '../PrivacyStatsProvider.js';
import { useVisibility } from '../../widget-list/widget-config.provider.js';
import { viewTransition } from '../../utils.js';
import { ShowHideButton } from '../../components/ShowHideButton.jsx';
Expand All @@ -29,47 +29,27 @@ import { DismissButton } from '../../components/DismissButton';
* @param {PrivacyStatsData} props.data
* @param {()=>void} props.toggle
* @param {Animation['kind']} [props.animation] - optionally configure animations
* @param {()=>void} [props.dismissHistoryMsg]
* @param {()=>void} [props.openHistory]
*/
export function PrivacyStats({ expansion, data, toggle, animation = 'auto-animate', dismissHistoryMsg, openHistory }) {
export function PrivacyStats({ expansion, data, toggle, animation = 'auto-animate' }) {
if (animation === 'view-transitions') {
return <WithViewTransitions data={data} expansion={expansion} toggle={toggle} />;
}

// no animations
return (
<PrivacyStatsConfigured
expansion={expansion}
data={data}
toggle={toggle}
dismissHistoryMsg={dismissHistoryMsg}
openHistory={openHistory}
/>
);
return <PrivacyStatsConfigured expansion={expansion} data={data} toggle={toggle} />;
}

/**
* @param {object} props
* @param {Expansion} props.expansion
* @param {PrivacyStatsData} props.data
* @param {()=>void} props.toggle
* @param {()=>void} [props.dismissHistoryMsg]
* @param {()=>void} [props.openHistory]
*/
function WithViewTransitions({ expansion, data, toggle, dismissHistoryMsg, openHistory }) {
function WithViewTransitions({ expansion, data, toggle }) {
const willToggle = useCallback(() => {
viewTransition(toggle);
}, [toggle]);
return (
<PrivacyStatsConfigured
expansion={expansion}
data={data}
toggle={willToggle}
dismissHistoryMsg={dismissHistoryMsg}
openHistory={openHistory}
/>
);
return <PrivacyStatsConfigured expansion={expansion} data={data} toggle={willToggle} />;
}

/**
Expand All @@ -78,10 +58,8 @@ function WithViewTransitions({ expansion, data, toggle, dismissHistoryMsg, openH
* @param {Expansion} props.expansion
* @param {PrivacyStatsData} props.data
* @param {()=>void} props.toggle
* @param {()=>void} [props.openHistory]
* @param {()=>void} [props.dismissHistoryMsg]
*/
function PrivacyStatsConfigured({ parentRef, expansion, data, toggle, openHistory, dismissHistoryMsg }) {
function PrivacyStatsConfigured({ parentRef, expansion, data, toggle }) {
const expanded = expansion === 'expanded';

const { hasNamedCompanies, recent } = useMemo(() => {
Expand Down Expand Up @@ -111,8 +89,6 @@ function PrivacyStatsConfigured({ parentRef, expansion, data, toggle, openHistor
'aria-controls': WIDGET_ID,
id: TOGGLE_ID,
}}
openHistory={openHistory}
dismissHistoryMsg={dismissHistoryMsg}
/>
{hasNamedCompanies && expanded && <PrivacyStatsBody trackerCompanies={data.trackerCompanies} listAttrs={{ id: WIDGET_ID }} />}
</div>
Expand All @@ -126,10 +102,8 @@ function PrivacyStatsConfigured({ parentRef, expansion, data, toggle, openHistor
* @param {boolean} props.canExpand
* @param {() => void} props.onToggle
* @param {import("preact").ComponentProps<'button'>} [props.buttonAttrs]
* @param {()=>void} [props.openHistory]
* @param {()=>void} [props.dismissHistoryMsg]
*/
export function Heading({ expansion, canExpand, recent, onToggle, buttonAttrs = {}, openHistory, dismissHistoryMsg }) {
export function Heading({ expansion, canExpand, recent, onToggle, buttonAttrs = {} }) {
const { t } = useTypedTranslationWith(/** @type {enStrings} */ ({}));
const [formatter] = useState(() => new Intl.NumberFormat());

Expand Down Expand Up @@ -161,22 +135,34 @@ export function Heading({ expansion, canExpand, recent, onToggle, buttonAttrs =
)}
{recent === 0 && <p className={cn(styles.subtitle)}>{t('stats_noActivity')}</p>}
{recent > 0 && <p className={cn(styles.subtitle, styles.uppercase)}>{t('stats_feedCountBlockedPeriod')}</p>}
<div class={styles.historyMsg}>
<p>
{t('stats_historyMovedMessage')}{' '}
<a onClick={openHistory} className={styles.historyLink} href="#">
{t('stats_history')}
</a>
</p>
<DismissButton className={styles.dismissBtn} onClick={dismissHistoryMsg} />
</div>
<HistoryOnboarding />
</div>
);
}

function HistoryOnboarding() {
const { t } = useTypedTranslationWith(/** @type {enStrings} */ ({}));
const { state } = useContext(PrivacyStatsContext);
const { openHistory, dismiss } = useContext(HistoryOnboardingContext);

if (!state.config) return null;
if (state.config.onboarding !== 'history') return null;
return (
<div className={styles.historyMsg}>
<p>
{t('stats_historyMovedMessage')}{' '}
<a onClick={openHistory} className={styles.historyLink} href="#">
{t('stats_history')}
</a>
</p>
<DismissButton className={styles.dismissBtn} onClick={dismiss} />
</div>
);
}

/**
* @param {object} props
* @param {import("preact").ComponentProps<'ul'>} [props.listAttrs]
* @param {import('preact').ComponentProps<'ul'>} [props.listAttrs]
* @param {TrackerCompany[]} props.trackerCompanies
*/

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { reducer } from '../../service.hooks.js';
*/
export function PrivacyStatsMockProvider({
data = stats.few,
config = { expansion: 'expanded', animation: { kind: 'auto-animate' } },
config = { expansion: 'expanded', animation: { kind: 'auto-animate' }, onboarding: null },
ticker = false,
children,
}) {
Expand Down
31 changes: 31 additions & 0 deletions special-pages/pages/new-tab/app/privacy-stats/privacy-stats.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ title: Privacy Stats
- {@link "NewTab Messages".StatsGetDataRequest `stats_getConfig`}
- Used to fetch the initial config data (eg: expanded vs collapsed)
- returns {@link "NewTab Messages".StatsConfig}

```json
{
"expansion": "expanded",
"onboarding": "history"
}
```

## Subscriptions:
- {@link "NewTab Messages".StatsOnDataUpdateSubscription `stats_onDataUpdate`}.
Expand All @@ -33,6 +40,30 @@ title: Privacy Stats
- Sent when the user chooses to show more stats (eg: more than the default 5)
- {@link "NewTab Messages".StatsShowLessNotification `stats_showLess`}
- Sent when the user chooses to show less stats (eg: from a long list back to the default)
- {@link "NewTab Messages".StatsOpenHistoryNotification `stats_openHistory`}
- Sent when the user clicks the `openHistory` button in the history onboarding
- {@link "NewTab Messages".StatsDismissHistoryMsgNotification `stats_dismissHistoryMsg`}
- Sent when the user dismisses the history onboarding
- This call should update the widget config associated with the stats widget, setting `onboarding` to `null`
- In turn, the subscription `stats_onConfigUpdate` will occur in other tabs/windows

Example config, from the first load

```json
{
"expansion": "expanded",
"onboarding": "history"
}
```

Example config, delivered via subscription, after the history onboarding was dismissed

```json
{
"expansion": "expanded",
"onboarding": null
}
```

## Example:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export class PrivacyStatsService {
* @internal
*/
constructor(ntp) {
this.ntp = ntp;
/** @type {Service<PrivacyStatsData>} */
this.dataService = new Service({
initial: () => ntp.messaging.request('stats_getData'),
Expand Down Expand Up @@ -77,4 +78,25 @@ export class PrivacyStatsService {
}
});
}

/**
*
*/
openHistory() {
this.ntp.messaging.notify('stats_openHistory', {});
}

/**
*
*/
dismiss() {
this.configService.updateWith(
(old) => {
return { ...old, onboarding: null };
},
(_) => {
this.ntp.messaging.notify('stats_dismissHistoryMsg', {});
},
);
}
}
35 changes: 30 additions & 5 deletions special-pages/pages/new-tab/app/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,32 @@ export class Service {
}
}

/**
* Apply a function over the current state.
*
* The change will be broadcast to observers immediately,
* and then persists after a debounced period.
*
* @param {(prev: Data) => Data} updaterFn - the function that returns the next state
* @param {((prev: Data) => void) | null} [persister] - the function the performs the persistence
*/
updateWith(updaterFn, persister = null) {
if (this.data === null) return;
const next = updaterFn(this.data);
if (next) {
this._accept(next, 'manual', persister);
} else {
console.warn('could not update');
}
}

/**
* @param {Data} data
* @param {'initial' | 'subscription' | 'manual'} source
* @param {((d: Data) => void) | null} persister
* @private
*/
_accept(data, source) {
_accept(data, source, persister = null) {
this.data = /** @type {NonNullable<Data>} */ (data);

// do nothing when it's the initial data
Expand All @@ -127,7 +147,7 @@ export class Service {
if (source === 'manual') {
const time = window.location.search.includes('p2') ? this.DEBOUNCE_TIME_MS * 20.5 : this.DEBOUNCE_TIME_MS;
this.debounceTimer = setTimeout(() => {
this.persist();
this.persist(persister);
}, time);
}
}
Expand All @@ -144,15 +164,20 @@ export class Service {

/**
* Persists the current in-memory widget configuration state to the internal data feed.
* @param {((d: Data) => void) | null} persister
*/
persist() {
persist(persister = null) {
// some services will not implement persistence
if (!this.impl.persist) return;
if (!this.impl.persist && !persister) return;

// if the data was never set, there's nothing to persist
if (this.data === null) return;

// send the data
this.impl.persist(this.data);
if (persister) {
persister(this.data);
} else if (this.impl.persist) {
this.impl.persist(this.data);
}
}
}
Loading

0 comments on commit f0abecd

Please sign in to comment.