diff --git a/package.json b/package.json index d94945e..2732922 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "@react-native-community/async-storage": "^1.5.1", "react-native-camera": "^2.11.1", "react-native-country-picker-modal": "^0.6.2", - "react-native-document-picker": "^3.2.4", "react-native-exception-handler": "2.9.0", "react-native-fast-image": "^6.1.1", "react-native-fs": "^2.13.3", diff --git a/src/component/AppNavigator.js b/src/component/AppNavigator.js index 9b5c1b7..6e625d0 100644 --- a/src/component/AppNavigator.js +++ b/src/component/AppNavigator.js @@ -1,5 +1,6 @@ import React from 'react'; import AboutPage from 'page/about'; +import ChannelCreatorPage from 'page/channelCreator'; import DiscoverPage from 'page/discover'; import DownloadsPage from 'page/downloads'; import DrawerContent from 'component/drawerContent'; @@ -159,6 +160,12 @@ const drawer = createDrawerNavigator( drawerIcon: ({ tintColor }) => , }, }, + ChannelCreator: { + screen: ChannelCreatorPage, + navigationOptions: { + drawerIcon: ({ tintColor }) => , + }, + }, Publish: { screen: PublishPage, navigationOptions: { diff --git a/src/component/channelIconItem/view.js b/src/component/channelIconItem/view.js index b9b601a..f1c1ce6 100644 --- a/src/component/channelIconItem/view.js +++ b/src/component/channelIconItem/view.js @@ -13,7 +13,6 @@ export default class ChannelIconItem extends React.PureComponent { autothumbStyle.autothumbBlue, autothumbStyle.autothumbLightBlue, autothumbStyle.autothumbCyan, - autothumbStyle.autothumbTeal, autothumbStyle.autothumbGreen, autothumbStyle.autothumbYellow, autothumbStyle.autothumbOrange, diff --git a/src/component/drawerContent/view.js b/src/component/drawerContent/view.js index 9078a9b..5ad4972 100644 --- a/src/component/drawerContent/view.js +++ b/src/component/drawerContent/view.js @@ -12,6 +12,7 @@ const groupedMenuItems = { { icon: 'globe-americas', label: 'All content', route: Constants.DRAWER_ROUTE_TRENDING }, ], 'Your content': [ + { icon: 'at', label: 'Channels', route: Constants.DRAWER_ROUTE_CHANNEL_CREATOR }, { icon: 'download', label: 'Library', route: Constants.DRAWER_ROUTE_MY_LBRY }, { icon: 'cloud-upload-alt', label: 'Publishes', route: Constants.DRAWER_ROUTE_PUBLISHES }, { icon: 'upload', label: 'New publish', route: Constants.DRAWER_ROUTE_PUBLISH }, diff --git a/src/constants.js b/src/constants.js index 3ebc1d2..2f0b353 100644 --- a/src/constants.js +++ b/src/constants.js @@ -25,6 +25,9 @@ const Constants = { PHASE_DETAILS: 'details', PHASE_PUBLISH: 'publish', + PHASE_LIST: 'list', + PHASE_NEW: 'create', + CONTENT_TAB: 'content', ABOUT_TAB: 'about', @@ -77,6 +80,7 @@ const Constants = { DRAWER_ROUTE_SEARCH: 'Search', DRAWER_ROUTE_TRANSACTION_HISTORY: 'TransactionHistory', DRAWER_ROUTE_TAG: 'Tag', + DRAWER_ROUTE_CHANNEL_CREATOR: 'ChannelCreator', FULL_ROUTE_NAME_DISCOVER: 'DiscoverStack', FULL_ROUTE_NAME_WALLET: 'WalletStack', @@ -146,4 +150,5 @@ export const DrawerRoutes = [ Constants.DRAWER_ROUTE_ABOUT, Constants.DRAWER_ROUTE_SEARCH, Constants.DRAWER_ROUTE_TRANSACTION_HISTORY, + Constants.DRAWER_ROUTE_CHANNEL_CREATOR, ]; diff --git a/src/page/channelCreator/index.js b/src/page/channelCreator/index.js new file mode 100644 index 0000000..e86c428 --- /dev/null +++ b/src/page/channelCreator/index.js @@ -0,0 +1,31 @@ +import { connect } from 'react-redux'; +import { + selectBalance, + selectMyChannelClaims, + selectFetchingMyChannels, + doFetchChannelListMine, + doCreateChannel, + doToast, +} from 'lbry-redux'; +import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer'; +import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api +import ChannelCreator from './view'; + +const select = state => ({ + channels: selectMyChannelClaims(state), + fetchingChannels: selectFetchingMyChannels(state), + balance: selectBalance(state), +}); + +const perform = dispatch => ({ + notify: data => dispatch(doToast(data)), + createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)), + fetchChannelListMine: () => dispatch(doFetchChannelListMine()), + pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_CHANNEL_CREATOR)), + setPlayerVisible: () => dispatch(doSetPlayerVisible(false)), +}); + +export default connect( + select, + perform +)(ChannelCreator); diff --git a/src/page/channelCreator/view.js b/src/page/channelCreator/view.js new file mode 100644 index 0000000..0da0aa6 --- /dev/null +++ b/src/page/channelCreator/view.js @@ -0,0 +1,473 @@ +import React from 'react'; +import { CLAIM_VALUES, isURIValid, regexInvalidURI } from 'lbry-redux'; +import { + ActivityIndicator, + DeviceEventEmitter, + FlatList, + Image, + NativeModules, + Picker, + Text, + TextInput, + TouchableOpacity, + View, +} from 'react-native'; +import Button from 'component/button'; +import ChannelIconItem from 'component/channelIconItem'; +import Colors from 'styles/colors'; +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 Link from 'component/link'; +import UriBar from 'component/uriBar'; +import channelCreatorStyle from 'styles/channelCreator'; +import channelIconStyle from 'styles/channelIcon'; + +export default class ChannelCreator extends React.PureComponent { + state = { + autoStyle: null, + currentSelectedValue: Constants.ITEM_ANONYMOUS, + currentPhase: Constants.PHASE_LIST, + displayName: null, + channelNameUserEdited: false, + newChannelTitle: '', + newChannelName: '', + newChannelBid: 0.1, + addingChannel: false, + creatingChannel: false, + newChannelNameError: '', + newChannelBidError: '', + createChannelError: undefined, + showCreateChannel: false, + thumbnailUrl: null, + coverImageUrl: null, + avatarImagePickerOpen: false, + coverImagePickerOpen: false, + }; + + didFocusListener; + + componentWillMount() { + const { navigation } = this.props; + // this.didFocusListener = navigation.addListener('didFocus', this.onComponentFocused); + } + + componentWillUnmount() { + if (this.didFocusListener) { + this.didFocusListener.remove(); + } + DeviceEventEmitter.removeListener('onDocumentPickerFilePicked', this.onFilePicked); + DeviceEventEmitter.removeListener('onDocumentPickerCanceled', this.onPickerCanceled); + } + + componentDidMount() { + this.setState({ + autoStyle: + ChannelIconItem.AUTO_THUMB_STYLES[Math.floor(Math.random() * ChannelIconItem.AUTO_THUMB_STYLES.length)], + }); + + this.onComponentFocused(); + } + + componentWillReceiveProps(nextProps) { + const { currentRoute } = nextProps; + const { currentRoute: prevRoute } = this.props; + + if (Constants.DRAWER_ROUTE_CHANNEL_CREATOR === currentRoute && currentRoute !== prevRoute) { + this.onComponentFocused(); + } + } + + onComponentFocused = () => { + const { channels, channelName, fetchChannelListMine, fetchingChannels } = this.props; + NativeModules.Firebase.setCurrentScreen('Channels').then(result => { + if (!channels.length && !fetchingChannels) { + fetchChannelListMine(); + } + + DeviceEventEmitter.addListener('onDocumentPickerFilePicked', this.onFilePicked); + DeviceEventEmitter.addListener('onDocumentPickerCanceled', this.onPickerCanceled); + }); + }; + + onFilePicked = evt => { + console.log(evt); + }; + + onPickerCanceled = () => { + this.setState({ avatarImagePickerOpen: false, coverImagePickerOpen: false }); + }; + + componentDidUpdate() { + const { channelName } = this.props; + if (this.state.currentSelectedValue !== channelName) { + this.setState({ currentSelectedValue: channelName }); + } + } + + handleCreateCancel = () => { + this.setState({ showCreateChannel: false, newChannelName: '', newChannelBid: 0.1 }); + }; + + handlePickerValueChange = (itemValue, itemIndex) => { + if (Constants.ITEM_CREATE_A_CHANNEL === itemValue) { + this.setState({ showCreateChannel: true }); + } else { + this.handleCreateCancel(); + this.handleChannelChange(Constants.ITEM_ANONYMOUS === itemValue ? CLAIM_VALUES.CHANNEL_ANONYMOUS : itemValue); + } + this.setState({ currentSelectedValue: itemValue }); + }; + + handleChannelChange = value => { + const { onChannelChange } = this.props; + const { newChannelBid } = this.state; + const channel = value; + + if (channel === CLAIM_VALUES.CHANNEL_NEW) { + this.setState({ addingChannel: true }); + if (onChannelChange) { + onChannelChange(value); + } + this.handleNewChannelBidChange(newChannelBid); + } else { + this.setState({ addingChannel: false }); + if (onChannelChange) { + onChannelChange(value); + } + } + }; + + handleNewChannelTitleChange = value => { + this.setState({ newChannelTitle: value }); + if (value && !this.state.channelNameUserEdited) { + // build the channel name based on the title + const channelName = value + .replace(new RegExp(regexInvalidURI.source, regexInvalidURI.flags + 'g'), '') + .toLowerCase(); + this.handleNewChannelNameChange(channelName, false); + } + }; + + handleNewChannelNameChange = (value, userInput) => { + const { notify } = this.props; + + let newChannelName = value, + newChannelNameError = ''; + + if (newChannelName.startsWith('@')) { + newChannelName = newChannelName.slice(1); + } + + if (newChannelName.trim().length > 0 && !isURIValid(newChannelName)) { + newChannelNameError = 'Your channel name contains invalid characters.'; + } else if (this.channelExists(newChannelName)) { + newChannelNameError = 'You have already created a channel with the same name.'; + } + + if (userInput) { + this.setState({ channelNameUserEdited: true }); + } + + this.setState({ + newChannelName, + newChannelNameError, + }); + }; + + handleNewChannelBidChange = newChannelBid => { + const { balance, notify } = this.props; + let newChannelBidError; + if (newChannelBid <= 0) { + newChannelBidError = __('Please enter a deposit above 0'); + } else if (newChannelBid === balance) { + newChannelBidError = __('Please decrease your deposit to account for transaction fees'); + } else if (newChannelBid > balance) { + newChannelBidError = __('Deposit cannot be higher than your balance'); + } + + notify({ message: newChannelBidError }); + + this.setState({ + newChannelBid, + newChannelBidError, + }); + }; + + handleCreateChannelClick = () => { + const { balance, createChannel, onChannelChange, notify } = this.props; + const { newChannelBid, newChannelName } = this.state; + + if (newChannelName.trim().length === 0 || !isURIValid(newChannelName.substr(1), false)) { + notify({ message: 'Your channel name contains invalid characters.' }); + return; + } + + if (this.channelExists(newChannelName)) { + notify({ message: 'You have already created a channel with the same name.' }); + return; + } + + if (newChannelBid > balance) { + notify({ message: 'Deposit cannot be higher than your balance' }); + return; + } + + const channelName = `@${newChannelName}`; + + this.setState({ + creatingChannel: true, + createChannelError: undefined, + }); + + const success = () => { + this.setState({ + creatingChannel: false, + addingChannel: false, + currentSelectedValue: channelName, + showCreateChannel: false, + }); + + if (onChannelChange) { + onChannelChange(channelName); + } + + // reset state and go back to the channel list + this.showChannelList(); + }; + + const failure = () => { + notify({ message: 'Unable to create channel due to an internal error.' }); + this.setState({ + creatingChannel: false, + }); + }; + + createChannel(channelName, newChannelBid).then(success, failure); + }; + + channelExists = name => { + const { channels = [] } = this.props; + for (let channel of channels) { + if ( + name.toLowerCase() === channel.name.toLowerCase() || + `@${name}`.toLowerCase() === channel.name.toLowerCase() + ) { + return true; + } + } + + return false; + }; + + onCoverImagePress = () => { + this.setState( + { + avatarImagePickerOpen: false, + coverImagePickerOpen: true, + }, + () => NativeModules.UtilityModule.openDocumentPicker('image/*') + ); + }; + + onAvatarImagePress = () => { + this.setState( + { + avatarImagePickerOpen: true, + coverImagePickerOpen: false, + }, + () => NativeModules.UtilityModule.openDocumentPicker('image/*') + ); + }; + + handleNewChannelPress = () => { + this.setState({ currentPhase: Constants.PHASE_CREATE }); + }; + + handleCreateCancel = () => { + this.showChannelList(); + }; + + showChannelList = () => { + this.resetChannelCreator(); + this.setState({ currentPhase: Constants.PHASE_LIST }); + }; + + resetChannelCreator = () => { + this.setState({ + displayName: null, + channelNameUserEdited: false, + newChannelTitle: '', + newChannelName: '', + newChannelBid: 0.1, + addingChannel: false, + creatingChannel: false, + newChannelNameError: '', + newChannelBidError: '', + createChannelError: undefined, + showCreateChannel: false, + thumbnailUrl: null, + coverImageUrl: null, + avatarImagePickerOpen: false, + coverImagePickerOpen: false, + }); + }; + + render() { + const channel = this.state.addingChannel ? 'new' : this.props.channel; + const { enabled, fetchingChannels, channels = [], navigation } = this.props; + + console.log(channels); + + const { + autoStyle, + coverImageUrl, + currentPhase, + newChannelName, + newChannelNameError, + newChannelBid, + newChannelBidError, + creatingChannel, + createChannelError, + addingChannel, + showCreateChannel, + thumbnailUrl, + } = this.state; + + return ( + + + + {currentPhase === Constants.PHASE_LIST && ( + + + You have not created a channel. Start now by creating a new channel! + + + } + ListFooterComponent={ + +