[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:
M-ZubairAhmed 2024-04-26 11:17:27 +00:00 committed by GitHub
parent 80e67ace86
commit 54d7011aba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 407 additions and 134 deletions

View File

@ -432,7 +432,8 @@ export function autoResetStatus(): ActionFuncAsync<UserStatus> {
} }
const {currentUserId} = state.entities.users; 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) { if (userStatus?.status === UserStatuses.OUT_OF_OFFICE || !userStatus?.manual) {
return {data: userStatus}; return {data: userStatus};

View File

@ -717,7 +717,7 @@ export function handleNewPostEvent(msg) {
) { ) {
myDispatch({ myDispatch({
type: UserTypes.RECEIVED_STATUSES, 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) { function handleStatusChangedEvent(msg) {
dispatch({ dispatch({
type: UserTypes.RECEIVED_STATUSES, type: UserTypes.RECEIVED_STATUSES,
data: [{user_id: msg.data.user_id, status: msg.data.status}], data: [{[msg.data.user_id]: msg.data.status}],
}); });
} }

View File

@ -507,7 +507,7 @@ describe('handleNewPostEvent', () => {
expect(testStore.getActions()).toContainEqual({ expect(testStore.getActions()).toContainEqual({
type: UserTypes.RECEIVED_STATUSES, 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({ expect(testStore.getActions()).not.toContainEqual({
type: UserTypes.RECEIVED_STATUSES, 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({ expect(testStore.getActions()).not.toContainEqual({
type: UserTypes.RECEIVED_STATUSES, 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({ expect(testStore.getActions()).not.toContainEqual({
type: UserTypes.RECEIVED_STATUSES, type: UserTypes.RECEIVED_STATUSES,
data: [{user_id: post.user_id, status: UserStatuses.ONLINE}], data: [{[post.user_id]: UserStatuses.ONLINE}],
}); });
}); });
}); });

View File

@ -54,8 +54,12 @@ export default keyMirror({
RECEIVED_SESSIONS: null, RECEIVED_SESSIONS: null,
RECEIVED_REVOKED_SESSION: null, RECEIVED_REVOKED_SESSION: null,
RECEIVED_AUDITS: null, RECEIVED_AUDITS: null,
RECEIVED_STATUS: null,
RECEIVED_STATUSES: null, RECEIVED_STATUSES: null,
RECEIVED_DND_END_TIMES: null,
RECEIVED_STATUSES_IS_MANUAL: null,
RECEIVED_LAST_ACTIVITIES: null,
RECEIVED_AUTOCOMPLETE_IN_CHANNEL: null, RECEIVED_AUTOCOMPLETE_IN_CHANNEL: null,
RESET_LOGOUT_STATE: null, RESET_LOGOUT_STATE: null,
RECEIVED_MY_USER_ACCESS_TOKEN: null, RECEIVED_MY_USER_ACCESS_TOKEN: null,

View File

@ -525,16 +525,27 @@ describe('Actions.Users', () => {
it('getStatusesByIds', async () => { it('getStatusesByIds', async () => {
nock(Client4.getBaseRoute()). nock(Client4.getBaseRoute()).
post('/users/status/ids'). 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( await store.dispatch(Actions.getStatusesByIds(
[TestHelper.basicUser!.id], [TestHelper.basicUser!.id],
)); ));
const statuses = store.getState().entities.users.statuses; 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(statuses[TestHelper.basicUser!.id]).toBe('online');
expect(Object.keys(statuses).length).toEqual(1); expect(dndEndTimes[TestHelper.basicUser!.id]).toBe(0);
expect(lastActivity[TestHelper.basicUser!.id]).toBe(1507662212199);
expect(isManualStatus[TestHelper.basicUser!.id]).toBe(false);
}); });
it('getTotalUsersStats', async () => { it('getTotalUsersStats', async () => {
@ -548,25 +559,10 @@ describe('Actions.Users', () => {
expect(stats.total_users_count).toEqual(2605); 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 () => { it('setStatus', async () => {
nock(Client4.getBaseRoute()). nock(Client4.getBaseRoute()).
put(`/users/${TestHelper.basicUser!.id}/status`). put(`/users/${TestHelper.basicUser!.id}/status`).
reply(200, OK_RESPONSE); reply(200, {user_id: TestHelper.basicUser!.id, status: 'away'});
await store.dispatch(Actions.setStatus( await store.dispatch(Actions.setStatus(
{user_id: TestHelper.basicUser!.id, status: 'away'}, {user_id: TestHelper.basicUser!.id, status: 'away'},

View File

@ -11,7 +11,7 @@ import type {UserProfile, UserStatus, GetFilteredUsersStatsOpts, UsersStats, Use
import {UserTypes, AdminTypes} from 'mattermost-redux/action_types'; import {UserTypes, AdminTypes} from 'mattermost-redux/action_types';
import {logError} from 'mattermost-redux/actions/errors'; import {logError} from 'mattermost-redux/actions/errors';
import {setServerVersion, getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general'; 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 {getServerLimits} from 'mattermost-redux/actions/limits';
import {getMyPreferences} from 'mattermost-redux/actions/preferences'; import {getMyPreferences} from 'mattermost-redux/actions/preferences';
import {loadRolesIfNeeded} from 'mattermost-redux/actions/roles'; 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 {getServerVersion} from 'mattermost-redux/selectors/entities/general';
import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences'; import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUserId, getUsers} from 'mattermost-redux/selectors/entities/users'; 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'; import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
export function generateMfaSecret(userId: string) { 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 export function getStatusesByIds(userIds: Array<UserProfile['id']>): ActionFuncAsync<UserStatus[]> {
// 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 {
return async (dispatch, getState) => { return async (dispatch, getState) => {
if (!userIds || userIds.length === 0) {
return {data: []};
}
let recievedStatuses: UserStatus[];
try { try {
await Client4.updateStatus(status); recievedStatuses = await Client4.getStatusesByIds(userIds);
} catch (error) { } catch (error) {
forceLogoutIfNecessary(error, dispatch, getState); forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error)); dispatch(logError(error));
return {error}; return {error};
} }
dispatch({ const statuses: Record<UserProfile['id'], UserStatus['status']> = {};
type: UserTypes.RECEIVED_STATUS, const dndEndTimes: Record<UserProfile['id'], UserStatus['dnd_end_time']> = {};
data: status, 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, getUser,
getMe, getMe,
getUserByUsername, getUserByUsername,
getStatus,
getStatusesByIds, getStatusesByIds,
getSessions, getSessions,
getTotalUsersStats, getTotalUsersStats,

View File

@ -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,
});
});
});

View File

@ -6,7 +6,7 @@ import type {AnyAction} from 'redux';
import {combineReducers} from 'redux'; import {combineReducers} from 'redux';
import type {Team} from '@mattermost/types/teams'; 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 type {IDMappedObjects, RelationOneToManyUnique, RelationOneToOne} from '@mattermost/types/utilities';
import {UserTypes, ChannelTypes} from 'mattermost-redux/action_types'; 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) { switch (action.type) {
case UserTypes.RECEIVED_ME: { case UserTypes.RECEIVED_ME: {
const data = action.data; const data = action.data;
@ -102,7 +102,7 @@ function currentUserId(state = '', action: AnyAction) {
return state; return state;
} }
function mySessions(state: Array<{id: string}> = [], action: AnyAction) { function mySessions(state: UsersState['mySessions'] = [], action: AnyAction) {
switch (action.type) { switch (action.type) {
case UserTypes.RECEIVED_SESSIONS: case UserTypes.RECEIVED_SESSIONS:
return [...action.data]; 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) { switch (action.type) {
case UserTypes.RECEIVED_ME: case UserTypes.RECEIVED_ME:
case UserTypes.RECEIVED_PROFILE: { 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> { function dndEndTimes(state: UsersState['dndEndTimes'] = {}, action: AnyAction) {
if (state[key] === value) {
return state;
}
return {
...state,
[key]: value,
};
}
function dndEndTimes(state: RelationOneToOne<UserProfile, number> = {}, action: AnyAction) {
switch (action.type) { switch (action.type) {
case UserTypes.RECEIVED_STATUS: { case UserTypes.RECEIVED_DND_END_TIMES: {
const userId = action.data.user_id; return {...state, ...action.data};
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.PROFILE_NO_LONGER_VISIBLE: { case UserTypes.PROFILE_NO_LONGER_VISIBLE: {
if (state[action.data.user_id]) { 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) { function statuses(state: RelationOneToOne<UserProfile, string> = {}, action: AnyAction) {
switch (action.type) { 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: { case UserTypes.RECEIVED_STATUSES: {
const userStatuses: UserStatus[] = action.data; return {...state, ...action.data};
return userStatuses.reduce((nextState, userStatus) => addToState(nextState, userStatus.user_id, userStatus.status), state);
} }
case UserTypes.PROFILE_NO_LONGER_VISIBLE: { 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) { function isManualStatus(state: RelationOneToOne<UserProfile, boolean> = {}, action: AnyAction) {
switch (action.type) { switch (action.type) {
case UserTypes.RECEIVED_STATUS: { case UserTypes.RECEIVED_STATUSES_IS_MANUAL: {
const userId = action.data.user_id; return {...state, ...action.data};
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.PROFILE_NO_LONGER_VISIBLE: { 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) { switch (action.type) {
case UserTypes.RECEIVED_MY_USER_ACCESS_TOKEN: { case UserTypes.RECEIVED_MY_USER_ACCESS_TOKEN: {
const nextState = {...state}; const nextState = {...state};
@ -671,20 +636,8 @@ function filteredStats(state: UsersState['filteredStats'] = {}, action: AnyActio
function lastActivity(state: UsersState['lastActivity'] = {}, action: AnyAction) { function lastActivity(state: UsersState['lastActivity'] = {}, action: AnyAction) {
switch (action.type) { switch (action.type) {
case UserTypes.RECEIVED_STATUS: { case UserTypes.RECEIVED_LAST_ACTIVITIES: {
const nextState = Object.assign({}, state); return {...state, ...action.data};
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.LOGOUT_SUCCESS: case UserTypes.LOGOUT_SUCCESS:
return {}; return {};