From f4af1687bf12543f89dfe37d12bd40061859dc7d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 26 Aug 2022 09:48:49 -0500 Subject: [PATCH 1/4] Filter out expiring ads --- app/soapbox/features/ads/providers/index.ts | 2 ++ app/soapbox/features/ads/providers/rumble.ts | 1 + app/soapbox/queries/ads.ts | 14 +++++++++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/soapbox/features/ads/providers/index.ts b/app/soapbox/features/ads/providers/index.ts index 65e593985f..b9c504bff6 100644 --- a/app/soapbox/features/ads/providers/index.ts +++ b/app/soapbox/features/ads/providers/index.ts @@ -20,6 +20,8 @@ interface Ad { card: Card, /** Impression URL to fetch when displaying the ad. */ impression?: string, + /** Time when the ad expires and should no longer be displayed. */ + expires?: Date, } /** Gets the current provider based on config. */ diff --git a/app/soapbox/features/ads/providers/rumble.ts b/app/soapbox/features/ads/providers/rumble.ts index 249fc85472..ace4021f06 100644 --- a/app/soapbox/features/ads/providers/rumble.ts +++ b/app/soapbox/features/ads/providers/rumble.ts @@ -43,6 +43,7 @@ const RumbleAdProvider: AdProvider = { image: item.asset, url: item.click, }), + expires: new Date(item.expires * 1000), })); } } diff --git a/app/soapbox/queries/ads.ts b/app/soapbox/queries/ads.ts index 7ad594a94d..0b296f6774 100644 --- a/app/soapbox/queries/ads.ts +++ b/app/soapbox/queries/ads.ts @@ -17,7 +17,19 @@ export default function useAds() { }); }; - return useQuery(['ads'], getAds, { + const result = useQuery(['ads'], getAds, { placeholderData: [], }); + + // Filter out expired ads. + const data = result.data?.filter(ad => { + const now = new Date(); + const isExpired = ad.expires && (now.getTime() > ad.expires.getTime()); + return !isExpired; + }); + + return { + ...result, + data, + }; } From d5a066050f9fbddcee093dbbf4c1a4599259df58 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 26 Aug 2022 10:14:56 -0500 Subject: [PATCH 2/4] Ads: move isExpired to tested utils function --- app/soapbox/queries/ads.ts | 7 ++----- app/soapbox/utils/__tests__/ads.test.ts | 22 ++++++++++++++++++++++ app/soapbox/utils/ads.ts | 16 ++++++++++++++++ 3 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 app/soapbox/utils/__tests__/ads.test.ts create mode 100644 app/soapbox/utils/ads.ts diff --git a/app/soapbox/queries/ads.ts b/app/soapbox/queries/ads.ts index 0b296f6774..91c7da2fb2 100644 --- a/app/soapbox/queries/ads.ts +++ b/app/soapbox/queries/ads.ts @@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { Ad, getProvider } from 'soapbox/features/ads/providers'; import { useAppDispatch } from 'soapbox/hooks'; +import { isExpired } from 'soapbox/utils/ads'; export default function useAds() { const dispatch = useAppDispatch(); @@ -22,11 +23,7 @@ export default function useAds() { }); // Filter out expired ads. - const data = result.data?.filter(ad => { - const now = new Date(); - const isExpired = ad.expires && (now.getTime() > ad.expires.getTime()); - return !isExpired; - }); + const data = result.data?.filter(isExpired); return { ...result, diff --git a/app/soapbox/utils/__tests__/ads.test.ts b/app/soapbox/utils/__tests__/ads.test.ts new file mode 100644 index 0000000000..9890481109 --- /dev/null +++ b/app/soapbox/utils/__tests__/ads.test.ts @@ -0,0 +1,22 @@ +import { normalizeCard } from 'soapbox/normalizers'; + +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 card = normalizeCard({}); + + // Sanity tests. + expect(isExpired({ expires: now, card })).toBe(true); + expect(isExpired({ expires: new Date(now.getTime() + 999999), card })).toBe(false); + + // Testing the 5-minute mark. + expect(isExpired({ expires: new Date(now.getTime() + threeMins), card }, fiveMins)).toBe(true); + expect(isExpired({ expires: new Date(now.getTime() + fiveMins + 1000), card }, fiveMins)).toBe(false); +}); diff --git a/app/soapbox/utils/ads.ts b/app/soapbox/utils/ads.ts new file mode 100644 index 0000000000..2d5a010403 --- /dev/null +++ b/app/soapbox/utils/ads.ts @@ -0,0 +1,16 @@ +import type { Ad } from 'soapbox/features/ads/providers'; + +/** 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: Ad, threshold = AD_EXPIRY_THRESHOLD): boolean => { + if (ad.expires) { + const now = new Date(); + return now.getTime() > (ad.expires.getTime() - threshold); + } else { + return false; + } +}; + +export { isExpired }; From 194cf89dd9b446f2da2e5fae956629b918c0d3be Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 26 Aug 2022 13:58:02 -0500 Subject: [PATCH 3/4] Ads: bust query-cache when an ad expires --- app/soapbox/components/status_list.tsx | 1 + app/soapbox/features/ads/components/ad.tsx | 25 +++++++++++++++++++++- app/soapbox/normalizers/soapbox/ad.ts | 1 + 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/app/soapbox/components/status_list.tsx b/app/soapbox/components/status_list.tsx index c45216e035..3f0b262b55 100644 --- a/app/soapbox/components/status_list.tsx +++ b/app/soapbox/components/status_list.tsx @@ -137,6 +137,7 @@ const StatusList: React.FC = ({ ); }; diff --git a/app/soapbox/features/ads/components/ad.tsx b/app/soapbox/features/ads/components/ad.tsx index 7ab86c2c04..871b76d067 100644 --- a/app/soapbox/features/ads/components/ad.tsx +++ b/app/soapbox/features/ads/components/ad.tsx @@ -1,3 +1,4 @@ +import { useQueryClient } from '@tanstack/react-query'; import React, { useState, useEffect, useRef } from 'react'; import { FormattedMessage } from 'react-intl'; @@ -13,15 +14,23 @@ interface IAd { card: CardEntity, /** Impression URL to fetch upon display. */ impression?: string, + expires?: Date, } /** Displays an ad in sponsored post format. */ -const Ad: React.FC = ({ card, impression }) => { +const Ad: React.FC = ({ card, impression, expires }) => { + const queryClient = useQueryClient(); const instance = useAppSelector(state => state.instance); + const timer = useRef(undefined); const infobox = useRef(null); const [showInfo, setShowInfo] = useState(false); + /** Invalidate query cache for ads. */ + const bustCache = (): void => { + queryClient.invalidateQueries(['ads']); + }; + /** Toggle the info box on click. */ const handleInfoButtonClick: React.MouseEventHandler = () => { setShowInfo(!showInfo); @@ -51,6 +60,20 @@ const Ad: React.FC = ({ card, impression }) => { } }, [impression]); + // Wait until the ad expires, then invalidate cache. + useEffect(() => { + if (expires) { + const delta = expires.getTime() - (new Date()).getTime(); + timer.current = setTimeout(bustCache, delta); + } + + return () => { + if (timer.current) { + clearTimeout(timer.current); + } + }; + }, [expires]); + return (
diff --git a/app/soapbox/normalizers/soapbox/ad.ts b/app/soapbox/normalizers/soapbox/ad.ts index c29ee9a3e6..115ad529c3 100644 --- a/app/soapbox/normalizers/soapbox/ad.ts +++ b/app/soapbox/normalizers/soapbox/ad.ts @@ -9,6 +9,7 @@ import { CardRecord, normalizeCard } from '../card'; export const AdRecord = ImmutableRecord({ card: CardRecord(), impression: undefined as string | undefined, + expires: undefined as Date | undefined, }); /** Normalizes an ad from Soapbox Config. */ From 4f11f3288dde4f2e77099882a9e3ff2aa5df081b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 29 Aug 2022 11:11:11 -0500 Subject: [PATCH 4/4] Ad: add missing jsx comment --- app/soapbox/features/ads/components/ad.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/soapbox/features/ads/components/ad.tsx b/app/soapbox/features/ads/components/ad.tsx index 871b76d067..5c1ff6a410 100644 --- a/app/soapbox/features/ads/components/ad.tsx +++ b/app/soapbox/features/ads/components/ad.tsx @@ -14,6 +14,7 @@ interface IAd { card: CardEntity, /** Impression URL to fetch upon display. */ impression?: string, + /** Time when the ad expires and should no longer be displayed. */ expires?: Date, }