From 73792d5526d8881e5304f8a14830ab0afa885b6f Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Tue, 11 Jun 2019 18:10:14 -0400 Subject: [PATCH] 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 --- app/actions/navigation.js | 78 +++++++++++++++++++++ app/app.js | 11 +-- app/constants/view.js | 3 - app/mattermost.js | 63 ++--------------- app/screens/entry/entry.js | 68 ------------------ app/screens/index.js | 1 + app/screens/login/index.js | 6 +- app/screens/login/login.js | 22 ++---- app/screens/login/login.test.js | 42 ++++++++++++ app/screens/login_options/login_options.js | 58 ++++++++++------ app/screens/select_server/index.js | 6 +- app/screens/select_server/select_server.js | 80 +++++++++++----------- 12 files changed, 221 insertions(+), 217 deletions(-) create mode 100644 app/actions/navigation.js diff --git a/app/actions/navigation.js b/app/actions/navigation.js new file mode 100644 index 000000000..b206b1e3b --- /dev/null +++ b/app/actions/navigation.js @@ -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, + }, + }, + }, + }], + }, + }, + }); + }; +} diff --git a/app/app.js b/app/app.js index 36846618d..36baf2a1a 100644 --- a/app/app.js +++ b/app/app.js @@ -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); } } diff --git a/app/constants/view.js b/app/constants/view.js index b885a87e9..32f89bbf1 100644 --- a/app/constants/view.js +++ b/app/constants/view.js @@ -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, diff --git a/app/mattermost.js b/app/mattermost.js index 9b740bc5e..a92049115 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -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: { diff --git a/app/screens/entry/entry.js b/app/screens/entry/entry.js index 500023a61..c7d576639 100644 --- a/app/screens/entry/entry.js +++ b/app/screens/entry/entry.js @@ -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'; diff --git a/app/screens/index.js b/app/screens/index.js index 44865bfbe..bd19a2f6d 100644 --- a/app/screens/index.js +++ b/app/screens/index.js @@ -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) { diff --git a/app/screens/login/index.js b/app/screens/login/index.js index 3a51f5254..22e7a1956 100644 --- a/app/screens/login/index.js +++ b/app/screens/login/index.js @@ -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), }; } diff --git a/app/screens/login/login.js b/app/screens/login/login.js index f05979b5d..672e1276b 100644 --- a/app/screens/login/login.js +++ b/app/screens/login/login.js @@ -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 = () => { diff --git a/app/screens/login/login.test.js b/app/screens/login/login.test.js index e2ed4935a..3b345c187 100644 --- a/app/screens/login/login.test.js +++ b/app/screens/login/login.test.js @@ -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(); + + 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 + }); }); diff --git a/app/screens/login_options/login_options.js b/app/screens/login_options/login_options.js index 7ce99601c..f0b05522d 100644 --- a/app/screens/login_options/login_options.js +++ b/app/screens/login_options/login_options.js @@ -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); diff --git a/app/screens/select_server/index.js b/app/screens/select_server/index.js index 99ef53694..5a7c593e8 100644 --- a/app/screens/select_server/index.js +++ b/app/screens/select_server/index.js @@ -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), }; } diff --git a/app/screens/select_server/select_server.js b/app/screens/select_server/select_server.js index e10e1b031..7d77448dd 100644 --- a/app/screens/select_server/select_server.js +++ b/app/screens/select_server/select_server.js @@ -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) => {