Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate to mobx for global app state #93

Merged
merged 3 commits into from
Jun 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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