2025-09-30 16:52:04 +02:00

254 lines
8.1 KiB
TypeScript

import { memo, useMemo, useRef } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiEmojiStatusType, ApiPeer, ApiSavedStarGift } from '../../../api/types';
import { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../../config';
import { getHasAdminRight } from '../../../global/helpers';
import { selectChat, selectPeer, selectUser } from '../../../global/selectors';
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
import buildClassName from '../../../util/buildClassName';
import { formatStarsAsIcon, formatTonAsIcon } from '../../../util/localization/format';
import { CUSTOM_PEER_HIDDEN } from '../../../util/objects/customPeer';
import { formatIntegerCompact } from '../../../util/textFormat';
import { getGiftAttributes, getStickerFromGift, getTotalGiftAvailability } from '../helpers/gifts';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useFlag from '../../../hooks/useFlag';
import { type ObserveFn } from '../../../hooks/useIntersectionObserver';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import StickerView from '../../common/StickerView';
import Button from '../../ui/Button';
import Menu from '../../ui/Menu';
import Avatar from '../Avatar';
import Icon from '../icons/Icon';
import RadialPatternBackground from '../profile/RadialPatternBackground';
import GiftMenuItems from './GiftMenuItems';
import GiftRibbon from './GiftRibbon';
import styles from './SavedGift.module.scss';
type OwnProps = {
peerId: string;
gift: ApiSavedStarGift;
style?: string;
className?: string;
observeIntersection?: ObserveFn;
};
type StateProps = {
fromPeer?: ApiPeer;
currentUserId?: string;
hasAdminRights?: boolean;
currentUserEmojiStatus?: ApiEmojiStatusType;
collectibleEmojiStatuses?: ApiEmojiStatusType[];
};
const GIFT_STICKER_SIZE = 90;
const SavedGift = ({
peerId,
gift,
style,
fromPeer,
currentUserId,
hasAdminRights,
collectibleEmojiStatuses,
currentUserEmojiStatus,
className,
observeIntersection,
}: OwnProps & StateProps) => {
const { openGiftInfoModal } = getActions();
const ref = useRef<HTMLDivElement>();
const stickerRef = useRef<HTMLDivElement>();
const lang = useLang();
const [isHover, markHover, unmarkHover] = useFlag();
const canManage = peerId === currentUserId || hasAdminRights;
const totalIssued = getTotalGiftAvailability(gift.gift);
const starGift = gift.gift;
const starGiftUnique = starGift.type === 'starGiftUnique' ? starGift : undefined;
const resellPrice = useMemo(() => {
if (!starGiftUnique?.resellPrice) return undefined;
if (starGiftUnique.resaleTonOnly) {
return starGiftUnique.resellPrice.find((amount) => amount.currency === TON_CURRENCY_CODE);
}
return starGiftUnique.resellPrice.find((amount) => amount.currency === STARS_CURRENCY_CODE);
}, [starGiftUnique]);
const ribbonText = (() => {
if (starGiftUnique?.resellPrice) {
return lang('GiftRibbonSale');
}
if (gift.isPinned && starGiftUnique) {
return lang('GiftSavedNumber', { number: starGiftUnique.number });
}
if (totalIssued) {
return lang('ActionStarGiftLimitedRibbon', { total: formatIntegerCompact(lang, totalIssued) });
}
return undefined;
})();
const ribbonColor = starGiftUnique?.resellPrice ? 'green' : 'blue';
const {
isContextMenuOpen, contextMenuAnchor,
handleBeforeContextMenu, handleContextMenu,
handleContextMenuClose, handleContextMenuHide,
} = useContextMenuHandlers(ref);
const getTriggerElement = useLastCallback(() => ref.current);
const getRootElement = useLastCallback(() => ref.current!.closest('.custom-scroll'));
const getMenuElement = useLastCallback(() => (
document.querySelector('#portals')?.querySelector('.saved-gift-context-menu .bubble')
));
const getLayout = useLastCallback(() => ({ withPortal: true }));
const handleClick = useLastCallback(() => {
openGiftInfoModal({
peerId,
gift,
});
});
const avatarPeer = (gift.isNameHidden && !fromPeer) ? CUSTOM_PEER_HIDDEN : fromPeer;
const sticker = getStickerFromGift(gift.gift);
const giftAttributes = useMemo(() => getGiftAttributes(gift.gift), [gift.gift]);
const { backdrop, pattern } = giftAttributes || {};
const radialPatternBackdrop = useMemo(() => {
if (!backdrop || !pattern) {
return undefined;
}
const backdropColors = [backdrop.centerColor, backdrop.edgeColor];
const patternColor = backdrop.patternColor;
return (
<RadialPatternBackground
className={styles.radialPattern}
backgroundColors={backdropColors}
patternColor={patternColor}
patternIcon={pattern.sticker}
/>
);
}, [backdrop, pattern]);
if (!sticker) return undefined;
return (
<div
ref={ref}
className={buildClassName('interactive-gift scroll-item', styles.root, className)}
style={style}
onClick={handleClick}
onContextMenu={handleContextMenu}
onMouseDown={handleBeforeContextMenu}
onMouseEnter={!IS_TOUCH_ENV ? markHover : undefined}
onMouseLeave={!IS_TOUCH_ENV ? unmarkHover : undefined}
>
{radialPatternBackdrop}
{!radialPatternBackdrop && <Avatar className={styles.topIcon} peer={avatarPeer} size="micro" />}
{gift.isPinned && <Icon name="pinned-message" className={styles.topIcon} />}
<div
ref={stickerRef}
className={styles.stickerWrapper}
style={`width: ${GIFT_STICKER_SIZE}px; height: ${GIFT_STICKER_SIZE}px`}
>
{sticker && (
<StickerView
containerRef={stickerRef}
sticker={sticker}
size={GIFT_STICKER_SIZE}
shouldLoop={isHover}
shouldPreloadPreview
observeIntersectionForPlaying={observeIntersection}
observeIntersectionForLoading={observeIntersection}
/>
)}
</div>
{gift.isUnsaved && (
<div className={styles.hiddenGift}>
<Icon name="eye-crossed-outline" />
</div>
)}
{resellPrice && (
<Button
className={styles.priceBadge}
nonInteractive
size="tiny"
withSparkleEffect
noSparkleAnimation
pill
fluid
>
{resellPrice.currency === 'TON'
? formatTonAsIcon(lang, resellPrice.amount, { shouldConvertFromNanos: true, className: styles.star })
: formatStarsAsIcon(lang, resellPrice.amount, { asFont: true, className: styles.star })}
</Button>
)}
{ribbonText && (
<GiftRibbon
color={ribbonColor}
text={ribbonText}
/>
)}
{contextMenuAnchor !== undefined && (
<Menu
isOpen={isContextMenuOpen}
anchor={contextMenuAnchor}
className="saved-gift-context-menu"
autoClose
withPortal
getMenuElement={getMenuElement}
getTriggerElement={getTriggerElement}
getRootElement={getRootElement}
getLayout={getLayout}
onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide}
>
<GiftMenuItems
peerId={peerId}
gift={gift}
canManage={canManage}
collectibleEmojiStatuses={collectibleEmojiStatuses}
currentUserEmojiStatus={currentUserEmojiStatus}
/>
</Menu>
)}
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, { peerId, gift }): Complete<StateProps> => {
const fromPeer = gift.fromId ? selectPeer(global, gift.fromId) : undefined;
const chat = selectChat(global, peerId);
const hasAdminRights = chat && getHasAdminRight(chat, 'postMessages');
const currentUserId = global.currentUserId;
const currentUser = currentUserId ? selectUser(global, currentUserId) : undefined;
const currentUserEmojiStatus = currentUser?.emojiStatus;
const collectibleEmojiStatuses = global.collectibleEmojiStatuses?.statuses;
return {
fromPeer,
hasAdminRights,
currentUserId,
currentUserEmojiStatus,
collectibleEmojiStatuses,
};
},
)(SavedGift));