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 = {
|
declare type LivestreamState = {
|
||||||
fetchingById: {},
|
fetchingById: {},
|
||||||
viewersById: {},
|
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 { connect } from 'react-redux';
|
||||||
import ClaimList from './view';
|
import ClaimList from './view';
|
||||||
import { SETTINGS, selectClaimSearchByQuery, selectClaimsByUri } from 'lbry-redux';
|
import { SETTINGS } from 'lbry-redux';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state) => ({
|
||||||
searchInLanguage: makeSelectClientSetting(SETTINGS.SEARCH_IN_LANGUAGE)(state),
|
searchInLanguage: makeSelectClientSetting(SETTINGS.SEARCH_IN_LANGUAGE)(state),
|
||||||
claimSearchByQuery: selectClaimSearchByQuery(state),
|
|
||||||
claimsByUri: selectClaimsByUri(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select)(ClaimList);
|
export default connect(select)(ClaimList);
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { FormField } from 'component/common/form';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import debounce from 'util/debounce';
|
import debounce from 'util/debounce';
|
||||||
import ClaimPreviewTile from 'component/claimPreviewTile';
|
import ClaimPreviewTile from 'component/claimPreviewTile';
|
||||||
import { prioritizeActiveLivestreams } from 'component/claimTilesDiscover/view';
|
|
||||||
|
|
||||||
const DEBOUNCE_SCROLL_HANDLER_MS = 150;
|
const DEBOUNCE_SCROLL_HANDLER_MS = 150;
|
||||||
const SORT_NEW = 'new';
|
const SORT_NEW = 'new';
|
||||||
|
@ -17,6 +16,7 @@ const SORT_OLD = 'old';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uris: Array<string>,
|
uris: Array<string>,
|
||||||
|
prefixUris?: Array<string>,
|
||||||
header: Node | boolean,
|
header: Node | boolean,
|
||||||
headerAltControls: Node,
|
headerAltControls: Node,
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
|
@ -41,9 +41,6 @@ type Props = {
|
||||||
hideMenu?: boolean,
|
hideMenu?: boolean,
|
||||||
claimSearchByQuery: { [string]: Array<string> },
|
claimSearchByQuery: { [string]: Array<string> },
|
||||||
claimsByUri: { [string]: any },
|
claimsByUri: { [string]: any },
|
||||||
liveLivestreamsFirst?: boolean,
|
|
||||||
livestreamMap?: { [string]: any },
|
|
||||||
searchOptions?: any,
|
|
||||||
collectionId?: string,
|
collectionId?: string,
|
||||||
showNoSourceClaims?: boolean,
|
showNoSourceClaims?: boolean,
|
||||||
onClick?: (e: any, claim?: ?Claim, index?: number) => void,
|
onClick?: (e: any, claim?: ?Claim, index?: number) => void,
|
||||||
|
@ -53,6 +50,7 @@ export default function ClaimList(props: Props) {
|
||||||
const {
|
const {
|
||||||
activeUri,
|
activeUri,
|
||||||
uris,
|
uris,
|
||||||
|
prefixUris,
|
||||||
headerAltControls,
|
headerAltControls,
|
||||||
loading,
|
loading,
|
||||||
persistedStorageKey,
|
persistedStorageKey,
|
||||||
|
@ -73,37 +71,25 @@ export default function ClaimList(props: Props) {
|
||||||
renderProperties,
|
renderProperties,
|
||||||
searchInLanguage,
|
searchInLanguage,
|
||||||
hideMenu,
|
hideMenu,
|
||||||
claimSearchByQuery,
|
|
||||||
claimsByUri,
|
|
||||||
liveLivestreamsFirst,
|
|
||||||
livestreamMap,
|
|
||||||
searchOptions,
|
|
||||||
collectionId,
|
collectionId,
|
||||||
showNoSourceClaims,
|
showNoSourceClaims,
|
||||||
onClick,
|
onClick,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
|
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 timedOut = uris === null;
|
||||||
const urisLength = (uris && uris.length) || 0;
|
const urisLength = (uris && uris.length) || 0;
|
||||||
|
|
||||||
const liveUris = [];
|
const tileUris = (prefixUris || []).concat(uris);
|
||||||
if (liveLivestreamsFirst && livestreamMap) {
|
const sortedUris = (urisLength > 0 && (currentSort === SORT_NEW ? tileUris : tileUris.slice().reverse())) || [];
|
||||||
prioritizeActiveLivestreams(uris, liveUris, livestreamMap, claimsByUri, claimSearchByQuery, searchOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedUris = (urisLength > 0 && (currentSort === SORT_NEW ? uris : uris.slice().reverse())) || [];
|
|
||||||
const noResultMsg = searchInLanguage
|
const noResultMsg = searchInLanguage
|
||||||
? __('No results. Contents may be hidden by the Language filter.')
|
? __('No results. Contents may be hidden by the Language filter.')
|
||||||
: __('No results');
|
: __('No results');
|
||||||
|
|
||||||
const resolveLive = (index) => {
|
|
||||||
if (liveLivestreamsFirst && livestreamMap && index < liveUris.length) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleSortChange() {
|
function handleSortChange() {
|
||||||
setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW);
|
setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW);
|
||||||
}
|
}
|
||||||
|
@ -138,13 +124,12 @@ export default function ClaimList(props: Props) {
|
||||||
return tileLayout && !header ? (
|
return tileLayout && !header ? (
|
||||||
<section className="claim-grid">
|
<section className="claim-grid">
|
||||||
{urisLength > 0 &&
|
{urisLength > 0 &&
|
||||||
uris.map((uri, index) => (
|
tileUris.map((uri) => (
|
||||||
<ClaimPreviewTile
|
<ClaimPreviewTile
|
||||||
key={uri}
|
key={uri}
|
||||||
uri={uri}
|
uri={uri}
|
||||||
showHiddenByUser={showHiddenByUser}
|
showHiddenByUser={showHiddenByUser}
|
||||||
properties={renderProperties}
|
properties={renderProperties}
|
||||||
live={resolveLive(index)}
|
|
||||||
collectionId={collectionId}
|
collectionId={collectionId}
|
||||||
showNoSourceClaims={showNoSourceClaims}
|
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
|
// 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';
|
return claim.name.length === 24 && !claim.name.includes(' ') && claim.value.author === 'Spee.ch';
|
||||||
}}
|
}}
|
||||||
live={resolveLive(index)}
|
|
||||||
onClick={(e, claim, index) => handleClaimClicked(e, claim, index)}
|
onClick={(e, claim, index) => handleClaimClicked(e, claim, index)}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
|
@ -13,11 +13,12 @@ import ClaimPreview from 'component/claimPreview';
|
||||||
import ClaimPreviewTile from 'component/claimPreviewTile';
|
import ClaimPreviewTile from 'component/claimPreviewTile';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import ClaimListHeader from 'component/claimListHeader';
|
import ClaimListHeader from 'component/claimListHeader';
|
||||||
|
import useFetchViewCount from 'effects/use-fetch-view-count';
|
||||||
import { useIsLargeScreen } from 'effects/use-screensize';
|
import { useIsLargeScreen } from 'effects/use-screensize';
|
||||||
import { getLivestreamOnlyOptions } from 'util/search';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uris: Array<string>,
|
uris: Array<string>,
|
||||||
|
prefixUris?: Array<string>,
|
||||||
name?: string,
|
name?: string,
|
||||||
type: string,
|
type: string,
|
||||||
pageSize?: number,
|
pageSize?: number,
|
||||||
|
@ -31,7 +32,6 @@ type Props = {
|
||||||
includeSupportAction?: boolean,
|
includeSupportAction?: boolean,
|
||||||
infiniteScroll?: Boolean,
|
infiniteScroll?: Boolean,
|
||||||
isChannel?: boolean,
|
isChannel?: boolean,
|
||||||
liveLivestreamsFirst?: boolean,
|
|
||||||
personalView: boolean,
|
personalView: boolean,
|
||||||
showHeader: boolean,
|
showHeader: boolean,
|
||||||
showHiddenByUser?: boolean,
|
showHiddenByUser?: boolean,
|
||||||
|
@ -64,7 +64,6 @@ type Props = {
|
||||||
channelIds?: Array<string>,
|
channelIds?: Array<string>,
|
||||||
claimIds?: Array<string>,
|
claimIds?: Array<string>,
|
||||||
subscribedChannels: Array<Subscription>,
|
subscribedChannels: Array<Subscription>,
|
||||||
livestreamMap?: { [string]: any },
|
|
||||||
|
|
||||||
header?: Node,
|
header?: Node,
|
||||||
headerLabel?: string | Node,
|
headerLabel?: string | Node,
|
||||||
|
@ -137,6 +136,7 @@ function ClaimListDiscover(props: Props) {
|
||||||
injectedItem,
|
injectedItem,
|
||||||
feeAmount,
|
feeAmount,
|
||||||
uris,
|
uris,
|
||||||
|
prefixUris,
|
||||||
tileLayout,
|
tileLayout,
|
||||||
hideFilters = false,
|
hideFilters = false,
|
||||||
claimIds,
|
claimIds,
|
||||||
|
@ -148,8 +148,6 @@ function ClaimListDiscover(props: Props) {
|
||||||
releaseTime,
|
releaseTime,
|
||||||
scrollAnchor,
|
scrollAnchor,
|
||||||
showHiddenByUser = false,
|
showHiddenByUser = false,
|
||||||
liveLivestreamsFirst,
|
|
||||||
livestreamMap,
|
|
||||||
hasSource,
|
hasSource,
|
||||||
hasNoSource,
|
hasNoSource,
|
||||||
isChannel = false,
|
isChannel = false,
|
||||||
|
@ -380,9 +378,9 @@ function ClaimListDiscover(props: Props) {
|
||||||
|
|
||||||
const hasMatureTags = tagsParam && tagsParam.split(',').some((t) => MATURE_TAGS.includes(t));
|
const hasMatureTags = tagsParam && tagsParam.split(',').some((t) => MATURE_TAGS.includes(t));
|
||||||
|
|
||||||
const mainSearchKey = createNormalizedClaimSearchKey(options);
|
const searchKey = createNormalizedClaimSearchKey(options);
|
||||||
let claimSearchResult = claimSearchByQuery[mainSearchKey];
|
const claimSearchResult = claimSearchByQuery[searchKey];
|
||||||
const claimSearchResultLastPageReached = claimSearchByQueryLastPageReached[mainSearchKey];
|
const claimSearchResultLastPageReached = claimSearchByQueryLastPageReached[searchKey];
|
||||||
|
|
||||||
// uncomment to fix an item on a page
|
// uncomment to fix an item on a page
|
||||||
// const fixUri = 'lbry://@corbettreport#0/lbryodysee#5';
|
// const fixUri = 'lbry://@corbettreport#0/lbryodysee#5';
|
||||||
|
@ -400,14 +398,6 @@ function ClaimListDiscover(props: Props) {
|
||||||
// claimSearchResult.splice(2, 0, fixUri);
|
// 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);
|
const [prevOptions, setPrevOptions] = React.useState(null);
|
||||||
|
|
||||||
if (!isJustScrollingToNewPage(prevOptions, options)) {
|
if (!isJustScrollingToNewPage(prevOptions, options)) {
|
||||||
|
@ -469,6 +459,8 @@ function ClaimListDiscover(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderUris = uris || claimSearchResult;
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// Helpers
|
// 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>) {
|
function resolveOrderByOption(orderBy: string | Array<string>, sortBy: string | Array<string>) {
|
||||||
const order_by =
|
const order_by =
|
||||||
orderBy === CS.ORDER_BY_TRENDING
|
orderBy === CS.ORDER_BY_TRENDING
|
||||||
|
@ -567,38 +524,15 @@ function ClaimListDiscover(props: Props) {
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
|
useFetchViewCount(fetchViewCount, renderUris, claimsByUri, doFetchViewCount);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (shouldPerformSearch) {
|
if (shouldPerformSearch) {
|
||||||
const searchOptions = JSON.parse(optionsStringForEffect);
|
const searchOptions = JSON.parse(optionsStringForEffect);
|
||||||
doClaimSearch(searchOptions);
|
doClaimSearch(searchOptions);
|
||||||
|
|
||||||
if (liveLivestreamsFirst && options.page === 1) {
|
|
||||||
doClaimSearch(getLivestreamOnlyOptions(searchOptions));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [doClaimSearch, shouldPerformSearch, optionsStringForEffect, forceRefresh]);
|
}, [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 || (
|
const headerToUse = header || (
|
||||||
<ClaimListHeader
|
<ClaimListHeader
|
||||||
channelIds={channelIds}
|
channelIds={channelIds}
|
||||||
|
@ -636,7 +570,8 @@ function ClaimListDiscover(props: Props) {
|
||||||
<ClaimList
|
<ClaimList
|
||||||
tileLayout
|
tileLayout
|
||||||
loading={loading}
|
loading={loading}
|
||||||
uris={finalUris}
|
uris={renderUris}
|
||||||
|
prefixUris={prefixUris}
|
||||||
onScrollBottom={handleScrollBottom}
|
onScrollBottom={handleScrollBottom}
|
||||||
page={page}
|
page={page}
|
||||||
pageSize={dynamicPageSize}
|
pageSize={dynamicPageSize}
|
||||||
|
@ -645,8 +580,6 @@ function ClaimListDiscover(props: Props) {
|
||||||
includeSupportAction={includeSupportAction}
|
includeSupportAction={includeSupportAction}
|
||||||
injectedItem={injectedItem}
|
injectedItem={injectedItem}
|
||||||
showHiddenByUser={showHiddenByUser}
|
showHiddenByUser={showHiddenByUser}
|
||||||
liveLivestreamsFirst={liveLivestreamsFirst}
|
|
||||||
livestreamMap={livestreamMap}
|
|
||||||
searchOptions={options}
|
searchOptions={options}
|
||||||
showNoSourceClaims={showNoSourceClaims}
|
showNoSourceClaims={showNoSourceClaims}
|
||||||
empty={empty}
|
empty={empty}
|
||||||
|
@ -670,7 +603,8 @@ function ClaimListDiscover(props: Props) {
|
||||||
<ClaimList
|
<ClaimList
|
||||||
type={type}
|
type={type}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
uris={finalUris}
|
uris={renderUris}
|
||||||
|
prefixUris={prefixUris}
|
||||||
onScrollBottom={handleScrollBottom}
|
onScrollBottom={handleScrollBottom}
|
||||||
page={page}
|
page={page}
|
||||||
pageSize={dynamicPageSize}
|
pageSize={dynamicPageSize}
|
||||||
|
@ -679,8 +613,6 @@ function ClaimListDiscover(props: Props) {
|
||||||
includeSupportAction={includeSupportAction}
|
includeSupportAction={includeSupportAction}
|
||||||
injectedItem={injectedItem}
|
injectedItem={injectedItem}
|
||||||
showHiddenByUser={showHiddenByUser}
|
showHiddenByUser={showHiddenByUser}
|
||||||
liveLivestreamsFirst={liveLivestreamsFirst}
|
|
||||||
livestreamMap={livestreamMap}
|
|
||||||
searchOptions={options}
|
searchOptions={options}
|
||||||
showNoSourceClaims={hasNoSource || showNoSourceClaims}
|
showNoSourceClaims={hasNoSource || showNoSourceClaims}
|
||||||
empty={empty}
|
empty={empty}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { selectMutedChannels, makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
import { selectMutedChannels, makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||||
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
||||||
|
import { makeSelectIsActiveLivestream } from 'redux/selectors/livestream';
|
||||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
import { makeSelectHasVisitedUri } from 'redux/selectors/content';
|
import { makeSelectHasVisitedUri } from 'redux/selectors/content';
|
||||||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||||
|
@ -56,6 +57,7 @@ const select = (state, props) => {
|
||||||
streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state),
|
streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state),
|
||||||
wasPurchased: props.uri && makeSelectClaimWasPurchased(props.uri)(state),
|
wasPurchased: props.uri && makeSelectClaimWasPurchased(props.uri)(state),
|
||||||
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
|
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
|
||||||
|
isLivestreamActive: makeSelectIsActiveLivestream(props.uri)(state),
|
||||||
isCollectionMine: makeSelectCollectionIsMine(props.collectionId)(state),
|
isCollectionMine: makeSelectCollectionIsMine(props.collectionId)(state),
|
||||||
collectionUris: makeSelectUrlsForCollectionId(props.collectionId)(state),
|
collectionUris: makeSelectUrlsForCollectionId(props.collectionId)(state),
|
||||||
collectionIndex: makeSelectIndexForUrlInCollection(props.uri, props.collectionId)(state),
|
collectionIndex: makeSelectIndexForUrlInCollection(props.uri, props.collectionId)(state),
|
||||||
|
|
|
@ -80,7 +80,7 @@ type Props = {
|
||||||
repostUrl?: string,
|
repostUrl?: string,
|
||||||
hideMenu?: boolean,
|
hideMenu?: boolean,
|
||||||
isLivestream?: boolean,
|
isLivestream?: boolean,
|
||||||
live?: boolean,
|
isLivestreamActive: boolean,
|
||||||
collectionId?: string,
|
collectionId?: string,
|
||||||
editCollection: (string, CollectionEditParams) => void,
|
editCollection: (string, CollectionEditParams) => void,
|
||||||
isCollectionMine: boolean,
|
isCollectionMine: boolean,
|
||||||
|
@ -145,7 +145,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
hideMenu = false,
|
hideMenu = false,
|
||||||
// repostUrl,
|
// repostUrl,
|
||||||
isLivestream, // need both? CHECK
|
isLivestream, // need both? CHECK
|
||||||
live,
|
isLivestreamActive,
|
||||||
collectionId,
|
collectionId,
|
||||||
collectionIndex,
|
collectionIndex,
|
||||||
editCollection,
|
editCollection,
|
||||||
|
@ -336,7 +336,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let liveProperty = null;
|
let liveProperty = null;
|
||||||
if (live === true) {
|
if (isLivestreamActive === true) {
|
||||||
liveProperty = (claim) => <>LIVE</>;
|
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--channel': isChannelUri && type !== 'inline',
|
||||||
'claim-preview__wrapper--inline': type === 'inline',
|
'claim-preview__wrapper--inline': type === 'inline',
|
||||||
'claim-preview__wrapper--small': type === 'small',
|
'claim-preview__wrapper--small': type === 'small',
|
||||||
'claim-preview__live': live,
|
'claim-preview__live': isLivestreamActive,
|
||||||
'claim-preview__active': active,
|
'claim-preview__active': active,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
@ -386,7 +386,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* @endif */}
|
{/* @endif */}
|
||||||
{!isLivestream && (
|
{(!isLivestream || isLivestreamActive) && (
|
||||||
<div className="claim-preview__file-property-overlay">
|
<div className="claim-preview__file-property-overlay">
|
||||||
<PreviewOverlayProperties uri={uri} small={type === 'small'} properties={liveProperty} />
|
<PreviewOverlayProperties uri={uri} small={type === 'small'} properties={liveProperty} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||||
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
||||||
|
import { makeSelectIsActiveLivestream } from 'redux/selectors/livestream';
|
||||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
import ClaimPreviewTile from './view';
|
import ClaimPreviewTile from './view';
|
||||||
import formatMediaDuration from 'util/formatMediaDuration';
|
import formatMediaDuration from 'util/formatMediaDuration';
|
||||||
|
@ -36,6 +37,7 @@ const select = (state, props) => {
|
||||||
showMature: selectShowMatureContent(state),
|
showMature: selectShowMatureContent(state),
|
||||||
isMature: makeSelectClaimIsNsfw(props.uri)(state),
|
isMature: makeSelectClaimIsNsfw(props.uri)(state),
|
||||||
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
|
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
|
||||||
|
isLivestreamActive: makeSelectIsActiveLivestream(props.uri)(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -48,10 +48,10 @@ type Props = {
|
||||||
showMature: boolean,
|
showMature: boolean,
|
||||||
showHiddenByUser?: boolean,
|
showHiddenByUser?: boolean,
|
||||||
properties?: (Claim) => void,
|
properties?: (Claim) => void,
|
||||||
live?: boolean,
|
|
||||||
collectionId?: string,
|
collectionId?: string,
|
||||||
showNoSourceClaims?: boolean,
|
showNoSourceClaims?: boolean,
|
||||||
isLivestream: boolean,
|
isLivestream: boolean,
|
||||||
|
isLivestreamActive: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
// preview image cards used in related video functionality
|
// preview image cards used in related video functionality
|
||||||
|
@ -75,9 +75,9 @@ function ClaimPreviewTile(props: Props) {
|
||||||
showMature,
|
showMature,
|
||||||
showHiddenByUser,
|
showHiddenByUser,
|
||||||
properties,
|
properties,
|
||||||
live,
|
|
||||||
showNoSourceClaims,
|
showNoSourceClaims,
|
||||||
isLivestream,
|
isLivestream,
|
||||||
|
isLivestreamActive,
|
||||||
collectionId,
|
collectionId,
|
||||||
mediaDuration,
|
mediaDuration,
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -192,7 +192,7 @@ function ClaimPreviewTile(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let liveProperty = null;
|
let liveProperty = null;
|
||||||
if (live === true) {
|
if (isLivestreamActive === true) {
|
||||||
liveProperty = (claim) => <>LIVE</>;
|
liveProperty = (claim) => <>LIVE</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,7 +201,7 @@ function ClaimPreviewTile(props: Props) {
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={classnames('card claim-preview--tile', {
|
className={classnames('card claim-preview--tile', {
|
||||||
'claim-preview__wrapper--channel': isChannel,
|
'claim-preview__wrapper--channel': isChannel,
|
||||||
'claim-preview__live': live,
|
'claim-preview__live': isLivestreamActive,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<NavLink {...navLinkProps} role="none" tabIndex={-1} aria-hidden>
|
<NavLink {...navLinkProps} role="none" tabIndex={-1} aria-hidden>
|
||||||
|
|
|
@ -1,19 +1,26 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
import {
|
import {
|
||||||
doClaimSearch,
|
doClaimSearch,
|
||||||
selectClaimSearchByQuery,
|
selectClaimSearchByQuery,
|
||||||
selectFetchingClaimSearchByQuery,
|
selectFetchingClaimSearchByQuery,
|
||||||
SETTINGS,
|
SETTINGS,
|
||||||
selectClaimsByUri,
|
selectClaimsByUri,
|
||||||
|
splitBySeparator,
|
||||||
|
MATURE_TAGS,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { doFetchViewCount } from 'lbryinc';
|
import { doFetchViewCount } from 'lbryinc';
|
||||||
import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
||||||
import { makeSelectClientSetting, selectShowMatureContent } from 'redux/selectors/settings';
|
import { makeSelectClientSetting, selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
import { selectModerationBlockList } from 'redux/selectors/comments';
|
import { selectModerationBlockList } from 'redux/selectors/comments';
|
||||||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
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';
|
import ClaimListDiscover from './view';
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state, props) => {
|
||||||
|
return {
|
||||||
claimSearchByQuery: selectClaimSearchByQuery(state),
|
claimSearchByQuery: selectClaimSearchByQuery(state),
|
||||||
claimsByUri: selectClaimsByUri(state),
|
claimsByUri: selectClaimsByUri(state),
|
||||||
fetchingClaimSearchByQuery: selectFetchingClaimSearchByQuery(state),
|
fetchingClaimSearchByQuery: selectFetchingClaimSearchByQuery(state),
|
||||||
|
@ -21,7 +28,9 @@ const select = (state) => ({
|
||||||
hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state),
|
hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state),
|
||||||
mutedUris: selectMutedChannels(state),
|
mutedUris: selectMutedChannels(state),
|
||||||
blockedUris: selectModerationBlockList(state),
|
blockedUris: selectModerationBlockList(state),
|
||||||
});
|
options: resolveSearchOptions({ pageSize: 8, ...props }),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const perform = {
|
const perform = {
|
||||||
doClaimSearch,
|
doClaimSearch,
|
||||||
|
@ -29,4 +38,102 @@ const perform = {
|
||||||
doFetchViewCount,
|
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
|
// @flow
|
||||||
import { ENABLE_NO_SOURCE_CLAIMS, SIMPLE_SITE } from 'config';
|
|
||||||
import * as CS from 'constants/claim_search';
|
|
||||||
import type { Node } from 'react';
|
import type { Node } from 'react';
|
||||||
import React 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 ClaimPreviewTile from 'component/claimPreviewTile';
|
||||||
import { useHistory } from 'react-router';
|
import useFetchViewCount from 'effects/use-fetch-view-count';
|
||||||
import { getLivestreamOnlyOptions } from 'util/search';
|
|
||||||
|
|
||||||
/**
|
type SearchOptions = {
|
||||||
* Updates 'uris' by adding and/or moving active livestreams to the front of
|
page_size: number,
|
||||||
* list.
|
no_totals: boolean,
|
||||||
* 'liveUris' is also updated with any entries that were moved to the
|
any_tags: Array<string>,
|
||||||
* front, for convenience.
|
channel_ids: Array<string>,
|
||||||
*
|
claim_ids?: Array<string>,
|
||||||
* @param uris [Ref]
|
not_channel_ids: Array<string>,
|
||||||
* @param liveUris [Ref]
|
not_tags: Array<string>,
|
||||||
* @param livestreamMap
|
order_by: Array<string>,
|
||||||
* @param claimsByUri
|
languages?: Array<string>,
|
||||||
* @param claimSearchByQuery
|
release_time?: string,
|
||||||
* @param options
|
claim_type?: string | Array<string>,
|
||||||
*/
|
timestamp?: string,
|
||||||
export function prioritizeActiveLivestreams(
|
fee_amount?: string,
|
||||||
uris: Array<string>,
|
limit_claims_per_channel?: number,
|
||||||
liveUris: Array<string>,
|
stream_types?: Array<string>,
|
||||||
livestreamMap: { [string]: any },
|
has_source?: boolean,
|
||||||
claimsByUri: { [string]: any },
|
has_no_source?: boolean,
|
||||||
claimSearchByQuery: { [string]: Array<string> },
|
};
|
||||||
options: any
|
|
||||||
) {
|
|
||||||
if (!livestreamMap || !uris) return;
|
|
||||||
|
|
||||||
const claimIsLive = (claim, liveChannelIds) => {
|
function urisEqual(prev: ?Array<string>, next: ?Array<string>) {
|
||||||
// This function relies on:
|
if (!prev || !next) {
|
||||||
// 1. Only 1 actual livestream per channel (i.e. all other livestream-claims
|
// ClaimList: "null" and "undefined" have special meaning,
|
||||||
// for that channel actually point to the same source).
|
// so we can't just compare array length here.
|
||||||
// 2. 'liveChannelIds' needs to be pruned after being accounted for,
|
// - null = "timed out"
|
||||||
// otherwise all livestream-claims will be "live" (we'll only take the
|
// - undefined = "no result".
|
||||||
// latest one as "live" ).
|
return prev === next;
|
||||||
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.
|
// $FlowFixMe - already checked for null above.
|
||||||
const newUris = liveUris.concat(uris.filter((uri) => !liveUris.includes(uri)));
|
return prev.length === next.length && prev.every((value, index) => value === next[index]);
|
||||||
uris.splice(0, uris.length, ...newUris);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ****************************************************************************
|
// ****************************************************************************
|
||||||
|
@ -88,8 +46,6 @@ type Props = {
|
||||||
prefixUris?: Array<string>,
|
prefixUris?: Array<string>,
|
||||||
pinUrls?: Array<string>,
|
pinUrls?: Array<string>,
|
||||||
uris: Array<string>,
|
uris: Array<string>,
|
||||||
liveLivestreamsFirst?: boolean,
|
|
||||||
livestreamMap?: { [string]: any },
|
|
||||||
showNoSourceClaims?: boolean,
|
showNoSourceClaims?: boolean,
|
||||||
renderProperties?: (Claim) => ?Node,
|
renderProperties?: (Claim) => ?Node,
|
||||||
fetchViewCount?: boolean,
|
fetchViewCount?: boolean,
|
||||||
|
@ -109,6 +65,7 @@ type Props = {
|
||||||
hasSource?: boolean,
|
hasSource?: boolean,
|
||||||
hasNoSource?: boolean,
|
hasNoSource?: boolean,
|
||||||
// --- select ---
|
// --- select ---
|
||||||
|
location: { search: string },
|
||||||
claimSearchByQuery: { [string]: Array<string> },
|
claimSearchByQuery: { [string]: Array<string> },
|
||||||
claimsByUri: { [string]: any },
|
claimsByUri: { [string]: any },
|
||||||
fetchingClaimSearchByQuery: { [string]: boolean },
|
fetchingClaimSearchByQuery: { [string]: boolean },
|
||||||
|
@ -116,6 +73,7 @@ type Props = {
|
||||||
hideReposts: boolean,
|
hideReposts: boolean,
|
||||||
mutedUris: Array<string>,
|
mutedUris: Array<string>,
|
||||||
blockedUris: Array<string>,
|
blockedUris: Array<string>,
|
||||||
|
options: SearchOptions,
|
||||||
// --- perform ---
|
// --- perform ---
|
||||||
doClaimSearch: ({}) => void,
|
doClaimSearch: ({}) => void,
|
||||||
doFetchViewCount: (claimIdCsv: string) => void,
|
doFetchViewCount: (claimIdCsv: string) => void,
|
||||||
|
@ -126,234 +84,72 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
doClaimSearch,
|
doClaimSearch,
|
||||||
claimSearchByQuery,
|
claimSearchByQuery,
|
||||||
claimsByUri,
|
claimsByUri,
|
||||||
showNsfw,
|
|
||||||
hideReposts,
|
|
||||||
fetchViewCount,
|
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,
|
fetchingClaimSearchByQuery,
|
||||||
hasSource,
|
|
||||||
hasNoSource,
|
hasNoSource,
|
||||||
renderProperties,
|
renderProperties,
|
||||||
blockedUris,
|
|
||||||
mutedUris,
|
|
||||||
liveLivestreamsFirst,
|
|
||||||
livestreamMap,
|
|
||||||
pinUrls,
|
pinUrls,
|
||||||
prefixUris,
|
prefixUris,
|
||||||
showNoSourceClaims,
|
showNoSourceClaims,
|
||||||
doFetchViewCount,
|
doFetchViewCount,
|
||||||
|
pageSize = 8,
|
||||||
|
options,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { location } = useHistory();
|
const searchKey = createNormalizedClaimSearchKey(options);
|
||||||
const urlParams = new URLSearchParams(location.search);
|
const fetchingClaimSearch = fetchingClaimSearchByQuery[searchKey];
|
||||||
const feeAmountInUrl = urlParams.get('fee_amount');
|
const claimSearchUris = claimSearchByQuery[searchKey] || [];
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't use the query from createNormalizedClaimSearchKey for the effect since that doesn't include page & release_time
|
// Don't use the query from createNormalizedClaimSearchKey for the effect since that doesn't include page & release_time
|
||||||
const optionsStringForEffect = JSON.stringify(options);
|
const optionsStringForEffect = JSON.stringify(options);
|
||||||
const shouldPerformSearch = !isLoading && uris.length === 0;
|
const shouldPerformSearch = !fetchingClaimSearch && claimSearchUris.length === 0;
|
||||||
|
|
||||||
if (
|
const uris = (prefixUris || []).concat(claimSearchUris);
|
||||||
prefixUris === undefined &&
|
|
||||||
(claimSearchByQuery[mainSearchKey] === undefined ||
|
|
||||||
(livestreamSearchKey && claimSearchByQuery[livestreamSearchKey] === undefined))
|
|
||||||
) {
|
|
||||||
// This is a new query and we don't have results yet ...
|
|
||||||
if (prevUris.length !== 0) {
|
|
||||||
// ... but we have previous results. Use it until new results are here.
|
|
||||||
uris = prevUris;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const modifiedUris = uris ? uris.slice() : [];
|
if (pinUrls && uris && uris.length > 2 && window.location.pathname === '/') {
|
||||||
const fixUris = pinUrls || [];
|
pinUrls.forEach((pin) => {
|
||||||
|
if (uris.indexOf(pin) !== -1) {
|
||||||
if (pinUrls && modifiedUris && modifiedUris.length > 2 && window.location.pathname === '/') {
|
uris.splice(uris.indexOf(pin), 1);
|
||||||
fixUris.forEach((fixUri) => {
|
|
||||||
if (modifiedUris.indexOf(fixUri) !== -1) {
|
|
||||||
modifiedUris.splice(modifiedUris.indexOf(fixUri), 1);
|
|
||||||
} else {
|
} else {
|
||||||
modifiedUris.pop();
|
uris.pop();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
modifiedUris.splice(2, 0, ...fixUris);
|
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.
|
||||||
function resolveLive(index) {
|
uris.push(...Array(pageSize - uris.length).fill(''));
|
||||||
if (liveLivestreamsFirst && livestreamMap && index < liveUris.length) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchViewCountForUris(uris) {
|
useFetchViewCount(fetchViewCount, uris, claimsByUri, doFetchViewCount);
|
||||||
const claimIds = [];
|
|
||||||
|
|
||||||
if (uris) {
|
|
||||||
uris.forEach((uri) => {
|
|
||||||
if (claimsByUri[uri]) {
|
|
||||||
claimIds.push(claimsByUri[uri].claim_id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (claimIds.length > 0) {
|
|
||||||
doFetchViewCount(claimIds.join(','));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
|
// Run `doClaimSearch`
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (shouldPerformSearch) {
|
if (shouldPerformSearch) {
|
||||||
const searchOptions = JSON.parse(optionsStringForEffect);
|
const searchOptions = JSON.parse(optionsStringForEffect);
|
||||||
doClaimSearch(searchOptions);
|
doClaimSearch(searchOptions);
|
||||||
|
|
||||||
if (liveLivestreamsFirst) {
|
|
||||||
doClaimSearch(getLivestreamOnlyOptions(searchOptions));
|
|
||||||
}
|
}
|
||||||
}
|
}, [doClaimSearch, shouldPerformSearch, optionsStringForEffect]);
|
||||||
}, [doClaimSearch, shouldPerformSearch, optionsStringForEffect, liveLivestreamsFirst]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (JSON.stringify(prevUris) !== JSON.stringify(uris) && !shouldPerformSearch) {
|
|
||||||
// Stash new results for next render cycle:
|
|
||||||
setPrevUris(uris);
|
|
||||||
// Fetch view count:
|
|
||||||
if (fetchViewCount) {
|
|
||||||
fetchViewCountForUris(uris);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [shouldPerformSearch, prevUris, uris]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="claim-grid">
|
<ul className="claim-grid">
|
||||||
{modifiedUris && modifiedUris.length
|
{uris && uris.length
|
||||||
? modifiedUris.map((uri, index) => (
|
? uris.map((uri, i) => {
|
||||||
|
if (uri) {
|
||||||
|
return (
|
||||||
<ClaimPreviewTile
|
<ClaimPreviewTile
|
||||||
showNoSourceClaims={hasNoSource || showNoSourceClaims}
|
showNoSourceClaims={hasNoSource || showNoSourceClaims}
|
||||||
key={uri}
|
key={uri}
|
||||||
uri={uri}
|
uri={uri}
|
||||||
properties={renderProperties}
|
properties={renderProperties}
|
||||||
live={resolveLive(index)}
|
|
||||||
/>
|
/>
|
||||||
))
|
);
|
||||||
|
} else {
|
||||||
|
return <ClaimPreviewTile showNoSourceClaims={hasNoSource || showNoSourceClaims} key={i} placeholder />;
|
||||||
|
}
|
||||||
|
})
|
||||||
: new Array(pageSize)
|
: new Array(pageSize)
|
||||||
.fill(1)
|
.fill(1)
|
||||||
.map((x, i) => (
|
.map((x, i) => (
|
||||||
|
@ -362,4 +158,75 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
</ul>
|
</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_COMPLETED = 'FETCH_NO_SOURCE_CLAIMS_COMPLETED';
|
||||||
export const FETCH_NO_SOURCE_CLAIMS_FAILED = 'FETCH_NO_SOURCE_CLAIMS_FAILED';
|
export const FETCH_NO_SOURCE_CLAIMS_FAILED = 'FETCH_NO_SOURCE_CLAIMS_FAILED';
|
||||||
export const VIEWERS_RECEIVED = 'VIEWERS_RECEIVED';
|
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 { connect } from 'react-redux';
|
||||||
import { SETTINGS } from 'lbry-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 { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
|
|
||||||
import ChannelsFollowingPage from './view';
|
import ChannelsFollowingPage from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = (state) => ({
|
||||||
subscribedChannels: selectSubscriptions(state),
|
subscribedChannels: selectSubscriptions(state),
|
||||||
tileLayout: makeSelectClientSetting(SETTINGS.TILE_LAYOUT)(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 Page from 'component/page';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import useGetLivestreams from 'effects/use-get-livestreams';
|
|
||||||
import { splitBySeparator } from 'lbry-redux';
|
import { splitBySeparator } from 'lbry-redux';
|
||||||
|
import { getLivestreamUris } from 'util/livestream';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
subscribedChannels: Array<Subscription>,
|
subscribedChannels: Array<Subscription>,
|
||||||
tileLayout: boolean,
|
tileLayout: boolean,
|
||||||
|
activeLivestreams: ?LivestreamInfo,
|
||||||
|
doFetchActiveLivestreams: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChannelsFollowingPage(props: Props) {
|
function ChannelsFollowingPage(props: Props) {
|
||||||
const { subscribedChannels, tileLayout } = props;
|
const { subscribedChannels, tileLayout, activeLivestreams, doFetchActiveLivestreams } = props;
|
||||||
|
|
||||||
const hasSubsribedChannels = subscribedChannels.length > 0;
|
const hasSubsribedChannels = subscribedChannels.length > 0;
|
||||||
const { livestreamMap } = useGetLivestreams();
|
const channelIds = subscribedChannels.map((sub) => splitBySeparator(sub.uri)[1]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
doFetchActiveLivestreams();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return !hasSubsribedChannels ? (
|
return !hasSubsribedChannels ? (
|
||||||
<ChannelsFollowingDiscoverPage />
|
<ChannelsFollowingDiscoverPage />
|
||||||
) : (
|
) : (
|
||||||
<Page noFooter fullWidthPage={tileLayout}>
|
<Page noFooter fullWidthPage={tileLayout}>
|
||||||
<ClaimListDiscover
|
<ClaimListDiscover
|
||||||
|
prefixUris={getLivestreamUris(activeLivestreams, channelIds)}
|
||||||
hideAdvancedFilter={SIMPLE_SITE}
|
hideAdvancedFilter={SIMPLE_SITE}
|
||||||
streamType={SIMPLE_SITE ? CS.CONTENT_ALL : undefined}
|
streamType={SIMPLE_SITE ? CS.CONTENT_ALL : undefined}
|
||||||
tileLayout={tileLayout}
|
tileLayout={tileLayout}
|
||||||
|
@ -37,7 +45,7 @@ function ChannelsFollowingPage(props: Props) {
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
defaultOrderBy={CS.ORDER_BY_NEW}
|
defaultOrderBy={CS.ORDER_BY_NEW}
|
||||||
channelIds={subscribedChannels.map((sub) => splitBySeparator(sub.uri)[1])}
|
channelIds={channelIds}
|
||||||
meta={
|
meta={
|
||||||
<Button
|
<Button
|
||||||
icon={ICONS.SEARCH}
|
icon={ICONS.SEARCH}
|
||||||
|
@ -46,8 +54,6 @@ function ChannelsFollowingPage(props: Props) {
|
||||||
navigate={`/$/${PAGES.CHANNELS_FOLLOWING_DISCOVER}`}
|
navigate={`/$/${PAGES.CHANNELS_FOLLOWING_DISCOVER}`}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
liveLivestreamsFirst
|
|
||||||
livestreamMap={livestreamMap}
|
|
||||||
showNoSourceClaims={ENABLE_NO_SOURCE_CLAIMS}
|
showNoSourceClaims={ENABLE_NO_SOURCE_CLAIMS}
|
||||||
hasSource
|
hasSource
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import * as CS from 'constants/claim_search';
|
import * as CS from 'constants/claim_search';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectClaimForUri, doResolveUri, SETTINGS } from 'lbry-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 { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
import { selectFollowedTags } from 'redux/selectors/tags';
|
import { selectFollowedTags } from 'redux/selectors/tags';
|
||||||
import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
||||||
|
@ -18,10 +20,12 @@ const select = (state, props) => {
|
||||||
repostedClaim: repostedUri ? makeSelectClaimForUri(repostedUri)(state) : null,
|
repostedClaim: repostedUri ? makeSelectClaimForUri(repostedUri)(state) : null,
|
||||||
isAuthenticated: selectUserVerifiedEmail(state),
|
isAuthenticated: selectUserVerifiedEmail(state),
|
||||||
tileLayout: makeSelectClientSetting(SETTINGS.TILE_LAYOUT)(state),
|
tileLayout: makeSelectClientSetting(SETTINGS.TILE_LAYOUT)(state),
|
||||||
|
activeLivestreams: selectActiveLivestreams(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(select, {
|
export default connect(select, {
|
||||||
doToggleTagFollowDesktop,
|
doToggleTagFollowDesktop,
|
||||||
doResolveUri,
|
doResolveUri,
|
||||||
|
doFetchActiveLivestreams,
|
||||||
})(Tags);
|
})(Tags);
|
||||||
|
|
|
@ -8,15 +8,17 @@ import Page from 'component/page';
|
||||||
import ClaimListDiscover from 'component/claimListDiscover';
|
import ClaimListDiscover from 'component/claimListDiscover';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import useHover from 'effects/use-hover';
|
import useHover from 'effects/use-hover';
|
||||||
import { useIsMobile } from 'effects/use-screensize';
|
import { useIsMobile, useIsLargeScreen } from 'effects/use-screensize';
|
||||||
import analytics from 'analytics';
|
import analytics from 'analytics';
|
||||||
import HiddenNsfw from 'component/common/hidden-nsfw';
|
import HiddenNsfw from 'component/common/hidden-nsfw';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import Ads from 'web/component/ads';
|
import Ads from 'web/component/ads';
|
||||||
import LbcSymbol from 'component/common/lbc-symbol';
|
import LbcSymbol from 'component/common/lbc-symbol';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import useGetLivestreams from 'effects/use-get-livestreams';
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { getLivestreamUris } from 'util/livestream';
|
||||||
|
|
||||||
|
const DEFAULT_LIVESTREAM_TILE_LIMIT = 8;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
location: { search: string },
|
location: { search: string },
|
||||||
|
@ -28,6 +30,8 @@ type Props = {
|
||||||
isAuthenticated: boolean,
|
isAuthenticated: boolean,
|
||||||
dynamicRouteProps: RowDataItem,
|
dynamicRouteProps: RowDataItem,
|
||||||
tileLayout: boolean,
|
tileLayout: boolean,
|
||||||
|
activeLivestreams: ?LivestreamInfo,
|
||||||
|
doFetchActiveLivestreams: (orderBy?: Array<string>, pageSize?: number, forceFetch?: boolean) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function DiscoverPage(props: Props) {
|
function DiscoverPage(props: Props) {
|
||||||
|
@ -40,12 +44,14 @@ function DiscoverPage(props: Props) {
|
||||||
doResolveUri,
|
doResolveUri,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
tileLayout,
|
tileLayout,
|
||||||
|
activeLivestreams,
|
||||||
|
doFetchActiveLivestreams,
|
||||||
dynamicRouteProps,
|
dynamicRouteProps,
|
||||||
} = props;
|
} = props;
|
||||||
const buttonRef = useRef();
|
const buttonRef = useRef();
|
||||||
const isHovering = useHover(buttonRef);
|
const isHovering = useHover(buttonRef);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const { livestreamMap } = useGetLivestreams();
|
const isLargeScreen = useIsLargeScreen();
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(search);
|
const urlParams = new URLSearchParams(search);
|
||||||
const claimType = urlParams.get('claim_type');
|
const claimType = urlParams.get('claim_type');
|
||||||
|
@ -58,6 +64,8 @@ function DiscoverPage(props: Props) {
|
||||||
// Eventually allow more than one tag on this page
|
// Eventually allow more than one tag on this page
|
||||||
// Restricting to one to make follow/unfollow simpler
|
// Restricting to one to make follow/unfollow simpler
|
||||||
const tag = (tags && tags[0]) || null;
|
const tag = (tags && tags[0]) || null;
|
||||||
|
const channelIds =
|
||||||
|
(dynamicRouteProps && dynamicRouteProps.options && dynamicRouteProps.options.channelIds) || undefined;
|
||||||
|
|
||||||
const isFollowing = followedTags.map(({ name }) => name).includes(tag);
|
const isFollowing = followedTags.map(({ name }) => name).includes(tag);
|
||||||
let label = isFollowing ? __('Following --[button label indicating a channel has been followed]--') : __('Follow');
|
let label = isFollowing ? __('Following --[button label indicating a channel has been followed]--') : __('Follow');
|
||||||
|
@ -65,6 +73,46 @@ function DiscoverPage(props: Props) {
|
||||||
label = __('Unfollow');
|
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(() => {
|
React.useEffect(() => {
|
||||||
if (repostedUri && !repostedClaimIsResolved) {
|
if (repostedUri && !repostedClaimIsResolved) {
|
||||||
doResolveUri(repostedUri);
|
doResolveUri(repostedUri);
|
||||||
|
@ -106,16 +154,53 @@ function DiscoverPage(props: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (showViewMoreLivestreams) {
|
||||||
|
doFetchActiveLivestreams(CS.ORDER_BY_TRENDING_VALUE);
|
||||||
|
} else {
|
||||||
|
doFetchActiveLivestreams();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page noFooter fullWidthPage={tileLayout}>
|
<Page noFooter fullWidthPage={tileLayout}>
|
||||||
|
{useDualList && (
|
||||||
|
<>
|
||||||
<ClaimListDiscover
|
<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}
|
hideAdvancedFilter={SIMPLE_SITE}
|
||||||
hideFilters={SIMPLE_SITE ? !dynamicRouteProps : undefined}
|
hideFilters={SIMPLE_SITE ? !dynamicRouteProps : undefined}
|
||||||
header={repostedUri ? <span /> : undefined}
|
header={useDualList ? <span /> : repostedUri ? <span /> : undefined}
|
||||||
tileLayout={repostedUri ? false : tileLayout}
|
tileLayout={repostedUri ? false : tileLayout}
|
||||||
defaultOrderBy={SIMPLE_SITE ? (dynamicRouteProps ? undefined : CS.ORDER_BY_TRENDING) : undefined}
|
defaultOrderBy={SIMPLE_SITE ? (dynamicRouteProps ? undefined : CS.ORDER_BY_TRENDING) : undefined}
|
||||||
claimType={claimType ? [claimType] : undefined}
|
claimType={claimType ? [claimType] : undefined}
|
||||||
headerLabel={headerLabel}
|
headerLabel={!useDualList && headerLabel}
|
||||||
tags={tags}
|
tags={tags}
|
||||||
hiddenNsfwMessage={<HiddenNsfw type="page" />}
|
hiddenNsfwMessage={<HiddenNsfw type="page" />}
|
||||||
repostedClaimId={repostedClaim ? repostedClaim.claim_id : null}
|
repostedClaimId={repostedClaim ? repostedClaim.claim_id : null}
|
||||||
|
@ -133,47 +218,14 @@ function DiscoverPage(props: Props) {
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
feeAmount={SIMPLE_SITE ? !dynamicRouteProps && CS.FEE_AMOUNT_ANY : undefined}
|
feeAmount={SIMPLE_SITE ? !dynamicRouteProps && CS.FEE_AMOUNT_ANY : undefined}
|
||||||
channelIds={
|
channelIds={channelIds}
|
||||||
(dynamicRouteProps && dynamicRouteProps.options && dynamicRouteProps.options.channelIds) || undefined
|
|
||||||
}
|
|
||||||
limitClaimsPerChannel={
|
limitClaimsPerChannel={
|
||||||
SIMPLE_SITE
|
SIMPLE_SITE
|
||||||
? (dynamicRouteProps && dynamicRouteProps.options && dynamicRouteProps.options.limitClaimsPerChannel) ||
|
? (dynamicRouteProps && dynamicRouteProps.options && dynamicRouteProps.options.limitClaimsPerChannel) ||
|
||||||
undefined
|
undefined
|
||||||
: 3
|
: 3
|
||||||
}
|
}
|
||||||
meta={
|
meta={!useDualList && getElemMeta()}
|
||||||
!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}
|
|
||||||
hasSource
|
hasSource
|
||||||
showNoSourceClaims={ENABLE_NO_SOURCE_CLAIMS}
|
showNoSourceClaims={ENABLE_NO_SOURCE_CLAIMS}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { doFetchActiveLivestreams } from 'redux/actions/livestream';
|
||||||
|
import { selectActiveLivestreams } from 'redux/selectors/livestream';
|
||||||
import { selectFollowedTags } from 'redux/selectors/tags';
|
import { selectFollowedTags } from 'redux/selectors/tags';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||||
|
@ -12,8 +14,11 @@ const select = (state) => ({
|
||||||
authenticated: selectUserVerifiedEmail(state),
|
authenticated: selectUserVerifiedEmail(state),
|
||||||
showNsfw: selectShowMatureContent(state),
|
showNsfw: selectShowMatureContent(state),
|
||||||
homepageData: selectHomepageData(state),
|
homepageData: selectHomepageData(state),
|
||||||
|
activeLivestreams: selectActiveLivestreams(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = {};
|
const perform = (dispatch) => ({
|
||||||
|
doFetchActiveLivestreams: () => dispatch(doFetchActiveLivestreams()),
|
||||||
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(DiscoverPage);
|
export default connect(select, perform)(DiscoverPage);
|
||||||
|
|
|
@ -9,8 +9,8 @@ import ClaimTilesDiscover from 'component/claimTilesDiscover';
|
||||||
import ClaimPreviewTile from 'component/claimPreviewTile';
|
import ClaimPreviewTile from 'component/claimPreviewTile';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import WaitUntilOnPage from 'component/common/wait-until-on-page';
|
import WaitUntilOnPage from 'component/common/wait-until-on-page';
|
||||||
import useGetLivestreams from 'effects/use-get-livestreams';
|
|
||||||
import { GetLinksData } from 'util/buildHomepage';
|
import { GetLinksData } from 'util/buildHomepage';
|
||||||
|
import { getLivestreamUris } from 'util/livestream';
|
||||||
|
|
||||||
// @if TARGET='web'
|
// @if TARGET='web'
|
||||||
import Pixel from 'web/component/pixel';
|
import Pixel from 'web/component/pixel';
|
||||||
|
@ -23,14 +23,23 @@ type Props = {
|
||||||
subscribedChannels: Array<Subscription>,
|
subscribedChannels: Array<Subscription>,
|
||||||
showNsfw: boolean,
|
showNsfw: boolean,
|
||||||
homepageData: any,
|
homepageData: any,
|
||||||
|
activeLivestreams: any,
|
||||||
|
doFetchActiveLivestreams: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function HomePage(props: Props) {
|
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 showPersonalizedChannels = (authenticated || !IS_WEB) && subscribedChannels && subscribedChannels.length > 0;
|
||||||
const showPersonalizedTags = (authenticated || !IS_WEB) && followedTags && followedTags.length > 0;
|
const showPersonalizedTags = (authenticated || !IS_WEB) && followedTags && followedTags.length > 0;
|
||||||
const showIndividualTags = showPersonalizedTags && followedTags.length < 5;
|
const showIndividualTags = showPersonalizedTags && followedTags.length < 5;
|
||||||
const { livestreamMap } = useGetLivestreams();
|
|
||||||
|
|
||||||
const rowData: Array<RowDataItem> = GetLinksData(
|
const rowData: Array<RowDataItem> = GetLinksData(
|
||||||
homepageData,
|
homepageData,
|
||||||
|
@ -52,13 +61,13 @@ function HomePage(props: Props) {
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
|
|
||||||
const claimTiles = (
|
const claimTiles = (
|
||||||
<ClaimTilesDiscover
|
<ClaimTilesDiscover
|
||||||
{...options}
|
{...options}
|
||||||
liveLivestreamsFirst
|
|
||||||
livestreamMap={livestreamMap}
|
|
||||||
showNoSourceClaims={ENABLE_NO_SOURCE_CLAIMS}
|
showNoSourceClaims={ENABLE_NO_SOURCE_CLAIMS}
|
||||||
hasSource
|
hasSource
|
||||||
|
prefixUris={getLivestreamUris(activeLivestreams, options.channelIds)}
|
||||||
pinUrls={pinUrls}
|
pinUrls={pinUrls}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -95,6 +104,10 @@ function HomePage(props: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
doFetchActiveLivestreams();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page fullWidthPage>
|
<Page fullWidthPage>
|
||||||
{!SIMPLE_SITE && (authenticated || !IS_WEB) && !subscribedChannels.length && (
|
{!SIMPLE_SITE && (authenticated || !IS_WEB) && !subscribedChannels.length && (
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
import { doClaimSearch } from 'lbry-redux';
|
import { doClaimSearch } from 'lbry-redux';
|
||||||
|
import { LIVESTREAM_LIVE_API } from 'constants/livestream';
|
||||||
|
|
||||||
export const doFetchNoSourceClaims = (channelId: string) => async (dispatch: Dispatch, getState: GetState) => {
|
export const doFetchNoSourceClaims = (channelId: string) => async (dispatch: Dispatch, getState: GetState) => {
|
||||||
dispatch({
|
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 = {
|
const defaultState: LivestreamState = {
|
||||||
fetchingById: {},
|
fetchingById: {},
|
||||||
viewersById: {},
|
viewersById: {},
|
||||||
|
fetchingActiveLivestreams: false,
|
||||||
|
activeLivestreams: null,
|
||||||
|
activeLivestreamsLastFetchedDate: 0,
|
||||||
|
activeLivestreamsLastFetchedOptions: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handleActions(
|
export default handleActions(
|
||||||
|
@ -36,6 +40,22 @@ export default handleActions(
|
||||||
newViewersById[claimId] = connected;
|
newViewersById[claimId] = connected;
|
||||||
return { ...state, viewersById: newViewersById };
|
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
|
defaultState
|
||||||
);
|
);
|
||||||
|
|
|
@ -40,3 +40,23 @@ export const makeSelectPendingLivestreamsForChannelId = (channelId: string) =>
|
||||||
claim.signing_channel.claim_id === channelId
|
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;
|
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
|
* getUriForSearchTerm
|
||||||
* @param term
|
* @param term
|
||||||
|
|
Loading…
Reference in a new issue