Reorganized navigation (#181)

* Moved ChannelDrawer into its own scene

* Moved RightSideMenu to a separate route

* Moved title back above scenes

* Moved right side menu into a separate scene

* Reorganized navigation so that it centers around ChannelView when logged in

* Stopped using NavigationExperimental to manage drawers

* Fixed copyright date

* Added unit tests for navigation drawers

* Renamed RightSideMenu to RightMenuDrawer

* Removed unnecessary ChannelDrawer style
This commit is contained in:
Harrison Healey 2017-01-24 09:56:35 -05:00 committed by enahum
parent d180f324c5
commit cea81f6497
36 changed files with 780 additions and 515 deletions

View file

@ -12,6 +12,14 @@ export function goBack() {
};
}
export function closeDrawers() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_CLOSE_DRAWERS
}, getState);
};
}
export function goToLogin() {
return async (dispatch, getState) => {
dispatch({
@ -21,6 +29,15 @@ export function goToLogin() {
};
}
export function goToLoadTeam() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: Routes.LoadTeam
}, getState);
};
}
export function goToSelectTeam() {
return async (dispatch, getState) => {
dispatch({
@ -30,43 +47,6 @@ export function goToSelectTeam() {
};
}
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);
};
}
export function goToChannelInfo() {
return async (dispatch, getState) => {
dispatch({
@ -75,3 +55,21 @@ export function goToChannelInfo() {
}, getState);
};
}
export function openChannelDrawer() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_OPEN_LEFT_DRAWER,
route: Routes.ChannelDrawer
}, getState);
};
}
export function openRightMenuDrawer() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_OPEN_RIGHT_DRAWER,
route: Routes.RightMenuDrawer
}, getState);
};
}

View file

@ -1,16 +0,0 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ViewTypes} from 'app/constants';
export function openChannelDrawer() {
return async (dispatch, getState) => {
dispatch({type: ViewTypes.TOGGLE_CHANNEL_DRAWER, data: true}, getState);
};
}
export function closeChannelDrawer() {
return async (dispatch, getState) => {
dispatch({type: ViewTypes.TOGGLE_CHANNEL_DRAWER, data: false}, getState);
};
}

View file

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

View file

@ -0,0 +1,50 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {batchActions} from 'redux-batched-actions';
import {NavigationTypes} from 'app/constants';
import Routes from 'app/navigation/routes';
export function goToSelectTeam() {
return async (dispatch, getState) => {
dispatch(batchActions([{
type: NavigationTypes.NAVIGATION_CLOSE_DRAWERS
}, {
type: NavigationTypes.NAVIGATION_PUSH,
route: Routes.SelectTeam
}]), getState);
};
}
export function goToRecentMentions() {
return async (dispatch, getState) => {
dispatch(batchActions([{
type: NavigationTypes.NAVIGATION_CLOSE_DRAWERS
}, {
type: NavigationTypes.NAVIGATION_PUSH,
route: {
...Routes.Search,
props: {
searchType: 'recent_mentions'
}
}
}]), getState);
};
}
export function goToFlaggedPosts() {
return async (dispatch, getState) => {
dispatch(batchActions([{
type: NavigationTypes.NAVIGATION_CLOSE_DRAWERS
}, {
type: NavigationTypes.NAVIGATION_PUSH,
route: {
...Routes.Search,
props: {
searchType: 'flagged_posts'
}
}
}]), getState);
};
}

View file

@ -1,112 +0,0 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {Platform, BackAndroid} from 'react-native';
import deepEqual from 'deep-equal';
import Drawer from 'react-native-drawer';
import ChannelList from './channel_list';
export default class ChannelDrawer extends React.Component {
static propTypes = {
children: React.PropTypes.element.isRequired,
actions: React.PropTypes.shape({
updateStorage: React.PropTypes.func.isRequired,
handleSelectChannel: React.PropTypes.func.isRequired,
viewChannel: React.PropTypes.func.isRequired,
closeDMChannel: React.PropTypes.func.isRequired,
closeChannelDrawer: React.PropTypes.func.isRequired
}).isRequired,
currentTeam: React.PropTypes.object,
currentChannel: React.PropTypes.object,
channels: React.PropTypes.object,
channelMembers: React.PropTypes.object,
theme: React.PropTypes.object.isRequired,
isOpen: React.PropTypes.bool.isRequired
};
constructor(props) {
super(props);
this.handleBackButton = this.handleBackButton.bind(this);
this.selectChannel = this.selectChannel.bind(this);
}
componentDidMount() {
if (Platform.OS === 'android') {
BackAndroid.addEventListener('hardwareBackPress', this.handleBackButton);
}
}
componentWillUnmount() {
if (Platform.OS === 'android') {
BackAndroid.removeEventListener('hardwareBackPress', this.handleBackButton);
}
}
shouldComponentUpdate(nextProps) {
return !deepEqual(this.props, nextProps, {strict: true});
}
componentWillReceiveProps(nextProps) {
if (!this.props.isOpen && nextProps.isOpen) {
this.drawer.open();
} else if (this.props.isOpen && !nextProps.isOpen) {
this.drawer.close();
}
}
handleBackButton() {
if (this.props.isOpen) {
this.props.actions.closeChannelDrawer();
return true;
}
return false;
}
selectChannel(channelId) {
this.props.actions.handleSelectChannel(channelId);
}
render() {
const {
currentChannel,
currentTeam,
channels,
channelMembers,
theme
} = this.props;
return (
<Drawer
ref={(ref) => {
this.drawer = ref;
}}
onClose={this.props.actions.closeChannelDrawer}
type='displace'
openDrawerOffset={0.2}
closedDrawerOffset={0}
panOpenMask={0.1}
panCloseMask={0.2}
panThreshold={0.2}
acceptPan={true}
tapToClose={true}
content={
<ChannelList
currentTeam={currentTeam}
currentChannel={currentChannel}
channels={channels}
channelMembers={channelMembers}
theme={theme}
onSelectChannel={this.selectChannel}
onViewChannel={this.props.actions.viewChannel}
handleCloseDM={this.props.actions.closeDMChannel}
closeChannelDrawer={this.props.actions.closeChannelDrawer}
/>
}
>
{this.props.children}
</Drawer>
);
}
}

