mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
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:
parent
03f71c9c84
commit
2e40ede7dd
@ -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
|
||||
|
@ -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{
|
||||
|
@ -44,6 +44,7 @@ describe('actions/status_actions', () => {
|
||||
config: {
|
||||
EnableCustomEmoji: 'true',
|
||||
EnableCustomUserStatuses: 'true',
|
||||
EnableUserStatuses: 'true',
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
|
@ -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};
|
||||
}
|
||||
|
||||
|
@ -102,7 +102,9 @@ describe('Actions.User', () => {
|
||||
},
|
||||
} as unknown as GlobalState['entities']['channels'],
|
||||
general: {
|
||||
config: {},
|
||||
config: {
|
||||
EnableUserStatuses: 'true',
|
||||
},
|
||||
} as GlobalState['entities']['general'],
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
|
@ -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};
|
||||
}
|
||||
|
||||
|
@ -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']);
|
||||
|
@ -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 (
|
||||
<>
|
||||
|
@ -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: {
|
||||
|
@ -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]));
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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))));
|
||||
}
|
||||
|
||||
|
@ -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)));
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
);
|
||||
|
@ -57,6 +57,7 @@ export type ClientConfig = {
|
||||
EnableCustomEmoji: string;
|
||||
EnableCustomGroups: string;
|
||||
EnableCustomUserStatuses: string;
|
||||
EnableUserStatuses: string;
|
||||
EnableLastActiveTime: string;
|
||||
EnableTimedDND: string;
|
||||
EnableCustomTermsOfService: string;
|
||||
|
Loading…
Reference in New Issue
Block a user