Mention Tooltip: Support catching "@" in the middle of input (#2131)

This commit is contained in:
Alexander Zinchuk 2022-11-13 17:06:21 +04:00
parent 99767907a7
commit a2a0161cc9
3 changed files with 35 additions and 18 deletions

View File

@ -104,9 +104,8 @@ const AttachmentModal: FC<OwnProps> = ({
isMentionTooltipOpen, closeMentionTooltip, insertMention, mentionFilteredUsers, isMentionTooltipOpen, closeMentionTooltip, insertMention, mentionFilteredUsers,
} = useMentionTooltip( } = useMentionTooltip(
isOpen, isOpen,
captionRef, `#${EDITABLE_INPUT_MODAL_ID}`,
onCaptionUpdate, onCaptionUpdate,
EDITABLE_INPUT_MODAL_ID,
groupChatMembers, groupChatMembers,
undefined, undefined,
currentUserId, currentUserId,

View File

@ -406,9 +406,8 @@ const Composer: FC<OwnProps & StateProps> = ({
isMentionTooltipOpen, closeMentionTooltip, insertMention, mentionFilteredUsers, isMentionTooltipOpen, closeMentionTooltip, insertMention, mentionFilteredUsers,
} = useMentionTooltip( } = useMentionTooltip(
!attachments.length, !attachments.length,
htmlRef, EDITABLE_INPUT_CSS_SELECTOR,
setHtml, setHtml,
undefined,
groupChatMembers, groupChatMembers,
topInlineBotIds, topInlineBotIds,
currentUserId, currentUserId,

View File

@ -5,14 +5,17 @@ import { getGlobal } from '../../../../global';
import type { ApiChatMember, ApiUser } from '../../../../api/types'; import type { ApiChatMember, ApiUser } from '../../../../api/types';
import { ApiMessageEntityTypes } from '../../../../api/types'; import { ApiMessageEntityTypes } from '../../../../api/types';
import { EDITABLE_INPUT_ID } from '../../../../config';
import { filterUsersByName, getUserFirstOrLastName } from '../../../../global/helpers'; import { filterUsersByName, getUserFirstOrLastName } from '../../../../global/helpers';
import { prepareForRegExp } from '../helpers/prepareForRegExp'; import { prepareForRegExp } from '../helpers/prepareForRegExp';
import focusEditableElement from '../../../../util/focusEditableElement'; import focusEditableElement from '../../../../util/focusEditableElement';
import { pickTruthy, unique } from '../../../../util/iteratees'; import { pickTruthy, unique } from '../../../../util/iteratees';
import { throttle } from '../../../../util/schedulers'; import { throttle } from '../../../../util/schedulers';
import { getHtmlBeforeSelection } from '../../../../util/selection';
import useFlag from '../../../../hooks/useFlag'; import useFlag from '../../../../hooks/useFlag';
import useCacheBuster from '../../../../hooks/useCacheBuster';
import useOnSelectionChange from '../../../../hooks/useOnSelectionChange';
const runThrottled = throttle((cb) => cb(), 500, true); const runThrottled = throttle((cb) => cb(), 500, true);
let RE_USERNAME_SEARCH: RegExp; let RE_USERNAME_SEARCH: RegExp;
@ -26,14 +29,14 @@ try {
export default function useMentionTooltip( export default function useMentionTooltip(
canSuggestMembers: boolean | undefined, canSuggestMembers: boolean | undefined,
htmlRef: { current: string }, inputSelector: string,
onUpdateHtml: (html: string) => void, onUpdateHtml: (html: string) => void,
inputId: string = EDITABLE_INPUT_ID,
groupChatMembers?: ApiChatMember[], groupChatMembers?: ApiChatMember[],
topInlineBotIds?: string[], topInlineBotIds?: string[],
currentUserId?: string, currentUserId?: string,
) { ) {
const [isOpen, markIsOpen, unmarkIsOpen] = useFlag(); const [isOpen, markIsOpen, unmarkIsOpen] = useFlag();
const [htmlBeforeSelection, setHtmlBeforeSelection] = useState('');
const [usersToMention, setUsersToMention] = useState<ApiUser[] | undefined>(); const [usersToMention, setUsersToMention] = useState<ApiUser[] | undefined>();
const updateFilteredUsers = useCallback((filter, withInlineBots: boolean) => { const updateFilteredUsers = useCallback((filter, withInlineBots: boolean) => {
@ -64,22 +67,35 @@ export default function useMentionTooltip(
}); });
}, [currentUserId, groupChatMembers, topInlineBotIds]); }, [currentUserId, groupChatMembers, topInlineBotIds]);
const html = htmlRef.current; const [cacheBuster, updateCacheBuster] = useCacheBuster();
const handleSelectionChange = useCallback((range: Range) => {
if (range.collapsed) {
updateCacheBuster(); // Update tooltip on cursor move
}
}, [updateCacheBuster]);
useOnSelectionChange(document.querySelector<HTMLDivElement>(inputSelector), handleSelectionChange);
useEffect(() => { useEffect(() => {
if (!canSuggestMembers || !html.length) { setHtmlBeforeSelection(getHtmlBeforeSelection(document.querySelector<HTMLDivElement>(inputSelector)!));
}, [inputSelector, cacheBuster]);
useEffect(() => {
if (!canSuggestMembers || !htmlBeforeSelection.length) {
unmarkIsOpen(); unmarkIsOpen();
return; return;
} }
const usernameFilter = html.includes('@') && getUsernameFilter(html); const usernameFilter = htmlBeforeSelection.includes('@') && getUsernameFilter(htmlBeforeSelection);
if (usernameFilter) { if (usernameFilter) {
const filter = usernameFilter ? usernameFilter.substr(1) : ''; const filter = usernameFilter ? usernameFilter.substr(1) : '';
updateFilteredUsers(filter, canSuggestInlineBots(html)); updateFilteredUsers(filter, canSuggestInlineBots(htmlBeforeSelection));
} else { } else {
unmarkIsOpen(); unmarkIsOpen();
} }
}, [canSuggestMembers, updateFilteredUsers, markIsOpen, unmarkIsOpen, html]); }, [canSuggestMembers, updateFilteredUsers, markIsOpen, unmarkIsOpen, htmlBeforeSelection]);
useEffect(() => { useEffect(() => {
if (usersToMention?.length) { if (usersToMention?.length) {
@ -104,18 +120,21 @@ export default function useMentionTooltip(
dir="auto" dir="auto"
>${getUserFirstOrLastName(user)}</a>`; >${getUserFirstOrLastName(user)}</a>`;
const currentHtml = htmlRef.current; const containerEl = document.querySelector<HTMLDivElement>(inputSelector)!;
const atIndex = currentHtml.lastIndexOf('@');
const atIndex = htmlBeforeSelection.lastIndexOf('@');
if (atIndex !== -1) { if (atIndex !== -1) {
onUpdateHtml(`${currentHtml.substr(0, atIndex)}${insertedHtml}&nbsp;`); const newHtml = `${htmlBeforeSelection.substr(0, atIndex)}${insertedHtml}&nbsp;`;
const messageInput = document.getElementById(inputId)!; const htmlAfterSelection = containerEl.innerHTML.substring(htmlBeforeSelection.length);
onUpdateHtml(`${newHtml}${htmlAfterSelection}`);
requestAnimationFrame(() => { requestAnimationFrame(() => {
focusEditableElement(messageInput, forceFocus); focusEditableElement(containerEl, forceFocus);
}); });
} }
unmarkIsOpen(); unmarkIsOpen();
}, [htmlRef, inputId, onUpdateHtml, unmarkIsOpen]); }, [htmlBeforeSelection, inputSelector, onUpdateHtml, unmarkIsOpen]);
return { return {
isMentionTooltipOpen: isOpen, isMentionTooltipOpen: isOpen,