View file

@ -1,37 +0,0 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {viewChannel} from 'service/actions/channels';
import {getChannelsByCategory} from 'service/selectors/entities/channels';
import {closeDMChannel, handleSelectChannel} from 'app/actions/views/channel';
import {closeChannelDrawer} from 'app/actions/views/drawer';
import {updateStorage} from 'app/actions/storage';
import ChannelDrawer from './channel_drawer';
function mapStateToProps(state, ownProps) {
const isOpen = state.views.drawer.channel;
const channelMembers = state.entities.channels.myMembers;
return {
...ownProps,
channels: getChannelsByCategory(state),
channelMembers,
isOpen
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
updateStorage,
handleSelectChannel,
viewChannel,
closeDMChannel,
closeChannelDrawer
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChannelDrawer);

View file

@ -60,8 +60,7 @@ class ChannelList extends React.Component {
theme: React.PropTypes.object.isRequired,
onSelectChannel: React.PropTypes.func.isRequired,
onViewChannel: React.PropTypes.func.isRequired,
handleCloseDM: React.PropTypes.func.isRequired,
closeChannelDrawer: React.PropTypes.func.isRequired
handleCloseDM: React.PropTypes.func.isRequired
};
constructor(props) {
@ -141,7 +140,6 @@ class ChannelList extends React.Component {
this.props.onSelectChannel(channel.id);
this.props.onViewChannel(currentTeam.id, channel.id, currentChannel.id);
this.props.closeChannelDrawer();
};
handleClose = (channel) => {

View file

@ -11,14 +11,19 @@ export default class Drawer extends BaseDrawer {
onRequestClose: React.PropTypes.func.isRequired
};
close = (type, callback) => {
this.props.onRequestClose();
processTapGestures = () => {
// Note that we explicitly don't support tap to open or double tap because I didn't copy them over
if (typeof type === 'function') {
// This function can be called with only a callback as its only argument
type();
} else if (callback) {
callback();
if (this._activeTween) { // eslint-disable-line no-underscore-dangle
return false;
}
}
if (this.props.tapToClose && this._open) { // eslint-disable-line no-underscore-dangle
this.props.onRequestClose();
return true;
}
return false;
};
}

View file

@ -1,6 +0,0 @@
// 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

@ -6,8 +6,12 @@ import keyMirror from 'service/utils/key_mirror';
const NavigationTypes = keyMirror({
NAVIGATION_PUSH: null,
NAVIGATION_POP: null,
NAVIGATION_OPEN_LEFT_DRAWER: null,
NAVIGATION_OPEN_RIGHT_DRAWER: null,
NAVIGATION_CLOSE_DRAWERS: null,
NAVIGATION_JUMP: null,
NAVIGATION_JUMP_TO_INDEX: null,
NAVIGATION_REPLACE: null,
NAVIGATION_RESET: null
});

View file

@ -257,7 +257,11 @@ const state = {
{
key: 'Root'
}
]
],
leftDrawerOpen: false,
leftDrawerRoute: null,
rightDrawerOpen: false,
rightDrawerRoute: null
},
views: {
i18n: {
@ -269,9 +273,6 @@ const state = {
},
selectServer: {
serverUrl: Config.DefaultServerUrl
},
drawer: {
channel: false
}
}
};

View file

@ -2,22 +2,26 @@
// See License.txt for license information.
import React from 'react';
import {bindActionCreators} from 'redux';
import {
Easing,
NavigationExperimental,
View
} from 'react-native';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {goBack} from 'app/actions/navigation';
import {getComponentForScene} from 'app/scenes';
import {Easing, NavigationExperimental, View} from 'react-native';
import {closeDrawers, goBack} from 'app/actions/navigation';
import Drawer from 'app/components/drawer';
import FormattedText from 'app/components/formatted_text';
import {RouteTransitions} from 'app/navigation/routes';
import {getComponentForScene} from 'app/scenes';
class Router extends React.Component {
static propTypes = {
navigation: React.PropTypes.object,
actions: React.PropTypes.shape({
goBack: React.PropTypes.func
closeDrawers: React.PropTypes.func.isRequired,
goBack: React.PropTypes.func.isRequired
}).isRequired
};
@ -39,12 +43,19 @@ class Router extends React.Component {
scene
};
let style;
if (scene.route.transition === RouteTransitions.Horizontal) {
style = NavigationExperimental.Card.PagerStyleInterpolator.forHorizontal({
...cardProps
});
} else {
style = {};
}
return (
<NavigationExperimental.Card
{...cardProps}
style={NavigationExperimental.Card.PagerStyleInterpolator.forHorizontal({
...cardProps
})}
style={style}
renderScene={this.renderScene}
key={scene.key}
/>
@ -52,11 +63,9 @@ class Router extends React.Component {
});
return (
<View style={{flex: 1, flexDirection: 'column-reverse'}}>
<View style={{flex: 1}}>
{renderedScenes}
</View>
<View style={{flex: 1}}>
{title}
{renderedScenes}
</View>
);
};
@ -79,11 +88,15 @@ class Router extends React.Component {
};
renderScene = ({scene}) => {
const SceneComponent = getComponentForScene(scene.route.key);
return <SceneComponent {...scene.route.props}/>;
return this.renderRoute(scene.route);
};
renderRoute = (route) => {
const SceneComponent = getComponentForScene(route.key);
return <SceneComponent {...route.props}/>;
}
configureTransition = () => {
return {
duration: 500,
@ -92,13 +105,49 @@ class Router extends React.Component {
};
render = () => {
const {
leftDrawerOpen,
leftDrawerRoute,
rightDrawerOpen,
rightDrawerRoute
} = this.props.navigation;
let leftDrawerContent;
if (leftDrawerRoute) {
leftDrawerContent = this.renderRoute(leftDrawerRoute);
}
let rightDrawerContent;
if (rightDrawerRoute) {
rightDrawerContent = this.renderRoute(rightDrawerRoute);
}
return (
<NavigationExperimental.Transitioner
style={{flex: 1}}
navigationState={this.props.navigation}
render={this.renderTransition}
configureTransition={this.configureTransition}
/>
<Drawer
open={leftDrawerOpen}
type='displace'
content={leftDrawerContent}
tapToClose={true}
openDrawerOffset={0.2}
onRequestClose={this.props.actions.closeDrawers}
>
<Drawer
open={rightDrawerOpen}
type='displace'
side='right'
content={rightDrawerContent}
tapToClose={true}
openDrawerOffset={0.2}
onRequestClose={this.props.actions.closeDrawers}
>
<NavigationExperimental.Transitioner
style={{flex: 1}}
navigationState={this.props.navigation}
render={this.renderTransition}
configureTransition={this.configureTransition}
/>
</Drawer>
</Drawer>
);
};
}
@ -112,6 +161,7 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
closeDrawers,
goBack
}, dispatch)
};

