diff --git a/android/app/build.gradle b/android/app/build.gradle
index 7ed78953f..45fcb1d8a 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -113,7 +113,7 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion 21
targetSdkVersion 23
- versionCode 110
+ versionCode 112
versionName "1.9.0"
ndk {
abiFilters "armeabi-v7a", "x86"
diff --git a/app/components/combined_system_message/combined_system_message.js b/app/components/combined_system_message/combined_system_message.js
index d904ae19e..7bb790121 100644
--- a/app/components/combined_system_message/combined_system_message.js
+++ b/app/components/combined_system_message/combined_system_message.js
@@ -156,8 +156,8 @@ const postTypeMessage = {
export default class CombinedSystemMessage extends React.PureComponent {
static propTypes = {
actions: PropTypes.shape({
- getProfilesByIds: PropTypes.func.isRequired,
- getProfilesByUsernames: PropTypes.func.isRequired,
+ getMissingProfilesByIds: PropTypes.func.isRequired,
+ getMissingProfilesByUsernames: PropTypes.func.isRequired,
}).isRequired,
allUserIds: PropTypes.array.isRequired,
allUsernames: PropTypes.array.isRequired,
@@ -168,6 +168,7 @@ export default class CombinedSystemMessage extends React.PureComponent {
showJoinLeave: PropTypes.bool.isRequired,
teammateNameDisplay: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
+ userProfiles: PropTypes.array.isRequired,
};
static defaultProps = {
@@ -179,14 +180,6 @@ export default class CombinedSystemMessage extends React.PureComponent {
intl: intlShape,
};
- constructor(props) {
- super(props);
-
- this.state = {
- userProfiles: [],
- };
- }
-
componentDidMount() {
this.loadUserProfiles(this.props.allUserIds, this.props.allUsernames);
}
@@ -197,34 +190,24 @@ export default class CombinedSystemMessage extends React.PureComponent {
}
}
- loadUserProfiles = async (allUserIds, allUsernames) => {
- const {actions} = this.props;
- const userProfiles = [];
+ loadUserProfiles = (allUserIds, allUsernames) => {
if (allUserIds.length > 0) {
- const {data} = await actions.getProfilesByIds(allUserIds);
- if (data.length > 0) {
- userProfiles.push(...data);
- }
+ this.props.actions.getMissingProfilesByIds(allUserIds);
}
if (allUsernames.length > 0) {
- const {data} = await actions.getProfilesByUsernames(allUsernames);
- if (data.length > 0) {
- userProfiles.push(...data);
- }
+ this.props.actions.getMissingProfilesByUsernames(allUsernames);
}
-
- this.setState({userProfiles});
}
getAllUsersDisplayName = () => {
- const {userProfiles} = this.state;
const {
allUserIds,
allUsernames,
currentUserId,
currentUsername,
teammateNameDisplay,
+ userProfiles,
} = this.props;
const {formatMessage} = this.context.intl;
const usersDisplayName = userProfiles.reduce((acc, user) => {
@@ -248,7 +231,11 @@ export default class CombinedSystemMessage extends React.PureComponent {
const usersDisplayName = this.getAllUsersDisplayName();
const displayNames = userIds.
filter((userId) => {
- return userId !== currentUserId && userId !== currentUsername;
+ return (
+ usersDisplayName[userId] &&
+ userId !== currentUserId &&
+ userId !== currentUsername
+ );
}).
map((userId) => {
return usersDisplayName[userId];
diff --git a/app/components/combined_system_message/combined_system_message.test.js b/app/components/combined_system_message/combined_system_message.test.js
index fe043dbd8..3823bd81e 100644
--- a/app/components/combined_system_message/combined_system_message.test.js
+++ b/app/components/combined_system_message/combined_system_message.test.js
@@ -13,13 +13,13 @@ import {Posts} from 'mattermost-redux/constants';
import CombinedSystemMessage from './combined_system_message';
-/* eslint-disable max-nested-callbacks, no-console */
+/* eslint-disable max-nested-callbacks */
describe('CombinedSystemMessage', () => {
const baseProps = {
actions: {
- getProfilesByIds: emptyFunction,
- getProfilesByUsernames: emptyFunction,
+ getMissingProfilesByIds: emptyFunction,
+ getMissingProfilesByUsernames: emptyFunction,
},
allUserIds: ['user_id_1', 'user_id_2', 'user_id_3'],
currentUserId: 'user_id_3',
@@ -31,46 +31,68 @@ describe('CombinedSystemMessage', () => {
showJoinLeave: true,
teammateNameDisplay: 'username',
theme: {centerChannelColor: '#aaa'},
+ userProfiles: [{id: 'user_id_1', username: 'user1'}, {id: 'user_id_2', username: 'user2'}, {id: 'user_id_3', username: 'user3'}],
};
test('should match snapshot', () => {
const props = {
...baseProps,
actions: {
- getProfilesByIds: jest.fn(() => Promise.resolve({data: true})),
- getProfilesByUsernames: emptyFunction,
+ getMissingProfilesByIds: jest.fn(),
+ getMissingProfilesByUsernames: emptyFunction,
},
};
const wrapper = shallowWithIntl(
);
- wrapper.setState({userProfiles: [{id: 'user_id_1', username: 'user1'}, {id: 'user_id_2', username: 'user2'}, {id: 'user_id_3', username: 'user3'}]});
const {postType, userIds, actorId} = baseProps.messageData[0];
expect(wrapper.instance().renderSystemMessage(postType, userIds, actorId, {activityType: {fontSize: 14}, text: {opacity: 0.6}}, 1)).toMatchSnapshot();
// on componentDidMount
- expect(props.actions.getProfilesByIds).toHaveBeenCalledTimes(1);
- expect(props.actions.getProfilesByIds).toHaveBeenCalledWith(props.allUserIds);
+ expect(props.actions.getMissingProfilesByIds).toHaveBeenCalledTimes(1);
+ expect(props.actions.getMissingProfilesByIds).toHaveBeenCalledWith(props.allUserIds);
});
test('should match snapshot', () => {
- const props = {
- ...baseProps,
- actions: {
- getProfilesByIds: jest.fn(() => Promise.resolve({data: true})),
- getProfilesByUsernames: emptyFunction,
- },
- };
const localeFormat = {
id: ['combined_system_message.first_user_and_second_user_were', 'combined_system_message.removed_from_team'],
defaultMessage: ['{firstUser} and {secondUser} were ', 'removed from the team'],
};
+ const wrapper = shallowWithIntl(
+
+ );
+
+ expect(wrapper.instance().renderFormattedMessage(localeFormat, 'first_user', 'second_user', 'actor', {activityType: {fontSize: 14}, text: {opacity: 0.6}})).toMatchSnapshot();
+ });
+
+ test('should call getMissingProfilesByIds and/or getMissingProfilesByUsernames on loadUserProfiles', () => {
+ const props = {
+ ...baseProps,
+ allUserIds: [],
+ actions: {
+ getMissingProfilesByIds: jest.fn(),
+ getMissingProfilesByUsernames: jest.fn(),
+ },
+ };
+
const wrapper = shallowWithIntl(
);
- wrapper.setState({userProfiles: [{id: 'user_id_1', username: 'user1'}, {id: 'user_id_2', username: 'user2'}, {id: 'user_id_3', username: 'user3'}]});
- expect(wrapper.instance().renderFormattedMessage(localeFormat, 'first_user', 'second_user', 'actor', {activityType: {fontSize: 14}, text: {opacity: 0.6}})).toMatchSnapshot();
+ wrapper.instance().loadUserProfiles([], []);
+ expect(props.actions.getMissingProfilesByIds).toHaveBeenCalledTimes(0);
+ expect(props.actions.getMissingProfilesByUsernames).toHaveBeenCalledTimes(0);
+
+ wrapper.instance().loadUserProfiles(['user_id_1'], []);
+ expect(props.actions.getMissingProfilesByIds).toHaveBeenCalledTimes(1);
+ expect(props.actions.getMissingProfilesByIds).toHaveBeenCalledWith(['user_id_1']);
+ expect(props.actions.getMissingProfilesByUsernames).toHaveBeenCalledTimes(0);
+
+ wrapper.instance().loadUserProfiles(['user_id_1', 'user_id_2'], ['user1']);
+ expect(props.actions.getMissingProfilesByIds).toHaveBeenCalledTimes(2);
+ expect(props.actions.getMissingProfilesByIds).toHaveBeenCalledWith(['user_id_1', 'user_id_2']);
+ expect(props.actions.getMissingProfilesByUsernames).toHaveBeenCalledTimes(1);
+ expect(props.actions.getMissingProfilesByUsernames).toHaveBeenCalledWith(['user1']);
});
});
diff --git a/app/components/combined_system_message/index.js b/app/components/combined_system_message/index.js
index 1c225c266..43e8022dd 100644
--- a/app/components/combined_system_message/index.js
+++ b/app/components/combined_system_message/index.js
@@ -4,30 +4,36 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
-import {getProfilesByIds, getProfilesByUsernames} from 'mattermost-redux/actions/users';
+import {getMissingProfilesByIds, getMissingProfilesByUsernames} from 'mattermost-redux/actions/users';
import {Preferences} from 'mattermost-redux/constants';
import {getBool, getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
-import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
+import {getCurrentUser, makeGetProfilesByIdsAndUsernames} from 'mattermost-redux/selectors/entities/users';
import CombinedSystemMessage from './combined_system_message';
-function mapStateToProps(state) {
- const currentUser = getCurrentUser(state);
- return {
- currentUserId: currentUser.id,
- currentUsername: currentUser.username,
- showJoinLeave: getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE, true),
- teammateNameDisplay: getTeammateNameDisplaySetting(state),
+function makeMapStateToProps() {
+ const getProfilesByIdsAndUsernames = makeGetProfilesByIdsAndUsernames();
+
+ return (state, ownProps) => {
+ const currentUser = getCurrentUser(state);
+ const {allUserIds, allUsernames} = ownProps;
+ return {
+ currentUserId: currentUser.id,
+ currentUsername: currentUser.username,
+ showJoinLeave: getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE, true),
+ teammateNameDisplay: getTeammateNameDisplaySetting(state),
+ userProfiles: getProfilesByIdsAndUsernames(state, {allUserIds, allUsernames}),
+ };
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
- getProfilesByIds,
- getProfilesByUsernames,
+ getMissingProfilesByIds,
+ getMissingProfilesByUsernames,
}, dispatch),
};
}
-export default connect(mapStateToProps, mapDispatchToProps)(CombinedSystemMessage);
+export default connect(makeMapStateToProps, mapDispatchToProps)(CombinedSystemMessage);
diff --git a/app/fetch_preconfig.js b/app/fetch_preconfig.js
new file mode 100644
index 000000000..c3c8af3e7
--- /dev/null
+++ b/app/fetch_preconfig.js
@@ -0,0 +1,96 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import RNFetchBlob from 'react-native-fetch-blob';
+
+import {Client4} from 'mattermost-redux/client';
+import {General} from 'mattermost-redux/constants';
+import EventEmitter from 'mattermost-redux/utils/event_emitter';
+
+const HEADER_X_VERSION_ID = 'X-Version-Id';
+const HEADER_X_CLUSTER_ID = 'X-Cluster-Id';
+const HEADER_TOKEN = 'Token';
+
+Client4.doFetchWithResponse = async (url, options) => {
+ if (!Client4.online) {
+ throw {
+ message: 'no internet connection',
+ url,
+ };
+ }
+ const response = await fetch(url, Client4.getOptions(options));
+ const headers = response.headers;
+
+ let data;
+ try {
+ data = await response.json();
+ } catch (err) {
+ if (response && response.resp && response.resp.data && response.resp.data.includes('SSL certificate')) {
+ throw {
+ message: 'You need to use a valid client certificate in order to connect to this Mattermost server',
+ status_code: 401,
+ url,
+ };
+ }
+
+ throw {
+ intl: {
+ id: 'mobile.request.invalid_response',
+ defaultMessage: 'Received invalid response from the server.',
+ },
+ };
+ }
+
+ // Need to only accept version in the header from requests that are not cached
+ // to avoid getting an old version from a cached response
+ if ((headers[HEADER_X_VERSION_ID] || headers[HEADER_X_VERSION_ID.toLowerCase()]) &&
+ (!headers['Cache-Control'] && !headers['cache-control'])) {
+ const serverVersion = headers[HEADER_X_VERSION_ID] || headers[HEADER_X_VERSION_ID.toLowerCase()];
+ if (serverVersion && this.serverVersion !== serverVersion) {
+ this.serverVersion = serverVersion;
+ EventEmitter.emit(General.SERVER_VERSION_CHANGED, serverVersion);
+ }
+ }
+
+ if (headers[HEADER_X_CLUSTER_ID] || headers[HEADER_X_CLUSTER_ID.toLowerCase()]) {
+ const clusterId = headers[HEADER_X_CLUSTER_ID] || headers[HEADER_X_CLUSTER_ID.toLowerCase()];
+ if (clusterId && this.clusterId !== clusterId) {
+ this.clusterId = clusterId;
+ }
+ }
+
+ if (headers[HEADER_TOKEN] || headers[HEADER_TOKEN.toLowerCase()]) {
+ const token = headers[HEADER_TOKEN] || headers[HEADER_TOKEN.toLowerCase()];
+ Client4.setToken(token);
+ }
+
+ if (response.ok) {
+ const headersMap = new Map();
+ Object.keys(headers).forEach((key) => {
+ headersMap.set(key, headers[key]);
+ });
+
+ return {
+ response,
+ headers: headersMap,
+ data,
+ };
+ }
+
+ const msg = data.message || '';
+
+ if (Client4.logToConsole) {
+ console.error(msg); // eslint-disable-line no-console
+ }
+
+ throw {
+ message: msg,
+ server_error_id: data.id,
+ status_code: data.status_code,
+ url,
+ };
+};
+
+window.fetch = new RNFetchBlob.polyfill.Fetch({
+ auto: true,
+}).build();
diff --git a/app/mattermost.js b/app/mattermost.js
index 23a234bd4..326e14af6 100644
--- a/app/mattermost.js
+++ b/app/mattermost.js
@@ -48,6 +48,7 @@ import avoidNativeBridge from 'app/utils/avoid_native_bridge';
import LocalConfig from 'assets/config';
import App from './app';
+import './fetch_preconfig';
const AUTHENTICATION_TIMEOUT = 5 * 60 * 1000;
diff --git a/app/screens/permalink/permalink.js b/app/screens/permalink/permalink.js
index 9dde08fc5..88963a3e2 100644
--- a/app/screens/permalink/permalink.js
+++ b/app/screens/permalink/permalink.js
@@ -103,21 +103,23 @@ export default class Permalink extends PureComponent {
}
componentWillMount() {
+ this.mounted = true;
+
if (this.state.loading) {
this.loadPosts(this.props);
}
}
componentWillReceiveProps(nextProps) {
- if (nextProps.channelId === '') {
+ if (nextProps.channelId === '' && this.mounted) {
this.handleClose();
}
- if (this.props.channelName !== nextProps.channelName) {
+ if (this.props.channelName !== nextProps.channelName && this.mounted) {
this.setState({title: nextProps.channelName});
}
- if (this.props.focusedPostId !== nextProps.focusedPostId) {
+ if (this.props.focusedPostId !== nextProps.focusedPostId && this.mounted) {
this.setState({loading: true});
if (nextProps.postIds && nextProps.postIds.length < 10) {
this.loadPosts(nextProps);
@@ -127,6 +129,10 @@ export default class Permalink extends PureComponent {
}
}
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
goToThread = preventDoubleTap((post) => {
const {actions, navigator, theme} = this.props;
const channelId = post.channel_id;
@@ -157,6 +163,7 @@ export default class Permalink extends PureComponent {
handleClose = () => {
const {actions, navigator, onClose} = this.props;
if (this.refs.view) {
+ this.mounted = false;
this.refs.view.zoomOut().then(() => {
actions.selectPost('');
navigator.dismissModal({animationType: 'none'});
@@ -233,7 +240,7 @@ export default class Permalink extends PureComponent {
let focusChannelId = channelId;
const post = await actions.getPostThread(focusedPostId, false);
- if (post.error && (!postIds || !postIds.length)) {
+ if (this.mounted && post.error && (!postIds || !postIds.length)) {
if (isPermalink && post.error.message.toLowerCase() !== 'network request failed') {
this.setState({
error: formatMessage({
@@ -281,8 +288,10 @@ export default class Permalink extends PureComponent {
};
retry = () => {
- this.setState({loading: true, error: null, retry: false});
- this.loadPosts(this.props);
+ if (this.mounted) {
+ this.setState({loading: true, error: null, retry: false});
+ this.loadPosts(this.props);
+ }
};
render() {
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index f8f4db91c..b8dd62eda 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -166,7 +166,7 @@ platform :ios do
def replace_ios_assets()
if ENV['IOS_REPLACE_ASSETS'] == 'true'
sh 'cp -R ../dist/assets/release/icons/ios/* ../ios/Mattermost/Images.xcassets/AppIcon.appiconset/'
- sh 'cp -R ../dist/assets/release/splash_screen/ios/* ../ios/SplashScreenResource/*'
+ sh 'cp -R ../dist/assets/release/splash_screen/ios/* ../ios/SplashScreenResource/'
end
end
@@ -571,7 +571,7 @@ def configure_from_env
unless ENV['SEGMENT_API_KEY'].nil? || ENV['SEGMENT_API_KEY'].empty?
find_replace_string(
path_to_file: './dist/assets/config.json',
- old_string: '"SegmentApiKey": "3MT7rAoC0OP7yy3ThzqFSAtKzmzqtUPX"',
+ old_string: '"SegmentApiKey": "oPPT2MA24VYZTBGDyXtUDa7u50lkPIE4"',
new_string: "\"SegmentApiKey\": \"#{ENV['SEGMENT_API_KEY']}\""
)
end
diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj
index adeec692e..962280f1c 100644
--- a/ios/Mattermost.xcodeproj/project.pbxproj
+++ b/ios/Mattermost.xcodeproj/project.pbxproj
@@ -2516,7 +2516,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- CURRENT_PROJECT_VERSION = 110;
+ CURRENT_PROJECT_VERSION = 112;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
@@ -2566,7 +2566,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- CURRENT_PROJECT_VERSION = 110;
+ CURRENT_PROJECT_VERSION = 112;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist
index c9046cfe6..da58d1c48 100644
--- a/ios/Mattermost/Info.plist
+++ b/ios/Mattermost/Info.plist
@@ -34,7 +34,7 @@
CFBundleVersion
- 110
+ 112
ITSAppUsesNonExemptEncryption
LSRequiresIPhoneOS
diff --git a/ios/MattermostShare/Info.plist b/ios/MattermostShare/Info.plist
index 9d67310f1..90f8a47f7 100644
--- a/ios/MattermostShare/Info.plist
+++ b/ios/MattermostShare/Info.plist
@@ -23,7 +23,7 @@
CFBundleShortVersionString
1.9.0
CFBundleVersion
- 110
+ 112
NSAppTransportSecurity
NSAllowsArbitraryLoads
diff --git a/ios/MattermostTests/Info.plist b/ios/MattermostTests/Info.plist
index 1a231c9ee..2168c7f28 100644
--- a/ios/MattermostTests/Info.plist
+++ b/ios/MattermostTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 110
+ 112
diff --git a/package-lock.json b/package-lock.json
index 3abc5f6f7..fbacbd451 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9878,8 +9878,8 @@
}
},
"mattermost-redux": {
- "version": "github:mattermost/mattermost-redux#85edf0d1cae3b786d9be1946f19a23603af18417",
- "from": "github:mattermost/mattermost-redux#85edf0d1cae3b786d9be1946f19a23603af18417",
+ "version": "github:mattermost/mattermost-redux#c9b633da7fc3fc9ba3cc40ecc9665e088defbe73",
+ "from": "github:mattermost/mattermost-redux#c9b633da7fc3fc9ba3cc40ecc9665e088defbe73",
"requires": {
"deep-equal": "1.0.1",
"eslint-plugin-header": "1.2.0",
@@ -14484,8 +14484,8 @@
}
},
"react-native-fetch-blob": {
- "version": "github:enahum/react-native-fetch-blob#2e8f9b05b012496bd3ff9790aace3f2b462e2fee",
- "from": "react-native-fetch-blob@github:enahum/react-native-fetch-blob#2e8f9b05b012496bd3ff9790aace3f2b462e2fee",
+ "version": "github:enahum/react-native-fetch-blob#c4c798032d9c255fbae75d13a0a985af9b8b42ca",
+ "from": "github:enahum/react-native-fetch-blob#c4c798032d9c255fbae75d13a0a985af9b8b42ca",
"requires": {
"base-64": "0.1.0",
"glob": "7.0.6"
diff --git a/package.json b/package.json
index 835fb3bac..25b80372b 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,7 @@
"intl": "1.2.5",
"jail-monkey": "1.0.0",
"jsc-android": "216113.0.3",
- "mattermost-redux": "github:mattermost/mattermost-redux#85edf0d1cae3b786d9be1946f19a23603af18417",
+ "mattermost-redux": "github:mattermost/mattermost-redux#c9b633da7fc3fc9ba3cc40ecc9665e088defbe73",
"mime-db": "1.33.0",
"prop-types": "15.6.1",
"react": "16.3.2",
@@ -32,7 +32,7 @@
"react-native-drawer": "2.5.0",
"react-native-exception-handler": "2.7.5",
"react-native-fast-image": "4.0.8",
- "react-native-fetch-blob": "enahum/react-native-fetch-blob.git#2e8f9b05b012496bd3ff9790aace3f2b462e2fee",
+ "react-native-fetch-blob": "enahum/react-native-fetch-blob.git#c4c798032d9c255fbae75d13a0a985af9b8b42ca",
"react-native-image-gallery": "enahum/react-native-image-gallery#a98b1051e94f5a394541ca1ff9b15e2c9ffed84f",
"react-native-image-picker": "0.26.7",
"react-native-keyboard-aware-scroll-view": "0.5.0",