Skip to content

Commit

Permalink
latest
Browse files Browse the repository at this point in the history
  • Loading branch information
shakyShane committed Jan 25, 2025
1 parent 228a68b commit 2b404f2
Show file tree
Hide file tree
Showing 8 changed files with 369 additions and 180 deletions.
17 changes: 9 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions special-pages/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
"@formkit/auto-animate": "^0.8.2",
"@lottielab/lottie-player": "^1.1.3",
"@preact/signals": "^1.3.1",
"@preact/signals": "^2.0.1",
"@rive-app/canvas-single": "^2.25.3",
"classnames": "^2.5.1",
"preact": "^10.24.3"
"preact": "^10.25.4"
}
}
143 changes: 138 additions & 5 deletions special-pages/pages/new-tab/app/activity/ActivityProvider.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { createContext, h } from 'preact';
import { useCallback, useEffect, useReducer, useRef } from 'preact/hooks';
import { useCallback, useEffect, useReducer, useRef, useContext } from 'preact/hooks';
import { useMessaging } from '../types.js';
import { ActivityService } from './activity.service.js';
import { reducer, useConfigSubscription, useDataSubscription, useInitialDataAndConfig } from '../service.hooks.js';
import { reducer, useConfigSubscription, useInitialDataAndConfig } from '../service.hooks.js';
import { eventToTarget } from '../utils.js';
import { usePlatformName } from '../settings.provider.js';
import { ACTION_ADD_FAVORITE, ACTION_BURN, ACTION_REMOVE, ACTION_REMOVE_FAVORITE } from './constants.js';
import { batch, signal, useSignal, useSignalEffect } from '@preact/signals';
import { DDG_DEFAULT_ICON_SIZE } from '../favorites/constants.js';

/**
* @typedef {import('../../types/new-tab.js').ActivityData} ActivityData
* @typedef {import('../../types/new-tab.js').ActivityConfig} ActivityConfig
* @typedef {import('../../types/new-tab').TrackingStatus} TrackingStatus
* @typedef {import('../../types/new-tab').HistoryEntry} HistoryEntry
* @typedef {import('../service.hooks.js').State<ActivityData, ActivityConfig>} State
* @typedef {import('../service.hooks.js').Events<ActivityData, ActivityConfig>} Events
*/
Expand Down Expand Up @@ -57,9 +61,6 @@ export function ActivityProvider(props) {
// get initial data
useInitialDataAndConfig({ dispatch, service });

// subscribe to data updates
useDataSubscription({ dispatch, service });

// subscribe to toggle + expose a fn for sync toggling
const { toggle } = useConfigSubscription({ dispatch, service });

Expand Down Expand Up @@ -115,6 +116,138 @@ export function ActivityProvider(props) {
);
}

/**
* @typedef Item
* @property {string} props.title
* @property {string} props.url
* @property {string|null|undefined} props.favoriteSrc
* @property {number} props.faviconMax
* @property {string} props.etldPlusOne
*/

/**
* @typedef Normalized
* @property {Record<string, Item>} items
* @property {Record<string, HistoryEntry[]>} history
* @property {Record<string, TrackingStatus>} trackingStatus
* @property {Record<string, boolean>} favorites
*/

/**
* @param {Normalized} prev
* @param {ActivityData} data
* @return {Normalized}
*/
function normalizeItems(prev, data) {
return {
favorites: Object.fromEntries(
data.activity.map((x) => {
return [x.url, x.favorite];
}),
),
items: Object.fromEntries(
data.activity.map((x) => {
/** @type {Item} */
const next = {
etldPlusOne: x.etldPlusOne,
title: x.title,
url: x.url,
faviconMax: x.favicon?.maxAvailableSize ?? DDG_DEFAULT_ICON_SIZE,
favoriteSrc: x.favicon?.src,
};
const differs = shallowDiffers(next, prev.items[x.url] || {});
return [x.url, differs ? next : prev.items[x.url] || {}];
}),
),
history: Object.fromEntries(
data.activity.map((x) => {
const differs = shallowDiffers(x.history, prev.history[x.url] || []);
return [x.url, differs ? [...x.history] : prev.history[x.url] || []];
}),
),
trackingStatus: Object.fromEntries(
data.activity.map((x) => {
const prevItem = prev.trackingStatus[x.url] || {
totalCount: 0,
trackerCompanies: [],
};
const differs = shallowDiffers(x.trackingStatus.trackerCompanies, prevItem.trackerCompanies);
if (prevItem.totalCount !== x.trackingStatus.totalCount || differs) {
const next = {
totalCount: x.trackingStatus.totalCount,
trackerCompanies: [...x.trackingStatus.trackerCompanies],
};
return [x.url, next];
}
return [x.url, prevItem];
}),
),
};
}

