diff --git a/app/constants/view.js b/app/constants/view.js
index f2e5c4a1f..d72516223 100644
--- a/app/constants/view.js
+++ b/app/constants/view.js
@@ -95,8 +95,16 @@ const ViewTypes = keyMirror({
LANDSCAPE: null,
});
+const RequiredServer = {
+ FULL_VERSION: 5.19,
+ MAJOR_VERSION: 5,
+ MIN_VERSION: 19,
+ PATCH_VERSION: 0,
+};
+
export default {
...ViewTypes,
+ RequiredServer,
POST_VISIBILITY_CHUNK_SIZE: 15,
FEATURE_TOGGLE_PREFIX: 'feature_enabled_',
EMBED_PREVIEW: 'embed_preview',
diff --git a/app/screens/channel/channel_base.js b/app/screens/channel/channel_base.js
index 600f547fa..19f68da4b 100644
--- a/app/screens/channel/channel_base.js
+++ b/app/screens/channel/channel_base.js
@@ -5,14 +5,16 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {
+ Alert,
Keyboard,
+ Linking,
StyleSheet,
} from 'react-native';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import {showModal, showModalOverCurrentContext} from '@actions/navigation';
import LocalConfig from '@assets/config';
-import {NavigationTypes} from '@constants';
+import {NavigationTypes, ViewTypes} from '@constants';
import EventEmitter from '@mm-redux/utils/event_emitter';
import EphemeralStore from '@store/ephemeral_store';
import {preventDoubleTap} from '@utils/tap';
@@ -37,6 +39,7 @@ export default class ChannelBase extends PureComponent {
currentChannelId: PropTypes.string,
currentTeamId: PropTypes.string,
disableTermsModal: PropTypes.bool,
+ isSupportedServer: PropTypes.bool,
teamName: PropTypes.string,
theme: PropTypes.object.isRequired,
showTermsOfService: PropTypes.bool,
@@ -67,31 +70,43 @@ export default class ChannelBase extends PureComponent {
}
componentDidMount() {
+ const {
+ actions,
+ currentChannelId,
+ currentTeamId,
+ disableTermsModal,
+ isSupportedServer,
+ showTermsOfService,
+ skipMetrics,
+ } = this.props;
EventEmitter.on(NavigationTypes.BLUR_POST_DRAFT, this.blurPostDraft);
EventEmitter.on('leave_team', this.handleLeaveTeam);
- if (this.props.currentTeamId) {
- this.loadChannels(this.props.currentTeamId);
+ if (currentTeamId) {
+ this.loadChannels(currentTeamId);
} else {
- this.props.actions.selectDefaultTeam();
+ actions.selectDefaultTeam();
}
- if (this.props.currentChannelId) {
- PushNotifications.clearChannelNotifications(this.props.currentChannelId);
+ if (currentChannelId) {
+ PushNotifications.clearChannelNotifications(currentChannelId);
requestAnimationFrame(() => {
- this.props.actions.getChannelStats(this.props.currentChannelId);
+ actions.getChannelStats(currentChannelId);
});
}
- if (tracker.initialLoad && !this.props.skipMetrics) {
- this.props.actions.recordLoadTime('Start time', 'initialLoad');
+ if (tracker.initialLoad && !skipMetrics) {
+ actions.recordLoadTime('Start time', 'initialLoad');
}
- if (this.props.showTermsOfService && !this.props.disableTermsModal) {
+ if (showTermsOfService && !disableTermsModal) {
this.showTermsOfServiceModal();
+ } else if (!isSupportedServer) {
+ // Only display the Alert if the TOS does not need to show first
+ this.showUnsupportedServer();
}
- if (!this.props.skipMetrics) {
+ if (!skipMetrics) {
telemetry.end(['start:channel_screen']);
}
}
@@ -140,31 +155,6 @@ export default class ChannelBase extends PureComponent {
}
};
- showTermsOfServiceModal = async () => {
- const {intl} = this.context;
- const {theme} = this.props;
- const screen = 'TermsOfService';
- const title = intl.formatMessage({id: 'mobile.tos_link', defaultMessage: 'Terms of Service'});
- MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).then((closeButton) => {
- const passProps = {closeButton};
- const options = {
- layout: {
- componentBackgroundColor: theme.centerChannelBg,
- },
- topBar: {
- visible: true,
- height: null,
- title: {
- color: theme.sidebarHeaderTextColor,
- text: title,
- },
- },
- };
-
- showModalOverCurrentContext(screen, passProps, options);
- });
- };
-
goToChannelInfo = preventDoubleTap(() => {
const {intl} = this.context;
const {theme} = this.props;
@@ -249,6 +239,62 @@ export default class ChannelBase extends PureComponent {
return null;
}
+ showTermsOfServiceModal = async () => {
+ const {intl} = this.context;
+ const {isSupportedServer, theme} = this.props;
+ const screen = 'TermsOfService';
+ const title = intl.formatMessage({id: 'mobile.tos_link', defaultMessage: 'Terms of Service'});
+ MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).then((closeButton) => {
+ const passProps = {
+ closeButton,
+ isSupportedServer,
+ showUnsupportedServer: this.showUnsupportedServer,
+ };
+ const options = {
+ layout: {
+ componentBackgroundColor: theme.centerChannelBg,
+ },
+ topBar: {
+ visible: true,
+ height: null,
+ title: {
+ color: theme.sidebarHeaderTextColor,
+ text: title,
+ },
+ },
+ };
+
+ showModalOverCurrentContext(screen, passProps, options);
+ });
+ };
+
+ showUnsupportedServer = () => {
+ const {formatMessage} = this.context.intl;
+ const title = formatMessage({id: 'mobile.server_upgrade.title', defaultMessage: 'Server upgrade required'});
+ const message = formatMessage({
+ id: 'mobile.server_upgrade.alert_description',
+ defaultMessage: 'This server version is unsupported and users will be exposed to compatibility issues that cause crashes or severe bugs breaking core functionality of the app. Upgrading to server version {serverVersion} or later is required.',
+ }, {serverVersion: ViewTypes.RequiredServer.FULL_VERSION});
+ const cancel = {
+ text: formatMessage({id: 'mobile.server_upgrade.dismiss', defaultMessage: 'Dismiss'}),
+ style: 'default',
+ };
+ const learnMore = {
+ text: formatMessage({id: 'mobile.server_upgrade.learn_more', defaultMessage: 'Learn More'}),
+ style: 'cancel',
+ onPress: () => {
+ const url = 'https://mattermost.com/blog/support-for-esr-5-9-has-ended/';
+ if (Linking.canOpenURL(url)) {
+ Linking.openURL(url);
+ }
+ },
+ };
+ const buttons = [cancel, learnMore];
+ const options = {cancelable: false};
+
+ Alert.alert(title, message, buttons, options);
+ };
+
updateNativeScrollView = () => {
if (this.keyboardTracker?.current) {
this.keyboardTracker.current.resetScrollView(this.props.currentChannelId);
diff --git a/app/screens/channel/channel_base.test.js b/app/screens/channel/channel_base.test.js
index 975d56703..c2b22b943 100644
--- a/app/screens/channel/channel_base.test.js
+++ b/app/screens/channel/channel_base.test.js
@@ -62,6 +62,7 @@ describe('ChannelBase', () => {
const wrapper = shallow(
,
+ {context: {intl: {formatMessage: jest.fn()}}},
);
expect(mergeNavigationOptions.mock.calls).toEqual([]);
diff --git a/app/screens/channel/index.js b/app/screens/channel/index.js
index d276db377..27ba47077 100644
--- a/app/screens/channel/index.js
+++ b/app/screens/channel/index.js
@@ -7,20 +7,37 @@ import {connect} from 'react-redux';
import {loadChannelsForTeam, selectInitialChannel} from '@actions/views/channel';
import {recordLoadTime} from '@actions/views/root';
import {selectDefaultTeam} from '@actions/views/select_team';
-import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels';
-import {getCurrentTeam} from '@mm-redux/selectors/entities/teams';
-import {getTheme} from '@mm-redux/selectors/entities/preferences';
-import {shouldShowTermsOfService} from '@mm-redux/selectors/entities/users';
+import {ViewTypes} from '@constants';
import {getChannelStats} from '@mm-redux/actions/channels';
+import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels';
+import {getServerVersion} from '@mm-redux/selectors/entities/general';
+import {getTheme} from '@mm-redux/selectors/entities/preferences';
+import {getCurrentTeam} from '@mm-redux/selectors/entities/teams';
+import {getCurrentUserId, getCurrentUserRoles, shouldShowTermsOfService} from '@mm-redux/selectors/entities/users';
+import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
+import {isSystemAdmin as checkIsSystemAdmin} from '@mm-redux/utils/user_utils';
import Channel from './channel';
function mapStateToProps(state) {
const currentTeam = getCurrentTeam(state);
+ const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : '';
+ const isSystemAdmin = checkIsSystemAdmin(roles);
+ let isSupportedServer = true;
+
+ if (isSystemAdmin) {
+ isSupportedServer = isMinimumServerVersion(
+ getServerVersion(state),
+ ViewTypes.RequiredServer.MAJOR_VERSION,
+ ViewTypes.RequiredServer.MIN_VERSION,
+ ViewTypes.RequiredServer.PATCH_VERSION,
+ );
+ }
return {
currentTeamId: currentTeam?.id,
currentChannelId: getCurrentChannelId(state),
+ isSupportedServer,
teamName: currentTeam?.display_name,
theme: getTheme(state),
showTermsOfService: shouldShowTermsOfService(state),
diff --git a/app/screens/terms_of_service/terms_of_service.js b/app/screens/terms_of_service/terms_of_service.js
index 93217da06..541aa928c 100644
--- a/app/screens/terms_of_service/terms_of_service.js
+++ b/app/screens/terms_of_service/terms_of_service.js
@@ -30,6 +30,8 @@ export default class TermsOfService extends PureComponent {
}).isRequired,
componentId: PropTypes.string,
closeButton: PropTypes.object,
+ isSupportedServer: PropTypes.bool,
+ showUnsupportedServer: PropTypes.func,
siteName: PropTypes.string,
theme: PropTypes.object,
};
@@ -145,11 +147,18 @@ export default class TermsOfService extends PureComponent {
};
handleAcceptTerms = () => {
+ const onSuccess = () => {
+ const {isSupportedServer, showUnsupportedServer} = this.props;
+ dismissModal();
+
+ if (!isSupportedServer) {
+ showUnsupportedServer();
+ }
+ };
+
this.registerUserAction(
true,
- () => {
- dismissModal();
- },
+ onSuccess,
this.handleAcceptTerms,
);
};
@@ -189,7 +198,6 @@ export default class TermsOfService extends PureComponent {
});
const {data} = await actions.updateMyTermsOfServiceStatus(this.state.termsId, accepted);
-
this.setState({
loading: false,
});
diff --git a/app/screens/terms_of_service/terms_of_service.test.js b/app/screens/terms_of_service/terms_of_service.test.js
index 4ce021837..4bb650ab0 100644
--- a/app/screens/terms_of_service/terms_of_service.test.js
+++ b/app/screens/terms_of_service/terms_of_service.test.js
@@ -7,6 +7,7 @@ import {shallow} from 'enzyme';
import Preferences from '@mm-redux/constants/preferences';
import * as NavigationActions from '@actions/navigation';
+import TestHelper from 'test/test_helper';
import TermsOfService from './terms_of_service.js';
@@ -23,16 +24,18 @@ jest.mock('@utils/theme', () => {
describe('TermsOfService', () => {
const actions = {
getTermsOfService: jest.fn(),
- updateMyTermsOfServiceStatus: jest.fn(),
+ updateMyTermsOfServiceStatus: jest.fn().mockResolvedValue({data: true}),
logout: jest.fn(),
};
const baseProps = {
actions,
- theme: Preferences.THEMES.default,
closeButton: {},
- siteName: 'Mattermost',
componentId: 'component-id',
+ isSupportedServer: true,
+ siteName: 'Mattermost',
+ showUnsupportedServer: jest.fn(),
+ theme: Preferences.THEMES.default,
};
test('should match snapshot', () => {
@@ -130,4 +133,35 @@ describe('TermsOfService', () => {
wrapper.instance().closeTermsAndLogout();
expect(baseProps.actions.logout).toHaveBeenCalledTimes(1);
});
+
+ test('should NOT call showUnsupportedServer on acceptTerms if server is supported', async () => {
+ const wrapper = shallow(
+ ,
+ {context: {intl: {formatMessage: jest.fn()}}},
+ );
+
+ wrapper.setState({loading: false, termsId: 1, termsText: 'Terms Text'});
+ wrapper.instance().handleAcceptTerms();
+ TestHelper.wait(100).then(() => {
+ expect(baseProps.showUnsupportedServer).not.toHaveBeenCalled();
+ });
+ });
+
+ test('should call showUnsupportedServer on acceptTerms if server not supported', async () => {
+ const wrapper = shallow(
+ ,
+ {context: {intl: {formatMessage: jest.fn()}}},
+ );
+
+ wrapper.setState({loading: false, termsId: 1, termsText: 'Terms Text'});
+ wrapper.instance().handleAcceptTerms();
+ TestHelper.wait(100).then(() => {
+ expect(baseProps.showUnsupportedServer).toHaveBeenCalledTimes(1);
+ });
+ });
});
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index f25743d4a..c910949a3 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -453,8 +453,11 @@
"mobile.server_link.unreachable_team.error": "This link belongs to a deleted team or to a team to which you do not have access.",
"mobile.server_ssl.error.text": "The certificate from {host} is not trusted.\n\nPlease contact your System Administrator to resolve the certificate issues and allow connections to this server.",
"mobile.server_ssl.error.title": "Untrusted Certificate",
+ "mobile.server_upgrade.alert_description": "This server version is unsupported and users will be exposed to compatibility issues that cause crashes or severe bugs breaking core functionality of the app. Upgrading to server version {serverVersion} or later is required.",
"mobile.server_upgrade.button": "OK",
"mobile.server_upgrade.description": "\nA server upgrade is required to use the Mattermost app. Please ask your System Administrator for details.\n",
+ "mobile.server_upgrade.dismiss": "Dismiss",
+ "mobile.server_upgrade.learn_more": "Learn More",
"mobile.server_upgrade.title": "Server upgrade required",
"mobile.server_url.invalid_format": "URL must start with http:// or https://",
"mobile.session_expired": "Session Expired: Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}}.",