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 {render} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
/* eslint-disable global-require */
@@ -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', () => {
@@ -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');
});
});
});

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

@@ -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';
@@ -22,6 +22,7 @@ type OwnProps = {
channelId?: string;
disableHighlight?: boolean;
disableGroupHighlight?: boolean;
fetchMissingUsers?: boolean;
}
type Props = OwnProps & PropsFromRedux;
@@ -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, {

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

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

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

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

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

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

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

@@ -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 = {
@@ -132,6 +139,7 @@ export function messageHtmlToComponent(html: string, options: Options = {}) {
disableHighlight={!mentionHighlight}
disableGroupHighlight={disableGroupHighlight}
channelId={options.channelId}
fetchMissingUsers={options.fetchMissingUsers}
>
{children}
</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();
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];
}
}
@@ -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
}