diff --git a/NOTICE.txt b/NOTICE.txt index 43e1be967..18573549a 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1090,3 +1090,36 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --- + +## react-native-status-bar-size + +This product contains 'react-native-status-bar-size', Watch and respond to changes in the iOS status bar height. + +* HOMEPAGE: + * https://github.com/jgkim/react-native-status-bar-size + +* LICENSE: + +The MIT License (MIT) + +Copyright (c) 2015 Brent Vatne + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- diff --git a/android/app/src/main/java/com/mattermost/CustomPushNotification.java b/android/app/src/main/java/com/mattermost/CustomPushNotification.java index 455afe960..a92fbe716 100644 --- a/android/app/src/main/java/com/mattermost/CustomPushNotification.java +++ b/android/app/src/main/java/com/mattermost/CustomPushNotification.java @@ -120,12 +120,15 @@ public class CustomPushNotification extends PushNotification { notification .setContentTitle(title) .setContentText(message) - .setGroup(GROUP_KEY_MESSAGES) .setGroupSummary(true) .setSmallIcon(smallIconResId) .setVisibility(Notification.VISIBILITY_PRIVATE) .setPriority(Notification.PRIORITY_HIGH); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + notification.setGroup(GROUP_KEY_MESSAGES); + } + if (numMessages > 1) { notification.setNumber(numMessages); } diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index 5280d9b5e..9099dd224 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -194,7 +194,7 @@ export function selectInitialChannel(teamId) { // Handle case when the default channel cannot be found // so we need to get the first available channel of the team const channelsInTeam = Object.values(channels).filter((c) => c.team_id === teamId); - const firstChannel = channelsInTeam[0].id; + const firstChannel = channelsInTeam.length ? channelsInTeam[0].id : {id: ''}; dispatch(setChannelDisplayName('')); await handleSelectChannel(firstChannel.id)(dispatch, getState); } diff --git a/app/actions/views/root.js b/app/actions/views/root.js index dd5c6ed65..7df664992 100644 --- a/app/actions/views/root.js +++ b/app/actions/views/root.js @@ -64,9 +64,17 @@ export function goToNotification(notification) { }; } +export function setStatusBarHeight(height = 20) { + return { + type: ViewTypes.STATUSBAR_HEIGHT_CHANGED, + data: height + }; +} + export default { loadConfigAndLicense, queueNotification, clearNotification, - goToNotification + goToNotification, + setStatusBarHeight }; diff --git a/app/components/autocomplete/at_mention/index.js b/app/components/autocomplete/at_mention/index.js index fc275ade9..aae27aa18 100644 --- a/app/components/autocomplete/at_mention/index.js +++ b/app/components/autocomplete/at_mention/index.js @@ -16,9 +16,15 @@ function mapStateToProps(state, ownProps) { let postDraft; if (ownProps.rootId.length) { - postDraft = state.views.thread.drafts[ownProps.rootId].draft; + const threadDraft = state.views.thread.drafts[ownProps.rootId]; + if (threadDraft) { + postDraft = threadDraft.draft; + } } else if (currentChannelId) { - postDraft = state.views.channel.drafts[currentChannelId].draft; + const channelDraft = state.views.channel.drafts[currentChannelId]; + if (channelDraft) { + postDraft = channelDraft.draft; + } } return { diff --git a/app/components/autocomplete/channel_mention/index.js b/app/components/autocomplete/channel_mention/index.js index e6c7852c8..d0b2614bd 100644 --- a/app/components/autocomplete/channel_mention/index.js +++ b/app/components/autocomplete/channel_mention/index.js @@ -17,9 +17,15 @@ function mapStateToProps(state, ownProps) { let postDraft; if (ownProps.rootId.length) { - postDraft = state.views.thread.drafts[ownProps.rootId].draft; + const threadDraft = state.views.thread.drafts[ownProps.rootId]; + if (threadDraft) { + postDraft = threadDraft.draft; + } } else if (currentChannelId) { - postDraft = state.views.channel.drafts[currentChannelId].draft; + const channelDraft = state.views.channel.drafts[currentChannelId]; + if (channelDraft) { + postDraft = channelDraft.draft; + } } const autocompleteChannels = { diff --git a/app/components/channel_drawer/index.js b/app/components/channel_drawer/index.js index a3c48623e..dfaa39a4b 100644 --- a/app/components/channel_drawer/index.js +++ b/app/components/channel_drawer/index.js @@ -20,8 +20,8 @@ function mapStateToProps(state, ownProps) { return { ...ownProps, - currentTeam: getCurrentTeam(state), - currentChannel: getCurrentChannel(state), + currentTeam: getCurrentTeam(state) || {}, + currentChannel: getCurrentChannel(state) || {}, currentDisplayName: state.views.channel.displayName, currentUserId, channels: getChannelsWithUnreadSection(state), diff --git a/app/components/channel_intro/channel_intro.js b/app/components/channel_intro/channel_intro.js index 28b050d4e..6fabbaa9e 100644 --- a/app/components/channel_intro/channel_intro.js +++ b/app/components/channel_intro/channel_intro.js @@ -101,18 +101,23 @@ class ChannelIntro extends PureComponent { buildDMContent = () => { const {currentChannelMembers, intl, theme} = this.props; const style = getStyleSheet(theme); - const teammate = this.getDisplayName(currentChannelMembers[0]); - return ( - - {intl.formatMessage({ - id: 'mobile.intro_messages.DM', - defaultMessage: 'This is the start of your direct message history with {teammate}. Direct messages and files shared here are not shown to people outside this area.' - }, { - teammate - })} - - ); + if (currentChannelMembers.length) { + const teammate = this.getDisplayName(currentChannelMembers[0]); + + return ( + + {intl.formatMessage({ + id: 'mobile.intro_messages.DM', + defaultMessage: 'This is the start of your direct message history with {teammate}. Direct messages and files shared here are not shown to people outside this area.' + }, { + teammate + })} + + ); + } + + return null; }; buildGMContent = () => { diff --git a/app/components/channel_intro/index.js b/app/components/channel_intro/index.js index bc872e8f5..a58a523de 100644 --- a/app/components/channel_intro/index.js +++ b/app/components/channel_intro/index.js @@ -12,15 +12,18 @@ import {getTheme} from 'app/selectors/preferences'; import ChannelIntro from './channel_intro'; function mapStateToProps(state) { - const currentChannel = getCurrentChannel(state); - const currentUser = getCurrentUser(state); + const currentChannel = getCurrentChannel(state) || {}; + const currentUser = getCurrentUser(state) || {}; let currentChannelMembers = []; if (currentChannel.type === General.DM_CHANNEL) { const otherChannelMember = currentChannel.name.split('__').find((m) => m !== currentUser.id); - currentChannelMembers.push(state.entities.users.profiles[otherChannelMember]); + const otherProfile = state.entities.users.profiles[otherChannelMember]; + if (otherProfile) { + currentChannelMembers.push(otherProfile); + } } else { - currentChannelMembers = getProfilesInCurrentChannel(state); + currentChannelMembers = getProfilesInCurrentChannel(state) || []; currentChannelMembers = currentChannelMembers.filter((m) => m.id !== currentUser.id); } diff --git a/app/components/file_attachment_list/file_attachment_icon.js b/app/components/file_attachment_list/file_attachment_icon.js index 5bfce7b1f..33949ac09 100644 --- a/app/components/file_attachment_list/file_attachment_icon.js +++ b/app/components/file_attachment_list/file_attachment_icon.js @@ -1,7 +1,7 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React, {Component} from 'react'; +import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import { View, @@ -35,7 +35,7 @@ const ICON_PATH_FROM_FILE_TYPE = { word: wordIcon }; -export default class FileAttachmentIcon extends Component { +export default class FileAttachmentIcon extends PureComponent { static propTypes = { file: PropTypes.object.isRequired, iconHeight: PropTypes.number, @@ -49,7 +49,7 @@ export default class FileAttachmentIcon extends Component { iconWidth: 60, wrapperHeight: 100, wrapperWidth: 100 - } + }; getFileIconPath(file) { const fileType = Utils.getFileType(file); diff --git a/app/constants/view.js b/app/constants/view.js index a9fd2f025..17a132921 100644 --- a/app/constants/view.js +++ b/app/constants/view.js @@ -47,7 +47,9 @@ const ViewTypes = keyMirror({ INCREASE_POST_VISIBILITY: null, RECEIVED_FOCUSED_POST: null, - LOADING_POSTS: null + LOADING_POSTS: null, + + STATUSBAR_HEIGHT_CHANGED: null }); export default { diff --git a/app/mattermost.js b/app/mattermost.js index 9d8645c28..3fbdca34e 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -10,21 +10,24 @@ import { Alert, AppState, InteractionManager, + NativeModules, Platform } from 'react-native'; import DeviceInfo from 'react-native-device-info'; import {setJSExceptionHandler} from 'react-native-exception-handler'; +import StatusBarSizeIOS from 'react-native-status-bar-size'; import semver from 'semver'; import {setAppState, setDeviceToken, setServerVersion} from 'mattermost-redux/actions/general'; import {markChannelAsRead} from 'mattermost-redux/actions/channels'; import {logError} from 'mattermost-redux/actions/errors'; import {logout} from 'mattermost-redux/actions/users'; +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 {goToNotification, loadConfigAndLicense, queueNotification} from 'app/actions/views/root'; +import {goToNotification, loadConfigAndLicense, queueNotification, setStatusBarHeight} from 'app/actions/views/root'; import {setChannelDisplayName} from 'app/actions/views/channel'; import {NavigationTypes, ViewTypes} from 'app/constants'; import {getTranslations} from 'app/i18n'; @@ -35,6 +38,7 @@ import configureStore from 'app/store'; import Config from 'assets/config'; +const {StatusBarManager} = NativeModules; const store = configureStore(initialState); registerScreens(store, Provider); @@ -52,10 +56,20 @@ export default class Mattermost { this.handleAppStateChange(AppState.currentState); Client4.setUserAgent(DeviceInfo.getUserAgent()); + + if (Platform.OS === 'ios') { + StatusBarSizeIOS.addEventListener('willChange', this.handleStatusBarHeightChange); + StatusBarManager.getHeight( + (data) => { + this.handleStatusBarHeightChange(data.height); + } + ); + } } errorHandler = (e, isFatal) => { const intl = this.getIntl(); + closeWebSocket()(store.dispatch, store.getState); logError(e)(store.dispatch); if (isFatal) { @@ -126,6 +140,10 @@ export default class Mattermost { store.dispatch(setChannelDisplayName(displayName)); }; + handleStatusBarHeightChange = (nextStatusBarHeight) => { + store.dispatch(setStatusBarHeight(nextStatusBarHeight)); + }; + handleVersionUpgrade = async () => { const {dispatch, getState} = store; diff --git a/app/reducers/views/root.js b/app/reducers/views/root.js index bd24bdba7..c07316acd 100644 --- a/app/reducers/views/root.js +++ b/app/reducers/views/root.js @@ -37,8 +37,18 @@ function purge(state = false, action) { } } +function statusBarHeight(state = 20, action) { + switch (action.type) { + case ViewTypes.STATUSBAR_HEIGHT_CHANGED: + return action.data; + default: + return state; + } +} + export default combineReducers({ appInitializing, hydrationComplete, - purge + purge, + statusBarHeight }); diff --git a/app/screens/account_notifications/account_notifications.js b/app/screens/account_notifications/account_notifications.js index 73e7cf568..ac37f328b 100644 --- a/app/screens/account_notifications/account_notifications.js +++ b/app/screens/account_notifications/account_notifications.js @@ -161,6 +161,10 @@ class AccountNotifications extends PureComponent { }); }; + focusMentionKeys = () => { + this.refs.mention_keys.getWrappedInstance().focus(); + }; + updateMentionKeys = (text) => { this.setState({ mention_keys: text @@ -280,6 +284,7 @@ class AccountNotifications extends PureComponent { labelId='user.settings.notifications.sensitiveWords' labelDefaultMessage='Other non-case sensitive words, separated by commas' theme={theme} + action={this.focusMentionKeys} > true, actionType: ActionTypes.DEFAULT }; diff --git a/app/screens/channel/channel.js b/app/screens/channel/channel.js index ce16bb1d2..10dfdc2cc 100644 --- a/app/screens/channel/channel.js +++ b/app/screens/channel/channel.js @@ -20,6 +20,7 @@ import KeyboardLayout from 'app/components/layout/keyboard_layout'; import Loading from 'app/components/loading'; import OfflineIndicator from 'app/components/offline_indicator'; import PostTextbox from 'app/components/post_textbox'; +import PostListRetry from 'app/components/post_list_retry'; import StatusBar from 'app/components/status_bar'; import {preventDoubleTap} from 'app/utils/tap'; import {makeStyleSheetFromTheme} from 'app/utils/theme'; @@ -51,7 +52,9 @@ class Channel extends PureComponent { theme: PropTypes.object.isRequired, subscribeToHeaderEvent: PropTypes.func, unsubscribeFromHeaderEvent: PropTypes.func, - webSocketRequest: PropTypes.object + webSocketRequest: PropTypes.object, + statusBarHeight: PropTypes.number, + channelsRequestStatus: PropTypes.string }; componentWillMount() { @@ -163,18 +166,32 @@ class Channel extends PureComponent { }); }; + retryLoadChannels = () => { + this.loadChannels(this.props.currentTeam.id); + }; + render() { const { - currentTeam, + channelsRequestStatus, currentChannel, + drafts, intl, navigator, + statusBarHeight, theme } = this.props; const style = getStyleFromTheme(theme); - if (!currentTeam || !currentChannel) { + if (!currentChannel.hasOwnProperty('id')) { + if (channelsRequestStatus === RequestStatus.FAILURE) { + return ( + + ); + } return ( @@ -182,7 +199,12 @@ class Channel extends PureComponent { ); } - const channelDraft = this.props.drafts[currentChannel.id] || {}; + const channelDraft = drafts[currentChannel.id] || {}; + + let height = 0; + if (statusBarHeight > 20) { + height = statusBarHeight - 20; + } return ( { }, keyboardLayout: { backgroundColor: theme.centerChannelBg, - flex: 1 + flex: 1, + paddingBottom: 0 } }); }); diff --git a/app/screens/channel/channel_post_list/channel_post_list.js b/app/screens/channel/channel_post_list/channel_post_list.js index 57bbd55af..ed8a03ce3 100644 --- a/app/screens/channel/channel_post_list/channel_post_list.js +++ b/app/screens/channel/channel_post_list/channel_post_list.js @@ -55,6 +55,7 @@ class ChannelPostList extends PureComponent { } componentDidMount() { + this.mounted = true; this.loadPosts(this.props.channel.id); } @@ -71,17 +72,25 @@ class ChannelPostList extends PureComponent { this.loadPosts(nextProps.channel.id); } - const showLoadMore = nextProps.posts.length === this.props.posts.length && nextProps.posts.length > nextProps.postVisibility; + const showLoadMore = nextProps.posts.length >= nextProps.postVisibility; this.setState({ showLoadMore }); } + componentWillUnmount() { + this.mounted = false; + } + channelLoaded = () => { Animated.timing(this.state.loaderOpacity, { toValue: 0, duration: 500 - }).start(() => this.setState({channelLoaded: true})); + }).start(() => { + if (this.mounted) { + this.setState({channelLoaded: true}); + } + }); }; goToThread = (post) => { diff --git a/app/screens/channel/channel_title.js b/app/screens/channel/channel_title.js index 7ada87cc2..4f244405e 100644 --- a/app/screens/channel/channel_title.js +++ b/app/screens/channel/channel_title.js @@ -72,7 +72,7 @@ ChannelTitle.defaultProps = { function mapStateToProps(state) { return { applicationInitializing: state.views.root.appInitializing, - currentChannel: getCurrentChannel(state), + currentChannel: getCurrentChannel(state) || {}, displayName: state.views.channel.displayName, theme: getTheme(state) }; diff --git a/app/screens/channel/index.js b/app/screens/channel/index.js index 3eda7688b..1fd49b2c6 100644 --- a/app/screens/channel/index.js +++ b/app/screens/channel/index.js @@ -27,14 +27,18 @@ import Channel from './channel'; function mapStateToProps(state, ownProps) { const {websocket} = state.requests.general; + const {myChannels: channelsRequest} = state.requests.channels; + const {statusBarHeight} = state.views.root; return { ...ownProps, ...state.views.channel, - currentTeam: getCurrentTeam(state), - currentChannel: getCurrentChannel(state), + currentTeam: getCurrentTeam(state) || {}, + currentChannel: getCurrentChannel(state) || {}, theme: getTheme(state), - webSocketRequest: websocket + webSocketRequest: websocket, + statusBarHeight, + channelsRequestStatus: channelsRequest.status }; } diff --git a/app/screens/channel_add_members/index.js b/app/screens/channel_add_members/index.js index 2c6f8e3a8..43d07eb9d 100644 --- a/app/screens/channel_add_members/index.js +++ b/app/screens/channel_add_members/index.js @@ -18,9 +18,9 @@ import ChannelAddMembers from './channel_add_members'; function mapStateToProps(state) { return { theme: getTheme(state), - currentChannel: getCurrentChannel(state), + currentChannel: getCurrentChannel(state) || {}, membersNotInChannel: getProfilesNotInCurrentChannel(state), - currentTeam: getCurrentTeam(state), + currentTeam: getCurrentTeam(state) || {}, preferences: getMyPreferences(state), loadMoreRequestStatus: state.requests.users.getProfilesNotInChannel.status, addChannelMemberRequestStatus: state.requests.channels.addChannelMember, diff --git a/app/screens/channel_info/index.js b/app/screens/channel_info/index.js index 38b7ab9fc..21bda5eaf 100644 --- a/app/screens/channel_info/index.js +++ b/app/screens/channel_info/index.js @@ -29,7 +29,7 @@ import ChannelInfo from './channel_info'; function mapStateToProps(state, ownProps) { const {config, license} = state.entities.general; - const currentChannel = getCurrentChannel(state); + const currentChannel = getCurrentChannel(state) || {}; const currentChannelCreator = getUser(state, currentChannel.creator_id); const currentChannelCreatorName = currentChannelCreator && currentChannelCreator.username; const currentChannelStats = getCurrentChannelStats(state); diff --git a/app/screens/channel_members/index.js b/app/screens/channel_members/index.js index fa1801e83..a448f19ec 100644 --- a/app/screens/channel_members/index.js +++ b/app/screens/channel_members/index.js @@ -16,7 +16,7 @@ import ChannelMembers from './channel_members'; function mapStateToProps(state) { return { theme: getTheme(state), - currentChannel: getCurrentChannel(state), + currentChannel: getCurrentChannel(state) || {}, currentChannelMembers: getProfilesInCurrentChannel(state), currentUserId: state.entities.users.currentUserId, preferences: getMyPreferences(state), diff --git a/app/screens/image_preview/image_preview.js b/app/screens/image_preview/image_preview.js index 6cc17a746..a2685ae01 100644 --- a/app/screens/image_preview/image_preview.js +++ b/app/screens/image_preview/image_preview.js @@ -44,6 +44,7 @@ export default class ImagePreview extends PureComponent { fileId: PropTypes.string.isRequired, files: PropTypes.array.isRequired, navigator: PropTypes.object, + statusBarHeight: PropTypes.number, theme: PropTypes.object.isRequired }; @@ -209,6 +210,17 @@ export default class ImagePreview extends PureComponent { render() { const maxImageHeight = this.state.deviceHeight - STATUSBAR_HEIGHT; + const marginStyle = { + ...Platform.select({ + ios: { + marginTop: this.props.statusBarHeight + }, + android: { + marginTop: 10 + } + }) + }; + return ( - + 20) { + height = statusBarHeight - 20; + } + return (