StatusList: incorporate feed injection algorithms
This commit is contained in:
parent
ec225ea1c5
commit
2681b32f7d
6 changed files with 71 additions and 16 deletions
|
@ -1,7 +1,9 @@
|
||||||
import classNames from 'clsx';
|
import classNames from 'clsx';
|
||||||
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import React, { useRef, useCallback } from 'react';
|
import React, { useRef, useCallback } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import LoadGap from 'soapbox/components/load_gap';
|
import LoadGap from 'soapbox/components/load_gap';
|
||||||
import ScrollableList from 'soapbox/components/scrollable_list';
|
import ScrollableList from 'soapbox/components/scrollable_list';
|
||||||
|
@ -9,6 +11,7 @@ import StatusContainer from 'soapbox/containers/status_container';
|
||||||
import Ad from 'soapbox/features/ads/components/ad';
|
import Ad from 'soapbox/features/ads/components/ad';
|
||||||
import FeedSuggestions from 'soapbox/features/feed-suggestions/feed-suggestions';
|
import FeedSuggestions from 'soapbox/features/feed-suggestions/feed-suggestions';
|
||||||
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status';
|
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status';
|
||||||
|
import { ALGORITHMS } from 'soapbox/features/timeline-insertion';
|
||||||
import PendingStatus from 'soapbox/features/ui/components/pending_status';
|
import PendingStatus from 'soapbox/features/ui/components/pending_status';
|
||||||
import { useSoapboxConfig } from 'soapbox/hooks';
|
import { useSoapboxConfig } from 'soapbox/hooks';
|
||||||
import useAds from 'soapbox/queries/ads';
|
import useAds from 'soapbox/queries/ads';
|
||||||
|
@ -60,8 +63,12 @@ const StatusList: React.FC<IStatusList> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const { data: ads } = useAds();
|
const { data: ads } = useAds();
|
||||||
const soapboxConfig = useSoapboxConfig();
|
const soapboxConfig = useSoapboxConfig();
|
||||||
const adsInterval = Number(soapboxConfig.extensions.getIn(['ads', 'interval'], 40)) || 0;
|
|
||||||
|
const adsAlgorithm = String(soapboxConfig.extensions.getIn(['ads', 'algorithm', 0]));
|
||||||
|
const adsOpts = (soapboxConfig.extensions.getIn(['ads', 'algorithm', 1], ImmutableMap()) as ImmutableMap<string, any>).toJS();
|
||||||
|
|
||||||
const node = useRef<VirtuosoHandle>(null);
|
const node = useRef<VirtuosoHandle>(null);
|
||||||
|
const seed = useRef<string>(uuidv4());
|
||||||
|
|
||||||
const getFeaturedStatusCount = () => {
|
const getFeaturedStatusCount = () => {
|
||||||
return featuredStatusIds?.size || 0;
|
return featuredStatusIds?.size || 0;
|
||||||
|
@ -132,9 +139,10 @@ const StatusList: React.FC<IStatusList> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAd = (ad: AdEntity) => {
|
const renderAd = (ad: AdEntity, index: number) => {
|
||||||
return (
|
return (
|
||||||
<Ad
|
<Ad
|
||||||
|
key={`ad-${index}`}
|
||||||
card={ad.card}
|
card={ad.card}
|
||||||
impression={ad.impression}
|
impression={ad.impression}
|
||||||
expires={ad.expires}
|
expires={ad.expires}
|
||||||
|
@ -175,9 +183,13 @@ const StatusList: React.FC<IStatusList> = ({
|
||||||
const renderStatuses = (): React.ReactNode[] => {
|
const renderStatuses = (): React.ReactNode[] => {
|
||||||
if (isLoading || statusIds.size > 0) {
|
if (isLoading || statusIds.size > 0) {
|
||||||
return statusIds.toList().reduce((acc, statusId, index) => {
|
return statusIds.toList().reduce((acc, statusId, index) => {
|
||||||
const adIndex = ads ? Math.floor((index + 1) / adsInterval) % ads.length : 0;
|
if (showAds && ads) {
|
||||||
const ad = ads ? ads[adIndex] : undefined;
|
const ad = ALGORITHMS[adsAlgorithm]?.(ads, index, { ...adsOpts, seed: seed.current });
|
||||||
const showAd = (index + 1) % adsInterval === 0;
|
|
||||||
|
if (ad) {
|
||||||
|
acc.push(renderAd(ad, index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (statusId === null) {
|
if (statusId === null) {
|
||||||
acc.push(renderLoadGap(index));
|
acc.push(renderLoadGap(index));
|
||||||
|
@ -189,10 +201,6 @@ const StatusList: React.FC<IStatusList> = ({
|
||||||
acc.push(renderStatus(statusId));
|
acc.push(renderStatus(statusId));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showAds && ad && showAd) {
|
|
||||||
acc.push(renderAd(ad));
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, [] as React.ReactNode[]);
|
}, [] as React.ReactNode[]);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -8,7 +8,7 @@ type Opts = {
|
||||||
/**
|
/**
|
||||||
* Start/end index of the slot by which one item will be randomly picked per page.
|
* Start/end index of the slot by which one item will be randomly picked per page.
|
||||||
*
|
*
|
||||||
* Eg. `[3, 7]` will cause one item to be picked between the third and seventh indexes per page.
|
* Eg. `[2, 6]` will cause one item to be picked among the third through seventh indexes.
|
||||||
*
|
*
|
||||||
* `end` must be larger than `start`.
|
* `end` must be larger than `start`.
|
||||||
*/
|
*/
|
||||||
|
@ -21,21 +21,34 @@ type Opts = {
|
||||||
* Algorithm to display items per-page.
|
* Algorithm to display items per-page.
|
||||||
* One item is randomly inserted into each page within the index range.
|
* One item is randomly inserted into each page within the index range.
|
||||||
*/
|
*/
|
||||||
const abovefoldAlgorithm: PickAlgorithm = (items, iteration, opts: Opts) => {
|
const abovefoldAlgorithm: PickAlgorithm = (items, iteration, rawOpts) => {
|
||||||
|
const opts = normalizeOpts(rawOpts);
|
||||||
/** Current page of the index. */
|
/** Current page of the index. */
|
||||||
const page = Math.floor(((iteration + 1) / opts.pageSize) - 1);
|
const page = Math.floor(iteration / opts.pageSize);
|
||||||
/** Current index within the page. */
|
/** Current index within the page. */
|
||||||
const pageIndex = ((iteration + 1) % opts.pageSize) - 1;
|
const pageIndex = (iteration % opts.pageSize);
|
||||||
/** RNG for the page. */
|
/** RNG for the page. */
|
||||||
const rng = seedrandom(`${opts.seed}-page-${page}`);
|
const rng = seedrandom(`${opts.seed}-page-${page}`);
|
||||||
/** Index to insert the item. */
|
/** Index to insert the item. */
|
||||||
const insertIndex = Math.floor(rng() * opts.range[1] - opts.range[0]) + opts.range[0];
|
const insertIndex = Math.floor(rng() * (opts.range[1] - opts.range[0])) + opts.range[0];
|
||||||
|
|
||||||
|
console.log({ page, iteration, pageIndex, insertIndex });
|
||||||
|
|
||||||
if (pageIndex === insertIndex) {
|
if (pageIndex === insertIndex) {
|
||||||
return items[page % items.length];
|
return items[page % items.length];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeOpts = (opts: unknown): Opts => {
|
||||||
|
const { seed, range, pageSize } = (opts && typeof opts === 'object' ? opts : {}) as Record<any, unknown>;
|
||||||
|
|
||||||
|
return {
|
||||||
|
seed: typeof seed === 'string' ? seed : '',
|
||||||
|
range: Array.isArray(range) ? [Number(range[0]), Number(range[1])] : [2, 6],
|
||||||
|
pageSize: typeof pageSize === 'number' ? pageSize : 20,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
abovefoldAlgorithm,
|
abovefoldAlgorithm,
|
||||||
};
|
};
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { abovefoldAlgorithm } from './abovefold';
|
||||||
|
import { linearAlgorithm } from './linear';
|
||||||
|
|
||||||
|
import type { PickAlgorithm } from './types';
|
||||||
|
|
||||||
|
const ALGORITHMS: Record<any, PickAlgorithm | undefined> = {
|
||||||
|
'linear': linearAlgorithm,
|
||||||
|
'abovefold': abovefoldAlgorithm,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ALGORITHMS };
|
|
@ -6,7 +6,8 @@ type Opts = {
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Picks the next item every iteration. */
|
/** Picks the next item every iteration. */
|
||||||
const linearAlgorithm: PickAlgorithm = (items, iteration, opts: Opts) => {
|
const linearAlgorithm: PickAlgorithm = (items, iteration, rawOpts) => {
|
||||||
|
const opts = normalizeOpts(rawOpts);
|
||||||
const itemIndex = items ? Math.floor((iteration + 1) / opts.interval) % items.length : 0;
|
const itemIndex = items ? Math.floor((iteration + 1) / opts.interval) % items.length : 0;
|
||||||
const item = items ? items[itemIndex] : undefined;
|
const item = items ? items[itemIndex] : undefined;
|
||||||
const showItem = (iteration + 1) % opts.interval === 0;
|
const showItem = (iteration + 1) % opts.interval === 0;
|
||||||
|
@ -14,6 +15,14 @@ const linearAlgorithm: PickAlgorithm = (items, iteration, opts: Opts) => {
|
||||||
return showItem ? item : undefined;
|
return showItem ? item : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeOpts = (opts: unknown): Opts => {
|
||||||
|
const { interval } = (opts && typeof opts === 'object' ? opts : {}) as Record<any, unknown>;
|
||||||
|
|
||||||
|
return {
|
||||||
|
interval: typeof interval === 'number' ? interval : 20,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
linearAlgorithm,
|
linearAlgorithm,
|
||||||
};
|
};
|
|
@ -7,7 +7,7 @@ type PickAlgorithm = <D = any>(
|
||||||
/** Current iteration by which an item may be chosen. */
|
/** Current iteration by which an item may be chosen. */
|
||||||
iteration: number,
|
iteration: number,
|
||||||
/** Implementation-specific opts. */
|
/** Implementation-specific opts. */
|
||||||
opts: any
|
opts: Record<string, unknown>
|
||||||
) => D | undefined;
|
) => D | undefined;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
|
@ -175,6 +175,19 @@ const normalizeFooterLinks = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMap
|
||||||
return soapboxConfig.setIn(path, items);
|
return soapboxConfig.setIn(path, items);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Migrate legacy ads config. */
|
||||||
|
const normalizeAdsAlgorithm = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMap => {
|
||||||
|
const interval = soapboxConfig.getIn(['extensions', 'ads', 'interval']);
|
||||||
|
const algorithm = soapboxConfig.getIn(['extensions', 'ads', 'algorithm']);
|
||||||
|
|
||||||
|
if (typeof interval === 'number' && !algorithm) {
|
||||||
|
const result = fromJS(['linear', { interval }]);
|
||||||
|
return soapboxConfig.setIn(['extensions', 'ads', 'algorithm'], result);
|
||||||
|
} else {
|
||||||
|
return soapboxConfig;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const normalizeSoapboxConfig = (soapboxConfig: Record<string, any>) => {
|
export const normalizeSoapboxConfig = (soapboxConfig: Record<string, any>) => {
|
||||||
return SoapboxConfigRecord(
|
return SoapboxConfigRecord(
|
||||||
ImmutableMap(fromJS(soapboxConfig)).withMutations(soapboxConfig => {
|
ImmutableMap(fromJS(soapboxConfig)).withMutations(soapboxConfig => {
|
||||||
|
@ -186,6 +199,7 @@ export const normalizeSoapboxConfig = (soapboxConfig: Record<string, any>) => {
|
||||||
maybeAddMissingColors(soapboxConfig);
|
maybeAddMissingColors(soapboxConfig);
|
||||||
normalizeCryptoAddresses(soapboxConfig);
|
normalizeCryptoAddresses(soapboxConfig);
|
||||||
normalizeAds(soapboxConfig);
|
normalizeAds(soapboxConfig);
|
||||||
|
normalizeAdsAlgorithm(soapboxConfig);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue