diff --git a/src/component/fileResultItem/index.js b/src/component/claimResultItem/index.js similarity index 95% rename from src/component/fileResultItem/index.js rename to src/component/claimResultItem/index.js index f1bff41..4a44701 100644 --- a/src/component/fileResultItem/index.js +++ b/src/component/claimResultItem/index.js @@ -12,7 +12,7 @@ import { } from 'lbry-redux'; import { selectBlackListedOutpoints, selectFilteredOutpoints, selectRewardContentClaimIds } from 'lbryinc'; import { selectShowNsfw } from 'redux/selectors/settings'; -import FileResultItem from './view'; +import ClaimResultItem from './view'; const select = (state, props) => ({ blackListedOutpoints: selectBlackListedOutpoints(state), @@ -37,4 +37,4 @@ const perform = dispatch => ({ export default connect( select, perform, -)(FileResultItem); +)(ClaimResultItem); diff --git a/src/component/fileResultItem/view.js b/src/component/claimResultItem/view.js similarity index 54% rename from src/component/fileResultItem/view.js rename to src/component/claimResultItem/view.js index 0c0b10e..1f55801 100644 --- a/src/component/fileResultItem/view.js +++ b/src/component/claimResultItem/view.js @@ -1,10 +1,13 @@ 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 { navigateToUri, formatTitle, getDownloadProgress, getStorageForFileInfo } from 'utils/helper'; import Colors from 'styles/colors'; +import ChannelIconItem from 'component/channelIconItem'; +import channelIconStyle from 'styles/channelIcon'; import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api import DateTime from 'component/dateTime'; +import FastImage from 'react-native-fast-image'; import FileItemMedia from 'component/fileItemMedia'; import FilePrice from 'component/filePrice'; import Icon from 'react-native-vector-icons/FontAwesome5'; @@ -12,29 +15,28 @@ import Link from 'component/link'; import NsfwOverlay from 'component/nsfwOverlay'; import ProgressBar from 'component/progressBar'; import fileListStyle from 'styles/fileList'; +import seedrandom from 'seedrandom'; -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})`; +class ClaimResultItem extends React.PureComponent { + state = { + autoStyle: null, + }; + + componentDidMount() { + const { result } = this.props; + + if (!result || !result.name || !result.claimId) { + this.setState({ + autoStyle: + ChannelIconItem.AUTO_THUMB_STYLES[Math.floor(Math.random() * ChannelIconItem.AUTO_THUMB_STYLES.length)], + }); + } else { + // result property set, use deterministic random style + const rng = seedrandom(normalizeURI(`${result.name}#${result.claimId}`)); + const index = Math.floor(rng.quick() * ChannelIconItem.AUTO_THUMB_STYLES.length); + this.setState({ autoStyle: ChannelIconItem.AUTO_THUMB_STYLES[index] }); } - - 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; @@ -58,6 +60,8 @@ class FileResultItem extends React.PureComponent { title, } = result; + const isChannel = name && name.startsWith('@'); + const hasThumbnail = !!thumbnailUrl; const obscure = obscureNsfw && nsfw; const url = normalizeURI(`${name}#${claimId}`); const hasChannel = !!channel; @@ -66,14 +70,39 @@ class FileResultItem extends React.PureComponent { 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} - /> + <TouchableOpacity + style={[style, isChannel ? fileListStyle.channelContainer : null]} + onPress={this.onPressHandler} + > + {!isChannel && ( + <FileItemMedia + style={fileListStyle.thumbnail} + duration={duration} + resizeMode="cover" + title={title || name || normalizeURI(url).substring(7)} + thumbnail={thumbnailUrl} + /> + )} + + {isChannel && ( + <View style={fileListStyle.thumbnail}> + <View style={[fileListStyle.channelThumbnailContainer, this.state.autoStyle]}> + {hasThumbnail && ( + <FastImage + style={fileListStyle.channelThumbnail} + resizeMode={FastImage.resizeMode.cover} + source={{ uri: thumbnailUrl }} + /> + )} + {!hasThumbnail && ( + <Text style={channelIconStyle.autothumbCharacter}> + {title ? title.substring(0, 1).toUpperCase() : name.substring(1, 2).toUpperCase()} + </Text> + )} + </View> + </View> + )} + {fileInfo && fileInfo.completed && fileInfo.download_path && ( <Icon style={featuredResult ? fileListStyle.featuredDownloadedIcon : fileListStyle.downloadedIcon} @@ -94,25 +123,32 @@ class FileResultItem extends React.PureComponent { {(title || name) && ( <View style={fileListStyle.titleContainer}> <Text style={featuredResult ? fileListStyle.featuredTitle : fileListStyle.title}> - {this.formatTitle(title) || this.formatTitle(name)} + {formatTitle(title) || 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); - }} - /> - )} + {hasChannel || + (isChannel && ( + <Link + style={fileListStyle.publisher} + text={isChannel ? name : channel} + onPress={() => { + navigateToUri( + navigation, + normalizeURI(isChannel ? url : channelUrl), + null, + false, + isChannel ? url : channelUrl, + ); + }} + /> + ))} <View style={fileListStyle.info}> {fileInfo && !isNaN(fileInfo.written_bytes) && fileInfo.written_bytes > 0 && ( - <Text>{this.getStorageForFileInfo(fileInfo)}</Text> + <Text>{getStorageForFileInfo(fileInfo)}</Text> )} <DateTime style={fileListStyle.publishInfo} @@ -130,7 +166,7 @@ class FileResultItem extends React.PureComponent { color={Colors.NextLbryGreen} height={3} style={fileListStyle.progress} - progress={this.getDownloadProgress(fileInfo)} + progress={getDownloadProgress(fileInfo)} /> )} </View> @@ -143,4 +179,4 @@ class FileResultItem extends React.PureComponent { } } -export default FileResultItem; +export default ClaimResultItem; diff --git a/src/component/fileListItem/view.js b/src/component/fileListItem/view.js index 8d8ffdc..97398d3 100644 --- a/src/component/fileListItem/view.js +++ b/src/component/fileListItem/view.js @@ -1,10 +1,13 @@ 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 { navigateToUri, formatTitle, getDownloadProgress, getStorageForFileInfo } from 'utils/helper'; import Colors from 'styles/colors'; +import ChannelIconItem from 'component/channelIconItem'; +import channelIconStyle from 'styles/channelIcon'; import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api import DateTime from 'component/dateTime'; +import FastImage from 'react-native-fast-image'; import FileItemMedia from 'component/fileItemMedia'; import FilePrice from 'component/filePrice'; import Icon from 'react-native-vector-icons/FontAwesome5'; @@ -15,27 +18,10 @@ import fileListStyle from 'styles/fileList'; class FileListItem extends React.PureComponent { state = { + autoStyle: null, url: null, }; - 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); }; @@ -45,6 +31,11 @@ class FileListItem extends React.PureComponent { if (!claim && !batchResolve) { resolveUri(uri); } + + this.setState({ + autoStyle: + ChannelIconItem.AUTO_THUMB_STYLES[Math.floor(Math.random() * ChannelIconItem.AUTO_THUMB_STYLES.length)], + }); } componentDidUpdate() { @@ -135,10 +126,13 @@ class FileListItem extends React.PureComponent { return null; } + const isChannel = name && name.startsWith('@'); + const hasThumbnail = !!thumbnail; + return ( <View style={style}> <TouchableOpacity - style={style} + style={[style, isChannel ? fileListStyle.channelContainer : null]} onPress={this.onPressHandler} onLongPress={() => { if (onLongPress) { @@ -146,13 +140,35 @@ class FileListItem extends React.PureComponent { } }} > - <FileItemMedia - style={fileListStyle.thumbnail} - duration={duration} - resizeMode="cover" - title={title || name || normalizeURI(uri).substring(7)} - thumbnail={thumbnail} - /> + {!isChannel && ( + <FileItemMedia + style={fileListStyle.thumbnail} + duration={duration} + resizeMode="cover" + title={title || name || normalizeURI(uri).substring(7)} + thumbnail={thumbnail} + /> + )} + + {isChannel && ( + <View style={fileListStyle.thumbnail}> + <View style={[fileListStyle.channelThumbnailContainer, this.state.autoStyle]}> + {hasThumbnail && ( + <FastImage + style={fileListStyle.channelThumbnail} + resizeMode={FastImage.resizeMode.cover} + source={{ uri: thumbnail }} + /> + )} + {!hasThumbnail && ( + <Text style={channelIconStyle.autothumbCharacter}> + {title ? title.substring(0, 1).toUpperCase() : claim.name.substring(1, 2).toUpperCase()} + </Text> + )} + </View> + </View> + )} + {selected && ( <View style={fileListStyle.selectedOverlay}> <Icon name={'check-circle'} solid color={Colors.NextLbryGreen} size={32} /> @@ -194,7 +210,7 @@ class FileListItem extends React.PureComponent { {(title || name) && ( <View style={fileListStyle.titleContainer}> <Text style={featuredResult ? fileListStyle.featuredTitle : fileListStyle.title}> - {this.formatTitle(title) || this.formatTitle(name)} + {formatTitle(title) || formatTitle(name)} </Text> {isRewardContent && <Icon style={fileListStyle.rewardIcon} name="award" size={12} />} </View> @@ -206,17 +222,17 @@ class FileListItem extends React.PureComponent { </View> )} - {channel && !hideChannel && ( + {(channel || isChannel) && !hideChannel && ( <Link style={fileListStyle.publisher} - text={channel} + text={isChannel ? name : channel} onPress={() => { navigateToUri( navigation, - normalizeURI(shortChannelUri || fullChannelUri), + normalizeURI(isChannel ? uri : shortChannelUri || fullChannelUri), null, false, - fullChannelUri, + isChannel ? claim && claim.permanent_url : fullChannelUri, ); }} /> @@ -224,7 +240,7 @@ class FileListItem extends React.PureComponent { <View style={fileListStyle.info}> {fileInfo && !isNaN(fileInfo.written_bytes) && fileInfo.written_bytes > 0 && ( - <Text style={fileListStyle.infoText}>{this.getStorageForFileInfo(fileInfo)}</Text> + <Text style={fileListStyle.infoText}>{getStorageForFileInfo(fileInfo)}</Text> )} <DateTime style={fileListStyle.publishInfo} textStyle={fileListStyle.infoText} timeAgo uri={uri} /> </View> @@ -237,7 +253,7 @@ class FileListItem extends React.PureComponent { color={Colors.NextLbryGreen} height={3} style={fileListStyle.progress} - progress={this.getDownloadProgress(fileInfo)} + progress={getDownloadProgress(fileInfo)} /> )} </View> diff --git a/src/component/relatedContent/view.js b/src/component/relatedContent/view.js index e0830f4..f5ab361 100644 --- a/src/component/relatedContent/view.js +++ b/src/component/relatedContent/view.js @@ -4,7 +4,7 @@ import { normalizeURI } from 'lbry-redux'; import { navigateToUri } from 'utils/helper'; import Colors from 'styles/colors'; import FileListItem from 'component/fileListItem'; -import FileResultItem from 'component/fileResultItem'; +import ClaimResultItem from 'component/claimResultItem'; import fileListStyle from 'styles/fileList'; import relatedContentStyle from 'styles/relatedContent'; @@ -30,7 +30,7 @@ export default class RelatedContent extends React.PureComponent { {isSearching && <ActivityIndicator size={'small'} color={Colors.NextLbryGreen} />} {recommendedContent && recommendedContent.map(result => ( - <FileResultItem + <ClaimResultItem style={fileListStyle.item} key={result.claimId} result={result} diff --git a/src/component/suggestedSubscriptionItem/view.js b/src/component/suggestedSubscriptionItem/view.js index ac24a31..5f7e37c 100644 --- a/src/component/suggestedSubscriptionItem/view.js +++ b/src/component/suggestedSubscriptionItem/view.js @@ -3,6 +3,8 @@ import { buildURI, normalizeURI } from 'lbry-redux'; import { ActivityIndicator, FlatList, Image, Text, View } from 'react-native'; import { navigateToUri } from 'utils/helper'; import Colors from 'styles/colors'; +import ChannelIconItem from 'component/channelIconItem'; +import channelIconStyle from 'styles/channelIcon'; import discoverStyle from 'styles/discover'; import FileItem from 'component/fileItem'; import SubscribeButton from 'component/subscribeButton'; @@ -11,11 +13,20 @@ import Link from 'component/link'; import Tag from 'component/tag'; class SuggestedSubscriptionItem extends React.PureComponent { + state = { + autoStyle: null, + }; + componentDidMount() { const { claim, uri, resolveUri } = this.props; if (!claim) { resolveUri(uri); } + + this.setState({ + autoStyle: + ChannelIconItem.AUTO_THUMB_STYLES[Math.floor(Math.random() * ChannelIconItem.AUTO_THUMB_STYLES.length)], + }); } render() { @@ -28,6 +39,7 @@ class SuggestedSubscriptionItem extends React.PureComponent { } } + const hasThumbnail = !!thumbnail; if (isResolvingUri) { return ( <View style={subscriptionsStyle.itemLoadingContainer}> @@ -38,12 +50,15 @@ class SuggestedSubscriptionItem extends React.PureComponent { return ( <View style={subscriptionsStyle.suggestedItem}> - <View style={subscriptionsStyle.suggestedItemThumbnailContainer}> - <Image - style={subscriptionsStyle.suggestedItemThumbnail} - resizeMode={'cover'} - source={thumbnail ? { uri: thumbnail } : require('../../assets/default_avatar.jpg')} - /> + <View style={[subscriptionsStyle.suggestedItemThumbnailContainer, this.state.autoStyle]}> + {hasThumbnail && ( + <Image style={subscriptionsStyle.suggestedItemThumbnail} resizeMode={'cover'} source={{ uri: thumbnail }} /> + )} + {!hasThumbnail && ( + <Text style={channelIconStyle.autothumbCharacter}> + {title ? title.substring(0, 1).toUpperCase() : claim ? claim.name.substring(1, 2).toUpperCase() : ''} + </Text> + )} </View> <View style={subscriptionsStyle.suggestedItemDetails}> diff --git a/src/page/channelCreator/view.js b/src/page/channelCreator/view.js index 8a86ad8..55baef9 100644 --- a/src/page/channelCreator/view.js +++ b/src/page/channelCreator/view.js @@ -21,6 +21,7 @@ import ChannelRewardsDriver from 'component/channelRewardsDriver'; import Colors from 'styles/colors'; import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api import EmptyStateView from 'component/emptyStateView'; +import FastImage from 'react-native-fast-image'; import FloatingWalletBalance from 'component/floatingWalletBalance'; import Icon from 'react-native-vector-icons/FontAwesome5'; import Link from 'component/link'; @@ -867,9 +868,9 @@ export default class ChannelCreator extends React.PureComponent { > <View style={[channelCreatorStyle.channelListAvatar, itemAutoStyle]}> {itemThumbnailUrl && ( - <Image + <FastImage style={channelCreatorStyle.avatarImage} - resizeMode={'cover'} + resizeMode={FastImage.resizeMode.cover} source={{ uri: itemThumbnailUrl }} /> )} diff --git a/src/page/search/view.js b/src/page/search/view.js index 5747637..22dc55b 100644 --- a/src/page/search/view.js +++ b/src/page/search/view.js @@ -15,7 +15,7 @@ import Colors from 'styles/colors'; import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api import PageHeader from 'component/pageHeader'; import FileListItem from 'component/fileListItem'; -import FileResultItem from 'component/fileResultItem'; +import ClaimResultItem from 'component/claimResultItem'; import FloatingWalletBalance from 'component/floatingWalletBalance'; import UriBar from 'component/uriBar'; import searchStyle from 'styles/search'; @@ -213,7 +213,12 @@ class SearchPage extends React.PureComponent { ListEmptyComponent={!isSearching ? this.listEmptyComponent() : null} ListHeaderComponent={this.listHeaderComponent(this.state.showTagResult, this.state.currentQuery)} renderItem={({ item }) => ( - <FileResultItem key={item.claimId} result={item} style={searchStyle.resultItem} navigation={navigation} /> + <ClaimResultItem + key={item.claimId} + result={item} + style={searchStyle.resultItem} + navigation={navigation} + /> )} /> )} diff --git a/src/page/subscriptions/view.js b/src/page/subscriptions/view.js index e48487e..83b9027 100644 --- a/src/page/subscriptions/view.js +++ b/src/page/subscriptions/view.js @@ -73,7 +73,7 @@ class SubscriptionsPage extends React.PureComponent { }; componentDidMount() { - // this.onComponentFocused(); + this.onComponentFocused(); } componentWillReceiveProps(nextProps) { diff --git a/src/styles/fileList.js b/src/styles/fileList.js index fe5811b..d3d5f4f 100644 --- a/src/styles/fileList.js +++ b/src/styles/fileList.js @@ -38,6 +38,18 @@ const fileListStyle = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, + channelThumbnailContainer: { + width: thumbnailHeight, // maintain same width and height + height: thumbnailHeight, + borderRadius: 140, + overflow: 'hidden', + alignItems: 'center', + justifyContent: 'center', + }, + channelThumbnail: { + width: '100%', + height: '100%', + }, selectedOverlay: { position: 'absolute', left: 0, @@ -63,6 +75,9 @@ const fileListStyle = StyleSheet.create({ marginTop: screenWidthPixels <= 720 ? 1 : 3, color: Colors.LbryGreen, }, + channelContainer: { + alignItems: 'center', + }, loading: { position: 'absolute', }, diff --git a/src/styles/subscriptions.js b/src/styles/subscriptions.js index bb1ff78..20e90d8 100644 --- a/src/styles/subscriptions.js +++ b/src/styles/subscriptions.js @@ -199,6 +199,8 @@ const subscriptionsStyle = StyleSheet.create({ height: 70, borderRadius: 140, overflow: 'hidden', + alignItems: 'center', + justifyContent: 'center', }, suggestedItemThumbnail: { width: '100%', diff --git a/src/utils/helper.js b/src/utils/helper.js index bf3e938..4f57b62 100644 --- a/src/utils/helper.js +++ b/src/utils/helper.js @@ -370,3 +370,25 @@ export function uploadImageAsset(filePath, success, failure) { export function formatLbryUrlForWeb(url) { return url.replace('lbry://', '/').replace(/#/g, ':'); } + +export function getDownloadProgress(fileInfo) { + return Math.ceil((fileInfo.written_bytes / fileInfo.total_bytes) * 100); +} + +export function 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); +} + +export function formatTitle(title) { + if (!title) { + return title; + } + + return title.length > 80 ? title.substring(0, 77).trim() + '...' : title; +}