From 54d7011aba0831882ce2f5b10b3da7523d534d5d Mon Sep 17 00:00:00 2001 From: M-ZubairAhmed Date: Fri, 26 Apr 2024 11:17:27 +0000 Subject: [PATCH] [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. --- webapp/channels/src/actions/user_actions.ts | 3 +- .../src/actions/websocket_actions.jsx | 4 +- .../src/actions/websocket_actions.test.jsx | 8 +- .../src/action_types/users.ts | 6 +- .../src/actions/users.test.ts | 34 +-- .../mattermost-redux/src/actions/users.ts | 128 +++++--- .../src/reducers/entities/users.test.ts | 285 ++++++++++++++++++ .../src/reducers/entities/users.ts | 73 +---- 8 files changed, 407 insertions(+), 134 deletions(-) diff --git a/webapp/channels/src/actions/user_actions.ts b/webapp/channels/src/actions/user_actions.ts index 93973084ba..afcb9ba377 100644 --- a/webapp/channels/src/actions/user_actions.ts +++ b/webapp/channels/src/actions/user_actions.ts @@ -432,7 +432,8 @@ export function autoResetStatus(): ActionFuncAsync { } 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}; diff --git a/webapp/channels/src/actions/websocket_actions.jsx b/webapp/channels/src/actions/websocket_actions.jsx index 05ff6bb751..877c8bbad0 100644 --- a/webapp/channels/src/actions/websocket_actions.jsx +++ b/webapp/channels/src/actions/websocket_actions.jsx @@ -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}], }); } diff --git a/webapp/channels/src/actions/websocket_actions.test.jsx b/webapp/channels/src/actions/websocket_actions.test.jsx index 8f48186615..651ddff497 100644 --- a/webapp/channels/src/actions/websocket_actions.test.jsx +++ b/webapp/channels/src/actions/websocket_actions.test.jsx @@ -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}], }); }); }); diff --git a/webapp/channels/src/packages/mattermost-redux/src/action_types/users.ts b/webapp/channels/src/packages/mattermost-redux/src/action_types/users.ts index a7eb46d1d9..7726a8b4ee 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/action_types/users.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/action_types/users.ts @@ -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, diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/users.test.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/users.test.ts index f8740ced6f..4ba67e3c78 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/users.test.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/users.test.ts @@ -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'}, diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/users.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/users.ts index 1501e9b894..aa67e6ea33 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/users.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/users.ts @@ -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): ActionFuncAsync { 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 = {}; + const dndEndTimes: Record = {}; + const isManualStatuses: Record = {}; + const lastActivity: Record = {}; - 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 { + 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, diff --git a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/users.test.ts b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/users.test.ts index 7e88a27b8a..0279d16bf4 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/users.test.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/users.test.ts @@ -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, + }); + }); +}); diff --git a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/users.ts b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/users.ts index b3a4aac89a..9c96f3eefd 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/users.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/users.ts @@ -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, }; } -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, received: UserP }; } -function profiles(state: IDMappedObjects = {}, 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(state: Record, key: string, value: T): Record { - if (state[key] === value) { - return state; - } - - return { - ...state, - [key]: value, - }; -} - -function dndEndTimes(state: RelationOneToOne = {}, 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 = {}, action: function statuses(state: RelationOneToOne = {}, 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 = {}, action: Any function isManualStatus(state: RelationOneToOne = {}, 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 = {}, acti } } -function myUserAccessTokens(state: Record = {}, 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 {};