Show ads in feed
This commit is contained in:
parent
6d1539cf9c
commit
b02141874e
|
@ -6,13 +6,17 @@ import { FormattedMessage } from 'react-intl';
|
||||||
import LoadGap from 'soapbox/components/load_gap';
|
import LoadGap from 'soapbox/components/load_gap';
|
||||||
import ScrollableList from 'soapbox/components/scrollable_list';
|
import ScrollableList from 'soapbox/components/scrollable_list';
|
||||||
import StatusContainer from 'soapbox/containers/status_container';
|
import StatusContainer from 'soapbox/containers/status_container';
|
||||||
|
import Ad from 'soapbox/features/ads/components/ad';
|
||||||
import FeedSuggestions from 'soapbox/features/feed-suggestions/feed-suggestions';
|
import FeedSuggestions from 'soapbox/features/feed-suggestions/feed-suggestions';
|
||||||
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status';
|
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status';
|
||||||
import PendingStatus from 'soapbox/features/ui/components/pending_status';
|
import PendingStatus from 'soapbox/features/ui/components/pending_status';
|
||||||
|
import { useSoapboxConfig } from 'soapbox/hooks';
|
||||||
|
import useAds from 'soapbox/queries/ads';
|
||||||
|
|
||||||
import type { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
import type { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||||
import type { VirtuosoHandle } from 'react-virtuoso';
|
import type { VirtuosoHandle } from 'react-virtuoso';
|
||||||
import type { IScrollableList } from 'soapbox/components/scrollable_list';
|
import type { IScrollableList } from 'soapbox/components/scrollable_list';
|
||||||
|
import type { Ad as AdEntity } from 'soapbox/features/ads/providers';
|
||||||
|
|
||||||
interface IStatusList extends Omit<IScrollableList, 'onLoadMore' | 'children'> {
|
interface IStatusList extends Omit<IScrollableList, 'onLoadMore' | 'children'> {
|
||||||
/** Unique key to preserve the scroll position when navigating back. */
|
/** Unique key to preserve the scroll position when navigating back. */
|
||||||
|
@ -37,6 +41,8 @@ interface IStatusList extends Omit<IScrollableList, 'onLoadMore' | 'children'> {
|
||||||
timelineId?: string,
|
timelineId?: string,
|
||||||
/** Whether to display a gap or border between statuses in the list. */
|
/** Whether to display a gap or border between statuses in the list. */
|
||||||
divideType?: 'space' | 'border',
|
divideType?: 'space' | 'border',
|
||||||
|
/** Whether to display ads. */
|
||||||
|
showAds?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Feed of statuses, built atop ScrollableList. */
|
/** Feed of statuses, built atop ScrollableList. */
|
||||||
|
@ -49,8 +55,12 @@ const StatusList: React.FC<IStatusList> = ({
|
||||||
timelineId,
|
timelineId,
|
||||||
isLoading,
|
isLoading,
|
||||||
isPartial,
|
isPartial,
|
||||||
|
showAds = false,
|
||||||
...other
|
...other
|
||||||
}) => {
|
}) => {
|
||||||
|
const { data: ads } = useAds();
|
||||||
|
const soapboxConfig = useSoapboxConfig();
|
||||||
|
const adsInterval = Number(soapboxConfig.extensions.getIn(['ads', 'interval'], 40)) || 0;
|
||||||
const node = useRef<VirtuosoHandle>(null);
|
const node = useRef<VirtuosoHandle>(null);
|
||||||
|
|
||||||
const getFeaturedStatusCount = () => {
|
const getFeaturedStatusCount = () => {
|
||||||
|
@ -123,6 +133,15 @@ const StatusList: React.FC<IStatusList> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderAd = (ad: AdEntity) => {
|
||||||
|
return (
|
||||||
|
<Ad
|
||||||
|
card={ad.card}
|
||||||
|
impression={ad.impression}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderPendingStatus = (statusId: string) => {
|
const renderPendingStatus = (statusId: string) => {
|
||||||
const idempotencyKey = statusId.replace(/^末pending-/, '');
|
const idempotencyKey = statusId.replace(/^末pending-/, '');
|
||||||
|
|
||||||
|
@ -156,17 +175,27 @@ const StatusList: React.FC<IStatusList> = ({
|
||||||
|
|
||||||
const renderStatuses = (): React.ReactNode[] => {
|
const renderStatuses = (): React.ReactNode[] => {
|
||||||
if (isLoading || statusIds.size > 0) {
|
if (isLoading || statusIds.size > 0) {
|
||||||
return statusIds.toArray().map((statusId, index) => {
|
return statusIds.toList().reduce((acc, statusId, index) => {
|
||||||
|
const adIndex = ads ? Math.floor((index + 1) / adsInterval) % ads.length : 0;
|
||||||
|
const ad = ads ? ads[adIndex] : undefined;
|
||||||
|
const showAd = (index + 1) % adsInterval === 0;
|
||||||
|
|
||||||
if (statusId === null) {
|
if (statusId === null) {
|
||||||
return renderLoadGap(index);
|
acc.push(renderLoadGap(index));
|
||||||
} else if (statusId.startsWith('末suggestions-')) {
|
} else if (statusId.startsWith('末suggestions-')) {
|
||||||
return renderFeedSuggestions();
|
acc.push(renderFeedSuggestions());
|
||||||
} else if (statusId.startsWith('末pending-')) {
|
} else if (statusId.startsWith('末pending-')) {
|
||||||
return renderPendingStatus(statusId);
|
acc.push(renderPendingStatus(statusId));
|
||||||
} else {
|
} else {
|
||||||
return renderStatus(statusId);
|
acc.push(renderStatus(statusId));
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
if (showAds && ad && showAd) {
|
||||||
|
acc.push(renderAd(ad));
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, [] as React.ReactNode[]);
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
|
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||||
|
|
||||||
import type { RootState } from 'soapbox/store';
|
import type { RootState } from 'soapbox/store';
|
||||||
import type { Card } from 'soapbox/types/entities';
|
import type { Card } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
/** Map of available provider modules. */
|
||||||
|
const PROVIDERS: Record<string, () => Promise<AdProvider>> = {
|
||||||
|
soapbox: async() => (await import(/* webpackChunkName: "features/ads/soapbox" */'./soapbox-config')).default,
|
||||||
|
};
|
||||||
|
|
||||||
/** Ad server implementation. */
|
/** Ad server implementation. */
|
||||||
interface AdProvider {
|
interface AdProvider {
|
||||||
getAds(getState: () => RootState): Promise<Ad[]>,
|
getAds(getState: () => RootState): Promise<Ad[]>,
|
||||||
|
@ -14,4 +21,17 @@ interface Ad {
|
||||||
impression?: string,
|
impression?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gets the current provider based on config. */
|
||||||
|
const getProvider = async(getState: () => RootState): Promise<AdProvider | undefined> => {
|
||||||
|
const state = getState();
|
||||||
|
const soapboxConfig = getSoapboxConfig(state);
|
||||||
|
const isEnabled = soapboxConfig.extensions.getIn(['ads', 'enabled'], false) === true;
|
||||||
|
const providerName = soapboxConfig.extensions.getIn(['ads', 'provider'], 'soapbox') as string;
|
||||||
|
|
||||||
|
if (isEnabled && PROVIDERS[providerName]) {
|
||||||
|
return PROVIDERS[providerName]();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getProvider };
|
||||||
export type { Ad, AdProvider };
|
export type { Ad, AdProvider };
|
||||||
|
|
|
@ -90,6 +90,7 @@ const HomeTimeline: React.FC = () => {
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
timelineId='home'
|
timelineId='home'
|
||||||
divideType='space'
|
divideType='space'
|
||||||
|
showAds
|
||||||
emptyMessage={
|
emptyMessage={
|
||||||
<Stack space={1}>
|
<Stack space={1}>
|
||||||
<Text size='xl' weight='medium' align='center'>
|
<Text size='xl' weight='medium' align='center'>
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { Ad, getProvider } from 'soapbox/features/ads/providers';
|
||||||
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
export default function useAds() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const getAds = async() => {
|
||||||
|
return dispatch(async(_, getState) => {
|
||||||
|
const provider = await getProvider(getState);
|
||||||
|
if (provider) {
|
||||||
|
return provider.getAds(getState);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return useQuery<Ad[]>(['ads'], getAds, {
|
||||||
|
placeholderData: [],
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue