MM-16011 Update navigation through Login and Channel screens (#2870)

* Update navigation through Login and Channel screens

* Stop using static options and mergeOptions

* Update unit tests
This commit is contained in:
Harrison Healey 2019-06-11 18:10:14 -04:00 committed by Elias Nahum
parent 8d2b2b09d9
commit 73792d5526
12 changed files with 221 additions and 217 deletions

78
app/actions/navigation.js Normal file
View file

@ -0,0 +1,78 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Navigation} from 'react-native-navigation';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
export function resetToChannel() {
return (dispatch, getState) => {
const theme = getTheme(getState());
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'Channel',
options: {
topBar: {
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
statusBar: {
visible: true,
},
title: {
color: theme.sidebarHeaderTextColor,
},
visible: false,
},
},
},
}],
},
},
});
};
}
export function resetToSelectServer(allowOtherServers) {
return (dispatch, getState) => {
const theme = getTheme(getState());
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'SelectServer',
passProps: {
allowOtherServers,
},
options: {
topBar: {
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
statusBar: {
visible: true,
},
visible: false,
},
},
},
}],
},
},
});
};
}

View file

@ -8,15 +8,14 @@ import {setGenericPassword, getGenericPassword, resetGenericPassword} from 'reac
import {loadMe} from 'mattermost-redux/actions/users';
import {Client4} from 'mattermost-redux/client';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {resetToChannel, resetToSelectServer} from 'app/actions/navigation';
import {setDeepLinkURL} from 'app/actions/views/root';
import {ViewTypes} from 'app/constants';
import tracker from 'app/utils/time_tracker';
import {getCurrentLocale} from 'app/selectors/i18n';
import {getTranslations as getLocalTranslations} from 'app/i18n';
import {store, handleManagedConfig} from 'app/mattermost';
import {store, handleManagedConfig, initializeModules} from 'app/mattermost';
import avoidNativeBridge from 'app/utils/avoid_native_bridge';
import {setCSRFFromCookie} from 'app/utils/security';
@ -302,13 +301,15 @@ export default class App {
switch (screen) {
case 'SelectServer':
EventEmitter.emit(ViewTypes.LAUNCH_LOGIN, true);
dispatch(resetToSelectServer(this.allowOtherServers));
break;
case 'Channel':
EventEmitter.emit(ViewTypes.LAUNCH_CHANNEL, true);
dispatch(resetToChannel());
break;
}
initializeModules();
this.setAppStarted(true);
}
}

View file

