Unique Gift: Show preview in transactions (#5514)

This commit is contained in:
zubiden 2025-01-21 18:22:11 +01:00 committed by Alexander Zinchuk
parent b8e9dae74f
commit ea2c3111eb
9 changed files with 116 additions and 98 deletions

View File

@ -5,7 +5,7 @@
flex-direction: column;
gap: 1rem;
padding-inline: 1rem !important;
max-height: min(92vh, 40rem) !important;
max-height: min(92vh, 45rem) !important;
overflow-x: hidden;
}

View File

@ -4,6 +4,7 @@
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: var(--_height);
margin-bottom: 0.5rem;
padding-bottom: 1rem;

View File

@ -8,18 +8,9 @@
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
position: relative;
}
.amount {
display: flex;
gap: 0.25rem;
font-size: 1rem;
font-weight: var(--font-weight-medium);
line-height: 1.325;
}
.title, .description, .amount {
.title, .description {
margin-bottom: 0;
}
@ -55,16 +46,6 @@
line-height: 1;
}
.radialPattern {
position: absolute;
top: -3rem;
left: -1rem;
right: -1rem;
height: 16.5rem;
z-index: -1;
}
.uniqueAttribute {
display: flex;
align-items: center;
@ -72,20 +53,7 @@
}
.uniqueGift {
gap: 0;
.giftSticker {
margin-block: 1rem;
}
.title {
font-size: 1.25rem;
color: white;
}
.description {
font-size: 0.875rem;
}
margin-bottom: 0;
}
.starAmountIcon {

View File

@ -10,7 +10,6 @@ import type { TabState } from '../../../../global/types';
import { getUserFullName } from '../../../../global/helpers';
import { selectUser } from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName';
import buildStyle from '../../../../util/buildStyle';
import { formatDateTimeToString } from '../../../../util/dates/dateFormat';
import { formatStarsAsIcon, formatStarsAsText } from '../../../../util/localization/format';
import { CUSTOM_PEER_HIDDEN } from '../../../../util/objects/customPeer';
@ -28,12 +27,11 @@ import useOldLang from '../../../../hooks/useOldLang';
import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker';
import Avatar from '../../../common/Avatar';
import BadgeButton from '../../../common/BadgeButton';
import StarIcon from '../../../common/icons/StarIcon';
import RadialPatternBackground from '../../../common/profile/RadialPatternBackground';
import Button from '../../../ui/Button';
import ConfirmDialog from '../../../ui/ConfirmDialog';
import Link from '../../../ui/Link';
import TableInfoModal, { type TableData } from '../../common/TableInfoModal';
import UniqueGiftHeader from '../UniqueGiftHeader';
import styles from './GiftInfoModal.module.scss';
@ -117,26 +115,6 @@ const GiftInfoModal = ({
return gift && getGiftAttributes(gift);
}, [gift]);
const radialPatternBackdrop = useMemo(() => {
const { backdrop, pattern } = giftAttributes || {};
if (!backdrop || !pattern || !isOpen) {
return undefined;
}
const backdropColors = [backdrop.centerColor, backdrop.edgeColor];
const patternColor = backdrop.patternColor;
return (
<RadialPatternBackground
className={styles.radialPattern}
backgroundColors={backdropColors}
patternColor={patternColor}
patternIcon={pattern.sticker}
/>
);
}, [giftAttributes, isOpen]);
const renderFooterButton = useLastCallback(() => {
if (canFocusUpgrade) {
return (
@ -173,11 +151,6 @@ const GiftInfoModal = ({
const isVisibleForMe = isNameHidden && targetUser;
const description = (() => {
if (gift.type === 'starGiftUnique') {
return lang('GiftInfoCollectible', {
number: gift.number,
});
}
if (!userGift) return lang('GiftInfoSoldOutDescription');
if (userGift.upgradeMsgId) return lang('GiftInfoDescriptionUpgraded');
if (userGift.canUpgrade && userGift.alreadyPaidUpgradeStars) {
@ -240,32 +213,30 @@ const GiftInfoModal = ({
return canUpdate ? lang('GiftInfoReceived') : lang('GiftInfoTitle');
}
const descriptionColor = giftAttributes?.backdrop?.textColor;
const isUniqueGift = gift.type === 'starGiftUnique';
const header = (
<div
className={buildClassName(styles.header, radialPatternBackdrop && styles.uniqueGift)}
style={buildStyle(descriptionColor && `--_color-description: ${descriptionColor}`)}
>
{radialPatternBackdrop}
const uniqueGiftHeader = isUniqueGift && (
<div className={buildClassName(styles.header, styles.uniqueGift)}>
<UniqueGiftHeader
backdropAttribute={giftAttributes!.backdrop!}
patternAttribute={giftAttributes!.pattern!}
modelAttribute={giftAttributes!.model!}
title={gift.title}
subtitle={lang('GiftInfoCollectible', { number: gift.number })}
/>
</div>
);
const regularHeader = (
<div className={styles.header}>
<AnimatedIconFromSticker
className={styles.giftSticker}
sticker={giftSticker}
noLoop={false}
nonInteractive
size={STICKER_SIZE}
/>
<h1 className={styles.title}>
{getTitle()}
</h1>
{gift.type === 'starGift' && (
<p className={styles.amount}>
<span className={styles.amount}>
{formatInteger(gift.stars)}
</span>
<StarIcon type="gold" size="middle" />
</p>
)}
{description && (
<p className={buildClassName(styles.description, !userGift && gift?.type === 'starGift' && styles.soldOut)}>
{description}
@ -487,13 +458,13 @@ const GiftInfoModal = ({
);
return {
header,
header: isUniqueGift ? uniqueGiftHeader : regularHeader,
tableData,
footer,
};
}, [
typeGift, userGift, targetUser, giftSticker, lang, canUpdate, canConvertDifference, isSender, oldLang, gift,
radialPatternBackdrop, giftAttributes, renderFooterButton,
giftAttributes, renderFooterButton,
]);
return (
@ -501,7 +472,7 @@ const GiftInfoModal = ({
<TableInfoModal
isOpen={isOpen}
header={modalData?.header}
hasBackdrop={Boolean(radialPatternBackdrop)}
hasBackdrop={gift?.type === 'starGiftUnique'}
tableData={modalData?.tableData}
footer={modalData?.footer}
className={styles.modal}

View File

@ -10,6 +10,7 @@ import { getPeerTitle } from '../../../../global/helpers';
import { selectPeer } from '../../../../global/selectors';
import { formatDateToString } from '../../../../util/dates/dateFormat';
import { formatInteger } from '../../../../util/textFormat';
import renderText from '../../../common/helpers/renderText';
import useSelector from '../../../../hooks/data/useSelector';
import useLastCallback from '../../../../hooks/useLastCallback';
@ -46,6 +47,7 @@ const StarsSubscriptionItem = ({ subscription }: OwnProps) => {
if (!peer) {
return undefined;
}
const hasExpired = until < Date.now() / 1000;
const formattedDate = formatDateToString(until * 1000, lang.code, true, 'long');
@ -56,11 +58,11 @@ const StarsSubscriptionItem = ({ subscription }: OwnProps) => {
<StarIcon className={styles.subscriptionStar} type="gold" size="small" />
</div>
<div className={styles.info}>
<h3 className={styles.title}>{getPeerTitle(lang, peer)}</h3>
<h3 className={styles.title}>{renderText(getPeerTitle(lang, peer) || '')}</h3>
{title && (
<p className={styles.subtitle}>
{photo && <Avatar webPhoto={photo} size="micro" />}
{title}
{renderText(title)}
</p>
)}
<p className={styles.description}>

View File

@ -68,3 +68,17 @@
@include mixins.filter-outline(1px, var(--color-background));
}
.uniqueGiftBackground {
position: absolute;
top: 0;
left: 0;
right: 0;
aspect-ratio: 1 / 1;
border-radius: 0.25rem;
}
.uniqueGift {
margin-inline: 0.25rem;
margin-top: 0.25rem;
}

View File

@ -14,6 +14,7 @@ import { selectPeer } from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName';
import { formatDateTimeToString } from '../../../../util/dates/dateFormat';
import { CUSTOM_PEER_PREMIUM } from '../../../../util/objects/customPeer';
import { getGiftAttributes, getStickerFromGift } from '../../../common/helpers/gifts';
import renderText from '../../../common/helpers/renderText';
import { getTransactionTitle, isNegativeStarsAmount } from '../helpers/transaction';
@ -22,8 +23,10 @@ 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 StarIcon from '../../../common/icons/StarIcon';
import RadialPatternBackground from '../../../common/profile/RadialPatternBackground';
import PaidMediaThumb from './PaidMediaThumb';
import styles from './StarsTransactionItem.module.scss';
@ -33,6 +36,8 @@ type OwnProps = {
className?: string;
};
const UNIQUE_GIFT_STICKER_SIZE = 36;
function selectOptionalPeer(peerId?: string) {
return (global: GlobalState) => (
peerId ? selectPeer(global, peerId) : undefined
@ -54,6 +59,8 @@ const StarsTransactionItem = ({ transaction, className }: OwnProps) => {
const peerId = transactionPeer.type === 'peer' ? transactionPeer.id : undefined;
const peer = useSelector(selectOptionalPeer(peerId));
const uniqueGift = transaction.starGift?.type === 'starGiftUnique' ? transaction.starGift : undefined;
const uniqueGiftSticker = uniqueGift && getStickerFromGift(uniqueGift);
const data = useMemo(() => {
let title = getTransactionTitle(oldLang, transaction);
@ -99,6 +106,41 @@ const StarsTransactionItem = ({ transaction, className }: OwnProps) => {
};
}, [oldLang, peer, transaction]);
const previewContent = useMemo(() => {
if (uniqueGiftSticker) {
const { backdrop } = getGiftAttributes(uniqueGift)!;
const backgroundColors = [backdrop!.centerColor, backdrop!.edgeColor];
return (
<>
<RadialPatternBackground
className={styles.uniqueGiftBackground}
backgroundColors={backgroundColors}
/>
<AnimatedIconFromSticker
className={styles.uniqueGift}
sticker={uniqueGiftSticker}
size={UNIQUE_GIFT_STICKER_SIZE}
play={false}
/>
</>
);
}
if (extendedMedia) {
return <PaidMediaThumb media={extendedMedia} isTransactionPreview />;
}
return (
<>
<Avatar size="medium" webPhoto={photo} peer={data.avatarPeer} />
{Boolean(subscriptionPeriod) && (
<StarIcon className={styles.subscriptionStar} type="gold" size="small" />
)}
</>
);
}, [extendedMedia, photo, uniqueGiftSticker, subscriptionPeriod, data.avatarPeer, uniqueGift]);
const handleClick = useLastCallback(() => {
openStarsTransactionModal({ transaction });
});
@ -106,11 +148,7 @@ const StarsTransactionItem = ({ transaction, className }: OwnProps) => {
return (
<div className={buildClassName(styles.root, className)} onClick={handleClick}>
<div className={styles.preview}>
{extendedMedia ? <PaidMediaThumb media={extendedMedia} isTransactionPreview />
: <Avatar size="medium" webPhoto={photo} peer={data.avatarPeer} />}
{Boolean(subscriptionPeriod) && (
<StarIcon className={styles.subscriptionStar} type="gold" size="small" />
)}
{previewContent}
</div>
<div className={styles.info}>
<h3 className={styles.title}>{data.title}</h3>

View File

@ -2,6 +2,10 @@
.modal {
z-index: calc(var(--z-modal-low-priority) + 1);
:global(.modal-dialog) {
overflow: hidden;
}
}
.positive {
@ -18,7 +22,10 @@
align-items: center;
gap: 0.25rem;
margin-block: 1rem;
position: relative;
}
.uniqueGift {
margin-block: 0;
}
.amount {

View File

@ -19,7 +19,7 @@ import {
import buildClassName from '../../../../util/buildClassName';
import { copyTextToClipboard } from '../../../../util/clipboard';
import { formatDateTimeToString } from '../../../../util/dates/dateFormat';
import { getStickerFromGift } from '../../../common/helpers/gifts';
import { getGiftAttributes, getStickerFromGift } from '../../../common/helpers/gifts';
import { getTransactionTitle, isNegativeStarsAmount } from '../helpers/transaction';
import useLang from '../../../../hooks/useLang';
@ -33,6 +33,7 @@ import Icon from '../../../common/icons/Icon';
import StarIcon from '../../../common/icons/StarIcon';
import SafeLink from '../../../common/SafeLink';
import TableInfoModal, { type TableData } from '../../common/TableInfoModal';
import UniqueGiftHeader from '../../gift/UniqueGiftHeader';
import PaidMediaThumb from './PaidMediaThumb';
import styles from './StarsTransactionModal.module.scss';
@ -58,8 +59,6 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
const oldLang = useOldLang();
const { transaction } = modal || {};
const sticker = transaction?.starGift ? getStickerFromGift(transaction.starGift) : topSticker;
const handleOpenMedia = useLastCallback(() => {
const media = transaction?.extendedMedia;
if (!media) return;
@ -79,6 +78,12 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
giveawayPostId, photo, stars, isGiftUpgrade, starGift,
} = transaction;
const gift = transaction?.starGift;
const isUniqueGift = gift?.type === 'starGiftUnique';
const sticker = transaction?.starGift ? getStickerFromGift(transaction.starGift) : topSticker;
const giftAttributes = isUniqueGift ? getGiftAttributes(gift) : undefined;
const customPeer = (transaction.peer && transaction.peer.type !== 'peer'
&& buildStarsTransactionCustomPeer(transaction.peer)) || undefined;
@ -108,7 +113,19 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
const shouldDisplayAvatar = !media && !sticker;
const avatarPeer = !photo ? (peer || customPeer) : undefined;
const header = (
const uniqueGiftHeader = isUniqueGift && (
<div className={buildClassName(styles.header, styles.uniqueGift)}>
<UniqueGiftHeader
backdropAttribute={giftAttributes!.backdrop!}
patternAttribute={giftAttributes!.pattern!}
modelAttribute={giftAttributes!.model!}
title={gift.title}
subtitle={lang('GiftInfoCollectible', { number: gift.number })}
/>
</div>
);
const regularHeader = (
<div className={styles.header}>
{media && (
<PaidMediaThumb
@ -123,7 +140,6 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
sticker={sticker}
play={canPlayAnimatedEmojis}
noLoop
nonInteractive
/>
)}
{shouldDisplayAvatar && (
@ -232,11 +248,11 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
);
return {
header,
header: isUniqueGift ? uniqueGiftHeader : regularHeader,
tableData,
footer,
};
}, [transaction, oldLang, lang, peer, sticker, canPlayAnimatedEmojis]);
}, [transaction, oldLang, lang, peer, canPlayAnimatedEmojis, topSticker]);
const prevModalData = usePrevious(starModalData);
const renderingModalData = prevModalData || starModalData;
@ -245,6 +261,7 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
<TableInfoModal
isOpen={Boolean(transaction)}
className={styles.modal}
hasBackdrop={transaction?.starGift?.type === 'starGiftUnique'}
header={renderingModalData?.header}
tableData={renderingModalData?.tableData}
footer={renderingModalData?.footer}