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:
parent
286415ee14
commit
ed2532dae1
44 changed files with 1730 additions and 405 deletions
83
package-lock.json
generated
83
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
import { connect } from 'react-redux';
|
||||
import CategoryList from './view';
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
null
|
||||
)(CategoryList);
|
||||
export default connect()(CategoryList);
|
||||
|
|
18
src/component/channelIconItem/index.js
Normal file
18
src/component/channelIconItem/index.js
Normal 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);
|
51
src/component/channelIconItem/view.js
Normal file
51
src/component/channelIconItem/view.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
40
src/component/claimList/index.js
Normal file
40
src/component/claimList/index.js
Normal 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);
|
210
src/component/claimList/view.js
Normal file
210
src/component/claimList/view.js
Normal 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;
|
|
@ -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} />
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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}
|
||||
|
|
4
src/component/modalPicker/index.js
Normal file
4
src/component/modalPicker/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ModalPicker from './view';
|
||||
|
||||
export default connect()(ModalPicker);
|
60
src/component/modalPicker/view.js
Normal file
60
src/component/modalPicker/view.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
27
src/component/modalTagSelector/index.js
Normal file
27
src/component/modalTagSelector/index.js
Normal 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);
|
66
src/component/modalTagSelector/view.js
Normal file
66
src/component/modalTagSelector/view.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
4
src/component/subscribedChannelList/index.js
Normal file
4
src/component/subscribedChannelList/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { connect } from 'react-redux';
|
||||
import SubscribedChannelList from './view';
|
||||
|
||||
export default connect()(SubscribedChannelList);
|
36
src/component/subscribedChannelList/view.js
Normal file
36
src/component/subscribedChannelList/view.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 } });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
19
src/page/tag/index.js
Normal 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
115
src/page/tag/view.js
Normal 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;
|
|
@ -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)),
|
||||
});
|
||||
|
|
|
@ -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
48
src/styles/channelIcon.js
Normal 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
34
src/styles/claimList.js
Normal 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;
|
|
@ -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
65
src/styles/modalPicker.js
Normal 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;
|
59
src/styles/modalTagSelector.js
Normal file
59
src/styles/modalTagSelector.js
Normal 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;
|
|
@ -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;
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -227,7 +227,7 @@ const walletStyle = StyleSheet.create({
|
|||
marginTop: 16,
|
||||
paddingBottom: 14,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: Colors.PageBackground
|
||||
borderBottomColor: Colors.PageBackground,
|
||||
},
|
||||
syncDriverLink: {
|
||||
color: Colors.LbryGreen,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue