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 58b667d0..ecdde671 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -4881,6 +4881,11 @@
"resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
"integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg="
},
+ "gfycat-style-urls": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/gfycat-style-urls/-/gfycat-style-urls-1.0.3.tgz",
+ "integrity": "sha512-HirQ+dsQWChjnfwZXB07ytzh3eZQFVlOdO2ML1YvpHBOXplumtzGIAejVu91wmj4Cw7t4760M2fKtgI+NAi/2w=="
+ },
"glob": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz",
@@ -5553,8 +5558,8 @@
}
},
"lbry-redux": {
- "version": "github:lbryio/lbry-redux#03998a2acf1a9e6c1b0818821612d137b31ebea3",
- "from": "github:lbryio/lbry-redux#03998a2acf1a9e6c1b0818821612d137b31ebea3",
+ "version": "github:lbryio/lbry-redux#9a676ee311d573b84d11f402d918aeee77be76e1",
+ "from": "github:lbryio/lbry-redux",
"requires": {
"proxy-polyfill": "0.1.6",
"reselect": "^3.0.0",
@@ -7617,6 +7622,14 @@
}
}
},
+ "react-native-camera": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/react-native-camera/-/react-native-camera-2.11.0.tgz",
+ "integrity": "sha512-ay8te4nvL5mGzRjb2QMTOyJX+JfaIW/9oFjFVIkXOB9DzFipfeVTPMdwNx9GMpdmQ0muSXkuF16pa7K/1QLHlQ==",
+ "requires": {
+ "prop-types": "^15.6.2"
+ }
+ },
"react-native-country-picker-modal": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/react-native-country-picker-modal/-/react-native-country-picker-modal-0.6.2.tgz",
@@ -7666,6 +7679,11 @@
}
}
},
+ "react-native-document-picker": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/react-native-document-picker/-/react-native-document-picker-2.3.0.tgz",
+ "integrity": "sha512-bHMyAOzFl+II0ZdfzobKsZKvTErmXfmQGalpxpGbeN8+/uhfhUcdp4WuIMecZhFyX6rbj3h3XXLdA12hVlGgmw=="
+ },
"react-native-exception-handler": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/react-native-exception-handler/-/react-native-exception-handler-2.9.0.tgz",
@@ -7676,6 +7694,15 @@
"resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-5.4.2.tgz",
"integrity": "sha512-S4E96Lwmx6z6QD3MaAuP7cNcXRLfgEUYU2GB694TbGEoOjk/FO1OnfbxfFp0vUs/klr4HJwACcwihPPxrFTt8w=="
},
+ "react-native-fs": {
+ "version": "2.13.3",
+ "resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.13.3.tgz",
+ "integrity": "sha512-B62LSSAEYQGItg7KVTzTVVCxezOYFBYp4DMVFbdoZUd1mZVFdqR2sy1HY1mye1VI/Lf3IbxSyZEQ0GmrrdwLjg==",
+ "requires": {
+ "base-64": "^0.1.0",
+ "utf8": "^2.1.1"
+ }
+ },
"react-native-gesture-handler": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.2.1.tgz",
@@ -7738,6 +7765,14 @@
"prop-types": "^15.6.2"
}
},
+ "react-native-super-grid": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/react-native-super-grid/-/react-native-super-grid-3.0.4.tgz",
+ "integrity": "sha512-aoK71FGP5sFcLujuODYkAqyFDAZZRpvTeEwwaoXsc0JENhExEG7rGg65T5ELqyykiDOLBihuCLKasK5gLb0WtQ==",
+ "requires": {
+ "prop-types": "^15.6.0"
+ }
+ },
"react-native-tab-view": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-1.4.1.tgz",
@@ -9534,6 +9569,11 @@
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="
},
+ "utf8": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.2.tgz",
+ "integrity": "sha1-H6DZJw6b6FDZsFAn9jUZv0ZFfZY="
+ },
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/app/package.json b/app/package.json
index 5326fd67..a488a898 100644
--- a/app/package.json
+++ b/app/package.json
@@ -10,7 +10,8 @@
"dependencies": {
"base-64": "^0.1.0",
"@expo/vector-icons": "^8.1.0",
- "lbry-redux": "lbryio/lbry-redux#03998a2acf1a9e6c1b0818821612d137b31ebea3",
+ "gfycat-style-urls": "^1.0.3",
+ "lbry-redux": "lbryio/lbry-redux",
"lbryinc": "lbryio/lbryinc",
"lodash": ">=4.17.11",
"merge": ">=1.2.1",
@@ -18,13 +19,17 @@
"react": "16.8.6",
"react-native": "0.59.3",
"@react-native-community/async-storage": "^1.2.2",
+ "react-native-camera": "^2.11.0",
"react-native-country-picker-modal": "^0.6.2",
+ "react-native-document-picker": "^2.3.0",
"react-native-exception-handler": "2.9.0",
"react-native-fast-image": "^5.0.3",
+ "react-native-fs": "^2.13.3",
"react-native-gesture-handler": "^1.1.0",
"react-native-image-zoom-viewer": "^2.2.5",
"react-native-password-strength-meter": "^0.0.2",
"react-native-phone-input": "lbryio/react-native-phone-input",
+ "react-native-super-grid": "^3.0.4",
"react-native-vector-icons": "^6.4.2",
"react-native-video": "lbryio/react-native-video#exoplayer-lbry-android",
"react-navigation": "^3.11.0",
diff --git a/app/src/component/AppNavigator.js b/app/src/component/AppNavigator.js
index ff35fcf0..74d3ddcb 100644
--- a/app/src/component/AppNavigator.js
+++ b/app/src/component/AppNavigator.js
@@ -5,6 +5,7 @@ import DownloadsPage from 'page/downloads';
import DrawerContent from 'component/drawerContent';
import FilePage from 'page/file';
import FirstRunScreen from 'page/firstRun';
+import PublishPage from 'page/publish';
import RewardsPage from 'page/rewards';
import TrendingPage from 'page/trending';
import SearchPage from 'page/search';
@@ -148,6 +149,12 @@ const drawer = createDrawerNavigator(
drawerIcon: ({ tintColor }) => ,
},
},
+ Publish: {
+ screen: PublishPage,
+ navigationOptions: {
+ drawerIcon: ({ tintColor }) => ,
+ },
+ },
Rewards: {
screen: RewardsPage,
navigationOptions: {
diff --git a/app/src/component/channelSelector/index.js b/app/src/component/channelSelector/index.js
new file mode 100644
index 00000000..daee8c76
--- /dev/null
+++ b/app/src/component/channelSelector/index.js
@@ -0,0 +1,27 @@
+import { connect } from 'react-redux';
+import {
+ selectBalance,
+ selectMyChannelClaims,
+ selectFetchingMyChannels,
+ doFetchChannelListMine,
+ doCreateChannel,
+ doToast,
+} from 'lbry-redux';
+import ChannelSelector 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()),
+});
+
+export default connect(
+ select,
+ perform
+)(ChannelSelector);
diff --git a/app/src/component/channelSelector/view.js b/app/src/component/channelSelector/view.js
new file mode 100644
index 00000000..b93d6514
--- /dev/null
+++ b/app/src/component/channelSelector/view.js
@@ -0,0 +1,241 @@
+import React from 'react';
+import { CLAIM_VALUES, isNameValid } from 'lbry-redux';
+import { ActivityIndicator, Picker, Text, TextInput, TouchableOpacity, View } from 'react-native';
+import Button from 'component/button';
+import Colors from 'styles/colors';
+import Constants from 'constants';
+import Icon from 'react-native-vector-icons/FontAwesome5';
+import Link from 'component/link';
+import channelSelectorStyle from 'styles/channelSelector';
+
+export default class ChannelSelector extends React.PureComponent {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ currentSelectedValue: Constants.ITEM_ANONYMOUS,
+ newChannelName: '',
+ newChannelBid: 0.1,
+ addingChannel: false,
+ creatingChannel: false,
+ newChannelNameError: '',
+ newChannelBidError: '',
+ createChannelError: undefined,
+ showCreateChannel: false,
+ };
+ }
+
+ componentDidMount() {
+ const { channels, fetchChannelListMine, fetchingChannels } = this.props;
+ if (!channels.length && !fetchingChannels) {
+ fetchChannelListMine();
+ }
+ }
+
+ 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(channel);
+ }
+ this.handleNewChannelBidChange(newChannelBid);
+ } else {
+ this.setState({ addingChannel: false });
+ if (onChannelChange) {
+ onChannelChange(channel);
+ }
+ }
+ };
+
+ handleNewChannelNameChange = value => {
+ const { notify } = this.props;
+
+ let newChannelName = value;
+
+ if (newChannelName.startsWith('@')) {
+ newChannelName = newChannelName.slice(1);
+ }
+
+ this.setState({
+ newChannelName,
+ });
+ };
+
+ 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 || !isNameValid(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);
+ }
+ };
+
+ 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;
+ };
+
+ render() {
+ const channel = this.state.addingChannel ? 'new' : this.props.channel;
+ const { fetchingChannels, channels = [] } = this.props;
+ const pickerItems = [{ name: Constants.ITEM_ANONYMOUS }, { name: Constants.ITEM_CREATE_A_CHANNEL }].concat(
+ channels
+ );
+ const {
+ newChannelName,
+ newChannelNameError,
+ newChannelBid,
+ newChannelBidError,
+ creatingChannel,
+ createChannelError,
+ addingChannel,
+ } = this.state;
+
+ return (
+
+
+ {pickerItems.map(item => (
+
+ ))}
+
+
+ {this.state.showCreateChannel && (
+
+
+ @
+
+
+
+
+ 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/app/src/component/fileItem/view.js b/app/src/component/fileItem/view.js
index 9e3e0bd7..0e448746 100644
--- a/app/src/component/fileItem/view.js
+++ b/app/src/component/fileItem/view.js
@@ -62,7 +62,8 @@ class FileItem extends React.PureComponent {
const uri = normalizeURI(this.props.uri);
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
- const channelName = claim ? claim.channel_name : null;
+ const signingChannel = claim ? claim.signing_channel : null;
+ const channelName = signingChannel ? signingChannel.name : null;
const channelClaimId =
claim && claim.value && claim.value.publisherSignature && claim.value.publisherSignature.certificateId;
const fullChannelUri = channelClaimId ? `${channelName}#${channelClaimId}` : channelName;
diff --git a/app/src/component/fileItemMedia/view.js b/app/src/component/fileItemMedia/view.js
index 502246a4..ac016678 100644
--- a/app/src/component/fileItemMedia/view.js
+++ b/app/src/component/fileItemMedia/view.js
@@ -97,6 +97,7 @@ class FileItemMedia extends React.PureComponent {
{!isResolvingUri && (
{title &&
+ title.trim().length > 0 &&
title
.replace(/\s+/g, '')
.substring(0, Math.min(title.replace(' ', '').length, 5))
diff --git a/app/src/component/fileListItem/view.js b/app/src/component/fileListItem/view.js
index 8722d1b4..baaeda8a 100644
--- a/app/src/component/fileListItem/view.js
+++ b/app/src/component/fileListItem/view.js
@@ -63,7 +63,8 @@ class FileListItem extends React.PureComponent {
let name, channel, height, channelClaimId, fullChannelUri;
if (claim) {
name = claim.name;
- channel = claim.channel_name;
+ signingChannel = claim.signing_channel;
+ channel = signingChannel ? signingChannel.name : null;
height = claim.height;
channelClaimId = claim.value && claim.value.publisherSignature && claim.value.publisherSignature.certificateId;
fullChannelUri = channelClaimId ? `${channel}#${channelClaimId}` : channel;
diff --git a/app/src/component/publishRewardsDriver/index.js b/app/src/component/publishRewardsDriver/index.js
new file mode 100644
index 00000000..92beb480
--- /dev/null
+++ b/app/src/component/publishRewardsDriver/index.js
@@ -0,0 +1,4 @@
+import { connect } from 'react-redux';
+import PublishRewardsDriver from './view';
+
+export default connect()(PublishRewardsDriver);
diff --git a/app/src/component/publishRewardsDriver/view.js b/app/src/component/publishRewardsDriver/view.js
new file mode 100644
index 00000000..33c91921
--- /dev/null
+++ b/app/src/component/publishRewardsDriver/view.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import { Text, TouchableOpacity } from 'react-native';
+import Colors from 'styles/colors';
+import Icon from 'react-native-vector-icons/FontAwesome5';
+import publishStyle from 'styles/publish';
+
+class PublishRewadsDriver extends React.PureComponent {
+ 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/constants.js b/app/src/constants.js
index c0e64505..1c802938 100644
--- a/app/src/constants.js
+++ b/app/src/constants.js
@@ -11,6 +11,10 @@ const Constants = {
PHASE_COLLECTION: 'collection',
PHASE_VERIFICATION: 'verification',
+ PHASE_SELECTOR: 'selector',
+ PHASE_DETAILS: 'details',
+ PHASE_PUBLISH: 'publish',
+
CONTENT_TAB: 'content',
ABOUT_TAB: 'about',
@@ -48,6 +52,7 @@ const Constants = {
DRAWER_ROUTE_TRENDING: 'Trending',
DRAWER_ROUTE_SUBSCRIPTIONS: 'Subscriptions',
DRAWER_ROUTE_MY_LBRY: 'Downloads',
+ DRAWER_ROUTE_PUBLISH: 'Publish',
DRAWER_ROUTE_REWARDS: 'Rewards',
DRAWER_ROUTE_WALLET: 'Wallet',
DRAWER_ROUTE_SETTINGS: 'Settings',
@@ -63,6 +68,9 @@ const Constants = {
ROUTE_FILE: 'File',
+ ITEM_CREATE_A_CHANNEL: 'Create a channel...',
+ ITEM_ANONYMOUS: 'Publish anonymously',
+
SUBSCRIPTIONS_VIEW_ALL: 'view_all',
SUBSCRIPTIONS_VIEW_LATEST_FIRST: 'view_latest_first',
diff --git a/app/src/index.js b/app/src/index.js
index 026c784d..f3e829c1 100644
--- a/app/src/index.js
+++ b/app/src/index.js
@@ -9,7 +9,9 @@ import {
fileReducer,
fileInfoReducer,
notificationsReducer,
+ publishReducer,
searchReducer,
+ tagsReducer,
walletReducer,
} from 'lbry-redux';
import {
@@ -90,11 +92,13 @@ const reducers = combineReducers({
homepage: homepageReducer,
nav: navigatorReducer,
notifications: notificationsReducer,
+ publish: publishReducer,
rewards: rewardsReducer,
settings: settingsReducer,
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
new file mode 100644
index 00000000..5c5604f1
--- /dev/null
+++ b/app/src/page/publish/index.js
@@ -0,0 +1,31 @@
+import { connect } from 'react-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),
+});
+
+const perform = dispatch => ({
+ notify: data => dispatch(doToast(data)),
+ uploadThumbnail: (filePath, fsAdapter) => dispatch(doUploadThumbnail(filePath, null, fsAdapter)),
+ publish: params => dispatch(doPublish(params)),
+ resolveUri: uri => dispatch(doResolveUri(uri)),
+ pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_PUBLISH)),
+ setPlayerVisible: () => dispatch(doSetPlayerVisible(false)),
+});
+
+export default connect(
+ select,
+ perform
+)(PublishPage);
diff --git a/app/src/page/publish/view.js b/app/src/page/publish/view.js
new file mode 100644
index 00000000..8963e0e0
--- /dev/null
+++ b/app/src/page/publish/view.js
@@ -0,0 +1,890 @@
+import React from 'react';
+import {
+ ActivityIndicator,
+ Clipboard,
+ Image,
+ NativeModules,
+ Picker,
+ ScrollView,
+ Switch,
+ Text,
+ TextInput,
+ TouchableOpacity,
+ TouchableWithoutFeedback,
+ View,
+} from 'react-native';
+import { FlatGrid } from 'react-native-super-grid';
+import { isNameValid, buildURI, regexInvalidURI, CLAIM_VALUES, LICENSES, THUMBNAIL_STATUSES } from 'lbry-redux';
+import { DocumentPicker, DocumentPickerUtil } from 'react-native-document-picker';
+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';
+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';
+
+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,
+
+ // 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: Constants.PHASE_SELECTOR,
+
+ // publish
+ advancedMode: false,
+ anonymous: true,
+ channelName: CLAIM_VALUES.CHANNEL_ANONYMOUS,
+ priceSet: false,
+
+ // input data
+ bid: 0.1,
+ description: null,
+ title: null,
+ language: 'en',
+ license: LICENSES.NONE,
+ mature: false,
+ name: null,
+ price: 0,
+ uri: null,
+ tags: [],
+ uploadedThumbnailUri: null,
+
+ // other
+ publishStarted: false,
+ };
+
+ didFocusListener;
+
+ componentWillMount() {
+ const { navigation } = this.props;
+ this.didFocusListener = navigation.addListener('didFocus', this.onComponentFocused);
+ }
+
+ componentWillUnmount() {
+ if (this.didFocusListener) {
+ this.didFocusListener.remove();
+ }
+ }
+
+ 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({ contentName: 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({ contentName: name });
+ resolveUri(uriLessChannel);
+ }
+ resolveUri(uri);
+ return uri;
+ }
+
+ return '';
+ }
+
+ handleModePressed = () => {
+ this.setState({ advancedMode: !this.state.advancedMode });
+ };
+
+ handlePublishPressed = () => {
+ const { notify, publish } = this.props;
+ const {
+ bid,
+ channelName,
+ currentMedia,
+ description,
+ language,
+ license,
+ mature,
+ 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;
+ }
+
+ 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,
+ nsfw: mature,
+ license,
+ licenseUrl: '',
+ otherLicenseDescription: '',
+ name: name || undefined,
+ contentIsFree: !priceSet,
+ fee: { currency: 'LBC', price },
+ uri: uri || undefined,
+ channel_name: CLAIM_VALUES.CHANNEL_ANONYMOUS === channelName ? undefined : channelName,
+ isStillEditing: false,
+ claim: null,
+ };
+
+ this.setState({ publishStarted: true }, () => publish(publishParams));
+ };
+
+ onComponentFocused = () => {
+ const { pushDrawerStack, setPlayerVisible } = this.props;
+
+ pushDrawerStack();
+ setPlayerVisible();
+
+ NativeModules.Gallery.canUseCamera().then(canUseCamera => this.setState({ canUseCamera }));
+ NativeModules.Gallery.getThumbnailPath().then(thumbnailPath => this.setState({ thumbnailPath }));
+ NativeModules.Gallery.getVideos().then(videos => this.setState({ videos }));
+ };
+
+ componentDidMount() {
+ this.onComponentFocused();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ const { currentRoute, publishFormValues } = nextProps;
+ const { currentRoute: prevRoute } = this.props;
+
+ if (Constants.DRAWER_ROUTE_PUBLISH === currentRoute && currentRoute !== prevRoute) {
+ this.onComponentFocused();
+ }
+
+ if (publishFormValues) {
+ if (publishFormValues.thumbnail && !this.state.uploadedThumbnailUri) {
+ this.setState({ uploadedThumbnailUri: publishFormValues.thumbnail });
+ }
+
+ if (this.state.publishStarted) {
+ if (publishFormValues.publishSuccess) {
+ this.setState({ publishStarted: false, currentPhase: Constants.PHASE_PUBLISH });
+ } else if (publishFormValues.publishError) {
+ // TODO: Display error if any
+ }
+
+ if (!publishFormValues.publishing && this.state.publishStarted) {
+ this.setState({ publishStarted: false });
+ }
+ }
+ }
+ }
+
+ setCurrentMedia(media) {
+ this.setState(
+ {
+ currentMedia: media,
+ title: media.name,
+ name: this.formatNameForTitle(media.name),
+ currentPhase: Constants.PHASE_DETAILS,
+ },
+ () => this.handleNameChange(this.state.name)
+ );
+ }
+
+ formatNameForTitle = title => {
+ return title.replace(regexInvalidURI, '-').toLowerCase();
+ };
+
+ showSelector() {
+ this.setState({
+ publishStarted: false,
+
+ currentMedia: null,
+ currentThumbnailUri: null,
+ currentPhase: Constants.PHASE_SELECTOR,
+
+ // publish
+ advancedMode: false,
+ anonymous: true,
+ channelName: CLAIM_VALUES.CHANNEL_ANONYMOUS,
+ priceSet: false,
+
+ // input data
+ bid: 0.1,
+ description: null,
+ title: null,
+ language: 'en',
+ license: LICENSES.NONE,
+ name: null,
+ price: 0,
+ uri: null,
+ tags: [],
+ uploadedThumbnailUri: 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 });
+ }
+ };
+
+ handleCloseCameraPressed = () => {
+ this.setState({ showCameraOverlay: false, videoRecordingMode: false });
+ };
+
+ getFilePathFromUri = uri => {
+ return uri.substring('file://'.length);
+ };
+
+ handleCameraActionPressed = () => {
+ // 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),
+ name: generateCombination(2, ' ', true),
+ 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,
+ });
+ });
+ }
+ } 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,
+ });
+ });
+ }
+ };
+
+ handleSwitchCameraPressed = () => {
+ const { cameraType } = this.state;
+ this.setState({
+ cameraType:
+ cameraType === RNCamera.Constants.Type.back ? RNCamera.Constants.Type.front : RNCamera.Constants.Type.back,
+ });
+ };
+
+ handleUploadPressed = () => {
+ DocumentPicker.show(
+ {
+ filetype: [DocumentPickerUtil.allFiles()],
+ },
+ (error, res) => {
+ if (!error) {
+ //console.log(res);
+ }
+ }
+ );
+ };
+
+ 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 => {
+ this.setState({ bid });
+ };
+
+ handlePriceChange = price => {
+ this.setState({ price });
+ };
+
+ handleNameChange = name => {
+ const { notify } = this.props;
+ this.setState({ name });
+ if (!isNameValid(name, false)) {
+ notify({ message: 'Your content address contains invalid characters' });
+ return;
+ }
+
+ const uri = this.getNewUri(name, this.state.channelName);
+ this.setState({ uri });
+ };
+
+ handleChannelChanged = channel => {
+ this.setState({ channelName: channel });
+ const uri = this.getNewUri(name, this.state.channelName);
+ 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;
+ }
+
+ 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 ('video' === mediaType && 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 ('image' === mediaType || 'video' === mediaType) {
+ const create =
+ 'image' === mediaType
+ ? 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 => {
+ this.setState(
+ {
+ title,
+ name: this.formatNameForTitle(title),
+ },
+ () => {
+ this.handleNameChange(this.state.name);
+ }
+ );
+ };
+
+ render() {
+ const { balance, navigation, notify, publishFormValues } = this.props;
+ const { thumbnailPath } = this.state;
+
+ let content;
+ if (Constants.PHASE_SELECTOR === this.state.currentPhase) {
+ content = (
+
+
+ {this.state.canUseCamera && (
+
+ )}
+
+
+
+ Record
+
+
+
+
+ Take a photo
+
+ {false && (
+
+
+ Upload a file
+
+ )}
+
+
+
+ {(!this.state.videos || !thumbnailPath) && (
+
+
+
+ )}
+ {this.state.videos && thumbnailPath && (
+ {
+ return (
+ this.setCurrentMedia(item)}>
+
+
+ );
+ }}
+ />
+ )}
+
+ );
+ } else if (Constants.PHASE_DETAILS === this.state.currentPhase && this.state.currentMedia) {
+ const { currentMedia, currentThumbnailUri } = this.state;
+ if (!currentThumbnailUri) {
+ this.updateThumbnailUriForMedia(currentMedia);
+ }
+ content = (
+
+ {currentThumbnailUri && currentThumbnailUri.trim().length > 0 && (
+
+
+
+ )}
+ {balance < 0.1 && }
+
+ {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
+
+
+
+
+
+
+ Price
+
+ this.setState({ priceSet: value })} />
+
+
+
+ {!this.state.priceSet && (
+ Your content will be free. Press the toggle to set a price.
+ )}
+
+ {this.state.priceSet && (
+
+
+ LBC
+
+ )}
+
+
+ {this.state.advancedMode && (
+
+ Content Address
+
+ The address where people can find your content (ex. lbry://myvideo)
+
+
+
+
+
+ LBC
+
+
+ This LBC remains yours and the deposit can be undone at any time.
+
+
+ )}
+
+ {this.state.advancedMode && (
+
+ Additional Options
+
+ this.setState({ mature: value })} />
+ Mature content
+
+
+
+ Language
+
+ {Object.keys(languages).map(lang => (
+
+ ))}
+
+
+
+
+ License
+
+
+
+ {LICENSES.CC_LICENSES.map(({ value, url }) => (
+
+ ))}
+
+
+
+
+
+ )}
+
+
+
+
+
+
+ {(this.state.publishStarted || publishFormValues.publishing) && (
+
+
+
+ )}
+
+ {!publishFormValues.publishing && !this.state.publishStarted && (
+ this.setState({ currentPhase: Constants.PHASE_SELECTOR })}
+ />
+ )}
+
+ {!publishFormValues.publishing && !this.state.publishStarted && (
+
+
+
+ )}
+
+
+ );
+ } else if (Constants.PHASE_PUBLISH === this.state.currentPhase) {
+ content = (
+
+
+ Success!
+ Congratulations! Your content was successfully uploaded.
+
+
+ {
+ Clipboard.setString(this.state.uri);
+ notify({ message: 'Copied.' });
+ }}
+ >
+
+
+
+
+ Your content will be live in a few minutes. In the mean time, feel free to publish more content or explore
+ the app.
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ {content}
+ {false && Constants.PHASE_SELECTOR !== this.state.currentPhase && (
+
+ )}
+ {this.state.canUseCamera && this.state.showCameraOverlay && (
+
+ {
+ 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',
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {this.state.recordingVideo && (
+
+ )}
+
+
+
+
+
+
+ )}
+
+ );
+ }
+}
+
+export default PublishPage;
diff --git a/app/src/styles/channelSelector.js b/app/src/styles/channelSelector.js
new file mode 100644
index 00000000..3186a161
--- /dev/null
+++ b/app/src/styles/channelSelector.js
@@ -0,0 +1,70 @@
+import { StyleSheet } from 'react-native';
+import Colors from './colors';
+
+const channelSelectorStyle = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ channelPicker: {
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 16,
+ height: 52,
+ width: '100%',
+ },
+ bidRow: {
+ flex: 1,
+ 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,
+ },
+ createChannelContainer: {
+ flex: 1,
+ marginLeft: 8,
+ marginRight: 8,
+ },
+ channelAt: {
+ position: 'absolute',
+ left: 4,
+ top: 13,
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 16,
+ },
+ buttonContainer: {
+ flex: 1,
+ marginTop: 16,
+ justifyContent: 'flex-end',
+ },
+ buttons: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'flex-end',
+ },
+ cancelLink: {
+ marginRight: 16,
+ },
+ createButton: {
+ backgroundColor: Colors.NextLbryGreen,
+ },
+});
+
+export default channelSelectorStyle;
diff --git a/app/src/styles/colors.js b/app/src/styles/colors.js
index 8b6298a2..10ca801a 100644
--- a/app/src/styles/colors.js
+++ b/app/src/styles/colors.js
@@ -3,11 +3,13 @@ const Colors = {
Black: '#000000',
ChannelGrey: '#9b9b9b',
+ DarkerGrey: '#222222',
DarkGrey: '#555555',
DescriptionGrey: '#999999',
LbryGreen: '#2f9176',
BrighterLbryGreen: '#40b887',
NextLbryGreen: '#38d9a9',
+ TagGreen: '#e3f6f1',
LightGrey: '#cccccc',
LighterGrey: '#e5e5e5',
Orange: '#ffbb00',
diff --git a/app/src/styles/filePage.js b/app/src/styles/filePage.js
index 3f1b39f2..812c4141 100644
--- a/app/src/styles/filePage.js
+++ b/app/src/styles/filePage.js
@@ -322,14 +322,16 @@ const filePageStyle = StyleSheet.create({
tagTitle: {
fontFamily: 'Inter-UI-SemiBold',
flex: 0.2,
+ marginTop: 4,
},
tagList: {
fontFamily: 'Inter-UI-Regular',
flex: 0.8,
flexDirection: 'row',
+ flexWrap: 'wrap',
},
tagItem: {
- marginRight: 16,
+ marginRight: 4,
},
rewardDriverCard: {
alignItems: 'center',
diff --git a/app/src/styles/publish.js b/app/src/styles/publish.js
new file mode 100644
index 00000000..f324d4c3
--- /dev/null
+++ b/app/src/styles/publish.js
@@ -0,0 +1,331 @@
+import { StyleSheet } from 'react-native';
+import Colors from './colors';
+
+const publishStyle = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.PageBackground,
+ },
+ gallerySelector: {
+ flex: 1,
+ marginTop: 62,
+ paddingTop: 2,
+ backgroundColor: Colors.DarkGrey,
+ },
+ galleryGrid: {
+ flex: 1,
+ },
+ galleryGridImage: {
+ width: 134,
+ height: 90,
+ },
+ inputText: {
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 16,
+ },
+ card: {
+ backgroundColor: Colors.White,
+ marginTop: 16,
+ marginLeft: 16,
+ marginRight: 16,
+ padding: 16,
+ },
+ actionButtons: {
+ marginLeft: 16,
+ marginRight: 16,
+ marginBottom: 16,
+ marginTop: 24,
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ rightActionButtons: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ modeLink: {
+ color: Colors.LbryGreen,
+ alignSelf: 'flex-end',
+ marginRight: 16,
+ },
+ publishButton: {
+ backgroundColor: Colors.LbryGreen,
+ alignSelf: 'flex-end',
+ },
+ cardTitle: {
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 20,
+ marginBottom: 8,
+ },
+ actionsView: {
+ width: '100%',
+ height: 240,
+ overflow: 'hidden',
+ },
+ record: {
+ backgroundColor: 'transparent',
+ flex: 0.5,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ subActions: {
+ flex: 0.5,
+ borderLeftWidth: 2,
+ borderLeftColor: Colors.DarkerGrey,
+ },
+ actionText: {
+ color: Colors.White,
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 14,
+ marginTop: 8,
+ },
+ photo: {
+ backgroundColor: 'transparent',
+ height: 240,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ upload: {
+ backgroundColor: Colors.Black,
+ height: 120,
+ borderTopWidth: 2,
+ borderTopColor: Colors.DarkerGrey,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ publishDetails: {
+ marginTop: 60,
+ },
+ mainThumbnailContainer: {
+ backgroundColor: Colors.Black,
+ width: '100%',
+ height: 240,
+ },
+ mainThumbnail: {
+ height: 240,
+ },
+ inputRow: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ priceInput: {
+ width: 80,
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 16,
+ },
+ currency: {
+ fontFamily: 'Inter-UI-Regular',
+ },
+ cardRow: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ },
+ switchRow: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginLeft: 24,
+ },
+ switchTitleRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginLeft: 24,
+ marginTop: -10,
+ },
+ switchText: {
+ marginRight: 4,
+ fontSize: 16,
+ },
+ loadingView: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ titleRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ },
+ cardText: {
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 14,
+ },
+ cameraPreview: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ height: 240,
+ },
+ actionsSubView: {
+ flex: 1,
+ flexDirection: 'row',
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ },
+ successContainer: {
+ padding: 16,
+ },
+ successTitle: {
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 28,
+ marginBottom: 16,
+ },
+ successText: {
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 16,
+ marginBottom: 16,
+ lineHeight: 20,
+ },
+ successRow: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ successUrl: {
+ flex: 0.9,
+ fontSize: 32,
+ fontFamily: 'Inter-UI-Regular',
+ color: Colors.NextLbryGreen,
+ marginRight: 16,
+ },
+ cameraOverlay: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ backgroundColor: Colors.Black,
+ elevation: 24,
+ },
+ fullCamera: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ zIndex: 100,
+ },
+ cameraControls: {
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ height: 120,
+ zIndex: 200,
+ alignItems: 'center',
+ },
+ controlsRow: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ mainControlsRow: {
+ flex: 0.8,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ switchCameraToggle: {
+ marginRight: 48,
+ },
+ cameraAction: {
+ width: 72,
+ height: 72,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ cameraActionIcon: {
+ position: 'absolute',
+ },
+ recordingIcon: {
+ position: 'absolute',
+ },
+ transparentControls: {
+ backgroundColor: '#00000022',
+ },
+ opaqueControls: {
+ backgroundColor: Colors.Black,
+ },
+ progress: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 16,
+ },
+ toggleContainer: {
+ marginTop: 24,
+ alignItems: 'center',
+ justifyContent: 'flex-end',
+ },
+ rewardDriverCard: {
+ alignItems: 'center',
+ backgroundColor: Colors.BrighterLbryGreen,
+ flexDirection: 'row',
+ paddingLeft: 16,
+ paddingRight: 16,
+ paddingTop: 12,
+ paddingBottom: 12,
+ },
+ rewardDriverText: {
+ fontFamily: 'Inter-UI-Regular',
+ color: Colors.White,
+ fontSize: 14,
+ },
+ rewardIcon: {
+ color: Colors.White,
+ marginRight: 8,
+ },
+ tag: {
+ marginRight: 4,
+ marginBottom: 4,
+ },
+ tagList: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ },
+ textInputLayout: {
+ marginBottom: 4,
+ },
+ textInputTitle: {
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 12,
+ marginBottom: -10,
+ marginLeft: 4,
+ },
+ thumbnailUploadContainer: {
+ marginTop: 16,
+ marginLeft: 16,
+ marginRight: 16,
+ paddingLeft: 2,
+ paddingRight: 2,
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ thumbnailUploadText: {
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 14,
+ marginLeft: 8,
+ },
+ toggleField: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginTop: 4,
+ marginBottom: 16,
+ },
+ toggleText: {
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 14,
+ marginLeft: 8,
+ },
+});
+
+export default publishStyle;
diff --git a/app/src/styles/tag.js b/app/src/styles/tag.js
new file mode 100644
index 00000000..3db63554
--- /dev/null
+++ b/app/src/styles/tag.js
@@ -0,0 +1,35 @@
+import { StyleSheet } from 'react-native';
+import Colors from './colors';
+
+const tagStyle = StyleSheet.create({
+ tag: {
+ marginRight: 4,
+ marginBottom: 4,
+ },
+ tagSearchInput: {
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 16,
+ },
+ content: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingLeft: 8,
+ paddingTop: 4,
+ paddingBottom: 4,
+ },
+ icon: {
+ marginRight: 8,
+ },
+ text: {
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 14,
+ marginRight: 8,
+ },
+ tagResultsList: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ },
+});
+
+export default tagStyle;
diff --git a/buildozer.spec.travis b/buildozer.spec.ci
similarity index 97%
rename from buildozer.spec.travis
rename to buildozer.spec.ci
index 7d799934..1851877d 100644
--- a/buildozer.spec.travis
+++ b/buildozer.spec.ci
@@ -86,7 +86,7 @@ fullscreen = 0
#android.presplash_color = #FFFFFF
# (list) Permissions
-android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE
+android.permissions = CAMERA,INTERNET,READ_EXTERNAL_STORAGE,RECORD_AUDIO,WRITE_EXTERNAL_STORAGE
# (int) Android API to use
android.api = 28
@@ -148,7 +148,7 @@ android.react_src = ./app
# (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap)
-android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.59.3, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0, com.squareup.picasso:picasso:2.71828, com.google.firebase:firebase-core:16.0.1
+android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.59.3, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0, com.squareup.picasso:picasso:2.71828, com.google.firebase:firebase-core:16.0.1, com.google.android.gms:play-services-base:16.1.0, com.android.support:exifinterface:27.1.1
# (str) python-for-android branch to use, defaults to master
#p4a.branch = stable
diff --git a/buildozer.spec.sample b/buildozer.spec.sample
index 7d799934..1851877d 100644
--- a/buildozer.spec.sample
+++ b/buildozer.spec.sample
@@ -86,7 +86,7 @@ fullscreen = 0
#android.presplash_color = #FFFFFF
# (list) Permissions
-android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE
+android.permissions = CAMERA,INTERNET,READ_EXTERNAL_STORAGE,RECORD_AUDIO,WRITE_EXTERNAL_STORAGE
# (int) Android API to use
android.api = 28
@@ -148,7 +148,7 @@ android.react_src = ./app
# (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap)
-android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.59.3, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0, com.squareup.picasso:picasso:2.71828, com.google.firebase:firebase-core:16.0.1
+android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.59.3, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0, com.squareup.picasso:picasso:2.71828, com.google.firebase:firebase-core:16.0.1, com.google.android.gms:play-services-base:16.1.0, com.android.support:exifinterface:27.1.1
# (str) python-for-android branch to use, defaults to master
#p4a.branch = stable
diff --git a/buildozer.spec.vagrant b/buildozer.spec.vagrant
index 7d799934..7ebeffd7 100644
--- a/buildozer.spec.vagrant
+++ b/buildozer.spec.vagrant
@@ -86,7 +86,7 @@ fullscreen = 0
#android.presplash_color = #FFFFFF
# (list) Permissions
-android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE
+android.permissions = CAMERA,INTERNET,READ_EXTERNAL_STORAGE,RECORD_AUDIO,WRITE_EXTERNAL_STORAGE
# (int) Android API to use
android.api = 28
@@ -148,7 +148,7 @@ android.react_src = ./app
# (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap)
-android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.59.3, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0, com.squareup.picasso:picasso:2.71828, com.google.firebase:firebase-core:16.0.1
+android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.59.3, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0, com.squareup.picasso:picasso:2.71828, com.google.firebase:firebase-core:16.0.1, com.google.android.gms:play-services-base:16.1.0, com.android.support:exifinterface:27.1.1
# (str) python-for-android branch to use, defaults to master
#p4a.branch = stable
@@ -272,3 +272,4 @@ warn_on_root = 1
# Then, invoke the command line with the "demo" profile:
#
#buildozer --profile demo android debug
+requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2018.11.29, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13, jsonschema==2.6.0, pbkdf2==1.3, pyyaml, protobuf==3.6.1, keyring==10.4.0, git+https://github.com/lbryio/aioupnp.git@a404269d91cff5358bcffb8067b0fd1d9c6842d3#egg=aioupnp, asn1crypto, mock, netifaces, cryptography, aiohttp==3.5.4, multidict==4.5.2, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir, "git+https://github.com/lbryio/lbry-sdk@v0.38.0#egg=lbry&subdirectory=lbry", "git+https://github.com/lbryio/lbry-sdk@v0.38.0#egg=torba&subdirectory=torba"
\ No newline at end of file
diff --git a/p4a/pythonforandroid/bootstraps/lbry/build/templates/build.tmpl.gradle b/p4a/pythonforandroid/bootstraps/lbry/build/templates/build.tmpl.gradle
index c981455a..97adc244 100644
--- a/p4a/pythonforandroid/bootstraps/lbry/build/templates/build.tmpl.gradle
+++ b/p4a/pythonforandroid/bootstraps/lbry/build/templates/build.tmpl.gradle
@@ -3,10 +3,11 @@ buildscript {
repositories {
google()
jcenter()
+ maven { url "https://jitpack.io" }
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0'
- classpath 'com.google.gms:google-services:4.0.1'
+ classpath 'com.google.gms:google-services:4.2.0'
}
}
@@ -36,6 +37,8 @@ android {
targetSdkVersion {{ android_api }}
versionCode {{ args.numeric_version }}
versionName '{{ args.version }}'
+ missingDimensionStrategy 'react-native-camera', 'general'
+ multiDexEnabled true
ndk {
abiFilters "armeabi-v7a", "x86"
@@ -85,6 +88,8 @@ ext {
minSdkVersion = {{ args.min_sdk_version }}
targetSdkVersion = {{ android_api }}
supportLibVersion = '27.1.1'
+ googlePlayServicesVersion = '16.1.0'
+ googlePlayServicesVisionVersion = '17.0.2'
}
subprojects {
@@ -100,8 +105,11 @@ subprojects {
dependencies {
compile project(':@react-native-community_async-storage')
+ compile project(':react-native-camera')
+ compile project(':react-native-document-picker')
compile project(':react-native-exception-handler')
compile project(':react-native-fast-image')
+ compile project(':react-native-fs')
compile project(':react-native-gesture-handler')
compile project(':react-native-video')
compile project(':rn-fetch-blob')
diff --git a/p4a/pythonforandroid/bootstraps/lbry/build/templates/settings.gradle b/p4a/pythonforandroid/bootstraps/lbry/build/templates/settings.gradle
index 91c1b4e9..72c57ba1 100644
--- a/p4a/pythonforandroid/bootstraps/lbry/build/templates/settings.gradle
+++ b/p4a/pythonforandroid/bootstraps/lbry/build/templates/settings.gradle
@@ -1,10 +1,16 @@
rootProject.name = 'browser'
include ':@react-native-community_async-storage'
project(':@react-native-community_async-storage').projectDir = new File(rootProject.projectDir, './react/node_modules/@react-native-community/async-storage/android')
+include ':react-native-camera'
+project(':react-native-camera').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-camera/android')
+include ':react-native-document-picker'
+project(':react-native-document-picker').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-document-picker/android')
include ':react-native-exception-handler'
project(':react-native-exception-handler').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-exception-handler/android')
include ':react-native-fast-image'
project(':react-native-fast-image').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-fast-image/android')
+include ':react-native-fs'
+project(':react-native-fs').projectDir = new File(settingsDir, './react/node_modules/react-native-fs/android')
include ':react-native-gesture-handler'
project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-gesture-handler/android')
include ':react-native-video'
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..48e341a0
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,3 @@
+{
+ "lockfileVersion": 1
+}
diff --git a/src/main/java/io/lbry/browser/MainActivity.java b/src/main/java/io/lbry/browser/MainActivity.java
index 901a424c..4892e591 100644
--- a/src/main/java/io/lbry/browser/MainActivity.java
+++ b/src/main/java/io/lbry/browser/MainActivity.java
@@ -1,5 +1,6 @@
package io.lbry.browser;
+import android.annotation.TargetApi;
import android.os.Build;
import android.os.Bundle;
import android.app.Activity;
@@ -31,9 +32,13 @@ import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
+import com.facebook.react.modules.core.PermissionAwareActivity;
+import com.facebook.react.modules.core.PermissionListener;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.react.ReactRootView;
import com.reactnativecommunity.asyncstorage.AsyncStoragePackage;
+import com.reactnativedocumentpicker.ReactNativeDocumentPicker;
+import com.rnfs.RNFSPackage;
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
import com.RNFetchBlob.RNFetchBlobPackage;
@@ -54,8 +59,9 @@ import java.util.Random;
import org.json.JSONObject;
import org.json.JSONArray;
import org.json.JSONException;
+import org.reactnative.camera.RNCameraPackage;
-public class MainActivity extends Activity implements DefaultHardwareBackBtnHandler {
+public class MainActivity extends Activity implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {
private static Activity currentActivity = null;
@@ -99,6 +105,8 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
private boolean receivedStopService;
+ private PermissionListener permissionListener;
+
protected String getMainComponentName() {
return "LBRYApp";
}
@@ -139,8 +147,11 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
.addPackage(new MainReactPackage())
.addPackage(new AsyncStoragePackage())
.addPackage(new FastImageViewPackage())
+ .addPackage(new ReactNativeDocumentPicker())
.addPackage(new ReactVideoPackage())
+ .addPackage(new RNCameraPackage())
.addPackage(new RNFetchBlobPackage())
+ .addPackage(new RNFSPackage())
.addPackage(new RNGestureHandlerPackage())
.addPackage(new LbryReactPackage())
.setUseDeveloperSupport(true)
@@ -352,6 +363,10 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
}
break;
}
+
+ if (permissionListener != null) {
+ permissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
}
public static String acquireDeviceId(Context context) {
@@ -473,6 +488,12 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
}
}
+ @TargetApi(Build.VERSION_CODES.M)
+ public void requestPermissions(String[] permissions, int requestCode, PermissionListener listener) {
+ permissionListener = listener;
+ ActivityCompat.requestPermissions(this, permissions, requestCode);
+ }
+
@Override
public void onNewIntent(Intent intent) {
if (mReactInstanceManager != null) {
diff --git a/src/main/java/io/lbry/browser/reactmodules/GalleryModule.java b/src/main/java/io/lbry/browser/reactmodules/GalleryModule.java
new file mode 100644
index 00000000..3dfa773f
--- /dev/null
+++ b/src/main/java/io/lbry/browser/reactmodules/GalleryModule.java
@@ -0,0 +1,320 @@
+package io.lbry.browser.reactmodules;
+
+import android.content.Context;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.Manifest;
+import android.media.ThumbnailUtils;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.MediaStore;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.WritableArray;
+import com.facebook.react.bridge.WritableMap;
+
+import io.lbry.browser.MainActivity;
+import io.lbry.browser.Utils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.ArrayList;
+
+public class GalleryModule extends ReactContextBaseJavaModule {
+ private Context context;
+
+ public GalleryModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+ this.context = reactContext;
+ }
+
+ @Override
+ public String getName() {
+ return "Gallery";
+ }
+
+ @ReactMethod
+ public void getVideos(Promise promise) {
+ WritableArray items = Arguments.createArray();
+ List videos = loadVideos();
+ for (int i = 0; i < videos.size(); i++) {
+ items.pushMap(videos.get(i).toMap());
+ }
+
+ promise.resolve(items);
+ }
+
+ @ReactMethod
+ public void getThumbnailPath(Promise promise) {
+ if (context != null) {
+ File cacheDir = context.getExternalCacheDir();
+ String thumbnailPath = String.format("%s/thumbnails", cacheDir.getAbsolutePath());
+ promise.resolve(thumbnailPath);
+ return;
+ }
+
+ promise.resolve(null);
+ }
+
+ /*
+ @ReactMethod
+ public void copyImage(String sourcePath, String destinationPath, Promise promise) {
+ try {
+ File source = new File(sourcePath);
+ File destination = new File(destinationPath);
+ if (source.exists()) {
+ FileChannel src = new FileInputStream(source).getChannel();
+ FileChannel dst = new FileOutputStream(destination).getChannel();
+ dst.transferFrom(src, 0, src.size());
+ src.close();
+ dst.close();
+
+ promise.resolve(true);
+ } else {
+ promise.reject("The source image could not be found. Please try again.");
+ }
+ } catch (Exception ex) {
+ promise.reject("The image could not be saved. Please try again.");
+ }
+ }*/
+
+ @ReactMethod
+ public void getUploadsPath(Promise promise) {
+ if (context != null) {
+ String baseFolder = Utils.getExternalStorageDir(context);
+ String uploadsPath = String.format("%s/LBRY/Uploads", baseFolder);
+ File uploadsDir = new File(uploadsPath);
+ if (!uploadsDir.isDirectory()) {
+ uploadsDir.mkdirs();
+ }
+ promise.resolve(uploadsPath);
+ }
+
+ promise.reject("The content could not be saved to the device. Please check your storage permissions.");
+ }
+
+ @ReactMethod
+ public void createVideoThumbnail(String targetId, String filePath, Promise promise) {
+ (new AsyncTask() {
+ protected String doInBackground(Void... param) {
+ String thumbnailPath = null;
+
+ if (context != null) {
+ Bitmap thumbnail = ThumbnailUtils.createVideoThumbnail(filePath, MediaStore.Video.Thumbnails.MINI_KIND);
+ File cacheDir = context.getExternalCacheDir();
+ thumbnailPath = String.format("%s/thumbnails/%s.png", cacheDir.getAbsolutePath(), targetId);
+
+ File file = new File(thumbnailPath);
+ try (FileOutputStream os = new FileOutputStream(thumbnailPath)) {
+ thumbnail.compress(Bitmap.CompressFormat.PNG, 80, os);
+ os.close();
+ } catch (IOException ex) {
+ promise.reject("Could not create a thumbnail for the video");
+ return null;
+ }
+ }
+
+ return thumbnailPath;
+ }
+
+ public void onPostExecute(String thumbnailPath) {
+ if (thumbnailPath != null && thumbnailPath.trim().length() > 0) {
+ promise.resolve(thumbnailPath);
+ }
+ }
+ }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ @ReactMethod
+ public void createImageThumbnail(String targetId, String filePath, Promise promise) {
+ (new AsyncTask() {
+ protected String doInBackground(Void... param) {
+ String thumbnailPath = null;
+ FileOutputStream os = null;
+ try {
+ Bitmap source = BitmapFactory.decodeFile(filePath);
+ // MINI_KIND dimensions
+ Bitmap thumbnail = Bitmap.createScaledBitmap(source, 512, 384, false);
+
+ if (context != null) {
+ File cacheDir = context.getExternalCacheDir();
+ thumbnailPath = String.format("%s/thumbnails/%s.png", cacheDir.getAbsolutePath(), targetId);
+ os = new FileOutputStream(thumbnailPath);
+ if (thumbnail != null) {
+ thumbnail.compress(Bitmap.CompressFormat.PNG, 80, os);
+ }
+ os.close();
+ }
+ } catch (IOException ex) {
+ promise.reject("Could not create a thumbnail for the image");
+ return null;
+ } finally {
+ if (os != null) {
+ try {
+ os.close();
+ } catch (IOException ex) {
+ // ignoe
+ }
+ }
+ }
+
+ return thumbnailPath;
+ }
+
+ public void onPostExecute(String thumbnailPath) {
+ if (thumbnailPath != null && thumbnailPath.trim().length() > 0) {
+ promise.resolve(thumbnailPath);
+ }
+ }
+ }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ private List loadVideos() {
+ String[] projection = {
+ MediaStore.MediaColumns._ID,
+ MediaStore.MediaColumns.DATA,
+ MediaStore.MediaColumns.DISPLAY_NAME,
+ MediaStore.MediaColumns.MIME_TYPE,
+ MediaStore.Video.Media.DURATION
+ };
+
+ List ids = new ArrayList();
+ List items = new ArrayList();
+ Cursor cursor = context.getContentResolver().query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, projection, null, null,
+ String.format("%s DESC", MediaStore.MediaColumns.DATE_MODIFIED));
+ while (cursor.moveToNext()) {
+ int idColumn = cursor.getColumnIndex(MediaStore.MediaColumns._ID);
+ int nameColumn = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
+ int typeColumn = cursor.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE);
+ int pathColumn = cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
+ int durationColumn = cursor.getColumnIndex(MediaStore.Video.Media.DURATION);
+
+ String id = cursor.getString(idColumn);
+ GalleryItem item = new GalleryItem();
+ item.setId(id);
+ item.setName(cursor.getString(nameColumn));
+ item.setType(cursor.getString(typeColumn));
+ item.setFilePath(cursor.getString(pathColumn));
+ items.add(item);
+ ids.add(id);
+ }
+
+ checkThumbnails(ids);
+
+ return items;
+ }
+
+ private void checkThumbnails(final List ids) {
+ (new AsyncTask() {
+ protected Void doInBackground(Void... param) {
+ if (context != null) {
+ ContentResolver resolver = context.getContentResolver();
+ for (int i = 0; i < ids.size(); i++) {
+ String id = ids.get(i);
+ File cacheDir = context.getExternalCacheDir();
+ File thumbnailsDir = new File(String.format("%s/thumbnails", cacheDir.getAbsolutePath()));
+ if (!thumbnailsDir.isDirectory()) {
+ thumbnailsDir.mkdirs();
+ }
+
+ String thumbnailPath = String.format("%s/%s.png", thumbnailsDir.getAbsolutePath(), id);
+ File file = new File(thumbnailPath);
+ if (!file.exists()) {
+ // save the thumbnail to the path
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = 1;
+ Bitmap thumbnail = MediaStore.Video.Thumbnails.getThumbnail(
+ resolver, Long.parseLong(id), MediaStore.Video.Thumbnails.MINI_KIND, options);
+ if (thumbnail != null) {
+ try (FileOutputStream os = new FileOutputStream(thumbnailPath)) {
+ thumbnail.compress(Bitmap.CompressFormat.PNG, 80, os);
+ } catch (IOException ex) {
+ // skip
+ }
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+ }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ private static class GalleryItem {
+ private String id;
+
+ private int duration;
+
+ private String filePath;
+
+ private String name;
+
+ private String type;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public int getDuration() {
+ return duration;
+ }
+
+ public void setDuration(int duration) {
+ this.duration = duration;
+ }
+
+ public String getFilePath() {
+ return filePath;
+ }
+
+ public void setFilePath(String filePath) {
+ this.filePath = filePath;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public WritableMap toMap() {
+ WritableMap map = Arguments.createMap();
+ map.putString("id", id);
+ map.putString("name", name);
+ map.putString("filePath", filePath);
+ map.putString("type", type);
+ map.putInt("duration", duration);
+
+ return map;
+ }
+ }
+
+ @ReactMethod
+ public void canUseCamera(final Promise promise) {
+ promise.resolve(MainActivity.hasPermission(Manifest.permission.CAMERA, MainActivity.getActivity()));
+ }
+}
diff --git a/src/main/java/io/lbry/browser/reactpackages/LbryReactPackage.java b/src/main/java/io/lbry/browser/reactpackages/LbryReactPackage.java
index e6191344..6b535bae 100644
--- a/src/main/java/io/lbry/browser/reactpackages/LbryReactPackage.java
+++ b/src/main/java/io/lbry/browser/reactpackages/LbryReactPackage.java
@@ -9,6 +9,7 @@ import io.lbry.browser.reactmodules.BackgroundMediaModule;
import io.lbry.browser.reactmodules.DaemonServiceControlModule;
import io.lbry.browser.reactmodules.FirstRunModule;
import io.lbry.browser.reactmodules.FirebaseModule;
+import io.lbry.browser.reactmodules.GalleryModule;
import io.lbry.browser.reactmodules.ScreenOrientationModule;
import io.lbry.browser.reactmodules.VersionInfoModule;
import io.lbry.browser.reactmodules.UtilityModule;;
@@ -31,6 +32,7 @@ public class LbryReactPackage implements ReactPackage {
modules.add(new DaemonServiceControlModule(reactContext));
modules.add(new FirstRunModule(reactContext));
modules.add(new FirebaseModule(reactContext));
+ modules.add(new GalleryModule(reactContext));
modules.add(new ScreenOrientationModule(reactContext));
modules.add(new UtilityModule(reactContext));
modules.add(new VersionInfoModule(reactContext));
diff --git a/src/main/python/lbrynetservice.py b/src/main/python/lbrynetservice.py
index 5b7eec34..74e24953 100644
--- a/src/main/python/lbrynetservice.py
+++ b/src/main/python/lbrynetservice.py
@@ -12,7 +12,6 @@ from lbry.extras.daemon.loggly_handler import get_loggly_handler
from lbry.extras.daemon.Components import DHT_COMPONENT, HASH_ANNOUNCER_COMPONENT, PEER_PROTOCOL_SERVER_COMPONENT
from lbry.extras.daemon.Daemon import Daemon
from lbry.extras.daemon.loggly_handler import get_loggly_handler
-from lbry.utils import check_connection
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)