Support TruthSocial v2 ads

environments/review-truth-ads-t82o6p/deployments/1183
Alex Gleason 2022-10-20 16:29:14 -05:00
rodzic a70950f013
commit 4296772093
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 7211D1F99744FBB7
9 zmienionych plików z 90 dodań i 44 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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,

Wyświetl plik

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

Wyświetl plik

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