Removed react-native-router-flux and added custom navigation logic (#102)

* Removed react-native-router-flux and replaced it with custom navigation

* Split Logout component to have a container

* Replaced RootContainer with RootLayout

* Removed lodash from package.json

* Removed unnecessary state from Logout component

* Added placeholder for RHS search

* Renamed routes to scenes and moved some logic to there

* Added Root route to handle any initialization on app load

* Added unit tests for navigation reducer

* Fixed eslint errors

* Removed commented out code from Router

* Renamed Channel route to ChannelView to avoid confusion

* Used a separate index.js for component containers

* Simplified mapStateToProps for RootLayoutContainer

* Removed file accidentally added in merge conflict
This commit is contained in:
Harrison Healey 2016-12-08 10:37:38 -05:00 committed by enahum
parent 9125d0cac2
commit 8732febd8c
37 changed files with 879 additions and 232 deletions

View file

@ -7,7 +7,6 @@
"intl": "1.2.5",
"isomorphic-fetch": "2.2.1",
"keymirror": "0.1.1",
"lodash": "4.16.4",
"react": "15.3.2",
"react-addons-pure-render-mixin": "15.3.2",
"react-intl": "2.1.5",
@ -15,7 +14,6 @@
"react-native-button": "1.7.1",
"react-native-drawer": "2.3.0",
"react-native-keyboard-spacer": "0.3.0",
"react-native-router-flux": "3.36.0",
"react-native-vector-icons": "2.1.0",
"react-redux": "4.4.5",
"redux": "3.6.0",

View file

@ -0,0 +1,68 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {NavigationTypes} from 'constants';
import Routes from 'navigation/routes';
export function goBack() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_POP
}, getState);
};
}
export function goToLogin() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: Routes.Login
}, getState);
};
}
export function goToSelectTeam() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: Routes.SelectTeam
}, getState);
};
}
export function goToChannelView() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: Routes.ChannelView
}, getState);
};
}
export function goToRecentMentions() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: {
...Routes.Search,
props: {
searchType: 'recent_mentions'
}
}
}, getState);
};
}
export function goToFlaggedPosts() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: {
...Routes.Search,
props: {
searchType: 'flagged_posts'
}
}
}, getState);
};
}

15
src/actions/views/root.js Normal file
View file

@ -0,0 +1,15 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {NavigationTypes} from 'constants';
import Routes from 'navigation/routes';
export function goToSelectServer() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_RESET,
routes: [Routes.SelectServer],
index: 0
}, getState);
};
}

View file

@ -1,50 +0,0 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component, PropTypes} from 'react';
import {TouchableHighlight, Text} from 'react-native';
import {logout} from 'actions/users';
import {RequestStatus} from 'constants';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {Actions as Routes} from 'react-native-router-flux';
const propTypes = {
logout: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
};
class Logout extends Component {
static propTypes = propTypes;
componentWillReceiveProps(nextProps) {
if (this.props.logout.status === RequestStatus.STARTED &&
nextProps.logout.status === RequestStatus.SUCCESS) {
Routes.popTo('goToSelectServer');
}
}
logout = () => this.props.actions.logout();
render() {
return (
<TouchableHighlight onPress={this.logout}>
<Text>{'logout'}</Text>
</TouchableHighlight>
);
}
}
function mapStateToProps(state) {
return {
logout: state.requests.users.logout
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({logout}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Logout);

View file

@ -0,0 +1,6 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import LogoutContainer from './logout_container';
export default LogoutContainer;

View file

@ -0,0 +1,24 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {TouchableHighlight} from 'react-native';
import FormattedText from 'components/formatted_text';
export default class Logout extends React.Component {
static propTypes = {
actions: React.PropTypes.object.isRequired
}
render() {
return (
<TouchableHighlight onPress={this.props.actions.logout}>
<FormattedText
id='sidebar_right_menu.logout'
defaultMessage='Logout'
/>
</TouchableHighlight>
);
}
}

View file

@ -0,0 +1,17 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {logout} from 'actions/users';
import Logout from './logout.js';
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({logout}, dispatch)
};
}
export default connect(null, mapDispatchToProps)(Logout);

View file

