Livestream category improvements (#7115)
* ❌ Remove old method of displaying active livestreams Completely remove it for now to make the commit deltas clearer. We'll replace it with the new method at the end. * Fetch and store active-livestream info in redux * Tiles can now query active-livestream state from redux instead of getting from parent. * ⏪ ClaimTilesDiscover: revert and cleanup ## 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 .... * 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. * ClaimTilesDiscover: reduce ~17 renders at startup to just 2. * 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. * 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). * ⏪ ClaimListDiscover: revert and cleanup ## 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. * ClaimListDiscover: add prefixUris, similar to ClaimTilesDiscover This will be initially used to append livestreams at the top. * ✅ Re-enable active livestream tiles using the new method * doFetchActiveLivestreams: add interval check - Added a default minimum of 5 minutes between fetches. Clients can bypass this through `forceFetch` if needed. * 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. * 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. * Use better tile-count on larger screens. Used the same method as how the homepage does it.
This commit is contained in:
parent
b78899d62c
commit
3b47edc3b9
25 changed files with 631 additions and 528 deletions
14
flow-typed/livestream.js
vendored
14
flow-typed/livestream.js
vendored
|
@ -24,4 +24,18 @@ declare type LivestreamReplayData = Array<LivestreamReplayItem>;
|
|||
declare type LivestreamState = {
|
||||
fetchingById: {},
|
||||
viewersById: {},
|
||||
fetchingActiveLivestreams: boolean,
|
||||
activeLivestreams: ?LivestreamInfo,
|
||||
activeLivestreamsLastFetchedDate: number,
|
||||
activeLivestreamsLastFetchedOptions: {},
|
||||
}
|
||||
|
||||
declare type LivestreamInfo = {
|
||||
[/* creatorId */ string]: {
|
||||
live: boolean,
|
||||
viewCount: number,
|
||||
creatorId: string,
|
||||
latestClaimId: string,
|
||||
latestClaimUri: string,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
@ -17,6 +16,7 @@ const SORT_OLD = 'old';
|
|||
|
||||
type Props = {
|
||||
uris: Array<string>,
|
||||
prefixUris?: Array<string>,
|
||||
header: Node | boolean,
|
||||
headerAltControls: Node,
|
||||
loading: boolean,
|
||||
|
@ -41,9 +41,6 @@ type Props = {
|
|||
hideMenu?: boolean,
|
||||
claimSearchByQuery: { [string]: Array<string> },
|
||||
claimsByUri: { [string]: any },
|
||||
liveLivestreamsFirst?: boolean,
|
||||
livestreamMap?: { [string]: any },
|
||||
searchOptions?: any,
|
||||
collectionId?: string,
|
||||
showNoSourceClaims?: boolean,
|
||||
onClick?: (e: any, claim?: ?Claim, index?: number) => void,
|
||||
|
@ -53,6 +50,7 @@ export default function ClaimList(props: Props) {
|
|||
const {
|
||||
activeUri,
|
||||
uris,
|
||||
prefixUris,
|
||||
headerAltControls,
|
||||
loading,
|
||||
persistedStorageKey,
|
||||
|
@ -73,37 +71,25 @@ export default function ClaimList(props: Props) {
|
|||
renderProperties,
|
||||
searchInLanguage,
|
||||
hideMenu,
|
||||
claimSearchByQuery,
|
||||
claimsByUri,
|
||||
liveLivestreamsFirst,
|
||||
livestreamMap,
|
||||
searchOptions,
|
||||
collectionId,
|
||||
showNoSourceClaims,
|
||||
onClick,
|
||||
} = 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 liveUris = [];
|
||||
if (liveLivestreamsFirst && livestreamMap) {
|
||||
prioritizeActiveLivestreams(uris, liveUris, livestreamMap, claimsByUri, claimSearchByQuery, searchOptions);
|
||||
}
|
||||
const tileUris = (prefixUris || []).concat(uris);
|
||||
const sortedUris = (urisLength > 0 && (currentSort === SORT_NEW ? tileUris : tileUris.slice().reverse())) || [];
|
||||
|
||||
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 +124,12 @@ export default function ClaimList(props: Props) {
|
|||
return tileLayout && !header ? (
|
||||
<section className="claim-grid">
|
||||
{urisLength > 0 &&
|
||||
uris.map((uri, index) => (
|
||||
tileUris.map((uri) => (
|
||||
<ClaimPreviewTile
|
||||
key={uri}
|
||||
uri={uri}
|
||||
showHiddenByUser={showHiddenByUser}
|
||||
properties={renderProperties}
|
||||
live={resolveLive(index)}
|
||||
collectionId={collectionId}
|
||||
showNoSourceClaims={showNoSourceClaims}
|
||||
/>
|
||||
|
@ -216,7 +201,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)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
|
|
|
@ -13,11 +13,12 @@ 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';
|
||||
import { getLivestreamOnlyOptions } from 'util/search';
|
||||
|
||||
type Props = {
|
||||
uris: Array<string>,
|
||||
prefixUris?: Array<string>,
|
||||
name?: string,
|
||||
type: string,
|
||||
pageSize?: number,
|
||||
|
@ -31,7 +32,6 @@ type Props = {
|
|||
includeSupportAction?: boolean,
|
||||
infiniteScroll?: Boolean,
|
||||
isChannel?: boolean,
|
||||
liveLivestreamsFirst?: boolean,
|
||||
personalView: boolean,
|
||||
showHeader: boolean,
|
||||
showHiddenByUser?: boolean,
|
||||
|
@ -64,7 +64,6 @@ type Props = {
|
|||
channelIds?: Array<string>,
|
||||
claimIds?: Array<string>,
|
||||
subscribedChannels: Array<Subscription>,
|
||||
livestreamMap?: { [string]: any },
|
||||
|
||||
header?: Node,
|
||||
headerLabel?: string | Node,
|
||||
|
@ -137,6 +136,7 @@ function ClaimListDiscover(props: Props) {
|
|||
injectedItem,
|
||||
feeAmount,
|
||||
uris,
|
||||
prefixUris,
|
||||
tileLayout,
|
||||
hideFilters = false,
|
||||
claimIds,
|
||||
|
@ -148,8 +148,6 @@ function ClaimListDiscover(props: Props) {
|
|||
releaseTime,
|
||||
scrollAnchor,
|
||||
showHiddenByUser = false,
|
||||
liveLivestreamsFirst,
|
||||
livestreamMap,
|
||||
hasSource,
|
||||
hasNoSource,
|
||||
isChannel = false,
|
||||
|
@ -380,9 +378,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';
|
||||
|
@ -400,14 +398,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)
|
||||
);
|
||||
const [prevOptions, setPrevOptions] = React.useState(null);
|
||||
|
||||
if (!isJustScrollingToNewPage(prevOptions, options)) {
|
||||
|
@ -469,6 +459,8 @@ function ClaimListDiscover(props: Props) {
|
|||
</div>
|
||||
);
|
||||
|
||||
const renderUris = uris || claimSearchResult;
|
||||
|
||||
// **************************************************************************
|
||||
// Helpers
|
||||
// **************************************************************************
|
||||
|
@ -514,41 +506,6 @@ function ClaimListDiscover(props: Props) {
|
|||
}
|
||||
}
|
||||
|
||||
function urisEqual(prev: Array<string>, next: Array<string>) {
|
||||
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 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<string>, sortBy: string | Array<string>) {
|
||||
const order_by =
|
||||
orderBy === CS.ORDER_BY_TRENDING
|
||||
|
@ -567,38 +524,15 @@ function ClaimListDiscover(props: Props) {
|
|||
// **************************************************************************
|
||||
// **************************************************************************
|
||||
|
||||
useFetchViewCount(fetchViewCount, renderUris, claimsByUri, doFetchViewCount);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (shouldPerformSearch) {
|
||||
const searchOptions = JSON.parse(optionsStringForEffect);
|
||||
doClaimSearch(searchOptions);
|
||||
|
||||
if (liveLivestreamsFirst && options.page === 1) {
|
||||
doClaimSearch(getLivestreamOnlyOptions(searchOptions));
|
||||
}
|
||||
}
|
||||
}, [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 || (liveLivestreamsFirst && livestreamSearchResult === undefined);
|
||||
if (!pending && !urisEqual(claimSearchResult, finalUris)) {
|
||||
setFinalUris(claimSearchResult);
|
||||
}
|
||||
}
|
||||
}, [uris, claimSearchResult, finalUris, setFinalUris, liveLivestreamsFirst, livestreamSearchResult]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (fetchViewCount) {
|
||||
fetchViewCountForUris(finalUris);
|
||||
}
|
||||
}, [finalUris]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const headerToUse = header || (
|
||||
<ClaimListHeader
|
||||
channelIds={channelIds}
|
||||
|
@ -636,7 +570,8 @@ function ClaimListDiscover(props: Props) {
|
|||
<ClaimList
|
||||
tileLayout
|
||||
loading={loading}
|
||||
uris={finalUris}
|
||||
uris={renderUris}
|
||||
prefixUris={prefixUris}
|
||||
onScrollBottom={handleScrollBottom}
|
||||
page={page}
|
||||
pageSize={dynamicPageSize}
|
||||
|
@ -645,8 +580,6 @@ function ClaimListDiscover(props: Props) {
|
|||
includeSupportAction={includeSupportAction}
|
||||
injectedItem={injectedItem}
|
||||
showHiddenByUser={showHiddenByUser}
|
||||
liveLivestreamsFirst={liveLivestreamsFirst}
|
||||
livestreamMap={livestreamMap}
|
||||
searchOptions={options}
|
||||
showNoSourceClaims={showNoSourceClaims}
|
||||
empty={empty}
|
||||
|
@ -670,7 +603,8 @@ function ClaimListDiscover(props: Props) {
|
|||
<ClaimList
|
||||
type={type}
|
||||
loading={loading}
|
||||
uris={finalUris}
|
||||
uris={renderUris}
|
||||
prefixUris={prefixUris}
|
||||
onScrollBottom={handleScrollBottom}
|
||||
page={page}
|
||||
pageSize={dynamicPageSize}
|
||||
|
@ -679,8 +613,6 @@ function ClaimListDiscover(props: Props) {
|
|||
includeSupportAction={includeSupportAction}
|
||||
injectedItem={injectedItem}
|
||||
showHiddenByUser={showHiddenByUser}
|
||||
liveLivestreamsFirst={liveLivestreamsFirst}
|
||||
livestreamMap={livestreamMap}
|
||||
searchOptions={options}
|
||||
showNoSourceClaims={hasNoSource || showNoSourceClaims}
|
||||
empty={empty}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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<any, {}>((props: Props, ref: any) => {
|
|||
hideMenu = false,
|
||||
// repostUrl,
|
||||
isLivestream, // need both? CHECK
|
||||
live,
|
||||
isLivestreamActive,
|
||||
collectionId,
|
||||
collectionIndex,
|
||||
editCollection,
|
||||
|
@ -336,7 +336,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
}
|
||||
|
||||
let liveProperty = null;
|
||||
if (live === true) {
|
||||
if (isLivestreamActive === true) {
|
||||
liveProperty = (claim) => <>LIVE</>;
|
||||
}
|
||||
|
||||
|
@ -349,7 +349,7 @@ const ClaimPreview = forwardRef<any, {}>((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<any, {}>((props: Props, ref: any) => {
|
|||
)}
|
||||
</div>
|
||||
{/* @endif */}
|
||||
{!isLivestream && (
|
||||
{(!isLivestream || isLivestreamActive) && (
|
||||
<div className="claim-preview__file-property-overlay">
|
||||
<PreviewOverlayProperties uri={uri} small={type === 'small'} properties={liveProperty} />
|
||||
</div>
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
})}
|
||||
>
|
||||
<NavLink {...navLinkProps} role="none" tabIndex={-1} aria-hidden>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,83 +1,41 @@
|
|||
// @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';
|
||||
import { getLivestreamOnlyOptions } from 'util/search';
|
||||
import useFetchViewCount from 'effects/use-fetch-view-count';
|
||||
|
||||
/**
|
||||
* 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<string>,
|
||||
liveUris: Array<string>,
|
||||
livestreamMap: { [string]: any },
|
||||
claimsByUri: { [string]: any },
|
||||
claimSearchByQuery: { [string]: Array<string> },
|
||||
options: any
|
||||
) {
|
||||
if (!livestreamMap || !uris) return;
|
||||
type SearchOptions = {
|
||||
page_size: number,
|
||||
no_totals: boolean,
|
||||
any_tags: Array<string>,
|
||||
channel_ids: Array<string>,
|
||||
claim_ids?: Array<string>,
|
||||
not_channel_ids: Array<string>,
|
||||
not_tags: Array<string>,
|
||||
order_by: Array<string>,
|
||||
languages?: Array<string>,
|
||||
release_time?: string,
|
||||
claim_type?: string | Array<string>,
|
||||
timestamp?: string,
|
||||
fee_amount?: string,
|
||||
limit_claims_per_channel?: number,
|
||||
stream_types?: Array<string>,
|
||||
has_source?: boolean,
|
||||
has_no_source?: boolean,
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
function urisEqual(prev: ?Array<string>, next: ?Array<string>) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
// $FlowFixMe - already checked for null above.
|
||||
return prev.length === next.length && prev.every((value, index) => value === next[index]);
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
|
@ -88,8 +46,6 @@ type Props = {
|
|||
prefixUris?: Array<string>,
|
||||
pinUrls?: Array<string>,
|
||||
uris: Array<string>,
|
||||
liveLivestreamsFirst?: boolean,
|
||||
livestreamMap?: { [string]: any },
|
||||
showNoSourceClaims?: boolean,
|
||||
renderProperties?: (Claim) => ?Node,
|
||||
fetchViewCount?: boolean,
|
||||
|
@ -109,6 +65,7 @@ type Props = {
|
|||
hasSource?: boolean,
|
||||
hasNoSource?: boolean,
|
||||
// --- select ---
|
||||
location: { search: string },
|
||||
claimSearchByQuery: { [string]: Array<string> },
|
||||
claimsByUri: { [string]: any },
|
||||
fetchingClaimSearchByQuery: { [string]: boolean },
|
||||
|
@ -116,6 +73,7 @@ type Props = {
|
|||
hideReposts: boolean,
|
||||
mutedUris: Array<string>,
|
||||
blockedUris: Array<string>,
|
||||
options: SearchOptions,
|
||||
// --- perform ---
|
||||
doClaimSearch: ({}) => void,
|
||||
doFetchViewCount: (claimIdCsv: string) => void,
|
||||
|
@ -126,234 +84,72 @@ 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,
|
||||
liveLivestreamsFirst,
|
||||
livestreamMap,
|
||||
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]))
|
||||
);
|
||||
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<string>,
|
||||
channel_ids: Array<string>,
|
||||
claim_ids?: Array<string>,
|
||||
not_channel_ids: Array<string>,
|
||||
not_tags: Array<string>,
|
||||
order_by: Array<string>,
|
||||
languages?: Array<string>,
|
||||
release_time?: string,
|
||||
claim_type?: string | Array<string>,
|
||||
timestamp?: string,
|
||||
fee_amount?: string,
|
||||
limit_claims_per_channel?: number,
|
||||
stream_types?: Array<string>,
|
||||
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);
|
||||
}
|
||||
const searchKey = createNormalizedClaimSearchKey(options);
|
||||
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[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 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);
|
||||
uris.splice(2, 0, ...pinUrls);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// **************************************************************************
|
||||
|
||||
function resolveLive(index) {
|
||||
if (liveLivestreamsFirst && livestreamMap && index < liveUris.length) {
|
||||
return true;
|
||||
}
|
||||
return undefined;
|
||||
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(''));
|
||||
}
|
||||
|
||||
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(','));
|
||||
}
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// **************************************************************************
|
||||
useFetchViewCount(fetchViewCount, uris, claimsByUri, doFetchViewCount);
|
||||
|
||||
// Run `doClaimSearch`
|
||||
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
|
||||
|
||||
// **************************************************************************
|
||||
// **************************************************************************
|
||||
}, [doClaimSearch, shouldPerformSearch, optionsStringForEffect]);
|
||||
|
||||
return (
|
||||
<ul className="claim-grid">
|
||||
{modifiedUris && modifiedUris.length
|
||||
? modifiedUris.map((uri, index) => (
|
||||
<ClaimPreviewTile
|
||||
showNoSourceClaims={hasNoSource || showNoSourceClaims}
|
||||
key={uri}
|
||||
uri={uri}
|
||||
properties={renderProperties}
|
||||
live={resolveLive(index)}
|
||||
/>
|
||||
))
|
||||
{uris && uris.length
|
||||
? uris.map((uri, i) => {
|
||||
if (uri) {
|
||||
return (
|
||||
<ClaimPreviewTile
|
||||
showNoSourceClaims={hasNoSource || showNoSourceClaims}
|
||||
key={uri}
|
||||
uri={uri}
|
||||
properties={renderProperties}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <ClaimPreviewTile showNoSourceClaims={hasNoSource || showNoSourceClaims} key={i} placeholder />;
|
||||
}
|
||||
})
|
||||
: new Array(pageSize)
|
||||
.fill(1)
|
||||
.map((x, i) => (
|
||||
|
@ -362,4 +158,75 @@ function ClaimTilesDiscover(props: Props) {
|
|||
</ul>
|
||||
);
|
||||
}
|
||||
export default ClaimTilesDiscover;
|
||||
|
||||
export default React.memo<Props>(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;
|
||||
}
|
||||
|
|
|
@ -352,3 +352,7 @@ 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_SKIPPED = 'FETCH_ACTIVE_LIVESTREAMS_SKIPPED';
|
||||
export const FETCH_ACTIVE_LIVESTREAMS_COMPLETED = 'FETCH_ACTIVE_LIVESTREAMS_COMPLETED';
|
||||
|
|
24
ui/effects/use-fetch-view-count.js
Normal file
24
ui/effects/use-fetch-view-count.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
// @flow
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function useFetchViewCount(
|
||||
shouldFetch: ?boolean,
|
||||
uris: Array<string>,
|
||||
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]);
|
||||
}
|
|
@ -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 };
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -9,24 +9,32 @@ 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';
|
||||
import { getLivestreamUris } from 'util/livestream';
|
||||
|
||||
type Props = {
|
||||
subscribedChannels: Array<Subscription>,
|
||||
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 { livestreamMap } = useGetLivestreams();
|
||||
const channelIds = subscribedChannels.map((sub) => splitBySeparator(sub.uri)[1]);
|
||||
|
||||
React.useEffect(() => {
|
||||
doFetchActiveLivestreams();
|
||||
}, []);
|
||||
|
||||
return !hasSubsribedChannels ? (
|
||||
<ChannelsFollowingDiscoverPage />
|
||||
) : (
|
||||
<Page noFooter fullWidthPage={tileLayout}>
|
||||
<ClaimListDiscover
|
||||
prefixUris={getLivestreamUris(activeLivestreams, channelIds)}
|
||||
hideAdvancedFilter={SIMPLE_SITE}
|
||||
streamType={SIMPLE_SITE ? CS.CONTENT_ALL : undefined}
|
||||
tileLayout={tileLayout}
|
||||
|
@ -37,7 +45,7 @@ function ChannelsFollowingPage(props: Props) {
|
|||
</span>
|
||||
}
|
||||
defaultOrderBy={CS.ORDER_BY_NEW}
|
||||
channelIds={subscribedChannels.map((sub) => splitBySeparator(sub.uri)[1])}
|
||||
channelIds={channelIds}
|
||||
meta={
|
||||
<Button
|
||||
icon={ICONS.SEARCH}
|
||||
|
@ -46,8 +54,6 @@ function ChannelsFollowingPage(props: Props) {
|
|||
navigate={`/$/${PAGES.CHANNELS_FOLLOWING_DISCOVER}`}
|
||||
/>
|
||||
}
|
||||
liveLivestreamsFirst
|
||||
livestreamMap={livestreamMap}
|
||||
showNoSourceClaims={ENABLE_NO_SOURCE_CLAIMS}
|
||||
hasSource
|
||||
/>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import * as CS from 'constants/claim_search';
|
||||
import { connect } from 'react-redux';
|
||||
import { makeSelectClaimForUri, doResolveUri, SETTINGS } from 'lbry-redux';
|
||||
import { doFetchActiveLivestreams } from 'redux/actions/livestream';
|
||||
import { selectActiveLivestreams } from 'redux/selectors/livestream';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectFollowedTags } from 'redux/selectors/tags';
|
||||
import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
||||
|
@ -18,10 +20,12 @@ const select = (state, props) => {
|
|||
repostedClaim: repostedUri ? makeSelectClaimForUri(repostedUri)(state) : null,
|
||||
isAuthenticated: selectUserVerifiedEmail(state),
|
||||
tileLayout: makeSelectClientSetting(SETTINGS.TILE_LAYOUT)(state),
|
||||
activeLivestreams: selectActiveLivestreams(state),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(select, {
|
||||
doToggleTagFollowDesktop,
|
||||
doResolveUri,
|
||||
doFetchActiveLivestreams,
|
||||
})(Tags);
|
||||
|
|
|
@ -8,15 +8,17 @@ import Page from 'component/page';
|
|||
import ClaimListDiscover from 'component/claimListDiscover';
|
||||
import Button from 'component/button';
|
||||
import useHover from 'effects/use-hover';
|
||||
import { useIsMobile } from 'effects/use-screensize';
|
||||
import { useIsMobile, useIsLargeScreen } from 'effects/use-screensize';
|
||||
import analytics from 'analytics';
|
||||
import HiddenNsfw from 'component/common/hidden-nsfw';
|
||||
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';
|
||||
import { getLivestreamUris } from 'util/livestream';
|
||||
|
||||
const DEFAULT_LIVESTREAM_TILE_LIMIT = 8;
|
||||
|
||||
type Props = {
|
||||
location: { search: string },
|
||||
|
@ -28,6 +30,8 @@ type Props = {
|
|||
isAuthenticated: boolean,
|
||||
dynamicRouteProps: RowDataItem,
|
||||
tileLayout: boolean,
|
||||
activeLivestreams: ?LivestreamInfo,
|
||||
doFetchActiveLivestreams: (orderBy?: Array<string>, pageSize?: number, forceFetch?: boolean) => void,
|
||||
};
|
||||
|
||||
function DiscoverPage(props: Props) {
|
||||
|
@ -40,12 +44,14 @@ function DiscoverPage(props: Props) {
|
|||
doResolveUri,
|
||||
isAuthenticated,
|
||||
tileLayout,
|
||||
activeLivestreams,
|
||||
doFetchActiveLivestreams,
|
||||
dynamicRouteProps,
|
||||
} = props;
|
||||
const buttonRef = useRef();
|
||||
const isHovering = useHover(buttonRef);
|
||||
const isMobile = useIsMobile();
|
||||
const { livestreamMap } = useGetLivestreams();
|
||||
const isLargeScreen = useIsLargeScreen();
|
||||
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const claimType = urlParams.get('claim_type');
|
||||
|
@ -58,6 +64,8 @@ function DiscoverPage(props: Props) {
|
|||
// Eventually allow more than one tag on this page
|
||||
// Restricting to one to make follow/unfollow simpler
|
||||
const tag = (tags && tags[0]) || null;
|
||||
const channelIds =
|
||||
(dynamicRouteProps && dynamicRouteProps.options && dynamicRouteProps.options.channelIds) || undefined;
|
||||
|
||||
const isFollowing = followedTags.map(({ name }) => name).includes(tag);
|
||||
let label = isFollowing ? __('Following --[button label indicating a channel has been followed]--') : __('Follow');
|
||||
|
@ -65,6 +73,46 @@ function DiscoverPage(props: Props) {
|
|||
label = __('Unfollow');
|
||||
}
|
||||
|
||||
const initialLivestreamTileLimit = getPageSize(DEFAULT_LIVESTREAM_TILE_LIMIT);
|
||||
|
||||
const [showViewMoreLivestreams, setShowViewMoreLivestreams] = React.useState(!dynamicRouteProps);
|
||||
const livestreamUris = getLivestreamUris(activeLivestreams, channelIds);
|
||||
const useDualList = showViewMoreLivestreams && livestreamUris.length > initialLivestreamTileLimit;
|
||||
|
||||
function getElemMeta() {
|
||||
return !dynamicRouteProps ? (
|
||||
<a
|
||||
className="help"
|
||||
href="https://lbry.com/faq/trending"
|
||||
title={__('Learn more about LBRY Credits on %DOMAIN%', { DOMAIN })}
|
||||
>
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
lbc: <LbcSymbol />,
|
||||
}}
|
||||
>
|
||||
Results boosted by %lbc%
|
||||
</I18nMessage>
|
||||
</a>
|
||||
) : (
|
||||
tag && !isMobile && (
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
button="alt"
|
||||
icon={ICONS.SUBSCRIBE}
|
||||
iconColor="red"
|
||||
onClick={handleFollowClick}
|
||||
requiresAuth={IS_WEB}
|
||||
label={label}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function getPageSize(originalSize) {
|
||||
return isLargeScreen ? originalSize * (3 / 2) : originalSize;
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (repostedUri && !repostedClaimIsResolved) {
|
||||
doResolveUri(repostedUri);
|
||||
|
@ -106,16 +154,53 @@ function DiscoverPage(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (showViewMoreLivestreams) {
|
||||
doFetchActiveLivestreams(CS.ORDER_BY_TRENDING_VALUE);
|
||||
} else {
|
||||
doFetchActiveLivestreams();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Page noFooter fullWidthPage={tileLayout}>
|
||||
{useDualList && (
|
||||
<>
|
||||
<ClaimListDiscover
|
||||
uris={livestreamUris.slice(0, initialLivestreamTileLimit)}
|
||||
headerLabel={headerLabel}
|
||||
header={repostedUri ? <span /> : undefined}
|
||||
tileLayout={repostedUri ? false : tileLayout}
|
||||
hideAdvancedFilter
|
||||
hideFilters
|
||||
infiniteScroll={false}
|
||||
showNoSourceClaims={ENABLE_NO_SOURCE_CLAIMS}
|
||||
meta={getElemMeta()}
|
||||
/>
|
||||
<div className="livestream-list--view-more">
|
||||
<Button
|
||||
label={__('Show more livestreams')}
|
||||
button="link"
|
||||
iconRight={ICONS.ARROW_RIGHT}
|
||||
className="claim-grid__title--secondary"
|
||||
onClick={() => {
|
||||
doFetchActiveLivestreams();
|
||||
setShowViewMoreLivestreams(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ClaimListDiscover
|
||||
prefixUris={useDualList ? undefined : livestreamUris}
|
||||
hideAdvancedFilter={SIMPLE_SITE}
|
||||
hideFilters={SIMPLE_SITE ? !dynamicRouteProps : undefined}
|
||||
header={repostedUri ? <span /> : undefined}
|
||||
header={useDualList ? <span /> : repostedUri ? <span /> : undefined}
|
||||
tileLayout={repostedUri ? false : tileLayout}
|
||||
defaultOrderBy={SIMPLE_SITE ? (dynamicRouteProps ? undefined : CS.ORDER_BY_TRENDING) : undefined}
|
||||
claimType={claimType ? [claimType] : undefined}
|
||||
headerLabel={headerLabel}
|
||||
headerLabel={!useDualList && headerLabel}
|
||||
tags={tags}
|
||||
hiddenNsfwMessage={<HiddenNsfw type="page" />}
|
||||
repostedClaimId={repostedClaim ? repostedClaim.claim_id : null}
|
||||
|
@ -133,47 +218,14 @@ function DiscoverPage(props: Props) {
|
|||
: undefined
|
||||
}
|
||||
feeAmount={SIMPLE_SITE ? !dynamicRouteProps && CS.FEE_AMOUNT_ANY : undefined}
|
||||
channelIds={
|
||||
(dynamicRouteProps && dynamicRouteProps.options && dynamicRouteProps.options.channelIds) || undefined
|
||||
}
|
||||
channelIds={channelIds}
|
||||
limitClaimsPerChannel={
|
||||
SIMPLE_SITE
|
||||
? (dynamicRouteProps && dynamicRouteProps.options && dynamicRouteProps.options.limitClaimsPerChannel) ||
|
||||
undefined
|
||||
: 3
|
||||
}
|
||||
meta={
|
||||
!dynamicRouteProps ? (
|
||||
<a
|
||||
className="help"
|
||||
href="https://lbry.com/faq/trending"
|
||||
title={__('Learn more about LBRY Credits on %DOMAIN%', { DOMAIN })}
|
||||
>
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
lbc: <LbcSymbol />,
|
||||
}}
|
||||
>
|
||||
Results boosted by %lbc%
|
||||
</I18nMessage>
|
||||
</a>
|
||||
) : (
|
||||
tag &&
|
||||
!isMobile && (
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
button="alt"
|
||||
icon={ICONS.SUBSCRIBE}
|
||||
iconColor="red"
|
||||
onClick={handleFollowClick}
|
||||
requiresAuth={IS_WEB}
|
||||
label={label}
|
||||
/>
|
||||
)
|
||||
)
|
||||
}
|
||||
liveLivestreamsFirst
|
||||
livestreamMap={livestreamMap}
|
||||
meta={!useDualList && getElemMeta()}
|
||||
hasSource
|
||||
showNoSourceClaims={ENABLE_NO_SOURCE_CLAIMS}
|
||||
/>
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doFetchActiveLivestreams } from 'redux/actions/livestream';
|
||||
import { selectActiveLivestreams } from 'redux/selectors/livestream';
|
||||
import { selectFollowedTags } from 'redux/selectors/tags';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||
|
@ -12,8 +14,11 @@ const select = (state) => ({
|
|||
authenticated: selectUserVerifiedEmail(state),
|
||||
showNsfw: selectShowMatureContent(state),
|
||||
homepageData: selectHomepageData(state),
|
||||
activeLivestreams: selectActiveLivestreams(state),
|
||||
});
|
||||
|
||||
const perform = {};
|
||||
const perform = (dispatch) => ({
|
||||
doFetchActiveLivestreams: () => dispatch(doFetchActiveLivestreams()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(DiscoverPage);
|
||||
|
|
|
@ -9,8 +9,8 @@ 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';
|
||||
import { getLivestreamUris } from 'util/livestream';
|
||||
|
||||
// @if TARGET='web'
|
||||
import Pixel from 'web/component/pixel';
|
||||
|
@ -23,14 +23,23 @@ type Props = {
|
|||
subscribedChannels: Array<Subscription>,
|
||||
showNsfw: boolean,
|
||||
homepageData: any,
|
||||
activeLivestreams: any,
|
||||
doFetchActiveLivestreams: () => void,
|
||||
};
|
||||
|
||||
function HomePage(props: Props) {
|
||||
const { followedTags, subscribedChannels, authenticated, showNsfw, homepageData } = props;
|
||||
const {
|
||||
followedTags,
|
||||
subscribedChannels,
|
||||
authenticated,
|
||||
showNsfw,
|
||||
homepageData,
|
||||
activeLivestreams,
|
||||
doFetchActiveLivestreams,
|
||||
} = 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<RowDataItem> = GetLinksData(
|
||||
homepageData,
|
||||
|
@ -52,13 +61,13 @@ function HomePage(props: Props) {
|
|||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
const claimTiles = (
|
||||
<ClaimTilesDiscover
|
||||
{...options}
|
||||
liveLivestreamsFirst
|
||||
livestreamMap={livestreamMap}
|
||||
showNoSourceClaims={ENABLE_NO_SOURCE_CLAIMS}
|
||||
hasSource
|
||||
prefixUris={getLivestreamUris(activeLivestreams, options.channelIds)}
|
||||
pinUrls={pinUrls}
|
||||
/>
|
||||
);
|
||||
|
@ -95,6 +104,10 @@ function HomePage(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
doFetchActiveLivestreams();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Page fullWidthPage>
|
||||
{!SIMPLE_SITE && (authenticated || !IS_WEB) && !subscribedChannels.length && (
|
||||
|
|
|
@ -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,90 @@ export const doFetchNoSourceClaims = (channelId: string) => async (dispatch: Dis
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
const FETCH_ACTIVE_LIVESTREAMS_MIN_INTERVAL_MS = 5 * 60 * 1000;
|
||||
|
||||
export const doFetchActiveLivestreams = (
|
||||
orderBy: Array<string> = ['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.activeLivestreamsLastFetchedDate;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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: nextOptions.page_size,
|
||||
has_no_source: true,
|
||||
channel_ids: Object.keys(activeLivestreams),
|
||||
claim_type: ['stream'],
|
||||
order_by: nextOptions.order_by, // **
|
||||
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,
|
||||
activeLivestreamsLastFetchedDate: now,
|
||||
activeLivestreamsLastFetchedOptions: nextOptions,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED });
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED });
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
|
@ -5,6 +5,10 @@ import { handleActions } from 'util/redux-utils';
|
|||
const defaultState: LivestreamState = {
|
||||
fetchingById: {},
|
||||
viewersById: {},
|
||||
fetchingActiveLivestreams: false,
|
||||
activeLivestreams: null,
|
||||
activeLivestreamsLastFetchedDate: 0,
|
||||
activeLivestreamsLastFetchedOptions: {},
|
||||
};
|
||||
|
||||
export default handleActions(
|
||||
|
@ -36,6 +40,22 @@ 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, activeLivestreamsLastFetchedDate, activeLivestreamsLastFetchedOptions } = action.data;
|
||||
return {
|
||||
...state,
|
||||
fetchingActiveLivestreams: false,
|
||||
activeLivestreams,
|
||||
activeLivestreamsLastFetchedDate,
|
||||
activeLivestreamsLastFetchedOptions,
|
||||
};
|
||||
},
|
||||
},
|
||||
defaultState
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -459,3 +459,9 @@ $recent-msg-button__height: 2rem;
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.livestream-list--view-more {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin-bottom: var(--spacing-m);
|
||||
}
|
||||
|
|
24
ui/util/livestream.js
Normal file
24
ui/util/livestream.js
Normal file
|
@ -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<string>) {
|
||||
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);
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue