MM-59854 Fully allow at mentions in message attachment field values and add E2E tests (#28018)

* Add descriptions to test cases for getNeededAtMentionedUsernames

* MM-59854 Load users who are at-mentioned in message attachment fields

* MM-59854 Add server support for at-mentioning users in message attachment fields

* Migrate support/external_commands from JS to TS

* MM-59584 Ensure that at-mentions never show in other message attachment fields

* MM-59584 Add E2E tests for how we load users based on at mentions

* Use new E2E test helpers in other places

* Update snapshots
Этот коммит содержится в:
Harrison Healey
2024-09-24 09:41:03 -04:00
коммит произвёл GitHub
родитель 3e84d20dbe
Коммит 46cad5c552
22 изменённых файлов: 503 добавлений и 198 удалений

Просмотреть файл

@@ -243,13 +243,8 @@ function createChannel(channelType: string, teamId: string, userToAdd: Cypress.U
if (userToAdd) {
// # Get user profile by email
return cy.apiGetUserByEmail(userToAdd.email).then(({user}) => {
// # Add user to team
cy.externalRequest({
user: getAdminAccount(),
method: 'post',
path: `channels/${channel.id}/members`,
data: {user_id: user.id},
}).then(() => {
// # Add user to channel
cy.externalAddUserToChannel(user.id, channel.id).then(() => {
// # Explicitly wait to give some time to index before searching
cy.wait(TIMEOUTS.TWO_SEC);
return cy.wrap(channel);

Просмотреть файл

@@ -10,11 +10,7 @@
// Stage: @prod
// Group: @channels @channel
import {getAdminAccount} from '../../../support/env';
describe('View Members modal', () => {
const sysadmin = getAdminAccount();
it('MM-20164 - Going from a Member to an Admin should update the modal', () => {
cy.apiInitSetup().then(({team, user}) => {
cy.apiCreateUser().then(({user: user1}) => {
@@ -24,27 +20,19 @@ describe('View Members modal', () => {
// # Promote user as a system admin
// # Visit default channel and verify members modal
cy.apiLogin(user);
promoteToSysAdmin(user, sysadmin);
cy.externalUpdateUserRoles(user.id, 'system_user system_admin');
cy.visit(`/${team.name}/channels/town-square`);
verifyMemberDropdownAction(true);
// # Make user a regular member
// # Reload and verify members modal
demoteToMember(user, sysadmin);
cy.externalUpdateUserRoles(user.id, 'system_user');
cy.reload();
verifyMemberDropdownAction(false);
});
});
});
const demoteToMember = (user, sysadmin) => {
cy.externalRequest({user: sysadmin, method: 'put', path: `users/${user.id}/roles`, data: {roles: 'system_user'}});
};
const promoteToSysAdmin = (user, sysadmin) => {
cy.externalRequest({user: sysadmin, method: 'put', path: `users/${user.id}/roles`, data: {roles: 'system_user system_admin'}});
};
function verifyMemberDropdownAction(hasActionItem) {
// # Click member count to open member rhs
cy.get('#member_rhs').click();

Просмотреть файл

@@ -11,10 +11,6 @@
import {getAdminAccount} from '../../../support/env';
const demoteToMember = (user, admin) => {
cy.externalRequest({user: admin, method: 'put', path: `users/${user.id}/roles`, data: {roles: 'system_user'}});
};
const demoteToChannelMember = (user, channelId, admin) => {
cy.externalRequest({
user: admin,
@@ -70,7 +66,7 @@ describe('Change Roles', () => {
cy.visit(`/${team.name}/channels/${channel.name}`);
// # Make user a regular member for channel and system
demoteToMember(testUser, admin);
cy.externalUpdateUserRoles(user.id, 'system_user');
demoteToChannelMember(testUser, testChannelId, admin);
// # Reload page to ensure no cache or saved information

Просмотреть файл

@@ -53,13 +53,8 @@ function createChannel(channelType, teamId, userToAdd = null) {
if (userToAdd) {
// # Get user profile by email
return cy.apiGetUserByEmail(userToAdd.email).then(({user}) => {
// # Add user to team
cy.externalRequest({
user: admin,
method: 'post',
path: `channels/${channel.id}/members`,
data: {user_id: user.id},
}).then(() => {
// # Add user to channel
cy.externalAddUserToChannel(user.id, channel.id).then(() => {
// # Explicitly wait to give some time to index before searching
cy.wait(TIMEOUTS.TWO_SEC);
return cy.wrap(channel);

Просмотреть файл

@@ -11,7 +11,6 @@
// Group: @channels @enterprise @system_console
import * as TIMEOUTS from '../../../../../fixtures/timeouts';
import {getAdminAccount} from '../../../../../support/env';
describe('System Console > Site Statistics', () => {
let testTeam;
@@ -102,8 +101,6 @@ describe('System Console > Site Statistics', () => {
it('MM-T902 - Reporting ➜ Site statistics line graphs show same date', () => {
cy.intercept('**/api/v4/**').as('resources');
const sysadmin = getAdminAccount();
let newChannel;
// # Create and visit new channel
@@ -114,7 +111,7 @@ describe('System Console > Site Statistics', () => {
// # Create a bot and get userID
cy.apiCreateBot().then(({bot}) => {
const botUserId = bot.user_id;
cy.externalRequest({user: sysadmin, method: 'put', path: `users/${botUserId}/roles`, data: {roles: 'system_user system_post_all system_admin'}});
cy.externalUpdateUserRoles(botUserId, 'system_user system_post_all system_admin');
// # Get token from bots id
cy.apiAccessToken(botUserId, 'Create token').then(({token}) => {

Просмотреть файл

@@ -60,7 +60,7 @@ const setUserTeamAndChannelMemberships = (user, team, channel, channelAdmin = fa
const admin = getAdminAccount();
// # Set user as regular system user
cy.externalRequest({user: admin, method: 'put', path: `users/${user.id}/roles`, data: {roles: 'system_user'}});
cy.externalUpdateUserRoles(user.id, 'system_user');
// # Get team membership
cy.externalRequest({user: admin, method: 'put', path: `teams/${team.id}/members/${user.id}/schemeRoles`, data: {scheme_user: true, scheme_admin: teamAdmin}});

Просмотреть файл

@@ -91,7 +91,7 @@ const setUserTeamAndChannelMemberships = (user, team, channel, channelAdmin = fa
const admin = getAdminAccount();
// # Set user as regular system user
cy.externalRequest({user: admin, method: 'put', path: `users/${user.id}/roles`, data: {roles: 'system_user'}});
cy.externalUpdateUserRoles(user.id, 'system_user');
// # Get team membership
cy.externalRequest({user: admin, method: 'put', path: `teams/${team.id}/members/${user.id}/schemeRoles`, data: {scheme_user: true, scheme_admin: teamAdmin}});

Просмотреть файл

@@ -0,0 +1,201 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {getAdminAccount} from '../../../../tests/support/env';
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Group: @channels @messaging
describe('loading of at-mentioned users', () => {
const admin = getAdminAccount();
let testChannel;
before(() => {
// # Login as test user and visit off-topic
cy.apiInitSetup({loginAfter: true}).then(({channel, channelUrl}) => {
testChannel = channel;
cy.visit(channelUrl);
// # Wait for the channel to visibly load
cy.findByText('Write to ' + testChannel.display_name);
});
});
it('should load a user who joins the channel', () => {
cy.externalCreateUser({}).then((otherUser) => {
// * The new user shouldn't be loaded because the current user hasn't seen them yet
assertUserNotLoaded(otherUser.id);
// # Have the admin add them to the team and channel
cy.externalAddUserToTeam(otherUser.id, testChannel.team_id);
cy.externalAddUserToChannel(otherUser.id, testChannel.id);
// * Wait for the system message at-mentioning that user to know that they've been loaded
cy.contains('a', '@' + otherUser.username).should('be.visible');
});
});
it('should load a user who posts in the channel', () => {
cy.externalCreateUser({}).then((otherUser) => {
// # Make the new user into an admin so that they can post without joining the channel to simulate
// someone posting in the channel for the first time in a long time
cy.externalUpdateUserRoles(otherUser.id, 'system_user system_admin');
// * The new user shouldn't be loaded because the current user hasn't seen them yet
assertUserNotLoaded(otherUser.id);
// # Make a post as the other user
cy.externalCreatePostAsUser(otherUser, {
channel_id: testChannel.id,
message: 'This is a post',
}).then((post) => {
cy.findByText(post.message).should('be.visible');
cy.get(`#${post.id}_message`).should('be.visible');
});
// * Wait for the user's name to know that they've been loaded
cy.contains('button.user-popover', otherUser.username).should('be.visible');
});
});
it("should load a user who's been at-mentioned in a post", () => {
cy.externalCreateUser({}).then((otherUser) => {
// * The new user shouldn't be loaded because the current user hasn't seen them yet
assertUserNotLoaded(otherUser.id);
// # Have the admin at-mention the new user
cy.externalCreatePostAsUser(admin, {
channel_id: testChannel.id,
message: `Created @${otherUser.username}`,
});
// * Confirm that the at-mention renders as a link
cy.contains('a', '@' + otherUser.username).should('be.visible');
});
});
it("should load a user who's been at-mentioned in a message attachment's text", () => {
cy.externalCreateUser({}).then((otherUser) => {
// * The new user shouldn't be loaded because the current user hasn't seen them yet
assertUserNotLoaded(otherUser.id);
// # Have the admin at-mention the new user
cy.externalCreatePostAsUser(admin, {
channel_id: testChannel.id,
props: {
attachments: [
{text: `Ticket updated by @${otherUser.username}`},
],
},
});
// * Confirm that the at-mention renders as a link
cy.contains('a', '@' + otherUser.username).should('be.visible');
});
});
it("should load a user who's been at-mentioned in a message attachment's pretext", () => {
cy.externalCreateUser({}).then((otherUser) => {
// * The new user shouldn't be loaded because the current user hasn't seen them yet
assertUserNotLoaded(otherUser.id);
// # Have the admin at-mention the new user
cy.externalCreatePostAsUser(admin, {
channel_id: testChannel.id,
props: {
attachments: [
{pretext: `@${otherUser.username} created a ticket`, text: 'Ticket #123 - Fix some bug'},
],
},
});
// * Confirm that the at-mention renders as a link
cy.contains('a', '@' + otherUser.username).should('be.visible');
});
});
it("should not load a user who's been at-mentioned in a message attachment's title", () => {
cy.externalCreateUser({}).then((otherUser) => {
// * The new user shouldn't be loaded because the current user hasn't seen them yet
assertUserNotLoaded(otherUser.id);
// # Have the admin at-mention the new user
cy.externalCreatePostAsUser(admin, {
channel_id: testChannel.id,
props: {
attachments: [
{title: `@${otherUser.username}'s ticket`, text: 'TODO'},
],
},
});
// * Confirm that the attachment title doesn't render as a link
cy.contains('h1', `@${otherUser.username}'s ticket`).should('be.visible');
});
});
it("should not load a user who's been at-mentioned in a message attachment's field's title", () => {
cy.externalCreateUser({}).then((otherUser) => {
// * The new user shouldn't be loaded because the current user hasn't seen them yet
assertUserNotLoaded(otherUser.id);
// # Have the admin at-mention the new user
cy.externalCreatePostAsUser(admin, {
channel_id: testChannel.id,
props: {
attachments: [
{
title: 'Ticket created',
fields: [
{title: `Note from @${otherUser.username}`, value: 'Something happened'},
],
},
],
},
});
// * Confirm that the field title doesn't render as a link
cy.contains('th', `Note from @${otherUser.username}`).should('be.visible');
});
});
it("should load a user who's been at-mentioned in a message attachment's field's value", () => {
cy.externalCreateUser({}).then((otherUser) => {
// * The new user shouldn't be loaded because the current user hasn't seen them yet
assertUserNotLoaded(otherUser.id);
// # Have the admin at-mention the new user
cy.externalCreatePostAsUser(admin, {
channel_id: testChannel.id,
props: {
attachments: [
{
title: 'Ticket created',
fields: [
{title: 'Assignee', value: `Created @${otherUser.username}`},
],
},
],
},
});
// * Confirm that the at-mention renders as a link
cy.contains('a', '@' + otherUser.username).should('be.visible');
});
});
});
function assertUserNotLoaded(userId: string) {
cy.window().then((win) => {
const state = (win as any).store.getState();
cy.wrap(state.entities.users.profiles[userId]).should('be.undefined');
});
}

Просмотреть файл

@@ -10,10 +10,7 @@
// Stage: @prod
// Group: @channels @messaging
import {getAdminAccount} from '../../../support/env';
describe('Messaging', () => {
const sysadmin = getAdminAccount();
let newChannel;
before(() => {
@@ -31,7 +28,7 @@ describe('Messaging', () => {
// # Create a bot and get userID
cy.apiCreateBot().then(({bot}) => {
const botUserId = bot.user_id;
cy.externalRequest({user: sysadmin, method: 'put', path: `users/${botUserId}/roles`, data: {roles: 'system_user system_post_all system_admin'}});
cy.externalUpdateUserRoles(botUserId, 'system_user system_post_all system_admin');
// # Get token from bots id
cy.apiAccessToken(botUserId, 'Create token').then(({token}) => {
@@ -98,7 +95,7 @@ describe('Messaging', () => {
// # Create a bot and get userID
cy.apiCreateBot().then(({bot}) => {
const botUserId = bot.user_id;
cy.externalRequest({user: sysadmin, method: 'put', path: `users/${botUserId}/roles`, data: {roles: 'system_user system_post_all system_admin'}});
cy.externalUpdateUserRoles(botUserId, 'system_user system_post_all system_admin');
// # Get token from bots id
cy.apiAccessToken(botUserId, 'Create token').then(({token}) => {

Просмотреть файл

@@ -10,14 +10,11 @@
// Stage: @prod
// Group: @channels @multi_team_and_dm
import {getAdminAccount} from '../../../support/env';
describe('Multi Team and DM', () => {
let testChannel;
let testTeam;
let testUser;
let otherUser;
const sysadmin = getAdminAccount();
before(() => {
// # Setup with the new team, channel and user
@@ -55,7 +52,7 @@ describe('Multi Team and DM', () => {
cy.findByLabelText('off-topic public channel').click();
// # Add second user to team in external session
cy.externalRequest({user: sysadmin, method: 'post', path: `teams/${testTeam.id}/members`, data: {team_id: testTeam.id, user_id: otherUser.id}});
cy.externalAddUserToTeam(otherUser.id, testTeam.id);
// * Assert that Town Square is still marked as read after second user added to team
cy.findByLabelText('town square public channel').should('be.visible');

Просмотреть файл

@@ -98,8 +98,7 @@ export function setupTestData(data, {team, channel, admin, anotherAdmin}) {
} = data;
// # Create another admin user so we can create override create_at of posts
const baseUrl = Cypress.config('baseUrl');
cy.externalRequest({user: admin, method: 'put', baseUrl, path: `users/${anotherAdmin.id}/roles`, data: {roles: 'system_user system_admin'}});
cy.externalUpdateUserRoles(anotherAdmin.id, 'system_user system_admin');
// # Create a post from today
cy.get('#postListContent', {timeout: TIMEOUTS.HALF_MIN}).should('be.visible');

Просмотреть файл

@@ -11,10 +11,8 @@
// Group: @channels @system_console @smoke
import * as TIMEOUTS from '../../../fixtures/timeouts';
import {getAdminAccount} from '../../../support/env';
describe('System Console', () => {
const sysadmin = getAdminAccount();
let testUser;
before(() => {
@@ -25,10 +23,8 @@ describe('System Console', () => {
});
it('MM-T922 Demoted user cannot continue to view System Console', () => {
const baseUrl = Cypress.config('baseUrl');
// # Set user to be a sysadmin, so it can access the system console
cy.externalRequest({user: sysadmin, method: 'put', baseUrl, path: `users/${testUser.id}/roles`, data: {roles: 'system_user system_admin'}});
cy.externalUpdateUserRoles(testUser.id, 'system_user system_admin');
// # Visit a page on the system console
cy.visit('/admin_console/reporting/system_analytics');
@@ -36,7 +32,7 @@ describe('System Console', () => {
cy.url().should('include', '/admin_console/reporting/system_analytics');
// # Change the role of the user back to user
cy.externalRequest({user: sysadmin, method: 'put', baseUrl, path: `users/${testUser.id}/roles`, data: {roles: 'system_user'}});
cy.externalUpdateUserRoles(testUser.id, 'system_user');
// # User should get redirected to town square
cy.get('#adminConsoleWrapper').should('not.exist');

Просмотреть файл

@@ -102,17 +102,7 @@ describe('Handle new post', () => {
const channel = response.data;
// # And then invite the current user
cy.externalRequest({
user: admin,
baseUrl,
method: 'post',
path: `channels/${channel.id}/members`,
data: {
user_id: user1.id,
},
}).then((addResponse) => {
expect(addResponse.status).to.equal(201);
});
cy.externalAddUserToChannel(user1.id, channel.id);
});
// * Verify that the channel is in the current user's sidebar and is unread with one mention
@@ -145,17 +135,7 @@ describe('Handle new post', () => {
cy.delayRequestToRoutes([`channels/${channel.id}`], 100);
// # And then invite the current user
cy.externalRequest({
user: admin,
baseUrl,
method: 'post',
path: `channels/${channel.id}/members`,
data: {
user_id: user1.id,
},
}).then((addResponse) => {
expect(addResponse.status).to.equal(201);
});
cy.externalAddUserToChannel(user1.id, channel.id);
});
// * Verify that the channel is in the current user's sidebar and is unread with one mention

Просмотреть файл

@@ -1,30 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `external` prefix, e.g. `externalActivateUser`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Makes an external request as a sysadmin and activate/deactivate a user directly via API
* @param {String} userId - The user ID
* @param {Boolean} active - Whether to activate or deactivate - true/false
*
* @example
* cy.externalActivateUser('user-id', false);
*/
externalActivateUser(userId: string, activate: boolean): Chainable;
}
}

Просмотреть файл

@@ -1,11 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {getAdminAccount} from './env';
Cypress.Commands.add('externalActivateUser', (userId, active = true) => {
const baseUrl = Cypress.config('baseUrl');
const admin = getAdminAccount();
cy.externalRequest({user: admin, method: 'put', baseUrl, path: `users/${userId}/active`, data: {active}});
});

Просмотреть файл

@@ -0,0 +1,138 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ChannelMembership} from '@mattermost/types/channels';
import type {Post} from '@mattermost/types/posts';
import type {TeamMembership} from '@mattermost/types/teams';
import type {UserProfile} from '@mattermost/types/users';
import {getAdminAccount} from './env';
import {getRandomId} from '../utils';
function externalActivateUser(userId: string, active = true) {
const admin = getAdminAccount();
cy.externalRequest({user: admin, method: 'PUT', path: `users/${userId}/active`, data: {active}});
}
Cypress.Commands.add('externalActivateUser', externalActivateUser);
function externalAddUserToChannel(userId: string, channelId: string): Cypress.Chainable<ChannelMembership> {
const admin = getAdminAccount();
return cy.externalRequest({
user: admin,
method: 'POST',
path: `channels/${channelId}/members`,
data: {
user_id: userId,
},
}).then((response) => response.data);
}
Cypress.Commands.add('externalAddUserToChannel', externalAddUserToChannel);
function externalAddUserToTeam(userId: string, teamId: string): Cypress.Chainable<TeamMembership> {
const admin = getAdminAccount();
return cy.externalRequest({
user: admin,
method: 'POST',
path: `teams/${teamId}/members`,
data: {
team_id: teamId,
user_id: userId,
},
}).then((response) => response.data);
}
Cypress.Commands.add('externalAddUserToTeam', externalAddUserToTeam);
function externalCreatePostAsUser(user: Pick<UserProfile, 'username' | 'password'>, post: Partial<Post>): Cypress.Chainable<Post> {
return cy.externalRequest({
user,
method: 'POST',
path: 'posts',
data: post,
}).then((response) => response.data);
}
Cypress.Commands.add('externalCreatePostAsUser', externalCreatePostAsUser);
function externalCreateUser(user: Partial<UserProfile>): Cypress.Chainable<UserProfile> {
const admin = getAdminAccount();
const randomValue = getRandomId();
return cy.externalRequest({
user: admin,
method: 'POST',
path: 'users',
data: {
username: 'user' + randomValue,
email: 'email' + randomValue + '@example.mattermost.com',
password: 'password' + randomValue,
...user,
},
}).then((response) => {
// Re-add the password to the result so that we can make requests as that user
return {
...response.data,
password: 'password' + randomValue,
};
});
}
Cypress.Commands.add('externalCreateUser', externalCreateUser);
function externalUpdateUserRoles(userId: string, roles: string): Cypress.Chainable<unknown> {
const admin = getAdminAccount();
return cy.externalRequest({
user: admin,
method: 'PUT',
path: `users/${userId}/roles`,
data: {roles},
});
}
Cypress.Commands.add('externalUpdateUserRoles', externalUpdateUserRoles);
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* Makes an external request as a sysadmin and activate/deactivate a user directly via API
* @param {String} userId - The user ID
* @param {Boolean} active - Whether to activate or deactivate - true/false
*
* @example
* cy.externalActivateUser('user-id', false);
*/
externalActivateUser: typeof externalActivateUser;
/**
* As the system admin, adds a user to a channel.
*/
externalAddUserToChannel: typeof externalAddUserToChannel;
/**
* As the system admin, adds a user to a team.
*/
externalAddUserToTeam: typeof externalAddUserToTeam;
/**
* As the given user, creates a post via the API.
*/
externalCreatePostAsUser: typeof externalCreatePostAsUser;
/**
* As the system admin, creates a new user via the API. The user is automatically given a username, email,
* and password, but the override parameter can be be used to specify any other fields if needed.
*/
externalCreateUser: typeof externalCreateUser;
/**
* As the system admin, updates a user's roles via the API.
*/
externalUpdateUserRoles: typeof externalUpdateUserRoles;
}
}
}

Просмотреть файл

@@ -1410,6 +1410,12 @@ func getMentionsEnabledFields(post *model.Post) model.StringArray {
if attachment.Text != "" {
ret = append(ret, attachment.Text)
}
for _, field := range attachment.Fields {
if valueString, ok := field.Value.(string); ok && valueString != "" {
ret = append(ret, valueString)
}
}
}
return ret
}

Просмотреть файл

@@ -1337,6 +1337,25 @@ func TestGetExplicitMentions(t *testing.T) {
HereMentioned: true,
},
},
"should include the mentions from attachment field values (but not field titles)": {
Message: "this is a message",
Attachments: []*model.SlackAttachment{
{
Fields: []*model.SlackAttachmentField{
{
Title: "@user1",
Value: "@user2",
},
},
},
},
Keywords: map[string][]string{"@user1": {id1}, "@user2": {id2}},
Expected: &MentionResults{
Mentions: map[string]MentionType{
id2: KeywordMention,
},
},
},
"Name on keywords is a prefix of a mention": {
Message: "@other @test-two",
Keywords: map[string][]string{"@test": {model.NewId()}},
@@ -2167,6 +2186,12 @@ func TestGetMentionsEnabledFields(t *testing.T) {
attachmentWithOutPreText := model.SlackAttachment{
Text: "some text",
Fields: []*model.SlackAttachmentField{
{
Title: "field title",
Value: "field value",
},
},
}
attachments := []*model.SlackAttachment{
&attachmentWithTextAndPreText,
@@ -2183,11 +2208,12 @@ func TestGetMentionsEnabledFields(t *testing.T) {
"This is the message",
"@Channel some comment for the channel",
"@here with mentions",
"some text"}
"some text",
"field value",
}
mentionEnabledFields := getMentionsEnabledFields(post)
assert.EqualValues(t, 4, len(mentionEnabledFields))
assert.EqualValues(t, expectedFields, mentionEnabledFields)
}

Просмотреть файл

@@ -424,6 +424,7 @@ exports[`components/post_view/MessageAttachment should match snapshot when the a
message="Do you like https://mattermost.com?"
options={
Object {
"atMentions": false,
"autolinkedUrlSchemes": Array [],
"mentionHighlight": false,
"renderer": LinkOnlyRenderer {
@@ -469,6 +470,7 @@ exports[`components/post_view/MessageAttachment should match snapshot when the a
message="Do you like :pizza:?"
options={
Object {
"atMentions": false,
"autolinkedUrlSchemes": Array [],
"mentionHighlight": false,
"renderer": LinkOnlyRenderer {
@@ -514,6 +516,7 @@ exports[`components/post_view/MessageAttachment should match snapshot when the a
message="Don't you like emojis?"
options={
Object {
"atMentions": false,
"autolinkedUrlSchemes": Array [],
"mentionHighlight": false,
"renderer": LinkOnlyRenderer {
@@ -559,6 +562,7 @@ exports[`components/post_view/MessageAttachment should match snapshot when the f
message="footer"
options={
Object {
"atMentions": false,
"autolinkedUrlSchemes": Array [],
"mentionHighlight": false,
"renderer": LinkOnlyRenderer {
@@ -620,6 +624,7 @@ exports[`components/post_view/MessageAttachment should match value on getFieldsT
message="title_1"
options={
Object {
"atMentions": false,
"markdown": false,
"mentionHighlight": false,
}
@@ -654,6 +659,7 @@ exports[`components/post_view/MessageAttachment should match value on getFieldsT
message="title_2"
options={
Object {
"atMentions": false,
"markdown": false,
"mentionHighlight": false,
}

Просмотреть файл

@@ -232,7 +232,7 @@ export default class MessageAttachment extends React.PureComponent<Props, State>
let rowPos = 0;
let lastWasLong = false;
let nrTables = 0;
const markdown = {markdown: false, mentionHighlight: false};
const markdown = {markdown: false, mentionHighlight: false, atMentions: false};
fields.forEach((field: MessageAttachmentField, i: number) => {
if (rowPos === 2 || !(field.short === true) || lastWasLong) {
@@ -421,6 +421,7 @@ export default class MessageAttachment extends React.PureComponent<Props, State>
<Markdown
message={attachment.title}
options={{
atMentions: false,
mentionHighlight: false,
renderer: new LinkOnlyRenderer(),
autolinkedUrlSchemes: [],

Просмотреть файл

@@ -570,7 +570,7 @@ describe('Actions.Posts', () => {
});
});
it('getNeededAtMentionedUsernames', async () => {
describe('getNeededAtMentionedUsernames', () => {
const state = {
entities: {
users: {
@@ -592,65 +592,55 @@ describe('Actions.Posts', () => {
},
} as unknown as GlobalState;
expect(
Actions.getNeededAtMentionedUsernamesAndGroups(state, [
TestHelper.getPostMock({message: 'aaa'}),
])).toEqual(
new Set(),
);
expect(
Actions.getNeededAtMentionedUsernamesAndGroups(state, [
TestHelper.getPostMock({message: '@aaa'}),
])).toEqual(
new Set(),
);
expect(
Actions.getNeededAtMentionedUsernamesAndGroups(state, [
TestHelper.getPostMock({message: '@zzz'}),
])).toEqual(
new Set(),
);
expect(
Actions.getNeededAtMentionedUsernamesAndGroups(state, [
TestHelper.getPostMock({message: '@aaa @bbb @ccc @zzz'}),
])).toEqual(
new Set(['bbb', 'ccc']),
);
expect(
Actions.getNeededAtMentionedUsernamesAndGroups(state, [
TestHelper.getPostMock({message: '@bbb. @ccc.ddd'}),
])).toEqual(
new Set(['bbb.', 'bbb', 'ccc.ddd']),
);
expect(
Actions.getNeededAtMentionedUsernamesAndGroups(state, [
TestHelper.getPostMock({message: '@bbb- @ccc-ddd'}),
])).toEqual(
new Set(['bbb-', 'bbb', 'ccc-ddd']),
);
expect(
Actions.getNeededAtMentionedUsernamesAndGroups(state, [
TestHelper.getPostMock({message: '@bbb_ @ccc_ddd'}),
])).toEqual(
new Set(['bbb_', 'ccc_ddd']),
);
expect(
Actions.getNeededAtMentionedUsernamesAndGroups(state, [
TestHelper.getPostMock({message: '(@bbb/@ccc) ddd@eee'}),
])).toEqual(
new Set(['bbb', 'ccc']),
);
expect(
Actions.getNeededAtMentionedUsernamesAndGroups(state, [
TestHelper.getPostMock({
const testCases = [
{
name: "shouldn't return anything when no users are at-mentioned",
input: TestHelper.getPostMock({message: 'aaa'}),
expected: new Set(),
},
{
name: "shouldn't return anything for a user that's already loaded",
input: TestHelper.getPostMock({message: '@aaa'}),
expected: new Set(),
},
{
name: "shouldn't return anything for a group that's already loaded",
input: TestHelper.getPostMock({message: '@zzz'}),
expected: new Set(),
},
{
name: 'should return any unrecognized at-mentions',
input: TestHelper.getPostMock({message: '@aaa @bbb @ccc @zzz'}),
expected: new Set(['bbb', 'ccc']),
},
{
name: 'should return at-mentions followed by period both with and without the period',
input: TestHelper.getPostMock({message: '@bbb. @ccc.ddd'}),
expected: new Set(['bbb.', 'bbb', 'ccc.ddd']),
},
{
name: 'should return at-mentions followed by hyphen both with and without the hyphen',
input: TestHelper.getPostMock({message: '@bbb- @ccc-ddd'}),
expected: new Set(['bbb-', 'bbb', 'ccc-ddd']),
},
{
name: 'should return at-mentions followed by underscores with the underscore',
input: TestHelper.getPostMock({message: '@bbb_ @ccc_ddd'}),
expected: new Set(['bbb_', 'ccc_ddd']),
},
{
name: 'should return at-mentions in brackets',
input: TestHelper.getPostMock({message: '(@bbb/@ccc)'}),
expected: new Set(['bbb', 'ccc']),
},
{
name: "shouldn't return anything when an at sign is in the middle of a word",
input: TestHelper.getPostMock({message: 'ddd@eee'}),
expected: new Set(),
},
{
name: 'should return at-mentions from inside message attachment props text and pretext',
input: TestHelper.getPostMock({
message: '@aaa @bbb',
props: {
attachments: [
@@ -659,23 +649,55 @@ describe('Actions.Posts', () => {
],
},
}),
]),
).toEqual(
new Set(['bbb', 'ccc', 'ddd', 'eee', 'fff', 'ggg']),
);
expected: new Set(['bbb', 'ccc', 'ddd', 'eee', 'fff', 'ggg']),
},
{
name: 'should return at-mentions from inside message attachment field values but not their titles',
input: TestHelper.getPostMock({
props: {
attachments: [
{
fields: [
{title: '@bbb', value: '@ccc'},
{value: '@ddd'},
],
},
{
fields: [
{title: '@eee', value: '@fff'},
{value: '@ggg'},
],
},
],
},
}),
expected: new Set(['ccc', 'ddd', 'fff', 'ggg']),
},
];
// should never try to request usernames matching special mentions
expect(
Actions.getNeededAtMentionedUsernamesAndGroups(state, [
TestHelper.getPostMock({message: '@all'}),
TestHelper.getPostMock({message: '@here'}),
TestHelper.getPostMock({message: '@channel'}),
TestHelper.getPostMock({message: '@all.'}),
TestHelper.getPostMock({message: '@here.'}),
TestHelper.getPostMock({message: '@channel.'}),
])).toEqual(
new Set(),
);
for (const specialMention of [
'@all',
'@here',
'@channel',
'@all.',
'@here.',
'@channel.',
]) {
testCases.push({
name: `should never return special mentions (${specialMention})`,
input: TestHelper.getPostMock({message: specialMention}),
expected: new Set(),
});
}
for (const testCase of testCases) {
test(testCase.name, () => {
expect(Actions.getNeededAtMentionedUsernamesAndGroups(
state,
[testCase.input],
)).toEqual(testCase.expected);
});
}
});
it('getPostsSince', async () => {

Просмотреть файл

@@ -1090,6 +1090,12 @@ export function getNeededAtMentionedUsernamesAndGroups(state: GlobalState, posts
for (const attachment of post.props.attachments) {
findNeededUsernamesAndGroups(attachment.pretext);
findNeededUsernamesAndGroups(attachment.text);
if (attachment.fields) {
for (const field of attachment.fields) {
findNeededUsernamesAndGroups(field.value);
}
}
}
}
}