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 (