diff --git a/app/actions/views/root.js b/app/actions/views/root.js index 11cdf78de8..c82601b812 100644 --- a/app/actions/views/root.js +++ b/app/actions/views/root.js @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import deepEqual from 'deep-equal'; import {batchActions} from 'redux-batched-actions'; import {Client4} from '@client/rest'; @@ -31,7 +32,10 @@ export function startDataCleanup() { export function loadConfigAndLicense() { return async (dispatch, getState) => { - const {currentUserId} = getState().entities.users; + const state = getState(); + const {currentUserId} = state.entities.users; + const {general} = state.entities; + const actions = []; try { const [config, license] = await Promise.all([ @@ -39,23 +43,31 @@ export function loadConfigAndLicense() { Client4.getClientLicenseOld(), ]); - const actions = [{ - type: GeneralTypes.CLIENT_CONFIG_RECEIVED, - data: config, - }, { - type: GeneralTypes.CLIENT_LICENSE_RECEIVED, - data: license, - }]; + if (!deepEqual(general.config, config)) { + actions.push({ + type: GeneralTypes.CLIENT_CONFIG_RECEIVED, + data: config, + }); + } - if (currentUserId) { - if (license?.IsLicensed === 'true' && license?.DataRetention === 'true') { - dispatch(getDataRetentionPolicy()); - } else { - actions.push({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}}); + if (!deepEqual(general.license, license)) { + actions.push({ + type: GeneralTypes.CLIENT_LICENSE_RECEIVED, + data: license, + }); + + if (currentUserId) { + if (license?.IsLicensed === 'true' && license?.DataRetention === 'true') { + dispatch(getDataRetentionPolicy()); + } else { + actions.push({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}}); + } } } - dispatch(batchActions(actions, 'BATCH_LOAD_CONFIG_AND_LICENSE')); + if (actions.length) { + dispatch(batchActions(actions, 'BATCH_LOAD_CONFIG_AND_LICENSE')); + } return {config, license}; } catch (error) { diff --git a/app/actions/views/user.js b/app/actions/views/user.js index 3fa07c6fb8..ee209c7a27 100644 --- a/app/actions/views/user.js +++ b/app/actions/views/user.js @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import deepEqual from 'deep-equal'; import {batchActions} from 'redux-batched-actions'; import {handleCRTPreferenceChange} from '@actions/views/crt'; @@ -121,9 +122,12 @@ export function loadMe(user, deviceToken, skipDispatch = false) { data.teams = teams; data.teamMembers = teamMembers; data.teamUnreads = teamUnreads; - data.config = config; data.url = Client4.getUrl(); + if (!deepEqual(state.entities?.general?.config, config)) { + data.config = config; + } + actions.push({ type: UserTypes.LOGIN, data, diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts index 847394a0a9..3d31f288c2 100644 --- a/app/actions/websocket/index.ts +++ b/app/actions/websocket/index.ts @@ -38,6 +38,7 @@ import { handleCallScreenOff, } from '@mmproducts/calls/store/actions/websockets'; import {getChannelSinceValue} from '@utils/channels'; +import {semverFromServerVersion} from '@utils/general'; import websocketClient from '@websocket'; import {handleRefreshAppsBindings} from './apps'; @@ -474,11 +475,8 @@ function handleEvent(msg: WebSocketMessage) { } function handleHelloEvent(msg: WebSocketMessage) { - const serverVersion = msg.data.server_version; - if (serverVersion && Client4.serverVersion !== serverVersion) { - Client4.serverVersion = serverVersion; - EventEmitter.emit(General.SERVER_VERSION_CHANGED, serverVersion); - } + const serverVersion = semverFromServerVersion(msg.data.server_version); + EventEmitter.emit(General.SERVER_VERSION_CHANGED, serverVersion); } // Helpers diff --git a/app/init/fetch.js b/app/init/fetch.js index 6aecc524c8..7b026eefc1 100644 --- a/app/init/fetch.js +++ b/app/init/fetch.js @@ -13,6 +13,7 @@ import ClientError from '@client/rest/error'; import mattermostManaged from '@mattermost-managed'; import {General} from '@mm-redux/constants'; import EventEmitter from '@mm-redux/utils/event_emitter'; +import {semverFromServerVersion} from '@utils/general'; import {t} from '@utils/i18n'; import mattermostBucket from 'app/mattermost_bucket'; @@ -116,7 +117,7 @@ Client4.doFetchWithResponse = async (url, options) => { Client4.setToken(token); } - const serverVersion = headers[HEADER_X_VERSION_ID] || headers[HEADER_X_VERSION_ID.toLowerCase()]; + const serverVersion = semverFromServerVersion(headers[HEADER_X_VERSION_ID] || headers[HEADER_X_VERSION_ID.toLowerCase()]); if (serverVersion && !headers['Cache-Control'] && Client4.serverVersion !== serverVersion) { Client4.serverVersion = serverVersion; /* eslint-disable-line require-atomic-updates */ EventEmitter.emit(General.SERVER_VERSION_CHANGED, serverVersion); diff --git a/app/init/fetch.test.js b/app/init/fetch.test.js index 8991cbe16b..8ef6edec81 100644 --- a/app/init/fetch.test.js +++ b/app/init/fetch.test.js @@ -29,7 +29,7 @@ describe('Fetch', () => { test('doFetchWithResponse handles title case headers', async () => { const setToken = jest.spyOn(Client4, 'setToken'); const headers = { - [HEADER_X_VERSION_ID]: 'VersionId', + [HEADER_X_VERSION_ID]: '6.1.0', [HEADER_X_CLUSTER_ID]: 'ClusterId', [HEADER_TOKEN]: 'Token', }; @@ -48,7 +48,7 @@ describe('Fetch', () => { test('doFetchWithResponse handles lower case headers', async () => { const setToken = jest.spyOn(Client4, 'setToken'); const headers = { - [HEADER_X_VERSION_ID.toLowerCase()]: 'versionid', + [HEADER_X_VERSION_ID.toLowerCase()]: '6.2.0', [HEADER_X_CLUSTER_ID.toLowerCase()]: 'clusterid', [HEADER_TOKEN.toLowerCase()]: 'token', }; @@ -66,7 +66,7 @@ describe('Fetch', () => { test('doFetchWithResponse handles server version change', async () => { const emit = jest.spyOn(EventEmitter, 'emit'); - const serverVersion1 = 'version1'; + const serverVersion1 = '6.3.0'; const response = { json: () => Promise.resolve('data'), ok: true, diff --git a/app/init/global_event_handler.js b/app/init/global_event_handler.js index a6f9a539bd..e33b617ae3 100644 --- a/app/init/global_event_handler.js +++ b/app/init/global_event_handler.js @@ -7,6 +7,7 @@ import CookieManager from '@react-native-cookies/cookies'; import {AppState, Dimensions, Keyboard, Linking, Platform} from 'react-native'; import DeviceInfo from 'react-native-device-info'; import {getLocales} from 'react-native-localize'; +import {valid as validVersion} from 'semver'; import {setDeviceDimensions, setDeviceOrientation, setDeviceAsTablet} from '@actions/device'; import {dismissAllModals, popToRoot, showOverlay} from '@actions/navigation'; @@ -249,9 +250,11 @@ class GlobalEventHandler { onServerVersionChanged = async (serverVersion) => { const {dispatch, getState} = Store.redux; const state = getState(); - if (serverVersion && state.entities.users && state.entities.users.currentUserId) { + const {general, users} = state.entities; + const isValid = validVersion(serverVersion); + const versionDidChange = general?.serverVersion !== serverVersion; + if (isValid && serverVersion && versionDidChange && users?.currentUserId) { dispatch(setServerVersion(serverVersion)); - dispatch(loadConfigAndLicense()); } }; diff --git a/app/init/global_event_handler.test.js b/app/init/global_event_handler.test.js index 9cf0d4c4cf..738381f1e8 100644 --- a/app/init/global_event_handler.test.js +++ b/app/init/global_event_handler.test.js @@ -147,6 +147,9 @@ describe('GlobalEventHandler', () => { const currentUserId = 'current-user-id'; Store.redux.getState = jest.fn().mockReturnValue({ entities: { + general: { + serverVersion: '', + }, users: { currentUserId, profiles: { @@ -164,21 +167,18 @@ describe('GlobalEventHandler', () => { const invalidVersion = 'a.b.c'; await GlobalEventHandler.onServerVersionChanged(invalidVersion); - expect(dispatch).toHaveBeenCalledTimes(2); - expect(dispatch).toHaveBeenCalledWith('setServerVersion'); - expect(dispatch).toHaveBeenCalledWith('loadConfigAndLicense'); + expect(dispatch).toHaveBeenCalledTimes(0); }); it('should dispatch on gte min server version with currentUserId', async () => { let version = minVersion.version; await GlobalEventHandler.onServerVersionChanged(version); - expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenCalledTimes(1); expect(dispatch).toHaveBeenCalledWith('setServerVersion'); - expect(dispatch).toHaveBeenCalledWith('loadConfigAndLicense'); version = semver.coerce(minVersion.major + 1).version; await GlobalEventHandler.onServerVersionChanged(version); - expect(dispatch).toHaveBeenCalledTimes(4); + expect(dispatch).toHaveBeenCalledTimes(2); }); it('should not dispatch on empty, null, undefined server version', async () => { diff --git a/app/mm-redux/reducers/entities/general.ts b/app/mm-redux/reducers/entities/general.ts index 99f0fdcc69..7f7c15ffaf 100644 --- a/app/mm-redux/reducers/entities/general.ts +++ b/app/mm-redux/reducers/entities/general.ts @@ -13,7 +13,10 @@ function config(state: Partial = {}, action: GenericAction) { return Object.assign({}, state, action.data); case UserTypes.LOGIN: // Used by the mobile app case GeneralTypes.SET_CONFIG_AND_LICENSE: - return Object.assign({}, state, action.data.config); + if (action.data.config) { + return Object.assign({}, state, action.data.config); + } + return state; case GeneralTypes.CLIENT_CONFIG_RESET: default: return state; diff --git a/app/utils/general.js b/app/utils/general.js index b3194a3dd2..1ce2efb56a 100644 --- a/app/utils/general.js +++ b/app/utils/general.js @@ -110,3 +110,17 @@ export function permalinkBadTeam(intl) { alertErrorWithFallback(intl, {}, message); } + +export function semverFromServerVersion(value) { + if (!value || typeof value !== 'string') { + return undefined; + } + + const split = value.split('.'); + + const major = parseInt(split[0], 10); + const minor = parseInt(split[1] || '0', 10); + const patch = parseInt(split[2] || '0', 10); + + return `${major}.${minor}.${patch}`; +}