TelegramPWA/src/components/middle/composer/CustomEmojiPicker.tsx
2023-02-13 03:35:01 +01:00

384 lines
13 KiB
TypeScript

import type { FC } from '../../../lib/teact/teact';
import React, {
useState, useEffect, memo, useRef, useMemo, useCallback,
} from '../../../lib/teact/teact';
import { getGlobal, withGlobal } from '../../../global';
import type { ApiStickerSet, ApiSticker } from '../../../api/types';
import type { StickerSetOrRecent } from '../../../types';
import {
CHAT_STICKER_SET_ID,
FAVORITE_SYMBOL_SET_ID,
PREMIUM_STICKER_SET_ID,
RECENT_SYMBOL_SET_ID,
SLIDE_TRANSITION_DURATION,
STICKER_PICKER_MAX_SHARED_COVERS,
STICKER_SIZE_PICKER_HEADER,
} from '../../../config';
import { IS_TOUCH_ENV } from '../../../util/environment';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import fastSmoothScroll from '../../../util/fastSmoothScroll';
import buildClassName from '../../../util/buildClassName';
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
import { pickTruthy, unique } from '../../../util/iteratees';
import {
selectIsAlwaysHighPriorityEmoji,
selectIsChatWithSelf,
selectIsCurrentUserPremium,
} from '../../../global/selectors';
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
import useLang from '../../../hooks/useLang';
import Loading from '../../ui/Loading';
import Button from '../../ui/Button';
import StickerButton from '../../common/StickerButton';
import StickerSet from './StickerSet';
import StickerSetCover from './StickerSetCover';
import './StickerPicker.scss';
type OwnProps = {
chatId?: string;
className?: string;
loadAndPlay: boolean;
isStatusPicker?: boolean;
idPrefix?: string;
withDefaultTopicIcons?: boolean;
onCustomEmojiSelect: (sticker: ApiSticker) => void;
onContextMenuOpen?: NoneToVoidFunction;
onContextMenuClose?: NoneToVoidFunction;
onContextMenuClick?: NoneToVoidFunction;
};
type StateProps = {
customEmojisById?: Record<string, ApiSticker>;
recentCustomEmojiIds?: string[];
recentStatusEmojis?: ApiSticker[];
stickerSetsById: Record<string, ApiStickerSet>;
addedCustomEmojiIds?: string[];
defaultTopicIconsId?: string;
defaultStatusIconsId?: string;
customEmojiFeaturedIds?: string[];
canAnimate?: boolean;
isSavedMessages?: boolean;
isCurrentUserPremium?: boolean;
};
const SMOOTH_SCROLL_DISTANCE = 500;
const HEADER_BUTTON_WIDTH = 52; // px (including margin)
const STICKER_INTERSECTION_THROTTLE = 200;
const DEFAULT_ID_PREFIX = 'custom-emoji-set';
const stickerSetIntersections: boolean[] = [];
const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
className,
loadAndPlay,
addedCustomEmojiIds,
customEmojisById,
recentCustomEmojiIds,
recentStatusEmojis,
stickerSetsById,
idPrefix = DEFAULT_ID_PREFIX,
customEmojiFeaturedIds,
canAnimate,
isStatusPicker,
isSavedMessages,
isCurrentUserPremium,
withDefaultTopicIcons,
defaultTopicIconsId,
defaultStatusIconsId,
onCustomEmojiSelect,
onContextMenuOpen,
onContextMenuClose,
onContextMenuClick,
}) => {
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const headerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
// eslint-disable-next-line no-null/no-null
const sharedCanvasHqRef = useRef<HTMLCanvasElement>(null);
const [activeSetIndex, setActiveSetIndex] = useState<number>(0);
const recentCustomEmojis = useMemo(() => {
return isStatusPicker
? recentStatusEmojis
: Object.values(pickTruthy(customEmojisById!, recentCustomEmojiIds!));
}, [customEmojisById, isStatusPicker, recentCustomEmojiIds, recentStatusEmojis]);
const { observe: observeIntersection } = useIntersectionObserver({
rootRef: containerRef,
throttleMs: STICKER_INTERSECTION_THROTTLE,
}, (entries) => {
entries.forEach((entry) => {
const { id } = entry.target as HTMLDivElement;
if (!id || !id.startsWith(idPrefix)) {
return;
}
const index = Number(id.replace(`${idPrefix}-`, ''));
stickerSetIntersections[index] = entry.isIntersecting;
});
const intersectingWithIndexes = stickerSetIntersections
.map((isIntersecting, index) => ({ index, isIntersecting }))
.filter(({ isIntersecting }) => isIntersecting);
if (!intersectingWithIndexes.length) {
return;
}
setActiveSetIndex(intersectingWithIndexes[Math.floor(intersectingWithIndexes.length / 2)].index);
});
const { observe: observeIntersectionForCovers } = useIntersectionObserver({ rootRef: headerRef });
const lang = useLang();
const areAddedLoaded = Boolean(addedCustomEmojiIds);
const allSets = useMemo(() => {
if (!addedCustomEmojiIds) {
return MEMO_EMPTY_ARRAY;
}
const defaultSets: StickerSetOrRecent[] = [];
if (isStatusPicker) {
const defaultStatusIconsPack = stickerSetsById[defaultStatusIconsId!];
if (defaultStatusIconsPack.stickers?.length) {
const stickers = (defaultStatusIconsPack.stickers || []).concat(recentCustomEmojis || []);
defaultSets.push({
...defaultStatusIconsPack,
stickers,
count: stickers.length,
id: RECENT_SYMBOL_SET_ID,
title: lang('RecentStickers'),
});
}
} else if (withDefaultTopicIcons) {
const defaultTopicIconsPack = stickerSetsById[defaultTopicIconsId!];
if (defaultTopicIconsPack.stickers?.length) {
defaultSets.push({
...defaultTopicIconsPack,
id: RECENT_SYMBOL_SET_ID,
title: lang('RecentStickers'),
});
}
} else if (recentCustomEmojis?.length) {
defaultSets.push({
id: RECENT_SYMBOL_SET_ID,
accessHash: '0',
title: lang('RecentStickers'),
stickers: recentCustomEmojis,
count: recentCustomEmojis.length,
isEmoji: true as true,
});
}
const setIdsToDisplay = unique(addedCustomEmojiIds.concat(customEmojiFeaturedIds || []));
const setsToDisplay = Object.values(pickTruthy(stickerSetsById, setIdsToDisplay));
return [
...defaultSets,
...setsToDisplay,
];
}, [
addedCustomEmojiIds, isStatusPicker, withDefaultTopicIcons, recentCustomEmojis,
customEmojiFeaturedIds, stickerSetsById, defaultStatusIconsId, lang, defaultTopicIconsId,
]);
const noPopulatedSets = useMemo(() => (
areAddedLoaded
&& allSets.filter((set) => set.stickers?.length).length === 0
), [allSets, areAddedLoaded]);
const canRenderContents = useAsyncRendering([], SLIDE_TRANSITION_DURATION);
const shouldRenderContents = areAddedLoaded && canRenderContents && !noPopulatedSets;
useHorizontalScroll(headerRef, !shouldRenderContents);
// Scroll container and header when active set changes
useEffect(() => {
if (!areAddedLoaded) {
return;
}
const header = headerRef.current;
if (!header) {
return;
}
const newLeft = activeSetIndex * HEADER_BUTTON_WIDTH - (header.offsetWidth / 2 - HEADER_BUTTON_WIDTH / 2);
fastSmoothScrollHorizontal(header, newLeft);
}, [areAddedLoaded, activeSetIndex]);
const selectStickerSet = useCallback((index: number) => {
setActiveSetIndex(index);
const stickerSetEl = document.getElementById(`${idPrefix}-${index}`)!;
fastSmoothScroll(containerRef.current!, stickerSetEl, 'start', undefined, SMOOTH_SCROLL_DISTANCE);
}, [idPrefix]);
const handleEmojiSelect = useCallback((emoji: ApiSticker) => {
onCustomEmojiSelect(emoji);
}, [onCustomEmojiSelect]);
function renderCover(stickerSet: StickerSetOrRecent, index: number) {
const firstSticker = stickerSet.stickers?.[0];
const buttonClassName = buildClassName(
'symbol-set-button sticker-set-button',
index === activeSetIndex && 'activated',
);
const withSharedCanvas = index < STICKER_PICKER_MAX_SHARED_COVERS;
const isHq = selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSet as ApiStickerSet);
if (stickerSet.id === RECENT_SYMBOL_SET_ID
|| stickerSet.id === FAVORITE_SYMBOL_SET_ID
|| stickerSet.id === CHAT_STICKER_SET_ID
|| stickerSet.id === PREMIUM_STICKER_SET_ID
|| stickerSet.hasThumbnail
|| !firstSticker
) {
return (
<Button
key={stickerSet.id}
className={buttonClassName}
ariaLabel={stickerSet.title}
round
faded={stickerSet.id === RECENT_SYMBOL_SET_ID || stickerSet.id === FAVORITE_SYMBOL_SET_ID}
color="translucent"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => selectStickerSet(index)}
>
{stickerSet.id === RECENT_SYMBOL_SET_ID ? (
<i className="icon-recent" />
) : (
<StickerSetCover
stickerSet={stickerSet as ApiStickerSet}
noAnimate={!canAnimate || !loadAndPlay}
observeIntersection={observeIntersectionForCovers}
sharedCanvasRef={withSharedCanvas ? (isHq ? sharedCanvasHqRef : sharedCanvasRef) : undefined}
/>
)}
</Button>
);
} else {
return (
<StickerButton
key={stickerSet.id}
sticker={firstSticker}
size={STICKER_SIZE_PICKER_HEADER}
title={stickerSet.title}
className={buttonClassName}
noAnimate={!canAnimate || !loadAndPlay}
observeIntersection={observeIntersectionForCovers}
noContextMenu
isCurrentUserPremium
sharedCanvasRef={withSharedCanvas ? (isHq ? sharedCanvasHqRef : sharedCanvasRef) : undefined}
onClick={selectStickerSet}
clickArg={index}
/>
);
}
}
const fullClassName = buildClassName('StickerPicker', 'CustomEmojiPicker', className);
if (!shouldRenderContents) {
return (
<div className={fullClassName}>
{noPopulatedSets ? (
<div className="picker-disabled">{lang('NoStickers')}</div>
) : (
<Loading />
)}
</div>
);
}
return (
<div className={fullClassName}>
<div
ref={headerRef}
className="StickerPicker-header no-selection no-scrollbar"
>
<div className="shared-canvas-container">
<canvas ref={sharedCanvasRef} className="shared-canvas" />
<canvas ref={sharedCanvasHqRef} className="shared-canvas" />
{allSets.map(renderCover)}
</div>
</div>
<div
ref={containerRef}
className={buildClassName('StickerPicker-main no-selection', IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll')}
>
{allSets.map((stickerSet, i) => (
<StickerSet
key={stickerSet.id}
stickerSet={stickerSet}
loadAndPlay={Boolean(canAnimate && loadAndPlay)}
index={i}
idPrefix={idPrefix}
observeIntersection={observeIntersection}
shouldRender={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
isSavedMessages={isSavedMessages}
isStatusPicker={isStatusPicker}
shouldHideRecentHeader={withDefaultTopicIcons || isStatusPicker}
withDefaultTopicIcon={withDefaultTopicIcons && stickerSet.id === RECENT_SYMBOL_SET_ID}
withDefaultStatusIcon={isStatusPicker && stickerSet.id === RECENT_SYMBOL_SET_ID}
isCurrentUserPremium={isCurrentUserPremium}
onStickerSelect={handleEmojiSelect}
onContextMenuOpen={onContextMenuOpen}
onContextMenuClose={onContextMenuClose}
onContextMenuClick={onContextMenuClick}
/>
))}
</div>
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, { chatId, isStatusPicker }): StateProps => {
const {
stickers: {
setsById: stickerSetsById,
},
customEmojis: {
byId: customEmojisById,
featuredIds: customEmojiFeaturedIds,
statusRecent: {
emojis: recentStatusEmojis,
},
},
recentCustomEmojis: recentCustomEmojiIds,
} = global;
const isSavedMessages = Boolean(chatId && selectIsChatWithSelf(global, chatId));
return {
customEmojisById: !isStatusPicker ? customEmojisById : undefined,
recentCustomEmojiIds: !isStatusPicker ? recentCustomEmojiIds : undefined,
recentStatusEmojis: isStatusPicker ? recentStatusEmojis : undefined,
stickerSetsById,
addedCustomEmojiIds: global.customEmojis.added.setIds,
canAnimate: global.settings.byKey.shouldLoopStickers,
isSavedMessages,
isCurrentUserPremium: selectIsCurrentUserPremium(global),
customEmojiFeaturedIds,
defaultTopicIconsId: global.defaultTopicIconsId,
defaultStatusIconsId: global.defaultStatusIconsId,
};
},
)(CustomEmojiPicker));