Support TruthSocial v2 ads
This commit is contained in:
parent
a70950f013
commit
4296772093
9 changed files with 90 additions and 44 deletions
|
@ -19,7 +19,7 @@ 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/features/ads/providers';
|
||||
import type { Ad as AdEntity } from 'soapbox/types/soapbox';
|
||||
|
||||
interface IStatusList extends Omit<IScrollableList, 'onLoadMore' | 'children'> {
|
||||
/** Unique key to preserve the scroll position when navigating back. */
|
||||
|
@ -141,12 +141,7 @@ const StatusList: React.FC<IStatusList> = ({
|
|||
|
||||
const renderAd = (ad: AdEntity, index: number) => {
|
||||
return (
|
||||
<Ad
|
||||
key={`ad-${index}`}
|
||||
card={ad.card}
|
||||
impression={ad.impression}
|
||||
expires={ad.expires}
|
||||
/>
|
||||
<Ad key={`ad-${index}`} ad={ad} />
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -7,19 +7,14 @@ import IconButton from 'soapbox/components/ui/icon-button/icon-button';
|
|||
import StatusCard from 'soapbox/features/status/components/card';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import type { Card as CardEntity } from 'soapbox/types/entities';
|
||||
import type { Ad as AdEntity } from 'soapbox/types/soapbox';
|
||||
|
||||
interface IAd {
|
||||
/** Embedded ad data in Card format (almost like OEmbed). */
|
||||
card: CardEntity,
|
||||
/** Impression URL to fetch upon display. */
|
||||
impression?: string,
|
||||
/** Time when the ad expires and should no longer be displayed. */
|
||||
expires?: Date,
|
||||
ad: AdEntity,
|
||||
}
|
||||
|
||||
/** Displays an ad in sponsored post format. */
|
||||
const Ad: React.FC<IAd> = ({ card, impression, expires }) => {
|
||||
const Ad: React.FC<IAd> = ({ ad }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const instance = useAppSelector(state => state.instance);
|
||||
|
||||
|
@ -29,9 +24,9 @@ const Ad: React.FC<IAd> = ({ card, impression, expires }) => {
|
|||
|
||||
// Fetch the impression URL (if any) upon displaying the ad.
|
||||
// Don't fetch it more than once.
|
||||
useQuery(['ads', 'impression', impression], () => {
|
||||
if (impression) {
|
||||
return fetch(impression);
|
||||
useQuery(['ads', 'impression', ad.impression], () => {
|
||||
if (ad.impression) {
|
||||
return fetch(ad.impression);
|
||||
}
|
||||
}, { cacheTime: Infinity, staleTime: Infinity });
|
||||
|
||||
|
@ -63,8 +58,8 @@ const Ad: React.FC<IAd> = ({ card, impression, expires }) => {
|
|||
|
||||
// Wait until the ad expires, then invalidate cache.
|
||||
useEffect(() => {
|
||||
if (expires) {
|
||||
const delta = expires.getTime() - (new Date()).getTime();
|
||||
if (ad.expires_at) {
|
||||
const delta = new Date(ad.expires_at).getTime() - (new Date()).getTime();
|
||||
timer.current = setTimeout(bustCache, delta);
|
||||
}
|
||||
|
||||
|
@ -73,7 +68,7 @@ const Ad: React.FC<IAd> = ({ card, impression, expires }) => {
|
|||
clearTimeout(timer.current);
|
||||
}
|
||||
};
|
||||
}, [expires]);
|
||||
}, [ad.expires_at]);
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
|
@ -112,7 +107,7 @@ const Ad: React.FC<IAd> = ({ card, impression, expires }) => {
|
|||
</Stack>
|
||||
</HStack>
|
||||
|
||||
<StatusCard card={card} onOpenMedia={() => {}} horizontal />
|
||||
<StatusCard card={ad.card} onOpenMedia={() => {}} horizontal />
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
|
@ -125,11 +120,15 @@ const Ad: React.FC<IAd> = ({ card, impression, expires }) => {
|
|||
</Text>
|
||||
|
||||
<Text size='sm' theme='muted'>
|
||||
<FormattedMessage
|
||||
id='sponsored.info.message'
|
||||
defaultMessage='{siteTitle} displays ads to help fund our service.'
|
||||
values={{ siteTitle: instance.title }}
|
||||
/>
|
||||
{ad.reason ? (
|
||||
ad.reason
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='sponsored.info.message'
|
||||
defaultMessage='{siteTitle} displays ads to help fund our service.'
|
||||
values={{ siteTitle: instance.title }}
|
||||
/>
|
||||
)}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
|
|
@ -7,6 +7,7 @@ import type { Card } from 'soapbox/types/entities';
|
|||
const PROVIDERS: Record<string, () => Promise<AdProvider>> = {
|
||||
soapbox: async() => (await import(/* webpackChunkName: "features/ads/soapbox" */'./soapbox-config')).default,
|
||||
rumble: async() => (await import(/* webpackChunkName: "features/ads/rumble" */'./rumble')).default,
|
||||
truth: async() => (await import(/* webpackChunkName: "features/ads/truth" */'./truth')).default,
|
||||
};
|
||||
|
||||
/** Ad server implementation. */
|
||||
|
@ -21,7 +22,9 @@ interface Ad {
|
|||
/** Impression URL to fetch when displaying the ad. */
|
||||
impression?: string,
|
||||
/** Time when the ad expires and should no longer be displayed. */
|
||||
expires?: Date,
|
||||
expires_at?: string,
|
||||
/** Reason the ad is displayed. */
|
||||
reason?: string,
|
||||
}
|
||||
|
||||
/** Gets the current provider based on config. */
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import { normalizeCard } from 'soapbox/normalizers';
|
||||
import { normalizeAd, normalizeCard } from 'soapbox/normalizers';
|
||||
|
||||
import type { AdProvider } from '.';
|
||||
|
||||
|
@ -36,14 +36,14 @@ const RumbleAdProvider: AdProvider = {
|
|||
|
||||
if (response.ok) {
|
||||
const data = await response.json() as RumbleApiResponse;
|
||||
return data.ads.map(item => ({
|
||||
return data.ads.map(item => normalizeAd({
|
||||
impression: item.impression,
|
||||
card: normalizeCard({
|
||||
type: item.type === 1 ? 'link' : 'rich',
|
||||
image: item.asset,
|
||||
url: item.click,
|
||||
}),
|
||||
expires: new Date(item.expires * 1000),
|
||||
expires_at: new Date(item.expires * 1000),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
39
app/soapbox/features/ads/providers/truth.ts
Normal file
39
app/soapbox/features/ads/providers/truth.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { normalizeCard } from 'soapbox/normalizers';
|
||||
|
||||
import type { AdProvider } from '.';
|
||||
import type { Card } from 'soapbox/types/entities';
|
||||
|
||||
/** TruthSocial ad API entity. */
|
||||
interface TruthAd {
|
||||
impression: string,
|
||||
card: Card,
|
||||
expires_at: string,
|
||||
reason: string,
|
||||
}
|
||||
|
||||
/** Provides ads from the TruthSocial API. */
|
||||
const TruthAdProvider: AdProvider = {
|
||||
getAds: async(getState) => {
|
||||
const state = getState();
|
||||
const settings = getSettings(state);
|
||||
|
||||
const response = await fetch('/api/v2/truth/ads?device=desktop', {
|
||||
headers: {
|
||||
'Accept-Language': settings.get('locale', '*') as string,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json() as TruthAd[];
|
||||
return data.map(item => ({
|
||||
...item,
|
||||
card: normalizeCard(item.card),
|
||||
}));
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
export default TruthAdProvider;
|
|
@ -6,15 +6,23 @@ import {
|
|||
|
||||
import { CardRecord, normalizeCard } from '../card';
|
||||
|
||||
export const AdRecord = ImmutableRecord({
|
||||
import type { Ad } from 'soapbox/features/ads/providers';
|
||||
|
||||
export const AdRecord = ImmutableRecord<Ad>({
|
||||
card: CardRecord(),
|
||||
impression: undefined as string | undefined,
|
||||
expires: undefined as Date | undefined,
|
||||
expires_at: undefined as string | undefined,
|
||||
reason: undefined as string | undefined,
|
||||
});
|
||||
|
||||
/** Normalizes an ad from Soapbox Config. */
|
||||
export const normalizeAd = (ad: Record<string, any>) => {
|
||||
const map = ImmutableMap<string, any>(fromJS(ad));
|
||||
const card = normalizeCard(map.get('card'));
|
||||
return AdRecord(map.set('card', card));
|
||||
const expiresAt = map.get('expires_at') || map.get('expires');
|
||||
|
||||
return AdRecord(map.merge({
|
||||
card,
|
||||
expires_at: expiresAt,
|
||||
}));
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query';
|
|||
|
||||
import { Ad, getProvider } from 'soapbox/features/ads/providers';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import { normalizeAd } from 'soapbox/normalizers';
|
||||
import { isExpired } from 'soapbox/utils/ads';
|
||||
|
||||
export default function useAds() {
|
||||
|
@ -23,7 +24,7 @@ export default function useAds() {
|
|||
});
|
||||
|
||||
// Filter out expired ads.
|
||||
const data = result.data?.filter(ad => !isExpired(ad));
|
||||
const data = result.data?.map(normalizeAd).filter(ad => !isExpired(ad));
|
||||
|
||||
return {
|
||||
...result,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { normalizeCard } from 'soapbox/normalizers';
|
||||
import { normalizeAd } from 'soapbox/normalizers';
|
||||
|
||||
import { isExpired } from '../ads';
|
||||
|
||||
|
@ -10,13 +10,14 @@ const fiveMins = 5 * 60 * 1000;
|
|||
|
||||
test('isExpired()', () => {
|
||||
const now = new Date();
|
||||
const card = normalizeCard({});
|
||||
const iso = now.toISOString();
|
||||
const epoch = now.getTime();
|
||||
|
||||
// Sanity tests.
|
||||
expect(isExpired({ expires: now, card })).toBe(true);
|
||||
expect(isExpired({ expires: new Date(now.getTime() + 999999), card })).toBe(false);
|
||||
expect(isExpired(normalizeAd({ expires_at: iso }))).toBe(true);
|
||||
expect(isExpired(normalizeAd({ expires_at: new Date(epoch + 999999).toISOString() }))).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);
|
||||
expect(isExpired(normalizeAd({ expires_at: new Date(epoch + threeMins).toISOString() }), fiveMins)).toBe(true);
|
||||
expect(isExpired(normalizeAd({ expires_at: new Date(epoch + fiveMins + 1000).toISOString() }), fiveMins)).toBe(false);
|
||||
});
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import type { Ad } from 'soapbox/features/ads/providers';
|
||||
import type { Ad } from 'soapbox/types/soapbox';
|
||||
|
||||
/** 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) {
|
||||
if (ad.expires_at) {
|
||||
const now = new Date();
|
||||
return now.getTime() > (ad.expires.getTime() - threshold);
|
||||
return now.getTime() > (new Date(ad.expires_at).getTime() - threshold);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue