lbry-react-native/src/page/publish/view.js
2019-09-15 12:46:58 +01:00

1233 lines
42 KiB
JavaScript

import React from 'react';
import {
ActivityIndicator,
Clipboard,
DeviceEventEmitter,
Image,
NativeModules,
Picker,
ScrollView,
Switch,
Text,
TextInput,
TouchableOpacity,
TouchableWithoutFeedback,
View,
} from 'react-native';
import { FlatGrid } from 'react-native-super-grid';
import {
isNameValid,
buildURI,
normalizeURI,
parseURI,
regexInvalidURI,
CLAIM_VALUES,
LICENSES,
MATURE_TAGS,
THUMBNAIL_STATUSES,
} from 'lbry-redux';
import { RNCamera } from 'react-native-camera';
import { generateCombination } from 'gfycat-style-urls';
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'; // 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';
import Feather from 'react-native-vector-icons/Feather';
import Link from 'component/link';
import PublishRewardsDriver from 'component/publishRewardsDriver';
import Tag from 'component/tag';
import TagSearch from 'component/tagSearch';
import UriBar from 'component/uriBar';
import publishStyle from 'styles/publish';
import { __, navigateToUri } from 'utils/helper';
const languages = {
en: 'English',
zh: 'Chinese',
fr: 'French',
de: 'German',
jp: 'Japanese',
ru: 'Russian',
es: 'Spanish',
id: 'Indonesian',
it: 'Italian',
nl: 'Dutch',
tr: 'Turkish',
pl: 'Polish',
ms: 'Malay',
pt: 'Portuguese',
vi: 'Vietnamese',
th: 'Thai',
ar: 'Arabic',
cs: 'Czech',
hr: 'Croatian',
km: 'Cambodian',
ko: 'Korean',
no: 'Norwegian',
ro: 'Romanian',
hi: 'Hindi',
el: 'Greek',
};
class PublishPage extends React.PureComponent {
camera = null;
state = {
canPublish: false,
canUseCamera: false,
documentPickerOpen: false,
editMode: false,
titleFocused: false,
descriptionFocused: false,
loadingVideos: false,
vanityUrl: null,
// gallery videos
videos: null,
allThumbnailsChecked: false,
checkedThumbnails: [],
// camera
cameraType: RNCamera.Constants.Type.back,
videoRecordingMode: false,
recordingVideo: false,
showCameraOverlay: false,
// paths and media
uploadsPath: null,
thumbnailPath: null,
currentMedia: null,
currentThumbnailUri: null,
updatingThumbnailUri: false,
currentPhase: null,
// publish
advancedMode: false,
anonymous: true,
channelName: CLAIM_VALUES.CHANNEL_ANONYMOUS,
priceSet: false,
// input data
hasEditedContentAddress: false,
bid: 0.1,
description: null,
title: null,
language: 'en',
license: LICENSES.NONE,
licenseUrl: '',
otherLicenseDescription: '',
name: null,
price: 0,
currency: 'LBC',
uri: null,
tags: [],
selectedChannel: null,
uploadedThumbnailUri: null,
vanityUrlSet: false,
// other
publishStarted: false,
};
didFocusListener;
componentWillMount() {
const { navigation } = this.props;
// this.didFocusListener = navigation.addListener('didFocus', this.onComponentFocused);
DeviceEventEmitter.addListener('onGalleryThumbnailChecked', this.handleGalleryThumbnailChecked);
DeviceEventEmitter.addListener('onAllGalleryThumbnailsChecked', this.handleAllGalleryThumbnailsChecked);
DeviceEventEmitter.addListener('onDocumentPickerFilePicked', this.onFilePicked);
DeviceEventEmitter.addListener('onDocumentPickerCanceled', this.onPickerCanceled);
}
componentWillUnmount() {
if (this.didFocusListener) {
this.didFocusListener.remove();
}
DeviceEventEmitter.removeListener('onGalleryThumbnailChecked', this.handleGalleryThumbnailChecked);
DeviceEventEmitter.removeListener('onAllGalleryThumbnailsChecked', this.handleAllGalleryThumbnailsChecked);
DeviceEventEmitter.removeListener('onDocumentPickerFilePicked', this.onFilePicked);
DeviceEventEmitter.removeListener('onDocumentPickerCanceled', this.onPickerCanceled);
}
handleGalleryThumbnailChecked = evt => {
const checkedThumbnails = [...this.state.checkedThumbnails];
const { id } = evt;
// using checked because we only want thumbnails that can be displayed
if (!checkedThumbnails.includes(id)) {
checkedThumbnails.push(id);
}
this.setState({ checkedThumbnails });
};
handleAllGalleryThumbnailsChecked = () => {
this.setState({ allThumbnailsChecked: true });
};
loadPendingFormState = () => {
const { publishFormState } = this.props;
const advancedMode = publishFormState.license !== null;
this.setState({ ...publishFormState, advancedMode });
};
onComponentFocused = () => {
const { balance, hasFormState, pushDrawerStack, setPlayerVisible, navigation } = this.props;
NativeModules.Firebase.setCurrentScreen('Publish').then(result => {
pushDrawerStack(Constants.DRAWER_ROUTE_PUBLISH, navigation.state.params ? navigation.state.params : null);
setPlayerVisible();
NativeModules.Gallery.canUseCamera().then(canUseCamera => this.setState({ canUseCamera }));
NativeModules.Gallery.getThumbnailPath().then(thumbnailPath => this.setState({ thumbnailPath }));
this.setState(
{
canPublish: balance >= 0.1,
loadingVideos: true,
},
() => {
NativeModules.Gallery.getVideos().then(videos => this.setState({ videos, loadingVideos: false }));
}
);
// Check if this is an edit action
let isEditMode = false,
vanityUrlSet = false;
if (navigation.state.params) {
const { displayForm, editMode, claimToEdit, vanityUrl } = navigation.state.params;
if (editMode) {
this.prepareEdit(claimToEdit);
isEditMode = true;
} else if (vanityUrl) {
const { claimName } = parseURI(vanityUrl);
vanityUrlSet = true;
this.setState({
name: claimName,
hasEditedContentAddress: true,
vanityUrlSet,
vanityUrl: claimName,
});
}
}
if (!isEditMode && hasFormState) {
this.loadPendingFormState();
if (vanityUrlSet) {
// replace name with the specified vanity URL if there was one in the pending state
this.setState({ name: this.state.vanityUrl });
}
this.setState({ currentPhase: Constants.PHASE_DETAILS });
} else {
this.setState({ currentPhase: Constants.PHASE_SELECTOR });
}
});
};
prepareEdit = claim => {
const { pushDrawerStack } = this.props;
const { amount, name, signing_channel: signingChannel, value } = claim;
const { description, fee, languages, license, license_url: licenseUrl, tags, thumbnail, title } = value;
let channelName;
if (signingChannel) {
channelName = signingChannel.name;
}
const thumbnailUrl = thumbnail ? thumbnail.url : null;
// Determine the license
let licenseType, otherLicenseDescription;
if (!LICENSES.CC_LICENSES.some(({ value }) => value === license)) {
if (!license || license === LICENSES.NONE || license === LICENSES.PUBLIC_DOMAIN) {
licenseType = license;
} else if (license && !licenseUrl && license !== LICENSES.NONE) {
licenseType = LICENSES.COPYRIGHT;
} else {
licenseType = LICENSES.OTHER;
}
otherLicenseDescription = license;
} else {
licenseType = license;
}
this.setState(
{
editMode: true,
currentPhase: Constants.PHASE_DETAILS,
hasEditedContentAddress: true,
bid: amount,
channelName,
description,
language: languages && languages.length > 0 ? languages[0] : 'en', // default to English
license: licenseType,
licenseUrl,
otherLicenseDescription,
name,
currency: fee && fee.currency ? fee.currency : 'LBC',
price: fee && fee.amount ? fee.amount : 0,
priceSet: fee && fee.amount > 0,
tags: tags && tags.length > 0 ? tags : [],
title,
currentThumbnailUri: thumbnailUrl,
uploadedThumbnailUri: thumbnailUrl,
vanityUrlSet: false,
},
() => {
this.handleNameChange(name);
if (channelName) {
this.handleChannelChange(channelName);
}
pushDrawerStack(Constants.DRAWER_ROUTE_PUBLISH_FORM);
}
);
};
getNewUri(name, channel) {
const { resolveUri } = this.props;
// If they are midway through a channel creation, treat it as anonymous until it completes
const channelName =
channel === CLAIM_VALUES.CHANNEL_ANONYMOUS || channel === CLAIM_VALUES.CHANNEL_NEW ? '' : channel;
// We are only going to store the full uri, but we need to resolve the uri with and without the channel name
let uri;
try {
uri = buildURI({ claimName: name, channelName });
} catch (e) {
// something wrong with channel or name
}
if (uri) {
if (channelName) {
// resolve without the channel name so we know the winning bid for it
const uriLessChannel = buildURI({ claimName: name });
resolveUri(uriLessChannel);
}
resolveUri(uri);
return uri;
}
return '';
}
handleModePressed = () => {
this.setState({ advancedMode: !this.state.advancedMode });
};
handlePublishPressed = () => {
const { notify, publish, updatePublishForm } = this.props;
const {
editMode,
bid,
channelName,
currentMedia,
currency,
description,
language,
license,
licenseUrl,
otherLicenseDescription,
name,
price,
priceSet,
tags,
title,
uploadedThumbnailUri: thumbnail,
uri,
} = this.state;
if (!title || title.trim().length === 0) {
notify({ message: 'Please provide a title' });
return;
}
if (!name) {
notify({ message: 'Please specify an address where people can find your content.' });
return;
}
if (!currentMedia && !editMode) {
// sanity check. normally shouldn't happen
notify({ message: 'No file selected. Please select a video or take a photo before publishing.' });
return;
}
const publishParams = {
filePath: currentMedia ? currentMedia.filePath : null,
bid: bid || 0.1,
title: title || '',
thumbnail,
description: description || '',
language,
license,
licenseType: license,
licenseUrl,
otherLicenseDescription,
name: name || undefined,
contentIsFree: !priceSet,
fee: { currency, amount: price },
uri: uri || undefined,
channel: CLAIM_VALUES.CHANNEL_ANONYMOUS === channelName ? null : channelName,
isStillEditing: false,
tags: tags.map(tag => {
return { name: tag };
}),
};
updatePublishForm(publishParams);
this.setState({ publishStarted: true }, () => publish(this.handlePublishSuccess, this.handlePublishFailure));
};
handlePublishSuccess = data => {
const { navigation, notify } = this.props;
notify({
message: `Your content was successfully published to ${this.state.uri}. It will be available in a few mintues.`,
});
navigation.navigate({ routeName: Constants.DRAWER_ROUTE_PUBLISHES, params: { publishSuccess: true } });
};
handlePublishFailure = error => {
const { notify } = this.props;
notify({ message: __('Your content could not be published at this time. Please try again.') });
this.setState({ publishStarted: false });
};
componentDidMount() {
this.onComponentFocused();
}
componentWillReceiveProps(nextProps) {
const { currentRoute: prevRoute, drawerStack: prevDrawerStack, notify, updatePublishFormState } = this.props;
const { currentRoute, drawerStack, publishFormValues } = nextProps;
if (Constants.DRAWER_ROUTE_PUBLISH === currentRoute && currentRoute !== prevRoute) {
this.onComponentFocused();
}
if (publishFormValues) {
if (publishFormValues.thumbnail && !this.state.uploadedThumbnailUri) {
const { thumbnail } = publishFormValues;
updatePublishFormState({ currentThumbnailUri: thumbnail, uploadedThumbnailUri: thumbnail });
this.setState({ currentThumbnailUri: thumbnail, uploadedThumbnailUri: thumbnail });
}
}
if (
this.state.currentPhase === Constants.PHASE_DETAILS &&
prevDrawerStack[prevDrawerStack.length - 1].route === Constants.DRAWER_ROUTE_PUBLISH_FORM &&
drawerStack[drawerStack.length - 1].route === Constants.DRAWER_ROUTE_PUBLISH
) {
// navigated back from the form
this.showSelector();
}
}
setCurrentMedia(media) {
const { pushDrawerStack, updatePublishFormState } = this.props;
const name = generateCombination(2, ' ', true);
const newName = this.state.hasEditedContentAddress ? this.state.name : this.formatNameForTitle(name);
updatePublishFormState({ currentMedia: media, name: newName });
this.setState(
{
currentMedia: media,
title: null, // no title autogeneration (user will fill this in)
name: newName,
currentPhase: Constants.PHASE_DETAILS,
},
() => {
this.handleNameChange(this.state.name);
pushDrawerStack(Constants.DRAWER_ROUTE_PUBLISH_FORM);
}
);
}
formatNameForTitle = title => {
return title.replace(new RegExp(regexInvalidURI.source, regexInvalidURI.flags + 'g'), '-').toLowerCase();
};
showSelector() {
const { updatePublishForm } = this.props;
this.setState(
{
publishStarted: false,
documentPickerOpen: false,
editMode: false,
vanityUrl: null,
currentMedia: null,
currentThumbnailUri: null,
currentPhase: Constants.PHASE_SELECTOR,
updatingThumbnailUri: false,
uploadThumbnailStarted: false,
// publish
advancedMode: false,
anonymous: true,
channelName: CLAIM_VALUES.CHANNEL_ANONYMOUS,
priceSet: false,
// input data
hasEditedContentAddress: false,
bid: 0.1,
description: null,
title: null,
language: 'en',
license: LICENSES.NONE,
licenseUrl: '',
otherLicenseDescription: '',
name: null,
price: 0,
uri: null,
tags: [],
selectedChannel: null,
uploadedThumbnailUri: null,
vanityUrlSet: false,
},
() => {
// reset thumbnail
updatePublishForm({ thumbnail: null });
}
);
}
handleRecordVideoPressed = () => {
if (!this.state.showCameraOverlay) {
this.setState({ canUseCamera: true, showCameraOverlay: true, videoRecordingMode: true });
}
};
handleTakePhotoPressed = () => {
if (!this.state.showCameraOverlay) {
this.setState({ canUseCamera: true, showCameraOverlay: true, videoRecordingMode: false });
}
};
handleUploadPressed = () => {
if (this.state.documentPickerOpen) {
return;
}
this.setState(
{
documentPickerOpen: true,
},
() => {
NativeModules.UtilityModule.openDocumentPicker('*/*');
}
);
};
onFilePicked = evt => {
this.setState({ documentPickerOpen: false }, () => {
const currentMedia = {
id: -1,
filePath: `file://${evt.path}`,
duration: 0,
};
this.setCurrentMedia(currentMedia);
});
};
onPickerCanceled = () => {
this.setState({ documentPickerOpen: false });
};
handleCloseCameraPressed = () => {
this.setState({ showCameraOverlay: false, videoRecordingMode: false });
};
getFilePathFromUri = uri => {
return uri.substring('file://'.length);
};
handleCameraActionPressed = () => {
const { pushDrawerStack } = this.props;
// check if it's video or photo mode
if (this.state.videoRecordingMode) {
if (this.state.recordingVideo) {
this.camera.stopRecording();
} else {
this.setState({ recordingVideo: true });
const options = { quality: RNCamera.Constants.VideoQuality['1080p'] };
this.camera.recordAsync(options).then(data => {
this.setState({ recordingVideo: false });
const currentMedia = {
id: -1,
filePath: this.getFilePathFromUri(data.uri),
type: 'video/mp4', // always MP4
duration: 0,
};
this.setCurrentMedia(currentMedia);
this.setState(
{
currentThumbnailUri: null,
updatingThumbnailUri: false,
currentPhase: Constants.PHASE_DETAILS,
showCameraOverlay: false,
videoRecordingMode: false,
recordingVideo: false,
},
() => pushDrawerStack(Constants.DRAWER_ROUTE_PUBLISH_FORM)
);
});
}
} else {
const options = { quality: 0.7 };
this.camera.takePictureAsync(options).then(data => {
const currentMedia = {
id: -1,
filePath: this.getFilePathFromUri(data.uri),
name: generateCombination(2, ' ', true),
type: 'image/jpg', // always JPEG
duration: 0,
};
this.setCurrentMedia(currentMedia);
this.setState(
{
currentPhase: Constants.PHASE_DETAILS,
currentThumbnailUri: null,
updatingThumbnailUri: false,
showCameraOverlay: false,
videoRecordingMode: false,
},
() => pushDrawerStack(Constants.DRAWER_ROUTE_PUBLISH)
);
});
}
};
handleSwitchCameraPressed = () => {
const { cameraType } = this.state;
this.setState({
cameraType:
cameraType === RNCamera.Constants.Type.back ? RNCamera.Constants.Type.front : RNCamera.Constants.Type.back,
});
};
handleUploadPressed = () => {};
getRandomFileId = () => {
// generate a random id for a photo or recorded video between 1 and 20 (for creating thumbnails)
const id = Math.floor(Math.random() * (20 - 2)) + 1;
return '_' + id;
};
handlePublishAgainPressed = () => {
this.showSelector();
};
handleBidChange = bid => {
const { updatePublishFormState } = this.props;
updatePublishFormState({ bid });
this.setState({ bid });
};
handlePriceChange = price => {
const { updatePublishFormState } = this.props;
updatePublishFormState({ price });
this.setState({ price });
};
handleNameChange = (name, userInput) => {
const { notify, updatePublishFormState } = this.props;
updatePublishFormState({ name });
this.setState({ name });
if (userInput) {
this.setState({ hasEditedContentAddress: true });
}
if (!isNameValid(name, false)) {
notify({ message: 'Your content address contains invalid characters' });
return;
}
const uri = this.getNewUri(name, this.state.channelName);
this.setState({ uri });
};
handleChannelChange = channel => {
const { updatePublishFormState } = this.props;
const { name } = this.state;
const uri = this.getNewUri(name, channel);
updatePublishFormState({ uri, channelName: channel, selectedChannel: channel });
this.setState({ uri, channelName: channel, selectedChannel: channel });
};
handleAddTag = tag => {
if (!tag || !this.state.canPublish || this.state.publishStarted) {
return;
}
const { notify, updatePublishFormState } = this.props;
const { tags } = this.state;
const index = tags.indexOf(tag.toLowerCase());
if (index === -1) {
const newTags = tags.slice();
newTags.push(tag);
updatePublishFormState({ tags: newTags });
this.setState({ tags: newTags });
} else {
notify({ message: __(`You already added the "${tag}" tag.`) });
}
};
handleRemoveTag = tag => {
if (!tag || !this.state.canPublish || this.state.publishStarted) {
return;
}
const { updatePublishFormState } = this.props;
const newTags = this.state.tags.slice();
const index = newTags.indexOf(tag.toLowerCase());
if (index > -1) {
newTags.splice(index, 1);
updatePublishFormState({ tags: newTags });
this.setState({ tags: newTags });
}
};
updateThumbnailUriForMedia = media => {
if (this.state.updatingThumbnailUri) {
return;
}
const { notify, uploadThumbnail } = this.props;
const { thumbnailPath } = this.state;
this.setState({ updatingThumbnailUri: true });
if (media.type) {
const mediaType = media.type.substring(0, 5);
const tempId = this.getRandomFileId();
if (mediaType === 'video' && media.id > -1) {
const uri = `file://${thumbnailPath}/${media.id}.png`;
this.setState({ currentThumbnailUri: uri, updatingThumbnailUri: false });
// upload the thumbnail
if (!this.state.uploadedThumbnailUri) {
this.setState({ uploadThumbnailStarted: true }, () => uploadThumbnail(this.getFilePathFromUri(uri), RNFS));
}
} else if (mediaType === 'image' || mediaType === 'video') {
const create =
mediaType === 'image'
? NativeModules.Gallery.createImageThumbnail
: NativeModules.Gallery.createVideoThumbnail;
create(tempId, media.filePath)
.then(path => {
this.setState({ currentThumbnailUri: `file://${path}`, updatingThumbnailUri: false });
if (!this.state.uploadedThumbnailUri) {
this.setState({ uploadThumbnailStarted: true }, () => uploadThumbnail(path, RNFS));
}
})
.catch(err => {
notify({ message: err });
this.setState({ updatingThumbnailUri: false });
});
}
}
};
handleTitleChange = title => {
const { updatePublishFormState } = this.props;
updatePublishFormState({ title });
this.setState({ title });
if (!this.state.editMode && !this.state.hasEditedContentAddress) {
// only autogenerate url if the user has not yet edited the field
// also shouldn't change url in edit mode
this.setState(
{
name: this.formatNameForTitle(title),
},
() => {
this.handleNameChange(this.state.name);
}
);
}
};
handleCurrencyValueChange = currency => {
const { updatePublishFormState } = this.props;
updatePublishFormState({ currency });
this.setState({ currency });
};
handleDescriptionChange = description => {
const { updatePublishFormState } = this.props;
updatePublishFormState({ description });
this.setState({ description });
};
handleLanguageValueChange = language => {
const { updatePublishFormState } = this.props;
updatePublishFormState({ language });
this.setState({ language });
};
handleLicenseValueChange = license => {
const { updatePublishFormState } = this.props;
const otherLicenseDescription = [LICENSES.COPYRIGHT, LICENSES.OTHER].includes(license)
? this.state.otherLicenseDescription
: '';
const licenseUrl = LICENSES.CC_LICENSES.reduce((value, item) => {
if (typeof value === 'object') {
value = '';
}
if (license === item.value) {
value = item.url;
}
return value;
});
updatePublishFormState({ otherLicenseDescription, license, licenseUrl });
this.setState({ otherLicenseDescription, license, licenseUrl });
};
handleChangeLicenseDescription = otherLicenseDescription => {
const { updatePublishFormState } = this.props;
updatePublishFormState({ otherLicenseDescription });
this.setState({ otherLicenseDescription });
};
render() {
const { balance, navigation, notify, publishFormValues } = this.props;
const {
allThumbnailsChecked,
canUseCamera,
currentPhase,
checkedThumbnails,
loadingVideos,
thumbnailPath,
videos,
} = this.state;
let content;
if (Constants.PHASE_SELECTOR === currentPhase) {
content = (
<View style={publishStyle.gallerySelector}>
<View style={publishStyle.actionsView}>
{canUseCamera && <RNCamera style={publishStyle.cameraPreview} type={RNCamera.Constants.Type.back} />}
<View style={publishStyle.actionsSubView}>
<TouchableOpacity
style={[
publishStyle.record,
canUseCamera ? publishStyle.transparentBackground : publishStyle.actionBackground,
]}
onPress={this.handleRecordVideoPressed}
>
<Icon name="video" size={48} color={Colors.White} />
<Text style={publishStyle.actionText}>Record</Text>
</TouchableOpacity>
<View style={publishStyle.subActions}>
<TouchableOpacity
style={[
publishStyle.photo,
canUseCamera ? publishStyle.transparentBackground : publishStyle.actionBackground,
]}
onPress={this.handleTakePhotoPressed}
>
<Icon name="camera" size={48} color={Colors.White} />
<Text style={publishStyle.actionText}>Take a photo</Text>
</TouchableOpacity>
<TouchableOpacity style={publishStyle.upload} onPress={this.handleUploadPressed}>
<Icon name="file-upload" size={48} color={Colors.White} />
<Text style={publishStyle.actionText}>Upload a file</Text>
</TouchableOpacity>
</View>
</View>
</View>
{(loadingVideos || !allThumbnailsChecked) && (
<View style={publishStyle.loadingView}>
<ActivityIndicator size="small" color={Colors.NextLbryGreen} />
<Text style={publishStyle.loadingText}>Please wait while we load your videos...</Text>
</View>
)}
{!loadingVideos && (!videos || videos.length === 0) && (
<View style={publishStyle.relativeCentered}>
<Text style={publishStyle.noVideos}>
We could not find any videos on your device. Take a photo or record a video to get started.
</Text>
</View>
)}
{videos && thumbnailPath && allThumbnailsChecked && (
<FlatGrid
style={publishStyle.galleryGrid}
initialNumToRender={18}
maxToRenderPerBatch={24}
removeClippedSubviews
itemDimension={134}
spacing={2}
items={videos.filter(video => checkedThumbnails.includes(video.id))}
renderItem={({ item, index }) => {
return (
<TouchableOpacity key={index} onPress={() => this.setCurrentMedia(item)}>
<FastImage
style={publishStyle.galleryGridImage}
resizeMode={FastImage.resizeMode.cover}
source={{ uri: `file://${thumbnailPath}/${item.id}.png` }}
/>
</TouchableOpacity>
);
}}
/>
)}
</View>
);
} else if (
Constants.PHASE_DETAILS === this.state.currentPhase &&
(this.state.editMode || this.state.currentMedia)
) {
const { currentMedia, currentThumbnailUri } = this.state;
if (!currentThumbnailUri && !this.state.editMode) {
this.updateThumbnailUriForMedia(currentMedia);
}
content = (
<ScrollView style={publishStyle.publishDetails}>
{currentThumbnailUri && currentThumbnailUri.trim().length > 0 && (
<View style={publishStyle.mainThumbnailContainer}>
<FastImage
style={publishStyle.mainThumbnail}
resizeMode={FastImage.resizeMode.contain}
source={{ uri: currentThumbnailUri }}
/>
{this.state.uploadThumbnailStarted && !this.state.uploadedThumbnailUri && (
<View style={publishStyle.thumbnailUploadContainer}>
<ActivityIndicator size={'small'} color={Colors.NextLbryGreen} />
<Text style={publishStyle.thumbnailUploadText}>Uploading thumbnail...</Text>
</View>
)}
</View>
)}
{!this.state.canPublish && <PublishRewardsDriver navigation={navigation} />}
<View style={publishStyle.card}>
<View style={publishStyle.textInputLayout}>
{(this.state.titleFocused || (this.state.title != null && this.state.title.trim().length > 0)) && (
<Text style={publishStyle.textInputTitle}>Title</Text>
)}
<TextInput
editable={this.state.canPublish && !this.state.publishStarted}
placeholder={this.state.titleFocused ? '' : 'Title'}
style={publishStyle.inputText}
value={this.state.title}
numberOfLines={1}
underlineColorAndroid={Colors.NextLbryGreen}
onChangeText={this.handleTitleChange}
onFocus={() => this.setState({ titleFocused: true })}
onBlur={() => this.setState({ titleFocused: false })}
/>
</View>
<View style={publishStyle.textInputLayout}>
{(this.state.descriptionFocused ||
(this.state.description != null && this.state.description.trim().length > 0)) && (
<Text style={publishStyle.textInputTitle}>Description</Text>
)}
<TextInput
editable={this.state.canPublish && !this.state.publishStarted}
multiline
placeholder={this.state.descriptionFocused ? '' : 'Description'}
style={publishStyle.inputText}
value={this.state.description}
underlineColorAndroid={Colors.NextLbryGreen}
onChangeText={this.handleDescriptionChange}
onFocus={() => this.setState({ descriptionFocused: true })}
onBlur={() => this.setState({ descriptionFocused: false })}
/>
</View>
</View>
<View style={publishStyle.card}>
<Text style={publishStyle.cardTitle}>Tags</Text>
<View style={publishStyle.tagList}>
{this.state.tags &&
this.state.tags.map(tag => (
<Tag
key={tag}
name={tag}
type={'remove'}
style={publishStyle.tag}
onRemovePress={this.handleRemoveTag}
/>
))}
</View>
<TagSearch handleAddTag={this.handleAddTag} selectedTags={this.state.tags} showNsfwTags />
</View>
<View style={publishStyle.card}>
<Text style={publishStyle.cardTitle}>Channel</Text>
<ChannelSelector
enabled={this.state.canPublish && !this.state.publishStarted}
channelName={this.state.channelName}
onChannelChange={this.handleChannelChange}
/>
</View>
<View style={publishStyle.card}>
<View style={publishStyle.titleRow}>
<Text style={publishStyle.cardTitle}>Price</Text>
<View style={publishStyle.switchTitleRow}>
<Switch value={this.state.priceSet} onValueChange={value => this.setState({ priceSet: value })} />
</View>
</View>
{!this.state.priceSet && (
<Text style={publishStyle.cardText}>Your content will be free. Press the toggle to set a price.</Text>
)}
{this.state.priceSet && (
<View style={[publishStyle.inputRow, publishStyle.priceInputRow]}>
<TextInput
editable={this.state.canPublish && !this.state.publishStarted}
placeholder={'0.00'}
keyboardType={'number-pad'}
style={publishStyle.priceInput}
underlineColorAndroid={Colors.NextLbryGreen}
numberOfLines={1}
value={String(this.state.price)}
onChangeText={this.handlePriceChange}
/>
<Picker
style={publishStyle.currencyPicker}
enabled={this.state.canPublish && !this.state.publishStarted}
selectedValue={this.state.currency}
itemStyle={publishStyle.pickerItem}
onValueChange={this.handleCurrencyValueChange}
>
<Picker.Item label={'LBC'} value={'LBC'} />
<Picker.Item label={'USD'} value={'USD'} />
</Picker>
</View>
)}
</View>
<View style={publishStyle.card}>
<Text style={publishStyle.cardTitle}>Content address</Text>
<Text style={publishStyle.helpText}>
The address where people can find your content (ex. lbry://myvideo).
{this.state.editMode &&
' You cannot change this address while editing your content. If you wish to use a new address, please republish the content.'}
</Text>
<TextInput
editable={!this.state.editMode && this.state.canPublish && !this.state.publishStarted}
placeholder={'lbry://'}
style={publishStyle.inputText}
underlineColorAndroid={Colors.NextLbryGreen}
numberOfLines={1}
value={this.state.name}
onChangeText={value => this.handleNameChange(value, true)}
/>
<View style={publishStyle.inputRow}>
<TextInput
editable={this.state.canPublish && !this.state.publishStarted}
placeholder={'0.00'}
style={publishStyle.priceInput}
underlineColorAndroid={Colors.NextLbryGreen}
numberOfLines={1}
keyboardType={'numeric'}
value={String(this.state.bid)}
onChangeText={this.handleBidChange}
/>
<Text style={publishStyle.currency}>LBC</Text>
</View>
<Text style={publishStyle.helpText}>This LBC remains yours and the deposit can be undone at any time.</Text>
</View>
{this.state.advancedMode && (
<View style={publishStyle.card}>
<Text style={publishStyle.cardTitle}>Additional Options</Text>
<View>
<Text style={publishStyle.cardText}>Language</Text>
<Picker
enabled={this.state.canPublish && !this.state.publishStarted}
selectedValue={this.state.language}
style={publishStyle.picker}
itemStyle={publishStyle.pickerItem}
onValueChange={this.handleLanguageValueChange}
>
{Object.keys(languages).map(lang => (
<Picker.Item label={languages[lang]} value={lang} key={lang} />
))}
</Picker>
</View>
<View>
<Text style={publishStyle.cardText}>License</Text>
<Picker
enabled={this.state.canPublish && !this.state.publishStarted}
selectedValue={this.state.license}
style={publishStyle.picker}
itemStyle={publishStyle.pickerItem}
onValueChange={this.handleLicenseValueChange}
>
<Picker.Item label={'None'} value={LICENSES.NONE} key={LICENSES.NONE} />
<Picker.Item label={'Public Domain'} value={LICENSES.PUBLIC_DOMAIN} key={LICENSES.PUBLIC_DOMAIN} />
{LICENSES.CC_LICENSES.map(({ value, url }) => (
<Picker.Item label={value} value={value} key={value} />
))}
<Picker.Item label={'Copyrighted...'} value={LICENSES.COPYRIGHT} key={LICENSES.COPYRIGHT} />
<Picker.Item label={'Other...'} value={LICENSES.OTHER} key={LICENSES.OTHER} />
</Picker>
{[LICENSES.COPYRIGHT, LICENSES.OTHER].includes(this.state.license) && (
<TextInput
editable={this.state.canPublish && !this.state.publishStarted}
placeholder={'License description'}
style={publishStyle.inputText}
underlineColorAndroid={Colors.NextLbryGreen}
numberOfLines={1}
value={this.state.otherLicenseDescription}
onChangeText={this.handleChangeLicenseDescription}
/>
)}
</View>
</View>
)}
<View style={publishStyle.toggleContainer}>
<Link
text={this.state.advancedMode ? 'Hide extra fields' : 'Show extra fields'}
onPress={this.handleModePressed}
style={publishStyle.modeLink}
/>
</View>
<View style={publishStyle.actionButtons}>
{(this.state.publishStarted || publishFormValues.publishing) && (
<View style={publishStyle.progress}>
<ActivityIndicator size={'small'} color={Colors.NextLbryGreen} />
</View>
)}
{!publishFormValues.publishing && !this.state.publishStarted && (
<Link style={publishStyle.cancelLink} text="Cancel" onPress={() => this.showSelector()} />
)}
{!publishFormValues.publishing && !this.state.publishStarted && (
<View style={publishStyle.rightActionButtons}>
<Button
style={publishStyle.publishButton}
disabled={!this.state.canPublish || !this.state.uploadedThumbnailUri}
text={this.state.editMode ? 'Save changes' : 'Publish'}
onPress={this.handlePublishPressed}
/>
</View>
)}
</View>
</ScrollView>
);
} else if (Constants.PHASE_PUBLISH === this.state.currentPhase) {
content = (
<ScrollView style={publishStyle.publishDetails}>
<View style={publishStyle.successContainer}>
<Text style={publishStyle.successTitle}>Success!</Text>
<Text style={publishStyle.successText}>Congratulations! Your content was successfully uploaded.</Text>
<View style={publishStyle.successRow}>
<Link
style={publishStyle.successUrl}
text={this.state.uri}
onPress={() => navigateToUri(navigation, this.state.uri)}
/>
<TouchableOpacity
onPress={() => {
Clipboard.setString(this.state.uri);
notify({ message: 'Copied.' });
}}
>
<Icon name="clipboard" size={24} color={Colors.LbryGreen} />
</TouchableOpacity>
</View>
<Text style={publishStyle.successText}>
Your content will be live in a few minutes. In the mean time, feel free to publish more content or explore
the app.
</Text>
</View>
<View style={publishStyle.actionButtons}>
<Button style={publishStyle.publishButton} text="Publish again" onPress={this.handlePublishAgainPressed} />
</View>
</ScrollView>
);
}
return (
<View style={publishStyle.container}>
<UriBar navigation={navigation} />
{content}
{false && Constants.PHASE_SELECTOR !== this.state.currentPhase && (
<FloatingWalletBalance navigation={navigation} />
)}
{this.state.canUseCamera && this.state.showCameraOverlay && (
<View style={publishStyle.cameraOverlay}>
<RNCamera
captureAudio={this.state.videoRecordingMode}
style={publishStyle.fullCamera}
ref={ref => {
this.camera = ref;
}}
type={this.state.cameraType}
flashMode={RNCamera.Constants.FlashMode.off}
androidCameraPermissionOptions={{
title: 'Camera',
message: 'Please grant access to make use of your camera',
buttonPositive: 'OK',
buttonNegative: 'Cancel',
}}
androidRecordAudioPermissionOptions={{
title: 'Audio',
message: 'Please grant access to record audio',
buttonPositive: 'OK',
buttonNegative: 'Cancel',
}}
notAuthorizedView={
<View style={publishStyle.fullCentered}>
<Text style={publishStyle.cameraInfo}>Camera not authorized</Text>
</View>
}
/>
<View
style={[
publishStyle.cameraControls,
this.state.videoRecordingMode ? publishStyle.transparentControls : publishStyle.opaqueControls,
]}
>
<View style={publishStyle.controlsRow}>
<TouchableOpacity onPress={this.handleCloseCameraPressed} style={publishStyle.backButtonControl}>
<Icon name="arrow-left" size={28} color={Colors.White} />
</TouchableOpacity>
<View style={publishStyle.mainControlsRow}>
<TouchableOpacity style={publishStyle.switchCameraToggle} onPress={this.handleSwitchCameraPressed}>
<Feather name="rotate-cw" size={36} color={Colors.White} />
</TouchableOpacity>
<TouchableOpacity onPress={this.handleCameraActionPressed}>
<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 size={44} color={Colors.Red} />
)}
</View>
</TouchableOpacity>
</View>
</View>
</View>
</View>
)}
</View>
);
}
}
export default PublishPage;