Add fetchMissingUsers option to AtMention and messageHtmlToComponent (#29000)

* Add fetchMissingUsers option to AtMention and messageHtmlToComponent

* Update snapshots
Этот коммит содержится в:
Harrison Healey
2024-11-01 11:44:39 -04:00
коммит произвёл GitHub
родитель ee20ac8f69
Коммит e1b0f1d70e
8 изменённых файлов: 101 добавлений и 24 удалений

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

@@ -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));
}

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

@@ -8,6 +8,7 @@ import {General} from 'mattermost-redux/constants';
import AtMention from 'components/at_mention/at_mention'; import AtMention from 'components/at_mention/at_mention';
import {render} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper'; import {TestHelper} from 'utils/test_helper';
/* eslint-disable global-require */ /* eslint-disable global-require */
@@ -28,7 +29,7 @@ describe('components/AtMention', () => {
marketing: TestHelper.getGroupMock({id: 'qwerty2', name: 'marketing', allow_reference: false}), marketing: TestHelper.getGroupMock({id: 'qwerty2', name: 'marketing', allow_reference: false}),
accounting: TestHelper.getGroupMock({id: 'qwerty3', name: 'accounting', allow_reference: true}), accounting: TestHelper.getGroupMock({id: 'qwerty3', name: 'accounting', allow_reference: true}),
}, },
dispatch: jest.fn(), getMissingMentionedUsers: jest.fn(),
}; };
test('should match snapshot when mentioning user', () => { test('should match snapshot when mentioning user', () => {
@@ -227,4 +228,34 @@ describe('components/AtMention', () => {
expect(wrapper).toMatchSnapshot(); 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');
});
});
}); });

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

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import classNames from 'classnames'; 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 {Client4} from 'mattermost-redux/client';
import {displayUsername} from 'mattermost-redux/utils/user_utils'; import {displayUsername} from 'mattermost-redux/utils/user_utils';
@@ -22,6 +22,7 @@ type OwnProps = {
channelId?: string; channelId?: string;
disableHighlight?: boolean; disableHighlight?: boolean;
disableGroupHighlight?: boolean; disableGroupHighlight?: boolean;
fetchMissingUsers?: boolean;
} }
type Props = OwnProps & PropsFromRedux; type Props = OwnProps & PropsFromRedux;
@@ -34,6 +35,12 @@ const AtMention = (props: Props) => {
[props.mentionName, props.usersByUsername, props.groupsByName, props.disableGroupHighlight], [props.mentionName, props.usersByUsername, props.groupsByName, props.disableGroupHighlight],
); );
useEffect(() => {
if (!user && !group && props.fetchMissingUsers) {
props.getMissingMentionedUsers(props.mentionName);
}
}, [props.mentionName]);
const returnFocus = () => { const returnFocus = () => {
document.dispatchEvent(new CustomEvent<A11yFocusEventDetail>( document.dispatchEvent(new CustomEvent<A11yFocusEventDetail>(
A11yCustomEventTypes.FOCUS, { A11yCustomEventTypes.FOCUS, {

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

@@ -10,6 +10,7 @@ import {getCurrentUserId, getUsersByUsername} from 'mattermost-redux/selectors/e
import type {GlobalState} from 'types/store'; import type {GlobalState} from 'types/store';
import {getMissingMentionedUsers} from './actions';
import AtMention from './at_mention'; import AtMention from './at_mention';
function mapStateToProps(state: GlobalState) { function mapStateToProps(state: GlobalState) {
@@ -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 type PropsFromRedux = ConnectedProps<typeof connector>;
export default connector(AtMention); export default connector(AtMention);

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

@@ -139,7 +139,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
> >
<Memo(AtMention) <Memo(AtMention)
currentUserId="admin1" currentUserId="admin1"
dispatch={[Function]} getMissingMentionedUsers={[Function]}
groupsByName={Object {}} groupsByName={Object {}}
mentionName="user-0" mentionName="user-0"
teammateNameDisplay="username" teammateNameDisplay="username"
@@ -153,7 +153,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
> >
<Memo(AtMention) <Memo(AtMention)
currentUserId="admin1" currentUserId="admin1"
dispatch={[Function]} getMissingMentionedUsers={[Function]}
groupsByName={Object {}} groupsByName={Object {}}
mentionName="user-1" mentionName="user-1"
teammateNameDisplay="username" teammateNameDisplay="username"
@@ -316,7 +316,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
> >
<Memo(AtMention) <Memo(AtMention)
currentUserId="admin1" currentUserId="admin1"
dispatch={[Function]} getMissingMentionedUsers={[Function]}
groupsByName={Object {}} groupsByName={Object {}}
mentionName="user-0" mentionName="user-0"
teammateNameDisplay="username" teammateNameDisplay="username"
@@ -330,7 +330,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
> >
<Memo(AtMention) <Memo(AtMention)
currentUserId="admin1" currentUserId="admin1"
dispatch={[Function]} getMissingMentionedUsers={[Function]}
groupsByName={Object {}} groupsByName={Object {}}
mentionName="user-1" mentionName="user-1"
teammateNameDisplay="username" teammateNameDisplay="username"
@@ -553,7 +553,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
> >
<Memo(AtMention) <Memo(AtMention)
currentUserId="admin1" currentUserId="admin1"
dispatch={[Function]} getMissingMentionedUsers={[Function]}
groupsByName={Object {}} groupsByName={Object {}}
mentionName="user-0" mentionName="user-0"
teammateNameDisplay="username" teammateNameDisplay="username"
@@ -873,7 +873,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
> >
<Memo(AtMention) <Memo(AtMention)
currentUserId="admin1" currentUserId="admin1"
dispatch={[Function]} getMissingMentionedUsers={[Function]}
groupsByName={Object {}} groupsByName={Object {}}
mentionName="user-0" mentionName="user-0"
teammateNameDisplay="username" teammateNameDisplay="username"

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

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

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

@@ -35,6 +35,13 @@ export type Options = Partial<{
images: boolean; images: boolean;
atPlanMentions: boolean; atPlanMentions: boolean;
channelId: string; 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 = { type ProcessingInstruction = {
@@ -132,6 +139,7 @@ export function messageHtmlToComponent(html: string, options: Options = {}) {
disableHighlight={!mentionHighlight} disableHighlight={!mentionHighlight}
disableGroupHighlight={disableGroupHighlight} disableGroupHighlight={disableGroupHighlight}
channelId={options.channelId} channelId={options.channelId}
fetchMissingUsers={options.fetchMissingUsers}
> >
{children} {children}
</AtMention> </AtMention>

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

@@ -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(); let mentionNameToLowerCase = mentionName.toLowerCase();
while (mentionNameToLowerCase.length > 0) { const potentialMentions = [mentionNameToLowerCase];
if (usersByUsername.hasOwnProperty(mentionNameToLowerCase)) {
return usersByUsername[mentionNameToLowerCase];
}
// Repeatedly trim off trailing punctuation in case this is at the end of a sentence // Repeatedly trim off trailing punctuation in case this is at the end of a sentence
if ((/[._-]$/).test(mentionNameToLowerCase)) { while (mentionNameToLowerCase.length > 0 && (/[._-]$/).test(mentionNameToLowerCase)) {
mentionNameToLowerCase = mentionNameToLowerCase.substring(0, mentionNameToLowerCase.length - 1); mentionNameToLowerCase = mentionNameToLowerCase.substring(0, mentionNameToLowerCase.length - 1);
} else {
break; 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];
} }
} }
@@ -783,11 +796,11 @@ export function getUserOrGroupFromMentionName(
groupsDisabled?: boolean, groupsDisabled?: boolean,
getMention = getMentionDetails, getMention = getMentionDetails,
): [UserProfile?, Group?] { ): [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. // prioritizes user if user exists with the same name as a group.
if (!user && !groupsDisabled) { if (!user && !groupsDisabled) {
const group = getMention(groups, mentionName) as Group | undefined; const group = getMention(groups, mentionName);
if (group && !group.allow_reference) { if (group && !group.allow_reference) {
return [undefined, undefined]; // remove group mention if not allowed to reference return [undefined, undefined]; // remove group mention if not allowed to reference
} }