From 8732febd8c375a862d574eaa59ab7458b043fd13 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Thu, 8 Dec 2016 10:37:38 -0500 Subject: [PATCH] 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 --- package.json | 2 - src/actions/navigation/index.js | 68 ++++++ src/actions/views/root.js | 15 ++ src/components/logout.js | 50 ---- src/components/logout/index.js | 6 + src/components/logout/logout.js | 24 ++ src/components/logout/logout_container.js | 17 ++ .../right_sidebar_menu/components/item.js | 42 ++++ src/components/right_sidebar_menu/index.js | 6 + .../right_sidebar_menu.js | 72 +++--- .../right_sidebar_menu_container.js | 26 ++ src/constants/index.js | 4 + src/constants/navigation.js | 14 ++ src/containers/root_container.js | 58 ----- src/layouts/root_layout/root_layout.js | 27 +++ .../root_layout/root_layout_container.js | 25 ++ src/mattermost.js | 8 +- src/navigation/router.js | 122 ++++++++++ src/navigation/routes.js | 26 ++ src/reducers/index.js | 4 +- src/reducers/navigation/index.js | 40 ++++ src/routes/index.js | 57 ----- src/{routes => scenes}/channel/channel.js | 10 +- .../channel/channel_container.js | 0 src/scenes/index.js | 22 ++ src/{routes => scenes}/login/login.js | 3 +- .../login/login_container.js | 4 +- src/scenes/root/root.js | 20 ++ src/scenes/root/root_container.js | 23 ++ src/scenes/search/search.js | 30 +++ src/scenes/search/search_container.js | 26 ++ .../select_server/select_server.js | 3 +- .../select_server/select_server_container.js | 4 +- .../select_team/select_team.js | 3 +- .../select_team/select_team_container.js | 8 +- test/actions/users.test.js | 19 +- test/reducers/navigation.test.js | 223 ++++++++++++++++++ 37 files changed, 879 insertions(+), 232 deletions(-) create mode 100644 src/actions/navigation/index.js create mode 100644 src/actions/views/root.js delete mode 100644 src/components/logout.js create mode 100644 src/components/logout/index.js create mode 100644 src/components/logout/logout.js create mode 100644 src/components/logout/logout_container.js create mode 100644 src/components/right_sidebar_menu/components/item.js create mode 100644 src/components/right_sidebar_menu/index.js rename src/components/{ => right_sidebar_menu}/right_sidebar_menu.js (80%) create mode 100644 src/components/right_sidebar_menu/right_sidebar_menu_container.js create mode 100644 src/constants/navigation.js delete mode 100644 src/containers/root_container.js create mode 100644 src/layouts/root_layout/root_layout.js create mode 100644 src/layouts/root_layout/root_layout_container.js create mode 100644 src/navigation/router.js create mode 100644 src/navigation/routes.js create mode 100644 src/reducers/navigation/index.js delete mode 100644 src/routes/index.js rename src/{routes => scenes}/channel/channel.js (90%) rename src/{routes => scenes}/channel/channel_container.js (100%) create mode 100644 src/scenes/index.js rename src/{routes => scenes}/login/login.js (98%) rename src/{routes => scenes}/login/login_container.js (90%) create mode 100644 src/scenes/root/root.js create mode 100644 src/scenes/root/root_container.js create mode 100644 src/scenes/search/search.js create mode 100644 src/scenes/search/search_container.js rename src/{routes => scenes}/select_server/select_server.js (96%) rename src/{routes => scenes}/select_server/select_server_container.js (88%) rename src/{routes => scenes}/select_team/select_team.js (96%) rename src/{routes => scenes}/select_team/select_team_container.js (79%) create mode 100644 test/reducers/navigation.test.js diff --git a/package.json b/package.json index 41c4658d1..1c838c329 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/actions/navigation/index.js b/src/actions/navigation/index.js new file mode 100644 index 000000000..e058ea725 --- /dev/null +++ b/src/actions/navigation/index.js @@ -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); + }; +} diff --git a/src/actions/views/root.js b/src/actions/views/root.js new file mode 100644 index 000000000..0c5d462bc --- /dev/null +++ b/src/actions/views/root.js @@ -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); + }; +} diff --git a/src/components/logout.js b/src/components/logout.js deleted file mode 100644 index af3397240..000000000 --- a/src/components/logout.js +++ /dev/null @@ -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 ( - - {'logout'} - - ); - } -} - -function mapStateToProps(state) { - return { - logout: state.requests.users.logout - }; -} - -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators({logout}, dispatch) - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(Logout); diff --git a/src/components/logout/index.js b/src/components/logout/index.js new file mode 100644 index 000000000..0e5754cd9 --- /dev/null +++ b/src/components/logout/index.js @@ -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; diff --git a/src/components/logout/logout.js b/src/components/logout/logout.js new file mode 100644 index 000000000..6324858cd --- /dev/null +++ b/src/components/logout/logout.js @@ -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 ( + + + + ); + } +} diff --git a/src/components/logout/logout_container.js b/src/components/logout/logout_container.js new file mode 100644 index 000000000..05687e3b1 --- /dev/null +++ b/src/components/logout/logout_container.js @@ -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); diff --git a/src/components/right_sidebar_menu/components/item.js b/src/components/right_sidebar_menu/components/item.js new file mode 100644 index 000000000..0942bb603 --- /dev/null +++ b/src/components/right_sidebar_menu/components/item.js @@ -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 ( + + + {this.props.children} + + + ); + } +} diff --git a/src/components/right_sidebar_menu/index.js b/src/components/right_sidebar_menu/index.js new file mode 100644 index 000000000..be9b8dd2f --- /dev/null +++ b/src/components/right_sidebar_menu/index.js @@ -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; diff --git a/src/components/right_sidebar_menu.js b/src/components/right_sidebar_menu/right_sidebar_menu.js similarity index 80% rename from src/components/right_sidebar_menu.js rename to src/components/right_sidebar_menu/right_sidebar_menu.js index 8c502999f..2b81fcbc1 100644 --- a/src/components/right_sidebar_menu.js +++ b/src/components/right_sidebar_menu/right_sidebar_menu.js @@ -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 ( - + {'@'} - + - + - + - + - + - + - + - + - + - + - + - - {children} - - - ); -} -Item.propTypes = { - onPress: React.PropTypes.func.isRequired, - children: React.PropTypes.node -}; - function Divider() { return ; } diff --git a/src/components/right_sidebar_menu/right_sidebar_menu_container.js b/src/components/right_sidebar_menu/right_sidebar_menu_container.js new file mode 100644 index 000000000..6e62baa67 --- /dev/null +++ b/src/components/right_sidebar_menu/right_sidebar_menu_container.js @@ -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); diff --git a/src/constants/index.js b/src/constants/index.js index d982e1852..8fda08805 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -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 }; diff --git a/src/constants/navigation.js b/src/constants/navigation.js new file mode 100644 index 000000000..660a34548 --- /dev/null +++ b/src/constants/navigation.js @@ -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; diff --git a/src/containers/root_container.js b/src/containers/root_container.js deleted file mode 100644 index 4ffc751fe..000000000 --- a/src/containers/root_container.js +++ /dev/null @@ -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 ( - - ); - } - - const locale = Config.DefaultLocale; - - return ( - - - - ); - } -} - -function mapStateToProps(state) { - return { - device: state.views.device - }; -} - -export default connect(mapStateToProps, { - loadDevice -})(RootContainer); diff --git a/src/layouts/root_layout/root_layout.js b/src/layouts/root_layout/root_layout.js new file mode 100644 index 000000000..36c539d2e --- /dev/null +++ b/src/layouts/root_layout/root_layout.js @@ -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 ( + + {this.props.children} + + ); + } +} diff --git a/src/layouts/root_layout/root_layout_container.js b/src/layouts/root_layout/root_layout_container.js new file mode 100644 index 000000000..192b62551 --- /dev/null +++ b/src/layouts/root_layout/root_layout_container.js @@ -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); diff --git a/src/mattermost.js b/src/mattermost.js index 00586f925..294a658b1 100644 --- a/src/mattermost.js +++ b/src/mattermost.js @@ -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 ( - + + + ); } diff --git a/src/navigation/router.js b/src/navigation/router.js new file mode 100644 index 000000000..b0ca125ea --- /dev/null +++ b/src/navigation/router.js @@ -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 = ( + + ); + } + + const renderedScenes = transitionProps.scenes.map((scene) => { + const cardProps = { + ...transitionProps, + scene + }; + + return ( + + ); + }); + + return ( + + + {renderedScenes} + + {title} + + ); + } + + renderTitle = ({scene}) => { + const title = scene.route.title; + + if (!title) { + return null; + } + + return ( + + + + ); + } + + renderScene = ({scene}) => { + const SceneComponent = getComponentForScene(scene.route.key); + + return ; + } + + configureTransition = () => { + return { + duration: 500, + easing: Easing.inOut(Easing.ease) + }; + } + + render = () => { + return ( + + ); + } +} + +function mapStateToProps(state) { + return { + navigation: state.navigation + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + goBack + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(Router); diff --git a/src/navigation/routes.js b/src/navigation/routes.js new file mode 100644 index 000000000..ab60c4512 --- /dev/null +++ b/src/navigation/routes.js @@ -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' + } +}; diff --git a/src/reducers/index.js b/src/reducers/index.js index cd1202fb2..34c152ae4 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -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 }); diff --git a/src/reducers/navigation/index.js b/src/reducers/navigation/index.js new file mode 100644 index 000000000..0e072d6ae --- /dev/null +++ b/src/reducers/navigation/index.js @@ -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; + } +} diff --git a/src/routes/index.js b/src/routes/index.js deleted file mode 100644 index 515f019d4..000000000 --- a/src/routes/index.js +++ /dev/null @@ -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 ( - - - - - - - } - /> - - - - ); - } -} - -export default injectIntl(Routes); diff --git a/src/routes/channel/channel.js b/src/scenes/channel/channel.js similarity index 90% rename from src/routes/channel/channel.js rename to src/scenes/channel/channel.js index 5c67047ce..59f43eb97 100644 --- a/src/routes/channel/channel.js +++ b/src/scenes/channel/channel.js @@ -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 { } + content={} side='right' tapToClose={true} onCloseStart={this.closeRightSidebar} openDrawerOffset={0.2} > - + + + {'This is a search for ' + this.props.searchType} + + + + ); + } +} diff --git a/src/scenes/search/search_container.js b/src/scenes/search/search_container.js new file mode 100644 index 000000000..3908b834e --- /dev/null +++ b/src/scenes/search/search_container.js @@ -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); diff --git a/src/routes/select_server/select_server.js b/src/scenes/select_server/select_server.js similarity index 96% rename from src/routes/select_server/select_server.js rename to src/scenes/select_server/select_server.js index aefb102a3..8109dda79 100644 --- a/src/routes/select_server/select_server.js +++ b/src/scenes/select_server/select_server.js @@ -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(); } }); }; diff --git a/src/routes/select_server/select_server_container.js b/src/scenes/select_server/select_server_container.js similarity index 88% rename from src/routes/select_server/select_server_container.js rename to src/scenes/select_server/select_server_container.js index 2e759f5c1..c74e5ae72 100644 --- a/src/routes/select_server/select_server_container.js +++ b/src/scenes/select_server/select_server_container.js @@ -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) }; } diff --git a/src/routes/select_team/select_team.js b/src/scenes/select_team/select_team.js similarity index 96% rename from src/routes/select_team/select_team.js rename to src/scenes/select_team/select_team.js index c62d4c0a8..0910516d5 100644 --- a/src/routes/select_team/select_team.js +++ b/src/scenes/select_team/select_team.js @@ -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() { diff --git a/src/routes/select_team/select_team_container.js b/src/scenes/select_team/select_team_container.js similarity index 79% rename from src/routes/select_team/select_team_container.js rename to src/scenes/select_team/select_team_container.js index 8bdd05550..126c2d809 100644 --- a/src/routes/select_team/select_team_container.js +++ b/src/scenes/select_team/select_team_container.js @@ -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) }; } diff --git a/test/actions/users.test.js b/test/actions/users.test.js index 53009fdbb..93dafff87 100644 --- a/test/actions/users.test.js +++ b/test/actions/users.test.js @@ -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(); } diff --git a/test/reducers/navigation.test.js b/test/reducers/navigation.test.js new file mode 100644 index 000000000..b6a3b6ce6 --- /dev/null +++ b/test/reducers/navigation.test.js @@ -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'}] + }); + }); + }); +});