From 757d8ba5451d944f30208b811a5f8e51386e859b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 19 Sep 2023 11:17:12 -0500 Subject: [PATCH] Remove Truth Social Ads --- package.json | 1 - src/components/status-list.tsx | 26 ---- src/features/ads/components/ad.tsx | 142 ------------------ src/features/ads/providers/index.ts | 42 ------ src/features/ads/providers/soapbox-config.ts | 14 -- src/features/ads/providers/truth.ts | 40 ----- .../__tests__/abovefold.test.ts | 18 --- .../__tests__/linear.test.ts | 19 --- src/features/timeline-insertion/abovefold.ts | 52 ------- src/features/timeline-insertion/index.ts | 11 -- src/features/timeline-insertion/linear.ts | 28 ---- src/features/timeline-insertion/types.ts | 15 -- src/normalizers/soapbox/soapbox-config.ts | 26 ---- src/queries/ads.ts | 42 ------ src/schemas/index.ts | 3 - src/schemas/soapbox/ad.ts | 14 -- src/utils/__tests__/ads.test.ts | 23 --- src/utils/ads.ts | 16 -- yarn.lock | 5 - 19 files changed, 537 deletions(-) delete mode 100644 src/features/ads/components/ad.tsx delete mode 100644 src/features/ads/providers/index.ts delete mode 100644 src/features/ads/providers/soapbox-config.ts delete mode 100644 src/features/ads/providers/truth.ts delete mode 100644 src/features/timeline-insertion/__tests__/abovefold.test.ts delete mode 100644 src/features/timeline-insertion/__tests__/linear.test.ts delete mode 100644 src/features/timeline-insertion/abovefold.ts delete mode 100644 src/features/timeline-insertion/index.ts delete mode 100644 src/features/timeline-insertion/linear.ts delete mode 100644 src/features/timeline-insertion/types.ts delete mode 100644 src/queries/ads.ts delete mode 100644 src/schemas/soapbox/ad.ts delete mode 100644 src/utils/__tests__/ads.test.ts delete mode 100644 src/utils/ads.ts diff --git a/package.json b/package.json index 005a39bbc..a3e0dd47d 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,6 @@ "reselect": "^4.0.0", "resize-observer-polyfill": "^1.5.1", "sass": "^1.66.1", - "seedrandom": "^3.0.5", "semver": "^7.3.8", "stringz": "^2.0.0", "substring-trie": "^1.0.2", diff --git a/src/components/status-list.tsx b/src/components/status-list.tsx index 1d4165d22..a2507df6a 100644 --- a/src/components/status-list.tsx +++ b/src/components/status-list.tsx @@ -1,25 +1,19 @@ import clsx 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'; 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'; import type { OrderedSet as ImmutableOrderedSet } from 'immutable'; import type { VirtuosoHandle } from 'react-virtuoso'; import type { IScrollableList } from 'soapbox/components/scrollable-list'; -import type { Ad as AdEntity } from 'soapbox/types/soapbox'; interface IStatusList extends Omit { /** Unique key to preserve the scroll position when navigating back. */ @@ -64,14 +58,8 @@ const StatusList: React.FC = ({ showGroup = true, ...other }) => { - const { data: ads } = useAds(); const soapboxConfig = useSoapboxConfig(); - - 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; @@ -144,12 +132,6 @@ const StatusList: React.FC = ({ ); }; - const renderAd = (ad: AdEntity, index: number) => { - return ( - - ); - }; - const renderPendingStatus = (statusId: string) => { const idempotencyKey = statusId.replace(/^ęœ«pending-/, ''); @@ -192,14 +174,6 @@ const StatusList: React.FC = ({ const renderStatuses = (): React.ReactNode[] => { if (isLoading || statusIds.size > 0) { return statusIds.toList().reduce((acc, statusId, index) => { - if (showAds && ads) { - const ad = ALGORITHMS[adsAlgorithm]?.(ads, index, { ...adsOpts, seed: seed.current }); - - if (ad) { - acc.push(renderAd(ad, index)); - } - } - if (statusId === null) { const gap = renderLoadGap(index); // one does not simply push a null item to Virtuoso: https://github.com/petyosi/react-virtuoso/issues/206#issuecomment-747363793 diff --git a/src/features/ads/components/ad.tsx b/src/features/ads/components/ad.tsx deleted file mode 100644 index 4636795bc..000000000 --- a/src/features/ads/components/ad.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { useQuery, useQueryClient } from '@tanstack/react-query'; -import axios from 'axios'; -import React, { useState, useEffect, useRef } from 'react'; -import { FormattedMessage } from 'react-intl'; - -import { Avatar, Card, HStack, Icon, IconButton, Stack, Text } from 'soapbox/components/ui'; -import StatusCard from 'soapbox/features/status/components/card'; -import { useInstance } from 'soapbox/hooks'; -import { AdKeys } from 'soapbox/queries/ads'; - -import type { Ad as AdEntity } from 'soapbox/types/soapbox'; - -interface IAd { - ad: AdEntity -} - -/** Displays an ad in sponsored post format. */ -const Ad: React.FC = ({ ad }) => { - const queryClient = useQueryClient(); - const instance = useInstance(); - - const timer = useRef(undefined); - const infobox = useRef(null); - const [showInfo, setShowInfo] = useState(false); - - // Fetch the impression URL (if any) upon displaying the ad. - // Don't fetch it more than once. - useQuery(['ads', 'impression', ad.impression], async () => { - if (ad.impression) { - return await axios.get(ad.impression); - } - }, { cacheTime: Infinity, staleTime: Infinity }); - - /** Invalidate query cache for ads. */ - const bustCache = (): void => { - queryClient.invalidateQueries(AdKeys.ads); - }; - - /** Toggle the info box on click. */ - const handleInfoButtonClick: React.MouseEventHandler = () => { - setShowInfo(!showInfo); - }; - - /** Hide the info box when clicked outside. */ - const handleClickOutside = (event: MouseEvent) => { - if (event.target && infobox.current && !infobox.current.contains(event.target as any)) { - setShowInfo(false); - } - }; - - // Hide the info box when clicked outside. - // https://stackoverflow.com/a/42234988 - useEffect(() => { - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [infobox]); - - // Wait until the ad expires, then invalidate cache. - useEffect(() => { - if (ad.expires_at) { - const delta = new Date(ad.expires_at).getTime() - (new Date()).getTime(); - timer.current = setTimeout(bustCache, delta); - } - - return () => { - if (timer.current) { - clearTimeout(timer.current); - } - }; - }, [ad.expires_at]); - - return ( -
- - - - - - - - - {instance.title} - - - - - - - - - - - - - - - - - - - - { }} horizontal /> - - - - {showInfo && ( -
- - - - - - - - {ad.reason ? ( - ad.reason - ) : ( - - )} - - - -
- )} -
- ); -}; - -export default Ad; diff --git a/src/features/ads/providers/index.ts b/src/features/ads/providers/index.ts deleted file mode 100644 index cd7ba44c7..000000000 --- a/src/features/ads/providers/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { getSoapboxConfig } from 'soapbox/actions/soapbox'; - -import type { RootState } from 'soapbox/store'; -import type { Card } from 'soapbox/types/entities'; - -/** Map of available provider modules. */ -const PROVIDERS: Record Promise> = { - soapbox: async() => (await import('./soapbox-config')).default, - truth: async() => (await import('./truth')).default, -}; - -/** Ad server implementation. */ -interface AdProvider { - getAds(getState: () => RootState): Promise -} - -/** Entity representing an advertisement. */ -interface Ad { - /** Ad data in Card (OEmbed-ish) format. */ - card: Card - /** Impression URL to fetch when displaying the ad. */ - impression?: string - /** Time when the ad expires and should no longer be displayed. */ - expires_at?: string - /** Reason the ad is displayed. */ - reason?: string -} - -/** Gets the current provider based on config. */ -const getProvider = async(getState: () => RootState): Promise => { - const state = getState(); - const soapboxConfig = getSoapboxConfig(state); - const isEnabled = soapboxConfig.extensions.getIn(['ads', 'enabled'], false) === true; - const providerName = soapboxConfig.extensions.getIn(['ads', 'provider'], 'soapbox') as string; - - if (isEnabled && PROVIDERS[providerName]) { - return PROVIDERS[providerName](); - } -}; - -export { getProvider }; -export type { Ad, AdProvider }; diff --git a/src/features/ads/providers/soapbox-config.ts b/src/features/ads/providers/soapbox-config.ts deleted file mode 100644 index 21163729c..000000000 --- a/src/features/ads/providers/soapbox-config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { getSoapboxConfig } from 'soapbox/actions/soapbox'; - -import type { AdProvider } from '.'; - -/** Provides ads from Soapbox Config. */ -const SoapboxConfigAdProvider: AdProvider = { - getAds: async(getState) => { - const state = getState(); - const soapboxConfig = getSoapboxConfig(state); - return soapboxConfig.ads.toArray(); - }, -}; - -export default SoapboxConfigAdProvider; diff --git a/src/features/ads/providers/truth.ts b/src/features/ads/providers/truth.ts deleted file mode 100644 index 5582bd3cf..000000000 --- a/src/features/ads/providers/truth.ts +++ /dev/null @@ -1,40 +0,0 @@ -import axios from 'axios'; -import { z } from 'zod'; - -import { getSettings } from 'soapbox/actions/settings'; -import { cardSchema } from 'soapbox/schemas/card'; -import { filteredArray } from 'soapbox/schemas/utils'; - -import type { AdProvider } from '.'; - -/** TruthSocial ad API entity. */ -const truthAdSchema = z.object({ - impression: z.string(), - card: cardSchema, - expires_at: z.string(), - reason: z.string().catch(''), -}); - -/** Provides ads from the TruthSocial API. */ -const TruthAdProvider: AdProvider = { - getAds: async(getState) => { - const state = getState(); - const settings = getSettings(state); - - try { - const { data } = await axios.get('/api/v2/truth/ads?device=desktop', { - headers: { - 'Accept-Language': z.string().catch('*').parse(settings.get('locale')), - }, - }); - - return filteredArray(truthAdSchema).parse(data); - } catch (e) { - // do nothing - } - - return []; - }, -}; - -export default TruthAdProvider; diff --git a/src/features/timeline-insertion/__tests__/abovefold.test.ts b/src/features/timeline-insertion/__tests__/abovefold.test.ts deleted file mode 100644 index 81de8c1a4..000000000 --- a/src/features/timeline-insertion/__tests__/abovefold.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -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/src/features/timeline-insertion/__tests__/linear.test.ts b/src/features/timeline-insertion/__tests__/linear.test.ts deleted file mode 100644 index 09d484f12..000000000 --- a/src/features/timeline-insertion/__tests__/linear.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -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/src/features/timeline-insertion/abovefold.ts b/src/features/timeline-insertion/abovefold.ts deleted file mode 100644 index 5298f029c..000000000 --- a/src/features/timeline-insertion/abovefold.ts +++ /dev/null @@ -1,52 +0,0 @@ -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. `[2, 6]` will cause one item to be picked among the third through seventh indexes. - * - * `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, iteration, rawOpts) => { - const opts = normalizeOpts(rawOpts); - /** Current page of the index. */ - const page = Math.floor(iteration / opts.pageSize); - /** Current index within the page. */ - 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]; - - 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, -}; diff --git a/src/features/timeline-insertion/index.ts b/src/features/timeline-insertion/index.ts deleted file mode 100644 index f4e00ed29..000000000 --- a/src/features/timeline-insertion/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -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/src/features/timeline-insertion/linear.ts b/src/features/timeline-insertion/linear.ts deleted file mode 100644 index cae923944..000000000 --- a/src/features/timeline-insertion/linear.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { PickAlgorithm } from './types'; - -type Opts = { - /** Number of iterations until the next item is picked. */ - interval: number -}; - -/** Picks the next item every iteration. */ -const linearAlgorithm: PickAlgorithm = (items, iteration, rawOpts) => { - const opts = normalizeOpts(rawOpts); - const itemIndex = items ? Math.floor(iteration / opts.interval) % items.length : 0; - const item = items ? items[itemIndex] : undefined; - const showItem = (iteration + 1) % opts.interval === 0; - - 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/src/features/timeline-insertion/types.ts b/src/features/timeline-insertion/types.ts deleted file mode 100644 index b874754d0..000000000 --- a/src/features/timeline-insertion/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Returns an item to insert at the index, or `undefined` if an item shouldn't be inserted. - */ -type PickAlgorithm = ( - /** Elligible candidates to pick. */ - items: readonly D[], - /** Current iteration by which an item may be chosen. */ - iteration: number, - /** Implementation-specific opts. */ - opts: Record -) => D | undefined; - -export { - PickAlgorithm, -}; \ No newline at end of file diff --git a/src/normalizers/soapbox/soapbox-config.ts b/src/normalizers/soapbox/soapbox-config.ts index d003f75e0..4f25992ef 100644 --- a/src/normalizers/soapbox/soapbox-config.ts +++ b/src/normalizers/soapbox/soapbox-config.ts @@ -6,8 +6,6 @@ import { } from 'immutable'; import trimStart from 'lodash/trimStart'; -import { adSchema } from 'soapbox/schemas'; -import { filteredArray } from 'soapbox/schemas/utils'; import { normalizeUsername } from 'soapbox/utils/input'; import { toTailwind } from 'soapbox/utils/tailwind'; import { generateAccent } from 'soapbox/utils/theme'; @@ -124,15 +122,6 @@ export const SoapboxConfigRecord = ImmutableRecord({ type SoapboxConfigMap = ImmutableMap; -const normalizeAds = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMap => { - if (soapboxConfig.has('ads')) { - const ads = filteredArray(adSchema).parse(ImmutableList(soapboxConfig.get('ads')).toJS()); - return soapboxConfig.set('ads', ads); - } else { - return soapboxConfig; - } -}; - const normalizeCryptoAddress = (address: unknown): CryptoAddress => { return CryptoAddressRecord(ImmutableMap(fromJS(address))).update('ticker', ticker => { return trimStart(ticker, '$').toLowerCase(); @@ -188,19 +177,6 @@ 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; - } -}; - /** Single user mode is now managed by `redirectRootNoLogin`. */ const upgradeSingleUserMode = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMap => { const singleUserMode = soapboxConfig.get('singleUserMode') as boolean | undefined; @@ -250,8 +226,6 @@ export const normalizeSoapboxConfig = (soapboxConfig: Record) => { normalizeFooterLinks(soapboxConfig); maybeAddMissingColors(soapboxConfig); normalizeCryptoAddresses(soapboxConfig); - normalizeAds(soapboxConfig); - normalizeAdsAlgorithm(soapboxConfig); upgradeSingleUserMode(soapboxConfig); normalizeRedirectRootNoLogin(soapboxConfig); }), diff --git a/src/queries/ads.ts b/src/queries/ads.ts deleted file mode 100644 index c45bf8b33..000000000 --- a/src/queries/ads.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; - -import { Ad, getProvider } from 'soapbox/features/ads/providers'; -import { useAppDispatch } from 'soapbox/hooks'; -import { adSchema } from 'soapbox/schemas'; -import { filteredArray } from 'soapbox/schemas/utils'; -import { isExpired } from 'soapbox/utils/ads'; - -const AdKeys = { - ads: ['ads'] as const, -}; - -function useAds() { - const dispatch = useAppDispatch(); - - const getAds = async () => { - return dispatch(async (_, getState) => { - const provider = await getProvider(getState); - if (provider) { - return provider.getAds(getState); - } else { - return []; - } - }); - }; - - const result = useQuery(AdKeys.ads, getAds, { - placeholderData: [], - }); - - // Filter out expired ads. - const data = filteredArray(adSchema) - .parse(result.data) - .filter(ad => !isExpired(ad)); - - return { - ...result, - data, - }; -} - -export { useAds as default, AdKeys }; diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 2c99ef8b8..81b507ce5 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -16,6 +16,3 @@ export { relationshipSchema, type Relationship } from './relationship'; export { statusSchema, type Status } from './status'; export { tagSchema, type Tag } from './tag'; export { tombstoneSchema, type Tombstone } from './tombstone'; - -// Soapbox -export { adSchema, type Ad } from './soapbox/ad'; \ No newline at end of file diff --git a/src/schemas/soapbox/ad.ts b/src/schemas/soapbox/ad.ts deleted file mode 100644 index 40dc05fb3..000000000 --- a/src/schemas/soapbox/ad.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { z } from 'zod'; - -import { cardSchema } from '../card'; - -const adSchema = z.object({ - card: cardSchema, - impression: z.string().optional().catch(undefined), - expires_at: z.string().datetime().optional().catch(undefined), - reason: z.string().optional().catch(undefined), -}); - -type Ad = z.infer; - -export { adSchema, type Ad }; \ No newline at end of file diff --git a/src/utils/__tests__/ads.test.ts b/src/utils/__tests__/ads.test.ts deleted file mode 100644 index 5ceb9d45b..000000000 --- a/src/utils/__tests__/ads.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { buildAd } from 'soapbox/jest/factory'; - -import { isExpired } from '../ads'; - -/** 3 minutes in milliseconds. */ -const threeMins = 3 * 60 * 1000; - -/** 5 minutes in milliseconds. */ -const fiveMins = 5 * 60 * 1000; - -test('isExpired()', () => { - const now = new Date(); - const iso = now.toISOString(); - const epoch = now.getTime(); - - // Sanity tests. - expect(isExpired(buildAd({ expires_at: iso }))).toBe(true); - expect(isExpired(buildAd({ expires_at: new Date(epoch + 999999).toISOString() }))).toBe(false); - - // Testing the 5-minute mark. - expect(isExpired(buildAd({ expires_at: new Date(epoch + threeMins).toISOString() }), fiveMins)).toBe(true); - expect(isExpired(buildAd({ expires_at: new Date(epoch + fiveMins + 1000).toISOString() }), fiveMins)).toBe(false); -}); diff --git a/src/utils/ads.ts b/src/utils/ads.ts deleted file mode 100644 index ed2bf0cad..000000000 --- a/src/utils/ads.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Ad } from 'soapbox/schemas'; - -/** Time (ms) window to not display an ad if it's about to expire. */ -const AD_EXPIRY_THRESHOLD = 5 * 60 * 1000; - -/** Whether the ad is expired or about to expire. */ -const isExpired = (ad: Pick, threshold = AD_EXPIRY_THRESHOLD): boolean => { - if (ad.expires_at) { - const now = new Date(); - return now.getTime() > (new Date(ad.expires_at).getTime() - threshold); - } else { - return false; - } -}; - -export { isExpired }; diff --git a/yarn.lock b/yarn.lock index b74ead8a1..76f2a23e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7902,11 +7902,6 @@ 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== - semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"