Skip to content

Commit

Permalink
Add fetchMissingUsers option to AtMention and messageHtmlToComponent (m…
Browse files Browse the repository at this point in the history
…attermost#29000)

* Add fetchMissingUsers option to AtMention and messageHtmlToComponent

* Update snapshots
  • Loading branch information
hmhealey authored Nov 1, 2024
1 parent ee20ac8 commit e1b0f1d
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 24 deletions.
13 changes: 13 additions & 0 deletions webapp/channels/src/components/at_mention/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import type {UserProfile} from '@mattermost/types/users';

import {getMissingProfilesByUsernames} from 'mattermost-redux/actions/users';
import type {ActionFuncAsync} from 'mattermost-redux/types/actions';

import {getPotentialMentionsForName} from 'utils/post_utils';

export function getMissingMentionedUsers(text: string): ActionFuncAsync<Array<UserProfile['username']>> {
return getMissingProfilesByUsernames(getPotentialMentionsForName(text));
}
33 changes: 32 additions & 1 deletion webapp/channels/src/components/at_mention/at_mention.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {General} from 'mattermost-redux/constants';

import AtMention from 'components/at_mention/at_mention';

import {render} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';

/* eslint-disable global-require */
Expand All @@ -28,7 +29,7 @@ describe('components/AtMention', () => {
marketing: TestHelper.getGroupMock({id: 'qwerty2', name: 'marketing', allow_reference: false}),
accounting: TestHelper.getGroupMock({id: 'qwerty3', name: 'accounting', allow_reference: true}),
},
dispatch: jest.fn(),
getMissingMentionedUsers: jest.fn(),
};

test('should match snapshot when mentioning user', () => {
Expand Down Expand Up @@ -227,4 +228,34 @@ describe('components/AtMention', () => {

expect(wrapper).toMatchSnapshot();
});

describe('fetchMissingUsers', () => {
test('when fetchMissingUsers is true, should fetch an unloaded user on mount', () => {
render(
<AtMention
{...baseProps}
mentionName='someuser'
fetchMissingUsers={true}
>
{'@someuser'}
</AtMention>,
);

expect(baseProps.getMissingMentionedUsers).toHaveBeenCalledWith('someuser');
});

test('when fetchMissingUsers is false, should not fetch an unloaded user on mount', () => {
shallow(
<AtMention
{...baseProps}
mentionName='someuser'
fetchMissingUsers={false}
>
{'@someuser'}
</AtMention>,
);

expect(baseProps.getMissingMentionedUsers).not.toHaveBeenCalledWith('someuser');
});
});
});
9 changes: 8 additions & 1 deletion webapp/channels/src/components/at_mention/at_mention.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// See LICENSE.txt for license information.

import classNames from 'classnames';
import React, {useRef, useMemo, memo} from 'react';
import React, {useRef, useMemo, memo, useEffect} from 'react';

import {Client4} from 'mattermost-redux/client';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
Expand All @@ -22,6 +22,7 @@ type OwnProps = {
channelId?: string;
disableHighlight?: boolean;
disableGroupHighlight?: boolean;
fetchMissingUsers?: boolean;
}

type Props = OwnProps & PropsFromRedux;
Expand All @@ -34,6 +35,12 @@ const AtMention = (props: Props) => {
[props.mentionName, props.usersByUsername, props.groupsByName, props.disableGroupHighlight],
);

useEffect(() => {
if (!user && !group && props.fetchMissingUsers) {
props.getMissingMentionedUsers(props.mentionName);
}
}, [props.mentionName]);

const returnFocus = () => {
document.dispatchEvent(new CustomEvent<A11yFocusEventDetail>(
A11yCustomEventTypes.FOCUS, {
Expand Down
7 changes: 6 additions & 1 deletion webapp/channels/src/components/at_mention/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {getCurrentUserId, getUsersByUsername} from 'mattermost-redux/selectors/e

import type {GlobalState} from 'types/store';

import {getMissingMentionedUsers} from './actions';
import AtMention from './at_mention';

function mapStateToProps(state: GlobalState) {
Expand All @@ -21,7 +22,11 @@ function mapStateToProps(state: GlobalState) {
};
}

const connector = connect(mapStateToProps);
const mapDispatchToProps = {
getMissingMentionedUsers,
};

const connector = connect(mapStateToProps, mapDispatchToProps);
export type PropsFromRedux = ConnectedProps<typeof connector>;

export default connector(AtMention);
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
>
<Memo(AtMention)
currentUserId="admin1"
dispatch={[Function]}
getMissingMentionedUsers={[Function]}
groupsByName={Object {}}
mentionName="user-0"
teammateNameDisplay="username"
Expand All @@ -153,7 +153,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
>
<Memo(AtMention)
currentUserId="admin1"
dispatch={[Function]}
getMissingMentionedUsers={[Function]}
groupsByName={Object {}}
mentionName="user-1"
teammateNameDisplay="username"
Expand Down Expand Up @@ -316,7 +316,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
>
<Memo(AtMention)
currentUserId="admin1"
dispatch={[Function]}
getMissingMentionedUsers={[Function]}
groupsByName={Object {}}
mentionName="user-0"
teammateNameDisplay="username"
Expand All @@ -330,7 +330,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
>
<Memo(AtMention)
currentUserId="admin1"
dispatch={[Function]}
getMissingMentionedUsers={[Function]}
groupsByName={Object {}}
mentionName="user-1"
teammateNameDisplay="username"
Expand Down Expand Up @@ -553,7 +553,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
>
<Memo(AtMention)
currentUserId="admin1"
dispatch={[Function]}
getMissingMentionedUsers={[Function]}
groupsByName={Object {}}
mentionName="user-0"
teammateNameDisplay="username"
Expand Down Expand Up @@ -873,7 +873,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
>
<Memo(AtMention)
currentUserId="admin1"
dispatch={[Function]}
getMissingMentionedUsers={[Function]}
groupsByName={Object {}}
mentionName="user-0"
teammateNameDisplay="username"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ export function getMissingProfilesByIds(userIds: string[]): ActionFuncAsync<Arra

export function getMissingProfilesByUsernames(usernames: string[]): ActionFuncAsync<Array<UserProfile['username']>> {
return async (dispatch, getState, {loaders}: any) => {
if (!loaders.missingUsernameLoader) {
loaders.missingUsernameLoader = new DelayedDataLoader<UserProfile['username']>({
if (!loaders.userByUsernameLoader) {
loaders.userByUsernameLoader = new DelayedDataLoader<UserProfile['username']>({
fetchBatch: (usernames) => dispatch(getProfilesByUsernames(usernames)),
maxBatchSize: maxUserIdsPerProfilesRequest,
wait: missingProfilesWait,
Expand All @@ -210,7 +210,7 @@ export function getMissingProfilesByUsernames(usernames: string[]): ActionFuncAs
const missingUsernames = usernames.filter((username) => !usersByUsername[username]);

if (missingUsernames.length > 0) {
await loaders.missingUsernameLoader.queueAndWait(missingUsernames);
await loaders.userByUsernameLoader.queueAndWait(missingUsernames);
}

return {data: missingUsernames};
Expand Down
8 changes: 8 additions & 0 deletions webapp/channels/src/utils/message_html_to_component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ export type Options = Partial<{
images: boolean;
atPlanMentions: boolean;
channelId: string;

/**
* Whether or not the AtMention component should attempt to fetch at-mentioned users if none can be found for
* something that looks like an at-mention. This defaults to false because the web app currently loads at-mentioned
* users automatically for all posts.
*/
fetchMissingUsers: boolean;
}>

type ProcessingInstruction = {
Expand Down Expand Up @@ -132,6 +139,7 @@ export function messageHtmlToComponent(html: string, options: Options = {}) {
disableHighlight={!mentionHighlight}
disableGroupHighlight={disableGroupHighlight}
channelId={options.channelId}
fetchMissingUsers={options.fetchMissingUsers}
>
{children}
</AtMention>
Expand Down
37 changes: 25 additions & 12 deletions webapp/channels/src/utils/post_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -757,19 +757,32 @@ export function makeGetIsReactionAlreadyAddedToPost(): (state: GlobalState, post
);
}

export function getMentionDetails(usersByUsername: Record<string, UserProfile | Group>, mentionName: string): UserProfile | Group | undefined {
/**
* Given the text of an at-mention without the @, returns an array containing that text and every substring of it that
* can be made by removing trailing punctiuation.
*
* For example, getPotentialMentionsForName('username') returns ['username'] and
* getPotentialMentionsForName('username..') return ['username..', 'username.', 'username'].
*/
export function getPotentialMentionsForName(mentionName: string): string[] {
let mentionNameToLowerCase = mentionName.toLowerCase();

while (mentionNameToLowerCase.length > 0) {
if (usersByUsername.hasOwnProperty(mentionNameToLowerCase)) {
return usersByUsername[mentionNameToLowerCase];
}
const potentialMentions = [mentionNameToLowerCase];

// Repeatedly trim off trailing punctuation in case this is at the end of a sentence
if ((/[._-]$/).test(mentionNameToLowerCase)) {
mentionNameToLowerCase = mentionNameToLowerCase.substring(0, mentionNameToLowerCase.length - 1);
} else {
break;
// Repeatedly trim off trailing punctuation in case this is at the end of a sentence
while (mentionNameToLowerCase.length > 0 && (/[._-]$/).test(mentionNameToLowerCase)) {
mentionNameToLowerCase = mentionNameToLowerCase.substring(0, mentionNameToLowerCase.length - 1);

potentialMentions.push(mentionNameToLowerCase);
}

return potentialMentions;
}

export function getMentionDetails<T extends UserProfile | Group>(entitiesByName: Record<string, T>, mentionName: string): T | undefined {
for (const potentialMention of getPotentialMentionsForName(mentionName)) {
if (Object.hasOwn(entitiesByName, potentialMention)) {
return entitiesByName[potentialMention];
}
}

Expand All @@ -783,11 +796,11 @@ export function getUserOrGroupFromMentionName(
groupsDisabled?: boolean,
getMention = getMentionDetails,
): [UserProfile?, Group?] {
const user = getMention(users, mentionName) as UserProfile | undefined;
const user = getMention(users, mentionName);

// prioritizes user if user exists with the same name as a group.
if (!user && !groupsDisabled) {
const group = getMention(groups, mentionName) as Group | undefined;
const group = getMention(groups, mentionName);
if (group && !group.allow_reference) {
return [undefined, undefined]; // remove group mention if not allowed to reference
}
Expand Down

0 comments on commit e1b0f1d

Please sign in to comment.