diff --git a/app/actions/navigation.js b/app/actions/navigation.js index ca261c905..f1e6852bb 100644 --- a/app/actions/navigation.js +++ b/app/actions/navigation.js @@ -178,7 +178,12 @@ export function popToRoot() { return () => { const componentId = EphemeralStore.getTopComponentId(); - Navigation.popToRoot(componentId); + Navigation.popToRoot(componentId).catch(() => { + // RNN returns a promise rejection if there are no screens + // atop the root screen to pop. We'll do nothing in this + // case but we will catch the rejection here so that the + // caller doesn't have to. + }); }; } @@ -289,7 +294,11 @@ export function dismissModal(options = {}) { export function dismissAllModals(options = {}) { return () => { - Navigation.dismissAllModals(options); + Navigation.dismissAllModals(options).catch(() => { + // RNN returns a promise rejection if there are no modals to + // dismiss. We'll do nothing in this case but we will catch + // the rejection here so that the caller doesn't have to. + }); }; } @@ -321,3 +330,32 @@ export function setButtons(componentId, buttons = {leftButtons: [], rightButtons }); }; } + +export function showOverlay(name, passProps, options = {}) { + return () => { + const defaultOptions = { + overlay: { + interceptTouchOutside: false, + }, + }; + + Navigation.showOverlay({ + component: { + name, + passProps, + options: merge(defaultOptions, options), + }, + }); + }; +} + +export function dismissOverlay(componentId) { + return () => { + return Navigation.dismissOverlay(componentId).catch(() => { + // RNN returns a promise rejection if there is no modal with + // this componentId to dismiss. We'll do nothing in this case + // but we will catch the rejection here so that the caller + // doesn't have to. + }); + }; +} diff --git a/app/actions/views/search.js b/app/actions/views/search.js index 0e6f0c3a2..2ccd0060d 100644 --- a/app/actions/views/search.js +++ b/app/actions/views/search.js @@ -1,8 +1,6 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; - import {ViewTypes} from 'app/constants'; export function handleSearchDraftChanged(text) { @@ -13,27 +11,3 @@ export function handleSearchDraftChanged(text) { }, getState); }; } - -// TODO: Remove this once all screens/components call -// app/actions/navigation's showSearchModal -export function showSearchModal(navigator, initialValue = '') { - return (dispatch, getState) => { - const theme = getTheme(getState()); - - const options = { - screen: 'Search', - animated: true, - backButtonTitle: '', - overrideBackPress: true, - passProps: { - initialValue, - }, - navigatorStyle: { - navBarHidden: true, - screenBackgroundColor: theme.centerChannelBg, - }, - }; - - navigator.showModal(options); - }; -} diff --git a/app/components/__snapshots__/profile_picture_button.test.js.snap b/app/components/__snapshots__/profile_picture_button.test.js.snap index 97f0ea52d..c4205e2cf 100644 --- a/app/components/__snapshots__/profile_picture_button.test.js.snap +++ b/app/components/__snapshots__/profile_picture_button.test.js.snap @@ -9,13 +9,6 @@ exports[`profile_picture_button should match snapshot 1`] = ` ] } maxFileSize={20971520} - navigator={ - Object { - "dismissModal": [MockFunction], - "push": [MockFunction], - "setButtons": [MockFunction], - } - } theme={ Object { "awayIndicator": "#ffbc42", diff --git a/app/components/client_upgrade_listener/client_upgrade_listener.js b/app/components/client_upgrade_listener/client_upgrade_listener.js index 31631d8ca..b05b3f14f 100644 --- a/app/components/client_upgrade_listener/client_upgrade_listener.js +++ b/app/components/client_upgrade_listener/client_upgrade_listener.js @@ -27,6 +27,8 @@ export default class ClientUpgradeListener extends PureComponent { actions: PropTypes.shape({ logError: PropTypes.func.isRequired, setLastUpgradeCheck: PropTypes.func.isRequired, + showModal: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, }).isRequired, currentVersion: PropTypes.string, downloadLink: PropTypes.string, @@ -35,7 +37,6 @@ export default class ClientUpgradeListener extends PureComponent { lastUpgradeCheck: PropTypes.number, latestVersion: PropTypes.string, minVersion: PropTypes.string, - navigator: PropTypes.object, theme: PropTypes.object.isRequired, }; @@ -140,27 +141,26 @@ export default class ClientUpgradeListener extends PureComponent { }; handleLearnMore = () => { + const {actions} = this.props; const {intl} = this.context; - this.props.navigator.dismissModal({animationType: 'none'}); - this.props.navigator.showModal({ - screen: 'ClientUpgrade', - title: intl.formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Upgrade App'}), - navigatorStyle: { - navBarHidden: false, - statusBarHidden: false, - statusBarHideWithNavBar: false, - }, - navigatorButtons: { + actions.dismissModal(); + + const screen = 'ClientUpgrade'; + const title = intl.formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Upgrade App'}); + const passProps = { + upgradeType: this.state.upgradeType, + }; + const options = { + topBar: { leftButtons: [{ id: 'close-upgrade', icon: this.closeButton, }], }, - passProps: { - upgradeType: this.state.upgradeType, - }, - }); + }; + + actions.showModal(screen, title, passProps, options); this.toggleUpgradeMessage(false); }; diff --git a/app/components/client_upgrade_listener/index.js b/app/components/client_upgrade_listener/index.js index 2e66b2377..173c14a50 100644 --- a/app/components/client_upgrade_listener/index.js +++ b/app/components/client_upgrade_listener/index.js @@ -4,11 +4,12 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {logError} from 'mattermost-redux/actions/errors'; +import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {showModal, dismissModal} from 'app/actions/navigation'; import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade'; import getClientUpgrade from 'app/selectors/client_upgrade'; import {isLandscape} from 'app/selectors/device'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import ClientUpgradeListener from './client_upgrade_listener'; @@ -32,6 +33,8 @@ function mapDispatchToProps(dispatch) { actions: bindActionCreators({ logError, setLastUpgradeCheck, + showModal, + dismissModal, }, dispatch), }; } diff --git a/app/components/message_attachments/index.js b/app/components/message_attachments/index.js index e3b287545..7f3c126ce 100644 --- a/app/components/message_attachments/index.js +++ b/app/components/message_attachments/index.js @@ -18,7 +18,6 @@ export default class MessageAttachments extends PureComponent { deviceWidth: PropTypes.number.isRequired, postId: PropTypes.string.isRequired, metadata: PropTypes.object, - navigator: PropTypes.object.isRequired, onHashtagPress: PropTypes.func, onPermalinkPress: PropTypes.func, theme: PropTypes.object, @@ -33,7 +32,6 @@ export default class MessageAttachments extends PureComponent { deviceHeight, deviceWidth, metadata, - navigator, onHashtagPress, onPermalinkPress, postId, @@ -52,7 +50,6 @@ export default class MessageAttachments extends PureComponent { deviceWidth={deviceWidth} key={'att_' + i} metadata={metadata} - navigator={navigator} onHashtagPress={onHashtagPress} onPermalinkPress={onPermalinkPress} postId={postId} diff --git a/app/components/post_attachment_opengraph/post_attachment_opengraph.test.js b/app/components/post_attachment_opengraph/post_attachment_opengraph.test.js index 4d03ae92d..dc1b926ff 100644 --- a/app/components/post_attachment_opengraph/post_attachment_opengraph.test.js +++ b/app/components/post_attachment_opengraph/post_attachment_opengraph.test.js @@ -37,7 +37,6 @@ describe('PostAttachmentOpenGraph', () => { }, isReplyPost: false, link: 'https://mattermost.com/', - navigator: {}, theme: Preferences.THEMES.default, }; diff --git a/app/components/post_textbox/__snapshots__/post_textbox.test.js.snap b/app/components/post_textbox/__snapshots__/post_textbox.test.js.snap index ac7535189..ea489c424 100644 --- a/app/components/post_textbox/__snapshots__/post_textbox.test.js.snap +++ b/app/components/post_textbox/__snapshots__/post_textbox.test.js.snap @@ -16,23 +16,11 @@ exports[`PostTextBox should match, full snapshot 1`] = ` } } > - { files: [], maxFileSize: 1024, maxMessageLength: 4000, - navigator: { - showModal: jest.fn(), - }, rootId: '', theme: Preferences.THEMES.default, uploadFileRequestStatus: 'NOT_STARTED', diff --git a/app/components/profile_picture_button.test.js b/app/components/profile_picture_button.test.js index a46a1a09c..44237c1a0 100644 --- a/app/components/profile_picture_button.test.js +++ b/app/components/profile_picture_button.test.js @@ -10,15 +10,8 @@ import ProfilePictureButton from './profile_picture_button.js'; import {Client4} from 'mattermost-redux/client'; describe('profile_picture_button', () => { - const navigator = { - setButtons: jest.fn(), - dismissModal: jest.fn(), - push: jest.fn(), - }; - const baseProps = { theme: Preferences.THEMES.default, - navigator, currentUser: { first_name: 'Dwight', last_name: 'Schrute', diff --git a/app/components/root/index.js b/app/components/root/index.js index 747bfdb9c..7f4b28031 100644 --- a/app/components/root/index.js +++ b/app/components/root/index.js @@ -4,7 +4,6 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; @@ -19,7 +18,6 @@ function mapStateToProps(state) { return { theme: getTheme(state), - currentChannelId: getCurrentChannelId(state), currentUrl: removeProtocol(getCurrentUrl(state)), locale, }; diff --git a/app/components/root/root.js b/app/components/root/root.js index 051c4c715..95e46af44 100644 --- a/app/components/root/root.js +++ b/app/components/root/root.js @@ -9,7 +9,7 @@ import {Platform} from 'react-native'; import {Client4} from 'mattermost-redux/client'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import {NavigationTypes, ViewTypes} from 'app/constants'; +import {NavigationTypes} from 'app/constants'; import {getTranslations} from 'app/i18n'; export default class Root extends PureComponent { @@ -18,9 +18,7 @@ export default class Root extends PureComponent { resetToTeams: PropTypes.func.isRequired, }).isRequired, children: PropTypes.node, - navigator: PropTypes.object, excludeEvents: PropTypes.bool, - currentChannelId: PropTypes.string, currentUrl: PropTypes.string, locale: PropTypes.string.isRequired, theme: PropTypes.object.isRequired, @@ -30,8 +28,6 @@ export default class Root extends PureComponent { Client4.setAcceptLanguage(this.props.locale); if (!this.props.excludeEvents) { - EventEmitter.on(ViewTypes.NOTIFICATION_IN_APP, this.handleInAppNotification); - EventEmitter.on(ViewTypes.NOTIFICATION_TAPPED, this.handleNotificationTapped); EventEmitter.on(NavigationTypes.NAVIGATION_NO_TEAMS, this.handleNoTeams); EventEmitter.on(NavigationTypes.NAVIGATION_ERROR_TEAMS, this.errorTeamsList); } @@ -45,30 +41,11 @@ export default class Root extends PureComponent { componentWillUnmount() { if (!this.props.excludeEvents) { - EventEmitter.off(ViewTypes.NOTIFICATION_IN_APP, this.handleInAppNotification); - EventEmitter.off(ViewTypes.NOTIFICATION_TAPPED, this.handleNotificationTapped); EventEmitter.off(NavigationTypes.NAVIGATION_NO_TEAMS, this.handleNoTeams); EventEmitter.off(NavigationTypes.NAVIGATION_ERROR_TEAMS, this.errorTeamsList); } } - handleInAppNotification = (notification) => { - const {data} = notification; - const {currentChannelId, navigator} = this.props; - - if (data && data.channel_id !== currentChannelId) { - navigator.showInAppNotification({ - screen: 'Notification', - position: 'top', - autoDismissTimerSec: 5, - dismissWithSwipe: true, - passProps: { - notification, - }, - }); - } - }; - handleNoTeams = () => { if (!this.refs.provider) { setTimeout(this.handleNoTeams, 200); @@ -119,18 +96,6 @@ export default class Root extends PureComponent { actions.resetToTeams(screen, title, passProps, options); } - handleNotificationTapped = async () => { - const {navigator} = this.props; - - if (Platform.OS === 'android') { - navigator.dismissModal({animation: 'none'}); - } - - navigator.popToRoot({ - animated: false, - }); - }; - render() { const locale = this.props.locale; diff --git a/app/components/sidebars/main/channels_list/channel_item/channel_item.test.js b/app/components/sidebars/main/channels_list/channel_item/channel_item.test.js index 38334340d..b9ced6211 100644 --- a/app/components/sidebars/main/channels_list/channel_item/channel_item.test.js +++ b/app/components/sidebars/main/channels_list/channel_item/channel_item.test.js @@ -31,7 +31,6 @@ describe('ChannelItem', () => { isUnread: true, hasDraft: false, mentions: 0, - navigator: {push: () => {}}, // eslint-disable-line no-empty-function onSelectChannel: () => {}, // eslint-disable-line no-empty-function shouldHideChannel: false, showUnreadForMsgs: true, diff --git a/app/components/sidebars/settings/settings_sidebar.js b/app/components/sidebars/settings/settings_sidebar.js index 9a3fff5b3..41a76f970 100644 --- a/app/components/sidebars/settings/settings_sidebar.js +++ b/app/components/sidebars/settings/settings_sidebar.js @@ -68,10 +68,12 @@ export default class SettingsDrawer extends PureComponent { } componentDidMount() { + EventEmitter.on('close_settings_sidebar', this.closeSettingsSidebar); BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack); } componentWillUnmount() { + EventEmitter.off('close_settings_sidebar', this.closeSettingsSidebar); BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack); } diff --git a/app/constants/navigation.js b/app/constants/navigation.js index 809997da3..618dc5b3f 100644 --- a/app/constants/navigation.js +++ b/app/constants/navigation.js @@ -9,6 +9,7 @@ const NavigationTypes = keyMirror({ NAVIGATION_NO_TEAMS: null, RESTART_APP: null, NAVIGATION_ERROR_TEAMS: null, + NAVIGATION_SHOW_OVERLAY: null, }); export default NavigationTypes; diff --git a/app/constants/view.js b/app/constants/view.js index 0c3dc6b37..010944a69 100644 --- a/app/constants/view.js +++ b/app/constants/view.js @@ -42,7 +42,6 @@ const ViewTypes = keyMirror({ COMMENT_DRAFT_SELECTION_CHANGED: null, NOTIFICATION_IN_APP: null, - NOTIFICATION_TAPPED: null, SET_POST_DRAFT: null, SET_COMMENT_DRAFT: null, diff --git a/app/init/global_event_handler.js b/app/init/global_event_handler.js index df17fb796..4eb258b93 100644 --- a/app/init/global_event_handler.js +++ b/app/init/global_event_handler.js @@ -11,11 +11,13 @@ import {close as closeWebSocket} from 'mattermost-redux/actions/websocket'; import {Client4} from 'mattermost-redux/client'; import {General} from 'mattermost-redux/constants'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; import {setDeviceDimensions, setDeviceOrientation, setDeviceAsTablet, setStatusBarHeight} from 'app/actions/device'; import {selectDefaultChannel} from 'app/actions/views/channel'; +import {showOverlay} from 'app/actions/navigation'; import {loadConfigAndLicense, setDeepLinkURL, startDataCleanup} from 'app/actions/views/root'; -import {NavigationTypes} from 'app/constants'; +import {NavigationTypes, ViewTypes} from 'app/constants'; import {getTranslations} from 'app/i18n'; import mattermostManaged from 'app/mattermost_managed'; import PushNotifications from 'app/push_notifications'; @@ -38,12 +40,15 @@ class GlobalEventHandler { EventEmitter.on(General.SERVER_VERSION_CHANGED, this.onServerVersionChanged); EventEmitter.on(General.CONFIG_CHANGED, this.onServerConfigChanged); EventEmitter.on(General.SWITCH_TO_DEFAULT_CHANNEL, this.onSwitchToDefaultChannel); + this.turnOnInAppNotificationHandling(); Dimensions.addEventListener('change', this.onOrientationChange); AppState.addEventListener('change', this.onAppStateChange); Linking.addEventListener('url', this.onDeepLink); } appActive = async () => { + this.turnOnInAppNotificationHandling(); + // if the app is being controlled by an EMM provider if (emmProvider.enabled && emmProvider.inAppPinCode) { const authExpired = (Date.now() - emmProvider.inBackgroundSince) >= PROMPT_IN_APP_PIN_CODE_AFTER; @@ -57,6 +62,8 @@ class GlobalEventHandler { }; appInactive = () => { + this.turnOffInAppNotificationHandling(); + const {dispatch} = this.store; // When the app is sent to the background we set the time when that happens @@ -227,6 +234,31 @@ class GlobalEventHandler { dispatch(logout()); } }; + + turnOnInAppNotificationHandling = () => { + EventEmitter.on(ViewTypes.NOTIFICATION_IN_APP, this.handleInAppNotification); + } + + turnOffInAppNotificationHandling = () => { + EventEmitter.off(ViewTypes.NOTIFICATION_IN_APP, this.handleInAppNotification); + } + + handleInAppNotification = (notification) => { + const {data} = notification; + const {dispatch, getState} = this.store; + const state = getState(); + const currentChannelId = getCurrentChannelId(state); + + if (data && data.channel_id !== currentChannelId) { + const screen = 'Notification'; + const passProps = { + notification, + }; + + EventEmitter.emit(NavigationTypes.NAVIGATION_SHOW_OVERLAY); + dispatch(showOverlay(screen, passProps)); + } + }; } export default new GlobalEventHandler(); diff --git a/app/screens/about/about.js b/app/screens/about/about.js index dc056e9ec..2417768c1 100644 --- a/app/screens/about/about.js +++ b/app/screens/about/about.js @@ -26,7 +26,6 @@ export default class About extends PureComponent { componentId: PropTypes.string, config: PropTypes.object.isRequired, license: PropTypes.object.isRequired, - navigator: PropTypes.object.isRequired, theme: PropTypes.object.isRequired, }; diff --git a/app/screens/add_reaction/add_reaction.js b/app/screens/add_reaction/add_reaction.js index f24978bf5..e03ddbf85 100644 --- a/app/screens/add_reaction/add_reaction.js +++ b/app/screens/add_reaction/add_reaction.js @@ -15,9 +15,12 @@ import {setNavigatorStyles} from 'app/utils/theme'; export default class AddReaction extends PureComponent { static propTypes = { + actions: PropTypes.shape({ + dismissModal: PropTypes.func.isRequired, + setButtons: PropTypes.func.isRequired, + }).isRequired, componentId: PropTypes.string, closeButton: PropTypes.object, - navigator: PropTypes.object.isRequired, onEmojiPress: PropTypes.func, theme: PropTypes.object.isRequired, }; @@ -33,7 +36,7 @@ export default class AddReaction extends PureComponent { constructor(props) { super(props); - props.navigator.setButtons({ + props.actions.setButtons(props.componentId, { leftButtons: [{...this.leftButton, icon: props.closeButton}], }); } @@ -55,9 +58,7 @@ export default class AddReaction extends PureComponent { } close = () => { - this.props.navigator.dismissModal({ - animationType: 'slide-down', - }); + this.props.actions.dismissModal(); }; handleEmojiPress = (emoji) => { diff --git a/app/screens/add_reaction/index.js b/app/screens/add_reaction/index.js index 1c3d7556b..eb24346b2 100644 --- a/app/screens/add_reaction/index.js +++ b/app/screens/add_reaction/index.js @@ -1,10 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {dismissModal, setButtons} from 'app/actions/navigation'; + import AddReaction from './add_reaction'; function mapStateToProps(state) { @@ -13,4 +16,13 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(AddReaction); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + dismissModal, + setButtons, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(AddReaction); diff --git a/app/screens/channel/channel.android.js b/app/screens/channel/channel.android.js index 14748150e..866911888 100644 --- a/app/screens/channel/channel.android.js +++ b/app/screens/channel/channel.android.js @@ -20,35 +20,30 @@ import ChannelBase, {ClientUpgradeListener, style} from './channel_base'; export default class ChannelAndroid extends ChannelBase { render() { const {height} = Dimensions.get('window'); - const { - navigator, - } = this.props; const channelLoaderStyle = [style.channelLoader, {height}]; const drawerContent = ( - + - + - {LocalConfig.EnableMobileClientUpgrade && } + {LocalConfig.EnableMobileClientUpgrade && } ); diff --git a/app/screens/channel/channel.ios.js b/app/screens/channel/channel.ios.js index 700de6ab7..5e9189102 100644 --- a/app/screens/channel/channel.ios.js +++ b/app/screens/channel/channel.ios.js @@ -38,7 +38,6 @@ export default class ChannelIOS extends ChannelBase { const {height} = Dimensions.get('window'); const { currentChannelId, - navigator, } = this.props; const channelLoaderStyle = [style.channelLoader, {height}]; @@ -48,17 +47,15 @@ export default class ChannelIOS extends ChannelBase { const drawerContent = ( - + @@ -74,7 +71,7 @@ export default class ChannelIOS extends ChannelBase { height={height} style={channelLoaderStyle} /> - {LocalConfig.EnableMobileClientUpgrade && } + {LocalConfig.EnableMobileClientUpgrade && } diff --git a/app/screens/code/code.js b/app/screens/code/code.js index 831538c8a..019b3346b 100644 --- a/app/screens/code/code.js +++ b/app/screens/code/code.js @@ -18,8 +18,10 @@ import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/ut export default class Code extends React.PureComponent { static propTypes = { + actions: PropTypes.shape({ + popTopScreen: PropTypes.func.isRequired, + }).isRequired, componentId: PropTypes.string, - navigator: PropTypes.object.isRequired, theme: PropTypes.object.isRequired, content: PropTypes.string.isRequired, }; @@ -39,7 +41,7 @@ export default class Code extends React.PureComponent { } handleAndroidBack = () => { - this.props.navigator.pop(); + this.props.actions.popTopScreen(); return true; }; diff --git a/app/screens/code/index.js b/app/screens/code/index.js index 4b0abdcd9..c833020be 100644 --- a/app/screens/code/index.js +++ b/app/screens/code/index.js @@ -1,10 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {popTopScreen} from 'app/actions/navigation'; + import Code from './code'; function mapStateToProps(state) { @@ -13,4 +16,12 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(Code); \ No newline at end of file +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + popTopScreen, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(Code); \ No newline at end of file diff --git a/app/screens/error_teams_list/error_teams_list.js b/app/screens/error_teams_list/error_teams_list.js index a795920c8..a972997cd 100644 --- a/app/screens/error_teams_list/error_teams_list.js +++ b/app/screens/error_teams_list/error_teams_list.js @@ -32,9 +32,9 @@ export default class ErrorTeamsList extends PureComponent { connection: PropTypes.func.isRequired, logout: PropTypes.func.isRequired, selectDefaultTeam: PropTypes.func.isRequired, + resetToChannel: PropTypes.func.isRequired, }).isRequired, componentId: PropTypes.string, - navigator: PropTypes.object, theme: PropTypes.object, }; @@ -64,21 +64,10 @@ export default class ErrorTeamsList extends PureComponent { } goToChannelView = () => { - const {navigator, theme} = this.props; - - navigator.resetTo({ - screen: 'Channel', - animated: false, - navigatorStyle: { - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - disableTermsModal: true, - }, - }); + const passProps = { + disableTermsModal: true, + }; + this.props.actions.resetToChannel(passProps); }; getUserInfo = async () => { diff --git a/app/screens/error_teams_list/error_teams_list.test.js b/app/screens/error_teams_list/error_teams_list.test.js index 1a8c053dc..229ec1f42 100644 --- a/app/screens/error_teams_list/error_teams_list.test.js +++ b/app/screens/error_teams_list/error_teams_list.test.js @@ -22,6 +22,7 @@ describe('ErrorTeamsList', () => { connection: () => {}, // eslint-disable-line no-empty-function logout: () => {}, // eslint-disable-line no-empty-function selectDefaultTeam: () => {}, // eslint-disable-line no-empty-function + resetToChannel: jest.fn(), }, componentId: 'component-id', theme: Preferences.THEMES.default, @@ -39,6 +40,7 @@ describe('ErrorTeamsList', () => { const selectDefaultTeam = jest.fn(); const logout = jest.fn(); const actions = { + ...baseProps.actions, loadMe, logout, selectDefaultTeam, diff --git a/app/screens/error_teams_list/index.js b/app/screens/error_teams_list/index.js index 31424ed03..981e62733 100644 --- a/app/screens/error_teams_list/index.js +++ b/app/screens/error_teams_list/index.js @@ -8,6 +8,8 @@ import {logout, loadMe} from 'mattermost-redux/actions/users'; import {connection} from 'app/actions/device'; import {selectDefaultTeam} from 'app/actions/views/select_team'; +import {resetToChannel} from 'app/actions/navigation'; + import ErrorTeamsList from './error_teams_list.js'; function mapDispatchToProps(dispatch) { @@ -17,6 +19,7 @@ function mapDispatchToProps(dispatch) { selectDefaultTeam, connection, loadMe, + resetToChannel, }, dispatch), }; } diff --git a/app/screens/expanded_announcement_banner/expanded_announcement_banner.js b/app/screens/expanded_announcement_banner/expanded_announcement_banner.js index 363105a53..3fb7a31ae 100644 --- a/app/screens/expanded_announcement_banner/expanded_announcement_banner.js +++ b/app/screens/expanded_announcement_banner/expanded_announcement_banner.js @@ -16,15 +16,15 @@ export default class ExpandedAnnouncementBanner extends React.PureComponent { static propTypes = { actions: PropTypes.shape({ dismissBanner: PropTypes.func.isRequired, + popTopScreen: PropTypes.func.isRequired, }).isRequired, allowDismissal: PropTypes.bool.isRequired, bannerText: PropTypes.string.isRequired, - navigator: PropTypes.object.isRequired, theme: PropTypes.object.isRequired, } close = () => { - this.props.navigator.pop(); + this.props.actions.popTopScreen(); }; dismissBanner = () => { @@ -67,7 +67,6 @@ export default class ExpandedAnnouncementBanner extends React.PureComponent { { - const {getItemMeasures, navigator} = this.props; + const {actions, getItemMeasures, componentId} = this.props; const {index} = this.state; this.setState({animating: true}); - navigator.setStyle({ - screenBackgroundColor: 'transparent', + Navigation.mergeOptions(componentId, { + layout: { + backgroundColor: 'transparent', + }, }); getItemMeasures(index, (origin) => { @@ -117,7 +126,7 @@ export default class ImagePreview extends PureComponent { } this.animateOpenAnimToValue(0, () => { - navigator.dismissModal({animationType: 'none'}); + actions.dismissModal(); }); }); }; @@ -177,7 +186,7 @@ export default class ImagePreview extends PureComponent { }; renderAttachmentDocument = (file) => { - const {canDownloadFiles, theme, navigator} = this.props; + const {canDownloadFiles, theme} = this.props; return ( @@ -190,7 +199,6 @@ export default class ImagePreview extends PureComponent { file={file} iconHeight={100} iconWidth={100} - navigator={navigator} theme={theme} wrapperHeight={200} wrapperWidth={200} @@ -441,6 +449,7 @@ export default class ImagePreview extends PureComponent { }; showDownloadOptionsIOS = async () => { + const {actions} = this.props; const {formatMessage} = this.context.intl; const file = this.getCurrentFile(); const items = []; @@ -504,30 +513,17 @@ export default class ImagePreview extends PureComponent { }); } - const options = { - title: file.caption, - items, - onCancelPress: () => this.setHeaderAndFooterVisible(true), - }; - if (items.length) { this.setHeaderAndFooterVisible(false); - this.props.navigator.showModal({ - screen: 'OptionsModal', - title: '', - animationType: 'none', - passProps: { - ...options, - }, - navigatorStyle: { - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: 'transparent', - modalPresentationStyle: 'overCurrentContext', - }, - }); + const screen = 'OptionsModal'; + const passProps = { + title: file.caption, + items, + onCancelPress: () => this.setHeaderAndFooterVisible(true), + }; + + actions.showModalOverCurrentContext(screen, passProps); } }; diff --git a/app/screens/image_preview/image_preview.test.js b/app/screens/image_preview/image_preview.test.js index e4e31a0f6..922e26551 100644 --- a/app/screens/image_preview/image_preview.test.js +++ b/app/screens/image_preview/image_preview.test.js @@ -6,6 +6,7 @@ import {shallow} from 'enzyme'; import { TouchableOpacity, } from 'react-native'; +import {Navigation} from 'react-native-navigation'; import Preferences from 'mattermost-redux/constants/preferences'; @@ -18,6 +19,13 @@ jest.mock('react-native-doc-viewer', () => { OpenFile: jest.fn(), }; }); +jest.mock('react-native-navigation', () => ({ + Navigation: { + mergeOptions: jest.fn(), + }, +})); + +Navigation.mergeOptions = jest.fn(); describe('ImagePreview', () => { const baseProps = { @@ -30,10 +38,14 @@ describe('ImagePreview', () => { ], getItemMeasures: jest.fn(), index: 0, - navigator: {setStyle: jest.fn()}, origin: {}, target: {}, theme: Preferences.THEMES.default, + componentId: 'component-id', + actions: { + dismissModal: jest.fn(), + showModalOverCurrentContext: jest.fn(), + }, }; test('should match snapshot', () => { @@ -67,21 +79,26 @@ describe('ImagePreview', () => { expect(wrapper.state('index')).toEqual(1); }); - test('should match call getItemMeasures & navigator.setStyle on close', () => { + test('should match call getItemMeasures & Navigation.mergeOptions on close', () => { const getItemMeasures = jest.fn(); - const navigator = {setStyle: jest.fn()}; const wrapper = shallow( , {context: {intl: {formatMessage: jest.fn()}}}, ); wrapper.instance().close(); - expect(navigator.setStyle).toHaveBeenCalledTimes(2); - expect(navigator.setStyle).toBeCalledWith({screenBackgroundColor: 'transparent'}); + expect(Navigation.mergeOptions).toHaveBeenCalledTimes(2); + expect(Navigation.mergeOptions).toHaveBeenCalledWith( + baseProps.componentId, + { + layout: { + backgroundColor: 'transparent', + }, + }, + ); }); }); diff --git a/app/screens/image_preview/index.js b/app/screens/image_preview/index.js index 425a1df99..e72f70bff 100644 --- a/app/screens/image_preview/index.js +++ b/app/screens/image_preview/index.js @@ -1,12 +1,15 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getDimensions} from 'app/selectors/device'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {canDownloadFilesOnMobile} from 'mattermost-redux/selectors/entities/general'; +import {dismissModal, showModalOverCurrentContext} from 'app/actions/navigation'; +import {getDimensions} from 'app/selectors/device'; + import ImagePreview from './image_preview'; function mapStateToProps(state) { @@ -17,4 +20,13 @@ function mapStateToProps(state) { }; } -export default connect(mapStateToProps)(ImagePreview); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + dismissModal, + showModalOverCurrentContext, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(ImagePreview); diff --git a/app/screens/index.js b/app/screens/index.js index 36d38b336..498e7171a 100644 --- a/app/screens/index.js +++ b/app/screens/index.js @@ -9,23 +9,12 @@ import Channel from 'app/screens/channel'; import Root from 'app/components/root'; import SelectServer from 'app/screens/select_server'; -// TODO remove dummy navigator object -const navigator = { - push: () => {}, // eslint-disable-line no-empty-function - setOnNavigatorEvent: () => {}, // eslint-disable-line no-empty-function - setStyle: () => {}, // eslint-disable-line no-empty-function - setButtons: () => {}, // eslint-disable-line no-empty-function -}; - export function registerScreens(store, Provider) { // TODO consolidate this with app/utils/wrap_context_provider const wrapper = (Comp) => (props) => ( // eslint-disable-line react/display-name - - + + ); diff --git a/app/screens/interactive_dialog/dialog_element.js b/app/screens/interactive_dialog/dialog_element.js index e675bcf78..5003c12ea 100644 --- a/app/screens/interactive_dialog/dialog_element.js +++ b/app/screens/interactive_dialog/dialog_element.js @@ -25,7 +25,6 @@ export default class DialogElement extends PureComponent { options: PropTypes.arrayOf(PropTypes.object), value: PropTypes.any, onChange: PropTypes.func, - navigator: PropTypes.object, theme: PropTypes.object, }; @@ -71,7 +70,6 @@ export default class DialogElement extends PureComponent { theme, dataSource, options, - navigator, } = this.props; let {maxLength} = this.props; @@ -128,7 +126,6 @@ export default class DialogElement extends PureComponent { placeholder={placeholder} showRequiredAsterisk={true} selected={this.state.selected} - navigator={navigator} roundedBorders={false} /> ); diff --git a/app/screens/interactive_dialog/index.js b/app/screens/interactive_dialog/index.js index e68b387ea..6170b7b6e 100644 --- a/app/screens/interactive_dialog/index.js +++ b/app/screens/interactive_dialog/index.js @@ -7,6 +7,8 @@ import {connect} from 'react-redux'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {submitInteractiveDialog} from 'mattermost-redux/actions/integrations'; +import {dismissModal} from 'app/actions/navigation'; + import InteractiveDialog from './interactive_dialog'; function mapStateToProps(state) { @@ -29,6 +31,7 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ submitInteractiveDialog, + dismissModal, }, dispatch), }; } diff --git a/app/screens/interactive_dialog/interactive_dialog.js b/app/screens/interactive_dialog/interactive_dialog.js index 94ace245b..e4888ee7e 100644 --- a/app/screens/interactive_dialog/interactive_dialog.js +++ b/app/screens/interactive_dialog/interactive_dialog.js @@ -22,10 +22,10 @@ export default class InteractiveDialog extends PureComponent { elements: PropTypes.arrayOf(PropTypes.object).isRequired, notifyOnCancel: PropTypes.bool, state: PropTypes.string, - navigator: PropTypes.object, theme: PropTypes.object, actions: PropTypes.shape({ submitInteractiveDialog: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, }).isRequired, }; @@ -136,9 +136,7 @@ export default class InteractiveDialog extends PureComponent { } handleHide = () => { - this.props.navigator.dismissModal({ - animationType: 'slide-down', - }); + this.props.actions.dismissModal(); } onChange = (name, value) => { @@ -147,7 +145,7 @@ export default class InteractiveDialog extends PureComponent { } render() { - const {elements, theme, navigator} = this.props; + const {elements, theme} = this.props; const style = getStyleFromTheme(theme); return ( @@ -172,7 +170,6 @@ export default class InteractiveDialog extends PureComponent { options={e.options} value={this.state.values[e.name]} onChange={this.onChange} - navigator={navigator} theme={theme} /> ); diff --git a/app/screens/interactive_dialog/interactive_dialog.test.js b/app/screens/interactive_dialog/interactive_dialog.test.js index 4219edf33..9030af549 100644 --- a/app/screens/interactive_dialog/interactive_dialog.test.js +++ b/app/screens/interactive_dialog/interactive_dialog.test.js @@ -19,8 +19,6 @@ describe('InteractiveDialog', () => { theme: Preferences.THEMES.default, actions: { submitInteractiveDialog: jest.fn(), - }, - navigator: { dismissModal: jest.fn(), }, componentId: 'component-id', @@ -37,11 +35,9 @@ describe('InteractiveDialog', () => { }); test('should submit dialog', async () => { - const submitInteractiveDialog = jest.fn(); const wrapper = shallow( , ); @@ -53,16 +49,14 @@ describe('InteractiveDialog', () => { }; wrapper.instance().handleSubmit(); - expect(submitInteractiveDialog).toHaveBeenCalledTimes(1); - expect(submitInteractiveDialog).toHaveBeenCalledWith(dialog); + expect(baseProps.actions.submitInteractiveDialog).toHaveBeenCalledTimes(1); + expect(baseProps.actions.submitInteractiveDialog).toHaveBeenCalledWith(dialog); }); test('should submit dialog on cancel', async () => { - const submitInteractiveDialog = jest.fn(); const wrapper = shallow( , ); @@ -75,21 +69,19 @@ describe('InteractiveDialog', () => { }; wrapper.instance().navigationButtonPressed({buttonId: 'close-dialog'}); - expect(submitInteractiveDialog).toHaveBeenCalledTimes(1); - expect(submitInteractiveDialog).toHaveBeenCalledWith(dialog); + expect(baseProps.actions.submitInteractiveDialog).toHaveBeenCalledTimes(1); + expect(baseProps.actions.submitInteractiveDialog).toHaveBeenCalledWith(dialog); }); test('should not submit dialog on cancel', async () => { - const submitInteractiveDialog = jest.fn(); const wrapper = shallow( , ); wrapper.instance().navigationButtonPressed({buttonId: 'close-dialog'}); - expect(submitInteractiveDialog).not.toHaveBeenCalled(); + expect(baseProps.actions.submitInteractiveDialog).not.toHaveBeenCalled(); }); }); diff --git a/app/screens/more_dms/index.js b/app/screens/more_dms/index.js index 3427eaff8..0f92b9b0a 100644 --- a/app/screens/more_dms/index.js +++ b/app/screens/more_dms/index.js @@ -14,6 +14,8 @@ import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selector import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentUserId, getUsers} from 'mattermost-redux/selectors/entities/users'; +import {dismissModal, setButtons} from 'app/actions/navigation'; + import MoreDirectMessages from './more_dms'; function mapStateToProps(state) { @@ -40,6 +42,8 @@ function mapDispatchToProps(dispatch) { getProfilesInTeam, searchProfiles, setChannelDisplayName, + dismissModal, + setButtons, }, dispatch), }; } diff --git a/app/screens/more_dms/more_dms.js b/app/screens/more_dms/more_dms.js index 0d8723af3..3f1640020 100644 --- a/app/screens/more_dms/more_dms.js +++ b/app/screens/more_dms/more_dms.js @@ -39,13 +39,14 @@ export default class MoreDirectMessages extends PureComponent { getProfilesInTeam: PropTypes.func.isRequired, searchProfiles: PropTypes.func.isRequired, setChannelDisplayName: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, + setButtons: PropTypes.func.isRequired, }).isRequired, componentId: PropTypes.string, allProfiles: PropTypes.object.isRequired, currentDisplayName: PropTypes.string, currentTeamId: PropTypes.string.isRequired, currentUserId: PropTypes.string.isRequired, - navigator: PropTypes.object, restrictDirectMessage: PropTypes.bool.isRequired, teammateNameDisplay: PropTypes.string, theme: PropTypes.object.isRequired, @@ -108,7 +109,7 @@ export default class MoreDirectMessages extends PureComponent { } close = () => { - this.props.navigator.dismissModal({animationType: 'slide-down'}); + this.props.actions.dismissModal(); }; clearSearch = () => { @@ -325,13 +326,14 @@ export default class MoreDirectMessages extends PureComponent { }; updateNavigationButtons = (startEnabled, context = this.context) => { + const {actions, componentId} = this.props; const {formatMessage} = context.intl; - this.props.navigator.setButtons({ + actions.setButtons(componentId, { rightButtons: [{ id: START_BUTTON, - title: formatMessage({id: 'mobile.more_dms.start', defaultMessage: 'Start'}), + text: formatMessage({id: 'mobile.more_dms.start', defaultMessage: 'Start'}), showAsAction: 'always', - disabled: !startEnabled, + enabled: startEnabled, }], }); }; diff --git a/app/screens/notification/index.js b/app/screens/notification/index.js index 7674cf74a..47d22b173 100644 --- a/app/screens/notification/index.js +++ b/app/screens/notification/index.js @@ -12,6 +12,8 @@ import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selector import {getUser} from 'mattermost-redux/selectors/entities/users'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; +import {dismissOverlay, dismissAllModals, popToRoot} from 'app/actions/navigation'; + import Notification from './notification'; function mapStateToProps(state, ownProps) { @@ -42,6 +44,9 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ loadFromPushNotification, + dismissOverlay, + dismissAllModals, + popToRoot, }, dispatch), }; } diff --git a/app/screens/notification/notification.js b/app/screens/notification/notification.js index ff2225f00..732796e12 100644 --- a/app/screens/notification/notification.js +++ b/app/screens/notification/notification.js @@ -8,55 +8,149 @@ import { InteractionManager, Platform, StyleSheet, - Text, TouchableOpacity, + Text, View, } from 'react-native'; - -import FormattedText from 'app/components/formatted_text'; -import ProfilePicture from 'app/components/profile_picture'; -import {changeOpacity} from 'app/utils/theme'; +import {Navigation} from 'react-native-navigation'; +import * as Animatable from 'react-native-animatable'; +import {PanGestureHandler} from 'react-native-gesture-handler'; import {isDirectChannel} from 'mattermost-redux/utils/channel_utils'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; import {displayUsername} from 'mattermost-redux/utils/user_utils'; +import FormattedText from 'app/components/formatted_text'; +import ProfilePicture from 'app/components/profile_picture'; +import {changeOpacity} from 'app/utils/theme'; +import {NavigationTypes} from 'app/constants'; + import logo from 'assets/images/icon.png'; import webhookIcon from 'assets/images/icons/webhook.jpg'; const IMAGE_SIZE = 33; +const AUTO_DISMISS_TIME_MILLIS = 5000; export default class Notification extends PureComponent { static propTypes = { actions: PropTypes.shape({ loadFromPushNotification: PropTypes.func.isRequired, + dismissOverlay: PropTypes.func.isRequired, + dismissAllModals: PropTypes.func.isRequired, + popToRoot: PropTypes.func.isRequired, }).isRequired, + componentId: PropTypes.string.isRequired, channel: PropTypes.object, config: PropTypes.object, deviceWidth: PropTypes.number.isRequired, notification: PropTypes.object.isRequired, teammateNameDisplay: PropTypes.string, - navigator: PropTypes.object, theme: PropTypes.object.isRequired, user: PropTypes.object, }; + state = { + keyFrames: { + from: { + translateY: -100, + }, + to: { + translateY: 0, + }, + }, + } + + tapped = false; + + componentDidMount() { + this.setDismissTimer(); + this.setDidDisappearListener(); + this.setShowOverlayListener(); + } + + componentWillUnmount() { + this.clearDismissTimer(); + this.clearDidDisappearListener(); + this.clearShowOverlayListener(); + } + + setDismissTimer = () => { + this.dismissTimer = setTimeout(() => { + if (!this.tapped) { + this.animateDismissOverlay(); + } + }, AUTO_DISMISS_TIME_MILLIS); + } + + clearDismissTimer = () => { + if (this.dismissTimer) { + clearTimeout(this.dismissTimer); + this.dismissTimer = null; + } + } + + setDidDisappearListener = () => { + this.didDismissListener = Navigation.events().registerComponentDidDisappearListener(({componentId}) => { + if (componentId === this.props.componentId && this.tapped) { + const {actions} = this.props; + actions.dismissAllModals(); + actions.popToRoot(); + } + }); + } + + clearDidDisappearListener = () => { + this.didDismissListener.remove(); + } + + setShowOverlayListener = () => { + EventEmitter.on(NavigationTypes.NAVIGATION_SHOW_OVERLAY, this.onNewOverlay); + } + + clearShowOverlayListener = () => { + EventEmitter.off(NavigationTypes.NAVIGATION_SHOW_OVERLAY, this.onNewOverlay); + } + + onNewOverlay = () => { + // Dismiss this overlay so that there is only ever one. + this.dismissOverlay(); + } + + dismissOverlay = () => { + this.clearDismissTimer(); + + const {actions, componentId} = this.props; + actions.dismissOverlay(componentId); + } + + animateDismissOverlay = () => { + this.clearDismissTimer(); + + this.setState({ + keyFrames: { + from: { + translateY: 0, + }, + to: { + translateY: -100, + }, + }, + }); + setTimeout(() => this.dismissOverlay(), 1000); + } + notificationTapped = () => { - const {actions, navigator, notification} = this.props; + this.tapped = true; + this.clearDismissTimer(); + + const {actions, notification} = this.props; EventEmitter.emit('close_channel_drawer'); + EventEmitter.emit('close_settings_sidebar'); InteractionManager.runAfterInteractions(() => { - navigator.dismissInAppNotification(); + this.dismissOverlay(); if (!notification.localNotification) { actions.loadFromPushNotification(notification); - - if (Platform.OS === 'android') { - navigator.dismissModal({animation: 'none'}); - } - - navigator.popToRoot({ - animated: false, - }); } }); }; @@ -202,28 +296,39 @@ export default class Notification extends PureComponent { const icon = this.getNotificationIcon(); return ( - - + - - {icon} + + + + {icon} + + + {title} + + + {messageText} + + + + - - {title} - - - {messageText} - - - - - + + ); } diff --git a/app/screens/permalink/index.js b/app/screens/permalink/index.js index 50d54399f..ef5f06525 100644 --- a/app/screens/permalink/index.js +++ b/app/screens/permalink/index.js @@ -16,13 +16,18 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import { + goToScreen, + dismissModal, + dismissAllModals, + resetToChannel, +} from 'app/actions/navigation'; import { handleSelectChannel, loadThreadIfNecessary, setChannelDisplayName, setChannelLoading, } from 'app/actions/views/channel'; -import {showSearchModal} from 'app/actions/views/search'; import {handleTeamChange} from 'app/actions/views/select_team'; import Permalink from './permalink'; @@ -74,7 +79,10 @@ function mapDispatchToProps(dispatch) { selectPost, setChannelDisplayName, setChannelLoading, - showSearchModal, + goToScreen, + dismissModal, + dismissAllModals, + resetToChannel, }, dispatch), }; } diff --git a/app/screens/permalink/permalink.js b/app/screens/permalink/permalink.js index d223b031b..3d59012b0 100644 --- a/app/screens/permalink/permalink.js +++ b/app/screens/permalink/permalink.js @@ -57,6 +57,10 @@ export default class Permalink extends PureComponent { selectPost: PropTypes.func.isRequired, setChannelDisplayName: PropTypes.func.isRequired, setChannelLoading: PropTypes.func.isRequired, + goToScreen: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, + dismissAllModals: PropTypes.func.isRequired, + resetToChannel: PropTypes.func.isRequired, }).isRequired, channelId: PropTypes.string, channelIsArchived: PropTypes.bool, @@ -67,7 +71,6 @@ export default class Permalink extends PureComponent { focusedPostId: PropTypes.string.isRequired, isPermalink: PropTypes.bool, myMembers: PropTypes.object.isRequired, - navigator: PropTypes.object, onClose: PropTypes.func, onPress: PropTypes.func, postIds: PropTypes.array, @@ -171,39 +174,29 @@ export default class Permalink extends PureComponent { } goToThread = preventDoubleTap((post) => { - const {actions, navigator, theme} = this.props; + const {actions} = this.props; const channelId = post.channel_id; const rootId = (post.root_id || post.id); + const screen = 'Thread'; + const title = ''; + const passProps = { + channelId, + rootId, + }; actions.loadThreadIfNecessary(rootId); actions.selectPost(rootId); - const options = { - screen: 'Thread', - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - channelId, - rootId, - }, - }; - - navigator.push(options); + actions.goToScreen(screen, title, passProps); }); handleClose = () => { - const {actions, navigator, onClose} = this.props; + const {actions, onClose} = this.props; if (this.refs.view) { this.mounted = false; this.refs.view.zoomOut().then(() => { actions.selectPost(''); - navigator.dismissModal({animationType: 'none'}); + actions.dismissModal(); if (onClose) { onClose(); @@ -232,7 +225,7 @@ export default class Permalink extends PureComponent { jumpToChannel = (channelId, channelDisplayName) => { if (channelId) { - const {actions, channelTeamId, currentTeamId, navigator, onClose, theme} = this.props; + const {actions, channelTeamId, currentTeamId, onClose} = this.props; const currentChannelId = this.props.channelId; const { handleSelectChannel, @@ -246,23 +239,13 @@ export default class Permalink extends PureComponent { if (channelId === currentChannelId) { EventEmitter.emit('reset_channel'); } else { - navigator.resetTo({ - screen: 'Channel', - animated: true, - animationType: 'fade', - navigatorStyle: { - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - disableTermsModal: true, - }, - }); + const passProps = { + disableTermsModal: true, + }; + actions.resetToChannel(passProps); } - navigator.dismissAllModals({animationType: 'slide-down'}); + actions.dismissAllModals(); if (onClose) { onClose(); @@ -350,7 +333,6 @@ export default class Permalink extends PureComponent { const { currentUserId, focusedPostId, - navigator, theme, } = this.props; const { @@ -395,7 +377,6 @@ export default class Permalink extends PureComponent { lastPostIndex={Platform.OS === 'android' ? getLastPostIndex(postIdsState) : -1} currentUserId={currentUserId} lastViewedAt={0} - navigator={navigator} highlightPinnedOrFlagged={false} /> ); diff --git a/app/screens/permalink/permalink.test.js b/app/screens/permalink/permalink.test.js index 5cc33b09c..bd1147c5a 100644 --- a/app/screens/permalink/permalink.test.js +++ b/app/screens/permalink/permalink.test.js @@ -11,13 +11,6 @@ import Permalink from './permalink.js'; jest.mock('react-intl'); describe('Permalink', () => { - const navigator = { - dismissAllModals: jest.fn(), - dismissModal: jest.fn(), - push: jest.fn(), - resetTo: jest.fn(), - }; - const actions = { getPostsAround: jest.fn(), getPostThread: jest.fn(), @@ -29,6 +22,10 @@ describe('Permalink', () => { selectPost: jest.fn(), setChannelDisplayName: jest.fn(), setChannelLoading: jest.fn(), + goToScreen: jest.fn(), + dismissModal: jest.fn(), + dismissAllModals: jest.fn(), + resetToChannel: jest.fn(), }; const baseProps = { @@ -42,7 +39,6 @@ describe('Permalink', () => { focusedPostId: 'focused_post_id', isPermalink: true, myMembers: {}, - navigator, onClose: jest.fn(), onPress: jest.fn(), postIds: ['post_id_1', 'focused_post_id', 'post_id_3'], diff --git a/app/screens/reaction_list/__snapshots__/reaction_list.test.js.snap b/app/screens/reaction_list/__snapshots__/reaction_list.test.js.snap index bb73b1098..bdfdeb767 100644 --- a/app/screens/reaction_list/__snapshots__/reaction_list.test.js.snap +++ b/app/screens/reaction_list/__snapshots__/reaction_list.test.js.snap @@ -22,7 +22,7 @@ exports[`ReactionList should match snapshot 1`] = ` } } > - - - - { - this.props.navigator.dismissModal({ - animationType: 'none', - }); + this.props.actions.dismissModal(); }; getMissingProfiles = () => { @@ -137,7 +135,6 @@ export default class ReactionList extends PureComponent { renderReactionRows = () => { const { - navigator, teammateNameDisplay, theme, } = this.props; @@ -157,7 +154,6 @@ export default class ReactionList extends PureComponent { > { const baseProps = { actions: { getMissingProfilesByIds: jest.fn(), + dismissModal: jest.fn(), }, allUserIds: ['user_id_1', 'user_id_2'], reactions: {'user_id_1-smile': {emoji_name: 'smile', user_id: 'user_id_1'}, 'user_id_2-+1': {emoji_name: '+1', user_id: 'user_id_2'}}, diff --git a/app/screens/reaction_list/__snapshots__/reaction_row.test.js.snap b/app/screens/reaction_list/reaction_row/__snapshots__/reaction_row.test.js.snap similarity index 100% rename from app/screens/reaction_list/__snapshots__/reaction_row.test.js.snap rename to app/screens/reaction_list/reaction_row/__snapshots__/reaction_row.test.js.snap diff --git a/app/screens/reaction_list/reaction_row/index.js b/app/screens/reaction_list/reaction_row/index.js new file mode 100644 index 000000000..d91992b1f --- /dev/null +++ b/app/screens/reaction_list/reaction_row/index.js @@ -0,0 +1,19 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; + +import {goToScreen} from 'app/actions/navigation'; + +import ReactionRow from './reaction_row'; + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + goToScreen, + }, dispatch), + }; +} + +export default connect(null, mapDispatchToProps)(ReactionRow); diff --git a/app/screens/reaction_list/reaction_row.js b/app/screens/reaction_list/reaction_row/reaction_row.js similarity index 82% rename from app/screens/reaction_list/reaction_row.js rename to app/screens/reaction_list/reaction_row/reaction_row.js index da33b73ff..8ad6e2e56 100644 --- a/app/screens/reaction_list/reaction_row.js +++ b/app/screens/reaction_list/reaction_row/reaction_row.js @@ -21,8 +21,10 @@ import Emoji from 'app/components/emoji'; export default class ReactionRow extends React.PureComponent { static propTypes = { + actions: PropTypes.shape({ + goToScreen: PropTypes.func.isRequired, + }).isRequired, emojiName: PropTypes.string.isRequired, - navigator: PropTypes.object, teammateNameDisplay: PropTypes.string.isRequired, theme: PropTypes.object.isRequired, user: PropTypes.object.isRequired, @@ -37,26 +39,15 @@ export default class ReactionRow extends React.PureComponent { }; goToUserProfile = () => { - const {navigator, theme, user} = this.props; + const {actions, user} = this.props; const {formatMessage} = this.context.intl; - - const options = { - screen: 'UserProfile', - title: formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}), - animated: true, - backButtonTitle: '', - passProps: { - userId: user.id, - }, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, + const screen = 'UserProfile'; + const title = formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}); + const passProps = { + userId: user.id, }; - navigator.push(options); + actions.goToScreen(screen, title, passProps); }; render() { diff --git a/app/screens/reaction_list/reaction_row.test.js b/app/screens/reaction_list/reaction_row/reaction_row.test.js similarity index 91% rename from app/screens/reaction_list/reaction_row.test.js rename to app/screens/reaction_list/reaction_row/reaction_row.test.js index 471b67658..21e7fcd10 100644 --- a/app/screens/reaction_list/reaction_row.test.js +++ b/app/screens/reaction_list/reaction_row/reaction_row.test.js @@ -9,8 +9,10 @@ import ReactionRow from './reaction_row'; describe('ReactionRow', () => { const baseProps = { + actions: { + goToScreen: jest.fn(), + }, emojiName: 'smile', - navigator: {}, teammateNameDisplay: 'username', theme: Preferences.THEMES.default, user: {id: 'user_id', username: 'username'}, diff --git a/app/screens/search/index.js b/app/screens/search/index.js index a8751a9f6..7f6918eae 100644 --- a/app/screens/search/index.js +++ b/app/screens/search/index.js @@ -15,7 +15,7 @@ import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils'; import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; -import {dismissModal} from 'app/actions/navigation'; +import {dismissModal, goToScreen, showModalOverCurrentContext} from 'app/actions/navigation'; import {loadChannelsByTeamName, loadThreadIfNecessary} from 'app/actions/views/channel'; import {handleSearchDraftChanged} from 'app/actions/views/search'; import {isLandscape} from 'app/selectors/device'; @@ -86,6 +86,8 @@ function mapDispatchToProps(dispatch) { getMorePostsForSearch, selectPost, dismissModal, + goToScreen, + showModalOverCurrentContext, }, dispatch), }; } diff --git a/app/screens/search/search.js b/app/screens/search/search.js index 82acb7e3e..d8d32a2f7 100644 --- a/app/screens/search/search.js +++ b/app/screens/search/search.js @@ -57,12 +57,13 @@ export default class Search extends PureComponent { selectFocusedPostId: PropTypes.func.isRequired, selectPost: PropTypes.func.isRequired, dismissModal: PropTypes.func.isRequired, + goToScreen: PropTypes.func.isRequired, + showModalOverCurrentContext: PropTypes.func.isRequired, }).isRequired, componentId: PropTypes.string.isRequired, currentTeamId: PropTypes.string.isRequired, initialValue: PropTypes.string, isLandscape: PropTypes.bool.isRequired, - navigator: PropTypes.object, postIds: PropTypes.array, archivedPostIds: PropTypes.arrayOf(PropTypes.string), recent: PropTypes.array.isRequired, @@ -183,7 +184,7 @@ export default class Search extends PureComponent { }); goToThread = (post) => { - const {actions, navigator, theme} = this.props; + const {actions} = this.props; const channelId = post.channel_id; const rootId = (post.root_id || post.id); @@ -191,23 +192,14 @@ export default class Search extends PureComponent { actions.loadThreadIfNecessary(rootId); actions.selectPost(rootId); - const options = { - screen: 'Thread', - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - channelId, - rootId, - }, + const screen = 'Thread'; + const title = ''; + const passProps = { + channelId, + rootId, }; - navigator.push(options); + actions.goToScreen(screen, title, passProps); }; handleHashtagPress = (hashtag) => { @@ -389,7 +381,6 @@ export default class Search extends PureComponent { postId={item} previewPost={this.previewPost} goToThread={this.goToThread} - navigator={this.props.navigator} onHashtagPress={this.handleHashtagPress} onPermalinkPress={this.handlePermalinkPress} managedConfig={mattermostManaged.getCachedConfig()} @@ -449,29 +440,24 @@ export default class Search extends PureComponent { }; showPermalinkView = (postId, isPermalink) => { - const {actions, navigator} = this.props; + const {actions} = this.props; actions.selectFocusedPostId(postId); if (!this.showingPermalink) { + const screen = 'Permalink'; + const passProps = { + isPermalink, + onClose: this.handleClosePermalink, + }; const options = { - screen: 'Permalink', - animationType: 'none', - backButtonTitle: '', - overrideBackPress: true, - navigatorStyle: { - navBarHidden: true, - screenBackgroundColor: changeOpacity('#000', 0.2), - modalPresentationStyle: 'overCurrentContext', - }, - passProps: { - isPermalink, - onClose: this.handleClosePermalink, + layout: { + backgroundColor: changeOpacity('#000', 0.2), }, }; this.showingPermalink = true; - navigator.showModal(options); + actions.showModalOverCurrentContext(screen, passProps, options); } }; diff --git a/app/screens/selector_screen/index.js b/app/screens/selector_screen/index.js index e5f69d1fd..1bd00b4ac 100644 --- a/app/screens/selector_screen/index.js +++ b/app/screens/selector_screen/index.js @@ -9,6 +9,8 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {getProfiles, searchProfiles} from 'mattermost-redux/actions/users'; import {getChannels, searchChannels} from 'mattermost-redux/actions/channels'; +import {popTopScreen} from 'app/actions/navigation'; + import SelectorScreen from './selector_screen'; function mapStateToProps(state) { @@ -32,6 +34,7 @@ function mapDispatchToProps(dispatch) { getChannels, searchProfiles, searchChannels, + popTopScreen, }, dispatch), }; } diff --git a/app/screens/selector_screen/selector_screen.js b/app/screens/selector_screen/selector_screen.js index 2694641e7..9db461989 100644 --- a/app/screens/selector_screen/selector_screen.js +++ b/app/screens/selector_screen/selector_screen.js @@ -34,12 +34,12 @@ export default class SelectorScreen extends PureComponent { getChannels: PropTypes.func.isRequired, searchProfiles: PropTypes.func.isRequired, searchChannels: PropTypes.func.isRequired, + popTopScreen: PropTypes.func.isRequired, }), componentId: PropTypes.string, currentTeamId: PropTypes.string.isRequired, data: PropTypes.arrayOf(PropTypes.object), dataSource: PropTypes.string, - navigator: PropTypes.object, onSelect: PropTypes.func.isRequired, theme: PropTypes.object.isRequired, }; @@ -93,7 +93,7 @@ export default class SelectorScreen extends PureComponent { }; close = () => { - this.props.navigator.pop({animated: true}); + this.props.actions.popTopScreen(); }; handleSelectItem = (id, item) => { diff --git a/app/screens/selector_screen/selector_screen.test.js b/app/screens/selector_screen/selector_screen.test.js index b017809f9..eb6d311cb 100644 --- a/app/screens/selector_screen/selector_screen.test.js +++ b/app/screens/selector_screen/selector_screen.test.js @@ -67,6 +67,7 @@ describe('SelectorScreen', () => { getChannels, searchProfiles, searchChannels, + popTopScreen: jest.fn(), }; const baseProps = { diff --git a/app/screens/terms_of_service/__snapshots__/terms_of_service.test.js.snap b/app/screens/terms_of_service/__snapshots__/terms_of_service.test.js.snap index f323d4cbd..313792907 100644 --- a/app/screens/terms_of_service/__snapshots__/terms_of_service.test.js.snap +++ b/app/screens/terms_of_service/__snapshots__/terms_of_service.test.js.snap @@ -45,87 +45,6 @@ exports[`TermsOfService should enable/disable navigator buttons on setNavigatorB disableAtMentions={true} disableChannelLink={true} disableHashtags={true} - navigator={ - Object { - "dismissAllModals": [MockFunction], - "dismissModal": [MockFunction], - "setButtons": [MockFunction] { - "calls": Array [ - Array [ - Object { - "leftButtons": Array [ - Object { - "disabled": true, - "icon": Object {}, - "id": "reject-terms-of-service", - }, - ], - "rightButtons": Array [ - Object { - "disabled": true, - "id": "accept-terms-of-service", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - Array [ - Object { - "leftButtons": Array [ - Object { - "disabled": true, - "icon": Object {}, - "id": "reject-terms-of-service", - }, - ], - "rightButtons": Array [ - Object { - "disabled": true, - "id": "accept-terms-of-service", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - Array [ - Object { - "leftButtons": Array [ - Object { - "disabled": false, - "icon": Object {}, - "id": "reject-terms-of-service", - }, - ], - "rightButtons": Array [ - Object { - "disabled": false, - "id": "accept-terms-of-service", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - Object { - "type": "return", - "value": undefined, - }, - Object { - "type": "return", - "value": undefined, - }, - ], - }, - } - } textStyles={ Object { "code": Object { @@ -261,110 +180,6 @@ exports[`TermsOfService should enable/disable navigator buttons on setNavigatorB disableAtMentions={true} disableChannelLink={true} disableHashtags={true} - navigator={ - Object { - "dismissAllModals": [MockFunction], - "dismissModal": [MockFunction], - "setButtons": [MockFunction] { - "calls": Array [ - Array [ - Object { - "leftButtons": Array [ - Object { - "disabled": true, - "icon": Object {}, - "id": "reject-terms-of-service", - }, - ], - "rightButtons": Array [ - Object { - "disabled": true, - "id": "accept-terms-of-service", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - Array [ - Object { - "leftButtons": Array [ - Object { - "disabled": true, - "icon": Object {}, - "id": "reject-terms-of-service", - }, - ], - "rightButtons": Array [ - Object { - "disabled": true, - "id": "accept-terms-of-service", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - Array [ - Object { - "leftButtons": Array [ - Object { - "disabled": false, - "icon": Object {}, - "id": "reject-terms-of-service", - }, - ], - "rightButtons": Array [ - Object { - "disabled": false, - "id": "accept-terms-of-service", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - Array [ - Object { - "leftButtons": Array [ - Object { - "disabled": true, - "icon": Object {}, - "id": "reject-terms-of-service", - }, - ], - "rightButtons": Array [ - Object { - "disabled": true, - "id": "accept-terms-of-service", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - Object { - "type": "return", - "value": undefined, - }, - Object { - "type": "return", - "value": undefined, - }, - Object { - "type": "return", - "value": undefined, - }, - ], - }, - } - } textStyles={ Object { "code": Object { @@ -572,87 +387,6 @@ exports[`TermsOfService should match snapshot on enableNavigatorLogout 1`] = ` disableAtMentions={true} disableChannelLink={true} disableHashtags={true} - navigator={ - Object { - "dismissAllModals": [MockFunction], - "dismissModal": [MockFunction], - "setButtons": [MockFunction] { - "calls": Array [ - Array [ - Object { - "leftButtons": Array [ - Object { - "disabled": true, - "icon": Object {}, - "id": "reject-terms-of-service", - }, - ], - "rightButtons": Array [ - Object { - "disabled": true, - "id": "accept-terms-of-service", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - Array [ - Object { - "leftButtons": Array [ - Object { - "disabled": true, - "icon": Object {}, - "id": "reject-terms-of-service", - }, - ], - "rightButtons": Array [ - Object { - "disabled": true, - "id": "accept-terms-of-service", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - Array [ - Object { - "leftButtons": Array [ - Object { - "disabled": false, - "icon": Object {}, - "id": "close-terms-of-service", - }, - ], - "rightButtons": Array [ - Object { - "disabled": true, - "id": "accept-terms-of-service", - "showAsAction": "always", - "title": undefined, - }, - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - Object { - "type": "return", - "value": undefined, - }, - Object { - "type": "return", - "value": undefined, - }, - ], - }, - } - } textStyles={ Object { "code": Object { diff --git a/app/screens/terms_of_service/index.js b/app/screens/terms_of_service/index.js index a9c843bac..51519d733 100644 --- a/app/screens/terms_of_service/index.js +++ b/app/screens/terms_of_service/index.js @@ -8,6 +8,12 @@ import {getTermsOfService, logout, updateMyTermsOfServiceStatus} from 'mattermos import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import { + setButtons, + dismissModal, + dismissAllModals, +} from 'app/actions/navigation'; + import TermsOfService from './terms_of_service.js'; function mapStateToProps(state) { @@ -25,6 +31,9 @@ function mapDispatchToProps(dispatch) { getTermsOfService, logout, updateMyTermsOfServiceStatus, + setButtons, + dismissModal, + dismissAllModals, }, dispatch), }; } diff --git a/app/screens/terms_of_service/terms_of_service.js b/app/screens/terms_of_service/terms_of_service.js index 850b36e57..14fb41968 100644 --- a/app/screens/terms_of_service/terms_of_service.js +++ b/app/screens/terms_of_service/terms_of_service.js @@ -36,10 +36,12 @@ export default class TermsOfService extends PureComponent { logout: PropTypes.func.isRequired, getTermsOfService: PropTypes.func.isRequired, updateMyTermsOfServiceStatus: PropTypes.func.isRequired, + setButtons: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, + dismissAllModals: PropTypes.func.isRequired, }).isRequired, componentId: PropTypes.string, closeButton: PropTypes.object, - navigator: PropTypes.object, siteName: PropTypes.string, theme: PropTypes.object, }; @@ -71,7 +73,7 @@ export default class TermsOfService extends PureComponent { termsText: '', }; - this.rightButton.title = context.intl.formatMessage({id: 'terms_of_service.agreeButton', defaultMessage: 'I Agree'}); + this.rightButton.text = context.intl.formatMessage({id: 'terms_of_service.agreeButton', defaultMessage: 'I Agree'}); this.leftButton.icon = props.closeButton; this.setNavigatorButtons(false); @@ -106,27 +108,28 @@ export default class TermsOfService extends PureComponent { } setNavigatorButtons = (enabled = true) => { + const {actions, componentId} = this.props; const buttons = { - leftButtons: [{...this.leftButton, disabled: !enabled}], - rightButtons: [{...this.rightButton, disabled: !enabled}], + leftButtons: [{...this.leftButton, enabled}], + rightButtons: [{...this.rightButton, enabled}], }; - this.props.navigator.setButtons(buttons); + actions.setButtons(componentId, buttons); }; enableNavigatorLogout = () => { const buttons = { - leftButtons: [{...this.leftButton, id: 'close-terms-of-service', disabled: false}], - rightButtons: [{...this.rightButton, disabled: true}], + leftButtons: [{...this.leftButton, id: 'close-terms-of-service', enabled: true}], + rightButtons: [{...this.rightButton, enabled: false}], }; - this.props.navigator.setButtons(buttons); + this.props.actions.setButtons(buttons); }; closeTermsAndLogout = () => { const {actions} = this.props; - this.props.navigator.dismissAllModals(); + actions.dismissAllModals(); actions.logout(); }; @@ -163,9 +166,7 @@ export default class TermsOfService extends PureComponent { this.registerUserAction( true, () => { - this.props.navigator.dismissModal({ - animationType: 'slide-down', - }); + this.props.actions.dismissModal(); }, this.handleAcceptTerms ); @@ -234,7 +235,7 @@ export default class TermsOfService extends PureComponent { }; render() { - const {navigator, theme} = this.props; + const {theme} = this.props; const styles = getStyleSheet(theme); const blockStyles = getMarkdownBlockStyles(theme); @@ -267,7 +268,6 @@ export default class TermsOfService extends PureComponent { > { getTermsOfService: jest.fn(), updateMyTermsOfServiceStatus: jest.fn(), logout: jest.fn(), + setButtons: jest.fn(), + dismissModal: jest.fn(), + dismissAllModals: jest.fn(), }; const baseProps = { actions, - navigator: { - dismissAllModals: jest.fn(), - dismissModal: jest.fn(), - setButtons: jest.fn(), - }, theme: Preferences.THEMES.default, closeButton: {}, siteName: 'Mattermost', @@ -83,18 +81,18 @@ describe('TermsOfService', () => { expect(wrapper.getElement()).toMatchSnapshot(); }); - test('should call props.navigator.setButtons on setNavigatorButtons', async () => { + test('should call props.actions.setButtons on setNavigatorButtons', async () => { const wrapper = shallow( , {context: {intl: {formatMessage: jest.fn()}}}, ); wrapper.setState({loading: false, termsId: 1, termsText: 'Terms Text'}); - expect(baseProps.navigator.setButtons).toHaveBeenCalledTimes(2); + expect(baseProps.actions.setButtons).toHaveBeenCalledTimes(2); wrapper.instance().setNavigatorButtons(true); - expect(baseProps.navigator.setButtons).toHaveBeenCalledTimes(3); + expect(baseProps.actions.setButtons).toHaveBeenCalledTimes(3); wrapper.instance().setNavigatorButtons(false); - expect(baseProps.navigator.setButtons).toHaveBeenCalledTimes(4); + expect(baseProps.actions.setButtons).toHaveBeenCalledTimes(4); }); test('should enable/disable navigator buttons on setNavigatorButtons true/false', () => { @@ -129,6 +127,6 @@ describe('TermsOfService', () => { wrapper.setState({loading: false, termsId: 1, termsText: 'Terms Text'}); wrapper.instance().closeTermsAndLogout(); - expect(baseProps.navigator.dismissAllModals).toHaveBeenCalledTimes(1); + expect(baseProps.actions.dismissAllModals).toHaveBeenCalledTimes(1); }); }); diff --git a/app/screens/text_preview/text_preview.js b/app/screens/text_preview/text_preview.js index 68e7b214b..daa637fa7 100644 --- a/app/screens/text_preview/text_preview.js +++ b/app/screens/text_preview/text_preview.js @@ -18,7 +18,6 @@ import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/ut export default class TextPreview extends React.PureComponent { static propTypes = { componentId: PropTypes.string, - navigator: PropTypes.object.isRequired, theme: PropTypes.object.isRequired, content: PropTypes.string.isRequired, }; diff --git a/app/screens/thread/__snapshots__/thread.ios.test.js.snap b/app/screens/thread/__snapshots__/thread.ios.test.js.snap index ebef67766..897c3640e 100644 --- a/app/screens/thread/__snapshots__/thread.ios.test.js.snap +++ b/app/screens/thread/__snapshots__/thread.ios.test.js.snap @@ -13,28 +13,6 @@ exports[`thread should match snapshot, has root post 1`] = ` lastPostIndex={2} lastViewedAt={0} location="thread" - navigator={ - Object { - "dismissModal": [MockFunction], - "pop": [MockFunction], - "resetTo": [MockFunction], - "setTitle": [MockFunction] { - "calls": Array [ - Array [ - Object { - "title": undefined, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - } - } onPostPress={[Function]} postIds={ Array [ @@ -76,28 +54,6 @@ exports[`thread should match snapshot, has root post 1`] = ` channelId="channel_id" channelIsArchived={false} cursorPositionEvent="onThreadTextBoxCursorChange" - navigator={ - Object { - "dismissModal": [MockFunction], - "pop": [MockFunction], - "resetTo": [MockFunction], - "setTitle": [MockFunction] { - "calls": Array [ - Array [ - Object { - "title": undefined, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - } - } onCloseChannel={[Function]} rootId="root_id" valueEvent="onThreadTextBoxValueChange" @@ -128,28 +84,6 @@ exports[`thread should match snapshot, render footer 1`] = ` lastPostIndex={2} lastViewedAt={0} location="thread" - navigator={ - Object { - "dismissModal": [MockFunction], - "pop": [MockFunction], - "resetTo": [MockFunction], - "setTitle": [MockFunction] { - "calls": Array [ - Array [ - Object { - "title": undefined, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - } - } onPostPress={[Function]} postIds={ Array [ @@ -176,28 +110,6 @@ exports[`thread should match snapshot, render footer 2`] = ` lastPostIndex={2} lastViewedAt={0} location="thread" - navigator={ - Object { - "dismissModal": [MockFunction], - "pop": [MockFunction], - "resetTo": [MockFunction], - "setTitle": [MockFunction] { - "calls": Array [ - Array [ - Object { - "title": undefined, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - } - } onPostPress={[Function]} postIds={ Array [ diff --git a/app/screens/thread/index.js b/app/screens/thread/index.js index 7f1573ab0..a089d7755 100644 --- a/app/screens/thread/index.js +++ b/app/screens/thread/index.js @@ -10,6 +10,8 @@ import {selectPost} from 'mattermost-redux/actions/posts'; import {makeGetChannel, getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels'; import {makeGetPostIdsForThread} from 'mattermost-redux/selectors/entities/posts'; +import {popTopScreen, resetToChannel} from 'app/actions/navigation'; + import Thread from './thread'; function makeMapStateToProps() { @@ -37,6 +39,8 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ selectPost, + popTopScreen, + resetToChannel, }, dispatch), }; } diff --git a/app/screens/thread/thread.android.js b/app/screens/thread/thread.android.js index 2184e0d88..849c3a2fa 100644 --- a/app/screens/thread/thread.android.js +++ b/app/screens/thread/thread.android.js @@ -19,7 +19,6 @@ export default class ThreadAndroid extends ThreadBase { const { channelId, myMember, - navigator, postIds, rootId, channelIsArchived, @@ -36,7 +35,6 @@ export default class ThreadAndroid extends ThreadBase { currentUserId={myMember && myMember.user_id} lastViewedAt={this.state.lastViewedAt} lastPostIndex={-1} - navigator={navigator} onPostPress={this.hideKeyboard} location={THREAD} /> @@ -47,7 +45,6 @@ export default class ThreadAndroid extends ThreadBase { channelIsArchived={channelIsArchived} rootId={rootId} channelId={channelId} - navigator={navigator} onCloseChannel={this.onCloseChannel} /> ); diff --git a/app/screens/thread/thread.ios.js b/app/screens/thread/thread.ios.js index 7ba12e368..863717d26 100644 --- a/app/screens/thread/thread.ios.js +++ b/app/screens/thread/thread.ios.js @@ -29,7 +29,6 @@ export default class ThreadIOS extends ThreadBase { const { channelId, myMember, - navigator, postIds, rootId, channelIsArchived, @@ -47,7 +46,6 @@ export default class ThreadIOS extends ThreadBase { lastPostIndex={getLastPostIndex(postIds)} currentUserId={myMember && myMember.user_id} lastViewedAt={this.state.lastViewedAt} - navigator={navigator} onPostPress={this.hideKeyboard} location={THREAD} scrollViewNativeID={SCROLLVIEW_NATIVE_ID} @@ -77,7 +75,6 @@ export default class ThreadIOS extends ThreadBase { channelIsArchived={channelIsArchived} rootId={rootId} channelId={channelId} - navigator={navigator} onCloseChannel={this.onCloseChannel} cursorPositionEvent={THREAD_POST_TEXTBOX_CURSOR_CHANGE} valueEvent={THREAD_POST_TEXTBOX_VALUE_CHANGE} diff --git a/app/screens/thread/thread.ios.test.js b/app/screens/thread/thread.ios.test.js index 309b71088..0478ed22d 100644 --- a/app/screens/thread/thread.ios.test.js +++ b/app/screens/thread/thread.ios.test.js @@ -11,22 +11,22 @@ import PostList from 'app/components/post_list'; import ThreadIOS from './thread.ios'; jest.mock('react-intl'); +jest.mock('react-native-navigation', () => ({ + Navigation: { + mergeOptions: jest.fn(), + }, +})); describe('thread', () => { - const navigator = { - dismissModal: jest.fn(), - pop: jest.fn(), - resetTo: jest.fn(), - setTitle: jest.fn(), - }; const baseProps = { actions: { selectPost: jest.fn(), + popTopScreen: jest.fn(), + resetToChannel: jest.fn(), }, channelId: 'channel_id', channelType: General.OPEN_CHANNEL, displayName: 'channel_display_name', - navigator, myMember: {last_viewed_at: 0, user_id: 'member_user_id'}, rootId: 'root_id', theme: Preferences.THEMES.default, @@ -55,35 +55,19 @@ describe('thread', () => { expect(wrapper.getElement()).toMatchSnapshot(); }); - test('should call props.navigator on onCloseChannel', () => { - const channelScreen = { - screen: 'Channel', - title: '', - animated: false, - backButtonTitle: '', - navigatorStyle: { - animated: true, - animationType: 'fade', - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: 'transparent', - }, - passProps: { - disableTermsModal: true, - }, + test('should call props.actions.resetToChannel on onCloseChannel', () => { + const passProps = { + disableTermsModal: true, }; - const newNavigator = {...navigator}; const wrapper = shallow( , {context: {intl: {formatMessage: jest.fn()}}}, ); wrapper.instance().onCloseChannel(); - expect(newNavigator.resetTo).toHaveBeenCalledTimes(1); - expect(newNavigator.resetTo).toBeCalledWith(channelScreen); + expect(baseProps.actions.resetToChannel).toHaveBeenCalledTimes(1); + expect(baseProps.actions.resetToChannel).toBeCalledWith(passProps); }); test('should match snapshot, render footer', () => { diff --git a/app/screens/thread/thread_base.js b/app/screens/thread/thread_base.js index 468142ebd..0b64e7e32 100644 --- a/app/screens/thread/thread_base.js +++ b/app/screens/thread/thread_base.js @@ -3,8 +3,9 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {Keyboard, Platform} from 'react-native'; +import {Keyboard} from 'react-native'; import {intlShape} from 'react-intl'; +import {Navigation} from 'react-native-navigation'; import {General, RequestStatus} from 'mattermost-redux/constants'; @@ -16,12 +17,13 @@ export default class ThreadBase extends PureComponent { static propTypes = { actions: PropTypes.shape({ selectPost: PropTypes.func.isRequired, + popTopScreen: PropTypes.func.isRequired, + resetToChannel: PropTypes.func.isRequired, }).isRequired, componentId: PropTypes.string, channelId: PropTypes.string.isRequired, channelType: PropTypes.string, displayName: PropTypes.string, - navigator: PropTypes.object, myMember: PropTypes.object.isRequired, rootId: PropTypes.string.isRequired, theme: PropTypes.object.isRequired, @@ -53,8 +55,12 @@ export default class ThreadBase extends PureComponent { this.postTextbox = React.createRef(); - this.props.navigator.setTitle({ - title, + Navigation.mergeOptions(props.componentId, { + topBar: { + title: { + text: title, + }, + }, }); this.state = { @@ -82,17 +88,7 @@ export default class ThreadBase extends PureComponent { } close = () => { - const {navigator} = this.props; - - if (Platform.OS === 'ios') { - navigator.pop({ - animated: true, - }); - } else { - navigator.dismissModal({ - animationType: 'slide-down', - }); - } + this.props.actions.popTopScreen(); }; handleAutoComplete = (value) => { @@ -124,22 +120,9 @@ export default class ThreadBase extends PureComponent { }; onCloseChannel = () => { - this.props.navigator.resetTo({ - screen: 'Channel', - title: '', - animated: false, - backButtonTitle: '', - navigatorStyle: { - animated: true, - animationType: 'fade', - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: 'transparent', - }, - passProps: { - disableTermsModal: true, - }, - }); + const passProps = { + disableTermsModal: true, + }; + this.props.actions.resetToChannel(passProps); }; } diff --git a/app/screens/user_profile/index.js b/app/screens/user_profile/index.js index adeb16750..f4e926c61 100644 --- a/app/screens/user_profile/index.js +++ b/app/screens/user_profile/index.js @@ -15,6 +15,13 @@ import {loadBot} from 'mattermost-redux/actions/bots'; import {getBotAccounts} from 'mattermost-redux/selectors/entities/bots'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import { + setButtons, + dismissModal, + resetToChannel, + goToScreen, +} from 'app/actions/navigation'; + import UserProfile from './user_profile'; function mapStateToProps(state, ownProps) { @@ -43,6 +50,10 @@ function mapDispatchToProps(dispatch) { makeDirectChannel, setChannelDisplayName, loadBot, + setButtons, + dismissModal, + resetToChannel, + goToScreen, }, dispatch), }; } diff --git a/app/screens/user_profile/user_profile.js b/app/screens/user_profile/user_profile.js index 130ffd3a1..848e8332d 100644 --- a/app/screens/user_profile/user_profile.js +++ b/app/screens/user_profile/user_profile.js @@ -33,11 +33,14 @@ export default class UserProfile extends PureComponent { makeDirectChannel: PropTypes.func.isRequired, setChannelDisplayName: PropTypes.func.isRequired, loadBot: PropTypes.func.isRequired, + setButtons: PropTypes.func.isRequired, + dismissModal: PropTypes.func.isRequired, + resetToChannel: PropTypes.func.isRequired, + goToScreen: PropTypes.func.isRequired, }).isRequired, componentId: PropTypes.string, config: PropTypes.object.isRequired, currentDisplayName: PropTypes.string, - navigator: PropTypes.object, teammateNameDisplay: PropTypes.string, theme: PropTypes.object.isRequired, user: PropTypes.object.isRequired, @@ -61,13 +64,13 @@ export default class UserProfile extends PureComponent { super(props); if (props.isMyUser) { - this.rightButton.title = context.intl.formatMessage({id: 'mobile.routes.user_profile.edit', defaultMessage: 'Edit'}); + this.rightButton.text = context.intl.formatMessage({id: 'mobile.routes.user_profile.edit', defaultMessage: 'Edit'}); const buttons = { rightButtons: [this.rightButton], }; - props.navigator.setButtons(buttons); + props.actions.setButtons(props.componentId, buttons); } } @@ -97,30 +100,17 @@ export default class UserProfile extends PureComponent { } close = () => { - const {navigator, theme} = this.props; + const {actions, fromSettings} = this.props; - if (this.props.fromSettings) { - navigator.dismissModal({ - animationType: 'slide-down', - }); + if (fromSettings) { + actions.dismissModal(); return; } - navigator.resetTo({ - screen: 'Channel', - animated: true, - navigatorStyle: { - animated: true, - animationType: 'fade', - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - disableTermsModal: true, - }, - }); + const passProps = { + disableTermsModal: true, + }; + actions.resetToChannel(passProps); }; getDisplayName = () => { @@ -231,27 +221,15 @@ export default class UserProfile extends PureComponent { }; goToEditProfile = () => { - const {user: currentUser} = this.props; + const {actions, user: currentUser} = this.props; const {formatMessage} = this.context.intl; const commandType = 'Push'; - - const {navigator, theme} = this.props; - const options = { - screen: 'EditProfile', - title: formatMessage({id: 'mobile.routes.edit_profile', defaultMessage: 'Edit Profile'}), - animated: true, - backButtonTitle: '', - passProps: {currentUser, commandType}, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - }; + const screen = 'EditProfile'; + const title = formatMessage({id: 'mobile.routes.edit_profile', defaultMessage: 'Edit Profile'}); + const passProps = {currentUser, commandType}; requestAnimationFrame(() => { - navigator.push(options); + actions.goToScreen(screen, title, passProps); }); }; diff --git a/app/screens/user_profile/user_profile.test.js b/app/screens/user_profile/user_profile.test.js index af4e8d903..5c425058f 100644 --- a/app/screens/user_profile/user_profile.test.js +++ b/app/screens/user_profile/user_profile.test.js @@ -22,6 +22,10 @@ describe('user_profile', () => { setChannelDisplayName: jest.fn(), makeDirectChannel: jest.fn(), loadBot: jest.fn(), + setButtons: jest.fn(), + dismissModal: jest.fn(), + resetToChannel: jest.fn(), + goToScreen: jest.fn(), }; const baseProps = { actions, @@ -29,11 +33,6 @@ describe('user_profile', () => { ShowEmailAddress: true, }, teammateNameDisplay: 'username', - navigator: { - resetTo: jest.fn(), - push: jest.fn(), - dismissModal: jest.fn(), - }, teams: [], theme: Preferences.THEMES.default, enableTimezone: false, @@ -90,16 +89,9 @@ describe('user_profile', () => { }); test('should push EditProfile', async () => { - const props = { - ...baseProps, - navigator: { - push: jest.fn(), - }, - }; - const wrapper = shallow( , {context: {intl: {formatMessage: jest.fn()}}}, @@ -107,18 +99,18 @@ describe('user_profile', () => { wrapper.instance().goToEditProfile(); setTimeout(() => { - expect(props.navigator.push).toHaveBeenCalledTimes(1); + expect(baseProps.actions.goToScreen).toHaveBeenCalledTimes(1); }, 16); }); test('should call goToEditProfile', () => { const props = { ...baseProps, - navigator: { - push: jest.fn(), + actions: { + ...baseProps.actions, + goToScreen: jest.fn(), }, }; - const wrapper = shallow( { const event = {buttonId: wrapper.instance().rightButton.id}; wrapper.instance().navigationButtonPressed(event); setTimeout(() => { - expect(props.navigator.push).toHaveBeenCalledTimes(1); + expect(baseProps.actions.goToScreen).toHaveBeenCalledTimes(1); }, 0); }); @@ -147,11 +139,11 @@ describe('user_profile', () => { const event = {buttonId: 'close-settings'}; wrapper.instance().navigationButtonPressed(event); - expect(props.navigator.dismissModal).toHaveBeenCalledTimes(1); + expect(props.actions.dismissModal).toHaveBeenCalledTimes(1); props.fromSettings = false; wrapper.setProps({...props}); wrapper.instance().navigationButtonPressed(event); - expect(props.navigator.resetTo).toHaveBeenCalledTimes(1); + expect(props.actions.resetToChannel).toHaveBeenCalledTimes(1); }); }); diff --git a/app/utils/push_notifications.js b/app/utils/push_notifications.js index 97a46032e..0214bbdd8 100644 --- a/app/utils/push_notifications.js +++ b/app/utils/push_notifications.js @@ -15,6 +15,7 @@ import { createPostForNotificationReply, loadFromPushNotification, } from 'app/actions/views/root'; +import {dismissAllModals, popToRoot} from 'app/actions/navigation'; import {ViewTypes} from 'app/constants'; import {getLocalizedMessage} from 'app/i18n'; import {getCurrentServerUrl, getAppCredentials} from 'app/init/credentials'; @@ -45,7 +46,13 @@ class PushNotificationUtils { await this.store.dispatch(loadFromPushNotification(notification, true)); if (!EphemeralStore.appStartedFromPushNotification) { - EventEmitter.emit(ViewTypes.NOTIFICATION_TAPPED); + EventEmitter.emit('close_channel_drawer'); + EventEmitter.emit('close_settings_sidebar'); + + const {dispatch} = this.store; + dispatch(dismissAllModals()); + dispatch(popToRoot()); + PushNotifications.resetNotification(); } }; diff --git a/app/utils/wrap_context_provider.js b/app/utils/wrap_context_provider.js index 015d9378a..cea58cabb 100644 --- a/app/utils/wrap_context_provider.js +++ b/app/utils/wrap_context_provider.js @@ -6,10 +6,8 @@ import IntlWrapper from 'app/components/root'; export function wrapWithContextProvider(Comp, excludeEvents = true) { return (props) => { //eslint-disable-line react/display-name - const {navigator} = props; //eslint-disable-line react/prop-types return ( diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0830f40fa..de555aa2b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -13,4 +13,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 63cb0d0aef536c8cbf15bf664fcfe31852a13dd1 -COCOAPODS: 1.6.1 +COCOAPODS: 1.5.3