Reworking navigation (#212)
* Reworking navigation * Fixed navigation testing and lint errors
This commit is contained in:
parent
89ebae1108
commit
ce58429a21
23 changed files with 478 additions and 75 deletions
|
|
@ -15,22 +15,27 @@ import deepEqual from 'deep-equal';
|
|||
|
||||
const Styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
marginTop: 20
|
||||
}
|
||||
})
|
||||
flex: 1
|
||||
},
|
||||
scrollContainer: {
|
||||
flex: 1
|
||||
},
|
||||
headerContainer: {
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
height: 50,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
height: 64,
|
||||
justifyContent: 'flex-end'
|
||||
},
|
||||
android: {
|
||||
height: 56,
|
||||
justifyContent: 'center',
|
||||
paddingTop: 10
|
||||
}
|
||||
}),
|
||||
width: 300,
|
||||
paddingLeft: 10
|
||||
paddingLeft: 10,
|
||||
paddingBottom: 12
|
||||
},
|
||||
header: {
|
||||
fontSize: 18,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
import Config from 'assets/config.json';
|
||||
|
||||
import Routes from 'app/navigation/routes';
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
|
|
@ -254,9 +256,7 @@ const state = {
|
|||
navigation: {
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
key: 'Root'
|
||||
}
|
||||
Routes.Root
|
||||
],
|
||||
leftDrawerOpen: false,
|
||||
leftDrawerRoute: null,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import Drawer from 'app/components/drawer';
|
|||
import FormattedText from 'app/components/formatted_text';
|
||||
import OptionsModal from 'app/components/options_modal';
|
||||
import {RouteTransitions} from 'app/navigation/routes';
|
||||
import {getComponentForScene} from 'app/scenes';
|
||||
|
||||
class Router extends React.Component {
|
||||
static propTypes = {
|
||||
|
|
@ -27,14 +26,53 @@ class Router extends React.Component {
|
|||
}).isRequired
|
||||
};
|
||||
|
||||
headerEventSubscriptions = {};
|
||||
|
||||
emitHeaderEvent = (event) => {
|
||||
const subscription = this.headerEventSubscriptions[event];
|
||||
if (subscription) {
|
||||
subscription();
|
||||
}
|
||||
}
|
||||
|
||||
subscribeToHeaderEvent = (event, callback) => {
|
||||
this.headerEventSubscriptions[event] = callback;
|
||||
}
|
||||
|
||||
unsubscribeFromHeaderEvent = (event) => {
|
||||
if (this.headerEventSubscriptions[event]) {
|
||||
Reflect.deleteProperty(this.headerEventSubscriptions, event);
|
||||
}
|
||||
}
|
||||
|
||||
wrapHeaderComponent = (fx) => (props) => {
|
||||
if (fx && props.scene.isActive) {
|
||||
return fx(props, this.emitHeaderEvent);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
extractNavigationProps = (route) => {
|
||||
return Object.assign({}, route.navigationProps, route.component.navigationProps);
|
||||
}
|
||||
|
||||
renderTransition = (transitionProps) => {
|
||||
let title;
|
||||
if (transitionProps.scene.route.title) {
|
||||
title = (
|
||||
const navigationProps = this.extractNavigationProps(transitionProps.scene.route);
|
||||
let navBar = null;
|
||||
if (!navigationProps.hideNavBar) {
|
||||
const renderLeftComponent = transitionProps.navigationState.index > 0 ? this.wrapHeaderComponent(navigationProps.renderBackButton) : this.wrapHeaderComponent(navigationProps.renderLeftComponent);
|
||||
const renderTitleComponent = navigationProps.renderTitleComponent ? this.wrapHeaderComponent(navigationProps.renderTitleComponent) : this.renderTitle;
|
||||
const renderRightComponent = this.wrapHeaderComponent(navigationProps.renderRightComponent);
|
||||
|
||||
navBar = (
|
||||
<NavigationExperimental.Header
|
||||
{...transitionProps}
|
||||
onNavigateBack={this.props.actions.goBack}
|
||||
renderTitleComponent={this.renderTitle}
|
||||
renderLeftComponent={renderLeftComponent}
|
||||
renderTitleComponent={renderTitleComponent}
|
||||
renderRightComponent={renderRightComponent}
|
||||
style={navigationProps.headerStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -57,6 +95,7 @@ class Router extends React.Component {
|
|||
return (
|
||||
<NavigationExperimental.Card
|
||||
{...cardProps}
|
||||
panHandlers={null}
|
||||
style={style}
|
||||
renderScene={this.renderScene}
|
||||
key={scene.key}
|
||||
|
|
@ -69,13 +108,14 @@ class Router extends React.Component {
|
|||
<View style={{flex: 1}}>
|
||||
{renderedScenes}
|
||||
</View>
|
||||
{title}
|
||||
{navBar}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderTitle = ({scene}) => {
|
||||
const title = scene.route.title;
|
||||
const navigationProps = this.extractNavigationProps(scene.route);
|
||||
const title = navigationProps.title;
|
||||
|
||||
if (!title) {
|
||||
return null;
|
||||
|
|
@ -96,9 +136,15 @@ class Router extends React.Component {
|
|||
};
|
||||
|
||||
renderRoute = (route) => {
|
||||
const SceneComponent = getComponentForScene(route.key);
|
||||
const SceneComponent = route.component;
|
||||
|
||||
return <SceneComponent {...route.props}/>;
|
||||
return (
|
||||
<SceneComponent
|
||||
subscribeToHeaderEvent={this.subscribeToHeaderEvent}
|
||||
unsubscribeFromHeaderEvent={this.unsubscribeFromHeaderEvent}
|
||||
{...route.props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
configureTransition = () => {
|
||||
|
|
@ -110,11 +156,14 @@ class Router extends React.Component {
|
|||
|
||||
render = () => {
|
||||
const {
|
||||
index,
|
||||
leftDrawerOpen,
|
||||
leftDrawerRoute,
|
||||
rightDrawerOpen,
|
||||
rightDrawerRoute
|
||||
rightDrawerRoute,
|
||||
routes
|
||||
} = this.props.navigation;
|
||||
const navigationProps = this.extractNavigationProps(routes[index]);
|
||||
|
||||
let leftDrawerContent;
|
||||
if (leftDrawerRoute) {
|
||||
|
|
@ -142,7 +191,7 @@ class Router extends React.Component {
|
|||
panOpenMask={0.1}
|
||||
panCloseMask={0.2}
|
||||
panThreshold={0.2}
|
||||
acceptPan={true}
|
||||
acceptPan={navigationProps.allowSwipe}
|
||||
negotiatePan={true}
|
||||
>
|
||||
<Drawer
|
||||
|
|
|
|||
|
|
@ -1,6 +1,21 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {
|
||||
ChannelView,
|
||||
ChannelDrawer,
|
||||
ChannelInfo,
|
||||
ChannelMembers,
|
||||
LoadTeam,
|
||||
Login,
|
||||
Mfa,
|
||||
RightMenuDrawer,
|
||||
Root,
|
||||
Search,
|
||||
SelectServer,
|
||||
SelectTeam
|
||||
} from 'app/scenes';
|
||||
|
||||
import keyMirror from 'service/utils/key_mirror';
|
||||
|
||||
export const RouteTransitions = keyMirror({
|
||||
|
|
@ -10,52 +25,76 @@ export const RouteTransitions = keyMirror({
|
|||
export const Routes = {
|
||||
ChannelInfo: {
|
||||
key: 'ChannelInfo',
|
||||
title: {id: 'mobile.routes.channelInfo', defaultMessage: 'Info'},
|
||||
transition: RouteTransitions.Horizontal
|
||||
transition: RouteTransitions.Horizontal,
|
||||
component: ChannelInfo,
|
||||
navigationProps: {
|
||||
title: {id: 'mobile.routes.channelInfo', defaultMessage: 'Info'}
|
||||
}
|
||||
},
|
||||
ChannelDrawer: {
|
||||
key: 'ChannelDrawer'
|
||||
key: 'ChannelDrawer',
|
||||
component: ChannelDrawer
|
||||
},
|
||||
ChannelMembers: {
|
||||
key: 'ChannelMembers',
|
||||
title: {id: 'channel_header.manageMembers', defaultMessage: 'Manage Members'},
|
||||
transition: RouteTransitions.Horizontal
|
||||
transition: RouteTransitions.Horizontal,
|
||||
component: ChannelMembers,
|
||||
navigationProps: {
|
||||
title: {id: 'channel_header.manageMembers', defaultMessage: 'Manage Members'}
|
||||
}
|
||||
},
|
||||
ChannelView: {
|
||||
key: 'ChannelView',
|
||||
transition: RouteTransitions.Horizontal
|
||||
transition: RouteTransitions.Horizontal,
|
||||
component: ChannelView
|
||||
},
|
||||
LoadTeam: {
|
||||
key: 'LoadTeam'
|
||||
key: 'LoadTeam',
|
||||
component: LoadTeam
|
||||
},
|
||||
Login: {
|
||||
key: 'Login',
|
||||
title: {id: 'mobile.routes.login', defaultMessage: 'Login'},
|
||||
transition: RouteTransitions.Horizontal
|
||||
transition: RouteTransitions.Horizontal,
|
||||
component: Login,
|
||||
navigationProps: {
|
||||
title: {id: 'mobile.routes.login', defaultMessage: 'Login'}
|
||||
}
|
||||
},
|
||||
Mfa: {
|
||||
key: 'Mfa',
|
||||
title: {id: 'mobile.routes.mfa', defaultMessage: 'Multi-factor Authentication'},
|
||||
transition: RouteTransitions.Horizontal
|
||||
transition: RouteTransitions.Horizontal,
|
||||
component: Mfa,
|
||||
navigationProps: {
|
||||
title: {id: 'mobile.routes.mfa', defaultMessage: 'Multi-factor Authentication'}
|
||||
}
|
||||
},
|
||||
RightMenuDrawer: {
|
||||
key: 'RightMenuDrawer'
|
||||
key: 'RightMenuDrawer',
|
||||
component: RightMenuDrawer
|
||||
},
|
||||
Root: {
|
||||
key: 'Root'
|
||||
key: 'Root',
|
||||
component: Root
|
||||
},
|
||||
Search: {
|
||||
key: 'Search',
|
||||
transition: RouteTransitions.Horizontal
|
||||
transition: RouteTransitions.Horizontal,
|
||||
component: Search
|
||||
},
|
||||
SelectServer: {
|
||||
key: 'SelectServer',
|
||||
title: {id: 'mobile.routes.enterServerUrl', defaultMessage: 'Enter Server URL'}
|
||||
component: SelectServer,
|
||||
navigationProps: {
|
||||
title: {id: 'mobile.routes.enterServerUrl', defaultMessage: 'Enter Server URL'}
|
||||
}
|
||||
},
|
||||
SelectTeam: {
|
||||
key: 'SelectTeam',
|
||||
title: {id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'},
|
||||
transition: RouteTransitions.Horizontal
|
||||
transition: RouteTransitions.Horizontal,
|
||||
component: SelectTeam,
|
||||
navigationProps: {
|
||||
title: {id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ import {
|
|||
|
||||
import PostTextbox from 'app/components/post_textbox';
|
||||
|
||||
import ChannelHeader from './channel_header';
|
||||
import ChannelDrawerButton from './channel_drawer_button';
|
||||
import ChannelMenuButton from './channel_menu_button';
|
||||
import ChannelTitle from './channel_title';
|
||||
import ChannelPostList from './channel_post_list';
|
||||
|
||||
export default class Channel extends React.PureComponent {
|
||||
|
|
@ -29,10 +31,37 @@ export default class Channel extends React.PureComponent {
|
|||
currentTeam: React.PropTypes.object,
|
||||
currentChannel: React.PropTypes.object,
|
||||
postDraft: React.PropTypes.string.isRequired,
|
||||
theme: React.PropTypes.object.isRequired
|
||||
theme: React.PropTypes.object.isRequired,
|
||||
subscribeToHeaderEvent: React.PropTypes.func,
|
||||
unsubscribeFromHeaderEvent: React.PropTypes.func
|
||||
};
|
||||
|
||||
static navigationProps = {
|
||||
allowSwipe: true,
|
||||
renderLeftComponent: (props, emitter) => {
|
||||
return <ChannelDrawerButton emitter={emitter}/>;
|
||||
},
|
||||
renderTitleComponent: (props, emitter) => {
|
||||
return <ChannelTitle emitter={emitter}/>;
|
||||
},
|
||||
renderRightComponent: (props, emitter) => {
|
||||
return <ChannelMenuButton emitter={emitter}/>;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
leftSidebarOpen: false,
|
||||
rightSidebarOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.props.subscribeToHeaderEvent('open_channel_drawer', this.openChannelDrawer);
|
||||
this.props.subscribeToHeaderEvent('open_right_menu', this.openRightMenuDrawer);
|
||||
this.props.subscribeToHeaderEvent('show_channel_info', this.props.actions.goToChannelInfo);
|
||||
const teamId = this.props.currentTeam.id;
|
||||
this.props.actions.initWebSocket();
|
||||
this.loadChannels(teamId);
|
||||
|
|
@ -47,6 +76,7 @@ export default class Channel extends React.PureComponent {
|
|||
|
||||
componentWillUnmount() {
|
||||
this.props.actions.closeWebSocket();
|
||||
this.props.unsubscribeFromHeaderEvent('open_channel_drawer');
|
||||
}
|
||||
|
||||
loadChannels = (teamId) => {
|
||||
|
|
@ -57,15 +87,19 @@ export default class Channel extends React.PureComponent {
|
|||
};
|
||||
|
||||
openChannelDrawer = () => {
|
||||
this.refs.postTextbox.getWrappedInstance().blur();
|
||||
this.postTextbox.getWrappedInstance().blur();
|
||||
this.props.actions.openChannelDrawer();
|
||||
};
|
||||
|
||||
openRightMenuDrawer = () => {
|
||||
this.refs.postTextbox.getWrappedInstance().blur();
|
||||
this.postTextbox.getWrappedInstance().blur();
|
||||
this.props.actions.openRightMenuDrawer();
|
||||
};
|
||||
|
||||
attachPostTextbox = (c) => {
|
||||
this.postTextbox = c;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
currentTeam,
|
||||
|
|
@ -84,16 +118,10 @@ export default class Channel extends React.PureComponent {
|
|||
behavior='padding'
|
||||
style={{flex: 1, backgroundColor: theme.centerChannelBg}}
|
||||
>
|
||||
<StatusBar barStyle='default'/>
|
||||
<ChannelHeader
|
||||
currentChannel={currentChannel}
|
||||
openLeftDrawer={this.openChannelDrawer}
|
||||
openRightDrawer={this.openRightMenuDrawer}
|
||||
goToChannelInfo={this.props.actions.goToChannelInfo}
|
||||
/>
|
||||
<StatusBar barStyle='light-content'/>
|
||||
<ChannelPostList channel={currentChannel}/>
|
||||
<PostTextbox
|
||||
ref='postTextbox'
|
||||
ref={this.attachPostTextbox}
|
||||
value={this.props.postDraft}
|
||||
teamId={currentChannel.team_id}
|
||||
channelId={currentChannel.id}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import navigationSceneConnect from '../navigationSceneConnect';
|
||||
|
||||
import {
|
||||
goToChannelInfo,
|
||||
|
|
@ -53,4 +54,4 @@ function mapDispatchToProps(dispatch) {
|
|||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Channel);
|
||||
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(Channel);
|
||||
|
|
|
|||
47
app/scenes/channel/channel_drawer_button.js
Normal file
47
app/scenes/channel/channel_drawer_button.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PropTypes} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import {getTheme} from 'service/selectors/entities/preferences';
|
||||
|
||||
function ChannelDrawerButton(props) {
|
||||
return (
|
||||
<View style={{flexDirection: 'column', justifyContent: 'center', alignItems: 'center', flex: 1}}>
|
||||
<TouchableOpacity
|
||||
onPress={() => props.emitter('open_channel_drawer')}
|
||||
style={{height: 25, width: 25, marginLeft: 10, marginRight: 10}}
|
||||
>
|
||||
<Icon
|
||||
name='bars'
|
||||
size={25}
|
||||
color={props.theme.sidebarHeaderTextColor}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
ChannelDrawerButton.propTypes = {
|
||||
emitter: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object
|
||||
};
|
||||
|
||||
ChannelDrawerButton.defaultProps = {
|
||||
currentChannel: {},
|
||||
theme: {}
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ChannelDrawerButton);
|
||||
47
app/scenes/channel/channel_menu_button.js
Normal file
47
app/scenes/channel/channel_menu_button.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PropTypes} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import {getTheme} from 'service/selectors/entities/preferences';
|
||||
|
||||
function ChannelMenuButton(props) {
|
||||
return (
|
||||
<View style={{flexDirection: 'column', justifyContent: 'center', alignItems: 'center', flex: 1}}>
|
||||
<TouchableOpacity
|
||||
onPress={() => props.emitter('open_right_menu')}
|
||||
style={{height: 25, width: 25, marginLeft: 10, marginRight: 10}}
|
||||
>
|
||||
<Icon
|
||||
name='ellipsis-v'
|
||||
size={25}
|
||||
color={props.theme.sidebarHeaderTextColor}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
ChannelMenuButton.propTypes = {
|
||||
emitter: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object
|
||||
};
|
||||
|
||||
ChannelMenuButton.defaultProps = {
|
||||
currentChannel: {},
|
||||
theme: {}
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ChannelMenuButton);
|
||||
49
app/scenes/channel/channel_title.js
Normal file
49
app/scenes/channel/channel_title.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PropTypes} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import {getCurrentChannel} from 'service/selectors/entities/channels';
|
||||
import {getTheme} from 'service/selectors/entities/preferences';
|
||||
|
||||
function ChannelTitle(props) {
|
||||
const channelName = props.currentChannel.display_name;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={{flexDirection: 'row', flex: 1, alignItems: 'center'}}
|
||||
onPress={() => props.emitter('show_channel_info')}
|
||||
>
|
||||
<View>
|
||||
<Text style={{color: props.theme.sidebarHeaderTextColor, fontSize: 15, fontWeight: 'bold'}}>
|
||||
{channelName}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
ChannelTitle.propTypes = {
|
||||
currentChannel: PropTypes.object,
|
||||
emitter: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object
|
||||
};
|
||||
|
||||
ChannelTitle.defaultProps = {
|
||||
currentChannel: {},
|
||||
theme: {}
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
currentChannel: getCurrentChannel(state),
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ChannelTitle);
|
||||
|
|
@ -2,7 +2,8 @@
|
|||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import navigationSceneConnect from '../navigationSceneConnect';
|
||||
|
||||
import {goToChannelMembers, goBack} from 'app/actions/navigation';
|
||||
import {getChannelStats} from 'service/actions/channels';
|
||||
|
|
@ -46,4 +47,4 @@ function mapDispatchToProps(dispatch) {
|
|||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChannelInfo);
|
||||
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(ChannelInfo);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import navigationSceneConnect from '../navigationSceneConnect';
|
||||
|
||||
import {getCurrentChannel, getCurrentChannelStats} from 'service/selectors/entities/channels';
|
||||
import {getMyPreferences} from 'service/selectors/entities/preferences';
|
||||
|
|
@ -33,4 +34,4 @@ function mapDispatchToProps(dispatch) {
|
|||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChannelMembers);
|
||||
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(ChannelMembers);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ 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 = {
|
||||
module.exports = {
|
||||
ChannelView: Channel, // Special case the name for this one to avoid ambiguity
|
||||
ChannelDrawer,
|
||||
ChannelInfo,
|
||||
|
|
@ -28,7 +28,3 @@ const scenes = {
|
|||
SelectServer,
|
||||
SelectTeam
|
||||
};
|
||||
|
||||
export function getComponentForScene(key) {
|
||||
return scenes[key];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import navigationSceneConnect from '../navigationSceneConnect';
|
||||
|
||||
import {getClientConfig, getLicenseConfig} from 'service/actions/general';
|
||||
import LoginActions from 'app/actions/views/login';
|
||||
|
|
@ -40,4 +41,4 @@ function mapDispatchToProps(dispatch) {
|
|||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Login);
|
||||
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(Login);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import navigationSceneConnect from '../navigationSceneConnect';
|
||||
|
||||
import {goBack} from 'app/actions/navigation';
|
||||
import {login} from 'service/actions/users';
|
||||
|
|
@ -28,4 +29,4 @@ function mapDispatchToProps(dispatch) {
|
|||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Mfa);
|
||||
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(Mfa);
|
||||
|
|
|
|||
66
app/scenes/navigationSceneConnect.js
Normal file
66
app/scenes/navigationSceneConnect.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import React, {PropTypes} from 'react';
|
||||
import {
|
||||
Platform,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
const defaults = {
|
||||
renderBackButton: (props) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={{flexDirection: 'row', ...Platform.select({ios: {marginTop: 10}, android: {marginTop: 15}}), marginLeft: 15, alignItems: 'center'}}
|
||||
onPress={props.onNavigateBack}
|
||||
>
|
||||
<Icon
|
||||
name='angle-left'
|
||||
size={25}
|
||||
color='#fff'
|
||||
/>
|
||||
<FormattedText
|
||||
id='mobile.routes.back'
|
||||
defaultMessage='Back'
|
||||
style={{color: '#fff', marginLeft: 10}}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
},
|
||||
renderTitleComponent: (props) => {
|
||||
const navProps = props.scene.route.navigationProps || {};
|
||||
const title = navProps.title;
|
||||
if (title) {
|
||||
return (
|
||||
<View style={{alignItems: 'center', justifyContent: 'center', flex: 1, marginHorizontal: 50}}>
|
||||
<FormattedText
|
||||
id={title.id}
|
||||
defaultMessage={title.defaultMessage}
|
||||
style={{color: '#fff', fontSize: 15, fontWeight: 'bold'}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
},
|
||||
headerStyle: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#367FB0'
|
||||
},
|
||||
allowSwipe: false
|
||||
};
|
||||
|
||||
export default (stateProps, dispatchProps) => (WrappedComponent) => {
|
||||
const componentNavigationProps = WrappedComponent.WrappedComponent ? WrappedComponent.WrappedComponent.navigationProps : WrappedComponent.navigationProps;
|
||||
|
||||
WrappedComponent.navigationProps = Object.assign({}, defaults, componentNavigationProps);
|
||||
|
||||
return connect(stateProps, dispatchProps)(WrappedComponent);
|
||||
};
|
||||
|
|
@ -21,7 +21,10 @@ const Styles = StyleSheet.create({
|
|||
flex: 1,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
marginTop: 20
|
||||
paddingTop: 20
|
||||
},
|
||||
android: {
|
||||
paddingTop: 5
|
||||
}
|
||||
})
|
||||
},
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ export default class Root extends React.Component {
|
|||
}).isRequired
|
||||
};
|
||||
|
||||
static navigationProps = {
|
||||
hideNavBar: true
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Any initialization logic for navigation, setting up the client, etc should go here
|
||||
this.init();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import navigationSceneConnect from '../navigationSceneConnect';
|
||||
|
||||
import {loadStorage, removeStorage} from 'app/actions/storage';
|
||||
import {goToSelectServer, setStoreFromLocalData} from 'app/actions/views/root';
|
||||
|
|
@ -33,4 +34,4 @@ function mapDispatchToProps(dispatch) {
|
|||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Root);
|
||||
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(Root);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import navigationSceneConnect from '../navigationSceneConnect';
|
||||
|
||||
import {getPing} from 'service/actions/general';
|
||||
import {goToLogin} from 'app/actions/navigation';
|
||||
|
|
@ -27,4 +28,4 @@ function mapDispatchToProps(dispatch) {
|
|||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SelectServer);
|
||||
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(SelectServer);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import navigationSceneConnect from '../navigationSceneConnect';
|
||||
|
||||
import {goBack} from 'app/actions/navigation';
|
||||
import {handleTeamChange} from 'app/actions/views/select_team';
|
||||
|
|
@ -30,4 +31,4 @@ function mapDispatchToProps(dispatch) {
|
|||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SelectTeam);
|
||||
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(SelectTeam);
|
||||
|
|
|
|||
|
|
@ -29,13 +29,19 @@
|
|||
"babel-preset-es2015": "6.18.0",
|
||||
"babel-preset-react-native": "1.9.1",
|
||||
"babel-register": "6.18.0",
|
||||
"chai": "3.5.0",
|
||||
"chai-enzyme": "0.6.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"enzyme": "2.7.1",
|
||||
"eslint": "3.12.2",
|
||||
"eslint-plugin-mocha": "4.8.0",
|
||||
"eslint-plugin-react": "6.8.0",
|
||||
"fetch-mock": "5.8.1",
|
||||
"mocha": "3.2.0",
|
||||
"react-addons-test-utils": "15.4.2",
|
||||
"react-dom": "15.4.2",
|
||||
"react-native-mock": "0.2.9",
|
||||
"react-native-svg-mock": "1.0.2",
|
||||
"react-test-renderer": "15.4.1",
|
||||
"redux-logger": "2.7.4",
|
||||
"remote-redux-devtools": "0.5.7",
|
||||
|
|
@ -47,7 +53,7 @@
|
|||
"run-ios": "node node_modules/react-native/local-cli/cli.js run-ios",
|
||||
"run-android": "node node_modules/react-native/local-cli/cli.js run-android",
|
||||
"start": "node node_modules/react-native/local-cli/cli.js start -- --reset-cache",
|
||||
"test": "NODE_ENV=test mocha --compilers js:babel-register --require babel-polyfill",
|
||||
"test": "NODE_ENV=test mocha --opts test/mocha.opts",
|
||||
"postinstall": "make post-install"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
--require test/setup.js
|
||||
--compilers js:babel-core/register
|
||||
--require react-native-mock/mock.js
|
||||
--require react-native-svg-mock/mock
|
||||
--require babel-polyfill
|
||||
--require isomorphic-fetch
|
||||
--recursive
|
||||
|
|
|
|||
52
test/setup.js
Normal file
52
test/setup.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
// Setup recommendation from the following blog:
|
||||
// https://blog.addjam.com/testing-react-native-with-mocha-and-enzyme-6b77cd9e52a1#.2awpwqwwb
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import register from 'babel-core/register';
|
||||
import chai from 'chai';
|
||||
import chaiEnzyme from 'chai-enzyme';
|
||||
|
||||
const m = require('module');
|
||||
const originalLoader = m._load;
|
||||
|
||||
// Image file ignore setup from:
|
||||
// http://valuemotive.com/2016/08/01/unit-testing-react-native-components-with-mocha-and-enzyme/
|
||||
m._load = function hookedLoader(request, parent, isMain) {
|
||||
if (request.match(/.jpeg|.jpg|.png$/)) {
|
||||
return {uri: request};
|
||||
}
|
||||
|
||||
return originalLoader(request, parent, isMain);
|
||||
};
|
||||
|
||||
// Ignore all node_modules except these
|
||||
const modulesToCompile = [
|
||||
'react-native',
|
||||
'react-native-mock',
|
||||
'react-native-svg-mock/mock',
|
||||
'react-native-vector-icons',
|
||||
'react-native-svg'
|
||||
].map((moduleName) => new RegExp(`/node_modules/${moduleName}`));
|
||||
|
||||
const rcPath = path.join(__dirname, '..', '.babelrc');
|
||||
const source = fs.readFileSync(rcPath).toString();
|
||||
const config = JSON.parse(source);
|
||||
config.ignore = function(filename) {
|
||||
if (!(/\/node_modules\//).test(filename)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const matches = modulesToCompile.filter((regex) => regex.test(filename));
|
||||
const shouldIgnore = matches.length === 0;
|
||||
return shouldIgnore;
|
||||
};
|
||||
|
||||
register(config);
|
||||
|
||||
chai.use(chaiEnzyme());
|
||||
Loading…
Reference in a new issue