Discovery (#2)

* show content for followed tags on the discover page
* pin rn-fetch-blob version. vertical claim lists.
* update trending page with claim_search results and scroll up to 500 items
* tag page and content sorting
* fix styles and tag page load
* update subscriptions view using claim_search results
* add horizontal subscribed channels list
* add tag customisation to explore and trending pages
* subscriptions updates and suggested channels
This commit is contained in:
Akinwale Ariwodola 2019-07-26 09:13:46 +01:00 committed by GitHub
parent 286415ee14
commit ed2532dae1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 1730 additions and 405 deletions

83
package-lock.json generated
View file

@ -1462,6 +1462,32 @@
}
}
},
"babel-eslint": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.2.tgz",
"integrity": "sha512-UdsurWPtgiPgpJ06ryUnuaSXC2s0WoSZnQmEpbAH65XZSdwowgN5MvyP7e88nW07FYXv72erVtpBkxyDVKhH1Q==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"@babel/parser": "^7.0.0",
"@babel/traverse": "^7.0.0",
"@babel/types": "^7.0.0",
"eslint-scope": "3.7.1",
"eslint-visitor-keys": "^1.0.0"
},
"dependencies": {
"eslint-scope": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz",
"integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=",
"dev": true,
"requires": {
"esrecurse": "^4.1.0",
"estraverse": "^4.1.1"
}
}
}
},
"babel-helper-bindify-decorators": {
"version": "6.24.1",
"resolved": "https://registry.npmjs.org/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz",
@ -2521,6 +2547,12 @@
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz",
"integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I="
},
"ci-info": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz",
"integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==",
"dev": true
},
"class-utils": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
@ -3769,7 +3801,8 @@
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true
"bundled": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@ -4134,7 +4167,8 @@
},
"safe-buffer": {
"version": "5.1.2",
"bundled": true
"bundled": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@ -4182,6 +4216,7 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -4220,11 +4255,13 @@
},
"wrappy": {
"version": "1.0.2",
"bundled": true
"bundled": true,
"optional": true
},
"yallist": {
"version": "3.0.3",
"bundled": true
"bundled": true,
"optional": true
}
}
},
@ -4449,6 +4486,25 @@
"toidentifier": "1.0.0"
}
},
"husky": {
"version": "0.14.3",
"resolved": "https://registry.npmjs.org/husky/-/husky-0.14.3.tgz",
"integrity": "sha512-e21wivqHpstpoiWA/Yi8eFti8E+sQDSS53cpJsPptPs295QTOQR0ZwnHo2TXy1XOpZFD9rPOd3NpmqTK6uMLJA==",
"dev": true,
"requires": {
"is-ci": "^1.0.10",
"normalize-path": "^1.0.0",
"strip-indent": "^2.0.0"
},
"dependencies": {
"normalize-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-1.0.0.tgz",
"integrity": "sha1-MtDkcvkf80VwHBWoMRAY07CpA3k=",
"dev": true
}
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -4575,6 +4631,15 @@
"integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==",
"dev": true
},
"is-ci": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz",
"integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==",
"dev": true,
"requires": {
"ci-info": "^1.5.0"
}
},
"is-data-descriptor": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
@ -4954,8 +5019,8 @@
}
},
"lbry-redux": {
"version": "github:lbryio/lbry-redux#0ff6364a40253387fbe1c4a5b5cd444f616d84e6",
"from": "github:lbryio/lbry-redux",
"version": "github:lbryio/lbry-redux#b2044499c5f43e519384433538c1225d56d3a1f2",
"from": "github:lbryio/lbry-redux#multi-claim-search",
"requires": {
"proxy-polyfill": "0.1.6",
"reselect": "^3.0.0",
@ -8693,6 +8758,12 @@
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
},
"strip-indent": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz",
"integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=",
"dev": true
},
"strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",

View file

