Channel Search pagination

Closes 605 "Add pagination support to channel search"

## Previous Attempt
The previous attempt (69de63c4) didn't work because the wunderbar is part of the list component, so it is unmounted when we switch between the Normal and Filtered list, causing it to lose focus while typing.

Also, creating another full-blown ClaimList* component is really redundant (we should be consolidating instead).

## Approach
ClaimListDiscover recently added a new `subSection`, so we can place the filtered `ClaimList` here without causing the wunderbar to unmount.

Wrapped the "lighthouse search with channel_id" into `searchResults.jsx` for now as a quick and isolated solution. When we refactor ClaimList*, we can then consider incorporating into `doSearch`.
This commit is contained in:
infinite-persistence 2022-03-09 01:25:39 +08:00 committed by Thomas Zarebczan
parent 7904e751ac
commit efbbba6958
2 changed files with 106 additions and 38 deletions

View file

@ -0,0 +1,92 @@
// @flow
import React from 'react';
import ClaimList from 'component/claimList';
import { DEBOUNCE_WAIT_DURATION_MS, SEARCH_PAGE_SIZE } from 'constants/search';
import { lighthouse } from 'redux/actions/search';
type Props = {
searchQuery: string,
claimId: ?string,
showMature: ?boolean,
tileLayout: boolean,
onResults?: (results: ?Array<string>) => void,
doResolveUris: (Array<string>, boolean) => void,
};
export function SearchResults(props: Props) {
const { searchQuery, claimId, showMature, tileLayout, onResults, doResolveUris } = props;
const [page, setPage] = React.useState(1);
const [searchResults, setSearchResults] = React.useState(undefined);
const [isSearching, setIsSearching] = React.useState(false);
const noMoreResults = React.useRef(false);
React.useEffect(() => {
setPage(1);
}, [searchQuery]);
React.useEffect(() => {
if (onResults) {
onResults(searchResults);
}
}, [searchResults, onResults]);
React.useEffect(() => {
const timer = setTimeout(() => {
if (searchQuery.trim().length < 3 || !claimId) {
return setSearchResults(null);
}
setIsSearching(true);
lighthouse
.search(
`from=${SEARCH_PAGE_SIZE * (page - 1)}` +
`&s=${encodeURIComponent(searchQuery)}` +
`&channel_id=${encodeURIComponent(claimId)}` +
`&nsfw=${showMature ? 'true' : 'false'}` +
`&size=${SEARCH_PAGE_SIZE}`
)
.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);
// De-dup (LH will return some duplicates) and concat results
setSearchResults((prev) => (page === 1 ? urls : Array.from(new Set((prev || []).concat(urls)))));
noMoreResults.current = !urls || urls.length < SEARCH_PAGE_SIZE;
})
.catch(() => {
setPage(1);
setSearchResults(null);
noMoreResults.current = false;
})
.finally(() => {
setIsSearching(false);
});
}, DEBOUNCE_WAIT_DURATION_MS);
return () => clearTimeout(timer);
}, [searchQuery, claimId, page, showMature, doResolveUris]);
if (!searchResults) {
return null;
}
return (
<ClaimList
uris={searchResults}
loading={isSearching}
onScrollBottom={() => setPage((prev) => (noMoreResults.current ? prev : prev + 1))}
page={page}
pageSize={SEARCH_PAGE_SIZE}
tileLayout={tileLayout}
useLoadingSpinner
/>
);
}

View file

@ -11,9 +11,8 @@ import Ads from 'web/component/ads';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
import LivestreamLink from 'component/livestreamLink'; import LivestreamLink from 'component/livestreamLink';
import { Form, FormField } from 'component/common/form'; 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 ScheduledStreams from 'component/scheduledStreams';
import { SearchResults } from './internal/searchResults';
const TYPES_TO_ALLOW_FILTER = ['stream', 'repost']; const TYPES_TO_ALLOW_FILTER = ['stream', 'repost'];
@ -66,7 +65,7 @@ function ChannelContent(props: Props) {
// const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0; // const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
const claimsInChannel = 9999; const claimsInChannel = 9999;
const [searchQuery, setSearchQuery] = React.useState(''); const [searchQuery, setSearchQuery] = React.useState('');
const [searchResults, setSearchResults] = React.useState(undefined); const [isSearching, setIsSearching] = React.useState(false);
const { const {
location: { pathname, search }, location: { pathname, search },
} = useHistory(); } = useHistory();
@ -84,41 +83,8 @@ function ChannelContent(props: Props) {
setSearchQuery(value); setSearchQuery(value);
} }
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);
} 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);
});
}
}, DEBOUNCE_WAIT_DURATION_MS);
return () => clearTimeout(timer);
}, [claimId, searchQuery, showMature, doResolveUris]);
React.useEffect(() => { React.useEffect(() => {
setSearchQuery(''); setSearchQuery('');
setSearchResults(null);
}, [url]); }, [url]);
const isInitialized = Boolean(activeLivestreamForChannel) || activeLivestreamInitialized; const isInitialized = Boolean(activeLivestreamForChannel) || activeLivestreamInitialized;
@ -186,7 +152,7 @@ function ChannelContent(props: Props) {
hideFilters={!showFilters} hideFilters={!showFilters}
hideAdvancedFilter={!showFilters} hideAdvancedFilter={!showFilters}
tileLayout={tileLayout} tileLayout={tileLayout}
uris={searchResults} uris={isSearching ? [] : null}
streamType={SIMPLE_SITE ? CS.CONTENT_ALL : undefined} streamType={SIMPLE_SITE ? CS.CONTENT_ALL : undefined}
channelIds={[claimId]} channelIds={[claimId]}
claimType={claimType} claimType={claimType}
@ -210,9 +176,19 @@ function ChannelContent(props: Props) {
</Form> </Form>
) )
} }
subSection={
<SearchResults
searchQuery={searchQuery}
claimId={claimId}
showMature={showMature}
tileLayout={tileLayout}
onResults={(results) => setIsSearching(results !== null)}
doResolveUris={doResolveUris}
/>
}
isChannel isChannel
channelIsMine={channelIsMine} channelIsMine={channelIsMine}
empty={empty} empty={isSearching ? ' ' : empty}
/> />
)} )}
</Fragment> </Fragment>