@ -0,0 +1,42 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {StyleSheet, TouchableHighlight, View} from 'react-native';
const Styles = StyleSheet.create({
item: {
alignItems: 'center',
height: 40,
paddingLeft: 10,
paddingRight: 10,
flex: 1,
flexDirection: 'row'
}
});
export default class Item extends React.Component {
static propTypes = {
children: React.PropTypes.node,
onPress: React.PropTypes.func,
style: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.array
])
}
render() {
return (
<TouchableHighlight
underlayColor='rgba(255, 255, 255, 0.3)'
onPress={this.props.onPress}
style={this.props.style}
>
<View style={Styles.item}>
{this.props.children}
</View>
</TouchableHighlight>
);
}
}

View file

@ -0,0 +1,6 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import RightSidebarMenuContainer from './right_sidebar_menu_container';
export default RightSidebarMenuContainer;

View file

@ -3,13 +3,13 @@
import React from 'react';
import FormattedText from './formatted_text.js';
import FormattedText from 'components/formatted_text';
import Icon from 'react-native-vector-icons/FontAwesome';
import Item from './components/item';
import {
ScrollView,
StyleSheet,
Text,
TouchableHighlight,
View
} from 'react-native';
@ -19,14 +19,6 @@ const Styles = StyleSheet.create({
flex: 1,
paddingTop: 20
},
item: {
alignItems: 'center',
height: 40,
paddingLeft: 10,
paddingRight: 10,
flex: 1,
flexDirection: 'row'
},
itemText: {
color: 'white'
},
@ -45,14 +37,29 @@ const Styles = StyleSheet.create({
});
export default class RightSidebarMenu extends React.Component {
handlePress() {
console.log('press'); // eslint-disable-line no-console
static propTypes = {
actions: React.PropTypes.shape({
goToFlaggedPosts: React.PropTypes.func.isRequired,
goToRecentMentions: React.PropTypes.func.isRequired,
logout: React.PropTypes.func.isRequired
}).isRequired,
onClose: React.PropTypes.func.isRequired
}
goToRecentMentions = () => {
this.props.onClose();
this.props.actions.goToRecentMentions();
}
goToFlaggedPosts = () => {
this.props.onClose();
this.props.actions.goToFlaggedPosts();
}
render() {
return (
<ScrollView style={Styles.container}>
<Item onPress={this.handlePress}>
<Item onPress={this.goToRecentMentions}>
<Text style={[Styles.icon, Styles.mentionIcon]}>{'@'}</Text>
<FormattedText
style={Styles.itemText}
@ -60,7 +67,7 @@ export default class RightSidebarMenu extends React.Component {
defaultMessage='Recent Mentions'
/>
</Item>
<Item onPress={this.handlePress}>
<Item onPress={this.goToFlaggedPosts}>
<Icon
style={Styles.icon}
name='flag'
@ -72,7 +79,7 @@ export default class RightSidebarMenu extends React.Component {
/>
</Item>
<Divider/>
<Item onPress={this.handlePress}>
<Item>
<Icon
style={Styles.icon}
name='cog'
@ -83,7 +90,7 @@ export default class RightSidebarMenu extends React.Component {
defaultMessage='Account Settings'
/>
</Item>
<Item onPress={this.handlePress}>
<Item>
<Icon
style={Styles.icon}
name='user-plus'
@ -94,7 +101,7 @@ export default class RightSidebarMenu extends React.Component {
defaultMessage='Invite New Member'
/>
</Item>
<Item onPress={this.handlePress}>
<Item>
<Icon
style={Styles.icon}
name='link'
@ -106,7 +113,7 @@ export default class RightSidebarMenu extends React.Component {
/>
</Item>
<Divider/>
<Item onPress={this.handlePress}>
<Item>
<Icon
style={Styles.icon}
name='globe'
@ -117,7 +124,7 @@ export default class RightSidebarMenu extends React.Component {
defaultMessage='Team Settings'
/>
</Item>
<Item onPress={this.handlePress}>
<Item>
<Icon
style={Styles.icon}
name='users'
@ -128,7 +135,7 @@ export default class RightSidebarMenu extends React.Component {
defaultMessage='Manage Members'
/>
</Item>
<Item onPress={this.handlePress}>
<Item>
<Icon
style={Styles.icon}
name='exchange'
@ -140,7 +147,7 @@ export default class RightSidebarMenu extends React.Component {
/>
</Item>
<Divider/>
<Item onPress={this.handlePress}>
<Item>
<Icon
style={Styles.icon}
name='question'
@ -151,7 +158,7 @@ export default class RightSidebarMenu extends React.Component {
defaultMessage='Help'
/>
</Item>
<Item onPress={this.handlePress}>
<Item>
<Icon
style={Styles.icon}
name='phone'
@ -162,7 +169,7 @@ export default class RightSidebarMenu extends React.Component {
defaultMessage='Report a Problem'
/>
</Item>
<Item onPress={this.handlePress}>
<Item>
<Icon
style={Styles.icon}
name='info'
@ -174,7 +181,7 @@ export default class RightSidebarMenu extends React.Component {
/>
</Item>
<Divider/>
<Item onPress={this.handlePress}>
<Item onPress={this.props.actions.logout}>
<Icon
style={Styles.icon}
name='sign-out'
@ -190,23 +197,6 @@ export default class RightSidebarMenu extends React.Component {
}
}
function Item({onPress, children}) {
return (
<TouchableHighlight
underlayColor='rgba(255, 255, 255, 0.3)'
onPress={onPress}
>
<View style={Styles.item}>
{children}
</View>
</TouchableHighlight>
);
}
Item.propTypes = {
onPress: React.PropTypes.func.isRequired,
children: React.PropTypes.node
};
function Divider() {
return <View style={Styles.divider}/>;
}

View file

@ -0,0 +1,26 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {goToFlaggedPosts, goToRecentMentions} from 'actions/navigation';
import {logout} from 'actions/users';
import RightSidebarMenu from './right_sidebar_menu';
function mapStateToProps(state, ownProps) {
return ownProps;
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToFlaggedPosts,
goToRecentMentions,
logout
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(RightSidebarMenu);

View file

@ -8,6 +8,8 @@ import GeneralTypes from './general';
import UsersTypes from './users';
import TeamsTypes from './teams';
import PostsTypes from './posts';
import NavigationTypes from './navigation';
import RequestStatus from './request_status';
export {
@ -18,5 +20,7 @@ export {
TeamsTypes,
ChannelTypes,
PostsTypes,
NavigationTypes,
RequestStatus
};

View file

@ -0,0 +1,14 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import keymirror from 'keymirror';
const NavigationTypes = keymirror({
NAVIGATION_PUSH: null,
NAVIGATION_POP: null,
NAVIGATION_JUMP: null,
NAVIGATION_JUMP_TO_INDEX: null,
NAVIGATION_RESET: null
});
export default NavigationTypes;

View file

@ -1,58 +0,0 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import Config from 'config';
import {connect} from 'react-redux';
import {loadDevice} from 'actions/device';
import Loading from 'components/loading';
import Routes from 'routes';
import {getTranslations} from 'i18n';
import {IntlProvider} from 'react-intl';
class RootContainer extends React.Component {
static propTypes = {
device: React.PropTypes.object.isRequired,
loadDevice: React.PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.props.loadDevice();
}
render() {
const device = this.props.device;
if (device.loading) {
return (
<Loading/>
);
}
const locale = Config.DefaultLocale;
return (
<IntlProvider
key={locale}
locale={locale}
messages={getTranslations(locale)}
>
<Routes/>
</IntlProvider>
);
}
}
function mapStateToProps(state) {
return {
device: state.views.device
};
}
export default connect(mapStateToProps, {
loadDevice
})(RootContainer);

View file

@ -0,0 +1,27 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {getTranslations} from 'i18n';
import {IntlProvider} from 'react-intl';
export default class RootLayout extends React.Component {
static propTypes = {
children: React.PropTypes.node,
locale: React.PropTypes.string.isRequired
}
render() {
const locale = this.props.locale;
return (
<IntlProvider
locale={locale}
messages={getTranslations(locale)}
>
{this.props.children}
</IntlProvider>
);
}
}

View file

@ -0,0 +1,25 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import Config from 'config';
import RootLayout from './root_layout';
function mapStateToProps(state, ownProps) {
const users = state.entities.users;
const currentUserId = users.currentId;
let locale = Config.DefaultLocale;
if (currentUserId && users.profiles[currentUserId]) {
locale = users.profiles[currentUserId].locale;
}
return {
...ownProps,
locale
};
}
export default connect(mapStateToProps)(RootLayout);

View file

@ -5,13 +5,17 @@ import React from 'react';
import {Provider} from 'react-redux';
import store from 'store';
import RootContainer from 'containers/root_container.js';
import Router from 'navigation/router';
import RootLayout from 'layouts/root_layout/root_layout_container';
export default class Mattermost extends React.Component {
render() {
return (
<Provider store={store}>
<RootContainer/>
<RootLayout>
<Router/>
</RootLayout>
</Provider>
);
}

122
src/navigation/router.js Normal file
View file

@ -0,0 +1,122 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {goBack} from 'actions/navigation';
import {getComponentForScene} from 'scenes';
import {Easing, NavigationExperimental, View} from 'react-native';
import FormattedText from 'components/formatted_text.js';
class Router extends React.Component {
static propTypes = {
navigation: React.PropTypes.object,
actions: React.PropTypes.shape({
goBack: React.PropTypes.func
}).isRequired
}
renderTransition = (transitionProps) => {
let title;
if (transitionProps.scene.route.title) {
title = (
<NavigationExperimental.Header
{...transitionProps}
onNavigateBack={this.props.actions.goBack}
renderTitleComponent={this.renderTitle}
/>
);
}
const renderedScenes = transitionProps.scenes.map((scene) => {
const cardProps = {
...transitionProps,
scene
};
return (
<NavigationExperimental.Card
{...cardProps}
style={NavigationExperimental.Card.PagerStyleInterpolator.forHorizontal({
...cardProps,
onNavigateBack: this.props.actions.goBack
})}
onNavigateBack={this.props.actions.goBack}
renderScene={this.renderScene}
key={scene.key}
/>
);
});
return (
<View style={{flex: 1, flexDirection: 'column-reverse'}}>
<View style={{flex: 1}}>
{renderedScenes}
</View>
{title}
</View>
);
}
renderTitle = ({scene}) => {
const title = scene.route.title;
if (!title) {
return null;
}
return (
<NavigationExperimental.Header.Title>
<FormattedText
id={title.id}
defaultMessage={title.defaultMessage}
/>
</NavigationExperimental.Header.Title>
);
}
renderScene = ({scene}) => {
const SceneComponent = getComponentForScene(scene.route.key);
return <SceneComponent {...scene.route.props}/>;
}
configureTransition = () => {
return {
duration: 500,
easing: Easing.inOut(Easing.ease)
};
}
render = () => {
return (
<NavigationExperimental.Transitioner
style={{flex: 1}}
navigationState={this.props.navigation}
render={this.renderTransition}
configureTransition={this.configureTransition}
/>
);
}
}
function mapStateToProps(state) {
return {
navigation: state.navigation
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goBack
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Router);

26
src/navigation/routes.js Normal file
View file

@ -0,0 +1,26 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
export default {
Root: {
key: 'Root'
},
SelectServer: {
key: 'SelectServer',
title: {id: 'mobile.routes.enterServerUrl', defaultMessage: 'Enter Server URL'}
},
Login: {
key: 'Login',
title: {id: 'mobile.routes.login', defaultMessage: 'Login'}
},
SelectTeam: {
key: 'SelectTeam',
title: {id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'}
},
ChannelView: {
key: 'ChannelView'
},
Search: {
key: 'Search'
}
};

View file

@ -6,9 +6,11 @@ import {combineReducers} from 'redux';
import entities from './entities';
import requests from './requests';
import views from './views';
import navigation from './navigation';
export default combineReducers({
entities,
requests,
views
views,
navigation
});

View file

@ -0,0 +1,40 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {NavigationExperimental} from 'react-native';
import {UsersTypes, NavigationTypes} from 'constants';
import Routes from 'navigation/routes.js';
const initialState = {
index: 0,
routes: [
Routes.Root
]
};
export default function(state = initialState, action) {
switch (action.type) {
case NavigationTypes.NAVIGATION_PUSH:
return NavigationExperimental.StateUtils.push(state, action.route);
case NavigationTypes.NAVIGATION_POP:
return NavigationExperimental.StateUtils.pop(state);
case NavigationTypes.NAVIGATION_JUMP:
return NavigationExperimental.StateUtils.jumpTo(state, action.key);
case NavigationTypes.NAVIGATION_JUMP_TO_INDEX:
return NavigationExperimental.StateUtils.jumpToIndex(state, action.index);
case NavigationTypes.NAVIGATION_RESET:
return NavigationExperimental.StateUtils.reset(state, action.routes, action.index);
case UsersTypes.LOGOUT_SUCCESS:
return NavigationExperimental.StateUtils.reset(state, initialState.routes, initialState.index);
default:
return state;
}
}

View file

@ -1,57 +0,0 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component} from 'react';
import {Scene, Router} from 'react-native-router-flux';
import LoginContainer from './login/login_container.js';
import SelectServerContainer from './select_server/select_server_container.js';
import SelectTeamContainer from './select_team/select_team_container.js';
import ChannelContainer from './channel/channel_container.js';
import Logout from 'components/logout';
import {logout} from 'actions/users';
import {injectIntl, intlShape} from 'react-intl';
class Routes extends Component {
static propTypes = {
intl: intlShape.isRequired
};
render() {
const {formatMessage} = this.props.intl;
return (
<Router>
<Scene key='root'>
<Scene
key='goToSelectServer'
component={SelectServerContainer}
title={formatMessage({id: 'mobile.routes.enterServerUrl', defaultMessage: 'Enter Server URL'})}
initial={true}
/>
<Scene
key='goToLogin'
component={LoginContainer}
title={formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'})}
/>
<Scene
key='goToSelectTeam'
component={SelectTeamContainer}
title={formatMessage({id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'})}
renderRightButton={() =>
<Logout actions={logout}/>
}
/>
<Scene
key='goToMain'
component={ChannelContainer}
hideNavBar={true}
/>
</Scene>
</Router>
);
}
}
export default injectIntl(Routes);

View file

@ -6,9 +6,9 @@ import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import Drawer from 'react-native-drawer';
import ChannelSidebar from 'components/channel_sidebar.js';
import Loading from 'components/loading.js';
import RightSidebarMenu from 'components/right_sidebar_menu.js';
import ChannelSidebar from 'components/channel_sidebar';
import Loading from 'components/loading';
import RightSidebarMenu from 'components/right_sidebar_menu';
import {StatusBar, Text, TouchableHighlight, View} from 'react-native';
export default class Channel extends React.Component {
@ -81,13 +81,13 @@ export default class Channel extends React.Component {
<Drawer
open={this.state.rightSidebarOpen}
type='displace'
content={<RightSidebarMenu/>}
content={<RightSidebarMenu onClose={this.closeRightSidebar}/>}
side='right'
tapToClose={true}
onCloseStart={this.closeRightSidebar}
openDrawerOffset={0.2}
>
<View style={{flex: 1, flexDirection: 'row', justifyContent: 'space-between', paddingTop: 20}}>
<View style={{backgroundColor: 'skyblue', flex: 1, flexDirection: 'row', justifyContent: 'space-between', marginTop: 20}}>
<TouchableHighlight
onPress={this.openLeftSidebar}
style={{height: 50, width: 50}}

22
src/scenes/index.js Normal file
View file

@ -0,0 +1,22 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import Channel from './channel/channel_container.js';
import Login from './login/login_container.js';
import Root from './root/root_container.js';
import Search from './search/search_container.js';
import SelectServer from './select_server/select_server_container.js';
import SelectTeam from './select_team/select_team_container.js';
const scenes = {
Root,
SelectServer,
Login,
SelectTeam,
ChannelView: Channel, // Special case the name for this one to avoid ambiguity
Search
};
export function getComponentForScene(key) {
return scenes[key];
}

View file

@ -3,7 +3,6 @@
import React, {Component, PropTypes} from 'react';
import {View, Text, TextInput, Image} from 'react-native';
import {Actions as Routes} from 'react-native-router-flux';
import Button from 'components/button';
import FormattedText from 'components/formatted_text';
@ -36,7 +35,7 @@ class Login extends Component {
componentWillReceiveProps(nextProps) {
if (this.props.loginRequest.status === RequestStatus.STARTED && nextProps.loginRequest.status === RequestStatus.SUCCESS) {
Routes.goToSelectTeam();
this.props.actions.goToSelectTeam();
}
}

View file

@ -6,6 +6,7 @@ import {connect} from 'react-redux';
import {getClientConfig, getLicenseConfig} from 'actions/general';
import * as LoginActions from 'actions/views/login';
import {goToSelectTeam} from 'actions/navigation';
import {login} from 'actions/users';
import Login from './login.js';
@ -27,7 +28,8 @@ function mapDispatchToProps(dispatch) {
...LoginActions,
login,
getClientConfig,
getLicenseConfig
getLicenseConfig,
goToSelectTeam
}, dispatch)
};
}

20
src/scenes/root/root.js Normal file
View file

@ -0,0 +1,20 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
export default class Root extends React.Component {
static propTypes = {
actions: React.PropTypes.object.isRequired
};
componentDidMount() {
// Any initialization logic for navigation, setting up the client, etc should go here
this.props.actions.goToSelectServer();
}
render() {
return null;
}
}

View file

@ -0,0 +1,23 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {goToSelectServer} from 'actions/views/root';
import Root from './root';
function mapStateToProps(state, ownProps) {
return ownProps;
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToSelectServer
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Root);

View file

@ -0,0 +1,30 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import Button from 'components/button';
import {GlobalStyles} from 'styles';
import {Text, View} from 'react-native';
export default class Search extends React.Component {
static propTypes = {
actions: React.PropTypes.object.isRequired,
searchType: React.PropTypes.string.isRequired
};
render() {
return (
<View style={GlobalStyles.container}>
<Text>
{'This is a search for ' + this.props.searchType}
</Text>
<Button onPress={this.props.actions.goBack}>
<Text>
{'<< Go back'}
</Text>
</Button>
</View>
);
}
}

View file

@ -0,0 +1,26 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {goBack} from 'actions/navigation';
import Search from './search';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
...state.views.search
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goBack
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Search);

View file

@ -4,7 +4,6 @@
import React, {Component} from 'react';
import {View, TextInput, Image} from 'react-native';
import KeyboardSpacer from 'react-native-keyboard-spacer';
import {Actions as Routes} from 'react-native-router-flux';
import Client from 'client';
import Button from 'components/button';
@ -30,7 +29,7 @@ class SelectServer extends Component {
this.props.actions.getPing().then(() => {
if (this.props.server.status === RequestStatus.SUCCESS) {
Routes.goToLogin();
this.props.actions.goToLogin();
}
});
};

View file

@ -5,6 +5,7 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getPing} from 'actions/general';
import {goToLogin} from 'actions/navigation';
import * as SelectServerActions from 'actions/views/select_server';
import SelectServer from './select_server';
@ -20,7 +21,8 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
...SelectServerActions,
getPing
getPing,
goToLogin
}, dispatch)
};
}

View file

@ -4,7 +4,6 @@
import React, {Component, PropTypes} from 'react';
import {View, Image, Text} from 'react-native';
import {Actions as Routes} from 'react-native-router-flux';
import Button from 'react-native-button';
import Loading from 'components/loading';
import Icon from 'react-native-vector-icons/MaterialIcons';
@ -30,7 +29,7 @@ export default class SelectTeam extends Component {
onSelectTeam(team) {
this.props.actions.selectTeam(team);
Routes.goToMain({currentTeamId: team.id});
this.props.actions.goToChannelView();
}
render() {

View file

@ -3,7 +3,10 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import * as teamActions from 'actions/teams';
import {goToChannelView} from 'actions/navigation';
import SelectTeamView from './select_team.js';
function mapStateToProps(state) {
@ -17,7 +20,10 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(teamActions, dispatch)
actions: bindActionCreators({
...teamActions,
goToChannelView
}, dispatch)
};
}

View file

@ -7,6 +7,7 @@ import * as Actions from 'actions/users';
import Client from 'client';
import configureStore from 'store/configureStore';
import {RequestStatus} from 'constants';
import Routes from 'navigation/routes';
import TestHelper from 'test_helper';
describe('Actions.Users', () => {
@ -54,13 +55,15 @@ describe('Actions.Users', () => {
const store = configureStore();
store.subscribe(() => {
const logoutRequest = store.getState().requests.users.logout;
const general = store.getState().entities.general;
const users = store.getState().entities.users;
const loginView = store.getState().views.login;
const teams = store.getState().entities.teams;
const channels = store.getState().entities.channels;
const posts = store.getState().entities.posts;
const state = store.getState();
const logoutRequest = state.requests.users.logout;
const general = state.entities.general;
const users = state.entities.users;
const loginView = state.views.login;
const teams = state.entities.teams;
const channels = state.entities.channels;
const posts = state.entities.posts;
const navigation = state.navigation;
if (logoutRequest.status === RequestStatus.SUCCESS || logoutRequest.status === RequestStatus.FAILURE) {
if (logoutRequest.error) {
@ -95,6 +98,8 @@ describe('Actions.Users', () => {
assert.strictEqual(posts.currentFocusedPostId, '', 'current focused post id is not empty');
assert.deepStrictEqual(posts.postsInfo, {}, 'posts info is not empty');
assert.deepStrictEqual(posts.latestPageTime, {}, 'posts latest page time is not empty');
assert.strictEqual(navigation.index, 0, 'navigation not reset to first element of stack');
assert.deepStrictEqual(navigation.routes, [Routes.Root], 'navigation not reset to root route');
done();
}

View file

@ -0,0 +1,223 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import assert from 'assert';
import deepFreeze from 'deep-freeze';
import {NavigationTypes} from 'constants';
import Routes from 'navigation/routes';
import reduceNavigation from 'reducers/navigation';
function reduceAndFreeze(state, actions) {
return deepFreeze(reduceNavigation(state, actions));
}
function initialState() {
return reduceAndFreeze(undefined, {});
}
describe('Reducers.Navigation', () => {
it('initial state', () => {
const state = initialState();
assert.deepEqual(state, {
index: 0,
routes: [Routes.Root]
}, 'initial state');
});
it('NAVIGATION_PUSH', () => {
let state = initialState();
it('first push', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
route: {key: 'fake'}
});
assert.deepEqual(state, {
index: 1,
routes: [Routes.Root, {key: 'fake'}]
});
});
it('second push', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
route: {key: 'fake2'}
});
assert.deepEqual(state, {
index: 1,
routes: [Routes.Root, {key: 'fake'}, {key: 'fake2'}]
});
});
it('push when not at head of stack', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.JUMP,
key: Routes.key.root
});
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
route: {key: 'fake3'}
});
// The next route on the stack is replaced, but anything past that is left unchanged
assert.deepEqual(state, {
index: 1,
routes: [Routes.Root, {key: 'fake3'}, {key: 'fake2'}]
});
});
});
it('NAVIGATION_POP', () => {
let state = initialState();
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
route: {key: 'fake'}
});
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
route: {key: 'fake2'}
});
it('first pop', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.POP
});
assert.deepEqual(state, {
index: 1,
routes: [Routes.Root, {key: 'fake'}]
});
});
it('second pop', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.POP
});
assert.deepEqual(state, {
index: 0,
routes: [Routes.Root]
});
});
it('pop last entry on stack', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.POP
});
assert.deepEqual(state, {
index: 0,
routes: [Routes.Root]
});
});
});
it('NAVIGATION_JUMP', () => {
let state = initialState();
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
route: {key: 'fake'}
});
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
route: {key: 'fake2'}
});
it('jump back', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_JUMP,
key: Routes.Root.key
});
// The rest of the stack is preserved
assert.deepEqual(state, {
index: 0,
routes: [Routes.Root, {key: 'fake'}, {key: 'fake2'}]
});
});
it('jump forward', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_JUMP,
key: 'fake'
});
assert.deepEqual(state, {
index: 2,
routes: [Routes.Root, {key: 'fake'}, {key: 'fake2'}]
});
});
});
it('NAVIGATION_JUMP_TO_INDEX', () => {
let state = initialState();
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
route: {key: 'fake'}
});
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
route: {key: 'fake2'}
});
it('jump back', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_JUMP_TO_INDEX,
index: 1
});
// The rest of the stack is preserved
assert.deepEqual(state, {
index: 0,
routes: [Routes.Root, {key: 'fake'}, {key: 'fake2'}]
});
});
it('jump forward', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_JUMP_TO_INDEX,
index: 2
});
assert.deepEqual(state, {
index: 2,
routes: [Routes.Root, {key: 'fake'}, {key: 'fake2'}]
});
});
});
it('NAVIGATION_RESET', () => {
let state = initialState();
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
route: {key: 'fake'}
});
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
route: {key: 'fake2'}
});
it('reset', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_RESET,
index: 1,
routes: [{key: 'fake3'}, {key: 'fake4'}]
});
// The navigation state is wiped out and replaced with the new state
assert.deepEqual(state, {
index: 1,
routes: [{key: 'fake3'}, {key: 'fake4'}]
});
});
});
});