31c6ce6426
Use trending group instead of trending global. Mixed already takes into account global. This index is now available on the wallet servers. huh
304 lines
9.2 KiB
JavaScript
304 lines
9.2 KiB
JavaScript
// @flow
|
|
import type { Node } from 'react';
|
|
import React, { Fragment, useEffect, useState } from 'react';
|
|
import { withRouter } from 'react-router';
|
|
import { createNormalizedClaimSearchKey, MATURE_TAGS } from 'lbry-redux';
|
|
import { FormField } from 'component/common/form';
|
|
import Button from 'component/button';
|
|
import moment from 'moment';
|
|
import ClaimList from 'component/claimList';
|
|
import Tag from 'component/tag';
|
|
import ClaimPreview from 'component/claimPreview';
|
|
import { toCapitalCase } from 'util/string';
|
|
import I18nMessage from 'component/i18nMessage';
|
|
|
|
const PAGE_SIZE = 20;
|
|
const TIME_DAY = 'day';
|
|
const TIME_WEEK = 'week';
|
|
const TIME_MONTH = 'month';
|
|
const TIME_YEAR = 'year';
|
|
const TIME_ALL = 'all';
|
|
const SEARCH_SORT_YOU = 'you';
|
|
const SEARCH_SORT_ALL = 'everyone';
|
|
const SEARCH_SORT_CHANNELS = 'channels';
|
|
|
|
const TYPE_TRENDING = 'trending';
|
|
const TYPE_TOP = 'top';
|
|
const TYPE_NEW = 'new';
|
|
const SEARCH_FILTER_TYPES = [SEARCH_SORT_YOU, SEARCH_SORT_CHANNELS, SEARCH_SORT_ALL];
|
|
const SEARCH_TYPES = [TYPE_TRENDING, TYPE_TOP, TYPE_NEW];
|
|
const SEARCH_TIMES = [TIME_DAY, TIME_WEEK, TIME_MONTH, TIME_YEAR, TIME_ALL];
|
|
|
|
type Props = {
|
|
uris: Array<string>,
|
|
subscribedChannels: Array<Subscription>,
|
|
doClaimSearch: ({}) => void,
|
|
tags: Array<string>,
|
|
loading: boolean,
|
|
personalView: boolean,
|
|
doToggleTagFollow: string => void,
|
|
meta?: Node,
|
|
showNsfw: boolean,
|
|
hideCustomization: boolean,
|
|
history: { action: string, push: string => void, replace: string => void },
|
|
location: { search: string, pathname: string },
|
|
claimSearchByQuery: {
|
|
[string]: Array<string>,
|
|
},
|
|
hiddenUris: Array<string>,
|
|
hiddenNsfwMessage?: Node,
|
|
};
|
|
|
|
function ClaimListDiscover(props: Props) {
|
|
const {
|
|
doClaimSearch,
|
|
claimSearchByQuery,
|
|
tags,
|
|
loading,
|
|
personalView,
|
|
meta,
|
|
subscribedChannels,
|
|
showNsfw,
|
|
history,
|
|
location,
|
|
hiddenUris,
|
|
hideCustomization,
|
|
hiddenNsfwMessage,
|
|
} = props;
|
|
const didNavigateForward = history.action === 'PUSH';
|
|
const [page, setPage] = useState(1);
|
|
const { search } = location;
|
|
const [forceRefresh, setForceRefresh] = useState();
|
|
const urlParams = new URLSearchParams(search);
|
|
const personalSort = urlParams.get('sort') || (hideCustomization ? SEARCH_SORT_ALL : SEARCH_SORT_YOU);
|
|
const typeSort = urlParams.get('type') || TYPE_TRENDING;
|
|
const timeSort = urlParams.get('time') || TIME_WEEK;
|
|
const tagsInUrl = urlParams.get('t') || '';
|
|
const options: {
|
|
page_size: number,
|
|
page: number,
|
|
no_totals: boolean,
|
|
any_tags: Array<string>,
|
|
channel_ids: Array<string>,
|
|
not_channel_ids: Array<string>,
|
|
not_tags: Array<string>,
|
|
order_by: Array<string>,
|
|
release_time?: string,
|
|
} = {
|
|
page_size: PAGE_SIZE,
|
|
page,
|
|
// 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: (personalView && !hideCustomization && personalSort === SEARCH_SORT_YOU) || !personalView ? tags : [],
|
|
channel_ids: personalSort === SEARCH_SORT_CHANNELS ? subscribedChannels.map(sub => sub.uri.split('#')[1]) : [],
|
|
not_channel_ids: hiddenUris && hiddenUris.length ? hiddenUris.map(hiddenUri => hiddenUri.split('#')[1]) : [],
|
|
not_tags: !showNsfw ? MATURE_TAGS : [],
|
|
order_by:
|
|
typeSort === TYPE_TRENDING
|
|
? ['trending_group', 'trending_mixed']
|
|
: typeSort === TYPE_NEW
|
|
? ['release_time']
|
|
: ['effective_amount'], // Sort by top
|
|
};
|
|
|
|
if (typeSort === TYPE_TOP && timeSort !== TIME_ALL) {
|
|
options.release_time = `>${Math.floor(
|
|
moment()
|
|
.subtract(1, timeSort)
|
|
.unix()
|
|
)}`;
|
|
}
|
|
const hasContent =
|
|
(personalSort === SEARCH_SORT_CHANNELS && subscribedChannels.length) ||
|
|
(personalSort === SEARCH_SORT_YOU && !!tags.length) ||
|
|
personalSort === SEARCH_SORT_ALL;
|
|
const hasMatureTags = tags.some(t => MATURE_TAGS.includes(t));
|
|
const claimSearchCacheQuery = createNormalizedClaimSearchKey(options);
|
|
const uris = (hasContent && claimSearchByQuery[claimSearchCacheQuery]) || [];
|
|
const shouldPerformSearch =
|
|
hasContent &&
|
|
(uris.length === 0 ||
|
|
didNavigateForward ||
|
|
(!loading && uris.length < PAGE_SIZE * page && uris.length % PAGE_SIZE === 0));
|
|
// Don't use the query from createNormalizedClaimSearchKey for the effect since that doesn't include page & release_time
|
|
const optionsStringForEffect = JSON.stringify(options);
|
|
|
|
const noChannels = (
|
|
<div>
|
|
<p>
|
|
<I18nMessage>You're not following any channels.</I18nMessage>
|
|
</p>
|
|
<p>
|
|
<I18nMessage
|
|
tokens={{
|
|
trending: (
|
|
<Button button="link" label={__('trending for everyone')} navigate={'/?type=trending&sort=everyone'} />
|
|
),
|
|
discover: <Button button="link" label={__('discover some channels!')} navigate={'/$/following'} />,
|
|
}}
|
|
>
|
|
Look what's %trending% or %discover%
|
|
</I18nMessage>
|
|
</p>
|
|
</div>
|
|
);
|
|
|
|
const noResults = (
|
|
<div>
|
|
<p>
|
|
<I18nMessage
|
|
tokens={{
|
|
again: (
|
|
<Button
|
|
button="link"
|
|
label={__('Please try again in a few seconds.')}
|
|
onClick={() => setForceRefresh(Date.now())}
|
|
/>
|
|
),
|
|
}}
|
|
>
|
|
Sorry, your request timed out. %again%
|
|
</I18nMessage>
|
|
</p>
|
|
<p>
|
|
<I18nMessage
|
|
tokens={{
|
|
support: <Button button="link" label={__('contact support')} href="https://lbry.com/faq/support" />,
|
|
}}
|
|
>
|
|
If you continue to have issues, please %support%.
|
|
</I18nMessage>
|
|
</p>
|
|
</div>
|
|
);
|
|
|
|
const emptyState = !loading && (personalSort === SEARCH_SORT_CHANNELS && !hasContent ? noChannels : noResults);
|
|
|
|
function getSearch() {
|
|
let search = `?`;
|
|
if (!personalView) {
|
|
search += `t=${tagsInUrl}&`;
|
|
}
|
|
|
|
return search;
|
|
}
|
|
|
|
function handleTypeSort(newTypeSort) {
|
|
let url = `${getSearch()}type=${newTypeSort}&sort=${personalSort}`;
|
|
if (newTypeSort === TYPE_TOP) {
|
|
url += `&time=${timeSort}`;
|
|
}
|
|
|
|
setPage(1);
|
|
history.push(url);
|
|
}
|
|
|
|
function handlePersonalSort(newPersonalSort) {
|
|
setPage(1);
|
|
history.push(`${getSearch()}type=${typeSort}&sort=${newPersonalSort}`);
|
|
}
|
|
|
|
function handleTimeSort(newTimeSort) {
|
|
setPage(1);
|
|
history.push(`${getSearch()}type=${typeSort}&sort=${personalSort}&time=${newTimeSort}`);
|
|
}
|
|
|
|
function handleScrollBottom() {
|
|
if (!loading) {
|
|
setPage(page + 1);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (shouldPerformSearch) {
|
|
const searchOptions = JSON.parse(optionsStringForEffect);
|
|
doClaimSearch(searchOptions);
|
|
}
|
|
}, [doClaimSearch, shouldPerformSearch, optionsStringForEffect, forceRefresh]);
|
|
|
|
const header = (
|
|
<Fragment>
|
|
<FormField
|
|
className="claim-list__dropdown"
|
|
type="select"
|
|
name="trending_sort"
|
|
value={typeSort}
|
|
onChange={e => handleTypeSort(e.target.value)}
|
|
>
|
|
{SEARCH_TYPES.map(type => (
|
|
<option key={type} value={type}>
|
|
{__(toCapitalCase(type))}
|
|
</option>
|
|
))}
|
|
</FormField>
|
|
{!hideCustomization && (
|
|
<Fragment>
|
|
<span>{__('For')}</span>
|
|
{!personalView && tags && tags.length ? (
|
|
tags.map(tag => <Tag key={tag} name={tag} disabled />)
|
|
) : (
|
|
<FormField
|
|
type="select"
|
|
name="trending_overview"
|
|
className="claim-list__dropdown"
|
|
value={personalSort}
|
|
onChange={e => {
|
|
handlePersonalSort(e.target.value);
|
|
}}
|
|
>
|
|
{SEARCH_FILTER_TYPES.map(type => (
|
|
<option key={type} value={type}>
|
|
{type === SEARCH_SORT_ALL
|
|
? __('Everyone')
|
|
: type === SEARCH_SORT_YOU
|
|
? __('Tags You Follow')
|
|
: __('Channels You Follow')}
|
|
</option>
|
|
))}
|
|
</FormField>
|
|
)}
|
|
</Fragment>
|
|
)}
|
|
{typeSort === 'top' && (
|
|
<FormField
|
|
className="claim-list__dropdown"
|
|
type="select"
|
|
name="trending_time"
|
|
value={timeSort}
|
|
onChange={e => handleTimeSort(e.target.value)}
|
|
>
|
|
{SEARCH_TIMES.map(time => (
|
|
<option key={time} value={time}>
|
|
{/* i18fixme */}
|
|
{time === TIME_DAY && __('Today')}
|
|
{time !== TIME_ALL && time !== TIME_DAY && `${__('This')} ${toCapitalCase(time)}`}
|
|
{time === TIME_ALL && __('All time')}
|
|
</option>
|
|
))}
|
|
</FormField>
|
|
)}
|
|
{hasMatureTags && hiddenNsfwMessage}
|
|
</Fragment>
|
|
);
|
|
|
|
return (
|
|
<div className="card">
|
|
<ClaimList
|
|
id={claimSearchCacheQuery}
|
|
loading={loading}
|
|
uris={uris}
|
|
header={header}
|
|
headerAltControls={meta}
|
|
onScrollBottom={handleScrollBottom}
|
|
page={page}
|
|
pageSize={PAGE_SIZE}
|
|
empty={emptyState}
|
|
/>
|
|
|
|
{loading && new Array(PAGE_SIZE).fill(1).map((x, i) => <ClaimPreview key={i} placeholder="loading" />)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default withRouter(ClaimListDiscover);
|