diff --git a/webapp/channels/src/actions/views/root.test.ts b/webapp/channels/src/actions/views/root.test.ts index 9534709653..907d5351d8 100644 --- a/webapp/channels/src/actions/views/root.test.ts +++ b/webapp/channels/src/actions/views/root.test.ts @@ -7,23 +7,6 @@ import * as i18nSelectors from 'selectors/i18n'; import mockStore from 'tests/test_store'; import {ActionTypes} from 'utils/constants'; -jest.mock('mattermost-redux/actions/general', () => { - const original = jest.requireActual('mattermost-redux/actions/general'); - return { - ...original, - getClientConfig: () => ({type: 'MOCK_GET_CLIENT_CONFIG'}), - getLicenseConfig: () => ({type: 'MOCK_GET_LICENSE_CONFIG'}), - }; -}); - -jest.mock('mattermost-redux/actions/users', () => { - const original = jest.requireActual('mattermost-redux/actions/users'); - return { - ...original, - loadMe: () => ({type: 'MOCK_LOAD_ME'}), - }; -}); - describe('root view actions', () => { const origCookies = document.cookie; const origWasLoggedIn = localStorage.getItem('was_logged_in'); @@ -38,25 +21,6 @@ describe('root view actions', () => { localStorage.setItem('was_logged_in', origWasLoggedIn || ''); }); - describe('loadConfigAndMe', () => { - test('loadConfigAndMe, without user logged in', async () => { - const testStore = mockStore({}); - - await testStore.dispatch(Actions.loadConfigAndMe()); - expect(testStore.getActions()).toEqual([{type: 'MOCK_GET_CLIENT_CONFIG'}, {type: 'MOCK_GET_LICENSE_CONFIG'}]); - }); - - test('loadConfigAndMe, with user logged in', async () => { - const testStore = mockStore({}); - - document.cookie = 'MMUSERID=userid'; - localStorage.setItem('was_logged_in', 'true'); - - await testStore.dispatch(Actions.loadConfigAndMe()); - expect(testStore.getActions()).toEqual([{type: 'MOCK_GET_CLIENT_CONFIG'}, {type: 'MOCK_GET_LICENSE_CONFIG'}, {type: 'MOCK_LOAD_ME'}]); - }); - }); - describe('registerPluginTranslationsSource', () => { test('Should not dispatch action when getTranslation is empty', () => { const testStore = mockStore({}); diff --git a/webapp/channels/src/actions/views/root.ts b/webapp/channels/src/actions/views/root.ts index d3e98e6baa..6d492ef44f 100644 --- a/webapp/channels/src/actions/views/root.ts +++ b/webapp/channels/src/actions/views/root.ts @@ -1,10 +1,6 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import type {ClientConfig} from '@mattermost/types/config'; - -import {getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general'; -import {loadMe} from 'mattermost-redux/actions/users'; import {Client4} from 'mattermost-redux/client'; import type {ActionFuncAsync, ThunkActionFunc} from 'mattermost-redux/types/actions'; @@ -20,26 +16,6 @@ const pluginTranslationSources: Record = {}; export type TranslationPluginFunction = (locale: string) => Translations -export function loadConfigAndMe(): ThunkActionFunc> { - return async (dispatch) => { - const results = await Promise.all([ - dispatch(getClientConfig()), - dispatch(getLicenseConfig()), - ]); - - let isMeLoaded = false; - if (document.cookie.includes('MMUSERID=')) { - const dataFromLoadMe = await dispatch(loadMe()); - isMeLoaded = dataFromLoadMe?.data ?? false; - } - - return { - config: results[0].data, - isMeLoaded, - }; - }; -} - export function registerPluginTranslationsSource(pluginId: string, sourceFunction: TranslationPluginFunction): ThunkActionFunc { pluginTranslationSources[pluginId] = sourceFunction; return (dispatch, getState) => { @@ -91,19 +67,3 @@ export function loadTranslations(locale: string, url: string): ActionFuncAsync { return {data: true}; }; } - -export function registerCustomPostRenderer(type: string, component: any, id: string): ActionFuncAsync { - return async (dispatch) => { - // piggyback on plugins state to register a custom post renderer - dispatch({ - type: ActionTypes.RECEIVED_PLUGIN_POST_COMPONENT, - data: { - postTypeId: id, - pluginId: id, - type, - component, - }, - }); - return {data: true}; - }; -} diff --git a/webapp/channels/src/components/logged_in/logged_in.tsx b/webapp/channels/src/components/logged_in/logged_in.tsx index f3f034225b..b061c7871f 100644 --- a/webapp/channels/src/components/logged_in/logged_in.tsx +++ b/webapp/channels/src/components/logged_in/logged_in.tsx @@ -17,7 +17,8 @@ import Constants from 'utils/constants'; import DesktopApp from 'utils/desktop_api'; import {isKeyPressed} from 'utils/keyboard'; import {getBrowserTimezone} from 'utils/timezone'; -import * as UserAgent from 'utils/user_agent'; +import {isAndroid, isIos} from 'utils/user_agent'; +import {doesCookieContainsMMUserId} from 'utils/utils'; declare global { interface Window { @@ -89,9 +90,9 @@ export default class LoggedIn extends React.PureComponent { }; // Device tracking setup - if (UserAgent.isIos()) { + if (isIos()) { document.body.classList.add('ios'); - } else if (UserAgent.isAndroid()) { + } else if (isAndroid()) { document.body.classList.add('android'); } @@ -202,7 +203,7 @@ export default class LoggedIn extends React.PureComponent { private handleBeforeUnload = (): void => { // remove the event listener to prevent getting stuck in a loop window.removeEventListener('beforeunload', this.handleBeforeUnload); - if (document.cookie.indexOf('MMUSERID=') > -1 && this.props.currentChannelId && !this.props.isCurrentChannelManuallyUnread) { + if (doesCookieContainsMMUserId() && this.props.currentChannelId && !this.props.isCurrentChannelManuallyUnread) { this.props.actions.updateApproximateViewTime(this.props.currentChannelId); } WebSocketActions.close(); diff --git a/webapp/channels/src/components/root/__snapshots__/root.test.tsx.snap b/webapp/channels/src/components/root/__snapshots__/root.test.tsx.snap deleted file mode 100644 index 81659831d8..0000000000 --- a/webapp/channels/src/components/root/__snapshots__/root.test.tsx.snap +++ /dev/null @@ -1,139 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`components/Root Routes Should mount public product routes 1`] = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - -
- - - -
-
-
-`; diff --git a/webapp/channels/src/components/root/actions.ts b/webapp/channels/src/components/root/actions.ts deleted file mode 100644 index 4a7606a2c7..0000000000 --- a/webapp/channels/src/components/root/actions.ts +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import type {History} from 'history'; - -import type {UserProfile} from '@mattermost/types/users'; - -import {getFirstAdminSetupComplete} from 'mattermost-redux/actions/general'; -import {getProfiles} from 'mattermost-redux/actions/users'; -import {General} from 'mattermost-redux/constants'; -import {getIsOnboardingFlowEnabled} from 'mattermost-redux/selectors/entities/preferences'; -import {getActiveTeamsList} from 'mattermost-redux/selectors/entities/teams'; -import {checkIsFirstAdmin, getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users'; -import type {ThunkActionFunc} from 'mattermost-redux/types/actions'; - -import * as GlobalActions from 'actions/global_actions'; - -import {StoragePrefixes} from 'utils/constants'; - -export function redirectToOnboardingOrDefaultTeam(history: History): ThunkActionFunc { - return async (dispatch, getState) => { - const state = getState(); - const isUserAdmin = isCurrentUserSystemAdmin(state); - if (!isUserAdmin) { - GlobalActions.redirectUserToDefaultTeam(); - return; - } - - const teams = getActiveTeamsList(state); - - const onboardingFlowEnabled = getIsOnboardingFlowEnabled(state); - - if (teams.length > 0 || !onboardingFlowEnabled) { - GlobalActions.redirectUserToDefaultTeam(); - return; - } - - const firstAdminSetupComplete = await dispatch(getFirstAdminSetupComplete()); - if (firstAdminSetupComplete?.data) { - GlobalActions.redirectUserToDefaultTeam(); - return; - } - - const profilesResult = await dispatch(getProfiles(0, General.PROFILE_CHUNK_SIZE, {roles: General.SYSTEM_ADMIN_ROLE})); - if (profilesResult.error) { - GlobalActions.redirectUserToDefaultTeam(); - return; - } - const currentUser = getCurrentUser(getState()); - const adminProfiles = profilesResult.data?.reduce( - (acc: Record, curr: UserProfile) => { - acc[curr.id] = curr; - return acc; - }, - {}, - ); - if (adminProfiles && checkIsFirstAdmin(currentUser, adminProfiles)) { - history.push('/preparing-workspace'); - return; - } - - GlobalActions.redirectUserToDefaultTeam(); - }; -} - -export function handleLoginLogoutSignal(e: StorageEvent): ThunkActionFunc { - return (dispatch, getState) => { - // when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out - const isNewLocalStorageEvent = (event: StorageEvent) => event.storageArea === localStorage && event.newValue; - - if (e.key === StoragePrefixes.LOGOUT && isNewLocalStorageEvent(e)) { - console.log('detected logout from a different tab'); //eslint-disable-line no-console - GlobalActions.emitUserLoggedOutEvent('/', false, false); - } - if (e.key === StoragePrefixes.LOGIN && isNewLocalStorageEvent(e)) { - const isLoggedIn = getCurrentUser(getState()); - - // make sure this is not the same tab which sent login signal - // because another tabs will also send login signal after reloading - if (isLoggedIn) { - return; - } - - // detected login from a different tab - function reloadOnFocus() { - location.reload(); - } - window.addEventListener('focus', reloadOnFocus); - } - }; -} diff --git a/webapp/channels/src/components/root/actions/index.test.ts b/webapp/channels/src/components/root/actions/index.test.ts new file mode 100644 index 0000000000..47c35dd3d7 --- /dev/null +++ b/webapp/channels/src/components/root/actions/index.test.ts @@ -0,0 +1,92 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import mockStore from 'tests/test_store'; + +import {loadConfigAndMe} from './index'; + +jest.mock('mattermost-redux/actions/general', () => { + const original = jest.requireActual('mattermost-redux/actions/general'); + return { + ...original, + getClientConfig: () => ({type: 'MOCK_GET_CLIENT_CONFIG'}), + getLicenseConfig: () => ({type: 'MOCK_GET_LICENSE_CONFIG'}), + }; +}); + +jest.mock('mattermost-redux/actions/users', () => { + const original = jest.requireActual('mattermost-redux/actions/users'); + return { + ...original, + getMe: () => ({type: 'MOCK_LOAD_ME'}), + }; +}); + +jest.mock('mattermost-redux/actions/preferences', () => { + const original = jest.requireActual('mattermost-redux/actions/preferences'); + return { + ...original, + getMyPreferences: () => ({type: 'MOCK_LOAD_PREFERENCES'}), + }; +}); + +jest.mock('mattermost-redux/actions/teams', () => { + const original = jest.requireActual('mattermost-redux/actions/teams'); + return { + ...original, + getMyTeamMembers: () => ({type: 'MOCK_GET_MY_TEAM_MEMBERS'}), + getMyTeams: () => ({type: 'MOCK_GET_MY_TEAMS'}), + getMyTeamUnreads: () => ({type: 'MOCK_GET_MY_TEAM_UNREADS'}), + }; +}); + +jest.mock('mattermost-redux/selectors/entities/preferences', () => { + const original = jest.requireActual('mattermost-redux/selectors/entities/preferences'); + return { + ...original, + isCollapsedThreadsEnabled: () => false, + }; +}); + +jest.mock('mattermost-redux/actions/limits', () => ({ + ...jest.requireActual('mattermost-redux/actions/limits'), + getServerLimits: () => ({type: 'MOCK_GET_SERVER_LIMITS'}), +})); + +describe('loadConfigAndMe', () => { + test('loadConfigAndMe, without user logged in', async () => { + const testStore = mockStore({}); + + await testStore.dispatch(loadConfigAndMe()); + expect(testStore.getActions()).toEqual([{type: 'MOCK_GET_CLIENT_CONFIG'}, {type: 'MOCK_GET_LICENSE_CONFIG'}]); + }); + + test('loadConfigAndMe, with user logged in', async () => { + const testStore = mockStore({ + entities: { + general: { + serverVersion: '1.0.0', + }, + users: { + currentUserId: 'userid', + }, + }, + }); + + document.cookie = 'MMUSERID=userid'; + localStorage.setItem('was_logged_in', 'true'); + + await testStore.dispatch(loadConfigAndMe()); + expect(testStore.getActions()).toEqual([ + {type: 'MOCK_GET_CLIENT_CONFIG'}, + {type: 'MOCK_GET_LICENSE_CONFIG'}, + {type: 'RECEIVED_SERVER_VERSION', data: '1.0.0'}, + {type: 'MOCK_LOAD_ME'}, + {type: 'MOCK_LOAD_PREFERENCES'}, + {type: 'MOCK_GET_MY_TEAMS'}, + {type: 'MOCK_GET_MY_TEAM_MEMBERS'}, + {type: 'MOCK_GET_MY_TEAM_UNREADS'}, + {type: 'MOCK_GET_SERVER_LIMITS'}, + ]); + }); +}); diff --git a/webapp/channels/src/components/root/actions/index.ts b/webapp/channels/src/components/root/actions/index.ts new file mode 100644 index 0000000000..3a4d91d09d --- /dev/null +++ b/webapp/channels/src/components/root/actions/index.ts @@ -0,0 +1,173 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import type {History} from 'history'; + +import type {ServerError} from '@mattermost/types/errors'; +import type {UserProfile} from '@mattermost/types/users'; + +import {GeneralTypes} from 'mattermost-redux/action_types'; +import {logError} from 'mattermost-redux/actions/errors'; +import {getClientConfig, getLicenseConfig, getFirstAdminSetupComplete} from 'mattermost-redux/actions/general'; +import {getServerLimits} from 'mattermost-redux/actions/limits'; +import {getMyPreferences} from 'mattermost-redux/actions/preferences'; +import {getMyTeamMembers, getMyTeams, getMyTeamUnreads} from 'mattermost-redux/actions/teams'; +import {getMe, getProfiles} from 'mattermost-redux/actions/users'; +import {Client4} from 'mattermost-redux/client'; +import {General} from 'mattermost-redux/constants'; +import {isCollapsedThreadsEnabled, getIsOnboardingFlowEnabled} from 'mattermost-redux/selectors/entities/preferences'; +import {getActiveTeamsList} from 'mattermost-redux/selectors/entities/teams'; +import {checkIsFirstAdmin, getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users'; +import type {ActionFuncAsync, ThunkActionFunc} from 'mattermost-redux/types/actions'; + +import {redirectUserToDefaultTeam, emitUserLoggedOutEvent} from 'actions/global_actions'; + +import {ActionTypes, StoragePrefixes} from 'utils/constants'; +import {doesCookieContainsMMUserId} from 'utils/utils'; + +import type {Translations} from 'types/store/i18n'; + +export type TranslationPluginFunction = (locale: string) => Translations + +/** + * This function meant to be used in root.tsx component loads config, license and if user is logged in, it loads user and its related data. + */ +export function loadConfigAndMe(): ThunkActionFunc> { + return async (dispatch, getState) => { + // attempt to load config and license regardless if user is logged in or not + try { + await Promise.all([ + dispatch(getClientConfig()), + dispatch(getLicenseConfig()), + ]); + } catch (error) { + dispatch(logError(error as ServerError)); + return { + isLoaded: false, + }; + } + + // Return early if user is not logged in + if (!doesCookieContainsMMUserId()) { + return { + isLoaded: true, + isMeRequested: false, + }; + } + + // Load user and its related data now that we know that user is logged in + const serverVersion = getState().entities.general.serverVersion || Client4.getServerVersion(); + dispatch({type: GeneralTypes.RECEIVED_SERVER_VERSION, data: serverVersion}); + + try { + await Promise.all([ + dispatch(getMe()), + dispatch(getMyPreferences()), + dispatch(getMyTeams()), + dispatch(getMyTeamMembers()), + ]); + + dispatch(getMyTeamUnreads(isCollapsedThreadsEnabled(getState()))); + dispatch(getServerLimits()); + } catch (error) { + dispatch(logError(error as ServerError)); + return { + isLoaded: false, + }; + } + + return { + isLoaded: true, + isMeRequested: true, + }; + }; +} + +export function registerCustomPostRenderer(type: string, component: any, id: string): ActionFuncAsync { + return async (dispatch) => { + // piggyback on plugins state to register a custom post renderer + dispatch({ + type: ActionTypes.RECEIVED_PLUGIN_POST_COMPONENT, + data: { + postTypeId: id, + pluginId: id, + type, + component, + }, + }); + return {data: true}; + }; +} + +export function redirectToOnboardingOrDefaultTeam(history: History): ThunkActionFunc { + return async (dispatch, getState) => { + const state = getState(); + const isUserAdmin = isCurrentUserSystemAdmin(state); + if (!isUserAdmin) { + redirectUserToDefaultTeam(); + return; + } + + const teams = getActiveTeamsList(state); + + const onboardingFlowEnabled = getIsOnboardingFlowEnabled(state); + + if (teams.length > 0 || !onboardingFlowEnabled) { + redirectUserToDefaultTeam(); + return; + } + + const firstAdminSetupComplete = await dispatch(getFirstAdminSetupComplete()); + if (firstAdminSetupComplete?.data) { + redirectUserToDefaultTeam(); + return; + } + + const profilesResult = await dispatch(getProfiles(0, General.PROFILE_CHUNK_SIZE, {roles: General.SYSTEM_ADMIN_ROLE})); + if (profilesResult.error) { + redirectUserToDefaultTeam(); + return; + } + const currentUser = getCurrentUser(getState()); + const adminProfiles = profilesResult.data?.reduce( + (acc: Record, curr: UserProfile) => { + acc[curr.id] = curr; + return acc; + }, + {}, + ); + if (adminProfiles && checkIsFirstAdmin(currentUser, adminProfiles)) { + history.push('/preparing-workspace'); + return; + } + + redirectUserToDefaultTeam(); + }; +} + +export function handleLoginLogoutSignal(e: StorageEvent): ThunkActionFunc { + return (dispatch, getState) => { + // when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out + const isNewLocalStorageEvent = (event: StorageEvent) => event.storageArea === localStorage && event.newValue; + + if (e.key === StoragePrefixes.LOGOUT && isNewLocalStorageEvent(e)) { + console.log('detected logout from a different tab'); //eslint-disable-line no-console + emitUserLoggedOutEvent('/', false, false); + } + if (e.key === StoragePrefixes.LOGIN && isNewLocalStorageEvent(e)) { + const isLoggedIn = getCurrentUser(getState()); + + // make sure this is not the same tab which sent login signal + // because another tabs will also send login signal after reloading + if (isLoggedIn) { + return; + } + + // detected login from a different tab + function reloadOnFocus() { + location.reload(); + } + window.addEventListener('focus', reloadOnFocus); + } + }; +} diff --git a/webapp/channels/src/components/root/index.ts b/webapp/channels/src/components/root/index.ts index c3717947f6..8060038e32 100644 --- a/webapp/channels/src/components/root/index.ts +++ b/webapp/channels/src/components/root/index.ts @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import isEmpty from 'lodash/isEmpty'; +import type {ConnectedProps} from 'react-redux'; import {connect} from 'react-redux'; import {withRouter} from 'react-router-dom'; import {bindActionCreators} from 'redux'; @@ -15,7 +17,6 @@ import {getTeam} from 'mattermost-redux/selectors/entities/teams'; import {shouldShowTermsOfService, getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {loadRecentlyUsedCustomEmojis, migrateRecentEmojis} from 'actions/emoji_actions'; -import {loadConfigAndMe, registerCustomPostRenderer} from 'actions/views/root'; import {getShowLaunchingWorkspace} from 'selectors/onboarding'; import {shouldShowAppBar} from 'selectors/plugins'; import { @@ -29,7 +30,12 @@ import {initializeProducts} from 'plugins/products'; import type {GlobalState} from 'types/store/index'; -import {handleLoginLogoutSignal, redirectToOnboardingOrDefaultTeam} from './actions'; +import { + loadConfigAndMe, + registerCustomPostRenderer, + handleLoginLogoutSignal, + redirectToOnboardingOrDefaultTeam, +} from './actions'; import Root from './root'; function mapStateToProps(state: GlobalState) { @@ -42,11 +48,16 @@ function mapStateToProps(state: GlobalState) { const teamId = LocalStorageStore.getPreviousTeamId(userId); const permalinkRedirectTeam = getTeam(state, teamId!); + const isConfigLoaded = config && !isEmpty(config); + return { theme: getTheme(state), + isConfigLoaded, telemetryEnabled: config.DiagnosticsEnabled === 'true', noAccounts: config.NoAccounts === 'true', telemetryId: config.DiagnosticId, + serviceEnvironment: config.ServiceEnvironment, + siteURL: config.SiteURL, iosDownloadLink: config.IosAppDownloadLink, androidDownloadLink: config.AndroidAppDownloadLink, appDownloadLink: config.AppDownloadLink, @@ -79,4 +90,8 @@ function mapDispatchToProps(dispatch: Dispatch) { }; } -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Root)); +const connector = connect(mapStateToProps, mapDispatchToProps); + +export type PropsFromRedux = ConnectedProps; + +export default withRouter(connector(Root)); diff --git a/webapp/channels/src/components/root/root.test.tsx b/webapp/channels/src/components/root/root.test.tsx index 461a111e68..a470edeace 100644 --- a/webapp/channels/src/components/root/root.test.tsx +++ b/webapp/channels/src/components/root/root.test.tsx @@ -1,11 +1,9 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {shallow} from 'enzyme'; import React from 'react'; import type {RouteComponentProps} from 'react-router-dom'; import {bindActionCreators} from 'redux'; -import rudderAnalytics from 'rudder-sdk-js'; import {ServiceEnvironment} from '@mattermost/types/config'; @@ -14,40 +12,56 @@ import type {Theme} from 'mattermost-redux/selectors/entities/preferences'; import * as GlobalActions from 'actions/global_actions'; -import Root from 'components/root/root'; - import testConfigureStore from 'packages/mattermost-redux/test/test_store'; +import {renderWithContext, waitFor} from 'tests/react_testing_utils'; import {StoragePrefixes} from 'utils/constants'; -import type {ProductComponent} from 'types/store/plugins'; - import {handleLoginLogoutSignal, redirectToOnboardingOrDefaultTeam} from './actions'; +import type {Props} from './root'; +import Root from './root'; -jest.mock('rudder-sdk-js', () => ({ - identify: jest.fn(), - load: jest.fn(), - page: jest.fn(), - ready: jest.fn((callback) => callback()), - track: jest.fn(), +jest.mock('mattermost-redux/client/rudder', () => ({ + rudderAnalytics: { + identify: jest.fn(), + load: jest.fn(), + page: jest.fn(), + ready: jest.fn((callback) => callback()), // Default behavior: calls the callback + track: jest.fn(), + }, + RudderTelemetryHandler: jest.fn(), })); +jest.mock('mattermost-redux/client/rudder', () => { + const actual = jest.requireActual('mattermost-redux/client/rudder'); + return { + ...actual, + rudderAnalytics: { + ...actual.rudderAnalytics, + ready: jest.fn((callback) => callback()), + }, + }; +}); + jest.mock('actions/telemetry_actions'); -jest.mock('actions/global_actions', () => ({ - redirectUserToDefaultTeam: jest.fn(), -})); +jest.mock('components/announcement_bar', () => () =>
); +jest.mock('components/team_sidebar', () => () =>
); +jest.mock('components/mobile_view_watcher', () => () =>
); +jest.mock('./performance_reporter_controller', () => () =>
); jest.mock('utils/utils', () => { const original = jest.requireActual('utils/utils'); return { ...original, - localizeMessage: () => {}, applyTheme: jest.fn(), - makeIsEligibleForClick: jest.fn(), }; }); +jest.mock('actions/global_actions', () => ({ + redirectUserToDefaultTeam: jest.fn(), +})); + jest.mock('mattermost-redux/actions/general', () => ({ getFirstAdminSetupComplete: jest.fn(() => Promise.resolve({ type: 'FIRST_ADMIN_COMPLETE_SETUP_RECEIVED', @@ -59,23 +73,37 @@ jest.mock('mattermost-redux/actions/general', () => ({ describe('components/Root', () => { const store = testConfigureStore(); - const baseProps = { - telemetryEnabled: true, - telemetryId: '1234ab', - noAccounts: false, - showTermsOfService: false, + const baseProps: Props = { theme: {} as Theme, + isConfigLoaded: true, + telemetryEnabled: true, + noAccounts: false, + telemetryId: '1234ab', + serviceEnvironment: undefined, + siteURL: 'http://localhost:8065', + iosDownloadLink: undefined, + androidDownloadLink: undefined, + appDownloadLink: undefined, + showTermsOfService: false, + plugins: [], + products: [], + showLaunchingWorkspace: false, + rhsIsExpanded: false, + rhsIsOpen: false, + rhsState: null, + shouldShowAppBar: false, + isCloud: false, actions: { loadConfigAndMe: jest.fn().mockImplementation(() => { return Promise.resolve({ - config: {}, - isMeLoaded: false, + isLoaded: true, + isMeRequested: false, }); }), + getFirstAdminSetupComplete: jest.fn(), getProfiles: jest.fn(), loadRecentlyUsedCustomEmojis: jest.fn(), migrateRecentEmojis: jest.fn(), - savePreferences: jest.fn(), registerCustomPostRenderer: jest.fn(), initializeProducts: jest.fn(), ...bindActionCreators({ @@ -84,37 +112,62 @@ describe('components/Root', () => { }, store.dispatch), }, permalinkRedirectTeamName: 'myTeam', - showLaunchingWorkspace: false, - plugins: [], - products: [], ...{ location: { pathname: '/', }, - } as RouteComponentProps, - isCloud: false, - rhsIsExpanded: false, - rhsIsOpen: false, - shouldShowAppBar: false, - }; - - test('should load config and license on mount and redirect to sign-up page', () => { - const props = { - ...baseProps, - noAccounts: true, history: { push: jest.fn(), } as unknown as RouteComponentProps['history'], - }; + } as RouteComponentProps, + }; - const wrapper = shallow(); + let originalMatchMedia: (query: string) => MediaQueryList; + let originalReload: () => void; - wrapper.instance().onConfigLoaded({}); - expect(props.history.push).toHaveBeenCalledWith('/signup_user_complete'); - wrapper.unmount(); + beforeAll(() => { + originalMatchMedia = window.matchMedia; + originalReload = window.location.reload; + + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + })), + }); + + Object.defineProperty(window.location, 'reload', { + configurable: true, + writable: true, + }); + + window.location.reload = jest.fn(); }); - test('should load user, config, and license on mount and redirect to defaultTeam on success', (done) => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + afterAll(() => { + window.matchMedia = originalMatchMedia; + window.location.reload = originalReload; + }); + + test('should load config and license on mount and redirect to sign-up page', async () => { + const props = { + ...baseProps, + noAccounts: true, + }; + + renderWithContext(); + + await waitFor(() => { + expect(props.history.push).toHaveBeenCalledWith('/signup_user_complete'); + }); + }); + + test('should load user, config, and license on mount and redirect to defaultTeam on success', async () => { document.cookie = 'MMUSERID=userid'; localStorage.setItem('was_logged_in', 'true'); @@ -124,28 +177,22 @@ describe('components/Root', () => { ...baseProps.actions, loadConfigAndMe: jest.fn().mockImplementation(() => { return Promise.resolve({ - config: {}, - isMeLoaded: true, + isLoaded: true, + isMeRequested: true, }); }), }, }; - // Mock the method by extending the class because we don't have a chance to do it before shallow mounts the component - class MockedRoot extends Root { - onConfigLoaded = jest.fn(() => { - expect(this.onConfigLoaded).toHaveBeenCalledTimes(1); - expect(GlobalActions.redirectUserToDefaultTeam).toHaveBeenCalledTimes(1); - expect(props.actions.loadConfigAndMe).toHaveBeenCalledTimes(1); - done(); - }); - } + renderWithContext(); - const wrapper = shallow(); - wrapper.unmount(); + await waitFor(() => { + expect(props.actions.loadConfigAndMe).toHaveBeenCalledTimes(1); + expect(GlobalActions.redirectUserToDefaultTeam).toHaveBeenCalledTimes(1); + }); }); - test('should load user, config, and license on mount and should not redirect to defaultTeam id pathname is not root', (done) => { + test('should load user, config, and license on mount and should not redirect to defaultTeam id pathname is not root', async () => { document.cookie = 'MMUSERID=userid'; localStorage.setItem('was_logged_in', 'true'); @@ -158,25 +205,19 @@ describe('components/Root', () => { ...baseProps.actions, loadConfigAndMe: jest.fn().mockImplementation(() => { return Promise.resolve({ - config: {}, - isMeLoaded: true, + isLoaded: true, + isMeRequested: true, }); }), }, }; - // Mock the method by extending the class because we don't have a chance to do it before shallow mounts the component - class MockedRoot extends Root { - onConfigLoaded = jest.fn(() => { - expect(this.onConfigLoaded).toHaveBeenCalledTimes(1); - expect(GlobalActions.redirectUserToDefaultTeam).not.toHaveBeenCalled(); - expect(props.actions.loadConfigAndMe).toHaveBeenCalledTimes(1); - done(); - }); - } + renderWithContext(); - const wrapper = shallow(); - wrapper.unmount(); + await waitFor(() => { + expect(props.actions.loadConfigAndMe).toHaveBeenCalledTimes(1); + expect(GlobalActions.redirectUserToDefaultTeam).not.toHaveBeenCalled(); + }); }); test('should call history on props change', () => { @@ -187,23 +228,24 @@ describe('components/Root', () => { push: jest.fn(), } as unknown as RouteComponentProps['history'], }; - const wrapper = shallow(); + + const {rerender} = renderWithContext(); + expect(props.history.push).not.toHaveBeenCalled(); + const props2 = { + ...props, noAccounts: true, }; - wrapper.setProps(props2); + + rerender(); + expect(props.history.push).toHaveBeenLastCalledWith('/signup_user_complete'); - wrapper.unmount(); }); test('should reload on focus after getting signal login event from another tab', () => { - Object.defineProperty(window.location, 'reload', { - configurable: true, - writable: true, - }); - window.location.reload = jest.fn(); - const wrapper = shallow(); + renderWithContext(); + const loginSignal = new StorageEvent('storage', { key: StoragePrefixes.LOGIN, newValue: String(Math.random()), @@ -212,89 +254,68 @@ describe('components/Root', () => { window.dispatchEvent(loginSignal); window.dispatchEvent(new Event('focus')); + expect(window.location.reload).toBeCalledTimes(1); - wrapper.unmount(); }); - describe('onConfigLoaded', () => { - afterEach(() => { - Client4.telemetryHandler = undefined; + test('should not set a TelemetryHandler when onConfigLoaded is called if Rudder is not configured', async () => { + const props = { + ...baseProps, + serviceEnvironment: ServiceEnvironment.DEV, + actions: { + ...baseProps.actions, + loadConfigAndMe: jest.fn().mockImplementation(() => { + return Promise.resolve({ + isLoaded: true, + isMeRequested: true, + }); + }), + }, + }; + + renderWithContext(); + + // Wait for the component to load config and call onConfigLoaded + await waitFor(() => { + expect(props.actions.loadConfigAndMe).toHaveBeenCalledTimes(1); }); - test('should not set a TelemetryHandler when onConfigLoaded is called if Rudder is not configured', () => { - const wrapper = shallow(); + Client4.trackEvent('category', 'event'); - wrapper.instance().onConfigLoaded({ - ServiceEnvironment: ServiceEnvironment.DEV, - }); - - Client4.trackEvent('category', 'event'); - - expect(Client4.telemetryHandler).not.toBeDefined(); - - wrapper.unmount(); - }); - - test('should set a TelemetryHandler when onConfigLoaded is called if Rudder is configured', () => { - const wrapper = shallow(); - - wrapper.instance().onConfigLoaded({ - ServiceEnvironment: ServiceEnvironment.TEST, - }); - - Client4.trackEvent('category', 'event'); - - expect(Client4.telemetryHandler).toBeDefined(); - - wrapper.unmount(); - }); - - test('should not set a TelemetryHandler when onConfigLoaded is called but Rudder has been blocked', () => { - (rudderAnalytics.ready as any).mockImplementation(() => { - // Simulate an error occurring and the callback not getting called - }); - - const wrapper = shallow(); - - wrapper.instance().onConfigLoaded({ - ServiceEnvironment: ServiceEnvironment.PRODUCTION, - }); - - Client4.trackEvent('category', 'event'); - - expect(Client4.telemetryHandler).not.toBeDefined(); - - wrapper.unmount(); - }); + expect(Client4.telemetryHandler).not.toBeDefined(); }); - describe('Routes', () => { - test('Should mount public product routes', () => { - const mainComponent = () => (

{'TestMainComponent'}

); - const publicComponent = () => (

{'TestPublicProduct'}

); + test('should set a TelemetryHandler when onConfigLoaded is called if Rudder is configured', async () => { + const props = { + ...baseProps, + isConfigLoaded: false, + serviceEnvironment: ServiceEnvironment.TEST, + actions: { + ...baseProps.actions, + loadConfigAndMe: jest.fn().mockImplementation(() => { + return Promise.resolve({ + isLoaded: true, + isMeRequested: true, + }); + }), + }, + }; - const props = { - ...baseProps, - products: [{ - id: 'productwithpublic', - baseURL: '/productwithpublic', - mainComponent, - publicComponent, - } as unknown as ProductComponent, - { - id: 'productwithoutpublic', - baseURL: '/productwithoutpublic', - mainComponent, - publicComponent: null, - } as unknown as ProductComponent], - }; + const {rerender} = renderWithContext(); - const wrapper = shallow(); - - wrapper.instance().setState({configLoaded: true}); - expect(wrapper).toMatchSnapshot(); - wrapper.unmount(); + // Wait for the component to load config and call onConfigLoaded + await waitFor(() => { + expect(props.actions.loadConfigAndMe).toHaveBeenCalledTimes(1); }); + + const props2 = { + ...props, + isConfigLoaded: true, + }; + + rerender(); + + expect(Client4.telemetryHandler).toBeDefined(); }); describe('showLandingPageIfNecessary', () => { @@ -309,19 +330,17 @@ describe('components/Root', () => { search: '', }, } as RouteComponentProps, - history: { - push: jest.fn(), - } as unknown as RouteComponentProps['history'], }; - test('should show for normal cases', () => { - const wrapper = shallow(); - wrapper.instance().onConfigLoaded({}); - expect(landingProps.history.push).toHaveBeenCalledWith('/landing#/'); - wrapper.unmount(); + test('should show for normal cases', async () => { + renderWithContext(); + + await waitFor(() => { + expect(landingProps.history.push).toHaveBeenCalledWith('/landing#/'); + }); }); - test('should not show for Desktop App login flow', () => { + test('should not show for Desktop App login flow', async () => { const props = { ...landingProps, ...{ @@ -330,10 +349,12 @@ describe('components/Root', () => { }, } as RouteComponentProps, }; - const wrapper = shallow(); - wrapper.instance().onConfigLoaded({}); - expect(props.history.push).not.toHaveBeenCalled(); - wrapper.unmount(); + + renderWithContext(); + + await waitFor(() => { + expect(props.history.push).not.toHaveBeenCalled(); + }); }); }); }); diff --git a/webapp/channels/src/components/root/root.tsx b/webapp/channels/src/components/root/root.tsx index 371ed25e12..6d265278b9 100644 --- a/webapp/channels/src/components/root/root.tsx +++ b/webapp/channels/src/components/root/root.tsx @@ -3,12 +3,10 @@ import classNames from 'classnames'; import deepEqual from 'fast-deep-equal'; -import type {History} from 'history'; import React from 'react'; import {Route, Switch, Redirect} from 'react-router-dom'; import type {RouteComponentProps} from 'react-router-dom'; -import type {ClientConfig} from '@mattermost/types/config'; import {ServiceEnvironment} from '@mattermost/types/config'; import {setSystemEmojis} from 'mattermost-redux/actions/emojis'; @@ -16,7 +14,6 @@ import {setUrl} from 'mattermost-redux/actions/general'; import {Client4} from 'mattermost-redux/client'; import {rudderAnalytics, RudderTelemetryHandler} from 'mattermost-redux/client/rudder'; import type {Theme} from 'mattermost-redux/selectors/entities/preferences'; -import type {ActionResult} from 'mattermost-redux/types/actions'; import {measurePageLoadTelemetry, temporarilySetPageLoadContext, trackEvent, trackSelectorMetrics} from 'actions/telemetry_actions.jsx'; import BrowserStore from 'stores/browser_store'; @@ -52,13 +49,13 @@ import {getSiteURL} from 'utils/url'; import * as UserAgent from 'utils/user_agent'; import * as Utils from 'utils/utils'; -import type {ProductComponent, PluginComponent} from 'types/store/plugins'; - import LuxonController from './luxon_controller'; import PerformanceReporterController from './performance_reporter_controller'; import RootProvider from './root_provider'; import RootRedirect from './root_redirect'; +import type {PropsFromRedux} from './index'; + import 'plugins/export.js'; const LazyErrorPage = React.lazy(() => import('components/error_page')); @@ -126,51 +123,19 @@ function LoggedInRoute(props: LoggedInRouteProps) { const noop = () => {}; -export type Actions = { - getProfiles: (page?: number, pageSize?: number, options?: Record) => Promise; - loadRecentlyUsedCustomEmojis: () => Promise; - migrateRecentEmojis: () => void; - loadConfigAndMe: () => Promise<{config?: Partial; isMeLoaded: boolean}>; - registerCustomPostRenderer: (type: string, component: any, id: string) => Promise; - initializeProducts: () => Promise; - handleLoginLogoutSignal: (e: StorageEvent) => unknown; - redirectToOnboardingOrDefaultTeam: (history: History) => unknown; -} - -type Props = { - theme: Theme; - telemetryEnabled: boolean; - telemetryId?: string; - iosDownloadLink?: string; - androidDownloadLink?: string; - appDownloadLink?: string; - noAccounts: boolean; - showTermsOfService: boolean; - permalinkRedirectTeamName: string; - isCloud: boolean; - actions: Actions; - plugins?: PluginComponent[]; - products: ProductComponent[]; - showLaunchingWorkspace: boolean; - rhsIsExpanded: boolean; - rhsIsOpen: boolean; - shouldShowAppBar: boolean; -} & RouteComponentProps +export type Props = PropsFromRedux & RouteComponentProps; interface State { - configLoaded?: boolean; + shouldMountAppRoutes?: boolean; } export default class Root extends React.PureComponent { - private mounted: boolean; - // The constructor adds a bunch of event listeners, // so we do need this. private a11yController: A11yController; constructor(props: Props) { super(props); - this.mounted = false; // Redux setUrl(getSiteURL()); @@ -199,18 +164,18 @@ export default class Root extends React.PureComponent { }); this.state = { - configLoaded: false, + shouldMountAppRoutes: false, }; this.a11yController = new A11yController(); } - onConfigLoaded = (config: Partial) => { + setRudderConfig = () => { const telemetryId = this.props.telemetryId; const rudderUrl = 'https://pdat.matterlytics.com'; let rudderKey = ''; - switch (config.ServiceEnvironment) { + switch (this.props.serviceEnvironment) { case ServiceEnvironment.PRODUCTION: rudderKey = '1aoejPqhgONMI720CsBSRWzzRQ9'; break; @@ -223,13 +188,15 @@ export default class Root extends React.PureComponent { if (rudderKey !== '' && this.props.telemetryEnabled) { const rudderCfg: {setCookieDomain?: string} = {}; - const siteURL = config.SiteURL; - if (siteURL !== '') { + if (this.props.siteURL !== '') { try { - rudderCfg.setCookieDomain = new URL(siteURL || '').hostname; - // eslint-disable-next-line no-empty - } catch (_) {} + rudderCfg.setCookieDomain = new URL(this.props.siteURL || '').hostname; + } catch (_) { + // eslint-disable-next-line no-console + console.error('Failed to set cookie domain for RudderStack'); + } } + rudderAnalytics.load(rudderKey, rudderUrl || '', rudderCfg); rudderAnalytics.identify(telemetryId, {}, { @@ -268,20 +235,14 @@ export default class Root extends React.PureComponent { } }); } + }; - if (this.props.location.pathname === '/' && this.props.noAccounts) { - this.props.history.push('/signup_user_complete'); - } - + onConfigLoaded = () => { Promise.all([ this.props.actions.initializeProducts(), initializePlugins(), ]).then(() => { - if (this.mounted) { - // supports enzyme tests, set state if and only if - // the component is still mounted on screen - this.setState({configLoaded: true}); - } + this.setState({shouldMountAppRoutes: true}); }); this.props.actions.migrateRecentEmojis(); @@ -352,6 +313,7 @@ export default class Root extends React.PureComponent { if (!deepEqual(prevProps.theme, this.props.theme)) { Utils.applyTheme(this.props.theme); } + if (this.props.location.pathname === '/') { if (this.props.noAccounts) { prevProps.history.push('/signup_user_complete'); @@ -359,6 +321,7 @@ export default class Root extends React.PureComponent { prevProps.history.push('/terms_of_service'); } } + if ( this.props.shouldShowAppBar !== prevProps.shouldShowAppBar || this.props.rhsIsOpen !== prevProps.rhsIsOpen || @@ -366,6 +329,10 @@ export default class Root extends React.PureComponent { ) { this.setRootMeta(); } + + if (!prevProps.isConfigLoaded && this.props.isConfigLoaded) { + this.setRudderConfig(); + } } captureUTMParams() { @@ -393,22 +360,26 @@ export default class Root extends React.PureComponent { } initiateMeRequests = async () => { - const {config, isMeLoaded} = await this.props.actions.loadConfigAndMe(); + const {isLoaded, isMeRequested} = await this.props.actions.loadConfigAndMe(); - if (isMeLoaded && this.props.location.pathname === '/') { - this.props.actions.redirectToOnboardingOrDefaultTeam(this.props.history); - } + if (isLoaded) { + const isUserAtRootRoute = this.props.location.pathname === '/'; - if (config) { - this.onConfigLoaded(config); + if (isUserAtRootRoute) { + if (isMeRequested) { + this.props.actions.redirectToOnboardingOrDefaultTeam(this.props.history); + } else if (this.props.noAccounts) { + this.props.history.push('/signup_user_complete'); + } + } + + this.onConfigLoaded(); } }; componentDidMount() { temporarilySetPageLoadContext(PageLoadContext.PAGE_LOAD); - this.mounted = true; - this.initiateMeRequests(); // See figma design on issue https://mattermost.atlassian.net/browse/MM-43649 @@ -419,7 +390,6 @@ export default class Root extends React.PureComponent { } componentWillUnmount() { - this.mounted = false; window.removeEventListener('storage', this.handleLogoutLoginSignal); } @@ -440,7 +410,7 @@ export default class Root extends React.PureComponent { }; render() { - if (!this.state.configLoaded) { + if (!this.state.shouldMountAppRoutes) { return
; } diff --git a/webapp/channels/src/packages/mattermost-redux/src/action_types/teams.ts b/webapp/channels/src/packages/mattermost-redux/src/action_types/teams.ts index d9c48f4d92..3e853e4c8f 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/action_types/teams.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/action_types/teams.ts @@ -8,10 +8,6 @@ export default keyMirror({ GET_TEAMS_SUCCESS: null, GET_TEAMS_FAILURE: null, - MY_TEAMS_REQUEST: null, - MY_TEAMS_SUCCESS: null, - MY_TEAMS_FAILURE: null, - CREATE_TEAM_REQUEST: null, CREATE_TEAM_SUCCESS: null, CREATE_TEAM_FAILURE: null, diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/teams.test.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/teams.test.ts index 2aa7fcc130..ac95caac90 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/teams.test.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/teams.test.ts @@ -61,13 +61,8 @@ describe('Actions.Teams', () => { reply(200, [TestHelper.basicTeam]); await store.dispatch(Actions.getMyTeams()); - const teamsRequest = store.getState().requests.teams.getMyTeams; const {teams} = store.getState().entities.teams; - if (teamsRequest.status === RequestStatus.FAILURE) { - throw new Error(JSON.stringify(teamsRequest.error)); - } - expect(teams).toBeTruthy(); expect(teams[TestHelper.basicTeam!.id]).toBeTruthy(); }); diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/teams.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/teams.ts index a46243b86c..7a27660526 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/teams.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/teams.ts @@ -66,9 +66,7 @@ export function selectTeam(team: Team | Team['id']) { export function getMyTeams() { return bindClientFunc({ clientFunc: Client4.getMyTeams, - onRequest: TeamTypes.MY_TEAMS_REQUEST, - onSuccess: [TeamTypes.RECEIVED_TEAMS_LIST, TeamTypes.MY_TEAMS_SUCCESS], - onFailure: TeamTypes.MY_TEAMS_FAILURE, + onSuccess: TeamTypes.RECEIVED_TEAMS_LIST, }); } diff --git a/webapp/channels/src/packages/mattermost-redux/src/reducers/requests/teams.ts b/webapp/channels/src/packages/mattermost-redux/src/reducers/requests/teams.ts index 6ef5e58352..4921fe501c 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/reducers/requests/teams.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/reducers/requests/teams.ts @@ -10,16 +10,6 @@ import {TeamTypes} from 'mattermost-redux/action_types'; import {handleRequest, initialRequestState} from './helpers'; -function getMyTeams(state: RequestStatusType = initialRequestState(), action: AnyAction): RequestStatusType { - return handleRequest( - TeamTypes.MY_TEAMS_REQUEST, - TeamTypes.MY_TEAMS_SUCCESS, - TeamTypes.MY_TEAMS_FAILURE, - state, - action, - ); -} - function getTeams(state: RequestStatusType = initialRequestState(), action: AnyAction): RequestStatusType { return handleRequest( TeamTypes.GET_TEAMS_REQUEST, @@ -32,5 +22,4 @@ function getTeams(state: RequestStatusType = initialRequestState(), action: AnyA export default combineReducers({ getTeams, - getMyTeams, }); diff --git a/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts b/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts index 7764c4b06a..3bb4f01e61 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts @@ -253,10 +253,6 @@ const state: GlobalState = { }, }, teams: { - getMyTeams: { - status: 'not_started', - error: null, - }, getTeams: { status: 'not_started', error: null, diff --git a/webapp/channels/src/utils/utils.tsx b/webapp/channels/src/utils/utils.tsx index 89ca72c036..059b322dd0 100644 --- a/webapp/channels/src/utils/utils.tsx +++ b/webapp/channels/src/utils/utils.tsx @@ -1657,3 +1657,7 @@ export function sortUsersAndGroups(a: UserProfile | Group, b: UserProfile | Grou return aSortString.localeCompare(bSortString); } + +export function doesCookieContainsMMUserId() { + return document.cookie.includes('MMUSERID='); +} diff --git a/webapp/platform/types/src/requests.ts b/webapp/platform/types/src/requests.ts index 1db26cb668..ae363e2898 100644 --- a/webapp/platform/types/src/requests.ts +++ b/webapp/platform/types/src/requests.ts @@ -29,7 +29,6 @@ export type ThreadsRequestStatuses = { }; export type TeamsRequestsStatuses = { - getMyTeams: RequestStatusType; getTeams: RequestStatusType; };