mirror of
https://github.com/LBRYFoundation/lbry-desktop.git
synced 2025-08-31 01:11:26 +00:00
changes almost done wip wip more changes after comment detect custom qs and show options ux and mobile styling bugfix console logs appstrings
479 lines
16 KiB
JavaScript
479 lines
16 KiB
JavaScript
// @flow
|
|
import type { Node } from 'react';
|
|
import classnames from 'classnames';
|
|
import React, { Fragment, useEffect, useState } from 'react';
|
|
import { withRouter } from 'react-router';
|
|
import * as CS from 'constants/claim_search';
|
|
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 ClaimPreview from 'component/claimPreview';
|
|
import { toCapitalCase } from 'util/string';
|
|
import I18nMessage from 'component/i18nMessage';
|
|
import * as ICONS from 'constants/icons';
|
|
|
|
type Props = {
|
|
uris: Array<string>,
|
|
subscribedChannels: Array<Subscription>,
|
|
doClaimSearch: ({}) => void,
|
|
loading: boolean,
|
|
personalView: boolean,
|
|
doToggleTagFollow: string => void,
|
|
meta?: Node,
|
|
showNsfw: boolean,
|
|
showReposts: boolean,
|
|
history: { action: string, push: string => void, replace: string => void },
|
|
location: { search: string, pathname: string },
|
|
claimSearchByQuery: {
|
|
[string]: Array<string>,
|
|
},
|
|
hiddenUris: Array<string>,
|
|
hiddenNsfwMessage?: Node,
|
|
channelIds?: Array<string>,
|
|
tags: Array<string>,
|
|
orderBy?: Array<string>,
|
|
defaultOrderBy?: string,
|
|
freshness?: string,
|
|
defaultFreshness?: string,
|
|
header?: Node,
|
|
headerLabel?: string | Node,
|
|
name?: string,
|
|
hideBlock?: boolean,
|
|
claimType?: string | Array<string>,
|
|
defaultClaimType?: string | Array<string>,
|
|
streamType?: string | Array<string>,
|
|
defaultStreamType?: string | Array<string>,
|
|
renderProperties?: Claim => Node,
|
|
includeSupportAction?: boolean,
|
|
noCustom?: boolean,
|
|
};
|
|
|
|
function ClaimListDiscover(props: Props) {
|
|
const {
|
|
doClaimSearch,
|
|
claimSearchByQuery,
|
|
tags,
|
|
loading,
|
|
meta,
|
|
channelIds,
|
|
showNsfw,
|
|
showReposts,
|
|
history,
|
|
location,
|
|
hiddenUris,
|
|
hiddenNsfwMessage,
|
|
defaultOrderBy,
|
|
orderBy,
|
|
headerLabel,
|
|
header,
|
|
name,
|
|
claimType,
|
|
pageSize,
|
|
hideBlock,
|
|
defaultClaimType,
|
|
streamType,
|
|
defaultStreamType,
|
|
freshness,
|
|
defaultFreshness,
|
|
renderProperties,
|
|
includeSupportAction,
|
|
noCustom,
|
|
} = props;
|
|
const didNavigateForward = history.action === 'PUSH';
|
|
const { search } = location;
|
|
|
|
const [page, setPage] = useState(1);
|
|
const [forceRefresh, setForceRefresh] = useState();
|
|
const [expanded, setExpanded] = useState(false);
|
|
|
|
const urlParams = new URLSearchParams(search);
|
|
const tagsParam = tags || urlParams.get(CS.TAGS_KEY) || null;
|
|
const orderParam = orderBy || urlParams.get(CS.ORDER_BY_KEY) || defaultOrderBy || CS.ORDER_BY_TRENDING;
|
|
const freshnessParam = freshness || urlParams.get(CS.FRESH_KEY) || defaultFreshness || CS.FRESH_WEEK;
|
|
const contentTypeParam = urlParams.get(CS.CONTENT_KEY);
|
|
const claimTypeParam =
|
|
claimType || (CS.CLAIM_TYPES.includes(contentTypeParam) && contentTypeParam) || defaultClaimType || null;
|
|
const streamTypeParam =
|
|
streamType || (CS.FILE_TYPES.includes(contentTypeParam) && contentTypeParam) || defaultStreamType || null;
|
|
const durationParam = urlParams.get(CS.DURATION_KEY) || null;
|
|
|
|
const isFiltered = () =>
|
|
Boolean(urlParams.get(CS.FRESH_KEY) || urlParams.get(CS.CONTENT_KEY) || urlParams.get(CS.DURATION_KEY));
|
|
|
|
useEffect(() => {
|
|
if (isFiltered()) setExpanded(true);
|
|
}, []);
|
|
|
|
const options: {
|
|
page_size: number,
|
|
page: number,
|
|
no_totals: boolean,
|
|
any_tags: Array<string>,
|
|
not_tags: Array<string>,
|
|
channel_ids: Array<string>,
|
|
not_channel_ids: Array<string>,
|
|
order_by: Array<string>,
|
|
release_time?: string,
|
|
claim_type?: Array<string>,
|
|
name?: string,
|
|
claim_type?: Array<string>,
|
|
duration?: string,
|
|
claim_type?: string | Array<string>,
|
|
stream_types?: any,
|
|
} = {
|
|
page_size: pageSize || CS.PAGE_SIZE,
|
|
page,
|
|
name,
|
|
claim_type: claimType || undefined,
|
|
// 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: tagsParam || [],
|
|
channel_ids: channelIds || [],
|
|
not_channel_ids:
|
|
// If channelIds were passed in, we don't need not_channel_ids
|
|
!channelIds && hiddenUris && hiddenUris.length ? hiddenUris.map(hiddenUri => hiddenUri.split('#')[1]) : [],
|
|
not_tags: !showNsfw ? MATURE_TAGS : [],
|
|
order_by:
|
|
orderParam === CS.ORDER_BY_TRENDING
|
|
? CS.ORDER_BY_TRENDING_VALUE
|
|
: orderParam === CS.ORDER_BY_NEW
|
|
? CS.ORDER_BY_NEW_VALUE
|
|
: CS.ORDER_BY_TOP_VALUE, // Sort by top
|
|
};
|
|
if (orderParam === CS.ORDER_BY_TOP && freshnessParam !== CS.FRESH_ALL) {
|
|
options.release_time = `>${Math.floor(
|
|
moment()
|
|
.subtract(1, freshnessParam)
|
|
.startOf('hour')
|
|
.unix()
|
|
)}`;
|
|
} else if (orderParam === CS.ORDER_BY_NEW || orderParam === CS.ORDER_BY_TRENDING) {
|
|
// Warning - hack below
|
|
// If users are following more than 10 channels or tags, limit results to stuff less than a year old
|
|
// For more than 20, drop it down to 6 months
|
|
// This helps with timeout issues for users that are following a ton of stuff
|
|
// https://github.com/lbryio/lbry-sdk/issues/2420
|
|
if (options.channel_ids.length > 20 || options.any_tags.length > 20) {
|
|
options.release_time = `>${Math.floor(
|
|
moment()
|
|
.subtract(3, CS.FRESH_MONTH)
|
|
.startOf('week')
|
|
.unix()
|
|
)}`;
|
|
} else if (options.channel_ids.length > 10 || options.any_tags.length > 10) {
|
|
options.release_time = `>${Math.floor(
|
|
moment()
|
|
.subtract(1, CS.FRESH_YEAR)
|
|
.startOf('week')
|
|
.unix()
|
|
)}`;
|
|
} else {
|
|
// Hack for at least the New page until https://github.com/lbryio/lbry-sdk/issues/2591 is fixed
|
|
options.release_time = `<${Math.floor(
|
|
moment()
|
|
.startOf('minute')
|
|
.unix()
|
|
)}`;
|
|
}
|
|
}
|
|
|
|
if (durationParam) {
|
|
if (durationParam === CS.DURATION_SHORT) {
|
|
options.duration = '<=1800';
|
|
} else if (durationParam === CS.DURATION_LONG) {
|
|
options.duration = '>=1800';
|
|
}
|
|
}
|
|
|
|
if (streamTypeParam) {
|
|
if (streamTypeParam !== CS.CONTENT_ALL) {
|
|
options.stream_types = [streamTypeParam];
|
|
}
|
|
}
|
|
|
|
if (claimTypeParam) {
|
|
if (claimTypeParam !== CS.CONTENT_ALL) {
|
|
options.claim_type = [claimTypeParam];
|
|
}
|
|
}
|
|
|
|
if (!showReposts) {
|
|
options.claim_type =
|
|
options.claim_type === undefined
|
|
? ['stream', 'channel']
|
|
: options.claim_type.filter(claimType => claimType !== 'repost');
|
|
}
|
|
|
|
const hasMatureTags = tags && tags.some(t => MATURE_TAGS.includes(t));
|
|
const claimSearchCacheQuery = createNormalizedClaimSearchKey(options);
|
|
const uris = claimSearchByQuery[claimSearchCacheQuery] || [];
|
|
const shouldPerformSearch =
|
|
uris.length === 0 ||
|
|
didNavigateForward ||
|
|
(!loading && uris.length < CS.PAGE_SIZE * page && uris.length % CS.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 noResults = (
|
|
<div>
|
|
<p>
|
|
<I18nMessage
|
|
tokens={{
|
|
again: (
|
|
<Button
|
|
button="link"
|
|
label={__('try again in a few seconds.')}
|
|
onClick={() => setForceRefresh(Date.now())}
|
|
/>
|
|
),
|
|
}}
|
|
>
|
|
Sorry, your request timed out. Modify your options or %again%
|
|
</I18nMessage>
|
|
</p>
|
|
<p>
|
|
<I18nMessage
|
|
tokens={{
|
|
contact_support: <Button button="link" label={__('contact support')} href="https://lbry.com/faq/support" />,
|
|
}}
|
|
>
|
|
If you continue to have issues, please %contact_support%.
|
|
</I18nMessage>
|
|
</p>
|
|
</div>
|
|
);
|
|
|
|
function handleChange(change) {
|
|
const url = buildUrl(change);
|
|
setPage(1);
|
|
history.push(url);
|
|
}
|
|
|
|
function buildUrl(delta) {
|
|
const newUrlParams = new URLSearchParams();
|
|
CS.KEYS.forEach(k => {
|
|
// $FlowFixMe append() can't take null as second arg, but get() can return null
|
|
if (urlParams.get(k) !== null) newUrlParams.append(k, urlParams.get(k));
|
|
});
|
|
|
|
switch (delta.key) {
|
|
case CS.ORDER_BY_KEY:
|
|
newUrlParams.set(CS.ORDER_BY_KEY, delta.value);
|
|
break;
|
|
case CS.FRESH_KEY:
|
|
newUrlParams.set(CS.FRESH_KEY, delta.value);
|
|
break;
|
|
case CS.CONTENT_KEY:
|
|
if (delta.value === CS.CLAIM_CHANNEL || delta.value === CS.CLAIM_REPOST) {
|
|
newUrlParams.delete(CS.DURATION_KEY);
|
|
newUrlParams.set(CS.CONTENT_KEY, delta.value);
|
|
} else if (delta.value === CS.CONTENT_ALL) {
|
|
newUrlParams.delete(CS.CONTENT_KEY);
|
|
} else {
|
|
newUrlParams.set(CS.CONTENT_KEY, delta.value);
|
|
}
|
|
break;
|
|
case CS.DURATION_KEY:
|
|
if (delta.value === CS.DURATION_ALL) {
|
|
newUrlParams.delete(CS.DURATION_KEY);
|
|
} else {
|
|
newUrlParams.set(CS.DURATION_KEY, delta.value);
|
|
}
|
|
break;
|
|
}
|
|
return `?${newUrlParams.toString()}`;
|
|
}
|
|
|
|
function handleScrollBottom() {
|
|
if (!loading) {
|
|
setPage(page + 1);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (shouldPerformSearch) {
|
|
const searchOptions = JSON.parse(optionsStringForEffect);
|
|
doClaimSearch(searchOptions);
|
|
}
|
|
}, [doClaimSearch, shouldPerformSearch, optionsStringForEffect, forceRefresh]);
|
|
|
|
const defaultHeader = (
|
|
<Fragment>
|
|
<div className={'claim-search__wrapper'}>
|
|
<div className={'claim-search__top'}>
|
|
<div className={'claim-search__top-row'}>
|
|
{CS.ORDER_BY_TYPES.map(type => (
|
|
<Button
|
|
key={type}
|
|
button="alt"
|
|
onClick={e =>
|
|
handleChange({
|
|
key: CS.ORDER_BY_KEY,
|
|
value: type,
|
|
})
|
|
}
|
|
className={classnames(`button-toggle button-toggle--${type}`, {
|
|
'button-toggle--active': orderParam === type,
|
|
})}
|
|
disabled={orderBy}
|
|
icon={toCapitalCase(type)}
|
|
label={__(toCapitalCase(type))}
|
|
/>
|
|
))}
|
|
</div>
|
|
<div>
|
|
{!noCustom && (
|
|
<Button
|
|
button={'alt'}
|
|
aria-label={__('More')}
|
|
className={classnames(`button-toggle button-toggle--top button-toggle--more`, {
|
|
'button-toggle--custom': isFiltered(),
|
|
})}
|
|
icon={toCapitalCase(ICONS.SLIDERS)}
|
|
onClick={() => setExpanded(!expanded)}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{expanded && (
|
|
<>
|
|
<div className={classnames('card--inline', `claim-search__menus`)}>
|
|
{/* FRESHNESS FIELD */}
|
|
{orderParam === CS.ORDER_BY_TOP && (
|
|
<div className={'claim-search__input-container'}>
|
|
<FormField
|
|
className="claim-search__dropdown"
|
|
type="select"
|
|
name="trending_time"
|
|
label={__('How Fresh')}
|
|
value={freshnessParam}
|
|
onChange={e =>
|
|
handleChange({
|
|
key: CS.FRESH_KEY,
|
|
value: e.target.value,
|
|
})
|
|
}
|
|
>
|
|
{CS.FRESH_TYPES.map(time => (
|
|
<option key={time} value={time}>
|
|
{/* i18fixme */}
|
|
{time === CS.FRESH_DAY && __('Today')}
|
|
{time !== CS.FRESH_ALL &&
|
|
time !== CS.FRESH_DAY &&
|
|
__('This ' + toCapitalCase(time)) /* yes, concat before i18n, since it is read from const */}
|
|
{time === CS.FRESH_ALL && __('All time')}
|
|
</option>
|
|
))}
|
|
</FormField>
|
|
</div>
|
|
)}
|
|
|
|
{/* CONTENT_TYPES FIELD */}
|
|
<div
|
|
className={classnames('claim-search__input-container', {
|
|
'claim-search__input-container--selected': contentTypeParam,
|
|
})}
|
|
>
|
|
<FormField
|
|
className={classnames('claim-search__dropdown', {
|
|
'claim-search__dropdown--selected': contentTypeParam,
|
|
})}
|
|
type="select"
|
|
name="claimType"
|
|
label={__('Content Type')}
|
|
value={contentTypeParam || CS.CONTENT_ALL}
|
|
onChange={e =>
|
|
handleChange({
|
|
key: CS.CONTENT_KEY,
|
|
value: e.target.value,
|
|
})
|
|
}
|
|
>
|
|
{CS.CONTENT_TYPES.map(type => {
|
|
if (type !== CS.CLAIM_CHANNEL || (type === CS.CLAIM_CHANNEL && !channelIds)) {
|
|
return (
|
|
<option key={type} value={type}>
|
|
{/* i18fixme */}
|
|
{type === CS.CLAIM_CHANNEL && __('Channel')}
|
|
{type === CS.CLAIM_REPOST && __('Repost')}
|
|
{type === CS.FILE_VIDEO && __('Video')}
|
|
{type === CS.FILE_AUDIO && __('Audio')}
|
|
{type === CS.FILE_DOCUMENT && __('Document')}
|
|
{type === CS.CONTENT_ALL && __('Any')}
|
|
</option>
|
|
);
|
|
}
|
|
})}
|
|
</FormField>
|
|
</div>
|
|
{options.claim_type !== CS.CLAIM_CHANNEL &&
|
|
options.claim_type !== CS.CLAIM_REPOST &&
|
|
JSON.stringify(options.stream_types) !== JSON.stringify([CS.FILE_DOCUMENT]) && (
|
|
<>
|
|
{/* DURATIONS FIELD */}
|
|
<div className={'claim-search__input-container'}>
|
|
<FormField
|
|
className={classnames('claim-search__dropdown', {
|
|
'claim-search__dropdown--selected': durationParam,
|
|
})}
|
|
label={__('Duration')}
|
|
type="select"
|
|
name="duration"
|
|
value={durationParam || CS.DURATION_ALL}
|
|
onChange={e =>
|
|
handleChange({
|
|
key: CS.DURATION_KEY,
|
|
value: e.target.value,
|
|
})
|
|
}
|
|
>
|
|
{CS.DURATION_TYPES.map(dur => (
|
|
<option key={dur} value={dur}>
|
|
{/* i18fixme */}
|
|
{dur === CS.DURATION_SHORT && __('Short')}
|
|
{dur === CS.DURATION_LONG && __('Long')}
|
|
{dur === CS.DURATION_ALL && __('Any')}
|
|
</option>
|
|
))}
|
|
</FormField>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{hasMatureTags && hiddenNsfwMessage}
|
|
</Fragment>
|
|
);
|
|
|
|
return (
|
|
<React.Fragment>
|
|
<ClaimList
|
|
id={claimSearchCacheQuery}
|
|
loading={loading}
|
|
uris={uris}
|
|
header={header || defaultHeader}
|
|
headerLabel={headerLabel}
|
|
headerAltControls={meta}
|
|
onScrollBottom={handleScrollBottom}
|
|
page={page}
|
|
pageSize={CS.PAGE_SIZE}
|
|
empty={noResults}
|
|
renderProperties={renderProperties}
|
|
includeSupportAction={includeSupportAction}
|
|
hideBlock={hideBlock}
|
|
/>
|
|
|
|
<div className="card">
|
|
{loading && new Array(CS.PAGE_SIZE).fill(1).map((x, i) => <ClaimPreview key={i} placeholder="loading" />)}
|
|
</div>
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
export default withRouter(ClaimListDiscover);
|