/**
* @param {string[]} prev
* @param {ActivityData} data
* @return {string[]}
*/
function normalize(prev, data) {
const keys = data.activity.map((x) => x.url);
return shallowDiffers(prev, keys) ? keys : prev;
}

/**
* Check if two objects have a different shape
* @param {object} a
* @param {object} b
* @returns {boolean}
*/
export function shallowDiffers(a, b) {
for (const i in a) if (i !== '__source' && !(i in b)) return true;
for (const i in b) if (i !== '__source' && a[i] !== b[i]) return true;
return false;
}

export const SignalStateContext = createContext({
items: signal(/** @type {Normalized} */ ({})),
keys: signal(/** @type {string[]} */ ([])),
});

export function SignalStateProvider({ children }) {
const { state } = useContext(ActivityContext);
const service = useContext(ActivityServiceContext);
if (state.status !== 'ready') throw new Error('must have ready status here');
if (!service) throw new Error('must have service here');

const keys = useSignal(normalize([], state.data));
const items = useSignal(
normalizeItems(
{
items: {},
history: {},
trackingStatus: {},
favorites: {},
},
state.data,
),
);

useSignalEffect(() => {
if (!service) return console.warn('could not access service');
const unsub = service.onData((evt) => {
const next = normalize(keys.value, evt.data);
batch(() => {
keys.value = next;
items.value = normalizeItems(items.value, evt.data);
});
});
return () => {
unsub();
};
});

return <SignalStateContext.Provider value={{ items, keys }}>{children}</SignalStateContext.Provider>;
}

/**
* @return {import("preact").RefObject<ActivityService>}
*/
Expand Down
27 changes: 14 additions & 13 deletions special-pages/pages/new-tab/app/activity/BurnProvider.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import { h, createContext } from 'preact';
import { useContext, useEffect, useState } from 'preact/hooks';
import { useContext, useEffect } from 'preact/hooks';
import { ActivityApiContext, ActivityServiceContext } from './ActivityProvider';
import { ACTION_BURN } from './constants.js';
import { useEnv } from '../../../../shared/components/EnvironmentProvider.js';
import { signal, useSignal } from '@preact/signals';

export const ActivityBurningContext = createContext({
/** @type {string[]} */
burning: [],
/** @type {string[]} */
exiting: [],
export const ActivityBurningSignalContext = createContext({
/** @type {import("@preact/signals").Signal<string[]>} */
burning: signal([]),
/** @type {import("@preact/signals").Signal<string[]>} */
exiting: signal([]),
});

/**
* @param {object} props
* @param {import("preact").ComponentChild} props.children
*/
export function BurnProvider({ children }) {
const [burning, setBurning] = useState(/** @type {string[]} */ ([]));
const [exiting, setExiting] = useState(/** @type {string[]} */ ([]));
const burning = useSignal(/** @type {string[]} */ ([]));
const exiting = useSignal(/** @type {string[]} */ ([]));
const { didClick: originalDidClick } = useContext(ActivityApiContext);
const service = useContext(ActivityServiceContext);
const { isReducedMotion } = useEnv();
Expand All @@ -33,15 +34,15 @@ export function BurnProvider({ children }) {
if (isReducedMotion) {
service?.burn(value);
} else {
setBurning((prev) => prev.concat(value));
burning.value = burning.value.concat(value);
}
}

useEffect(() => {
const handler = (e) => {
if (e.detail.url) {
setBurning((prev) => prev.filter((x) => x !== e.detail.url));
setExiting((prev) => prev.concat(e.detail.url));
burning.value = burning.value.filter((x) => x !== e.detail.url);
exiting.value = exiting.value.concat(e.detail.url);
}
};
window.addEventListener('done-burning', handler);
Expand All @@ -63,8 +64,8 @@ export function BurnProvider({ children }) {
}, [service]);

return (
<ActivityBurningContext.Provider value={{ burning, exiting }}>
<ActivityBurningSignalContext.Provider value={{ burning, exiting }}>
<ActivityApiContext.Provider value={{ didClick }}>{children}</ActivityApiContext.Provider>
</ActivityBurningContext.Provider>
</ActivityBurningSignalContext.Provider>
);
}
Loading

0 comments on commit 2b404f2

Please sign in to comment.