channel creator page

This commit is contained in:
Akinwale Ariwodola 2019-08-30 18:31:12 +01:00
parent e74a419ffd
commit 44856e1ba1
11 changed files with 688 additions and 15 deletions

View file

@ -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",

View file

@ -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 }) => <Icon name="wallet" size={drawerIconSize} style={{ color: tintColor }} />,
},
},
ChannelCreator: {
screen: ChannelCreatorPage,
navigationOptions: {
drawerIcon: ({ tintColor }) => <Icon name="at" size={drawerIconSize} style={{ color: tintColor }} />,
},
},
Publish: {
screen: PublishPage,
navigationOptions: {

View file

@ -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,

View file

@ -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 },

View file

@ -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,
];

View file

@ -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);

View file

@ -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 (
<View style={channelCreatorStyle.container}>
<UriBar navigation={navigation} />
{currentPhase === Constants.PHASE_LIST && (
<FlatList
ListEmptyComponent={
<View style={channelCreatorStyle.listEmptyView}>
<Text style={channelCreatorStyle.listEmptyText}>
You have not created a channel. Start now by creating a new channel!
</Text>
</View>
}
ListFooterComponent={
<View style={channelCreatorStyle.listFooterView}>
<Button
style={channelCreatorStyle.createChannelButton}
text={'Create a channel'}
onPress={this.handleNewChannelPress}
/>
</View>
}
style={channelCreatorStyle.scrollContainer}
contentContainerStyle={channelCreatorStyle.scrollPadding}
initialNumToRender={10}
maxToRenderPerBatch={20}
removeClippedSubviews
renderItem={({ item }) => (
<View style={channelCreatorStyle.channelListItem}>
<View style={[channelCreatorStyle.channelListAvatar, autoStyle]}>
<Text style={channelIconStyle.autothumbCharacter}>{item.name.substring(1, 2).toUpperCase()}</Text>
</View>
<View style={channelCreatorStyle.channelListDetails}>
<Text style={channelCreatorStyle.channelListName}>{item.name}</Text>
</View>
</View>
)}
data={channels}
keyExtractor={(item, index) => item.claim_id}
/>
)}
{currentPhase === Constants.PHASE_CREATE && (
<View style={channelCreatorStyle.createChannelContainer}>
<View style={channelCreatorStyle.imageSelectors}>
<TouchableOpacity style={channelCreatorStyle.coverImageTouchArea} onPress={this.onCoverImagePress}>
<Image
style={channelCreatorStyle.coverImage}
resizeMode={'cover'}
source={
coverImageUrl && coverImageUrl.trim().length > 0
? { uri: coverImageUrl }
: require('../../assets/default_channel_cover.png')
}
/>
</TouchableOpacity>
<View style={[channelCreatorStyle.avatarImageContainer, autoStyle]}>
<TouchableOpacity style={channelCreatorStyle.avatarTouchArea} onPress={this.onAvatarImagePress}>
{thumbnailUrl && (
<Image
style={channelCreatorStyle.avatarImage}
resizeMode={'cover'}
source={{ uri: thumbnailUrl }}
/>
)}
{(!thumbnailUrl || thumbnailUrl.trim().length === 0) && newChannelName.length > 1 && (
<Text style={channelIconStyle.autothumbCharacter}>
{newChannelName.substring(0, 1).toUpperCase()}
</Text>
)}
</TouchableOpacity>
</View>
</View>
<View style={channelCreatorStyle.card}>
<TextInput
style={channelCreatorStyle.channelTitleInput}
value={this.state.newChannelTitle}
onChangeText={this.handleNewChannelTitleChange}
placeholder={'Title'}
underlineColorAndroid={Colors.NextLbryGreen}
/>
<View style={channelCreatorStyle.channelInputContainer}>
<Text style={channelCreatorStyle.channelAt}>@</Text>
<TextInput
style={channelCreatorStyle.channelNameInput}
value={this.state.newChannelName}
onChangeText={value => this.handleNewChannelNameChange(value, true)}
placeholder={'Channel name'}
underlineColorAndroid={Colors.NextLbryGreen}
/>
</View>
{newChannelNameError.length > 0 && (
<Text style={channelCreatorStyle.inlineError}>{newChannelNameError}</Text>
)}
<View style={channelCreatorStyle.bidRow}>
<Text style={channelCreatorStyle.label}>Deposit</Text>
<TextInput
style={channelCreatorStyle.bidAmountInput}
value={String(newChannelBid)}
onChangeText={this.handleNewChannelBidChange}
placeholder={'0.00'}
keyboardType={'number-pad'}
underlineColorAndroid={Colors.NextLbryGreen}
/>
<Text style={channelCreatorStyle.currency}>LBC</Text>
</View>
<Text style={channelCreatorStyle.helpText}>
This LBC remains yours. It is a deposit to reserve the name and can be undone at any time.
</Text>
</View>
<View style={channelCreatorStyle.buttonContainer}>
{creatingChannel && <ActivityIndicator size={'small'} color={Colors.NextLbryGreen} />}
{!creatingChannel && (
<View style={channelCreatorStyle.buttons}>
<Link style={channelCreatorStyle.cancelLink} text="Cancel" onPress={this.handleCreateCancel} />
<Button
style={channelCreatorStyle.createButton}
disabled={!(newChannelName.trim().length > 0 && newChannelBid > 0)}
text="Create"
onPress={this.handleCreateChannelClick}
/>
</View>
)}
</View>
</View>
)}
<FloatingWalletBalance navigation={navigation} />
</View>
);
}
}

View file

@ -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)

View file

@ -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();

View file

@ -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;

View file

@ -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',