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

View file

@ -8,7 +8,7 @@
"dependencies": {
"base-64": "^0.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",
"lodash": ">=4.17.11",
"merge": ">=1.2.1",
@ -25,16 +25,15 @@
"react-native-phone-input": "lbryio/react-native-phone-input",
"react-native-vector-icons": "^6.4.2",
"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-redux": "^5.0.3",
"redux": "^3.6.0",
"redux-logger": "3.0.6",
"redux-persist": "^4.8.0",
"redux-persist": "^4.10.2",
"redux-persist-filesystem-storage": "^1.3.2",
"redux-persist-transform-compress": "^4.2.0",
"redux-persist-transform-filter": "0.0.10",
"redux-thunk": "^2.2.0",
"redux-persist-transform-filter": "0.0.18",
"redux-thunk": "^2.3.0",
"rn-fetch-blob": "^0.10.15"
},
"devDependencies": {

View file

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

View file

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

View file

@ -1,11 +1,12 @@
import { connect } from 'react-redux';
import {
doPurchaseUri,
makeSelectFileInfoForUri,
makeSelectDownloadingForUri,
makeSelectLoadingForUri,
} from 'lbry-redux';
import { doFetchCostInfoForUri, makeSelectCostInfoForUri } from 'lbryinc';
import { doPurchaseUri, doStartDownload } from 'redux/actions/file';
import { doStartDownload } from 'redux/actions/file';
import FileDownloadButton from './view';
const select = (state, props) => ({
@ -16,7 +17,7 @@ const select = (state, props) => ({
});
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)),
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
});

View file

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

View file

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

View file

@ -87,7 +87,8 @@ class FileListItem extends React.PureComponent {
resizeMode="cover"
title={(title || name)}
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}>
{featuredResult && <Text style={fileListStyle.featuredUri} numberOfLines={1}>{uri}</Text>}
@ -106,7 +107,8 @@ class FileListItem extends React.PureComponent {
}} />}
<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} />
</View>

View file

