sdk 0.37.2 update for release (#547)

This commit is contained in:
Akinwale Ariwodola 2019-05-27 10:30:33 +01:00 committed by GitHub
parent 5a737ce38d
commit abeadd858e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
89 changed files with 1982 additions and 928 deletions

89
app/package-lock.json generated
View file

@ -895,9 +895,9 @@
} }
}, },
"@react-navigation/core": { "@react-navigation/core": {
"version": "3.4.1", "version": "3.4.2",
"resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-3.4.1.tgz", "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-3.4.2.tgz",
"integrity": "sha512-slslu4FmjKQMO/EKGGqqGsfC6evQLdbJM2ROACcC2Xxf0+nPeZV5ND8HHukUZZucJRE6Bg/NI+zC1XSBYRjhnw==", "integrity": "sha512-7G+iDzLSTeOUU4vVZeRZKJ+Bd7ds7ZxYNqZcB8i0KlBeQEQfR74Ounfu/p0KIEq2RiNnaE3QT7WVP3C87sebzw==",
"requires": { "requires": {
"hoist-non-react-statics": "^3.3.0", "hoist-non-react-statics": "^3.3.0",
"path-to-regexp": "^1.7.0", "path-to-regexp": "^1.7.0",
@ -916,12 +916,12 @@
} }
}, },
"@react-navigation/native": { "@react-navigation/native": {
"version": "3.4.1", "version": "3.5.0",
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-3.4.1.tgz", "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-3.5.0.tgz",
"integrity": "sha512-pMAPQfvwC4DvhQfsrXKAf+FiU+A5XAh216v17rEePSFcbeOEt7cvewmWxCxydN/vFjJChFiPV+xnjJyJBdPLOg==", "integrity": "sha512-TmGOis++ejEXG3sqNJhCSKqB0/qLu3FQgDtO959qpqif36R/diR8SQwJqeSdofoEiK3CepdhFlTCeHdS1/+MsQ==",
"requires": { "requires": {
"hoist-non-react-statics": "^3.0.1", "hoist-non-react-statics": "^3.0.1",
"react-native-safe-area-view": "^0.13.0", "react-native-safe-area-view": "^0.14.1",
"react-native-screens": "^1.0.0 || ^1.0.0-alpha" "react-native-screens": "^1.0.0 || ^1.0.0-alpha"
}, },
"dependencies": { "dependencies": {
@ -934,9 +934,9 @@
} }
}, },
"react-native-safe-area-view": { "react-native-safe-area-view": {
"version": "0.13.1", "version": "0.14.4",
"resolved": "https://registry.npmjs.org/react-native-safe-area-view/-/react-native-safe-area-view-0.13.1.tgz", "resolved": "https://registry.npmjs.org/react-native-safe-area-view/-/react-native-safe-area-view-0.14.4.tgz",
"integrity": "sha512-d/pu2866jApSwLtK/xWAvMXZkNTIQcFrjjbcTATBrmIfFNnu8TNFUcMRFpfJ+eOn5nmx7uGmDvs9B53Ft7JGpQ==", "integrity": "sha512-ypDQVoAyNHBhMR1IGfadm8kskNzPg5czrDAzQEu5MXG9Ahoi5f1cL/rT2KO+R9f6xRjf6b1IjY53m0B0xHRd0A==",
"requires": { "requires": {
"hoist-non-react-statics": "^2.3.1" "hoist-non-react-statics": "^2.3.1"
}, },
@ -3350,11 +3350,6 @@
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
}, },
"deep-diff": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz",
"integrity": "sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ="
},
"define-property": { "define-property": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
@ -4823,8 +4818,8 @@
} }
}, },
"lbry-redux": { "lbry-redux": {
"version": "github:lbryio/lbry-redux#4b3769fc2dcc4c93771aa4c5dbb64d0e97f6375f", "version": "github:lbryio/lbry-redux#5cff70a26b05b40f2693bbae6bf50100a6781e50",
"from": "github:lbryio/lbry-redux", "from": "github:lbryio/lbry-redux#purchase-uri-failures",
"requires": { "requires": {
"proxy-polyfill": "0.1.6", "proxy-polyfill": "0.1.6",
"reselect": "^3.0.0", "reselect": "^3.0.0",
@ -4876,6 +4871,11 @@
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.11.tgz", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.11.tgz",
"integrity": "sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q==" "integrity": "sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q=="
}, },
"lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
},
"lodash.forin": { "lodash.forin": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.forin/-/lodash.forin-4.4.0.tgz", "resolved": "https://registry.npmjs.org/lodash.forin/-/lodash.forin-4.4.0.tgz",
@ -5467,9 +5467,9 @@
"integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s="
}, },
"nan": { "nan": {
"version": "2.13.2", "version": "2.14.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
"integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==", "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==",
"optional": true "optional": true
}, },
"nanomatch": { "nanomatch": {
@ -6356,9 +6356,9 @@
} }
}, },
"react-native-tab-view": { "react-native-tab-view": {
"version": "1.3.4", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-1.3.4.tgz", "resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-1.4.1.tgz",
"integrity": "sha512-iufNROTPr4+Z/IKijlp5faEANiDBWxhpgx9NSCg3esZ+HN5+UtFwB0xkn4XpNRqCvbzeBkgKMRJL3V6kr5NhWg==", "integrity": "sha512-Bke8KkDcDhvB/z0AS7MnQKMD2p6Kwfc1rSKlMOvg9CC5CnClQ2QEnhPSbwegKDYhUkBI92iH/BYy7hNSm5kbUQ==",
"requires": { "requires": {
"prop-types": "^15.6.1" "prop-types": "^15.6.1"
} }
@ -6606,15 +6606,15 @@
} }
}, },
"react-navigation": { "react-navigation": {
"version": "3.9.1", "version": "3.11.0",
"resolved": "https://registry.npmjs.org/react-navigation/-/react-navigation-3.9.1.tgz", "resolved": "https://registry.npmjs.org/react-navigation/-/react-navigation-3.11.0.tgz",
"integrity": "sha512-4rUQXGT0yvLb9yX9NDuKdrXb/NcAPGUHDTlto8Fg4Tm23uuyBBSrDVStqC59rUM4JcoQnRqhenN2wXGvWE+WYA==", "integrity": "sha512-wlPcDtNiIdPeYxNQ/MN4arY5Xe9EphD2QVpRuvvuPWW+BamF3AJaIy060r3Yz59DODAoWllscabat/yqnih8Tg==",
"requires": { "requires": {
"@react-navigation/core": "~3.4.1", "@react-navigation/core": "~3.4.1",
"@react-navigation/native": "~3.4.0", "@react-navigation/native": "~3.5.0",
"react-navigation-drawer": "1.2.1", "react-navigation-drawer": "~1.2.1",
"react-navigation-stack": "1.3.0", "react-navigation-stack": "~1.4.0",
"react-navigation-tabs": "1.1.2" "react-navigation-tabs": "~1.1.4"
} }
}, },
"react-navigation-drawer": { "react-navigation-drawer": {
@ -6634,19 +6634,19 @@
} }
}, },
"react-navigation-stack": { "react-navigation-stack": {
"version": "1.3.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/react-navigation-stack/-/react-navigation-stack-1.3.0.tgz", "resolved": "https://registry.npmjs.org/react-navigation-stack/-/react-navigation-stack-1.4.0.tgz",
"integrity": "sha512-ouyD1GkRksJSGuvAuqrJnlJnZ5g2g/+/WB/MTa8BzjSBvyOgruD5TrmEkpViCOMr1R17C8D4Htln90H4D+NV3Q==" "integrity": "sha512-zEe9wCA0Ot8agarYb//0nSWYW1GM+1R0tY/nydUV0EizeJ27At0EklYVWvYEuYU6C48va6cu8OPL7QD/CcJACw=="
}, },
"react-navigation-tabs": { "react-navigation-tabs": {
"version": "1.1.2", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/react-navigation-tabs/-/react-navigation-tabs-1.1.2.tgz", "resolved": "https://registry.npmjs.org/react-navigation-tabs/-/react-navigation-tabs-1.1.4.tgz",
"integrity": "sha512-D4fecSwZfvNh5WHTURmUVrNSgy3tiNfID0n5eKTOhCz4Sls4EM2l27UTX833ngxXhQ1FqRtBxzQZ+Dp1FWJ1pw==", "integrity": "sha512-py2hLCRxPwXOzmY1W9XcY1rWXxdK6RGW/aXh56G9gIf8cpHNDhy/bJV4e46/JrVcse3ybFaN0liT09/DM/NdwQ==",
"requires": { "requires": {
"hoist-non-react-statics": "^2.5.0", "hoist-non-react-statics": "^2.5.0",
"prop-types": "^15.6.1", "prop-types": "^15.6.1",
"react-lifecycles-compat": "^3.0.4", "react-lifecycles-compat": "^3.0.4",
"react-native-tab-view": "^1.3.4" "react-native-tab-view": "^1.4.1"
} }
}, },
"react-proxy": { "react-proxy": {
@ -6735,14 +6735,6 @@
"symbol-observable": "^1.0.3" "symbol-observable": "^1.0.3"
} }
}, },
"redux-logger": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz",
"integrity": "sha1-91VZZvMJjzyIYExEnPC69XeCdL8=",
"requires": {
"deep-diff": "^0.3.5"
}
},
"redux-persist": { "redux-persist": {
"version": "4.10.2", "version": "4.10.2",
"resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-4.10.2.tgz", "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-4.10.2.tgz",
@ -6771,10 +6763,11 @@
} }
}, },
"redux-persist-transform-filter": { "redux-persist-transform-filter": {
"version": "0.0.10", "version": "0.0.18",
"resolved": "https://registry.npmjs.org/redux-persist-transform-filter/-/redux-persist-transform-filter-0.0.10.tgz", "resolved": "https://registry.npmjs.org/redux-persist-transform-filter/-/redux-persist-transform-filter-0.0.18.tgz",
"integrity": "sha1-mjsQbOiTnSy79SEsdH7Rd//xQoA=", "integrity": "sha512-x9NxuHNDnK/THLLBqwP1tqw0yIcuxuVYXBssgGcmm5anxL0flbpLQGB5CbFYHWGG68VdQKr1vUneVnttxWJDtA==",
"requires": { "requires": {
"lodash.clonedeep": "^4.5.0",
"lodash.forin": "^4.4.0", "lodash.forin": "^4.4.0",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.isempty": "^4.4.0", "lodash.isempty": "^4.4.0",

View file

@ -8,7 +8,7 @@
"dependencies": { "dependencies": {
"base-64": "^0.1.0", "base-64": "^0.1.0",
"@expo/vector-icons": "^8.1.0", "@expo/vector-icons": "^8.1.0",
"lbry-redux": "lbryio/lbry-redux", "lbry-redux": "lbryio/lbry-redux#purchase-uri-failures",
"lbryinc": "lbryio/lbryinc#check-sync", "lbryinc": "lbryio/lbryinc#check-sync",
"lodash": ">=4.17.11", "lodash": ">=4.17.11",
"merge": ">=1.2.1", "merge": ">=1.2.1",
@ -25,16 +25,15 @@
"react-native-phone-input": "lbryio/react-native-phone-input", "react-native-phone-input": "lbryio/react-native-phone-input",
"react-native-vector-icons": "^6.4.2", "react-native-vector-icons": "^6.4.2",
"react-native-video": "lbryio/react-native-video#exoplayer-lbry-android", "react-native-video": "lbryio/react-native-video#exoplayer-lbry-android",
"react-navigation": "^3.6.1", "react-navigation": "^3.11.0",
"react-navigation-redux-helpers": "^3.0.0", "react-navigation-redux-helpers": "^3.0.0",
"react-redux": "^5.0.3", "react-redux": "^5.0.3",
"redux": "^3.6.0", "redux": "^3.6.0",
"redux-logger": "3.0.6", "redux-persist": "^4.10.2",
"redux-persist": "^4.8.0",
"redux-persist-filesystem-storage": "^1.3.2", "redux-persist-filesystem-storage": "^1.3.2",
"redux-persist-transform-compress": "^4.2.0", "redux-persist-transform-compress": "^4.2.0",
"redux-persist-transform-filter": "0.0.10", "redux-persist-transform-filter": "0.0.18",
"redux-thunk": "^2.2.0", "redux-thunk": "^2.3.0",
"rn-fetch-blob": "^0.10.15" "rn-fetch-blob": "^0.10.15"
}, },
"devDependencies": { "devDependencies": {

View file

@ -34,7 +34,6 @@ import {
TextInput, TextInput,
ToastAndroid ToastAndroid
} from 'react-native'; } from 'react-native';
import { doDeleteCompleteBlobs } from 'redux/actions/file';
import { selectDrawerStack } from 'redux/selectors/drawer'; import { selectDrawerStack } from 'redux/selectors/drawer';
import { SETTINGS, doDismissToast, doToast, selectToast } from 'lbry-redux'; import { SETTINGS, doDismissToast, doToast, selectToast } from 'lbry-redux';
import { import {
@ -116,7 +115,7 @@ const myLbryStack = createStackNavigator({
Downloads: { Downloads: {
screen: DownloadsPage, screen: DownloadsPage,
navigationOptions: ({ navigation }) => ({ navigationOptions: ({ navigation }) => ({
title: 'Downloads', title: 'Library',
header: null header: null
}) })
} }
@ -179,7 +178,7 @@ const drawer = createDrawerNavigator({
drawerIcon: ({ tintColor }) => <Icon name="award" size={20} style={{ color: tintColor }} /> drawerIcon: ({ tintColor }) => <Icon name="award" size={20} style={{ color: tintColor }} />
}}, }},
MyLBRYStack: { screen: myLbryStack, navigationOptions: { MyLBRYStack: { screen: myLbryStack, navigationOptions: {
title: 'Downloads', drawerIcon: ({ tintColor }) => <Icon name="folder" size={20} style={{ color: tintColor }} /> title: 'Library', drawerIcon: ({ tintColor }) => <Icon name="download" size={20} style={{ color: tintColor }} />
}}, }},
Settings: { screen: SettingsPage, navigationOptions: { Settings: { screen: SettingsPage, navigationOptions: {
drawerLockMode: 'locked-closed', drawerLockMode: 'locked-closed',
@ -225,7 +224,6 @@ const mainStackNavigator = new createStackNavigator({
}); });
export const AppNavigator = mainStackNavigator; export const AppNavigator = mainStackNavigator;
export const reactNavigationMiddleware = createReactNavigationReduxMiddleware( export const reactNavigationMiddleware = createReactNavigationReduxMiddleware(
state => state.nav, state => state.nav,
@ -299,9 +297,11 @@ class AppWithNavigationState extends React.Component {
ToastAndroid.show('Your email address was successfully verified.', ToastAndroid.LONG); ToastAndroid.show('Your email address was successfully verified.', ToastAndroid.LONG);
// upon successful email verification, check wallet sync // upon successful email verification, do wallet sync (if password has been set)
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => { NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => {
dispatch(doGetSync(walletPassword)); if (walletPassword && walletPassword.trim().length > 0) {
dispatch(doGetSync(walletPassword));
}
}); });
} }
} }
@ -343,6 +343,7 @@ class AppWithNavigationState extends React.Component {
if (!emailVerifyErrorMessage) { if (!emailVerifyErrorMessage) {
AsyncStorage.removeItem(Constants.KEY_FIRST_RUN_EMAIL); AsyncStorage.removeItem(Constants.KEY_FIRST_RUN_EMAIL);
} }
AsyncStorage.removeItem(Constants.KEY_SHOULD_VERIFY_EMAIL); AsyncStorage.removeItem(Constants.KEY_SHOULD_VERIFY_EMAIL);
dispatch(doToast({ message })); dispatch(doToast({ message }));
} }
@ -370,8 +371,6 @@ class AppWithNavigationState extends React.Component {
} }
if (AppState.currentState && AppState.currentState.match(/active/)) { if (AppState.currentState && AppState.currentState.match(/active/)) {
// Cleanup blobs for completed files upon app resume to save space
dispatch(doDeleteCompleteBlobs());
if (backgroundPlayEnabled || NativeModules.BackgroundMedia) { if (backgroundPlayEnabled || NativeModules.BackgroundMedia) {
NativeModules.BackgroundMedia.hidePlaybackNotification(); NativeModules.BackgroundMedia.hidePlaybackNotification();
} }

View file

@ -13,6 +13,9 @@ class CategoryList extends React.PureComponent {
<FlatList <FlatList
style={discoverStyle.horizontalScrollContainer} style={discoverStyle.horizontalScrollContainer}
contentContainerStyle={discoverStyle.horizontalScrollPadding} contentContainerStyle={discoverStyle.horizontalScrollPadding}
initialNumToRender={3}
maxToRenderPerBatch={3}
removeClippedSubviews={true}
renderItem={ ({item}) => ( renderItem={ ({item}) => (
<FileItem <FileItem
style={discoverStyle.fileItem} style={discoverStyle.fileItem}

View file

@ -1,11 +1,12 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
doPurchaseUri,
makeSelectFileInfoForUri, makeSelectFileInfoForUri,
makeSelectDownloadingForUri, makeSelectDownloadingForUri,
makeSelectLoadingForUri, makeSelectLoadingForUri,
} from 'lbry-redux'; } from 'lbry-redux';
import { doFetchCostInfoForUri, makeSelectCostInfoForUri } from 'lbryinc'; import { doFetchCostInfoForUri, makeSelectCostInfoForUri } from 'lbryinc';
import { doPurchaseUri, doStartDownload } from 'redux/actions/file'; import { doStartDownload } from 'redux/actions/file';
import FileDownloadButton from './view'; import FileDownloadButton from './view';
const select = (state, props) => ({ const select = (state, props) => ({
@ -16,7 +17,7 @@ const select = (state, props) => ({
}); });
const perform = dispatch => ({ const perform = dispatch => ({
purchaseUri: (uri, failureCallback) => dispatch(doPurchaseUri(uri, null, failureCallback)), purchaseUri: (uri, costInfo, saveFile) => dispatch(doPurchaseUri(uri, costInfo, saveFile)),
restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)), restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)),
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)), fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
}); });

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { NativeModules, Text, View, TouchableOpacity } from 'react-native'; import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
import Button from '../button'; import Button from '../button';
import fileDownloadButtonStyle from '../../styles/fileDownloadButton'; import fileDownloadButtonStyle from 'styles/fileDownloadButton';
class FileDownloadButton extends React.PureComponent { class FileDownloadButton extends React.PureComponent {
componentDidMount() { componentDidMount() {
@ -13,7 +13,7 @@ class FileDownloadButton extends React.PureComponent {
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
//this.checkAvailability(nextProps.uri); //this.checkAvailability(nextProps.uri);
this.restartDownload(nextProps); //this.restartDownload(nextProps);
} }
restartDownload(props) { restartDownload(props) {
@ -46,7 +46,6 @@ class FileDownloadButton extends React.PureComponent {
style, style,
openFile, openFile,
onButtonLayout, onButtonLayout,
onStartDownloadFailed
} = this.props; } = this.props;
if ((fileInfo && !fileInfo.stopped) || loading || downloading) { if ((fileInfo && !fileInfo.stopped) || loading || downloading) {
@ -60,7 +59,7 @@ class FileDownloadButton extends React.PureComponent {
<Text style={fileDownloadButtonStyle.text}>{label}</Text> <Text style={fileDownloadButtonStyle.text}>{label}</Text>
</View> </View>
); );
} else if (fileInfo === null && !downloading) { } else if (!fileInfo && !downloading) {
if (!costInfo) { if (!costInfo) {
return ( return (
<View style={[style, fileDownloadButtonStyle.container]}> <View style={[style, fileDownloadButtonStyle.container]}>
@ -76,7 +75,10 @@ class FileDownloadButton extends React.PureComponent {
if (NativeModules.Firebase) { if (NativeModules.Firebase) {
NativeModules.Firebase.track('purchase_uri', { uri: uri }); NativeModules.Firebase.track('purchase_uri', { uri: uri });
} }
purchaseUri(uri, onStartDownloadFailed); purchaseUri(uri, costInfo, !isPlayable);
if (NativeModules.UtilityModule) {
NativeModules.UtilityModule.checkDownloads();
}
if (isPlayable && onPlay) { if (isPlayable && onPlay) {
this.props.onPlay(); this.props.onPlay();
} }

View file

@ -78,8 +78,10 @@ class FileItem extends React.PureComponent {
isResolvingUri={isResolvingUri} isResolvingUri={isResolvingUri}
style={mediaStyle} /> style={mediaStyle} />
{(!compactView && fileInfo && fileInfo.completed) && <Icon style={discoverStyle.downloadedIcon} solid={true} color={Colors.BrightGreen} name={"folder"} size={16} />} {(!compactView && fileInfo && fileInfo.completed && fileInfo.download_path) &&
{(!compactView && (!fileInfo || !fileInfo.completed)) && <FilePrice uri={uri} style={discoverStyle.filePriceContainer} textStyle={discoverStyle.filePriceText} />} <Icon style={discoverStyle.downloadedIcon} solid={true} color={Colors.NextLbryGreen} name={"folder"} size={16} />}
{(!compactView && (!fileInfo || !fileInfo.completed || !fileInfo.download_path)) &&
<FilePrice uri={uri} style={discoverStyle.filePriceContainer} textStyle={discoverStyle.filePriceText} />}
{!compactView && <View style={isRewardContent ? discoverStyle.rewardTitleContainer : null}> {!compactView && <View style={isRewardContent ? discoverStyle.rewardTitleContainer : null}>
<Text numberOfLines={1} style={[discoverStyle.fileItemName, discoverStyle.rewardTitle]}>{title}</Text> <Text numberOfLines={1} style={[discoverStyle.fileItemName, discoverStyle.rewardTitle]}>{title}</Text>
{isRewardContent && <Icon style={discoverStyle.rewardIcon} name="award" size={14} />} {isRewardContent && <Icon style={discoverStyle.rewardIcon} name="award" size={14} />}

View file

@ -87,7 +87,8 @@ class FileListItem extends React.PureComponent {
resizeMode="cover" resizeMode="cover"
title={(title || name)} title={(title || name)}
thumbnail={thumbnail} /> thumbnail={thumbnail} />
{fileInfo && fileInfo.completed && <Icon style={fileListStyle.downloadedIcon} solid={true} color={Colors.BrightGreen} name={"folder"} size={16} />} {(fileInfo && fileInfo.completed && fileInfo.download_path) &&
<Icon style={fileListStyle.downloadedIcon} solid={true} color={Colors.BrightGreen} name={"folder"} size={16} />}
<View style={fileListStyle.detailsContainer}> <View style={fileListStyle.detailsContainer}>
{featuredResult && <Text style={fileListStyle.featuredUri} numberOfLines={1}>{uri}</Text>} {featuredResult && <Text style={fileListStyle.featuredUri} numberOfLines={1}>{uri}</Text>}
@ -106,7 +107,8 @@ class FileListItem extends React.PureComponent {
}} />} }} />}
<View style={fileListStyle.info}> <View style={fileListStyle.info}>
{fileInfo && <Text style={fileListStyle.infoText}>{this.getStorageForFileInfo(fileInfo)}</Text>} {(fileInfo && !isNaN(fileInfo.written_bytes) && fileInfo.written_bytes > 0) &&
<Text style={fileListStyle.infoText}>{this.getStorageForFileInfo(fileInfo)}</Text>}
<DateTime style={fileListStyle.publishInfo} textStyle={fileListStyle.infoText} timeAgo uri={uri} /> <DateTime style={fileListStyle.publishInfo} textStyle={fileListStyle.infoText} timeAgo uri={uri} />
</View> </View>

View file

@ -108,7 +108,7 @@ class FilePrice extends React.PureComponent {
<CreditAmount <CreditAmount
style={textStyle} style={textStyle}
label={false} label={false}
amount={costInfo.cost} amount={parseFloat(costInfo.cost)}
isEstimate={isEstimate} isEstimate={isEstimate}
showFree showFree
showFullPrice={showFullPrice}>???</CreditAmount> showFullPrice={showFullPrice}>???</CreditAmount>

View file

@ -0,0 +1,4 @@
import { connect } from 'react-redux';
import FileRewardsDriver from './view';
export default connect()(FileRewardsDriver);

View file

@ -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 filePageStyle from 'styles/filePage';
class FileRewardsDriver extends React.PureComponent<Props> {
render() {
const { navigation } = this.props;
return (
<TouchableOpacity style={filePageStyle.rewardDriverCard} onPress={() => navigation.navigate('Rewards')}>
<Icon name="award" size={16} style={filePageStyle.rewardIcon} />
<Text style={filePageStyle.rewardDriverText}>Earn some credits to access this content.</Text>
</TouchableOpacity>
);
}
}
export default FileRewardsDriver;

View file

@ -1,14 +1,18 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SETTINGS, savePosition } from 'lbry-redux'; import { SETTINGS, savePosition } from 'lbry-redux';
import { makeSelectClientSetting } from '../../redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doSetPlayerVisible } from 'redux/actions/drawer';
import { selectIsPlayerVisible } from 'redux/selectors/drawer';
import MediaPlayer from './view'; import MediaPlayer from './view';
const select = state => ({ const select = state => ({
backgroundPlayEnabled: makeSelectClientSetting(SETTINGS.BACKGROUND_PLAY_ENABLED)(state), backgroundPlayEnabled: makeSelectClientSetting(SETTINGS.BACKGROUND_PLAY_ENABLED)(state),
isPlayerVisible: selectIsPlayerVisible(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
savePosition: (claimId, outpoint, position) => dispatch(savePosition(claimId, outpoint, position)), savePosition: (claimId, outpoint, position) => dispatch(savePosition(claimId, outpoint, position)),
setPlayerVisible: () => dispatch(doSetPlayerVisible(true)),
}); });
export default connect(select, perform)(MediaPlayer); export default connect(select, perform)(MediaPlayer);

View file

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import { Lbry } from 'lbry-redux'; import { Lbry } from 'lbry-redux';
import { import {
AppState,
ActivityIndicator,
DeviceEventEmitter, DeviceEventEmitter,
NativeModules, NativeModules,
PanResponder, PanResponder,
@ -9,12 +11,15 @@ import {
ScrollView, ScrollView,
TouchableOpacity TouchableOpacity
} from 'react-native'; } from 'react-native';
import Colors from 'styles/colors';
import FastImage from 'react-native-fast-image' import FastImage from 'react-native-fast-image'
import Video from 'react-native-video'; import Video from 'react-native-video';
import Icon from 'react-native-vector-icons/FontAwesome5'; import Icon from 'react-native-vector-icons/FontAwesome5';
import FileItemMedia from 'component/fileItemMedia'; import FileItemMedia from 'component/fileItemMedia';
import mediaPlayerStyle from 'styles/mediaPlayer'; import mediaPlayerStyle from 'styles/mediaPlayer';
const positionSaveInterval = 10
class MediaPlayer extends React.PureComponent { class MediaPlayer extends React.PureComponent {
static ControlsTimeout = 3000; static ControlsTimeout = 3000;
@ -31,7 +36,9 @@ class MediaPlayer extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
encodedFilePath: null, buffering: false,
backgroundPlayEnabled: false,
autoPaused: false,
rate: 1, rate: 1,
volume: 1, volume: 1,
muted: false, muted: false,
@ -85,6 +92,7 @@ class MediaPlayer extends React.PureComponent {
const { position } = this.props; const { position } = this.props;
if (!isNaN(parseFloat(position)) && position > 0) { if (!isNaN(parseFloat(position)) && position > 0) {
this.video.seek(position); this.video.seek(position);
this.setState({ currentTime: position }, () => this.setSeekerPosition(this.calculateSeekerPosition()));
} }
if (this.props.onMediaLoaded) { if (this.props.onMediaLoaded) {
@ -93,10 +101,13 @@ class MediaPlayer extends React.PureComponent {
} }
onProgress = (data) => { onProgress = (data) => {
const { savePosition, fileInfo } = this.props; const { savePosition, claim } = this.props;
this.setState({ buffering: false, currentTime: data.currentTime });
this.setState({ currentTime: data.currentTime }, () => savePosition(fileInfo.claim_id, fileInfo.outpoint, data.currentTime)); if (data.currentTime > 0 && Math.floor(data.currentTime) % positionSaveInterval === 0) {
const { claim_id: claimId, txid, nout } = claim;
savePosition(claimId, `${txid}:${nout}`, data.currentTime);
}
if (!this.state.seeking) { if (!this.state.seeking) {
this.setSeekerPosition(this.calculateSeekerPosition()); this.setSeekerPosition(this.calculateSeekerPosition());
@ -140,6 +151,11 @@ class MediaPlayer extends React.PureComponent {
} }
togglePlayerControls = () => { togglePlayerControls = () => {
const { setPlayerVisible, isPlayerVisible } = this.props;
if (!isPlayerVisible) {
setPlayerVisible();
}
if (this.state.areControlsVisible) { if (this.state.areControlsVisible) {
this.manualHidePlayerControls(); this.manualHidePlayerControls();
} else { } else {
@ -149,7 +165,14 @@ class MediaPlayer extends React.PureComponent {
togglePlay = () => { togglePlay = () => {
this.showPlayerControls(); this.showPlayerControls();
this.setState({ paused: !this.state.paused }); this.setState({ paused: !this.state.paused }, this.handlePausedState);
}
handlePausedState = () => {
if (!this.state.paused) {
// onProgress will automatically clear this, so it's fine
this.setState({ buffering: true });
}
} }
toggleFullscreenMode = () => { toggleFullscreenMode = () => {
@ -217,7 +240,7 @@ class MediaPlayer extends React.PureComponent {
onPanResponderRelease: (evt, gestureState) => { onPanResponderRelease: (evt, gestureState) => {
const time = this.getCurrentTimeForSeekerPosition(); const time = this.getCurrentTimeForSeekerPosition();
if (time >= this.state.duration) { if (time >= this.state.duration) {
this.setState({ paused: true }); this.setState({ paused: true }, this.handlePausedState);
this.onEnd(); this.onEnd();
} else { } else {
this.seekTo(time); this.seekTo(time);
@ -251,18 +274,29 @@ class MediaPlayer extends React.PureComponent {
this.initSeeker(); this.initSeeker();
} }
componentWillReceiveProps(nextProps) {
const { isPlayerVisible } = nextProps;
if (!isPlayerVisible && !this.state.backgroundPlayEnabled) {
// force pause if the player is not visible and background play is not enabled
this.setState({ paused: true });
}
}
componentDidMount() { componentDidMount() {
const { assignPlayer } = this.props; const { assignPlayer, backgroundPlayEnabled } = this.props;
if (assignPlayer) { if (assignPlayer) {
assignPlayer(this); assignPlayer(this);
} }
this.setState({ backgroundPlayEnabled: !!backgroundPlayEnabled });
this.setSeekerPosition(this.calculateSeekerPosition()); this.setSeekerPosition(this.calculateSeekerPosition());
AppState.addEventListener('change', this.handleAppStateChange);
DeviceEventEmitter.addListener('onBackgroundPlayPressed', this.play); DeviceEventEmitter.addListener('onBackgroundPlayPressed', this.play);
DeviceEventEmitter.addListener('onBackgroundPausePressed', this.pause); DeviceEventEmitter.addListener('onBackgroundPausePressed', this.pause);
} }
componentWillUnmount() { componentWillUnmount() {
AppState.removeEventListener('change', this.handleAppStateChange);
DeviceEventEmitter.removeListener('onBackgroundPlayPressed', this.play); DeviceEventEmitter.removeListener('onBackgroundPlayPressed', this.play);
DeviceEventEmitter.removeListener('onBackgroundPausePressed', this.pause); DeviceEventEmitter.removeListener('onBackgroundPausePressed', this.pause);
this.clearControlsTimeout(); this.clearControlsTimeout();
@ -273,6 +307,26 @@ class MediaPlayer extends React.PureComponent {
} }
} }
handleAppStateChange = () => {
if (AppState.currentState && AppState.currentState.match(/inactive|background/)) {
if (!this.state.backgroundPlayEnabled && !this.state.paused) {
this.setState({ paused: true, autoPaused: true });
}
}
if (AppState.currentState && AppState.currentState.match(/active/)) {
if (!this.state.backgroundPlayEnabled && this.state.autoPaused) {
this.setState({ paused: false, autoPaused: false });
}
}
}
onBuffer = () => {
if (!this.state.paused) {
this.setState({ buffering: true }, () => this.manualHidePlayerControls());
}
}
play = () => { play = () => {
this.setState({ paused: false }, this.updateBackgroundMediaNotification); this.setState({ paused: false }, this.updateBackgroundMediaNotification);
} }
@ -282,6 +336,7 @@ class MediaPlayer extends React.PureComponent {
} }
updateBackgroundMediaNotification = () => { updateBackgroundMediaNotification = () => {
this.handlePausedState();
const { backgroundPlayEnabled } = this.props; const { backgroundPlayEnabled } = this.props;
if (backgroundPlayEnabled) { if (backgroundPlayEnabled) {
if (NativeModules.BackgroundMedia && window.currentMediaInfo) { if (NativeModules.BackgroundMedia && window.currentMediaInfo) {
@ -292,13 +347,19 @@ class MediaPlayer extends React.PureComponent {
} }
renderPlayerControls() { renderPlayerControls() {
const { onBackButtonPressed } = this.props;
if (this.state.areControlsVisible) { if (this.state.areControlsVisible) {
return ( return (
<View style={mediaPlayerStyle.playerControlsContainer}> <View style={mediaPlayerStyle.playerControlsContainer}>
<TouchableOpacity style={mediaPlayerStyle.backButton} onPress={onBackButtonPressed}>
<Icon name={"arrow-left"} size={18} style={mediaPlayerStyle.backButtonIcon} />
</TouchableOpacity>
<TouchableOpacity style={mediaPlayerStyle.playPauseButton} <TouchableOpacity style={mediaPlayerStyle.playPauseButton}
onPress={this.togglePlay}> onPress={this.togglePlay}>
{this.state.paused && <Icon name="play" size={32} color="#ffffff" />} {this.state.paused && <Icon name="play" size={40} color="#ffffff" />}
{!this.state.paused && <Icon name="pause" size={32} color="#ffffff" />} {!this.state.paused && <Icon name="pause" size={40} color="#ffffff" />}
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity style={mediaPlayerStyle.toggleFullscreenButton} onPress={this.toggleFullscreenMode}> <TouchableOpacity style={mediaPlayerStyle.toggleFullscreenButton} onPress={this.toggleFullscreenMode}>
@ -315,18 +376,6 @@ class MediaPlayer extends React.PureComponent {
return null; return null;
} }
getEncodedDownloadPath = (fileInfo) => {
if (this.state.encodedFilePath) {
return this.state.encodedFilePath;
}
const { file_name: fileName } = fileInfo;
const encodedFileName = encodeURIComponent(fileName).replace(/!/g, '%21');
const encodedFilePath = fileInfo.download_path.replace(fileName, encodedFileName);
this.setState({ encodedFilePath });
return encodedFilePath;
}
onSeekerTouchAreaPressed = (evt) => { onSeekerTouchAreaPressed = (evt) => {
if (evt && evt.nativeEvent) { if (evt && evt.nativeEvent) {
const newSeekerPosition = evt.nativeEvent.locationX; const newSeekerPosition = evt.nativeEvent.locationX;
@ -338,8 +387,14 @@ class MediaPlayer extends React.PureComponent {
} }
} }
onTrackingLayout = (evt) => {
this.trackingOffset = evt.nativeEvent.layout.x;
this.seekerWidth = evt.nativeEvent.layout.width;
this.setSeekerPosition(this.calculateSeekerPosition());
}
render() { render() {
const { backgroundPlayEnabled, fileInfo, thumbnail, onLayout, style } = this.props; const { onLayout, source, style, thumbnail } = this.props;
const completedWidth = this.getCurrentTimePercentage() * this.seekerWidth; const completedWidth = this.getCurrentTimePercentage() * this.seekerWidth;
const remainingWidth = this.seekerWidth - completedWidth; const remainingWidth = this.seekerWidth - completedWidth;
let styles = [this.state.fullscreenMode ? mediaPlayerStyle.fullscreenContainer : mediaPlayerStyle.container]; let styles = [this.state.fullscreenMode ? mediaPlayerStyle.fullscreenContainer : mediaPlayerStyle.container];
@ -356,17 +411,27 @@ class MediaPlayer extends React.PureComponent {
return ( return (
<View style={styles} onLayout={onLayout}> <View style={styles} onLayout={onLayout}>
<Video source={{ uri: 'file:///' + this.getEncodedDownloadPath(fileInfo) }} <Video source={{
uri: source,
headers: {
"Save-Data": "on",
"Accept": "*/*"
}
}}
bufferConfig={{ minBufferMs: 3000, maxBufferMs: 60000, bufferForPlaybackMs: 3000, bufferForPlaybackAfterRebufferMs: 3000 }}
ref={(ref: Video) => { this.video = ref; }} ref={(ref: Video) => { this.video = ref; }}
resizeMode={this.state.resizeMode} resizeMode={this.state.resizeMode}
playInBackground={backgroundPlayEnabled} playInBackground={this.state.backgroundPlayEnabled}
style={mediaPlayerStyle.player} style={mediaPlayerStyle.player}
rate={this.state.rate} rate={this.state.rate}
volume={this.state.volume} volume={this.state.volume}
paused={this.state.paused} paused={this.state.paused}
onLoad={this.onLoad} onLoad={this.onLoad}
onBuffer={this.onBuffer}
onProgress={this.onProgress} onProgress={this.onProgress}
onEnd={this.onEnd} onEnd={this.onEnd}
onError={this.onError}
minLoadRetryCount={999}
/> />
{this.state.firstPlay && thumbnail && thumbnail.trim().length > 0 && {this.state.firstPlay && thumbnail && thumbnail.trim().length > 0 &&
@ -381,16 +446,18 @@ class MediaPlayer extends React.PureComponent {
</TouchableOpacity> </TouchableOpacity>
{(!this.state.fullscreenMode || (this.state.fullscreenMode && this.state.areControlsVisible)) && {(!this.state.fullscreenMode || (this.state.fullscreenMode && this.state.areControlsVisible)) &&
<View style={trackingStyle} onLayout={(evt) => { <View style={trackingStyle} onLayout={this.onTrackingLayout}>
this.trackingOffset = evt.nativeEvent.layout.x;
this.seekerWidth = evt.nativeEvent.layout.width;
}}>
<View style={mediaPlayerStyle.progress}> <View style={mediaPlayerStyle.progress}>
<View style={[mediaPlayerStyle.innerProgressCompleted, { width: completedWidth }]} /> <View style={[mediaPlayerStyle.innerProgressCompleted, { width: completedWidth }]} />
<View style={[mediaPlayerStyle.innerProgressRemaining, { width: remainingWidth }]} /> <View style={[mediaPlayerStyle.innerProgressRemaining, { width: remainingWidth }]} />
</View> </View>
</View>} </View>}
{this.state.buffering &&
<View style={mediaPlayerStyle.loadingContainer}>
<ActivityIndicator color={Colors.LbryGreen} size="large" />
</View>}
{this.state.areControlsVisible && {this.state.areControlsVisible &&
<View style={{ left: this.getTrackingOffset(), width: this.seekerWidth }}> <View style={{ left: this.getTrackingOffset(), width: this.seekerWidth }}>
<View style={[mediaPlayerStyle.seekerHandle, <View style={[mediaPlayerStyle.seekerHandle,

View file

@ -9,8 +9,8 @@ import {
View View
} from 'react-native'; } from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome5'; import Icon from 'react-native-vector-icons/FontAwesome5';
import NavigationButton from '../navigationButton'; import NavigationButton from 'component/navigationButton';
import pageHeaderStyle from '../../styles/pageHeader'; import pageHeaderStyle from 'styles/pageHeader';
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56; const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
const AnimatedText = Animated.Text; const AnimatedText = Animated.Text;

View file

@ -21,7 +21,7 @@ class RewardEnrolment extends React.Component {
onEnrollPressed = () => { onEnrollPressed = () => {
const { navigation } = this.props; const { navigation } = this.props;
navigation.navigate({ routeName: 'Verification' }) navigation.navigate({ routeName: 'Verification', key: 'verification', params: { syncFlow: false }});
} }
render() { render() {

View file

@ -66,6 +66,10 @@ class StorageStatsCard extends React.PureComponent {
} }
render() { render() {
if (this.state.totalBytes == 0) {
return null;
}
return ( return (
<View style={storageStatsStyle.card}> <View style={storageStatsStyle.card}>
<View style={[storageStatsStyle.row, storageStatsStyle.totalSizeContainer]}> <View style={[storageStatsStyle.row, storageStatsStyle.totalSizeContainer]}>

View file

@ -9,7 +9,7 @@ import discoverStyle from 'styles/discover';
import uriBarStyle from 'styles/uriBar'; import uriBarStyle from 'styles/uriBar';
class UriBar extends React.PureComponent { class UriBar extends React.PureComponent {
static INPUT_TIMEOUT = 1000; // 1 second static INPUT_TIMEOUT = 2500; // 2.5 seconds
textInput = null; textInput = null;

View file

@ -32,7 +32,7 @@ class WalletAddress extends React.PureComponent<Props> {
<Address address={receiveAddress} style={walletStyle.bottomMarginSmall} /> <Address address={receiveAddress} style={walletStyle.bottomMarginSmall} />
<Button style={[walletStyle.button, walletStyle.bottomMarginLarge]} <Button style={[walletStyle.button, walletStyle.bottomMarginLarge]}
icon={'sync'} icon={'sync'}
text={'Get New Address'} text={'Get new address'}
onPress={getNewAddress} onPress={getNewAddress}
disabled={gettingNewAddress} disabled={gettingNewAddress}
/> />

View file

@ -1,6 +1,4 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import WalletRewardsDriver from './view'; import WalletRewardsDriver from './view';
const select = state => ({}); export default connect()(WalletRewardsDriver);
export default connect(select, null)(WalletRewardsDriver);

View file

@ -108,6 +108,7 @@ class WalletSend extends React.PureComponent<Props> {
<TextInput ref={ref => this.amountInput = ref} <TextInput ref={ref => this.amountInput = ref}
onChangeText={value => this.setState({amount: value})} onChangeText={value => this.setState({amount: value})}
keyboardType={'numeric'} keyboardType={'numeric'}
placeholder={'0'}
value={this.state.amount} value={this.state.amount}
style={[walletStyle.input, walletStyle.amountInput]} /> style={[walletStyle.input, walletStyle.amountInput]} />
<Text style={[walletStyle.text, walletStyle.currency]}>LBC</Text> <Text style={[walletStyle.text, walletStyle.currency]}>LBC</Text>

View file

@ -0,0 +1,10 @@
import { connect } from 'react-redux';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import Constants from 'constants';
import WalletSyncDriver from './view';
const select = state => ({
deviceWalletSynced: makeSelectClientSetting(Constants.SETTING_DEVICE_WALLET_SYNCED)(state),
});
export default connect(select, null)(WalletSyncDriver);

View file

@ -0,0 +1,29 @@
import React from 'react';
import { Text, View } from 'react-native';
import Button from 'component/button';
import Link from 'component/link';
import walletStyle from 'styles/wallet';
class WalletSyncDriver extends React.PureComponent<Props> {
onEnableSyncPressed = () => {
const { navigation } = this.props;
navigation.navigate({ routeName: 'Verification', key: 'verification', params: { syncFlow: true } });
}
render() {
const { deviceWalletSynced } = this.props;
return (
<View style={walletStyle.syncDriverCard}>
<Text style={walletStyle.syncDriverTitle}>Wallet sync is {deviceWalletSynced ? 'on' : 'off'}.</Text>
{!deviceWalletSynced &&
<View style={walletStyle.actionRow}>
<Button style={walletStyle.enrollButton} theme={"light"} text={"Enable"} onPress={this.onEnableSyncPressed} />
<Link text="Manual backup" href="https://lbry.com/faq/how-to-backup-wallet#android" style={walletStyle.syncDriverText} />
</View>}
</View>
);
}
}
export default WalletSyncDriver;

View file

@ -25,12 +25,14 @@ const Constants = {
SETTING_RATING_REMINDER_DISABLED: "ratingReminderDisabled", SETTING_RATING_REMINDER_DISABLED: "ratingReminderDisabled",
SETTING_BACKUP_DISMISSED: "backupDismissed", SETTING_BACKUP_DISMISSED: "backupDismissed",
SETTING_REWARDS_NOT_INTERESTED: "rewardsNotInterested", SETTING_REWARDS_NOT_INTERESTED: "rewardsNotInterested",
SETTING_DEVICE_WALLET_SYNCED: "deviceWalletSynced",
ACTION_DELETE_COMPLETED_BLOBS: "DELETE_COMPLETED_BLOBS", ACTION_DELETE_COMPLETED_BLOBS: "DELETE_COMPLETED_BLOBS",
ACTION_FIRST_RUN_PAGE_CHANGED: "FIRST_RUN_PAGE_CHANGED", ACTION_FIRST_RUN_PAGE_CHANGED: "FIRST_RUN_PAGE_CHANGED",
ACTION_PUSH_DRAWER_STACK: "PUSH_DRAWER_STACK", ACTION_PUSH_DRAWER_STACK: "PUSH_DRAWER_STACK",
ACTION_POP_DRAWER_STACK: "POP_DRAWER_STACK", ACTION_POP_DRAWER_STACK: "POP_DRAWER_STACK",
ACTION_SET_PLAYER_VISIBLE: "SET_PLAYER_VISIBLE",
PAGE_REWARDS: "rewards", PAGE_REWARDS: "rewards",
PAGE_SETTINGS: "settings", PAGE_SETTINGS: "settings",

View file

@ -3,7 +3,6 @@ import { setJSExceptionHandler } from 'react-native-exception-handler';
import { Provider, connect } from 'react-redux'; import { Provider, connect } from 'react-redux';
import { import {
AppRegistry, AppRegistry,
AppState,
Text, Text,
View, View,
NativeModules NativeModules
@ -12,6 +11,7 @@ import {
Lbry, Lbry,
claimsReducer, claimsReducer,
contentReducer, contentReducer,
fileReducer,
fileInfoReducer, fileInfoReducer,
notificationsReducer, notificationsReducer,
searchReducer, searchReducer,
@ -28,7 +28,6 @@ import {
userReducer userReducer
} from 'lbryinc'; } from 'lbryinc';
import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import { createLogger } from 'redux-logger';
import { AppNavigator } from 'component/AppNavigator'; import { AppNavigator } from 'component/AppNavigator';
import { persistStore, autoRehydrate } from 'redux-persist'; import { persistStore, autoRehydrate } from 'redux-persist';
import AppWithNavigationState, { reactNavigationMiddleware } from './component/AppNavigator'; import AppWithNavigationState, { reactNavigationMiddleware } from './component/AppNavigator';
@ -44,8 +43,7 @@ import thunk from 'redux-thunk';
const globalExceptionHandler = (error, isFatal) => { const globalExceptionHandler = (error, isFatal) => {
if (error && NativeModules.Firebase) { if (error && NativeModules.Firebase) {
console.log(error); NativeModules.Firebase.logException(isFatal, error.message ? error.message : "No message", JSON.stringify(error));
NativeModules.Firebase.logException(isFatal, error.message ? error.message : "No message", error);
} }
}; };
setJSExceptionHandler(globalExceptionHandler, true); setJSExceptionHandler(globalExceptionHandler, true);
@ -93,6 +91,7 @@ const reducers = combineReducers({
content: contentReducer, content: contentReducer,
costInfo: costInfoReducer, costInfo: costInfoReducer,
drawer: drawerReducer, drawer: drawerReducer,
file: fileReducer,
fileInfo: fileInfoReducer, fileInfo: fileInfoReducer,
homepage: homepageReducer, homepage: homepageReducer,
nav: navigatorReducer, nav: navigatorReducer,
@ -107,7 +106,6 @@ const reducers = combineReducers({
}); });
const bulkThunk = createBulkThunkMiddleware(); const bulkThunk = createBulkThunkMiddleware();
const logger = createLogger({ collapsed: true });
const middleware = [thunk, bulkThunk, reactNavigationMiddleware]; const middleware = [thunk, bulkThunk, reactNavigationMiddleware];
// eslint-disable-next-line no-underscore-dangle // eslint-disable-next-line no-underscore-dangle

View file

@ -1,7 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doToast } from 'lbry-redux'; import { doToast } from 'lbry-redux';
import { doFetchAccessToken, selectAccessToken, selectUserEmail } from 'lbryinc'; import { doFetchAccessToken, selectAccessToken, selectUserEmail } from 'lbryinc';
import { doPushDrawerStack, doPopDrawerStack } from 'redux/actions/drawer'; import { doPushDrawerStack, doPopDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
import { selectDrawerStack } from 'redux/selectors/drawer'; import { selectDrawerStack } from 'redux/selectors/drawer';
import AboutPage from './view'; import AboutPage from './view';
import Constants from 'constants'; import Constants from 'constants';
@ -17,6 +17,7 @@ const perform = dispatch => ({
notify: data => dispatch(doToast(data)), notify: data => dispatch(doToast(data)),
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_ABOUT)), pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_ABOUT)),
popDrawerStack: () => dispatch(doPopDrawerStack()), popDrawerStack: () => dispatch(doPopDrawerStack()),
setPlayerVisible: () => dispatch(doSetPlayerVisible(false)),
}); });
export default connect(select, perform)(AboutPage); export default connect(select, perform)(AboutPage);

View file

@ -14,7 +14,10 @@ class AboutPage extends React.PureComponent {
}; };
componentDidMount() { componentDidMount() {
this.props.pushDrawerStack(); const { pushDrawerStack, setPlayerVisible } = this.props;
pushDrawerStack();
setPlayerVisible();
if (NativeModules.VersionInfo) { if (NativeModules.VersionInfo) {
NativeModules.VersionInfo.getAppVersion().then(version => { NativeModules.VersionInfo.getAppVersion().then(version => {
this.setState({appVersion: version}); this.setState({appVersion: version});

View file

@ -178,12 +178,10 @@ class ChannelPage extends React.PureComponent {
} }
} }
return ( return (
<View style={channelPageStyle.container}> <View style={channelPageStyle.container}>
<UriBar value={uri} navigation={navigation} /> <UriBar value={uri} navigation={navigation} />
<View style={channelPageStyle.viewContainer}> <View style={channelPageStyle.viewContainer}>
<View style={channelPageStyle.cover}> <View style={channelPageStyle.cover}>
<Image <Image

View file

@ -174,6 +174,9 @@ class DiscoverPage extends React.PureComponent {
(<SectionList (<SectionList
style={discoverStyle.scrollContainer} style={discoverStyle.scrollContainer}
contentContainerStyle={discoverStyle.scrollPadding} contentContainerStyle={discoverStyle.scrollPadding}
initialNumToRender={4}
maxToRenderPerBatch={4}
removeClippedSubviews={true}
renderItem={ ({item, index, section}) => ( renderItem={ ({item, index, section}) => (
<CategoryList <CategoryList
key={item} key={item}

View file

@ -5,7 +5,7 @@ import {
selectMyClaimsWithoutChannels, selectMyClaimsWithoutChannels,
selectIsFetchingFileList, selectIsFetchingFileList,
} from 'lbry-redux'; } from 'lbry-redux';
import { doPushDrawerStack } from 'redux/actions/drawer'; import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
import Constants from 'constants'; import Constants from 'constants';
import DownloadsPage from './view'; import DownloadsPage from './view';
@ -17,7 +17,8 @@ const select = (state) => ({
const perform = dispatch => ({ const perform = dispatch => ({
fileList: () => dispatch(doFileList()), fileList: () => dispatch(doFileList()),
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_MY_LBRY)) pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_MY_LBRY)),
setPlayerVisible: () => dispatch(doSetPlayerVisible(false))
}); });
export default connect(select, perform)(DownloadsPage); export default connect(select, perform)(DownloadsPage);

View file

@ -25,8 +25,9 @@ class DownloadsPage extends React.PureComponent {
}; };
componentDidMount() { componentDidMount() {
const { fileList, pushDrawerStack } = this.props; const { fileList, pushDrawerStack, setPlayerVisible } = this.props;
pushDrawerStack(); pushDrawerStack();
setPlayerVisible();
fileList(); fileList();
} }
@ -39,7 +40,7 @@ class DownloadsPage extends React.PureComponent {
<UriBar navigation={navigation} /> <UriBar navigation={navigation} />
{!fetching && !hasDownloads && {!fetching && !hasDownloads &&
<View style={downloadsStyle.busyContainer}> <View style={downloadsStyle.busyContainer}>
<Text style={downloadsStyle.noDownloadsText}>You have not downloaded anything from LBRY yet.</Text> <Text style={downloadsStyle.noDownloadsText}>You have not watched or downloaded any content from LBRY yet.</Text>
</View>} </View>}
{fetching && !hasDownloads && {fetching && !hasDownloads &&
<View style={downloadsStyle.busyContainer}> <View style={downloadsStyle.busyContainer}>

View file

@ -1,6 +1,8 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
doFetchFileInfo, doFetchFileInfo,
doPurchaseUri,
doDeletePurchasedUri,
doResolveUri, doResolveUri,
doSendTip, doSendTip,
doToast, doToast,
@ -11,9 +13,13 @@ import {
makeSelectContentPositionForUri, makeSelectContentPositionForUri,
makeSelectContentTypeForUri, makeSelectContentTypeForUri,
makeSelectMetadataForUri, makeSelectMetadataForUri,
makeSelectStreamingUrlForUri,
makeSelectThumbnailForUri, makeSelectThumbnailForUri,
makeSelectTitleForUri, makeSelectTitleForUri,
selectBalance, selectBalance,
selectPurchasedUris,
selectFailedPurchaseUris,
selectPurchaseUriErrorMessage,
} from 'lbry-redux'; } from 'lbry-redux';
import { import {
doFetchCostInfoForUri, doFetchCostInfoForUri,
@ -21,7 +27,15 @@ import {
selectRewardContentClaimIds, selectRewardContentClaimIds,
selectBlackListedOutpoints selectBlackListedOutpoints
} from 'lbryinc'; } from 'lbryinc';
import { doDeleteFile, doPurchaseUri, doStopDownloadingFile } from 'redux/actions/file'; import {
doStartDownload,
doUpdateDownload,
doCompleteDownload,
doDeleteFile,
doStopDownloadingFile
} from 'redux/actions/file';
import { doPopDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
import { selectDrawerStack } from 'redux/selectors/drawer';
import FilePage from './view'; import FilePage from './view';
const select = (state, props) => { const select = (state, props) => {
@ -30,6 +44,7 @@ const select = (state, props) => {
balance: selectBalance(state), balance: selectBalance(state),
blackListedOutpoints: selectBlackListedOutpoints(state), blackListedOutpoints: selectBlackListedOutpoints(state),
claim: makeSelectClaimForUri(selectProps.uri)(state), claim: makeSelectClaimForUri(selectProps.uri)(state),
drawerStack: selectDrawerStack(state),
isResolvingUri: makeSelectIsUriResolving(selectProps.uri)(state), isResolvingUri: makeSelectIsUriResolving(selectProps.uri)(state),
contentType: makeSelectContentTypeForUri(selectProps.uri)(state), contentType: makeSelectContentTypeForUri(selectProps.uri)(state),
costInfo: makeSelectCostInfoForUri(selectProps.uri)(state), costInfo: makeSelectCostInfoForUri(selectProps.uri)(state),
@ -40,6 +55,10 @@ const select = (state, props) => {
rewardedContentClaimIds: selectRewardContentClaimIds(state, selectProps), rewardedContentClaimIds: selectRewardContentClaimIds(state, selectProps),
channelUri: makeSelectChannelForClaimUri(selectProps.uri, true)(state), channelUri: makeSelectChannelForClaimUri(selectProps.uri, true)(state),
position: makeSelectContentPositionForUri(selectProps.uri)(state), position: makeSelectContentPositionForUri(selectProps.uri)(state),
purchasedUris: selectPurchasedUris(state),
failedPurchaseUris: selectFailedPurchaseUris(state),
purchaseUriErrorMessage: selectPurchaseUriErrorMessage(state),
streamingUrl: makeSelectStreamingUrlForUri(selectProps.uri)(state),
thumbnail: makeSelectThumbnailForUri(selectProps.uri)(state), thumbnail: makeSelectThumbnailForUri(selectProps.uri)(state),
title: makeSelectTitleForUri(selectProps.uri)(state), title: makeSelectTitleForUri(selectProps.uri)(state),
}; };
@ -52,10 +71,16 @@ const perform = dispatch => ({
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)), fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)), fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
notify: data => dispatch(doToast(data)), notify: data => dispatch(doToast(data)),
purchaseUri: (uri, failureCallback) => dispatch(doPurchaseUri(uri, null, failureCallback)), popDrawerStack: () => dispatch(doPopDrawerStack()),
purchaseUri: (uri, costInfo, saveFile) => dispatch(doPurchaseUri(uri, costInfo, saveFile)),
deletePurchasedUri: uri => dispatch(doDeletePurchasedUri(uri)),
resolveUri: uri => dispatch(doResolveUri(uri)), resolveUri: uri => dispatch(doResolveUri(uri)),
sendTip: (amount, claimId, uri, successCallback, errorCallback) => dispatch(doSendTip(amount, claimId, uri, successCallback, errorCallback)), sendTip: (amount, claimId, uri, successCallback, errorCallback) => dispatch(doSendTip(amount, claimId, uri, successCallback, errorCallback)),
setPlayerVisible: () => dispatch(doSetPlayerVisible(true)),
stopDownload: (uri, fileInfo) => dispatch(doStopDownloadingFile(uri, fileInfo)), stopDownload: (uri, fileInfo) => dispatch(doStopDownloadingFile(uri, fileInfo)),
startDownload: (uri, outpoint, fileInfo) => dispatch(doStartDownload(uri, outpoint, fileInfo)),
updateDownload: (uri, outpoint, fileInfo, progress) => dispatch(doUpdateDownload(uri, outpoint, fileInfo, progress)),
completeDownload: (uri, outpoint, fileInfo) => dispatch(doCompleteDownload(uri, outpoint, fileInfo)),
}); });
export default connect(select, perform)(FilePage); export default connect(select, perform)(FilePage);

View file

@ -4,6 +4,7 @@ import { Lbryio } from 'lbryinc';
import { import {
ActivityIndicator, ActivityIndicator,
Alert, Alert,
DeviceEventEmitter,
Dimensions, Dimensions,
NativeModules, NativeModules,
ScrollView, ScrollView,
@ -11,11 +12,13 @@ import {
StyleSheet, StyleSheet,
Text, Text,
TextInput, TextInput,
TouchableOpacity,
TouchableWithoutFeedback, TouchableWithoutFeedback,
View, View,
WebView WebView
} from 'react-native'; } from 'react-native';
import { navigateToUri } from 'utils/helper'; import { NavigationEvents } from 'react-navigation';
import { navigateBack, navigateToUri } from 'utils/helper';
import Icon from 'react-native-vector-icons/FontAwesome5'; import Icon from 'react-native-vector-icons/FontAwesome5';
import ImageViewer from 'react-native-image-zoom-viewer'; import ImageViewer from 'react-native-image-zoom-viewer';
import Button from 'component/button'; import Button from 'component/button';
@ -33,6 +36,7 @@ import SubscribeButton from 'component/subscribeButton';
import SubscribeNotificationButton from 'component/subscribeNotificationButton'; import SubscribeNotificationButton from 'component/subscribeNotificationButton';
import UriBar from 'component/uriBar'; import UriBar from 'component/uriBar';
import Video from 'react-native-video'; import Video from 'react-native-video';
import FileRewardsDriver from 'component/fileRewardsDriver';
import filePageStyle from 'styles/filePage'; import filePageStyle from 'styles/filePage';
import uriBarStyle from 'styles/uriBar'; import uriBarStyle from 'styles/uriBar';
@ -72,14 +76,19 @@ class FilePage extends React.PureComponent {
tipAmount: null, tipAmount: null,
uri: null, uri: null,
uriVars: null, uriVars: null,
stopDownloadConfirmed: false stopDownloadConfirmed: false,
streamingMode: false
}; };
} }
componentDidMount() { componentDidMount() {
StatusBar.setHidden(false); StatusBar.setHidden(false);
const { isResolvingUri, resolveUri, navigation } = this.props; DeviceEventEmitter.addListener('onDownloadStarted', this.handleDownloadStarted);
DeviceEventEmitter.addListener('onDownloadUpdated', this.handleDownloadUpdated);
DeviceEventEmitter.addListener('onDownloadCompleted', this.handleDownloadCompleted);
const { fileInfo, isResolvingUri, resolveUri, navigation } = this.props;
const { uri, uriVars } = navigation.state.params; const { uri, uriVars } = navigation.state.params;
this.setState({ uri, uriVars }); this.setState({ uri, uriVars });
@ -96,8 +105,46 @@ class FilePage extends React.PureComponent {
} }
} }
componentWillReceiveProps(nextProps) {
const {
claim,
failedPurchaseUris: prevFailedPurchaseUris,
purchasedUris: prevPurchasedUris,
navigation,
contentType,
notify
} = this.props;
const { uri } = navigation.state.params;
const { failedPurchaseUris, fileInfo, purchasedUris, purchaseUriErrorMessage, streamingUrl } = nextProps;
if (failedPurchaseUris.includes(uri) && !purchasedUris.includes(uri)) {
if (purchaseUriErrorMessage && purchaseUriErrorMessage.trim().length > 0) {
notify({ message: purchaseUriErrorMessage, isError: true });
}
this.setState({ downloadPressed: false, fileViewLogged: false, mediaLoaded: false });
}
const mediaType = Lbry.getMediaType(contentType);
const isPlayable = mediaType === 'video' || mediaType === 'audio';
if (prevPurchasedUris.length != purchasedUris.length && NativeModules.UtilityModule) {
if (purchasedUris.includes(uri)) {
const { nout, txid } = claim;
const outpoint = `${txid}:${nout}`;
NativeModules.UtilityModule.queueDownload(outpoint);
}
NativeModules.UtilityModule.checkDownloads();
}
if (!this.state.streamingMode && isPlayable) {
if (streamingUrl) {
this.setState({ streamingMode: true, currentStreamUrl: streamingUrl });
} else if (fileInfo && fileInfo.streaming_url) {
this.setState({ streamingMode: true, currentStreamUrl: fileInfo.streaming_url });
}
}
}
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
this.fetchFileInfo(this.props);
const { claim, contentType, fileInfo, isResolvingUri, resolveUri, navigation } = this.props; const { claim, contentType, fileInfo, isResolvingUri, resolveUri, navigation } = this.props;
const { uri } = this.state; const { uri } = this.state;
if (!isResolvingUri && claim === undefined && uri) { if (!isResolvingUri && claim === undefined && uri) {
@ -116,12 +163,11 @@ class FilePage extends React.PureComponent {
const prevFileInfo = prevProps.fileInfo; const prevFileInfo = prevProps.fileInfo;
if (!prevFileInfo && fileInfo) { if (!prevFileInfo && fileInfo) {
// started downloading
const mediaType = Lbry.getMediaType(contentType); const mediaType = Lbry.getMediaType(contentType);
const isPlayable = mediaType === 'video' || mediaType === 'audio'; const isPlayable = mediaType === 'video' || mediaType === 'audio';
// If the media is playable, file/view will be done in onPlaybackStarted // If the media is playable, file/view will be done in onPlaybackStarted
if (!isPlayable && !this.state.fileViewLogged) { if (!isPlayable && !this.state.fileViewLogged) {
this.logFileView(uri, fileInfo); this.logFileView(uri, claim);
} }
} }
} }
@ -161,7 +207,7 @@ class FilePage extends React.PureComponent {
} }
onDeletePressed = () => { onDeletePressed = () => {
const { deleteFile, fileInfo } = this.props; const { claim, deleteFile, deletePurchasedUri, fileInfo, navigation } = this.props;
Alert.alert( Alert.alert(
'Delete file', 'Delete file',
@ -169,7 +215,12 @@ class FilePage extends React.PureComponent {
[ [
{ text: 'No' }, { text: 'No' },
{ text: 'Yes', onPress: () => { { text: 'Yes', onPress: () => {
deleteFile(fileInfo.outpoint, true); const { uri } = navigation.state.params;
deleteFile(`${claim.txid}:${claim.nout}`, true);
deletePurchasedUri(uri);
if (NativeModules.UtilityModule) {
NativeModules.UtilityModule.deleteDownload(uri);
}
this.setState({ this.setState({
downloadPressed: false, downloadPressed: false,
fileViewLogged: false, fileViewLogged: false,
@ -183,7 +234,7 @@ class FilePage extends React.PureComponent {
} }
onStopDownloadPressed = () => { onStopDownloadPressed = () => {
const { fileInfo, navigation, notify, stopDownload } = this.props; const { deletePurchasedUri, fileInfo, navigation, notify, stopDownload } = this.props;
Alert.alert( Alert.alert(
'Stop download', 'Stop download',
@ -191,7 +242,12 @@ class FilePage extends React.PureComponent {
[ [
{ text: 'No' }, { text: 'No' },
{ text: 'Yes', onPress: () => { { text: 'Yes', onPress: () => {
stopDownload(navigation.state.params.uri, fileInfo); const { uri } = navigation.state.params;
stopDownload(uri, fileInfo);
deletePurchasedUri(uri);
if (NativeModules.UtilityModule) {
NativeModules.UtilityModule.deleteDownload(uri);
}
this.setState({ this.setState({
downloadPressed: false, downloadPressed: false,
fileViewLogged: false, fileViewLogged: false,
@ -224,6 +280,28 @@ class FilePage extends React.PureComponent {
window.currentMediaInfo = null; window.currentMediaInfo = null;
} }
window.player = null; window.player = null;
DeviceEventEmitter.removeListener('onDownloadStarted', this.handleDownloadStarted);
DeviceEventEmitter.removeListener('onDownloadUpdated', this.handleDownloadUpdated);
DeviceEventEmitter.removeListener('onDownloadCompleted', this.handleDownloadCompleted);
}
handleDownloadStarted = (evt) => {
const { startDownload } = this.props;
const { uri, outpoint, fileInfo } = evt;
startDownload(uri, outpoint, fileInfo);
}
handleDownloadUpdated = (evt) => {
const { updateDownload } = this.props;
const { uri, outpoint, fileInfo, progress } = evt;
updateDownload(uri, outpoint, fileInfo, progress);
}
handleDownloadCompleted = (evt) => {
const { completeDownload } = this.props;
const { uri, outpoint, fileInfo } = evt;
completeDownload(uri, outpoint, fileInfo);
} }
localUriForFileInfo = (fileInfo) => { localUriForFileInfo = (fileInfo) => {
@ -233,6 +311,33 @@ class FilePage extends React.PureComponent {
return 'file:///' + fileInfo.download_path; return 'file:///' + fileInfo.download_path;
} }
playerUriForFileInfo = (fileInfo) => {
const { streamingUrl } = this.props;
if (streamingUrl) {
return streamingUrl;
}
if (this.state.currentStreamUrl) {
return this.state.currentStreamUrl;
}
if (fileInfo && fileInfo.download_path) {
return this.getEncodedDownloadPath(fileInfo);
}
return null;
}
getEncodedDownloadPath = (fileInfo) => {
if (this.state.encodedFilePath) {
return this.state.encodedFilePath;
}
const { file_name: fileName } = fileInfo;
const encodedFileName = encodeURIComponent(fileName).replace(/!/g, '%21');
const encodedFilePath = fileInfo.download_path.replace(fileName, encodedFileName);
return encodedFilePath;
}
linkify = (text) => { linkify = (text) => {
let linkifiedContent = []; let linkifiedContent = [];
let lines = text.split(/\n/g); let lines = text.split(/\n/g);
@ -321,8 +426,13 @@ class FilePage extends React.PureComponent {
} }
} }
logFileView = (uri, fileInfo, timeToStart) => { logFileView = (uri, claim, timeToStart) => {
const { outpoint, claim_id: claimId } = fileInfo; if (!claim) {
return;
}
const { nout, claim_id: claimId, txid } = claim;
const outpoint = `${txid}:${nout}`;
const params = { const params = {
uri, uri,
outpoint, outpoint,
@ -351,23 +461,30 @@ class FilePage extends React.PureComponent {
sendTip(tipAmount, claim.claim_id, uri, () => { this.setState({ tipAmount: 0, showTipView: false }) }); sendTip(tipAmount, claim.claim_id, uri, () => { this.setState({ tipAmount: 0, showTipView: false }) });
} }
startDownloadFailed = () => {
this.startTime = null;
setTimeout(() => {
this.setState({ downloadPressed: false, fileViewLogged: false, mediaLoaded: false });
}, 500);
}
renderTags = (tags) => { renderTags = (tags) => {
return tags.map((tag, i) => ( return tags.map((tag, i) => (
<Text style={filePageStyle.tagItem} key={`${tag}-${i}`}>{tag}</Text> <Text style={filePageStyle.tagItem} key={`${tag}-${i}`}>{tag}</Text>
)); ));
} }
onFileDownloadButtonPlayed = () => {
const { setPlayerVisible } = this.props;
this.startTime = Date.now();
this.setState({ downloadPressed: true, autoPlayMedia: true, stopDownloadConfirmed: false });
setPlayerVisible();
}
onBackButtonPressed = () => {
const { navigation, drawerStack, popDrawerStack } = this.props;
navigateBack(navigation, drawerStack, popDrawerStack);
}
render() { render() {
const { const {
balance,
claim, claim,
channelUri, channelUri,
costInfo,
fileInfo, fileInfo,
metadata, metadata,
contentType, contentType,
@ -442,7 +559,7 @@ class FilePage extends React.PureComponent {
const mediaType = Lbry.getMediaType(contentType); const mediaType = Lbry.getMediaType(contentType);
const isPlayable = mediaType === 'video' || mediaType === 'audio'; const isPlayable = mediaType === 'video' || mediaType === 'audio';
const { height, channel_name: channelName, value } = claim; const { height, channel_name: channelName, value } = claim;
const showActions = !this.state.fullscreenMode && !this.state.showImageViewer && !this.state.showWebView; const showActions = !this.state.streamingMode && !this.state.fullscreenMode && !this.state.showImageViewer && !this.state.showWebView;
const showFileActions = (completed || (fileInfo && !fileInfo.stopped && fileInfo.written_bytes < fileInfo.total_bytes)); const showFileActions = (completed || (fileInfo && !fileInfo.stopped && fileInfo.written_bytes < fileInfo.total_bytes));
const channelClaimId = claim && claim.signing_channel && claim.signing_channel.claim_id; const channelClaimId = claim && claim.signing_channel && claim.signing_channel.claim_id;
const canSendTip = this.state.tipAmount > 0; const canSendTip = this.state.tipAmount > 0;
@ -454,8 +571,8 @@ class FilePage extends React.PureComponent {
const playerBgStyle = [filePageStyle.playerBackground, filePageStyle.containedPlayerBackground]; const playerBgStyle = [filePageStyle.playerBackground, filePageStyle.containedPlayerBackground];
const fsPlayerBgStyle = [filePageStyle.playerBackground, filePageStyle.fullscreenPlayerBackground]; const fsPlayerBgStyle = [filePageStyle.playerBackground, filePageStyle.fullscreenPlayerBackground];
// at least 2MB (or the full download) before media can be loaded // at least 2MB (or the full download) before media can be loaded
const canLoadMedia = fileInfo && const canLoadMedia = (this.state.streamingMode) || (fileInfo &&
(fileInfo.written_bytes >= 2097152 || fileInfo.written_bytes == fileInfo.total_bytes); // 2MB = 1024*1024*2 (fileInfo.written_bytes >= 2097152 || fileInfo.written_bytes == fileInfo.total_bytes)); // 2MB = 1024*1024*2
const isViewable = (mediaType === 'image' || mediaType === 'text'); const isViewable = (mediaType === 'image' || mediaType === 'text');
const isWebViewable = mediaType === 'text'; const isWebViewable = mediaType === 'text';
const canOpen = isViewable && completed; const canOpen = isViewable && completed;
@ -485,7 +602,10 @@ class FilePage extends React.PureComponent {
if (fileInfo && !this.state.autoDownloadStarted && this.state.uriVars && 'true' === this.state.uriVars.download) { if (fileInfo && !this.state.autoDownloadStarted && this.state.uriVars && 'true' === this.state.uriVars.download) {
this.setState({ autoDownloadStarted: true }, () => { this.setState({ autoDownloadStarted: true }, () => {
purchaseUri(uri, this.startDownloadFailed); purchaseUri(uri, costInfo, !isPlayable);
if (NativeModules.UtilityModule) {
NativeModules.UtilityModule.checkDownloads();
}
}); });
} }
@ -512,22 +632,23 @@ class FilePage extends React.PureComponent {
<FileItemMedia style={filePageStyle.thumbnail} title={title} thumbnail={thumbnail} />} <FileItemMedia style={filePageStyle.thumbnail} title={title} thumbnail={thumbnail} />}
{((!this.state.downloadButtonShown || this.state.downloadPressed) && !this.state.mediaLoaded) && {((!this.state.downloadButtonShown || this.state.downloadPressed) && !this.state.mediaLoaded) &&
<ActivityIndicator size="large" color={Colors.LbryGreen} style={filePageStyle.loading} />} <ActivityIndicator size="large" color={Colors.LbryGreen} style={filePageStyle.loading} />}
{((isPlayable && !completed && !canLoadMedia) || !completed || canOpen) && (!this.state.downloadPressed) && {((isPlayable && !completed && !canLoadMedia) || canOpen || (!completed && !this.state.streamingMode)) &&
(!this.state.downloadPressed) &&
<FileDownloadButton uri={uri} <FileDownloadButton uri={uri}
style={filePageStyle.downloadButton} style={filePageStyle.downloadButton}
openFile={openFile} openFile={openFile}
isPlayable={isPlayable} isPlayable={isPlayable}
isViewable={isViewable} isViewable={isViewable}
onPlay={() => { onPlay={this.onFileDownloadButtonPlayed}
this.startTime = Date.now();
this.setState({ downloadPressed: true, autoPlayMedia: true, stopDownloadConfirmed: false });
}}
onView={() => this.setState({ downloadPressed: true })} onView={() => this.setState({ downloadPressed: true })}
onButtonLayout={() => this.setState({ downloadButtonShown: true })} onButtonLayout={() => this.setState({ downloadButtonShown: true })} />}
onStartDownloadFailed={this.startDownloadFailed} />}
{!fileInfo && <FilePrice uri={uri} style={filePageStyle.filePriceContainer} textStyle={filePageStyle.filePriceText} />} {!fileInfo && <FilePrice uri={uri} style={filePageStyle.filePriceContainer} textStyle={filePageStyle.filePriceText} />}
<TouchableOpacity style={filePageStyle.backButton} onPress={this.onBackButtonPressed}>
<Icon name={"arrow-left"} size={18} style={filePageStyle.backButtonIcon} />
</TouchableOpacity>
</View> </View>
{(canLoadMedia && fileInfo && isPlayable) && {(this.state.streamingMode || (canLoadMedia && fileInfo && isPlayable)) &&
<View style={playerBgStyle} <View style={playerBgStyle}
ref={(ref) => { this.playerBackground = ref; }} ref={(ref) => { this.playerBackground = ref; }}
onLayout={(evt) => { onLayout={(evt) => {
@ -535,12 +656,14 @@ class FilePage extends React.PureComponent {
this.setState({ playerBgHeight: evt.nativeEvent.layout.height }); this.setState({ playerBgHeight: evt.nativeEvent.layout.height });
} }
}} />} }} />}
{(canLoadMedia && fileInfo && isPlayable && this.state.fullscreenMode) && <View style={fsPlayerBgStyle} />} {((this.state.streamingMode || (canLoadMedia && fileInfo && isPlayable)) && this.state.fullscreenMode) &&
{(canLoadMedia && fileInfo && isPlayable) && <View style={fsPlayerBgStyle} />}
{(this.state.streamingMode || (canLoadMedia && fileInfo && isPlayable)) &&
<MediaPlayer <MediaPlayer
fileInfo={fileInfo} claim={claim}
assignPlayer={(ref) => { this.player = ref; }} assignPlayer={(ref) => { this.player = ref; }}
uri={uri} uri={uri}
source={this.playerUriForFileInfo(fileInfo)}
style={playerStyle} style={playerStyle}
autoPlay={autoplay || this.state.autoPlayMedia} autoPlay={autoplay || this.state.autoPlayMedia}
onFullscreenToggled={this.handleFullscreenToggle} onFullscreenToggled={this.handleFullscreenToggle}
@ -550,6 +673,7 @@ class FilePage extends React.PureComponent {
} }
}} }}
onMediaLoaded={() => this.onMediaLoaded(channelName, title, uri)} onMediaLoaded={() => this.onMediaLoaded(channelName, title, uri)}
onBackButtonPressed={this.onBackButtonPressed}
onPlaybackStarted={this.onPlaybackStarted} onPlaybackStarted={this.onPlaybackStarted}
onPlaybackFinished={this.onPlaybackFinished} onPlaybackFinished={this.onPlaybackFinished}
thumbnail={thumbnail} thumbnail={thumbnail}
@ -580,13 +704,15 @@ class FilePage extends React.PureComponent {
style={showActions ? filePageStyle.scrollContainerActions : filePageStyle.scrollContainer} style={showActions ? filePageStyle.scrollContainerActions : filePageStyle.scrollContainer}
contentContainerstyle={showActions ? null : filePageStyle.scrollContent} contentContainerstyle={showActions ? null : filePageStyle.scrollContent}
ref={(ref) => { this.scrollView = ref; }}> ref={(ref) => { this.scrollView = ref; }}>
<View style={filePageStyle.titleRow}> <TouchableWithoutFeedback style={filePageStyle.titleTouch}
<Text style={filePageStyle.title} selectable={true}>{title}</Text> onPress={() => this.setState({ showDescription: !this.state.showDescription })}>
<TouchableWithoutFeedback style={filePageStyle.descriptionToggle} <View style={filePageStyle.titleRow}>
onPress={() => this.setState({ showDescription: !this.state.showDescription })}> <Text style={filePageStyle.title} selectable={true}>{title}</Text>
<Icon name={this.state.showDescription ? "caret-up" : "caret-down"} size={24} /> <View style={filePageStyle.descriptionToggle}>
</TouchableWithoutFeedback> <Icon name={this.state.showDescription ? "caret-up" : "caret-down"} size={24} />
</View> </View>
</View>
</TouchableWithoutFeedback>
{channelName && {channelName &&
<View style={filePageStyle.channelRow}> <View style={filePageStyle.channelRow}>
<View style={filePageStyle.publishInfo}> <View style={filePageStyle.publishInfo}>
@ -630,6 +756,7 @@ class FilePage extends React.PureComponent {
<TextInput ref={ref => this.tipAmountInput = ref} <TextInput ref={ref => this.tipAmountInput = ref}
onChangeText={value => this.setState({tipAmount: value})} onChangeText={value => this.setState({tipAmount: value})}
keyboardType={'numeric'} keyboardType={'numeric'}
placeholder={'0'}
value={this.state.tipAmount} value={this.state.tipAmount}
style={[filePageStyle.input, filePageStyle.tipAmountInput]} /> style={[filePageStyle.input, filePageStyle.tipAmountInput]} />
<Text style={[filePageStyle.text, filePageStyle.currency]}>LBC</Text> <Text style={[filePageStyle.text, filePageStyle.currency]}>LBC</Text>
@ -654,12 +781,15 @@ class FilePage extends React.PureComponent {
)} )}
</View>)} </View>)}
{(costInfo && parseFloat(costInfo.cost) > balance) && <FileRewardsDriver navigation={navigation} />}
<View onLayout={this.setRelatedContentPosition} /> <View onLayout={this.setRelatedContentPosition} />
<RelatedContent navigation={navigation} uri={uri} /> <RelatedContent navigation={navigation} uri={uri} />
</ScrollView> </ScrollView>
</View> </View>
)} )}
{!this.state.fullscreenMode && <FloatingWalletBalance navigation={navigation} />} {(!this.state.fullscreenMode && !this.state.showImageViewer && !this.state.showWebView) &&
<FloatingWalletBalance navigation={navigation} />}
</View> </View>
); );
} }

View file

@ -3,6 +3,8 @@ import { doToast } from 'lbry-redux';
import { import {
doAuthenticate, doAuthenticate,
doCheckSync, doCheckSync,
doGetSync,
doSyncApply,
doUserEmailNew, doUserEmailNew,
doUserResendVerificationEmail, doUserResendVerificationEmail,
selectAuthToken, selectAuthToken,
@ -11,9 +13,14 @@ import {
selectEmailToVerify, selectEmailToVerify,
selectAuthenticationIsPending, selectAuthenticationIsPending,
selectHasSyncedWallet, selectHasSyncedWallet,
selectIsRetrievingSync, selectGetSyncIsPending,
selectSyncApplyIsPending,
selectSyncApplyErrorMessage,
selectSyncData,
selectSyncHash,
selectUser, selectUser,
} from 'lbryinc'; } from 'lbryinc';
import { doSetClientSetting } from 'redux/actions/settings';
import FirstRun from './view'; import FirstRun from './view';
const select = (state) => ({ const select = (state) => ({
@ -23,13 +30,20 @@ const select = (state) => ({
emailNewErrorMessage: selectEmailNewErrorMessage(state), emailNewErrorMessage: selectEmailNewErrorMessage(state),
emailNewPending: selectEmailNewIsPending(state), emailNewPending: selectEmailNewIsPending(state),
hasSyncedWallet: selectHasSyncedWallet(state), hasSyncedWallet: selectHasSyncedWallet(state),
isRetrievingSync: selectIsRetrievingSync(state), getSyncIsPending: selectGetSyncIsPending(state),
syncApplyErrorMessage: selectSyncApplyErrorMessage(state),
syncApplyIsPending: selectSyncApplyIsPending(state),
syncHash: selectSyncHash(state),
syncData: selectSyncData(state),
user: selectUser(state), user: selectUser(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
addUserEmail: email => dispatch(doUserEmailNew(email)), addUserEmail: email => dispatch(doUserEmailNew(email)),
authenticate: (appVersion, os) => dispatch(doAuthenticate(appVersion, os)), authenticate: (appVersion, os) => dispatch(doAuthenticate(appVersion, os)),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
syncApply: (hash, data, password) => dispatch(doSyncApply(hash, data, password)),
getSync: password => dispatch(doGetSync(password)),
checkSync: () => dispatch(doCheckSync()), checkSync: () => dispatch(doCheckSync()),
notify: data => dispatch(doToast(data)), notify: data => dispatch(doToast(data)),
resendVerificationEmail: email => dispatch(doUserResendVerificationEmail(email)) resendVerificationEmail: email => dispatch(doUserResendVerificationEmail(email))

View file

@ -96,7 +96,7 @@ class EmailCollectPage extends React.PureComponent {
); );
} else if (!authToken || authenticating || this.state.verifying) { } else if (!authToken || authenticating || this.state.verifying) {
content = ( content = (
<View> <View style={firstRunStyle.centered}>
<ActivityIndicator size="large" color={Colors.White} style={firstRunStyle.waiting} /> <ActivityIndicator size="large" color={Colors.White} style={firstRunStyle.waiting} />
<Text style={firstRunStyle.paragraph}>Please wait while we get some things ready...</Text> <Text style={firstRunStyle.paragraph}>Please wait while we get some things ready...</Text>
</View> </View>

View file

@ -16,6 +16,8 @@ import Colors from 'styles/colors';
import Constants from 'constants'; import Constants from 'constants';
import firstRunStyle from 'styles/firstRun'; import firstRunStyle from 'styles/firstRun';
const firstRunMargins = 80;
class WalletPage extends React.PureComponent { class WalletPage extends React.PureComponent {
state = { state = {
password: null, password: null,
@ -27,20 +29,20 @@ class WalletPage extends React.PureComponent {
componentDidMount() { componentDidMount() {
this.checkWalletReady(); this.checkWalletReady();
this.props.checkSync();
setTimeout(() => this.setState({ hasCheckedSync: true}), 1000);
} }
checkWalletReady = () => { checkWalletReady = () => {
// make sure the sdk wallet component is ready // make sure the sdk wallet component is ready
Lbry.status().then(status => { Lbry.status().then(status => {
if (status.startup_status && status.startup_status.wallet) { if (status.startup_status && status.startup_status.wallet) {
this.setState({ walletReady: true }); this.setState({ walletReady: true }, () => {
this.props.checkSync();
setTimeout(() => this.setState({ hasCheckedSync: true}), 1000);
});
return; return;
} }
setTimeout(this.checkWalletReady, 1000); setTimeout(this.checkWalletReady, 1000);
}).catch((e) => { }).catch((e) => {
console.log(e);
setTimeout(this.checkWalletReady, 1000); setTimeout(this.checkWalletReady, 1000);
}); });
} }
@ -52,23 +54,24 @@ class WalletPage extends React.PureComponent {
if (onPasswordChanged) { if (onPasswordChanged) {
onPasswordChanged(text); onPasswordChanged(text);
} }
if (NativeModules.UtilityModule) {
NativeModules.UtilityModule.setSecureValue(Constants.KEY_FIRST_RUN_PASSWORD, text);
// simply set any string value to indicate that a passphrase was set on first run
AsyncStorage.setItem(Constants.KEY_FIRST_RUN_PASSWORD, "true");
}
} }
render() { render() {
const { onPasswordChanged, onWalletViewLayout, isRetrievingSync, hasSyncedWallet } = this.props; const { onPasswordChanged, onWalletViewLayout, getSyncIsPending, hasSyncedWallet, syncApplyIsPending } = this.props;
let content; let content;
if (!this.state.walletReady || !this.state.hasCheckedSync || isRetrievingSync) { if (!this.state.walletReady || !this.state.hasCheckedSync || getSyncIsPending) {
content = ( content = (
<View> <View style={firstRunStyle.centered}>
<ActivityIndicator size="large" color={Colors.White} style={firstRunStyle.waiting} /> <ActivityIndicator size="large" color={Colors.White} style={firstRunStyle.waiting} />
<Text style={firstRunStyle.paragraph}>Retrieving your account information...</Text> <Text style={firstRunStyle.paragraph}>Retrieving your account information...</Text>
</View>
);
} else if (syncApplyIsPending) {
content = (
<View style={firstRunStyle.centered}>
<ActivityIndicator size="large" color={Colors.White} style={firstRunStyle.waiting} />
<Text style={firstRunStyle.paragraph}>Validating password...</Text>
</View> </View>
); );
} else { } else {
@ -96,10 +99,11 @@ class WalletPage extends React.PureComponent {
} }
}} }}
/> />
{(this.state.password && this.state.password.trim().length) > 0 &&
{(!hasSyncedWallet && this.state.password && this.state.password.trim().length) > 0 &&
<View style={firstRunStyle.passwordStrength}> <View style={firstRunStyle.passwordStrength}>
<BarPasswordStrengthDisplay <BarPasswordStrengthDisplay
width={Dimensions.get('window').width - 80} width={Dimensions.get('window').width - firstRunMargins}
minLength={1} minLength={1}
password={this.state.password} /> password={this.state.password} />
</View>} </View>}

View file

@ -38,7 +38,8 @@ class FirstRunScreen extends React.PureComponent {
isEmailVerified: false, isEmailVerified: false,
skipAccountConfirmed: false, skipAccountConfirmed: false,
showBottomContainer: true, showBottomContainer: true,
walletPassword: null walletPassword: null,
syncApplyStarted: false
}; };
componentDidMount() { componentDidMount() {
@ -65,12 +66,12 @@ class FirstRunScreen extends React.PureComponent {
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
const { emailNewErrorMessage, emailNewPending, user } = nextProps; const { emailNewErrorMessage, emailNewPending, syncApplyErrorMessage, syncApplyIsPending, user } = nextProps;
const { notify } = this.props; const { notify, isApplyingSync, setClientSetting } = this.props;
if (this.state.emailSubmitted && !emailNewPending) { if (this.state.emailSubmitted && !emailNewPending) {
this.setState({ emailSubmitted: false }); this.setState({ emailSubmitted: false });
if (emailNewErrorMessage) { if (emailNewErrorMessage && emailNewErrorMessage.trim().length > 0) {
notify ({ message: String(emailNewErrorMessage), isError: true }); notify ({ message: String(emailNewErrorMessage), isError: true });
} else { } else {
// Request successful. Navigate to email verify page. // Request successful. Navigate to email verify page.
@ -78,6 +79,21 @@ class FirstRunScreen extends React.PureComponent {
} }
} }
if (this.state.syncApplyStarted && !syncApplyIsPending) {
this.setState({ syncApplyStarted: false });
if (syncApplyErrorMessage && syncApplyErrorMessage.trim().length > 0) {
notify({ message: syncApplyErrorMessage, isError: true });
this.setState({ showBottomContainer: true });
} else {
// password successfully verified
if (NativeModules.UtilityModule) {
NativeModules.UtilityModule.setSecureValue(Constants.KEY_FIRST_RUN_PASSWORD, this.state.walletPassword);
}
setClientSetting(Constants.SETTING_DEVICE_WALLET_SYNCED, true);
this.closeFinalPage();
}
}
this.checkVerificationStatus(user); this.checkVerificationStatus(user);
} }
@ -121,15 +137,27 @@ class FirstRunScreen extends React.PureComponent {
} }
} }
checkWalletPassword = () => {
const { syncApply, syncHash, syncData } = this.props;
this.setState({ syncApplyStarted: true, showBottomContainer: false }, () => {
syncApply(syncHash, syncData, this.state.walletPassword);
});
}
handleContinuePressed = () => { handleContinuePressed = () => {
const { notify, user } = this.props; const { notify, user, hasSyncedWallet } = this.props;
const pageIndex = FirstRunScreen.pages.indexOf(this.state.currentPage); const pageIndex = FirstRunScreen.pages.indexOf(this.state.currentPage);
if (Constants.FIRST_RUN_PAGE_WALLET === this.state.currentPage) { if (Constants.FIRST_RUN_PAGE_WALLET === this.state.currentPage) {
if (!this.state.walletPassword || this.state.walletPassword.trim().length === 0) { if (!this.state.walletPassword || this.state.walletPassword.trim().length === 0) {
return notify({ message: 'Please enter a wallet password' }); return notify({ message: 'Please enter a wallet password' });
} }
this.closeFinalPage(); // do apply sync to check if the password is valid
if (hasSyncedWallet) {
this.checkWalletPassword();
} else {
this.setFreshPassword();
}
return; return;
} }
@ -167,20 +195,25 @@ class FirstRunScreen extends React.PureComponent {
}); });
} }
checkBottomContainer = (pageName) => {
if (Constants.FIRST_RUN_PAGE_EMAIL_COLLECT === pageName || Constants.FIRST_RUN_PAGE_WALLET === pageName) {
// do not show the buttons (because we're waiting to get things ready)
this.setState({ showBottomContainer: false });
}
}
showNextPage = () => { showNextPage = () => {
const pageIndex = FirstRunScreen.pages.indexOf(this.state.currentPage); const pageIndex = FirstRunScreen.pages.indexOf(this.state.currentPage);
const nextPage = FirstRunScreen.pages[pageIndex + 1]; const nextPage = FirstRunScreen.pages[pageIndex + 1];
this.setState({ currentPage: nextPage }); this.setState({ currentPage: nextPage });
if (nextPage === Constants.FIRST_RUN_PAGE_EMAIL_COLLECT) { this.checkBottomContainer(nextPage);
// do not show the buttons (because we're waiting to get things ready)
this.setState({ showBottomContainer: false });
}
} }
showPage(pageName) { showPage(pageName) {
const pageIndex = FirstRunScreen.pages.indexOf(pageName); const pageIndex = FirstRunScreen.pages.indexOf(pageName);
if (pageIndex > -1) { if (pageIndex > -1) {
this.setState({ currentPage: pageName }); this.setState({ currentPage: pageName });
this.checkBottomContainer(pageName);
} }
} }
@ -222,6 +255,21 @@ class FirstRunScreen extends React.PureComponent {
this.setState({ skipAccountConfirmed: checked }); this.setState({ skipAccountConfirmed: checked });
} }
setFreshPassword = () => {
const { getSync, setClientSetting } = this.props;
if (NativeModules.UtilityModule) {
NativeModules.UtilityModule.setSecureValue(Constants.KEY_FIRST_RUN_PASSWORD, this.state.walletPassword);
Lbry.account_encrypt({ new_password: this.state.walletPassword }).then(() => {
Lbry.account_unlock({ password: this.state.walletPassword }).then(() => {
// fresh account, new password set
getSync(this.state.walletPassword);
setClientSetting(Constants.SETTING_DEVICE_WALLET_SYNCED, true);
this.closeFinalPage();
});
});
}
}
render() { render() {
const { const {
authenticate, authenticate,
@ -233,7 +281,8 @@ class FirstRunScreen extends React.PureComponent {
emailToVerify, emailToVerify,
notify, notify,
hasSyncedWallet, hasSyncedWallet,
isRetrievingSync, getSyncIsPending,
syncApplyIsPending,
resendVerificationEmail, resendVerificationEmail,
user user
} = this.props; } = this.props;
@ -267,7 +316,8 @@ class FirstRunScreen extends React.PureComponent {
page = (<WalletPage page = (<WalletPage
checkSync={checkSync} checkSync={checkSync}
hasSyncedWallet={hasSyncedWallet} hasSyncedWallet={hasSyncedWallet}
isRetrievingSync={isRetrievingSync} getSyncIsPending={getSyncIsPending}
syncApplyIsPending={syncApplyIsPending}
onWalletViewLayout={this.onWalletViewLayout} onWalletViewLayout={this.onWalletViewLayout}
onPasswordChanged={this.onWalletPasswordChanged} />); onPasswordChanged={this.onWalletPasswordChanged} />);
break; break;
@ -293,7 +343,7 @@ class FirstRunScreen extends React.PureComponent {
Constants.FIRST_RUN_PAGE_EMAIL_VERIFY === this.state.currentPage) && Constants.FIRST_RUN_PAGE_EMAIL_VERIFY === this.state.currentPage) &&
<TouchableOpacity style={firstRunStyle.leftButton} onPress={this.handleLeftButtonPressed}> <TouchableOpacity style={firstRunStyle.leftButton} onPress={this.handleLeftButtonPressed}>
<Text style={firstRunStyle.buttonText}> <Text style={firstRunStyle.buttonText}>
« {Constants.FIRST_RUN_PAGE_SKIP_ACCOUNT === this.state.currentPage ? 'Setup account' : 'Change Email'}</Text> « {Constants.FIRST_RUN_PAGE_SKIP_ACCOUNT === this.state.currentPage ? 'Setup account' : 'Change email'}</Text>
</TouchableOpacity>} </TouchableOpacity>}
{!emailNewPending && (Constants.FIRST_RUN_PAGE_EMAIL_COLLECT === this.state.currentPage) && {!emailNewPending && (Constants.FIRST_RUN_PAGE_EMAIL_COLLECT === this.state.currentPage) &&
<TouchableOpacity style={firstRunStyle.leftButton} onPress={this.handleLeftButtonPressed}> <TouchableOpacity style={firstRunStyle.leftButton} onPress={this.handleLeftButtonPressed}>

View file

@ -10,7 +10,7 @@ import {
selectUser, selectUser,
} from 'lbryinc'; } from 'lbryinc';
import { doToast } from 'lbry-redux'; import { doToast } from 'lbry-redux';
import { doPushDrawerStack } from 'redux/actions/drawer'; import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
import Constants from 'constants'; import Constants from 'constants';
import RewardsPage from './view'; import RewardsPage from './view';
@ -27,7 +27,8 @@ const perform = dispatch => ({
claimReward: reward => dispatch(doClaimRewardType(reward.reward_type, true)), claimReward: reward => dispatch(doClaimRewardType(reward.reward_type, true)),
fetchRewards: () => dispatch(doRewardList()), fetchRewards: () => dispatch(doRewardList()),
notify: data => dispatch(doToast(data)), notify: data => dispatch(doToast(data)),
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_REWARDS)) pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_REWARDS)),
setPlayerVisible: () => dispatch(doSetPlayerVisible(false))
}); });
export default connect(select, perform)(RewardsPage); export default connect(select, perform)(RewardsPage);

View file

@ -32,9 +32,10 @@ class RewardsPage extends React.PureComponent {
scrollView = null; scrollView = null;
componentDidMount() { componentDidMount() {
const { fetchRewards, pushDrawerStack, navigation, user } = this.props; const { fetchRewards, pushDrawerStack, navigation, setPlayerVisible, user } = this.props;
pushDrawerStack(); pushDrawerStack();
setPlayerVisible();
fetchRewards(); fetchRewards();
this.setState({ this.setState({
@ -156,10 +157,10 @@ class RewardsPage extends React.PureComponent {
return ( return (
<View style={rewardStyle.container}> <View style={rewardStyle.container}>
<UriBar navigation={navigation} /> <UriBar navigation={navigation} />
{(!this.state.isEmailVerified || !this.state.isIdentityVerified || !this.state.isRewardApproved) && {(!this.state.isEmailVerified || !this.state.isRewardApproved) &&
<RewardEnrolment navigation={navigation} />} <RewardEnrolment navigation={navigation} />}
{(this.state.isEmailVerified && this.state.isIdentityVerified && this.state.isRewardApproved) && {(this.state.isEmailVerified && this.state.isRewardApproved) &&
<ScrollView <ScrollView
ref={ref => this.scrollView = ref} ref={ref => this.scrollView = ref}
keyboardShouldPersistTaps={'handled'} keyboardShouldPersistTaps={'handled'}

View file

@ -8,7 +8,7 @@ import {
makeSelectQueryWithOptions, makeSelectQueryWithOptions,
selectSearchUrisByQuery selectSearchUrisByQuery
} from 'lbry-redux'; } from 'lbry-redux';
import { doPushDrawerStack } from 'redux/actions/drawer'; import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
import Constants from 'constants'; import Constants from 'constants';
import SearchPage from './view'; import SearchPage from './view';
@ -23,6 +23,7 @@ const perform = dispatch => ({
search: (query) => dispatch(doSearch(query, 25)), search: (query) => dispatch(doSearch(query, 25)),
updateSearchQuery: query => dispatch(doUpdateSearchQuery(query)), updateSearchQuery: query => dispatch(doUpdateSearchQuery(query)),
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_SEARCH)), pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_SEARCH)),
setPlayerVisible: () => dispatch(doSetPlayerVisible(false))
}); });
export default connect(select, perform)(SearchPage); export default connect(select, perform)(SearchPage);

View file

@ -26,12 +26,17 @@ class SearchPage extends React.PureComponent {
}; };
componentWillMount() { componentWillMount() {
this.props.pushDrawerStack(); const { pushDrawerStack, setPlayerVisible } = this.props;
pushDrawerStack();
setPlayerVisible();
} }
componentDidMount() { componentDidMount() {
const { navigation, search } = this.props; const { navigation, search } = this.props;
const { searchQuery } = navigation.state.params; let searchQuery;
if (navigation && navigation.state) {
searchQuery = navigation.state.params.searchQuery;
}
if (searchQuery && searchQuery.trim().length > 0) { if (searchQuery && searchQuery.trim().length > 0) {
this.setState({ currentUri: (isURIValid(searchQuery)) ? normalizeURI(searchQuery) : null }) this.setState({ currentUri: (isURIValid(searchQuery)) ? normalizeURI(searchQuery) : null })
search(searchQuery); search(searchQuery);

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SETTINGS } from 'lbry-redux'; import { SETTINGS } from 'lbry-redux';
import { doPushDrawerStack, doPopDrawerStack } from 'redux/actions/drawer'; import { doPushDrawerStack, doPopDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
import { doSetClientSetting } from 'redux/actions/settings'; import { doSetClientSetting } from 'redux/actions/settings';
import { selectDrawerStack } from 'redux/selectors/drawer'; import { selectDrawerStack } from 'redux/selectors/drawer';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
@ -18,6 +18,7 @@ const perform = dispatch => ({
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_SETTINGS)), pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_SETTINGS)),
popDrawerStack: () => dispatch(doPopDrawerStack()), popDrawerStack: () => dispatch(doPopDrawerStack()),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)), setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
setPlayerVisible: () => dispatch(doSetPlayerVisible(false)),
}); });
export default connect(select, perform)(SettingsPage); export default connect(select, perform)(SettingsPage);

View file

@ -11,7 +11,9 @@ class SettingsPage extends React.PureComponent {
} }
componentDidMount() { componentDidMount() {
this.props.pushDrawerStack(); const { pushDrawerStack, setPlayerVisible } = this.props;
pushDrawerStack();
setPlayerVisible();
} }
render() { render() {

View file

@ -13,7 +13,7 @@ import {
selectUser, selectUser,
selectEmailToVerify selectEmailToVerify
} from 'lbryinc'; } from 'lbryinc';
import { doDeleteCompleteBlobs } from 'redux/actions/file'; import { doSetClientSetting } from 'redux/actions/settings';
import SplashScreen from './view'; import SplashScreen from './view';
const select = state => ({ const select = state => ({
@ -26,11 +26,11 @@ const perform = dispatch => ({
balanceSubscribe: () => dispatch(doBalanceSubscribe()), balanceSubscribe: () => dispatch(doBalanceSubscribe()),
blacklistedOutpointsSubscribe: () => dispatch(doBlackListedOutpointsSubscribe()), blacklistedOutpointsSubscribe: () => dispatch(doBlackListedOutpointsSubscribe()),
checkSubscriptionsInit: () => dispatch(doCheckSubscriptionsInit()), checkSubscriptionsInit: () => dispatch(doCheckSubscriptionsInit()),
deleteCompleteBlobs: () => dispatch(doDeleteCompleteBlobs()),
fetchRewardedContent: () => dispatch(doFetchRewardedContent()), fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
fetchSubscriptions: (callback) => dispatch(doFetchMySubscriptions(callback)), fetchSubscriptions: (callback) => dispatch(doFetchMySubscriptions(callback)),
getSync: password => dispatch(doGetSync(password)), getSync: password => dispatch(doGetSync(password)),
notify: data => dispatch(doToast(data)), notify: data => dispatch(doToast(data)),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
setEmailToVerify: email => dispatch(doUserEmailToVerify(email)), setEmailToVerify: email => dispatch(doUserEmailToVerify(email)),
updateBlockHeight: () => dispatch(doUpdateBlockHeight()), updateBlockHeight: () => dispatch(doUpdateBlockHeight()),
verifyUserEmail: (token, recaptcha) => dispatch(doUserEmailVerify(token, recaptcha)), verifyUserEmail: (token, recaptcha) => dispatch(doUserEmailVerify(token, recaptcha)),

View file

@ -28,7 +28,7 @@ class SplashScreen extends React.PureComponent {
componentWillMount() { componentWillMount() {
this.setState({ this.setState({
daemonReady: false, daemonReady: false,
details: 'Starting daemon', details: 'Starting up...',
message: 'Connecting', message: 'Connecting',
isRunning: false, isRunning: false,
isLagging: false, isLagging: false,
@ -114,22 +114,18 @@ class SplashScreen extends React.PureComponent {
if (this.state.daemonReady && this.state.shouldAuthenticate && user && user.id) { if (this.state.daemonReady && this.state.shouldAuthenticate && user && user.id) {
this.setState({ shouldAuthenticate: false }, () => { this.setState({ shouldAuthenticate: false }, () => {
AsyncStorage.getItem(Constants.KEY_FIRST_RUN_EMAIL).then(email => { // user is authenticated, navigate to the main view
if (email) { if (user.has_verified_email) {
setEmailToVerify(email); NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => {
} if (walletPassword && walletPassword.trim().length > 0) {
// user is authenticated, navigate to the main view
if (user.has_verified_email) {
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => {
getSync(walletPassword); getSync(walletPassword);
this.navigateToMain(); }
}); this.navigateToMain();
return; });
} return;
}
this.navigateToMain(); this.navigateToMain();
});
}); });
} }
} }
@ -140,9 +136,11 @@ class SplashScreen extends React.PureComponent {
balanceSubscribe, balanceSubscribe,
blacklistedOutpointsSubscribe, blacklistedOutpointsSubscribe,
checkSubscriptionsInit, checkSubscriptionsInit,
updateBlockHeight, getSync,
navigation, navigation,
notify notify,
updateBlockHeight,
user
} = this.props; } = this.props;
Lbry.resolve({ urls: 'lbry://one' }).then(() => { Lbry.resolve({ urls: 'lbry://one' }).then(() => {
@ -152,21 +150,30 @@ class SplashScreen extends React.PureComponent {
checkSubscriptionsInit(); checkSubscriptionsInit();
updateBlockHeight(); updateBlockHeight();
setInterval(() => { updateBlockHeight(); }, BLOCK_HEIGHT_INTERVAL); setInterval(() => { updateBlockHeight(); }, BLOCK_HEIGHT_INTERVAL);
NativeModules.VersionInfo.getAppVersion().then(appVersion => {
this.setState({ shouldAuthenticate: true }); if (user && user.id && user.has_verified_email) {
authenticate(appVersion, Platform.OS); // user already authenticated
}); NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => {
if (walletPassword && walletPassword.trim().length > 0) {
getSync(walletPassword);
}
this.navigateToMain();
});
} else {
NativeModules.VersionInfo.getAppVersion().then(appVersion => {
this.setState({ shouldAuthenticate: true });
authenticate(appVersion, Platform.OS);
});
}
}); });
} }
_updateStatusCallback(status) { _updateStatusCallback(status) {
const { deleteCompleteBlobs, fetchSubscriptions } = this.props; const { fetchSubscriptions, getSync, setClientSetting } = this.props;
const startupStatus = status.startup_status; const startupStatus = status.startup_status;
// At the minimum, wallet should be started and blocks_behind equal to 0 before calling resolve // At the minimum, wallet should be started and blocks_behind equal to 0 before calling resolve
const hasStarted = startupStatus.stream_manager && startupStatus.wallet && status.wallet.blocks_behind <= 0; const hasStarted = startupStatus.stream_manager && startupStatus.wallet && status.wallet.blocks_behind <= 0;
if (hasStarted) { if (hasStarted) {
deleteCompleteBlobs();
// Wait until we are able to resolve a name before declaring // Wait until we are able to resolve a name before declaring
// that we are done. // that we are done.
// TODO: This is a hack, and the logic should live in the daemon // TODO: This is a hack, and the logic should live in the daemon
@ -179,37 +186,18 @@ class SplashScreen extends React.PureComponent {
isRunning: true, isRunning: true,
}); });
AsyncStorage.getItem(Constants.KEY_FIRST_RUN_PASSWORD).then(passwordSet => {
if ("true" === passwordSet) {
// encrypt the wallet
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(password => {
if (!password || password.trim().length === 0) {
this.finishSplashScreen();
return;
}
Lbry.account_encrypt({ new_password: password }).then((result) => {
AsyncStorage.removeItem(Constants.KEY_FIRST_RUN_PASSWORD);
Lbry.account_unlock({ password }).then(() => this.finishSplashScreen());
});
});
// For now, automatically unlock the wallet if a password is set so that downloads work
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(password => {
if (password && password.trim().length > 0) {
// unlock the wallet and then finish the splash screen
Lbry.account_unlock({ password }).then(() => this.finishSplashScreen()).catch(() => this.finishSplashScreen());
return; return;
} }
// For now, automatically unlock the wallet if a password is set so that downloads work this.finishSplashScreen();
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(password => {
if (password && password.trim().length > 0) {
// unlock the wallet and then finish the splash screen
Lbry.account_unlock({ password }).then(() => this.finishSplashScreen());
return;
}
this.finishSplashScreen();
});
}); });
return; return;
} }

View file

@ -12,7 +12,7 @@ import {
selectFirstRunCompleted, selectFirstRunCompleted,
selectShowSuggestedSubs selectShowSuggestedSubs
} from 'lbryinc'; } from 'lbryinc';
import { doPushDrawerStack } from 'redux/actions/drawer'; import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
import { doSetClientSetting } from 'redux/actions/settings'; import { doSetClientSetting } from 'redux/actions/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import Constants from 'constants'; import Constants from 'constants';
@ -37,6 +37,7 @@ const perform = dispatch => ({
doSetViewMode: (viewMode) => dispatch(doSetViewMode(viewMode)), doSetViewMode: (viewMode) => dispatch(doSetViewMode(viewMode)),
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_SUBSCRIPTIONS)), pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_SUBSCRIPTIONS)),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)), setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
setPlayerVisible: () => dispatch(doSetPlayerVisible(false))
}); });
export default connect(select, perform)(SubscriptionsPage); export default connect(select, perform)(SubscriptionsPage);

View file

@ -34,9 +34,11 @@ class SubscriptionsPage extends React.PureComponent {
doFetchMySubscriptions, doFetchMySubscriptions,
doFetchRecommendedSubscriptions, doFetchRecommendedSubscriptions,
pushDrawerStack, pushDrawerStack,
setPlayerVisible
} = this.props; } = this.props;
pushDrawerStack(); pushDrawerStack();
setPlayerVisible();
doFetchMySubscriptions(); doFetchMySubscriptions();
doFetchRecommendedSubscriptions(); doFetchRecommendedSubscriptions();
} }

View file

@ -4,7 +4,7 @@ import {
selectTransactionItems, selectTransactionItems,
selectIsFetchingTransactions, selectIsFetchingTransactions,
} from 'lbry-redux'; } from 'lbry-redux';
import { doPushDrawerStack } from 'redux/actions/drawer'; import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
import Constants from 'constants'; import Constants from 'constants';
import TransactionHistoryPage from './view'; import TransactionHistoryPage from './view';
@ -16,6 +16,7 @@ const select = state => ({
const perform = dispatch => ({ const perform = dispatch => ({
fetchTransactions: () => dispatch(doFetchTransactions()), fetchTransactions: () => dispatch(doFetchTransactions()),
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_TRANSACTION_HISTORY)), pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_TRANSACTION_HISTORY)),
setPlayerVisible: () => dispatch(doSetPlayerVisible(false))
}); });
export default connect(select, perform)(TransactionHistoryPage); export default connect(select, perform)(TransactionHistoryPage);

View file

@ -6,7 +6,9 @@ import walletStyle from 'styles/wallet';
class TransactionHistoryPage extends React.PureComponent { class TransactionHistoryPage extends React.PureComponent {
componentWillMount() { componentWillMount() {
this.props.pushDrawerStack(); const { pushDrawerStack, setPlayerVisible } = this.props;
pushDrawerStack();
setPlayerVisible();
} }
componentDidMount() { componentDidMount() {

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doFetchTrendingUris, selectTrendingUris, selectFetchingTrendingUris } from 'lbryinc'; import { doFetchTrendingUris, selectTrendingUris, selectFetchingTrendingUris } from 'lbryinc';
import { doPushDrawerStack } from 'redux/actions/drawer'; import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
import Constants from 'constants'; import Constants from 'constants';
import TrendingPage from './view'; import TrendingPage from './view';
@ -11,7 +11,8 @@ const select = state => ({
const perform = dispatch => ({ const perform = dispatch => ({
fetchTrendingUris: () => dispatch(doFetchTrendingUris()), fetchTrendingUris: () => dispatch(doFetchTrendingUris()),
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_TRENDING)) pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_TRENDING)),
setPlayerVisible: () => dispatch(doSetPlayerVisible(false))
}); });
export default connect(select, perform)(TrendingPage); export default connect(select, perform)(TrendingPage);

View file

@ -19,8 +19,9 @@ import UriBar from 'component/uriBar';
class TrendingPage extends React.PureComponent { class TrendingPage extends React.PureComponent {
componentDidMount() { componentDidMount() {
const { fetchTrendingUris, pushDrawerStack } = this.props; const { fetchTrendingUris, pushDrawerStack, setPlayerVisible } = this.props;
pushDrawerStack(); pushDrawerStack();
setPlayerVisible();
fetchTrendingUris(); fetchTrendingUris();
} }

View file

@ -1,6 +1,9 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doToast } from 'lbry-redux'; import { doToast } from 'lbry-redux';
import { import {
doCheckSync,
doGetSync,
doSyncApply,
doUserEmailNew, doUserEmailNew,
doUserEmailToVerify, doUserEmailToVerify,
doUserResendVerificationEmail, doUserResendVerificationEmail,
@ -14,8 +17,18 @@ import {
selectEmailNewErrorMessage, selectEmailNewErrorMessage,
selectEmailNewIsPending, selectEmailNewIsPending,
selectEmailToVerify, selectEmailToVerify,
selectHasSyncedWallet,
selectGetSyncIsPending,
selectSetSyncIsPending,
selectSyncApplyIsPending,
selectSyncApplyErrorMessage,
selectSyncData,
selectSyncHash,
selectUser, selectUser,
} from 'lbryinc'; } from 'lbryinc';
import { doSetClientSetting } from 'redux/actions/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import Constants from 'constants';
import Verification from './view'; import Verification from './view';
const select = (state) => ({ const select = (state) => ({
@ -28,15 +41,27 @@ const select = (state) => ({
phone: selectPhoneToVerify(state), phone: selectPhoneToVerify(state),
phoneNewErrorMessage: selectPhoneNewErrorMessage(state), phoneNewErrorMessage: selectPhoneNewErrorMessage(state),
phoneNewIsPending: selectPhoneNewIsPending(state), phoneNewIsPending: selectPhoneNewIsPending(state),
deviceWalletSynced: makeSelectClientSetting(Constants.SETTING_DEVICE_WALLET_SYNCED)(state),
hasSyncedWallet: selectHasSyncedWallet(state),
getSyncIsPending: selectGetSyncIsPending(state),
setSyncIsPending: selectSetSyncIsPending(state),
syncApplyIsPending: selectSyncApplyIsPending(state),
syncApplyErrorMessage: selectSyncApplyErrorMessage(state),
syncData: selectSyncData(state),
syncHash: selectSyncHash(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
addUserEmail: email => dispatch(doUserEmailNew(email)), addUserEmail: email => dispatch(doUserEmailNew(email)),
addUserPhone: (phone, country_code) => dispatch(doUserPhoneNew(phone, country_code)), addUserPhone: (phone, country_code) => dispatch(doUserPhoneNew(phone, country_code)),
getSync: password => dispatch(doGetSync(password)),
checkSync: () => dispatch(doCheckSync()),
verifyPhone: (verificationCode) => dispatch(doUserPhoneVerify(verificationCode)), verifyPhone: (verificationCode) => dispatch(doUserPhoneVerify(verificationCode)),
notify: data => dispatch(doToast(data)), notify: data => dispatch(doToast(data)),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
setEmailToVerify: email => dispatch(doUserEmailToVerify(email)), setEmailToVerify: email => dispatch(doUserEmailToVerify(email)),
resendVerificationEmail: email => dispatch(doUserResendVerificationEmail(email)) syncApply: (hash, data, password) => dispatch(doSyncApply(hash, data, password)),
resendVerificationEmail: email => dispatch(doUserResendVerificationEmail(email)),
}); });
export default connect(select, perform)(Verification); export default connect(select, perform)(Verification);

View file

@ -142,13 +142,15 @@ class EmailVerifyPage extends React.PureComponent {
text={"Send verification email"} text={"Send verification email"}
onPress={this.onSendVerificationPressed} />} onPress={this.onSendVerificationPressed} />}
{this.state.verifyStarted && emailNewPending && {this.state.verifyStarted && emailNewPending &&
<ActivityIndicator size={"small"} color={Colors.White} style={rewardStyle.loading} />} <View style={firstRunStyle.centerInside}>
<ActivityIndicator size={"small"} color={Colors.White} />
</View>}
</View> </View>
</View>} </View>}
{(Constants.PHASE_VERIFICATION === this.state.phase) && {(Constants.PHASE_VERIFICATION === this.state.phase) &&
<View> <View>
<Text style={firstRunStyle.paragraph}>An email has been sent to {this.state.email}. Please follow the instructions in the message to verify your email address.</Text> <Text style={firstRunStyle.paragraph}>An email has been sent to <Text style={firstRunStyle.nowrap}>{this.state.email}</Text>. Please follow the instructions in the message to verify your email address.</Text>
<View style={rewardStyle.buttonContainer}> <View style={rewardStyle.buttonContainer}>
<Button style={rewardStyle.verificationButton} theme={"light"} text={"Resend"} onPress={this.onResendPressed} /> <Button style={rewardStyle.verificationButton} theme={"light"} text={"Resend"} onPress={this.onResendPressed} />

View file

@ -21,7 +21,7 @@ class ManualVerifyPage extends React.PureComponent {
return ( return (
<View style={firstRunStyle.container}> <View style={firstRunStyle.container}>
<Text style={rewardStyle.verificationTitle}>Manual Reward Verification</Text> <Text style={rewardStyle.verificationTitle}>Manual Reward Verification</Text>
<Text style={firstRunStyle.paragraph}>You need to be manually verified before you can start claiming rewards. Please request to be verified on the <Link style={rewardStyle.underlinedTextLink} href="https://discordapp.com/invite/Z3bERWA" text="LBRY Discord server" />.</Text> <Text style={firstRunStyle.spacedParagraph}>You need to be manually verified before you can start claiming rewards. Please request to be verified on the <Link style={rewardStyle.underlinedTextLink} href="https://discordapp.com/invite/Z3bERWA" text="LBRY Discord server" />.</Text>
</View> </View>
); );
} }

View file

@ -171,10 +171,12 @@ class PhoneVerifyPage extends React.PureComponent {
text={"Send verification text"} text={"Send verification text"}
onPress={this.onSendTextPressed} />} onPress={this.onSendTextPressed} />}
{phoneNewIsPending && {phoneNewIsPending &&
<ActivityIndicator <View style={firstRunStyle.centerInside}>
style={[rewardStyle.loading, rewardStyle.topMarginMedium]} <ActivityIndicator
size="small" style={rewardStyle.topMarginMedium}
color={Colors.White} />} size="small"
color={Colors.White} />
</View>}
</View> </View>
</View>} </View>}
@ -204,12 +206,12 @@ class PhoneVerifyPage extends React.PureComponent {
</View> </View>
} }
{phoneVerifyIsPending && {phoneVerifyIsPending &&
<View> <View style={firstRunStyle.centered}>
<Text style={firstRunStyle.paragraph}>Verifying your phone number...</Text> <Text style={firstRunStyle.paragraph}>Verifying your phone number...</Text>
<ActivityIndicator <ActivityIndicator
color={Colors.White} color={Colors.White}
size="small" size="small"
style={[rewardStyle.loading, rewardStyle.topMarginMedium, rewardStyle.leftRightMargin]} /> style={[rewardStyle.topMarginMedium, rewardStyle.leftRightMargin]} />
</View>} </View>}
</View> </View>
} }

View file

@ -0,0 +1,170 @@
import React from 'react';
import { Lbry } from 'lbry-redux';
import {
ActivityIndicator,
Dimensions,
NativeModules,
Text,
TextInput,
View
} from 'react-native';
import { BarPasswordStrengthDisplay } from 'react-native-password-strength-meter';
import Button from 'component/button';
import Link from 'component/link';
import Colors from 'styles/colors';
import Constants from 'constants';
import firstRunStyle from 'styles/firstRun';
import rewardStyle from 'styles/reward';
class SyncVerifyPage extends React.PureComponent {
state = {
checkSyncStarted: false,
password: null,
placeholder: 'password',
syncApplyStarted: false,
syncChecked: false,
}
componentDidMount() {
const { checkSync, setEmailVerificationPhase } = this.props;
this.setState({ checkSyncStarted: true }, () => checkSync());
if (setEmailVerificationPhase) {
setEmailVerificationPhase(false);
}
}
onEnableSyncPressed = () => {
const {
getSync,
hasSyncedWallet,
navigation,
setClientSetting,
syncApply,
syncData,
syncHash
} = this.props;
this.setState({ syncApplyStarted: true }, () => {
if (!hasSyncedWallet) {
// fresh account with no sync
Lbry.account_encrypt({ new_password: this.state.password }).then(() => {
Lbry.account_unlock({ password: this.state.password }).then(() => {
getSync(this.state.password);
setClientSetting(Constants.SETTING_DEVICE_WALLET_SYNCED, true);
navigation.goBack();
});
});
} else {
syncApply(syncHash, syncData, this.state.password);
}
});
}
componentWillReceiveProps(nextProps) {
const { getSyncIsPending, syncApplyIsPending, syncApplyErrorMessage } = nextProps;
const { getSync, setClientSetting, navigation, notify, hasSyncedWallet } = this.props;
if (this.state.checkSyncStarted && !getSyncIsPending) {
this.setState({ syncChecked: true });
}
if (this.state.syncApplyStarted && !syncApplyIsPending) {
if (syncApplyErrorMessage && syncApplyErrorMessage.trim().length > 0) {
notify({ message: syncApplyErrorMessage, isError: true });
this.setState({ syncApplyStarted: false });
} else {
// password successfully verified
if (NativeModules.UtilityModule) {
NativeModules.UtilityModule.setSecureValue(Constants.KEY_FIRST_RUN_PASSWORD, this.state.password);
}
setClientSetting(Constants.SETTING_DEVICE_WALLET_SYNCED, true);
navigation.goBack();
}
}
}
handleChangeText = (text) => {
// save the value to the state email
const { onPasswordChanged } = this.props;
this.setState({ password: text });
if (onPasswordChanged) {
onPasswordChanged(text);
}
}
render() {
const { hasSyncedWallet, syncApplyIsPending } = this.props;
let paragraph;
if (!hasSyncedWallet) {
paragraph = (<Text style={firstRunStyle.paragraph}>Please enter a password to secure your account and wallet.</Text>);
} else {
paragraph = (<Text style={firstRunStyle.paragraph}>Please enter the password you used to secure your wallet.</Text>);
}
let content;
if (!this.state.syncChecked) {
content = (
<View style={firstRunStyle.centered}>
<ActivityIndicator size="large" color={Colors.White} style={firstRunStyle.waiting} />
<Text style={firstRunStyle.paragraph}>Retrieving your account information...</Text>
</View>
);
} else {
content = (
<View>
<Text style={rewardStyle.verificationTitle}>Wallet Sync</Text>
{paragraph}
<TextInput style={firstRunStyle.passwordInput}
placeholder={this.state.placeholder}
underlineColorAndroid="transparent"
secureTextEntry={true}
value={this.state.password}
onChangeText={text => this.handleChangeText(text)}
onFocus={() => {
if (!this.state.password || this.state.password.length === 0) {
this.setState({ placeholder: '' });
}
}}
onBlur={() => {
if (!this.state.password || this.state.password.length === 0) {
this.setState({ placeholder: 'password' });
}
}}
/>
{(!hasSyncedWallet && this.state.password && this.state.password.trim().length) > 0 &&
<View style={firstRunStyle.passwordStrength}>
<BarPasswordStrengthDisplay
width={Dimensions.get('window').width - 80}
minLength={1}
password={this.state.password} />
</View>}
<Text style={firstRunStyle.infoParagraph}>Note: for wallet security purposes, LBRY is unable to reset your password.</Text>
<View style={rewardStyle.buttonContainer}>
{!this.state.syncApplyStarted &&
<Button
style={rewardStyle.verificationButton}
theme={"light"}
text={"Enable sync"}
onPress={this.onEnableSyncPressed} />}
{syncApplyIsPending &&
<View style={firstRunStyle.centerInside}>
<ActivityIndicator size={"small"} color={Colors.White} />
</View>}
</View>
</View>
);
}
return (
<View style={firstRunStyle.container}>
{content}
</View>
);
}
}
export default SyncVerifyPage;

View file

@ -15,6 +15,7 @@ import Constants from 'constants';
import EmailVerifyPage from './internal/email-verify-page'; import EmailVerifyPage from './internal/email-verify-page';
import ManualVerifyPage from './internal/manual-verify-page'; import ManualVerifyPage from './internal/manual-verify-page';
import PhoneVerifyPage from './internal/phone-verify-page'; import PhoneVerifyPage from './internal/phone-verify-page';
import SyncVerifyPage from './internal/sync-verify-page';
import firstRunStyle from 'styles/firstRun'; import firstRunStyle from 'styles/firstRun';
class VerificationScreen extends React.PureComponent { class VerificationScreen extends React.PureComponent {
@ -43,7 +44,8 @@ class VerificationScreen extends React.PureComponent {
} }
checkVerificationStatus = (user) => { checkVerificationStatus = (user) => {
const { navigation } = this.props; const { deviceWalletSynced, navigation } = this.props;
const { syncFlow } = navigation.state.params;
this.setState({ this.setState({
isEmailVerified: (user && user.primary_email && user.has_verified_email), isEmailVerified: (user && user.primary_email && user.has_verified_email),
@ -53,11 +55,23 @@ class VerificationScreen extends React.PureComponent {
if (!this.state.isEmailVerified) { if (!this.state.isEmailVerified) {
this.setState({ currentPage: 'emailVerify' }); this.setState({ currentPage: 'emailVerify' });
} }
if (this.state.isEmailVerified && !this.state.isIdentityVerified) {
this.setState({ currentPage: 'phoneVerify' }); if (syncFlow) {
if (this.state.isEmailVerified && !deviceWalletSynced) {
this.setState({ currentPage: 'syncVerify' });
}
} else {
if (this.state.isEmailVerified && !this.state.isIdentityVerified) {
this.setState({ currentPage: 'phoneVerify' });
}
if (this.state.isEmailVerified && this.state.isIdentityVerified && !this.state.isRewardApproved) {
this.setState({ currentPage: 'manualVerify' });
}
} }
if (this.state.isEmailVerified && this.state.isIdentityVerified && !this.state.isRewardApproved) {
this.setState({ currentPage: 'manualVerify' }); if (this.state.isEmailVerified && syncFlow && deviceWalletSynced) {
navigation.goBack();
return;
} }
if (this.state.isEmailVerified && this.state.isIdentityVerified && this.state.isRewardApproved) { if (this.state.isEmailVerified && this.state.isIdentityVerified && this.state.isRewardApproved) {
@ -81,18 +95,29 @@ class VerificationScreen extends React.PureComponent {
render() { render() {
const { const {
addUserEmail, addUserEmail,
checkSync,
emailNewErrorMessage, emailNewErrorMessage,
emailNewPending, emailNewPending,
emailToVerify, emailToVerify,
getSync,
navigation, navigation,
notify, notify,
addUserPhone, addUserPhone,
getSyncIsPending,
hasSyncedWallet,
setSyncIsPending,
syncApplyIsPending,
syncApplyErrorMessage,
syncApply,
syncData,
syncHash,
phone, phone,
phoneVerifyIsPending, phoneVerifyIsPending,
phoneVerifyErrorMessage, phoneVerifyErrorMessage,
phoneNewIsPending, phoneNewIsPending,
phoneNewErrorMessage, phoneNewErrorMessage,
resendVerificationEmail, resendVerificationEmail,
setClientSetting,
verifyPhone verifyPhone
} = this.props; } = this.props;
@ -111,6 +136,7 @@ class VerificationScreen extends React.PureComponent {
/> />
); );
break; break;
case 'phoneVerify': case 'phoneVerify':
page = ( page = (
<PhoneVerifyPage <PhoneVerifyPage
@ -126,10 +152,33 @@ class VerificationScreen extends React.PureComponent {
/> />
); );
break; break;
case 'syncVerify':
page = (
<SyncVerifyPage
checkSync={checkSync}
getSync={getSync}
getSyncIsPending={getSyncIsPending}
hasSyncedWallet={hasSyncedWallet}
navigation={navigation}
notify={notify}
setEmailVerificationPhase={this.setEmailVerificationPhase}
setClientSetting={setClientSetting}
setSyncIsPending={setSyncIsPending}
syncApplyIsPending={syncApplyIsPending}
syncApplyErrorMessage={syncApplyErrorMessage}
syncApply={syncApply}
syncData={syncData}
syncHash={syncHash}
/>
);
break;
case 'manualVerify': case 'manualVerify':
page = ( page = (
<ManualVerifyPage setEmailVerificationPhase={this.setEmailVerificationPhase} /> <ManualVerifyPage setEmailVerificationPhase={this.setEmailVerificationPhase} />
); );
break;
} }
return ( return (

View file

@ -1,24 +1,27 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doSetClientSetting } from 'redux/actions/settings'; import { doSetClientSetting } from 'redux/actions/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doPushDrawerStack } from 'redux/actions/drawer'; import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
import { selectBalance } from 'lbry-redux'; import { selectBalance } from 'lbry-redux';
import { doGetSync, selectUser } from 'lbryinc'; import { doCheckSync, doGetSync, selectUser, selectHasSyncedWallet } from 'lbryinc';
import Constants from 'constants'; import Constants from 'constants';
import WalletPage from './view'; import WalletPage from './view';
const select = state => ({ const select = state => ({
user: selectUser(state), user: selectUser(state),
balance: selectBalance(state), balance: selectBalance(state),
hasSyncedWallet: selectHasSyncedWallet(state),
understandsRisks: makeSelectClientSetting(Constants.SETTING_ALPHA_UNDERSTANDS_RISKS)(state), understandsRisks: makeSelectClientSetting(Constants.SETTING_ALPHA_UNDERSTANDS_RISKS)(state),
backupDismissed: makeSelectClientSetting(Constants.SETTING_BACKUP_DISMISSED)(state), backupDismissed: makeSelectClientSetting(Constants.SETTING_BACKUP_DISMISSED)(state),
rewardsNotInterested: makeSelectClientSetting(Constants.SETTING_REWARDS_NOT_INTERESTED)(state), rewardsNotInterested: makeSelectClientSetting(Constants.SETTING_REWARDS_NOT_INTERESTED)(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
checkSync: () => dispatch(doCheckSync()),
getSync: password => dispatch(doGetSync(password)), getSync: password => dispatch(doGetSync(password)),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)), setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_WALLET)) pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_WALLET)),
setPlayerVisible: () => dispatch(doSetPlayerVisible(false))
}); });
export default connect(select, perform)(WalletPage); export default connect(select, perform)(WalletPage);

View file

@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import { NativeModules, ScrollView, Text, View } from 'react-native'; import { NativeModules, ScrollView, Text, View } from 'react-native';
import TransactionListRecent from 'component/transactionListRecent'; import TransactionListRecent from 'component/transactionListRecent';
import WalletRewardsDriver from 'component/walletRewardsDriver';
import WalletAddress from 'component/walletAddress'; import WalletAddress from 'component/walletAddress';
import WalletBalance from 'component/walletBalance'; import WalletBalance from 'component/walletBalance';
import WalletSend from 'component/walletSend'; import WalletSend from 'component/walletSend';
import WalletRewardsDriver from 'component/walletRewardsDriver';
import WalletSyncDriver from 'component/walletSyncDriver';
import Button from 'component/button'; import Button from 'component/button';
import Link from 'component/link'; import Link from 'component/link';
import UriBar from 'component/uriBar'; import UriBar from 'component/uriBar';
@ -13,11 +14,17 @@ import walletStyle from 'styles/wallet';
class WalletPage extends React.PureComponent { class WalletPage extends React.PureComponent {
componentDidMount() { componentDidMount() {
this.props.pushDrawerStack(); const { pushDrawerStack, setPlayerVisible } = this.props;
pushDrawerStack();
setPlayerVisible();
const { user, getSync } = this.props; const { getSync, user } = this.props;
if (user && user.has_verified_email) { if (user && user.has_verified_email) {
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => getSync(walletPassword)); NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => {
if (walletPassword && walletPassword.trim().length > 0) {
getSync(walletPassword);
}
});
} }
} }
@ -30,6 +37,7 @@ class WalletPage extends React.PureComponent {
const { const {
balance, balance,
backupDismissed, backupDismissed,
hasSyncedWallet,
rewardsNotInterested, rewardsNotInterested,
understandsRisks, understandsRisks,
setClientSetting, setClientSetting,
@ -41,8 +49,15 @@ class WalletPage extends React.PureComponent {
<View> <View>
<UriBar navigation={navigation} /> <UriBar navigation={navigation} />
<View style={walletStyle.warning}> <View style={walletStyle.warning}>
<Text style={walletStyle.warningParagraph}>
This is beta software. You may lose any credits that you send to your wallet due to software bugs, deleted files, or malicious third-party software. You should not use this wallet as your primary wallet.
</Text>
{!hasSyncedWallet &&
<Text style={walletStyle.warningParagraph}>
If you are not using the LBRY sync service, you will lose all of your credits if you uninstall this application. Instructions on how to enroll as well as how to backup your wallet manually are available on the next page.
</Text>}
<Text style={walletStyle.warningText}> <Text style={walletStyle.warningText}>
This is beta software. You may lose any LBC that you send to your wallet due to uninstallation, software bugs, deleted files, or malicious third-party software. You should not use this wallet as your primary wallet. If you understand the risks and you wish to continue, please tap the button below. If you understand the risks and you wish to continue, please tap the button below.
</Text> </Text>
</View> </View>
<Button text={'I understand the risks'} style={[walletStyle.button, walletStyle.understand]} <Button text={'I understand the risks'} style={[walletStyle.button, walletStyle.understand]}
@ -55,14 +70,7 @@ class WalletPage extends React.PureComponent {
<View style={walletStyle.container}> <View style={walletStyle.container}>
<UriBar navigation={navigation} /> <UriBar navigation={navigation} />
<ScrollView style={walletStyle.scrollContainer} keyboardShouldPersistTaps={'handled'}> <ScrollView style={walletStyle.scrollContainer} keyboardShouldPersistTaps={'handled'}>
{!backupDismissed && <WalletSyncDriver navigation={navigation} />
<View style={walletStyle.warningCard}>
<Text style={walletStyle.warningText}>
Please backup your wallet file using the instructions at <Link style={walletStyle.warningText} text="https://lbry.com/faq/how-to-backup-wallet#android" href="https://lbry.com/faq/how-to-backup-wallet#android" />.
</Text>
<Button text={'Dismiss'} style={walletStyle.button} onPress={this.onDismissBackupPressed} />
</View>}
{(!rewardsNotInterested) && (!balance || balance === 0) && <WalletRewardsDriver navigation={navigation} />} {(!rewardsNotInterested) && (!balance || balance === 0) && <WalletRewardsDriver navigation={navigation} />}
<WalletBalance /> <WalletBalance />
<WalletAddress /> <WalletAddress />

View file

@ -8,3 +8,8 @@ export const doPushDrawerStack = (routeName) => (dispatch) => dispatch({
export const doPopDrawerStack = () => (dispatch) => dispatch({ export const doPopDrawerStack = () => (dispatch) => dispatch({
type: Constants.ACTION_POP_DRAWER_STACK type: Constants.ACTION_POP_DRAWER_STACK
}); });
export const doSetPlayerVisible = (visible) => (dispatch) => dispatch({
type: Constants.ACTION_SET_PLAYER_VISIBLE,
data: { visible }
});

View file

@ -1,112 +1,8 @@
import { import { ACTIONS, Lbry } from 'lbry-redux';
ACTIONS, import { doClaimEligiblePurchaseRewards } from 'lbryinc';
Lbry,
doToast,
formatCredits,
selectBalance,
makeSelectFileInfoForUri,
makeSelectMetadataForUri,
selectDownloadingByOutpoint,
} from 'lbry-redux';
import { doClaimEligiblePurchaseRewards, makeSelectCostInfoForUri } from 'lbryinc';
import { Alert, NativeModules } from 'react-native'; import { Alert, NativeModules } from 'react-native';
import Constants from 'constants';
const DOWNLOAD_POLL_INTERVAL = 250; export function doStartDownload(uri, outpoint, fileInfo) {
const deleteBlobsForSdHash = (sdHash) => {
Lbry.blob_list({ sd_hash: sdHash }).then(hashes => {
hashes.filter(hash => hash != sdHash).forEach(hash => {
Lbry.blob_delete({ blob_hash: hash });
});
});
};
export function doUpdateLoadStatus(uri, outpoint) {
return (dispatch, getState) => {
const state = getState();
Lbry.file_list({
outpoint,
full_status: true,
}).then(([fileInfo]) => {
if (!fileInfo || fileInfo.written_bytes === 0) {
// if the outpoint isn't in the state, then it was probably canceled, so stop checking the load status
const { downloadingByOutpoint = {} } = state.fileInfo;
if (!downloadingByOutpoint[outpoint]) return;
// download hasn't started yet
setTimeout(() => {
dispatch(doUpdateLoadStatus(uri, outpoint));
}, DOWNLOAD_POLL_INTERVAL);
} else if (fileInfo.completed) {
// TODO this isn't going to get called if they reload the client before
// the download finished
const { total_bytes: totalBytes, written_bytes: writtenBytes } = fileInfo;
dispatch({
type: ACTIONS.DOWNLOADING_COMPLETED,
data: {
uri,
outpoint,
fileInfo,
},
});
if (NativeModules.LbryDownloadManager) {
NativeModules.LbryDownloadManager.updateDownload(
uri,
fileInfo.file_name ? fileInfo.file_name : '',
100,
writtenBytes ? writtenBytes : 0,
totalBytes ? totalBytes : 0
);
}
// Once a download has been completed, delete the individual blob files to save space
Lbry.file_set_status({ status: 'stop', sd_hash: fileInfo.sd_hash }).then(() => {
deleteBlobsForSdHash(fileInfo.sd_hash);
}).catch(() => {
deleteBlobsForSdHash(fileInfo.sd_hash);
});
/*const notif = new window.Notification('LBRY Download Complete', {
body: fileInfo.metadata.stream.metadata.title,
silent: false,
});*/
} else {
// ready to play
const { total_bytes: totalBytes, written_bytes: writtenBytes } = fileInfo;
const progress = writtenBytes / totalBytes * 100;
dispatch({
type: ACTIONS.DOWNLOADING_PROGRESSED,
data: {
uri,
outpoint,
fileInfo,
progress,
},
});
if (NativeModules.LbryDownloadManager) {
NativeModules.LbryDownloadManager.updateDownload(
uri,
fileInfo.file_name ? fileInfo.file_name : '',
progress ? progress : 0,
writtenBytes ? writtenBytes : 0,
totalBytes ? totalBytes: 0
);
}
setTimeout(() => {
dispatch(doUpdateLoadStatus(uri, outpoint));
}, DOWNLOAD_POLL_INTERVAL);
}
});
};
}
export function doStartDownload(uri, outpoint) {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
@ -118,22 +14,45 @@ export function doStartDownload(uri, outpoint) {
if (downloadingByOutpoint[outpoint]) return; if (downloadingByOutpoint[outpoint]) return;
Lbry.file_list({ outpoint, full_status: true }).then(([fileInfo]) => { dispatch({
type: ACTIONS.DOWNLOADING_STARTED,
data: {
uri,
outpoint,
fileInfo,
},
});
dispatch(doClaimEligiblePurchaseRewards());
};
}
export function doUpdateDownload(uri, outpoint, fileInfo, progress) {
return (dispatch) => {
dispatch({
type: ACTIONS.DOWNLOADING_PROGRESSED,
data: {
uri,
outpoint,
fileInfo,
progress,
},
});
};
}
export function doCompleteDownload(uri, outpoint, fileInfo) {
return (dispatch) => {
if (fileInfo.completed) {
dispatch({ dispatch({
type: ACTIONS.DOWNLOADING_STARTED, type: ACTIONS.DOWNLOADING_COMPLETED,
data: { data: {
uri, uri,
outpoint, outpoint,
fileInfo, fileInfo,
}, },
}); });
}
if (NativeModules.LbryDownloadManager) {
NativeModules.LbryDownloadManager.startDownload(uri, fileInfo.file_name ? fileInfo.file_name : '');
}
dispatch(doUpdateLoadStatus(uri, outpoint));
});
}; };
} }
@ -155,22 +74,10 @@ export function doStopDownloadingFile(uri, fileInfo) {
// Should also delete the file after the user stops downloading // Should also delete the file after the user stops downloading
dispatch(doDeleteFile(fileInfo.outpoint, uri)); dispatch(doDeleteFile(fileInfo.outpoint, uri));
if (NativeModules.LbryDownloadManager) {
NativeModules.LbryDownloadManager.stopDownload(uri, fileInfo.file_name ? fileInfo.file_name : '');
}
}); });
}; };
} }
export function doDownloadFile(uri, streamInfo) {
return dispatch => {
const { outpoint } = streamInfo;
dispatch(doStartDownload(uri, outpoint));
dispatch(doClaimEligiblePurchaseRewards());
};
}
export function doSetPlayingUri(uri) { export function doSetPlayingUri(uri) {
return dispatch => { return dispatch => {
dispatch({ dispatch({
@ -180,133 +87,6 @@ export function doSetPlayingUri(uri) {
}; };
} }
export function doLoadVideo(uri, failureCallback) {
return dispatch => {
dispatch({
type: ACTIONS.LOADING_VIDEO_STARTED,
data: {
uri,
},
});
Lbry.get({ uri })
.then(streamInfo => {
const timeout =
streamInfo === null || typeof streamInfo !== 'object' || streamInfo.error === 'Timeout';
if (timeout) {
dispatch(doSetPlayingUri(null));
dispatch({
type: ACTIONS.LOADING_VIDEO_FAILED,
data: { uri },
});
dispatch(doToast({
message: `File timeout for uri ${uri}`,
}));
if (failureCallback) {
failureCallback();
}
} else {
dispatch(doDownloadFile(uri, streamInfo));
}
})
.catch(() => {
dispatch(doSetPlayingUri(null));
dispatch({
type: ACTIONS.LOADING_VIDEO_FAILED,
data: { uri },
});
dispatch(doToast({
message: `Failed to download ${uri}, please try again. If this problem persists, visit https://lbry.com/faq/support for support.`,
}));
if (failureCallback) {
failureCallback();
}
});
};
}
export function doPurchaseUri(uri, specificCostInfo, failureCallback) {
return (dispatch, getState) => {
const state = getState();
const balance = selectBalance(state);
const fileInfo = makeSelectFileInfoForUri(uri)(state);
const metadata = makeSelectMetadataForUri(uri)(state);
const title = metadata ? metadata.title : uri;
const downloadingByOutpoint = selectDownloadingByOutpoint(state);
const alreadyDownloading = fileInfo && !!downloadingByOutpoint[fileInfo.outpoint];
function attemptPlay(cost, instantPurchaseMax = null) {
if (cost > 0 && (!instantPurchaseMax || cost > instantPurchaseMax)) {
// display alert
const formattedCost = formatCredits(cost, 2);
const unit = cost === 1 ? 'credit' : 'credits';
Alert.alert('Confirm purchase',
`This will purchase "${title}" for ${formattedCost} ${unit}`,
[
{ text: 'OK', onPress: () => dispatch(doLoadVideo(uri)) },
{ text: 'Cancel', style: 'cancel', onPress: () => failureCallback && failureCallback() }
],
{ cancelable: true });
} else {
dispatch(doLoadVideo(uri, failureCallback));
}
}
// we already fully downloaded the file.
if (fileInfo && fileInfo.completed) {
// If written_bytes is false that means the user has deleted/moved the
// file manually on their file system, so we need to dispatch a
// doLoadVideo action to reconstruct the file from the blobs
if (!fileInfo.written_bytes) dispatch(doLoadVideo(uri));
Promise.resolve();
return;
}
// we are already downloading the file
if (alreadyDownloading) {
Promise.resolve();
return;
}
const costInfo = makeSelectCostInfoForUri(uri)(state) || specificCostInfo;
const { cost } = costInfo;
if (cost > balance) {
dispatch(doSetPlayingUri(null));
dispatch(doToast({
message: 'Insufficient credits',
}));
if (failureCallback) {
failureCallback();
}
Promise.resolve();
return;
}
attemptPlay(cost);
/*if (cost === 0 || !makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state)) {
attemptPlay(cost);
} else {
const instantPurchaseMax = makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state);
if (instantPurchaseMax.currency === 'LBC') {
attemptPlay(cost, instantPurchaseMax.amount);
} else {
// Need to convert currency of instant purchase maximum before trying to play
Lbryio.getExchangeRates().then(({ LBC_USD }) => {
attemptPlay(cost, instantPurchaseMax.amount / LBC_USD);
});
}
}*/
};
}
export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) { export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) {
return (dispatch, getState) => { return (dispatch, getState) => {
Lbry.file_delete({ Lbry.file_delete({
@ -334,29 +114,5 @@ export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) {
outpoint, outpoint,
}, },
}); });
//const totalProgress = selectTotalDownloadProgress(getState());
//setProgressBar(totalProgress);
};
}
export function doDeleteCompleteBlobs() {
return dispatch => {
dispatch({
type: Constants.ACTION_DELETE_COMPLETED_BLOBS,
data: {},
});
Lbry.file_list().then(files => {
files.forEach(fileInfo => {
if (fileInfo.completed) {
Lbry.file_set_status({ status: 'stop', sd_hash: fileInfo.sd_hash }).then(() => {
deleteBlobsForSdHash(fileInfo.sd_hash);
}).catch(() => {
deleteBlobsForSdHash(fileInfo.sd_hash);
});
}
});
});
}; };
} }

View file

@ -2,9 +2,15 @@ import Constants from 'constants';
const reducers = {}; const reducers = {};
const defaultState = { const defaultState = {
stack: [ Constants.DRAWER_ROUTE_DISCOVER ] // Discover is always the first drawer route stack: [ Constants.DRAWER_ROUTE_DISCOVER ], // Discover is always the first drawer route
playerVisible: false
}; };
reducers[Constants.ACTION_SET_PLAYER_VISIBLE] = (state, action) =>
Object.assign({}, state, {
playerVisible: action.data.visible
});
reducers[Constants.ACTION_PUSH_DRAWER_STACK] = (state, action) => { reducers[Constants.ACTION_PUSH_DRAWER_STACK] = (state, action) => {
const routeName = action.data; const routeName = action.data;
const newStack = state.stack.slice(); const newStack = state.stack.slice();

View file

@ -2,9 +2,11 @@ import { createSelector } from 'reselect';
export const selectState = state => state.drawer || {}; export const selectState = state => state.drawer || {};
export const selectDrawerStack = createSelector(selectState, (state) => state.stack); export const selectDrawerStack = createSelector(selectState, state => state.stack);
export const selectLastDrawerRoute = createSelector(selectState, (state) => { export const selectIsPlayerVisible = createSelector(selectState, state => state.playerVisible);
export const selectLastDrawerRoute = createSelector(selectState, state => {
if (state.stack.length) { if (state.stack.length) {
return state.stack[state.stack.length - 1]; return state.stack[state.stack.length - 1];
} }

View file

@ -6,8 +6,8 @@ const Colors = {
DarkGrey: '#555555', DarkGrey: '#555555',
DescriptionGrey: '#999999', DescriptionGrey: '#999999',
LbryGreen: '#2f9176', LbryGreen: '#2f9176',
BrightGreen: '#61fcd8',
BrighterLbryGreen: '#40b887', BrighterLbryGreen: '#40b887',
NextLbryGreen: '#38d9a9',
LightGrey: '#cccccc', LightGrey: '#cccccc',
LighterGrey: '#e5e5e5', LighterGrey: '#e5e5e5',
Orange: '#ffbb00', Orange: '#ffbb00',

View file

@ -77,7 +77,7 @@ const discoverStyle = StyleSheet.create({
top: 8 top: 8
}, },
filePriceContainer: { filePriceContainer: {
backgroundColor: Colors.BrightGreen, backgroundColor: Colors.NextLbryGreen,
justifyContent: 'center', justifyContent: 'center',
position: 'absolute', position: 'absolute',
right: 8, right: 8,

View file

@ -1,4 +1,5 @@
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import Colors from 'styles/colors';
const fileDownloadButtonStyle = StyleSheet.create({ const fileDownloadButtonStyle = StyleSheet.create({
container: { container: {
@ -7,11 +8,11 @@ const fileDownloadButtonStyle = StyleSheet.create({
height: 36, height: 36,
borderRadius: 18, borderRadius: 18,
justifyContent: 'center', justifyContent: 'center',
backgroundColor: '#40c0a9', backgroundColor: Colors.LbryGreen,
}, },
text: { text: {
fontFamily: 'Inter-UI-Medium', fontFamily: 'Inter-UI-Medium',
color: '#ffffff', color: Colors.White,
fontSize: 14, fontSize: 14,
textAlign: 'center' textAlign: 'center'
} }

View file

@ -58,6 +58,9 @@ const filePageStyle = StyleSheet.create({
fontSize: 16, fontSize: 16,
flex: 18 flex: 18
}, },
titleTouch: {
flex: 1,
},
titleRow: { titleRow: {
flexDirection: 'row', flexDirection: 'row',
marginTop: 12, marginTop: 12,
@ -159,7 +162,7 @@ const filePageStyle = StyleSheet.create({
bottom: 0 bottom: 0
}, },
filePriceContainer: { filePriceContainer: {
backgroundColor: '#61fcd8', backgroundColor: Colors.NextLbryGreen,
justifyContent: 'center', justifyContent: 'center',
position: 'absolute', position: 'absolute',
right: 16, right: 16,
@ -239,8 +242,8 @@ const filePageStyle = StyleSheet.create({
flex: 1, flex: 1,
left: 0, left: 0,
right: 0, right: 0,
top: 0, top: 60,
bottom: 60, bottom: 0,
zIndex: 100 zIndex: 100
}, },
link: { link: {
@ -286,7 +289,9 @@ const filePageStyle = StyleSheet.create({
}, },
currency: { currency: {
alignSelf: 'flex-start', alignSelf: 'flex-start',
marginTop: 17 fontSize: 12,
marginTop: 15,
marginLeft: 4
}, },
descriptionToggle: { descriptionToggle: {
alignItems: 'center', alignItems: 'center',
@ -320,6 +325,36 @@ const filePageStyle = StyleSheet.create({
}, },
tagItem: { tagItem: {
marginRight: 16 marginRight: 16
},
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
},
backButton: {
position: 'absolute',
left: 0,
top: 0,
width: 48,
height: 48,
alignItems: 'center',
justifyContent: 'center'
},
backButtonIcon: {
color: Colors.White
} }
}); });

View file

@ -33,6 +33,15 @@ const firstRunStyle = StyleSheet.create({
marginBottom: 20, marginBottom: 20,
color: Colors.White color: Colors.White
}, },
spacedParagraph: {
fontFamily: 'Inter-UI-Regular',
fontSize: 18,
lineHeight: 28,
marginLeft: 32,
marginRight: 32,
marginBottom: 20,
color: Colors.White
},
infoParagraph: { infoParagraph: {
fontFamily: 'Inter-UI-Regular', fontFamily: 'Inter-UI-Regular',
fontSize: 14, fontSize: 14,
@ -163,6 +172,13 @@ const firstRunStyle = StyleSheet.create({
marginRight: 32, marginRight: 32,
marginBottom: 48 marginBottom: 48
}, },
centered: {
alignItems: 'center'
},
centerInside: {
flex: 1,
alignItems:'center'
},
nowrap: { nowrap: {
flex: 1, flex: 1,
flexWrap: 'nowrap' flexWrap: 'nowrap'

View file

@ -74,7 +74,7 @@ const mediaPlayerStyle = StyleSheet.create({
height: 36, height: 36,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
right: 0, right: 4,
bottom: 14, bottom: 14,
}, },
elapsedDuration: { elapsedDuration: {
@ -82,15 +82,15 @@ const mediaPlayerStyle = StyleSheet.create({
position: 'absolute', position: 'absolute',
left: 8, left: 8,
bottom: 24, bottom: 24,
fontSize: 14, fontSize: 12,
color: '#ffffff' color: '#ffffff'
}, },
totalDuration: { totalDuration: {
fontFamily: 'Inter-UI-Regular', fontFamily: 'Inter-UI-Regular',
position: 'absolute', position: 'absolute',
right: 40, right: 48,
bottom: 24, bottom: 24,
fontSize: 14, fontSize: 12,
color: '#ffffff' color: '#ffffff'
}, },
seekerCircle: { seekerCircle: {
@ -136,6 +136,27 @@ const mediaPlayerStyle = StyleSheet.create({
height: 24, height: 24,
width: 24, width: 24,
backgroundColor: Colors.LbryGreen backgroundColor: Colors.LbryGreen
},
loadingContainer: {
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0
},
backButton: {
position: 'absolute',
left: 0,
top: 0,
width: 48,
height: 48,
alignItems: 'center',
justifyContent: 'center'
},
backButtonIcon: {
color: Colors.White
} }
}); });

View file

@ -29,6 +29,10 @@ const walletStyle = StyleSheet.create({
backgroundColor: Colors.LbryGreen, backgroundColor: Colors.LbryGreen,
alignSelf: 'flex-start' alignSelf: 'flex-start'
}, },
enrollButton: {
backgroundColor: Colors.White,
alignSelf: 'flex-start'
},
historyList: { historyList: {
backgroundColor: '#ffffff' backgroundColor: '#ffffff'
}, },
@ -51,12 +55,12 @@ const walletStyle = StyleSheet.create({
margin: 16 margin: 16
}, },
title: { title: {
fontFamily: 'Inter-UI-Bold', fontFamily: 'Inter-UI-SemiBold',
fontSize: 20, fontSize: 20,
marginBottom: 24 marginBottom: 24
}, },
transactionsTitle: { transactionsTitle: {
fontFamily: 'Inter-UI-Bold', fontFamily: 'Inter-UI-SemiBold',
fontSize: 20 fontSize: 20
}, },
transactionsHeader: { transactionsHeader: {
@ -93,8 +97,8 @@ const walletStyle = StyleSheet.create({
}, },
balanceTitle: { balanceTitle: {
color: '#ffffff', color: '#ffffff',
fontFamily: 'Inter-UI-Bold', fontFamily: 'Inter-UI-SemiBold',
fontSize: 18, fontSize: 20,
marginLeft: 16, marginLeft: 16,
marginTop: 16 marginTop: 16
}, },
@ -141,6 +145,13 @@ const walletStyle = StyleSheet.create({
margin: 16, margin: 16,
marginTop: 76 marginTop: 76
}, },
warningParagraph: {
color: Colors.White,
fontFamily: 'Inter-UI-Regular',
fontSize: 16,
lineHeight: 24,
marginBottom: 16
},
warningText: { warningText: {
color: Colors.White, color: Colors.White,
fontFamily: 'Inter-UI-Regular', fontFamily: 'Inter-UI-Regular',
@ -156,7 +167,9 @@ const walletStyle = StyleSheet.create({
}, },
currency: { currency: {
alignSelf: 'flex-start', alignSelf: 'flex-start',
marginTop: 17 fontSize: 12,
marginTop: 16,
marginLeft: 4
}, },
sendButton: { sendButton: {
marginTop: 8 marginTop: 8
@ -185,6 +198,29 @@ const walletStyle = StyleSheet.create({
fontFamily: 'Inter-UI-Regular', fontFamily: 'Inter-UI-Regular',
fontSize: 14, fontSize: 14,
lineHeight: 16 lineHeight: 16
},
syncDriverCard: {
padding: 16,
backgroundColor: Colors.LbryGreen,
marginLeft: 16,
marginTop: 16,
marginRight: 16
},
syncDriverTitle: {
color: Colors.White,
fontFamily: 'Inter-UI-SemiBold',
fontSize: 20
},
syncDriverText: {
color: Colors.White,
fontFamily: 'Inter-UI-Regular',
fontSize: 14,
},
actionRow: {
marginTop: 20,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between'
} }
}); });

View file

@ -1,6 +1,6 @@
import { NavigationActions, StackActions } from 'react-navigation'; import { NavigationActions, StackActions } from 'react-navigation';
import { buildURI, isURIValid } from 'lbry-redux'; import { buildURI, isURIValid } from 'lbry-redux';
import { doPopDrawerStack, doPushDrawerStack } from 'redux/actions/drawer'; import { doPopDrawerStack, doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
import { DrawerRoutes } from 'constants'; import { DrawerRoutes } from 'constants';
import Constants from 'constants'; import Constants from 'constants';
@ -36,6 +36,7 @@ export function dispatchNavigateToUri(dispatch, nav, uri, isNavigatingBack) {
if (!isNavigatingBack) { if (!isNavigatingBack) {
dispatch(doPushDrawerStack(uri)); dispatch(doPushDrawerStack(uri));
dispatch(doSetPlayerVisible(true));
} }
if (nav && nav.routes && nav.routes.length > 0 && 'Main' === nav.routes[0].routeName) { if (nav && nav.routes && nav.routes.length > 0 && 'Main' === nav.routes[0].routeName) {
@ -120,6 +121,7 @@ export function navigateToUri(navigation, uri, additionalParams, isNavigatingBac
navigation.dispatch(stackAction); navigation.dispatch(stackAction);
if (store && store.dispatch && !isNavigatingBack) { if (store && store.dispatch && !isNavigatingBack) {
store.dispatch(doPushDrawerStack(uri)); store.dispatch(doPushDrawerStack(uri));
store.dispatch(doSetPlayerVisible(true));
} }
return; return;
} }
@ -127,6 +129,7 @@ export function navigateToUri(navigation, uri, additionalParams, isNavigatingBac
navigation.navigate({ routeName: 'File', key: uri, params }); navigation.navigate({ routeName: 'File', key: uri, params });
if (store && store.dispatch && !isNavigatingBack) { if (store && store.dispatch && !isNavigatingBack) {
store.dispatch(doPushDrawerStack(uri)); store.dispatch(doPushDrawerStack(uri));
store.dispatch(doSetPlayerVisible(true));
} }
} }

View file

@ -36,7 +36,7 @@ version.filename = %(source.dir)s/main.py
# (list) Application requirements # (list) Application requirements
# comma seperated e.g. requirements = sqlite3,kivy # comma seperated e.g. requirements = sqlite3,kivy
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/lbry.git@v0.36.0#egg=lbrynet, git+https://github.com/lbryio/aioupnp.git#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, git+https://github.com/lbryio/torba#egg=torba, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir 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/lbry.git@v0.37.2#egg=lbrynet, 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, git+https://github.com/lbryio/torba@397ebe842856a4297bea5281206f9fa40a84699c#egg=torba, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir
# (str) Custom source folders for requirements # (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes # Sets custom source for any requirements with recipes

View file

@ -36,7 +36,7 @@ version.filename = %(source.dir)s/main.py
# (list) Application requirements # (list) Application requirements
# comma seperated e.g. requirements = sqlite3,kivy # comma seperated e.g. requirements = sqlite3,kivy
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/lbry.git@v0.36.0#egg=lbrynet, git+https://github.com/lbryio/aioupnp.git#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, git+https://github.com/lbryio/torba#egg=torba, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir 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/lbry.git@v0.37.2#egg=lbrynet, 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, git+https://github.com/lbryio/torba@397ebe842856a4297bea5281206f9fa40a84699c#egg=torba, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir
# (str) Custom source folders for requirements # (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes # Sets custom source for any requirements with recipes

View file

@ -36,7 +36,7 @@ version.filename = %(source.dir)s/main.py
# (list) Application requirements # (list) Application requirements
# comma seperated e.g. requirements = sqlite3,kivy # comma seperated e.g. requirements = sqlite3,kivy
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/lbry.git@v0.36.0#egg=lbrynet, git+https://github.com/lbryio/aioupnp.git#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, git+https://github.com/lbryio/torba#egg=torba, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir 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/lbry.git@v0.37.2#egg=lbrynet, 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, git+https://github.com/lbryio/torba@397ebe842856a4297bea5281206f9fa40a84699c#egg=torba, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir
# (str) Custom source folders for requirements # (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes # Sets custom source for any requirements with recipes

View file

@ -2,5 +2,6 @@
<style name="LbryAppTheme" parent="@android:style/Theme.Material.Light"> <style name="LbryAppTheme" parent="@android:style/Theme.Material.Light">
<item name="android:windowBackground">@color/lbrygreen</item> <item name="android:windowBackground">@color/lbrygreen</item>
<item name="colorControlActivated">@color/white</item> <item name="colorControlActivated">@color/white</item>
<item name="colorAccent">@color/white</item>
</style> </style>
</resources> </resources>

View file

@ -0,0 +1,357 @@
package io.lbry.browser;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import io.lbry.browser.receivers.NotificationDeletedReceiver;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Random;
import java.util.List;
import java.util.ArrayList;
public class DownloadManager {
private Context context;
private List<String> activeDownloads = new ArrayList<String>();
private List<String> completedDownloads = new ArrayList<String>();
private HashMap<Integer, NotificationCompat.Builder> builders = new HashMap<Integer, NotificationCompat.Builder>();
private HashMap<String, Integer> downloadIdNotificationIdMap = new HashMap<String, Integer>();
private HashMap<String, Boolean> stoppedDownloadsMap = new HashMap<String, Boolean>();
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#");
private static final int MAX_FILENAME_LENGTH = 20;
private static final int MAX_PROGRESS = 100;
private static final String GROUP_DOWNLOADS = "io.lbry.browser.GROUP_DOWNLOADS";
private static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.DOWNLOADS_NOTIFICATION_CHANNEL";
private static boolean channelCreated = false;
private static NotificationCompat.Builder groupBuilder = null;
public static final String NOTIFICATION_ID_KEY = "io.lbry.browser.notificationId";
public static final String ACTION_DOWNLOAD_EVENT = "io.lbry.browser.ACTION_DOWNLOAD_EVENT";
public static final String ACTION_START = "start";
public static final String ACTION_COMPLETE = "complete";
public static final String ACTION_UPDATE = "update";
public static final int DOWNLOAD_NOTIFICATION_GROUP_ID = 20;
public static boolean groupCreated = false;
public DownloadManager(Context context) {
this.context = context;
}
private int generateNotificationId() {
int id = 0;
Random random = new Random();
do {
id = random.nextInt();
} while (id < 1000);
return id;
}
private void createNotificationChannel() {
// Only applies to Android 8.0 Oreo (API Level 26) or higher
if (!channelCreated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel(
NOTIFICATION_CHANNEL_ID, "LBRY Downloads", NotificationManager.IMPORTANCE_LOW);
channel.setDescription("LBRY file downloads");
notificationManager.createNotificationChannel(channel);
}
}
private void createNotificationGroup() {
if (!groupCreated) {
Intent intent = new Intent(context, NotificationDeletedReceiver.class);
intent.putExtra(NOTIFICATION_ID_KEY, DOWNLOAD_NOTIFICATION_GROUP_ID);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, DOWNLOAD_NOTIFICATION_GROUP_ID, intent, 0);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
groupBuilder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
groupBuilder.setContentTitle("Active LBRY downloads")
// contentText will be displayed if there are no notifications in the group
.setContentText("There are no active LBRY downloads.")
.setSmallIcon(android.R.drawable.stat_sys_download)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setGroup(GROUP_DOWNLOADS)
.setGroupSummary(true)
.setDeleteIntent(pendingIntent);
notificationManager.notify(DOWNLOAD_NOTIFICATION_GROUP_ID, groupBuilder.build());
groupCreated = true;
}
}
public static PendingIntent getLaunchPendingIntent(String uri, Context context) {
Intent launchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent intent = PendingIntent.getActivity(context, 0, launchIntent, 0);
return intent;
}
public void startDownload(String id, String filename) {
if (filename == null || filename.trim().length() == 0) {
return;
}
synchronized (this) {
if (!isDownloadActive(id)) {
activeDownloads.add(id);
}
createNotificationChannel();
createNotificationGroup();
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
// The file URI is used as the unique ID
builder.setContentIntent(getLaunchPendingIntent(id, context))
.setContentTitle(String.format("Downloading %s", truncateFilename(filename)))
.setGroup(GROUP_DOWNLOADS)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setProgress(MAX_PROGRESS, 0, false)
.setSmallIcon(android.R.drawable.stat_sys_download);
int notificationId = getNotificationId(id);
downloadIdNotificationIdMap.put(id, notificationId);
builders.put(notificationId, builder);
notificationManager.notify(notificationId, builder.build());
if (groupCreated && groupBuilder != null) {
groupBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
notificationManager.notify(DOWNLOAD_NOTIFICATION_GROUP_ID, groupBuilder.build());
}
}
}
public void updateDownload(String id, String filename, double writtenBytes, double totalBytes) {
if (filename == null || filename.trim().length() == 0) {
return;
}
synchronized (this) {
createNotificationChannel();
createNotificationGroup();
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
NotificationCompat.Builder builder = null;
int notificationId = getNotificationId(id);
if (builders.containsKey(notificationId)) {
builder = builders.get(notificationId);
} else {
builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
builder.setContentTitle(String.format("Downloading %s", truncateFilename(filename)))
.setPriority(NotificationCompat.PRIORITY_LOW);
builders.put(notificationId, builder);
}
double progress = (writtenBytes / totalBytes) * 100;
builder.setContentIntent(getLaunchPendingIntent(id, context))
.setContentText(String.format("%.0f%% (%s / %s)", progress, formatBytes(writtenBytes), formatBytes(totalBytes)))
.setGroup(GROUP_DOWNLOADS)
.setProgress(MAX_PROGRESS, new Double(progress).intValue(), false)
.setSmallIcon(android.R.drawable.stat_sys_download);
notificationManager.notify(notificationId, builder.build());
if (progress >= MAX_PROGRESS) {
builder.setContentTitle(String.format("Downloaded %s", truncateFilename(filename, 30)))
.setContentText(String.format("%s", formatBytes(totalBytes)))
.setGroup(GROUP_DOWNLOADS)
.setProgress(0, 0, false)
.setSmallIcon(android.R.drawable.stat_sys_download_done);
notificationManager.notify(notificationId, builder.build());
if (downloadIdNotificationIdMap.containsKey(id)) {
downloadIdNotificationIdMap.remove(id);
}
if (builders.containsKey(notificationId)) {
builders.remove(notificationId);
}
// If there are no more downloads and the group exists, set the icon to stop animating
if (groupCreated && groupBuilder != null && downloadIdNotificationIdMap.size() == 0) {
groupBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
notificationManager.notify(DOWNLOAD_NOTIFICATION_GROUP_ID, groupBuilder.build());
}
completeDownload(id, filename, totalBytes);
}
}
}
public void completeDownload(String id, String filename, double totalBytes) {
synchronized (this) {
if (isDownloadActive(id)) {
activeDownloads.remove(id);
}
if (!isDownloadCompleted(id)) {
completedDownloads.add(id);
}
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
NotificationCompat.Builder builder = null;
int notificationId = getNotificationId(id);
if (builders.containsKey(notificationId)) {
builder = builders.get(notificationId);
} else {
builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
builder.setPriority(NotificationCompat.PRIORITY_LOW);
builders.put(notificationId, builder);
}
builder.setContentTitle(String.format("Downloaded %s", truncateFilename(filename, 30)))
.setContentText(String.format("%s", formatBytes(totalBytes)))
.setGroup(GROUP_DOWNLOADS)
.setProgress(0, 0, false)
.setSmallIcon(android.R.drawable.stat_sys_download_done);
notificationManager.notify(notificationId, builder.build());
// If there are no more downloads and the group exists, set the icon to stop animating
checkGroupDownloadIcon(notificationManager);
}
}
public void abortDownload(String id) {
synchronized (this) {
if (downloadIdNotificationIdMap.containsKey(id)) {
removeDownloadNotification(id);
}
activeDownloads.remove(id);
}
}
public boolean isDownloadActive(String id) {
return (activeDownloads.contains(id));
}
public boolean isDownloadCompleted(String id) {
return (completedDownloads.contains(id));
}
public boolean hasActiveDownloads() {
return activeDownloads.size() > 0;
}
public List<String> getActiveDownloads() {
return activeDownloads;
}
public List<String> getCompletedDownloads() {
return completedDownloads;
}
public void deleteDownloadUri(String uri) {
synchronized (this) {
activeDownloads.remove(uri);
completedDownloads.remove(uri);
if (downloadIdNotificationIdMap.containsKey(uri)) {
removeDownloadNotification(uri);
}
}
}
private void removeDownloadNotification(String id) {
int notificationId = downloadIdNotificationIdMap.get(id);
if (downloadIdNotificationIdMap.containsKey(id)) {
downloadIdNotificationIdMap.remove(id);
}
if (builders.containsKey(notificationId)) {
builders.remove(notificationId);
}
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
NotificationCompat.Builder builder = builders.get(notificationId);
notificationManager.cancel(notificationId);
checkGroupDownloadIcon(notificationManager);
if (builders.values().size() == 0) {
notificationManager.cancel(DOWNLOAD_NOTIFICATION_GROUP_ID);
groupCreated = false;
}
}
private int getNotificationId(String id) {
if (downloadIdNotificationIdMap.containsKey(id)) {
return downloadIdNotificationIdMap.get(id);
}
int notificationId = generateNotificationId();
if (MainActivity.downloadNotificationIds != null &&
!MainActivity.downloadNotificationIds.contains(notificationId)) {
MainActivity.downloadNotificationIds.add(notificationId);
}
downloadIdNotificationIdMap.put(id, notificationId);
return notificationId;
}
private void checkGroupDownloadIcon(NotificationManagerCompat notificationManager) {
if (groupCreated && groupBuilder != null && downloadIdNotificationIdMap.size() == 0) {
groupBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
notificationManager.notify(DOWNLOAD_NOTIFICATION_GROUP_ID, groupBuilder.build());
}
}
private static String formatBytes(double bytes)
{
if (bytes < 1048576) { // < 1MB
return String.format("%s KB", DECIMAL_FORMAT.format(bytes / 1024.0));
}
if (bytes < 1073741824) { // < 1GB
return String.format("%s MB", DECIMAL_FORMAT.format(bytes / (1024.0 * 1024.0)));
}
return String.format("%s GB", DECIMAL_FORMAT.format(bytes / (1024.0 * 1024.0 * 1024.0)));
}
private static String truncateFilename(String filename, int alternateMaxLength) {
int maxLength = alternateMaxLength > 0 ? alternateMaxLength : MAX_FILENAME_LENGTH;
if (filename.length() < maxLength) {
return filename;
}
// Get the extension
int dotIndex = filename.lastIndexOf(".");
if (dotIndex > -1) {
String extension = filename.substring(dotIndex);
return String.format("%s...%s", filename.substring(0, maxLength - extension.length() - 4), extension);
}
return String.format("%s...", filename.substring(0, maxLength - 3));
}
private static String truncateFilename(String filename) {
return truncateFilename(filename, 0);
}
}

View file

@ -9,6 +9,7 @@ import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Binder; import android.os.Binder;
@ -17,12 +18,30 @@ import android.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.util.Log; import android.util.Log;
import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.DataOutputStream;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.json.JSONObject;
import org.json.JSONArray;
import org.json.JSONException;
import org.kivy.android.PythonService; import org.kivy.android.PythonService;
import org.renpy.android.AssetExtract; import org.renpy.android.AssetExtract;
import org.renpy.android.ResourceManager; import org.renpy.android.ResourceManager;
@ -41,6 +60,12 @@ public class LbrynetService extends PythonService {
public static final String ACTION_STOP_SERVICE = "io.lbry.browser.ACTION_STOP_SERVICE"; public static final String ACTION_STOP_SERVICE = "io.lbry.browser.ACTION_STOP_SERVICE";
public static final String ACTION_CHECK_DOWNLOADS = "io.lbry.browser.ACTION_CHECK_DOWNLOADS";
public static final String ACTION_QUEUE_DOWNLOAD = "io.lbry.browser.ACTION_QUEUE_DOWNLOAD";
public static final String ACTION_DELETE_DOWNLOAD = "io.lbry.browser.ACTION_DELETE_DOWNLOAD";
public static final String GROUP_SERVICE = "io.lbry.browser.GROUP_SERVICE"; public static final String GROUP_SERVICE = "io.lbry.browser.GROUP_SERVICE";
public static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.DAEMON_NOTIFICATION_CHANNEL"; public static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.DAEMON_NOTIFICATION_CHANNEL";
@ -49,8 +74,22 @@ public class LbrynetService extends PythonService {
public static LbrynetService serviceInstance; public static LbrynetService serviceInstance;
private static final String SDK_URL = "http://127.0.0.1:5279";
private static final int SDK_POLL_INTERVAL = 500; // 500 milliseconds
private BroadcastReceiver stopServiceReceiver; private BroadcastReceiver stopServiceReceiver;
private BroadcastReceiver downloadReceiver;
private DownloadManager downloadManager;
private ScheduledExecutorService taskExecutor;
private ScheduledFuture taskExecutorHandle = null;
private boolean streamManagerReady = false;
@Override @Override
public boolean canDisplayNotification() { public boolean canDisplayNotification() {
return true; return true;
@ -70,6 +109,29 @@ public class LbrynetService extends PythonService {
} }
}; };
registerReceiver(stopServiceReceiver, intentFilter); registerReceiver(stopServiceReceiver, intentFilter);
IntentFilter downloadFilter = new IntentFilter();
downloadFilter.addAction(ACTION_CHECK_DOWNLOADS);
downloadFilter.addAction(ACTION_DELETE_DOWNLOAD);
downloadFilter.addAction(ACTION_QUEUE_DOWNLOAD);
downloadReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (ACTION_QUEUE_DOWNLOAD.equals(action)) {
String outpoint = intent.getStringExtra("outpoint");
if (outpoint != null && outpoint.trim().length() > 0) {
LbrynetService.this.queueDownload(outpoint);
}
} else if (ACTION_DELETE_DOWNLOAD.equals(action)) {
String uri = intent.getStringExtra("uri");
LbrynetService.this.deleteDownload(uri);
} else if (ACTION_CHECK_DOWNLOADS.equals(action)) {
LbrynetService.this.checkDownloads();
}
}
};
registerReceiver(downloadReceiver, downloadFilter);
} }
@Override @Override
@ -78,6 +140,7 @@ public class LbrynetService extends PythonService {
String serviceDescription = "The LBRY service is running in the background."; String serviceDescription = "The LBRY service is running in the background.";
Context context = getApplicationContext(); Context context = getApplicationContext();
downloadManager = new DownloadManager(context);
NotificationManager notificationManager = NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -118,6 +181,286 @@ public class LbrynetService extends PythonService {
startForeground(1, notification); startForeground(1, notification);
} }
private void checkDownloads() {
if (taskExecutor == null) {
taskExecutor = Executors.newScheduledThreadPool(1);
taskExecutorHandle = taskExecutor.scheduleAtFixedRate(new Runnable() {
public void run() {
LbrynetService.this.pollFileList();
}
}, 0, SDK_POLL_INTERVAL, TimeUnit.MILLISECONDS);
}
}
private String sdkCall(String method) throws ConnectException {
return sdkCall(method, null);
}
private String sdkCall(String method, Map<String, String> params) throws ConnectException {
BufferedReader reader = null;
DataOutputStream dos = null;
HttpURLConnection conn = null;
try {
JSONObject request = new JSONObject();
request.put("method", method);
if (params != null) {
JSONObject requestParams = new JSONObject();
for (Map.Entry<String, String> entry : params.entrySet()) {
requestParams.put(entry.getKey(), entry.getValue());
}
request.put("params", requestParams);
}
URL url = new URL(SDK_URL);
conn = (HttpURLConnection) url.openConnection();
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setUseCaches(false);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-type", "application/json");
dos = new DataOutputStream(conn.getOutputStream());
dos.writeBytes(request.toString());
dos.flush();
dos.close();
if (conn.getResponseCode() == 200) {
reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
StringBuilder sb = new StringBuilder();
String input;
while ((input = reader.readLine()) != null) {
sb.append(input);
}
return sb.toString();
} else {
reader = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
StringBuilder sb = new StringBuilder();
String error;
while ((error = reader.readLine()) != null) {
sb.append(error);
}
return sb.toString();
}
} catch (ConnectException ex) {
// sdk not started yet. rethrow
throw ex;
} catch (IOException ex) {
Log.e(TAG, ex.getMessage(), ex);
// ignore and continue
} catch (Exception ex) {
Log.e(TAG, ex.getMessage(), ex);
// ignore
} finally {
try {
if (reader != null) {
reader.close();
}
if (conn != null) {
conn.disconnect();
}
} catch (IOException ex) {
// pass
}
}
return null;
}
private void pollFileList() {
try {
if (!streamManagerReady) {
String statusResponse = sdkCall("status");
if (statusResponse != null) {
JSONObject status = new JSONObject(statusResponse);
if (status.has("error")) {
return;
}
if (status.has("result")) {
JSONObject result = status.getJSONObject("result");
if (result.has("startup_status")) {
JSONObject startupStatus = result.getJSONObject("startup_status");
streamManagerReady = startupStatus.has("stream_manager") && startupStatus.getBoolean("stream_manager");
}
}
}
}
if (streamManagerReady) {
String fileList = sdkCall("file_list");
if (fileList != null) {
JSONObject response = new JSONObject(fileList);
if (!response.has("error")) {
handlePollFileResponse(response);
}
}
}
} catch (ConnectException ex) {
// pass
} catch (JSONException ex) {
Log.e(TAG, ex.getMessage(), ex);
}
}
private void queueDownload(String outpoint) {
(new AsyncTask<Void, Void, String>() {
protected String doInBackground(Void... param) {
try {
Map<String, String> params = new HashMap<String, String>();
params.put("outpoint", outpoint);
return sdkCall("file_list", params);
} catch (ConnectException ex) {
return null;
}
}
protected void onPostExecute(String fileList) {
if (fileList != null) {
try {
JSONObject response = new JSONObject(fileList);
if (!response.has("error")) {
JSONArray fileItems = response.optJSONArray("result");
if (fileItems != null && fileItems.length() > 0) {
// TODO: Create Java FileItem class
JSONObject item = fileItems.getJSONObject(0);
String downloadPath = item.isNull("download_path") ? null : item.getString("download_path");
if (downloadPath == null || downloadPath.trim().length() == 0) {
return;
}
String claimId = item.getString("claim_id");
String claimName = item.getString("claim_name");
String uri = String.format("lbry://%s#%s", claimName, claimId);
if (!downloadManager.isDownloadActive(uri) && !downloadManager.isDownloadCompleted(uri)) {
File file = new File(downloadPath);
Intent intent = createDownloadEventIntent(uri, outpoint, item.toString());
intent.putExtra("action", "start");
downloadManager.startDownload(uri, file.getName());
Context context = getApplicationContext();
if (context != null) {
context.sendBroadcast(intent);
}
}
}
}
} catch (JSONException ex) {
// pass
}
}
checkDownloads();
}
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void deleteDownload(String uri) {
if (downloadManager.isDownloadActive(uri)) {
downloadManager.abortDownload(uri);
}
downloadManager.deleteDownloadUri(uri);
}
private void handlePollFileResponse(JSONObject response) {
Context context = getApplicationContext();
if (response.has("result")) {
JSONArray fileItems = response.optJSONArray("result");
if (fileItems != null) {
try {
//List<String> itemUris = new ArrayList<String>();
for (int i = 0; i < fileItems.length(); i++) {
JSONObject item = fileItems.getJSONObject(i);
String downloadPath = item.isNull("download_path") ? null : item.getString("download_path");
if (downloadPath == null || downloadPath.trim().length() == 0) {
continue;
}
String claimId = item.getString("claim_id");
String claimName = item.getString("claim_name");
String uri = String.format("lbry://%s#%s", claimName, claimId);
boolean completed = item.getBoolean("completed");
double writtenBytes = item.optDouble("written_bytes", -1);
double totalBytes = item.optDouble("total_bytes", -1);
String outpoint = item.getString("outpoint");
if (downloadManager.isDownloadActive(uri) && (writtenBytes == -1 || totalBytes == -1)) {
// possibly deleted, abort the download
downloadManager.abortDownload(uri);
continue;
}
File file = new File(downloadPath);
Intent intent = createDownloadEventIntent(uri, outpoint, item.toString());
if (downloadManager.isDownloadActive(uri)) {
if (writtenBytes >= totalBytes || completed) {
// completed download
intent.putExtra("action", "complete");
downloadManager.completeDownload(uri, file.getName(), totalBytes);
} else {
intent.putExtra("action", "update");
intent.putExtra("progress", (writtenBytes / totalBytes) * 100);
downloadManager.updateDownload(uri, file.getName(), writtenBytes, totalBytes);
}
if (context != null) {
context.sendBroadcast(intent);
}
} else {
if (writtenBytes == -1 || writtenBytes >= totalBytes) {
// do not start a download that is considered completed
continue;
}
if (!completed && downloadPath != null) {
intent.putExtra("action", "start");
downloadManager.startDownload(uri, file.getName());
if (context != null) {
context.sendBroadcast(intent);
}
}
}
}
// check download manager uris and clear downloads that may have been cancelled / deleted
/*List<String> activeUris = downloadManager.getActiveDownloads();
for (int i = 0; i < activeUris.size(); i++) {
String activeUri = activeUris.get(i);
if (!itemUris.contains(activeUri)) {
downloadManager.abortDownload(activeUri);
fileListUris.remove(activeUri); // remove URIs from the session that may have been deleted
}
}*/
} catch (JSONException ex) {
// pass
Log.e(TAG, ex.getMessage(), ex);
}
}
}
if (!downloadManager.hasActiveDownloads()) {
// stop polling
if (taskExecutorHandle != null) {
taskExecutorHandle.cancel(true);
taskExecutorHandle = null;
}
if (taskExecutor != null) {
taskExecutor.shutdownNow();
taskExecutor = null;
}
}
}
private static Intent createDownloadEventIntent(String uri, String outpoint, String fileInfo) {
Intent intent = new Intent();
intent.setAction(DownloadManager.ACTION_DOWNLOAD_EVENT);
intent.putExtra("uri", uri);
intent.putExtra("outpoint", outpoint);
intent.putExtra("file_info", fileInfo);
return intent;
}
@Override @Override
public int startType() { public int startType() {
return START_STICKY; return START_STICKY;
@ -137,13 +480,19 @@ public class LbrynetService extends PythonService {
getApplicationContext(), "", LbrynetService.class, "lbrynetservice"); getApplicationContext(), "", LbrynetService.class, "lbrynetservice");
} }
// Register broadcast receiver // no need to iterate the checks repeatedly here, because this is service startup
checkDownloads();
return super.onStartCommand(intent, flags, startId); return super.onStartCommand(intent, flags, startId);
} }
@Override @Override
public void onDestroy() { public void onDestroy() {
if (downloadReceiver != null) {
unregisterReceiver(downloadReceiver);
downloadReceiver = null;
}
if (stopServiceReceiver != null) { if (stopServiceReceiver != null) {
unregisterReceiver(stopServiceReceiver); unregisterReceiver(stopServiceReceiver);
stopServiceReceiver = null; stopServiceReceiver = null;

View file

@ -28,6 +28,7 @@ import com.facebook.react.ReactRootView;
import com.facebook.react.ReactInstanceManager; import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.shell.MainReactPackage; import com.facebook.react.shell.MainReactPackage;
@ -39,16 +40,21 @@ import com.RNFetchBlob.RNFetchBlobPackage;
import io.lbry.browser.reactpackages.LbryReactPackage; import io.lbry.browser.reactpackages.LbryReactPackage;
import io.lbry.browser.reactmodules.BackgroundMediaModule; import io.lbry.browser.reactmodules.BackgroundMediaModule;
import io.lbry.browser.reactmodules.DownloadManagerModule;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.math.BigInteger; import java.math.BigInteger;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import org.json.JSONObject;
import org.json.JSONArray;
import org.json.JSONException;
public class MainActivity extends Activity implements DefaultHardwareBackBtnHandler { public class MainActivity extends Activity implements DefaultHardwareBackBtnHandler {
private static Activity currentActivity = null; private static Activity currentActivity = null;
@ -67,6 +73,8 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
private BroadcastReceiver stopServiceReceiver; private BroadcastReceiver stopServiceReceiver;
private BroadcastReceiver downloadEventReceiver;
private ReactRootView mReactRootView; private ReactRootView mReactRootView;
private ReactInstanceManager mReactInstanceManager; private ReactInstanceManager mReactInstanceManager;
@ -114,6 +122,9 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
// Register SMS receiver for handling verification texts // Register SMS receiver for handling verification texts
registerSmsReceiver(); registerSmsReceiver();
// Register the receiver to emit download events
registerDownloadEventReceiver();
// Start the daemon service if it is not started // Start the daemon service if it is not started
serviceRunning = isServiceRunning(LbrynetService.class); serviceRunning = isServiceRunning(LbrynetService.class);
if (!serviceRunning) { if (!serviceRunning) {
@ -142,6 +153,50 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
setContentView(mReactRootView); setContentView(mReactRootView);
} }
private void registerDownloadEventReceiver() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_EVENT);
downloadEventReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String downloadAction = intent.getStringExtra("action");
String uri = intent.getStringExtra("uri");
String outpoint = intent.getStringExtra("outpoint");
String fileInfoJson = intent.getStringExtra("file_info");
if (uri == null || outpoint == null || fileInfoJson == null) {
return;
}
try {
String eventName = null;
JSONObject json = new JSONObject(fileInfoJson);
WritableMap fileInfo = JSONObjectToMap(json);
WritableMap params = Arguments.createMap();
params.putString("uri", uri);
params.putString("outpoint", outpoint);
params.putMap("fileInfo", fileInfo);
if (DownloadManager.ACTION_UPDATE.equals(downloadAction)) {
double progress = intent.getDoubleExtra("progress", 0);
params.putDouble("progress", progress);
eventName = "onDownloadUpdated";
} else {
eventName = (DownloadManager.ACTION_START.equals(downloadAction)) ? "onDownloadStarted" : "onDownloadCompleted";
}
ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
if (reactContext != null) {
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params);
}
} catch (JSONException ex) {
// pass
}
}
};
registerReceiver(downloadEventReceiver, intentFilter);
}
private void registerStopReceiver() { private void registerStopReceiver() {
IntentFilter intentFilter = new IntentFilter(); IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(LbrynetService.ACTION_STOP_SERVICE); intentFilter.addAction(LbrynetService.ACTION_STOP_SERVICE);
@ -381,6 +436,11 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
smsReceiver = null; smsReceiver = null;
} }
if (downloadEventReceiver != null) {
unregisterReceiver(downloadEventReceiver);
downloadEventReceiver = null;
}
if (stopServiceReceiver != null) { if (stopServiceReceiver != null) {
unregisterReceiver(stopServiceReceiver); unregisterReceiver(stopServiceReceiver);
stopServiceReceiver = null; stopServiceReceiver = null;
@ -388,7 +448,7 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.cancel(BackgroundMediaModule.NOTIFICATION_ID); notificationManager.cancel(BackgroundMediaModule.NOTIFICATION_ID);
notificationManager.cancel(DownloadManagerModule.GROUP_ID); notificationManager.cancel(DownloadManager.DOWNLOAD_NOTIFICATION_GROUP_ID);
if (downloadNotificationIds != null) { if (downloadNotificationIds != null) {
for (int i = 0; i < downloadNotificationIds.size(); i++) { for (int i = 0; i < downloadNotificationIds.size(); i++) {
notificationManager.cancel(downloadNotificationIds.get(i)); notificationManager.cancel(downloadNotificationIds.get(i));
@ -478,4 +538,54 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
return false; return false;
} }
private static WritableMap JSONObjectToMap(JSONObject jsonObject) throws JSONException {
WritableMap map = Arguments.createMap();
Iterator<String> keys = jsonObject.keys();
while(keys.hasNext()) {
String key = keys.next();
Object value = jsonObject.get(key);
if (value instanceof JSONArray) {
map.putArray(key, JSONArrayToList((JSONArray) value));
} else if (value instanceof JSONObject) {
map.putMap(key, JSONObjectToMap((JSONObject) value));
} else if (value instanceof Boolean) {
map.putBoolean(key, (Boolean) value);
} else if (value instanceof Integer) {
map.putInt(key, (Integer) value);
} else if (value instanceof Double) {
map.putDouble(key, (Double) value);
} else if (value instanceof String) {
map.putString(key, (String) value);
} else {
map.putString(key, value.toString());
}
}
return map;
}
private static WritableArray JSONArrayToList(JSONArray jsonArray) throws JSONException {
WritableArray array = Arguments.createArray();
for(int i = 0; i < jsonArray.length(); i++) {
Object value = jsonArray.get(i);
if (value instanceof JSONArray) {
array.pushArray(JSONArrayToList((JSONArray) value));
} else if (value instanceof JSONObject) {
array.pushMap(JSONObjectToMap((JSONObject) value));
} else if (value instanceof Boolean) {
array.pushBoolean((Boolean) value);
} else if (value instanceof Integer) {
array.pushInt((Integer) value);
} else if (value instanceof Double) {
array.pushDouble((Double) value);
} else if (value instanceof String) {
array.pushString((String) value);
} else {
array.pushString(value.toString());
}
}
return array;
}
} }

View file

@ -1,299 +0,0 @@
package io.lbry.browser.reactmodules;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import io.lbry.browser.MainActivity;
import io.lbry.browser.R;
import io.lbry.browser.receivers.NotificationDeletedReceiver;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Random;
public class DownloadManagerModule extends ReactContextBaseJavaModule {
private Context context;
private HashMap<Integer, NotificationCompat.Builder> builders = new HashMap<Integer, NotificationCompat.Builder>();
private HashMap<String, Integer> downloadIdNotificationIdMap = new HashMap<String, Integer>();
private HashMap<String, Boolean> stoppedDownloadsMap = new HashMap<String, Boolean>();
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#");
private static final int MAX_FILENAME_LENGTH = 20;
private static final int MAX_PROGRESS = 100;
private static final String GROUP_DOWNLOADS = "io.lbry.browser.GROUP_DOWNLOADS";
private static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.DOWNLOADS_NOTIFICATION_CHANNEL";
private static boolean channelCreated = false;
public static final String NOTIFICATION_ID_KEY = "io.lbry.browser.notificationId";
public static final int GROUP_ID = 20;
private static NotificationCompat.Builder groupBuilder = null;
public static boolean groupCreated = false;
public DownloadManagerModule(ReactApplicationContext reactContext) {
super(reactContext);
this.context = reactContext;
}
private int generateNotificationId() {
int id = 0;
Random random = new Random();
do {
id = random.nextInt();
} while (id < 1000);
return id;
}
@Override
public String getName() {
return "LbryDownloadManager";
}
private void createNotificationChannel() {
// Only applies to Android 8.0 Oreo (API Level 26) or higher
if (!channelCreated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel(
NOTIFICATION_CHANNEL_ID, "LBRY Downloads", NotificationManager.IMPORTANCE_LOW);
channel.setDescription("LBRY file downloads");
notificationManager.createNotificationChannel(channel);
}
}
private void createNotificationGroup() {
if (!groupCreated) {
Intent intent = new Intent(context, NotificationDeletedReceiver.class);
intent.putExtra(NOTIFICATION_ID_KEY, GROUP_ID);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, GROUP_ID, intent, 0);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
groupBuilder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
groupBuilder.setContentTitle("Active LBRY downloads")
// contentText will be displayed if there are no notifications in the group
.setContentText("There are no active LBRY downloads.")
.setSmallIcon(android.R.drawable.stat_sys_download)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setGroup(GROUP_DOWNLOADS)
.setGroupSummary(true)
.setDeleteIntent(pendingIntent);
notificationManager.notify(GROUP_ID, groupBuilder.build());
groupCreated = true;
}
}
public static PendingIntent getLaunchPendingIntent(String uri, Context context) {
Intent launchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent intent = PendingIntent.getActivity(context, 0, launchIntent, 0);
return intent;
}
@ReactMethod
public void startDownload(String id, String filename) {
if (filename == null || filename.trim().length() == 0) {
return;
}
createNotificationChannel();
createNotificationGroup();
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
// The file URI is used as the unique ID
builder.setContentIntent(getLaunchPendingIntent(id, context))
.setContentTitle(String.format("Downloading %s", truncateFilename(filename)))
.setGroup(GROUP_DOWNLOADS)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setProgress(MAX_PROGRESS, 0, false)
.setSmallIcon(android.R.drawable.stat_sys_download);
int notificationId = getNotificationId(id);
downloadIdNotificationIdMap.put(id, notificationId);
builders.put(notificationId, builder);
notificationManager.notify(notificationId, builder.build());
if (groupCreated && groupBuilder != null) {
groupBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
notificationManager.notify(GROUP_ID, groupBuilder.build());
}
}
@ReactMethod
public void updateDownload(String id, String filename, double progress, double writtenBytes, double totalBytes) {
if (filename == null || filename.trim().length() == 0) {
return;
}
int notificationId = getNotificationId(id);
if (notificationId == -1) {
return;
}
if (stoppedDownloadsMap.containsKey(id) && stoppedDownloadsMap.get(id)) {
// if this happens, the download was canceled, so remove the notification
// TODO: Figure out why updateDownload is called in the React Native code after stopDownload
removeDownloadNotification(id);
stoppedDownloadsMap.remove(id);
return;
}
createNotificationChannel();
createNotificationGroup();
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
NotificationCompat.Builder builder = null;
if (builders.containsKey(notificationId)) {
builder = builders.get(notificationId);
} else {
builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
builder.setContentTitle(String.format("Downloading %s", truncateFilename(filename)))
.setPriority(NotificationCompat.PRIORITY_LOW);
builders.put(notificationId, builder);
}
builder.setContentIntent(getLaunchPendingIntent(id, context))
.setContentText(String.format("%.0f%% (%s / %s)", progress, formatBytes(writtenBytes), formatBytes(totalBytes)))
.setGroup(GROUP_DOWNLOADS)
.setProgress(MAX_PROGRESS, new Double(progress).intValue(), false)
.setSmallIcon(android.R.drawable.stat_sys_download);
notificationManager.notify(notificationId, builder.build());
if (progress == MAX_PROGRESS) {
builder.setContentTitle(String.format("Downloaded %s", truncateFilename(filename, 30)))
.setContentText(String.format("%s", formatBytes(totalBytes)))
.setGroup(GROUP_DOWNLOADS)
.setProgress(0, 0, false)
.setSmallIcon(android.R.drawable.stat_sys_download_done);
notificationManager.notify(notificationId, builder.build());
if (downloadIdNotificationIdMap.containsKey(id)) {
downloadIdNotificationIdMap.remove(id);
}
if (builders.containsKey(notificationId)) {
builders.remove(notificationId);
}
// If there are no more downloads and the group exists, set the icon to stop animating
if (groupCreated && groupBuilder != null && downloadIdNotificationIdMap.size() == 0) {
groupBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
notificationManager.notify(GROUP_ID, groupBuilder.build());
}
String spKey = String.format("dl__%s", id);
SharedPreferences sp = context.getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.remove(spKey);
editor.apply();
}
}
@ReactMethod
public void stopDownload(String id, String filename) {
android.util.Log.d("ReactNativeJS", "Stop download for id=" + id + "; filename=" + filename);
stoppedDownloadsMap.put(id, true);
removeDownloadNotification(id);
}
private void removeDownloadNotification(String id) {
if (!downloadIdNotificationIdMap.containsKey(id)) {
return;
}
int notificationId = downloadIdNotificationIdMap.get(id);
if (!builders.containsKey(notificationId)) {
return;
}
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
NotificationCompat.Builder builder = builders.get(notificationId);
notificationManager.cancel(notificationId);
downloadIdNotificationIdMap.remove(id);
builders.remove(notificationId);
if (builders.values().size() == 0) {
notificationManager.cancel(GROUP_ID);
groupCreated = false;
}
}
private int getNotificationId(String id) {
String spKey = String.format("dl__%s", id);
SharedPreferences sp = context.getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
int notificationId = sp.getInt(spKey, -1);
if (notificationId == -1) {
notificationId = generateNotificationId();
SharedPreferences.Editor editor = sp.edit();
editor.putInt(spKey, notificationId);
editor.apply();
}
if (MainActivity.downloadNotificationIds != null &&
!MainActivity.downloadNotificationIds.contains(notificationId)) {
MainActivity.downloadNotificationIds.add(notificationId);
}
downloadIdNotificationIdMap.put(id, notificationId);
return notificationId;
}
private static String formatBytes(double bytes)
{
if (bytes < 1048576) { // < 1MB
return String.format("%s KB", DECIMAL_FORMAT.format(bytes / 1024.0));
}
if (bytes < 1073741824) { // < 1GB
return String.format("%s MB", DECIMAL_FORMAT.format(bytes / (1024.0 * 1024.0)));
}
return String.format("%s GB", DECIMAL_FORMAT.format(bytes / (1024.0 * 1024.0 * 1024.0)));
}
private static String truncateFilename(String filename, int alternateMaxLength) {
int maxLength = alternateMaxLength > 0 ? alternateMaxLength : MAX_FILENAME_LENGTH;
if (filename.length() < maxLength) {
return filename;
}
// Get the extension
int dotIndex = filename.lastIndexOf(".");
if (dotIndex > -1) {
String extension = filename.substring(dotIndex);
return String.format("%s...%s", filename.substring(0, maxLength - extension.length() - 4), extension);
}
return String.format("%s...", filename.substring(0, maxLength - 3));
}
private static String truncateFilename(String filename) {
return truncateFilename(filename, 0);
}
}

View file

@ -53,21 +53,12 @@ public class FirebaseModule extends ReactContextBaseJavaModule {
} }
@ReactMethod @ReactMethod
public void logException(boolean fatal, String message, ReadableMap payload) { public void logException(boolean fatal, String message, String error) {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putString("message", message); bundle.putString("message", message);
if (payload != null) { bundle.putString("error", error);
HashMap<String, Object> payloadMap = payload.toHashMap();
for (Map.Entry<String, Object> entry : payloadMap.entrySet()) {
Object value = entry.getValue();
if (value != null) {
bundle.putString(entry.getKey(), entry.getValue().toString());
}
}
}
if (firebaseAnalytics != null) { if (firebaseAnalytics != null) {
firebaseAnalytics.logEvent(fatal ? "exception" : "warning", bundle); firebaseAnalytics.logEvent(fatal ? "reactjs_exception" : "reactjs_warning", bundle);
} }
if (fatal) { if (fatal) {

View file

@ -35,10 +35,11 @@ import java.util.Map;
import java.util.Random; import java.util.Random;
import java.security.KeyStore; import java.security.KeyStore;
import io.lbry.browser.DownloadManager;
import io.lbry.browser.MainActivity; import io.lbry.browser.MainActivity;
import io.lbry.browser.LbrynetService;
import io.lbry.browser.R; import io.lbry.browser.R;
import io.lbry.browser.Utils; import io.lbry.browser.Utils;
import io.lbry.browser.reactmodules.DownloadManagerModule;
public class UtilityModule extends ReactContextBaseJavaModule { public class UtilityModule extends ReactContextBaseJavaModule {
private static final Map<String, Integer> activeNotifications = new HashMap<String, Integer>(); private static final Map<String, Integer> activeNotifications = new HashMap<String, Integer>();
@ -194,14 +195,13 @@ public class UtilityModule extends ReactContextBaseJavaModule {
if (fileUri != null) { if (fileUri != null) {
Intent shareIntent = new Intent(); Intent shareIntent = new Intent();
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
// Android 6 and lower
shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
shareIntent.setAction(Intent.ACTION_SEND); shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.setType("text/plain"); shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_STREAM, fileUri); shareIntent.putExtra(Intent.EXTRA_STREAM, fileUri);
context.startActivity(Intent.createChooser(shareIntent, "Send LBRY log"));
Intent sendLogIntent = Intent.createChooser(shareIntent, "Send LBRY log");
sendLogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(sendLogIntent);
} }
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
errorCallback.invoke("The lbrynet.log file cannot be shared due to permission restrictions."); errorCallback.invoke("The lbrynet.log file cannot be shared due to permission restrictions.");
@ -240,7 +240,7 @@ public class UtilityModule extends ReactContextBaseJavaModule {
final NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID); final NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
builder.setAutoCancel(true) builder.setAutoCancel(true)
.setColor(ContextCompat.getColor(context, R.color.lbrygreen)) .setColor(ContextCompat.getColor(context, R.color.lbrygreen))
.setContentIntent(DownloadManagerModule.getLaunchPendingIntent(uri, context)) .setContentIntent(DownloadManager.getLaunchPendingIntent(uri, context))
.setContentTitle(publisher) .setContentTitle(publisher)
.setContentText(title) .setContentText(title)
.setSmallIcon(R.drawable.ic_lbry) .setSmallIcon(R.drawable.ic_lbry)
@ -325,4 +325,33 @@ public class UtilityModule extends ReactContextBaseJavaModule {
promise.resolve(Utils.getSecureValue(key, context, keyStore)); promise.resolve(Utils.getSecureValue(key, context, keyStore));
} }
@ReactMethod
public void checkDownloads() {
Intent intent = new Intent();
intent.setAction(LbrynetService.ACTION_CHECK_DOWNLOADS);
if (context != null) {
context.sendBroadcast(intent);
}
}
@ReactMethod
public void queueDownload(String outpoint) {
Intent intent = new Intent();
intent.setAction(LbrynetService.ACTION_QUEUE_DOWNLOAD);
intent.putExtra("outpoint", outpoint);
if (context != null) {
context.sendBroadcast(intent);
}
}
@ReactMethod
public void deleteDownload(String uri) {
Intent intent = new Intent();
intent.setAction(LbrynetService.ACTION_DELETE_DOWNLOAD);
intent.putExtra("uri", uri);
if (context != null) {
context.sendBroadcast(intent);
}
}
} }

View file

@ -7,7 +7,6 @@ import com.facebook.react.uimanager.ViewManager;
import io.lbry.browser.reactmodules.BackgroundMediaModule; import io.lbry.browser.reactmodules.BackgroundMediaModule;
import io.lbry.browser.reactmodules.DaemonServiceControlModule; import io.lbry.browser.reactmodules.DaemonServiceControlModule;
import io.lbry.browser.reactmodules.DownloadManagerModule;
import io.lbry.browser.reactmodules.FirstRunModule; import io.lbry.browser.reactmodules.FirstRunModule;
import io.lbry.browser.reactmodules.FirebaseModule; import io.lbry.browser.reactmodules.FirebaseModule;
import io.lbry.browser.reactmodules.ScreenOrientationModule; import io.lbry.browser.reactmodules.ScreenOrientationModule;
@ -30,7 +29,6 @@ public class LbryReactPackage implements ReactPackage {
modules.add(new BackgroundMediaModule(reactContext)); modules.add(new BackgroundMediaModule(reactContext));
modules.add(new DaemonServiceControlModule(reactContext)); modules.add(new DaemonServiceControlModule(reactContext));
modules.add(new DownloadManagerModule(reactContext));
modules.add(new FirstRunModule(reactContext)); modules.add(new FirstRunModule(reactContext));
modules.add(new FirebaseModule(reactContext)); modules.add(new FirebaseModule(reactContext));
modules.add(new ScreenOrientationModule(reactContext)); modules.add(new ScreenOrientationModule(reactContext));

View file

@ -4,14 +4,14 @@ import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import io.lbry.browser.reactmodules.DownloadManagerModule; import io.lbry.browser.DownloadManager;
public class NotificationDeletedReceiver extends BroadcastReceiver { public class NotificationDeletedReceiver extends BroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
int notificationId = intent.getExtras().getInt(DownloadManagerModule.NOTIFICATION_ID_KEY); int notificationId = intent.getExtras().getInt(DownloadManager.NOTIFICATION_ID_KEY);
if (DownloadManagerModule.GROUP_ID == notificationId) { if (DownloadManager.DOWNLOAD_NOTIFICATION_GROUP_ID == notificationId) {
DownloadManagerModule.groupCreated = false; DownloadManager.groupCreated = false;
} }
} }
} }

View file

@ -78,7 +78,10 @@ def start():
data_dir=f'{private_storage_dir}/lbrynet', data_dir=f'{private_storage_dir}/lbrynet',
wallet_dir=f'{private_storage_dir}/lbryum', wallet_dir=f'{private_storage_dir}/lbryum',
download_dir=f'{lbrynet_android_utils.getInternalStorageDir(service.getApplicationContext())}/Download', download_dir=f'{lbrynet_android_utils.getInternalStorageDir(service.getApplicationContext())}/Download',
blob_lru_cache_size=32,
components_to_skip=[DHT_COMPONENT, HASH_ANNOUNCER_COMPONENT, PEER_PROTOCOL_SERVER_COMPONENT], components_to_skip=[DHT_COMPONENT, HASH_ANNOUNCER_COMPONENT, PEER_PROTOCOL_SERVER_COMPONENT],
save_blobs=False,
save_files=False,
use_upnp=False use_upnp=False
) )

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B