diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ff1e01ed..5e30e8da 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,7 +25,7 @@ build apk: - cp -f $CI_PROJECT_DIR/scripts/mangled-glibc-syscalls.h ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21/arch-arm/usr/include/crystax/bionic/libc/include/sys/mangled-glibc-syscalls.h - rm ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz - git secret reveal - - mv buildozer.spec.travis buildozer.spec + - mv buildozer.spec.ci buildozer.spec - "./release.sh | grep -Fv -e 'working:' -e 'copy' -e 'Compiling' --line-buffered" - cp $CI_PROJECT_DIR/bin/browser-$BUILD_VERSION-release.apk /dev/null diff --git a/app/package-lock.json b/app/package-lock.json index 6cb46196..ecdde671 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -5558,8 +5558,8 @@ } }, "lbry-redux": { - "version": "github:lbryio/lbry-redux#b998577698d703714b0aa0c0e80c991902725a5c", - "from": "github:lbryio/lbry-redux#publishing", + "version": "github:lbryio/lbry-redux#9a676ee311d573b84d11f402d918aeee77be76e1", + "from": "github:lbryio/lbry-redux", "requires": { "proxy-polyfill": "0.1.6", "reselect": "^3.0.0", diff --git a/app/package.json b/app/package.json index ea9b9718..a488a898 100644 --- a/app/package.json +++ b/app/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#publishing", + "lbry-redux": "lbryio/lbry-redux", "lbryinc": "lbryio/lbryinc", "lodash": ">=4.17.11", "merge": ">=1.2.1", diff --git a/app/src/component/channelSelector/view.js b/app/src/component/channelSelector/view.js index 3031227c..b93d6514 100644 --- a/app/src/component/channelSelector/view.js +++ b/app/src/component/channelSelector/view.js @@ -74,13 +74,7 @@ export default class ChannelSelector extends React.PureComponent { newChannelName = newChannelName.slice(1); } - let newChannelNameError; - if (newChannelName.length > 1 && !isNameValid(newChannelName.substr(1), false)) { - notify({ message: 'LBRY channel names must contain only letters, numbers and dashes.' }); - } - this.setState({ - newChannelNameError, newChannelName, }); }; @@ -88,8 +82,8 @@ export default class ChannelSelector extends React.PureComponent { handleNewChannelBidChange = newChannelBid => { const { balance, notify } = this.props; let newChannelBidError; - if (newChannelBid === 0) { - newChannelBidError = __('Your deposit cannot be 0'); + 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) { @@ -109,7 +103,7 @@ export default class ChannelSelector extends React.PureComponent { const { newChannelBid, newChannelName } = this.state; if (newChannelName.trim().length === 0 || !isNameValid(newChannelName.substr(1), false)) { - notify({ message: 'LBRY channel names must contain only letters, numbers and dashes.' }); + notify({ message: 'Your channel name contains invalid characters.' }); return; } @@ -198,13 +192,17 @@ export default class ChannelSelector extends React.PureComponent { {this.state.showCreateChannel && ( - + + @ + + + Deposit { + render() { + const { navigation } = this.props; + + return ( + navigation.navigate('Rewards')}> + + Earn some credits to be able to publish your content. + + ); + } +} + +export default PublishRewadsDriver; diff --git a/app/src/component/tag/index.js b/app/src/component/tag/index.js new file mode 100644 index 00000000..728a2358 --- /dev/null +++ b/app/src/component/tag/index.js @@ -0,0 +1,4 @@ +import { connect } from 'react-redux'; +import Tag from './view'; + +export default connect()(Tag); diff --git a/app/src/component/tag/view.js b/app/src/component/tag/view.js new file mode 100644 index 00000000..e6b47bcf --- /dev/null +++ b/app/src/component/tag/view.js @@ -0,0 +1,55 @@ +import React from 'react'; +import { Text, TouchableOpacity, View } from 'react-native'; +import tagStyle from 'styles/tag'; +import Colors from 'styles/colors'; +import Icon from 'react-native-vector-icons/FontAwesome5'; + +export default class Tag extends React.PureComponent { + onPressDefault = () => { + const { name, navigation, type, onAddPress, onRemovePress } = this.props; + if ('add' === type) { + if (onAddPress) { + onAddPress(name); + } + return; + } + if ('remove' === type) { + if (onRemovePress) { + onRemovePress(name); + } + return; + } + + if (navigation) { + // navigate to tag page + } + }; + + render() { + const { name, onPress, style, type } = this.props; + + let styles = []; + if (style) { + if (style.length) { + styles = styles.concat(style); + } else { + styles.push(style); + } + } + + styles.push({ + backgroundColor: Colors.TagGreen, + borderRadius: 8, + marginBottom: 4, + }); + + return ( + + + {name} + {type && } + + + ); + } +} diff --git a/app/src/component/tagSearch/index.js b/app/src/component/tagSearch/index.js new file mode 100644 index 00000000..8cb7fba8 --- /dev/null +++ b/app/src/component/tagSearch/index.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux'; +import { selectUnfollowedTags } from 'lbry-redux'; +import TagSearch from './view'; + +const select = state => ({ + unfollowedTags: selectUnfollowedTags(state), +}); + +export default connect( + select, + null +)(TagSearch); diff --git a/app/src/component/tagSearch/view.js b/app/src/component/tagSearch/view.js new file mode 100644 index 00000000..5756230e --- /dev/null +++ b/app/src/component/tagSearch/view.js @@ -0,0 +1,81 @@ +import React from 'react'; +import { Text, TextInput, TouchableOpacity, View } from 'react-native'; +import Tag from 'component/tag'; +import tagStyle from 'styles/tag'; +import Colors from 'styles/colors'; +import Icon from 'react-native-vector-icons/FontAwesome5'; + +export default class TagSearch extends React.PureComponent { + state = { + tag: null, + tagResults: [], + }; + + componentDidMount() { + const { selectedTags = [] } = this.props; + this.updateTagResults(this.state.tag, selectedTags); + } + + componentWillReceiveProps(nextProps) { + const { selectedTags: prevSelectedTags = [] } = this.props; + const { selectedTags = [] } = nextProps; + + if (selectedTags.length !== prevSelectedTags.length) { + this.updateTagResults(this.state.tag, selectedTags); + } + } + + onAddTagPress = tag => { + const { handleAddTag } = this.props; + if (handleAddTag) { + handleAddTag(tag); + } + }; + + handleTagChange = tag => { + const { selectedTags = [] } = this.props; + this.setState({ tag }); + this.updateTagResults(tag, selectedTags); + }; + + updateTagResults = (tag, selectedTags = []) => { + const { unfollowedTags } = this.props; + + // the search term should always be the first result + let results = []; + const tagNotSelected = name => selectedTags.indexOf(name.toLowerCase()) === -1; + const suggestedTagsSet = new Set(unfollowedTags.map(tag => tag.name)); + const suggestedTags = Array.from(suggestedTagsSet).filter(tagNotSelected); + if (tag && tag.trim().length > 0) { + results.push(tag.toLowerCase()); + const doesTagMatch = name => name.toLowerCase().includes(tag.toLowerCase()); + results = results.concat(suggestedTags.filter(doesTagMatch).slice(0, 5)); + } else { + results = results.concat(suggestedTags.slice(0, 5)); + } + + this.setState({ tagResults: results }); + }; + + render() { + const { name, style, type, selectedTags = [] } = this.props; + + return ( + + + + {this.state.tagResults.map(tag => ( + this.onAddTagPress(name)} /> + ))} + + + ); + } +} diff --git a/app/src/index.js b/app/src/index.js index 629807e0..7f0b014e 100644 --- a/app/src/index.js +++ b/app/src/index.js @@ -11,6 +11,7 @@ import { notificationsReducer, publishReducer, searchReducer, + tagsReducer, walletReducer, } from 'lbry-redux'; import { @@ -41,7 +42,6 @@ import thunk from 'redux-thunk'; const globalExceptionHandler = (error, isFatal) => { if (error && NativeModules.Firebase) { - console.log(error); NativeModules.Firebase.logException(isFatal, error.message ? error.message : 'No message', JSON.stringify(error)); } }; @@ -97,6 +97,7 @@ const reducers = combineReducers({ search: searchReducer, subscriptions: subscriptionsReducer, sync: syncReducer, + tags: tagsReducer, user: userReducer, wallet: walletReducer, }); diff --git a/app/src/page/file/view.js b/app/src/page/file/view.js index cc1e34a2..f184bbb1 100644 --- a/app/src/page/file/view.js +++ b/app/src/page/file/view.js @@ -22,6 +22,7 @@ import { navigateBack, navigateToUri } from 'utils/helper'; import Icon from 'react-native-vector-icons/FontAwesome5'; import ImageViewer from 'react-native-image-zoom-viewer'; import Button from 'component/button'; +import Tag from 'component/tag'; import ChannelPage from 'page/channel'; import Colors from 'styles/colors'; import Constants from 'constants'; @@ -502,11 +503,7 @@ class FilePage extends React.PureComponent { }; renderTags = tags => { - return tags.map((tag, i) => ( - - {tag} - - )); + return tags.map((tag, i) => ); }; onFileDownloadButtonPlayed = () => { @@ -618,7 +615,8 @@ class FilePage extends React.PureComponent { const description = metadata.description ? metadata.description : null; const mediaType = Lbry.getMediaType(contentType); const isPlayable = mediaType === 'video' || mediaType === 'audio'; - const { height, channel_name: channelName, value } = claim; + const { height, signing_channel: signingChannel, value } = claim; + const channelName = signingChannel && signingChannel.name; const showActions = fileInfo && fileInfo.download_path && diff --git a/app/src/page/publish/index.js b/app/src/page/publish/index.js index 3724a5e3..5c5604f1 100644 --- a/app/src/page/publish/index.js +++ b/app/src/page/publish/index.js @@ -1,10 +1,18 @@ import { connect } from 'react-redux'; -import { doPublish, doResolveUri, doToast, doUploadThumbnail, selectPublishFormValues } from 'lbry-redux'; +import { + doPublish, + doResolveUri, + doToast, + doUploadThumbnail, + selectBalance, + selectPublishFormValues, +} from 'lbry-redux'; import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer'; import Constants from 'constants'; import PublishPage from './view'; const select = state => ({ + balance: selectBalance(state), publishFormValues: selectPublishFormValues(state), }); diff --git a/app/src/page/publish/view.js b/app/src/page/publish/view.js index 8844897f..cd5351b5 100644 --- a/app/src/page/publish/view.js +++ b/app/src/page/publish/view.js @@ -28,14 +28,47 @@ 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'; +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 = { canUseCamera: false, + titleFocused: false, + descriptionFocused: false, // gallery videos videos: null, @@ -64,9 +97,13 @@ class PublishPage extends React.PureComponent { bid: 0.1, description: null, title: null, + language: 'en', + license: LICENSES.NONE, + mature: false, name: null, price: 0, uri: null, + tags: [], uploadedThumbnailUri: null, // other @@ -124,9 +161,13 @@ class PublishPage extends React.PureComponent { channelName, currentMedia, description, + language, + license, + mature, name, price, priceSet, + tags, title, uploadedThumbnailUri: thumbnail, uri, @@ -142,22 +183,28 @@ class PublishPage extends React.PureComponent { return; } + const publishTags = tags.slice(); + if (mature) { + publishTags.push('nsfw'); + } + const publishParams = { filePath: currentMedia.filePath, bid: bid || 0.1, + tags: publishTags, title: title || '', thumbnail: thumbnail, description: description || '', - language: 'en', - nsfw: false, - license: '', + language, + nsfw: mature, + license, licenseUrl: '', otherLicenseDescription: '', name: name || undefined, contentIsFree: !priceSet, fee: { currency: 'LBC', price }, uri: uri || undefined, - channel: CLAIM_VALUES.CHANNEL_ANONYMOUS === channelName ? undefined : channelName, + channel_name: CLAIM_VALUES.CHANNEL_ANONYMOUS === channelName ? undefined : channelName, isStillEditing: false, claim: null, }; @@ -189,7 +236,6 @@ class PublishPage extends React.PureComponent { } if (publishFormValues) { - // TODO: Check thumbnail upload progress after thumbnail starts uploading if (publishFormValues.thumbnail && !this.state.uploadedThumbnailUri) { this.setState({ uploadedThumbnailUri: publishFormValues.thumbnail }); } @@ -242,9 +288,12 @@ class PublishPage extends React.PureComponent { bid: 0.1, description: null, title: null, + language: 'en', + license: LICENSES.NONE, name: null, price: 0, uri: null, + tags: [], uploadedThumbnailUri: null, }); } @@ -334,11 +383,8 @@ class PublishPage extends React.PureComponent { filetype: [DocumentPickerUtil.allFiles()], }, (error, res) => { - console.log(error); - console.log('***'); - console.log(res); if (!error) { - console.log(res); + //console.log(res); } } ); @@ -366,7 +412,7 @@ class PublishPage extends React.PureComponent { const { notify } = this.props; this.setState({ name }); if (!isNameValid(name, false)) { - notify({ message: 'LBRY names must contain only letters, numbers and dashes.' }); + notify({ message: 'Your content address contains invalid characters' }); return; } @@ -380,6 +426,37 @@ class PublishPage extends React.PureComponent { this.setState({ uri }); }; + handleAddTag = tag => { + if (!tag) { + return; + } + + const { notify } = this.props; + const { tags } = this.state; + const index = tags.indexOf(tag.toLowerCase()); + if (index === -1) { + const newTags = tags.slice(); + newTags.push(tag); + this.setState({ tags: newTags }); + } else { + notify({ message: `You already added the "${tag}" tag.` }); + } + }; + + handleRemoveTag = tag => { + if (!tag) { + return; + } + + const newTags = this.state.tags.slice(); + const index = newTags.indexOf(tag.toLowerCase()); + + if (index > -1) { + newTags.splice(index, 1); + this.setState({ tags: newTags }); + } + }; + updateThumbnailUriForMedia = media => { if (this.state.updatingThumbnailUri) { return; @@ -400,7 +477,7 @@ class PublishPage extends React.PureComponent { // upload the thumbnail if (!this.state.uploadedThumbnailUri) { - uploadThumbnail(this.getFilePathFromUri(uri), RNFS); + //this.setState({ uploadThumbnailStarted: true }, () => uploadThumbnail(this.getFilePathFromUri(uri), RNFS)); } } else if ('image' === mediaType || 'video' === mediaType) { const create = @@ -411,7 +488,7 @@ class PublishPage extends React.PureComponent { .then(path => { this.setState({ currentThumbnailUri: `file://${path}`, updatingThumbnailUri: false }); if (!this.state.uploadedThumbnailUri) { - uploadThumbnail(path, RNFS); + //this.setState({ uploadThumbnailStarted: true }, () => uploadThumbnail(path, RNFS)); } }) .catch(err => { @@ -435,7 +512,7 @@ class PublishPage extends React.PureComponent { }; render() { - const { navigation, notify, publishFormValues } = this.props; + const { balance, navigation, notify, publishFormValues } = this.props; const { thumbnailPath } = this.state; let content; @@ -443,11 +520,9 @@ class PublishPage extends React.PureComponent { content = ( - {this.state.canUseCamera && - - } + {this.state.canUseCamera && ( + + )} @@ -458,10 +533,12 @@ class PublishPage extends React.PureComponent { Take a photo - - - Upload a file - + {false && ( + + + Upload a file + + )} @@ -507,34 +584,68 @@ class PublishPage extends React.PureComponent { /> )} + {balance < 0.1 && } - - Title - - - - - Description - - - - - - Channel + {this.state.uploadThumbnailStarted && !this.state.uploadedThumbnailUri && ( + + + Uploading thumbnail... + )} + + + + {(this.state.titleFocused || (this.state.title != null && this.state.title.trim().length > 0)) && ( + Title + )} + this.setState({ titleFocused: true })} + onBlur={() => this.setState({ titleFocused: false })} + /> + + + + {(this.state.descriptionFocused || + (this.state.description != null && this.state.description.trim().length > 0)) && ( + Description + )} + this.setState({ descriptionFocused: true })} + onBlur={() => this.setState({ descriptionFocused: false })} + /> + + + + + Tags + + {this.state.tags && + this.state.tags.map(tag => ( + + ))} + + + + + + Channel @@ -600,11 +711,54 @@ class PublishPage extends React.PureComponent { )} + {this.state.advancedMode && ( + + Additional Options + + this.setState({ mature: value })} /> + Mature content + + + + Language + + {Object.keys(languages).map(lang => ( + + ))} + + + + + License + + + + {LICENSES.CC_LICENSES.map(({ value, url }) => ( + + ))} + + + + + + )} + + style={publishStyle.modeLink} + /> @@ -626,7 +780,7 @@ class PublishPage extends React.PureComponent {