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
|
<Ad
|
||||||
card={ad.card}
|
card={ad.card}
|
||||||
impression={ad.impression}
|
impression={ad.impression}
|
||||||
|
expires={ad.expires}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
@ -13,15 +14,24 @@ interface IAd {
|
||||||
card: CardEntity,
|
card: CardEntity,
|
||||||
/** Impression URL to fetch upon display. */
|
/** Impression URL to fetch upon display. */
|
||||||
impression?: string,
|
impression?: string,
|
||||||
|
/** Time when the ad expires and should no longer be displayed. */
|
||||||
|
expires?: Date,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Displays an ad in sponsored post format. */
|
/** 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 instance = useAppSelector(state => state.instance);
|
||||||
|
|
||||||
|
const timer = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||||
const infobox = useRef<HTMLDivElement>(null);
|
const infobox = useRef<HTMLDivElement>(null);
|
||||||
const [showInfo, setShowInfo] = useState(false);
|
const [showInfo, setShowInfo] = useState(false);
|
||||||
|
|
||||||
|
/** Invalidate query cache for ads. */
|
||||||
|
const bustCache = (): void => {
|
||||||
|
queryClient.invalidateQueries(['ads']);
|
||||||
|
};
|
||||||
|
|
||||||
/** Toggle the info box on click. */
|
/** Toggle the info box on click. */
|
||||||
const handleInfoButtonClick: React.MouseEventHandler = () => {
|
const handleInfoButtonClick: React.MouseEventHandler = () => {
|
||||||
setShowInfo(!showInfo);
|
setShowInfo(!showInfo);
|
||||||
|
@ -51,6 +61,20 @@ const Ad: React.FC<IAd> = ({ card, impression }) => {
|
||||||
}
|
}
|
||||||
}, [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 (
|
return (
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<Card className='py-6 sm:p-5' variant='rounded'>
|
<Card className='py-6 sm:p-5' variant='rounded'>
|
||||||
|
|
|
@ -20,6 +20,8 @@ interface Ad {
|
||||||
card: Card,
|
card: Card,
|
||||||
/** Impression URL to fetch when displaying the ad. */
|
/** Impression URL to fetch when displaying the ad. */
|
||||||
impression?: string,
|
impression?: string,
|
||||||
|
/** Time when the ad expires and should no longer be displayed. */
|
||||||
|
expires?: Date,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gets the current provider based on config. */
|
/** Gets the current provider based on config. */
|
||||||
|
|
|
@ -43,6 +43,7 @@ const RumbleAdProvider: AdProvider = {
|
||||||
image: item.asset,
|
image: item.asset,
|
||||||
url: item.click,
|
url: item.click,
|
||||||
}),
|
}),
|
||||||
|
expires: new Date(item.expires * 1000),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { CardRecord, normalizeCard } from '../card';
|
||||||
export const AdRecord = ImmutableRecord({
|
export const AdRecord = ImmutableRecord({
|
||||||
card: CardRecord(),
|
card: CardRecord(),
|
||||||
impression: undefined as string | undefined,
|
impression: undefined as string | undefined,
|
||||||
|
expires: undefined as Date | undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Normalizes an ad from Soapbox Config. */
|
/** 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 { Ad, getProvider } from 'soapbox/features/ads/providers';
|
||||||
import { useAppDispatch } from 'soapbox/hooks';
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
|
import { isExpired } from 'soapbox/utils/ads';
|
||||||
|
|
||||||
export default function useAds() {
|
export default function useAds() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
@ -17,7 +18,15 @@ export default function useAds() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return useQuery<Ad[]>(['ads'], getAds, {
|
const result = useQuery<Ad[]>(['ads'], getAds, {
|
||||||
placeholderData: [],
|
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