164 lines
4.9 KiB
TypeScript
164 lines
4.9 KiB
TypeScript
import React, {
|
|
FC, useCallback, useEffect, useState, useRef, memo,
|
|
} from '../../../lib/teact/teact';
|
|
import usePrevious from '../../../hooks/usePrevious';
|
|
|
|
import { ApiChatMember, ApiUser } from '../../../api/types';
|
|
|
|
import useShowTransition from '../../../hooks/useShowTransition';
|
|
import buildClassName from '../../../util/buildClassName';
|
|
import captureKeyboardListeners from '../../../util/captureKeyboardListeners';
|
|
import findInViewport from '../../../util/findInViewport';
|
|
import isFullyVisible from '../../../util/isFullyVisible';
|
|
import fastSmoothScroll from '../../../util/fastSmoothScroll';
|
|
import cycleRestrict from '../../../util/cycleRestrict';
|
|
|
|
import ListItem from '../../ui/ListItem';
|
|
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
|
|
|
import './MentionTooltip.scss';
|
|
|
|
const VIEWPORT_MARGIN = 8;
|
|
const SCROLL_MARGIN = 10;
|
|
|
|
function setItemVisible(index: number, containerRef: Record<string, any>) {
|
|
const container = containerRef.current!;
|
|
if (!container || index < 0) {
|
|
return;
|
|
}
|
|
const { visibleIndexes, allElements } = findInViewport(
|
|
container,
|
|
'.chat-item-clickable',
|
|
VIEWPORT_MARGIN,
|
|
true,
|
|
true,
|
|
);
|
|
if (!allElements.length || !allElements[index]) {
|
|
return;
|
|
}
|
|
const first = visibleIndexes[0];
|
|
if (!visibleIndexes.includes(index)
|
|
|| (index === first && !isFullyVisible(container, allElements[first]))) {
|
|
const position = index > visibleIndexes[visibleIndexes.length - 1] ? 'start' : 'end';
|
|
fastSmoothScroll(container, allElements[index], position, SCROLL_MARGIN);
|
|
}
|
|
}
|
|
|
|
export type OwnProps = {
|
|
isOpen: boolean;
|
|
filter: string;
|
|
onClose: () => void;
|
|
onInsertUserName: (user: ApiUser, forceFocus?: boolean) => void;
|
|
filteredChatMembers?: ApiChatMember[];
|
|
usersById?: Record<number, ApiUser>;
|
|
};
|
|
|
|
const MentionTooltip: FC<OwnProps> = ({
|
|
isOpen,
|
|
filter,
|
|
onClose,
|
|
onInsertUserName,
|
|
usersById,
|
|
filteredChatMembers,
|
|
}) => {
|
|
// eslint-disable-next-line no-null/no-null
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false);
|
|
|
|
const getSelectedIndex = useCallback((newIndex: number) => {
|
|
if (!filteredChatMembers) {
|
|
return -1;
|
|
}
|
|
const membersCount = filteredChatMembers!.length;
|
|
return cycleRestrict(membersCount, newIndex);
|
|
}, [filteredChatMembers]);
|
|
|
|
const [selectedMentionIndex, setSelectedMentionIndex] = useState(-1);
|
|
|
|
const handleArrowKey = useCallback((value: number, e: KeyboardEvent) => {
|
|
e.preventDefault();
|
|
setSelectedMentionIndex((index) => (getSelectedIndex(index + value)));
|
|
}, [setSelectedMentionIndex, getSelectedIndex]);
|
|
|
|
const handleUserSelect = useCallback((userId: number, forceFocus = false) => {
|
|
const user = usersById && usersById[userId];
|
|
if (!user) {
|
|
return;
|
|
}
|
|
|
|
onInsertUserName(user, forceFocus);
|
|
}, [usersById, onInsertUserName]);
|
|
|
|
const handleSelectMention = useCallback((e: KeyboardEvent) => {
|
|
if (filteredChatMembers && filteredChatMembers.length && selectedMentionIndex > -1) {
|
|
const member = filteredChatMembers[selectedMentionIndex];
|
|
if (member) {
|
|
e.preventDefault();
|
|
handleUserSelect(member.userId, true);
|
|
}
|
|
}
|
|
}, [filteredChatMembers, selectedMentionIndex, handleUserSelect]);
|
|
|
|
useEffect(() => (isOpen ? captureKeyboardListeners({
|
|
onEsc: onClose,
|
|
onUp: (e: KeyboardEvent) => handleArrowKey(-1, e),
|
|
onDown: (e: KeyboardEvent) => handleArrowKey(1, e),
|
|
onEnter: handleSelectMention,
|
|
onTab: handleSelectMention,
|
|
}) : undefined), [isOpen, onClose, handleArrowKey, handleSelectMention]);
|
|
|
|
useEffect(() => {
|
|
if (filteredChatMembers && !filteredChatMembers.length) {
|
|
onClose();
|
|
}
|
|
}, [filteredChatMembers, onClose]);
|
|
|
|
useEffect(() => {
|
|
setSelectedMentionIndex(0);
|
|
}, [filter]);
|
|
|
|
useEffect(() => {
|
|
setItemVisible(selectedMentionIndex, containerRef);
|
|
}, [selectedMentionIndex]);
|
|
|
|
const prevChatMembers = usePrevious(
|
|
filteredChatMembers && filteredChatMembers.length
|
|
? filteredChatMembers
|
|
: undefined,
|
|
shouldRender,
|
|
);
|
|
const renderedChatMembers = filteredChatMembers && !filteredChatMembers.length
|
|
? prevChatMembers
|
|
: filteredChatMembers;
|
|
|
|
if (!shouldRender || (renderedChatMembers && !renderedChatMembers.length)) {
|
|
return undefined;
|
|
}
|
|
|
|
const className = buildClassName(
|
|
'MentionTooltip composer-tooltip custom-scroll',
|
|
transitionClassNames,
|
|
);
|
|
|
|
return (
|
|
<div className={className} ref={containerRef}>
|
|
{renderedChatMembers && renderedChatMembers.map(({ userId }, index) => (
|
|
<ListItem
|
|
key={userId}
|
|
className="chat-item-clickable scroll-item"
|
|
onClick={() => handleUserSelect(userId)}
|
|
focus={selectedMentionIndex === index}
|
|
>
|
|
<PrivateChatInfo
|
|
userId={userId}
|
|
avatarSize="small"
|
|
withUsername
|
|
/>
|
|
</ListItem>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default memo(MentionTooltip);
|