implement direct URI navigation (#134)

This commit is contained in:
akinwale 2018-05-24 23:47:55 +01:00 committed by GitHub
parent 077af85181
commit 4fbf90654e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 287 additions and 83 deletions

View file

@ -1,5 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
doFetchCostInfoForUri,
makeSelectFileInfoForUri, makeSelectFileInfoForUri,
makeSelectDownloadingForUri, makeSelectDownloadingForUri,
makeSelectLoadingForUri, makeSelectLoadingForUri,
@ -17,7 +18,8 @@ const select = (state, props) => ({
const perform = dispatch => ({ const perform = dispatch => ({
purchaseUri: uri => dispatch(doPurchaseUri(uri)), purchaseUri: uri => dispatch(doPurchaseUri(uri)),
restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)) restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)),
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
}); });
export default connect(select, perform)(FileDownloadButton); export default connect(select, perform)(FileDownloadButton);

View file

@ -3,6 +3,13 @@ import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
import fileDownloadButtonStyle from '../../styles/fileDownloadButton'; import fileDownloadButtonStyle from '../../styles/fileDownloadButton';
class FileDownloadButton extends React.PureComponent { class FileDownloadButton extends React.PureComponent {
componentDidMount() {
const { costInfo, fetchCostInfo, uri } = this.props;
if (costInfo === undefined) {
fetchCostInfo(uri);
}
}
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
//this.checkAvailability(nextProps.uri); //this.checkAvailability(nextProps.uri);
this.restartDownload(nextProps); this.restartDownload(nextProps);
@ -54,7 +61,7 @@ class FileDownloadButton extends React.PureComponent {
if (!costInfo) { if (!costInfo) {
return ( return (
<View style={[style, fileDownloadButtonStyle.container]}> <View style={[style, fileDownloadButtonStyle.container]}>
<Text>Fetching cost info...</Text> <Text style={fileDownloadButtonStyle.text}>Fetching cost info...</Text>
</View> </View>
); );
} }

View file

@ -0,0 +1,12 @@
import { connect } from 'react-redux';
import UriBar from './view';
const select = state => ({
});
const perform = dispatch => ({
});
export default connect(select, perform)(UriBar);

View file

@ -0,0 +1,44 @@
// @flow
import React from 'react';
import { normalizeURI } from 'lbry-redux';
import { TextInput, View } from 'react-native';
import uriBarStyle from '../../styles/uriBar';
class UriBar extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
uri: null,
currentValue: null
};
}
render() {
const { value, navigation } = this.props;
if (!this.state.currentValue) {
this.setState({ currentValue: value });
}
// TODO: Search and URI suggestions overlay
return (
<View style={uriBarStyle.uriContainer}>
<TextInput style={uriBarStyle.uriText}
placeholder={'Enter a LBRY URI or some text'}
underlineColorAndroid={'transparent'}
numberOfLines={1}
value={this.state.currentValue}
returnKeyType={'go'}
onChangeText={(text) => this.setState({uri: text, currentValue: text})}
onSubmitEditing={() => {
if (this.state.uri) {
let uri = this.state.uri;
uri = uri.replace(/ /g, '-');
navigation.navigate('File', { uri: normalizeURI(uri) });
}
}}/>
</View>
);
}
}
export default UriBar;

View file

@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import {
makeSelectClaimForUri,
makeSelectClaimsInChannelForCurrentPage,
makeSelectFetchingChannelClaims,
} from 'lbry-redux';
import ChannelPage from './view';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
claimsInChannel: makeSelectClaimsInChannelForCurrentPage(props.uri)(state),
fetching: makeSelectFetchingChannelClaims(props.uri)(state),
});
const perform = dispatch => ({
});
export default connect(select, perform)(ChannelPage);

View file

@ -0,0 +1,24 @@
// @flow
import React from 'react';
import { ScrollView, Text, View } from 'react-native';
import UriBar from '../../component/uriBar';
import channelPageStyle from '../../styles/channelPage';
class ChannelPage extends React.PureComponent {
render() {
const { claim, navigation, uri } = this.props;
const { name } = claim;
return (
<View style={channelPageStyle.container}>
<Text style={channelPageStyle.title}>{name}</Text>
<ScrollView style={channelPageStyle.content}>
</ScrollView>
<UriBar value={uri} navigation={navigation} />
</View>
)
}
}
export default ChannelPage;

