TelegramPWA/src/components/middle/composer/InlineBotTooltip.tsx
2021-07-23 17:05:31 +03:00

238 lines
7.2 KiB
TypeScript

import React, {
FC, memo, useCallback, useEffect, useRef, useState,
} from '../../../lib/teact/teact';
import { withGlobal } from '../../../lib/teact/teactn';
import { GlobalActions } from '../../../global/types';
import { ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm } from '../../../api/types';
import { IAllowedAttachmentOptions } from '../../../modules/helpers';
import { LoadMoreDirection } from '../../../types';
import { IS_TOUCH_ENV } from '../../../util/environment';
import setTooltipItemVisible from '../../../util/setTooltipItemVisible';
import buildClassName from '../../../util/buildClassName';
import captureKeyboardListeners from '../../../util/captureKeyboardListeners';
import cycleRestrict from '../../../util/cycleRestrict';
import useShowTransition from '../../../hooks/useShowTransition';
import { throttle } from '../../../util/schedulers';
import { pick } from '../../../util/iteratees';
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
import usePrevious from '../../../hooks/usePrevious';
import MediaResult from './inlineResults/MediaResult';
import ArticleResult from './inlineResults/ArticleResult';
import GifResult from './inlineResults/GifResult';
import StickerResult from './inlineResults/StickerResult';
import ListItem from '../../ui/ListItem';
import InfiniteScroll from '../../ui/InfiniteScroll';
import './InlineBotTooltip.scss';
const INTERSECTION_DEBOUNCE_MS = 200;
const runThrottled = throttle((cb) => cb(), 500, true);
export type OwnProps = {
isOpen: boolean;
botId?: number;
isGallery?: boolean;
allowedAttachmentOptions: IAllowedAttachmentOptions;
inlineBotResults?: (ApiBotInlineResult | ApiBotInlineMediaResult)[];
switchPm?: ApiBotInlineSwitchPm;
onSelectResult: (inlineResult: ApiBotInlineMediaResult | ApiBotInlineResult) => void;
loadMore: NoneToVoidFunction;
onClose: NoneToVoidFunction;
};
type DispatchProps = Pick<GlobalActions, ('sendBotCommand' | 'openChat' | 'sendInlineBotResult')>;
const InlineBotTooltip: FC<OwnProps & DispatchProps> = ({
isOpen,
botId,
isGallery,
inlineBotResults,
switchPm,
loadMore,
onClose,
openChat,
sendBotCommand,
onSelectResult,
}) => {
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false);
const [selectedIndex, setSelectedIndex] = useState(-1);
const {
observe: observeIntersection,
} = useIntersectionObserver({
rootRef: containerRef,
debounceMs: INTERSECTION_DEBOUNCE_MS,
isDisabled: !isOpen,
});
useEffect(() => {
setSelectedIndex(isGallery ? -1 : 0);
}, [inlineBotResults, isGallery]);
useEffect(() => {
setTooltipItemVisible('.chat-item-clickable', selectedIndex, containerRef);
}, [selectedIndex]);
const getSelectedIndex = useCallback((newIndex: number) => {
if (!inlineBotResults || !inlineBotResults.length) {
return -1;
}
return cycleRestrict(inlineBotResults.length, newIndex);
}, [inlineBotResults]);
const handleArrowKey = useCallback((value: number, e: KeyboardEvent) => {
if (isGallery) {
return;
}
e.preventDefault();
setSelectedIndex((index) => (getSelectedIndex(index + value)));
}, [isGallery, getSelectedIndex]);
const handleSelectInlineBotResult = useCallback((e: KeyboardEvent) => {
if (inlineBotResults && inlineBotResults.length && selectedIndex > -1) {
const inlineResult = inlineBotResults[selectedIndex];
if (inlineResult) {
e.preventDefault();
onSelectResult(inlineResult);
}
}
}, [inlineBotResults, onSelectResult, selectedIndex]);
const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => {
if (direction === LoadMoreDirection.Backwards) {
runThrottled(loadMore);
}
}, [loadMore]);
useEffect(() => (isOpen ? captureKeyboardListeners({
onEsc: onClose,
onUp: (e: KeyboardEvent) => handleArrowKey(-1, e),
onDown: (e: KeyboardEvent) => handleArrowKey(1, e),
onEnter: handleSelectInlineBotResult,
}) : undefined), [handleArrowKey, handleSelectInlineBotResult, isGallery, isOpen, onClose]);
const handleSendPm = useCallback(() => {
openChat({ id: botId });
sendBotCommand({ chatId: botId, command: `/start ${switchPm!.startParam}` });
}, [botId, openChat, sendBotCommand, switchPm]);
const prevInlineBotResults = usePrevious(
inlineBotResults && inlineBotResults.length
? inlineBotResults
: undefined,
shouldRender,
);
const renderedInlineBotResults = inlineBotResults && !inlineBotResults.length
? prevInlineBotResults
: inlineBotResults;
if (!shouldRender || !renderedInlineBotResults || (!renderedInlineBotResults.length && !switchPm)) {
return undefined;
}
const className = buildClassName(
'InlineBotTooltip composer-tooltip',
IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll',
isGallery && 'gallery',
transitionClassNames,
);
function renderSwitchPm() {
return (
<ListItem ripple className="switch-pm scroll-item" onClick={handleSendPm}>
<span className="title">{switchPm!.text}</span>
</ListItem>
);
}
function renderContent() {
return renderedInlineBotResults!.map((inlineBotResult, index) => {
switch (inlineBotResult.type) {
case 'gif':
return (
<GifResult
key={inlineBotResult.id}
inlineResult={inlineBotResult}
observeIntersection={observeIntersection}
onClick={onSelectResult}
/>
);
case 'photo':
return (
<MediaResult
key={inlineBotResult.id}
isForGallery={isGallery}
inlineResult={inlineBotResult}
onClick={onSelectResult}
/>
);
case 'sticker':
return (
<StickerResult
key={inlineBotResult.id}
inlineResult={inlineBotResult}
observeIntersection={observeIntersection}
onClick={onSelectResult}
/>
);
case 'video':
case 'game':
return (
<MediaResult
key={inlineBotResult.id}
focus={selectedIndex === index}
inlineResult={inlineBotResult}
onClick={onSelectResult}
/>
);
case 'article':
case 'audio':
return (
<ArticleResult
key={inlineBotResult.id}
focus={selectedIndex === index}
inlineResult={inlineBotResult}
onClick={onSelectResult}
/>
);
default:
return undefined;
}
});
}
return (
<InfiniteScroll
ref={containerRef}
className={className}
items={renderedInlineBotResults}
itemSelector=".chat-item-clickable"
noFastList
onLoadMore={handleLoadMore}
sensitiveArea={160}
>
{switchPm && renderSwitchPm()}
{renderContent()}
</InfiniteScroll>
);
};
export default memo(withGlobal<OwnProps>(
undefined,
(setGlobal, actions): DispatchProps => pick(actions, [
'sendBotCommand', 'openChat', 'sendInlineBotResult',
]),
)(InlineBotTooltip));