// @flow import type { Node } from 'react'; import React, { useState } from 'react'; import Button from 'component/button'; import ClaimPreviewTile from 'component/claimPreviewTile'; import I18nMessage from 'component/i18nMessage'; import useFetchViewCount from 'effects/use-fetch-view-count'; import useGetLastVisibleSlot from 'effects/use-get-last-visible-slot'; import useResolvePins from 'effects/use-resolve-pins'; import useGetUserMemberships from 'effects/use-get-user-memberships'; const SHOW_TIMEOUT_MSG = false; 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, pins?: { urls?: Array, claimIds?: Array, onlyPinForOrder?: string }, uris: Array, injectedItem?: ListInjectedItem, 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, forceShowReposts?: boolean, // overrides SETTINGS.HIDE_REPOSTS loading: boolean, // --- select --- location: { search: string }, claimSearchResults: Array, claimsByUri: { [string]: any }, claimsById: { [string]: any }, fetchingClaimSearch: boolean, showNsfw: boolean, hideReposts: boolean, optionsStringified: string, // --- perform --- doClaimSearch: ({}) => void, doFetchViewCount: (claimIdCsv: string) => void, doFetchUserMemberships: (claimIdCsv: string) => void, doResolveClaimIds: (Array) => Promise, doResolveUris: (Array, boolean) => Promise, }; function ClaimTilesDiscover(props: Props) { const { doClaimSearch, claimSearchResults, claimsByUri, claimsById, fetchViewCount, fetchingClaimSearch, hasNoSource, // forceShowReposts = false, renderProperties, pins, prefixUris, injectedItem, showNoSourceClaims, doFetchViewCount, pageSize = 8, optionsStringified, doFetchUserMemberships, doResolveClaimIds, doResolveUris, loading, } = props; const listRef = React.useRef(); const findLastVisibleSlot = injectedItem && injectedItem.node && injectedItem.index === undefined; const lastVisibleIndex = useGetLastVisibleSlot(listRef, !findLastVisibleSlot); const prevUris = React.useRef(); const claimSearchUris = claimSearchResults || []; const isUnfetchedClaimSearch = claimSearchResults === undefined; const resolvedPinUris = useResolvePins({ pins, claimsById, doResolveClaimIds, doResolveUris }); const [uriBuffer, setUriBuffer] = useState([]); const timedOut = claimSearchResults === null; const shouldPerformSearch = !fetchingClaimSearch && !timedOut && claimSearchUris.length === 0; const uris = (prefixUris || []).concat(claimSearchUris); if (prefixUris && prefixUris.length) uris.splice(prefixUris.length * -1, prefixUris.length); if (window.location.pathname === '/') { injectPinUrls(uris, pins, resolvedPinUris); } 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; // -------------------------------------------------------------------------- // -------------------------------------------------------------------------- function injectPinUrls(uris, pins, resolvedPinUris) { if (!pins || !uris || uris.length <= 2) { return; } if (resolvedPinUris) { resolvedPinUris.forEach((pin) => { if (uris.includes(pin)) { uris.splice(uris.indexOf(pin), 1); } else { uris.pop(); } }); uris.splice(2, 0, ...resolvedPinUris); } } const getInjectedItem = (index) => { if (injectedItem && injectedItem.node) { if (typeof injectedItem.node === 'function') { return injectedItem.node(index, lastVisibleIndex, pageSize); } else { if (injectedItem.index === undefined || injectedItem.index === null) { return index === lastVisibleIndex ? injectedItem.node : null; } else { return index === injectedItem.index ? injectedItem.node : null; } } } return null; }; // -------------------------------------------------------------------------- // -------------------------------------------------------------------------- useFetchViewCount(fetchViewCount, uris, claimsByUri, doFetchViewCount); useGetUserMemberships(true, uris, claimsByUri, doFetchUserMemberships); React.useEffect(() => { if (shouldPerformSearch) { const searchOptions = JSON.parse(optionsStringified); doClaimSearch(searchOptions); } }, [doClaimSearch, shouldPerformSearch, optionsStringified]); React.useEffect(() => { refreshBuffer(); }, [finalUris, injectedItem, lastVisibleIndex, pageSize]); function refreshBuffer() { finalUris.forEach((uri, index) => { if (uri) { const inj = getInjectedItem(index); if (inj) { if (uriBuffer.indexOf(index) === -1) { const newUriBuffer = uriBuffer; newUriBuffer.push(index); setUriBuffer(newUriBuffer); } } } }); } // -------------------------------------------------------------------------- // -------------------------------------------------------------------------- if (timedOut && SHOW_TIMEOUT_MSG) { return (

{__('Sorry, your request timed out. Try refreshing in a bit.')}

), }} > If you continue to have issues, please %contact_support%.

); } return (
    {!loading && finalUris && finalUris.length ? finalUris.map((uri, i) => { if (uri) { const inj = getInjectedItem(i); if (inj) refreshBuffer(); return ( {inj && inj} {(i < finalUris.length - uriBuffer.length || i < pageSize - uriBuffer.length) && ( )} ); } 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; }