display resolved search results #107

Merged
akinwale merged 3 commits from resolved-search into master 2020-01-05 19:08:36 +01:00
11 changed files with 234 additions and 85 deletions

4
package-lock.json generated
View file

@ -7047,8 +7047,8 @@
} }
}, },
"lbry-redux": { "lbry-redux": {
"version": "github:lbryio/lbry-redux#eb8fff0e5afd43be00822b3f476eea9e0eb7b075", "version": "github:lbryio/lbry-redux#e8f29f1c47b136669df265babb109d2488a37db0",
"from": "github:lbryio/lbry-redux#eb8fff0e5afd43be00822b3f476eea9e0eb7b075", "from": "github:lbryio/lbry-redux#e8f29f1c47b136669df265babb109d2488a37db0",
"requires": { "requires": {
"proxy-polyfill": "0.1.6", "proxy-polyfill": "0.1.6",
"reselect": "^3.0.0", "reselect": "^3.0.0",

View file

@ -12,7 +12,7 @@
"base-64": "^0.1.0", "base-64": "^0.1.0",
"@expo/vector-icons": "^8.1.0", "@expo/vector-icons": "^8.1.0",
"gfycat-style-urls": "^1.0.3", "gfycat-style-urls": "^1.0.3",
"lbry-redux": "lbryio/lbry-redux#eb8fff0e5afd43be00822b3f476eea9e0eb7b075", "lbry-redux": "lbryio/lbry-redux#e8f29f1c47b136669df265babb109d2488a37db0",
"lbryinc": "lbryio/lbryinc#053ca52f4f7f9bf8eb62a3581b183671a475ad1c", "lbryinc": "lbryio/lbryinc#053ca52f4f7f9bf8eb62a3581b183671a475ad1c",
"lodash": ">=4.17.11", "lodash": ">=4.17.11",
"merge": ">=1.2.1", "merge": ">=1.2.1",

View file

@ -61,6 +61,7 @@ class FileItemMedia extends React.PureComponent {
let style = this.props.style; let style = this.props.style;
const { duration, isResolvingUri, thumbnail, title, resizeMode } = this.props; const { duration, isResolvingUri, thumbnail, title, resizeMode } = this.props;
const atStyle = this.state.autoThumbStyle; const atStyle = this.state.autoThumbStyle;
const hasDuration = !!duration;
if (this.isThumbnailValid(thumbnail) && !this.state.imageLoadFailed) { if (this.isThumbnailValid(thumbnail) && !this.state.imageLoadFailed) {
if (style == null) { if (style == null) {
@ -75,7 +76,7 @@ class FileItemMedia extends React.PureComponent {
resizeMode={this.getFastImageResizeMode(resizeMode)} resizeMode={this.getFastImageResizeMode(resizeMode)}
style={fileItemMediaStyle.image} style={fileItemMediaStyle.image}
/> />
{duration && ( {duration > 0 && (
<VideoDuration <VideoDuration
duration={duration} duration={duration}
style={fileItemMediaStyle.duration} style={fileItemMediaStyle.duration}
@ -104,7 +105,7 @@ class FileItemMedia extends React.PureComponent {
.toUpperCase()} .toUpperCase()}
</Text> </Text>
)} )}
{duration && ( {duration > 0 && (
<VideoDuration <VideoDuration
duration={duration} duration={duration}
style={fileItemMediaStyle.duration} style={fileItemMediaStyle.duration}

View file

@ -63,14 +63,6 @@ class CreditAmount extends React.PureComponent {
} }
} }
/* {this.props.isEstimate ? (
<span
className="credit-amount__estimate"
title={__('This is an estimate and does not include data fees')}
>
*
</span>
) : null} */
return ( return (
<Text style={style} numberOfLines={1}> <Text style={style} numberOfLines={1}>
{amountText} {amountText}
@ -81,11 +73,17 @@ class CreditAmount extends React.PureComponent {
class FilePrice extends React.PureComponent { class FilePrice extends React.PureComponent {
componentWillMount() { componentWillMount() {
this.fetchCost(this.props); const { cost } = this.props;
if (isNaN(parseFloat(cost))) {
this.fetchCost(this.props);
}
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
this.fetchCost(nextProps); const { cost } = this.props;
if (isNaN(parseFloat(cost))) {
this.fetchCost(nextProps);
}
} }
fetchCost(props) { fetchCost(props) {
@ -97,11 +95,11 @@ class FilePrice extends React.PureComponent {
} }
render() { render() {
const { costInfo, look = 'indicator', showFullPrice = false, style, iconStyle, textStyle } = this.props; const { cost, costInfo, look = 'indicator', showFullPrice = false, style, iconStyle, textStyle } = this.props;
const isEstimate = costInfo ? !costInfo.includesData : null; const isEstimate = costInfo ? !costInfo.includesData : null;
const amount = costInfo ? parseFloat(costInfo.cost) : 0; const amount = cost ? parseFloat(cost) : costInfo ? parseFloat(costInfo.cost) : 0;
if (!costInfo || isNaN(amount) || amount === 0) { if (isNaN(amount) || amount === 0) {
return null; return null;
} }

View file

@ -0,0 +1,40 @@
import { connect } from 'react-redux';
import {
doResolveUri,
makeSelectClaimForUri,
makeSelectMetadataForUri,
makeSelectFileInfoForUri,
makeSelectIsUriResolving,
makeSelectClaimIsNsfw,
makeSelectShortUrlForUri,
makeSelectTitleForUri,
makeSelectThumbnailForUri,
} from 'lbry-redux';
import { selectBlackListedOutpoints, selectFilteredOutpoints, selectRewardContentClaimIds } from 'lbryinc';
import { selectShowNsfw } from 'redux/selectors/settings';
import FileResultItem from './view';
const select = (state, props) => ({
blackListedOutpoints: selectBlackListedOutpoints(state),
claim: makeSelectClaimForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
filteredOutpoints: selectFilteredOutpoints(state),
isDownloaded: !!makeSelectFileInfoForUri(props.uri)(state),
metadata: makeSelectMetadataForUri(props.uri)(state),
nsfw: makeSelectClaimIsNsfw(props.uri)(state),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
obscureNsfw: !selectShowNsfw(state),
rewardedContentClaimIds: selectRewardContentClaimIds(state),
shortUrl: makeSelectShortUrlForUri(props.uri)(state),
title: makeSelectTitleForUri(props.uri)(state),
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
});
const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri)),
});
export default connect(
select,
perform,
)(FileResultItem);

View file

@ -0,0 +1,146 @@
import React from 'react';
import { normalizeURI, parseURI } from 'lbry-redux';
import { ActivityIndicator, Platform, Text, TouchableOpacity, View } from 'react-native';
import { navigateToUri, formatBytes } from 'utils/helper';
import Colors from 'styles/colors';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import DateTime from 'component/dateTime';
import FileItemMedia from 'component/fileItemMedia';
import FilePrice from 'component/filePrice';
import Icon from 'react-native-vector-icons/FontAwesome5';
import Link from 'component/link';
import NsfwOverlay from 'component/nsfwOverlay';
import ProgressBar from 'component/progressBar';
import fileListStyle from 'styles/fileList';
class FileResultItem extends React.PureComponent {
getStorageForFileInfo = fileInfo => {
if (!fileInfo.completed) {
const written = formatBytes(fileInfo.written_bytes);
const total = formatBytes(fileInfo.total_bytes);
return `(${written} / ${total})`;
}
return formatBytes(fileInfo.written_bytes);
};
formatTitle = title => {
if (!title) {
return title;
}
return title.length > 80 ? title.substring(0, 77).trim() + '...' : title;
};
getDownloadProgress = fileInfo => {
return Math.ceil((fileInfo.written_bytes / fileInfo.total_bytes) * 100);
};
onPressHandler = () => {
const { autoplay, navigation, result } = this.props;
const { claimId, name } = result;
const url = normalizeURI(`${name}#${claimId}`);
navigateToUri(navigation, url, { autoplay }, false, url);
};
render() {
const { featuredResult, fileInfo, navigation, obscureNsfw, result, rewardedContentClaimIds, style } = this.props;
const {
channel,
channel_claim_id: channelClaimId,
claimId,
duration,
fee,
name,
nsfw,
release_time: releaseTime,
thumbnail_url: thumbnailUrl,
title,
} = result;
const obscure = obscureNsfw && nsfw;
const url = normalizeURI(`${name}#${claimId}`);
const hasChannel = !!channel;
const channelUrl = channel ? normalizeURI(`${channel}#${channelClaimId}`) : null;
const isRewardContent = rewardedContentClaimIds.includes(claimId);
return (
<View style={style}>
<TouchableOpacity style={style} onPress={this.onPressHandler}>
<FileItemMedia
style={fileListStyle.thumbnail}
duration={duration}
resizeMode="cover"
title={title || name || normalizeURI(url).substring(7)}
thumbnail={thumbnailUrl}
/>
{fileInfo && fileInfo.completed && fileInfo.download_path && (
<Icon
style={featuredResult ? fileListStyle.featuredDownloadedIcon : fileListStyle.downloadedIcon}
solid
color={Colors.NextLbryGreen}
name={'folder'}
size={16}
/>
)}
<FilePrice
cost={fee ? parseFloat(fee) / 100000000 : 0}
uri={url}
style={fileListStyle.filePriceContainer}
iconStyle={fileListStyle.filePriceIcon}
textStyle={fileListStyle.filePriceText}
/>
<View style={fileListStyle.detailsContainer}>
{(title || name) && (
<View style={fileListStyle.titleContainer}>
<Text style={featuredResult ? fileListStyle.featuredTitle : fileListStyle.title}>
{this.formatTitle(title) || this.formatTitle(name)}
</Text>
{isRewardContent && <Icon style={fileListStyle.rewardIcon} name="award" size={12} />}
</View>
)}
{hasChannel && (
<Link
style={fileListStyle.publisher}
text={channel}
onPress={() => {
navigateToUri(navigation, normalizeURI(channelUrl), null, false, channelUrl);
}}
/>
)}
<View style={fileListStyle.info}>
{fileInfo && !isNaN(fileInfo.written_bytes) && fileInfo.written_bytes > 0 && (
<Text>{this.getStorageForFileInfo(fileInfo)}</Text>
)}
<DateTime
style={fileListStyle.publishInfo}
textStyle={fileListStyle.infoText}
timeAgo
date={releaseTime}
/>
</View>
{fileInfo && fileInfo.download_path && (
<View style={fileListStyle.downloadInfo}>
{!fileInfo.completed && (
<ProgressBar
borderRadius={3}
color={Colors.NextLbryGreen}
height={3}
style={fileListStyle.progress}
progress={this.getDownloadProgress(fileInfo)}
/>
)}
</View>
)}
</View>
</TouchableOpacity>
{obscure && <NsfwOverlay onPress={() => navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />}
</View>
);
}
}
export default FileResultItem;

View file

@ -1,24 +1,27 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
doResolveUris, doResolveUris,
doSearch, doResolvedSearch,
makeSelectClaimForUri, makeSelectClaimForUri,
makeSelectRecommendedContentForUri, makeSelectResolvedRecommendedContentForUri,
selectResolvingUris, selectResolvingUris,
selectIsSearching, selectIsSearching,
} from 'lbry-redux'; } from 'lbry-redux';
import RelatedContent from './view'; import RelatedContent from './view';
const RESULT_SIZE = 16;
const select = (state, props) => ({ const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
isSearching: selectIsSearching(state), isSearching: selectIsSearching(state),
recommendedContent: makeSelectRecommendedContentForUri(props.uri)(state), recommendedContent: makeSelectResolvedRecommendedContentForUri(props.uri, RESULT_SIZE)(state),
resolvingUris: selectResolvingUris(state), resolvingUris: selectResolvingUris(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
resolveUris: uris => dispatch(doResolveUris(uris)), resolveUris: uris => dispatch(doResolveUris(uris)),
searchRecommended: (query, claimId) => dispatch(doSearch(query, 20, undefined, true, { related_to: claimId }, false)), searchRecommended: (query, claimId) =>
dispatch(doResolvedSearch(query, RESULT_SIZE, undefined, true, { related_to: claimId }, false)),
}); });
export default connect( export default connect(

View file

@ -4,14 +4,11 @@ import { normalizeURI } from 'lbry-redux';
import { navigateToUri } from 'utils/helper'; import { navigateToUri } from 'utils/helper';
import Colors from 'styles/colors'; import Colors from 'styles/colors';
import FileListItem from 'component/fileListItem'; import FileListItem from 'component/fileListItem';
import FileResultItem from 'component/fileResultItem';
import fileListStyle from 'styles/fileList'; import fileListStyle from 'styles/fileList';
import relatedContentStyle from 'styles/relatedContent'; import relatedContentStyle from 'styles/relatedContent';
export default class RelatedContent extends React.PureComponent { export default class RelatedContent extends React.PureComponent {
state = {
resolveStarted: false,
};
componentDidMount() { componentDidMount() {
const { title, claimId, searchRecommended } = this.props; const { title, claimId, searchRecommended } = this.props;
if (title && claimId) { if (title && claimId) {
@ -21,32 +18,7 @@ export default class RelatedContent extends React.PureComponent {
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { isSearching, recommendedContent } = nextProps; const { isSearching, recommendedContent } = nextProps;
return isSearching || (!isSearching && this.allContentResolved()); return isSearching || (!isSearching && recommendedContent && recommendedContent.length > 0);
}
allContentResolved() {
const { recommendedContent, resolvingUris } = this.props;
if (recommendedContent) {
let allResolved = true;
recommendedContent.forEach(uri => {
allResolved = allResolved && !resolvingUris.includes(uri);
});
return allResolved;
}
return false;
}
componentDidUpdate() {
const { isSearching, resolveUris, recommendedContent } = this.props;
if (!isSearching && !this.state.resolveStarted) {
this.setState({ resolveStarted: true }, () => {
if (recommendedContent && recommendedContent.length > 0) {
// batch resolve the uris
resolveUris(recommendedContent);
}
});
}
} }
render() { render() {
@ -57,18 +29,15 @@ export default class RelatedContent extends React.PureComponent {
<Text style={relatedContentStyle.title}>{__('Related Content')}</Text> <Text style={relatedContentStyle.title}>{__('Related Content')}</Text>
{isSearching && <ActivityIndicator size={'small'} color={Colors.NextLbryGreen} />} {isSearching && <ActivityIndicator size={'small'} color={Colors.NextLbryGreen} />}
{recommendedContent && {recommendedContent &&
recommendedContent recommendedContent.map(result => (
.filter(recommendedUri => recommendedUri !== normalizeURI(fullUri)) <FileResultItem
.map(recommendedUri => ( style={fileListStyle.item}
<FileListItem key={result.claimId}
style={fileListStyle.item} result={result}
key={recommendedUri} navigation={navigation}
uri={recommendedUri} autoplay
batchResolve />
navigation={navigation} ))}
autoplay
/>
))}
</View> </View>
); );
} }

View file

@ -148,7 +148,6 @@ class FilePage extends React.PureComponent {
navigation, navigation,
contentType, contentType,
notify, notify,
recommendedContent: prevRecommendedContent,
drawerStack: prevDrawerStack, drawerStack: prevDrawerStack,
} = this.props; } = this.props;
const { uri } = navigation.state.params; const { uri } = navigation.state.params;
@ -160,7 +159,6 @@ class FilePage extends React.PureComponent {
purchaseUriErrorMessage, purchaseUriErrorMessage,
streamingUrl, streamingUrl,
drawerStack, drawerStack,
recommendedContent,
resolveUris, resolveUris,
} = nextProps; } = nextProps;
@ -214,13 +212,6 @@ class FilePage extends React.PureComponent {
if (claim && !this.state.viewCountFetched) { if (claim && !this.state.viewCountFetched) {
this.setState({ viewCountFetched: true }, () => fetchViewCount(claim.claim_id)); this.setState({ viewCountFetched: true }, () => fetchViewCount(claim.claim_id));
} }
if (
(!prevRecommendedContent && recommendedContent) ||
(recommendedContent && prevRecommendedContent && recommendedContent.length !== prevRecommendedContent.length)
) {
resolveUris(recommendedContent);
}
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
@ -760,8 +751,6 @@ class FilePage extends React.PureComponent {
position, position,
purchaseUri, purchaseUri,
pushDrawerStack, pushDrawerStack,
isSearchingRecommendContent,
recommendedContent,
thumbnail, thumbnail,
title, title,
viewCount, viewCount,

View file

@ -2,8 +2,9 @@ import { connect } from 'react-redux';
import { import {
doClaimSearch, doClaimSearch,
doResolveUris, doResolveUris,
doSearch, doResolvedSearch,
doUpdateSearchQuery, doUpdateSearchQuery,
makeSelectResolvedSearchResults,
makeSelectSearchUris, makeSelectSearchUris,
selectClaimSearchByQuery, selectClaimSearchByQuery,
selectIsSearching, selectIsSearching,
@ -25,10 +26,11 @@ const select = state => ({
query: selectSearchValue(state), query: selectSearchValue(state),
resolvingUris: selectResolvingUris(state), resolvingUris: selectResolvingUris(state),
uris: makeSelectSearchUris(makeSelectQueryWithOptions(null, numSearchResults)(state))(state), uris: makeSelectSearchUris(makeSelectQueryWithOptions(null, numSearchResults)(state))(state),
results: makeSelectResolvedSearchResults(makeSelectQueryWithOptions(null, numSearchResults)(state))(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
search: query => dispatch(doSearch(query, numSearchResults, null, false, {}, false)), search: query => dispatch(doResolvedSearch(query, numSearchResults, null, false, {})),
claimSearch: options => dispatch(doClaimSearch(options)), claimSearch: options => dispatch(doClaimSearch(options)),
updateSearchQuery: query => dispatch(doUpdateSearchQuery(query)), updateSearchQuery: query => dispatch(doUpdateSearchQuery(query)),
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_SEARCH)), pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_SEARCH)),

View file

@ -15,6 +15,7 @@ import Colors from 'styles/colors';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import PageHeader from 'component/pageHeader'; import PageHeader from 'component/pageHeader';
import FileListItem from 'component/fileListItem'; import FileListItem from 'component/fileListItem';
import FileResultItem from 'component/fileResultItem';
import FloatingWalletBalance from 'component/floatingWalletBalance'; import FloatingWalletBalance from 'component/floatingWalletBalance';
import UriBar from 'component/uriBar'; import UriBar from 'component/uriBar';
import searchStyle from 'styles/search'; import searchStyle from 'styles/search';
@ -104,20 +105,20 @@ class SearchPage extends React.PureComponent {
return false; return false;
} }
shouldComponentUpdate(nextProps) { /* shouldComponentUpdate(nextProps) {
const { isSearching, uris } = this.props; const { isSearching, uris } = this.props;
return (isSearching && (!uris || uris.length === 0)) || (!isSearching && this.allContentResolved(this.props)); return (isSearching && (!uris || uris.length === 0)) || (!isSearching && this.allContentResolved(this.props));
} } */
componentDidUpdate() { componentDidUpdate() {
const { isSearching, resolveUris, uris } = this.props; /* const { isSearching, resolveUris, uris } = this.props;
if (!isSearching && !this.state.resultsResolved) { if (!isSearching && !this.state.resultsResolved) {
this.setState({ resultsResolved: true }, () => { this.setState({ resultsResolved: true }, () => {
if (uris && uris.length > 0) { if (uris && uris.length > 0) {
resolveUris(uris); resolveUris(uris);
} }
}); });
} } */
} }
getSearchQuery() { getSearchQuery() {
@ -191,7 +192,7 @@ class SearchPage extends React.PureComponent {
}; };
render() { render() {
const { isSearching, navigation, query, uris } = this.props; const { isSearching, navigation, query, results } = this.props;
return ( return (
<View style={searchStyle.container}> <View style={searchStyle.container}>
@ -209,15 +210,15 @@ class SearchPage extends React.PureComponent {
style={searchStyle.scrollContainer} style={searchStyle.scrollContainer}
contentContainerStyle={searchStyle.scrollPadding} contentContainerStyle={searchStyle.scrollPadding}
keyboardShouldPersistTaps={'handled'} keyboardShouldPersistTaps={'handled'}
data={uris} data={results}
keyExtractor={(item, index) => item} keyExtractor={(item, index) => item.claimId}
initialNumToRender={8} initialNumToRender={8}
maxToRenderPerBatch={20} maxToRenderPerBatch={20}
removeClippedSubviews removeClippedSubviews
ListEmptyComponent={!isSearching ? this.listEmptyComponent() : null} ListEmptyComponent={!isSearching ? this.listEmptyComponent() : null}
ListHeaderComponent={this.state.currentUri ? this.listHeaderComponent(this.state.showTagResult) : null} ListHeaderComponent={this.state.currentUri ? this.listHeaderComponent(this.state.showTagResult) : null}
renderItem={({ item }) => ( renderItem={({ item }) => (
<FileListItem key={item} uri={item} style={searchStyle.resultItem} batchResolve navigation={navigation} /> <FileResultItem key={item.claimId} result={item} style={searchStyle.resultItem} navigation={navigation} />
)} )}
/> />
)} )}