Add pagination support to channel search (#791)
* Add pagination support to channel search * fix #605 - channelContent component - replace lighthouse.search() to doSearch() * #605 - Add pagination support to channel search - create ClaimListSearch component to support channel search instead of ClaimListDiscover * #605 - Add pagination support to channel search - fix lint errors & component naming error Co-authored-by: Kyle <kyle.mai@wiredcraft.com>
This commit is contained in:
parent
2606758c0d
commit
b3c4ce05fa
6 changed files with 400 additions and 69 deletions
|
@ -7,7 +7,6 @@ import {
|
|||
makeSelectTotalPagesInChannelSearch,
|
||||
selectClaimForUri,
|
||||
} from 'redux/selectors/claims';
|
||||
import { doResolveUris } from 'redux/actions/claims';
|
||||
import * as SETTINGS from 'constants/settings';
|
||||
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||
import { withRouter } from 'react-router';
|
||||
|
@ -41,7 +40,6 @@ const select = (state, props) => {
|
|||
};
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
doResolveUris: (uris, returnCachedUris) => dispatch(doResolveUris(uris, returnCachedUris)),
|
||||
doFetchChannelLiveStatus: (channelID) => dispatch(doFetchChannelLiveStatus(channelID)),
|
||||
});
|
||||
|
||||
|
|
|
@ -7,13 +7,14 @@ import HiddenNsfwClaims from 'component/hiddenNsfwClaims';
|
|||
import { useHistory } from 'react-router-dom';
|
||||
import Button from 'component/button';
|
||||
import ClaimListDiscover from 'component/claimListDiscover';
|
||||
import ClaimListSearch from 'component/claimListSearch';
|
||||
import Ads from 'web/component/ads';
|
||||
import Icon from 'component/common/icon';
|
||||
import LivestreamLink from 'component/livestreamLink';
|
||||
import { Form, FormField } from 'component/common/form';
|
||||
import { DEBOUNCE_WAIT_DURATION_MS } from 'constants/search';
|
||||
import { lighthouse } from 'redux/actions/search';
|
||||
import ScheduledStreams from 'component/scheduledStreams';
|
||||
import { useIsLargeScreen } from 'effects/use-screensize';
|
||||
|
||||
const TYPES_TO_ALLOW_FILTER = ['stream', 'repost'];
|
||||
|
||||
|
@ -34,7 +35,6 @@ type Props = {
|
|||
showMature: boolean,
|
||||
tileLayout: boolean,
|
||||
viewHiddenChannels: boolean,
|
||||
doResolveUris: (Array<string>, boolean) => void,
|
||||
claimType: string,
|
||||
empty?: string,
|
||||
doFetchChannelLiveStatus: (string) => void,
|
||||
|
@ -56,7 +56,6 @@ function ChannelContent(props: Props) {
|
|||
showMature,
|
||||
tileLayout,
|
||||
viewHiddenChannels,
|
||||
doResolveUris,
|
||||
claimType,
|
||||
empty,
|
||||
doFetchChannelLiveStatus,
|
||||
|
@ -66,7 +65,8 @@ function ChannelContent(props: Props) {
|
|||
// const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
|
||||
const claimsInChannel = 9999;
|
||||
const [searchQuery, setSearchQuery] = React.useState('');
|
||||
const [searchResults, setSearchResults] = React.useState(undefined);
|
||||
const [isSearch, setIsSearch] = React.useState(false);
|
||||
const isLargeScreen = useIsLargeScreen();
|
||||
const {
|
||||
location: { pathname, search },
|
||||
} = useHistory();
|
||||
|
@ -78,6 +78,7 @@ function ChannelContent(props: Props) {
|
|||
(Array.isArray(claimType)
|
||||
? claimType.every((ct) => TYPES_TO_ALLOW_FILTER.includes(ct))
|
||||
: TYPES_TO_ALLOW_FILTER.includes(claimType));
|
||||
const dynamicPageSize = isLargeScreen ? Math.ceil(defaultPageSize * (3 / 2)) : defaultPageSize;
|
||||
|
||||
function handleInputChange(e) {
|
||||
const { value } = e.target;
|
||||
|
@ -87,38 +88,17 @@ function ChannelContent(props: Props) {
|
|||
React.useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (searchQuery.trim().length < 3 || !claimId) {
|
||||
// In order to display original search results, search results must be set to null. A query of '' should display original results.
|
||||
return setSearchResults(null);
|
||||
setIsSearch(false);
|
||||
} else {
|
||||
lighthouse
|
||||
.search(
|
||||
`s=${encodeURIComponent(searchQuery)}&channel_id=${encodeURIComponent(claimId)}${
|
||||
!showMature ? '&nsfw=false&size=50&from=0' : ''
|
||||
}`
|
||||
)
|
||||
.then(({ body: results }) => {
|
||||
const urls = results.map(({ name, claimId }) => {
|
||||
return `lbry://${name}#${claimId}`;
|
||||
});
|
||||
|
||||
// Batch-resolve the urls before calling 'setSearchResults', as the
|
||||
// latter will immediately cause the tiles to resolve, ending up
|
||||
// calling doResolveUri one by one before the batched one.
|
||||
doResolveUris(urls, true);
|
||||
|
||||
setSearchResults(urls);
|
||||
})
|
||||
.catch(() => {
|
||||
setSearchResults(null);
|
||||
});
|
||||
setIsSearch(true);
|
||||
}
|
||||
}, DEBOUNCE_WAIT_DURATION_MS);
|
||||
return () => clearTimeout(timer);
|
||||
}, [claimId, searchQuery, showMature, doResolveUris]);
|
||||
}, [claimId, searchQuery]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setSearchQuery('');
|
||||
setSearchResults(null);
|
||||
setIsSearch(false);
|
||||
}, [url]);
|
||||
|
||||
const isInitialized = Boolean(activeLivestreamForChannel) || activeLivestreamInitialized;
|
||||
|
@ -177,44 +157,80 @@ function ChannelContent(props: Props) {
|
|||
|
||||
{/* <Ads type="homepage" /> */}
|
||||
|
||||
{!fetching && (
|
||||
<ClaimListDiscover
|
||||
hasSource
|
||||
defaultFreshness={CS.FRESH_ALL}
|
||||
showHiddenByUser={viewHiddenChannels}
|
||||
forceShowReposts
|
||||
fetchViewCount
|
||||
hideFilters={!showFilters}
|
||||
hideAdvancedFilter={!showFilters}
|
||||
tileLayout={tileLayout}
|
||||
uris={searchResults}
|
||||
streamType={SIMPLE_SITE ? CS.CONTENT_ALL : undefined}
|
||||
channelIds={[claimId]}
|
||||
claimType={claimType}
|
||||
feeAmount={CS.FEE_AMOUNT_ANY}
|
||||
defaultOrderBy={CS.ORDER_BY_NEW}
|
||||
pageSize={defaultPageSize}
|
||||
infiniteScroll={defaultInfiniteScroll}
|
||||
injectedItem={SHOW_ADS && !isAuthenticated && IS_WEB && <Ads type="video" />}
|
||||
meta={
|
||||
showFilters && (
|
||||
<Form onSubmit={() => {}} className="wunderbar--inline">
|
||||
<Icon icon={ICONS.SEARCH} />
|
||||
<FormField
|
||||
className="wunderbar__input--inline"
|
||||
value={searchQuery}
|
||||
onChange={handleInputChange}
|
||||
type="text"
|
||||
placeholder={__('Search')}
|
||||
/>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
isChannel
|
||||
channelIsMine={channelIsMine}
|
||||
empty={empty}
|
||||
/>
|
||||
)}
|
||||
{!fetching &&
|
||||
(isSearch ? (
|
||||
<ClaimListSearch
|
||||
defaultFreshness={CS.FRESH_ALL}
|
||||
showHiddenByUser={viewHiddenChannels}
|
||||
fetchViewCount
|
||||
hideFilters={!showFilters}
|
||||
hideAdvancedFilter={!showFilters}
|
||||
tileLayout={tileLayout}
|
||||
streamType={SIMPLE_SITE ? CS.CONTENT_ALL : undefined}
|
||||
channelIds={[claimId]}
|
||||
claimId={claimId}
|
||||
claimType={claimType}
|
||||
feeAmount={CS.FEE_AMOUNT_ANY}
|
||||
defaultOrderBy={CS.ORDER_BY_NEW}
|
||||
pageSize={dynamicPageSize}
|
||||
infiniteScroll={defaultInfiniteScroll}
|
||||
injectedItem={SHOW_ADS && !isAuthenticated && IS_WEB && <Ads type="video" />}
|
||||
meta={
|
||||
showFilters && (
|
||||
<Form onSubmit={() => {}} className="wunderbar--inline">
|
||||
<Icon icon={ICONS.SEARCH} />
|
||||
<FormField
|
||||
className="wunderbar__input--inline"
|
||||
value={searchQuery}
|
||||
onChange={handleInputChange}
|
||||
type="text"
|
||||
placeholder={__('Search')}
|
||||
/>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
channelIsMine={channelIsMine}
|
||||
empty={empty}
|
||||
showMature={showMature}
|
||||
searchKeyword={searchQuery}
|
||||
/>
|
||||
) : (
|
||||
<ClaimListDiscover
|
||||
hasSource
|
||||
defaultFreshness={CS.FRESH_ALL}
|
||||
showHiddenByUser={viewHiddenChannels}
|
||||
forceShowReposts
|
||||
fetchViewCount
|
||||
hideFilters={!showFilters}
|
||||
hideAdvancedFilter={!showFilters}
|
||||
tileLayout={tileLayout}
|
||||
streamType={SIMPLE_SITE ? CS.CONTENT_ALL : undefined}
|
||||
channelIds={[claimId]}
|
||||
claimType={claimType}
|
||||
feeAmount={CS.FEE_AMOUNT_ANY}
|
||||
defaultOrderBy={CS.ORDER_BY_NEW}
|
||||
pageSize={defaultPageSize}
|
||||
infiniteScroll={defaultInfiniteScroll}
|
||||
injectedItem={SHOW_ADS && !isAuthenticated && IS_WEB && <Ads type="video" />}
|
||||
meta={
|
||||
showFilters && (
|
||||
<Form onSubmit={() => {}} className="wunderbar--inline">
|
||||
<Icon icon={ICONS.SEARCH} />
|
||||
<FormField
|
||||
className="wunderbar__input--inline"
|
||||
value={searchQuery}
|
||||
onChange={handleInputChange}
|
||||
type="text"
|
||||
placeholder={__('Search')}
|
||||
/>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
isChannel
|
||||
channelIsMine={channelIsMine}
|
||||
empty={empty}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
|
47
ui/component/claimListSearch/index.js
Normal file
47
ui/component/claimListSearch/index.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectClaimsByUri } from 'redux/selectors/claims';
|
||||
import {
|
||||
selectIsSearching,
|
||||
makeSelectSearchUrisForQuery,
|
||||
makeSelectHasReachedMaxResultsLength,
|
||||
} from 'redux/selectors/search';
|
||||
import { getSearchQueryString } from 'util/query-params';
|
||||
import { doSearch } from 'redux/actions/search';
|
||||
import ClaimListSearch from './view';
|
||||
import { doFetchViewCount } from 'lbryinc';
|
||||
|
||||
const select = (state, props) => {
|
||||
const { searchKeyword, pageSize, claimId, showMature } = props;
|
||||
const channel_id = encodeURIComponent(claimId);
|
||||
const isBackgroundSearch = false;
|
||||
const searchOptions = showMature
|
||||
? {
|
||||
channel_id,
|
||||
isBackgroundSearch,
|
||||
}
|
||||
: {
|
||||
channel_id,
|
||||
size: pageSize,
|
||||
nsfw: false,
|
||||
isBackgroundSearch,
|
||||
};
|
||||
|
||||
const searchQueryString = getSearchQueryString(searchKeyword, searchOptions);
|
||||
const searchResult = makeSelectSearchUrisForQuery(searchQueryString)(state);
|
||||
const searchResultLastPageReached = makeSelectHasReachedMaxResultsLength(searchQueryString)(state);
|
||||
|
||||
return {
|
||||
claimsByUri: selectClaimsByUri(state),
|
||||
loading: props.loading !== undefined ? props.loading : selectIsSearching(state),
|
||||
searchOptions,
|
||||
searchResult,
|
||||
searchResultLastPageReached,
|
||||
};
|
||||
};
|
||||
|
||||
const perform = {
|
||||
doFetchViewCount,
|
||||
doSearch,
|
||||
};
|
||||
|
||||
export default connect(select, perform)(ClaimListSearch);
|
267
ui/component/claimListSearch/view.jsx
Normal file
267
ui/component/claimListSearch/view.jsx
Normal file
|
@ -0,0 +1,267 @@
|
|||
// @flow
|
||||
import { SIMPLE_SITE } from 'config';
|
||||
import type { Node } from 'react';
|
||||
import * as CS from 'constants/claim_search';
|
||||
import React from 'react';
|
||||
import { withRouter } from 'react-router';
|
||||
import { MATURE_TAGS } from 'constants/tags';
|
||||
import ClaimList from 'component/claimList';
|
||||
import ClaimPreview from 'component/claimPreview';
|
||||
import ClaimPreviewTile from 'component/claimPreviewTile';
|
||||
import ClaimListHeader from 'component/claimListHeader';
|
||||
import useFetchViewCount from 'effects/use-fetch-view-count';
|
||||
|
||||
export type SearchOptions = {
|
||||
size?: number,
|
||||
from?: number,
|
||||
related_to?: string,
|
||||
nsfw?: boolean,
|
||||
channel_id?: string,
|
||||
isBackgroundSearch?: boolean,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
uris: Array<string>,
|
||||
type: string,
|
||||
pageSize: number,
|
||||
|
||||
fetchViewCount?: boolean,
|
||||
hideAdvancedFilter?: boolean,
|
||||
hideFilters?: boolean,
|
||||
infiniteScroll?: Boolean,
|
||||
showHeader: boolean,
|
||||
showHiddenByUser?: boolean,
|
||||
tileLayout: boolean,
|
||||
|
||||
defaultOrderBy?: string,
|
||||
defaultFreshness?: string,
|
||||
|
||||
tags: string, // these are just going to be string. pass a CSV if you want multi
|
||||
defaultTags: string,
|
||||
|
||||
claimType?: string | Array<string>,
|
||||
|
||||
streamType?: string | Array<string>,
|
||||
defaultStreamType?: string | Array<string>,
|
||||
|
||||
empty?: string,
|
||||
feeAmount?: string,
|
||||
repostedClaimId?: string,
|
||||
maxPages?: number,
|
||||
|
||||
channelIds?: Array<string>,
|
||||
claimId: string,
|
||||
|
||||
header?: Node,
|
||||
headerLabel?: string | Node,
|
||||
injectedItem: ?Node,
|
||||
meta?: Node,
|
||||
|
||||
location: { search: string, pathname: string },
|
||||
|
||||
// --- select ---
|
||||
claimsByUri: { [string]: any },
|
||||
loading: boolean,
|
||||
searchResult: Array<string>,
|
||||
searchResultLastPageReached: boolean,
|
||||
|
||||
// --- perform ---
|
||||
doFetchViewCount: (claimIdCsv: string) => void,
|
||||
doSearch: (query: string, options: SearchOptions) => void,
|
||||
|
||||
hideLayoutButton?: boolean,
|
||||
maxClaimRender?: number,
|
||||
useSkeletonScreen?: boolean,
|
||||
excludeUris?: Array<string>,
|
||||
|
||||
swipeLayout: boolean,
|
||||
|
||||
showMature: boolean,
|
||||
searchKeyword: string,
|
||||
searchOptions: SearchOptions,
|
||||
};
|
||||
|
||||
function ClaimListSearch(props: Props) {
|
||||
const {
|
||||
showHeader = true,
|
||||
type,
|
||||
tags,
|
||||
defaultTags,
|
||||
loading,
|
||||
meta,
|
||||
channelIds,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
claimId,
|
||||
fetchViewCount,
|
||||
location,
|
||||
defaultOrderBy,
|
||||
headerLabel,
|
||||
header,
|
||||
claimType,
|
||||
pageSize,
|
||||
streamType,
|
||||
defaultStreamType = SIMPLE_SITE ? [CS.FILE_VIDEO, CS.FILE_AUDIO] : undefined, // add param for DEFAULT_STREAM_TYPE
|
||||
defaultFreshness = CS.FRESH_WEEK,
|
||||
repostedClaimId,
|
||||
hideAdvancedFilter,
|
||||
infiniteScroll = true,
|
||||
injectedItem,
|
||||
feeAmount,
|
||||
uris,
|
||||
tileLayout,
|
||||
hideFilters = false,
|
||||
maxPages,
|
||||
showHiddenByUser = false,
|
||||
empty,
|
||||
claimsByUri,
|
||||
doFetchViewCount,
|
||||
hideLayoutButton = false,
|
||||
maxClaimRender,
|
||||
useSkeletonScreen = true,
|
||||
excludeUris = [],
|
||||
swipeLayout = false,
|
||||
|
||||
// search
|
||||
showMature,
|
||||
searchKeyword,
|
||||
searchOptions,
|
||||
searchResult,
|
||||
searchResultLastPageReached,
|
||||
doSearch,
|
||||
} = props;
|
||||
const { search } = location;
|
||||
const [page, setPage] = React.useState(1);
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const tagsParam = // can be 'x,y,z' or 'x' or ['x','y'] or CS.CONSTANT
|
||||
(tags && getParamFromTags(tags)) ||
|
||||
(urlParams.get(CS.TAGS_KEY) !== null && urlParams.get(CS.TAGS_KEY)) ||
|
||||
(defaultTags && getParamFromTags(defaultTags));
|
||||
const hasMatureTags = tagsParam && tagsParam.split(',').some((t) => MATURE_TAGS.includes(t));
|
||||
|
||||
const renderUris = uris || searchResult;
|
||||
|
||||
// **************************************************************************
|
||||
// Helpers
|
||||
// **************************************************************************
|
||||
function getParamFromTags(t) {
|
||||
if (t === CS.TAGS_ALL || t === CS.TAGS_FOLLOWED) {
|
||||
return t;
|
||||
} else if (Array.isArray(t)) {
|
||||
return t.join(',');
|
||||
}
|
||||
}
|
||||
|
||||
function handleScrollBottom() {
|
||||
if (maxPages !== undefined && page === maxPages) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!loading && infiniteScroll) {
|
||||
if (searchResult && !searchResultLastPageReached) {
|
||||
setPage(page + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// **************************************************************************
|
||||
|
||||
useFetchViewCount(fetchViewCount, renderUris, claimsByUri, doFetchViewCount);
|
||||
|
||||
React.useEffect(() => {
|
||||
doSearch(
|
||||
searchKeyword,
|
||||
showMature
|
||||
? searchOptions
|
||||
: {
|
||||
...searchOptions,
|
||||
from: pageSize * (page - 1),
|
||||
}
|
||||
);
|
||||
}, [doSearch, searchKeyword, showMature, pageSize, page]);
|
||||
|
||||
const headerToUse = header || (
|
||||
<ClaimListHeader
|
||||
channelIds={channelIds}
|
||||
defaultTags={defaultTags}
|
||||
tags={tags}
|
||||
defaultFreshness={defaultFreshness}
|
||||
claimType={claimType}
|
||||
streamType={streamType}
|
||||
defaultStreamType={defaultStreamType}
|
||||
feeAmount={feeAmount} // ENABLE_PAID_CONTENT_DISCOVER or something
|
||||
defaultOrderBy={defaultOrderBy}
|
||||
hideAdvancedFilter={hideAdvancedFilter}
|
||||
hasMatureTags={hasMatureTags}
|
||||
setPage={setPage}
|
||||
tileLayout={tileLayout}
|
||||
hideLayoutButton={hideLayoutButton}
|
||||
hideFilters={hideFilters}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{headerLabel && <label className="claim-list__header-label">{headerLabel}</label>}
|
||||
{tileLayout ? (
|
||||
<div>
|
||||
{!repostedClaimId && (
|
||||
<div className="section__header--actions">
|
||||
{headerToUse}
|
||||
{meta && <div className="section__actions--no-margin">{meta}</div>}
|
||||
</div>
|
||||
)}
|
||||
<ClaimList
|
||||
tileLayout
|
||||
loading={loading}
|
||||
uris={renderUris}
|
||||
onScrollBottom={handleScrollBottom}
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
injectedItem={injectedItem}
|
||||
showHiddenByUser={showHiddenByUser}
|
||||
empty={empty}
|
||||
maxClaimRender={maxClaimRender}
|
||||
excludeUris={excludeUris}
|
||||
swipeLayout={swipeLayout}
|
||||
/>
|
||||
{loading && useSkeletonScreen && (
|
||||
<div className="claim-grid">
|
||||
{new Array(pageSize).fill(1).map((x, i) => (
|
||||
<ClaimPreviewTile key={i} placeholder="loading" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{showHeader && (
|
||||
<div className="section__header--actions">
|
||||
{headerToUse}
|
||||
{meta && <div className="section__actions--no-margin">{meta}</div>}
|
||||
</div>
|
||||
)}
|
||||
<ClaimList
|
||||
type={type}
|
||||
loading={loading}
|
||||
uris={renderUris}
|
||||
onScrollBottom={handleScrollBottom}
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
injectedItem={injectedItem}
|
||||
showHiddenByUser={showHiddenByUser}
|
||||
empty={empty}
|
||||
maxClaimRender={maxClaimRender}
|
||||
excludeUris={excludeUris}
|
||||
swipeLayout={swipeLayout}
|
||||
/>
|
||||
{loading &&
|
||||
useSkeletonScreen &&
|
||||
new Array(pageSize).fill(1).map((x, i) => <ClaimPreview key={i} placeholder="loading" type={type} />)}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(ClaimListSearch);
|
|
@ -34,6 +34,7 @@ export const SEARCH_OPTIONS = {
|
|||
TIME_FILTER_THIS_WEEK: 'thisweek',
|
||||
TIME_FILTER_THIS_MONTH: 'thismonth',
|
||||
TIME_FILTER_THIS_YEAR: 'thisyear',
|
||||
CHANNEL_ID: 'channel_id',
|
||||
};
|
||||
|
||||
export const SEARCH_PAGE_SIZE = 20;
|
||||
|
|
|
@ -80,10 +80,12 @@ export const getSearchQueryString = (query: string, options: any = {}) => {
|
|||
const { related_to } = options;
|
||||
const { nsfw } = options;
|
||||
const { free_only } = options;
|
||||
const { channel_id } = options;
|
||||
|
||||
if (related_to) additionalOptions[SEARCH_OPTIONS.RELATED_TO] = related_to;
|
||||
if (free_only) additionalOptions[SEARCH_OPTIONS.PRICE_FILTER_FREE] = true;
|
||||
if (nsfw === false) additionalOptions[SEARCH_OPTIONS.INCLUDE_MATURE] = false;
|
||||
if (channel_id) additionalOptions[SEARCH_OPTIONS.CHANNEL_ID] = channel_id;
|
||||
|
||||
if (additionalOptions) {
|
||||
Object.keys(additionalOptions).forEach((key) => {
|
||||
|
|
Loading…
Reference in a new issue