Unverified Commit 195d155c authored by Akinwale Ariwodola's avatar Akinwale Ariwodola Committed by GitHub
Browse files

Fast lite mode (#129)

* added lite file page
* update splash logic
* updates to navigation handling for lite mode
* update packages
* show firebase token on About page
* handle additional params for notifiication cold start
* handle opening urls in lite mode
parent 30f66d23
Subproject commit 37b893103da874282f2bdef4a8a1bb543d2c9859
Subproject commit b7b6c05bd3a61c1cec09a08f1b5c413a68597026
#!/bin/bash
react-native bundle --platform android --dev false --entry-file src/index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/
......@@ -5,6 +5,7 @@ import DiscoverPage from 'page/discover';
import DownloadsPage from 'page/downloads';
import DrawerContent from 'component/drawerContent';
import FilePage from 'page/file';
import LiteFilePage from 'page/liteFile';
import FirstRunScreen from 'page/firstRun';
import InvitesPage from 'page/invites';
import PublishPage from 'page/publish';
......@@ -19,7 +20,7 @@ import SubscriptionsPage from 'page/subscriptions';
import TransactionHistoryPage from 'page/transactionHistory';
import VerificationScreen from 'page/verification';
import WalletPage from 'page/wallet';
import { NavigationActions } from 'react-navigation';
import { NavigationActions, StackActions } from 'react-navigation';
import { createDrawerNavigator } from 'react-navigation-drawer';
import { createStackNavigator } from 'react-navigation-stack';
import {
......@@ -30,6 +31,7 @@ import {
import { connect } from 'react-redux';
import {
AppState,
Alert,
BackHandler,
DeviceEventEmitter,
Linking,
......@@ -76,16 +78,6 @@ import Snackbar from 'react-native-snackbar';
const SYNC_GET_INTERVAL = 1000 * 60 * 5; // every 5 minutes
const menuNavigationButton = navigation => (
<NavigationButton
name="bars"
size={24}
style={discoverStyle.drawerMenuButton}
iconStyle={discoverStyle.drawerHamburger}
onPress={() => navigation.openDrawer()}
/>
);
const discoverStack = createStackNavigator(
{
Subscriptions: {
......@@ -278,6 +270,12 @@ const mainStackNavigator = new createStackNavigator(
drawerLockMode: 'locked-closed',
},
},
LiteFile: {
screen: LiteFilePage,
navigationOptions: {
drawerLockMode: 'locked-closed',
},
},
},
{
headerMode: 'none',
......@@ -314,6 +312,8 @@ class AppWithNavigationState extends React.Component {
'hardwareBackPress',
function() {
const { dispatch, nav, drawerStack } = this.props;
console.log(nav);
if (drawerStack.length > 1) {
dispatchNavigateBack(dispatch, nav, drawerStack);
return true;
......
......@@ -39,10 +39,14 @@ class ClaimResultItem extends React.PureComponent {
}
onPressHandler = () => {
const { autoplay, navigation, result, setPlayerVisible } = this.props;
const { autoplay, navigation, result, urlOpenHandler, setPlayerVisible } = this.props;
const { claimId, name } = result;
const url = normalizeURI(`${name}#${claimId}`);
navigateToUri(navigation, url, { autoplay }, false, url, setPlayerVisible);
if (urlOpenHandler) {
urlOpenHandler(url);
} else {
navigateToUri(navigation, url, { autoplay }, false, url, setPlayerVisible);
}
};
render() {
......
......@@ -36,7 +36,7 @@ class MediaPlayer extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
buffering: false,
buffering: true,
backgroundPlayEnabled: false,
autoPaused: false,
rate: 1,
......@@ -45,9 +45,9 @@ class MediaPlayer extends React.PureComponent {
resizeMode: 'contain',
duration: 0.0,
currentTime: 0.0,
paused: !props.autoPlay,
paused: true,
fullscreenMode: false,
areControlsVisible: true,
areControlsVisible: false,
controlsTimeout: -1,
seekerOffset: 0,
seekerPosition: 0,
......@@ -87,7 +87,9 @@ class MediaPlayer extends React.PureComponent {
}
onLoad = data => {
const { autoPlay } = this.props;
this.setState({
buffering: false,
duration: data.duration,
});
......@@ -100,13 +102,17 @@ class MediaPlayer extends React.PureComponent {
if (this.props.onMediaLoaded) {
this.props.onMediaLoaded();
}
if (autoPlay) {
this.setState({ paused: false });
}
};
onProgress = data => {
const { savePosition, claim } = this.props;
this.setState({ buffering: false, currentTime: data.currentTime });
if (data.currentTime > 0 && Math.floor(data.currentTime) % positionSaveInterval === 0) {
if (claim && data.currentTime > 0 && Math.floor(data.currentTime) % positionSaveInterval === 0) {
const { claim_id: claimId, txid, nout } = claim;
savePosition(claimId, `${txid}:${nout}`, data.currentTime);
}
......
......@@ -15,7 +15,13 @@ const RESULT_SIZE = 16;
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
isSearching: selectIsSearching(state),
recommendedContent: makeSelectResolvedRecommendedContentForUri(props.uri, RESULT_SIZE)(state),
recommendedContent: makeSelectResolvedRecommendedContentForUri(
props.uri,
RESULT_SIZE,
props.claimId,
props.claimName,
props.title,
)(state),
resolvingUris: selectResolvingUris(state),
showNsfwContent: selectShowNsfw(state),
});
......@@ -26,7 +32,4 @@ const perform = dispatch => ({
dispatch(doResolvedSearch(query, RESULT_SIZE, undefined, true, { related_to: claimId }, nsfw)),
});
export default connect(
select,
perform,
)(RelatedContent);
export default connect(select, perform)(RelatedContent);
......@@ -22,7 +22,7 @@ export default class RelatedContent extends React.PureComponent {
}
render() {
const { isSearching, recommendedContent, navigation, uri, fullUri } = this.props;
const { isSearching, recommendedContent, navigation, urlOpenHandler, uri, fullUri } = this.props;
return (
<View style={relatedContentStyle.container}>
......@@ -33,6 +33,7 @@ export default class RelatedContent extends React.PureComponent {
<ClaimResultItem
style={fileListStyle.item}
uri={result ? normalizeURI(`${result.name}#${result.claimId}`) : null}
urlOpenHandler={urlOpenHandler}
key={result.claimId}
result={result}
navigation={navigation}
......
// @flow
import React from 'react';
import { SEARCH_TYPES, isNameValid, isURIValid, normalizeURI } from 'lbry-redux';
import { Dimensions, FlatList, Keyboard, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { Alert, Dimensions, FlatList, Keyboard, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { navigateToUri, transformUrl } from 'utils/helper';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import UriBarItem from './internal/uri-bar-item';
import Icon from 'react-native-vector-icons/FontAwesome5';
import NavigationButton from 'component/navigationButton';
import uriBarStyle from 'styles/uriBar';
import { NavigationActions, StackActions } from 'react-navigation';
class UriBar extends React.PureComponent {
static INPUT_TIMEOUT = 2500; // 2.5 seconds
......@@ -183,6 +184,33 @@ class UriBar extends React.PureComponent {
});
};
handleNavigationButtonPress = () => {
const { navigation } = this.props;
if (!navigation.openDrawer) {
Alert.alert(
__('Stop watching?'),
'The LBRY service is still loading stuff in the background. Would you like to continue?',
[
{ text: __('No') },
{
text: __('Yes'),
onPress: () => {
const resetAction = StackActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: 'Splash', params: { resetUrl: 'lbry://?subscriptions' } }),
],
});
navigation.dispatch(resetAction);
},
},
],
);
} else {
navigation.openDrawer();
}
};
render() {
const {
allowEdit,
......@@ -262,7 +290,7 @@ class UriBar extends React.PureComponent {
size={24}
style={uriBarStyle.drawerMenuButton}
iconStyle={uriBarStyle.drawerHamburger}
onPress={() => navigation.openDrawer()}
onPress={this.handleNavigationButtonPress}
/>
)}
{!selectionMode && (
......
......@@ -93,6 +93,7 @@ const Constants = {
DRAWER_ROUTE_CHANNEL_CREATOR: 'ChannelCreator',
DRAWER_ROUTE_CHANNEL_CREATOR_FORM: 'ChannnelCreatorForm',
DRAWER_ROUTE_INVITES: 'Invites',
DRAWER_ROUTE_LITE_FILE: 'LiteFile',
FULL_ROUTE_NAME_DISCOVER: 'DiscoverStack',
FULL_ROUTE_NAME_WALLET: 'WalletStack',
......
......@@ -58,6 +58,7 @@ window.__ = __;
const globalExceptionHandler = (error, isFatal) => {
if (error && NativeModules.Firebase) {
console.log(error);
NativeModules.Firebase.logException(!!isFatal, error.message ? error.message : 'No message', JSON.stringify(error));
}
};
......
......@@ -10,6 +10,7 @@ import aboutStyle from 'styles/about';
class AboutPage extends React.PureComponent {
state = {
appVersion: null,
firebaseToken: null,
lbryId: null,
versionInfo: null,
};
......@@ -45,11 +46,12 @@ class AboutPage extends React.PureComponent {
setPlayerVisible();
NativeModules.Firebase.setCurrentScreen('About').then(result => {
if (NativeModules.VersionInfo) {
NativeModules.VersionInfo.getAppVersion().then(version => {
this.setState({ appVersion: version });
});
}
NativeModules.VersionInfo.getAppVersion().then(version => {
this.setState({ appVersion: version });
});
NativeModules.Firebase.getMessagingToken().then(firebaseToken => {
this.setState({ firebaseToken });
});
Lbry.version().then(info => {
this.setState({
versionInfo: info,
......@@ -77,7 +79,7 @@ class AboutPage extends React.PureComponent {
<Text style={aboutStyle.title}>{__('Content Freedom')}</Text>
<Text style={aboutStyle.paragraph}>
{__(
'LBRY is a free, open, and community-run digital marketplace. It is a decentralized peer-to-peer content distribution platform for creators to upload and share content, and earn LBRY credits for their effort. Users will be able to find a wide selection of videos, music, ebooks and other digital content they are interested in.'
'LBRY is a free, open, and community-run digital marketplace. It is a decentralized peer-to-peer content distribution platform for creators to upload and share content, and earn LBRY credits for their effort. Users will be able to find a wide selection of videos, music, ebooks and other digital content they are interested in.',
)}
</Text>
<View style={aboutStyle.links}>
......@@ -88,7 +90,7 @@ class AboutPage extends React.PureComponent {
<Text style={aboutStyle.socialTitle}>{__('Get Social')}</Text>
<Text style={aboutStyle.paragraph}>
{__(
'You can interact with the LBRY team and members of the community on Discord, Facebook, Instagram, Twitter or Reddit.'
'You can interact with the LBRY team and members of the community on Discord, Facebook, Instagram, Twitter or Reddit.',
)}
</Text>
<View style={aboutStyle.links}>
......@@ -164,6 +166,15 @@ class AboutPage extends React.PureComponent {
</View>
</View>
<View style={aboutStyle.row}>
<View style={aboutStyle.col}>
<Text style={aboutStyle.text}>{__('Firebase Token')}</Text>
<Text selectable style={aboutStyle.lineValueText}>
{this.state.firebaseToken ? this.state.firebaseToken : loading}
</Text>
</View>
</View>
<View style={aboutStyle.row}>
<View style={aboutStyle.col}>
<Text style={aboutStyle.text}>{__('Logs')}</Text>
......
import { connect } from 'react-redux';
import { makeSelectContentPositionForUri, selectBalance } from 'lbry-redux';
import { doClaimEligiblePurchaseRewards, makeSelectViewCountForUri, selectRewardContentClaimIds } from 'lbryinc';
import { doSetPlayerVisible } from 'redux/actions/drawer';
import { makeSelectPlayerVisible } from 'redux/selectors/drawer';
import { doToggleFullscreenMode } from 'redux/actions/settings';
import LiteFilePage from './view';
const select = (state, props) => {
const { uri, fullUri } = props.navigation.state.params;
const contentUri = fullUri || uri;
const selectProps = { uri: contentUri };
return {
balance: selectBalance(state),
isPlayerVisible: makeSelectPlayerVisible(uri)(state), // use navigation uri for this selector
position: makeSelectContentPositionForUri(contentUri)(state),
viewCount: makeSelectViewCountForUri(contentUri)(state),
rewardedContentClaimIds: selectRewardContentClaimIds(state),
};
};
const perform = dispatch => ({
claimEligibleRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
setPlayerVisible: (visible, uri) => dispatch(doSetPlayerVisible(visible, uri)),
toggleFullscreenMode: mode => dispatch(doToggleFullscreenMode(mode)),
});
export default connect(
select,
perform,
)(LiteFilePage);
import React from 'react';
import { Lbry, formatCredits, normalizeURI, parseURI, parseQueryParams } from 'lbry-redux';
import { Lbryio } from 'lbryinc';
import {
ActivityIndicator,
Alert,
DeviceEventEmitter,
Dimensions,
Image,
Linking,
NativeModules,
Platform,
ScrollView,
StatusBar,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
TouchableWithoutFeedback,
View,
} from 'react-native';
import UriBar from 'component/uriBar';
import Link from 'component/link';
import MediaPlayer from 'component/mediaPlayer';
import RelatedContent from 'component/relatedContent';
import filePageStyle from 'styles/filePage';
import { decode, formatLbryUrlForWeb, navigateToUri } from 'utils/helper';
import Icon from 'react-native-vector-icons/FontAwesome5';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import uriBarStyle from 'styles/uriBar';
import { NavigationActions, StackActions } from 'react-navigation';
// This page will only be used for playing audio / video content from a remote stream URL
class LiteFilePage extends React.PureComponent {
playerBackground = null;
scrollView = null;
player = null;
state = {
channelName: null,
channelUrl: null,
title: null,
fullscreenMode: false,
playerHeight: null,
isLandscape: false,
sdkReady: false, // TODO: progressively enable features (e.g. tip) when sdk is ready
showRecommended: false,
viewCount: 0,
};
checkOrientation = () => {
if (this.state.fullscreenMode) {
return;
}
const screenDimension = Dimensions.get('window');
const screenWidth = screenDimension.width;
const screenHeight = screenDimension.height;
const isLandscape = screenWidth > screenHeight;
this.setState({ isLandscape });
if (!this.playerBackground) {
return;
}
if (isLandscape) {
this.playerBackground.setNativeProps({
height: screenHeight - StyleSheet.flatten(uriBarStyle.uriContainer).height,
});
} else if (this.state.playerBgHeight > 0) {
this.playerBackground.setNativeProps({ height: this.state.playerBgHeight });
}
};
handleFullscreenToggle = isFullscreen => {
const { toggleFullscreenMode } = this.props;
toggleFullscreenMode(isFullscreen);
if (isFullscreen) {
// fullscreen, so change orientation to landscape mode
NativeModules.ScreenOrientation.lockOrientationLandscape();
// hide the navigation bar (on devices that have the soft navigation bar)
NativeModules.UtilityModule.hideNavigationBar();
} else {
// Switch back to portrait mode when the media is not fullscreen
NativeModules.ScreenOrientation.lockOrientationPortrait();
// show the navigation bar (on devices that have the soft navigation bar)
NativeModules.UtilityModule.showNavigationBar();
}
this.setState({ fullscreenMode: isFullscreen });
StatusBar.setHidden(isFullscreen);
};
getStreamUrl = url => {
const { claimName, claimId } = parseURI(url);
return `https://player.lbry.tv/content/claims/${claimName}/${claimId}/stream`;
};
handleSharePress = url => {
const shareUrl = Constants.SHARE_BASE_URL + formatLbryUrlForWeb(url);
NativeModules.UtilityModule.shareUrl(shareUrl);
};
handleOpenUrl = url => {
const { navigation } = this.props;
Alert.alert(
__('Stop watching?'),
'The LBRY service is still loading stuff in the background. Would you like to continue?',
[
{ text: __('No') },
{
text: __('Yes'),
onPress: () => {
const resetAction = StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: 'Splash', params: { resetUrl: url } })],
});
navigation.dispatch(resetAction);
},
},
],
);
};
componentDidUpdate() {
const { navigation } = this.props;
const { uri } = navigation.state.params;
if (!this.state.title) {
const params = parseQueryParams(uri);
const { channelUrl, contentTitle } = params;
const channelName = channelUrl ? parseURI(decode(channelUrl)).claimName : null;
this.setState({
title: decode(contentTitle),
channelUrl,
channelName,
showRecommended: true,
});
}
}
render() {
const { navigation, rewardedContentClaimIds } = this.props;
const { channelName, channelUrl, title, sdkReady, viewCount } = this.state;
const { uri } = navigation.state.params;
const { claimName, claimId } = parseURI(uri);
const isRewardContent = rewardedContentClaimIds.includes(claimId);
const playerBgStyle = [filePageStyle.playerBackground, filePageStyle.containedPlayerBackground];
const fsPlayerBgStyle = [filePageStyle.playerBackground, filePageStyle.fullscreenPlayerBackground];
const playerStyle = [
filePageStyle.player,
this.state.isLandscape
? filePageStyle.containedPlayerLandscape
: this.state.fullscreenMode
? filePageStyle.fullscreenPlayer
: filePageStyle.containedPlayer,
];
return (
<View style={filePageStyle.pageContainer}>
{!this.state.fullscreenMode && <UriBar value={uri.split('?')[0]} navigation={navigation} />}
<View
style={this.state.fullscreenMode ? filePageStyle.innerPageContainerFsMode : filePageStyle.innerPageContainer}
onLayout={this.checkOrientation}
>
<TouchableOpacity activeOpacity={0.5} style={filePageStyle.mediaContainer} />
<View
style={playerBgStyle}
ref={ref => {
this.playerBackground = ref;
}}
onLayout={evt => {
if (!this.state.playerBgHeight) {
this.setState({ playerBgHeight: evt.nativeEvent.layout.height });
}
}}
/>
{this.state.fullscreenMode && <View style={fsPlayerBgStyle} />}
<MediaPlayer
assignPlayer={ref => {
this.player = ref;
}}
uri={uri}
source={this.getStreamUrl(uri)}
style={playerStyle}
autoPlay
onFullscreenToggled={this.handleFullscreenToggle}
onLayout={evt => {
if (!this.state.playerHeight) {
this.setState({ playerHeight: evt.nativeEvent.layout.height });
}
}}
/>
<ScrollView
style={filePageStyle.scrollContainer}
contentContainerStyle={filePageStyle.scrollContent}
keyboardShouldPersistTaps={'handled'}
ref={ref => {
this.scrollView = ref;
}}
>
<TouchableWithoutFeedback
style={filePageStyle.titleTouch}
onPress={() => this.setState({ showDescription: !this.state.showDescription })}
>
<View style={filePageStyle.titleArea}>
<View style={filePageStyle.titleRow}>
<Text style={filePageStyle.title} selectable>
{title}
</Text>
{isRewardContent && <Icon name="award" style={filePageStyle.rewardIcon} size={16} />}
</View>
<Text style={filePageStyle.viewCount}>
{viewCount === 1 && __('%view% view', { view: viewCount })}
{viewCount > 1 && __('%view% views', { view: viewCount })}
</Text>
</View>
</TouchableWithoutFeedback>
<View style={filePageStyle.largeButtonsRow}>
<TouchableOpacity style={filePageStyle.largeButton} onPress={() => this.handleSharePress(uri)}>
<Icon name={'share-alt'} size={16} style={filePageStyle.largeButtonIcon} />
<Text style={filePageStyle.largeButtonText}>{__('Share'