// @flow import type { Node } from 'react'; import React from 'react'; import ClaimPreviewTile from 'component/claimPreviewTile'; import useFetchViewCount from 'effects/use-fetch-view-count'; import useLastVisibleItem from 'effects/use-last-visible-item'; function urisEqual(prev: ?Array, next: ?Array) { if (!prev || !next) { // ClaimList: "null" and "undefined" have special meaning, // so we can't just compare array length here. // - null = "timed out" // - undefined = "no result". return prev === next; } // $FlowFixMe - already checked for null above. return prev.length === next.length && prev.every((value, index) => value === next[index]); } // **************************************************************************** // ClaimTilesDiscover // **************************************************************************** type Props = { prefixUris?: Array, pinUrls?: Array, uris: Array, injectedItem?: { node: Node, index?: number, replace?: boolean }, showNoSourceClaims?: boolean, renderProperties?: (Claim) => ?Node, fetchViewCount?: boolean, // claim search options are below tags: Array, claimIds?: Array, channelIds?: Array, pageSize: number, orderBy?: Array, releaseTime?: string, languages?: Array, claimType?: string | Array, streamTypes?: Array, timestamp?: string, feeAmount?: string, limitClaimsPerChannel?: number, hasSource?: boolean, hasNoSource?: boolean, // --- select --- location: { search: string }, claimSearchResults: Array, claimsByUri: { [string]: any }, fetchingClaimSearch: boolean, showNsfw: boolean, hideReposts: boolean, optionsStringified: string, // --- perform --- doClaimSearch: ({}) => void, doFetchViewCount: (claimIdCsv: string) => void, }; function ClaimTilesDiscover(props: Props) { const { doClaimSearch, claimSearchResults, claimsByUri, fetchViewCount, fetchingClaimSearch, hasNoSource, renderProperties, pinUrls, prefixUris, injectedItem, showNoSourceClaims, doFetchViewCount, pageSize = 8, optionsStringified, } = props; // reference to the claim-grid const sectionRef = React.useRef(); // determine the index where the ad should be injected const injectedIndex = useLastVisibleItem(injectedItem, sectionRef); const prevUris = React.useRef(); const claimSearchUris = claimSearchResults || []; const isUnfetchedClaimSearch = claimSearchResults === undefined; const shouldPerformSearch = !fetchingClaimSearch && claimSearchUris.length === 0; const uris = (prefixUris || []).concat(claimSearchUris); if (prefixUris && prefixUris.length) uris.splice(prefixUris.length * -1, prefixUris.length); if (pinUrls && uris && uris.length > 2 && window.location.pathname === '/') { pinUrls.forEach((pin) => { if (uris.indexOf(pin) !== -1) { uris.splice(uris.indexOf(pin), 1); } else { uris.pop(); } }); uris.splice(2, 0, ...pinUrls); } if (uris.length > 0 && uris.length < pageSize && shouldPerformSearch) { // prefixUri and pinUrls might already be present while waiting for the // remaining claim_search results. Fill the space to prevent layout shifts. uris.push(...Array(pageSize - uris.length).fill('')); } // Show previous results while we fetch to avoid blinkies and poor CLS. const finalUris = isUnfetchedClaimSearch && prevUris.current ? prevUris.current : uris; prevUris.current = finalUris; // -------------------------------------------------------------------------- // -------------------------------------------------------------------------- // populate the view counts for the current claim uris useFetchViewCount(fetchViewCount, uris, claimsByUri, doFetchViewCount); // Run `doClaimSearch` React.useEffect(() => { if (shouldPerformSearch) { const searchOptions = JSON.parse(optionsStringified); doClaimSearch(searchOptions); } }, [doClaimSearch, shouldPerformSearch, optionsStringified]); return (
    {finalUris && finalUris.length ? finalUris.map((uri, i) => { if (uri) { // if indexes match, inject ad in place of tile (aka replace it) if (injectedIndex === i && injectedItem && injectedItem.replace) { return {injectedItem.node}; } return ( {injectedIndex === i && injectedItem && injectedItem.node} {/* inject ad */} ); } else { return ; } }) : new Array(pageSize) .fill(1) .map((x, i) => ( ))}
); } export default React.memo(ClaimTilesDiscover, areEqual); // **************************************************************************** // **************************************************************************** function trace(key, value) { // @if process.env.DEBUG_TILE_RENDER // $FlowFixMe "cannot coerce certain types". console.log(`[claimTilesDiscover] ${key}: ${value}`); // eslint-disable-line no-console // @endif } function areEqual(prev: Props, next: Props) { // --- Deep-compare --- // These are props that are hard to memoize from where it is passed. if (prev.claimType !== next.claimType) { // Array: confirm the contents are actually different. if (prev.claimType && next.claimType && JSON.stringify(prev.claimType) !== JSON.stringify(next.claimType)) { trace('claimType', next.claimType); return false; } } const ARRAY_KEYS = ['prefixUris', 'channelIds']; for (let i = 0; i < ARRAY_KEYS.length; ++i) { const key = ARRAY_KEYS[i]; if (!urisEqual(prev[key], next[key])) { trace(key, next[key]); return false; } } // --- Default the rest(*) to shallow-compare --- // (*) including new props introduced in the future, in case developer forgets // to update this function. Better to render more than miss an important one. const KEYS_TO_IGNORE = [ ...ARRAY_KEYS, 'claimType', // Handled above. 'claimsByUri', // Used for view-count. Just ignore it for now. 'location', 'history', 'match', 'doClaimSearch', ]; const propKeys = Object.keys(next); for (let i = 0; i < propKeys.length; ++i) { const pk = propKeys[i]; if (!KEYS_TO_IGNORE.includes(pk) && prev[pk] !== next[pk]) { trace(pk, next[pk]); return false; } } return true; }