Mm 52993 - expose enable user statuses to client (#25455)

* MM-52993_expose enable user statuses to client and prevent statuses fetch when disabled

* modify the logic, add selector, add unit tests

* implement PR feedback

* fix unit tests

* remove client console warnings

* remove another unnecessary call if the flag is disabled

* fix merge conflicts
This commit is contained in:
Pablo Vélez 2024-02-07 15:39:19 +01:00 committed by GitHub
parent 03f71c9c84
commit 2e40ede7dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 176 additions and 24 deletions

View File

@ -284,6 +284,7 @@ func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *m
props["DefaultClientLocale"] = *c.LocalizationSettings.DefaultClientLocale
props["EnableCustomEmoji"] = strconv.FormatBool(*c.ServiceSettings.EnableCustomEmoji)
props["EnableUserStatuses"] = strconv.FormatBool(*c.ServiceSettings.EnableUserStatuses)
props["AppDownloadLink"] = *c.NativeAppSettings.AppDownloadLink
props["AndroidAppDownloadLink"] = *c.NativeAppSettings.AndroidAppDownloadLink
props["IosAppDownloadLink"] = *c.NativeAppSettings.IosAppDownloadLink

View File

@ -240,6 +240,19 @@ func TestGetClientConfig(t *testing.T) {
"ExperimentalSharedChannels": "true",
},
},
{
"disable EnableUserStatuses",
&model.Config{
ServiceSettings: model.ServiceSettings{
EnableUserStatuses: model.NewBool(false),
},
},
"",
nil,
map[string]string{
"EnableUserStatuses": "false",
},
},
{
"Shared channels enterprise license",
&model.Config{

View File

@ -44,6 +44,7 @@ describe('actions/status_actions', () => {
config: {
EnableCustomEmoji: 'true',
EnableCustomUserStatuses: 'true',
EnableUserStatuses: 'true',
},
},
teams: {

View File

@ -5,6 +5,7 @@ import type {UserProfile} from '@mattermost/types/users';
import {getStatusesByIds} from 'mattermost-redux/actions/users';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getIsUserStatusesConfigEnabled} from 'mattermost-redux/selectors/entities/common';
import {getPostsInCurrentChannel} from 'mattermost-redux/selectors/entities/posts';
import {getDirectShowPreferences} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
@ -84,8 +85,11 @@ export function loadStatusesForProfilesMap(users: Record<string, UserProfile> |
}
export function loadStatusesByIds(userIds: string[]): ActionFunc {
return (dispatch) => {
if (userIds.length === 0) {
return (dispatch, getState) => {
const state = getState();
const enabledUserStatuses = getIsUserStatusesConfigEnabled(state);
if (userIds.length === 0 || !enabledUserStatuses) {
return {data: false};
}
@ -98,13 +102,15 @@ export function loadStatusesByIds(userIds: string[]): ActionFunc {
export function loadProfilesMissingStatus(users: UserProfile[]): ActionFunc {
return (dispatch, getState) => {
const state = getState();
const enabledUserStatuses = getIsUserStatusesConfigEnabled(state);
const statuses = state.entities.users.statuses;
const missingStatusByIds = users.
filter((user) => !statuses[user.id]).
map((user) => user.id);
if (missingStatusByIds.length === 0) {
if (missingStatusByIds.length === 0 || !enabledUserStatuses) {
return {data: false};
}

View File

@ -102,7 +102,9 @@ describe('Actions.User', () => {
},
} as unknown as GlobalState['entities']['channels'],
general: {
config: {},
config: {
EnableUserStatuses: 'true',
},
} as GlobalState['entities']['general'],
preferences: {
myPreferences: {

View File

@ -20,6 +20,7 @@ import {
getMyChannelMember,
getMyChannels,
} from 'mattermost-redux/selectors/entities/channels';
import {getIsUserStatusesConfigEnabled} from 'mattermost-redux/selectors/entities/common';
import {getBool, isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeamId, getTeamMember} from 'mattermost-redux/selectors/entities/teams';
import * as Selectors from 'mattermost-redux/selectors/entities/users';
@ -423,10 +424,17 @@ export function autocompleteUsers(username: string): ThunkActionFunc<Promise<Use
export function autoResetStatus(): ActionFuncAsync<UserStatus> {
return async (doDispatch) => {
const {currentUserId} = getState().entities.users;
const state = getState();
const enabledUserStatuses = getIsUserStatusesConfigEnabled(state);
if (!enabledUserStatuses) {
return {data: undefined};
}
const {currentUserId} = state.entities.users;
const {data: userStatus} = await doDispatch(UserActions.getStatus(currentUserId));
if (userStatus!.status === UserStatuses.OUT_OF_OFFICE || !userStatus!.manual) {
if (userStatus?.status === UserStatuses.OUT_OF_OFFICE || !userStatus?.manual) {
return {data: userStatus};
}

View File

@ -1,9 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {render, act} from '@testing-library/react';
import React from 'react';
import {Provider} from 'react-redux';
import {getClassnamesForBody} from './channel_controller';
import * as actions from 'actions/status_actions';
import mockStore from 'tests/test_store';
import Constants from 'utils/constants';
import type {GlobalState} from 'types/store';
import ChannelController, {getClassnamesForBody} from './channel_controller';
let mockState: GlobalState;
jest.mock('components/reset_status_modal', () => () => <div/>);
jest.mock('components/sidebar', () => () => <div/>);
@ -13,6 +24,63 @@ jest.mock('components/unreads_status_handler', () => () => <div/>);
jest.mock('components/product_notices_modal', () => () => <div/>);
jest.mock('plugins/pluggable', () => () => <div/>);
jest.mock('actions/status_actions', () => ({
loadStatusesForChannelAndSidebar: jest.fn().mockImplementation(() => () => {}),
}));
jest.mock('mattermost-redux/selectors/entities/general', () => ({
...jest.requireActual('mattermost-redux/selectors/entities/general') as typeof import('mattermost-redux/selectors/entities/general'),
}));
describe('ChannelController', () => {
beforeEach(() => {
mockState = {
entities: {
general: {
config: {
EnableUserStatuses: 'false',
},
},
},
} as unknown as GlobalState;
jest.useFakeTimers();
});
it('dispatches loadStatusesForChannelAndSidebar when enableUserStatuses is true', () => {
mockState.entities.general.config.EnableUserStatuses = 'true';
const store = mockStore(mockState);
render(
<Provider store={store}>
<ChannelController shouldRenderCenterChannel={true}/>
</Provider>,
);
act(() => {
jest.advanceTimersByTime(Constants.STATUS_INTERVAL);
});
expect(actions.loadStatusesForChannelAndSidebar).toHaveBeenCalled();
});
it('does not dispatch loadStatusesForChannelAndSidebar when enableUserStatuses is false', () => {
const store = mockStore(mockState);
mockState.entities.general.config.EnableUserStatuses = 'false';
render(
<Provider store={store}>
<ChannelController shouldRenderCenterChannel={true}/>
</Provider>,
);
act(() => {
jest.advanceTimersByTime(Constants.STATUS_INTERVAL);
});
expect(actions.loadStatusesForChannelAndSidebar).not.toHaveBeenCalled();
});
});
describe('components/channel_layout/ChannelController', () => {
test('Should have app__body and channel-view classes by default', () => {
expect(getClassnamesForBody('')).toEqual(['app__body', 'channel-view']);

View File

@ -3,7 +3,9 @@
import classNames from 'classnames';
import React, {useEffect} from 'react';
import {useDispatch} from 'react-redux';
import {useDispatch, useSelector} from 'react-redux';
import {getIsUserStatusesConfigEnabled} from 'mattermost-redux/selectors/entities/common';
import {loadStatusesForChannelAndSidebar} from 'actions/status_actions';
@ -25,11 +27,15 @@ type Props = {
}
export default function ChannelController(props: Props) {
const enabledUserStatuses = useSelector(getIsUserStatusesConfigEnabled);
const dispatch = useDispatch();
useEffect(() => {
const isMsBrowser = isInternetExplorer() || isEdge();
const platform = window.navigator.platform;
const {navigator} = window;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const platform = navigator?.userAgentData?.platform || navigator?.platform || 'unknown';
document.body.classList.add(...getClassnamesForBody(platform, isMsBrowser));
return () => {
@ -38,14 +44,16 @@ export default function ChannelController(props: Props) {
}, []);
useEffect(() => {
const loadStatusesIntervalId = setInterval(() => {
dispatch(loadStatusesForChannelAndSidebar());
}, Constants.STATUS_INTERVAL);
let loadStatusesIntervalId: ReturnType<typeof setInterval>;
if (enabledUserStatuses) {
loadStatusesIntervalId = setInterval(() => {
dispatch(loadStatusesForChannelAndSidebar());
}, Constants.STATUS_INTERVAL);
}
return () => {
clearInterval(loadStatusesIntervalId);
};
}, []);
}, [dispatch, enabledUserStatuses]);
return (
<>

View File

@ -18,7 +18,9 @@ describe('handleUserTypingEvent', () => {
const initialState = {
entities: {
general: {
config: {},
config: {
EnableUserStatuses: 'true',
},
},
users: {
currentUserId: 'user',
@ -81,6 +83,25 @@ describe('handleUserTypingEvent', () => {
expect(getStatusesByIds).toHaveBeenCalled();
});
test('should NOT load statuses for users if enableUserStatuses config is disabled', async () => {
const state = mergeObjects(initialState, {
entities: {
general: {
config: {
EnableUserStatuses: 'false',
},
},
},
});
const store = configureStore(state);
store.dispatch(userStartedTyping(userId, channelId, rootId, Date.now()));
// Wait for side effects to resolve
await Promise.resolve();
expect(getStatusesByIds).not.toHaveBeenCalled();
});
test('should not load statuses for users that are online', async () => {
const state = mergeObjects(initialState, {
entities: {

View File

@ -5,6 +5,7 @@ import type {GlobalState} from '@mattermost/types/store';
import {getMissingProfilesByIds, getStatusesByIds} from 'mattermost-redux/actions/users';
import {General, Preferences, WebsocketEvents} from 'mattermost-redux/constants';
import {getIsUserStatusesConfigEnabled} from 'mattermost-redux/selectors/entities/common';
import {getConfig, isPerformanceDebuggingEnabled} from 'mattermost-redux/selectors/entities/general';
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUserId, getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
@ -49,6 +50,7 @@ function fillInMissingInfo(userId: string): ActionFuncAsync {
return async (dispatch, getState) => {
const state = getState();
const currentUserId = getCurrentUserId(state);
const enabledUserStatuses = getIsUserStatusesConfigEnabled(state);
if (userId !== currentUserId) {
const result = await dispatch(getMissingProfilesByIds([userId]));
@ -59,7 +61,7 @@ function fillInMissingInfo(userId: string): ActionFuncAsync {
}
const status = getStatusForUserId(state, userId);
if (status !== General.ONLINE) {
if (status !== General.ONLINE && enabledUserStatuses) {
dispatch(getStatusesByIds([userId]));
}

View File

@ -174,13 +174,16 @@ export default class ResetStatusModal extends React.PureComponent<Props, State>
public componentDidMount(): void {
this.props.actions.autoResetStatus().then(
(result) => {
if (result.data! === null) {
return;
}
const status = result.data!;
const statusIsManual = status.manual;
const statusIsManual = status?.manual;
const autoResetPrefNotSet = this.props.autoResetPref === '';
this.setState({
currentUserStatus: status, // Set in state until status refactor where we store 'manual' field in redux
show: Boolean(status.status === UserStatuses.OUT_OF_OFFICE || (statusIsManual && autoResetPrefNotSet)),
show: Boolean(status?.status === UserStatuses.OUT_OF_OFFICE || (statusIsManual && autoResetPrefNotSet)),
});
},
);
@ -220,7 +223,7 @@ export default class ResetStatusModal extends React.PureComponent<Props, State>
};
public render(): JSX.Element {
const userStatus = this.state.currentUserStatus.status || '';
const userStatus = this.state.currentUserStatus?.status || '';
const manualStatusTitle = messages[userStatus] ? (<FormattedMessage {...messages[userStatus].title}/>) : '';
const manualStatusMessage = this.renderModalMessage();

View File

@ -25,6 +25,7 @@ import {getProfilesByIds, getProfilesByUsernames, getStatusesByIds} from 'matter
import {Client4, DEFAULT_LIMIT_AFTER, DEFAULT_LIMIT_BEFORE} from 'mattermost-redux/client';
import {General, Preferences, Posts} from 'mattermost-redux/constants';
import {getCurrentChannelId, getMyChannelMember as getMyChannelMemberSelector} from 'mattermost-redux/selectors/entities/channels';
import {getIsUserStatusesConfigEnabled} from 'mattermost-redux/selectors/entities/common';
import {getCustomEmojisByName as selectCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
import {getAllGroupsByName} from 'mattermost-redux/selectors/entities/groups';
import * as PostSelectors from 'mattermost-redux/selectors/entities/posts';
@ -1004,6 +1005,7 @@ export async function getMentionsAndStatusesForPosts(postsArrayOrMap: Post[]|Pos
const state = getState();
const {currentUserId, profiles, statuses} = state.entities.users;
const enabledUserStatuses = getIsUserStatusesConfigEnabled(state);
// Statuses and profiles of the users who made the posts
const userIdsToLoad = new Set<string>();
@ -1053,7 +1055,7 @@ export async function getMentionsAndStatusesForPosts(postsArrayOrMap: Post[]|Pos
promises.push(dispatch(getProfilesByIds(Array.from(userIdsToLoad))));
}
if (statusesToLoad.size > 0) {
if (statusesToLoad.size > 0 && enabledUserStatuses) {
promises.push(dispatch(getStatusesByIds(Array.from(statusesToLoad))));
}

View File

@ -16,17 +16,21 @@ import {loadRolesIfNeeded} from 'mattermost-redux/actions/roles';
import {getProfilesByIds, getStatusesByIds} from 'mattermost-redux/actions/users';
import {Client4} from 'mattermost-redux/client';
import {General} from 'mattermost-redux/constants';
import {getIsUserStatusesConfigEnabled} from 'mattermost-redux/selectors/entities/common';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import type {ActionResult, DispatchFunc, GetStateFunc, ActionFuncAsync} from 'mattermost-redux/types/actions';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
async function getProfilesAndStatusesForMembers(userIds: string[], dispatch: DispatchFunc, getState: GetStateFunc) {
const state = getState();
const {
currentUserId,
profiles,
statuses,
} = getState().entities.users;
} = state.entities.users;
const enabledUserStatuses = getIsUserStatusesConfigEnabled(state);
const profilesToLoad: string[] = [];
const statusesToLoad: string[] = [];
userIds.forEach((userId) => {
@ -44,7 +48,7 @@ async function getProfilesAndStatusesForMembers(userIds: string[], dispatch: Dis
requests.push(dispatch(getProfilesByIds(profilesToLoad)));
}
if (statusesToLoad.length) {
if (statusesToLoad.length && enabledUserStatuses) {
requests.push(dispatch(getStatusesByIds(statusesToLoad)));
}

View File

@ -18,6 +18,7 @@ import {loadRolesIfNeeded} from 'mattermost-redux/actions/roles';
import {getMyTeams, getMyTeamMembers, getMyTeamUnreads} from 'mattermost-redux/actions/teams';
import {Client4} from 'mattermost-redux/client';
import {General} from 'mattermost-redux/constants';
import {getIsUserStatusesConfigEnabled} from 'mattermost-redux/selectors/entities/common';
import {getServerVersion} from 'mattermost-redux/selectors/entities/general';
import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUserId, getUsers} from 'mattermost-redux/selectors/entities/users';
@ -153,7 +154,9 @@ export function getProfiles(page = 0, perPage: number = General.PROFILE_CHUNK_SI
export function getMissingProfilesByIds(userIds: string[]): ActionFuncAsync<UserProfile[]> {
return async (dispatch, getState) => {
const {profiles} = getState().entities.users;
const state = getState();
const {profiles} = state.entities.users;
const enabledUserStatuses = getIsUserStatusesConfigEnabled(state);
const missingIds: string[] = [];
userIds.forEach((id) => {
if (!profiles[id]) {
@ -162,7 +165,9 @@ export function getMissingProfilesByIds(userIds: string[]): ActionFuncAsync<User
});
if (missingIds.length > 0) {
dispatch(getStatusesByIds(missingIds));
if (enabledUserStatuses) {
dispatch(getStatusesByIds(missingIds));
}
return dispatch(getProfilesByIds(missingIds));
}

View File

@ -82,3 +82,10 @@ export function getCallsConfig(state: GlobalState): CallsConfig {
// @ts-ignore
return state[CALLS_PLUGIN].callsConfig;
}
// Config
export const getIsUserStatusesConfigEnabled: (a: GlobalState) => boolean = createSelector(
'getIsUserStatusesConfigEnabled',
(state: GlobalState) => state.entities.general.config.EnableUserStatuses,
(EnableUserStatuses) => EnableUserStatuses === 'true',
);

View File

@ -57,6 +57,7 @@ export type ClientConfig = {
EnableCustomEmoji: string;
EnableCustomGroups: string;
EnableCustomUserStatuses: string;
EnableUserStatuses: string;
EnableLastActiveTime: string;
EnableTimedDND: string;
EnableCustomTermsOfService: string;