@ -11,7 +11,7 @@
"base-64": "^0.1.0",
"@expo/vector-icons": "^8.1.0",
"gfycat-style-urls": "^1.0.3",
"lbry-redux": "lbryio/lbry-redux",
"lbry-redux": "lbryio/lbry-redux#multi-claim-search",
"lbryinc": "lbryio/lbryinc",
"lodash": ">=4.17.11",
"merge": ">=1.2.1",
@ -41,10 +41,11 @@
"redux-persist-transform-compress": "^4.2.0",
"redux-persist-transform-filter": "0.0.18",
"redux-thunk": "^2.3.0",
"rn-fetch-blob": "^0.10.15"
"rn-fetch-blob": "0.10.15"
},
"devDependencies": {
"@babel/core": "^7.5.4",
"babel-eslint": "10.0.2",
"@babel/plugin-proposal-object-rest-spread": "^7.5.4",
"babel-preset-env": "^1.6.1",
"babel-preset-stage-2": "^6.18.0",
@ -59,6 +60,7 @@
"eslint-plugin-react": "^7.12.4",
"eslint-plugin-standard": "^4.0.0",
"flow-babel-webpack-plugin": "^1.1.1",
"husky": "^0.14.3",
"lint-staged": "^7.0.4",
"metro-react-native-babel-preset": "^0.55.0",
"prettier": "^1.11.1"

View file

@ -7,6 +7,7 @@ import FilePage from 'page/file';
import FirstRunScreen from 'page/firstRun';
import PublishPage from 'page/publish';
import RewardsPage from 'page/rewards';
import TagPage from 'page/tag';
import TrendingPage from 'page/trending';
import SearchPage from 'page/search';
import SettingsPage from 'page/settings';
@ -40,7 +41,7 @@ import { decode as atob } from 'base-64';
import { dispatchNavigateBack, dispatchNavigateToUri } from 'utils/helper';
import AsyncStorage from '@react-native-community/async-storage';
import Colors from 'styles/colors';
import Constants from 'constants';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import Icon from 'react-native-vector-icons/FontAwesome5';
import NavigationButton from 'component/navigationButton';
import discoverStyle from 'styles/discover';
@ -72,6 +73,12 @@ const discoverStack = createStackNavigator(
header: null,
}),
},
Tag: {
screen: TagPage,
navigationOptions: ({ navigation }) => ({
header: null,
}),
},
Search: {
screen: SearchPage,
navigationOptions: ({ navigation }) => ({
@ -87,9 +94,9 @@ const discoverStack = createStackNavigator(
discoverStack.navigationOptions = ({ navigation }) => {
let drawerLockMode = 'unlocked';
/*if (navigation.state.index > 0) {
/* if (navigation.state.index > 0) {
drawerLockMode = 'locked-closed';
}*/
} */
return {
drawerLockMode,
@ -139,7 +146,7 @@ const drawer = createDrawerNavigator(
screen: SubscriptionsPage,
navigationOptions: {
title: 'Subscriptions',
drawerIcon: ({ tintColor }) => <Icon name="heart" solid={true} size={20} style={{ color: tintColor }} />,
drawerIcon: ({ tintColor }) => <Icon name="heart" solid size={20} style={{ color: tintColor }} />,
},
},
WalletStack: {
@ -279,8 +286,8 @@ class AppWithNavigationState extends React.Component {
checkEmailVerification = () => {
const { dispatch } = this.props;
AsyncStorage.getItem(Constants.KEY_EMAIL_VERIFY_PENDING).then(pending => {
this.setState({ verifyPending: 'true' === pending });
if ('true' === pending) {
this.setState({ verifyPending: pending === Constants.TRUE_STRING });
if (pending === Constants.TRUE_STRING) {
dispatch(doUserCheckEmailVerified());
}
});
@ -322,7 +329,7 @@ class AppWithNavigationState extends React.Component {
currentDisplayType = 'toast';
}
if ('toast' === currentDisplayType) {
if (currentDisplayType === 'toast') {
ToastAndroid.show(message, ToastAndroid.LONG);
}
@ -331,7 +338,7 @@ class AppWithNavigationState extends React.Component {
if (user && !emailVerifyPending && !this.state.emailVerifyDone && (emailToVerify || emailVerifyErrorMessage)) {
AsyncStorage.getItem(Constants.KEY_SHOULD_VERIFY_EMAIL).then(shouldVerify => {
if ('true' === shouldVerify) {
if (shouldVerify === 'true') {
this.setState({ emailVerifyDone: true });
const message = emailVerifyErrorMessage
? String(emailVerifyErrorMessage)

View file

@ -1,7 +1,4 @@
import { connect } from 'react-redux';
import CategoryList from './view';
export default connect(
null,
null
)(CategoryList);
export default connect()(CategoryList);

View file

@ -0,0 +1,18 @@
import { connect } from 'react-redux';
import { doResolveUri, makeSelectClaimForUri, makeSelectThumbnailForUri, makeSelectIsUriResolving } from 'lbry-redux';
import ChannelIconItem from './view';
const select = (state, props) => ({
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
});
const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri)),
});
export default connect(
select,
perform
)(ChannelIconItem);

View file

@ -0,0 +1,51 @@
import React from 'react';
import { ActivityIndicator, Image, Text, TouchableOpacity, View } from 'react-native';
import Colors from 'styles/colors';
import channelIconStyle from 'styles/channelIcon';
export default class ChannelIconItem extends React.PureComponent {
componentDidMount() {
const { claim, isPlaceholder, uri, resolveUri } = this.props;
if (!claim && !isPlaceholder) {
resolveUri(uri);
}
}
render() {
const { claim, isPlaceholder, isResolvingUri, onPress, thumbnail, title } = this.props;
return (
<TouchableOpacity style={channelIconStyle.container} onPress={onPress}>
{isResolvingUri && (
<View style={channelIconStyle.centered}>
<ActivityIndicator size={'small'} color={Colors.LbryGreen} />
</View>
)}
<View
style={[
channelIconStyle.thumbnailContainer,
isPlaceholder ? channelIconStyle.borderedThumbnailContainer : null,
]}
>
{isPlaceholder && (
<View style={channelIconStyle.centered}>
<Text style={channelIconStyle.placeholderText}>ALL</Text>
</View>
)}
{!isPlaceholder && (
<Image
style={channelIconStyle.thumbnail}
resizeMode={'cover'}
source={thumbnail ? { uri: thumbnail } : require('../../assets/default_avatar.jpg')}
/>
)}
</View>
{!isPlaceholder && (
<Text style={channelIconStyle.title} numberOfLines={1}>
{title || (claim ? claim.name : '')}
</Text>
)}
</TouchableOpacity>
);
}
}

View file

@ -0,0 +1,40 @@
import { connect } from 'react-redux';
import {
MATURE_TAGS,
doClaimSearch,
doClaimSearchByTags,
makeSelectClaimSearchUrisForTags,
makeSelectFetchingClaimSearchForTags,
selectFetchingClaimSearch,
selectLastClaimSearchUris,
} from 'lbry-redux';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import ClaimList from './view';
const select = (state, props) => {
return {
loading: makeSelectFetchingClaimSearchForTags(props.tags)(state),
uris: makeSelectClaimSearchUrisForTags(props.tags)(state),
// for subscriptions
claimSearchLoading: selectFetchingClaimSearch(state),
claimSearchUris: selectLastClaimSearchUris(state),
};
};
const perform = dispatch => ({
claimSearch: options => dispatch(doClaimSearch(Constants.DEFAULT_PAGE_SIZE, options)),
searchByTags: (tags, orderBy = Constants.DEFAULT_ORDER_BY, page = 1) =>
dispatch(
doClaimSearchByTags(tags, Constants.DEFAULT_PAGE_SIZE, {
no_totals: true,
order_by: orderBy,
page,
not_tags: MATURE_TAGS,
})
),
});
export default connect(
select,
perform
)(ClaimList);

View file

@ -0,0 +1,210 @@
import React from 'react';
import NavigationActions from 'react-navigation';
import { ActivityIndicator, FlatList, Text, View } from 'react-native';
import { MATURE_TAGS, normalizeURI } from 'lbry-redux';
import _ from 'lodash';
import FileItem from 'component/fileItem';
import FileListItem from 'component/fileListItem';
import Colors from 'styles/colors';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import claimListStyle from 'styles/claimList';
import discoverStyle from 'styles/discover';
const horizontalLimit = 10;
const softLimit = 500;
class ClaimList extends React.PureComponent {
scrollView = null;
state = {
currentPage: 1, // initial page load is page 1
subscriptionsView: false, // whether or not this claim list is for subscriptions
trendingForAllView: false,
};
componentDidMount() {
const {
channelIds,
trendingForAll,
claimSearch,
orderBy = Constants.DEFAULT_ORDER_BY,
searchByTags,
tags,
} = this.props;
if (channelIds || trendingForAll) {
const options = {
order_by: orderBy,
no_totals: true,
not_tags: MATURE_TAGS,
page: this.state.currentPage,
};
if (channelIds) {
this.setState({ subscriptionsView: true });
options.channel_ids = channelIds;
} else if (trendingForAll) {
this.setState({ trendingForAllView: true });
}
claimSearch(options);
} else if (tags && tags.length > 0) {
searchByTags(tags, orderBy, this.state.currentPgae);
}
}
componentWillReceiveProps(nextProps) {
const {
claimSearch,
orderBy: prevOrderBy,
searchByTags,
tags: prevTags,
channelIds: prevChannelIds,
trendingForAll: prevTrendingForAll,
} = this.props;
const { orderBy, tags, channelIds, trendingForAll } = nextProps;
if (
!_.isEqual(orderBy, prevOrderBy) ||
!_.isEqual(tags, prevTags) ||
!_.isEqual(channelIds, prevChannelIds) ||
trendingForAll !== prevTrendingForAll
) {
// reset to page 1 because the order, tags or channelIds changed
this.setState({ currentPage: 1 }, () => {
if (this.scrollView) {
this.scrollView.scrollToOffset({ animated: true, offset: 0 });
}
if (trendingForAll || (prevChannelIds && channelIds)) {
const options = {
order_by: orderBy,
no_totals: true,
not_tags: MATURE_TAGS,
page: this.state.currentPage,
};
if (channelIds) {
this.setState({ subscriptionsView: true });
options.channel_ids = channelIds;
}
if (trendingForAll) {
this.setState({ trendingForAllView: true });
}
claimSearch(options);
} else if (tags && tags.length > 0) {
this.setState({ subscriptionsView: false, trendingForAllView: false });
searchByTags(tags, orderBy, this.state.currentPage);
}
});
}
}
handleVerticalEndReached = () => {
// fetch more content
const { channelIds, claimSearch, claimSearchUris, orderBy, searchByTags, tags, uris } = this.props;
const { subscriptionsView, trendingForAllView } = this.state;
if ((claimSearchUris && claimSearchUris.length >= softLimit) || (uris && uris.length >= softLimit)) {
// don't fetch more than the specified limit to be displayed
return;
}
this.setState({ currentPage: this.state.currentPage + 1 }, () => {
if (subscriptionsView || trendingForAllView) {
const options = {
order_by: orderBy,
no_totals: true,
not_tags: MATURE_TAGS,
page: this.state.currentPage,
};
if (subscriptionsView) {
options.channel_ids = channelIds;
}
claimSearch(options);
} else {
searchByTags(tags, orderBy, this.state.currentPage);
}
});
};
render() {
const {
ListHeaderComponent,
loading,
claimSearchLoading,
claimSearchUris,
navigation,
orientation = Constants.ORIENTATION_VERTICAL,
style,
uris,
} = this.props;
const { subscriptionsView, trendingForAllView } = this.state;
if (Constants.ORIENTATION_VERTICAL === orientation) {
const data = subscriptionsView || trendingForAllView ? claimSearchUris : uris;
return (
<View style={style}>
<FlatList
ref={ref => {
this.scrollView = ref;
}}
ListHeaderComponent={ListHeaderComponent}
style={claimListStyle.verticalScrollContainer}
contentContainerStyle={claimListStyle.verticalScrollPadding}
initialNumToRender={8}
maxToRenderPerBatch={24}
removeClippedSubviews
renderItem={({ item }) => (
<FileListItem key={item} uri={item} style={claimListStyle.verticalListItem} navigation={navigation} />
)}
data={data}
keyExtractor={(item, index) => item}
onEndReached={this.handleVerticalEndReached}
onEndReachedThreshold={0.9}
/>
{(((subscriptionsView || trendingForAllView) && claimSearchLoading) || loading) && (
<View style={claimListStyle.verticalLoading}>
<ActivityIndicator size={'small'} color={Colors.LbryGreen} />
</View>
)}
</View>
);
}
if (Constants.ORIENTATION_HORIZONTAL === orientation) {
if (loading) {
return (
<View style={discoverStyle.listLoading}>
<ActivityIndicator size={'small'} color={Colors.LbryGreen} />
</View>
);
}
return (
<FlatList
style={style || claimListStyle.horizontalScrollContainer}
contentContainerStyle={claimListStyle.horizontalScrollPadding}
initialNumToRender={3}
maxToRenderPerBatch={3}
removeClippedSubviews
renderItem={({ item }) => (
<FileItem
style={discoverStyle.fileItem}
mediaStyle={discoverStyle.fileItemMedia}
key={item}
uri={normalizeURI(item)}
navigation={navigation}
showDetails
compactView={false}
/>
)}
horizontal
showsHorizontalScrollIndicator={false}
data={uris ? uris.slice(0, horizontalLimit) : []}
keyExtractor={(item, index) => item}
/>
);
}
return null;
}
}
export default ClaimList;

View file

@ -13,10 +13,6 @@ import NsfwOverlay from 'component/nsfwOverlay';
import discoverStyle from 'styles/discover';
class FileItem extends React.PureComponent {
constructor(props) {
super(props);
}
componentWillMount() {
this.resolve(this.props);
}
@ -64,8 +60,7 @@ class FileItem extends React.PureComponent {
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
const signingChannel = claim ? claim.signing_channel : null;
const channelName = signingChannel ? signingChannel.name : null;
const channelClaimId =
claim && claim.value && claim.value.publisherSignature && claim.value.publisherSignature.certificateId;
const channelClaimId = signingChannel ? signingChannel.claim_id : null;
const fullChannelUri = channelClaimId ? `${channelName}#${channelClaimId}` : channelName;
const height = claim ? claim.height : null;
@ -87,13 +82,7 @@ class FileItem extends React.PureComponent {
/>
{!compactView && fileInfo && fileInfo.completed && fileInfo.download_path && (
<Icon
style={discoverStyle.downloadedIcon}
solid={true}
color={Colors.NextLbryGreen}
name={'folder'}
size={16}
/>
<Icon style={discoverStyle.downloadedIcon} solid color={Colors.NextLbryGreen} name={'folder'} size={16} />
)}
{!compactView && (!fileInfo || !fileInfo.completed || !fileInfo.download_path) && (
<FilePrice uri={uri} style={discoverStyle.filePriceContainer} textStyle={discoverStyle.filePriceText} />

View file

@ -41,6 +41,11 @@ class FileListItem extends React.PureComponent {
}
}
defaultOnPress = () => {
const { navigation, uri } = this.props;
navigateToUri(navigation, uri);
};
render() {
const {
claim,
@ -60,13 +65,13 @@ class FileListItem extends React.PureComponent {
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
const isResolving = !fileInfo && isResolvingUri;
let name, channel, height, channelClaimId, fullChannelUri;
let name, channel, height, channelClaimId, fullChannelUri, signingChannel;
if (claim) {
name = claim.name;
signingChannel = claim.signing_channel;
channel = signingChannel ? signingChannel.name : null;
height = claim.height;
channelClaimId = claim.value && claim.value.publisherSignature && claim.value.publisherSignature.certificateId;
channelClaimId = signingChannel ? signingChannel.claim_id : null;
fullChannelUri = channelClaimId ? `${channel}#${channelClaimId}` : channel;
}
@ -76,7 +81,7 @@ class FileListItem extends React.PureComponent {
return (
<View style={style}>
<TouchableOpacity style={style} onPress={onPress}>
<TouchableOpacity style={style} onPress={onPress || this.defaultOnPress}>
<FileItemMedia
style={fileListStyle.thumbnail}
blurRadius={obscureNsfw ? 15 : 0}
@ -85,13 +90,7 @@ class FileListItem extends React.PureComponent {
thumbnail={thumbnail}
/>
{fileInfo && fileInfo.completed && fileInfo.download_path && (
<Icon
style={fileListStyle.downloadedIcon}
solid={true}
color={Colors.NextLbryGreen}
name={'folder'}
size={16}
/>
<Icon style={fileListStyle.downloadedIcon} solid color={Colors.NextLbryGreen} name={'folder'} size={16} />
)}
<View style={fileListStyle.detailsContainer}>
{featuredResult && (

View file

@ -428,7 +428,7 @@ class MediaPlayer extends React.PureComponent {
bufferForPlaybackMs: 5000,
bufferForPlaybackAfterRebufferMs: 5000,
}}
ref={(ref: Video) => {
ref={ref => {
this.video = ref;
}}
resizeMode={this.state.resizeMode}
@ -445,7 +445,7 @@ class MediaPlayer extends React.PureComponent {
minLoadRetryCount={999}
/>
{this.state.firstPlay && thumbnail && thumbnail.trim().length > 0 && (
{this.state.firstPlay && thumbnail && (
<FastImage
source={{ uri: thumbnail }}
resizeMode={FastImage.resizeMode.cover}

View file

@ -0,0 +1,4 @@
import { connect } from 'react-redux';
import ModalPicker from './view';
export default connect()(ModalPicker);

View file

@ -0,0 +1,60 @@
import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import modalPickerStyle from 'styles/modalPicker';
import Colors from 'styles/colors';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import Icon from 'react-native-vector-icons/FontAwesome5';
export default class ModalPicker extends React.PureComponent {
state = {
selectedItem: null,
};
componentDidMount() {
const { items, selectedItem } = this.props;
if (!selectedItem && items && items.length > 0) {
this.setState({ selectedItem: items[0] });
return;
}
this.setState({ selectedItem });
}
componentWillReceiveProps(nextProps) {
const { selectedItem: prevSelectedItem } = this.props;
const { selectedItem } = nextProps;
if (selectedItem && selectedItem.name !== prevSelectedItem.name) {
this.setState({ selectedItem });
}
}
render() {
const { items, onItemSelected, title, onOverlayPress } = this.props;
const { selectedItem } = this.state;
return (
<TouchableOpacity style={modalPickerStyle.overlay} activeOpacity={1} onPress={onOverlayPress}>
<View style={modalPickerStyle.container}>
<Text style={modalPickerStyle.title}>{title}</Text>
<View style={modalPickerStyle.divider} />
<View style={modalPickerStyle.list}>
{items.length &&
items.map(item => (
<TouchableOpacity
key={item.name}
style={modalPickerStyle.listItem}
onPress={() => onItemSelected(item)}
>
<Icon style={modalPickerStyle.itemIcon} name={item.icon} size={16} />
<Text style={modalPickerStyle.itemLabel}>{item.label}</Text>
{selectedItem && selectedItem.name === item.name && (
<Icon style={modalPickerStyle.itemSelected} name={'check'} color={Colors.LbryGreen} size={16} />
)}
</TouchableOpacity>
))}
</View>
</View>
</TouchableOpacity>
);
}
}

View file

@ -0,0 +1,27 @@
import { connect } from 'react-redux';
import {
selectUnfollowedTags,
selectFollowedTags,
doReplaceTags,
doToggleTagFollow,
doAddTag,
doDeleteTag,
doToast,
} from 'lbry-redux';
import ModalTagSelector from './view';
const select = state => ({
unfollowedTags: selectUnfollowedTags(state),
followedTags: selectFollowedTags(state),
});
export default connect(
select,
{
doToggleTagFollow,
doAddTag,
doDeleteTag,
doReplaceTags,
doToast,
}
)(ModalTagSelector);

View file

@ -0,0 +1,66 @@
import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { DEFAULT_FOLLOWED_TAGS } from 'lbry-redux';
import Button from 'component/button';
import Colors from 'styles/colors';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import Icon from 'react-native-vector-icons/FontAwesome5';
import Tag from 'component/tag';
import TagSearch from 'component/tagSearch';
import modalTagSelectorStyle from 'styles/modalTagSelector';
import __ from 'utils/helper';
export default class ModalTagSelector extends React.PureComponent {
handleAddTag = tag => {
if (!tag) {
return;
}
const { followedTags, doToast } = this.props;
if (followedTags.map(followedTag => followedTag.name).includes(tag.toLowerCase())) {
doToast({ message: __(`You already added the "${tag}" tag.`) });
return;
}
this.props.doToggleTagFollow(tag);
};
handleRemoveTag = tag => {
if (!tag) {
return;
}
this.props.doToggleTagFollow(tag);
};
render() {
const { followedTags, onOverlayPress, onDonePress } = this.props;
const tags = followedTags ? followedTags.map(tag => tag.name) : DEFAULT_FOLLOWED_TAGS;
return (
<TouchableOpacity style={modalTagSelectorStyle.overlay} activeOpacity={1} onPress={onOverlayPress}>
<View style={modalTagSelectorStyle.container}>
<View style={modalTagSelectorStyle.titleRow}>
<Text style={modalTagSelectorStyle.title}>Customize your tags</Text>
</View>
<View style={modalTagSelectorStyle.tagList}>
{tags &&
tags.map(tag => (
<Tag
key={tag}
name={tag}
type={'remove'}
style={modalTagSelectorStyle.tag}
onRemovePress={this.handleRemoveTag}
/>
))}
</View>
<TagSearch handleAddTag={this.handleAddTag} selectedTags={tags} />
<View style={modalTagSelectorStyle.buttons}>
<Button style={modalTagSelectorStyle.doneButton} text={'Done'} onPress={onDonePress} />
</View>
</View>
</TouchableOpacity>
);
}
}

View file

@ -0,0 +1,4 @@
import { connect } from 'react-redux';
import SubscribedChannelList from './view';
export default connect()(SubscribedChannelList);

View file

@ -0,0 +1,36 @@
import React from 'react';
import { Text, FlatList, View } from 'react-native';
import { normalizeURI } from 'lbry-redux';
import ChannelIconItem from 'component/channelIconItem';
import Colors from 'styles/colors';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import subscriptionsStyle from 'styles/subscriptions';
export default class SubscribedChannelList extends React.PureComponent {
render() {
const { subscribedChannels, onChannelSelected } = this.props;
return (
<View style={subscriptionsStyle.channelList}>
<FlatList
contentContainerStyle={subscriptionsStyle.channelListScrollContainer}
initialNumToRender={5}
maxToRenderPerBatch={5}
removeClippedSubviews
horizontal
showsHorizontalScrollIndicator={false}
renderItem={({ item }) => (
<ChannelIconItem
key={item}
isPlaceholder={item.toLowerCase() === Constants.ALL_PLACEHOLDER}
uri={normalizeURI(item)}
onPress={() => onChannelSelected(item)}
/>
)}
data={subscribedChannels}
keyExtractor={(item, index) => item}
/>
</View>
);
}
}

View file

@ -1,22 +1,22 @@
import { connect } from 'react-redux';
import {
makeSelectFetchingChannelClaims,
makeSelectClaimsInChannelForPage,
doFetchClaimsByChannel,
doResolveUris,
doResolveUri,
makeSelectClaimForUri,
makeSelectThumbnailForUri,
makeSelectTitleForUri,
makeSelectIsUriResolving,
} from 'lbry-redux';
import { selectShowNsfw } from 'redux/selectors/settings';
import SuggestedSubscriptionItem from './view';
const select = (state, props) => ({
claims: makeSelectClaimsInChannelForPage(props.categoryLink)(state),
fetching: makeSelectFetchingChannelClaims(props.categoryLink)(state),
obscureNsfw: !selectShowNsfw(state),
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
title: makeSelectTitleForUri(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
});
const perform = dispatch => ({
fetchChannel: channel => dispatch(doFetchClaimsByChannel(channel)),
resolveUris: uris => dispatch(doResolveUris(uris, true)),
resolveUri: uri => dispatch(doResolveUri(uri)),
});
export default connect(

View file

@ -1,76 +1,70 @@
import React from 'react';
import { buildURI, normalizeURI } from 'lbry-redux';
import { ActivityIndicator, FlatList, Text, View } from 'react-native';
import { ActivityIndicator, FlatList, Image, Text, View } from 'react-native';
import Colors from 'styles/colors';
import discoverStyle from 'styles/discover';
import FileItem from 'component/fileItem';
import SubscribeButton from 'component/subscribeButton';
import subscriptionsStyle from 'styles/subscriptions';
import Tag from 'component/tag';
class SuggestedSubscriptionItem extends React.PureComponent {
componentDidMount() {
const { fetching, categoryLink, fetchChannel, resolveUris, claims } = this.props;
if (!fetching && categoryLink && (!claims || claims.length)) {
fetchChannel(categoryLink);
const { claim, uri, resolveUri } = this.props;
if (!claim) {
resolveUri(uri);
}
}
uriForClaim = claim => {
const { name: claimName, claim_name: claimNameDownloaded, claim_id: claimId } = claim;
const uriParams = {};
// This is unfortunate
// https://github.com/lbryio/lbry/issues/1159
const name = claimName || claimNameDownloaded;
uriParams.contentName = name;
uriParams.claimId = claimId;
const uri = buildURI(uriParams);
return uri;
};
render() {
const { categoryLink, fetching, obscureNsfw, claims, navigation } = this.props;
const { claim, isResolvingUri, navigation, thumbnail, title, uri } = this.props;
let tags;
if (claim && claim.value) {
tags = claim.value.tags;
}
if (!claims || !claims.length) {
if (isResolvingUri) {
return (
<View style={subscriptionsStyle.busyContainer}>
<View style={subscriptionsStyle.itemLoadingContainer}>
<ActivityIndicator size={'small'} color={Colors.LbryGreen} />
</View>
);
}
if (claims && claims.length > 0) {
return (
<View style={subscriptionsStyle.suggestedContainer}>
<FileItem
style={subscriptionsStyle.compactMainFileItem}
mediaStyle={subscriptionsStyle.fileItemMedia}
uri={this.uriForClaim(claims[0])}
navigation={navigation}
return (
<View style={subscriptionsStyle.suggestedItem}>
<View style={subscriptionsStyle.suggestedItemThumbnailContainer}>
<Image
style={subscriptionsStyle.suggestedItemThumbnail}
resizeMode={'cover'}
source={thumbnail ? { uri: thumbnail } : require('../../assets/default_avatar.jpg')}
/>
{claims.length > 1 && (
<FlatList
style={subscriptionsStyle.compactItems}
horizontal={true}
renderItem={({ item }) => (
<FileItem
style={subscriptionsStyle.compactFileItem}
mediaStyle={subscriptionsStyle.compactFileItemMedia}
key={item}
uri={normalizeURI(item)}
navigation={navigation}
compactView={true}
/>
)}
data={claims.slice(1, 4).map(claim => this.uriForClaim(claim))}
keyExtractor={(item, index) => item}
/>
)}
</View>
);
}
return null;
<View style={subscriptionsStyle.suggestedItemDetails}>
<View style={subscriptionsStyle.suggestedItemInfo}>
{title && (
<Text style={subscriptionsStyle.suggestedItemTitle} numberOfLines={1}>
{title}
</Text>
)}
<Text style={subscriptionsStyle.suggestedItemName} numberOfLines={1}>
{claim && claim.name}
</Text>
{tags && (
<View style={subscriptionsStyle.suggestedItemTagList}>
{tags &&
tags
.slice(0, 3)
.map(tag => <Tag style={subscriptionsStyle.tag} key={tag} name={tag} navigation={navigation} />)}
</View>
)}
</View>
</View>
<SubscribeButton style={subscriptionsStyle.suggestedItemSubscribe} uri={normalizeURI(uri)} />
</View>
);
}
}

View file

@ -1,13 +1,20 @@
import { connect } from 'react-redux';
import { doClaimSearch, selectFetchingClaimSearch, selectLastClaimSearchUris, selectFollowedTags } from 'lbry-redux';
import { selectSuggestedChannels, selectIsFetchingSuggested } from 'lbryinc';
import SuggestedSubscriptions from './view';
const select = state => ({
followedTags: selectFollowedTags(state),
suggested: selectSuggestedChannels(state),
loading: selectIsFetchingSuggested(state),
loading: selectIsFetchingSuggested(state) || selectFetchingClaimSearch(state),
claimSearchUris: selectLastClaimSearchUris(state),
});
const perform = dispatch => ({
claimSearch: options => dispatch(doClaimSearch(10, options)),
});
export default connect(
select,
null
perform
)(SuggestedSubscriptions);

View file

@ -1,53 +1,67 @@
import React from 'react';
import { ActivityIndicator, SectionList, Text, View } from 'react-native';
import { ActivityIndicator, FlatList, SectionList, Text, View } from 'react-native';
import { normalizeURI } from 'lbry-redux';
import { navigateToUri } from 'utils/helper';
import __, { navigateToUri } from 'utils/helper';
import SubscribeButton from 'component/subscribeButton';
import SuggestedSubscriptionItem from 'component/suggestedSubscriptionItem';
import Colors from 'styles/colors';
import discoverStyle from 'styles/discover';
import subscriptionsStyle from 'styles/subscriptions';
import Link from 'component/link';
import _ from 'lodash';
class SuggestedSubscriptions extends React.PureComponent {
componentDidMount() {
const { claimSearch, followedTags } = this.props;
const options = {
any_tags: _.shuffle(followedTags.map(tag => tag.name)).slice(0, 2),
page: 1,
no_totals: true,
claim_type: 'channel',
};
claimSearch(options);
}
buildSections = () => {
const { suggested, claimSearchUris } = this.props;
const suggestedUris = suggested ? suggested.map(suggested => suggested.uri) : [];
return [
{
title: __('You might like'),
data: suggestedUris,
},
{
title: __('Tags you follow'),
data: claimSearchUris ? claimSearchUris.filter(uri => !suggestedUris.includes(uri)) : [],
},
];
};
render() {
const { suggested, loading, navigation } = this.props;
if (loading) {
return (
<View>
<View style={subscriptionsStyle.centered}>
<ActivityIndicator size="large" color={Colors.LbryGreen} />
</View>
);
}
return suggested ? (
return (
<SectionList
style={subscriptionsStyle.scrollContainer}
contentContainerStyle={subscriptionsStyle.suggestedScrollPadding}
renderItem={({ item, index, section }) => (
<SuggestedSubscriptionItem key={item} categoryLink={normalizeURI(item)} navigation={navigation} />
<SuggestedSubscriptionItem key={item} uri={normalizeURI(item)} navigation={navigation} />
)}
renderSectionHeader={({ section: { title } }) => {
const titleParts = title.split(';');
const channelName = titleParts[0];
const channelUri = normalizeURI(titleParts[1]);
return (
<View style={subscriptionsStyle.titleRow}>
<Link
style={subscriptionsStyle.channelTitle}
text={channelName}
onPress={() => {
navigateToUri(navigation, normalizeURI(channelUri));
}}
/>
<SubscribeButton style={subscriptionsStyle.subscribeButton} uri={channelUri} name={channelName} />
</View>
);
}}
sections={suggested.map(({ uri, label }) => ({ title: label + ';' + uri, data: [uri] }))}
renderSectionHeader={({ section: { title } }) => (
<Text style={subscriptionsStyle.suggestedSubTitle}>{title}</Text>
)}
sections={this.buildSections()}
keyExtractor={(item, index) => item}
/>
) : null;
);
}
}

View file

@ -2,6 +2,7 @@ import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import tagStyle from 'styles/tag';
import Colors from 'styles/colors';
import Constants from 'constants';
import Icon from 'react-native-vector-icons/FontAwesome5';
export default class Tag extends React.PureComponent {
@ -22,6 +23,7 @@ export default class Tag extends React.PureComponent {
if (navigation) {
// navigate to tag page
navigation.navigate({ routeName: Constants.DRAWER_ROUTE_TAG, key: `tagPage`, params: { tag: name } });
}
};

View file

@ -1,5 +1,5 @@
import React from 'react';
import { Text, TextInput, TouchableOpacity, View } from 'react-native';
import { KeyboardAvoidingView, Text, TextInput, TouchableOpacity, View } from 'react-native';
import Tag from 'component/tag';
import tagStyle from 'styles/tag';
import Colors from 'styles/colors';
@ -47,9 +47,17 @@ export default class TagSearch extends React.PureComponent {
const suggestedTagsSet = new Set(unfollowedTags.map(tag => tag.name));
const suggestedTags = Array.from(suggestedTagsSet).filter(tagNotSelected);
if (tag && tag.trim().length > 0) {
results.push(tag.toLowerCase());
const lcTag = tag.toLowerCase();
if (!results.includes(lcTag)) {
results.push(lcTag);
}
const doesTagMatch = name => name.toLowerCase().includes(tag.toLowerCase());
results = results.concat(suggestedTags.filter(doesTagMatch).slice(0, 5));
results = results.concat(
suggestedTags
.filter(doesTagMatch)
.filter(suggested => lcTag !== suggested.toLowerCase())
.slice(0, 5)
);
} else {
results = results.concat(suggestedTags.slice(0, 5));
}
@ -70,11 +78,13 @@ export default class TagSearch extends React.PureComponent {
numberOfLines={1}
onChangeText={this.handleTagChange}
/>
<View style={tagStyle.tagResultsList}>
{this.state.tagResults.map(tag => (
<Tag key={tag} name={tag} style={tagStyle.tag} type="add" onAddPress={name => this.onAddTagPress(name)} />
))}
</View>
<KeyboardAvoidingView behavior={'position'}>
<View style={tagStyle.tagResultsList}>
{this.state.tagResults.map(tag => (
<Tag key={tag} name={tag} style={tagStyle.tag} type="add" onAddPress={name => this.onAddTagPress(name)} />
))}
</View>
</KeyboardAvoidingView>
</View>
);
}

View file

@ -136,19 +136,19 @@ class UriBar extends React.PureComponent {
}
render() {
const { navigation, suggestions, query, value } = this.props;
const { navigation, suggestions, query, value, belowOverlay } = this.props;
if (this.state.currentValue === null) {
this.setState({ currentValue: value });
}
let style = [uriBarStyle.overlay];
let style = [uriBarStyle.overlay, belowOverlay ? null : uriBarStyle.overlayElevated];
// TODO: Add optional setting to enable URI / search bar suggestions
/*if (this.state.focused) { style.push(uriBarStyle.inFocus); }*/
return (
<View style={style}>
<View style={uriBarStyle.uriContainer}>
<View style={[uriBarStyle.uriContainer, belowOverlay ? null : uriBarStyle.containerElevated]}>
<NavigationButton
name="bars"
size={24}

View file

@ -1,3 +1,7 @@
const SORT_BY_NEW = 'new';
const SORT_BY_HOT = 'hot';
const SORT_BY_TOP = 'top';
const Constants = {
FIRST_RUN_PAGE_WELCOME: 'welcome',
FIRST_RUN_PAGE_EMAIL_COLLECT: 'email-collect',
@ -43,6 +47,9 @@ const Constants = {
ACTION_REACT_NAVIGATION_NAVIGATE: 'Navigation/NAVIGATE',
ACTION_REACT_NAVIGATION_REPLACE: 'Navigation/REPLACE',
ORIENTATION_HORIZONTAL: 'horizontal',
ORIENTATION_VERTICAL: 'vertical',
PAGE_REWARDS: 'rewards',
PAGE_SETTINGS: 'settings',
PAGE_TRENDING: 'trending',
@ -59,6 +66,7 @@ const Constants = {
DRAWER_ROUTE_ABOUT: 'About',
DRAWER_ROUTE_SEARCH: 'Search',
DRAWER_ROUTE_TRANSACTION_HISTORY: 'TransactionHistory',
DRAWER_ROUTE_TAG: 'Tag',
FULL_ROUTE_NAME_DISCOVER: 'DiscoverStack',
FULL_ROUTE_NAME_TRENDING: 'TrendingStack',
@ -76,6 +84,24 @@ const Constants = {
PLAY_STORE_URL: 'https://play.google.com/store/apps/details?id=io.lbry.browser',
RATING_REMINDER_INTERVAL: 604800, // 7 days (7 * 24 * 3600s)
SORT_BY_HOT,
SORT_BY_NEW,
SORT_BY_TOP,
CLAIM_SEARCH_SORT_BY_ITEMS: [
{ icon: 'fire-alt', name: SORT_BY_HOT, label: 'Hot content' },
{ icon: 'certificate', name: SORT_BY_NEW, label: 'New content' },
{ icon: 'chart-line', name: SORT_BY_TOP, label: 'Top content' },
],
DEFAULT_ORDER_BY: ['trending_global', 'trending_mixed'],
DEFAULT_PAGE_SIZE: 10,
ALL_PLACEHOLDER: '_all',
TRUE_STRING: 'true',
};
export default Constants;

View file

@ -42,7 +42,6 @@ import thunk from 'redux-thunk';
const globalExceptionHandler = (error, isFatal) => {
if (error && NativeModules.Firebase) {
console.log(error);
NativeModules.Firebase.logException(isFatal, error.message ? error.message : 'No message', JSON.stringify(error));
}
};
@ -76,9 +75,9 @@ function enableBatching(reducer) {
};
}
/*const router = AppNavigator.router;
/* const router = AppNavigator.router;
const navAction = router.getActionForPathAndParams('FirstRun');
const initialNavState = router.getStateForAction(navAction);*/
const initialNavState = router.getStateForAction(navAction); */
const reducers = combineReducers({
auth: authReducer,
@ -122,10 +121,11 @@ const contentFilter = createFilter('content', ['positions']);
const saveClaimsFilter = createFilter('claims', ['byId', 'claimsByUri']);
const subscriptionsFilter = createFilter('subscriptions', ['enabledChannelNotifications', 'subscriptions']);
const settingsFilter = createFilter('settings', ['clientSettings']);
const tagsFilter = createFilter('tags', ['followedTags']);
const walletFilter = createFilter('wallet', ['receiveAddress']);
const persistOptions = {
whitelist: ['auth', 'claims', 'content', 'subscriptions', 'settings', 'wallet'],
whitelist: ['auth', 'claims', 'content', 'subscriptions', 'settings', 'tags', 'wallet'],
// Order is important. Needs to be compressed last or other transforms can't
// read the data
transforms: [authFilter, saveClaimsFilter, subscriptionsFilter, settingsFilter, walletFilter, compressor],

View file

@ -1,5 +1,12 @@
import { connect } from 'react-redux';
import { doFileList, selectBalance, selectFileInfosDownloaded } from 'lbry-redux';
import {
doClaimSearch,
doFileList,
selectBalance,
selectFileInfosDownloaded,
selectLastClaimSearchUris,
selectFollowedTags,
} from 'lbry-redux';
import {
doFetchFeaturedUris,
doFetchRewardedContent,
@ -11,9 +18,10 @@ import {
selectSubscriptionClaims,
selectUnreadSubscriptions,
} from 'lbryinc';
import { doSetClientSetting } from 'redux/actions/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import Constants from 'constants';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import DiscoverPage from './view';
const select = state => ({
@ -23,12 +31,15 @@ const select = state => ({
featuredUris: selectFeaturedUris(state),
fetchingFeaturedUris: selectFetchingFeaturedUris(state),
fileInfos: selectFileInfosDownloaded(state),
followedTags: selectFollowedTags(state),
ratingReminderDisabled: makeSelectClientSetting(Constants.SETTING_RATING_REMINDER_DISABLED)(state),
ratingReminderLastShown: makeSelectClientSetting(Constants.SETTING_RATING_REMINDER_LAST_SHOWN)(state),
unreadSubscriptions: selectUnreadSubscriptions(state),
uris: selectLastClaimSearchUris(state),
});
const perform = dispatch => ({
doClaimSearch,
fetchFeaturedUris: () => dispatch(doFetchFeaturedUris()),
fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
fetchSubscriptions: () => dispatch(doFetchMySubscriptions()),

View file

@ -1,17 +1,41 @@
import React from 'react';
import NavigationActions from 'react-navigation';
import { Alert, ActivityIndicator, Linking, NativeModules, SectionList, Text, View } from 'react-native';
import { Lbry, normalizeURI, parseURI } from 'lbry-redux';
import {
Alert,
ActivityIndicator,
Linking,
NativeModules,
SectionList,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { DEFAULT_FOLLOWED_TAGS, Lbry, normalizeURI, parseURI } from 'lbry-redux';
import __, { formatTagTitle } from 'utils/helper';
import AsyncStorage from '@react-native-community/async-storage';
import moment from 'moment';
import CategoryList from 'component/categoryList';
import Constants from 'constants';
import ClaimList from 'component/claimList';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import Colors from 'styles/colors';
import discoverStyle from 'styles/discover';
import FloatingWalletBalance from 'component/floatingWalletBalance';
import Icon from 'react-native-vector-icons/FontAwesome5';
import Link from 'component/link';
import ModalTagSelector from 'component/modalTagSelector';
import ModalPicker from 'component/modalPicker';
import UriBar from 'component/uriBar';
import _ from 'lodash';
class DiscoverPage extends React.PureComponent {
state = {
tagCollection: [],
showModalTagSelector: false,
showSortPicker: false,
orderBy: Constants.DEFAULT_ORDER_BY,
currentSortByItem: Constants.CLAIM_SEARCH_SORT_BY_ITEMS[0],
};
componentDidMount() {
// Track the total time taken if this is the first launch
AsyncStorage.getItem('firstLaunchTime').then(startTime => {
@ -19,7 +43,7 @@ class DiscoverPage extends React.PureComponent {
// We don't need this value anymore once we've retrieved it
AsyncStorage.removeItem('firstLaunchTime');
// We know this is the first app launch because firstLaunchTime is set and it's a valid number
// We know this is the first app launch because firstLaunchTime is set and it"s a valid number
const start = parseInt(startTime, 10);
const now = moment().unix();
const delta = now - start;
@ -36,9 +60,9 @@ class DiscoverPage extends React.PureComponent {
}
});
const { fetchFeaturedUris, fetchRewardedContent, fetchSubscriptions, fileList } = this.props;
const { fetchRewardedContent, fetchSubscriptions, fileList, followedTags } = this.props;
fetchFeaturedUris();
this.buildTagCollection(followedTags);
fetchRewardedContent();
fetchSubscriptions();
fileList();
@ -46,6 +70,25 @@ class DiscoverPage extends React.PureComponent {
this.showRatingReminder();
}
handleSortByItemSelected = item => {
let orderBy = [];
switch (item.name) {
case Constants.SORT_BY_HOT:
orderBy = Constants.DEFAULT_ORDER_BY;
break;
case Constants.SORT_BY_NEW:
orderBy = ['release_time'];
break;
case Constants.SORT_BY_TOP:
orderBy = ['effective_amount'];
break;
}
this.setState({ currentSortByItem: item, orderBy, showSortPicker: false });
};
subscriptionForUri = (uri, channelName) => {
const { allSubscriptions } = this.props;
const { claimId, claimName } = parseURI(uri);
@ -63,6 +106,14 @@ class DiscoverPage extends React.PureComponent {
return null;
};
componentWillReceiveProps(nextProps) {
const { followedTags: prevFollowedTags } = this.props;
const { followedTags } = nextProps;
if (!_.isEqual(followedTags, prevFollowedTags)) {
this.buildTagCollection(followedTags);
}
}
componentDidUpdate(prevProps, prevState) {
const { unreadSubscriptions, enabledChannelNotifications } = this.props;
@ -112,7 +163,7 @@ class DiscoverPage extends React.PureComponent {
const { ratingReminderDisabled, ratingReminderLastShown, setClientSetting } = this.props;
const now = moment().unix();
if ('true' !== ratingReminderDisabled && ratingReminderLastShown) {
if (ratingReminderDisabled !== 'true' && ratingReminderLastShown) {
const lastShownParts = ratingReminderLastShown.split('|');
if (lastShownParts.length === 2) {
const lastShownTime = parseInt(lastShownParts[0], 10);
@ -154,43 +205,104 @@ class DiscoverPage extends React.PureComponent {
setClientSetting(Constants.SETTING_RATING_REMINDER_LAST_SHOWN, settingString);
};
trimClaimIdFromCategory(category) {
return category.split('#')[0];
}
buildSections = () => {
return this.state.tagCollection.map(tags => ({
title: tags.length === 1 ? tags[0] : 'Trending',
data: [tags],
}));
};
buildTagCollection = followedTags => {
const tags = followedTags.map(tag => tag.name);
// each of the followed tags
const tagCollection = tags.map(tag => [tag]);
// everything
tagCollection.unshift(tags);
this.setState({ tagCollection });
};
handleTagPress = name => {
const { navigation } = this.props;
if (name.toLowerCase() !== 'trending') {
navigation.navigate({ routeName: Constants.DRAWER_ROUTE_TAG, key: `tagPage`, params: { tag: name } });
} else {
// navigate to the trending page
navigation.navigate({ routeName: Constants.FULL_ROUTE_NAME_TRENDING });
}
};
render() {
const { featuredUris, fetchingFeaturedUris, navigation } = this.props;
const hasContent = typeof featuredUris === 'object' && Object.keys(featuredUris).length,
failedToLoad = !fetchingFeaturedUris && !hasContent;
const { navigation } = this.props;
const { currentSortByItem, orderBy, showModalTagSelector, showSortPicker } = this.state;
return (
<View style={discoverStyle.container}>
<UriBar navigation={navigation} />
{!hasContent && fetchingFeaturedUris && (
<View style={discoverStyle.busyContainer}>
<ActivityIndicator size="large" color={Colors.LbryGreen} />
<Text style={discoverStyle.title}>Fetching content...</Text>
</View>
)}
{!!hasContent && (
<SectionList
style={discoverStyle.scrollContainer}
contentContainerStyle={discoverStyle.scrollPadding}
initialNumToRender={4}
maxToRenderPerBatch={4}
removeClippedSubviews={true}
renderItem={({ item, index, section }) => (
<CategoryList key={item} category={item} categoryMap={featuredUris} navigation={navigation} />
)}
renderSectionHeader={({ section: { title } }) => <Text style={discoverStyle.categoryName}>{title}</Text>}
sections={Object.keys(featuredUris).map(category => ({
title: this.trimClaimIdFromCategory(category),
data: [category],
}))}
keyExtractor={(item, index) => item}
<UriBar navigation={navigation} belowOverlay={showModalTagSelector} />
<SectionList
ListHeaderComponent={
<View style={discoverStyle.titleRow}>
<Text style={discoverStyle.pageTitle}>Explore</Text>
<View style={discoverStyle.rightTitleRow}>
<Link
style={discoverStyle.customizeLink}
text={'Customize'}
onPress={() => this.setState({ showModalTagSelector: true })}
/>
<TouchableOpacity
style={discoverStyle.tagSortBy}
onPress={() => this.setState({ showSortPicker: true })}
>
<Text style={discoverStyle.tagSortText}>{currentSortByItem.label.split(' ')[0]}</Text>
<Icon style={discoverStyle.tagSortIcon} name={'sort-down'} size={14} />
</TouchableOpacity>
</View>
</View>
}
style={discoverStyle.scrollContainer}
contentContainerStyle={discoverStyle.scrollPadding}
initialNumToRender={4}
maxToRenderPerBatch={4}
removeClippedSubviews
renderItem={({ item, index, section }) => (
<ClaimList
key={item.join(',')}
orderBy={item.length > 1 ? Constants.DEFAULT_ORDER_BY : orderBy}
tags={item}
navigation={navigation}
orientation={Constants.ORIENTATION_HORIZONTAL}
/>
)}
renderSectionHeader={({ section: { title } }) => (
<View style={discoverStyle.categoryTitleRow}>
<Text style={discoverStyle.categoryName} onPress={() => this.handleTagPress(title)}>
{formatTagTitle(title)}
</Text>
<TouchableOpacity onPress={() => this.handleTagPress(title)}>
<Icon name={'ellipsis-v'} size={16} />
</TouchableOpacity>
</View>
)}
sections={this.buildSections()}
keyExtractor={(item, index) => item}
/>
{!showModalTagSelector && !showSortPicker && <FloatingWalletBalance navigation={navigation} />}
{showModalTagSelector && (
<ModalTagSelector
onOverlayPress={() => this.setState({ showModalTagSelector: false })}
onDonePress={() => this.setState({ showModalTagSelector: false })}
/>
)}
{showSortPicker && (
<ModalPicker
title={__('Sort content by')}
onOverlayPress={() => this.setState({ showSortPicker: false })}
onItemSelected={this.handleSortByItemSelected}
selectedItem={currentSortByItem}
items={Constants.CLAIM_SEARCH_SORT_BY_ITEMS}
/>
)}
<FloatingWalletBalance navigation={navigation} />
</View>
);
}

View file

@ -25,7 +25,7 @@ import Button from 'component/button';
import Tag from 'component/tag';
import ChannelPage from 'page/channel';
import Colors from 'styles/colors';
import Constants from 'constants';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import DateTime from 'component/dateTime';
import FileDownloadButton from 'component/fileDownloadButton';
import FileItemMedia from 'component/fileItemMedia';
@ -379,23 +379,23 @@ class FilePage extends React.PureComponent {
tokens.length === 0
? ''
: tokens.map((token, j) => {
let hasSpace = j !== tokens.length - 1;
let space = hasSpace ? ' ' : '';
let hasSpace = j !== tokens.length - 1;
let space = hasSpace ? ' ' : '';
if (token.match(/^(lbry|https?):\/\//g)) {
return (
<Link
key={j}
style={filePageStyle.link}
href={token}
text={token}
effectOnTap={filePageStyle.linkTapped}
/>
);
} else {
return token + space;
}
});
if (token.match(/^(lbry|https?):\/\//g)) {
return (
<Link
key={j}
style={filePageStyle.link}
href={token}
text={token}
effectOnTap={filePageStyle.linkTapped}
/>
);
} else {
return token + space;
}
});
lineContent.push('\n');
return <Text key={i}>{lineContent}</Text>;
@ -503,7 +503,10 @@ class FilePage extends React.PureComponent {
};
renderTags = tags => {
return tags.map((tag, i) => <Tag style={filePageStyle.tagItem} key={`${tag}-${i}`} name={tag} />);
const { navigation } = this.props;
return tags.map((tag, i) => (
<Tag style={filePageStyle.tagItem} key={`${tag}-${i}`} name={tag} navigation={navigation} />
));
};
onFileDownloadButtonPlayed = () => {
@ -637,15 +640,15 @@ class FilePage extends React.PureComponent {
this.state.isLandscape
? filePageStyle.containedPlayerLandscape
: this.state.fullscreenMode
? filePageStyle.fullscreenPlayer
: filePageStyle.containedPlayer,
? filePageStyle.fullscreenPlayer
: filePageStyle.containedPlayer,
];
const playerBgStyle = [filePageStyle.playerBackground, filePageStyle.containedPlayerBackground];
const fsPlayerBgStyle = [filePageStyle.playerBackground, filePageStyle.fullscreenPlayerBackground];
// at least 2MB (or the full download) before media can be loaded
const canLoadMedia =
this.state.streamingMode ||
(fileInfo && (fileInfo.written_bytes >= 2097152 || fileInfo.written_bytes == fileInfo.total_bytes)); // 2MB = 1024*1024*2
(fileInfo && (fileInfo.written_bytes >= 2097152 || fileInfo.written_bytes === fileInfo.total_bytes)); // 2MB = 1024*1024*2
const isViewable = mediaType === 'image' || mediaType === 'text';
const isWebViewable = mediaType === 'text';
const canOpen = isViewable && completed;
@ -679,7 +682,7 @@ class FilePage extends React.PureComponent {
fileInfo &&
!this.state.autoDownloadStarted &&
this.state.uriVars &&
'true' === this.state.uriVars.download
this.state.uriVars.download === 'true'
) {
this.setState({ autoDownloadStarted: true }, () => {
purchaseUri(uri, costInfo, !isPlayable);
@ -727,17 +730,17 @@ class FilePage extends React.PureComponent {
canOpen ||
(!completed && !this.state.streamingMode)) &&
!this.state.downloadPressed && (
<FileDownloadButton
uri={uri}
style={filePageStyle.downloadButton}
openFile={openFile}
isPlayable={isPlayable}
isViewable={isViewable}
onPlay={this.onFileDownloadButtonPlayed}
onView={() => this.setState({ downloadPressed: true })}
onButtonLayout={() => this.setState({ downloadButtonShown: true })}
/>
)}
<FileDownloadButton
uri={uri}
style={filePageStyle.downloadButton}
openFile={openFile}
isPlayable={isPlayable}
isViewable={isViewable}
onPlay={this.onFileDownloadButtonPlayed}
onView={() => this.setState({ downloadPressed: true })}
onButtonLayout={() => this.setState({ downloadButtonShown: true })}
/>
)}
{!fileInfo && (
<FilePrice
uri={uri}
@ -808,14 +811,14 @@ class FilePage extends React.PureComponent {
!fileInfo.stopped &&
fileInfo.written_bytes < fileInfo.total_bytes &&
!this.state.stopDownloadConfirmed && (
<Button
style={filePageStyle.actionButton}
icon={'stop'}
theme={'light'}
text={'Stop Download'}
onPress={this.onStopDownloadPressed}
/>
)}
<Button
style={filePageStyle.actionButton}
icon={'stop'}
theme={'light'}
text={'Stop Download'}
onPress={this.onStopDownloadPressed}
/>
)}
</View>
)}
</View>
@ -833,7 +836,7 @@ class FilePage extends React.PureComponent {
onPress={() => this.setState({ showDescription: !this.state.showDescription })}
>
<View style={filePageStyle.titleRow}>
<Text style={filePageStyle.title} selectable={true}>
<Text style={filePageStyle.title} selectable>
{title}
</Text>
<View style={filePageStyle.descriptionToggle}>
@ -846,7 +849,7 @@ class FilePage extends React.PureComponent {
<View style={filePageStyle.publishInfo}>
<Link
style={filePageStyle.channelName}
selectable={true}
selectable
text={channelName}
numberOfLines={1}
ellipsizeMode={'tail'}
@ -865,13 +868,13 @@ class FilePage extends React.PureComponent {
<View style={filePageStyle.subscriptionRow}>
{false &&
((isPlayable && !fileInfo) || (isPlayable && fileInfo && !fileInfo.download_path)) && (
<Button
style={[filePageStyle.actionButton, filePageStyle.saveFileButton]}
theme={'light'}
icon={'download'}
onPress={this.onSaveFilePressed}
/>
)}
<Button
style={[filePageStyle.actionButton, filePageStyle.saveFileButton]}
theme={'light'}
icon={'download'}
onPress={this.onSaveFilePressed}
/>
)}
<Button
style={[filePageStyle.actionButton, filePageStyle.tipButton]}
theme={'light'}
@ -928,7 +931,7 @@ class FilePage extends React.PureComponent {
)}
{this.state.showDescription && description && (
<View>
<Text style={filePageStyle.description} selectable={true}>
<Text style={filePageStyle.description} selectable>
{this.linkify(description)}
</Text>
{tags && tags.length > 0 && (

View file

@ -22,7 +22,7 @@ import RNFS from 'react-native-fs';
import Button from 'component/button';
import ChannelSelector from 'component/channelSelector';
import Colors from 'styles/colors';
import Constants from 'constants';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import FastImage from 'react-native-fast-image';
import FloatingWalletBalance from 'component/floatingWalletBalance';
import Icon from 'react-native-vector-icons/FontAwesome5';
@ -384,7 +384,7 @@ class PublishPage extends React.PureComponent {
},
(error, res) => {
if (!error) {
//console.log(res);
// console.log(res);
}
}
);
@ -439,7 +439,7 @@ class PublishPage extends React.PureComponent {
newTags.push(tag);
this.setState({ tags: newTags });
} else {
notify({ message: `You already added the "${tag}" tag.` });
notify({ message: __(`You already added the "${tag}" tag.`) });
}
};
@ -471,7 +471,7 @@ class PublishPage extends React.PureComponent {
const mediaType = media.type.substring(0, 5);
const tempId = this.getRandomFileId();
if ('video' === mediaType && media.id > -1) {
if (mediaType === 'video' && media.id > -1) {
const uri = `file://${thumbnailPath}/${media.id}.png`;
this.setState({ currentThumbnailUri: uri, updatingThumbnailUri: false });
@ -479,9 +479,9 @@ class PublishPage extends React.PureComponent {
if (!this.state.uploadedThumbnailUri) {
this.setState({ uploadThumbnailStarted: true }, () => uploadThumbnail(this.getFilePathFromUri(uri), RNFS));
}
} else if ('image' === mediaType || 'video' === mediaType) {
} else if (mediaType === 'image' || mediaType === 'video') {
const create =
'image' === mediaType
mediaType === 'image'
? NativeModules.Gallery.createImageThumbnail
: NativeModules.Gallery.createVideoThumbnail;
create(tempId, media.filePath)
@ -867,13 +867,7 @@ class PublishPage extends React.PureComponent {
<View style={publishStyle.cameraAction}>
<Feather style={publishStyle.cameraActionIcon} name="circle" size={72} color={Colors.White} />
{this.state.recordingVideo && (
<Icon
style={publishStyle.recordingIcon}
name="circle"
solid={true}
size={44}
color={Colors.Red}
/>
<Icon style={publishStyle.recordingIcon} name="circle" solid size={44} color={Colors.Red} />
)}
</View>
</TouchableOpacity>

View file

@ -1,24 +1,41 @@
import React from 'react';
import NavigationActions from 'react-navigation';
import { ActivityIndicator, FlatList, NativeModules, SectionList, ScrollView, Text, View } from 'react-native';
import {
ActivityIndicator,
FlatList,
NativeModules,
SectionList,
ScrollView,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { buildURI, parseURI } from 'lbry-redux';
import { uriFromFileInfo } from 'utils/helper';
import __, { uriFromFileInfo } from 'utils/helper';
import AsyncStorage from '@react-native-community/async-storage';
import moment from 'moment';
import Button from 'component/button';
import ClaimList from 'component/claimList';
import Colors from 'styles/colors';
import Constants from 'constants';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import fileListStyle from 'styles/fileList';
import subscriptionsStyle from 'styles/subscriptions';
import FloatingWalletBalance from 'component/floatingWalletBalance';
import FileItem from 'component/fileItem';
import Icon from 'react-native-vector-icons/FontAwesome5';
import Link from 'component/link';
import ModalPicker from 'component/modalPicker';
import SubscribedChannelList from 'component/subscribedChannelList';
import SuggestedSubscriptions from 'component/suggestedSubscriptions';
import UriBar from 'component/uriBar';
class SubscriptionsPage extends React.PureComponent {
state = {
showingSuggestedSubs: false,
showSortPicker: false,
orderBy: ['release_time'],
filteredChannels: [],
currentSortByItem: Constants.CLAIM_SEARCH_SORT_BY_ITEMS[1], // should always default to sorting subscriptions by new
};
didFocusListener;
@ -48,7 +65,6 @@ class SubscriptionsPage extends React.PureComponent {
setPlayerVisible();
doFetchMySubscriptions();
doFetchRecommendedSubscriptions();
doSetViewMode(subscriptionsViewMode ? subscriptionsViewMode : Constants.SUBSCRIPTIONS_VIEW_ALL);
};
componentDidMount() {
@ -63,10 +79,38 @@ class SubscriptionsPage extends React.PureComponent {
}
}
changeViewMode = viewMode => {
const { setClientSetting, doSetViewMode } = this.props;
setClientSetting(Constants.SETTING_SUBSCRIPTIONS_VIEW_MODE, viewMode);
doSetViewMode(viewMode);
handleSortByItemSelected = item => {
let orderBy = [];
switch (item.name) {
case Constants.SORT_BY_HOT:
orderBy = Constants.DEFAULT_ORDER_BY;
break;
case Constants.SORT_BY_NEW:
orderBy = ['release_time'];
break;
case Constants.SORT_BY_TOP:
orderBy = ['effective_amount'];
break;
}
this.setState({ currentSortByItem: item, orderBy, showSortPicker: false });
};
handleChannelSelected = channelUri => {
const { subscribedChannels } = this.props;
this.setState({
filteredChannels:
channelUri === Constants.ALL_PLACEHOLDER
? []
: subscribedChannels.filter(channel => channel.uri === channelUri),
});
};
prependSubscribedChannelsWithAll = subscribedChannels => {
const channelUris = subscribedChannels.map(channel => channel.uri);
return [Constants.ALL_PLACEHOLDER].concat(channelUris);
};
render() {
@ -84,6 +128,7 @@ class SubscriptionsPage extends React.PureComponent {
unreadSubscriptions,
navigation,
} = this.props;
const { currentSortByItem, filteredChannels } = this.state;
const numberOfSubscriptions = subscribedChannels ? subscribedChannels.length : 0;
const hasSubscriptions = numberOfSubscriptions > 0;
@ -92,91 +137,46 @@ class SubscriptionsPage extends React.PureComponent {
this.setState({ showingSuggestedSubs: true });
}
const channelIds =
filteredChannels.length > 0
? filteredChannels.map(channel => {
const { claimId } = parseURI(channel.uri);
return claimId;
})
: subscribedChannels &&
subscribedChannels.map(channel => {
const { claimId } = parseURI(channel.uri);
return claimId;
});
return (
<View style={subscriptionsStyle.container}>
<UriBar navigation={navigation} />
{!this.state.showingSuggestedSubs && hasSubscriptions && !loading && (
<View style={subscriptionsStyle.viewModeRow}>
<Link
text={'All Subscriptions'}
style={[
subscriptionsStyle.viewModeLink,
viewMode === Constants.SUBSCRIPTIONS_VIEW_ALL
? subscriptionsStyle.activeMode
: subscriptionsStyle.inactiveMode,
]}
onPress={() => this.changeViewMode(Constants.SUBSCRIPTIONS_VIEW_ALL)}
/>
<Link
text={'Latest Only'}
style={[
subscriptionsStyle.viewModeLink,
viewMode === Constants.SUBSCRIPTIONS_VIEW_LATEST_FIRST
? subscriptionsStyle.activeMode
: subscriptionsStyle.inactiveMode,
]}
onPress={() => this.changeViewMode(Constants.SUBSCRIPTIONS_VIEW_LATEST_FIRST)}
/>
</View>
)}
<UriBar navigation={navigation} belowOverlay={this.state.showSortPicker} />
<View style={subscriptionsStyle.titleRow}>
<Text style={subscriptionsStyle.pageTitle}>Channels you follow</Text>
{!this.state.showingSuggestedSubs && hasSubscriptions && (
<TouchableOpacity
style={subscriptionsStyle.tagSortBy}
onPress={() => this.setState({ showSortPicker: true })}
>
<Text style={subscriptionsStyle.tagSortText}>{currentSortByItem.label.split(' ')[0]}</Text>
<Icon style={subscriptionsStyle.tagSortIcon} name={'sort-down'} size={14} />
</TouchableOpacity>
)}
</View>
{!this.state.showingSuggestedSubs && hasSubscriptions && !loading && (
<View style={subscriptionsStyle.subContainer}>
{viewMode === Constants.SUBSCRIPTIONS_VIEW_ALL && (
<FlatList
style={subscriptionsStyle.scrollContainer}
contentContainerStyle={subscriptionsStyle.scrollPadding}
renderItem={({ item }) => (
<FileItem
style={subscriptionsStyle.fileItem}
mediaStyle={fileListStyle.fileItemMedia}
key={item}
uri={uriFromFileInfo(item)}
navigation={navigation}
compactView={false}
showDetails={true}
/>
)}
data={allSubscriptions.sort((a, b) => {
return b.height - a.height;
})}
keyExtractor={(item, index) => uriFromFileInfo(item)}
/>
)}
{viewMode === Constants.SUBSCRIPTIONS_VIEW_LATEST_FIRST && (
<View style={subscriptionsStyle.subContainer}>
{unreadSubscriptions.length ? (
<ScrollView
style={subscriptionsStyle.scrollContainer}
contentContainerStyle={subscriptionsStyle.scrollPadding}
>
{unreadSubscriptions.map(({ channel, uris }) => {
const { claimName } = parseURI(channel);
return uris.map(uri => (
<FileItem
style={subscriptionsStyle.fileItem}
mediaStyle={fileListStyle.fileItemMedia}
key={uri}
uri={uri}
navigation={navigation}
compactView={false}
showDetails={true}
/>
));
})}
</ScrollView>
) : (
<View style={subscriptionsStyle.contentContainer}>
<Text style={subscriptionsStyle.contentText}>
All caught up! You might like the channels below.
</Text>
<SuggestedSubscriptions navigation={navigation} />
</View>
)}
</View>
)}
<SubscribedChannelList
subscribedChannels={this.prependSubscribedChannelsWithAll(subscribedChannels)}
onChannelSelected={this.handleChannelSelected}
/>
<ClaimList
style={subscriptionsStyle.claimList}
channelIds={channelIds}
orderBy={this.state.orderBy}
navigation={navigation}
orientation={Constants.ORIENTATION_VERTICAL}
/>
</View>
)}
@ -189,14 +189,13 @@ class SubscriptionsPage extends React.PureComponent {
{this.state.showingSuggestedSubs && (
<View style={subscriptionsStyle.suggestedSubsContainer}>
{!hasSubscriptions && (
<Text style={subscriptionsStyle.infoText}>
You are not subscribed to any channels at the moment. Here are some channels that we think you might
enjoy.
</Text>
<View style={subscriptionsStyle.infoArea}>
<Text style={subscriptionsStyle.infoText}>You are not subscribed to any channels at the moment.</Text>
</View>
)}
{hasSubscriptions && (
<View>
<View style={subscriptionsStyle.infoArea}>
<Text style={subscriptionsStyle.infoText}>
You are currently subscribed to {numberOfSubscriptions} channel{numberOfSubscriptions > 1 ? 's' : ''}.
</Text>
@ -209,13 +208,25 @@ class SubscriptionsPage extends React.PureComponent {
)}
{loadingSuggested && (
<ActivityIndicator size="large" colors={Colors.LbryGreen} style={subscriptionsStyle.loading} />
<View style={subscriptionsStyle.centered}>
<ActivityIndicator size="large" colors={Colors.LbryGreen} style={subscriptionsStyle.loading} />
\\
</View>
)}
{!loadingSuggested && <SuggestedSubscriptions navigation={navigation} />}
</View>
)}
<FloatingWalletBalance navigation={navigation} />
{!this.state.showSortPicker && <FloatingWalletBalance navigation={navigation} />}
{this.state.showSortPicker && (
<ModalPicker
title={__('Sort content by')}
onOverlayPress={() => this.setState({ showSortPicker: false })}
onItemSelected={this.handleSortByItemSelected}
selectedItem={this.state.currentSortByItem}
items={Constants.CLAIM_SEARCH_SORT_BY_ITEMS}
/>
)}
</View>
);
}

19
src/page/tag/index.js Normal file
View file

@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
import { selectCurrentRoute } from 'redux/selectors/drawer';
import Constants from 'constants';
import TagPage from './view';
const select = state => ({
currentRoute: selectCurrentRoute(state),
});
const perform = dispatch => ({
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_TAG)),
setPlayerVisible: () => dispatch(doSetPlayerVisible(false)),
});
export default connect(
select,
perform
)(TagPage);

115
src/page/tag/view.js Normal file
View file

@ -0,0 +1,115 @@
import React from 'react';
import { ActivityIndicator, NativeModules, FlatList, Text, TouchableOpacity, View } from 'react-native';
import { DEFAULT_FOLLOWED_TAGS, normalizeURI } from 'lbry-redux';
import { formatTagTitle } from 'utils/helper';
import AsyncStorage from '@react-native-community/async-storage';
import moment from 'moment';
import ClaimList from 'component/claimList';
import FileItem from 'component/fileItem';
import Icon from 'react-native-vector-icons/FontAwesome5';
import discoverStyle from 'styles/discover';
import fileListStyle from 'styles/fileList';
import Colors from 'styles/colors';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import FloatingWalletBalance from 'component/floatingWalletBalance';
import ModalPicker from 'component/modalPicker';
import UriBar from 'component/uriBar';
class TagPage extends React.PureComponent {
state = {
tag: null,
showSortPicker: false,
orderBy: Constants.DEFAULT_ORDER_BY,
currentSortByItem: Constants.CLAIM_SEARCH_SORT_BY_ITEMS[0],
};
didFocusListener;
componentWillMount() {
const { navigation } = this.props;
this.didFocusListener = navigation.addListener('didFocus', this.onComponentFocused);
}
componentWillUnmount() {
if (this.didFocusListener) {
this.didFocusListener.remove();
}
}
onComponentFocused = () => {
const { pushDrawerStack, setPlayerVisible, navigation } = this.props;
this.setState({ tag: navigation.state.params.tag });
pushDrawerStack();
setPlayerVisible();
};
componentDidMount() {
this.onComponentFocused();
}
componentWillReceiveProps(nextProps) {
const { currentRoute, navigation } = nextProps;
const { currentRoute: prevRoute } = this.props;
if (Constants.DRAWER_ROUTE_TAG === currentRoute && currentRoute !== prevRoute) {
this.onComponentFocused();
}
}
handleSortByItemSelected = item => {
let orderBy = [];
switch (item.name) {
case Constants.SORT_BY_HOT:
orderBy = Constants.DEFAULT_ORDER_BY;
break;
case Constants.SORT_BY_NEW:
orderBy = ['release_time'];
break;
case Constants.SORT_BY_TOP:
orderBy = ['effective_amount'];
break;
}
this.setState({ currentSortByItem: item, orderBy, showSortPicker: false });
};
render() {
const { navigation } = this.props;
const { tag, currentSortByItem } = this.state;
return (
<View style={discoverStyle.container}>
<UriBar navigation={navigation} belowOverlay={this.state.showSortPicker} />
<ClaimList
ListHeaderComponent={
<View style={discoverStyle.tagTitleRow}>
<Text style={discoverStyle.tagPageTitle}>{formatTagTitle(tag)}</Text>
<TouchableOpacity style={discoverStyle.tagSortBy} onPress={() => this.setState({ showSortPicker: true })}>
<Text style={discoverStyle.tagSortText}>{currentSortByItem.label.split(' ')[0]}</Text>
<Icon style={discoverStyle.tagSortIcon} name={'sort-down'} size={14} />
</TouchableOpacity>
</View>
}
style={discoverStyle.tagPageClaimList}
orderBy={this.state.orderBy}
tags={[tag]}
navigation={navigation}
orientation={Constants.ORIENTATION_VERTICAL}
/>
{!this.state.showSortPicker && <FloatingWalletBalance navigation={navigation} />}
{this.state.showSortPicker && (
<ModalPicker
title={__('Sort content by')}
onOverlayPress={() => this.setState({ showSortPicker: false })}
onItemSelected={this.handleSortByItemSelected}
selectedItem={this.state.currentSortByItem}
items={Constants.CLAIM_SEARCH_SORT_BY_ITEMS}
/>
)}
</View>
);
}
}
export default TagPage;

View file

@ -1,18 +1,16 @@
import { connect } from 'react-redux';
import { doFetchTrendingUris, selectTrendingUris, selectFetchingTrendingUris } from 'lbryinc';
import { selectFollowedTags } from 'lbry-redux';
import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
import { selectCurrentRoute } from 'redux/selectors/drawer';
import Constants from 'constants';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import TrendingPage from './view';
const select = state => ({
currentRoute: selectCurrentRoute(state),
trendingUris: selectTrendingUris(state),
fetchingTrendingUris: selectFetchingTrendingUris(state),
followedTags: selectFollowedTags(state),
});
const perform = dispatch => ({
fetchTrendingUris: () => dispatch(doFetchTrendingUris()),
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_TRENDING)),
setPlayerVisible: () => dispatch(doSetPlayerVisible(false)),
});

View file

@ -1,17 +1,32 @@
import React from 'react';
import { ActivityIndicator, NativeModules, FlatList, Text, View } from 'react-native';
import { normalizeURI } from 'lbry-redux';
import { ActivityIndicator, NativeModules, FlatList, Text, TouchableOpacity, View } from 'react-native';
import { DEFAULT_FOLLOWED_TAGS, normalizeURI } from 'lbry-redux';
import AsyncStorage from '@react-native-community/async-storage';
import moment from 'moment';
import ClaimList from 'component/claimList';
import FileItem from 'component/fileItem';
import discoverStyle from 'styles/discover';
import fileListStyle from 'styles/fileList';
import Link from 'component/link';
import ModalPicker from 'component/modalPicker';
import ModalTagSelector from 'component/modalTagSelector';
import Colors from 'styles/colors';
import Constants from 'constants';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import FloatingWalletBalance from 'component/floatingWalletBalance';
import Icon from 'react-native-vector-icons/FontAwesome5';
import UriBar from 'component/uriBar';
import discoverStyle from 'styles/discover';
const TRENDING_FOR_ITEMS = [
{ icon: 'globe-americas', name: 'everyone', label: 'Everyone' },
{ icon: 'hashtag', name: 'tags', label: 'Tags you follow' },
];
class TrendingPage extends React.PureComponent {
state = {
showModalTagSelector: false,
showTrendingForPicker: false,
currentTrendingForItem: TRENDING_FOR_ITEMS[0],
};
didFocusListener;
componentWillMount() {
@ -26,10 +41,9 @@ class TrendingPage extends React.PureComponent {
}
onComponentFocused = () => {
const { fetchTrendingUris, pushDrawerStack, setPlayerVisible } = this.props;
const { pushDrawerStack, setPlayerVisible } = this.props;
pushDrawerStack();
setPlayerVisible();
fetchTrendingUris();
};
componentDidMount() {
@ -44,39 +58,62 @@ class TrendingPage extends React.PureComponent {
}
}
handleTrendingForItemSelected = item => {
this.setState({ currentTrendingForItem: item, showTrendingForPicker: false });
};
render() {
const { trendingUris, fetchingTrendingUris, navigation } = this.props;
const hasContent = typeof trendingUris === 'object' && trendingUris.length,
failedToLoad = !fetchingTrendingUris && !hasContent;
const { followedTags, navigation } = this.props;
const { currentTrendingForItem, showModalTagSelector, showTrendingForPicker } = this.state;
return (
<View style={discoverStyle.container}>
<UriBar navigation={navigation} />
{!hasContent && fetchingTrendingUris && (
<View style={discoverStyle.busyContainer}>
<ActivityIndicator size="large" color={Colors.LbryGreen} />
<Text style={discoverStyle.title}>Fetching content...</Text>
</View>
)}
{hasContent && (
<FlatList
style={discoverStyle.trendingContainer}
renderItem={({ item }) => (
<FileItem
style={fileListStyle.fileItem}
mediaStyle={fileListStyle.fileItemMedia}
key={item}
uri={normalizeURI(item)}
navigation={navigation}
showDetails={true}
compactView={false}
/>
)}
data={trendingUris.map(uri => uri.url)}
keyExtractor={(item, index) => item}
<ClaimList
ListHeaderComponent={
<View style={discoverStyle.titleRow}>
<Text style={discoverStyle.pageTitle}>Trending</Text>
<View style={discoverStyle.rightTitleRow}>
{TRENDING_FOR_ITEMS[1].name === currentTrendingForItem.name && (
<Link
style={discoverStyle.customizeLink}
text={'Customize'}
onPress={() => this.setState({ showModalTagSelector: true })}
/>
)}
<TouchableOpacity
style={discoverStyle.tagSortBy}
onPress={() => this.setState({ showTrendingForPicker: true })}
>
<Text style={discoverStyle.tagSortText}>{currentTrendingForItem.label.split(' ')[0]}</Text>
<Icon style={discoverStyle.tagSortIcon} name={'sort-down'} size={14} />
</TouchableOpacity>
</View>
</View>
}
style={discoverStyle.verticalClaimList}
orderBy={Constants.DEFAULT_ORDER_BY}
trendingForAll={TRENDING_FOR_ITEMS[0].name === currentTrendingForItem.name}
tags={followedTags.map(tag => tag.name)}
navigation={navigation}
orientation={Constants.ORIENTATION_VERTICAL}
/>
{!showModalTagSelector && <FloatingWalletBalance navigation={navigation} />}
{showModalTagSelector && (
<ModalTagSelector
onOverlayPress={() => this.setState({ showModalTagSelector: false })}
onDonePress={() => this.setState({ showModalTagSelector: false })}
/>
)}
{showTrendingForPicker && (
<ModalPicker
title={'Trending for'}
onOverlayPress={() => this.setState({ showTrendingForPicker: false })}
onItemSelected={this.handleTrendingForItemSelected}
selectedItem={currentTrendingForItem}
items={TRENDING_FOR_ITEMS}
/>
)}
<FloatingWalletBalance navigation={navigation} />
</View>
);
}

48
src/styles/channelIcon.js Normal file
View file

@ -0,0 +1,48 @@
import { StyleSheet } from 'react-native';
import Colors from './colors';
const channelIconStyle = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
marginRight: 16,
alignSelf: 'flex-start',
},
placeholderText: {
fontFamily: 'Inter-UI-SemiBold',
fontSize: 14,
},
thumbnailContainer: {
width: 80,
height: 80,
borderRadius: 160,
overflow: 'hidden',
},
borderedThumbnailContainer: {
borderWidth: 1,
borderColor: Colors.LighterGrey,
},
thumbnail: {
width: '100%',
height: '100%',
},
centered: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontFamily: 'Inter-UI-Regular',
fontSize: 12,
width: 80,
marginTop: 4,
textAlign: 'center',
},
});
export default channelIconStyle;

34
src/styles/claimList.js Normal file
View file

@ -0,0 +1,34 @@
import { StyleSheet } from 'react-native';
import Colors from './colors';
const claimListStyle = StyleSheet.create({
horizontalScrollContainer: {
marginBottom: 12,
},
horizontalScrollPadding: {
paddingLeft: 16,
},
verticalScrollContainer: {
flex: 1,
},
verticalScrollPadding: {
paddingBottom: 16,
},
verticalListItem: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
marginLeft: 8,
marginRight: 8,
marginTop: 4,
marginBottom: 4,
},
verticalLoading: {
width: '100%',
height: 48,
alignItems: 'center',
justifyContent: 'center',
},
});
export default claimListStyle;

View file

@ -27,8 +27,28 @@ const discoverStyle = StyleSheet.create({
},
scrollContainer: {
flex: 1,
paddingTop: 12,
marginTop: 60,
},
titleRow: {
flexDirection: 'row',
marginTop: 76,
marginBottom: 8,
alignItems: 'center',
justifyContent: 'space-between',
marginLeft: 16,
marginRight: 16,
},
rightTitleRow: {
flexDirection: 'row',
alignItems: 'center',
},
pageTitle: {
fontFamily: 'Inter-UI-Regular',
fontSize: 24,
},
customizeLink: {
fontFamily: 'Inter-UI-Regular',
fontSize: 14,
marginRight: 48,
},
trendingContainer: {
flex: 1,
@ -47,12 +67,18 @@ const discoverStyle = StyleSheet.create({
textAlign: 'center',
marginLeft: 10,
},
categoryTitleRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginLeft: 16,
marginRight: 16,
marginTop: 6,
marginBottom: 6,
},
categoryName: {
fontFamily: 'Inter-UI-SemiBold',
fontSize: 18,
marginLeft: 24,
marginTop: 12,
marginBottom: 6,
color: Colors.Black,
},
fileItem: {
@ -163,12 +189,49 @@ const discoverStyle = StyleSheet.create({
scrollPadding: {
paddingBottom: 24,
},
listLoading: {
flex: 1,
height: 64,
alignItems: 'center',
justifyContent: 'center',
},
horizontalScrollContainer: {
marginBottom: 12,
},
horizontalScrollPadding: {
paddingLeft: 20,
},
verticalClaimList: {
flex: 1,
},
tagPageTitle: {
fontFamily: 'Inter-UI-Regular',
fontSize: 24,
},
tagPageClaimList: {
flex: 1,
},
tagTitleRow: {
marginTop: 76,
marginLeft: 16,
marginRight: 16,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
tagSortBy: {
flexDirection: 'row',
alignItems: 'center',
marginRight: 4,
},
tagSortText: {
fontFamily: 'Inter-UI-Regular',
fontSize: 14,
marginRight: 4,
},
tagSortIcon: {
marginTop: -6,
},
});
export default discoverStyle;

65
src/styles/modalPicker.js Normal file
View file

@ -0,0 +1,65 @@
import { StyleSheet } from 'react-native';
import Colors from './colors';
const modalPickerStyle = StyleSheet.create({
overlay: {
backgroundColor: '#00000055',
flex: 1,
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
zIndex: 300,
},
overlayTouchArea: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
container: {
position: 'absolute',
left: 8,
right: 8,
bottom: 8,
borderRadius: 8,
backgroundColor: Colors.White,
padding: 12,
},
title: {
fontFamily: 'Inter-UI-SemiBold',
fontSize: 12,
marginTop: 4,
textTransform: 'uppercase',
},
listItem: {
paddingTop: 10,
paddingBottom: 10,
flexDirection: 'row',
alignItems: 'center',
},
divider: {
marginTop: 12,
marginBottom: 8,
borderBottomColor: Colors.LighterGrey,
borderBottomWidth: 1,
width: '100%',
},
itemIcon: {
marginLeft: 8,
marginRight: 12,
},
itemLabel: {
alignSelf: 'flex-start',
fontFamily: 'Inter-UI-Regular',
fontSize: 16,
},
itemSelected: {
position: 'absolute',
right: 8,
},
});
export default modalPickerStyle;

View file

@ -0,0 +1,59 @@
import { StyleSheet } from 'react-native';
import Colors from './colors';
const modalTagSelectorStyle = StyleSheet.create({
overlay: {
backgroundColor: '#00000099',
flex: 1,
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
zIndex: 300,
alignItems: 'center',
},
overlayTouchArea: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
container: {
flex: 1,
borderRadius: 8,
backgroundColor: Colors.White,
padding: 16,
position: 'absolute',
left: 8,
right: 8,
bottom: 8,
},
tag: {
marginRight: 4,
marginBottom: 4,
},
tagList: {
flexDirection: 'row',
flexWrap: 'wrap',
},
titleRow: {
marginBottom: 12,
},
title: {
fontFamily: 'Inter-UI-Regular',
fontSize: 24,
},
buttons: {
marginTop: 16,
},
doneButton: {
alignSelf: 'flex-start',
backgroundColor: Colors.LbryGreen,
paddingLeft: 16,
paddingRight: 16,
},
});
export default modalTagSelectorStyle;

View file

@ -12,15 +12,16 @@ const subscriptionsStyle = StyleSheet.create({
},
suggestedSubsContainer: {
flex: 1,
marginTop: 60,
},
suggestedScrollPadding: {
paddingTop: 8,
},
button: {
alignSelf: 'flex-start',
backgroundColor: Colors.LbryGreen,
paddingLeft: 16,
paddingRight: 16,
marginLeft: 16,
marginBottom: 16,
marginBottom: 8,
},
busyContainer: {
flex: 1,
@ -38,7 +39,15 @@ const subscriptionsStyle = StyleSheet.create({
infoText: {
fontFamily: 'Inter-UI-Regular',
fontSize: 16,
margin: 16,
marginTop: 8,
marginBottom: 8,
},
infoArea: {
marginLeft: 16,
marginRight: 16,
paddingBottom: 4,
borderBottomWidth: 1,
borderBottomColor: Colors.LighterGrey,
},
suggestedContainer: {
flex: 1,
@ -91,6 +100,14 @@ const subscriptionsStyle = StyleSheet.create({
marginTop: 8,
fontSize: 18,
},
channelList: {
marginLeft: 16,
marginRight: 16,
marginTop: 8,
paddingBottom: 8,
borderBottomColor: Colors.LighterGrey,
borderBottomWidth: 1,
},
channelTitle: {
fontFamily: 'Inter-UI-SemiBold',
fontSize: 20,
@ -99,11 +116,6 @@ const subscriptionsStyle = StyleSheet.create({
marginBottom: 16,
color: Colors.LbryGreen,
},
titleRow: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
},
subscribeButton: {
alignSelf: 'flex-start',
marginRight: 24,
@ -129,6 +141,99 @@ const subscriptionsStyle = StyleSheet.create({
activeMode: {
fontFamily: 'Inter-UI-SemiBold',
},
claimList: {
flex: 1,
},
pageTitle: {
fontFamily: 'Inter-UI-Regular',
fontSize: 24,
},
titleRow: {
marginTop: 76,
marginLeft: 16,
marginRight: 16,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
tagSortBy: {
flexDirection: 'row',
alignItems: 'center',
marginRight: 4,
},
tagSortText: {
fontFamily: 'Inter-UI-Regular',
fontSize: 14,
marginRight: 4,
},
tagSortIcon: {
marginTop: -6,
},
centered: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center',
},
suggestedItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16,
marginLeft: 16,
marginRight: 16,
},
suggestedItemThumbnailContainer: {
width: 70,
height: 70,
borderRadius: 140,
overflow: 'hidden',
},
suggestedItemThumbnail: {
width: '100%',
height: '100%',
},
suggestedItemDetails: {
marginLeft: 16,
flexDirection: 'row',
},
suggestedItemSubscribe: {
backgroundColor: Colors.White,
position: 'absolute',
right: 0,
top: 0,
},
suggestedItemTitle: {
fontFamily: 'Inter-UI-Regular',
fontSize: 16,
marginBottom: 4,
},
suggestedItemName: {
fontFamily: 'Inter-UI-SemiBold',
fontSize: 14,
marginBottom: 4,
color: Colors.LbryGreen,
},
suggestedItemTagList: {
flexDirection: 'row',
flexWrap: 'wrap',
},
suggestedSubTitle: {
fontFamily: 'Inter-UI-Regular',
fontSize: 20,
marginLeft: 16,
marginRight: 16,
marginBottom: 12,
},
suggestedSectionSeparator: {
marginBottom: 16,
},
tag: {
marginRight: 4,
marginBottom: 4,
},
});
export default subscriptionsStyle;

View file

@ -16,6 +16,8 @@ const uriBarStyle = StyleSheet.create({
shadowOffset: {
height: StyleSheet.hairlineWidth,
},
},
containerElevated: {
elevation: 4,
},
uriText: {
@ -34,6 +36,8 @@ const uriBarStyle = StyleSheet.create({
top: 0,
width: '100%',
zIndex: 200,
},
overlayElevated: {
elevation: 16,
},
inFocus: {

View file

@ -227,7 +227,7 @@ const walletStyle = StyleSheet.create({
marginTop: 16,
paddingBottom: 14,
borderBottomWidth: 1,
borderBottomColor: Colors.PageBackground
borderBottomColor: Colors.PageBackground,
},
syncDriverLink: {
color: Colors.LbryGreen,

View file

@ -1,8 +1,7 @@
import { NavigationActions, StackActions } from 'react-navigation';
import { buildURI, isURIValid } from 'lbry-redux';
import { doPopDrawerStack, doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
import { DrawerRoutes } from 'constants';
import Constants from 'constants';
import Constants, { DrawerRoutes } from 'constants'; // eslint-disable-line node/no-deprecated-api
function getRouteForSpecialUri(uri) {
let targetRoute;
@ -35,7 +34,8 @@ export function dispatchNavigateToUri(dispatch, nav, uri, isNavigatingBack) {
return;
}
let uriVars = {};
let uriVars = {},
uriVarsStr;
if (uri.indexOf('?') > -1) {
uriVarsStr = uri.substring(uri.indexOf('?') + 1);
uri = uri.substring(0, uri.indexOf('?'));
@ -49,10 +49,10 @@ export function dispatchNavigateToUri(dispatch, nav, uri, isNavigatingBack) {
dispatch(doSetPlayerVisible(true));
}
if (nav && nav.routes && nav.routes.length > 0 && 'Main' === nav.routes[0].routeName) {
if (nav && nav.routes && nav.routes.length > 0 && nav.routes[0].routeName === 'Main') {
const mainRoute = nav.routes[0];
const discoverRoute = mainRoute.routes[0];
if (discoverRoute.index > 0 && 'File' === discoverRoute.routes[discoverRoute.index].routeName) {
if (discoverRoute.index > 0 && discoverRoute.routes[discoverRoute.index].routeName === 'File') {
const fileRoute = discoverRoute.routes[discoverRoute.index];
// Currently on a file page, so we can ignore (if the URI is the same) or replace (different URIs)
if (uri !== fileRoute.params.uri) {
@ -119,7 +119,8 @@ export function navigateToUri(navigation, uri, additionalParams, isNavigatingBac
return;
}
let uriVars = {};
let uriVars = {},
uriVarsStr;
if (uri.indexOf('?') > -1) {
uriVarsStr = uri.substring(uri.indexOf('?') + 1);
uri = uri.substring(0, uri.indexOf('?'));
@ -128,7 +129,7 @@ export function navigateToUri(navigation, uri, additionalParams, isNavigatingBac
const { store } = window;
const params = Object.assign({ uri, uriVars }, additionalParams);
if ('File' === navigation.state.routeName) {
if (navigation.state.routeName === 'File') {
const stackAction = StackActions.replace({ routeName: 'File', newKey: uri, params });
navigation.dispatch(stackAction);
if (store && store.dispatch && !isNavigatingBack) {
@ -182,3 +183,15 @@ export function uriFromFileInfo(fileInfo) {
uriParams.claimId = claimId;
return buildURI(uriParams);
}
export function formatTagTitle(title) {
if (!title) {
return null;
}
return title.charAt(0).toUpperCase() + title.substring(1);
}
// i18n placeholder until we find a good react-native i18n module
export function __(str) {
return str;
}