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={
+
+
+
+ }
+ style={channelCreatorStyle.scrollContainer}
+ contentContainerStyle={channelCreatorStyle.scrollPadding}
+ initialNumToRender={10}
+ maxToRenderPerBatch={20}
+ removeClippedSubviews
+ renderItem={({ item }) => (
+
+
+ {item.name.substring(1, 2).toUpperCase()}
+
+
+ {item.name}
+
+
+ )}
+ data={channels}
+ keyExtractor={(item, index) => item.claim_id}
+ />
+ )}
+
+ {currentPhase === Constants.PHASE_CREATE && (
+
+
+
+ 0
+ ? { uri: coverImageUrl }
+ : require('../../assets/default_channel_cover.png')
+ }
+ />
+
+
+
+
+ {thumbnailUrl && (
+
+ )}
+ {(!thumbnailUrl || thumbnailUrl.trim().length === 0) && newChannelName.length > 1 && (
+
+ {newChannelName.substring(0, 1).toUpperCase()}
+
+ )}
+
+
+
+
+
+
+
+
+ @
+
+ this.handleNewChannelNameChange(value, true)}
+ placeholder={'Channel name'}
+ underlineColorAndroid={Colors.NextLbryGreen}
+ />
+
+ {newChannelNameError.length > 0 && (
+ {newChannelNameError}
+ )}
+
+ Deposit
+
+ LBC
+
+
+ This LBC remains yours. It is a deposit to reserve the name and can be undone at any time.
+
+
+
+
+ {creatingChannel && }
+ {!creatingChannel && (
+
+
+
+ )}
+
+
+ )}
+
+
+
+ );
+ }
+}
diff --git a/src/page/publish/view.js b/src/page/publish/view.js
index 190ad51..427bc1b 100644
--- a/src/page/publish/view.js
+++ b/src/page/publish/view.js
@@ -27,7 +27,6 @@ import {
} from 'lbry-redux';
import { RNCamera } from 'react-native-camera';
import { generateCombination } from 'gfycat-style-urls';
-import DocumentPicker from 'react-native-document-picker';
import RNFS from 'react-native-fs';
import Button from 'component/button';
import ChannelSelector from 'component/channelSelector';
@@ -512,18 +511,7 @@ class PublishPage extends React.PureComponent {
});
};
- handleUploadPressed = () => {
- DocumentPicker.pick({ type: [DocumentPicker.types.allFiles] })
- .then(file => {
- // console.log(file);
- })
- .catch(error => {
- if (!DocumentPicker.isCancel(error)) {
- // notify the user
- // console.log(error);
- }
- });
- };
+ handleUploadPressed = () => {};
getRandomFileId = () => {
// generate a random id for a photo or recorded video between 1 and 20 (for creating thumbnails)
diff --git a/src/page/publishes/view.js b/src/page/publishes/view.js
index e0cd223..3654cc1 100644
--- a/src/page/publishes/view.js
+++ b/src/page/publishes/view.js
@@ -33,6 +33,15 @@ class PublishesPage extends React.PureComponent {
this.onComponentFocused();
}
+ componentWillReceiveProps(nextProps) {
+ const { currentRoute } = nextProps;
+ const { currentRoute: prevRoute } = this.props;
+
+ if (Constants.DRAWER_ROUTE_PUBLISHES === currentRoute && currentRoute !== prevRoute) {
+ this.onComponentFocused();
+ }
+ }
+
onComponentFocused = () => {
const { checkPendingPublishes, fetchMyClaims, pushDrawerStack, setPlayerVisible } = this.props;
pushDrawerStack();
diff --git a/src/styles/channelCreator.js b/src/styles/channelCreator.js
new file mode 100644
index 0000000..5e6fd7c
--- /dev/null
+++ b/src/styles/channelCreator.js
@@ -0,0 +1,157 @@
+import { Dimensions, StyleSheet } from 'react-native';
+import Colors from './colors';
+
+const screenDimension = Dimensions.get('window');
+const screenWidth = screenDimension.width;
+
+const channelCreatorStyle = StyleSheet.create({
+ card: {
+ backgroundColor: Colors.White,
+ marginTop: 16,
+ marginLeft: 16,
+ marginRight: 16,
+ padding: 16,
+ },
+ container: {
+ flex: 1,
+ backgroundColor: Colors.PageBackground,
+ },
+ channelPicker: {
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 16,
+ height: 52,
+ width: '100%',
+ },
+ bidRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ label: {
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 16,
+ },
+ channelNameInput: {
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 16,
+ paddingLeft: 20,
+ },
+ bidAmountInput: {
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 16,
+ marginLeft: 16,
+ textAlign: 'right',
+ width: 80,
+ },
+ helpText: {
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 12,
+ },
+ channelTitleInput: {
+ marginBottom: 4,
+ },
+ createChannelContainer: {
+ marginTop: 60,
+ flex: 1,
+ },
+ channelAt: {
+ position: 'absolute',
+ left: 4,
+ top: 13,
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 16,
+ },
+ buttonContainer: {
+ marginTop: 24,
+ },
+ buttons: {
+ marginLeft: 16,
+ marginRight: 16,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ cancelLink: {
+ marginRight: 16,
+ },
+ createButton: {
+ backgroundColor: Colors.LbryGreen,
+ alignSelf: 'flex-end',
+ },
+ inlineError: {
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 12,
+ color: Colors.Red,
+ marginTop: 2,
+ },
+ imageSelectors: {
+ width: '100%',
+ height: 160,
+ },
+ coverImageTouchArea: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ coverImage: {
+ width: '100%',
+ height: '100%',
+ },
+ avatarImageContainer: {
+ position: 'absolute',
+ left: screenWidth / 2 - 80 / 2,
+ bottom: -16,
+ width: 80,
+ height: 80,
+ borderRadius: 160,
+ overflow: 'hidden',
+ },
+ avatarTouchArea: {
+ width: 80,
+ height: 80,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ avatarImage: {
+ width: '100%',
+ height: '100%',
+ },
+ listFooterView: {
+ marginTop: 24,
+ },
+ createChannelButton: {
+ backgroundColor: Colors.LbryGreen,
+ alignSelf: 'flex-start',
+ },
+ scrollContainer: {
+ marginTop: 60,
+ marginLeft: 16,
+ marginRight: 16,
+ },
+ scrollPadding: {
+ paddingTop: 16,
+ },
+ channelListItem: {
+ flexDirection: 'row',
+ marginBottom: 16,
+ alignItems: 'center',
+ },
+ channelListName: {
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 18,
+ },
+ channelListAvatar: {
+ marginRight: 16,
+ width: 80,
+ height: 80,
+ borderRadius: 160,
+ overflow: 'hidden',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+});
+
+export default channelCreatorStyle;
diff --git a/src/styles/publish.js b/src/styles/publish.js
index ebe7694..e6aaa6e 100644
--- a/src/styles/publish.js
+++ b/src/styles/publish.js
@@ -156,6 +156,10 @@ const publishStyle = StyleSheet.create({
fontSize: 16,
marginLeft: 8,
},
+ listEmptyText: {
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 14,
+ },
titleRow: {
flexDirection: 'row',
justifyContent: 'space-between',