// @flow import { ENABLE_NO_SOURCE_CLAIMS, SIMPLE_SITE } from 'config'; import * as CS from 'constants/claim_search'; import type { Node } from 'react'; import React from 'react'; import { createNormalizedClaimSearchKey, MATURE_TAGS, splitBySeparator } from 'lbry-redux'; import ClaimPreviewTile from 'component/claimPreviewTile'; import { useHistory } from 'react-router'; import { getLivestreamOnlyOptions } from 'util/search'; /** * Updates 'uris' by adding and/or moving active livestreams to the front of * list. * 'liveUris' is also updated with any entries that were moved to the * front, for convenience. * * @param uris [Ref] * @param liveUris [Ref] * @param livestreamMap * @param claimsByUri * @param claimSearchByQuery * @param options */ export function prioritizeActiveLivestreams( uris: Array, liveUris: Array, livestreamMap: { [string]: any }, claimsByUri: { [string]: any }, claimSearchByQuery: { [string]: Array }, options: any ) { if (!livestreamMap || !uris) return; const claimIsLive = (claim, liveChannelIds) => { // This function relies on: // 1. Only 1 actual livestream per channel (i.e. all other livestream-claims // for that channel actually point to the same source). // 2. 'liveChannelIds' needs to be pruned after being accounted for, // otherwise all livestream-claims will be "live" (we'll only take the // latest one as "live" ). return ( claim && claim.value_type === 'stream' && claim.value.source === undefined && claim.signing_channel && liveChannelIds.includes(claim.signing_channel.claim_id) ); }; let liveChannelIds = Object.keys(livestreamMap); // 1. Collect active livestreams from the primary search to put in front. uris.forEach((uri) => { const claim = claimsByUri[uri]; if (claimIsLive(claim, liveChannelIds)) { liveUris.push(uri); // This live channel has been accounted for, so remove it. liveChannelIds.splice(liveChannelIds.indexOf(claim.signing_channel.claim_id), 1); } }); // 2. Now, repeat on the secondary search. if (options) { const livestreamsOnlySearchCacheQuery = createNormalizedClaimSearchKey(getLivestreamOnlyOptions(options)); const livestreamsOnlyUris = claimSearchByQuery[livestreamsOnlySearchCacheQuery]; if (livestreamsOnlyUris) { livestreamsOnlyUris.forEach((uri) => { const claim = claimsByUri[uri]; if (!uris.includes(uri) && claimIsLive(claim, liveChannelIds)) { liveUris.push(uri); // This live channel has been accounted for, so remove it. liveChannelIds.splice(liveChannelIds.indexOf(claim.signing_channel.claim_id), 1); } }); } } // 3. Finalize uris by putting live livestreams in front. const newUris = liveUris.concat(uris.filter((uri) => !liveUris.includes(uri))); uris.splice(0, uris.length, ...newUris); } // **************************************************************************** // ClaimTilesDiscover // **************************************************************************** type Props = { prefixUris?: Array, pinUrls?: Array, uris: Array, liveLivestreamsFirst?: boolean, livestreamMap?: { [string]: any }, 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 --- claimSearchByQuery: { [string]: Array }, claimsByUri: { [string]: any }, fetchingClaimSearchByQuery: { [string]: boolean }, showNsfw: boolean, hideReposts: boolean, mutedUris: Array, blockedUris: Array, // --- perform --- doClaimSearch: ({}) => void, doFetchViewCount: (claimIdCsv: string) => void, }; function ClaimTilesDiscover(props: Props) { const { doClaimSearch, claimSearchByQuery, claimsByUri, showNsfw, hideReposts, fetchViewCount, // Below are options to pass that are forwarded to claim_search tags, channelIds, claimIds, orderBy, pageSize = 8, releaseTime, languages, claimType, streamTypes, timestamp, feeAmount, limitClaimsPerChannel, fetchingClaimSearchByQuery, hasSource, hasNoSource, renderProperties, blockedUris, mutedUris, liveLivestreamsFirst, livestreamMap, pinUrls, prefixUris, showNoSourceClaims, doFetchViewCount, } = props; const { location } = useHistory(); const urlParams = new URLSearchParams(location.search); const feeAmountInUrl = urlParams.get('fee_amount'); const feeAmountParam = feeAmountInUrl || feeAmount; const mutedAndBlockedChannelIds = Array.from( new Set(mutedUris.concat(blockedUris).map((uri) => splitBySeparator(uri)[1])) ); const liveUris = []; let streamTypesParam; if (streamTypes) { streamTypesParam = streamTypes; } else if (SIMPLE_SITE && !hasNoSource && streamTypes !== null) { streamTypesParam = [CS.FILE_VIDEO, CS.FILE_AUDIO]; } const [prevUris, setPrevUris] = React.useState([]); const options: { page_size: number, no_totals: boolean, any_tags: Array, channel_ids: Array, claim_ids?: Array, not_channel_ids: Array, not_tags: Array, order_by: Array, languages?: Array, release_time?: string, claim_type?: string | Array, timestamp?: string, fee_amount?: string, limit_claims_per_channel?: number, stream_types?: Array, has_source?: boolean, has_no_source?: boolean, } = { page_size: pageSize, claim_type: claimType || ['stream', 'repost', 'channel'], // no_totals makes it so the sdk doesn't have to calculate total number pages for pagination // it's faster, but we will need to remove it if we start using total_pages no_totals: true, any_tags: tags || [], not_tags: !showNsfw ? MATURE_TAGS : [], any_languages: languages, channel_ids: channelIds || [], not_channel_ids: mutedAndBlockedChannelIds, order_by: orderBy || ['trending_group', 'trending_mixed'], stream_types: streamTypesParam, }; if (ENABLE_NO_SOURCE_CLAIMS && hasNoSource) { options.has_no_source = true; } else if (hasSource || (!ENABLE_NO_SOURCE_CLAIMS && (!claimType || claimType === 'stream'))) { options.has_source = true; } if (releaseTime) { options.release_time = releaseTime; } if (feeAmountParam) { options.fee_amount = feeAmountParam; } if (limitClaimsPerChannel) { options.limit_claims_per_channel = limitClaimsPerChannel; } // https://github.com/lbryio/lbry-desktop/issues/3774 if (hideReposts) { if (Array.isArray(options.claim_type)) { options.claim_type = options.claim_type.filter((claimType) => claimType !== 'repost'); } else { options.claim_type = ['stream', 'channel']; } } if (claimType) { options.claim_type = claimType; } if (timestamp) { options.timestamp = timestamp; } if (claimIds) { options.claim_ids = claimIds; } const mainSearchKey = createNormalizedClaimSearchKey(options); const livestreamSearchKey = liveLivestreamsFirst ? createNormalizedClaimSearchKey(getLivestreamOnlyOptions(options)) : undefined; let uris = (prefixUris || []).concat(claimSearchByQuery[mainSearchKey] || []); const isLoading = fetchingClaimSearchByQuery[mainSearchKey]; if (liveLivestreamsFirst && livestreamMap && !isLoading) { prioritizeActiveLivestreams(uris, liveUris, livestreamMap, claimsByUri, claimSearchByQuery, options); } // Don't use the query from createNormalizedClaimSearchKey for the effect since that doesn't include page & release_time const optionsStringForEffect = JSON.stringify(options); const shouldPerformSearch = !isLoading && uris.length === 0; if ( prefixUris === undefined && (claimSearchByQuery[mainSearchKey] === undefined || (livestreamSearchKey && claimSearchByQuery[livestreamSearchKey] === undefined)) ) { // This is a new query and we don't have results yet ... if (prevUris.length !== 0) { // ... but we have previous results. Use it until new results are here. uris = prevUris; } } const modifiedUris = uris ? uris.slice() : []; const fixUris = pinUrls || []; if (pinUrls && modifiedUris && modifiedUris.length > 2 && window.location.pathname === '/') { fixUris.forEach((fixUri) => { if (modifiedUris.indexOf(fixUri) !== -1) { modifiedUris.splice(modifiedUris.indexOf(fixUri), 1); } else { modifiedUris.pop(); } }); modifiedUris.splice(2, 0, ...fixUris); } // ************************************************************************** // ************************************************************************** function resolveLive(index) { if (liveLivestreamsFirst && livestreamMap && index < liveUris.length) { return true; } return undefined; } function fetchViewCountForUris(uris) { const claimIds = []; if (uris) { uris.forEach((uri) => { if (claimsByUri[uri]) { claimIds.push(claimsByUri[uri].claim_id); } }); } if (claimIds.length > 0) { doFetchViewCount(claimIds.join(',')); } } // ************************************************************************** // ************************************************************************** React.useEffect(() => { if (shouldPerformSearch) { const searchOptions = JSON.parse(optionsStringForEffect); doClaimSearch(searchOptions); if (liveLivestreamsFirst) { doClaimSearch(getLivestreamOnlyOptions(searchOptions)); } } }, [doClaimSearch, shouldPerformSearch, optionsStringForEffect, liveLivestreamsFirst]); React.useEffect(() => { if (JSON.stringify(prevUris) !== JSON.stringify(uris) && !shouldPerformSearch) { // Stash new results for next render cycle: setPrevUris(uris); // Fetch view count: if (fetchViewCount) { fetchViewCountForUris(uris); } } }, [shouldPerformSearch, prevUris, uris]); // eslint-disable-line react-hooks/exhaustive-deps // ************************************************************************** // ************************************************************************** return (
    {modifiedUris && modifiedUris.length ? modifiedUris.map((uri, index) => ( )) : new Array(pageSize) .fill(1) .map((x, i) => ( ))}
); } export default ClaimTilesDiscover;