From fe8966fc3e45b6a0e0ec74f09e254da23b41d02d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 9 Sep 2022 20:44:52 -0500 Subject: [PATCH 1/7] Scaffold out timeline insertion modules --- .../features/timeline-insertion/abovefold.ts | 41 +++++++++++++++++++ .../features/timeline-insertion/index.ts | 0 .../features/timeline-insertion/linear.ts | 19 +++++++++ .../features/timeline-insertion/types.ts | 15 +++++++ package.json | 2 + yarn.lock | 10 +++++ 6 files changed, 87 insertions(+) create mode 100644 app/soapbox/features/timeline-insertion/abovefold.ts create mode 100644 app/soapbox/features/timeline-insertion/index.ts create mode 100644 app/soapbox/features/timeline-insertion/linear.ts create mode 100644 app/soapbox/features/timeline-insertion/types.ts diff --git a/app/soapbox/features/timeline-insertion/abovefold.ts b/app/soapbox/features/timeline-insertion/abovefold.ts new file mode 100644 index 000000000..78a8b6e6f --- /dev/null +++ b/app/soapbox/features/timeline-insertion/abovefold.ts @@ -0,0 +1,41 @@ +import seedrandom from 'seedrandom'; + +import type { PickAlgorithm } from './types'; + +type Opts = { + /** Randomization seed. */ + seed: string, + /** + * 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. + * + * `end` must be larger than `start`. + */ + range: [start: number, end: number], + /** Number of items in the page. */ + pageSize: number, +}; + +/** + * Algorithm to display items per-page. + * One item is randomly inserted into each page within the index range. + */ +const abovefoldAlgorithm: PickAlgorithm = (items, index, opts: Opts) => { + /** Current page of the index. */ + const page = Math.floor(((index + 1) / opts.pageSize) - 1); + /** Current index within the page. */ + const pageIndex = ((index + 1) % opts.pageSize) - 1; + /** RNG for the page. */ + const rng = seedrandom(`${opts.seed}-page-${page}`); + /** Index to insert the item. */ + const insertIndex = Math.floor(rng() * opts.range[1] - opts.range[0]) + opts.range[0]; + + if (pageIndex === insertIndex) { + return items[page]; + } +}; + +export { + abovefoldAlgorithm, +}; \ No newline at end of file diff --git a/app/soapbox/features/timeline-insertion/index.ts b/app/soapbox/features/timeline-insertion/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/app/soapbox/features/timeline-insertion/linear.ts b/app/soapbox/features/timeline-insertion/linear.ts new file mode 100644 index 000000000..923ef1207 --- /dev/null +++ b/app/soapbox/features/timeline-insertion/linear.ts @@ -0,0 +1,19 @@ +import type { PickAlgorithm } from './types'; + +type Opts = { + /** Number of iterations until the next item is picked. */ + interval: number, +}; + +/** Picks the next item every `interval` iterations. */ +const linearAlgorithm: PickAlgorithm = (items, index, opts: Opts) => { + const itemIndex = items ? Math.floor((index + 1) / opts.interval) % items.length : 0; + const item = items ? items[itemIndex] : undefined; + const showItem = (index + 1) % opts.interval === 0; + + return showItem ? item : undefined; +}; + +export { + linearAlgorithm, +}; \ No newline at end of file diff --git a/app/soapbox/features/timeline-insertion/types.ts b/app/soapbox/features/timeline-insertion/types.ts new file mode 100644 index 000000000..0b6fd6c0c --- /dev/null +++ b/app/soapbox/features/timeline-insertion/types.ts @@ -0,0 +1,15 @@ +/** + * Returns an item to insert at the index, or `undefined` if an item shouldn't be inserted. + */ +type PickAlgorithm = ( + /** Elligible candidates to pick. */ + items: D[], + /** Current iteration by which an item may be chosen. */ + index: number, + /** Implementation-specific opts. */ + opts: any +) => D | undefined; + +export { + PickAlgorithm, +}; \ No newline at end of file diff --git a/package.json b/package.json index eef743978..1ba60028b 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@types/react-swipeable-views": "^0.13.1", "@types/react-toggle": "^4.0.3", "@types/redux-mock-store": "^1.0.3", + "@types/seedrandom": "^3.0.2", "@types/semver": "^7.3.9", "@types/uuid": "^8.3.4", "array-includes": "^3.1.5", @@ -184,6 +185,7 @@ "resize-observer-polyfill": "^1.5.1", "sass": "^1.20.3", "sass-loader": "^13.0.0", + "seedrandom": "^3.0.5", "semver": "^7.3.2", "stringz": "^2.0.0", "substring-trie": "^1.0.2", diff --git a/yarn.lock b/yarn.lock index 1e2044863..a3cbcff2e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2841,6 +2841,11 @@ dependencies: schema-utils "*" +"@types/seedrandom@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-3.0.2.tgz#7f30db28221067a90b02e73ffd46b6685b18df1a" + integrity sha512-YPLqEOo0/X8JU3rdiq+RgUKtQhQtrppE766y7vMTu8dGML7TVtZNiiiaC/hhU9Zqw9UYopXxhuWWENclMVBwKQ== + "@types/semver@^7.3.9": version "7.3.9" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.9.tgz#152c6c20a7688c30b967ec1841d31ace569863fc" @@ -10461,6 +10466,11 @@ scroll-behavior@^0.9.1: dom-helpers "^3.4.0" invariant "^2.2.4" +seedrandom@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" + integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" From 5749821b365d1696082222eea810512cf1386880 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 9 Sep 2022 20:47:51 -0500 Subject: [PATCH 2/7] Algorithms: index --> iteration --- app/soapbox/features/timeline-insertion/abovefold.ts | 6 +++--- app/soapbox/features/timeline-insertion/linear.ts | 8 ++++---- app/soapbox/features/timeline-insertion/types.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/soapbox/features/timeline-insertion/abovefold.ts b/app/soapbox/features/timeline-insertion/abovefold.ts index 78a8b6e6f..dc6e553d1 100644 --- a/app/soapbox/features/timeline-insertion/abovefold.ts +++ b/app/soapbox/features/timeline-insertion/abovefold.ts @@ -21,11 +21,11 @@ type Opts = { * Algorithm to display items per-page. * One item is randomly inserted into each page within the index range. */ -const abovefoldAlgorithm: PickAlgorithm = (items, index, opts: Opts) => { +const abovefoldAlgorithm: PickAlgorithm = (items, iteration, opts: Opts) => { /** Current page of the index. */ - const page = Math.floor(((index + 1) / opts.pageSize) - 1); + const page = Math.floor(((iteration + 1) / opts.pageSize) - 1); /** Current index within the page. */ - const pageIndex = ((index + 1) % opts.pageSize) - 1; + const pageIndex = ((iteration + 1) % opts.pageSize) - 1; /** RNG for the page. */ const rng = seedrandom(`${opts.seed}-page-${page}`); /** Index to insert the item. */ diff --git a/app/soapbox/features/timeline-insertion/linear.ts b/app/soapbox/features/timeline-insertion/linear.ts index 923ef1207..3037c3837 100644 --- a/app/soapbox/features/timeline-insertion/linear.ts +++ b/app/soapbox/features/timeline-insertion/linear.ts @@ -5,11 +5,11 @@ type Opts = { interval: number, }; -/** Picks the next item every `interval` iterations. */ -const linearAlgorithm: PickAlgorithm = (items, index, opts: Opts) => { - const itemIndex = items ? Math.floor((index + 1) / opts.interval) % items.length : 0; +/** Picks the next item every iteration. */ +const linearAlgorithm: PickAlgorithm = (items, iteration, opts: Opts) => { + const itemIndex = items ? Math.floor((iteration + 1) / opts.interval) % items.length : 0; const item = items ? items[itemIndex] : undefined; - const showItem = (index + 1) % opts.interval === 0; + const showItem = (iteration + 1) % opts.interval === 0; return showItem ? item : undefined; }; diff --git a/app/soapbox/features/timeline-insertion/types.ts b/app/soapbox/features/timeline-insertion/types.ts index 0b6fd6c0c..c1cc1ed1d 100644 --- a/app/soapbox/features/timeline-insertion/types.ts +++ b/app/soapbox/features/timeline-insertion/types.ts @@ -5,7 +5,7 @@ type PickAlgorithm = ( /** Elligible candidates to pick. */ items: D[], /** Current iteration by which an item may be chosen. */ - index: number, + iteration: number, /** Implementation-specific opts. */ opts: any ) => D | undefined; From ec225ea1c5b4de5737970f42d46d62b580260922 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 9 Sep 2022 20:49:17 -0500 Subject: [PATCH 3/7] abovefoldAlgorithm: wrap item selection --- app/soapbox/features/timeline-insertion/abovefold.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/features/timeline-insertion/abovefold.ts b/app/soapbox/features/timeline-insertion/abovefold.ts index dc6e553d1..71ab51f81 100644 --- a/app/soapbox/features/timeline-insertion/abovefold.ts +++ b/app/soapbox/features/timeline-insertion/abovefold.ts @@ -32,7 +32,7 @@ const abovefoldAlgorithm: PickAlgorithm = (items, iteration, opts: Opts) => { const insertIndex = Math.floor(rng() * opts.range[1] - opts.range[0]) + opts.range[0]; if (pageIndex === insertIndex) { - return items[page]; + return items[page % items.length]; } }; From 2681b32f7d21eb3d7a6dc0d66e5cd287f18c8b1a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 9 Sep 2022 22:26:36 -0500 Subject: [PATCH 4/7] StatusList: incorporate feed injection algorithms --- app/soapbox/components/status_list.tsx | 26 ++++++++++++------- .../features/timeline-insertion/abovefold.ts | 23 ++++++++++++---- .../features/timeline-insertion/index.ts | 11 ++++++++ .../features/timeline-insertion/linear.ts | 11 +++++++- .../features/timeline-insertion/types.ts | 2 +- .../normalizers/soapbox/soapbox_config.ts | 14 ++++++++++ 6 files changed, 71 insertions(+), 16 deletions(-) diff --git a/app/soapbox/components/status_list.tsx b/app/soapbox/components/status_list.tsx index 295538da2..3bee7a03a 100644 --- a/app/soapbox/components/status_list.tsx +++ b/app/soapbox/components/status_list.tsx @@ -1,7 +1,9 @@ import classNames from 'clsx'; +import { Map as ImmutableMap } from 'immutable'; import debounce from 'lodash/debounce'; import React, { useRef, useCallback } from 'react'; import { FormattedMessage } from 'react-intl'; +import { v4 as uuidv4 } from 'uuid'; import LoadGap from 'soapbox/components/load_gap'; 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 FeedSuggestions from 'soapbox/features/feed-suggestions/feed-suggestions'; 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 { useSoapboxConfig } from 'soapbox/hooks'; import useAds from 'soapbox/queries/ads'; @@ -60,8 +63,12 @@ const StatusList: React.FC = ({ }) => { const { data: ads } = useAds(); 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).toJS(); + const node = useRef(null); + const seed = useRef(uuidv4()); const getFeaturedStatusCount = () => { return featuredStatusIds?.size || 0; @@ -132,9 +139,10 @@ const StatusList: React.FC = ({ ); }; - const renderAd = (ad: AdEntity) => { + const renderAd = (ad: AdEntity, index: number) => { return ( = ({ const renderStatuses = (): React.ReactNode[] => { if (isLoading || statusIds.size > 0) { return statusIds.toList().reduce((acc, statusId, index) => { - const adIndex = ads ? Math.floor((index + 1) / adsInterval) % ads.length : 0; - const ad = ads ? ads[adIndex] : undefined; - const showAd = (index + 1) % adsInterval === 0; + if (showAds && ads) { + const ad = ALGORITHMS[adsAlgorithm]?.(ads, index, { ...adsOpts, seed: seed.current }); + + if (ad) { + acc.push(renderAd(ad, index)); + } + } if (statusId === null) { acc.push(renderLoadGap(index)); @@ -189,10 +201,6 @@ const StatusList: React.FC = ({ acc.push(renderStatus(statusId)); } - if (showAds && ad && showAd) { - acc.push(renderAd(ad)); - } - return acc; }, [] as React.ReactNode[]); } else { diff --git a/app/soapbox/features/timeline-insertion/abovefold.ts b/app/soapbox/features/timeline-insertion/abovefold.ts index 71ab51f81..5ab3b4306 100644 --- a/app/soapbox/features/timeline-insertion/abovefold.ts +++ b/app/soapbox/features/timeline-insertion/abovefold.ts @@ -8,7 +8,7 @@ type Opts = { /** * 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`. */ @@ -21,21 +21,34 @@ type Opts = { * Algorithm to display items per-page. * 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. */ - const page = Math.floor(((iteration + 1) / opts.pageSize) - 1); + const page = Math.floor(iteration / opts.pageSize); /** Current index within the page. */ - const pageIndex = ((iteration + 1) % opts.pageSize) - 1; + const pageIndex = (iteration % opts.pageSize); /** RNG for the page. */ const rng = seedrandom(`${opts.seed}-page-${page}`); /** 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) { return items[page % items.length]; } }; +const normalizeOpts = (opts: unknown): Opts => { + const { seed, range, pageSize } = (opts && typeof opts === 'object' ? opts : {}) as Record; + + 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 { abovefoldAlgorithm, }; \ No newline at end of file diff --git a/app/soapbox/features/timeline-insertion/index.ts b/app/soapbox/features/timeline-insertion/index.ts index e69de29bb..f4e00ed29 100644 --- a/app/soapbox/features/timeline-insertion/index.ts +++ b/app/soapbox/features/timeline-insertion/index.ts @@ -0,0 +1,11 @@ +import { abovefoldAlgorithm } from './abovefold'; +import { linearAlgorithm } from './linear'; + +import type { PickAlgorithm } from './types'; + +const ALGORITHMS: Record = { + 'linear': linearAlgorithm, + 'abovefold': abovefoldAlgorithm, +}; + +export { ALGORITHMS }; \ No newline at end of file diff --git a/app/soapbox/features/timeline-insertion/linear.ts b/app/soapbox/features/timeline-insertion/linear.ts index 3037c3837..a3cbce685 100644 --- a/app/soapbox/features/timeline-insertion/linear.ts +++ b/app/soapbox/features/timeline-insertion/linear.ts @@ -6,7 +6,8 @@ type Opts = { }; /** 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 item = items ? items[itemIndex] : undefined; const showItem = (iteration + 1) % opts.interval === 0; @@ -14,6 +15,14 @@ const linearAlgorithm: PickAlgorithm = (items, iteration, opts: Opts) => { return showItem ? item : undefined; }; +const normalizeOpts = (opts: unknown): Opts => { + const { interval } = (opts && typeof opts === 'object' ? opts : {}) as Record; + + return { + interval: typeof interval === 'number' ? interval : 20, + }; +}; + export { linearAlgorithm, }; \ No newline at end of file diff --git a/app/soapbox/features/timeline-insertion/types.ts b/app/soapbox/features/timeline-insertion/types.ts index c1cc1ed1d..69b6280c4 100644 --- a/app/soapbox/features/timeline-insertion/types.ts +++ b/app/soapbox/features/timeline-insertion/types.ts @@ -7,7 +7,7 @@ type PickAlgorithm = ( /** Current iteration by which an item may be chosen. */ iteration: number, /** Implementation-specific opts. */ - opts: any + opts: Record ) => D | undefined; export { diff --git a/app/soapbox/normalizers/soapbox/soapbox_config.ts b/app/soapbox/normalizers/soapbox/soapbox_config.ts index a471401c5..0e6b5c280 100644 --- a/app/soapbox/normalizers/soapbox/soapbox_config.ts +++ b/app/soapbox/normalizers/soapbox/soapbox_config.ts @@ -175,6 +175,19 @@ const normalizeFooterLinks = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMap 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) => { return SoapboxConfigRecord( ImmutableMap(fromJS(soapboxConfig)).withMutations(soapboxConfig => { @@ -186,6 +199,7 @@ export const normalizeSoapboxConfig = (soapboxConfig: Record) => { maybeAddMissingColors(soapboxConfig); normalizeCryptoAddresses(soapboxConfig); normalizeAds(soapboxConfig); + normalizeAdsAlgorithm(soapboxConfig); }), ); }; From 94c2f5e97882cb2195234f6cf4579c6e33c41688 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 12 Sep 2022 10:47:01 -0500 Subject: [PATCH 5/7] abovefoldAlgorithm: remove accidental console.log --- app/soapbox/features/timeline-insertion/abovefold.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/soapbox/features/timeline-insertion/abovefold.ts b/app/soapbox/features/timeline-insertion/abovefold.ts index 5ab3b4306..9ca2f5e28 100644 --- a/app/soapbox/features/timeline-insertion/abovefold.ts +++ b/app/soapbox/features/timeline-insertion/abovefold.ts @@ -32,8 +32,6 @@ const abovefoldAlgorithm: PickAlgorithm = (items, iteration, rawOpts) => { /** Index to insert the item. */ const insertIndex = Math.floor(rng() * (opts.range[1] - opts.range[0])) + opts.range[0]; - console.log({ page, iteration, pageIndex, insertIndex }); - if (pageIndex === insertIndex) { return items[page % items.length]; } From 4ff9918fe05d1e94a3c3e396a595309e614222bf Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 13 Sep 2022 11:44:21 -0500 Subject: [PATCH 6/7] abovefoldAlgorithm: add basic tests --- .../__tests__/abovefold.test.ts | 18 ++++++++++++++++++ .../features/timeline-insertion/types.ts | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 app/soapbox/features/timeline-insertion/__tests__/abovefold.test.ts diff --git a/app/soapbox/features/timeline-insertion/__tests__/abovefold.test.ts b/app/soapbox/features/timeline-insertion/__tests__/abovefold.test.ts new file mode 100644 index 000000000..81de8c1a4 --- /dev/null +++ b/app/soapbox/features/timeline-insertion/__tests__/abovefold.test.ts @@ -0,0 +1,18 @@ +import { abovefoldAlgorithm } from '../abovefold'; + +const DATA = Object.freeze(['a', 'b', 'c', 'd']); + +test('abovefoldAlgorithm', () => { + const result = Array(50).fill('').map((_, i) => { + return abovefoldAlgorithm(DATA, i, { seed: '!', range: [2, 6], pageSize: 20 }); + }); + + // console.log(result); + expect(result[0]).toBe(undefined); + expect(result[4]).toBe('a'); + expect(result[5]).toBe(undefined); + expect(result[24]).toBe('b'); + expect(result[30]).toBe(undefined); + expect(result[42]).toBe('c'); + expect(result[43]).toBe(undefined); +}); \ No newline at end of file diff --git a/app/soapbox/features/timeline-insertion/types.ts b/app/soapbox/features/timeline-insertion/types.ts index 69b6280c4..b874754d0 100644 --- a/app/soapbox/features/timeline-insertion/types.ts +++ b/app/soapbox/features/timeline-insertion/types.ts @@ -3,7 +3,7 @@ */ type PickAlgorithm = ( /** Elligible candidates to pick. */ - items: D[], + items: readonly D[], /** Current iteration by which an item may be chosen. */ iteration: number, /** Implementation-specific opts. */ From 474d7da02ad2f61fac5b0d6c65eac1305bcda222 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 13 Sep 2022 11:57:28 -0500 Subject: [PATCH 7/7] linearAlgorithm: add test, fix selection order --- .../__tests__/linear.test.ts | 19 +++++++++++++++++++ .../features/timeline-insertion/linear.ts | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 app/soapbox/features/timeline-insertion/__tests__/linear.test.ts diff --git a/app/soapbox/features/timeline-insertion/__tests__/linear.test.ts b/app/soapbox/features/timeline-insertion/__tests__/linear.test.ts new file mode 100644 index 000000000..09d484f12 --- /dev/null +++ b/app/soapbox/features/timeline-insertion/__tests__/linear.test.ts @@ -0,0 +1,19 @@ +import { linearAlgorithm } from '../linear'; + +const DATA = Object.freeze(['a', 'b', 'c', 'd']); + +test('linearAlgorithm', () => { + const result = Array(50).fill('').map((_, i) => { + return linearAlgorithm(DATA, i, { interval: 5 }); + }); + + // console.log(result); + expect(result[0]).toBe(undefined); + expect(result[4]).toBe('a'); + expect(result[8]).toBe(undefined); + expect(result[9]).toBe('b'); + expect(result[10]).toBe(undefined); + expect(result[14]).toBe('c'); + expect(result[15]).toBe(undefined); + expect(result[19]).toBe('d'); +}); \ No newline at end of file diff --git a/app/soapbox/features/timeline-insertion/linear.ts b/app/soapbox/features/timeline-insertion/linear.ts index a3cbce685..a542e1fce 100644 --- a/app/soapbox/features/timeline-insertion/linear.ts +++ b/app/soapbox/features/timeline-insertion/linear.ts @@ -8,7 +8,7 @@ type Opts = { /** Picks the next item every iteration. */ 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 / opts.interval) % items.length : 0; const item = items ? items[itemIndex] : undefined; const showItem = (iteration + 1) % opts.interval === 0;