View file

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import NavigationActions from 'react-navigation'; import NavigationActions from 'react-navigation';
import { import {
ActivityIndicator,
AsyncStorage, AsyncStorage,
NativeModules, NativeModules,
SectionList, SectionList,
ScrollView,
Text, Text,
View View
} from 'react-native'; } from 'react-native';
@ -12,6 +12,8 @@ import { normalizeURI } from 'lbry-redux';
import moment from 'moment'; import moment from 'moment';
import FileItem from '../../component/fileItem'; import FileItem from '../../component/fileItem';
import discoverStyle from '../../styles/discover'; import discoverStyle from '../../styles/discover';
import Colors from '../../styles/colors';
import UriBar from '../../component/uriBar';
import Feather from 'react-native-vector-icons/Feather'; import Feather from 'react-native-vector-icons/Feather';
class DiscoverPage extends React.PureComponent { class DiscoverPage extends React.PureComponent {
@ -48,7 +50,12 @@ class DiscoverPage extends React.PureComponent {
return ( return (
<View style={discoverStyle.container}> <View style={discoverStyle.container}>
{!hasContent && fetchingFeaturedUris && <Text style={discoverStyle.title}>Fetching content...</Text>} {!hasContent && fetchingFeaturedUris && (
<View style={discoverStyle.busyContainer}>
<ActivityIndicator size="large" color={Colors.LbryGreen} />
<Text style={discoverStyle.title}>Fetching content...</Text>
</View>
)}
{hasContent && {hasContent &&
<SectionList style={discoverStyle.scrollContainer} <SectionList style={discoverStyle.scrollContainer}
renderItem={ ({item, index, section}) => ( renderItem={ ({item, index, section}) => (
@ -66,6 +73,7 @@ class DiscoverPage extends React.PureComponent {
keyExtractor={(item, index) => item} keyExtractor={(item, index) => item}
/> />
} }
<UriBar navigation={navigation} />
</View> </View>
); );
} }

View file

@ -1,13 +1,16 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
doFetchFileInfo, doFetchFileInfo,
makeSelectFileInfoForUri, doResolveUri,
doFetchCostInfoForUri, doFetchCostInfoForUri,
makeSelectIsUriResolving,
makeSelectCostInfoForUri,
makeSelectFileInfoForUri,
makeSelectClaimForUri, makeSelectClaimForUri,
makeSelectContentTypeForUri, makeSelectContentTypeForUri,
makeSelectMetadataForUri, makeSelectMetadataForUri,
selectRewardContentClaimIds, selectRewardContentClaimIds,
makeSelectCostInfoForUri selectBlackListedOutpoints,
} from 'lbry-redux'; } from 'lbry-redux';
import { doDeleteFile, doStopDownloadingFile } from '../../redux/actions/file'; import { doDeleteFile, doStopDownloadingFile } from '../../redux/actions/file';
import FilePage from './view'; import FilePage from './view';
@ -15,7 +18,9 @@ import FilePage from './view';
const select = (state, props) => { const select = (state, props) => {
const selectProps = { uri: props.navigation.state.params.uri }; const selectProps = { uri: props.navigation.state.params.uri };
return { return {
blackListedOutpoints: selectBlackListedOutpoints(state),
claim: makeSelectClaimForUri(selectProps.uri)(state), claim: makeSelectClaimForUri(selectProps.uri)(state),
isResolvingUri: makeSelectIsUriResolving(selectProps.uri)(state),
contentType: makeSelectContentTypeForUri(selectProps.uri)(state), contentType: makeSelectContentTypeForUri(selectProps.uri)(state),
costInfo: makeSelectCostInfoForUri(selectProps.uri)(state), costInfo: makeSelectCostInfoForUri(selectProps.uri)(state),
metadata: makeSelectMetadataForUri(selectProps.uri)(state), metadata: makeSelectMetadataForUri(selectProps.uri)(state),
@ -29,6 +34,7 @@ const select = (state, props) => {
const perform = dispatch => ({ const perform = dispatch => ({
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)), fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)), fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
resolveUri: uri => dispatch(doResolveUri(uri)),
stopDownload: (uri, fileInfo) => dispatch(doStopDownloadingFile(uri, fileInfo)), stopDownload: (uri, fileInfo) => dispatch(doStopDownloadingFile(uri, fileInfo)),
deleteFile: (fileInfo, deleteFromDevice, abandonClaim) => { deleteFile: (fileInfo, deleteFromDevice, abandonClaim) => {
dispatch(doDeleteFile(fileInfo, deleteFromDevice, abandonClaim)); dispatch(doDeleteFile(fileInfo, deleteFromDevice, abandonClaim));

View file

@ -13,9 +13,11 @@ import {
NativeModules NativeModules
} from 'react-native'; } from 'react-native';
import Colors from '../../styles/colors'; import Colors from '../../styles/colors';
import ChannelPage from '../channel';
import FileItemMedia from '../../component/fileItemMedia'; import FileItemMedia from '../../component/fileItemMedia';
import FileDownloadButton from '../../component/fileDownloadButton'; import FileDownloadButton from '../../component/fileDownloadButton';
import MediaPlayer from '../../component/mediaPlayer'; import MediaPlayer from '../../component/mediaPlayer';
import UriBar from '../../component/uriBar';
import Video from 'react-native-video'; import Video from 'react-native-video';
import filePageStyle from '../../styles/filePage'; import filePageStyle from '../../styles/filePage';
@ -34,15 +36,27 @@ class FilePage extends React.PureComponent {
componentDidMount() { componentDidMount() {
StatusBar.setHidden(false); StatusBar.setHidden(false);
const { isResolvingUri, resolveUri, navigation } = this.props;
const { uri } = navigation.state.params;
if (!isResolvingUri) resolveUri(uri);
this.fetchFileInfo(this.props); this.fetchFileInfo(this.props);
this.fetchCostInfo(this.props); this.fetchCostInfo(this.props);
if (NativeModules.Mixpanel) { if (NativeModules.Mixpanel) {
NativeModules.Mixpanel.track('Open File Page', { Uri: this.props.navigation.state.params.uri }); NativeModules.Mixpanel.track('Open File Page', { Uri: uri });
} }
} }
componentWillReceiveProps(nextProps) { componentDidUpdate() {
this.fetchFileInfo(nextProps); this.fetchFileInfo(this.props);
const { isResolvingUri, resolveUri, claim, navigation } = this.props;
const { uri } = navigation.state.params;
if (!isResolvingUri && claim === undefined && uri) {
resolveUri(uri);
}
} }
fetchFileInfo(props) { fetchFileInfo(props) {
@ -113,72 +127,87 @@ class FilePage extends React.PureComponent {
contentType, contentType,
tab, tab,
rewardedContentClaimIds, rewardedContentClaimIds,
isResolvingUri,
blackListedOutpoints,
navigation navigation
} = this.props; } = this.props;
const { uri } = navigation.state.params;
if (!claim || !metadata) { let innerContent = null;
return ( if ((isResolvingUri && !claim) || !claim) {
innerContent = (
<View style={filePageStyle.container}> <View style={filePageStyle.container}>
<Text style={filePageStyle.emptyClaimText}>Empty claim or metadata info.</Text> {isResolvingUri &&
<View style={filePageStyle.busyContainer}>
<ActivityIndicator size="large" color={Colors.LbryGreen} />
<Text style={filePageStyle.infoText}>Loading decentralized data...</Text>
</View>}
{ claim === null && !isResolvingUri &&
<View style={filePageStyle.container}>
<Text style={filePageStyle.emptyClaimText}>There's nothing at this location.</Text>
</View>
}
<UriBar value={uri} navigation={navigation} />
</View>
);
} else if (claim && claim.name.length && claim.name[0] === '@') {
innerContent = (
<ChannelPage uri={uri} navigation={navigation} />
);
} else if (claim) {
const completed = fileInfo && fileInfo.completed;
const title = metadata.title;
const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id);
const description = metadata.description ? metadata.description : null;
const mediaType = Lbry.getMediaType(contentType);
const isPlayable = mediaType === 'video' || mediaType === 'audio';
const { height, channel_name: channelName, value } = claim;
const showActions = !this.state.fullscreenMode &&
(completed || (fileInfo && !fileInfo.stopped && fileInfo.written_bytes < fileInfo.total_bytes));
const channelClaimId =
value && value.publisherSignature && value.publisherSignature.certificateId;
const playerStyle = [filePageStyle.player, this.state.fullscreenMode ?
filePageStyle.fullscreenPlayer : filePageStyle.containedPlayer];
const playerBgStyle = [filePageStyle.playerBackground, this.state.fullscreenMode ?
filePageStyle.fullscreenPlayerBackground : filePageStyle.containedPlayerBackground];
// 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
innerContent = (
<View style={filePageStyle.pageContainer}>
<View style={filePageStyle.mediaContainer}>
{(!fileInfo || (isPlayable && !canLoadMedia)) &&
<FileItemMedia style={filePageStyle.thumbnail} title={title} thumbnail={metadata.thumbnail} />}
{isPlayable && !this.state.mediaLoaded && <ActivityIndicator size="large" color={Colors.LbryGreen} style={filePageStyle.loading} />}
{!completed && !canLoadMedia && <FileDownloadButton uri={uri} style={filePageStyle.downloadButton} />}
</View>
{canLoadMedia && <View style={playerBgStyle} />}
{canLoadMedia && <MediaPlayer fileInfo={fileInfo}
uri={uri}
style={playerStyle}
onFullscreenToggled={this.handleFullscreenToggle}
onMediaLoaded={() => { this.setState({ mediaLoaded: true }); }}/>}
{ showActions &&
<View style={filePageStyle.actions}>
{completed && <Button color="red" title="Delete" onPress={this.onDeletePressed} />}
{!completed && fileInfo && !fileInfo.stopped && fileInfo.written_bytes < fileInfo.total_bytes &&
<Button color="red" title="Stop Download" onPress={this.onStopDownloadPressed} />
}
</View>}
<ScrollView style={showActions ? filePageStyle.scrollContainerActions : filePageStyle.scrollContainer}>
<Text style={filePageStyle.title} selectable={true}>{title}</Text>
{channelName && <Text style={filePageStyle.channelName} selectable={true}>{channelName}</Text>}
{description && <Text style={filePageStyle.description} selectable={true}>{description}</Text>}
</ScrollView>
<UriBar value={uri} navigation={navigation} />
</View> </View>
); );
} }
const completed = fileInfo && fileInfo.completed; return innerContent;
const title = metadata.title;
const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id);
const description = metadata.description ? metadata.description : null;
const mediaType = Lbry.getMediaType(contentType);
const isPlayable = mediaType === 'video' || mediaType === 'audio';
const { height, channel_name: channelName, value } = claim;
const showActions = !this.state.fullscreenMode &&
(completed || (fileInfo && !fileInfo.stopped && fileInfo.written_bytes < fileInfo.total_bytes));
const channelClaimId =
value && value.publisherSignature && value.publisherSignature.certificateId;
const playerStyle = [filePageStyle.player, this.state.fullscreenMode ?
filePageStyle.fullscreenPlayer : filePageStyle.containedPlayer];
const playerBgStyle = [filePageStyle.playerBackground, this.state.fullscreenMode ?
filePageStyle.fullscreenPlayerBackground : filePageStyle.containedPlayerBackground];
// 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
return (
<View style={filePageStyle.pageContainer}>
<View style={filePageStyle.mediaContainer}>
{(!fileInfo || (isPlayable && !canLoadMedia)) &&
<FileItemMedia style={filePageStyle.thumbnail} title={title} thumbnail={metadata.thumbnail} />}
{isPlayable && !this.state.mediaLoaded && <ActivityIndicator size="large" color={Colors.LbryGreen} style={filePageStyle.loading} />}
{!completed && !canLoadMedia && <FileDownloadButton uri={navigation.state.params.uri} style={filePageStyle.downloadButton} />}
</View>
{canLoadMedia && <View style={playerBgStyle} />}
{canLoadMedia && <MediaPlayer fileInfo={fileInfo}
uri={navigation.state.params.uri}
style={playerStyle}
onFullscreenToggled={this.handleFullscreenToggle}
onMediaLoaded={() => { this.setState({ mediaLoaded: true }); }}/>}
{ showActions &&
<View style={filePageStyle.actions}>
{completed && <Button color="red" title="Delete" onPress={this.onDeletePressed} />}
{!completed && fileInfo && !fileInfo.stopped && fileInfo.written_bytes < fileInfo.total_bytes &&
<Button color="red" title="Stop Download" onPress={this.onStopDownloadPressed} />
}
</View>}
<ScrollView style={showActions ? filePageStyle.scrollContainerActions : filePageStyle.scrollContainer}>
<Text style={filePageStyle.title} selectable={true}>{title}</Text>
{channelName && <Text style={filePageStyle.channelName} selectable={true}>{channelName}</Text>}
{description && <Text style={filePageStyle.description} selectable={true}>{description}</Text>}
</ScrollView>
<View style={filePageStyle.uriContainer}>
<TextInput style={filePageStyle.uriText}
underlineColorAndroid={'transparent'}
numberOfLines={1}
value={navigation.state.params.uri} />
</View>
</View>
);
} }
} }

View file

@ -0,0 +1,20 @@
import { StyleSheet } from 'react-native';
import Colors from './colors';
const channelPageStyle = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
content: {
flex: 1
},
title: {
color: Colors.LbryGreen,
fontFamily: 'Metropolis-SemiBold',
fontSize: 30,
margin: 16
}
});
export default channelPageStyle;

View file

@ -2,17 +2,22 @@ import { StyleSheet } from 'react-native';
const discoverStyle = StyleSheet.create({ const discoverStyle = StyleSheet.create({
container: { container: {
flex: 1, flex: 1
justifyContent: 'center',
}, },
scrollContainer: { scrollContainer: {
flex: 1 flex: 1
}, },
busyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row'
},
title: { title: {
fontFamily: 'Metropolis-Regular', fontFamily: 'Metropolis-Regular',
fontSize: 20, fontSize: 20,
textAlign: 'center', textAlign: 'center',
margin: 10, marginLeft: 10
}, },
categoryName: { categoryName: {
fontFamily: 'Metropolis-Regular', fontFamily: 'Metropolis-Regular',

View file

@ -2,7 +2,7 @@ import { StyleSheet } from 'react-native';
const fileDownloadButtonStyle = StyleSheet.create({ const fileDownloadButtonStyle = StyleSheet.create({
container: { container: {
width: 120, width: 160,
height: 36, height: 36,
borderRadius: 18, borderRadius: 18,
justifyContent: 'center', justifyContent: 'center',

View file

@ -116,19 +116,17 @@ const filePageStyle = StyleSheet.create({
position: 'absolute', position: 'absolute',
top: '40%' top: '40%'
}, },
uriContainer: { busyContainer: {
padding: 8, flex: 1,
backgroundColor: Colors.VeryLightGrey, justifyContent: 'center',
alignSelf: 'flex-end' alignItems: 'center',
flexDirection: 'row'
}, },
uriText: { infoText: {
backgroundColor: Colors.White,
borderWidth: 1,
borderColor: Colors.LightGrey,
padding: 8,
borderRadius: 4,
fontFamily: 'Metropolis-Regular', fontFamily: 'Metropolis-Regular',
fontSize: 16 fontSize: 20,
textAlign: 'center',
marginLeft: 10
} }
}); });

30
app/src/styles/uriBar.js Normal file
View file

@ -0,0 +1,30 @@
import { StyleSheet } from 'react-native';
import Colors from './colors';
const uriBarStyle = StyleSheet.create({
uriContainer: {
backgroundColor: Colors.White,
padding: 8,
alignSelf: 'flex-end',
width: '100%',
shadowColor: Colors.Black,
shadowOpacity: 0.1,
shadowRadius: StyleSheet.hairlineWidth,
shadowOffset: {
height: StyleSheet.hairlineWidth,
},
elevation: 4
},
uriText: {
backgroundColor: Colors.White,
borderWidth: 1,
borderColor: Colors.LightGrey,
padding: 8,
borderRadius: 4,
fontFamily: 'Metropolis-Regular',
fontSize: 16,
width: '100%'
}
});
export default uriBarStyle;