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 <asisrout@Asiss-MacBook-Air.local>
Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>
This commit is contained in:
AsisRout 2023-10-04 00:52:18 +05:30 committed by GitHub
parent b7f1a7f262
commit 631d59249e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 275 additions and 1 deletions

View File

@ -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'),

View File

@ -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**.',

View File

@ -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**.",

View File

@ -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,

View File

@ -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', () => {

View File

@ -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],