View file

@ -1,30 +1,52 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
export default {
import keyMirror from 'service/utils/key_mirror';
export const RouteTransitions = keyMirror({
Horizontal: null
});
export const Routes = {
ChannelInfo: {
key: 'ChannelInfo',
title: {id: 'mobile.routes.channelInfo', defaultMessage: 'Info'},
transition: RouteTransitions.Horizontal
},
ChannelDrawer: {
key: 'ChannelDrawer'
},
ChannelView: {
key: 'ChannelView',
transition: RouteTransitions.Horizontal
},
LoadTeam: {
key: 'LoadTeam'
},
Login: {
key: 'Login',
title: {id: 'mobile.routes.login', defaultMessage: 'Login'},
transition: RouteTransitions.Horizontal
},
RightMenuDrawer: {
key: 'RightMenuDrawer'
},
Root: {
key: 'Root'
},
Search: {
key: 'Search',
transition: RouteTransitions.Horizontal
},
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'
},
ChannelInfo: {
key: 'ChannelInfo',
title: {id: 'mobile.routes.channelInfo', defaultMessage: 'Info'}
title: {id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'},
transition: RouteTransitions.Horizontal
}
};
export default Routes;

View file

@ -12,7 +12,11 @@ const initialState = {
index: 0,
routes: [
Routes.Root
]
],
leftDrawerOpen: false,
leftDrawerRoute: null,
rightDrawerOpen: false,
rightDrawerRoute: null
};
export default function(state = initialState, action) {
@ -21,8 +25,37 @@ export default function(state = initialState, action) {
return NavigationExperimental.StateUtils.push(state, action.route);
case NavigationTypes.NAVIGATION_POP:
if (state.leftDrawerOpen || state.rightDrawerOpen) {
return {
...state,
leftDrawerOpen: false,
rightDrawerOpen: false
};
}
return NavigationExperimental.StateUtils.pop(state);
case NavigationTypes.NAVIGATION_OPEN_LEFT_DRAWER:
return {
...state,
leftDrawerOpen: true,
leftDrawerRoute: action.route
};
case NavigationTypes.NAVIGATION_OPEN_RIGHT_DRAWER:
return {
...state,
rightDrawerOpen: true,
rightDrawerRoute: action.route
};
case NavigationTypes.NAVIGATION_CLOSE_DRAWERS:
return {
...state,
leftDrawerOpen: false,
rightDrawerOpen: false
};
case NavigationTypes.NAVIGATION_JUMP:
return NavigationExperimental.StateUtils.jumpTo(state, action.key);
@ -30,7 +63,15 @@ export default function(state = initialState, action) {
return NavigationExperimental.StateUtils.jumpToIndex(state, action.index);
case NavigationTypes.NAVIGATION_RESET:
return NavigationExperimental.StateUtils.reset(state, action.routes, action.index);
return {
...state,
...NavigationExperimental.StateUtils.reset(state, action.routes, action.index),
leftDrawerOpen: false,
rightDrawerOpen: false
};
case NavigationTypes.NAVIGATION_REPLACE:
return NavigationExperimental.StateUtils.replaceAtIndex(state, state.index, action.route);
case UsersTypes.LOGOUT_SUCCESS:
return NavigationExperimental.StateUtils.reset(state, initialState.routes, initialState.index);

View file

@ -1,20 +0,0 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {combineReducers} from 'redux';
import {ViewTypes} from 'app/constants';
function channel(state = false, action) {
switch (action.type) {
case ViewTypes.TOGGLE_CHANNEL_DRAWER:
return action.data;
default:
return state;
}
}
export default combineReducers({
channel
});

View file

@ -4,14 +4,12 @@
import {combineReducers} from 'redux';
import channel from './channel';
import drawer from './drawer';
import i18n from './i18n';
import login from './login';
import selectServer from './select_server';
export default combineReducers({
channel,
drawer,
i18n,
login,
selectServer

View file

@ -7,11 +7,8 @@ import {
StatusBar,
Text
} from 'react-native';
import Drawer from 'react-native-drawer';
import ChannelDrawer from 'app/components/channel_drawer';
import PostTextbox from 'app/components/post_textbox';
import RightSidebarMenu from 'app/components/right_sidebar_menu';
import ChannelHeader from './channel_header';
import ChannelPostList from './channel_post_list';
@ -23,6 +20,7 @@ export default class Channel extends React.PureComponent {
loadProfilesAndTeamMembersForDMSidebar: React.PropTypes.func.isRequired,
selectInitialChannel: React.PropTypes.func.isRequired,
openChannelDrawer: React.PropTypes.func.isRequired,
openRightMenuDrawer: React.PropTypes.func.isRequired,
handlePostDraftChanged: React.PropTypes.func.isRequired,
goToChannelInfo: React.PropTypes.func.isRequired
}).isRequired,
@ -60,20 +58,16 @@ export default class Channel extends React.PureComponent {
});
};
openRightSidebar = () => {
this.refs.postTextbox.getWrappedInstance().blur();
this.setState({rightSidebarOpen: true});
};
closeRightSidebar = () => {
this.setState({rightSidebarOpen: false});
};
openChannelDrawer = () => {
this.refs.postTextbox.getWrappedInstance().blur();
this.props.actions.openChannelDrawer();
}
openRightMenuDrawer = () => {
this.refs.postTextbox.getWrappedInstance().blur();
this.props.actions.openRightMenuDrawer();
}
render() {
const {
currentTeam,
@ -93,36 +87,20 @@ export default class Channel extends React.PureComponent {
style={{flex: 1, backgroundColor: theme.centerChannelBg}}
>
<StatusBar barStyle='default'/>
<ChannelDrawer
currentTeam={currentTeam}
<ChannelHeader
currentChannel={currentChannel}
theme={theme}
>
<Drawer
open={this.state.rightSidebarOpen}
type='displace'
content={<RightSidebarMenu onClose={this.closeRightSidebar}/>}
side='right'
tapToClose={true}
onCloseStart={this.closeRightSidebar}
openDrawerOffset={0.2}
>
<ChannelHeader
currentChannel={currentChannel}
openLeftDrawer={this.openChannelDrawer}
openRightDrawer={this.openRightSidebar}
goToChannelInfo={this.props.actions.goToChannelInfo}
/>
<ChannelPostList channel={currentChannel}/>
<PostTextbox
ref='postTextbox'
value={this.props.postDraft}
teamId={currentChannel.team_id}
channelId={currentChannel.id}
onChangeText={this.props.actions.handlePostDraftChanged}
/>
</Drawer>
</ChannelDrawer>
openLeftDrawer={this.openChannelDrawer}
openRightDrawer={this.openRightMenuDrawer}
goToChannelInfo={this.props.actions.goToChannelInfo}
/>
<ChannelPostList channel={currentChannel}/>
<PostTextbox
ref='postTextbox'
value={this.props.postDraft}
teamId={currentChannel.team_id}
channelId={currentChannel.id}
onChangeText={this.props.actions.handlePostDraftChanged}
/>
</KeyboardAvoidingView>
);
}

View file

@ -4,18 +4,21 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {
goToChannelInfo,
openChannelDrawer,
openRightMenuDrawer
} from 'app/actions/navigation';
import {
loadChannelsIfNecessary,
loadProfilesAndTeamMembersForDMSidebar,
selectInitialChannel,
handlePostDraftChanged
} from 'app/actions/views/channel';
import {openChannelDrawer} from 'app/actions/views/drawer';
import {getCurrentChannel} from 'service/selectors/entities/channels';
import {getTheme} from 'service/selectors/entities/preferences';
import {getCurrentTeam} from 'service/selectors/entities/teams';
import {goToChannelInfo} from 'app/actions/navigation';
import Channel from './channel';
@ -36,6 +39,7 @@ function mapDispatchToProps(dispatch) {
loadProfilesAndTeamMembersForDMSidebar,
selectInitialChannel,
openChannelDrawer,
openRightMenuDrawer,
handlePostDraftChanged,
goToChannelInfo
}, dispatch)

View file

@ -0,0 +1,50 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import ChannelList from 'app/components/channel_drawer/channel_list';
export default class ChannelDrawer extends React.PureComponent {
static propTypes = {
actions: React.PropTypes.shape({
selectChannel: React.PropTypes.func.isRequired,
viewChannel: React.PropTypes.func.isRequired,
closeDMChannel: React.PropTypes.func.isRequired,
closeDrawers: React.PropTypes.func.isRequired
}).isRequired,
currentTeam: React.PropTypes.object,
currentChannel: React.PropTypes.object,
channels: React.PropTypes.object,
channelMembers: React.PropTypes.object,
theme: React.PropTypes.object.isRequired
};
selectChannel = (id) => {
this.props.actions.selectChannel(id);
this.props.actions.closeDrawers();
}
render() {
const {
currentChannel,
currentTeam,
channels,
channelMembers,
theme
} = this.props;
return (
<ChannelList
currentTeam={currentTeam}
currentChannel={currentChannel}
channels={channels}
channelMembers={channelMembers}
theme={theme}
onSelectChannel={this.selectChannel}
onViewChannel={this.props.actions.viewChannel}
handleCloseDM={this.props.actions.closeDMChannel}
/>
);
}
}

View file

@ -0,0 +1,38 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {closeDrawers} from 'app/actions/navigation';
import {closeDMChannel} from 'app/actions/views/channel';
import {selectChannel, viewChannel} from 'service/actions/channels';
import {getChannelsByCategory, getCurrentChannel} from 'service/selectors/entities/channels';
import {getTheme} from 'service/selectors/entities/preferences';
import {getCurrentTeam} from 'service/selectors/entities/teams';
import ChannelDrawer from './channel_drawer.js';
function mapStateToProps(state) {
return {
currentTeam: getCurrentTeam(state),
currentChannel: getCurrentChannel(state),
channels: getChannelsByCategory(state),
channelMembers: state.entities.channels.myMembers,
theme: getTheme(state)
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
selectChannel,
viewChannel,
closeDMChannel,
closeDrawers
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChannelDrawer);

View file

@ -1,4 +1,4 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import ChannelDrawerContainer from './channel_drawer_container';

View file

@ -2,21 +2,27 @@
// See License.txt for license information.
import Channel from './channel';
import ChannelDrawer from './channel_drawer';
import ChannelInfo from './channel_info';
import LoadTeam from './load_team';
import Login from './login/login_container.js';
import RightMenuDrawer from './right_menu_drawer';
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';
import ChannelInfo from './channel_info';
const scenes = {
Root,
SelectServer,
Login,
SelectTeam,
ChannelView: Channel, // Special case the name for this one to avoid ambiguity
ChannelDrawer,
ChannelInfo,
LoadTeam,
Login,
RightMenuDrawer,
Root,
Search,
ChannelInfo
SelectServer,
SelectTeam
};
export function getComponentForScene(key) {

View file

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

View file

@ -0,0 +1,60 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import Loading from 'app/components/loading';
import {RequestStatus} from 'service/constants';
export default class LoadTeam extends React.Component {
static propTypes = {
teams: React.PropTypes.object.isRequired,
myMembers: React.PropTypes.object.isRequired,
teamsRequest: React.PropTypes.object.isRequired,
currentTeam: React.PropTypes.object,
actions: React.PropTypes.shape({
goToChannelView: React.PropTypes.func.isRequired,
handleTeamChange: React.PropTypes.func.isRequired,
initWebsocket: React.PropTypes.func.isRequired
}).isRequired
};
componentWillMount() {
this.props.actions.initWebsocket();
}
componentDidMount() {
const {currentTeam, myMembers, teams} = this.props;
if (currentTeam) {
this.onSelectTeam(currentTeam);
} else if (!currentTeam) {
this.selectFirstTeam(teams, myMembers);
}
}
componentWillReceiveProps(nextProps) {
if (this.props.teamsRequest.status === RequestStatus.STARTED &&
nextProps.teamsRequest.status === RequestStatus.SUCCESS) {
this.selectFirstTeam(nextProps.teams, nextProps.myMembers);
}
}
selectFirstTeam(allTeams, myMembers) {
const teams = Object.keys(myMembers).map((key) => allTeams[key]);
const firstTeam = Object.values(teams).sort((a, b) => a.display_name.localeCompare(b.display_name))[0];
if (firstTeam) {
this.onSelectTeam(firstTeam);
}
}
onSelectTeam(team) {
this.props.actions.handleTeamChange(team).then(this.props.actions.goToChannelView);
}
render() {
return <Loading/>;
}
}

View file

@ -0,0 +1,35 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {goToChannelView} from 'app/actions/views/load_team';
import {handleTeamChange} from 'app/actions/views/select_team';
import {init} from 'service/actions/websocket';
import {getCurrentTeam} from 'service/selectors/entities/teams';
import LoadTeam from './load_team.js';
function mapStateToProps(state) {
return {
config: state.entities.general.config,
teamsRequest: state.requests.teams.allTeams,
teams: state.entities.teams.teams,
currentTeam: getCurrentTeam(state),
myMembers: state.entities.teams.myMembers
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToChannelView,
handleTeamChange,
initWebsocket: init
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(LoadTeam);

View file

@ -35,7 +35,7 @@ class Login extends Component {
componentWillReceiveProps(nextProps) {
if (this.props.loginRequest.status === RequestStatus.STARTED && nextProps.loginRequest.status === RequestStatus.SUCCESS) {
this.props.actions.handleSuccessfulLogin().then(this.props.actions.goToSelectTeam);
this.props.actions.handleSuccessfulLogin().then(this.props.actions.goToLoadTeam);
}
}

View file

@ -6,8 +6,7 @@ import {connect} from 'react-redux';
import {getClientConfig, getLicenseConfig} from 'service/actions/general';
import LoginActions from 'app/actions/views/login';
import {updateRootStorage} from 'app/actions/storage';
import {goToSelectTeam} from 'app/actions/navigation';
import {goToLoadTeam} from 'app/actions/navigation';
import {login} from 'service/actions/users';
import Login from './login.js';
@ -27,11 +26,10 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
...LoginActions,
updateRootStorage,
login,
getClientConfig,
getLicenseConfig,
goToSelectTeam
goToLoadTeam
}, dispatch)
};
}

View file

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

View file

@ -2,16 +2,17 @@
// See License.txt for license information.
import React from 'react';
import FormattedText from 'app/components/formatted_text';
import Icon from 'react-native-vector-icons/FontAwesome';
import Item from './components/item';
import {
ScrollView,
StyleSheet,
Text,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import FormattedText from 'app/components/formatted_text';
import RightMenuDrawerItem from './right_menu_drawer_item';
const Styles = StyleSheet.create({
container: {
@ -36,43 +37,28 @@ const Styles = StyleSheet.create({
}
});
export default class RightSidebarMenu extends React.Component {
export default class RightMenuDrawer extends React.Component {
static propTypes = {
actions: React.PropTypes.shape({
goBack: React.PropTypes.func.isRequired,
goToFlaggedPosts: React.PropTypes.func.isRequired,
goToRecentMentions: React.PropTypes.func.isRequired,
goToSelectTeam: 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();
}
goToTeamSelection = () => {
this.props.actions.goBack();
}).isRequired
}
render() {
return (
<ScrollView style={Styles.container}>
<Item onPress={this.goToRecentMentions}>
<RightMenuDrawerItem onPress={this.props.actions.goToRecentMentions}>
<Text style={[Styles.icon, Styles.mentionIcon]}>{'@'}</Text>
<FormattedText
style={Styles.itemText}
id='sidebar_right_menu.recentMentions'
defaultMessage='Recent Mentions'
/>
</Item>
<Item onPress={this.goToFlaggedPosts}>
</RightMenuDrawerItem>
<RightMenuDrawerItem onPress={this.props.actions.goToFlaggedPosts}>
<Icon
style={Styles.icon}
name='flag'
@ -82,9 +68,9 @@ export default class RightSidebarMenu extends React.Component {
id='sidebar_right_menu.flagged'
defaultMessage='Flagged Posts'
/>
</Item>
</RightMenuDrawerItem>
<Divider/>
<Item>
<RightMenuDrawerItem>
<Icon
style={Styles.icon}
name='cog'
@ -94,8 +80,8 @@ export default class RightSidebarMenu extends React.Component {
id='sidebar_right_menu.accountSettings'
defaultMessage='Account Settings'
/>
</Item>
<Item>
</RightMenuDrawerItem>
<RightMenuDrawerItem>
<Icon
style={Styles.icon}
name='user-plus'
@ -105,8 +91,8 @@ export default class RightSidebarMenu extends React.Component {
id='sidebar_right_menu.inviteNew'
defaultMessage='Invite New Member'
/>
</Item>
<Item>
</RightMenuDrawerItem>
<RightMenuDrawerItem>
<Icon
style={Styles.icon}
name='link'
@ -116,9 +102,9 @@ export default class RightSidebarMenu extends React.Component {
id='sidebar_right_menu.teamLink'
defaultMessage='Get Team Invite Link'
/>
</Item>
</RightMenuDrawerItem>
<Divider/>
<Item>
<RightMenuDrawerItem>
<Icon
style={Styles.icon}
name='globe'
@ -128,8 +114,8 @@ export default class RightSidebarMenu extends React.Component {
id='sidebar_right_menu.teamSettings'
defaultMessage='Team Settings'
/>
</Item>
<Item>
</RightMenuDrawerItem>
<RightMenuDrawerItem>
<Icon
style={Styles.icon}
name='users'
@ -139,8 +125,8 @@ export default class RightSidebarMenu extends React.Component {
id='sidebar_right_menu.manageMembers'
defaultMessage='Manage Members'
/>
</Item>
<Item onPress={this.goToTeamSelection}>
</RightMenuDrawerItem>
<RightMenuDrawerItem onPress={this.props.actions.goToSelectTeam}>
<Icon
style={Styles.icon}
name='exchange'
@ -150,9 +136,9 @@ export default class RightSidebarMenu extends React.Component {
id='sidebar_right_menu.switch_team'
defaultMessage='Team Selection'
/>
</Item>
</RightMenuDrawerItem>
<Divider/>
<Item>
<RightMenuDrawerItem>
<Icon
style={Styles.icon}
name='question'
@ -162,8 +148,8 @@ export default class RightSidebarMenu extends React.Component {
id='sidebar_right_menu.help'
defaultMessage='Help'
/>
</Item>
<Item>
</RightMenuDrawerItem>
<RightMenuDrawerItem>
<Icon
style={Styles.icon}
name='phone'
@ -173,8 +159,8 @@ export default class RightSidebarMenu extends React.Component {
id='sidebar_right_menu.report'
defaultMessage='Report a Problem'
/>
</Item>
<Item>
</RightMenuDrawerItem>
<RightMenuDrawerItem>
<Icon
style={Styles.icon}
name='info'
@ -184,9 +170,9 @@ export default class RightSidebarMenu extends React.Component {
id='navbar_dropdown.about'
defaultMessage='About Mattermost'
/>
</Item>
</RightMenuDrawerItem>
<Divider/>
<Item onPress={this.props.actions.logout}>
<RightMenuDrawerItem onPress={this.props.actions.logout}>
<Icon
style={Styles.icon}
name='sign-out'
@ -196,7 +182,7 @@ export default class RightSidebarMenu extends React.Component {
id='sidebar_right_menu.logout'
defaultMessage='Logout'
/>
</Item>
</RightMenuDrawerItem>
</ScrollView>
);
}

View file

@ -4,10 +4,12 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {goBack, goToFlaggedPosts, goToRecentMentions} from 'app/actions/navigation';
import {goBack} from 'app/actions/navigation';
import {goToFlaggedPosts, goToRecentMentions, goToSelectTeam} from 'app/actions/views/right_menu_drawer';
import {logout} from 'service/actions/users';
import RightSidebarMenu from './right_sidebar_menu';
import RightMenuDrawer from './right_menu_drawer';
function mapStateToProps(state, ownProps) {
return ownProps;
@ -19,9 +21,10 @@ function mapDispatchToProps(dispatch) {
goBack,
goToFlaggedPosts,
goToRecentMentions,
goToSelectTeam,
logout
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(RightSidebarMenu);
export default connect(mapStateToProps, mapDispatchToProps)(RightMenuDrawer);

View file

@ -16,7 +16,7 @@ const Styles = StyleSheet.create({
}
});
export default class Item extends React.Component {
export default class MainMenuItem extends React.Component {
static propTypes = {
children: React.PropTypes.node,
onPress: React.PropTypes.func,

View file

@ -12,9 +12,8 @@ export default class Root extends React.Component {
logoutRequest: React.PropTypes.object,
loginRequest: React.PropTypes.object,
actions: React.PropTypes.shape({
goToChannelView: React.PropTypes.func,
goToLoadTeam: React.PropTypes.func,
goToSelectServer: React.PropTypes.func,
goToSelectTeam: React.PropTypes.func,
loadStorage: React.PropTypes.func,
removeStorage: React.PropTypes.func,
setStoreFromLocalData: React.PropTypes.func,
@ -24,7 +23,6 @@ export default class Root extends React.Component {
componentDidMount() {
// Any initialization logic for navigation, setting up the client, etc should go here
this.init();
}
@ -53,7 +51,7 @@ export default class Root extends React.Component {
if (this.props.credentials.token && this.props.credentials.url) {
this.props.actions.setStoreFromLocalData(this.props.credentials).then(() => {
if (this.props.loginRequest.status === RequestStatus.SUCCESS) {
this.props.actions.goToSelectTeam();
this.props.actions.goToLoadTeam();
} else {
this.props.actions.goToSelectServer();
}

View file

@ -6,7 +6,7 @@ import {connect} from 'react-redux';
import {loadStorage, removeStorage} from 'app/actions/storage';
import {goToSelectServer, setStoreFromLocalData} from 'app/actions/views/root';
import {goToSelectTeam, goToChannelView} from 'app/actions/navigation';
import {goToLoadTeam} from 'app/actions/navigation';
import {resetLogout} from 'service/actions/users';
import Root from './root';
@ -23,9 +23,8 @@ function mapStateToProps(state, ownProps) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToChannelView,
goToLoadTeam,
goToSelectServer,
goToSelectTeam,
loadStorage,
removeStorage,
setStoreFromLocalData,

View file

@ -1,68 +1,32 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component, PropTypes} from 'react';
import React from 'react';
import {View, Image, Text} from 'react-native';
import Button from 'react-native-button';
import Icon from 'react-native-vector-icons/MaterialIcons';
import ErrorText from 'app/components/error_text';
import FormattedText from 'app/components/formatted_text';
import Loading from 'app/components/loading';
import {GlobalStyles} from 'app/styles';
import logo from 'assets/images/logo.png';
import {RequestStatus} from 'service/constants';
export default class SelectTeam extends Component {
export default class SelectTeam extends React.Component {
static propTypes = {
config: PropTypes.object.isRequired,
teams: PropTypes.object.isRequired,
myMembers: PropTypes.object.isRequired,
teamsRequest: PropTypes.object.isRequired,
currentTeam: PropTypes.object,
actions: PropTypes.object.isRequired
config: React.PropTypes.object.isRequired,
teams: React.PropTypes.object.isRequired,
myMembers: React.PropTypes.object.isRequired,
actions: React.PropTypes.shape({
goBackToChannelView: React.PropTypes.func.isRequired,
handleTeamChange: React.PropTypes.func.isRequired
}).isRequired
};
componentWillMount() {
this.props.actions.websocket();
}
componentDidMount() {
const {currentTeam, myMembers, teams} = this.props;
if (currentTeam) {
this.onSelectTeam(currentTeam);
} else if (!currentTeam) {
this.selectFirstTeam(teams, myMembers);
}
}
componentWillReceiveProps(nextProps) {
if (this.props.teamsRequest.status === RequestStatus.STARTED &&
nextProps.teamsRequest.status === RequestStatus.SUCCESS) {
this.selectFirstTeam(nextProps.teams, nextProps.myMembers);
}
}
selectFirstTeam(allTeams, myMembers) {
const teams = Object.keys(myMembers).map((key) => allTeams[key]);
const firstTeam = Object.values(teams).sort((a, b) => a.display_name.localeCompare(b.display_name))[0];
if (firstTeam) {
this.onSelectTeam(firstTeam);
}
}
onSelectTeam(team) {
this.props.actions.handleTeamChange(team).then(this.props.actions.goToChannelView);
this.props.actions.handleTeamChange(team).then(this.props.actions.goBackToChannelView);
}
render() {
if (this.props.teamsRequest.status === RequestStatus.STARTED) {
return <Loading/>;
}
const teams = [];
for (const id of Object.keys(this.props.teams)) {
const team = this.props.teams[id];
@ -106,7 +70,6 @@ export default class SelectTeam extends Component {
defaultMessage='Your teams:'
/>
{teams}
<ErrorText error={this.props.teamsRequest.error}/>
</View>
);
}

View file

@ -4,13 +4,12 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {goBack} from 'app/actions/navigation';
import {handleTeamChange} from 'app/actions/views/select_team';
import {init as websocket} from 'service/actions/websocket';
import {updateRootStorage, loadTeamStorage} from 'app/actions/storage';
import {goToChannelView} from 'app/actions/navigation';
import {getCurrentTeam} from 'service/selectors/entities/teams';
import SelectTeamView from './select_team.js';
import SelectTeam from './select_team.js';
function mapStateToProps(state) {
return {
@ -25,13 +24,10 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToChannelView,
updateRootStorage,
loadTeamStorage,
handleTeamChange,
websocket
goBackToChannelView: goBack,
handleTeamChange
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(SelectTeamView);
export default connect(mapStateToProps, mapDispatchToProps)(SelectTeam);

View file

@ -22,7 +22,11 @@ describe('Reducers.Navigation', () => {
assert.deepEqual(state, {
index: 0,
routes: [Routes.Root]
routes: [Routes.Root],
leftDrawerOpen: false,
leftDrawerRoute: null,
rightDrawerOpen: false,
rightDrawerRoute: null
}, 'initial state');
});
@ -31,44 +35,38 @@ describe('Reducers.Navigation', () => {
it('first push', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
type: NavigationTypes.NAVIGATION_PUSH,
route: {key: 'fake'}
});
assert.deepEqual(state, {
index: 1,
routes: [Routes.Root, {key: 'fake'}]
});
assert.equal(state.index, 1);
assert.deepEqual(state.routes, [Routes.Root, {key: 'fake'}]);
});
it('second push', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
type: NavigationTypes.NAVIGATION_PUSH,
route: {key: 'fake2'}
});
assert.deepEqual(state, {
index: 1,
routes: [Routes.Root, {key: 'fake'}, {key: 'fake2'}]
});
assert.equal(state.index, 2);
assert.deepEqual(state.routes, [Routes.Root, {key: 'fake2'}]);
});
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_JUMP,
key: Routes.Root.key
});
it('push when not at head of stack', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.JUMP,
key: Routes.key.root
});
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
type: NavigationTypes.NAVIGATION_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'}]
});
assert.equal(state.index, 1);
assert.deepEqual(state.routes, [Routes.Root, {key: 'fake3'}, {key: 'fake2'}]);
});
});
@ -76,45 +74,146 @@ describe('Reducers.Navigation', () => {
let state = initialState();
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
type: NavigationTypes.NAVIGATION_PUSH,
route: {key: 'fake'}
});
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
type: NavigationTypes.NAVIGATION_PUSH,
route: {key: 'fake2'}
});
it('first pop', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.POP
type: NavigationTypes.NAVIGATION_POP
});
assert.deepEqual(state, {
index: 1,
routes: [Routes.Root, {key: 'fake'}]
});
assert.equal(state.index, 1);
assert.deepEqual(state.routes, [Routes.Root, {key: 'fake'}]);
});
it('second pop', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.POP
type: NavigationTypes.NAVIGATION_POP
});
assert.deepEqual(state, {
index: 0,
routes: [Routes.Root]
});
assert.equal(state.index, 0);
assert.deepEqual(state.routes, [Routes.Root]);
});
it('pop last entry on stack', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.POP
type: NavigationTypes.NAVIGATION_POP
});
assert.deepEqual(state, {
index: 0,
routes: [Routes.Root]
// Nothing happens
assert.equal(state.index, 0);
assert.deepEqual(state.routes, [Routes.Root]);
});
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_PUSH,
route: {key: 'fake3'}
});
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_OPEN_LEFT_DRAWER,
route: {key: 'fake'}
});
it('popping to close left drawer', () => {
state.reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_POP
});
// Routes not popped, but drawer closed
assert.equal(state.index, 1);
assert.deepEqual(state.routes, [Routes.Root, {key: 'fake3'}]);
assert.equal(state.leftDrawerOpen, false);
assert.equal(state.rightDrawerOpen, false);
});
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_OPEN_RIGHT_DRAWER,
route: {key: 'fake'}
});
it('popping to close right drawer', () => {
state.reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_POP
});
// Routes not popped, but drawer closed
assert.equal(state.index, 1);
assert.deepEqual(state.routes, [Routes.Root, {key: 'fake'}]);
assert.equal(state.leftDrawerOpen, false);
assert.equal(state.rightDrawerOpen, false);
});
});
it('NAVIGATION_OPEN_LEFT_DRAWER', () => {
let state = initialState();
it('open left drawer', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_OPEN_LEFT_DRAWER,
route: {key: 'fake'}
});
assert.equal(state.index, 0);
assert.deepEqual(state.routes, [Routes.Root]);
assert.equal(state.leftDrawerOpen, true);
assert.deepEqual(state.leftDrawerRoute, {key: 'fake'});
assert.equal(state.rightDrawerOpen, false);
});
});
it('NAVIGATION_OPEN_RIGHT_DRAWER', () => {
let state = initialState();
it('open right drawer', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_OPEN_RIGHT_DRAWER,
route: {key: 'fake'}
});
assert.equal(state.index, 0);
assert.deepEqual(state.routes, [Routes.Root]);
assert.equal(state.leftDrawerOpen, false);
assert.equal(state.rightDrawerOpen, true);
assert.deepEqual(state.rightDrawerRoute, {key: 'fake'});
});
});
it('NAVIGATION_CLOSE_DRAWERS', () => {
let state = initialState();
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_OPEN_LEFT_DRAWER,
route: {key: 'fake'}
});
it('close left drawer', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_CLOSE_DRAWERS
});
assert.equal(state.index, 0);
assert.deepEqual(state.routes, [Routes.Root]);
assert.equal(state.leftDrawerOpen, false);
});
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_OPEN_RIGHT_DRAWER,
route: {key: 'fake'}
});
it('close right drawer', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_CLOSE_DRAWERS
});
assert.equal(state.index, 0);
assert.deepEqual(state.routes, [Routes.Root]);
assert.equal(state.rightDrawerOpen, false);
});
});
@ -122,11 +221,11 @@ describe('Reducers.Navigation', () => {
let state = initialState();
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
type: NavigationTypes.NAVIGATION_PUSH,
route: {key: 'fake'}
});
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
type: NavigationTypes.NAVIGATION_PUSH,
route: {key: 'fake2'}
});
@ -137,10 +236,8 @@ describe('Reducers.Navigation', () => {
});
// The rest of the stack is preserved
assert.deepEqual(state, {
index: 0,
routes: [Routes.Root, {key: 'fake'}, {key: 'fake2'}]
});
assert.equal(state.index, 0);
assert.deepEqual(state.routes, [Routes.Root, {key: 'fake'}, {key: 'fake2'}]);
});
it('jump forward', () => {
@ -149,10 +246,8 @@ describe('Reducers.Navigation', () => {
key: 'fake'
});
assert.deepEqual(state, {
index: 2,
routes: [Routes.Root, {key: 'fake'}, {key: 'fake2'}]
});
assert.equal(state.index, 2);
assert.deepEqual(state.routes, [Routes.Root, {key: 'fake'}, {key: 'fake2'}]);
});
});
@ -160,11 +255,11 @@ describe('Reducers.Navigation', () => {
let state = initialState();
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
type: NavigationTypes.NAVIGATION_PUSH,
route: {key: 'fake'}
});
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
type: NavigationTypes.NAVIGATION_PUSH,
route: {key: 'fake2'}
});
@ -175,10 +270,8 @@ describe('Reducers.Navigation', () => {
});
// The rest of the stack is preserved
assert.deepEqual(state, {
index: 0,
routes: [Routes.Root, {key: 'fake'}, {key: 'fake2'}]
});
assert.equal(state.index, 0);
assert.deepEqual(state.routes, [Routes.Root, {key: 'fake'}, {key: 'fake2'}]);
});
it('jump forward', () => {
@ -187,10 +280,8 @@ describe('Reducers.Navigation', () => {
index: 2
});
assert.deepEqual(state, {
index: 2,
routes: [Routes.Root, {key: 'fake'}, {key: 'fake2'}]
});
assert.equal(state.index, 2);
assert.deepEqual(state.routes, [Routes.Root, {key: 'fake'}, {key: 'fake2'}]);
});
});
@ -198,11 +289,11 @@ describe('Reducers.Navigation', () => {
let state = initialState();
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
type: NavigationTypes.NAVIGATION_PUSH,
route: {key: 'fake'}
});
state = reduceAndFreeze(state, {
type: NavigationTypes.PUSH,
type: NavigationTypes.NAVIGATION_PUSH,
route: {key: 'fake2'}
});
@ -215,9 +306,67 @@ describe('Reducers.Navigation', () => {
// The navigation state is wiped out and replaced with the new state
assert.deepEqual(state, {
index: 1,
routes: [{key: 'fake3'}, {key: 'fake4'}],
leftDrawerOpen: false,
leftDrawerRoute: null,
rightDrawerOpen: false,
rightDrawerRoute: null
});
});
state = initialState();
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_PUSH,
route: {key: 'fake'}
});
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_OPEN_LEFT_DRAWER,
route: {key: 'fake2'}
});
it('reset with left drawer open', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_RESET,
index: 1,
routes: [{key: 'fake3'}, {key: 'fake4'}]
});
assert.deepEqual(state, {
index: 1,
routes: [{key: 'fake3'}, {key: 'fake4'}],
leftDrawerOpen: false,
leftDrawerRoute: {key: 'fake2'}, // Leave drawer routes in tact so that the close animation still works
rightDrawerOpen: false,
rightDrawerRoute: null
});
});
state = initialState();
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_PUSH,
route: {key: 'fake'}
});
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_OPEN_RIGHT_DRAWER,
route: {key: 'fake2'}
});
it('reset with left drawer open', () => {
state = reduceAndFreeze(state, {
type: NavigationTypes.NAVIGATION_RESET,
index: 1,
routes: [{key: 'fake3'}, {key: 'fake4'}]
});
assert.deepEqual(state, {
index: 1,
routes: [{key: 'fake3'}, {key: 'fake4'}],
leftDrawerOpen: false,
leftDrawerRoute: null,
rightDrawerOpen: false,
rightDrawerRoute: {key: 'fake2'} // Leave drawer routes in tact so that the close animation still works
});
});
});
});