diff --git a/webapp/channels/src/actions/websocket_actions.jsx b/webapp/channels/src/actions/websocket_actions.jsx index ded39be457..b2d08ae1da 100644 --- a/webapp/channels/src/actions/websocket_actions.jsx +++ b/webapp/channels/src/actions/websocket_actions.jsx @@ -1449,30 +1449,39 @@ function handleGroupUpdatedEvent(msg) { ); } +function handleMyGroupUpdate(groupMember) { + dispatch(batchActions([ + { + type: GroupTypes.ADD_MY_GROUP, + id: groupMember.group_id, + }, + { + type: GroupTypes.RECEIVED_MEMBER_TO_ADD_TO_GROUP, + data: groupMember, + id: groupMember.group_id, + }, + { + type: UserTypes.RECEIVED_PROFILES_FOR_GROUP, + data: [groupMember], + id: groupMember.group_id, + }, + ])); +} + export function handleGroupAddedMemberEvent(msg) { return async (doDispatch, doGetState) => { const state = doGetState(); const currentUserId = getCurrentUserId(state); - const groupInfo = JSON.parse(msg.data.group_member); + const groupMember = JSON.parse(msg.data.group_member); - if (currentUserId === groupInfo.user_id) { - const group = getGroup(state, groupInfo.group_id); + if (currentUserId === groupMember.user_id) { + const group = getGroup(state, groupMember.group_id); if (group) { - dispatch( - { - type: GroupTypes.ADD_MY_GROUP, - id: groupInfo.group_id, - }, - ); + handleMyGroupUpdate(groupMember); } else { - const {error} = await doDispatch(fetchGroup(groupInfo.group_id, true)); + const {error} = await doDispatch(fetchGroup(groupMember.group_id, true)); if (!error) { - dispatch( - { - type: GroupTypes.ADD_MY_GROUP, - id: groupInfo.group_id, - }, - ); + handleMyGroupUpdate(groupMember); } } } @@ -1486,13 +1495,23 @@ function handleGroupDeletedMemberEvent(msg) { const data = JSON.parse(msg.data.group_member); if (currentUserId === data.user_id) { - dispatch( + dispatch(batchActions([ { type: GroupTypes.REMOVE_MY_GROUP, data, id: data.group_id, }, - ); + { + type: UserTypes.RECEIVED_PROFILES_LIST_TO_REMOVE_FROM_GROUP, + data: [data], + id: data.group_id, + }, + { + type: GroupTypes.RECEIVED_MEMBER_TO_REMOVE_FROM_GROUP, + data, + id: data.group_id, + }, + ])); } }; } diff --git a/webapp/channels/src/actions/websocket_actions.test.jsx b/webapp/channels/src/actions/websocket_actions.test.jsx index 399a0e4fd5..d231d17488 100644 --- a/webapp/channels/src/actions/websocket_actions.test.jsx +++ b/webapp/channels/src/actions/websocket_actions.test.jsx @@ -270,8 +270,34 @@ describe('handleGroupAddedMemberEvent', () => { testStore.dispatch(handleGroupAddedMemberEvent(msg)); expect(store.dispatch).toHaveBeenCalledWith({ - type: 'ADD_MY_GROUP', - id: 'group-1', + meta: {batch: true}, + payload: [ + { + type: 'ADD_MY_GROUP', + id: 'group-1', + }, + { + data: { + create_at: 1691178673417, + delete_at: 0, + group_id: 'group-1', + user_id: 'currentUserId', + }, + id: 'group-1', + type: 'RECEIVED_MEMBER_TO_ADD_TO_GROUP', + }, + { + data: [{ + create_at: 1691178673417, + delete_at: 0, + group_id: 'group-1', + user_id: 'currentUserId', + }], + id: 'group-1', + type: 'RECEIVED_PROFILES_FOR_GROUP', + }, + ], + type: 'BATCHING_REDUCER.BATCH', }); }); diff --git a/webapp/channels/src/packages/mattermost-redux/src/action_types/groups.ts b/webapp/channels/src/packages/mattermost-redux/src/action_types/groups.ts index 72d9a43e1b..6acaf74fc0 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/action_types/groups.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/action_types/groups.ts @@ -37,6 +37,9 @@ export default keyMirror({ RECEIVED_GROUP_NOT_ASSOCIATED_TO_CHANNEL: null, RECEIVED_GROUPS_NOT_ASSOCIATED_TO_CHANNEL: null, + RECEIVED_MEMBER_TO_REMOVE_FROM_GROUP: null, + RECEIVED_MEMBER_TO_ADD_TO_GROUP: null, + PATCHED_GROUP_TEAM: null, PATCHED_GROUP_CHANNEL: null, diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/groups.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/groups.ts index c64f0f01df..6a2a64a163 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/groups.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/groups.ts @@ -4,9 +4,8 @@ import type {AnyAction} from 'redux'; import {batchActions} from 'redux-batched-actions'; -import type {GroupPatch, SyncablePatch, GroupCreateWithUserIds, CustomGroupPatch, GroupSearchParams, GetGroupsParams, GetGroupsForUserParams, Group} from '@mattermost/types/groups'; +import type {GroupPatch, SyncablePatch, GroupCreateWithUserIds, CustomGroupPatch, GroupSearchParams, GetGroupsParams, GetGroupsForUserParams, Group, GroupMember} from '@mattermost/types/groups'; import {SyncableType, GroupSource} from '@mattermost/types/groups'; -import type {UserProfile} from '@mattermost/types/users'; import {ChannelTypes, GroupTypes, UserTypes} from 'mattermost-redux/action_types'; import {Client4} from 'mattermost-redux/client'; @@ -344,7 +343,7 @@ export function createGroupWithUserIds(group: GroupCreateWithUserIds): ActionFun }; } -export function addUsersToGroup(groupId: string, userIds: string[]): ActionFuncAsync { +export function addUsersToGroup(groupId: string, userIds: string[]): ActionFuncAsync { return async (dispatch, getState) => { let data; try { @@ -366,7 +365,7 @@ export function addUsersToGroup(groupId: string, userIds: string[]): ActionFuncA }; } -export function removeUsersFromGroup(groupId: string, userIds: string[]): ActionFuncAsync { +export function removeUsersFromGroup(groupId: string, userIds: string[]): ActionFuncAsync { return async (dispatch, getState) => { let data; try { diff --git a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/groups.test.ts b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/groups.test.ts index 277667c629..216ceb8b91 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/groups.test.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/groups.test.ts @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import type {Group} from '@mattermost/types/groups'; + import {GroupTypes} from 'mattermost-redux/action_types'; import reducer from 'mattermost-redux/reducers/entities/groups'; @@ -341,4 +343,100 @@ describe('reducers/entities/groups', () => { expect(newState.syncables).toEqual(expectedState.syncables); }); }); + describe('Groups', () => { + const groupTemplate: Omit = { + name: 'Test Group', + display_name: 'Test Group', + description: 'Test Description', + source: '', + create_at: 1, + update_at: 1, + delete_at: 1, + has_syncables: false, + scheme_admin: false, + allow_reference: false, + remote_id: null, + }; + const groupId = 'ge63nq31sbfy3duzq5f7yqn7ii'; + const userId = 'o3tdawqxot8kikzq8bk54zggbc'; + const stateTemplate = { + syncables: {}, + stats: {}, + myGroups: [], + }; + + const receivedMemberToAddAction = { + type: GroupTypes.RECEIVED_MEMBER_TO_ADD_TO_GROUP, + data: { + user_id: userId, + group_id: groupId, + }, + id: groupId, + }; + const receivedMemberToDelAction = {...receivedMemberToAddAction, type: GroupTypes.RECEIVED_MEMBER_TO_REMOVE_FROM_GROUP}; + + it('GroupTypes.RECEIVED_MEMBER_TO_ADD_TO_GROUP, with member list', () => { + const state = { + ...stateTemplate, + groups: { + [userId]: {...groupTemplate, id: userId, member_ids: [], member_count: 0}, + [groupId]: {...groupTemplate, id: groupId, member_ids: [], member_count: 0}, + }, + }; + + const expectedState = { + ...stateTemplate, + groups: { + [userId]: {...groupTemplate, id: userId, member_ids: [], member_count: 0}, + [groupId]: {...groupTemplate, id: groupId, member_ids: [userId], member_count: 1}, + }, + }; + + const newState = reducer(state, receivedMemberToAddAction); + expect(newState.groups).toEqual(expectedState.groups); + }); + it('GroupTypes.RECEIVED_MEMBER_TO_ADD_TO_GROUP, doublon', () => { + const state = { + ...stateTemplate, + groups: { + [groupId]: {...groupTemplate, id: groupId, member_ids: [userId], member_count: 1}, + }, + }; + + const newState = reducer(state, receivedMemberToAddAction); + expect(newState.groups).toEqual(state.groups); + }); + + it('GroupTypes.RECEIVED_MEMBER_TO_REMOVE_FROM_GROUP, with member list', () => { + const state = { + ...stateTemplate, + groups: { + [userId]: {...groupTemplate, id: userId, member_ids: [], member_count: 0}, + [groupId]: {...groupTemplate, id: groupId, member_ids: [userId], member_count: 1}, + }, + }; + + const expectedState = { + ...stateTemplate, + groups: { + [userId]: {...groupTemplate, id: userId, member_ids: [], member_count: 0}, + [groupId]: {...groupTemplate, id: groupId, member_ids: [], member_count: 0}, + }, + }; + + const newState = reducer(state, receivedMemberToDelAction); + expect(newState.groups).toEqual(expectedState.groups); + }); + it('GroupTypes.RECEIVED_MEMBER_TO_REMOVE_FROM_GROUP, Not existant', () => { + const state = { + ...stateTemplate, + groups: { + [groupId]: {...groupTemplate, id: groupId, member_ids: [groupId], member_count: 1}, + }, + }; + + const newState = reducer(state, receivedMemberToDelAction); + expect(newState.groups).toEqual(state.groups); + }); + }); }); diff --git a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/groups.ts b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/groups.ts index 8183d9a84e..e9cf76cd43 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/groups.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/groups.ts @@ -3,7 +3,7 @@ import {combineReducers} from 'redux'; -import type {GroupChannel, GroupSyncablesState, GroupTeam, Group} from '@mattermost/types/groups'; +import type {GroupChannel, GroupSyncablesState, GroupTeam, Group, GroupMember} from '@mattermost/types/groups'; import type {MMReduxAction} from 'mattermost-redux/action_types'; import {GroupTypes} from 'mattermost-redux/action_types'; @@ -243,6 +243,46 @@ function groups(state: Record = {}, action: MMReduxAction) { return nextState; } + case GroupTypes.RECEIVED_MEMBER_TO_REMOVE_FROM_GROUP: { + const dataInfo: GroupMember = action.data; + + const group = state[dataInfo.group_id]; + + if (Array.isArray(group?.member_ids)) { + const newMemberIds = new Set(group.member_ids); + newMemberIds.delete(dataInfo.user_id); + const newGroup = {...group, + member_ids: [...newMemberIds], + member_count: newMemberIds.size, + }; + return { + ...state, + [group.id]: newGroup, + }; + } + + return state; + } + case GroupTypes.RECEIVED_MEMBER_TO_ADD_TO_GROUP: { + const {group_id: groupId, user_id: userId}: GroupMember = action.data; + + const group = state[groupId]; + + if (Array.isArray(group?.member_ids)) { + const newMemberIds = new Set(group.member_ids); + newMemberIds.add(userId); + const newGroup = {...group, + member_ids: [...newMemberIds], + member_count: newMemberIds.size, + }; + return { + ...state, + [group.id]: newGroup, + }; + } + + return state; + } default: return state; } diff --git a/webapp/platform/client/src/client4.ts b/webapp/platform/client/src/client4.ts index 075ef3743f..94e4a50791 100644 --- a/webapp/platform/client/src/client4.ts +++ b/webapp/platform/client/src/client4.ts @@ -77,6 +77,7 @@ import type { GetGroupsParams, GetGroupsForUserParams, GroupStats, + GroupMember, } from '@mattermost/types/groups'; import type {PostActionResponse} from '@mattermost/types/integration_actions'; import type { @@ -3745,14 +3746,14 @@ export default class Client4 { }; addUsersToGroup = (groupId: string, userIds: string[]) => { - return this.doFetch( + return this.doFetch( `${this.getGroupRoute(groupId)}/members`, {method: 'post', body: JSON.stringify({user_ids: userIds})}, ); }; removeUsersFromGroup = (groupId: string, userIds: string[]) => { - return this.doFetch( + return this.doFetch( `${this.getGroupRoute(groupId)}/members`, {method: 'delete', body: JSON.stringify({user_ids: userIds})}, ); diff --git a/webapp/platform/types/src/groups.ts b/webapp/platform/types/src/groups.ts index 07682d284b..76b47a63b8 100644 --- a/webapp/platform/types/src/groups.ts +++ b/webapp/platform/types/src/groups.ts @@ -182,6 +182,13 @@ export type GroupSearchParams = GetGroupsParams & { include_channel_member_count?: string; } +export type GroupMember = { + group_id: string; + user_id: string; + create_at: number; + deleted_at: number; +} + export type GroupMembership = { user_id: string; roles: string;