import { memo, useMemo, useRef, useState } from '../../../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../../../global'; import type { ApiEmojiStatusType, ApiPeer, ApiUser, } from '../../../../api/types'; import type { TabState } from '../../../../global/types'; import { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../../../config'; import { getHasAdminRight } from '../../../../global/helpers'; import { getPeerTitle, isApiPeerChat, isApiPeerUser } from '../../../../global/helpers/peers'; import { getMainUsername } from '../../../../global/helpers/users'; import { selectPeer, selectUser } from '../../../../global/selectors'; import buildClassName from '../../../../util/buildClassName'; import { copyTextToClipboard } from '../../../../util/clipboard'; import { formatDateTimeToString } from '../../../../util/dates/dateFormat'; import { formatCurrencyAsString } from '../../../../util/formatCurrency'; import { formatStarsAsIcon, formatStarsAsText, formatTonAsIcon, formatTonAsText, } from '../../../../util/localization/format'; import { CUSTOM_PEER_HIDDEN } from '../../../../util/objects/customPeer'; import { getServerTime } from '../../../../util/serverTime'; import { formatPercent } from '../../../../util/textFormat'; import { renderGiftOriginalInfo } from '../../../common/helpers/giftOriginalInfo'; import { getGiftAttributes, getStickerFromGift } from '../../../common/helpers/gifts'; import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities'; import useContextMenuHandlers from '../../../../hooks/useContextMenuHandlers'; import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev'; import useFlag from '../../../../hooks/useFlag'; import useLang from '../../../../hooks/useLang'; import useLastCallback from '../../../../hooks/useLastCallback'; import useOldLang from '../../../../hooks/useOldLang'; import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker'; import Avatar from '../../../common/Avatar'; import BadgeButton from '../../../common/BadgeButton'; import GiftMenuItems from '../../../common/gift/GiftMenuItems'; import GiftTransferPreview from '../../../common/gift/GiftTransferPreview'; import Icon from '../../../common/icons/Icon'; import SafeLink from '../../../common/SafeLink'; import Button from '../../../ui/Button'; import Checkbox from '../../../ui/Checkbox'; import ConfirmDialog from '../../../ui/ConfirmDialog'; import Link from '../../../ui/Link'; import Menu from '../../../ui/Menu'; import TableInfoModal, { type TableData } from '../../common/TableInfoModal'; import UniqueGiftHeader from '../UniqueGiftHeader'; import styles from './GiftInfoModal.module.scss'; export type OwnProps = { modal: TabState['giftInfoModal']; }; type StateProps = { fromPeer?: ApiPeer; targetPeer?: ApiPeer; releasedByPeer?: ApiPeer; currentUserId?: string; starGiftMaxConvertPeriod?: number; hasAdminRights?: boolean; currentUserEmojiStatus?: ApiEmojiStatusType; collectibleEmojiStatuses?: ApiEmojiStatusType[]; tonExplorerUrl?: string; currentUser?: ApiUser; recipientPeer?: ApiPeer; }; const STICKER_SIZE = 120; const GiftInfoModal = ({ modal, fromPeer, targetPeer, releasedByPeer, currentUserId, starGiftMaxConvertPeriod, hasAdminRights, currentUserEmojiStatus, collectibleEmojiStatuses, tonExplorerUrl, currentUser, recipientPeer, }: OwnProps & StateProps) => { const { closeGiftInfoModal, changeGiftVisibility, convertGiftToStars, openChatWithInfo, focusMessage, openGiftUpgradeModal, showNotification, buyStarGift, closeGiftModal, openGiftInfoValueModal, updateResaleGiftsFilter, openGiftInMarket, openGiftDescriptionRemoveModal, } = getActions(); const [isConvertConfirmOpen, openConvertConfirm, closeConvertConfirm] = useFlag(); const lang = useLang(); const oldLang = useOldLang(); const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false); const [shouldPayInTon, setShouldPayInTon] = useState(false); const splitButtonRef = useRef(); const menuRef = useRef(); const uniqueGiftHeaderRef = useRef(); const { isContextMenuOpen, contextMenuAnchor, handleContextMenu, handleContextMenuClose, handleContextMenuHide, } = useContextMenuHandlers(splitButtonRef); const handleSymbolClick = useLastCallback(() => { if (!gift || !giftAttributes?.pattern) return; openGiftInMarket({ gift }); updateResaleGiftsFilter({ filter: { sortType: 'byDate', modelAttributes: [], backdropAttributes: [], patternAttributes: [{ type: 'pattern', documentId: giftAttributes.pattern.sticker.id, }], }, }); }); const handleBackdropClick = useLastCallback(() => { if (!gift || !giftAttributes?.backdrop) return; openGiftInMarket({ gift }); updateResaleGiftsFilter({ filter: { sortType: 'byDate', modelAttributes: [], backdropAttributes: [{ type: 'backdrop', backdropId: giftAttributes.backdrop.backdropId, }], patternAttributes: [], }, }); }); const handleModelClick = useLastCallback(() => { if (!gift || !giftAttributes?.model) return; openGiftInMarket({ gift }); updateResaleGiftsFilter({ filter: { sortType: 'byDate', modelAttributes: [{ type: 'model', documentId: giftAttributes.model.sticker.id, }], backdropAttributes: [], patternAttributes: [], }, }); }); const isOpen = Boolean(modal); const renderingModal = useCurrentOrPrev(modal); const renderingFromPeer = useCurrentOrPrev(fromPeer); const renderingTargetPeer = useCurrentOrPrev(targetPeer); const isTargetChat = renderingTargetPeer && isApiPeerChat(renderingTargetPeer); const { gift: typeGift } = renderingModal || {}; const isSavedGift = typeGift && 'gift' in typeGift; const savedGift = isSavedGift ? typeGift : undefined; const isSender = savedGift?.fromId === currentUserId; const canConvertDifference = (savedGift && starGiftMaxConvertPeriod && ( savedGift.date + starGiftMaxConvertPeriod - getServerTime() )) || 0; const conversionLeft = Math.ceil(canConvertDifference / 60 / 60 / 24); const gift = isSavedGift ? typeGift.gift : typeGift; const giftSticker = gift && getStickerFromGift(gift); const hasConvertOption = canConvertDifference > 0 && Boolean(savedGift?.starsToConvert); const isGiftUnique = gift && gift.type === 'starGiftUnique'; const uniqueGift = isGiftUnique ? gift : undefined; const giftSubtitle = useMemo(() => { if (!gift || gift.type !== 'starGiftUnique') return undefined; if (releasedByPeer) { const releasedByUsername = `@${getMainUsername(releasedByPeer)}`; const ownerTitle = releasedByUsername || getPeerTitle(lang, releasedByPeer); const fallbackText = isApiPeerUser(releasedByPeer) ? lang('ActionFallbackUser') : lang('ActionFallbackChannel'); return lang('GiftInfoCollectibleBy', { number: gift.number, owner: ownerTitle || fallbackText }, { withNodes: true, withMarkdown: true, }); } return lang('GiftInfoCollectible', { number: gift.number }); }, [gift, releasedByPeer, lang]); const starGiftUniqueSlug = gift?.type === 'starGiftUnique' ? gift.slug : undefined; const selfCollectibleStatus = useMemo(() => { if (!starGiftUniqueSlug) return undefined; return collectibleEmojiStatuses?.find((status) => status.type === 'collectible' && status.slug === starGiftUniqueSlug); }, [starGiftUniqueSlug, collectibleEmojiStatuses]); const isSelfUnique = Boolean(selfCollectibleStatus); const canFocusUpgrade = Boolean(savedGift?.upgradeMsgId); const canManage = !canFocusUpgrade && savedGift?.inputGift && ( isTargetChat ? hasAdminRights : gift?.type === 'starGift' ? renderingTargetPeer?.id === currentUserId : gift?.ownerId === currentUserId || isSelfUnique ); function getResalePrice(isInTon?: boolean) { if (!isGiftUnique) return undefined; const amounts = gift.resellPrice; if (!amounts) return undefined; if (gift?.resaleTonOnly || isInTon) { return amounts.find((amount) => amount.currency === TON_CURRENCY_CODE); } return amounts.find((amount) => amount.currency === STARS_CURRENCY_CODE); } const resellPrice = getResalePrice(); const confirmPrice = getResalePrice(shouldPayInTon); const canBuyGift = !isSelfUnique && gift?.type === 'starGiftUnique' && gift.ownerId !== currentUserId && Boolean(resellPrice); const giftOwnerTitle = (() => { if (!isGiftUnique) return undefined; const { ownerName, ownerId } = gift; const global = getGlobal(); // Peer titles do not need to be reactive const owner = ownerId ? selectPeer(global, ownerId) : undefined; return owner ? getPeerTitle(lang, owner) : ownerName; })(); const handleClose = useLastCallback(() => { closeGiftInfoModal(); }); const handleFocusUpgraded = useLastCallback(() => { const giftChat = isSender ? renderingTargetPeer : renderingFromPeer; if (!savedGift?.upgradeMsgId || !giftChat) return; const { upgradeMsgId } = savedGift; focusMessage({ chatId: giftChat.id, messageId: upgradeMsgId }); handleClose(); }); const handleTriggerVisibility = useLastCallback(() => { const { inputGift, isUnsaved } = savedGift!; changeGiftVisibility({ gift: inputGift!, shouldUnsave: !isUnsaved }); handleClose(); }); const handleConvertToStars = useLastCallback(() => { const { inputGift } = savedGift!; convertGiftToStars({ gift: inputGift! }); closeConvertConfirm(); handleClose(); }); const handleRemoveMessage = useLastCallback(() => { if (!savedGift?.inputGift || !savedGift.dropOriginalDetailsStars || !gift || !giftAttributes) return; const { originalDetails } = giftAttributes; if (!originalDetails) return; openGiftDescriptionRemoveModal({ gift: savedGift, price: savedGift.dropOriginalDetailsStars, details: originalDetails, }); }); const handleOpenUpgradeModal = useLastCallback(() => { if (!savedGift) return; const giftOwnerId = renderingTargetPeer?.id; openGiftUpgradeModal({ giftId: savedGift.gift.id, gift: savedGift, peerId: giftOwnerId }); }); const handleBuyGift = useLastCallback(() => { if (gift?.type !== 'starGiftUnique' || !getResalePrice()) return; setIsConfirmModalOpen(true); }); const closeConfirmModal = useLastCallback(() => { setIsConfirmModalOpen(false); }); const handleConfirmBuyGift = useLastCallback(() => { const peer = recipientPeer || currentUser; const price = getResalePrice(shouldPayInTon); if (!peer || !price || gift?.type !== 'starGiftUnique') return; closeConfirmModal(); closeGiftModal(); buyStarGift({ peerId: peer.id, slug: gift.slug, price }); }); const handleOpenValueModal = useLastCallback(() => { if (!gift || gift.type !== 'starGiftUnique') return; openGiftInfoValueModal({ gift, }); }); const giftAttributes = useMemo(() => { return gift && getGiftAttributes(gift); }, [gift]); const SettingsMenuButton = useMemo(() => { return (
); }, [lang, handleContextMenu]); const renderFooterButton = useLastCallback(() => { if (canBuyGift) { return ( ); } if (canFocusUpgrade) { return ( ); } if (canManage && savedGift?.alreadyPaidUpgradeStars && !savedGift.upgradeMsgId) { return ( ); } if (canManage && savedGift?.canUpgrade && !savedGift.upgradeMsgId) { return ( ); } if (savedGift?.prepaidUpgradeHash) { return ( ); } return ( ); }); const saleDateInfo = useMemo(() => { if (!gift) return undefined; let text = ''; if (gift.type === 'starGift') { if (gift.firstSaleDate) { text += `${lang('GiftInfoFirstSale')} ${formatDateTimeToString(gift.firstSaleDate * 1000, lang.code, true)}`; } if (gift.lastSaleDate) { text += '\n'; text += `${lang('GiftInfoLastSale')} ${formatDateTimeToString(gift.lastSaleDate * 1000, lang.code, true)}`; } } return text; }, [gift, lang]); const modalData = useMemo(() => { if (!typeGift || !gift) { return undefined; } const { fromId, isNameHidden, starsToConvert, isUnsaved, isConverted, upgradeMsgId, } = savedGift || {}; const canConvert = hasConvertOption && Boolean(starsToConvert); const isVisibleForMe = isNameHidden && renderingTargetPeer; const description = (() => { if (!savedGift) return lang('GiftInfoSoldOutDescription'); if (isTargetChat) return undefined; if (savedGift.upgradeMsgId) return lang('GiftInfoDescriptionUpgraded'); if (canManage && savedGift.canUpgrade && savedGift.alreadyPaidUpgradeStars && !savedGift.upgradeMsgId) { return lang('GiftInfoDescriptionUpgrade2'); } if (savedGift.canUpgrade && canManage) { return canManage ? lang('GiftInfoDescriptionFreeUpgrade') : lang('GiftInfoPeerDescriptionFreeUpgradeOut', { peer: getPeerTitle(lang, renderingTargetPeer!)! }); } if (!canManage && !isSender) return undefined; if (isConverted && canConvert) { return canManage ? lang('GiftInfoDescriptionConverted', { amount: starsToConvert, }, { pluralValue: starsToConvert, withNodes: true, withMarkdown: true, }) : lang('GiftInfoPeerDescriptionOutConverted', { amount: starsToConvert, peer: getPeerTitle(lang, renderingTargetPeer!)!, }, { pluralValue: starsToConvert, withNodes: true, withMarkdown: true, }); } if (savedGift.canUpgrade && canManage) { if (canConvert) { return lang('GiftInfoDescriptionUpgrade', { amount: starsToConvert, }, { pluralValue: starsToConvert, withNodes: true, withMarkdown: true, }); } return lang('GiftInfoDescriptionUpgradeRegular'); } if (canManage) { if (canConvert) { return lang('GiftInfoDescription', { amount: starsToConvert, }, { withNodes: true, withMarkdown: true, pluralValue: starsToConvert, }); } return lang('GiftInfoDescriptionRegular'); } if (canConvert) { return lang('GiftInfoPeerDescriptionOut', { amount: starsToConvert, peer: getPeerTitle(lang, renderingTargetPeer!)!, }, { withNodes: true, withMarkdown: true, pluralValue: starsToConvert, }); } return lang('GiftInfoPeerDescriptionOutRegular', { peer: getPeerTitle(lang, renderingTargetPeer!)! }); })(); function getTitle() { if (isGiftUnique) return gift.title; if (!savedGift) return lang('GiftInfoSoldOutTitle'); return canManage ? lang('GiftInfoReceived') : lang('GiftInfoTitle'); } const uniqueGiftModalHeader = (
{Boolean(resellPrice?.amount) && (
{resellPrice.currency === TON_CURRENCY_CODE ? formatTonAsIcon(lang, resellPrice.amount, { className: styles.giftResalePriceStar, shouldConvertFromNanos: true, }) : formatStarsAsIcon(lang, resellPrice.amount, { asFont: true, className: styles.giftResalePriceStar, })}
)}
{SettingsMenuButton}
); const uniqueGiftHeader = isGiftUnique && (
); const regularHeader = (

{getTitle()}

{Boolean(description) && (

{description}

)}
); const tableData: TableData = []; if (gift.type === 'starGift') { const hasFrom = fromId || isNameHidden; if (hasFrom) { tableData.push([ lang('GiftInfoFrom'), !fromId ? ( <> {oldLang(CUSTOM_PEER_HIDDEN.titleKey!)} ) : { chatId: fromId }, ]); } if (savedGift?.date) { tableData.push([ lang('GiftInfoDate'), {formatDateTimeToString(savedGift.date * 1000, lang.code, true)}, ]); } if (gift.firstSaleDate && !savedGift) { tableData.push([ lang('GiftInfoFirstSale'), formatDateTimeToString(gift.firstSaleDate * 1000, lang.code, true), ]); } if (gift.lastSaleDate && !savedGift) { tableData.push([ lang('GiftInfoLastSale'), formatDateTimeToString(gift.lastSaleDate * 1000, lang.code, true), ]); } const starsValue = gift.stars + (savedGift?.alreadyPaidUpgradeStars || 0); tableData.push([ lang('GiftInfoValue'),
{formatStarsAsIcon(lang, starsValue, { className: styles.starAmountIcon })} {canManage && hasConvertOption && Boolean(starsToConvert) && ( {lang('GiftInfoConvert', { amount: starsToConvert }, { pluralValue: starsToConvert })} )}
, ]); if (gift.availabilityTotal) { tableData.push([ lang('GiftInfoAvailability'), lang('GiftInfoAvailabilityValue', { count: gift.availabilityRemains || 0, total: gift.availabilityTotal, }, { pluralValue: gift.availabilityRemains || 0, }), ]); } if (gift.upgradeStars && !upgradeMsgId) { tableData.push([ lang('GiftInfoStatus'),
{lang('GiftInfoStatusNonUnique')}
, ]); } if (savedGift?.message) { tableData.push([ undefined, renderTextWithEntities(savedGift.message), ]); } } if (isGiftUnique) { const { ownerName, ownerAddress, ownerId } = gift; const ownerPeer = ownerId ? selectPeer(getGlobal(), ownerId) : undefined; const { model, backdrop, pattern, originalDetails, } = giftAttributes || {}; if (ownerAddress) { tableData.push([ lang('GiftInfoOwner'), { copyTextToClipboard(ownerAddress); showNotification({ message: { key: 'WalletAddressCopied' }, icon: 'copy', }); }} > {ownerAddress} , ]); } else if (ownerPeer || ownerName) { tableData.push([ lang('GiftInfoOwner'), ownerId ? { chatId: ownerId, withEmojiStatus: true } : ownerName || '', ]); } if (model) { tableData.push([ lang('GiftAttributeModel'), {model.name} {formatPercent(model.rarityPercent)} , ]); } if (backdrop) { tableData.push([ lang('GiftAttributeBackdrop'), {backdrop.name} {formatPercent(backdrop.rarityPercent)} , ]); } if (pattern) { tableData.push([ lang('GiftAttributeSymbol'), {pattern.name} {formatPercent(pattern.rarityPercent)} , ]); } tableData.push([ lang('GiftInfoAvailability'), lang('GiftInfoIssued', { issued: gift.issuedCount, total: gift.totalCount, }), ]); if (gift.valueAmount && gift.valueCurrency) { tableData.push([ lang('GiftInfoValue'), ~ {' '} {formatCurrencyAsString( gift.valueAmount, gift.valueCurrency, lang.code, )} {lang('GiftInfoValueLinkMore')} , ]); } if (originalDetails) { const { recipientId, senderId } = originalDetails; const global = getGlobal(); // Peer titles do not need to be reactive const openChat = (id: string) => { openChatWithInfo({ id }); closeGiftInfoModal(); }; const recipient = selectPeer(global, recipientId)!; const sender = senderId ? selectPeer(global, senderId) : undefined; const text = renderGiftOriginalInfo({ originalDetails, recipient, sender, onOpenChat: openChat, lang, }); tableData.push([ undefined,
{text}
{Boolean(savedGift?.dropOriginalDetailsStars) && (
, ]); } } const tonLink = tonExplorerUrl && isGiftUnique && gift.giftAddress && ( `${tonExplorerUrl}${gift.giftAddress}` ); const footer = (
{(canManage || tonLink || canBuyGift) && (
{tonLink && (
{lang('GiftInfoTonText', { link: , }, { withNodes: true })}
)} {canManage && (
{lang(`GiftInfo${isTargetChat ? 'Channel' : ''}${isUnsaved ? 'Hidden' : 'Saved'}`, { link: ( {lang(`GiftInfoSaved${isUnsaved ? 'Show' : 'Hide'}`)} ), }, { withNodes: true, })}
)} {!canBuyGift && isVisibleForMe && (
{lang('GiftInfoSenderHidden')}
)} {canBuyGift && giftOwnerTitle && (
{lang('GiftInfoBuyGift', { user: giftOwnerTitle, }, { withNodes: true })}
)}
)} {renderFooterButton()}
); return { modalHeader: isGiftUnique ? uniqueGiftModalHeader : undefined, header: isGiftUnique ? uniqueGiftHeader : regularHeader, tableData, footer, }; }, [ typeGift, savedGift, renderingTargetPeer, giftSticker, lang, canManage, hasConvertOption, isSender, oldLang, tonExplorerUrl, gift, giftAttributes, renderFooterButton, isTargetChat, SettingsMenuButton, isGiftUnique, saleDateInfo, canBuyGift, giftOwnerTitle, resellPrice, giftSubtitle, releasedByPeer, handleSymbolClick, handleBackdropClick, handleModelClick, ]); const getRootElement = useLastCallback(() => uniqueGiftHeaderRef.current); const getTriggerElement = useLastCallback(() => splitButtonRef.current); const getMenuElement = useLastCallback(() => menuRef.current); const getLayout = useLastCallback(() => ({ withPortal: true })); const uniqueGiftContextMenu = contextMenuAnchor && typeGift && ( ); return ( <> {uniqueGiftContextMenu} {uniqueGift && currentUser && Boolean(confirmPrice) && ( {uniqueGift.resaleTonOnly && (
{lang('ConfirmBuyGiftForTonDescription')}
)}
{lang('TitleConfirmPayment')}
{!recipientPeer && (

{lang('GiftBuyConfirmDescription', { gift: lang('GiftUnique', { title: uniqueGift.title, number: uniqueGift.number }), stars: confirmPrice?.currency === TON_CURRENCY_CODE ? formatTonAsText(lang, confirmPrice.amount, true) : formatStarsAsText(lang, confirmPrice.amount), }, { withNodes: true, withMarkdown: true, })}

)} {recipientPeer && (

{lang('GiftBuyForPeerConfirmDescription', { gift: lang('GiftUnique', { title: uniqueGift.title, number: uniqueGift.number }), stars: confirmPrice?.currency === TON_CURRENCY_CODE ? formatTonAsText(lang, confirmPrice.amount, true) : formatStarsAsText(lang, confirmPrice.amount), peer: getPeerTitle(lang, recipientPeer), }, { withNodes: true, withMarkdown: true, })}

)} {!uniqueGift.resaleTonOnly && ( <>
{lang('DescriptionPayInTON')}
)}
)} {savedGift && (
{lang('GiftInfoPeerConvertDescription', { amount: formatStarsAsText(lang, savedGift.starsToConvert!), peer: getPeerTitle(lang, renderingFromPeer!)!, }, { withNodes: true, withMarkdown: true, })}
{hasConvertOption && (
{lang('GiftInfoConvertDescriptionPeriod', { count: conversionLeft, }, { withNodes: true, withMarkdown: true, pluralValue: conversionLeft, })}
)}
{lang('GiftInfoConvertDescription2')}
)} ); }; export default memo(withGlobal( (global, { modal }): Complete => { const typeGift = modal?.gift; const isSavedGift = typeGift && 'gift' in typeGift; const currentUserId = global.currentUserId; const fromId = isSavedGift && typeGift.fromId; const fromPeer = fromId ? selectPeer(global, fromId) : undefined; const targetPeer = modal?.peerId ? selectPeer(global, modal.peerId) : undefined; const chat = targetPeer && isApiPeerChat(targetPeer) ? targetPeer : undefined; const hasAdminRights = chat && getHasAdminRight(chat, 'postMessages'); const currentUser = selectUser(global, currentUserId!); const recipientPeer = modal?.recipientId && currentUserId !== modal.recipientId ? selectPeer(global, modal.recipientId) : undefined; const currentUserEmojiStatus = currentUser?.emojiStatus; const collectibleEmojiStatuses = global.collectibleEmojiStatuses?.statuses; const gift = isSavedGift ? typeGift.gift : typeGift; const releasedByPeerId = gift?.type === 'starGiftUnique' && gift.releasedByPeerId; const releasedByPeer = releasedByPeerId ? selectPeer(global, releasedByPeerId) : undefined; return { fromPeer, targetPeer, releasedByPeer, currentUserId, starGiftMaxConvertPeriod: global.appConfig.starGiftMaxConvertPeriod, tonExplorerUrl: global.appConfig.tonExplorerUrl, hasAdminRights, currentUserEmojiStatus, collectibleEmojiStatuses, currentUser, recipientPeer, }; }, )(GiftInfoModal));