Merge branch 'expiring-ads' into 'develop'

Expiring ads

See merge request soapbox-pub/soapbox-fe!1753
This commit is contained in:
Alex Gleason 2022-08-29 16:57:45 +00:00
commit 195f81329b
8 changed files with 78 additions and 2 deletions

View file

@ -137,6 +137,7 @@ const StatusList: React.FC<IStatusList> = ({
<Ad
card={ad.card}
impression={ad.impression}
expires={ad.expires}
/>
);
};

View file

@ -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,24 @@ 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,
}
/** Displays an ad in sponsored post format. */
const Ad: React.FC<IAd> = ({ card, impression }) => {
const Ad: React.FC<IAd> = ({ card, impression, expires }) => {
const queryClient = useQueryClient();
const instance = useAppSelector(state => state.instance);
const timer = useRef<NodeJS.Timeout | undefined>(undefined);
const infobox = useRef<HTMLDivElement>(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 +61,20 @@ const Ad: React.FC<IAd> = ({ 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 (
<div className='relative'>
<Card className='py-6 sm:p-5' variant='rounded'>

View file

@ -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. */

View file

@ -43,6 +43,7 @@ const RumbleAdProvider: AdProvider = {
image: item.asset,
url: item.click,
}),
expires: new Date(item.expires * 1000),
}));
}
}

View file

@ -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. */

View file

@ -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();
@ -17,7 +18,15 @@ export default function useAds() {
});
};
return useQuery<Ad[]>(['ads'], getAds, {
const result = useQuery<Ad[]>(['ads'], getAds, {
placeholderData: [],
});
// Filter out expired ads.
const data = result.data?.filter(isExpired);
return {
...result,
data,
};
}

View file

@ -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);
});

16
app/soapbox/utils/ads.ts Normal file
View file

@ -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 };