diff --git a/.changeset/fresh-socks-fix.md b/.changeset/fresh-socks-fix.md new file mode 100644 index 000000000000..004677e019b8 --- /dev/null +++ b/.changeset/fresh-socks-fix.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Mentioning users that are not in the channel now dispatches a warning message with actions diff --git a/apps/meteor/app/lib/server/index.ts b/apps/meteor/app/lib/server/index.ts index c2d4bdda7472..80aaa2a64a9e 100644 --- a/apps/meteor/app/lib/server/index.ts +++ b/apps/meteor/app/lib/server/index.ts @@ -21,6 +21,7 @@ import './methods/createToken'; import './methods/deleteMessage'; import './methods/deleteUserOwnAccount'; import './methods/executeSlashCommandPreview'; +import './startup/mentionUserNotInChannel'; import './methods/getChannelHistory'; import './methods/getRoomJoinCode'; import './methods/getRoomRoles'; diff --git a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js index 4ed882cfe874..395ddfe6d460 100644 --- a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js +++ b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js @@ -1,6 +1,4 @@ -import { Room } from '@rocket.chat/core-services'; import { Subscriptions, Users } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; import moment from 'moment'; import { callbacks } from '../../../../lib/callbacks'; @@ -347,50 +345,7 @@ export async function sendAllNotifications(message, room) { return message; } - const { sender, hasMentionToAll, hasMentionToHere, notificationMessage, mentionIds, mentionIdsWithoutGroups } = - await sendMessageNotifications(message, room); - - // on public channels, if a mentioned user is not member of the channel yet, he will first join the channel and then be notified based on his preferences. - if (room.t === 'c') { - // get subscriptions from users already in room (to not send them a notification) - const mentions = [...mentionIdsWithoutGroups]; - const cursor = Subscriptions.findByRoomIdAndUserIds(room._id, mentionIdsWithoutGroups, { - projection: { 'u._id': 1 }, - }); - - for await (const subscription of cursor) { - const index = mentions.indexOf(subscription.u._id); - if (index !== -1) { - mentions.splice(index, 1); - } - } - - const users = await Promise.all( - mentions.map(async (userId) => { - await Room.join({ room, user: { _id: userId } }); - - return userId; - }), - ).catch((error) => { - throw new Meteor.Error(error); - }); - - const subscriptions = await Subscriptions.findByRoomIdAndUserIds(room._id, users).toArray(); - users.forEach((userId) => { - const subscription = subscriptions.find((subscription) => subscription.u._id === userId); - - void sendNotification({ - subscription, - sender, - hasMentionToAll, - hasMentionToHere, - message, - notificationMessage, - room, - mentionIds, - }); - }); - } + await sendMessageNotifications(message, room); return message; } diff --git a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts new file mode 100644 index 000000000000..8b8836451472 --- /dev/null +++ b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts @@ -0,0 +1,142 @@ +import { api } from '@rocket.chat/core-services'; +import type { IMessage } from '@rocket.chat/core-typings'; +import { isDirectMessageRoom, isEditedMessage, isRoomFederated } from '@rocket.chat/core-typings'; +import { Subscriptions, Rooms, Users, Settings } from '@rocket.chat/models'; +import type { ActionsBlock } from '@rocket.chat/ui-kit'; +import moment from 'moment'; + +import { callbacks } from '../../../../lib/callbacks'; +import { getUserDisplayName } from '../../../../lib/getUserDisplayName'; +import { isTruthy } from '../../../../lib/isTruthy'; +import { i18n } from '../../../../server/lib/i18n'; +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; + +const APP_ID = 'mention-core'; +const getBlocks = (mentions: IMessage['mentions'], messageId: string, lng: string | undefined) => { + const stringifiedMentions = JSON.stringify(mentions); + return { + addUsersBlock: { + type: 'button', + appId: APP_ID, + blockId: messageId, + value: stringifiedMentions, + actionId: 'add-users', + text: { + type: 'plain_text', + text: i18n.t('Add_them', undefined, lng), + }, + }, + dismissBlock: { + type: 'button', + appId: APP_ID, + blockId: messageId, + value: stringifiedMentions, + actionId: 'dismiss', + text: { + type: 'plain_text', + text: i18n.t('Do_nothing', undefined, lng), + }, + }, + dmBlock: { + type: 'button', + appId: APP_ID, + value: stringifiedMentions, + blockId: messageId, + actionId: 'share-message', + text: { + type: 'plain_text', + text: i18n.t('Let_them_know', undefined, lng), + }, + }, + } as const; +}; + +callbacks.add( + 'beforeSaveMessage', + async (message) => { + // TODO: check if I need to test this 60 second rule. + // If the message was edited, or is older than 60 seconds (imported) + // the notifications will be skipped, so we can also skip this validation + if (isEditedMessage(message) || (message.ts && Math.abs(moment(message.ts).diff(moment())) > 60000) || !message.mentions) { + return message; + } + + const mentions = message.mentions.filter(({ _id }) => _id !== 'all' && _id !== 'here'); + if (!mentions.length) { + return message; + } + + const room = await Rooms.findOneById(message.rid); + if (!room || isDirectMessageRoom(room) || isRoomFederated(room) || room.t === 'l') { + return message; + } + + const subs = await Subscriptions.findByRoomIdAndUserIds( + message.rid, + mentions.map(({ _id }) => _id), + { projection: { u: 1 } }, + ).toArray(); + + // get all users that are mentioned but not in the channel + const mentionsUsersNotInChannel = mentions.filter(({ _id }) => !subs.some((sub) => sub.u._id === _id)); + + if (!mentionsUsersNotInChannel.length) { + return message; + } + + const canAddUsersToThisRoom = await hasPermissionAsync(message.u._id, 'add-user-to-joined-room', message.rid); + const canAddToAnyRoom = await (room.t === 'c' + ? hasPermissionAsync(message.u._id, 'add-user-to-any-c-room') + : hasPermissionAsync(message.u._id, 'add-user-to-any-p-room')); + const canDMUsers = await hasPermissionAsync(message.u._id, 'create-d'); // TODO: Perhaps check if user has DM with mentioned user (might be too expensive) + const canAddUsers = canAddUsersToThisRoom || canAddToAnyRoom; + const { language } = (await Users.findOneById(message.u._id)) || {}; + + const actionBlocks = getBlocks(mentionsUsersNotInChannel, message._id, language); + const elements: ActionsBlock['elements'] = [ + canAddUsers && actionBlocks.addUsersBlock, + (canAddUsers || canDMUsers) && actionBlocks.dismissBlock, + canDMUsers && actionBlocks.dmBlock, + ].filter(isTruthy); + + const messageLabel = canAddUsers + ? 'You_mentioned___mentions__but_theyre_not_in_this_room' + : 'You_mentioned___mentions__but_theyre_not_in_this_room_You_can_ask_a_room_admin_to_add_them'; + + const { value: useRealName } = (await Settings.findOneById('UI_Use_Real_Name')) || {}; + + const usernamesOrNames = mentionsUsersNotInChannel.map( + ({ username, name }) => `*${getUserDisplayName(name, username, Boolean(useRealName))}*`, + ); + + const mentionsText = usernamesOrNames.join(', '); + + // TODO: Mentions style + void api.broadcast('notify.ephemeralMessage', message.u._id, message.rid, { + msg: '', + mentions: mentionsUsersNotInChannel, + tmid: message.tmid, + blocks: [ + { + appId: APP_ID, + type: 'section', + text: { + type: 'mrkdwn', + text: i18n.t(messageLabel, { mentions: mentionsText }, language), + }, + } as const, + Boolean(elements.length) && + ({ + type: 'actions', + appId: APP_ID, + elements, + } as const), + ].filter(isTruthy), + private: true, + }); + + return message; + }, + callbacks.priority.LOW, + 'mention-user-not-in-channel', +); diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 1680bab15cb6..de0e3cfed58e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -328,6 +328,7 @@ "add-team-channel_description": "Permission to add a channel to a team", "add-team-member": "Add Team Member", "add-team-member_description": "Permission to add members to a team", + "Add_them": "Add them", "add-user": "Add User", "add-user_description": "Permission to add new users to the server via users screen", "add-user-to-any-c-room": "Add User to Any Public Channel", @@ -1705,6 +1706,7 @@ "Do_not_display_unread_counter": "Do not display any counter of this channel", "Do_not_provide_this_code_to_anyone": "Do not provide this code to anyone.", "Do_Nothing": "Do Nothing", + "Do_nothing": "Do nothing", "Do_you_have_any_notes_for_this_conversation": "Do you have any notes for this conversation?", "Do_you_want_to_accept": "Do you want to accept?", "Do_you_want_to_change_to_s_question": "Do you want to change to %s?", @@ -3074,6 +3076,7 @@ "leave-p": "Leave Private Groups", "leave-p_description": "Permission to leave private groups", "Lets_get_you_new_one": "Let's get you a new one!", + "Let_them_know": "Let them know", "License": "License", "Line": "Line", "Link": "Link", @@ -5841,6 +5844,9 @@ "You_have_not_verified_your_email": "You have not verified your email.", "You_have_successfully_unsubscribed": "You have successfully unsubscribed from our Mailling List.", "You_must_join_to_view_messages_in_this_channel": "You must join to view messages in this channel", + "You_mentioned___mentions__but_theyre_not_in_this_room": "You mentioned {{mentions}}, but they're not in this room.", + "You_mentioned___mentions__but_theyre_not_in_this_room_You_can_ask_a_room_admin_to_add_them": "You mentioned {{mentions}}, but they're not in this room. You can ask a room admin to add them.", + "You_mentioned___mentions__but_theyre_not_in_this_room_You_let_them_know_via_dm": "You mentioned {{mentions}}, but they're not in this room. You let them know via DM.", "You_need_confirm_email": "You need to confirm your email to login!", "You_need_install_an_extension_to_allow_screen_sharing": "You need install an extension to allow screen sharing", "You_need_to_change_your_password": "You need to change your password", @@ -5879,6 +5885,7 @@ "Your_TOTP_has_been_reset": "Your Two Factor TOTP has been reset.", "Your_web_browser_blocked_Rocket_Chat_from_opening_tab": "Your web browser blocked Rocket.Chat from opening a new tab.", "Your_workspace_is_ready": "Your workspace is ready to use 🎉", + "Youre_not_a_part_of__channel__and_I_mentioned_you_there": "You're not a part of {{channel}} and I mentioned you there", "Zapier": "Zapier", "registration.page.login.errors.wrongCredentials": "User not found or incorrect password", "registration.page.login.errors.invalidEmail": "Invalid Email", diff --git a/apps/meteor/server/modules/core-apps/mention.module.ts b/apps/meteor/server/modules/core-apps/mention.module.ts new file mode 100644 index 000000000000..ac615def8a5a --- /dev/null +++ b/apps/meteor/server/modules/core-apps/mention.module.ts @@ -0,0 +1,125 @@ +import { api } from '@rocket.chat/core-services'; +import type { IUiKitCoreApp } from '@rocket.chat/core-services'; +import type { IMessage } from '@rocket.chat/core-typings'; +import { Subscriptions, Messages } from '@rocket.chat/models'; +import { Meteor } from 'meteor/meteor'; + +import { processWebhookMessage } from '../../../app/lib/server/functions/processWebhookMessage'; +import { addUsersToRoomMethod } from '../../../app/lib/server/methods/addUsersToRoom'; +import { i18n } from '../../lib/i18n'; +import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; + +const retrieveMentionsFromPayload = (stringifiedMentions: string): Exclude => { + try { + const mentions = JSON.parse(stringifiedMentions); + if (!Array.isArray(mentions) || !mentions.length || !('username' in mentions[0])) { + throw new Error('Invalid payload'); + } + return mentions; + } catch (error) { + throw new Error('Invalid payload'); + } +}; + +export class MentionModule implements IUiKitCoreApp { + appId = 'mention-core'; + + async blockAction(payload: any): Promise { + const { + actionId, + payload: { value: stringifiedMentions, blockId: referenceMessageId }, + } = payload; + + const mentions = retrieveMentionsFromPayload(stringifiedMentions); + + const usernames = mentions.map(({ username }) => username); + + const message = await Messages.findOneById(referenceMessageId, { projection: { _id: 1, tmid: 1 } }); + + if (!message) { + throw new Error('Mention bot - Failed to retrieve message information'); + } + + const joinedUsernames = `@${usernames.join(', @')}`; + + if (actionId === 'dismiss') { + void api.broadcast('notify.ephemeralMessage', payload.user._id, payload.room, { + msg: i18n.t( + 'You_mentioned___mentions__but_theyre_not_in_this_room', + { + mentions: joinedUsernames, + }, + payload.user.language, + ), + _id: payload.message, + tmid: message.tmid, + mentions, + }); + return; + } + + if (actionId === 'add-users') { + void addUsersToRoomMethod(payload.user._id, { rid: payload.room, users: usernames as string[] }, payload.user); + void api.broadcast('notify.ephemeralMessage', payload.user._id, payload.room, { + msg: i18n.t( + 'You_mentioned___mentions__but_theyre_not_in_this_room', + { + mentions: joinedUsernames, + }, + payload.user.language, + ), + tmid: message.tmid, + _id: payload.message, + mentions, + }); + return; + } + + if (actionId === 'share-message') { + const sub = await Subscriptions.findOneByRoomIdAndUserId(payload.room, payload.user._id, { projection: { t: 1, rid: 1, name: 1 } }); + // this should exist since the event is fired from withing the room (e.g the user sent a message) + if (!sub) { + throw new Error('Mention bot - Failed to retrieve room information'); + } + + const roomPath = roomCoordinator.getRouteLink(sub.t, { rid: sub.rid, name: sub.name }); + if (!roomPath) { + throw new Error('Mention bot - Failed to retrieve path to room'); + } + + const messageText = i18n.t( + 'Youre_not_a_part_of__channel__and_I_mentioned_you_there', + { + channel: `#${sub.name}`, + }, + payload.user.language, + ); + + const link = new URL(Meteor.absoluteUrl(roomPath)); + link.searchParams.set('msg', message._id); + const text = `[ ](${link.toString()})\n${messageText}`; + + // forwards message to all DMs + await processWebhookMessage( + { + roomId: mentions.map(({ _id }) => _id), + text, + }, + payload.user, + ); + + void api.broadcast('notify.ephemeralMessage', payload.user._id, payload.room, { + msg: i18n.t( + 'You_mentioned___mentions__but_theyre_not_in_this_room_You_let_them_know_via_dm', + { + mentions: joinedUsernames, + }, + payload.user.language, + ), + tmid: message.tmid, + _id: payload.message, + mentions, + }); + } + } +} diff --git a/apps/meteor/server/startup/coreApps.ts b/apps/meteor/server/startup/coreApps.ts index 14b2234f6603..9638d99ab559 100644 --- a/apps/meteor/server/startup/coreApps.ts +++ b/apps/meteor/server/startup/coreApps.ts @@ -1,5 +1,6 @@ import { BannerModule } from '../modules/core-apps/banner.module'; import { CloudAnnouncementsModule } from '../modules/core-apps/cloudAnnouncements.module'; +import { MentionModule } from '../modules/core-apps/mention.module'; import { Nps } from '../modules/core-apps/nps.module'; import { VideoConfModule } from '../modules/core-apps/videoconf.module'; import { registerCoreApp } from '../services/uikit-core-app/service'; @@ -8,3 +9,4 @@ registerCoreApp(new CloudAnnouncementsModule()); registerCoreApp(new Nps()); registerCoreApp(new BannerModule()); registerCoreApp(new VideoConfModule()); +registerCoreApp(new MentionModule()); diff --git a/apps/meteor/tests/e2e/message-mentions.spec.ts b/apps/meteor/tests/e2e/message-mentions.spec.ts index aa3e5b73036e..7645d5b14470 100644 --- a/apps/meteor/tests/e2e/message-mentions.spec.ts +++ b/apps/meteor/tests/e2e/message-mentions.spec.ts @@ -1,9 +1,26 @@ +import { faker } from '@faker-js/faker'; + import { Users } from './fixtures/userStates'; import { HomeChannel } from './page-objects'; +import { createTargetPrivateChannel } from './utils'; import { test, expect } from './utils/test'; + test.use({ storageState: Users.admin.state }); +const getMentionText = (username: string, kind?: number): string => { + if (kind === 1) { + return `You mentioned ${username}, but they're not in this room.`; + } + if (kind === 2) { + return `You mentioned ${username}, but they're not in this room. You can ask a room admin to add them.`; + } + if (kind === 3) { + return `You mentioned ${username}, but they're not in this room. You let them know via DM.`; + } + return `Hello @${username}, how are you`; +}; + test.describe.serial('message-mentions', () => { let poHomeChannel: HomeChannel; @@ -20,4 +37,180 @@ test.describe.serial('message-mentions', () => { await expect(poHomeChannel.content.messagePopupUsers.locator('role=listitem >> text="all"')).toBeVisible(); await expect(poHomeChannel.content.messagePopupUsers.locator('role=listitem >> text="here"')).toBeVisible(); }); + + test.describe('users not in channel', () => { + let targetChannel: string; + let targetChannel2: string; + test.beforeAll(async ({ api }) => { + targetChannel = await createTargetPrivateChannel(api); + }); + + test('all actions', async ({ page }) => { + const adminPage = new HomeChannel(page); + const mentionText = getMentionText(Users.user1.data.username, 1); + + await test.step('receive bot message', async () => { + await adminPage.sidenav.openChat(targetChannel); + await adminPage.content.sendMessage(getMentionText(Users.user1.data.username)); + await expect(adminPage.content.lastUserMessage.locator('.rcx-message-block')).toContainText(mentionText); + }); + + await test.step('show "Do nothing" action', async () => { + await expect(adminPage.content.lastUserMessage.locator('button >> text="Do nothing"')).toBeVisible(); + }); + await test.step('show "Add them" action', async () => { + await expect(adminPage.content.lastUserMessage.locator('button >> text="Add them"')).toBeVisible(); + }); + await test.step('show "Let them know" action', async () => { + await expect(adminPage.content.lastUserMessage.locator('button >> text="Let them know"')).toBeVisible(); + }); + + await test.step('dismiss', async () => { + await adminPage.content.lastUserMessage.locator('button >> text="Do nothing"').click(); + }); + + await test.step('receive second bot message', async () => { + await adminPage.content.sendMessage(getMentionText(Users.user1.data.username)); + await expect(adminPage.content.lastUserMessage.locator('.rcx-message-block')).toContainText(mentionText); + }); + await test.step('send message to users', async () => { + await adminPage.content.lastUserMessage.locator('button >> text="Let them know"').click(); + await expect(adminPage.content.lastUserMessageBody).toContainText(getMentionText(Users.user1.data.username, 3)); + }); + + await test.step('receive third bot message', async () => { + await adminPage.content.sendMessage(getMentionText(Users.user1.data.username)); + await expect(adminPage.content.lastUserMessage.locator('.rcx-message-block')).toContainText(mentionText); + }); + await test.step('add users to room', async () => { + await adminPage.content.lastUserMessage.locator('button >> text="Add them"').click(); + await expect(adminPage.content.lastSystemMessageBody).toContainText('added'); + }); + }); + + test.describe(() => { + test.use({ storageState: Users.user1.state }); + + test('dismiss and share message actions', async ({ page }) => { + const mentionText = getMentionText(Users.user2.data.username, 1); + const userPage = new HomeChannel(page); + + await test.step('receive bot message', async () => { + await userPage.sidenav.openChat(targetChannel); + await userPage.content.sendMessage(getMentionText(Users.user2.data.username)); + await expect(userPage.content.lastUserMessage.locator('.rcx-message-block')).toContainText(mentionText); + }); + + await test.step('show "Do nothing" action', async () => { + await expect(userPage.content.lastUserMessage.locator('button >> text="Do nothing"')).toBeVisible(); + }); + await test.step('show "Let them know" action', async () => { + await expect(userPage.content.lastUserMessage.locator('button >> text="Let them know"')).toBeVisible(); + }); + await test.step('not show "Add them action', async () => { + await expect(userPage.content.lastUserMessage.locator('button >> text="Add them"')).not.toBeVisible(); + }); + + await test.step('dismiss', async () => { + await userPage.content.lastUserMessage.locator('button >> text="Do nothing"').click(); + }); + + await test.step('receive second bot message', async () => { + await userPage.sidenav.openChat(targetChannel); + await userPage.content.sendMessage(getMentionText(Users.user2.data.username)); + await expect(userPage.content.lastUserMessage.locator('.rcx-message-block')).toContainText(mentionText); + }); + await test.step('send message to users', async () => { + await userPage.content.lastUserMessage.locator('button >> text="Let them know"').click(); + await expect(userPage.content.lastUserMessageBody).toContainText(getMentionText(Users.user2.data.username, 3)); + }); + }); + }) + + test.describe(() => { + test.use({ storageState: Users.user1.state }); + test.beforeAll(async ({ api }) => { + expect((await api.post('/permissions.update', { permissions: [{ '_id': 'create-d', 'roles': ['admin'] }] })).status()).toBe(200); + }); + + test.afterAll(async ({ api }) => { + expect((await api.post('/permissions.update', { permissions: [{ '_id': 'create-d', 'roles': ['admin', 'user', 'bot', 'app'] }] })).status()).toBe(200); + }); + + test('dismiss and add users actions', async ({ page }) => { + const mentionText = getMentionText(Users.user2.data.username, 1); + const userPage = new HomeChannel(page); + + await test.step('create private room', async () => { + targetChannel2 = faker.string.uuid(); + + await poHomeChannel.sidenav.openNewByLabel('Channel'); + await poHomeChannel.sidenav.inputChannelName.type(targetChannel2); + await poHomeChannel.sidenav.btnCreate.click(); + + await expect(page).toHaveURL(`/group/${targetChannel2}`); + }) + + await test.step('receive bot message', async () => { + await userPage.sidenav.openChat(targetChannel2); + await userPage.content.sendMessage(getMentionText(Users.user2.data.username)); + await expect(userPage.content.lastUserMessage.locator('.rcx-message-block')).toContainText(mentionText); + }); + await test.step('show "Do nothing" action', async () => { + await expect(userPage.content.lastUserMessage.locator('button >> text="Do nothing"')).toBeVisible(); + }); + await test.step('show "Add them" action', async () => { + await expect(userPage.content.lastUserMessage.locator('button >> text="Add them"')).toBeVisible(); + }); + await test.step('not show "Let them know" action', async () => { + await expect(userPage.content.lastUserMessage.locator('button >> text="Let them know"')).not.toBeVisible(); + }); + + await test.step('dismiss', async () => { + await userPage.content.lastUserMessage.locator('button >> text="Do nothing"').click(); + }); + + await test.step('receive second bot message', async () => { + await userPage.sidenav.openChat(targetChannel2); + await userPage.content.sendMessage(getMentionText(Users.user2.data.username)); + await expect(userPage.content.lastUserMessage.locator('.rcx-message-block')).toContainText(mentionText); + }); + await test.step('add users to room', async () => { + await userPage.content.lastUserMessage.locator('button >> text="Add them"').click(); + await expect(userPage.content.lastSystemMessageBody).toContainText('added'); + }); + }); + }); + + test.describe(() => { + test.use({ storageState: Users.user2.state }); + test.beforeAll(async ({ api }) => { + expect((await api.post('/permissions.update', { permissions: [{ '_id': 'create-d', 'roles': ['admin'] }] })).status()).toBe(200); + }); + + test.afterAll(async ({ api }) => { + expect((await api.post('/permissions.update', { permissions: [{ '_id': 'create-d', 'roles': ['admin', 'user', 'bot', 'app'] }] })).status()).toBe(200); + }); + test('no actions', async ({ page }) => { + const userPage = new HomeChannel(page); + + await test.step('receive bot message', async () => { + await userPage.sidenav.openChat(targetChannel2); + await userPage.content.sendMessage(getMentionText(Users.user3.data.username)); + await expect(userPage.content.lastUserMessage.locator('.rcx-message-block')).toContainText(getMentionText(Users.user3.data.username, 2)); + }); + + await test.step('not show "Do nothing" action', async () => { + await expect(userPage.content.lastUserMessage.locator('button >> text="Do nothing"')).not.toBeVisible(); + }); + await test.step('not show "Add them" action', async () => { + await expect(userPage.content.lastUserMessage.locator('button >> text="Add them"')).not.toBeVisible(); + }); + await test.step('not show "Let them know" action', async () => { + await expect(userPage.content.lastUserMessage.locator('button >> text="Let them know"')).not.toBeVisible(); + }); + }); + }) + + }) });