UNPKG

react-native-chunky

Version:
410 lines (336 loc) 14.2 kB
import { StackNavigator, TabNavigator, DrawerNavigator, DrawerView, NavigationActions } from 'react-navigation' import URL from 'url-parse' import React, { PureComponent } from 'react' import MaterialIcons from 'react-native-vector-icons/MaterialIcons' import { Icon } from 'react-native-elements' import { Image, Platform, NetInfo, Button, View, Text, ScrollView, ActivityIndicator } from 'react-native' import { Styles } from 'react-chunky' /** * The main React Native Chunky App instance is in charge of parsing the chunky.json * content and generating the app sections and navigators. This is the main entry point * for a Chunky mobile app. This class is not to be instantiated manually, but it is rather * invoked by the main Chunky AppContainer. * @extends {PureComponent} */ export default class App extends PureComponent { /** * This App constructor is not to be called manually, but it's injected by the Chunky AppContainer * @param {object} props the App gets its properties injected by the Chunky AppContainer */ constructor(props) { super(props) // We're ready to keep track of our app sections and navigator this.state = { hasNetworkConnection: true } } renderProgress() { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: this.props.theme.primaryColor }}> <ActivityIndicator color= "#ffffff"/> </View>) } restart(hasNetworkConnection) { const sections = this._createSections() const navigator = this._createAppNavigator(sections) this.setState({ sections, navigator, hasNetworkConnection }) } componentDidMount() { NetInfo.isConnected.fetch().done(hasNetworkConnection => this._handleConnectivityChange(hasNetworkConnection)) NetInfo.isConnected.addEventListener( 'change', (hasNetworkConnection) => this._handleConnectivityChange(hasNetworkConnection)) } _handleConnectivityChange (hasNetworkConnection) { if (this.state.sections && (this.state.hasNetworkConnection === hasNetworkConnection)) { return } this.setState({ hasNetworkConnection }) this.restart(hasNetworkConnection) } componentWillUnmount() { NetInfo.isConnected.removeEventListener('change', this._handleConnectivityChange) } _resolveTransitionFromURI(uri) { const url = new URL(uri, true) return { name: `show${url.hostname.charAt(0).toUpperCase()}${url.hostname.substring(1)}`, type: url.protocol.slice(0, -1).toLowerCase(), route: url.hostname } } _createSectionNavigatorRoutes(element, section) { // We want to look at a stack element and figure out its parent chunk; // Note that chunks may also have flavours so this looks for the flavor, if any const [ chunkName, chunkFlavorName ] = element.split("/") // This is our chunk, if it actually exists const chunk = this.props.chunks[chunkName] if (!chunk) { // Let's verify that it actually points to a real chunk return } if (chunkFlavorName && (!chunk.flavors || !chunk.flavors[chunkFlavorName])) { // Great, let's check the flavor now return } // Great, so we've cleared the chunk and its flavor, if any, let's check the icon // const iconName = `${chunkName}/${ chunkFlavorName ? chunkFlavorName : 'icon' }` if (!chunk.routes || chunk.routes.length === 0) { // One last thing, let's also make sure the chunk has routes return } // These routes will be the ones we want to parse out of the chunk, as necessary var routes = {} var rootRoute = {} // Let's build up global transitions, if any var globalTransitions = {} if (this.props.transitions) { this.props.transitions.forEach(transitionUri => { // Let's resolve global transitions const transition = this._resolveTransitionFromURI(transitionUri) globalTransitions[transition.name] = transition }) } for (let routeName in chunk.routes) { // Great, this chunk has routes, let's look through all of them var route = chunk.routes[routeName] if (Object.keys(rootRoute).length === 0) { route.root = true route.menuTitle = route.title rootRoute = Object.assign({}, route) } else { route.icon = rootRoute.icon route.menuTitle = rootRoute.menuTitle } // Let's build up the transitions, if any var transitions = {} if (chunk.transitions) { chunk.transitions.forEach(transitionUri => { // Parse this transition's URI const transition = this._resolveTransitionFromURI(transitionUri) if (transition.route && chunk.routes[transition.route]) { // This is a local transition, so let's resolve locally transition.route = `${section.name}/${chunkName}/${transition.route}` transitions[transition.name] = transition return } if (globalTransitions[transition.name]) { // Let's look through the global transitions, if any transitions[transition.name] = Object.assign({}, globalTransitions[transition.name]) } }) } // Let's pass over the theme as well const theme = this.props.theme // For each route, we want to compose its properties const screenProps = Object.assign({ // Defaults strings: {}, startOperationsOnMount: true }, { theme, transitions, ...route, chunkName }) // Resolve strings var resolvedStrings = {} for (const string in screenProps.strings) { resolvedStrings[string] = this.props.strings[screenProps.strings[string]] || `??${screenProps.strings[string]}??` } screenProps.strings = Object.assign({}, this.props.strings, resolvedStrings) // Now that we have properties, we're ready to initialize the route's screen const RouteScreen = route.screen const Screen = (props) => { return <RouteScreen {...props} {...screenProps}/> } // Good, so let's add this route to the navigator routes[`${section.name}/${chunkName}/${routeName}`] = { screen: Screen, navigationOptions: this._createRouteNavigationOptions(section, route) } } // We've got ourselves some routes so we should be done with this return routes } _createRouteNavigationOptions(section, route) { // Construct a top left menu button, if necessary const headerLeft = (navigation) => { if (section.layout === 'drawer' && route.root) { return (<MaterialIcons.Button name="menu" size={28} color={Styles.styleColor(this.props.theme.navigationTintColor)} backgroundColor={Styles.styleColor(this.props.theme.navigationColor)} onPress={() => { navigation.navigate("DrawerOpen") }}/> ) } if (!route.root || route.forceBack) { return (<MaterialIcons.Button name="navigate-before" size={28} color={Styles.styleColor(this.props.theme.navigationTintColor)} backgroundColor={Styles.styleColor(this.props.theme.navigationColor)} onPress={() => { navigation.goBack() }}/> ) } return } // Before we keep track of the screen inside our navigator, we need some navigation options return ({ navigation }) => { return { title: (navigation.state.params && navigation.state.params.title ? navigation.state.params.title : route.title || ""), headerTintColor: Styles.styleColor(this.props.theme.navigationTintColor), headerStyle: { backgroundColor: Styles.styleColor(this.props.theme.navigationColor) }, headerLeft: headerLeft(navigation), gesturesEnabled: false, tabBarLabel: route.menuTitle || "", tabBarIcon: ({ tintColor }) => this._createRouteIcon(route, tintColor) } } } _createRouteIcon(route, tintColor) { if (!route.icon) { return <Icon name='help' type='material' color={tintColor}/> } var [iconType, iconName] = route.icon.split("/") if (!iconName) { iconName = iconType iconType = 'material' } return (<Icon name={iconName} type={iconType} color={tintColor}/>) } _createSectionNavigator(section) { if (!section || !section.stack) { // We don't even consider stackless sections return } // These are the routes that we need to compile for this section's navigator var routes = {} // Let's look through the stack and build some routes for this section's navigator var elementIndex = 0 section.stack.forEach(element => { var elementRoutes = {} if (element && typeof element === 'string') { // The first kind of element in the sack is a plain string, that signifies a chunk elementRoutes = Object.assign({}, elementRoutes, this._createSectionNavigatorRoutes(element, section)) } else if (element && Array.isArray(element) && element.length > 0) { // Another type of element in the sack is a list of strings, that each signifies a chunk var composedRoutes = {} element.forEach(subElement => { composedRoutes = Object.assign({}, composedRoutes, this._createSectionNavigatorRoutes(subElement, section)) }) elementRoutes = Object.assign({}, elementRoutes, composedRoutes) } // Compile a list of options for this section's navigator const navigatorConfig = { headerMode: (section.hideHeader ? 'none' : (Platform.OS === 'ios' ? 'float' : 'screen')), transitionConfig: (props) => {} } routes[`${section.name}/${elementIndex}`] = { screen: StackNavigator(elementRoutes, navigatorConfig) } elementIndex = elementIndex + 1 }) if (section.layout === "drawer") { return DrawerNavigator(routes, { drawerWidth: 300, drawerPosition: 'left', contentOptions: { activeTintColor: Styles.styleColor(this.props.theme.navigationTintColor), inactiveTintColor: Styles.styleColor(this.props.theme.navigationTintColor), style: { marginVertical: 0, backgroundColor: Styles.styleColor(this.props.theme.navigationColor) } }, contentComponent: props => <ScrollView style={{ backgroundColor: Styles.styleColor(this.props.theme.navigationColor) }}><DrawerView.Items {...props} /></ScrollView> }) } if (section.layout === "tabs") { return TabNavigator(routes, { headerMode: 'none', tabBarOptions: { scrollEnabled: false, showIcon: true, showLabel: (Platform.OS === 'ios') } }) } return StackNavigator(routes, { headerMode: 'none', transitionConfig: (props) => {} }) } _createSections() { // These are the sections we want to generate and initialize var sections = {} for(const sectionName in this.props.sections) { // Look through all the app's sections and for each, build defaults if necessary var section = this.props.sections[sectionName] section.name = sectionName section.layout = section.layout || "default" // Let's also generate a navigator for this section section.navigator = this._createSectionNavigator(section) if (!section.navigator) { // We want to skip sections without navigators continue } // Let's keep track of all the resolved sections sections[sectionName] = section } // And here are all our valid app sections return sections } _createAppNavigator(sections) { // We will use the section navigators to compose the main app navigator var subNavigators = {} for (let name in sections) { // We want to look through all the sections and pull out each section navigator const section = sections[name] subNavigators[name] = { screen: section.navigator } } // Let's put them all together into a headerless stack navigator const navigator = StackNavigator(subNavigators, { headerMode: 'none' }) // Save the original handler const defaultGetStateForAction = navigator.router.getStateForAction navigator.router.getStateForAction = (action, state) => { if (action.type === 'Navigation/BACK' && state.routes[state.index].index === 0) { // Ignore the back on the first screen // return state } var newState = defaultGetStateForAction(action, state) if (action.type === 'Navigation/NAVIGATE' && action.params && action.params.transition && action.params.transition.type === 'replace') { this.trimRoutes(action.routeName, newState) } // Handle all other actions with the default handler return newState } return navigator } trimRoutes(name, state, deep) { if (!state || !state.routes) { return } var found = false var index = 0 state.routes.forEach(route => { if (found) { return true } if (route.routeName === name) { found = Object.assign({}, route) if (index > 0 && deep) { state.routes.splice(index - 1, 1) state.index = index - 1 return true } } index = index + 1 }) if (found) { return true } state.routes.forEach(route => { if (this.trimRoutes(name, route, true)) { found = true return } }) return found } renderNoNetworkConnection() { return ( <View style={{ flex: 1, flexDirection: 'column', backgroundColor: this.props.theme.primaryColor, alignItems: "center", justifyContent: "center" }}> <Text style={{ fontSize: 20, fontWeight: "bold", color: "#ffffff", textAlign: "center" }}> You are currently offline. Please reconnect to the Internet. </Text> </View>) } render() { if (!this.state.sections) { return this.renderProgress() } if (!this.state.hasNetworkConnection) { return this.renderNoNetworkConnection() } // The only element we need to render here is the main app navigator const AppNavigator = this.state.navigator return <AppNavigator/> } }