TelegramPWA/src/components/common/StickerSetModal.tsx
2025-04-04 13:05:44 +02:00

294 lines
9.7 KiB
TypeScript

import type { FC } from '../../lib/teact/teact';
import React, {
memo, useCallback, useEffect, useMemo, useRef,
} from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import type { ApiSticker, ApiStickerSet } from '../../api/types';
import type { MessageList } from '../../types';
import { EMOJI_SIZE_MODAL, STICKER_SIZE_MODAL, TME_LINK_PREFIX } from '../../config';
import { getAllowedAttachmentOptions, getCanPostInChat } from '../../global/helpers';
import {
selectCanScheduleUntilOnline,
selectChat,
selectChatFullInfo,
selectCurrentMessageList,
selectIsChatWithSelf,
selectIsCurrentUserPremium,
selectPeerPaidMessagesStars,
selectShouldSchedule,
selectStickerSet,
selectThreadInfo,
selectTopic,
} from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import { copyTextToClipboard } from '../../util/clipboard';
import renderText from './helpers/renderText';
import useAppLayout from '../../hooks/useAppLayout';
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
import useOldLang from '../../hooks/useOldLang';
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
import useSchedule from '../../hooks/useSchedule';
import useScrolledState from '../../hooks/useScrolledState';
import Button from '../ui/Button';
import DropdownMenu from '../ui/DropdownMenu';
import Loading from '../ui/Loading';
import MenuItem from '../ui/MenuItem';
import Modal from '../ui/Modal';
import Icon from './icons/Icon';
import StickerButton from './StickerButton';
import './StickerSetModal.scss';
export type OwnProps = {
isOpen: boolean;
fromSticker?: ApiSticker;
stickerSetShortName?: string;
onClose: () => void;
};
type StateProps = {
currentMessageList?: MessageList;
canSendStickers?: boolean;
stickerSet?: ApiStickerSet;
canScheduleUntilOnline?: boolean;
shouldSchedule?: boolean;
isSavedMessages?: boolean;
isCurrentUserPremium?: boolean;
shouldUpdateStickerSetOrder?: boolean;
};
const INTERSECTION_THROTTLE = 200;
const StickerSetModal: FC<OwnProps & StateProps> = ({
isOpen,
fromSticker,
stickerSetShortName,
stickerSet,
canSendStickers,
canScheduleUntilOnline,
shouldSchedule,
isSavedMessages,
isCurrentUserPremium,
shouldUpdateStickerSetOrder,
currentMessageList,
onClose,
}) => {
const {
loadStickers,
toggleStickerSet,
sendMessage,
showNotification,
} = getActions();
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
const lang = useOldLang();
const { isMobile } = useAppLayout();
const prevStickerSet = usePreviousDeprecated(stickerSet);
const renderingStickerSet = stickerSet || prevStickerSet;
const isAdded = Boolean(!renderingStickerSet?.isArchived && renderingStickerSet?.installedDate);
const isEmoji = renderingStickerSet?.isEmoji;
const [requestCalendar, calendar] = useSchedule(canScheduleUntilOnline);
const {
handleScroll: handleContentScroll,
isAtBeginning: shouldHideTopBorder,
} = useScrolledState();
const {
observe: observeIntersection,
} = useIntersectionObserver({ rootRef: containerRef, throttleMs: INTERSECTION_THROTTLE, isDisabled: !isOpen });
useEffect(() => {
if (isOpen && !renderingStickerSet?.stickers) {
loadStickers({
stickerSetInfo: fromSticker ? fromSticker.stickerSetInfo : { shortName: stickerSetShortName! },
});
}
}, [isOpen, fromSticker, loadStickers, stickerSetShortName, renderingStickerSet]);
const handleSelect = useCallback((sticker: ApiSticker, isSilent?: boolean, isScheduleRequested?: boolean) => {
if (!currentMessageList) {
return;
}
sticker = {
...sticker,
isPreloadedGlobally: true,
};
if (shouldSchedule || isScheduleRequested) {
requestCalendar((scheduledAt) => {
sendMessage({
messageList: currentMessageList, sticker, isSilent, scheduledAt,
});
onClose();
});
} else {
sendMessage({
messageList: currentMessageList,
sticker,
isSilent,
shouldUpdateStickerSetOrder: shouldUpdateStickerSetOrder && isAdded,
});
onClose();
}
}, [currentMessageList, shouldSchedule, requestCalendar, onClose, shouldUpdateStickerSetOrder, isAdded]);
const handleButtonClick = useCallback(() => {
if (renderingStickerSet) {
toggleStickerSet({ stickerSetId: renderingStickerSet.id });
onClose();
}
}, [onClose, renderingStickerSet, toggleStickerSet]);
const handleCopyLink = useCallback(() => {
if (!renderingStickerSet) return;
const { shortName } = renderingStickerSet;
const suffix = isEmoji ? 'addemoji' : 'addstickers';
const url = `${TME_LINK_PREFIX}${suffix}/${shortName}`;
copyTextToClipboard(url);
showNotification({
message: lang('LinkCopied'),
});
}, [isEmoji, lang, renderingStickerSet, showNotification]);
const renderButtonText = () => {
if (!renderingStickerSet) return lang('Loading');
const suffix = isEmoji ? 'Emoji' : 'Sticker';
return lang(
isAdded ? `StickerPack.Remove${suffix}Count` : `StickerPack.Add${suffix}Count`,
renderingStickerSet.count,
'i',
);
};
const MoreMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
return ({ onTrigger, isOpen: isMenuOpen }) => (
<Button
round
ripple={!isMobile}
size="smaller"
color="translucent"
className={isMenuOpen ? 'active' : ''}
onClick={onTrigger}
ariaLabel="More actions"
>
<Icon name="more" />
</Button>
);
}, [isMobile]);
function renderHeader() {
const fullClassName = buildClassName('modal-header', !shouldHideTopBorder && 'with-top-border');
return (
<div className={fullClassName} dir={lang.isRtl ? 'rtl' : undefined}>
<Button round color="translucent" size="smaller" ariaLabel={lang('Close')} onClick={onClose}>
<Icon name="close" />
</Button>
<div className="modal-title">
{renderingStickerSet ? renderText(renderingStickerSet.title, ['emoji', 'links']) : lang('AccDescrStickerSet')}
</div>
<DropdownMenu
className="stickers-more-menu with-menu-transitions"
trigger={MoreMenuButton}
positionX="right"
>
<MenuItem icon="copy" onClick={handleCopyLink}>{lang('StickersCopy')}</MenuItem>
</DropdownMenu>
</div>
);
}
return (
<Modal
className={buildClassName('StickerSetModal', isEmoji && 'custom-emoji')}
isOpen={isOpen}
onClose={onClose}
header={renderHeader()}
>
{renderingStickerSet?.stickers ? (
<>
<div ref={containerRef} className="stickers custom-scroll" onScroll={handleContentScroll}>
<div className="shared-canvas-container stickers-grid">
<canvas ref={sharedCanvasRef} className="shared-canvas" />
{renderingStickerSet.stickers.map((sticker) => (
<StickerButton
sticker={sticker}
size={isEmoji ? EMOJI_SIZE_MODAL : STICKER_SIZE_MODAL}
observeIntersection={observeIntersection}
onClick={canSendStickers && !isEmoji ? handleSelect : undefined}
clickArg={sticker}
isSavedMessages={isSavedMessages}
isCurrentUserPremium={isCurrentUserPremium}
sharedCanvasRef={sharedCanvasRef}
/>
))}
</div>
</div>
<div className="button-wrapper">
<Button
size="smaller"
fluid
color={isAdded ? 'danger' : 'primary'}
onClick={handleButtonClick}
>
{renderButtonText()}
</Button>
</div>
</>
) : (
<Loading />
)}
{calendar}
</Modal>
);
};
export default memo(withGlobal<OwnProps>(
(global, { fromSticker, stickerSetShortName }): StateProps => {
const currentMessageList = selectCurrentMessageList(global);
const { chatId, threadId } = currentMessageList || {};
const chat = chatId && selectChat(global, chatId);
const chatFullInfo = chatId ? selectChatFullInfo(global, chatId) : undefined;
const sendOptions = chat ? getAllowedAttachmentOptions(chat, chatFullInfo) : undefined;
const threadInfo = chatId && threadId ? selectThreadInfo(global, chatId, threadId) : undefined;
const isMessageThread = Boolean(!threadInfo?.isCommentsInfo && threadInfo?.fromChannelId);
const topic = chatId && threadId ? selectTopic(global, chatId, threadId) : undefined;
const canSendStickers = Boolean(
chat && threadId && getCanPostInChat(chat, topic, isMessageThread, chatFullInfo)
&& sendOptions?.canSendStickers,
);
const isSavedMessages = Boolean(chatId) && selectIsChatWithSelf(global, chatId);
const stickerSetInfo = fromSticker ? fromSticker.stickerSetInfo
: stickerSetShortName ? { shortName: stickerSetShortName } : undefined;
const stickerSet = stickerSetInfo ? selectStickerSet(global, stickerSetInfo) : undefined;
const paidMessagesStars = chatId ? selectPeerPaidMessagesStars(global, chatId) : undefined;
return {
canScheduleUntilOnline: Boolean(chatId) && selectCanScheduleUntilOnline(global, chatId),
canSendStickers,
isSavedMessages,
shouldSchedule: !paidMessagesStars && selectShouldSchedule(global),
stickerSet,
isCurrentUserPremium: selectIsCurrentUserPremium(global),
shouldUpdateStickerSetOrder: global.settings.byKey.shouldUpdateStickerSetOrder,
currentMessageList,
};
},
)(StickerSetModal));