Merge pull request #166 from lbryio/first-run-improvements

implement extendable first run experience starting with welcome page
This commit is contained in:
Akinwale Ariwodola 2018-06-11 04:41:55 +01:00 committed by GitHub
commit 31168d9ed8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 333 additions and 78 deletions

View file

@ -2,6 +2,7 @@ import React from 'react';
import AboutPage from '../page/about'; import AboutPage from '../page/about';
import DiscoverPage from '../page/discover'; import DiscoverPage from '../page/discover';
import FilePage from '../page/file'; import FilePage from '../page/file';
import FirstRunScreen from '../page/firstRun';
import SearchPage from '../page/search'; import SearchPage from '../page/search';
import TrendingPage from '../page/trending'; import TrendingPage from '../page/trending';
import SettingsPage from '../page/settings'; import SettingsPage from '../page/settings';
@ -99,6 +100,12 @@ const drawer = DrawerNavigator({
}); });
export const AppNavigator = new StackNavigator({ export const AppNavigator = new StackNavigator({
FirstRun: {
screen: FirstRunScreen,
navigationOptions: {
drawerLockMode: 'locked-closed'
}
},
Splash: { Splash: {
screen: SplashScreen, screen: SplashScreen,
navigationOptions: { navigationOptions: {

View file

@ -61,7 +61,7 @@ function enableBatching(reducer) {
} }
const router = AppNavigator.router; const router = AppNavigator.router;
const navAction = router.getActionForPathAndParams('Splash'); const navAction = router.getActionForPathAndParams('FirstRun');
const initialNavState = router.getStateForAction(navAction); const initialNavState = router.getStateForAction(navAction);
const navigatorReducer = (state = initialNavState, action) => { const navigatorReducer = (state = initialNavState, action) => {
const nextState = AppNavigator.router.getStateForAction(action, state); const nextState = AppNavigator.router.getStateForAction(action, state);
@ -116,16 +116,6 @@ persistStore(store, persistOptions, err => {
}); });
class LBRYApp extends React.Component { class LBRYApp extends React.Component {
componentDidMount() {
AsyncStorage.getItem('hasLaunched').then(value => {
if (value == null || value !== 'true') {
AsyncStorage.setItem('hasLaunched', 'true');
// only set firstLaunchTime since we've determined that this is the first app launch ever
AsyncStorage.setItem('firstLaunchTime', String(moment().unix()));
}
});
}
render() { render() {
return ( return (
<Provider store={store}> <Provider store={store}>

View file

@ -0,0 +1,6 @@
import { connect } from 'react-redux';
import FirstRun from './view';
const perform = dispatch => ({});
export default connect(null, perform)(FirstRun);

View file

@ -0,0 +1,21 @@
import React from 'react';
import { Lbry } from 'lbry-redux';
import { View, Text, Linking } from 'react-native';
import Colors from '../../../styles/colors';
import firstRunStyle from '../../../styles/firstRun';
class WelcomePage extends React.PureComponent {
render() {
return (
<View style={firstRunStyle.container}>
<Text style={firstRunStyle.title}>Welcome to LBRY.</Text>
<Text style={firstRunStyle.paragraph}>LBRY is a decentralized peer-to-peer content sharing platform where
you can upload and download videos, music, ebooks and other forms of digital content.</Text>
<Text style={firstRunStyle.paragraph}>We make use of a blockchain which needs to be synchronized before
you can use the app. Synchronization may take a while because this is the first app launch.</Text>
</View>
);
}
}
export default WelcomePage;

View file

@ -0,0 +1,96 @@
import React from 'react';
import { Lbry } from 'lbry-redux';
import {
Linking,
NativeModules,
Text,
TouchableOpacity,
View
} from 'react-native';
import { NavigationActions } from 'react-navigation';
import Colors from '../../styles/colors';
import WelcomePage from './internal/welcome-page';
import firstRunStyle from '../../styles/firstRun';
class FirstRunScreen extends React.PureComponent {
static pages = ['welcome'];
constructor() {
super();
this.state = {
currentPage: null,
launchUrl: null,
isFirstRun: false
}
}
componentDidMount() {
Linking.getInitialURL().then((url) => {
if (url) {
this.setState({ launchUrl: url });
}
});
if (NativeModules.FirstRun) {
NativeModules.FirstRun.isFirstRun().then(firstRun => {
this.setState({ isFirstRun: firstRun });
if (firstRun) {
this.setState({ currentPage: FirstRunScreen.pages[0] });
} else {
// Not the first run. Navigate to the splash screen right away
this.launchSplashScreen();
}
});
} else {
// The first run module was not detected. Go straight to the splash screen.
this.launchSplashScreen();
}
}
launchSplashScreen() {
const { navigation } = this.props;
const resetAction = NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: 'Splash', params: { launchUri: this.state.launchUri } })
]
});
navigation.dispatch(resetAction);
}
handleContinuePressed = () => {
const pageIndex = FirstRunScreen.pages.indexOf(this.state.currentPage);
if (pageIndex === (FirstRunScreen.pages.length - 1)) {
// Final page. Let the app know that first run experience is completed.
if (NativeModules.FirstRun) {
NativeModules.FirstRun.firstRunCompleted();
}
// Navigate to the splash screen
this.launchSplashScreen();
} else {
// TODO: Page transition animation?
this.state.currentPage = FirstRunScreen.pages[pageIndex + 1];
}
}
render() {
let page = null;
if (this.state.currentPage === 'welcome') {
// show welcome page
page = (<WelcomePage />);
}
return (
<View style={firstRunStyle.screenContainer}>
{page}
{this.state.currentPage &&
<TouchableOpacity style={firstRunStyle.button} onPress={this.handleContinuePressed}>
<Text style={firstRunStyle.buttonText}>Continue</Text>
</TouchableOpacity>}
</View>
)
}
}
export default FirstRunScreen;

View file

@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import { Lbry } from 'lbry-redux'; import { Lbry } from 'lbry-redux';
import { View, Text, Linking, NativeModules } from 'react-native'; import { ActivityIndicator, View, Text, Linking, NativeModules } from 'react-native';
import { NavigationActions } from 'react-navigation'; import { NavigationActions } from 'react-navigation';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Colors from '../../styles/colors';
import splashStyle from '../../styles/splash'; import splashStyle from '../../styles/splash';
class SplashScreen extends React.PureComponent { class SplashScreen extends React.PureComponent {
@ -18,6 +19,21 @@ class SplashScreen extends React.PureComponent {
isLagging: false, isLagging: false,
launchUrl: null launchUrl: null
}); });
if (NativeModules.DaemonServiceControl) {
NativeModules.DaemonServiceControl.startService();
}
}
componentDidMount() {
// Start measuring the first launch time from the splash screen (time from daemon start to user interaction)
AsyncStorage.getItem('hasLaunched').then(value => {
if (value == null || value !== 'true') {
AsyncStorage.setItem('hasLaunched', 'true');
// only set firstLaunchTime since we've determined that this is the first app launch ever
AsyncStorage.setItem('firstLaunchTime', String(moment().unix()));
}
});
} }
updateStatus() { updateStatus() {
@ -53,8 +69,9 @@ class SplashScreen extends React.PureComponent {
}); });
navigation.dispatch(resetAction); navigation.dispatch(resetAction);
if (this.state.launchUrl) { const launchUrl = navigation.state.params.launchUrl || this.state.launchUrl;
navigation.navigate({ routeName: 'File', key: this.state.launchUrl, params: { uri: this.state.launchUrl } }); if (launchUrl) {
navigation.navigate({ routeName: 'File', key: launchUrl, params: { uri: launchUrl } });
} }
}); });
return; return;
@ -111,6 +128,7 @@ class SplashScreen extends React.PureComponent {
return ( return (
<View style={splashStyle.container}> <View style={splashStyle.container}>
<Text style={splashStyle.title}>LBRY</Text> <Text style={splashStyle.title}>LBRY</Text>
<ActivityIndicator color={Colors.White} style={splashStyle.loading} size={"small"} />
<Text style={splashStyle.message}>{message}</Text> <Text style={splashStyle.message}>{message}</Text>
<Text style={splashStyle.details}>{details}</Text> <Text style={splashStyle.details}>{details}</Text>
</View> </View>

View file

@ -0,0 +1,44 @@
import { StyleSheet } from 'react-native';
import Colors from './colors';
const firstRunStyle = StyleSheet.create({
screenContainer: {
flex: 1,
backgroundColor: Colors.LbryGreen
},
container: {
flex: 9,
justifyContent: 'center',
backgroundColor: Colors.LbryGreen
},
title: {
fontFamily: 'Metropolis-SemiBold',
fontSize: 40,
marginLeft: 32,
marginRight: 32,
marginBottom: 32,
color: Colors.White
},
paragraph: {
fontFamily: 'Metropolis-Regular',
fontSize: 18,
lineHeight: 24,
marginLeft: 32,
marginRight: 32,
marginBottom: 20,
color: Colors.White
},
button: {
flex: 1,
alignSelf: 'flex-end',
marginLeft: 32,
marginRight: 32
},
buttonText: {
fontFamily: 'Metropolis-Regular',
fontSize: 28,
color: Colors.White
}
});
export default firstRunStyle;

View file

@ -1,30 +1,34 @@
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import Colors from './colors';
const splashStyle = StyleSheet.create({ const splashStyle = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
backgroundColor: '#40b89a' backgroundColor: Colors.LbryGreen
}, },
title: { title: {
fontFamily: 'Metropolis-Bold', fontFamily: 'Metropolis-Bold',
fontSize: 64, fontSize: 64,
textAlign: 'center', textAlign: 'center',
marginBottom: 48, marginBottom: 48,
color: '#ffffff' color: Colors.White
},
loading: {
marginBottom: 36
}, },
details: { details: {
fontFamily: 'Metropolis-Regular', fontFamily: 'Metropolis-Regular',
fontSize: 14, fontSize: 14,
marginLeft: 16, marginLeft: 16,
marginRight: 16, marginRight: 16,
color: '#ffffff', color: Colors.White,
textAlign: 'center' textAlign: 'center'
}, },
message: { message: {
fontFamily: 'Metropolis-Bold', fontFamily: 'Metropolis-Bold',
fontSize: 18, fontSize: 18,
color: '#ffffff', color: Colors.White,
marginLeft: 16, marginLeft: 16,
marginRight: 16, marginRight: 16,
marginBottom: 4, marginBottom: 4,

View file

@ -36,7 +36,7 @@ version.filename = %(source.dir)s/main.py
# (list) Application requirements # (list) Application requirements
# comma seperated e.g. requirements = sqlite3,kivy # comma seperated e.g. requirements = sqlite3,kivy
requirements = openssl, sqlite3, hostpython2, android, pyjnius, certifi==2018.4.16, constantly, incremental, functools32, miniupnpc==1.9, gmpy==1.17, twisted==16.6.0, appdirs==1.4.3, argparse==1.2.1, docopt==0.6.2, base58==0.2.2, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse==0.2.0, jsonrpc==1.2, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2==1.3, pyyaml==3.12, qrcode==5.2.2, requests==2.9.1, seccure==0.3.1.3, attrs==18.1.0, pyasn1, pyasn1-modules, service_identity==16.0.0, six==1.9.0, slowaes==0.1a1, txJSON-RPC==0.5, wsgiref==0.1.2, zope.interface==4.3.3, protobuf==3.2.0, keyring==10.4.0, git+https://github.com/lbryio/lbryschema.git@v0.0.15#egg=lbryschema, git+https://github.com/lbryio/lbryum.git#egg=lbryum, git+https://github.com/lbryio/lbry.git#egg=lbrynet, asn1crypto, cryptography==2.2.2, pyopenssl==17.4.0, treq==17.8.0, funcsigs, mock, pbr, unqlite requirements = openssl, sqlite3, hostpython2, android, distro, pyjnius, certifi==2018.4.16, constantly, incremental, functools32, miniupnpc==1.9, gmpy==1.17, twisted==16.6.0, appdirs==1.4.3, argparse==1.2.1, docopt==0.6.2, base58==0.2.2, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse==0.2.0, jsonrpc==1.2, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2==1.3, pyyaml==3.12, qrcode==5.2.2, requests==2.9.1, seccure==0.3.1.3, attrs==18.1.0, pyasn1, pyasn1-modules, service_identity==16.0.0, six==1.9.0, slowaes==0.1a1, txJSON-RPC==0.5, wsgiref==0.1.2, zope.interface==4.3.3, protobuf==3.2.0, keyring==10.4.0, git+https://github.com/lbryio/lbryschema.git@v0.0.15#egg=lbryschema, git+https://github.com/lbryio/lbryum.git#egg=lbryum, git+https://github.com/lbryio/lbry.git#egg=lbrynet, asn1crypto, cryptography==2.2.2, pyopenssl==17.4.0, treq==17.8.0, funcsigs, mock, pbr, unqlite
# (str) Custom source folders for requirements # (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes # Sets custom source for any requirements with recipes

View file

@ -36,7 +36,7 @@ version.filename = %(source.dir)s/main.py
# (list) Application requirements # (list) Application requirements
# comma seperated e.g. requirements = sqlite3,kivy # comma seperated e.g. requirements = sqlite3,kivy
requirements = openssl, sqlite3, hostpython2, android, pyjnius, certifi==2018.4.16, constantly, incremental, functools32, miniupnpc==1.9, gmpy==1.17, twisted==16.6.0, appdirs==1.4.3, argparse==1.2.1, docopt==0.6.2, base58==0.2.2, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse==0.2.0, jsonrpc==1.2, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2==1.3, pyyaml==3.12, qrcode==5.2.2, requests==2.9.1, seccure==0.3.1.3, attrs==18.1.0, pyasn1, pyasn1-modules, service_identity==16.0.0, six==1.9.0, slowaes==0.1a1, txJSON-RPC==0.5, wsgiref==0.1.2, zope.interface==4.3.3, protobuf==3.2.0, keyring==10.4.0, git+https://github.com/lbryio/lbryschema.git@v0.0.15#egg=lbryschema, git+https://github.com/lbryio/lbryum.git#egg=lbryum, git+https://github.com/lbryio/lbry.git#egg=lbrynet, asn1crypto, cryptography==2.2.2, pyopenssl==17.4.0, treq==17.8.0, funcsigs, mock, pbr, unqlite requirements = openssl, sqlite3, hostpython2, android, distro, pyjnius, certifi==2018.4.16, constantly, incremental, functools32, miniupnpc==1.9, gmpy==1.17, twisted==16.6.0, appdirs==1.4.3, argparse==1.2.1, docopt==0.6.2, base58==0.2.2, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse==0.2.0, jsonrpc==1.2, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2==1.3, pyyaml==3.12, qrcode==5.2.2, requests==2.9.1, seccure==0.3.1.3, attrs==18.1.0, pyasn1, pyasn1-modules, service_identity==16.0.0, six==1.9.0, slowaes==0.1a1, txJSON-RPC==0.5, wsgiref==0.1.2, zope.interface==4.3.3, protobuf==3.2.0, keyring==10.4.0, git+https://github.com/lbryio/lbryschema.git@v0.0.15#egg=lbryschema, git+https://github.com/lbryio/lbryum.git#egg=lbryum, git+https://github.com/lbryio/lbry.git#egg=lbrynet, asn1crypto, cryptography==2.2.2, pyopenssl==17.4.0, treq==17.8.0, funcsigs, mock, pbr, unqlite
# (str) Custom source folders for requirements # (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes # Sets custom source for any requirements with recipes

View file

@ -380,6 +380,14 @@ main.py that loads it.''')
url_scheme=url_scheme, url_scheme=url_scheme,
) )
# add themes.xml
render(
'themes.tmpl.xml',
'src/main/res/values/themes.xml',
args=args,
url_scheme=url_scheme,
)
# add activity_service_control # add activity_service_control
render( render(
'activity_service_control.xml', 'activity_service_control.xml',

View file

@ -52,7 +52,7 @@
<application android:label="@string/app_name" <application android:label="@string/app_name"
android:icon="@drawable/icon" android:icon="@drawable/icon"
android:allowBackup="true" android:allowBackup="true"
android:theme="@android:style/Theme.Material.Light" android:theme="@style/LbryAppTheme"
android:hardwareAccelerated="true"> android:hardwareAccelerated="true">
{% for m in args.meta_data %} {% for m in args.meta_data %}

View file

@ -6,4 +6,5 @@
<color name="red">#FF0000</color> <color name="red">#FF0000</color>
<color name="green">#00C000</color> <color name="green">#00C000</color>
<color name="lbrygreen">#40B89A</color>
</resources> </resources>

View file

@ -0,0 +1,5 @@
<resources>
<style name="LbryAppTheme" parent="@android:style/Theme.Material.Light">
<item name="android:windowBackground">@color/lbrygreen</item>
</style>
</resources>

View file

@ -141,10 +141,15 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
SharedPreferences sp = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
if (!sp.getBoolean("firstRun", true)) {
// We're not showing the welcome page, so it's okay to start the daemon service
// because this is not the first run experience
serviceRunning = isServiceRunning(LbrynetService.class); serviceRunning = isServiceRunning(LbrynetService.class);
if (!serviceRunning) { if (!serviceRunning) {
ServiceHelper.start(this, "", LbrynetService.class, "lbrynetservice"); ServiceHelper.start(this, "", LbrynetService.class, "lbrynetservice");
} }
}
if (mReactInstanceManager != null) { if (mReactInstanceManager != null) {
mReactInstanceManager.onHostResume(this, this); mReactInstanceManager.onHostResume(this, this);

View file

@ -24,6 +24,11 @@ public class DaemonServiceControlModule extends ReactContextBaseJavaModule {
return "DaemonServiceControl"; return "DaemonServiceControl";
} }
@ReactMethod
public void startService() {
ServiceHelper.start(context, "", LbrynetService.class, "lbrynetservice");
}
@ReactMethod @ReactMethod
public void stopService() { public void stopService() {
ServiceHelper.stop(context, LbrynetService.class); ServiceHelper.stop(context, LbrynetService.class);

View file

@ -0,0 +1,44 @@
package io.lbry.browser.reactmodules;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.SharedPreferences;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import io.lbry.browser.MainActivity;
public class FirstRunModule extends ReactContextBaseJavaModule {
private Context context;
private SharedPreferences sp;
public FirstRunModule(ReactApplicationContext reactContext) {
super(reactContext);
this.context = reactContext;
this.sp = reactContext.getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
}
@Override
public String getName() {
return "FirstRun";
}
@ReactMethod
public void isFirstRun(final Promise promise) {
// If firstRun flag does not exist, default to true
boolean firstRun = sp.getBoolean("firstRun", true);
promise.resolve(firstRun);
}
@ReactMethod
public void firstRunCompleted() {
SharedPreferences.Editor editor = sp.edit();
editor.putBoolean("firstRun", false);
editor.commit();
}
}

View file

@ -1,15 +1,14 @@
package io.lbry.browser.reactmodules; package io.lbry.browser.reactmodules;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReactMethod;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
public class VersionInfoModule extends ReactContextBaseJavaModule { public class VersionInfoModule extends ReactContextBaseJavaModule {
private Context context; private Context context;

View file

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