mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
[MM-57744] Improve status received reducers (#26670)
Rationale As seen below, we can deduce that the memory consumption of the reducers related to user status was the maximum, given that the app is idle with 50,000 users logged in and posting 35 messages per second. The memory consumption of the affected reducer (35%) is even more than that of the functions responsible for the incoming messages (27%). This implementation iterated over the received statuses multiple times across different reducers, each time processing a subset of the same data. This comes with the overhead of array function calls, creation of intermediary objects, and arrays. Changes Modified the getStatusesByIds action to process received statuses once, extracting and transforming all necessary data for statuses, dndEndTimes, isManualStatuses, and lastActivity reducer in a single iteration. Improvements By avoiding multiple iterations, we reduce the runtime complexity from O(4n) to O(n), where n is number of statuses. Simplified reducer for user statuses, which includes statuses, dndEntTimes, isManualStatuses, lastActivity. Also created for single and multiple items of these reducers.
This commit is contained in:
parent
80e67ace86
commit
54d7011aba
@ -432,7 +432,8 @@ export function autoResetStatus(): ActionFuncAsync<UserStatus> {
|
||||
}
|
||||
|
||||
const {currentUserId} = state.entities.users;
|
||||
const {data: userStatus} = await doDispatch(UserActions.getStatus(currentUserId));
|
||||
const {data} = await doDispatch(UserActions.getStatusesByIds([currentUserId]));
|
||||
const userStatus = data?.[0];
|
||||
|
||||
if (userStatus?.status === UserStatuses.OUT_OF_OFFICE || !userStatus?.manual) {
|
||||
return {data: userStatus};
|
||||
|
@ -717,7 +717,7 @@ export function handleNewPostEvent(msg) {
|
||||
) {
|
||||
myDispatch({
|
||||
type: UserTypes.RECEIVED_STATUSES,
|
||||
data: [{user_id: post.user_id, status: UserStatuses.ONLINE}],
|
||||
data: [{[post.user_id]: UserStatuses.ONLINE}],
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1265,7 +1265,7 @@ function addedNewGmUser(preference) {
|
||||
function handleStatusChangedEvent(msg) {
|
||||
dispatch({
|
||||
type: UserTypes.RECEIVED_STATUSES,
|
||||
data: [{user_id: msg.data.user_id, status: msg.data.status}],
|
||||
data: [{[msg.data.user_id]: msg.data.status}],
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -507,7 +507,7 @@ describe('handleNewPostEvent', () => {
|
||||
|
||||
expect(testStore.getActions()).toContainEqual({
|
||||
type: UserTypes.RECEIVED_STATUSES,
|
||||
data: [{user_id: post.user_id, status: UserStatuses.ONLINE}],
|
||||
data: [{[post.user_id]: UserStatuses.ONLINE}],
|
||||
});
|
||||
});
|
||||
|
||||
@ -526,7 +526,7 @@ describe('handleNewPostEvent', () => {
|
||||
|
||||
expect(testStore.getActions()).not.toContainEqual({
|
||||
type: UserTypes.RECEIVED_STATUSES,
|
||||
data: [{user_id: post.user_id, status: UserStatuses.ONLINE}],
|
||||
data: [{[post.user_id]: UserStatuses.ONLINE}],
|
||||
});
|
||||
});
|
||||
|
||||
@ -556,7 +556,7 @@ describe('handleNewPostEvent', () => {
|
||||
|
||||
expect(testStore.getActions()).not.toContainEqual({
|
||||
type: UserTypes.RECEIVED_STATUSES,
|
||||
data: [{user_id: post.user_id, status: UserStatuses.ONLINE}],
|
||||
data: [{[post.user_id]: UserStatuses.ONLINE}],
|
||||
});
|
||||
});
|
||||
|
||||
@ -575,7 +575,7 @@ describe('handleNewPostEvent', () => {
|
||||
|
||||
expect(testStore.getActions()).not.toContainEqual({
|
||||
type: UserTypes.RECEIVED_STATUSES,
|
||||
data: [{user_id: post.user_id, status: UserStatuses.ONLINE}],
|
||||
data: [{[post.user_id]: UserStatuses.ONLINE}],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -54,8 +54,12 @@ export default keyMirror({
|
||||
RECEIVED_SESSIONS: null,
|
||||
RECEIVED_REVOKED_SESSION: null,
|
||||
RECEIVED_AUDITS: null,
|
||||
RECEIVED_STATUS: null,
|
||||
|
||||
RECEIVED_STATUSES: null,
|
||||
RECEIVED_DND_END_TIMES: null,
|
||||
RECEIVED_STATUSES_IS_MANUAL: null,
|
||||
RECEIVED_LAST_ACTIVITIES: null,
|
||||
|
||||
RECEIVED_AUTOCOMPLETE_IN_CHANNEL: null,
|
||||
RESET_LOGOUT_STATE: null,
|
||||
RECEIVED_MY_USER_ACCESS_TOKEN: null,
|
||||
|
@ -525,16 +525,27 @@ describe('Actions.Users', () => {
|
||||
it('getStatusesByIds', async () => {
|
||||
nock(Client4.getBaseRoute()).
|
||||
post('/users/status/ids').
|
||||
reply(200, [{user_id: TestHelper.basicUser!.id, status: 'online', manual: false, last_activity_at: 1507662212199}]);
|
||||
reply(200, [{
|
||||
user_id: TestHelper.basicUser!.id,
|
||||
status: 'online',
|
||||
manual: false,
|
||||
last_activity_at: 1507662212199,
|
||||
dnd_end_time: 0,
|
||||
}]);
|
||||
|
||||
await store.dispatch(Actions.getStatusesByIds(
|
||||
[TestHelper.basicUser!.id],
|
||||
));
|
||||
|
||||
const statuses = store.getState().entities.users.statuses;
|
||||
const dndEndTimes = store.getState().entities.users.dndEndTimes;
|
||||
const lastActivity = store.getState().entities.users.lastActivity;
|
||||
const isManualStatus = store.getState().entities.users.isManualStatus;
|
||||
|
||||
expect(statuses[TestHelper.basicUser!.id]).toBeTruthy();
|
||||
expect(Object.keys(statuses).length).toEqual(1);
|
||||
expect(statuses[TestHelper.basicUser!.id]).toBe('online');
|
||||
expect(dndEndTimes[TestHelper.basicUser!.id]).toBe(0);
|
||||
expect(lastActivity[TestHelper.basicUser!.id]).toBe(1507662212199);
|
||||
expect(isManualStatus[TestHelper.basicUser!.id]).toBe(false);
|
||||
});
|
||||
|
||||
it('getTotalUsersStats', async () => {
|
||||
@ -548,25 +559,10 @@ describe('Actions.Users', () => {
|
||||
expect(stats.total_users_count).toEqual(2605);
|
||||
});
|
||||
|
||||
it('getStatus', async () => {
|
||||
const user = TestHelper.basicUser;
|
||||
|
||||
nock(Client4.getBaseRoute()).
|
||||
get(`/users/${user!.id}/status`).
|
||||
reply(200, {user_id: user!.id, status: 'online', manual: false, last_activity_at: 1507662212199});
|
||||
|
||||
await store.dispatch(Actions.getStatus(
|
||||
user!.id,
|
||||
));
|
||||
|
||||
const statuses = store.getState().entities.users.statuses;
|
||||
expect(statuses[user!.id]).toBeTruthy();
|
||||
});
|
||||
|
||||
it('setStatus', async () => {
|
||||
nock(Client4.getBaseRoute()).
|
||||
put(`/users/${TestHelper.basicUser!.id}/status`).
|
||||
reply(200, OK_RESPONSE);
|
||||
reply(200, {user_id: TestHelper.basicUser!.id, status: 'away'});
|
||||
|
||||
await store.dispatch(Actions.setStatus(
|
||||
{user_id: TestHelper.basicUser!.id, status: 'away'},
|
||||
|
@ -11,7 +11,7 @@ import type {UserProfile, UserStatus, GetFilteredUsersStatsOpts, UsersStats, Use
|
||||
import {UserTypes, AdminTypes} from 'mattermost-redux/action_types';
|
||||
import {logError} from 'mattermost-redux/actions/errors';
|
||||
import {setServerVersion, getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general';
|
||||
import {bindClientFunc, forceLogoutIfNecessary, debounce} from 'mattermost-redux/actions/helpers';
|
||||
import {bindClientFunc, forceLogoutIfNecessary} from 'mattermost-redux/actions/helpers';
|
||||
import {getServerLimits} from 'mattermost-redux/actions/limits';
|
||||
import {getMyPreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {loadRolesIfNeeded} from 'mattermost-redux/actions/roles';
|
||||
@ -22,7 +22,7 @@ import {getIsUserStatusesConfigEnabled} from 'mattermost-redux/selectors/entitie
|
||||
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';
|
||||
import type {DispatchFunc, ActionFuncAsync} from 'mattermost-redux/types/actions';
|
||||
import type {ActionFuncAsync} from 'mattermost-redux/types/actions';
|
||||
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
|
||||
|
||||
export function generateMfaSecret(userId: string) {
|
||||
@ -589,59 +589,94 @@ export function getUserByEmail(email: string) {
|
||||
});
|
||||
}
|
||||
|
||||
// We create an array to hold the id's that we want to get a status for. We build our
|
||||
// debounced function that will get called after a set period of idle time in which
|
||||
// the array of id's will be passed to the getStatusesByIds with a cb that clears out
|
||||
// the array. Helps with performance because instead of making 75 different calls for
|
||||
// statuses, we are only making one call for 75 ids.
|
||||
// We could maybe clean it up somewhat by storing the array of ids in redux state possbily?
|
||||
let ids: string[] = [];
|
||||
const debouncedGetStatusesByIds = debounce(async (dispatch: DispatchFunc) => {
|
||||
dispatch(getStatusesByIds([...new Set(ids)]));
|
||||
}, 20, false, () => {
|
||||
ids = [];
|
||||
});
|
||||
export function getStatusesByIdsBatchedDebounced(id: string) {
|
||||
ids = [...ids, id];
|
||||
return debouncedGetStatusesByIds;
|
||||
}
|
||||
|
||||
export function getStatusesByIds(userIds: string[]) {
|
||||
return bindClientFunc({
|
||||
clientFunc: Client4.getStatusesByIds,
|
||||
onSuccess: UserTypes.RECEIVED_STATUSES,
|
||||
params: [
|
||||
userIds,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export function getStatus(userId: string) {
|
||||
return bindClientFunc({
|
||||
clientFunc: Client4.getStatus,
|
||||
onSuccess: UserTypes.RECEIVED_STATUS,
|
||||
params: [
|
||||
userId,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export function setStatus(status: UserStatus): ActionFuncAsync {
|
||||
export function getStatusesByIds(userIds: Array<UserProfile['id']>): ActionFuncAsync<UserStatus[]> {
|
||||
return async (dispatch, getState) => {
|
||||
if (!userIds || userIds.length === 0) {
|
||||
return {data: []};
|
||||
}
|
||||
|
||||
let recievedStatuses: UserStatus[];
|
||||
try {
|
||||
await Client4.updateStatus(status);
|
||||
recievedStatuses = await Client4.getStatusesByIds(userIds);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: UserTypes.RECEIVED_STATUS,
|
||||
data: status,
|
||||
});
|
||||
const statuses: Record<UserProfile['id'], UserStatus['status']> = {};
|
||||
const dndEndTimes: Record<UserProfile['id'], UserStatus['dnd_end_time']> = {};
|
||||
const isManualStatuses: Record<UserProfile['id'], UserStatus['manual']> = {};
|
||||
const lastActivity: Record<UserProfile['id'], UserStatus['last_activity_at']> = {};
|
||||
|
||||
return {data: status};
|
||||
for (const recievedStatus of recievedStatuses) {
|
||||
statuses[recievedStatus.user_id] = recievedStatus?.status ?? '';
|
||||
dndEndTimes[recievedStatus.user_id] = recievedStatus?.dnd_end_time ?? 0;
|
||||
isManualStatuses[recievedStatus.user_id] = recievedStatus?.manual ?? false;
|
||||
lastActivity[recievedStatus.user_id] = recievedStatus?.last_activity_at ?? 0;
|
||||
}
|
||||
|
||||
dispatch(batchActions([
|
||||
{
|
||||
type: UserTypes.RECEIVED_STATUSES,
|
||||
data: statuses,
|
||||
},
|
||||
{
|
||||
type: UserTypes.RECEIVED_DND_END_TIMES,
|
||||
data: dndEndTimes,
|
||||
},
|
||||
{
|
||||
type: UserTypes.RECEIVED_STATUSES_IS_MANUAL,
|
||||
data: isManualStatuses,
|
||||
},
|
||||
{
|
||||
type: UserTypes.RECEIVED_LAST_ACTIVITIES,
|
||||
data: lastActivity,
|
||||
},
|
||||
],
|
||||
'BATCHING_STATUSES',
|
||||
));
|
||||
|
||||
return {data: recievedStatuses};
|
||||
};
|
||||
}
|
||||
|
||||
export function setStatus(status: UserStatus): ActionFuncAsync<UserStatus> {
|
||||
return async (dispatch, getState) => {
|
||||
let recievedStatus: UserStatus;
|
||||
try {
|
||||
recievedStatus = await Client4.updateStatus(status);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
}
|
||||
|
||||
const updatedStatus = {[recievedStatus.user_id]: recievedStatus.status};
|
||||
const dndEndTimes = {[recievedStatus.user_id]: recievedStatus?.dnd_end_time ?? 0};
|
||||
const isManualStatus = {[recievedStatus.user_id]: recievedStatus?.manual ?? false};
|
||||
const lastActivity = {[recievedStatus.user_id]: recievedStatus?.last_activity_at ?? 0};
|
||||
|
||||
dispatch(batchActions([
|
||||
{
|
||||
type: UserTypes.RECEIVED_STATUSES,
|
||||
data: updatedStatus,
|
||||
},
|
||||
{
|
||||
type: UserTypes.RECEIVED_DND_END_TIMES,
|
||||
data: dndEndTimes,
|
||||
},
|
||||
{
|
||||
type: UserTypes.RECEIVED_STATUSES_IS_MANUAL,
|
||||
data: isManualStatus,
|
||||
},
|
||||
{
|
||||
type: UserTypes.RECEIVED_LAST_ACTIVITIES,
|
||||
data: lastActivity,
|
||||
},
|
||||
], 'BATCHING_STATUS'));
|
||||
|
||||
return {data: recievedStatus};
|
||||
};
|
||||
}
|
||||
|
||||
@ -1305,7 +1340,6 @@ export default {
|
||||
getUser,
|
||||
getMe,
|
||||
getUserByUsername,
|
||||
getStatus,
|
||||
getStatusesByIds,
|
||||
getSessions,
|
||||
getTotalUsersStats,
|
||||
|
@ -1115,3 +1115,288 @@ describe('Reducers.users', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dndEndTimes', () => {
|
||||
const initialState = {} as UsersState;
|
||||
|
||||
test('should return the initial state', () => {
|
||||
expect(reducer(initialState, {} as any).dndEndTimes).toEqual({});
|
||||
expect(reducer(undefined, {} as any).dndEndTimes).toEqual({});
|
||||
|
||||
const usersState1 = deepFreezeAndThrowOnMutation({
|
||||
dndEndTimes: {},
|
||||
}) as UsersState;
|
||||
expect(reducer(usersState1, {} as any).dndEndTimes).toBe(usersState1.dndEndTimes);
|
||||
|
||||
const usersState2 = deepFreezeAndThrowOnMutation({
|
||||
dndEndTimes: {
|
||||
test_user_id: 123456789,
|
||||
},
|
||||
}) as UsersState;
|
||||
expect(reducer(usersState2, {} as any).dndEndTimes).toBe(usersState2.dndEndTimes);
|
||||
});
|
||||
|
||||
test('should store the dnd end time', () => {
|
||||
const action1 = {
|
||||
type: UserTypes.RECEIVED_DND_END_TIMES,
|
||||
data: {
|
||||
test_user_id: 123456789,
|
||||
},
|
||||
};
|
||||
expect(reducer(initialState, action1).dndEndTimes).toEqual({
|
||||
test_user_id: 123456789,
|
||||
});
|
||||
|
||||
const state = deepFreezeAndThrowOnMutation({
|
||||
dndEndTimes: {
|
||||
test_user_id: 123456789,
|
||||
},
|
||||
}) as UsersState;
|
||||
const action2 = {
|
||||
type: UserTypes.RECEIVED_DND_END_TIMES,
|
||||
data: {
|
||||
test_user_id: 987654321,
|
||||
},
|
||||
};
|
||||
expect(reducer(state, action2).dndEndTimes).toEqual({
|
||||
test_user_id: 987654321,
|
||||
});
|
||||
});
|
||||
|
||||
test('should store the dnd end time for multiple users', () => {
|
||||
const action1 = {
|
||||
type: UserTypes.RECEIVED_DND_END_TIMES,
|
||||
data: {
|
||||
test_user_id: 123456789,
|
||||
test_user_id_2: 987654321,
|
||||
},
|
||||
};
|
||||
expect(reducer(initialState, action1).dndEndTimes).toEqual({
|
||||
test_user_id: 123456789,
|
||||
test_user_id_2: 987654321,
|
||||
});
|
||||
|
||||
const state = deepFreezeAndThrowOnMutation({
|
||||
dndEndTimes: {
|
||||
test_user_id: 1,
|
||||
test_user_id_2: 2,
|
||||
test_user_id_4: 4,
|
||||
},
|
||||
}) as UsersState;
|
||||
const action2 = {
|
||||
type: UserTypes.RECEIVED_DND_END_TIMES,
|
||||
data: {
|
||||
test_user_id: 10,
|
||||
test_user_id_2: 20,
|
||||
test_user_id_3: 30,
|
||||
},
|
||||
};
|
||||
expect(reducer(state, action2).dndEndTimes).toEqual({
|
||||
test_user_id: 10,
|
||||
test_user_id_2: 20,
|
||||
test_user_id_3: 30,
|
||||
test_user_id_4: 4,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('statuses', () => {
|
||||
const initialState = {} as UsersState;
|
||||
|
||||
test('should return the initial state', () => {
|
||||
expect(reducer(initialState, {} as any).statuses).toEqual({});
|
||||
expect(reducer(undefined, {} as any).statuses).toEqual({});
|
||||
|
||||
const usersState1 = deepFreezeAndThrowOnMutation({
|
||||
statuses: {
|
||||
test_user_id: 'online',
|
||||
},
|
||||
}) as UsersState;
|
||||
expect(reducer(usersState1, {} as any).statuses).toBe(usersState1.statuses);
|
||||
});
|
||||
|
||||
test('should store the status', () => {
|
||||
const action1 = {
|
||||
type: UserTypes.RECEIVED_STATUSES,
|
||||
data: {
|
||||
test_user_id: 'away',
|
||||
},
|
||||
};
|
||||
expect(reducer(initialState, action1).statuses).toEqual({
|
||||
test_user_id: 'away',
|
||||
});
|
||||
|
||||
const state = deepFreezeAndThrowOnMutation({
|
||||
statuses: {
|
||||
test_user_id: 'away',
|
||||
},
|
||||
}) as UsersState;
|
||||
const action2 = {
|
||||
type: UserTypes.RECEIVED_STATUSES,
|
||||
data: {
|
||||
test_user_id: 'dnd',
|
||||
},
|
||||
};
|
||||
expect(reducer(state, action2).statuses).toEqual({
|
||||
test_user_id: 'dnd',
|
||||
});
|
||||
});
|
||||
|
||||
test('should store the status for multiple users', () => {
|
||||
const action1 = {
|
||||
type: UserTypes.RECEIVED_STATUSES,
|
||||
data: {
|
||||
test_user_id: 'away',
|
||||
test_user_id_2: 'dnd',
|
||||
},
|
||||
};
|
||||
expect(reducer(initialState, action1).statuses).toEqual({
|
||||
test_user_id: 'away',
|
||||
test_user_id_2: 'dnd',
|
||||
});
|
||||
|
||||
const state = deepFreezeAndThrowOnMutation({
|
||||
statuses: {
|
||||
test_user_id: 'away',
|
||||
test_user_id_2: 'dnd',
|
||||
test_user_id_4: 'offline',
|
||||
},
|
||||
}) as UsersState;
|
||||
const action2 = {
|
||||
type: UserTypes.RECEIVED_STATUSES,
|
||||
data: {
|
||||
test_user_id: 'online',
|
||||
test_user_id_2: 'offline',
|
||||
test_user_id_3: 'away',
|
||||
},
|
||||
};
|
||||
expect(reducer(state, action2).statuses).toEqual({
|
||||
test_user_id: 'online',
|
||||
test_user_id_2: 'offline',
|
||||
test_user_id_3: 'away',
|
||||
test_user_id_4: 'offline',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isManualStatus', () => {
|
||||
const initialState = {} as UsersState;
|
||||
|
||||
test('should return the initial state', () => {
|
||||
expect(reducer(initialState, {} as any).isManualStatus).toEqual({});
|
||||
expect(reducer(undefined, {} as any).isManualStatus).toEqual({});
|
||||
|
||||
const usersState1 = deepFreezeAndThrowOnMutation({
|
||||
isManualStatus: {
|
||||
test_user_id: true,
|
||||
},
|
||||
}) as UsersState;
|
||||
expect(reducer(usersState1, {} as any).isManualStatus).toBe(usersState1.isManualStatus);
|
||||
});
|
||||
|
||||
test('should store the isManualStatus', () => {
|
||||
const action1 = {
|
||||
type: UserTypes.RECEIVED_STATUSES_IS_MANUAL,
|
||||
data: {
|
||||
test_user_id: true,
|
||||
},
|
||||
};
|
||||
expect(reducer(initialState, action1).isManualStatus).toEqual({
|
||||
test_user_id: true,
|
||||
});
|
||||
|
||||
const state = deepFreezeAndThrowOnMutation({
|
||||
isManualStatus: {
|
||||
test_user_id: false,
|
||||
},
|
||||
}) as UsersState;
|
||||
const action2 = {
|
||||
type: UserTypes.RECEIVED_STATUSES_IS_MANUAL,
|
||||
data: {
|
||||
test_user_id: true,
|
||||
},
|
||||
};
|
||||
expect(reducer(state, action2).isManualStatus).toEqual({
|
||||
test_user_id: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('lastActivity', () => {
|
||||
const initialState = {} as UsersState;
|
||||
|
||||
test('should return the initial state', () => {
|
||||
expect(reducer(initialState, {} as any).lastActivity).toEqual({});
|
||||
expect(reducer(undefined, {} as any).lastActivity).toEqual({});
|
||||
|
||||
const state = deepFreezeAndThrowOnMutation({
|
||||
lastActivity: {
|
||||
test_user_id: 123456789,
|
||||
},
|
||||
}) as UsersState;
|
||||
expect(reducer(state, {} as any).lastActivity).toBe(state.lastActivity);
|
||||
});
|
||||
|
||||
test('should store the last activity', () => {
|
||||
const action1 = {
|
||||
type: UserTypes.RECEIVED_LAST_ACTIVITIES,
|
||||
data: {
|
||||
test_user_id: 123456789,
|
||||
},
|
||||
};
|
||||
expect(reducer(initialState, action1).lastActivity).toEqual({
|
||||
test_user_id: 123456789,
|
||||
});
|
||||
|
||||
const state = deepFreezeAndThrowOnMutation({
|
||||
lastActivity: {
|
||||
test_user_id: 123456789,
|
||||
},
|
||||
}) as UsersState;
|
||||
const action2 = {
|
||||
type: UserTypes.RECEIVED_LAST_ACTIVITIES,
|
||||
data: {
|
||||
test_user_id: 987654321,
|
||||
},
|
||||
};
|
||||
expect(reducer(state, action2).lastActivity).toEqual({
|
||||
test_user_id: 987654321,
|
||||
});
|
||||
});
|
||||
|
||||
test('should store the last activity for multiple users', () => {
|
||||
const action1 = {
|
||||
type: UserTypes.RECEIVED_LAST_ACTIVITIES,
|
||||
data: {
|
||||
test_user_id: 123456789,
|
||||
test_user_id_2: 987654321,
|
||||
},
|
||||
};
|
||||
expect(reducer(initialState, action1).lastActivity).toEqual({
|
||||
test_user_id: 123456789,
|
||||
test_user_id_2: 987654321,
|
||||
});
|
||||
|
||||
const state = deepFreezeAndThrowOnMutation({
|
||||
lastActivity: {
|
||||
test_user_id: 1,
|
||||
test_user_id_2: 2,
|
||||
test_user_id_4: 4,
|
||||
},
|
||||
}) as UsersState;
|
||||
const action2 = {
|
||||
type: UserTypes.RECEIVED_LAST_ACTIVITIES,
|
||||
data: {
|
||||
test_user_id: 10,
|
||||
test_user_id_2: 20,
|
||||
test_user_id_3: 30,
|
||||
},
|
||||
};
|
||||
expect(reducer(state, action2).lastActivity).toEqual({
|
||||
test_user_id: 10,
|
||||
test_user_id_2: 20,
|
||||
test_user_id_3: 30,
|
||||
test_user_id_4: 4,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -6,7 +6,7 @@ import type {AnyAction} from 'redux';
|
||||
import {combineReducers} from 'redux';
|
||||
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
import type {UserAccessToken, UserProfile, UserStatus, UsersState} from '@mattermost/types/users';
|
||||
import type {UserProfile, UsersState} from '@mattermost/types/users';
|
||||
import type {IDMappedObjects, RelationOneToManyUnique, RelationOneToOne} from '@mattermost/types/utilities';
|
||||
|
||||
import {UserTypes, ChannelTypes} from 'mattermost-redux/action_types';
|
||||
@ -82,7 +82,7 @@ function removeProfileFromSet(state: RelationOneToManyUnique<Team, UserProfile>,
|
||||
};
|
||||
}
|
||||
|
||||
function currentUserId(state = '', action: AnyAction) {
|
||||
function currentUserId(state: UsersState['currentUserId'] = '', action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case UserTypes.RECEIVED_ME: {
|
||||
const data = action.data;
|
||||
@ -102,7 +102,7 @@ function currentUserId(state = '', action: AnyAction) {
|
||||
return state;
|
||||
}
|
||||
|
||||
function mySessions(state: Array<{id: string}> = [], action: AnyAction) {
|
||||
function mySessions(state: UsersState['mySessions'] = [], action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case UserTypes.RECEIVED_SESSIONS:
|
||||
return [...action.data];
|
||||
@ -212,7 +212,7 @@ function receiveUserProfile(state: IDMappedObjects<UserProfile>, received: UserP
|
||||
};
|
||||
}
|
||||
|
||||
function profiles(state: IDMappedObjects<UserProfile> = {}, action: AnyAction) {
|
||||
function profiles(state: UsersState['profiles'] = {}, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case UserTypes.RECEIVED_ME:
|
||||
case UserTypes.RECEIVED_PROFILE: {
|
||||
@ -490,29 +490,10 @@ function profilesNotInGroup(state: UsersState['profilesNotInGroup'] = {}, action
|
||||
}
|
||||
}
|
||||
|
||||
function addToState<T>(state: Record<string, T>, key: string, value: T): Record<string, T> {
|
||||
if (state[key] === value) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
[key]: value,
|
||||
};
|
||||
}
|
||||
|
||||
function dndEndTimes(state: RelationOneToOne<UserProfile, number> = {}, action: AnyAction) {
|
||||
function dndEndTimes(state: UsersState['dndEndTimes'] = {}, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case UserTypes.RECEIVED_STATUS: {
|
||||
const userId = action.data.user_id;
|
||||
const endTime = action.data.dnd_end_time;
|
||||
|
||||
return addToState(state, userId, endTime);
|
||||
}
|
||||
case UserTypes.RECEIVED_STATUSES: {
|
||||
const userStatuses: UserStatus[] = action.data;
|
||||
|
||||
return userStatuses.reduce((nextState, userStatus) => addToState(nextState, userStatus.user_id, userStatus.dnd_end_time || 0), state);
|
||||
case UserTypes.RECEIVED_DND_END_TIMES: {
|
||||
return {...state, ...action.data};
|
||||
}
|
||||
case UserTypes.PROFILE_NO_LONGER_VISIBLE: {
|
||||
if (state[action.data.user_id]) {
|
||||
@ -532,16 +513,8 @@ function dndEndTimes(state: RelationOneToOne<UserProfile, number> = {}, action:
|
||||
|
||||
function statuses(state: RelationOneToOne<UserProfile, string> = {}, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case UserTypes.RECEIVED_STATUS: {
|
||||
const userId = action.data.user_id;
|
||||
const status = action.data.status;
|
||||
|
||||
return addToState(state, userId, status);
|
||||
}
|
||||
case UserTypes.RECEIVED_STATUSES: {
|
||||
const userStatuses: UserStatus[] = action.data;
|
||||
|
||||
return userStatuses.reduce((nextState, userStatus) => addToState(nextState, userStatus.user_id, userStatus.status), state);
|
||||
return {...state, ...action.data};
|
||||
}
|
||||
|
||||
case UserTypes.PROFILE_NO_LONGER_VISIBLE: {
|
||||
@ -562,16 +535,8 @@ function statuses(state: RelationOneToOne<UserProfile, string> = {}, action: Any
|
||||
|
||||
function isManualStatus(state: RelationOneToOne<UserProfile, boolean> = {}, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case UserTypes.RECEIVED_STATUS: {
|
||||
const userId = action.data.user_id;
|
||||
const manual = action.data.manual;
|
||||
|
||||
return addToState(state, userId, manual);
|
||||
}
|
||||
case UserTypes.RECEIVED_STATUSES: {
|
||||
const userStatuses: UserStatus[] = action.data;
|
||||
|
||||
return userStatuses.reduce((nextState, userStatus) => addToState(nextState, userStatus.user_id, userStatus.manual || false), state);
|
||||
case UserTypes.RECEIVED_STATUSES_IS_MANUAL: {
|
||||
return {...state, ...action.data};
|
||||
}
|
||||
|
||||
case UserTypes.PROFILE_NO_LONGER_VISIBLE: {
|
||||
@ -590,7 +555,7 @@ function isManualStatus(state: RelationOneToOne<UserProfile, boolean> = {}, acti
|
||||
}
|
||||
}
|
||||
|
||||
function myUserAccessTokens(state: Record<string, UserAccessToken> = {}, action: AnyAction) {
|
||||
function myUserAccessTokens(state: UsersState['myUserAccessTokens'] = {}, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case UserTypes.RECEIVED_MY_USER_ACCESS_TOKEN: {
|
||||
const nextState = {...state};
|
||||
@ -671,20 +636,8 @@ function filteredStats(state: UsersState['filteredStats'] = {}, action: AnyActio
|
||||
|
||||
function lastActivity(state: UsersState['lastActivity'] = {}, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case UserTypes.RECEIVED_STATUS: {
|
||||
const nextState = Object.assign({}, state);
|
||||
nextState[action.data.user_id] = action.data.last_activity_at;
|
||||
|
||||
return nextState;
|
||||
}
|
||||
case UserTypes.RECEIVED_STATUSES: {
|
||||
const nextState = Object.assign({}, state);
|
||||
|
||||
for (const s of action.data) {
|
||||
nextState[s.user_id] = s.last_activity_at;
|
||||
}
|
||||
|
||||
return nextState;
|
||||
case UserTypes.RECEIVED_LAST_ACTIVITIES: {
|
||||
return {...state, ...action.data};
|
||||
}
|
||||
case UserTypes.LOGOUT_SUCCESS:
|
||||
return {};
|
||||
|
Loading…
Reference in New Issue
Block a user