Skip to content

Commit

Permalink
Merge pull request #93 from thornbill/mobx
Browse files Browse the repository at this point in the history
Migrate to mobx for global app state
  • Loading branch information
anthonylavado authored Jun 30, 2020
2 parents 05c1ad5 + f11774d commit cb3dd5c
Show file tree
Hide file tree
Showing 13 changed files with 359 additions and 81 deletions.
44 changes: 41 additions & 3 deletions App.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,66 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import 'mobx-react-lite/batchingForReactNative';

import React, { useEffect, useState } from 'react';
import { Platform, StatusBar } from 'react-native';
import { AsyncStorage, Platform, StatusBar } from 'react-native';
import { ThemeProvider } from 'react-native-elements';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { observer } from 'mobx-react';
import { AsyncTrunk } from 'mobx-sync';
import { AppLoading } from 'expo';
import { Asset } from 'expo-asset';
import * as Font from 'expo-font';
import * as ScreenOrientation from 'expo-screen-orientation';
import { Ionicons } from '@expo/vector-icons';
import PropTypes from 'prop-types';

import { useStores } from './hooks/useStores';
import StorageKeys from './constants/Storage';
import AppNavigator from './navigation/AppNavigator';
import CachingStorage from './utils/CachingStorage';
import Theme from './utils/Theme';

function App({ skipLoadingScreen }) {
const App = observer(({ skipLoadingScreen }) => {
const [isSplashReady, setIsSplashReady] = useState(false);
const { rootStore } = useStores();

const trunk = new AsyncTrunk(rootStore, {
storage: AsyncStorage
});

const hydrateStores = async () => {
// Migrate servers and settings
// TODO: Remove this for next release
const servers = await CachingStorage.getInstance().getItem(StorageKeys.Servers);
if (servers) {
const activeServer = await CachingStorage.getInstance().getItem(StorageKeys.ActiveServer) || 0;

// Initialize the store with the existing servers and settings
await trunk.init({
serverStore: { servers },
settingStore: { activeServer }
});

// Remove old data values
AsyncStorage.multiRemove(Object.values(StorageKeys));
} else {
// No servers saved in the old method, initialize normally
await trunk.init();
}

rootStore.storeLoaded = true;
};

useEffect(() => {
// Lock portrait orientation on iPhone
if (Platform.OS === 'ios' && !Platform.isPad) {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP);
}

// Hydrate mobx data stores
hydrateStores();
}, []);

const loadImagesAsync = () => {
Expand Down Expand Up @@ -64,7 +102,7 @@ function App({ skipLoadingScreen }) {
</ThemeProvider>
</SafeAreaProvider>
);
}
});

App.propTypes = {
skipLoadingScreen: PropTypes.bool
Expand Down
2 changes: 1 addition & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo']
presets: ['babel-preset-expo', 'mobx']
};
};
24 changes: 12 additions & 12 deletions components/ServerInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,20 @@ import React from 'react';
import { ActivityIndicator, Platform, StyleSheet } from 'react-native';
import { Input, colors } from 'react-native-elements';
import { useNavigation } from '@react-navigation/native';
import { observer } from 'mobx-react';
import PropTypes from 'prop-types';

import { useStores } from '../hooks/useStores';
import Colors from '../constants/Colors';
import StorageKeys from '../constants/Storage';
import CachingStorage from '../utils/CachingStorage';
import JellyfinValidator from '../utils/JellyfinValidator';

const sanitizeHost = (url = '') => url.trim();

@observer
class ServerInput extends React.Component {
static propTypes = {
navigation: PropTypes.object.isRequired,
rootStore: PropTypes.object.isRequired,
onSuccess: PropTypes.func,
successScreen: PropTypes.string
}
Expand Down Expand Up @@ -67,11 +69,9 @@ class ServerInput extends React.Component {
return;
}

// Save the server details to app storage
let servers = await CachingStorage.getInstance().getItem(StorageKeys.Servers) || [];
servers = servers.concat([{ url }]);
await CachingStorage.getInstance().setItem(StorageKeys.Servers, servers);
await CachingStorage.getInstance().setItem(StorageKeys.ActiveServer, servers.length - 1);
// Save the server details
this.props.rootStore.serverStore.addServer({ url });
this.props.rootStore.settingStore.activeServer = this.props.rootStore.serverStore.servers.length - 1;
// Call the success callback if present
if (this.props.onSuccess) {
this.props.onSuccess();
Expand All @@ -81,7 +81,7 @@ class ServerInput extends React.Component {
index: 0,
routes: [{
name: this.props.successScreen || 'Main',
props: { activeServer: servers.length - 1 }
props: { activeServer: this.props.rootStore.settingStore.activeServer }
}]
});
} else {
Expand Down Expand Up @@ -135,9 +135,9 @@ const styles = StyleSheet.create({
});

// Inject the Navigation Hook as a prop to mimic the legacy behavior
const ServerInputWithNavigation = function(props) {
const navigation = useNavigation();
return <ServerInput {...props} navigation={navigation} />;
};
const ServerInputWithNavigation = observer((props) => {
const stores = useStores();
return <ServerInput {...props} navigation={useNavigation()} {...stores} />;
});

export default ServerInputWithNavigation;
14 changes: 14 additions & 0 deletions hooks/useStores.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { createContext, useContext } from 'react';

import RootStore from '../stores/RootStore';

export const storesContext = createContext({
rootStore: new RootStore()
});

export const useStores = () => useContext(storesContext);
30 changes: 7 additions & 23 deletions navigation/AppNavigator.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import React, { useState } from 'react';
import React from 'react';
import { NavigationContainer, DarkTheme } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { observer } from 'mobx-react';
import { SplashScreen } from 'expo';
import { Ionicons } from '@expo/vector-icons';

import { useStores } from '../hooks/useStores';
import Colors from '../constants/Colors';
import StorageKeys from '../constants/Storage';
import CachingStorage from '../utils/CachingStorage';
import AddServerScreen from '../screens/AddServerScreen';
import HomeScreen from '../screens/HomeScreen';
import LoadingScreen from '../screens/LoadingScreen';
import SettingsScreen from '../screens/SettingsScreen';

// Customize theme for navigator
Expand Down Expand Up @@ -63,31 +62,16 @@ function Main() {
);
}

