435 lines
13 KiB
TypeScript
435 lines
13 KiB
TypeScript
import type { FC } from '../../../lib/teact/teact';
|
|
import { memo, useEffect, useMemo, useRef } from '../../../lib/teact/teact';
|
|
import { getActions, withGlobal } from '../../../global';
|
|
|
|
import type { ApiChat, ApiSticker, ApiStickerSet } from '../../../api/types';
|
|
import type { StickerSetOrReactionsSetOrRecent, ThreadId } from '../../../types';
|
|
|
|
import {
|
|
CHAT_STICKER_SET_ID,
|
|
EFFECT_EMOJIS_SET_ID,
|
|
EFFECT_STICKERS_SET_ID,
|
|
FAVORITE_SYMBOL_SET_ID,
|
|
RECENT_SYMBOL_SET_ID,
|
|
SLIDE_TRANSITION_DURATION,
|
|
STICKER_PICKER_MAX_SHARED_COVERS,
|
|
STICKER_SIZE_PICKER_HEADER,
|
|
} from '../../../config';
|
|
import {
|
|
selectChat,
|
|
selectChatFullInfo,
|
|
selectIsChatWithSelf,
|
|
selectIsCurrentUserPremium,
|
|
selectShouldLoopStickers,
|
|
} from '../../../global/selectors';
|
|
import animateHorizontalScroll from '../../../util/animateHorizontalScroll';
|
|
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
|
|
import buildClassName from '../../../util/buildClassName';
|
|
import { isUserId } from '../../../util/entities/ids';
|
|
import { pickTruthy } from '../../../util/iteratees';
|
|
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
|
import { REM } from '../../common/helpers/mediaDimensions';
|
|
|
|
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
|
import useLastCallback from '../../../hooks/useLastCallback';
|
|
import useOldLang from '../../../hooks/useOldLang';
|
|
import useScrolledState from '../../../hooks/useScrolledState';
|
|
import useSendMessageAction from '../../../hooks/useSendMessageAction';
|
|
import { useStickerPickerObservers } from '../../common/hooks/useStickerPickerObservers';
|
|
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
|
|
|
import Avatar from '../../common/Avatar';
|
|
import Icon from '../../common/icons/Icon';
|
|
import StickerButton from '../../common/StickerButton';
|
|
import StickerSet from '../../common/StickerSet';
|
|
import Button from '../../ui/Button';
|
|
import Loading from '../../ui/Loading';
|
|
import Transition from '../../ui/Transition.tsx';
|
|
import StickerSetCover from './StickerSetCover';
|
|
|
|
import styles from './StickerPicker.module.scss';
|
|
|
|
type OwnProps = {
|
|
chatId: string;
|
|
threadId?: ThreadId;
|
|
className: string;
|
|
isHidden?: boolean;
|
|
isTranslucent?: boolean;
|
|
loadAndPlay: boolean;
|
|
canSendStickers?: boolean;
|
|
noContextMenus?: boolean;
|
|
idPrefix: string;
|
|
onStickerSelect: (
|
|
sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean, canUpdateStickerSetsOrder?: boolean,
|
|
) => void;
|
|
isForEffects?: boolean;
|
|
};
|
|
|
|
type StateProps = {
|
|
chat?: ApiChat;
|
|
recentStickers: ApiSticker[];
|
|
favoriteStickers: ApiSticker[];
|
|
effectStickers?: ApiSticker[];
|
|
effectEmojis?: ApiSticker[];
|
|
stickerSetsById: Record<string, ApiStickerSet>;
|
|
chatStickerSetId?: string;
|
|
addedSetIds?: string[];
|
|
canAnimate?: boolean;
|
|
isSavedMessages?: boolean;
|
|
isCurrentUserPremium?: boolean;
|
|
};
|
|
|
|
const HEADER_BUTTON_WIDTH = 2.5 * REM; // px (including margin)
|
|
|
|
const StickerPicker: FC<OwnProps & StateProps> = ({
|
|
chat,
|
|
threadId,
|
|
className,
|
|
isHidden,
|
|
isTranslucent,
|
|
loadAndPlay,
|
|
canSendStickers,
|
|
recentStickers,
|
|
favoriteStickers,
|
|
effectStickers,
|
|
effectEmojis,
|
|
addedSetIds,
|
|
stickerSetsById,
|
|
chatStickerSetId,
|
|
canAnimate,
|
|
isSavedMessages,
|
|
isCurrentUserPremium,
|
|
noContextMenus,
|
|
idPrefix,
|
|
onStickerSelect,
|
|
isForEffects,
|
|
}) => {
|
|
const {
|
|
loadRecentStickers,
|
|
addRecentSticker,
|
|
unfaveSticker,
|
|
faveSticker,
|
|
removeRecentSticker,
|
|
} = getActions();
|
|
|
|
const containerRef = useRef<HTMLDivElement>();
|
|
const headerRef = useRef<HTMLDivElement>();
|
|
const sharedCanvasRef = useRef<HTMLCanvasElement>();
|
|
|
|
const {
|
|
handleScroll: handleContentScroll,
|
|
isAtBeginning: shouldHideTopBorder,
|
|
} = useScrolledState();
|
|
|
|
const sendMessageAction = useSendMessageAction(chat?.id, threadId);
|
|
|
|
const prefix = `${idPrefix}-sticker-set`;
|
|
const {
|
|
activeSetIndex,
|
|
observeIntersectionForSet,
|
|
observeIntersectionForPlayingItems,
|
|
observeIntersectionForShowingItems,
|
|
observeIntersectionForCovers,
|
|
selectStickerSet,
|
|
} = useStickerPickerObservers(containerRef, headerRef, prefix, isHidden);
|
|
|
|
const lang = useOldLang();
|
|
|
|
const areAddedLoaded = Boolean(addedSetIds);
|
|
|
|
const allSets = useMemo(() => {
|
|
if (isForEffects && effectStickers) {
|
|
const effectSets: StickerSetOrReactionsSetOrRecent[] = [];
|
|
if (effectEmojis?.length) {
|
|
effectSets.push({
|
|
id: EFFECT_EMOJIS_SET_ID,
|
|
accessHash: '0',
|
|
title: '',
|
|
stickers: effectEmojis,
|
|
count: effectEmojis.length,
|
|
isEmoji: true,
|
|
});
|
|
}
|
|
if (effectStickers?.length) {
|
|
effectSets.push({
|
|
id: EFFECT_STICKERS_SET_ID,
|
|
accessHash: '0',
|
|
title: lang('StickerEffects'),
|
|
stickers: effectStickers,
|
|
count: effectStickers.length,
|
|
});
|
|
}
|
|
return effectSets;
|
|
}
|
|
|
|
if (!addedSetIds) {
|
|
return MEMO_EMPTY_ARRAY;
|
|
}
|
|
|
|
const defaultSets = [];
|
|
|
|
if (favoriteStickers.length) {
|
|
defaultSets.push({
|
|
id: FAVORITE_SYMBOL_SET_ID,
|
|
accessHash: '0',
|
|
title: lang('FavoriteStickers'),
|
|
stickers: favoriteStickers,
|
|
count: favoriteStickers.length,
|
|
});
|
|
}
|
|
|
|
if (recentStickers.length) {
|
|
defaultSets.push({
|
|
id: RECENT_SYMBOL_SET_ID,
|
|
accessHash: '0',
|
|
title: lang('RecentStickers'),
|
|
stickers: recentStickers,
|
|
count: recentStickers.length,
|
|
});
|
|
}
|
|
|
|
const userSetIds = [...(addedSetIds || [])];
|
|
if (chatStickerSetId) {
|
|
userSetIds.unshift(chatStickerSetId);
|
|
}
|
|
|
|
const existingAddedSetIds = Object.values(pickTruthy(stickerSetsById, userSetIds));
|
|
|
|
return [
|
|
...defaultSets,
|
|
...existingAddedSetIds,
|
|
];
|
|
}, [
|
|
addedSetIds,
|
|
stickerSetsById,
|
|
favoriteStickers,
|
|
recentStickers,
|
|
chatStickerSetId,
|
|
lang,
|
|
effectStickers,
|
|
isForEffects,
|
|
effectEmojis,
|
|
]);
|
|
|
|
const noPopulatedSets = useMemo(() => (
|
|
areAddedLoaded
|
|
&& allSets.filter((set) => set.stickers?.length).length === 0
|
|
), [allSets, areAddedLoaded]);
|
|
|
|
useEffect(() => {
|
|
if (!loadAndPlay) return;
|
|
loadRecentStickers();
|
|
if (!canSendStickers) return;
|
|
sendMessageAction({ type: 'chooseSticker' });
|
|
}, [canSendStickers, loadAndPlay, loadRecentStickers, sendMessageAction]);
|
|
|
|
const canRenderContents = useAsyncRendering([], SLIDE_TRANSITION_DURATION);
|
|
const shouldRenderContents = areAddedLoaded && canRenderContents
|
|
&& !noPopulatedSets && (canSendStickers || isForEffects);
|
|
|
|
useHorizontalScroll(headerRef, !shouldRenderContents || !headerRef.current);
|
|
|
|
// 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);
|
|
|
|
animateHorizontalScroll(header, newLeft);
|
|
}, [areAddedLoaded, activeSetIndex]);
|
|
|
|
const handleStickerSelect = useLastCallback((sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => {
|
|
onStickerSelect(sticker, isSilent, shouldSchedule, true);
|
|
addRecentSticker({ sticker });
|
|
});
|
|
|
|
const handleStickerUnfave = useLastCallback((sticker: ApiSticker) => {
|
|
unfaveSticker({ sticker });
|
|
});
|
|
|
|
const handleStickerFave = useLastCallback((sticker: ApiSticker) => {
|
|
faveSticker({ sticker });
|
|
});
|
|
|
|
const handleMouseMove = useLastCallback(() => {
|
|
if (!canSendStickers) return;
|
|
sendMessageAction({ type: 'chooseSticker' });
|
|
});
|
|
|
|
const handleRemoveRecentSticker = useLastCallback((sticker: ApiSticker) => {
|
|
removeRecentSticker({ sticker });
|
|
});
|
|
|
|
if (!chat) return undefined;
|
|
|
|
function renderCover(stickerSet: StickerSetOrReactionsSetOrRecent, index: number) {
|
|
const firstSticker = stickerSet.stickers?.[0];
|
|
const buttonClassName = buildClassName(styles.stickerCover, index === activeSetIndex && styles.activated);
|
|
const withSharedCanvas = index < STICKER_PICKER_MAX_SHARED_COVERS;
|
|
|
|
if (stickerSet.id === RECENT_SYMBOL_SET_ID
|
|
|| stickerSet.id === FAVORITE_SYMBOL_SET_ID
|
|
|| stickerSet.id === CHAT_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"
|
|
|
|
onClick={() => selectStickerSet(index)}
|
|
>
|
|
{stickerSet.id === RECENT_SYMBOL_SET_ID ? (
|
|
<Icon name="recent" />
|
|
) : stickerSet.id === FAVORITE_SYMBOL_SET_ID ? (
|
|
<Icon name="favorite" />
|
|
) : stickerSet.id === CHAT_STICKER_SET_ID ? (
|
|
<Avatar peer={chat} size="small" />
|
|
) : (
|
|
<StickerSetCover
|
|
stickerSet={stickerSet as ApiStickerSet}
|
|
noPlay={!canAnimate || !loadAndPlay}
|
|
observeIntersection={observeIntersectionForCovers}
|
|
sharedCanvasRef={withSharedCanvas ? sharedCanvasRef : undefined}
|
|
forcePlayback
|
|
/>
|
|
)}
|
|
</Button>
|
|
);
|
|
} else {
|
|
return (
|
|
<StickerButton
|
|
key={stickerSet.id}
|
|
sticker={firstSticker}
|
|
size={STICKER_SIZE_PICKER_HEADER}
|
|
title={stickerSet.title}
|
|
className={buttonClassName}
|
|
noPlay={!canAnimate || !loadAndPlay}
|
|
observeIntersection={observeIntersectionForCovers}
|
|
noContextMenu
|
|
isCurrentUserPremium
|
|
sharedCanvasRef={withSharedCanvas ? sharedCanvasRef : undefined}
|
|
withTranslucentThumb={isTranslucent}
|
|
onClick={selectStickerSet}
|
|
clickArg={index}
|
|
forcePlayback
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
|
|
const fullClassName = buildClassName(styles.root, className);
|
|
const headerClassName = buildClassName(
|
|
styles.header,
|
|
'no-scrollbar',
|
|
!shouldHideTopBorder && styles.headerWithBorder,
|
|
);
|
|
|
|
const isLoading = !shouldRenderContents && (canSendStickers || isForEffects) && !noPopulatedSets;
|
|
|
|
return (
|
|
<Transition className={fullClassName} activeKey={isLoading ? 0 : 1} name="fade" shouldCleanup>
|
|
{!shouldRenderContents ? (
|
|
!canSendStickers && !isForEffects ? (
|
|
<div className={styles.pickerDisabled}>{lang('ErrorSendRestrictedStickersAll')}</div>
|
|
) : noPopulatedSets ? (
|
|
<div className={styles.pickerDisabled}>{lang('NoStickers')}</div>
|
|
) : (
|
|
<Loading />
|
|
)
|
|
) : (
|
|
<>
|
|
{!isForEffects && (
|
|
<div ref={headerRef} className={headerClassName}>
|
|
<div className="shared-canvas-container">
|
|
<canvas ref={sharedCanvasRef} className="shared-canvas" />
|
|
{allSets.map(renderCover)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div
|
|
ref={containerRef}
|
|
onMouseMove={handleMouseMove}
|
|
onScroll={handleContentScroll}
|
|
className={
|
|
buildClassName(
|
|
styles.main,
|
|
IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll',
|
|
!isForEffects && styles.hasHeader,
|
|
)
|
|
}
|
|
>
|
|
{allSets.map((stickerSet, i) => (
|
|
<StickerSet
|
|
key={stickerSet.id}
|
|
stickerSet={stickerSet}
|
|
loadAndPlay={Boolean(canAnimate && loadAndPlay)}
|
|
noContextMenus={noContextMenus}
|
|
index={i}
|
|
idPrefix={prefix}
|
|
observeIntersection={observeIntersectionForSet}
|
|
observeIntersectionForPlayingItems={observeIntersectionForPlayingItems}
|
|
observeIntersectionForShowingItems={observeIntersectionForShowingItems}
|
|
isNearActive={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
|
|
favoriteStickers={favoriteStickers}
|
|
isSavedMessages={isSavedMessages}
|
|
isCurrentUserPremium={isCurrentUserPremium}
|
|
isTranslucent={isTranslucent}
|
|
isChatStickerSet={stickerSet.id === chatStickerSetId}
|
|
onStickerSelect={handleStickerSelect}
|
|
onStickerUnfave={handleStickerUnfave}
|
|
onStickerFave={handleStickerFave}
|
|
onStickerRemoveRecent={handleRemoveRecentSticker}
|
|
forcePlayback
|
|
shouldHideHeader={stickerSet.id === EFFECT_EMOJIS_SET_ID}
|
|
/>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
</Transition>
|
|
);
|
|
};
|
|
|
|
export default memo(withGlobal<OwnProps>(
|
|
(global, { chatId }): StateProps => {
|
|
const {
|
|
setsById,
|
|
added,
|
|
recent,
|
|
favorite,
|
|
effect,
|
|
} = global.stickers;
|
|
|
|
const isSavedMessages = selectIsChatWithSelf(global, chatId);
|
|
const chat = selectChat(global, chatId);
|
|
const chatStickerSetId = !isUserId(chatId) ? selectChatFullInfo(global, chatId)?.stickerSet?.id : undefined;
|
|
|
|
return {
|
|
chat,
|
|
effectStickers: effect?.stickers,
|
|
effectEmojis: effect?.emojis,
|
|
recentStickers: recent.stickers,
|
|
favoriteStickers: favorite.stickers,
|
|
stickerSetsById: setsById,
|
|
addedSetIds: added.setIds,
|
|
canAnimate: selectShouldLoopStickers(global),
|
|
isSavedMessages,
|
|
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
|
chatStickerSetId,
|
|
};
|
|
},
|
|
)(StickerPicker));
|