diff --git a/package-lock.json b/package-lock.json index 9421550..1842396 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index cab4656..11da733 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/component/AppNavigator.js b/src/component/AppNavigator.js index 74d3ddc..71cf69a 100644 --- a/src/component/AppNavigator.js +++ b/src/component/AppNavigator.js @@ -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 }) => , + drawerIcon: ({ 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) diff --git a/src/component/categoryList/index.js b/src/component/categoryList/index.js index 928bcf6..2d87b90 100644 --- a/src/component/categoryList/index.js +++ b/src/component/categoryList/index.js @@ -1,7 +1,4 @@ import { connect } from 'react-redux'; import CategoryList from './view'; -export default connect( - null, - null -)(CategoryList); +export default connect()(CategoryList); diff --git a/src/component/channelIconItem/index.js b/src/component/channelIconItem/index.js new file mode 100644 index 0000000..0096792 --- /dev/null +++ b/src/component/channelIconItem/index.js @@ -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); diff --git a/src/component/channelIconItem/view.js b/src/component/channelIconItem/view.js new file mode 100644 index 0000000..73eba59 --- /dev/null +++ b/src/component/channelIconItem/view.js @@ -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 ( + + {isResolvingUri && ( + + + + )} + + {isPlaceholder && ( + + ALL + + )} + {!isPlaceholder && ( + + )} + + {!isPlaceholder && ( + + {title || (claim ? claim.name : '')} + + )} + + ); + } +} diff --git a/src/component/claimList/index.js b/src/component/claimList/index.js new file mode 100644 index 0000000..2ab44d1 --- /dev/null +++ b/src/component/claimList/index.js @@ -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); diff --git a/src/component/claimList/view.js b/src/component/claimList/view.js new file mode 100644 index 0000000..54f6246 --- /dev/null +++ b/src/component/claimList/view.js @@ -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 ( + + { + this.scrollView = ref; + }} + ListHeaderComponent={ListHeaderComponent} + style={claimListStyle.verticalScrollContainer} + contentContainerStyle={claimListStyle.verticalScrollPadding} + initialNumToRender={8} + maxToRenderPerBatch={24} + removeClippedSubviews + renderItem={({ item }) => ( + + )} + data={data} + keyExtractor={(item, index) => item} + onEndReached={this.handleVerticalEndReached} + onEndReachedThreshold={0.9} + /> + {(((subscriptionsView || trendingForAllView) && claimSearchLoading) || loading) && ( + + + + )} + + ); + } + + if (Constants.ORIENTATION_HORIZONTAL === orientation) { + if (loading) { + return ( + + + + ); + } + + return ( + ( + + )} + horizontal + showsHorizontalScrollIndicator={false} + data={uris ? uris.slice(0, horizontalLimit) : []} + keyExtractor={(item, index) => item} + /> + ); + } + + return null; + } +} + +export default ClaimList; diff --git a/src/component/fileItem/view.js b/src/component/fileItem/view.js index 0e44874..8cc0237 100644 --- a/src/component/fileItem/view.js +++ b/src/component/fileItem/view.js @@ -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 && ( - + )} {!compactView && (!fileInfo || !fileInfo.completed || !fileInfo.download_path) && ( diff --git a/src/component/fileListItem/view.js b/src/component/fileListItem/view.js index baaeda8..fa24157 100644 --- a/src/component/fileListItem/view.js +++ b/src/component/fileListItem/view.js @@ -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 ( - + {fileInfo && fileInfo.completed && fileInfo.download_path && ( - + )} {featuredResult && ( diff --git a/src/component/mediaPlayer/view.js b/src/component/mediaPlayer/view.js index 0fc7ac4..64ed1cc 100644 --- a/src/component/mediaPlayer/view.js +++ b/src/component/mediaPlayer/view.js @@ -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 && ( 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 ( + + + {title} + + + {items.length && + items.map(item => ( + onItemSelected(item)} + > + + {item.label} + {selectedItem && selectedItem.name === item.name && ( + + )} + + ))} + + + + ); + } +} diff --git a/src/component/modalTagSelector/index.js b/src/component/modalTagSelector/index.js new file mode 100644 index 0000000..1bf7cc7 --- /dev/null +++ b/src/component/modalTagSelector/index.js @@ -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); diff --git a/src/component/modalTagSelector/view.js b/src/component/modalTagSelector/view.js new file mode 100644 index 0000000..a37c90b --- /dev/null +++ b/src/component/modalTagSelector/view.js @@ -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 ( + + + + Customize your tags + + + {tags && + tags.map(tag => ( + + ))} + + + +