function AppNavigator() {
const [isLoading, setIsLoading] = useState(true);
const [servers, setServers] = useState(null);

async function bootstrap() {
// Fetch any saved servers
const savedServers = await CachingStorage.getInstance().getItem(StorageKeys.Servers);
setServers(savedServers);
setIsLoading(false);
}

bootstrap();

// Display the loading screen until bootstrapping is complete
if (isLoading) {
return <LoadingScreen />;
}
const AppNavigator = observer(() => {
const { rootStore } = useStores();

// Ensure the splash screen is hidden when loading is finished
SplashScreen.hide();

return (
<NavigationContainer theme={theme}>
<Stack.Navigator
initialRouteName={(servers && servers.length > 0) ? 'Main' : 'AddServer'}
initialRouteName={(rootStore.serverStore.servers.length > 0) ? 'Main' : 'AddServer'}
headerMode='screen'
screenOptions={{ headerShown: false }}
>
Expand All @@ -111,6 +95,6 @@ function AppNavigator() {
</Stack.Navigator>
</NavigationContainer>
);
}
});

export default AppNavigator;
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"expo-keep-awake": "~8.1.0",
"expo-screen-orientation": "~1.0.0",
"expo-web-browser": "~8.1.0",
"mobx": "^5.15.4",
"mobx-react": "^6.2.2",
"mobx-sync": "^3.0.0",
"prop-types": "^15.7.2",
"react": "16.9.0",
"react-lifecycles-compat": "^3.0.4",
Expand All @@ -37,6 +40,7 @@
"devDependencies": {
"babel-eslint": "^10.0.2",
"babel-preset-expo": "^8.1.0",
"babel-preset-mobx": "^2.0.0",
"eslint": "^6.1.0",
"eslint-plugin-eslint-comments": "^3.1.2",
"eslint-plugin-import": "^2.20.2",
Expand Down
28 changes: 16 additions & 12 deletions screens/HomeScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import { Button, Text } from 'react-native-elements';
import { SafeAreaView } from 'react-native-safe-area-context';
import { WebView } from 'react-native-webview';
import { useNavigation, useRoute } from '@react-navigation/native';
import { observer } from 'mobx-react';
import Constants from 'expo-constants';
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
import * as ScreenOrientation from 'expo-screen-orientation';
import PropTypes from 'prop-types';

import { useStores } from '../hooks/useStores';
import Colors from '../constants/Colors';
import StorageKeys from '../constants/Storage';
import CachingStorage from '../utils/CachingStorage';
import { getAppName, getSafeDeviceName } from '../utils/Device';
import JellyfinValidator from '../utils/JellyfinValidator';
import NativeShell from '../utils/NativeShell';
Expand All @@ -35,6 +35,7 @@ ${NativeShell}
true;
`;

@observer
class HomeScreen extends React.Component {
state = {
server: null,
Expand All @@ -47,21 +48,23 @@ class HomeScreen extends React.Component {

static propTypes = {
navigation: PropTypes.object.isRequired,
route: PropTypes.object.isRequired
route: PropTypes.object.isRequired,
rootStore: PropTypes.object.isRequired
}

async bootstrapAsync() {
let server = await CachingStorage.getInstance().getItem(StorageKeys.Servers);
let activeServer = await CachingStorage.getInstance().getItem(StorageKeys.ActiveServer) || 0;
const servers = this.props.rootStore.serverStore.servers;
let activeServer = this.props.rootStore.settingStore.activeServer;

// If the activeServer is greater than the length of the server array, reset it to 0
if (activeServer && server.length && activeServer > server.length - 1) {
await CachingStorage.getInstance().setItem(StorageKeys.ActiveServer, 0);
if (activeServer && servers.length && activeServer > servers.length - 1) {
this.props.rootStore.settingStore.activeServer = 0;
activeServer = 0;
}

if (server.length > 0) {
server = server[activeServer];
let server;
if (servers.length > 0) {
server = servers[activeServer];
}

const serverUrl = JellyfinValidator.getServerUrl(server);
Expand Down Expand Up @@ -287,8 +290,9 @@ const styles = StyleSheet.create({
});

// Inject the Navigation Hook as a prop to mimic the legacy behavior
const HomeScreenWithNavigation = function(props) {
return <HomeScreen {...props} navigation={useNavigation()} route={useRoute()} />;
};
const HomeScreenWithNavigation = observer((props) => {
const stores = useStores();
return <HomeScreen {...props} navigation={useNavigation()} route={useRoute()} {...stores} />;
});

export default HomeScreenWithNavigation;
Loading

0 comments on commit cb3dd5c

Please sign in to comment.