Merge branch 'expiring-ads' into 'develop'
Expiring ads See merge request soapbox-pub/soapbox-fe!1753
This commit is contained in:
commit
195f81329b
8 changed files with 78 additions and 2 deletions
|
@ -137,6 +137,7 @@ const StatusList: React.FC<IStatusList> = ({
|
|||
<Ad
|
||||
card={ad.card}
|
||||
impression={ad.impression}
|
||||
expires={ad.expires}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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'>
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -43,6 +43,7 @@ const RumbleAdProvider: AdProvider = {
|
|||
image: item.asset,
|
||||
url: item.click,
|
||||
}),
|
||||
expires: new Date(item.expires * 1000),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
22
app/soapbox/utils/__tests__/ads.test.ts
Normal file
22
app/soapbox/utils/__tests__/ads.test.ts
Normal 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
16
app/soapbox/utils/ads.ts
Normal 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 };
|
Loading…
Reference in a new issue