@ -108,7 +108,7 @@ class FilePrice extends React.PureComponent {
<CreditAmount
style={textStyle}
label={false}
amount={costInfo.cost}
amount={parseFloat(costInfo.cost)}
isEstimate={isEstimate}
showFree
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 { 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';
const select = state => ({
backgroundPlayEnabled: makeSelectClientSetting(SETTINGS.BACKGROUND_PLAY_ENABLED)(state),
isPlayerVisible: selectIsPlayerVisible(state),
});
const perform = dispatch => ({
savePosition: (claimId, outpoint, position) => dispatch(savePosition(claimId, outpoint, position)),
setPlayerVisible: () => dispatch(doSetPlayerVisible(true)),
});
export default connect(select, perform)(MediaPlayer);

View file

@ -1,6 +1,8 @@
import React from 'react';
import { Lbry } from 'lbry-redux';
import {
AppState,
ActivityIndicator,
DeviceEventEmitter,
NativeModules,
PanResponder,
@ -9,12 +11,15 @@ import {
ScrollView,
TouchableOpacity
} from 'react-native';
import Colors from 'styles/colors';
import FastImage from 'react-native-fast-image'
import Video from 'react-native-video';
import Icon from 'react-native-vector-icons/FontAwesome5';
import FileItemMedia from 'component/fileItemMedia';
import mediaPlayerStyle from 'styles/mediaPlayer';
const positionSaveInterval = 10
class MediaPlayer extends React.PureComponent {
static ControlsTimeout = 3000;
@ -31,7 +36,9 @@ class MediaPlayer extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
encodedFilePath: null,
buffering: false,
backgroundPlayEnabled: false,
autoPaused: false,
rate: 1,
volume: 1,
muted: false,
@ -85,6 +92,7 @@ class MediaPlayer extends React.PureComponent {
const { position } = this.props;
if (!isNaN(parseFloat(position)) && position > 0) {
this.video.seek(position);
this.setState({ currentTime: position }, () => this.setSeekerPosition(this.calculateSeekerPosition()));
}
if (this.props.onMediaLoaded) {
@ -93,10 +101,13 @@ class MediaPlayer extends React.PureComponent {
}
onProgress = (data) => {
const { savePosition, fileInfo } = this.props;
const { savePosition, claim } = this.props;
this.setState({ currentTime: data.currentTime }, () => savePosition(fileInfo.claim_id, fileInfo.outpoint, data.currentTime));
this.setState({ buffering: false, currentTime: 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) {
this.setSeekerPosition(this.calculateSeekerPosition());
@ -140,6 +151,11 @@ class MediaPlayer extends React.PureComponent {
}
togglePlayerControls = () => {
const { setPlayerVisible, isPlayerVisible } = this.props;
if (!isPlayerVisible) {
setPlayerVisible();
}
if (this.state.areControlsVisible) {
this.manualHidePlayerControls();
} else {
@ -149,7 +165,14 @@ class MediaPlayer extends React.PureComponent {
togglePlay = () => {
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 = () => {
@ -217,7 +240,7 @@ class MediaPlayer extends React.PureComponent {
onPanResponderRelease: (evt, gestureState) => {
const time = this.getCurrentTimeForSeekerPosition();
if (time >= this.state.duration) {
this.setState({ paused: true });
this.setState({ paused: true }, this.handlePausedState);
this.onEnd();
} else {
this.seekTo(time);
@ -251,18 +274,29 @@ class MediaPlayer extends React.PureComponent {
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() {
const { assignPlayer } = this.props;
const { assignPlayer, backgroundPlayEnabled } = this.props;
if (assignPlayer) {
assignPlayer(this);
}
this.setState({ backgroundPlayEnabled: !!backgroundPlayEnabled });
this.setSeekerPosition(this.calculateSeekerPosition());
AppState.addEventListener('change', this.handleAppStateChange);
DeviceEventEmitter.addListener('onBackgroundPlayPressed', this.play);
DeviceEventEmitter.addListener('onBackgroundPausePressed', this.pause);
}
componentWillUnmount() {
AppState.removeEventListener('change', this.handleAppStateChange);
DeviceEventEmitter.removeListener('onBackgroundPlayPressed', this.play);
DeviceEventEmitter.removeListener('onBackgroundPausePressed', this.pause);
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 = () => {
this.setState({ paused: false }, this.updateBackgroundMediaNotification);
}
@ -282,6 +336,7 @@ class MediaPlayer extends React.PureComponent {
}
updateBackgroundMediaNotification = () => {
this.handlePausedState();
const { backgroundPlayEnabled } = this.props;
if (backgroundPlayEnabled) {
if (NativeModules.BackgroundMedia && window.currentMediaInfo) {
@ -292,13 +347,19 @@ class MediaPlayer extends React.PureComponent {
}
renderPlayerControls() {
const { onBackButtonPressed } = this.props;
if (this.state.areControlsVisible) {
return (
<View style={mediaPlayerStyle.playerControlsContainer}>
<TouchableOpacity style={mediaPlayerStyle.backButton} onPress={onBackButtonPressed}>
<Icon name={"arrow-left"} size={18} style={mediaPlayerStyle.backButtonIcon} />
</TouchableOpacity>
<TouchableOpacity style={mediaPlayerStyle.playPauseButton}
onPress={this.togglePlay}>
{this.state.paused && <Icon name="play" size={32} color="#ffffff" />}
{!this.state.paused && <Icon name="pause" size={32} color="#ffffff" />}
{this.state.paused && <Icon name="play" size={40} color="#ffffff" />}
{!this.state.paused && <Icon name="pause" size={40} color="#ffffff" />}
</TouchableOpacity>
<TouchableOpacity style={mediaPlayerStyle.toggleFullscreenButton} onPress={this.toggleFullscreenMode}>
@ -315,18 +376,6 @@ class MediaPlayer extends React.PureComponent {
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) => {
if (evt && evt.nativeEvent) {
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() {
const { backgroundPlayEnabled, fileInfo, thumbnail, onLayout, style } = this.props;
const { onLayout, source, style, thumbnail } = this.props;
const completedWidth = this.getCurrentTimePercentage() * this.seekerWidth;
const remainingWidth = this.seekerWidth - completedWidth;
let styles = [this.state.fullscreenMode ? mediaPlayerStyle.fullscreenContainer : mediaPlayerStyle.container];
@ -356,17 +411,27 @@ class MediaPlayer extends React.PureComponent {
return (
<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; }}
resizeMode={this.state.resizeMode}
playInBackground={backgroundPlayEnabled}
playInBackground={this.state.backgroundPlayEnabled}
style={mediaPlayerStyle.player}
rate={this.state.rate}
volume={this.state.volume}
paused={this.state.paused}
onLoad={this.onLoad}
onBuffer={this.onBuffer}
onProgress={this.onProgress}
onEnd={this.onEnd}
onError={this.onError}
minLoadRetryCount={999}
/>
{this.state.firstPlay && thumbnail && thumbnail.trim().length > 0 &&
@ -381,16 +446,18 @@ class MediaPlayer extends React.PureComponent {
</TouchableOpacity>
{(!this.state.fullscreenMode || (this.state.fullscreenMode && this.state.areControlsVisible)) &&
<View style={trackingStyle} onLayout={(evt) => {
this.trackingOffset = evt.nativeEvent.layout.x;
this.seekerWidth = evt.nativeEvent.layout.width;
}}>
<View style={trackingStyle} onLayout={this.onTrackingLayout}>
<View style={mediaPlayerStyle.progress}>
<View style={[mediaPlayerStyle.innerProgressCompleted, { width: completedWidth }]} />
<View style={[mediaPlayerStyle.innerProgressRemaining, { width: remainingWidth }]} />
</View>
</View>}
{this.state.buffering &&
<View style={mediaPlayerStyle.loadingContainer}>
<ActivityIndicator color={Colors.LbryGreen} size="large" />
</View>}
{this.state.areControlsVisible &&
<View style={{ left: this.getTrackingOffset(), width: this.seekerWidth }}>
<View style={[mediaPlayerStyle.seekerHandle,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -108,6 +108,7 @@ class WalletSend extends React.PureComponent<Props> {
<TextInput ref={ref => this.amountInput = ref}
onChangeText={value => this.setState({amount: value})}
keyboardType={'numeric'}
placeholder={'0'}
value={this.state.amount}
style={[walletStyle.input, walletStyle.amountInput]} />
<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_BACKUP_DISMISSED: "backupDismissed",
SETTING_REWARDS_NOT_INTERESTED: "rewardsNotInterested",
SETTING_DEVICE_WALLET_SYNCED: "deviceWalletSynced",
ACTION_DELETE_COMPLETED_BLOBS: "DELETE_COMPLETED_BLOBS",
ACTION_FIRST_RUN_PAGE_CHANGED: "FIRST_RUN_PAGE_CHANGED",
ACTION_PUSH_DRAWER_STACK: "PUSH_DRAWER_STACK",
ACTION_POP_DRAWER_STACK: "POP_DRAWER_STACK",
ACTION_SET_PLAYER_VISIBLE: "SET_PLAYER_VISIBLE",
PAGE_REWARDS: "rewards",
PAGE_SETTINGS: "settings",

View file

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

View file

@ -1,7 +1,7 @@
import { connect } from 'react-redux';
import { doToast } from 'lbry-redux';
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 AboutPage from './view';
import Constants from 'constants';
@ -17,6 +17,7 @@ const perform = dispatch => ({
notify: data => dispatch(doToast(data)),
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_ABOUT)),
popDrawerStack: () => dispatch(doPopDrawerStack()),
setPlayerVisible: () => dispatch(doSetPlayerVisible(false)),
});
export default connect(select, perform)(AboutPage);

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@ import {
selectMyClaimsWithoutChannels,
selectIsFetchingFileList,
} from 'lbry-redux';
import { doPushDrawerStack } from 'redux/actions/drawer';
import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
import Constants from 'constants';
import DownloadsPage from './view';
@ -17,7 +17,8 @@ const select = (state) => ({
const perform = dispatch => ({
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);

View file

@ -25,8 +25,9 @@ class DownloadsPage extends React.PureComponent {
};
componentDidMount() {
const { fileList, pushDrawerStack } = this.props;
const { fileList, pushDrawerStack, setPlayerVisible } = this.props;
pushDrawerStack();
setPlayerVisible();
fileList();
}
@ -39,7 +40,7 @@ class DownloadsPage extends React.PureComponent {
<UriBar navigation={navigation} />
{!fetching && !hasDownloads &&
<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>}
{fetching && !hasDownloads &&
<View style={downloadsStyle.busyContainer}>

View file

@ -1,6 +1,8 @@
import { connect } from 'react-redux';
import {
doFetchFileInfo,
doPurchaseUri,
doDeletePurchasedUri,
doResolveUri,
doSendTip,
doToast,
@ -11,9 +13,13 @@ import {
makeSelectContentPositionForUri,
makeSelectContentTypeForUri,
makeSelectMetadataForUri,
makeSelectStreamingUrlForUri,
makeSelectThumbnailForUri,
makeSelectTitleForUri,
selectBalance,
selectPurchasedUris,
selectFailedPurchaseUris,
selectPurchaseUriErrorMessage,
} from 'lbry-redux';
import {
doFetchCostInfoForUri,
@ -21,7 +27,15 @@ import {
selectRewardContentClaimIds,
selectBlackListedOutpoints
} 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';
const select = (state, props) => {
@ -30,6 +44,7 @@ const select = (state, props) => {
balance: selectBalance(state),
blackListedOutpoints: selectBlackListedOutpoints(state),
claim: makeSelectClaimForUri(selectProps.uri)(state),
drawerStack: selectDrawerStack(state),
isResolvingUri: makeSelectIsUriResolving(selectProps.uri)(state),
contentType: makeSelectContentTypeForUri(selectProps.uri)(state),
costInfo: makeSelectCostInfoForUri(selectProps.uri)(state),
@ -40,6 +55,10 @@ const select = (state, props) => {
rewardedContentClaimIds: selectRewardContentClaimIds(state, selectProps),
channelUri: makeSelectChannelForClaimUri(selectProps.uri, true)(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),
title: makeSelectTitleForUri(selectProps.uri)(state),
};
@ -52,10 +71,16 @@ const perform = dispatch => ({
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
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)),
sendTip: (amount, claimId, uri, successCallback, errorCallback) => dispatch(doSendTip(amount, claimId, uri, successCallback, errorCallback)),
setPlayerVisible: () => dispatch(doSetPlayerVisible(true)),
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);

View file

@ -4,6 +4,7 @@ import { Lbryio } from 'lbryinc';
import {
ActivityIndicator,
Alert,
DeviceEventEmitter,
Dimensions,
NativeModules,
ScrollView,
@ -11,11 +12,13 @@ import {
StyleSheet,
Text,
TextInput,
TouchableOpacity,
TouchableWithoutFeedback,
View,
WebView
} 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 ImageViewer from 'react-native-image-zoom-viewer';
import Button from 'component/button';
@ -33,6 +36,7 @@ import SubscribeButton from 'component/subscribeButton';
import SubscribeNotificationButton from 'component/subscribeNotificationButton';
import UriBar from 'component/uriBar';
import Video from 'react-native-video';
import FileRewardsDriver from 'component/fileRewardsDriver';
import filePageStyle from 'styles/filePage';
import uriBarStyle from 'styles/uriBar';
@ -72,14 +76,19 @@ class FilePage extends React.PureComponent {
tipAmount: null,
uri: null,
uriVars: null,
stopDownloadConfirmed: false
stopDownloadConfirmed: false,
streamingMode: false
};
}
componentDidMount() {
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;
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) {
this.fetchFileInfo(this.props);
const { claim, contentType, fileInfo, isResolvingUri, resolveUri, navigation } = this.props;
const { uri } = this.state;
if (!isResolvingUri && claim === undefined && uri) {
@ -116,12 +163,11 @@ class FilePage extends React.PureComponent {
const prevFileInfo = prevProps.fileInfo;
if (!prevFileInfo && fileInfo) {
// started downloading
const mediaType = Lbry.getMediaType(contentType);
const isPlayable = mediaType === 'video' || mediaType === 'audio';
// If the media is playable, file/view will be done in onPlaybackStarted
if (!isPlayable && !this.state.fileViewLogged) {
this.logFileView(uri, fileInfo);
this.logFileView(uri, claim);
}
}
}
@ -161,7 +207,7 @@ class FilePage extends React.PureComponent {
}
onDeletePressed = () => {
const { deleteFile, fileInfo } = this.props;
const { claim, deleteFile, deletePurchasedUri, fileInfo, navigation } = this.props;
Alert.alert(
'Delete file',
@ -169,7 +215,12 @@ class FilePage extends React.PureComponent {
[
{ text: 'No' },
{ 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({
downloadPressed: false,
fileViewLogged: false,
@ -183,7 +234,7 @@ class FilePage extends React.PureComponent {
}
onStopDownloadPressed = () => {
const { fileInfo, navigation, notify, stopDownload } = this.props;
const { deletePurchasedUri, fileInfo, navigation, notify, stopDownload } = this.props;
Alert.alert(
'Stop download',
@ -191,7 +242,12 @@ class FilePage extends React.PureComponent {
[
{ text: 'No' },
{ 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({
downloadPressed: false,
fileViewLogged: false,
@ -224,6 +280,28 @@ class FilePage extends React.PureComponent {
window.currentMediaInfo = 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) => {
@ -233,6 +311,33 @@ class FilePage extends React.PureComponent {
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) => {
let linkifiedContent = [];
let lines = text.split(/\n/g);
@ -321,8 +426,13 @@ class FilePage extends React.PureComponent {
}
}
logFileView = (uri, fileInfo, timeToStart) => {
const { outpoint, claim_id: claimId } = fileInfo;
logFileView = (uri, claim, timeToStart) => {
if (!claim) {
return;
}
const { nout, claim_id: claimId, txid } = claim;
const outpoint = `${txid}:${nout}`;
const params = {
uri,
outpoint,
@ -351,23 +461,30 @@ class FilePage extends React.PureComponent {
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) => {
return tags.map((tag, i) => (
<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() {
const {
balance,
claim,
channelUri,
costInfo,
fileInfo,
metadata,
contentType,
@ -442,7 +559,7 @@ class FilePage extends React.PureComponent {
const mediaType = Lbry.getMediaType(contentType);
const isPlayable = mediaType === 'video' || mediaType === 'audio';
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 channelClaimId = claim && claim.signing_channel && claim.signing_channel.claim_id;
const canSendTip = this.state.tipAmount > 0;
@ -454,8 +571,8 @@ class FilePage extends React.PureComponent {
const playerBgStyle = [filePageStyle.playerBackground, filePageStyle.containedPlayerBackground];
const fsPlayerBgStyle = [filePageStyle.playerBackground, filePageStyle.fullscreenPlayerBackground];
// at least 2MB (or the full download) before media can be loaded
const canLoadMedia = fileInfo &&
(fileInfo.written_bytes >= 2097152 || fileInfo.written_bytes == fileInfo.total_bytes); // 2MB = 1024*1024*2
const canLoadMedia = (this.state.streamingMode) || (fileInfo &&
(fileInfo.written_bytes >= 2097152 || fileInfo.written_bytes == fileInfo.total_bytes)); // 2MB = 1024*1024*2
const isViewable = (mediaType === 'image' || mediaType === 'text');
const isWebViewable = mediaType === 'text';
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) {
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} />}
{((!this.state.downloadButtonShown || this.state.downloadPressed) && !this.state.mediaLoaded) &&
<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}
style={filePageStyle.downloadButton}
openFile={openFile}
isPlayable={isPlayable}
isViewable={isViewable}
onPlay={() => {
this.startTime = Date.now();
this.setState({ downloadPressed: true, autoPlayMedia: true, stopDownloadConfirmed: false });
}}
onPlay={this.onFileDownloadButtonPlayed}
onView={() => this.setState({ downloadPressed: true })}
onButtonLayout={() => this.setState({ downloadButtonShown: true })}
onStartDownloadFailed={this.startDownloadFailed} />}
onButtonLayout={() => this.setState({ downloadButtonShown: true })} />}
{!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>
{(canLoadMedia && fileInfo && isPlayable) &&
{(this.state.streamingMode || (canLoadMedia && fileInfo && isPlayable)) &&
<View style={playerBgStyle}
ref={(ref) => { this.playerBackground = ref; }}
onLayout={(evt) => {
@ -535,12 +656,14 @@ class FilePage extends React.PureComponent {
this.setState({ playerBgHeight: evt.nativeEvent.layout.height });
}
}} />}
{(canLoadMedia && fileInfo && isPlayable && this.state.fullscreenMode) && <View style={fsPlayerBgStyle} />}
{(canLoadMedia && fileInfo && isPlayable) &&
{((this.state.streamingMode || (canLoadMedia && fileInfo && isPlayable)) && this.state.fullscreenMode) &&
<View style={fsPlayerBgStyle} />}
{(this.state.streamingMode || (canLoadMedia && fileInfo && isPlayable)) &&
<MediaPlayer
fileInfo={fileInfo}
claim={claim}
assignPlayer={(ref) => { this.player = ref; }}
uri={uri}
source={this.playerUriForFileInfo(fileInfo)}
style={playerStyle}
autoPlay={autoplay || this.state.autoPlayMedia}
onFullscreenToggled={this.handleFullscreenToggle}
@ -550,6 +673,7 @@ class FilePage extends React.PureComponent {
}
}}
onMediaLoaded={() => this.onMediaLoaded(channelName, title, uri)}
onBackButtonPressed={this.onBackButtonPressed}
onPlaybackStarted={this.onPlaybackStarted}
onPlaybackFinished={this.onPlaybackFinished}
thumbnail={thumbnail}
@ -580,13 +704,15 @@ class FilePage extends React.PureComponent {
style={showActions ? filePageStyle.scrollContainerActions : filePageStyle.scrollContainer}
contentContainerstyle={showActions ? null : filePageStyle.scrollContent}
ref={(ref) => { this.scrollView = ref; }}>
<TouchableWithoutFeedback style={filePageStyle.titleTouch}
onPress={() => this.setState({ showDescription: !this.state.showDescription })}>
<View style={filePageStyle.titleRow}>
<Text style={filePageStyle.title} selectable={true}>{title}</Text>
<TouchableWithoutFeedback style={filePageStyle.descriptionToggle}
onPress={() => this.setState({ showDescription: !this.state.showDescription })}>
<View style={filePageStyle.descriptionToggle}>
<Icon name={this.state.showDescription ? "caret-up" : "caret-down"} size={24} />
</TouchableWithoutFeedback>
</View>
</View>
</TouchableWithoutFeedback>
{channelName &&
<View style={filePageStyle.channelRow}>
<View style={filePageStyle.publishInfo}>
@ -630,6 +756,7 @@ class FilePage extends React.PureComponent {
<TextInput ref={ref => this.tipAmountInput = ref}
onChangeText={value => this.setState({tipAmount: value})}
keyboardType={'numeric'}
placeholder={'0'}
value={this.state.tipAmount}
style={[filePageStyle.input, filePageStyle.tipAmountInput]} />
<Text style={[filePageStyle.text, filePageStyle.currency]}>LBC</Text>
@ -654,12 +781,15 @@ class FilePage extends React.PureComponent {
)}
</View>)}
{(costInfo && parseFloat(costInfo.cost) > balance) && <FileRewardsDriver navigation={navigation} />}
<View onLayout={this.setRelatedContentPosition} />
<RelatedContent navigation={navigation} uri={uri} />
</ScrollView>
</View>
)}
{!this.state.fullscreenMode && <FloatingWalletBalance navigation={navigation} />}
{(!this.state.fullscreenMode && !this.state.showImageViewer && !this.state.showWebView) &&
<FloatingWalletBalance navigation={navigation} />}
</View>
);
}

View file

@ -3,6 +3,8 @@ import { doToast } from 'lbry-redux';
import {
doAuthenticate,
doCheckSync,
doGetSync,
doSyncApply,
doUserEmailNew,
doUserResendVerificationEmail,
selectAuthToken,
@ -11,9 +13,14 @@ import {
selectEmailToVerify,
selectAuthenticationIsPending,
selectHasSyncedWallet,
selectIsRetrievingSync,
selectGetSyncIsPending,
selectSyncApplyIsPending,
selectSyncApplyErrorMessage,
selectSyncData,
selectSyncHash,
selectUser,
} from 'lbryinc';
import { doSetClientSetting } from 'redux/actions/settings';
import FirstRun from './view';
const select = (state) => ({
@ -23,13 +30,20 @@ const select = (state) => ({
emailNewErrorMessage: selectEmailNewErrorMessage(state),
emailNewPending: selectEmailNewIsPending(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),
});
const perform = dispatch => ({
addUserEmail: email => dispatch(doUserEmailNew(email)),
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()),
notify: data => dispatch(doToast(data)),
resendVerificationEmail: email => dispatch(doUserResendVerificationEmail(email))

View file

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

View file

@ -16,6 +16,8 @@ import Colors from 'styles/colors';
import Constants from 'constants';
import firstRunStyle from 'styles/firstRun';
const firstRunMargins = 80;
class WalletPage extends React.PureComponent {
state = {
password: null,
@ -27,20 +29,20 @@ class WalletPage extends React.PureComponent {
componentDidMount() {
this.checkWalletReady();
this.props.checkSync();
setTimeout(() => this.setState({ hasCheckedSync: true}), 1000);
}
checkWalletReady = () => {
// make sure the sdk wallet component is ready
Lbry.status().then(status => {
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;
}
setTimeout(this.checkWalletReady, 1000);
}).catch((e) => {
console.log(e);
setTimeout(this.checkWalletReady, 1000);
});
}
@ -52,25 +54,26 @@ class WalletPage extends React.PureComponent {
if (onPasswordChanged) {
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() {
const { onPasswordChanged, onWalletViewLayout, isRetrievingSync, hasSyncedWallet } = this.props;
const { onPasswordChanged, onWalletViewLayout, getSyncIsPending, hasSyncedWallet, syncApplyIsPending } = this.props;
let content;
if (!this.state.walletReady || !this.state.hasCheckedSync || isRetrievingSync) {
if (!this.state.walletReady || !this.state.hasCheckedSync || getSyncIsPending) {
content = (
<View>
<View style={firstRunStyle.centered}>
<ActivityIndicator size="large" color={Colors.White} style={firstRunStyle.waiting} />
<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>
);
} else {
content = (
<View onLayout={onWalletViewLayout}>
@ -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}>
<BarPasswordStrengthDisplay
width={Dimensions.get('window').width - 80}
width={Dimensions.get('window').width - firstRunMargins}
minLength={1}
password={this.state.password} />
</View>}

View file

@ -38,7 +38,8 @@ class FirstRunScreen extends React.PureComponent {
isEmailVerified: false,
skipAccountConfirmed: false,
showBottomContainer: true,
walletPassword: null
walletPassword: null,
syncApplyStarted: false
};
componentDidMount() {
@ -65,12 +66,12 @@ class FirstRunScreen extends React.PureComponent {
}
componentWillReceiveProps(nextProps) {
const { emailNewErrorMessage, emailNewPending, user } = nextProps;
const { notify } = this.props;
const { emailNewErrorMessage, emailNewPending, syncApplyErrorMessage, syncApplyIsPending, user } = nextProps;
const { notify, isApplyingSync, setClientSetting } = this.props;
if (this.state.emailSubmitted && !emailNewPending) {
this.setState({ emailSubmitted: false });
if (emailNewErrorMessage) {
if (emailNewErrorMessage && emailNewErrorMessage.trim().length > 0) {
notify ({ message: String(emailNewErrorMessage), isError: true });
} else {
// 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);
}
@ -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 = () => {
const { notify, user } = this.props;
const { notify, user, hasSyncedWallet } = this.props;
const pageIndex = FirstRunScreen.pages.indexOf(this.state.currentPage);
if (Constants.FIRST_RUN_PAGE_WALLET === this.state.currentPage) {
if (!this.state.walletPassword || this.state.walletPassword.trim().length === 0) {
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;
}
@ -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 = () => {
const pageIndex = FirstRunScreen.pages.indexOf(this.state.currentPage);
const nextPage = FirstRunScreen.pages[pageIndex + 1];
this.setState({ currentPage: nextPage });
if (nextPage === Constants.FIRST_RUN_PAGE_EMAIL_COLLECT) {
// do not show the buttons (because we're waiting to get things ready)
this.setState({ showBottomContainer: false });
}
this.checkBottomContainer(nextPage);
}
showPage(pageName) {
const pageIndex = FirstRunScreen.pages.indexOf(pageName);
if (pageIndex > -1) {
this.setState({ currentPage: pageName });
this.checkBottomContainer(pageName);
}
}
@ -222,6 +255,21 @@ class FirstRunScreen extends React.PureComponent {
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() {
const {
authenticate,
@ -233,7 +281,8 @@ class FirstRunScreen extends React.PureComponent {
emailToVerify,
notify,
hasSyncedWallet,
isRetrievingSync,
getSyncIsPending,
syncApplyIsPending,
resendVerificationEmail,
user
} = this.props;
@ -267,7 +316,8 @@ class FirstRunScreen extends React.PureComponent {
page = (<WalletPage
checkSync={checkSync}
hasSyncedWallet={hasSyncedWallet}
isRetrievingSync={isRetrievingSync}
getSyncIsPending={getSyncIsPending}
syncApplyIsPending={syncApplyIsPending}
onWalletViewLayout={this.onWalletViewLayout}
onPasswordChanged={this.onWalletPasswordChanged} />);
break;
@ -293,7 +343,7 @@ class FirstRunScreen extends React.PureComponent {
Constants.FIRST_RUN_PAGE_EMAIL_VERIFY === this.state.currentPage) &&
<TouchableOpacity style={firstRunStyle.leftButton} onPress={this.handleLeftButtonPressed}>
<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>}
{!emailNewPending && (Constants.FIRST_RUN_PAGE_EMAIL_COLLECT === this.state.currentPage) &&
<TouchableOpacity style={firstRunStyle.leftButton} onPress={this.handleLeftButtonPressed}>

View file

@ -10,7 +10,7 @@ import {
selectUser,
} from 'lbryinc';
import { doToast } from 'lbry-redux';
import { doPushDrawerStack } from 'redux/actions/drawer';
import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
import Constants from 'constants';
import RewardsPage from './view';
@ -27,7 +27,8 @@ const perform = dispatch => ({
claimReward: reward => dispatch(doClaimRewardType(reward.reward_type, true)),
fetchRewards: () => dispatch(doRewardList()),
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);

View file

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

View file

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

View file

@ -26,12 +26,17 @@ class SearchPage extends React.PureComponent {
};
componentWillMount() {
this.props.pushDrawerStack();
const { pushDrawerStack, setPlayerVisible } = this.props;
pushDrawerStack();
setPlayerVisible();
}
componentDidMount() {
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) {
this.setState({ currentUri: (isURIValid(searchQuery)) ? normalizeURI(searchQuery) : null })
search(searchQuery);

View file

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

View file

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

View file

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

View file

@ -28,7 +28,7 @@ class SplashScreen extends React.PureComponent {
componentWillMount() {
this.setState({
daemonReady: false,
details: 'Starting daemon',
details: 'Starting up...',
message: 'Connecting',
isRunning: false,
isLagging: false,
@ -114,15 +114,12 @@ class SplashScreen extends React.PureComponent {
if (this.state.daemonReady && this.state.shouldAuthenticate && user && user.id) {
this.setState({ shouldAuthenticate: false }, () => {
AsyncStorage.getItem(Constants.KEY_FIRST_RUN_EMAIL).then(email => {
if (email) {
setEmailToVerify(email);
}
// user is authenticated, navigate to the main view
if (user.has_verified_email) {
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => {
if (walletPassword && walletPassword.trim().length > 0) {
getSync(walletPassword);
}
this.navigateToMain();
});
return;
@ -130,7 +127,6 @@ class SplashScreen extends React.PureComponent {
this.navigateToMain();
});
});
}
}
@ -140,9 +136,11 @@ class SplashScreen extends React.PureComponent {
balanceSubscribe,
blacklistedOutpointsSubscribe,
checkSubscriptionsInit,
updateBlockHeight,
getSync,
navigation,
notify
notify,
updateBlockHeight,
user
} = this.props;
Lbry.resolve({ urls: 'lbry://one' }).then(() => {
@ -152,21 +150,30 @@ class SplashScreen extends React.PureComponent {
checkSubscriptionsInit();
updateBlockHeight();
setInterval(() => { updateBlockHeight(); }, BLOCK_HEIGHT_INTERVAL);
if (user && user.id && user.has_verified_email) {
// 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) {
const { deleteCompleteBlobs, fetchSubscriptions } = this.props;
const { fetchSubscriptions, getSync, setClientSetting } = this.props;
const startupStatus = status.startup_status;
// 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;
if (hasStarted) {
deleteCompleteBlobs();
// Wait until we are able to resolve a name before declaring
// that we are done.
// TODO: This is a hack, and the logic should live in the daemon
@ -179,36 +186,17 @@ class SplashScreen extends React.PureComponent {
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());
});
});
return;
}
// 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());
Lbry.account_unlock({ password }).then(() => this.finishSplashScreen()).catch(() => this.finishSplashScreen());
return;
}
this.finishSplashScreen();
});
});
return;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { doFetchTrendingUris, selectTrendingUris, selectFetchingTrendingUris } from 'lbryinc';
import { doPushDrawerStack } from 'redux/actions/drawer';
import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
import Constants from 'constants';
import TrendingPage from './view';
@ -11,7 +11,8 @@ const select = state => ({
const perform = dispatch => ({
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);

View file

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

View file

@ -1,6 +1,9 @@
import { connect } from 'react-redux';
import { doToast } from 'lbry-redux';
import {
doCheckSync,
doGetSync,
doSyncApply,
doUserEmailNew,
doUserEmailToVerify,
doUserResendVerificationEmail,
@ -14,8 +17,18 @@ import {
selectEmailNewErrorMessage,
selectEmailNewIsPending,
selectEmailToVerify,
selectHasSyncedWallet,
selectGetSyncIsPending,
selectSetSyncIsPending,
selectSyncApplyIsPending,
selectSyncApplyErrorMessage,
selectSyncData,
selectSyncHash,
selectUser,
} from 'lbryinc';
import { doSetClientSetting } from 'redux/actions/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import Constants from 'constants';
import Verification from './view';
const select = (state) => ({
@ -28,15 +41,27 @@ const select = (state) => ({
phone: selectPhoneToVerify(state),
phoneNewErrorMessage: selectPhoneNewErrorMessage(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 => ({
addUserEmail: email => dispatch(doUserEmailNew(email)),
addUserPhone: (phone, country_code) => dispatch(doUserPhoneNew(phone, country_code)),
getSync: password => dispatch(doGetSync(password)),
checkSync: () => dispatch(doCheckSync()),
verifyPhone: (verificationCode) => dispatch(doUserPhoneVerify(verificationCode)),
notify: data => dispatch(doToast(data)),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
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);

View file

@ -142,13 +142,15 @@ class EmailVerifyPage extends React.PureComponent {
text={"Send verification email"}
onPress={this.onSendVerificationPressed} />}
{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>}
{(Constants.PHASE_VERIFICATION === this.state.phase) &&
<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}>
<Button style={rewardStyle.verificationButton} theme={"light"} text={"Resend"} onPress={this.onResendPressed} />

View file

@ -21,7 +21,7 @@ class ManualVerifyPage extends React.PureComponent {
return (
<View style={firstRunStyle.container}>
<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 file

@ -171,10 +171,12 @@ class PhoneVerifyPage extends React.PureComponent {
text={"Send verification text"}
onPress={this.onSendTextPressed} />}
{phoneNewIsPending &&
<View style={firstRunStyle.centerInside}>
<ActivityIndicator
style={[rewardStyle.loading, rewardStyle.topMarginMedium]}
style={rewardStyle.topMarginMedium}
size="small"
color={Colors.White} />}
color={Colors.White} />
</View>}
</View>
</View>}
@ -204,12 +206,12 @@ class PhoneVerifyPage extends React.PureComponent {
</View>
}
{phoneVerifyIsPending &&
<View>
<View style={firstRunStyle.centered}>
<Text style={firstRunStyle.paragraph}>Verifying your phone number...</Text>
<ActivityIndicator
color={Colors.White}
size="small"
style={[rewardStyle.loading, rewardStyle.topMarginMedium, rewardStyle.leftRightMargin]} />
style={[rewardStyle.topMarginMedium, rewardStyle.leftRightMargin]} />
</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 ManualVerifyPage from './internal/manual-verify-page';
import PhoneVerifyPage from './internal/phone-verify-page';
import SyncVerifyPage from './internal/sync-verify-page';
import firstRunStyle from 'styles/firstRun';
class VerificationScreen extends React.PureComponent {
@ -43,7 +44,8 @@ class VerificationScreen extends React.PureComponent {
}
checkVerificationStatus = (user) => {
const { navigation } = this.props;
const { deviceWalletSynced, navigation } = this.props;
const { syncFlow } = navigation.state.params;
this.setState({
isEmailVerified: (user && user.primary_email && user.has_verified_email),
@ -53,12 +55,24 @@ class VerificationScreen extends React.PureComponent {
if (!this.state.isEmailVerified) {
this.setState({ currentPage: 'emailVerify' });
}
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 && syncFlow && deviceWalletSynced) {
navigation.goBack();
return;
}
if (this.state.isEmailVerified && this.state.isIdentityVerified && this.state.isRewardApproved) {
// verification steps already completed
@ -81,18 +95,29 @@ class VerificationScreen extends React.PureComponent {
render() {
const {
addUserEmail,
checkSync,
emailNewErrorMessage,
emailNewPending,
emailToVerify,
getSync,
navigation,
notify,
addUserPhone,
getSyncIsPending,
hasSyncedWallet,
setSyncIsPending,
syncApplyIsPending,
syncApplyErrorMessage,
syncApply,
syncData,
syncHash,
phone,
phoneVerifyIsPending,
phoneVerifyErrorMessage,
phoneNewIsPending,
phoneNewErrorMessage,
resendVerificationEmail,
setClientSetting,
verifyPhone
} = this.props;
@ -111,6 +136,7 @@ class VerificationScreen extends React.PureComponent {
/>
);
break;
case 'phoneVerify':
page = (
<PhoneVerifyPage
@ -126,10 +152,33 @@ class VerificationScreen extends React.PureComponent {
/>
);
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':
page = (
<ManualVerifyPage setEmailVerificationPhase={this.setEmailVerificationPhase} />
);
break;
}
return (

View file

@ -1,24 +1,27 @@
import { connect } from 'react-redux';
import { doSetClientSetting } from 'redux/actions/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 { doGetSync, selectUser } from 'lbryinc';
import { doCheckSync, doGetSync, selectUser, selectHasSyncedWallet } from 'lbryinc';
import Constants from 'constants';
import WalletPage from './view';
const select = state => ({
user: selectUser(state),
balance: selectBalance(state),
hasSyncedWallet: selectHasSyncedWallet(state),
understandsRisks: makeSelectClientSetting(Constants.SETTING_ALPHA_UNDERSTANDS_RISKS)(state),
backupDismissed: makeSelectClientSetting(Constants.SETTING_BACKUP_DISMISSED)(state),
rewardsNotInterested: makeSelectClientSetting(Constants.SETTING_REWARDS_NOT_INTERESTED)(state),
});
const perform = dispatch => ({
checkSync: () => dispatch(doCheckSync()),
getSync: password => dispatch(doGetSync(password)),
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);

View file

@ -1,10 +1,11 @@
import React from 'react';
import { NativeModules, ScrollView, Text, View } from 'react-native';
import TransactionListRecent from 'component/transactionListRecent';
import WalletRewardsDriver from 'component/walletRewardsDriver';
import WalletAddress from 'component/walletAddress';
import WalletBalance from 'component/walletBalance';
import WalletSend from 'component/walletSend';
import WalletRewardsDriver from 'component/walletRewardsDriver';
import WalletSyncDriver from 'component/walletSyncDriver';
import Button from 'component/button';
import Link from 'component/link';
import UriBar from 'component/uriBar';
@ -13,11 +14,17 @@ import walletStyle from 'styles/wallet';
class WalletPage extends React.PureComponent {
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) {
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 {
balance,
backupDismissed,
hasSyncedWallet,
rewardsNotInterested,
understandsRisks,
setClientSetting,
@ -41,8 +49,15 @@ class WalletPage extends React.PureComponent {
<View>
<UriBar navigation={navigation} />
<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}>
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>
</View>
<Button text={'I understand the risks'} style={[walletStyle.button, walletStyle.understand]}
@ -55,14 +70,7 @@ class WalletPage extends React.PureComponent {
<View style={walletStyle.container}>
<UriBar navigation={navigation} />
<ScrollView style={walletStyle.scrollContainer} keyboardShouldPersistTaps={'handled'}>
{!backupDismissed &&
<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>}
<WalletSyncDriver navigation={navigation} />
{(!rewardsNotInterested) && (!balance || balance === 0) && <WalletRewardsDriver navigation={navigation} />}
<WalletBalance />
<WalletAddress />

View file

@ -8,3 +8,8 @@ export const doPushDrawerStack = (routeName) => (dispatch) => dispatch({
export const doPopDrawerStack = () => (dispatch) => dispatch({
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 {
ACTIONS,
Lbry,
doToast,
formatCredits,
selectBalance,
makeSelectFileInfoForUri,
makeSelectMetadataForUri,
selectDownloadingByOutpoint,
} from 'lbry-redux';
import { doClaimEligiblePurchaseRewards, makeSelectCostInfoForUri } from 'lbryinc';
import { ACTIONS, Lbry } from 'lbry-redux';
import { doClaimEligiblePurchaseRewards } from 'lbryinc';
import { Alert, NativeModules } from 'react-native';
import Constants from 'constants';
const DOWNLOAD_POLL_INTERVAL = 250;
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) {
export function doStartDownload(uri, outpoint, fileInfo) {
return (dispatch, getState) => {
const state = getState();
@ -118,7 +14,6 @@ export function doStartDownload(uri, outpoint) {
if (downloadingByOutpoint[outpoint]) return;
Lbry.file_list({ outpoint, full_status: true }).then(([fileInfo]) => {
dispatch({
type: ACTIONS.DOWNLOADING_STARTED,
data: {
@ -128,15 +23,39 @@ export function doStartDownload(uri, outpoint) {
},
});
if (NativeModules.LbryDownloadManager) {
NativeModules.LbryDownloadManager.startDownload(uri, fileInfo.file_name ? fileInfo.file_name : '');
}
dispatch(doClaimEligiblePurchaseRewards());
};
}
dispatch(doUpdateLoadStatus(uri, outpoint));
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({
type: ACTIONS.DOWNLOADING_COMPLETED,
data: {
uri,
outpoint,
fileInfo,
},
});
}
};
}
export function doStopDownloadingFile(uri, fileInfo) {
return dispatch => {
let params = { status: 'stop' };
@ -155,22 +74,10 @@ export function doStopDownloadingFile(uri, fileInfo) {
// Should also delete the file after the user stops downloading
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) {
return 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) {
return (dispatch, getState) => {
Lbry.file_delete({
@ -334,29 +114,5 @@ export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) {
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 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) => {
const routeName = action.data;
const newStack = state.stack.slice();

View file

@ -2,9 +2,11 @@ import { createSelector } from 'reselect';
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) {
return state.stack[state.stack.length - 1];
}

View file

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

View file

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

View file

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

View file

@ -58,6 +58,9 @@ const filePageStyle = StyleSheet.create({
fontSize: 16,
flex: 18
},
titleTouch: {
flex: 1,
},
titleRow: {
flexDirection: 'row',
marginTop: 12,
@ -159,7 +162,7 @@ const filePageStyle = StyleSheet.create({
bottom: 0
},
filePriceContainer: {
backgroundColor: '#61fcd8',
backgroundColor: Colors.NextLbryGreen,
justifyContent: 'center',
position: 'absolute',
right: 16,
@ -239,8 +242,8 @@ const filePageStyle = StyleSheet.create({
flex: 1,
left: 0,
right: 0,
top: 0,
bottom: 60,
top: 60,
bottom: 0,
zIndex: 100
},
link: {
@ -286,7 +289,9 @@ const filePageStyle = StyleSheet.create({
},
currency: {
alignSelf: 'flex-start',
marginTop: 17
fontSize: 12,
marginTop: 15,
marginLeft: 4
},
descriptionToggle: {
alignItems: 'center',
@ -320,6 +325,36 @@ const filePageStyle = StyleSheet.create({
},
tagItem: {
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,
color: Colors.White
},
spacedParagraph: {
fontFamily: 'Inter-UI-Regular',
fontSize: 18,
lineHeight: 28,
marginLeft: 32,
marginRight: 32,
marginBottom: 20,
color: Colors.White
},
infoParagraph: {
fontFamily: 'Inter-UI-Regular',
fontSize: 14,
@ -163,6 +172,13 @@ const firstRunStyle = StyleSheet.create({
marginRight: 32,
marginBottom: 48
},
centered: {
alignItems: 'center'
},
centerInside: {
flex: 1,
alignItems:'center'
},
nowrap: {
flex: 1,
flexWrap: 'nowrap'

View file

@ -74,7 +74,7 @@ const mediaPlayerStyle = StyleSheet.create({
height: 36,
alignItems: 'center',
justifyContent: 'center',
right: 0,
right: 4,
bottom: 14,
},
elapsedDuration: {
@ -82,15 +82,15 @@ const mediaPlayerStyle = StyleSheet.create({
position: 'absolute',
left: 8,
bottom: 24,
fontSize: 14,
fontSize: 12,
color: '#ffffff'
},
totalDuration: {
fontFamily: 'Inter-UI-Regular',
position: 'absolute',
right: 40,
right: 48,
bottom: 24,
fontSize: 14,
fontSize: 12,
color: '#ffffff'
},
seekerCircle: {
@ -136,6 +136,27 @@ const mediaPlayerStyle = StyleSheet.create({
height: 24,
width: 24,
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,
alignSelf: 'flex-start'
},
enrollButton: {
backgroundColor: Colors.White,
alignSelf: 'flex-start'
},
historyList: {
backgroundColor: '#ffffff'
},
@ -51,12 +55,12 @@ const walletStyle = StyleSheet.create({
margin: 16
},
title: {
fontFamily: 'Inter-UI-Bold',
fontFamily: 'Inter-UI-SemiBold',
fontSize: 20,
marginBottom: 24
},
transactionsTitle: {
fontFamily: 'Inter-UI-Bold',
fontFamily: 'Inter-UI-SemiBold',
fontSize: 20
},
transactionsHeader: {
@ -93,8 +97,8 @@ const walletStyle = StyleSheet.create({
},
balanceTitle: {
color: '#ffffff',
fontFamily: 'Inter-UI-Bold',
fontSize: 18,
fontFamily: 'Inter-UI-SemiBold',
fontSize: 20,
marginLeft: 16,
marginTop: 16
},
@ -141,6 +145,13 @@ const walletStyle = StyleSheet.create({
margin: 16,
marginTop: 76
},
warningParagraph: {
color: Colors.White,
fontFamily: 'Inter-UI-Regular',
fontSize: 16,
lineHeight: 24,
marginBottom: 16
},
warningText: {
color: Colors.White,
fontFamily: 'Inter-UI-Regular',
@ -156,7 +167,9 @@ const walletStyle = StyleSheet.create({
},
currency: {
alignSelf: 'flex-start',
marginTop: 17
fontSize: 12,
marginTop: 16,
marginLeft: 4
},
sendButton: {
marginTop: 8
@ -185,6 +198,29 @@ const walletStyle = StyleSheet.create({
fontFamily: 'Inter-UI-Regular',
fontSize: 14,
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 { 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 Constants from 'constants';
@ -36,6 +36,7 @@ export function dispatchNavigateToUri(dispatch, nav, uri, isNavigatingBack) {
if (!isNavigatingBack) {
dispatch(doPushDrawerStack(uri));
dispatch(doSetPlayerVisible(true));
}
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);
if (store && store.dispatch && !isNavigatingBack) {
store.dispatch(doPushDrawerStack(uri));
store.dispatch(doSetPlayerVisible(true));
}
return;
}
@ -127,6 +129,7 @@ export function navigateToUri(navigation, uri, additionalParams, isNavigatingBac
navigation.navigate({ routeName: 'File', key: uri, params });
if (store && store.dispatch && !isNavigatingBack) {
store.dispatch(doPushDrawerStack(uri));
store.dispatch(doSetPlayerVisible(true));
}
}

View file

@ -36,7 +36,7 @@ version.filename = %(source.dir)s/main.py
# (list) Application requirements
# 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
# Sets custom source for any requirements with recipes

View file

@ -36,7 +36,7 @@ version.filename = %(source.dir)s/main.py
# (list) Application requirements
# 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
# Sets custom source for any requirements with recipes

View file

@ -36,7 +36,7 @@ version.filename = %(source.dir)s/main.py
# (list) Application requirements
# 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
# Sets custom source for any requirements with recipes

View file

@ -2,5 +2,6 @@
<style name="LbryAppTheme" parent="@android:style/Theme.Material.Light">
<item name="android:windowBackground">@color/lbrygreen</item>
<item name="colorControlActivated">@color/white</item>
<item name="colorAccent">@color/white</item>
</style>
</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.Intent;
import android.content.IntentFilter;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Binder;
@ -17,12 +18,30 @@ import android.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
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.renpy.android.AssetExtract;
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_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 NOTIFICATION_CHANNEL_ID = "io.lbry.browser.DAEMON_NOTIFICATION_CHANNEL";
@ -49,8 +74,22 @@ public class LbrynetService extends PythonService {
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 downloadReceiver;
private DownloadManager downloadManager;
private ScheduledExecutorService taskExecutor;
private ScheduledFuture taskExecutorHandle = null;
private boolean streamManagerReady = false;
@Override
public boolean canDisplayNotification() {
return true;
@ -70,6 +109,29 @@ public class LbrynetService extends PythonService {
}
};
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
@ -78,6 +140,7 @@ public class LbrynetService extends PythonService {
String serviceDescription = "The LBRY service is running in the background.";
Context context = getApplicationContext();
downloadManager = new DownloadManager(context);
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -118,6 +181,286 @@ public class LbrynetService extends PythonService {
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
public int startType() {
return START_STICKY;
@ -137,13 +480,19 @@ public class LbrynetService extends PythonService {
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);
}
@Override
public void onDestroy() {
if (downloadReceiver != null) {
unregisterReceiver(downloadReceiver);
downloadReceiver = null;
}
if (stopServiceReceiver != null) {
unregisterReceiver(stopServiceReceiver);
stopServiceReceiver = null;

View file

@ -28,6 +28,7 @@ import com.facebook.react.ReactRootView;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.shell.MainReactPackage;
@ -39,16 +40,21 @@ import com.RNFetchBlob.RNFetchBlobPackage;
import io.lbry.browser.reactpackages.LbryReactPackage;
import io.lbry.browser.reactmodules.BackgroundMediaModule;
import io.lbry.browser.reactmodules.DownloadManagerModule;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import org.json.JSONObject;
import org.json.JSONArray;
import org.json.JSONException;
public class MainActivity extends Activity implements DefaultHardwareBackBtnHandler {
private static Activity currentActivity = null;
@ -67,6 +73,8 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
private BroadcastReceiver stopServiceReceiver;
private BroadcastReceiver downloadEventReceiver;
private ReactRootView mReactRootView;
private ReactInstanceManager mReactInstanceManager;
@ -114,6 +122,9 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
// Register SMS receiver for handling verification texts
registerSmsReceiver();
// Register the receiver to emit download events
registerDownloadEventReceiver();
// Start the daemon service if it is not started
serviceRunning = isServiceRunning(LbrynetService.class);
if (!serviceRunning) {
@ -142,6 +153,50 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
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() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(LbrynetService.ACTION_STOP_SERVICE);
@ -381,6 +436,11 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
smsReceiver = null;
}
if (downloadEventReceiver != null) {
unregisterReceiver(downloadEventReceiver);
downloadEventReceiver = null;
}
if (stopServiceReceiver != null) {
unregisterReceiver(stopServiceReceiver);
stopServiceReceiver = null;
@ -388,7 +448,7 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.cancel(BackgroundMediaModule.NOTIFICATION_ID);
notificationManager.cancel(DownloadManagerModule.GROUP_ID);
notificationManager.cancel(DownloadManager.DOWNLOAD_NOTIFICATION_GROUP_ID);
if (downloadNotificationIds != null) {
for (int i = 0; i < downloadNotificationIds.size(); i++) {
notificationManager.cancel(downloadNotificationIds.get(i));
@ -478,4 +538,54 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
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
public void logException(boolean fatal, String message, ReadableMap payload) {
public void logException(boolean fatal, String message, String error) {
Bundle bundle = new Bundle();
bundle.putString("message", message);
if (payload != null) {
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());
}
}
}
bundle.putString("error", error);
if (firebaseAnalytics != null) {
firebaseAnalytics.logEvent(fatal ? "exception" : "warning", bundle);
firebaseAnalytics.logEvent(fatal ? "reactjs_exception" : "reactjs_warning", bundle);
}
if (fatal) {

View file

@ -35,10 +35,11 @@ import java.util.Map;
import java.util.Random;
import java.security.KeyStore;
import io.lbry.browser.DownloadManager;
import io.lbry.browser.MainActivity;
import io.lbry.browser.LbrynetService;
import io.lbry.browser.R;
import io.lbry.browser.Utils;
import io.lbry.browser.reactmodules.DownloadManagerModule;
public class UtilityModule extends ReactContextBaseJavaModule {
private static final Map<String, Integer> activeNotifications = new HashMap<String, Integer>();
@ -194,14 +195,13 @@ public class UtilityModule extends ReactContextBaseJavaModule {
if (fileUri != null) {
Intent shareIntent = new Intent();
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.setType("text/plain");
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) {
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);
builder.setAutoCancel(true)
.setColor(ContextCompat.getColor(context, R.color.lbrygreen))
.setContentIntent(DownloadManagerModule.getLaunchPendingIntent(uri, context))
.setContentIntent(DownloadManager.getLaunchPendingIntent(uri, context))
.setContentTitle(publisher)
.setContentText(title)
.setSmallIcon(R.drawable.ic_lbry)
@ -325,4 +325,33 @@ public class UtilityModule extends ReactContextBaseJavaModule {
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.DaemonServiceControlModule;
import io.lbry.browser.reactmodules.DownloadManagerModule;
import io.lbry.browser.reactmodules.FirstRunModule;
import io.lbry.browser.reactmodules.FirebaseModule;
import io.lbry.browser.reactmodules.ScreenOrientationModule;
@ -30,7 +29,6 @@ public class LbryReactPackage implements ReactPackage {
modules.add(new BackgroundMediaModule(reactContext));
modules.add(new DaemonServiceControlModule(reactContext));
modules.add(new DownloadManagerModule(reactContext));
modules.add(new FirstRunModule(reactContext));
modules.add(new FirebaseModule(reactContext));
modules.add(new ScreenOrientationModule(reactContext));

View file

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

View file

@ -78,7 +78,10 @@ def start():
data_dir=f'{private_storage_dir}/lbrynet',
wallet_dir=f'{private_storage_dir}/lbryum',
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],
save_blobs=False,
save_files=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