From ebda47576dd2f190f8565207720c8bea78fdb2f5 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Thu, 16 Sep 2021 16:04:05 +0800 Subject: [PATCH 01/15] =?UTF-8?q?=E2=9D=8C=20Remove=20old=20method=20of=20?= =?UTF-8?q?displaying=20active=20livestreams?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completely remove it for now to make the commit deltas clearer. We'll replace it with the new method at the end. --- ui/component/claimList/index.js | 4 +- ui/component/claimList/view.jsx | 25 +---- ui/component/claimListDiscover/view.jsx | 22 +--- ui/component/claimTilesDiscover/view.jsx | 125 ++--------------------- ui/page/channelsFollowing/view.jsx | 4 - ui/page/discover/view.jsx | 4 - ui/page/home/view.jsx | 11 +- ui/util/search.js | 19 ---- 8 files changed, 12 insertions(+), 202 deletions(-) diff --git a/ui/component/claimList/index.js b/ui/component/claimList/index.js index 67a4024f0..d9d20509c 100644 --- a/ui/component/claimList/index.js +++ b/ui/component/claimList/index.js @@ -1,12 +1,10 @@ import { connect } from 'react-redux'; import ClaimList from './view'; -import { SETTINGS, selectClaimSearchByQuery, selectClaimsByUri } from 'lbry-redux'; +import { SETTINGS } from 'lbry-redux'; import { makeSelectClientSetting } from 'redux/selectors/settings'; const select = (state) => ({ searchInLanguage: makeSelectClientSetting(SETTINGS.SEARCH_IN_LANGUAGE)(state), - claimSearchByQuery: selectClaimSearchByQuery(state), - claimsByUri: selectClaimsByUri(state), }); export default connect(select)(ClaimList); diff --git a/ui/component/claimList/view.jsx b/ui/component/claimList/view.jsx index 4da171d4f..30153d28f 100644 --- a/ui/component/claimList/view.jsx +++ b/ui/component/claimList/view.jsx @@ -9,7 +9,6 @@ import { FormField } from 'component/common/form'; import usePersistedState from 'effects/use-persisted-state'; import debounce from 'util/debounce'; import ClaimPreviewTile from 'component/claimPreviewTile'; -import { prioritizeActiveLivestreams } from 'component/claimTilesDiscover/view'; const DEBOUNCE_SCROLL_HANDLER_MS = 150; const SORT_NEW = 'new'; @@ -41,9 +40,6 @@ type Props = { hideMenu?: boolean, claimSearchByQuery: { [string]: Array }, claimsByUri: { [string]: any }, - liveLivestreamsFirst?: boolean, - livestreamMap?: { [string]: any }, - searchOptions?: any, collectionId?: string, showNoSourceClaims?: boolean, onClick?: (e: any, claim?: ?Claim, index?: number) => void, @@ -73,11 +69,6 @@ export default function ClaimList(props: Props) { renderProperties, searchInLanguage, hideMenu, - claimSearchByQuery, - claimsByUri, - liveLivestreamsFirst, - livestreamMap, - searchOptions, collectionId, showNoSourceClaims, onClick, @@ -87,23 +78,11 @@ export default function ClaimList(props: Props) { const timedOut = uris === null; const urisLength = (uris && uris.length) || 0; - const liveUris = []; - if (liveLivestreamsFirst && livestreamMap) { - prioritizeActiveLivestreams(uris, liveUris, livestreamMap, claimsByUri, claimSearchByQuery, searchOptions); - } - const sortedUris = (urisLength > 0 && (currentSort === SORT_NEW ? uris : uris.slice().reverse())) || []; const noResultMsg = searchInLanguage ? __('No results. Contents may be hidden by the Language filter.') : __('No results'); - const resolveLive = (index) => { - if (liveLivestreamsFirst && livestreamMap && index < liveUris.length) { - return true; - } - return undefined; - }; - function handleSortChange() { setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW); } @@ -138,13 +117,12 @@ export default function ClaimList(props: Props) { return tileLayout && !header ? (
{urisLength > 0 && - uris.map((uri, index) => ( + uris.map((uri) => ( @@ -216,7 +194,6 @@ export default function ClaimList(props: Props) { // https://github.com/lbryio/lbry-redux/blob/master/src/redux/actions/publish.js#L74-L79 return claim.name.length === 24 && !claim.name.includes(' ') && claim.value.author === 'Spee.ch'; }} - live={resolveLive(index)} onClick={(e, claim, index) => handleClaimClicked(e, claim, index)} /> diff --git a/ui/component/claimListDiscover/view.jsx b/ui/component/claimListDiscover/view.jsx index f04929183..cccbdf067 100644 --- a/ui/component/claimListDiscover/view.jsx +++ b/ui/component/claimListDiscover/view.jsx @@ -14,7 +14,6 @@ import ClaimPreviewTile from 'component/claimPreviewTile'; import I18nMessage from 'component/i18nMessage'; import ClaimListHeader from 'component/claimListHeader'; import { useIsLargeScreen } from 'effects/use-screensize'; -import { getLivestreamOnlyOptions } from 'util/search'; type Props = { uris: Array, @@ -31,7 +30,6 @@ type Props = { includeSupportAction?: boolean, infiniteScroll?: Boolean, isChannel?: boolean, - liveLivestreamsFirst?: boolean, personalView: boolean, showHeader: boolean, showHiddenByUser?: boolean, @@ -64,7 +62,6 @@ type Props = { channelIds?: Array, claimIds?: Array, subscribedChannels: Array, - livestreamMap?: { [string]: any }, header?: Node, headerLabel?: string | Node, @@ -148,8 +145,6 @@ function ClaimListDiscover(props: Props) { releaseTime, scrollAnchor, showHiddenByUser = false, - liveLivestreamsFirst, - livestreamMap, hasSource, hasNoSource, isChannel = false, @@ -400,11 +395,6 @@ function ClaimListDiscover(props: Props) { // claimSearchResult.splice(2, 0, fixUri); // } - const livestreamSearchKey = liveLivestreamsFirst - ? createNormalizedClaimSearchKey(getLivestreamOnlyOptions(options)) - : undefined; - const livestreamSearchResult = livestreamSearchKey && claimSearchByQuery[livestreamSearchKey]; - const [finalUris, setFinalUris] = React.useState( getFinalUrisInitialState(history.action === 'POP', claimSearchResult) ); @@ -571,10 +561,6 @@ function ClaimListDiscover(props: Props) { if (shouldPerformSearch) { const searchOptions = JSON.parse(optionsStringForEffect); doClaimSearch(searchOptions); - - if (liveLivestreamsFirst && options.page === 1) { - doClaimSearch(getLivestreamOnlyOptions(searchOptions)); - } } }, [doClaimSearch, shouldPerformSearch, optionsStringForEffect, forceRefresh]); @@ -586,12 +572,12 @@ function ClaimListDiscover(props: Props) { } } else { // Wait until all queries are done before updating the uris to avoid layout shifts. - const pending = claimSearchResult === undefined || (liveLivestreamsFirst && livestreamSearchResult === undefined); + const pending = claimSearchResult === undefined; if (!pending && !urisEqual(claimSearchResult, finalUris)) { setFinalUris(claimSearchResult); } } - }, [uris, claimSearchResult, finalUris, setFinalUris, liveLivestreamsFirst, livestreamSearchResult]); + }, [uris, claimSearchResult, finalUris, setFinalUris]); React.useEffect(() => { if (fetchViewCount) { @@ -645,8 +631,6 @@ function ClaimListDiscover(props: Props) { includeSupportAction={includeSupportAction} injectedItem={injectedItem} showHiddenByUser={showHiddenByUser} - liveLivestreamsFirst={liveLivestreamsFirst} - livestreamMap={livestreamMap} searchOptions={options} showNoSourceClaims={showNoSourceClaims} empty={empty} @@ -679,8 +663,6 @@ function ClaimListDiscover(props: Props) { includeSupportAction={includeSupportAction} injectedItem={injectedItem} showHiddenByUser={showHiddenByUser} - liveLivestreamsFirst={liveLivestreamsFirst} - livestreamMap={livestreamMap} searchOptions={options} showNoSourceClaims={hasNoSource || showNoSourceClaims} empty={empty} diff --git a/ui/component/claimTilesDiscover/view.jsx b/ui/component/claimTilesDiscover/view.jsx index f28e98817..f22379a4a 100644 --- a/ui/component/claimTilesDiscover/view.jsx +++ b/ui/component/claimTilesDiscover/view.jsx @@ -6,90 +6,11 @@ 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, @@ -148,8 +69,6 @@ function ClaimTilesDiscover(props: Props) { renderProperties, blockedUris, mutedUris, - liveLivestreamsFirst, - livestreamMap, pinUrls, prefixUris, showNoSourceClaims, @@ -163,7 +82,7 @@ function ClaimTilesDiscover(props: Props) { const mutedAndBlockedChannelIds = Array.from( new Set(mutedUris.concat(blockedUris).map((uri) => splitBySeparator(uri)[1])) ); - const liveUris = []; + let streamTypesParam; if (streamTypes) { streamTypesParam = streamTypes; @@ -245,28 +164,16 @@ function ClaimTilesDiscover(props: Props) { options.claim_ids = claimIds; } - const mainSearchKey = createNormalizedClaimSearchKey(options); - const livestreamSearchKey = liveLivestreamsFirst - ? createNormalizedClaimSearchKey(getLivestreamOnlyOptions(options)) - : undefined; + const searchKey = createNormalizedClaimSearchKey(options); + const isLoading = fetchingClaimSearchByQuery[searchKey]; - let uris = (prefixUris || []).concat(claimSearchByQuery[mainSearchKey] || []); - - const isLoading = fetchingClaimSearchByQuery[mainSearchKey]; - - if (liveLivestreamsFirst && livestreamMap && !isLoading) { - prioritizeActiveLivestreams(uris, liveUris, livestreamMap, claimsByUri, claimSearchByQuery, options); - } + let uris = (prefixUris || []).concat(claimSearchByQuery[searchKey] || []); // 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)) - ) { + if (prefixUris === undefined && claimSearchByQuery[searchKey] === 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. @@ -288,16 +195,6 @@ function ClaimTilesDiscover(props: Props) { modifiedUris.splice(2, 0, ...fixUris); } - // ************************************************************************** - // ************************************************************************** - - function resolveLive(index) { - if (liveLivestreamsFirst && livestreamMap && index < liveUris.length) { - return true; - } - return undefined; - } - function fetchViewCountForUris(uris) { const claimIds = []; @@ -314,19 +211,12 @@ function ClaimTilesDiscover(props: Props) { } } - // ************************************************************************** - // ************************************************************************** - React.useEffect(() => { if (shouldPerformSearch) { const searchOptions = JSON.parse(optionsStringForEffect); doClaimSearch(searchOptions); - - if (liveLivestreamsFirst) { - doClaimSearch(getLivestreamOnlyOptions(searchOptions)); - } } - }, [doClaimSearch, shouldPerformSearch, optionsStringForEffect, liveLivestreamsFirst]); + }, [doClaimSearch, shouldPerformSearch, optionsStringForEffect]); React.useEffect(() => { if (JSON.stringify(prevUris) !== JSON.stringify(uris) && !shouldPerformSearch) { @@ -345,13 +235,12 @@ function ClaimTilesDiscover(props: Props) { return (
    {modifiedUris && modifiedUris.length - ? modifiedUris.map((uri, index) => ( + ? modifiedUris.map((uri) => ( )) : new Array(pageSize) diff --git a/ui/page/channelsFollowing/view.jsx b/ui/page/channelsFollowing/view.jsx index 71e91d2b3..87bb5423f 100644 --- a/ui/page/channelsFollowing/view.jsx +++ b/ui/page/channelsFollowing/view.jsx @@ -9,7 +9,6 @@ import ClaimListDiscover from 'component/claimListDiscover'; import Page from 'component/page'; import Button from 'component/button'; import Icon from 'component/common/icon'; -import useGetLivestreams from 'effects/use-get-livestreams'; import { splitBySeparator } from 'lbry-redux'; type Props = { @@ -20,7 +19,6 @@ type Props = { function ChannelsFollowingPage(props: Props) { const { subscribedChannels, tileLayout } = props; const hasSubsribedChannels = subscribedChannels.length > 0; - const { livestreamMap } = useGetLivestreams(); return !hasSubsribedChannels ? ( @@ -46,8 +44,6 @@ function ChannelsFollowingPage(props: Props) { navigate={`/$/${PAGES.CHANNELS_FOLLOWING_DISCOVER}`} /> } - liveLivestreamsFirst - livestreamMap={livestreamMap} showNoSourceClaims={ENABLE_NO_SOURCE_CLAIMS} hasSource /> diff --git a/ui/page/discover/view.jsx b/ui/page/discover/view.jsx index 14a9705a5..8fa5511cf 100644 --- a/ui/page/discover/view.jsx +++ b/ui/page/discover/view.jsx @@ -15,7 +15,6 @@ import Icon from 'component/common/icon'; import Ads from 'web/component/ads'; import LbcSymbol from 'component/common/lbc-symbol'; import I18nMessage from 'component/i18nMessage'; -import useGetLivestreams from 'effects/use-get-livestreams'; import moment from 'moment'; type Props = { @@ -45,7 +44,6 @@ function DiscoverPage(props: Props) { const buttonRef = useRef(); const isHovering = useHover(buttonRef); const isMobile = useIsMobile(); - const { livestreamMap } = useGetLivestreams(); const urlParams = new URLSearchParams(search); const claimType = urlParams.get('claim_type'); @@ -172,8 +170,6 @@ function DiscoverPage(props: Props) { ) ) } - liveLivestreamsFirst - livestreamMap={livestreamMap} hasSource showNoSourceClaims={ENABLE_NO_SOURCE_CLAIMS} /> diff --git a/ui/page/home/view.jsx b/ui/page/home/view.jsx index 3e2b295f0..dde5eb299 100644 --- a/ui/page/home/view.jsx +++ b/ui/page/home/view.jsx @@ -9,7 +9,6 @@ import ClaimTilesDiscover from 'component/claimTilesDiscover'; import ClaimPreviewTile from 'component/claimPreviewTile'; import Icon from 'component/common/icon'; import WaitUntilOnPage from 'component/common/wait-until-on-page'; -import useGetLivestreams from 'effects/use-get-livestreams'; import { GetLinksData } from 'util/buildHomepage'; // @if TARGET='web' @@ -30,7 +29,6 @@ function HomePage(props: Props) { const showPersonalizedChannels = (authenticated || !IS_WEB) && subscribedChannels && subscribedChannels.length > 0; const showPersonalizedTags = (authenticated || !IS_WEB) && followedTags && followedTags.length > 0; const showIndividualTags = showPersonalizedTags && followedTags.length < 5; - const { livestreamMap } = useGetLivestreams(); const rowData: Array = GetLinksData( homepageData, @@ -53,14 +51,7 @@ function HomePage(props: Props) {
); const claimTiles = ( - + ); return ( diff --git a/ui/util/search.js b/ui/util/search.js index 1b70a2e22..0793e674b 100644 --- a/ui/util/search.js +++ b/ui/util/search.js @@ -21,25 +21,6 @@ export function createNormalizedSearchKey(query: string) { return normalizedQuery; } -/** - * Returns the "livestream only" version of the given 'options'. - * - * Currently, the 'has_source' attribute is being used to identify livestreams. - * - * @param options - * @returns {*} - */ -export function getLivestreamOnlyOptions(options: any) { - const newOptions = Object.assign({}, options); - delete newOptions.has_source; - delete newOptions.stream_types; - newOptions.has_no_source = true; - newOptions.claim_type = ['stream']; - newOptions.page_size = 50; - newOptions.order_by = ['release_time']; - return newOptions; -} - /** * getUriForSearchTerm * @param term -- 2.45.2 From b29f56d53f1a0fb1b7e32445f5b7e1a0b6c8206d Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Thu, 16 Sep 2021 16:02:41 +0800 Subject: [PATCH 02/15] Fetch and store active-livestream info in redux --- flow-typed/livestream.js | 12 +++++ ui/constants/action_types.js | 3 ++ ui/effects/use-get-livestreams.js | 55 ----------------------- ui/redux/actions/livestream.js | 73 +++++++++++++++++++++++++++++++ ui/redux/reducers/livestream.js | 12 +++++ ui/redux/selectors/livestream.js | 20 +++++++++ ui/util/livestream.js | 24 ++++++++++ 7 files changed, 144 insertions(+), 55 deletions(-) delete mode 100644 ui/effects/use-get-livestreams.js create mode 100644 ui/util/livestream.js diff --git a/flow-typed/livestream.js b/flow-typed/livestream.js index 40ed13f19..63f1dd6a7 100644 --- a/flow-typed/livestream.js +++ b/flow-typed/livestream.js @@ -24,4 +24,16 @@ declare type LivestreamReplayData = Array; declare type LivestreamState = { fetchingById: {}, viewersById: {}, + fetchingActiveLivestreams: boolean, + activeLivestreams: ?LivestreamInfo, +} + +declare type LivestreamInfo = { + [/* creatorId */ string]: { + live: boolean, + viewCount: number, + creatorId: string, + latestClaimId: string, + latestClaimUri: string, + } } diff --git a/ui/constants/action_types.js b/ui/constants/action_types.js index 4b02a2128..6baee5377 100644 --- a/ui/constants/action_types.js +++ b/ui/constants/action_types.js @@ -352,3 +352,6 @@ export const FETCH_NO_SOURCE_CLAIMS_STARTED = 'FETCH_NO_SOURCE_CLAIMS_STARTED'; export const FETCH_NO_SOURCE_CLAIMS_COMPLETED = 'FETCH_NO_SOURCE_CLAIMS_COMPLETED'; export const FETCH_NO_SOURCE_CLAIMS_FAILED = 'FETCH_NO_SOURCE_CLAIMS_FAILED'; export const VIEWERS_RECEIVED = 'VIEWERS_RECEIVED'; +export const FETCH_ACTIVE_LIVESTREAMS_STARTED = 'FETCH_ACTIVE_LIVESTREAMS_STARTED'; +export const FETCH_ACTIVE_LIVESTREAMS_FAILED = 'FETCH_ACTIVE_LIVESTREAMS_FAILED'; +export const FETCH_ACTIVE_LIVESTREAMS_COMPLETED = 'FETCH_ACTIVE_LIVESTREAMS_COMPLETED'; diff --git a/ui/effects/use-get-livestreams.js b/ui/effects/use-get-livestreams.js deleted file mode 100644 index fb77c1395..000000000 --- a/ui/effects/use-get-livestreams.js +++ /dev/null @@ -1,55 +0,0 @@ -// @flow -import React from 'react'; -import { LIVESTREAM_LIVE_API } from 'constants/livestream'; - -/** - * Gets latest livestream info list. Returns null (instead of a blank object) - * when there are no active livestreams. - * - * @param minViewers - * @param refreshMs - * @returns {{livestreamMap: null, loading: boolean}} - */ -export default function useGetLivestreams(minViewers: number = 0, refreshMs: number = 0) { - const [loading, setLoading] = React.useState(true); - const [livestreamMap, setLivestreamMap] = React.useState(null); - - React.useEffect(() => { - function checkCurrentLivestreams() { - fetch(LIVESTREAM_LIVE_API) - .then((res) => res.json()) - .then((res) => { - setLoading(false); - if (!res.data) { - setLivestreamMap(null); - return; - } - - const livestreamMap = res.data.reduce((acc, curr) => { - if (curr.viewCount >= minViewers) { - acc[curr.claimId] = curr; - } - return acc; - }, {}); - - setLivestreamMap(livestreamMap); - }) - .catch((err) => { - setLoading(false); - }); - } - - checkCurrentLivestreams(); - - if (refreshMs > 0) { - let fetchInterval = setInterval(checkCurrentLivestreams, refreshMs); - return () => { - if (fetchInterval) { - clearInterval(fetchInterval); - } - }; - } - }, []); - - return { livestreamMap, loading }; -} diff --git a/ui/redux/actions/livestream.js b/ui/redux/actions/livestream.js index c33d3ebc7..9ab7051a4 100644 --- a/ui/redux/actions/livestream.js +++ b/ui/redux/actions/livestream.js @@ -1,6 +1,7 @@ // @flow import * as ACTIONS from 'constants/action_types'; import { doClaimSearch } from 'lbry-redux'; +import { LIVESTREAM_LIVE_API } from 'constants/livestream'; export const doFetchNoSourceClaims = (channelId: string) => async (dispatch: Dispatch, getState: GetState) => { dispatch({ @@ -31,3 +32,75 @@ export const doFetchNoSourceClaims = (channelId: string) => async (dispatch: Dis }); } }; + +export const doFetchActiveLivestreams = () => { + return async (dispatch: Dispatch) => { + dispatch({ + type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_STARTED, + }); + + fetch(LIVESTREAM_LIVE_API) + .then((res) => res.json()) + .then((res) => { + if (!res.data) { + dispatch({ + type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED, + }); + return; + } + + const activeLivestreams: LivestreamInfo = res.data.reduce((acc, curr) => { + acc[curr.claimId] = { + live: curr.live, + viewCount: curr.viewCount, + creatorId: curr.claimId, + }; + return acc; + }, {}); + + dispatch( + // ** Creators can have multiple livestream claims (each with unique + // chat), and all of them will play the same stream when creator goes + // live. The UI usually just wants to report the latest claim, so we + // query that store it in `latestClaimUri`. + doClaimSearch({ + page_size: 50, + has_no_source: true, + channel_ids: Object.keys(activeLivestreams), + claim_type: ['stream'], + order_by: ['release_time'], // ** + limit_claims_per_channel: 1, // ** + no_totals: true, + }) + ) + .then((resolveInfo) => { + Object.values(resolveInfo).forEach((x) => { + // $FlowFixMe + const channelId = x.stream.signing_channel.claim_id; + activeLivestreams[channelId] = { + ...activeLivestreams[channelId], + // $FlowFixMe + latestClaimId: x.stream.claim_id, + // $FlowFixMe + latestClaimUri: x.stream.canonical_url, + }; + }); + + dispatch({ + type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_COMPLETED, + data: activeLivestreams, + }); + }) + .catch(() => { + dispatch({ + type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED, + }); + }); + }) + .catch((err) => { + dispatch({ + type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED, + }); + }); + }; +}; diff --git a/ui/redux/reducers/livestream.js b/ui/redux/reducers/livestream.js index 5a74b0a39..932be2cb9 100644 --- a/ui/redux/reducers/livestream.js +++ b/ui/redux/reducers/livestream.js @@ -5,6 +5,8 @@ import { handleActions } from 'util/redux-utils'; const defaultState: LivestreamState = { fetchingById: {}, viewersById: {}, + fetchingActiveLivestreams: false, + activeLivestreams: null, }; export default handleActions( @@ -36,6 +38,16 @@ export default handleActions( newViewersById[claimId] = connected; return { ...state, viewersById: newViewersById }; }, + [ACTIONS.FETCH_ACTIVE_LIVESTREAMS_STARTED]: (state: LivestreamState) => { + return { ...state, fetchingActiveLivestreams: true }; + }, + [ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED]: (state: LivestreamState) => { + return { ...state, fetchingActiveLivestreams: false }; + }, + [ACTIONS.FETCH_ACTIVE_LIVESTREAMS_COMPLETED]: (state: LivestreamState, action: any) => { + const activeLivestreams: LivestreamInfo = action.data; + return { ...state, fetchingActiveLivestreams: false, activeLivestreams }; + }, }, defaultState ); diff --git a/ui/redux/selectors/livestream.js b/ui/redux/selectors/livestream.js index 27ea16ddb..a48c4e10a 100644 --- a/ui/redux/selectors/livestream.js +++ b/ui/redux/selectors/livestream.js @@ -40,3 +40,23 @@ export const makeSelectPendingLivestreamsForChannelId = (channelId: string) => claim.signing_channel.claim_id === channelId ); }); + +export const selectActiveLivestreams = createSelector(selectState, (state) => state.activeLivestreams); + +export const makeSelectIsActiveLivestream = (uri: string) => + createSelector(selectState, (state) => { + const activeLivestreamValues = (state.activeLivestreams && Object.values(state.activeLivestreams)) || []; + // $FlowFixMe + return Boolean(activeLivestreamValues.find((v) => v.latestClaimUri === uri)); + }); + +export const makeSelectActiveLivestreamUris = (uri: string) => + createSelector(selectState, (state) => { + const activeLivestreamValues = (state.activeLivestreams && Object.values(state.activeLivestreams)) || []; + const uris = []; + activeLivestreamValues.forEach((v) => { + // $FlowFixMe + if (v.latestClaimUri) uris.push(v.latestClaimUri); + }); + return uris; + }); diff --git a/ui/util/livestream.js b/ui/util/livestream.js new file mode 100644 index 000000000..2cf026194 --- /dev/null +++ b/ui/util/livestream.js @@ -0,0 +1,24 @@ +// @flow + +/** + * Helper to extract livestream claim uris from the output of + * `selectActiveLivestreams`. + * + * @param activeLivestreams Object obtained from `selectActiveLivestreams`. + * @param channelIds List of channel IDs to filter the results with. + * @returns {[]|Array<*>} + */ +export function getLivestreamUris(activeLivestreams: ?LivestreamInfo, channelIds: ?Array) { + let values = (activeLivestreams && Object.values(activeLivestreams)) || []; + + if (channelIds && channelIds.length > 0) { + // $FlowFixMe + values = values.filter((v) => channelIds.includes(v.creatorId) && Boolean(v.latestClaimUri)); + } else { + // $FlowFixMe + values = values.filter((v) => Boolean(v.latestClaimUri)); + } + + // $FlowFixMe + return values.map((v) => v.latestClaimUri); +} -- 2.45.2 From 16b33fe05f2a7adb077a51e910761cd1545f73c9 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Fri, 17 Sep 2021 10:12:23 +0800 Subject: [PATCH 03/15] Tiles can now query active-livestream state from redux instead of getting from parent. --- ui/component/claimPreview/index.js | 2 ++ ui/component/claimPreview/view.jsx | 10 +++++----- ui/component/claimPreviewTile/index.js | 2 ++ ui/component/claimPreviewTile/view.jsx | 8 ++++---- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/ui/component/claimPreview/index.js b/ui/component/claimPreview/index.js index c35b0c8ce..832004474 100644 --- a/ui/component/claimPreview/index.js +++ b/ui/component/claimPreview/index.js @@ -20,6 +20,7 @@ import { } from 'lbry-redux'; import { selectMutedChannels, makeSelectChannelIsMuted } from 'redux/selectors/blocked'; import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc'; +import { makeSelectIsActiveLivestream } from 'redux/selectors/livestream'; import { selectShowMatureContent } from 'redux/selectors/settings'; import { makeSelectHasVisitedUri } from 'redux/selectors/content'; import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions'; @@ -56,6 +57,7 @@ const select = (state, props) => { streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state), wasPurchased: props.uri && makeSelectClaimWasPurchased(props.uri)(state), isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state), + isLivestreamActive: makeSelectIsActiveLivestream(props.uri)(state), isCollectionMine: makeSelectCollectionIsMine(props.collectionId)(state), collectionUris: makeSelectUrlsForCollectionId(props.collectionId)(state), collectionIndex: makeSelectIndexForUrlInCollection(props.uri, props.collectionId)(state), diff --git a/ui/component/claimPreview/view.jsx b/ui/component/claimPreview/view.jsx index 22f3812db..343424d5f 100644 --- a/ui/component/claimPreview/view.jsx +++ b/ui/component/claimPreview/view.jsx @@ -80,7 +80,7 @@ type Props = { repostUrl?: string, hideMenu?: boolean, isLivestream?: boolean, - live?: boolean, + isLivestreamActive: boolean, collectionId?: string, editCollection: (string, CollectionEditParams) => void, isCollectionMine: boolean, @@ -145,7 +145,7 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { hideMenu = false, // repostUrl, isLivestream, // need both? CHECK - live, + isLivestreamActive, collectionId, collectionIndex, editCollection, @@ -336,7 +336,7 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { } let liveProperty = null; - if (live === true) { + if (isLivestreamActive === true) { liveProperty = (claim) => <>LIVE; } @@ -349,7 +349,7 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { 'claim-preview__wrapper--channel': isChannelUri && type !== 'inline', 'claim-preview__wrapper--inline': type === 'inline', 'claim-preview__wrapper--small': type === 'small', - 'claim-preview__live': live, + 'claim-preview__live': isLivestreamActive, 'claim-preview__active': active, })} > @@ -386,7 +386,7 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { )} {/* @endif */} - {!isLivestream && ( + {(!isLivestream || isLivestreamActive) && (
diff --git a/ui/component/claimPreviewTile/index.js b/ui/component/claimPreviewTile/index.js index 6bfa3760d..e9372e4eb 100644 --- a/ui/component/claimPreviewTile/index.js +++ b/ui/component/claimPreviewTile/index.js @@ -13,6 +13,7 @@ import { } from 'lbry-redux'; import { selectMutedChannels } from 'redux/selectors/blocked'; import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc'; +import { makeSelectIsActiveLivestream } from 'redux/selectors/livestream'; import { selectShowMatureContent } from 'redux/selectors/settings'; import ClaimPreviewTile from './view'; import formatMediaDuration from 'util/formatMediaDuration'; @@ -36,6 +37,7 @@ const select = (state, props) => { showMature: selectShowMatureContent(state), isMature: makeSelectClaimIsNsfw(props.uri)(state), isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state), + isLivestreamActive: makeSelectIsActiveLivestream(props.uri)(state), }; }; diff --git a/ui/component/claimPreviewTile/view.jsx b/ui/component/claimPreviewTile/view.jsx index 1372e5918..235b86e4b 100644 --- a/ui/component/claimPreviewTile/view.jsx +++ b/ui/component/claimPreviewTile/view.jsx @@ -48,10 +48,10 @@ type Props = { showMature: boolean, showHiddenByUser?: boolean, properties?: (Claim) => void, - live?: boolean, collectionId?: string, showNoSourceClaims?: boolean, isLivestream: boolean, + isLivestreamActive: boolean, }; // preview image cards used in related video functionality @@ -75,9 +75,9 @@ function ClaimPreviewTile(props: Props) { showMature, showHiddenByUser, properties, - live, showNoSourceClaims, isLivestream, + isLivestreamActive, collectionId, mediaDuration, } = props; @@ -192,7 +192,7 @@ function ClaimPreviewTile(props: Props) { } let liveProperty = null; - if (live === true) { + if (isLivestreamActive === true) { liveProperty = (claim) => <>LIVE; } @@ -201,7 +201,7 @@ function ClaimPreviewTile(props: Props) { onClick={handleClick} className={classnames('card claim-preview--tile', { 'claim-preview__wrapper--channel': isChannel, - 'claim-preview__live': live, + 'claim-preview__live': isLivestreamActive, })} > -- 2.45.2 From 673d679083f96fb96e0ab04060104d77127c03f0 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Thu, 16 Sep 2021 21:23:40 +0800 Subject: [PATCH 04/15] =?UTF-8?q?=E2=8F=AA=20ClaimTilesDiscover:=20revert?= =?UTF-8?q?=20and=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Simplify - Simplify to just `uris` instead of having multiple arrays (`uris`, `modifiedUris`, `prevUris`) - The `prevUris` is for CLS prevention. With this removal, the CLS issue is back, but we'll handle it differently later. - Temporarily disable the view-count fetching. Code is left there so that I don't forget. ## Fix - `shouldPerformSearch` was never true when `prefixUris` is present. Corrected the logic. - Aside: prefix and pin is so similar in function. Hm .... --- ui/component/claimTilesDiscover/view.jsx | 77 +++++++++--------------- 1 file changed, 30 insertions(+), 47 deletions(-) diff --git a/ui/component/claimTilesDiscover/view.jsx b/ui/component/claimTilesDiscover/view.jsx index f22379a4a..cc3b60e03 100644 --- a/ui/component/claimTilesDiscover/view.jsx +++ b/ui/component/claimTilesDiscover/view.jsx @@ -90,8 +90,6 @@ function ClaimTilesDiscover(props: Props) { streamTypesParam = [CS.FILE_VIDEO, CS.FILE_AUDIO]; } - const [prevUris, setPrevUris] = React.useState([]); - const options: { page_size: number, no_totals: boolean, @@ -165,52 +163,27 @@ function ClaimTilesDiscover(props: Props) { } const searchKey = createNormalizedClaimSearchKey(options); - const isLoading = fetchingClaimSearchByQuery[searchKey]; - - let uris = (prefixUris || []).concat(claimSearchByQuery[searchKey] || []); + const fetchingClaimSearch = fetchingClaimSearchByQuery[searchKey]; + const claimSearchUris = claimSearchByQuery[searchKey] || []; // 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; + const shouldPerformSearch = !fetchingClaimSearch && claimSearchUris.length === 0; - if (prefixUris === undefined && claimSearchByQuery[searchKey] === 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 uris = (prefixUris || []).concat(claimSearchUris); - 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); + if (pinUrls && uris && uris.length > 2 && window.location.pathname === '/') { + pinUrls.forEach((pin) => { + if (uris.indexOf(pin) !== -1) { + uris.splice(uris.indexOf(pin), 1); } else { - modifiedUris.pop(); + uris.pop(); } }); - modifiedUris.splice(2, 0, ...fixUris); - } - - 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(',')); - } + uris.splice(2, 0, ...pinUrls); } + // Run `doClaimSearch` React.useEffect(() => { if (shouldPerformSearch) { const searchOptions = JSON.parse(optionsStringForEffect); @@ -218,24 +191,34 @@ function ClaimTilesDiscover(props: Props) { } }, [doClaimSearch, shouldPerformSearch, optionsStringForEffect]); + // Fetch view count for uris 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); + if (fetchViewCount && uris && uris.length > 0) { + const claimIds = []; + + uris.forEach((uri) => { + if (claimsByUri[uri]) { + claimIds.push(claimsByUri[uri].claim_id); + } + }); + + if (claimIds.length > 0) { + // TODO: this is a rough port. Need to only do this when necessary. + const TODO = true; + if (!TODO) { + doFetchViewCount(claimIds.join(',')); + } } } - }, [shouldPerformSearch, prevUris, uris]); // eslint-disable-line react-hooks/exhaustive-deps + }, [uris, fetchViewCount]); // ************************************************************************** // ************************************************************************** return (
    - {modifiedUris && modifiedUris.length - ? modifiedUris.map((uri) => ( + {uris && uris.length + ? uris.map((uri) => ( Date: Thu, 16 Sep 2021 21:48:18 +0800 Subject: [PATCH 05/15] ClaimTilesDiscover: factor out options ## Change Move the `option` code outside and passed in as a pre-calculated prop. ## Reason To skip rendering while waiting for `claim_search`, we need to add `React.memo(areEqual)`. However, the flag that determines if we are fetching `claim_search` (fetchingClaimSearchByQuery[]) depends on the derived options as the key. Instead of calculating `options` twice, we moved it to the props so both sides can use it. It also makes the component a bit more readable. The downside is that the prop-passing might not be clear. --- ui/component/claimTilesDiscover/index.js | 127 ++++++++++++++++++-- ui/component/claimTilesDiscover/view.jsx | 142 +++++------------------ 2 files changed, 147 insertions(+), 122 deletions(-) diff --git a/ui/component/claimTilesDiscover/index.js b/ui/component/claimTilesDiscover/index.js index a12963202..ebdcd3acb 100644 --- a/ui/component/claimTilesDiscover/index.js +++ b/ui/component/claimTilesDiscover/index.js @@ -1,27 +1,36 @@ import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; import { doClaimSearch, selectClaimSearchByQuery, selectFetchingClaimSearchByQuery, SETTINGS, selectClaimsByUri, + splitBySeparator, + MATURE_TAGS, } from 'lbry-redux'; import { doFetchViewCount } from 'lbryinc'; import { doToggleTagFollowDesktop } from 'redux/actions/tags'; import { makeSelectClientSetting, selectShowMatureContent } from 'redux/selectors/settings'; import { selectModerationBlockList } from 'redux/selectors/comments'; import { selectMutedChannels } from 'redux/selectors/blocked'; +import { ENABLE_NO_SOURCE_CLAIMS, SIMPLE_SITE } from 'config'; +import * as CS from 'constants/claim_search'; + import ClaimListDiscover from './view'; -const select = (state) => ({ - claimSearchByQuery: selectClaimSearchByQuery(state), - claimsByUri: selectClaimsByUri(state), - fetchingClaimSearchByQuery: selectFetchingClaimSearchByQuery(state), - showNsfw: selectShowMatureContent(state), - hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state), - mutedUris: selectMutedChannels(state), - blockedUris: selectModerationBlockList(state), -}); +const select = (state, props) => { + return { + claimSearchByQuery: selectClaimSearchByQuery(state), + claimsByUri: selectClaimsByUri(state), + fetchingClaimSearchByQuery: selectFetchingClaimSearchByQuery(state), + showNsfw: selectShowMatureContent(state), + hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state), + mutedUris: selectMutedChannels(state), + blockedUris: selectModerationBlockList(state), + options: resolveSearchOptions({ pageSize: 8, ...props }), + }; +}; const perform = { doClaimSearch, @@ -29,4 +38,102 @@ const perform = { doFetchViewCount, }; -export default connect(select, perform)(ClaimListDiscover); +export default withRouter(connect(select, perform)(ClaimListDiscover)); + +// **************************************************************************** +// **************************************************************************** + +function resolveSearchOptions(props) { + const { + pageSize, + claimType, + tags, + showNsfw, + languages, + channelIds, + mutedUris, + blockedUris, + orderBy, + streamTypes, + hasNoSource, + hasSource, + releaseTime, + feeAmount, + limitClaimsPerChannel, + hideReposts, + timestamp, + claimIds, + location, + } = props; + + const mutedAndBlockedChannelIds = Array.from( + new Set((mutedUris || []).concat(blockedUris || []).map((uri) => splitBySeparator(uri)[1])) + ); + + const urlParams = new URLSearchParams(location.search); + const feeAmountInUrl = urlParams.get('fee_amount'); + const feeAmountParam = feeAmountInUrl || feeAmount; + + let streamTypesParam; + if (streamTypes) { + streamTypesParam = streamTypes; + } else if (SIMPLE_SITE && !hasNoSource && streamTypes !== null) { + streamTypesParam = [CS.FILE_VIDEO, CS.FILE_AUDIO]; + } + + const options = { + 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; + } + + return options; +} diff --git a/ui/component/claimTilesDiscover/view.jsx b/ui/component/claimTilesDiscover/view.jsx index cc3b60e03..ce399f1c1 100644 --- a/ui/component/claimTilesDiscover/view.jsx +++ b/ui/component/claimTilesDiscover/view.jsx @@ -1,11 +1,32 @@ // @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 { createNormalizedClaimSearchKey } from 'lbry-redux'; import ClaimPreviewTile from 'component/claimPreviewTile'; -import { useHistory } from 'react-router'; + +type SearchOptions = { + 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, +}; + +// **************************************************************************** +// ClaimTilesDiscover +// **************************************************************************** type Props = { prefixUris?: Array, @@ -30,6 +51,7 @@ type Props = { hasSource?: boolean, hasNoSource?: boolean, // --- select --- + location: { search: string }, claimSearchByQuery: { [string]: Array }, claimsByUri: { [string]: any }, fetchingClaimSearchByQuery: { [string]: boolean }, @@ -37,6 +59,7 @@ type Props = { hideReposts: boolean, mutedUris: Array, blockedUris: Array, + options: SearchOptions, // --- perform --- doClaimSearch: ({}) => void, doFetchViewCount: (claimIdCsv: string) => void, @@ -47,121 +70,18 @@ function ClaimTilesDiscover(props: Props) { 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, pinUrls, prefixUris, showNoSourceClaims, doFetchViewCount, + pageSize = 8, + options, } = 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])) - ); - - let streamTypesParam; - if (streamTypes) { - streamTypesParam = streamTypes; - } else if (SIMPLE_SITE && !hasNoSource && streamTypes !== null) { - streamTypesParam = [CS.FILE_VIDEO, CS.FILE_AUDIO]; - } - - 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 searchKey = createNormalizedClaimSearchKey(options); const fetchingClaimSearch = fetchingClaimSearchByQuery[searchKey]; const claimSearchUris = claimSearchByQuery[searchKey] || []; @@ -212,9 +132,6 @@ function ClaimTilesDiscover(props: Props) { } }, [uris, fetchViewCount]); - // ************************************************************************** - // ************************************************************************** - return (
      {uris && uris.length @@ -234,4 +151,5 @@ function ClaimTilesDiscover(props: Props) {
    ); } + export default ClaimTilesDiscover; -- 2.45.2 From 6cfdff7eec2f70a7b8b95fcaed03b0a598a99823 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Thu, 16 Sep 2021 23:21:34 +0800 Subject: [PATCH 06/15] ClaimTilesDiscover: reduce ~17 renders at startup to just 2. --- ui/component/claimTilesDiscover/view.jsx | 85 +++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/ui/component/claimTilesDiscover/view.jsx b/ui/component/claimTilesDiscover/view.jsx index ce399f1c1..7d86f4256 100644 --- a/ui/component/claimTilesDiscover/view.jsx +++ b/ui/component/claimTilesDiscover/view.jsx @@ -24,6 +24,19 @@ type SearchOptions = { has_no_source?: boolean, }; +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 // **************************************************************************** @@ -152,4 +165,74 @@ function ClaimTilesDiscover(props: Props) { ); } -export default ClaimTilesDiscover; +export default React.memo(ClaimTilesDiscover, areEqual); + +function debug_trace(val) { + if (process.env.DEBUG_TRACE) console.log(`Render due to: ${val}`); +} + +function areEqual(prev: Props, next: Props) { + const prevOptions: SearchOptions = prev.options; + const nextOptions: SearchOptions = next.options; + + const prevSearchKey = createNormalizedClaimSearchKey(prevOptions); + const nextSearchKey = createNormalizedClaimSearchKey(nextOptions); + + // "Pause" render when fetching to solve the layout-shift problem in #5979 + // (previous solution used a stashed copy of the rendered uris while fetching + // to make it stay still). + // This version works as long as we are not doing anything during a fetch, + // such as showing a spinner. + const nextCsFetch = next.fetchingClaimSearchByQuery[nextSearchKey]; + if (nextCsFetch) { + return true; + } + + // --- Deep-compare --- + if (prev.claimSearchByQuery[prevSearchKey] !== next.claimSearchByQuery[nextSearchKey]) { + debug_trace('claimSearchByQuery'); + return false; + } + + if (prev.fetchingClaimSearchByQuery[prevSearchKey] !== next.fetchingClaimSearchByQuery[nextSearchKey]) { + debug_trace('fetchingClaimSearchByQuery'); + return false; + } + + const ARRAY_KEYS = ['prefixUris', 'channelIds', 'mutedUris', 'blockedUris']; + + for (let i = 0; i < ARRAY_KEYS.length; ++i) { + const key = ARRAY_KEYS[i]; + if (!urisEqual(prev[key], next[key])) { + debug_trace(`${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, + 'claimSearchByQuery', + 'fetchingClaimSearchByQuery', + 'location', + 'history', + 'match', + 'claimsByUri', + 'options', + 'doClaimSearch', + 'doToggleTagFollowDesktop', + ]; + + 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]) { + debug_trace(`${pk}`); + return false; + } + } + + return true; +} -- 2.45.2 From d47718da8e32fba6cae60a35d0c870eb7eb52c4d Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Fri, 17 Sep 2021 11:22:39 +0800 Subject: [PATCH 07/15] ClaimTilesDiscover: fill with placeholder while waiting for claim_search ## Issue Livestream claims are fetched seperately, so they might already exists. While claim_search is running, the list only consists of livestreams (collapsed). ## Fix Fill up the space with placeholders to prevent layout shift. --- ui/component/claimTilesDiscover/view.jsx | 30 +++++++++++++++++------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/ui/component/claimTilesDiscover/view.jsx b/ui/component/claimTilesDiscover/view.jsx index 7d86f4256..4e95412b6 100644 --- a/ui/component/claimTilesDiscover/view.jsx +++ b/ui/component/claimTilesDiscover/view.jsx @@ -116,6 +116,12 @@ function ClaimTilesDiscover(props: Props) { 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('')); + } + // Run `doClaimSearch` React.useEffect(() => { if (shouldPerformSearch) { @@ -130,7 +136,7 @@ function ClaimTilesDiscover(props: Props) { const claimIds = []; uris.forEach((uri) => { - if (claimsByUri[uri]) { + if (uri && claimsByUri[uri]) { claimIds.push(claimsByUri[uri].claim_id); } }); @@ -148,14 +154,20 @@ function ClaimTilesDiscover(props: Props) { return (
      {uris && uris.length - ? uris.map((uri) => ( - - )) + ? uris.map((uri, i) => { + if (uri) { + return ( + + ); + } else { + return ; + } + }) : new Array(pageSize) .fill(1) .map((x, i) => ( -- 2.45.2 From 1d8609d6495176d5a7f1d21a82e61f2679bfc600 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Fri, 17 Sep 2021 16:55:02 +0800 Subject: [PATCH 08/15] Add 'useFetchViewCount' to handle fetching from lists This effect also stashes fetched uris, so that we won't re-fetch the same uris during the same instance (e.g. during infinite scroll). --- ui/component/claimListDiscover/view.jsx | 25 +++--------------------- ui/component/claimTilesDiscover/view.jsx | 24 +++-------------------- ui/effects/use-fetch-view-count.js | 24 +++++++++++++++++++++++ 3 files changed, 30 insertions(+), 43 deletions(-) create mode 100644 ui/effects/use-fetch-view-count.js diff --git a/ui/component/claimListDiscover/view.jsx b/ui/component/claimListDiscover/view.jsx index cccbdf067..712ead5c8 100644 --- a/ui/component/claimListDiscover/view.jsx +++ b/ui/component/claimListDiscover/view.jsx @@ -13,6 +13,7 @@ import ClaimPreview from 'component/claimPreview'; import ClaimPreviewTile from 'component/claimPreviewTile'; import I18nMessage from 'component/i18nMessage'; import ClaimListHeader from 'component/claimListHeader'; +import useFetchViewCount from 'effects/use-fetch-view-count'; import { useIsLargeScreen } from 'effects/use-screensize'; type Props = { @@ -523,22 +524,6 @@ function ClaimListDiscover(props: Props) { } } - 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(',')); - } - } - function resolveOrderByOption(orderBy: string | Array, sortBy: string | Array) { const order_by = orderBy === CS.ORDER_BY_TRENDING @@ -557,6 +542,8 @@ function ClaimListDiscover(props: Props) { // ************************************************************************** // ************************************************************************** + useFetchViewCount(fetchViewCount, finalUris, claimsByUri, doFetchViewCount); + React.useEffect(() => { if (shouldPerformSearch) { const searchOptions = JSON.parse(optionsStringForEffect); @@ -579,12 +566,6 @@ function ClaimListDiscover(props: Props) { } }, [uris, claimSearchResult, finalUris, setFinalUris]); - React.useEffect(() => { - if (fetchViewCount) { - fetchViewCountForUris(finalUris); - } - }, [finalUris]); // eslint-disable-line react-hooks/exhaustive-deps - const headerToUse = header || ( { if (shouldPerformSearch) { @@ -130,27 +133,6 @@ function ClaimTilesDiscover(props: Props) { } }, [doClaimSearch, shouldPerformSearch, optionsStringForEffect]); - // Fetch view count for uris - React.useEffect(() => { - if (fetchViewCount && uris && uris.length > 0) { - const claimIds = []; - - uris.forEach((uri) => { - if (uri && claimsByUri[uri]) { - claimIds.push(claimsByUri[uri].claim_id); - } - }); - - if (claimIds.length > 0) { - // TODO: this is a rough port. Need to only do this when necessary. - const TODO = true; - if (!TODO) { - doFetchViewCount(claimIds.join(',')); - } - } - } - }, [uris, fetchViewCount]); - return (
        {uris && uris.length diff --git a/ui/effects/use-fetch-view-count.js b/ui/effects/use-fetch-view-count.js new file mode 100644 index 000000000..b4f8b0892 --- /dev/null +++ b/ui/effects/use-fetch-view-count.js @@ -0,0 +1,24 @@ +// @flow +import { useState, useEffect } from 'react'; + +export default function useFetchViewCount( + shouldFetch: ?boolean, + uris: Array, + claimsByUri: any, + doFetchViewCount: (string) => void +) { + const [fetchedUris, setFetchedUris] = useState([]); + + useEffect(() => { + if (shouldFetch && uris && uris.length > 0) { + const urisToFetch = uris.filter((uri) => uri && !fetchedUris.includes(uri) && Boolean(claimsByUri[uri])); + + if (urisToFetch.length > 0) { + const claimIds = urisToFetch.map((uri) => claimsByUri[uri].claim_id); + doFetchViewCount(claimIds.join(',')); + setFetchedUris([...fetchedUris, ...urisToFetch]); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [uris]); +} -- 2.45.2 From ad158bcd935d71280fb546f8ae0959cfcd5bacab Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Fri, 17 Sep 2021 15:55:18 +0800 Subject: [PATCH 09/15] =?UTF-8?q?=E2=8F=AA=20ClaimListDiscover:=20revert?= =?UTF-8?q?=20and=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Revert - Removed the 'finalUris' stuff that was meant to "pause" visual changes when fetching. I think it'll be cleaner to use React.memo to achieve that. ## Alterations - Added `renderUri` to make it clear which array that this component will render. - Re-do the way we fetch view counts now that 'finalUris' is gone. Not the best method, but at least correct for now. --- ui/component/claimListDiscover/view.jsx | 51 ++++--------------------- 1 file changed, 8 insertions(+), 43 deletions(-) diff --git a/ui/component/claimListDiscover/view.jsx b/ui/component/claimListDiscover/view.jsx index 712ead5c8..a0d85f29a 100644 --- a/ui/component/claimListDiscover/view.jsx +++ b/ui/component/claimListDiscover/view.jsx @@ -376,9 +376,9 @@ function ClaimListDiscover(props: Props) { const hasMatureTags = tagsParam && tagsParam.split(',').some((t) => MATURE_TAGS.includes(t)); - const mainSearchKey = createNormalizedClaimSearchKey(options); - let claimSearchResult = claimSearchByQuery[mainSearchKey]; - const claimSearchResultLastPageReached = claimSearchByQueryLastPageReached[mainSearchKey]; + const searchKey = createNormalizedClaimSearchKey(options); + const claimSearchResult = claimSearchByQuery[searchKey]; + const claimSearchResultLastPageReached = claimSearchByQueryLastPageReached[searchKey]; // uncomment to fix an item on a page // const fixUri = 'lbry://@corbettreport#0/lbryodysee#5'; @@ -396,9 +396,6 @@ function ClaimListDiscover(props: Props) { // claimSearchResult.splice(2, 0, fixUri); // } - const [finalUris, setFinalUris] = React.useState( - getFinalUrisInitialState(history.action === 'POP', claimSearchResult) - ); const [prevOptions, setPrevOptions] = React.useState(null); if (!isJustScrollingToNewPage(prevOptions, options)) { @@ -460,6 +457,8 @@ function ClaimListDiscover(props: Props) { ); + const renderUris = uris || claimSearchResult; + // ************************************************************************** // Helpers // ************************************************************************** @@ -505,25 +504,6 @@ function ClaimListDiscover(props: Props) { } } - function urisEqual(prev: Array, next: Array) { - if (!prev || !next) { - // From '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; - } - return prev.length === next.length && prev.every((value, index) => value === next[index]); - } - - function getFinalUrisInitialState(isNavigatingBack, claimSearchResult) { - if (isNavigatingBack && claimSearchResult && claimSearchResult.length > 0) { - return claimSearchResult; - } else { - return []; - } - } - function resolveOrderByOption(orderBy: string | Array, sortBy: string | Array) { const order_by = orderBy === CS.ORDER_BY_TRENDING @@ -542,7 +522,7 @@ function ClaimListDiscover(props: Props) { // ************************************************************************** // ************************************************************************** - useFetchViewCount(fetchViewCount, finalUris, claimsByUri, doFetchViewCount); + useFetchViewCount(fetchViewCount, renderUris, claimsByUri, doFetchViewCount); React.useEffect(() => { if (shouldPerformSearch) { @@ -551,21 +531,6 @@ function ClaimListDiscover(props: Props) { } }, [doClaimSearch, shouldPerformSearch, optionsStringForEffect, forceRefresh]); - // Resolve 'finalUri' - React.useEffect(() => { - if (uris) { - if (!urisEqual(uris, finalUris)) { - setFinalUris(uris); - } - } else { - // Wait until all queries are done before updating the uris to avoid layout shifts. - const pending = claimSearchResult === undefined; - if (!pending && !urisEqual(claimSearchResult, finalUris)) { - setFinalUris(claimSearchResult); - } - } - }, [uris, claimSearchResult, finalUris, setFinalUris]); - const headerToUse = header || ( Date: Fri, 17 Sep 2021 17:20:39 +0800 Subject: [PATCH 10/15] ClaimListDiscover: add prefixUris, similar to ClaimTilesDiscover This will be initially used to append livestreams at the top. --- ui/component/claimList/view.jsx | 11 +++++++++-- ui/component/claimListDiscover/view.jsx | 4 ++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/ui/component/claimList/view.jsx b/ui/component/claimList/view.jsx index 30153d28f..bdfd5450a 100644 --- a/ui/component/claimList/view.jsx +++ b/ui/component/claimList/view.jsx @@ -16,6 +16,7 @@ const SORT_OLD = 'old'; type Props = { uris: Array, + prefixUris?: Array, header: Node | boolean, headerAltControls: Node, loading: boolean, @@ -49,6 +50,7 @@ export default function ClaimList(props: Props) { const { activeUri, uris, + prefixUris, headerAltControls, loading, persistedStorageKey, @@ -75,10 +77,15 @@ export default function ClaimList(props: Props) { } = props; const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW); + + // Exclude prefix uris in these results variables. We don't want to show + // anything if the search failed or timed out. const timedOut = uris === null; const urisLength = (uris && uris.length) || 0; - const sortedUris = (urisLength > 0 && (currentSort === SORT_NEW ? uris : uris.slice().reverse())) || []; + const tileUris = (prefixUris || []).concat(uris); + const sortedUris = (urisLength > 0 && (currentSort === SORT_NEW ? tileUris : tileUris.slice().reverse())) || []; + const noResultMsg = searchInLanguage ? __('No results. Contents may be hidden by the Language filter.') : __('No results'); @@ -117,7 +124,7 @@ export default function ClaimList(props: Props) { return tileLayout && !header ? (
        {urisLength > 0 && - uris.map((uri) => ( + tileUris.map((uri) => ( , + prefixUris?: Array, name?: string, type: string, pageSize?: number, @@ -135,6 +136,7 @@ function ClaimListDiscover(props: Props) { injectedItem, feeAmount, uris, + prefixUris, tileLayout, hideFilters = false, claimIds, @@ -569,6 +571,7 @@ function ClaimListDiscover(props: Props) { tileLayout loading={loading} uris={renderUris} + prefixUris={prefixUris} onScrollBottom={handleScrollBottom} page={page} pageSize={dynamicPageSize} @@ -601,6 +604,7 @@ function ClaimListDiscover(props: Props) { type={type} loading={loading} uris={renderUris} + prefixUris={prefixUris} onScrollBottom={handleScrollBottom} page={page} pageSize={dynamicPageSize} -- 2.45.2 From dcb44e647ddcf6212d8881785d6861d1150b14dc Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Fri, 17 Sep 2021 17:21:10 +0800 Subject: [PATCH 11/15] =?UTF-8?q?=E2=9C=85=20Re-enable=20active=20livestre?= =?UTF-8?q?am=20tiles=20using=20the=20new=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/page/channelsFollowing/index.js | 9 +++++++-- ui/page/channelsFollowing/view.jsx | 14 ++++++++++++-- ui/page/discover/index.js | 4 ++++ ui/page/discover/view.jsx | 16 +++++++++++++--- ui/page/home/index.js | 7 ++++++- ui/page/home/view.jsx | 26 ++++++++++++++++++++++++-- 6 files changed, 66 insertions(+), 10 deletions(-) diff --git a/ui/page/channelsFollowing/index.js b/ui/page/channelsFollowing/index.js index 599cbf841..c08a2107f 100644 --- a/ui/page/channelsFollowing/index.js +++ b/ui/page/channelsFollowing/index.js @@ -1,13 +1,18 @@ import { connect } from 'react-redux'; import { SETTINGS } from 'lbry-redux'; +import { doFetchActiveLivestreams } from 'redux/actions/livestream'; +import { selectActiveLivestreams } from 'redux/selectors/livestream'; import { selectSubscriptions } from 'redux/selectors/subscriptions'; import { makeSelectClientSetting } from 'redux/selectors/settings'; import ChannelsFollowingPage from './view'; -const select = state => ({ +const select = (state) => ({ subscribedChannels: selectSubscriptions(state), tileLayout: makeSelectClientSetting(SETTINGS.TILE_LAYOUT)(state), + activeLivestreams: selectActiveLivestreams(state), }); -export default connect(select)(ChannelsFollowingPage); +export default connect(select, { + doFetchActiveLivestreams, +})(ChannelsFollowingPage); diff --git a/ui/page/channelsFollowing/view.jsx b/ui/page/channelsFollowing/view.jsx index 87bb5423f..c7f9d7222 100644 --- a/ui/page/channelsFollowing/view.jsx +++ b/ui/page/channelsFollowing/view.jsx @@ -10,21 +10,31 @@ import Page from 'component/page'; import Button from 'component/button'; import Icon from 'component/common/icon'; import { splitBySeparator } from 'lbry-redux'; +import { getLivestreamUris } from 'util/livestream'; type Props = { subscribedChannels: Array, tileLayout: boolean, + activeLivestreams: ?LivestreamInfo, + doFetchActiveLivestreams: () => void, }; function ChannelsFollowingPage(props: Props) { - const { subscribedChannels, tileLayout } = props; + const { subscribedChannels, tileLayout, activeLivestreams, doFetchActiveLivestreams } = props; + const hasSubsribedChannels = subscribedChannels.length > 0; + const channelIds = subscribedChannels.map((sub) => splitBySeparator(sub.uri)[1]); + + React.useEffect(() => { + doFetchActiveLivestreams(); + }, []); return !hasSubsribedChannels ? ( ) : ( } defaultOrderBy={CS.ORDER_BY_NEW} - channelIds={subscribedChannels.map((sub) => splitBySeparator(sub.uri)[1])} + channelIds={channelIds} meta={
      ); + const claimTiles = ( - + ); return ( @@ -86,6 +104,10 @@ function HomePage(props: Props) { ); } + React.useEffect(() => { + doFetchActiveLivestreams(); + }, []); + return ( {!SIMPLE_SITE && (authenticated || !IS_WEB) && !subscribedChannels.length && ( -- 2.45.2 From 8340783e043fc4b10c73beaa23a03879639ea1f3 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Mon, 20 Sep 2021 10:30:23 +0800 Subject: [PATCH 12/15] doFetchActiveLivestreams: add interval check - Added a default minimum of 5 minutes between fetches. Clients can bypass this through `forceFetch` if needed. --- flow-typed/livestream.js | 1 + ui/constants/action_types.js | 1 + ui/redux/actions/livestream.js | 36 +++++++++++++++++++-------------- ui/redux/reducers/livestream.js | 10 +++++++-- 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/flow-typed/livestream.js b/flow-typed/livestream.js index 63f1dd6a7..29743f06b 100644 --- a/flow-typed/livestream.js +++ b/flow-typed/livestream.js @@ -26,6 +26,7 @@ declare type LivestreamState = { viewersById: {}, fetchingActiveLivestreams: boolean, activeLivestreams: ?LivestreamInfo, + lastFetchedActiveLivestreams: number, } declare type LivestreamInfo = { diff --git a/ui/constants/action_types.js b/ui/constants/action_types.js index 6baee5377..17748007e 100644 --- a/ui/constants/action_types.js +++ b/ui/constants/action_types.js @@ -354,4 +354,5 @@ export const FETCH_NO_SOURCE_CLAIMS_FAILED = 'FETCH_NO_SOURCE_CLAIMS_FAILED'; export const VIEWERS_RECEIVED = 'VIEWERS_RECEIVED'; export const FETCH_ACTIVE_LIVESTREAMS_STARTED = 'FETCH_ACTIVE_LIVESTREAMS_STARTED'; export const FETCH_ACTIVE_LIVESTREAMS_FAILED = 'FETCH_ACTIVE_LIVESTREAMS_FAILED'; +export const FETCH_ACTIVE_LIVESTREAMS_SKIPPED = 'FETCH_ACTIVE_LIVESTREAMS_SKIPPED'; export const FETCH_ACTIVE_LIVESTREAMS_COMPLETED = 'FETCH_ACTIVE_LIVESTREAMS_COMPLETED'; diff --git a/ui/redux/actions/livestream.js b/ui/redux/actions/livestream.js index 9ab7051a4..1674a0f2d 100644 --- a/ui/redux/actions/livestream.js +++ b/ui/redux/actions/livestream.js @@ -33,19 +33,26 @@ export const doFetchNoSourceClaims = (channelId: string) => async (dispatch: Dis } }; -export const doFetchActiveLivestreams = () => { - return async (dispatch: Dispatch) => { - dispatch({ - type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_STARTED, - }); +const FETCH_ACTIVE_LIVESTREAMS_MIN_INTERVAL_MS = 5 * 60 * 1000; + +export const doFetchActiveLivestreams = (forceFetch: boolean) => { + return async (dispatch: Dispatch, getState: GetState) => { + const state = getState(); + const now = Date.now(); + const timeDelta = now - state.livestream.lastFetchedActiveLivestreams; + + if (!forceFetch && timeDelta < FETCH_ACTIVE_LIVESTREAMS_MIN_INTERVAL_MS) { + dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_SKIPPED }); + return; + } + + dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_STARTED }); fetch(LIVESTREAM_LIVE_API) .then((res) => res.json()) .then((res) => { if (!res.data) { - dispatch({ - type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED, - }); + dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED }); return; } @@ -88,19 +95,18 @@ export const doFetchActiveLivestreams = () => { dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_COMPLETED, - data: activeLivestreams, + data: { + activeLivestreams, + lastFetchedActiveLivestreams: now, + }, }); }) .catch(() => { - dispatch({ - type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED, - }); + dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED }); }); }) .catch((err) => { - dispatch({ - type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED, - }); + dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED }); }); }; }; diff --git a/ui/redux/reducers/livestream.js b/ui/redux/reducers/livestream.js index 932be2cb9..fe89b618b 100644 --- a/ui/redux/reducers/livestream.js +++ b/ui/redux/reducers/livestream.js @@ -7,6 +7,7 @@ const defaultState: LivestreamState = { viewersById: {}, fetchingActiveLivestreams: false, activeLivestreams: null, + lastFetchedActiveLivestreams: 0, }; export default handleActions( @@ -45,8 +46,13 @@ export default handleActions( return { ...state, fetchingActiveLivestreams: false }; }, [ACTIONS.FETCH_ACTIVE_LIVESTREAMS_COMPLETED]: (state: LivestreamState, action: any) => { - const activeLivestreams: LivestreamInfo = action.data; - return { ...state, fetchingActiveLivestreams: false, activeLivestreams }; + const { activeLivestreams, lastFetchedActiveLivestreams } = action.data; + return { + ...state, + fetchingActiveLivestreams: false, + activeLivestreams, + lastFetchedActiveLivestreams, + }; }, }, defaultState -- 2.45.2 From 39c7159f2e023c382e0f441fe1b39e48a6b40811 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Thu, 23 Sep 2021 16:33:19 +0800 Subject: [PATCH 13/15] doFetchActiveLivestreams: add option check We'll need to support different 'orderBy', so adding an "options check" when determining if we just made the same fetch. --- flow-typed/livestream.js | 3 ++- ui/redux/actions/livestream.js | 21 +++++++++++++++------ ui/redux/reducers/livestream.js | 8 +++++--- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/flow-typed/livestream.js b/flow-typed/livestream.js index 29743f06b..9bee62676 100644 --- a/flow-typed/livestream.js +++ b/flow-typed/livestream.js @@ -26,7 +26,8 @@ declare type LivestreamState = { viewersById: {}, fetchingActiveLivestreams: boolean, activeLivestreams: ?LivestreamInfo, - lastFetchedActiveLivestreams: number, + activeLivestreamsLastFetchedDate: number, + activeLivestreamsLastFetchedOptions: {}, } declare type LivestreamInfo = { diff --git a/ui/redux/actions/livestream.js b/ui/redux/actions/livestream.js index 1674a0f2d..3f94b5a4c 100644 --- a/ui/redux/actions/livestream.js +++ b/ui/redux/actions/livestream.js @@ -35,13 +35,21 @@ export const doFetchNoSourceClaims = (channelId: string) => async (dispatch: Dis const FETCH_ACTIVE_LIVESTREAMS_MIN_INTERVAL_MS = 5 * 60 * 1000; -export const doFetchActiveLivestreams = (forceFetch: boolean) => { +export const doFetchActiveLivestreams = ( + orderBy: Array = ['release_time'], + pageSize: number = 50, + forceFetch: boolean = false +) => { return async (dispatch: Dispatch, getState: GetState) => { const state = getState(); const now = Date.now(); - const timeDelta = now - state.livestream.lastFetchedActiveLivestreams; + const timeDelta = now - state.livestream.activeLivestreamsLastFetchedDate; - if (!forceFetch && timeDelta < FETCH_ACTIVE_LIVESTREAMS_MIN_INTERVAL_MS) { + const prevOptions = state.livestream.activeLivestreamsLastFetchedOptions; + const nextOptions = { page_size: pageSize, order_by: orderBy }; + const sameOptions = JSON.stringify(prevOptions) === JSON.stringify(nextOptions); + + if (!forceFetch && sameOptions && timeDelta < FETCH_ACTIVE_LIVESTREAMS_MIN_INTERVAL_MS) { dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_SKIPPED }); return; } @@ -71,11 +79,11 @@ export const doFetchActiveLivestreams = (forceFetch: boolean) => { // live. The UI usually just wants to report the latest claim, so we // query that store it in `latestClaimUri`. doClaimSearch({ - page_size: 50, + page_size: nextOptions.page_size, has_no_source: true, channel_ids: Object.keys(activeLivestreams), claim_type: ['stream'], - order_by: ['release_time'], // ** + order_by: nextOptions.order_by, // ** limit_claims_per_channel: 1, // ** no_totals: true, }) @@ -97,7 +105,8 @@ export const doFetchActiveLivestreams = (forceFetch: boolean) => { type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_COMPLETED, data: { activeLivestreams, - lastFetchedActiveLivestreams: now, + activeLivestreamsLastFetchedDate: now, + activeLivestreamsLastFetchedOptions: nextOptions, }, }); }) diff --git a/ui/redux/reducers/livestream.js b/ui/redux/reducers/livestream.js index fe89b618b..7a5d8af84 100644 --- a/ui/redux/reducers/livestream.js +++ b/ui/redux/reducers/livestream.js @@ -7,7 +7,8 @@ const defaultState: LivestreamState = { viewersById: {}, fetchingActiveLivestreams: false, activeLivestreams: null, - lastFetchedActiveLivestreams: 0, + activeLivestreamsLastFetchedDate: 0, + activeLivestreamsLastFetchedOptions: {}, }; export default handleActions( @@ -46,12 +47,13 @@ export default handleActions( return { ...state, fetchingActiveLivestreams: false }; }, [ACTIONS.FETCH_ACTIVE_LIVESTREAMS_COMPLETED]: (state: LivestreamState, action: any) => { - const { activeLivestreams, lastFetchedActiveLivestreams } = action.data; + const { activeLivestreams, activeLivestreamsLastFetchedDate, activeLivestreamsLastFetchedOptions } = action.data; return { ...state, fetchingActiveLivestreams: false, activeLivestreams, - lastFetchedActiveLivestreams, + activeLivestreamsLastFetchedDate, + activeLivestreamsLastFetchedOptions, }; }, }, -- 2.45.2 From 111dc695ca84564666eb74bc4c08463274ff5b1f Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Thu, 23 Sep 2021 16:38:16 +0800 Subject: [PATCH 14/15] WildWest: limit livestream tiles + add ability to show more Most likely this behavior will change in the future, so we'll leave `ClaimListDiscover` untouched and handle the logic at the page level. This solution uses 2 `ClaimListDiscover` -- if the reduced livestream list is visible, it handles the header; else the normal list handles the header. --- ui/page/discover/view.jsx | 109 ++++++++++++++++++++--------- ui/scss/component/_livestream.scss | 6 ++ 2 files changed, 80 insertions(+), 35 deletions(-) diff --git a/ui/page/discover/view.jsx b/ui/page/discover/view.jsx index a88e34666..d8d59a280 100644 --- a/ui/page/discover/view.jsx +++ b/ui/page/discover/view.jsx @@ -18,6 +18,8 @@ import I18nMessage from 'component/i18nMessage'; import moment from 'moment'; import { getLivestreamUris } from 'util/livestream'; +const INITIAL_LIVESTREAM_TILE_LIMIT = 8; + type Props = { location: { search: string }, followedTags: Array, @@ -29,7 +31,7 @@ type Props = { dynamicRouteProps: RowDataItem, tileLayout: boolean, activeLivestreams: ?LivestreamInfo, - doFetchActiveLivestreams: () => void, + doFetchActiveLivestreams: (orderBy?: Array, pageSize?: number, forceFetch?: boolean) => void, }; function DiscoverPage(props: Props) { @@ -70,6 +72,40 @@ function DiscoverPage(props: Props) { label = __('Unfollow'); } + const [showViewMoreLivestreams, setShowViewMoreLivestreams] = React.useState(!dynamicRouteProps); + const livestreamUris = getLivestreamUris(activeLivestreams, channelIds); + const useDualList = showViewMoreLivestreams && livestreamUris.length > INITIAL_LIVESTREAM_TILE_LIMIT; + + function getElemMeta() { + return !dynamicRouteProps ? ( + + , + }} + > + Results boosted by %lbc% + + + ) : ( + tag && !isMobile && ( +