From f7cb1c71f0eb866a16e7e1a01a62aba183f2d619 Mon Sep 17 00:00:00 2001 From: Clovis Date: Mon, 11 Nov 2024 21:02:16 +0100 Subject: [PATCH] refactor: remove Ads and try to optimize status render (#343) --- app/soapbox/components/status-hover-card.tsx | 21 +- app/soapbox/components/status-media.tsx | 41 +-- app/soapbox/components/status.tsx | 114 ++++---- app/soapbox/components/status_list.tsx | 253 ++++++++---------- app/soapbox/features/ads/components/ad.tsx | 118 -------- app/soapbox/features/ads/providers/index.ts | 38 --- app/soapbox/features/ads/providers/rumble.ts | 54 ---- .../features/ads/providers/soapbox-config.ts | 14 - .../ui/components/{bundle.js => bundle.tsx} | 25 +- app/soapbox/normalizers/index.ts | 1 - app/soapbox/normalizers/soapbox/ad.ts | 19 -- .../normalizers/soapbox/soapbox_config.ts | 10 - app/soapbox/types/soapbox.ts | 3 - package.json | 2 +- yarn.lock | 8 +- 15 files changed, 220 insertions(+), 501 deletions(-) delete mode 100644 app/soapbox/features/ads/components/ad.tsx delete mode 100644 app/soapbox/features/ads/providers/index.ts delete mode 100644 app/soapbox/features/ads/providers/rumble.ts delete mode 100644 app/soapbox/features/ads/providers/soapbox-config.ts rename app/soapbox/features/ui/components/{bundle.js => bundle.tsx} (80%) delete mode 100644 app/soapbox/normalizers/soapbox/ad.ts diff --git a/app/soapbox/components/status-hover-card.tsx b/app/soapbox/components/status-hover-card.tsx index dc02af3f4..8f9feefc6 100644 --- a/app/soapbox/components/status-hover-card.tsx +++ b/app/soapbox/components/status-hover-card.tsx @@ -64,19 +64,6 @@ export const StatusHoverCard: React.FC = ({ visible = true }) if (!statusId) return null; - const renderStatus = (statusId: string) => { - return ( - // @ts-ignore - - ); - }; - return (
= ({ visible = true }) > - {renderStatus(statusId)} +
diff --git a/app/soapbox/components/status-media.tsx b/app/soapbox/components/status-media.tsx index 2721d726b..421858f2f 100644 --- a/app/soapbox/components/status-media.tsx +++ b/app/soapbox/components/status-media.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { openModal } from 'soapbox/actions/modals'; import AttachmentThumbs from 'soapbox/components/attachment-thumbs'; @@ -24,6 +24,18 @@ interface IStatusMedia { onToggleVisibility?: () => void, } +const renderLoadingMediaGallery = (): JSX.Element => { + return
; +}; + +const renderLoadingVideoPlayer = (): JSX.Element => { + return
; +}; + +const renderLoadingAudioPlayer = (): JSX.Element => { + return
; +}; + /** Render media attachments for a status. */ const StatusMedia: React.FC = ({ status, @@ -35,36 +47,25 @@ const StatusMedia: React.FC = ({ const dispatch = useAppDispatch(); const [mediaWrapperWidth, setMediaWrapperWidth] = useState(undefined); - const size = status.media_attachments.size; - const firstAttachment = status.media_attachments.first(); + const size = useMemo(() => status.media_attachments.size, [status.media_attachments]); + const firstAttachment = useMemo(() => status.media_attachments.first(), [status.media_attachments]); let media = null; - const setRef = (c: HTMLDivElement): void => { + const setRef = useCallback((c: HTMLDivElement): void => { if (c) { setMediaWrapperWidth(c.offsetWidth); } - }; - - const renderLoadingMediaGallery = (): JSX.Element => { - return
; - }; - - const renderLoadingVideoPlayer = (): JSX.Element => { - return
; - }; + }, []); - const renderLoadingAudioPlayer = (): JSX.Element => { - return
; - }; - const openMedia = (media: ImmutableList, index: number) => { + const openMedia = useCallback((media: ImmutableList, index: number) => { dispatch(openModal('MEDIA', { media, index })); - }; + }, [dispatch]); - const openVideo = (media: Attachment, time: number): void => { + const openVideo = useCallback((media: Attachment, time: number): void => { dispatch(openModal('VIDEO', { media, time })); - }; + }, [dispatch]); if (size > 0 && firstAttachment) { if (muted) { diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 9aff65404..90f688d81 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -1,5 +1,6 @@ +/* eslint-disable jsx-a11y/interactive-supports-focus */ import classNames from 'classnames'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { HotKeys } from 'react-hotkeys'; import { useIntl, FormattedMessage, defineMessages } from 'react-intl'; import { NavLink, useHistory } from 'react-router-dom'; @@ -50,6 +51,7 @@ export interface IStatus { withDismiss?: boolean, } + const Status: React.FC = (props) => { const { status, @@ -77,22 +79,22 @@ const Status: React.FC = (props) => { const [showMedia, setShowMedia] = useState(defaultMediaVisibility(status, displayMedia)); - const actualStatus = getActualStatus(status); + const actualStatus = useMemo(() => getActualStatus(status), [status]) ; const logo = useLogo(); // Track height changes we know about to compensate scrolling. useEffect(() => { didShowCard.current = Boolean(!muted && !hidden && status?.card); - }, []); + }, [muted, hidden, status]); useEffect(() => { setShowMedia(defaultMediaVisibility(status, displayMedia)); - }, [status.id]); + }, [displayMedia, status, status.id]); - const handleToggleMediaVisibility = (): void => { + const handleToggleMediaVisibility = useCallback((): void => { setShowMedia(!showMedia); - }; + }, [showMedia]); const handleClick = (): void => { if (onClick) { @@ -106,7 +108,7 @@ const Status: React.FC = (props) => { dispatch(toggleStatusHidden(actualStatus)); }; - const handleHotkeyOpenMedia = (e?: KeyboardEvent): void => { + const handleHotkeyOpenMedia = useCallback((e?: KeyboardEvent): void => { const status = actualStatus; const firstAttachment = status.media_attachments.first(); @@ -119,18 +121,18 @@ const Status: React.FC = (props) => { dispatch(openModal('MEDIA', { media: status.media_attachments, index: 0 })); } } - }; + }, [actualStatus, dispatch]); - const handleHotkeyReply = (e?: KeyboardEvent): void => { + const handleHotkeyReply = useCallback((e?: KeyboardEvent): void => { e?.preventDefault(); dispatch(replyComposeWithConfirmation(actualStatus, intl)); - }; + }, [actualStatus, dispatch, intl]); - const handleHotkeyFavourite = (): void => { + const handleHotkeyFavourite = useCallback((): void => { toggleFavourite(actualStatus); - }; + }, [actualStatus]); - const handleHotkeyBoost = (e?: KeyboardEvent): void => { + const handleHotkeyBoost = useCallback((e?: KeyboardEvent): void => { const modalReblog = () => dispatch(toggleReblog(actualStatus)); const boostModal = settings.get('boostModal'); if ((e && e.shiftKey) || !boostModal) { @@ -138,44 +140,44 @@ const Status: React.FC = (props) => { } else { dispatch(openModal('BOOST', { status: actualStatus, onReblog: modalReblog })); } - }; + }, [actualStatus, dispatch, settings]); - const handleHotkeyMention = (e?: KeyboardEvent): void => { + const handleHotkeyMention = useCallback((e?: KeyboardEvent): void => { e?.preventDefault(); dispatch(mentionCompose(actualStatus.account as AccountEntity)); - }; + }, [actualStatus.account, dispatch]); - const handleHotkeyOpen = (): void => { + const handleHotkeyOpen = useCallback((): void => { history.push(`/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`); - }; + }, [actualStatus, history]); - const handleHotkeyOpenProfile = (): void => { + const handleHotkeyOpenProfile = useCallback((): void => { history.push(`/@${actualStatus.getIn(['account', 'acct'])}`); - }; + }, [actualStatus, history]); - const handleHotkeyMoveUp = (e?: KeyboardEvent): void => { + const handleHotkeyMoveUp = useCallback((e?: KeyboardEvent): void => { if (onMoveUp) { onMoveUp(status.id, featured); } - }; + }, [featured, onMoveUp, status.id]); - const handleHotkeyMoveDown = (e?: KeyboardEvent): void => { + const handleHotkeyMoveDown = useCallback((e?: KeyboardEvent): void => { if (onMoveDown) { onMoveDown(status.id, featured); } - }; + }, [featured, onMoveDown, status.id]); - const handleHotkeyToggleHidden = (): void => { + const handleHotkeyToggleHidden = useCallback((): void => { dispatch(toggleStatusHidden(actualStatus)); - }; + }, [actualStatus, dispatch]); - const handleHotkeyToggleSensitive = (): void => { + const handleHotkeyToggleSensitive = useCallback((): void => { handleToggleMediaVisibility(); - }; + }, [handleToggleMediaVisibility]); - const handleHotkeyReact = (): void => { + const handleHotkeyReact = useCallback((): void => { _expandEmojiSelector(); - }; + }, []); const _expandEmojiSelector = (): void => { const firstEmoji: HTMLDivElement | null | undefined = node.current?.querySelector('.emoji-react-selector .emoji-react-selector__emoji'); @@ -191,34 +193,24 @@ const Status: React.FC = (props) => { case 'private': return require('@tabler/icons/lock.svg'); case 'direct': return require('@tabler/icons/mail.svg'); } - }, [actualStatus?.visibility]); - - if (!status) return null; - - if (hidden) { - return ( -
- {actualStatus.getIn(['account', 'display_name']) || actualStatus.getIn(['account', 'username'])} - {actualStatus.content} -
- ); - } + }, [actualStatus?.visibility, logo]); - let quote; - if (actualStatus.quote) { - if (actualStatus.pleroma.get('quote_visible', true) === false) { - quote = ( -
-

-
- ); - } else { - quote = ; + const quote = useMemo(() => { + if (actualStatus.quote) { + if (actualStatus.pleroma.get('quote_visible', true) === false) { + return ( +
+

+
+ ); + } else { + return ; + } } - } + }, [actualStatus.pleroma, actualStatus.quote]); - const handlers = muted ? undefined : { + const handlers = useMemo(() => muted ? undefined : { reply: handleHotkeyReply, favourite: handleHotkeyFavourite, boost: handleHotkeyBoost, @@ -231,12 +223,24 @@ const Status: React.FC = (props) => { toggleSensitive: handleHotkeyToggleSensitive, openMedia: handleHotkeyOpenMedia, react: handleHotkeyReact, - }; + }, [handleHotkeyBoost, handleHotkeyFavourite, handleHotkeyMention, handleHotkeyMoveDown, handleHotkeyMoveUp, handleHotkeyOpen, handleHotkeyOpenMedia, handleHotkeyOpenProfile, handleHotkeyReact, handleHotkeyReply, handleHotkeyToggleHidden, handleHotkeyToggleSensitive, muted]); const statusUrl = `/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`; + if (!status) return null; + + if (hidden) { + return ( +
+ {actualStatus.getIn(['account', 'display_name']) || actualStatus.getIn(['account', 'username'])} + {actualStatus.content} +
+ ); + } + + return (
{ /** Unique key to preserve the scroll position when navigating back. */ @@ -45,6 +42,98 @@ interface IStatusList extends Omit { showAds?: boolean, } +const renderPendingStatus = (statusId: string) => { + const idempotencyKey = statusId.replace(/^末pending-/, ''); + + return ( + + ); +}; + +const renderFeedSuggestions = (suggestedProfiles, areSuggestedProfilesLoaded): React.ReactNode => { + if (!areSuggestedProfilesLoaded && suggestedProfiles.size === 0) return null; + + return ; +}; + +const renderLoadGap = (index: number, statusIds: ImmutableOrderedSet, onLoadMore, isLoading) => { + const ids = statusIds.toList(); + const nextId = ids.get(index + 1); + const prevId = ids.get(index - 1); + + if (index < 1 || !nextId || !prevId || !onLoadMore) return null; + + return ( + + ); +}; + +const renderFeaturedStatuses = (featuredStatusIds, handleMoveUp, handleMoveDown, timelineId): React.ReactNode[] => { + if (!featuredStatusIds) return []; + + return featuredStatusIds.toArray().map(statusId => ( + + )); +}; + +const renderStatuses = (isLoading, statusIds, onLoadMore, handleMoveUp, handleMoveDown, timelineId, suggestedProfiles, areSuggestedProfilesLoaded): React.ReactNode[] => { + if (isLoading || statusIds.size > 0) { + return statusIds.toList().reduce((acc, statusId, index) => { + + if (statusId === null) { + acc.push(renderLoadGap(index, statusIds, onLoadMore, isLoading)); + } else if (statusId.startsWith('末suggestions-')) { + const suggestions = renderFeedSuggestions(suggestedProfiles, areSuggestedProfilesLoaded); + if (suggestions) acc.push(suggestions); + } else if (statusId.startsWith('末pending-')) { + acc.push(renderPendingStatus(statusId)); + } else { + acc.push(); + } + + return acc; + }, [] as React.ReactNode[]); + } else { + return []; + } +}; + + + +const renderScrollableContent = (featuredStatusIds, isLoading, statusIds, onLoadMore, handleMoveUp, handleMoveDown, timelineId, suggestedProfiles, areSuggestedProfilesLoaded) => { + const featuredStatuses = renderFeaturedStatuses(featuredStatusIds, handleMoveUp, handleMoveDown, timelineId); + const statuses = renderStatuses(isLoading, statusIds, onLoadMore, handleMoveUp, handleMoveDown, timelineId, suggestedProfiles, areSuggestedProfilesLoaded); + + if (featuredStatuses && statuses) { + return featuredStatuses.concat(statuses); + } else { + return statuses; + } +}; + + + /** Feed of statuses, built atop ScrollableList. */ const StatusList: React.FC = ({ statusIds, @@ -55,47 +144,27 @@ const StatusList: React.FC = ({ timelineId, isLoading, isPartial, - showAds = false, ...other }) => { - const { data: ads } = useAds(); - const soapboxConfig = useSoapboxConfig(); - const adsInterval = Number(soapboxConfig.extensions.getIn(['ads', 'interval'], 40)) || 0; const node = useRef(null); const suggestedProfiles = useAppSelector((state) => state.suggestions.items); const areSuggestedProfilesLoaded = useAppSelector((state) => state.suggestions.isLoading); - const getFeaturedStatusCount = () => { + + const getFeaturedStatusCount = useCallback(() => { return featuredStatusIds?.size || 0; - }; + }, [featuredStatusIds?.size]); - const getCurrentStatusIndex = (id: string, featured: boolean): number => { + const getCurrentStatusIndex = useCallback((id: string, featured: boolean): number => { if (featured) { return featuredStatusIds?.keySeq().findIndex(key => key === id) || 0; } else { return statusIds.keySeq().findIndex(key => key === id) + getFeaturedStatusCount(); } - }; - - const handleMoveUp = (id: string, featured: boolean = false) => { - const elementIndex = getCurrentStatusIndex(id, featured) - 1; - selectChild(elementIndex); - }; + }, [featuredStatusIds, getFeaturedStatusCount, statusIds]); - const handleMoveDown = (id: string, featured: boolean = false) => { - const elementIndex = getCurrentStatusIndex(id, featured) + 1; - selectChild(elementIndex); - }; - - const handleLoadOlder = useCallback(debounce(() => { - const maxId = lastStatusId || statusIds.last(); - if (onLoadMore && maxId) { - onLoadMore(maxId.replace('末suggestions-', '')); - } - }, 300, { leading: true }), [onLoadMore, lastStatusId, statusIds.last()]); - - const selectChild = (index: number) => { + const selectChild = useCallback((index: number) => { node.current?.scrollIntoView({ index, behavior: 'smooth', @@ -104,116 +173,28 @@ const StatusList: React.FC = ({ element?.focus(); }, }); - }; - - const renderLoadGap = (index: number) => { - const ids = statusIds.toList(); - const nextId = ids.get(index + 1); - const prevId = ids.get(index - 1); - - if (index < 1 || !nextId || !prevId || !onLoadMore) return null; - - return ( - - ); - }; - - const renderStatus = (statusId: string) => { - return ( - - ); - }; + }, []); - const renderAd = (ad: AdEntity) => { - return ( - - ); - }; + const handleMoveUp = useCallback((id: string, featured: boolean = false) => { + const elementIndex = getCurrentStatusIndex(id, featured) - 1; + selectChild(elementIndex); + }, [getCurrentStatusIndex, selectChild]); - const renderPendingStatus = (statusId: string) => { - const idempotencyKey = statusId.replace(/^末pending-/, ''); + const handleMoveDown = useCallback((id: string, featured: boolean = false) => { + const elementIndex = getCurrentStatusIndex(id, featured) + 1; + selectChild(elementIndex); + }, [getCurrentStatusIndex, selectChild]); - return ( - - ); - }; - - const renderFeaturedStatuses = (): React.ReactNode[] => { - if (!featuredStatusIds) return []; - - return featuredStatusIds.toArray().map(statusId => ( - - )); - }; - - const renderFeedSuggestions = useCallback((): React.ReactNode => { - if (!areSuggestedProfilesLoaded && suggestedProfiles.size === 0) return null; - return ; - }, [areSuggestedProfilesLoaded, suggestedProfiles.size]); - - const renderStatuses = (): React.ReactNode[] => { - if (isLoading || statusIds.size > 0) { - 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) { - acc.push(renderLoadGap(index)); - } else if (statusId.startsWith('末suggestions-')) { - const suggestions = renderFeedSuggestions(); - if (suggestions) acc.push(suggestions); - } else if (statusId.startsWith('末pending-')) { - acc.push(renderPendingStatus(statusId)); - } else { - acc.push(renderStatus(statusId)); - } - - if (showAds && ad && showAd) { - acc.push(renderAd(ad)); - } - - return acc; - }, [] as React.ReactNode[]); - } else { - return []; + // eslint-disable-next-line react-hooks/exhaustive-deps + const handleLoadOlder = useCallback(debounce(() => { + const maxId = lastStatusId || statusIds.last(); + if (onLoadMore && maxId) { + onLoadMore(maxId.replace('末suggestions-', '')); } - }; + }, 300, { leading: true }), [onLoadMore, lastStatusId, statusIds.last()]); - const renderScrollableContent = () => { - const featuredStatuses = renderFeaturedStatuses(); - const statuses = renderStatuses(); - if (featuredStatuses && statuses) { - return featuredStatuses.concat(statuses); - } else { - return statuses; - } - }; + const scrollableContent = useMemo(() => renderScrollableContent(featuredStatusIds, isLoading, statusIds, onLoadMore, handleMoveUp, handleMoveDown, timelineId, suggestedProfiles, areSuggestedProfilesLoaded), [areSuggestedProfilesLoaded, featuredStatusIds, handleMoveDown, handleMoveUp, isLoading, onLoadMore, statusIds, suggestedProfiles, timelineId]); if (isPartial) { return ( @@ -243,7 +224,7 @@ const StatusList: React.FC = ({ })} {...other} > - {renderScrollableContent()} + {scrollableContent} ); }; diff --git a/app/soapbox/features/ads/components/ad.tsx b/app/soapbox/features/ads/components/ad.tsx deleted file mode 100644 index 1556d56e5..000000000 --- a/app/soapbox/features/ads/components/ad.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { FormattedMessage } from 'react-intl'; - -import { Stack, HStack, Card, Avatar, Text, Icon } from 'soapbox/components/ui'; -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'; - -interface IAd { - /** Embedded ad data in Card format (almost like OEmbed). */ - card: CardEntity, - /** Impression URL to fetch upon display. */ - impression?: string, -} - -/** Displays an ad in sponsored post format. */ -const Ad: React.FC = ({ card, impression }) => { - const instance = useAppSelector(state => state.instance); - - const infobox = useRef(null); - const [showInfo, setShowInfo] = useState(false); - - /** Toggle the info box on click. */ - const handleInfoButtonClick: React.MouseEventHandler = () => { - setShowInfo(!showInfo); - }; - - /** Hide the info box when clicked outside. */ - const handleClickOutside = (event: MouseEvent) => { - if (event.target && infobox.current && !infobox.current.contains(event.target as any)) { - setShowInfo(false); - } - }; - - // Hide the info box when clicked outside. - // https://stackoverflow.com/a/42234988 - useEffect(() => { - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [infobox]); - - // Fetch the impression URL (if any) upon displaying the ad. - // It's common for ad providers to provide this. - useEffect(() => { - if (impression) { - fetch(impression); - } - }, [impression]); - - return ( -
- - - - - - - - - {instance.title} - - - - - - - - - - - - - - - - - - - - {}} horizontal /> - - - - {showInfo && ( -
- - - - - - - - - - - -
- )} -
- ); -}; - -export default Ad; diff --git a/app/soapbox/features/ads/providers/index.ts b/app/soapbox/features/ads/providers/index.ts deleted file mode 100644 index 65e593985..000000000 --- a/app/soapbox/features/ads/providers/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { getSoapboxConfig } from 'soapbox/actions/soapbox'; - -import type { RootState } from 'soapbox/store'; -import type { Card } from 'soapbox/types/entities'; - -/** Map of available provider modules. */ -const PROVIDERS: Record Promise> = { - soapbox: async() => (await import(/* webpackChunkName: "features/ads/soapbox" */'./soapbox-config')).default, - rumble: async() => (await import(/* webpackChunkName: "features/ads/rumble" */'./rumble')).default, -}; - -/** Ad server implementation. */ -interface AdProvider { - getAds(getState: () => RootState): Promise, -} - -/** Entity representing an advertisement. */ -interface Ad { - /** Ad data in Card (OEmbed-ish) format. */ - card: Card, - /** Impression URL to fetch when displaying the ad. */ - impression?: string, -} - -/** Gets the current provider based on config. */ -const getProvider = async(getState: () => RootState): Promise => { - 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 }; diff --git a/app/soapbox/features/ads/providers/rumble.ts b/app/soapbox/features/ads/providers/rumble.ts deleted file mode 100644 index 249fc8547..000000000 --- a/app/soapbox/features/ads/providers/rumble.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { getSettings } from 'soapbox/actions/settings'; -import { getSoapboxConfig } from 'soapbox/actions/soapbox'; -import { normalizeCard } from 'soapbox/normalizers'; - -import type { AdProvider } from '.'; - -/** Rumble ad API entity. */ -interface RumbleAd { - type: number, - impression: string, - click: string, - asset: string, - expires: number, -} - -/** Response from Rumble ad server. */ -interface RumbleApiResponse { - count: number, - ads: RumbleAd[], -} - -/** Provides ads from Soapbox Config. */ -const RumbleAdProvider: AdProvider = { - getAds: async(getState) => { - const state = getState(); - const settings = getSettings(state); - const soapboxConfig = getSoapboxConfig(state); - const endpoint = soapboxConfig.extensions.getIn(['ads', 'endpoint']) as string | undefined; - - if (endpoint) { - const response = await fetch(endpoint, { - headers: { - 'Accept-Language': settings.get('locale', '*') as string, - }, - }); - - if (response.ok) { - const data = await response.json() as RumbleApiResponse; - return data.ads.map(item => ({ - impression: item.impression, - card: normalizeCard({ - type: item.type === 1 ? 'link' : 'rich', - image: item.asset, - url: item.click, - }), - })); - } - } - - return []; - }, -}; - -export default RumbleAdProvider; diff --git a/app/soapbox/features/ads/providers/soapbox-config.ts b/app/soapbox/features/ads/providers/soapbox-config.ts deleted file mode 100644 index 21163729c..000000000 --- a/app/soapbox/features/ads/providers/soapbox-config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { getSoapboxConfig } from 'soapbox/actions/soapbox'; - -import type { AdProvider } from '.'; - -/** Provides ads from Soapbox Config. */ -const SoapboxConfigAdProvider: AdProvider = { - getAds: async(getState) => { - const state = getState(); - const soapboxConfig = getSoapboxConfig(state); - return soapboxConfig.ads.toArray(); - }, -}; - -export default SoapboxConfigAdProvider; diff --git a/app/soapbox/features/ui/components/bundle.js b/app/soapbox/features/ui/components/bundle.tsx similarity index 80% rename from app/soapbox/features/ui/components/bundle.js rename to app/soapbox/features/ui/components/bundle.tsx index 11622ec19..cffc05246 100644 --- a/app/soapbox/features/ui/components/bundle.js +++ b/app/soapbox/features/ui/components/bundle.tsx @@ -1,21 +1,9 @@ -import PropTypes from 'prop-types'; import React from 'react'; const emptyComponent = () => null; const noop = () => { }; -class Bundle extends React.PureComponent { - - static propTypes = { - fetchComponent: PropTypes.func.isRequired, - loading: PropTypes.func, - error: PropTypes.func, - children: PropTypes.func.isRequired, - renderDelay: PropTypes.number, - onFetch: PropTypes.func, - onFetchSuccess: PropTypes.func, - onFetchFail: PropTypes.func, - } +class Bundle extends React.PureComponent<{ fetchComponent: any, loading: any, error: any, renderDelay: any, children: any }> { static defaultProps = { loading: emptyComponent, @@ -28,6 +16,10 @@ class Bundle extends React.PureComponent { static cache = new Map + unmounted: boolean = false; + timeout; + timestamp: Date; + state = { mod: undefined, forceRender: false, @@ -44,6 +36,7 @@ class Bundle extends React.PureComponent { } componentWillUnmount() { + this.unmounted = true; if (this.timeout) { clearTimeout(this.timeout); } @@ -53,6 +46,8 @@ class Bundle extends React.PureComponent { const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props; const cachedMod = Bundle.cache.get(fetchComponent); + if (this.unmounted) return Promise.resolve(); + if (fetchComponent === undefined) { this.setState({ mod: null }); return Promise.resolve(); @@ -76,10 +71,12 @@ class Bundle extends React.PureComponent { return fetchComponent() .then((mod) => { Bundle.cache.set(fetchComponent, mod); + if (this.unmounted) return; this.setState({ mod: mod.default }); onFetchSuccess(); }) .catch((error) => { + if (this.unmounted) return; this.setState({ mod: null }); onFetchFail(error); }); @@ -88,7 +85,7 @@ class Bundle extends React.PureComponent { render() { const { loading: Loading, error: Error, children, renderDelay } = this.props; const { mod, forceRender } = this.state; - const elapsed = this.timestamp ? (new Date() - this.timestamp) : renderDelay; + const elapsed = this.timestamp ? (new Date().getTime() - this.timestamp.getTime()) : renderDelay; if (mod === undefined) { return (elapsed >= renderDelay || forceRender) ? : null; diff --git a/app/soapbox/normalizers/index.ts b/app/soapbox/normalizers/index.ts index 9fdc04f68..d25cbd014 100644 --- a/app/soapbox/normalizers/index.ts +++ b/app/soapbox/normalizers/index.ts @@ -20,5 +20,4 @@ export { StatusRecord, normalizeStatus } from './status'; export { StatusEditRecord, normalizeStatusEdit } from './status_edit'; export { TagRecord, normalizeTag } from './tag'; -export { AdRecord, normalizeAd } from './soapbox/ad'; export { SoapboxConfigRecord, normalizeSoapboxConfig } from './soapbox/soapbox_config'; diff --git a/app/soapbox/normalizers/soapbox/ad.ts b/app/soapbox/normalizers/soapbox/ad.ts deleted file mode 100644 index c29ee9a3e..000000000 --- a/app/soapbox/normalizers/soapbox/ad.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { - Map as ImmutableMap, - Record as ImmutableRecord, - fromJS, -} from 'immutable'; - -import { CardRecord, normalizeCard } from '../card'; - -export const AdRecord = ImmutableRecord({ - card: CardRecord(), - impression: undefined as string | undefined, -}); - -/** Normalizes an ad from Soapbox Config. */ -export const normalizeAd = (ad: Record) => { - const map = ImmutableMap(fromJS(ad)); - const card = normalizeCard(map.get('card')); - return AdRecord(map.set('card', card)); -}; diff --git a/app/soapbox/normalizers/soapbox/soapbox_config.ts b/app/soapbox/normalizers/soapbox/soapbox_config.ts index 9c4d4805a..b7bf99b59 100644 --- a/app/soapbox/normalizers/soapbox/soapbox_config.ts +++ b/app/soapbox/normalizers/soapbox/soapbox_config.ts @@ -9,10 +9,7 @@ import trimStart from 'lodash/trimStart'; import { toTailwind } from 'soapbox/utils/tailwind'; import { generateAccent } from 'soapbox/utils/theme'; -import { normalizeAd } from './ad'; - import type { - Ad, PromoPanelItem, FooterItem, CryptoAddress, @@ -81,7 +78,6 @@ export const CryptoAddressRecord = ImmutableRecord({ }); export const SoapboxConfigRecord = ImmutableRecord({ - ads: ImmutableList(), appleAppId: null, authProvider: '', customRegProvider: '', @@ -131,11 +127,6 @@ export const SoapboxConfigRecord = ImmutableRecord({ type SoapboxConfigMap = ImmutableMap; -const normalizeAds = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMap => { - const ads = ImmutableList>(soapboxConfig.get('ads')); - return soapboxConfig.set('ads', ads.map(normalizeAd)); -}; - const normalizeCryptoAddress = (address: unknown): CryptoAddress => { return CryptoAddressRecord(ImmutableMap(fromJS(address))).update('ticker', ticker => { return trimStart(ticker, '$').toLowerCase(); @@ -200,7 +191,6 @@ export const normalizeSoapboxConfig = (soapboxConfig: Record) => { normalizeFooterLinks(soapboxConfig); maybeAddMissingColors(soapboxConfig); normalizeCryptoAddresses(soapboxConfig); - normalizeAds(soapboxConfig); }), ); }; diff --git a/app/soapbox/types/soapbox.ts b/app/soapbox/types/soapbox.ts index 1a37d1a88..32c2f681c 100644 --- a/app/soapbox/types/soapbox.ts +++ b/app/soapbox/types/soapbox.ts @@ -1,4 +1,3 @@ -import { AdRecord } from 'soapbox/normalizers/soapbox/ad'; import { PromoPanelItemRecord, FooterItemRecord, @@ -8,7 +7,6 @@ import { type Me = string | null | false | undefined; -type Ad = ReturnType; type PromoPanelItem = ReturnType; type FooterItem = ReturnType; type CryptoAddress = ReturnType; @@ -16,7 +14,6 @@ type SoapboxConfig = ReturnType; export { Me, - Ad, PromoPanelItem, FooterItem, CryptoAddress, diff --git a/package.json b/package.json index 37a81123a..707005adb 100644 --- a/package.json +++ b/package.json @@ -226,7 +226,7 @@ "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-promise": "^5.1.0", "eslint-plugin-react": "^7.25.1", - "eslint-plugin-react-hooks": "^4.2.0", + "eslint-plugin-react-hooks": "^5.0.0", "fake-indexeddb": "^3.1.7", "husky": "^9.1.6", "jest": "^28.1.2", diff --git a/yarn.lock b/yarn.lock index 47763c15a..6751d1a43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5945,10 +5945,10 @@ eslint-plugin-promise@^5.1.0: resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-5.1.0.tgz#fb2188fb734e4557993733b41aa1a688f46c6f24" integrity sha512-NGmI6BH5L12pl7ScQHbg7tvtk4wPxxj8yPHH47NvSmMtFneC077PSeY3huFj06ZWZvtbfxSPt3RuOQD5XcR4ng== -eslint-plugin-react-hooks@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556" - integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ== +eslint-plugin-react-hooks@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz#72e2eefbac4b694f5324154619fee44f5f60f101" + integrity sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw== eslint-plugin-react@^7.25.1: version "7.25.1"