@ -80,9 +80,6 @@ const ViewTypes = keyMirror({
INCREMENT_EMOJI_PICKER_PAGE: null,
LAUNCH_LOGIN: null,
LAUNCH_CHANNEL: null,
SET_DEEP_LINK_URL: null,
SET_PROFILE_IMAGE_URI: null,

View file

@ -26,6 +26,7 @@ import {General} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {resetToChannel, resetToSelectServer} from 'app/actions/navigation';
import {selectDefaultChannel} from 'app/actions/views/channel';
import {setDeviceDimensions, setDeviceOrientation, setDeviceAsTablet, setStatusBarHeight} from 'app/actions/device';
import {handleLoginIdChanged} from 'app/actions/views/login';
@ -75,7 +76,7 @@ const lazyLoadAnalytics = () => {
};
};
const initializeModules = () => {
export const initializeModules = () => {
const {
StatusBarSizeIOS,
initializeErrorHandling,
@ -141,7 +142,7 @@ const handleLogout = () => {
app.clearNativeCache();
deleteFileCache();
resetBadgeAndVersion();
launchSelectServer();
store.dispatch(resetToSelectServer(app.allowOtherServers));
};
const restartApp = async () => {
@ -165,7 +166,7 @@ const restartApp = async () => {
console.warn('Failed to load initial data while restarting', e); // eslint-disable-line no-console
}
launchChannel();
store.dispatch(resetToChannel());
};
const handleServerVersionChanged = async (serverVersion) => {
@ -384,59 +385,6 @@ const handleSwitchToDefaultChannel = (teamId) => {
store.dispatch(selectDefaultChannel(teamId));
};
const launchSelectServer = () => {
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'SelectServer',
passProps: {
allowOtherServers: app.allowOtherServers,
},
},
}],
options: {
layout: {
backgroundColor: 'transparent',
},
statusBar: {
visible: true,
},
topBar: {
visible: false,
},
},
},
},
});
};
const launchChannel = () => {
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'Channel',
},
}],
options: {
layout: {
backgroundColor: 'transparent',
},
statusBar: {
visible: true,
},
topBar: {
visible: false,
},
},
},
},
});
};
const handleAppStateChange = (appState) => {
const isActive = appState === 'active';
@ -506,9 +454,6 @@ const launchEntry = () => {
children: [{
component: {
name: 'Entry',
passProps: {
initializeModules,
},
},
}],
options: {

View file

@ -13,16 +13,13 @@ import DeviceInfo from 'react-native-device-info';
import {setSystemEmojis} from 'mattermost-redux/actions/emojis';
import {Client4} from 'mattermost-redux/client';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {
app,
store,
} from 'app/mattermost';
import {ViewTypes} from 'app/constants';
import PushNotifications from 'app/push_notifications';
import {stripTrailingSlashes} from 'app/utils/url';
import {wrapWithContextProvider} from 'app/utils/wrap_context_provider';
import ChannelLoader from 'app/components/channel_loader';
import EmptyToolbar from 'app/components/start/empty_toolbar';
@ -30,14 +27,6 @@ import Loading from 'app/components/loading';
import SafeAreaView from 'app/components/safe_area_view';
import StatusBar from 'app/components/status_bar';
const lazyLoadSelectServer = () => {
return require('app/screens/select_server').default;
};
const lazyLoadChannel = () => {
return require('app/screens/channel').default;
};
const lazyLoadPushNotifications = () => {
return require('app/utils/push_notifications').configurePushNotifications;
};
@ -61,7 +50,6 @@ export default class Entry extends PureComponent {
isLandscape: PropTypes.bool,
enableTimezone: PropTypes.bool,
deviceTimezone: PropTypes.string,
initializeModules: PropTypes.func,
actions: PropTypes.shape({
autoUpdateTimezone: PropTypes.func.isRequired,
setDeviceToken: PropTypes.func.isRequired,
@ -71,11 +59,6 @@ export default class Entry extends PureComponent {
constructor(props) {
super(props);
this.state = {
launchLogin: false,
launchChannel: false,
};
this.unsubscribeFromStore = null;
}
@ -87,32 +70,8 @@ export default class Entry extends PureComponent {
} else {
this.unsubscribeFromStore = store.subscribe(this.listenForHydration);
}
EventEmitter.on(ViewTypes.LAUNCH_LOGIN, this.handleLaunchLogin);
EventEmitter.on(ViewTypes.LAUNCH_CHANNEL, this.handleLaunchChannel);
}
componentWillUnmount() {
EventEmitter.off(ViewTypes.LAUNCH_LOGIN, this.handleLaunchLogin);
EventEmitter.off(ViewTypes.LAUNCH_CHANNEL, this.handleLaunchChannel);
}
handleLaunchLogin = (initializeModules) => {
this.setState({launchLogin: true});
if (initializeModules) {
this.props.initializeModules();
}
};
handleLaunchChannel = (initializeModules) => {
this.setState({launchChannel: true});
if (initializeModules) {
this.props.initializeModules();
}
};
listenForHydration = () => {
const {getState} = store;
const state = getState();
@ -245,39 +204,12 @@ export default class Entry extends PureComponent {
setSystemEmojis(EmojiIndicesByAlias);
};
renderLogin = () => {
const SelectServer = lazyLoadSelectServer();
const props = {
allowOtherServers: app.allowOtherServers,
navigator: this.props.navigator,
};
return wrapWithContextProvider(SelectServer)(props);
};
renderChannel = () => {
const ChannelScreen = lazyLoadChannel();
const props = {
navigator: this.props.navigator,
};
return wrapWithContextProvider(ChannelScreen, false)(props);
};
render() {
const {
navigator,
isLandscape,
} = this.props;
if (this.state.launchLogin) {
return this.renderLogin();
}
if (this.state.launchChannel) {
return this.renderChannel();
}
let toolbar = null;
let loading = null;
const backgroundColor = app.appBackground ? app.appBackground : '#ffff';

View file

@ -14,6 +14,7 @@ import SelectServer from 'app/screens/select_server';
const navigator = {
push: () => {}, // eslint-disable-line no-empty-function
setOnNavigatorEvent: () => {}, // eslint-disable-line no-empty-function
setStyle: () => {}, // eslint-disable-line no-empty-function
};
export function registerScreens(store, Provider) {

View file

@ -4,11 +4,12 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import LoginActions from 'app/actions/views/login';
import {login} from 'mattermost-redux/actions/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {login} from 'mattermost-redux/actions/users';
import {resetToChannel} from 'app/actions/navigation';
import LoginActions from 'app/actions/views/login';
import Login from './login.js';
@ -30,6 +31,7 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators({
...LoginActions,
login,
resetToChannel,
}, dispatch),
};
}

View file

@ -36,15 +36,16 @@ const mfaExpectedErrors = ['mfa.validate_token.authenticate.app_error', 'ent.mfa
export default class Login extends PureComponent {
static propTypes = {
navigator: PropTypes.object,
theme: PropTypes.object,
actions: PropTypes.shape({
handleLoginIdChanged: PropTypes.func.isRequired,
handlePasswordChanged: PropTypes.func.isRequired,
handleSuccessfulLogin: PropTypes.func.isRequired,
scheduleExpiredNotification: PropTypes.func.isRequired,
login: PropTypes.func.isRequired,
resetToChannel: PropTypes.func.isRequired,
}).isRequired,
navigator: PropTypes.object.isRequired, // TODO remove me
theme: PropTypes.object,
config: PropTypes.object.isRequired,
license: PropTypes.object.isRequired,
loginId: PropTypes.string.isRequired,
@ -66,6 +67,7 @@ export default class Login extends PureComponent {
componentDidMount() {
Dimensions.addEventListener('change', this.orientationDidChange);
setMfaPreflightDone(false);
}
@ -84,25 +86,11 @@ export default class Login extends PureComponent {
goToChannel = () => {
telemetry.remove(['start:overall']);
const {navigator} = this.props;
tracker.initialLoad = Date.now();
this.scheduleSessionExpiredNotification();
navigator.resetTo({
screen: 'Channel',
title: '',
animated: false,
backButtonTitle: '',
navigatorStyle: {
animated: true,
animationType: 'fade',
navBarHidden: true,
statusBarHidden: false,
statusBarHideWithNavBar: false,
screenBackgroundColor: 'transparent',
},
});
this.props.actions.resetToChannel();
};
goToMfa = () => {

View file

@ -3,6 +3,8 @@
import React from 'react';
import {RequestStatus} from 'mattermost-redux/constants';
import FormattedText from 'app/components/formatted_text';
import {shallowWithIntl} from 'test/intl-test-helper';
@ -21,12 +23,14 @@ describe('Login', () => {
loginId: '',
password: '',
loginRequest: {},
navigator: {},
actions: {
handleLoginIdChanged: jest.fn(),
handlePasswordChanged: jest.fn(),
handleSuccessfulLogin: jest.fn(),
scheduleExpiredNotification: jest.fn(),
login: jest.fn(),
resetToChannel: jest.fn(),
},
};
@ -72,4 +76,42 @@ describe('Login', () => {
expect(wrapper.find(FormattedText).find({id: 'login.forgot'}).exists()).toBe(false);
});
test('should send the user to the login screen after login', (done) => {
let props = {
...baseProps,
loginRequest: {
status: RequestStatus.NOT_STARTED,
},
};
props.actions.handleSuccessfulLogin.mockImplementation(() => Promise.resolve());
props.actions.resetToChannel.mockImplementation(() => {
done();
});
const wrapper = shallowWithIntl(<Login {...props}/>);
expect(props.actions.resetToChannel).not.toHaveBeenCalled();
props = {
...props,
loginRequest: {
status: RequestStatus.STARTED,
},
};
wrapper.setProps(props);
expect(props.actions.resetToChannel).not.toHaveBeenCalled();
props = {
...props,
loginRequest: {
status: RequestStatus.SUCCESS,
},
};
wrapper.setProps(props);
// This test times out if resetToChannel hasn't been called
});
});

View file

@ -3,7 +3,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {intlShape} from 'react-intl';
import {
Dimensions,
Image,
@ -12,6 +12,7 @@ import {
Text,
} from 'react-native';
import Button from 'react-native-button';
import {Navigation} from 'react-native-navigation';
import {ViewTypes} from 'app/constants';
import FormattedText from 'app/components/formatted_text';
@ -23,16 +24,20 @@ import LocalConfig from 'assets/config';
import gitlab from 'assets/images/gitlab.png';
import logo from 'assets/images/logo.png';
class LoginOptions extends PureComponent {
export default class LoginOptions extends PureComponent {
static propTypes = {
intl: intlShape.isRequired,
navigator: PropTypes.object,
componentId: PropTypes.string.isRequired,
navigator: PropTypes.object.isRequired, // TODO remove me
config: PropTypes.object.isRequired,
license: PropTypes.object.isRequired,
theme: PropTypes.object,
};
componentWillMount() {
static contextTypes = {
intl: intlShape.isRequired,
};
componentDidMount() {
Dimensions.addEventListener('change', this.orientationDidChange);
}
@ -41,23 +46,38 @@ class LoginOptions extends PureComponent {
}
goToLogin = preventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.push({
screen: 'Login',
title: intl.formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'}),
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
const {intl} = this.context;
const {componentId, theme} = this.props;
Navigation.push(componentId, {
component: {
name: 'Login',
passProps: {
theme,
},
options: {
topBar: {
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
text: intl.formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'}),
},
visible: true,
},
},
},
});
});
goToSSO = (ssoType) => {
const {intl, navigator, theme} = this.props;
const {intl} = this.context;
const {navigator, theme} = this.props;
navigator.push({
screen: 'SSO',
title: intl.formatMessage({id: 'mobile.routes.sso', defaultMessage: 'Single Sign-On'}),
@ -306,8 +326,6 @@ const style = StyleSheet.create({
flexDirection: 'column',
justifyContent: 'center',
paddingHorizontal: 15,
paddingVertical: 50,
flex: 1,
},
});
export default injectIntl(LoginOptions);

View file

@ -6,14 +6,15 @@ import {connect} from 'react-redux';
import {getPing, resetPing, setServerVersion} from 'mattermost-redux/actions/general';
import {login} from 'mattermost-redux/actions/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {resetToChannel} from 'app/actions/navigation';
import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade';
import {handleSuccessfulLogin, scheduleExpiredNotification} from 'app/actions/views/login';
import {loadConfigAndLicense} from 'app/actions/views/root';
import {handleServerUrlChanged} from 'app/actions/views/select_server';
import getClientUpgrade from 'app/selectors/client_upgrade';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import SelectServer from './select_server';
@ -46,6 +47,7 @@ function mapDispatchToProps(dispatch) {
resetPing,
setLastUpgradeCheck,
setServerVersion,
resetToChannel,
}, dispatch),
};
}

View file

@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {Navigation} from 'react-native-navigation';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {
@ -48,17 +49,19 @@ export default class SelectServer extends PureComponent {
loadConfigAndLicense: PropTypes.func.isRequired,
login: PropTypes.func.isRequired,
resetPing: PropTypes.func.isRequired,
resetToChannel: PropTypes.func.isRequired,
setLastUpgradeCheck: PropTypes.func.isRequired,
setServerVersion: PropTypes.func.isRequired,
}).isRequired,
allowOtherServers: PropTypes.bool,
componentId: PropTypes.string.isRequired,
config: PropTypes.object,
currentVersion: PropTypes.string,
hasConfigAndLicense: PropTypes.bool.isRequired,
latestVersion: PropTypes.string,
license: PropTypes.object,
minVersion: PropTypes.string,
navigator: PropTypes.object,
navigator: PropTypes.object.isRequired, // TODO remove me
serverUrl: PropTypes.string.isRequired,
theme: PropTypes.object,
};
@ -93,10 +96,11 @@ export default class SelectServer extends PureComponent {
}
this.certificateListener = DeviceEventEmitter.addListener('RNFetchBlobCertificate', this.selectCertificate);
this.props.navigator.setOnNavigatorEvent(this.handleNavigatorEvent);
telemetry.end(['start:select_server_screen']);
telemetry.save();
this.navigationEventListener = Navigation.events().bindComponent(this);
}
componentWillUpdate(nextProps, nextState) {
@ -117,10 +121,19 @@ export default class SelectServer extends PureComponent {
}
componentWillUnmount() {
this.certificateListener.remove();
if (Platform.OS === 'android') {
Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard);
}
this.certificateListener.remove();
this.navigationEventListener.remove();
}
componentDidDisappear() {
this.setState({
connected: false,
});
}
blur = () => {
@ -145,18 +158,28 @@ export default class SelectServer extends PureComponent {
};
goToNextScreen = (screen, title) => {
const {navigator, theme} = this.props;
navigator.push({
screen,
title,
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarHidden: LocalConfig.AutoSelectServerUrl,
disabledBackGesture: LocalConfig.AutoSelectServerUrl,
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
const {componentId, theme} = this.props;
Navigation.push(componentId, {
component: {
name: screen,
options: {
popGesture: !LocalConfig.AutoSelectServerUrl,
topBar: {
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
visible: !LocalConfig.AutoSelectServerUrl,
},
},
},
});
};
@ -244,16 +267,6 @@ export default class SelectServer extends PureComponent {
}
};
handleNavigatorEvent = (event) => {
switch (event.id) {
case 'didDisappear':
this.setState({
connected: false,
});
break;
}
};
handleShowClientUpgrade = (upgradeType) => {
const {formatMessage} = this.context.intl;
const {theme} = this.props;
@ -287,28 +300,13 @@ export default class SelectServer extends PureComponent {
};
loginWithCertificate = async () => {
const {navigator} = this.props;
tracker.initialLoad = Date.now();
await this.props.actions.login('credential', 'password');
await this.props.actions.handleSuccessfulLogin();
this.scheduleSessionExpiredNotification();
navigator.resetTo({
screen: 'Channel',
title: '',
animated: false,
backButtonTitle: '',
navigatorStyle: {
animated: true,
animationType: 'fade',
navBarHidden: true,
statusBarHidden: false,
statusBarHideWithNavBar: false,
screenBackgroundColor: 'transparent',
},
});
this.props.actions.resetToChannel();
};
pingServer = (url, retryWithHttp = true) => {