From 631d59249e237c0ccb3f24d0037683ee59ef20a4 Mon Sep 17 00:00:00 2001 From: AsisRout Date: Wed, 4 Oct 2023 00:52:18 +0530 Subject: [PATCH] MM-24276 Added "User joined and left" system messages (#24332) * update log message * Delete package-lock.json * update Message ID * Add Logic for Joined-Left Event * fix join-leave single user * Revert Log File * Fix i18 extract file * Add tests * update tests * Add a few more test cases --------- Co-authored-by: Asis Rout Co-authored-by: Mattermost Build Co-authored-by: Harrison Healey --- .../combined_system_message.tsx | 20 +- .../combined_system_message/last_users.tsx | 4 + webapp/channels/src/i18n/en.json | 5 + .../mattermost-redux/src/constants/posts.ts | 1 + .../src/utils/post_list.test.ts | 205 ++++++++++++++++++ .../mattermost-redux/src/utils/post_list.ts | 41 ++++ 6 files changed, 275 insertions(+), 1 deletion(-) diff --git a/webapp/channels/src/components/post_view/combined_system_message/combined_system_message.tsx b/webapp/channels/src/components/post_view/combined_system_message/combined_system_message.tsx index 5b20c3116a..8030766568 100644 --- a/webapp/channels/src/components/post_view/combined_system_message/combined_system_message.tsx +++ b/webapp/channels/src/components/post_view/combined_system_message/combined_system_message.tsx @@ -17,7 +17,7 @@ import {t} from 'utils/i18n'; import LastUsers from './last_users'; const { - JOIN_CHANNEL, ADD_TO_CHANNEL, REMOVE_FROM_CHANNEL, LEAVE_CHANNEL, + JOIN_CHANNEL, ADD_TO_CHANNEL, REMOVE_FROM_CHANNEL, LEAVE_CHANNEL, JOIN_LEAVE_CHANNEL, JOIN_TEAM, ADD_TO_TEAM, REMOVE_FROM_TEAM, LEAVE_TEAM, } = Posts.POST_TYPES; @@ -94,6 +94,24 @@ const postTypeMessage = { defaultMessage: '{users} and {lastUser} **left the channel**.', }, }, + [JOIN_LEAVE_CHANNEL]: { + one: { + id: t('combined_system_message.join_left_channel.one'), + defaultMessage: '{firstUser} **joined and left the channel**.', + }, + one_you: { + id: t('combined_system_message.join_left_channel.one_you'), + defaultMessage: 'You **joined and left the channel**.', + }, + two: { + id: t('combined_system_message.join_left_channel.two'), + defaultMessage: '{firstUser} and {secondUser} **joined and left the channel**.', + }, + many_expanded: { + id: t('combined_system_message.join_left_channel.many_expanded'), + defaultMessage: '{users} and {lastUser} **joined and left the channel**.', + }, + }, [JOIN_TEAM]: { one: { id: t('combined_system_message.joined_team.one'), diff --git a/webapp/channels/src/components/post_view/combined_system_message/last_users.tsx b/webapp/channels/src/components/post_view/combined_system_message/last_users.tsx index 37ee516cdc..e12c9218e9 100644 --- a/webapp/channels/src/components/post_view/combined_system_message/last_users.tsx +++ b/webapp/channels/src/components/post_view/combined_system_message/last_users.tsx @@ -25,6 +25,10 @@ const typeMessage = { id: t('last_users_message.left_channel.type'), defaultMessage: '**left the channel**.', }, + [Posts.POST_TYPES.JOIN_LEAVE_CHANNEL]: { + id: t('last_users_message.joined_left_channel.type'), + defaultMessage: '**joined and left the channel**.', + }, [Posts.POST_TYPES.REMOVE_FROM_CHANNEL]: { id: t('last_users_message.removed_from_channel.type'), defaultMessage: 'were **removed from the channel**.', diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 4cdd6a785e..1b7077c324 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -3132,6 +3132,10 @@ "combined_system_message.added_to_team.one": "{firstUser} **added to the team** by {actor}.", "combined_system_message.added_to_team.one_you": "You were **added to the team** by {actor}.", "combined_system_message.added_to_team.two": "{firstUser} and {secondUser} **added to the team** by {actor}.", + "combined_system_message.join_left_channel.many_expanded": "{users} and {lastUser} **joined and left the channel**.", + "combined_system_message.join_left_channel.one": "{firstUser} **joined and left the channel**.", + "combined_system_message.join_left_channel.one_you": "You **joined and left the channel**.", + "combined_system_message.join_left_channel.two": "{firstUser} and {secondUser} **joined and left the channel**.", "combined_system_message.joined_channel.many_expanded": "{users} and {lastUser} **joined the channel**.", "combined_system_message.joined_channel.one": "{firstUser} **joined the channel**.", "combined_system_message.joined_channel.one_you": "You **joined the channel**.", @@ -3843,6 +3847,7 @@ "last_users_message.added_to_team.type": "were **added to the team** by {actor}.", "last_users_message.first": "{firstUser} and ", "last_users_message.joined_channel.type": "**joined the channel**.", + "last_users_message.joined_left_channel.type": "**joined and left the channel**.", "last_users_message.joined_team.type": "**joined the team**.", "last_users_message.left_channel.type": "**left the channel**.", "last_users_message.left_team.type": "**left the team**.", diff --git a/webapp/channels/src/packages/mattermost-redux/src/constants/posts.ts b/webapp/channels/src/packages/mattermost-redux/src/constants/posts.ts index abed36223d..98a76a4777 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/constants/posts.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/constants/posts.ts @@ -17,6 +17,7 @@ export const PostTypes = { JOIN_CHANNEL: 'system_join_channel' as PostType, GUEST_JOIN_CHANNEL: 'system_guest_join_channel' as PostType, LEAVE_CHANNEL: 'system_leave_channel' as PostType, + JOIN_LEAVE_CHANNEL: 'system_join_leave_channel' as PostType, ADD_REMOVE: 'system_add_remove' as PostType, ADD_TO_CHANNEL: 'system_add_to_channel' as PostType, ADD_GUEST_TO_CHANNEL: 'system_add_guest_to_chan' as PostType, diff --git a/webapp/channels/src/packages/mattermost-redux/src/utils/post_list.test.ts b/webapp/channels/src/packages/mattermost-redux/src/utils/post_list.test.ts index 53f0c74861..9df0bff0e3 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/utils/post_list.test.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/utils/post_list.test.ts @@ -1476,6 +1476,211 @@ describe('combineUserActivityData', () => { }; expect(combineUserActivitySystemPost(posts)).toEqual(expectedOutput); }); + it('correctly combine Join and Leave Posts', () => { + const postJoinChannel1 = TestHelper.getPostMock({type: PostTypes.JOIN_CHANNEL, user_id: 'user_id_1'}); + const postLeaveChannel1 = TestHelper.getPostMock({type: PostTypes.LEAVE_CHANNEL, user_id: 'user_id_1'}); + const postJoinChannel2 = TestHelper.getPostMock({type: PostTypes.JOIN_CHANNEL, user_id: 'user_id_2'}); + const postLeaveChannel2 = TestHelper.getPostMock({type: PostTypes.LEAVE_CHANNEL, user_id: 'user_id_2'}); + const postJoinChannel3 = TestHelper.getPostMock({type: PostTypes.JOIN_CHANNEL, user_id: 'user_id_3'}); + const postLeaveChannel3 = TestHelper.getPostMock({type: PostTypes.LEAVE_CHANNEL, user_id: 'user_id_3'}); + + const post = [postJoinChannel1, postLeaveChannel1].reverse(); + const expectedOutput = { + allUserIds: ['user_id_1'], + allUsernames: [], + messageData: [ + {postType: PostTypes.JOIN_LEAVE_CHANNEL, userIds: ['user_id_1']}, + ], + }; + expect(combineUserActivitySystemPost(post)).toEqual(expectedOutput); + + const post1 = [postJoinChannel1, postLeaveChannel1, postJoinChannel2, postLeaveChannel2, postJoinChannel3, postLeaveChannel3].reverse(); + const expectedOutput1 = { + allUserIds: ['user_id_1', 'user_id_2', 'user_id_3'], + allUsernames: [], + messageData: [ + {postType: PostTypes.JOIN_LEAVE_CHANNEL, userIds: ['user_id_1', 'user_id_2', 'user_id_3']}, + ], + }; + expect(combineUserActivitySystemPost(post1)).toEqual(expectedOutput1); + + const post2 = [postJoinChannel1, postJoinChannel2, postJoinChannel3, postLeaveChannel1, postLeaveChannel2, postLeaveChannel3].reverse(); + const expectedOutput2 = { + allUserIds: ['user_id_1', 'user_id_2', 'user_id_3'], + allUsernames: [], + messageData: [ + {postType: PostTypes.JOIN_LEAVE_CHANNEL, userIds: ['user_id_1', 'user_id_2', 'user_id_3']}, + ], + }; + expect(combineUserActivitySystemPost(post2)).toEqual(expectedOutput2); + + const post3 = [postJoinChannel1, postJoinChannel2, postLeaveChannel2, postLeaveChannel1, postJoinChannel3, postLeaveChannel3].reverse(); + const expectedOutput3 = { + allUserIds: ['user_id_1', 'user_id_2', 'user_id_3'], + allUsernames: [], + messageData: [ + {postType: PostTypes.JOIN_LEAVE_CHANNEL, userIds: ['user_id_1', 'user_id_2', 'user_id_3']}, + ], + }; + expect(combineUserActivitySystemPost(post3)).toEqual(expectedOutput3); + }); + it('should only partially combine mismatched join and leave posts', () => { + const postJoinChannel1 = TestHelper.getPostMock({type: PostTypes.JOIN_CHANNEL, user_id: 'user_id_1'}); + const postLeaveChannel1 = TestHelper.getPostMock({type: PostTypes.LEAVE_CHANNEL, user_id: 'user_id_1'}); + const postJoinChannel2 = TestHelper.getPostMock({type: PostTypes.JOIN_CHANNEL, user_id: 'user_id_2'}); + const postLeaveChannel2 = TestHelper.getPostMock({type: PostTypes.LEAVE_CHANNEL, user_id: 'user_id_2'}); + + let posts = [postJoinChannel1, postLeaveChannel1, postJoinChannel2].reverse(); + let expectedOutput = { + allUserIds: ['user_id_1', 'user_id_2'], + allUsernames: [], + messageData: [ + {postType: PostTypes.JOIN_LEAVE_CHANNEL, userIds: ['user_id_1']}, + {postType: PostTypes.JOIN_CHANNEL, userIds: ['user_id_2']}, + ], + }; + expect(combineUserActivitySystemPost(posts)).toEqual(expectedOutput); + + posts = [postJoinChannel1, postLeaveChannel1, postLeaveChannel2].reverse(); + expectedOutput = { + allUserIds: ['user_id_1', 'user_id_2'], + allUsernames: [], + messageData: [ + {postType: PostTypes.JOIN_LEAVE_CHANNEL, userIds: ['user_id_1']}, + {postType: PostTypes.LEAVE_CHANNEL, userIds: ['user_id_2']}, + ], + }; + expect(combineUserActivitySystemPost(posts)).toEqual(expectedOutput); + + posts = [postJoinChannel1, postJoinChannel2, postLeaveChannel1].reverse(); + expectedOutput = { + allUserIds: ['user_id_1', 'user_id_2'], + allUsernames: [], + messageData: [ + {postType: PostTypes.JOIN_CHANNEL, userIds: ['user_id_1', 'user_id_2']}, + {postType: PostTypes.LEAVE_CHANNEL, userIds: ['user_id_1']}, + ], + }; + expect(combineUserActivitySystemPost(posts)).toEqual(expectedOutput); + + posts = [postJoinChannel1, postLeaveChannel2, postLeaveChannel1].reverse(); + expectedOutput = { + allUserIds: ['user_id_1', 'user_id_2'], + allUsernames: [], + messageData: [ + {postType: PostTypes.JOIN_CHANNEL, userIds: ['user_id_1']}, + {postType: PostTypes.LEAVE_CHANNEL, userIds: ['user_id_2', 'user_id_1']}, + ], + }; + expect(combineUserActivitySystemPost(posts)).toEqual(expectedOutput); + + posts = [postJoinChannel2, postJoinChannel1, postLeaveChannel1].reverse(); + expectedOutput = { + allUserIds: ['user_id_2', 'user_id_1'], + allUsernames: [], + messageData: [ + + // This case is arguably incorrect, but it's an edge case + {postType: PostTypes.JOIN_CHANNEL, userIds: ['user_id_2', 'user_id_1']}, + {postType: PostTypes.LEAVE_CHANNEL, userIds: ['user_id_1']}, + ], + }; + expect(combineUserActivitySystemPost(posts)).toEqual(expectedOutput); + + posts = [postLeaveChannel2, postJoinChannel1, postLeaveChannel1].reverse(); + expectedOutput = { + allUserIds: ['user_id_2', 'user_id_1'], + allUsernames: [], + messageData: [ + {postType: PostTypes.LEAVE_CHANNEL, userIds: ['user_id_2']}, + {postType: PostTypes.JOIN_LEAVE_CHANNEL, userIds: ['user_id_1']}, + ], + }; + expect(combineUserActivitySystemPost(posts)).toEqual(expectedOutput); + }); + it('should not combine join and leave posts with other actions in between', () => { + const postJoinChannel1 = TestHelper.getPostMock({type: PostTypes.JOIN_CHANNEL, user_id: 'user_id_1'}); + const postLeaveChannel1 = TestHelper.getPostMock({type: PostTypes.LEAVE_CHANNEL, user_id: 'user_id_1'}); + + const postAddToChannel2 = TestHelper.getPostMock({type: PostTypes.ADD_TO_CHANNEL, user_id: 'user_id_2', props: {addedUserId: 'added_user_id_1', addedUsername: 'added_username_1'}}); + const postAddToTeam2 = TestHelper.getPostMock({type: PostTypes.ADD_TO_TEAM, user_id: 'user_id_2', props: {addedUserId: 'added_user_id_1'}}); + const postJoinTeam2 = TestHelper.getPostMock({type: PostTypes.JOIN_TEAM, user_id: 'user_id_2'}); + const postLeaveTeam2 = TestHelper.getPostMock({type: PostTypes.LEAVE_TEAM, user_id: 'user_id_2'}); + const postRemoveFromChannel2 = TestHelper.getPostMock({type: PostTypes.REMOVE_FROM_CHANNEL, user_id: 'user_id_2', props: {removedUserId: 'removed_user_id_1', removedUsername: 'removed_username_1'}}); + const postRemoveFromTeam2 = TestHelper.getPostMock({type: PostTypes.REMOVE_FROM_TEAM, user_id: 'removed_user_id_1'}); + + let posts = [postJoinChannel1, postAddToChannel2, postLeaveChannel1].reverse(); + let expectedOutput = { + allUserIds: ['user_id_1', 'added_user_id_1', 'user_id_2'], + allUsernames: ['added_username_1'], + messageData: [ + {postType: PostTypes.JOIN_CHANNEL, userIds: ['user_id_1']}, + {postType: PostTypes.ADD_TO_CHANNEL, actorId: 'user_id_2', userIds: ['added_user_id_1']}, + {postType: PostTypes.LEAVE_CHANNEL, userIds: ['user_id_1']}, + ], + }; + expect(combineUserActivitySystemPost(posts)).toEqual(expectedOutput); + + posts = [postJoinChannel1, postAddToTeam2, postLeaveChannel1].reverse(); + expectedOutput = { + allUserIds: ['user_id_1', 'added_user_id_1', 'user_id_2'], + allUsernames: [], + messageData: [ + {postType: PostTypes.JOIN_CHANNEL, userIds: ['user_id_1']}, + {postType: PostTypes.ADD_TO_TEAM, actorId: 'user_id_2', userIds: ['added_user_id_1']}, + {postType: PostTypes.LEAVE_CHANNEL, userIds: ['user_id_1']}, + ], + }; + expect(combineUserActivitySystemPost(posts)).toEqual(expectedOutput); + + posts = [postJoinChannel1, postJoinTeam2, postLeaveChannel1].reverse(); + expectedOutput = { + allUserIds: ['user_id_1', 'user_id_2'], + allUsernames: [], + messageData: [ + {postType: PostTypes.JOIN_CHANNEL, userIds: ['user_id_1']}, + {postType: PostTypes.JOIN_TEAM, userIds: ['user_id_2']}, + {postType: PostTypes.LEAVE_CHANNEL, userIds: ['user_id_1']}, + ], + }; + expect(combineUserActivitySystemPost(posts)).toEqual(expectedOutput); + + posts = [postJoinChannel1, postLeaveTeam2, postLeaveChannel1].reverse(); + expectedOutput = { + allUserIds: ['user_id_1', 'user_id_2'], + allUsernames: [], + messageData: [ + {postType: PostTypes.JOIN_CHANNEL, userIds: ['user_id_1']}, + {postType: PostTypes.LEAVE_TEAM, userIds: ['user_id_2']}, + {postType: PostTypes.LEAVE_CHANNEL, userIds: ['user_id_1']}, + ], + }; + expect(combineUserActivitySystemPost(posts)).toEqual(expectedOutput); + + posts = [postJoinChannel1, postRemoveFromChannel2, postLeaveChannel1].reverse(); + expectedOutput = { + allUserIds: ['user_id_1', 'removed_user_id_1', 'user_id_2'], + allUsernames: ['removed_username_1'], + messageData: [ + {postType: PostTypes.JOIN_CHANNEL, userIds: ['user_id_1']}, + {postType: PostTypes.REMOVE_FROM_CHANNEL, actorId: 'user_id_2', userIds: ['removed_user_id_1']}, + {postType: PostTypes.LEAVE_CHANNEL, userIds: ['user_id_1']}, + ], + }; + expect(combineUserActivitySystemPost(posts)).toEqual(expectedOutput); + + posts = [postJoinChannel1, postRemoveFromTeam2, postLeaveChannel1].reverse(); + expectedOutput = { + allUserIds: ['user_id_1', 'removed_user_id_1'], + allUsernames: [], + messageData: [ + {postType: PostTypes.JOIN_CHANNEL, userIds: ['user_id_1']}, + {postType: PostTypes.REMOVE_FROM_TEAM, userIds: ['removed_user_id_1']}, + {postType: PostTypes.LEAVE_CHANNEL, userIds: ['user_id_1']}, + ], + }; + expect(combineUserActivitySystemPost(posts)).toEqual(expectedOutput); + }); }); describe('shouldShowJoinLeaveMessages', () => { diff --git a/webapp/channels/src/packages/mattermost-redux/src/utils/post_list.ts b/webapp/channels/src/packages/mattermost-redux/src/utils/post_list.ts index 9046890061..6086c7423c 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/utils/post_list.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/utils/post_list.ts @@ -368,6 +368,33 @@ function isUsersRelatedPost(postType: string) { postType === Posts.POST_TYPES.REMOVE_FROM_CHANNEL ); } +function mergeLastSimilarPosts(userActivities: ActivityEntry[]) { + const prevPost = userActivities[userActivities.length - 1]; + const prePrevPost = userActivities[userActivities.length - 2]; + const prevPostType = prevPost && prevPost.postType; + const prePrevPostType = prePrevPost && prePrevPost.postType; + + if (prevPostType === prePrevPostType) { + userActivities.pop(); + prePrevPost.actorId.push(...prevPost.actorId); + } +} +function isSameActorsInUserActivities(prevActivity: ActivityEntry, curActivity: ActivityEntry) { + const prevPostActorsSet = new Set(prevActivity.actorId); + const currentPostActorsSet = new Set(curActivity.actorId); + + if (prevPostActorsSet.size !== currentPostActorsSet.size) { + return false; + } + let hasAllActors = true; + + currentPostActorsSet.forEach((actor) => { + if (!prevPostActorsSet.has(actor)) { + hasAllActors = false; + } + }); + return hasAllActors; +} export function combineUserActivitySystemPost(systemPosts: Post[] = []) { if (systemPosts.length === 0) { return null; @@ -385,12 +412,26 @@ export function combineUserActivitySystemPost(systemPosts: Post[] = []) { const prevPost = userActivities[userActivities.length - 1]; const isSamePostType = prevPost && prevPost.postType === post.type; const isSameActor = prevPost && prevPost.actorId[0] === post.user_id; + const isJoinedPrevPost = prevPost && prevPost.postType === Posts.POST_TYPES.JOIN_CHANNEL; + const isLeftCurrentPost = post.type === Posts.POST_TYPES.LEAVE_CHANNEL; + const prePrevPost = userActivities[userActivities.length - 2]; + const isJoinedPrePrevPost = prePrevPost && prePrevPost.postType === Posts.POST_TYPES.JOIN_CHANNEL; + const isLeftPrevPost = prevPost && prevPost.postType === Posts.POST_TYPES.LEAVE_CHANNEL; if (prevPost && isSamePostType && (isSameActor || isRemovedPost)) { prevPost.userIds.push(userId); prevPost.usernames.push(username); } else if (isSamePostType && !isSameActor && !isUsersRelatedPost(postType)) { prevPost.actorId.push(actorId); + const isSameActors = (prePrevPost && isSameActorsInUserActivities(prePrevPost, prevPost)); + if (isJoinedPrePrevPost && isLeftPrevPost && isSameActors) { + userActivities.pop(); + prePrevPost.postType = Posts.POST_TYPES.JOIN_LEAVE_CHANNEL; + mergeLastSimilarPosts(userActivities); + } + } else if (isJoinedPrevPost && isLeftCurrentPost && prevPost.actorId.length === 1 && isSameActor) { + prevPost.postType = Posts.POST_TYPES.JOIN_LEAVE_CHANNEL; + mergeLastSimilarPosts(userActivities); } else { userActivities.push({ actorId: [actorId],