diff --git a/app/actions/views/file_upload.js b/app/actions/views/file_upload.js index 4a637ee82..9fd01f370 100644 --- a/app/actions/views/file_upload.js +++ b/app/actions/views/file_upload.js @@ -68,3 +68,11 @@ export function handleRemoveFile(clientId, channelId, rootId) { rootId }; } + +export function handleRemoveLastFile(channelId, rootId) { + return { + type: ViewTypes.REMOVE_LAST_FILE_FROM_POST_DRAFT, + channelId, + rootId + }; +} diff --git a/app/actions/views/more_dms.js b/app/actions/views/more_dms.js index 753aa416d..68dfd90f1 100644 --- a/app/actions/views/more_dms.js +++ b/app/actions/views/more_dms.js @@ -3,7 +3,7 @@ import {getDirectChannelName} from 'mattermost-redux/utils/channel_utils'; import {createDirectChannel} from 'mattermost-redux/actions/channels'; -import {getTeamMember} from 'mattermost-redux/actions/teams'; +import {getProfilesByIds, getStatusesByIds} from 'mattermost-redux/actions/users'; import {handleSelectChannel, toggleDMChannel} from 'app/actions/views/channel'; export function makeDirectChannel(otherUserId) { @@ -15,7 +15,8 @@ export function makeDirectChannel(otherUserId) { const channel = Object.values(channels).find((c) => c.name === channelName); const {currentTeamId} = state.entities.teams; - await getTeamMember(currentTeamId, otherUserId)(dispatch, getState); + getProfilesByIds([otherUserId])(dispatch, getState); + getStatusesByIds([otherUserId])(dispatch, getState); if (channel && myMembers[channel.id]) { await toggleDMChannel(otherUserId, 'true')(dispatch, getState); diff --git a/app/components/post_textbox/post_textbox.js b/app/components/post_textbox/post_textbox.js index 1c79bbebc..2724aea02 100644 --- a/app/components/post_textbox/post_textbox.js +++ b/app/components/post_textbox/post_textbox.js @@ -3,6 +3,9 @@ import React, {PropTypes, PureComponent} from 'react'; import { + BackAndroid, + Keyboard, + Platform, StyleSheet, TouchableHighlight, View, @@ -35,6 +38,7 @@ export default class PostTextbox extends PureComponent { actions: PropTypes.shape({ closeModal: PropTypes.func.isRequired, createPost: PropTypes.func.isRequired, + handleRemoveLastFile: PropTypes.func.isRequired, handleUploadFiles: PropTypes.func.isRequired, showOptionsModal: PropTypes.func.isRequired, userTyping: PropTypes.func.isRequired @@ -56,6 +60,20 @@ export default class PostTextbox extends PureComponent { }; } + componentDidMount() { + if (Platform.OS === 'android') { + Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard); + BackAndroid.addEventListener('hardwareBackPress', this.handleAndroidBack); + } + } + + componentWillUnmount() { + if (Platform.OS === 'android') { + Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard); + BackAndroid.removeEventListener('hardwareBackPress', this.handleAndroidBack); + } + } + blur = () => { this.refs.input.getWrappedInstance().blur(); }; @@ -66,6 +84,19 @@ export default class PostTextbox extends PureComponent { }); }; + handleAndroidKeyboard = () => { + this.blur(); + }; + + handleAndroidBack = () => { + const {channelId, files, rootId} = this.props; + if (files.length) { + this.props.actions.handleRemoveLastFile(channelId, rootId); + return true; + } + return false; + }; + sendMessage = () => { if ((this.props.value.trim().length === 0 && this.props.files.length === 0) || this.props.uploadFileRequestStatus === RequestStatus.STARTED) { return; diff --git a/app/components/post_textbox/post_textbox_container.js b/app/components/post_textbox/post_textbox_container.js index 18a2f1e55..1c7ad47f5 100644 --- a/app/components/post_textbox/post_textbox_container.js +++ b/app/components/post_textbox/post_textbox_container.js @@ -7,7 +7,7 @@ import {createPost} from 'mattermost-redux/actions/posts'; import {userTyping} from 'mattermost-redux/actions/websocket'; import {showOptionsModal, requestCloseModal} from 'app/actions/navigation'; -import {handleUploadFiles} from 'app/actions/views/file_upload'; +import {handleRemoveLastFile, handleUploadFiles} from 'app/actions/views/file_upload'; import {getTheme} from 'app/selectors/preferences'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {getUsersTyping} from 'mattermost-redux/selectors/entities/typing'; @@ -29,6 +29,7 @@ function mapDispatchToProps(dispatch) { actions: bindActionCreators({ createPost, closeModal: requestCloseModal, + handleRemoveLastFile, handleUploadFiles, showOptionsModal, userTyping diff --git a/app/components/root/root.js b/app/components/root/root.js index 8760f4c4e..f5e3ce1cb 100644 --- a/app/components/root/root.js +++ b/app/components/root/root.js @@ -2,7 +2,16 @@ // See License.txt for license information. import React, {Component, PropTypes} from 'react'; -import {Alert, AppState, AsyncStorage, InteractionManager, StatusBar, View} from 'react-native'; +import { + Alert, + AppState, + AsyncStorage, + BackAndroid, + InteractionManager, + Platform, + StatusBar, + View +} from 'react-native'; import {IntlProvider} from 'react-intl'; import DeviceInfo from 'react-native-device-info'; import semver from 'semver'; @@ -22,8 +31,11 @@ export default class Root extends Component { currentTeamId: PropTypes.string, currentChannelId: PropTypes.string, locale: PropTypes.string.isRequired, + navigation: PropTypes.object.isRequired, actions: PropTypes.shape({ closeDrawers: PropTypes.func.isRequired, + closeModal: PropTypes.func.isRequired, + goBack: PropTypes.func.isRequired, loadConfigAndLicense: PropTypes.func.isRequired, logout: PropTypes.func.isRequired, setAppState: PropTypes.func.isRequired, @@ -44,11 +56,19 @@ export default class Root extends Component { AppState.addEventListener('change', this.handleAppStateChange); EventEmitter.on(Constants.CONFIG_CHANGED, this.handleConfigChanged); Client.setUserAgent(DeviceInfo.getUserAgent()); + + if (Platform.OS === 'android') { + BackAndroid.addEventListener('hardwareBackPress', this.handleAndroidBack); + } } componentWillUnmount() { AppState.removeEventListener('change', this.handleAppStateChange); EventEmitter.off(Constants.CONFIG_CHANGED, this.handleConfigChanged); + + if (Platform.OS === 'android') { + BackAndroid.removeEventListener('hardwareBackPress', this.handleAndroidBack); + } } handleAppStateChange(appState) { @@ -59,6 +79,30 @@ export default class Root extends Component { } } + handleAndroidBack = () => { + const {index, isModal, leftDrawerOpen, modal} = this.props.navigation; + const {closeDrawers, closeModal, goBack} = this.props.actions; + + if (isModal) { + if (modal.index > 0) { + goBack(); + return true; + } + closeModal(); + return true; + } + + if (leftDrawerOpen) { + closeDrawers(); + return true; + } else if (index > 0) { + goBack(); + return true; + } + + return false; + }; + handleVersionUpgrade = async () => { const {closeDrawers, logout, unrenderDrawer} = this.props.actions; diff --git a/app/components/root/root_container.js b/app/components/root/root_container.js index 1b7c54459..83c440a3e 100644 --- a/app/components/root/root_container.js +++ b/app/components/root/root_container.js @@ -6,7 +6,7 @@ import {connect} from 'react-redux'; import Config from 'assets/config.json'; -import {closeDrawers, unrenderDrawer} from 'app/actions/navigation'; +import {closeDrawers, closeModal, goBack, unrenderDrawer} from 'app/actions/navigation'; import {flushToStorage} from 'app/actions/storage'; import {goToNotification, loadConfigAndLicense, queueNotification} from 'app/actions/views/root'; import {setAppState, setDeviceToken} from 'mattermost-redux/actions/general'; @@ -29,7 +29,8 @@ function mapStateToProps(state, ownProps) { ...ownProps, currentTeamId, currentChannelId, - locale + locale, + navigation: state.navigation }; } @@ -37,6 +38,8 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ closeDrawers, + closeModal, + goBack, loadConfigAndLicense, logout, goToNotification, diff --git a/app/navigation/router.js b/app/navigation/router.js index ad645d812..228124c2a 100644 --- a/app/navigation/router.js +++ b/app/navigation/router.js @@ -293,6 +293,9 @@ class Router extends Component { useInteractionManager={false} tweenHandler={this.handleDrawerTween} elevation={-5} + styles={{ + main: {shadowColor: '#000000', shadowOpacity: 0.8, shadowRadius: 5} + }} > (file.clientId !== action.clientId)); + + return { + ...state, + [action.rootId]: Object.assign({}, state[action.rootId], {files}) + }; + } + case ViewTypes.REMOVE_LAST_FILE_FROM_POST_DRAFT: { + if (!action.rootId) { + return state; + } + + const files = [...state[action.rootId].files]; + files.splice(-1); + + return { + ...state, + [action.rootId]: Object.assign({}, state[action.rootId], {files}) + }; + } default: return state; } diff --git a/app/scenes/account_notifications/account_notifications.js b/app/scenes/account_notifications/account_notifications.js index 7718b7feb..e7809851b 100644 --- a/app/scenes/account_notifications/account_notifications.js +++ b/app/scenes/account_notifications/account_notifications.js @@ -3,6 +3,8 @@ import React, {PropTypes, PureComponent} from 'react'; import { + Keyboard, + Platform, ScrollView, StyleSheet, View @@ -81,8 +83,18 @@ export default class AccountNotifications extends PureComponent { this.props.subscribeToHeaderEvent(SAVE_NOTIFY_PROPS, this.saveUserNotifyProps); } + componentDidMount() { + if (Platform.OS === 'android') { + Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard); + } + } + componentWillUnmount() { this.props.unsubscribeFromHeaderEvent(SAVE_NOTIFY_PROPS); + + if (Platform.OS === 'android') { + Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard); + } } componentWillReceiveProps(nextProps) { @@ -99,6 +111,10 @@ export default class AccountNotifications extends PureComponent { } } + handleAndroidKeyboard = () => { + this.refs.mention_keys.getWrappedInstance().blur(); + }; + setStateFromNotifyProps = (notifyProps) => { const mentionKeys = (notifyProps.mention_keys || '').split(','); const usernameMentionIndex = mentionKeys.indexOf(this.props.currentUser.username); @@ -262,6 +278,7 @@ export default class AccountNotifications extends PureComponent { theme={theme} > + ); + } + return ( {channelName} - + {icon} ); diff --git a/app/scenes/login/login.js b/app/scenes/login/login.js index 372cf8c93..20cf62451 100644 --- a/app/scenes/login/login.js +++ b/app/scenes/login/login.js @@ -6,6 +6,7 @@ import {injectIntl, intlShape} from 'react-intl'; import { Image, Keyboard, + Platform, Text, TextInput, TouchableWithoutFeedback, @@ -50,12 +51,28 @@ class Login extends Component { }; } + componentDidMount() { + if (Platform.OS === 'android') { + Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard); + } + } + componentWillReceiveProps(nextProps) { if (this.props.loginRequest.status === RequestStatus.STARTED && nextProps.loginRequest.status === RequestStatus.SUCCESS) { this.props.actions.handleSuccessfulLogin().then(this.props.actions.goToLoadTeam); } } + componentWillUnmount() { + if (Platform.OS === 'android') { + Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard); + } + } + + handleAndroidKeyboard = () => { + this.blur(); + }; + blur = () => { this.loginId.blur(); this.passwd.blur(); diff --git a/app/scenes/mfa/mfa.js b/app/scenes/mfa/mfa.js index e4f3ec522..c40be86cb 100644 --- a/app/scenes/mfa/mfa.js +++ b/app/scenes/mfa/mfa.js @@ -5,6 +5,7 @@ import React, {Component} from 'react'; import { Image, Keyboard, + Platform, TouchableWithoutFeedback, View } from 'react-native'; @@ -40,6 +41,12 @@ export default class Mfa extends Component { }; } + componentDidMount() { + if (Platform.OS === 'android') { + Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard); + } + } + componentWillReceiveProps(nextProps) { // In case the login is successful the previous scene (login) will take care of the transition if (this.props.loginRequest.status === RequestStatus.STARTED && @@ -48,6 +55,16 @@ export default class Mfa extends Component { } } + componentWillUnmount() { + if (Platform.OS === 'android') { + Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard); + } + } + + handleAndroidKeyboard = () => { + this.blur(); + }; + handleInput = (token) => { this.setState({ token, diff --git a/app/scenes/more_channels/more_channels.js b/app/scenes/more_channels/more_channels.js index 61de31764..7177bfd8d 100644 --- a/app/scenes/more_channels/more_channels.js +++ b/app/scenes/more_channels/more_channels.js @@ -4,6 +4,8 @@ import React, {PropTypes, PureComponent} from 'react'; import {injectIntl, intlShape} from 'react-intl'; import { + Keyboard, + Platform, InteractionManager, StyleSheet, TouchableOpacity, @@ -95,6 +97,10 @@ class MoreChannels extends PureComponent { } componentDidMount() { + if (Platform.OS === 'android') { + Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard); + } + InteractionManager.runAfterInteractions(() => { this.props.actions.getMoreChannels(this.props.currentTeamId, 0); }); @@ -103,6 +109,10 @@ class MoreChannels extends PureComponent { componentWillUnmount() { this.props.unsubscribeFromHeaderEvent('close'); this.props.unsubscribeFromHeaderEvent('new_channel'); + + if (Platform.OS === 'android') { + Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard); + } } filterChannels = (channels, term) => { @@ -111,6 +121,10 @@ class MoreChannels extends PureComponent { }); }; + handleAndroidKeyboard = () => { + this.onSearchButtonPress(); + }; + searchBarRef = (ref) => { this.searchBar = ref; }; diff --git a/app/scenes/more_dms/more_dms.js b/app/scenes/more_dms/more_dms.js index baa771cb6..c875c63ea 100644 --- a/app/scenes/more_dms/more_dms.js +++ b/app/scenes/more_dms/more_dms.js @@ -4,6 +4,8 @@ import React, {PropTypes, PureComponent} from 'react'; import {injectIntl, intlShape} from 'react-intl'; import { + Keyboard, + Platform, InteractionManager, StyleSheet, TouchableOpacity, @@ -86,6 +88,10 @@ class MoreDirectMessages extends PureComponent { } componentDidMount() { + if (Platform.OS === 'android') { + Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard); + } + InteractionManager.runAfterInteractions(() => { this.props.actions.getProfiles(0); }); @@ -93,8 +99,16 @@ class MoreDirectMessages extends PureComponent { componentWillUnmount() { this.props.unsubscribeFromHeaderEvent('close'); + + if (Platform.OS === 'android') { + Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard); + } } + handleAndroidKeyboard = () => { + this.onSearchButtonPress(); + }; + searchBarRef = (ref) => { this.searchBar = ref; }; diff --git a/app/scenes/select_server/select_server.js b/app/scenes/select_server/select_server.js index 009818f19..cac01f0c8 100644 --- a/app/scenes/select_server/select_server.js +++ b/app/scenes/select_server/select_server.js @@ -5,6 +5,7 @@ import React, {PropTypes, PureComponent} from 'react'; import { Image, Keyboard, + Platform, StatusBar, TouchableWithoutFeedback, View @@ -50,6 +51,9 @@ export default class SelectServer extends PureComponent { componentDidMount() { this.props.actions.unrenderDrawer(); + if (Platform.OS === 'android') { + Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard); + } } componentWillReceiveProps(nextProps) { @@ -60,6 +64,16 @@ export default class SelectServer extends PureComponent { } } + componentWillUnmount() { + if (Platform.OS === 'android') { + Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard); + } + } + + handleAndroidKeyboard = () => { + this.blur(); + }; + onClick = async () => { const url = this.props.serverUrl; let error = null; diff --git a/package.json b/package.json index 75f8f59e2..8902d7763 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "harmony-reflect": "1.5.1", "intl": "1.2.5", "isomorphic-fetch": "2.2.1", - "mattermost-redux": "mattermost/mattermost-redux#c0451f9fe8a6df3330c4b5d415a208e85ed08cba", + "mattermost-redux": "mattermost/mattermost-redux#release-3.7", "react": "15.4.1", "react-addons-pure-render-mixin": "15.4.1", "react-intl": "2.2.2",