Unique gifts: Follow up (#5397)

This commit is contained in:
zubiden 2025-01-05 20:18:47 +01:00 committed by Alexander Zinchuk
parent 783a5c2c01
commit 683384074a
13 changed files with 246 additions and 47 deletions

View File

@ -14,6 +14,7 @@ import type {
ApiKeyboardButton, ApiKeyboardButton,
ApiMessage, ApiMessage,
ApiMessageActionStarGift, ApiMessageActionStarGift,
ApiMessageActionStarGiftUnique,
ApiMessageEntity, ApiMessageEntity,
ApiMessageForwardInfo, ApiMessageForwardInfo,
ApiMessageReportResult, ApiMessageReportResult,
@ -380,6 +381,24 @@ function buildApiMessageActionStarGift(action: GramJs.MessageActionStarGift) : A
}; };
} }
function buildApiMessageActionStarGiftUnique(
action: GramJs.MessageActionStarGiftUnique,
): ApiMessageActionStarGiftUnique {
const {
gift, canExportAt, refunded, saved, transferStars, transferred, upgrade,
} = action;
return {
gift: buildApiStarGift(gift),
canExportAt,
isRefunded: refunded,
isSaved: saved,
transferStars: transferStars?.toJSNumber(),
isTransferred: transferred,
isUpgrade: upgrade,
};
}
function buildAction( function buildAction(
action: GramJs.TypeMessageAction, action: GramJs.TypeMessageAction,
senderId: string | undefined, senderId: string | undefined,
@ -395,7 +414,7 @@ function buildAction(
let call: Partial<ApiGroupCall> | undefined; let call: Partial<ApiGroupCall> | undefined;
let amount: number | undefined; let amount: number | undefined;
let stars: number | undefined; let stars: number | undefined;
let starGift: ApiMessageActionStarGift | undefined; let starGift: ApiMessageActionStarGift | ApiMessageActionStarGiftUnique | undefined;
let currency: string | undefined; let currency: string | undefined;
let giftCryptoInfo: { let giftCryptoInfo: {
currency: string; currency: string;
@ -710,6 +729,7 @@ function buildAction(
transactionId = action.transactionId; transactionId = action.transactionId;
} else if (action instanceof GramJs.MessageActionStarGift && action.gift instanceof GramJs.StarGift) { } else if (action instanceof GramJs.MessageActionStarGift && action.gift instanceof GramJs.StarGift) {
type = 'starGift'; type = 'starGift';
starGift = buildApiMessageActionStarGift(action);
if (isOutgoing) { if (isOutgoing) {
text = 'ActionGiftOutbound'; text = 'ActionGiftOutbound';
translationValues.push('%gift_payment_amount%'); translationValues.push('%gift_payment_amount%');
@ -725,7 +745,15 @@ function buildAction(
amount = action.gift.stars.toJSNumber(); amount = action.gift.stars.toJSNumber();
currency = STARS_CURRENCY_CODE; currency = STARS_CURRENCY_CODE;
starGift = buildApiMessageActionStarGift(action); } else if (action instanceof GramJs.MessageActionStarGiftUnique && action.gift instanceof GramJs.StarGiftUnique) {
type = 'starGiftUnique';
text = isOutgoing ? 'Notification.StarsGift.UpgradeYou' : 'Notification.StarsGift.Upgrade';
starGift = buildApiMessageActionStarGiftUnique(action);
if (targetPeerId) {
targetUserIds.push(targetPeerId);
targetChatId = targetPeerId;
}
} else { } else {
text = 'ChatList.UnsupportedMessage'; text = 'ChatList.UnsupportedMessage';
} }

View File

@ -467,6 +467,16 @@ export interface ApiMessageActionStarGift {
starsToConvert?: number; starsToConvert?: number;
} }
export interface ApiMessageActionStarGiftUnique {
isUpgrade?: true;
isTransferred?: true;
isSaved?: true;
isRefunded?: true;
gift: ApiStarGift;
canExportAt?: number;
transferStars?: number;
}
export interface ApiAction { export interface ApiAction {
mediaType: 'action'; mediaType: 'action';
text: string; text: string;
@ -487,6 +497,7 @@ export interface ApiAction {
| 'giftCode' | 'giftCode'
| 'prizeStars' | 'prizeStars'
| 'starGift' | 'starGift'
| 'starGiftUnique'
| 'other'; | 'other';
photo?: ApiPhoto; photo?: ApiPhoto;
amount?: number; amount?: number;
@ -497,7 +508,7 @@ export interface ApiAction {
currency: string; currency: string;
amount: number; amount: number;
}; };
starGift?: ApiMessageActionStarGift; starGift?: ApiMessageActionStarGift | ApiMessageActionStarGiftUnique;
translationValues: string[]; translationValues: string[];
call?: Partial<ApiGroupCall>; call?: Partial<ApiGroupCall>;
phoneCall?: PhoneCallAction; phoneCall?: PhoneCallAction;

View File

@ -93,6 +93,10 @@ export function renderActionMessageText(
.replace('un2', '%action_origin%') .replace('un2', '%action_origin%')
.replace(/\*\*/g, ''); .replace(/\*\*/g, '');
} }
if (translationKey.startsWith('Notification.StarsGift.Upgrade')) {
unprocessed = unprocessed
.replace('%@', '%action_origin%');
}
if (translationKey === 'BoostingReceivedPrizeFrom') { if (translationKey === 'BoostingReceivedPrizeFrom') {
unprocessed = unprocessed unprocessed = unprocessed
.replace('**%s**', '%target_chat%') .replace('**%s**', '%target_chat%')

View File

@ -6,7 +6,9 @@
content: ''; content: '';
position: absolute; position: absolute;
inset: 0; inset: 0;
background: linear-gradient(var(--_bg-1), var(--_bg-2)), radial-gradient(circle, #ffffff32, #ffffff00); background-image:
radial-gradient(circle closest-side, #ffffff32, #ffffff00),
radial-gradient(closest-side, var(--_bg-1), var(--_bg-2));
} }
} }

View File

@ -13,14 +13,14 @@ import { preloadImage } from '../../../util/files';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useMedia from '../../../hooks/useMedia'; import useMedia from '../../../hooks/useMedia';
import useResizeObserver from '../../../hooks/useResizeObserver'; import useResizeObserver from '../../../hooks/useResizeObserver';
import { useSignalEffect } from '../../../hooks/useSignalEffect'; import useDevicePixelRatio from '../../../hooks/window/useDevicePixelRatio';
import styles from './RadialPatternBackground.module.scss'; import styles from './RadialPatternBackground.module.scss';
type OwnProps = { type OwnProps = {
backgroundColors: string[]; backgroundColors: string[];
patternColor: string; patternColor?: string;
patternIcon: ApiSticker; patternIcon?: ApiSticker;
className?: string; className?: string;
}; };
@ -28,32 +28,33 @@ const RINGS = 3;
const BASE_RING_ITEM_COUNT = 8; const BASE_RING_ITEM_COUNT = 8;
const RING_INCREMENT = 0.5; const RING_INCREMENT = 0.5;
const CENTER_EMPTINESS = 0.05; const CENTER_EMPTINESS = 0.05;
const MAX_RADIUS = 0.5; const MAX_RADIUS = 0.4;
const BASE_ICON_SIZE = 20; const BASE_ICON_SIZE = 20;
const MIN_SIZE = 200; const MIN_SIZE = 250;
const PATTERN_POSITIONS = (() => { const PATTERN_POSITIONS = (() => {
const coordinates: { x: number; y: number; alpha: number; sizeFactor: number }[] = []; const coordinates: { x: number; y: number; sizeFactor: number }[] = [];
for (let ring = 1; ring <= RINGS; ring++) { for (let ring = 1; ring <= RINGS; ring++) {
const ringItemCount = Math.floor(BASE_RING_ITEM_COUNT * (1 + (ring - 1) * RING_INCREMENT)); const ringItemCount = Math.floor(BASE_RING_ITEM_COUNT * (1 + (ring - 1) * RING_INCREMENT));
const ringProgress = ring / RINGS; const ringProgress = ring / RINGS;
const ringRadius = CENTER_EMPTINESS + (MAX_RADIUS - CENTER_EMPTINESS) * ringProgress; const ringRadius = CENTER_EMPTINESS + (MAX_RADIUS - CENTER_EMPTINESS) * ringProgress;
const angleShift = ring % 2 === 0 ? Math.PI / ringItemCount : 0;
for (let i = 0; i < ringItemCount; i++) { for (let i = 0; i < ringItemCount; i++) {
const angle = (i / ringItemCount) * Math.PI * 2; const angle = (i / ringItemCount) * Math.PI * 2 + angleShift;
// Slightly oval // Slightly oval
const xOffset = ringRadius * 1.71 * Math.cos(angle); const xOffset = ringRadius * 1.71 * Math.cos(angle);
const yOffset = ringRadius * Math.sin(angle); const yOffset = ringRadius * Math.sin(angle);
const x = 0.5 + xOffset; const x = 0.5 + xOffset;
const y = 0.5 + yOffset; const y = 0.5 + yOffset;
const alpha = 0.2 + Math.min((1 - ringProgress + (Math.random() / 2 - 0.5)), 0) * 0.8;
const sizeFactor = 1.4 - ringProgress * Math.random(); const sizeFactor = 1.4 - ringProgress * Math.random();
coordinates.push({ coordinates.push({
x, y, alpha, sizeFactor, x, y, sizeFactor,
}); });
} }
} }
@ -73,9 +74,11 @@ const RadialPatternBackground = ({
const [getContainerSize, setContainerSize] = useSignal({ width: 0, height: 0 }); const [getContainerSize, setContainerSize] = useSignal({ width: 0, height: 0 });
const dpr = useDevicePixelRatio();
const [emojiImage, setEmojiImage] = useState<HTMLImageElement | undefined>(); const [emojiImage, setEmojiImage] = useState<HTMLImageElement | undefined>();
const previewMediaHash = getStickerMediaHash(patternIcon, 'preview'); const previewMediaHash = patternIcon && getStickerMediaHash(patternIcon, 'preview');
const previewUrl = useMedia(previewMediaHash); const previewUrl = useMedia(previewMediaHash);
useEffect(() => { useEffect(() => {
@ -109,22 +112,34 @@ const RadialPatternBackground = ({
ctx.save(); ctx.save();
PATTERN_POSITIONS.forEach(({ PATTERN_POSITIONS.forEach(({
x, y, alpha, sizeFactor, x, y, sizeFactor,
}) => { }) => {
const centerShift = (width - Math.max(width, MIN_SIZE)) / 2; // Shift coords if canvas is smaller than `MIN_SIZE` const centerShift = (width - Math.max(width, MIN_SIZE * dpr)) / 2; // Shift coords if canvas is smaller than `MIN_SIZE`
const renderX = x * Math.max(width, MIN_SIZE) + centerShift; const renderX = x * Math.max(width, MIN_SIZE * dpr) + centerShift;
const renderY = y * Math.max(height, MIN_SIZE) + centerShift; const renderY = y * Math.max(height, MIN_SIZE * dpr) + centerShift;
const size = BASE_ICON_SIZE * sizeFactor * (centerShift ? 0.8 : 1); const size = BASE_ICON_SIZE * dpr * sizeFactor * (centerShift ? 0.8 : 1);
ctx.globalAlpha = alpha;
ctx.drawImage(emojiImage, renderX - size / 2, renderY - size / 2, size, size); ctx.drawImage(emojiImage, renderX - size / 2, renderY - size / 2, size, size);
}); });
ctx.restore(); ctx.restore();
if (patternColor) {
ctx.save();
ctx.fillStyle = patternColor;
ctx.globalCompositeOperation = 'source-atop';
ctx.fillRect(0, 0, width, height);
ctx.restore();
}
const radialGradient = ctx.createRadialGradient(width / 2, height / 2, 0, width / 2, height / 2, width / 2);
radialGradient.addColorStop(0, '#FFFFFF00');
radialGradient.addColorStop(1, '#FFFFFF');
// Alpha mask
ctx.save(); ctx.save();
ctx.fillStyle = patternColor; ctx.globalCompositeOperation = 'destination-out';
ctx.globalCompositeOperation = 'source-atop'; ctx.fillStyle = radialGradient;
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, width, height);
ctx.restore(); ctx.restore();
}); });
@ -133,7 +148,7 @@ const RadialPatternBackground = ({
draw(); draw();
}, [emojiImage]); }, [emojiImage]);
useSignalEffect(() => { useEffect(() => {
const { width, height } = getContainerSize(); const { width, height } = getContainerSize();
const canvas = canvasRef.current!; const canvas = canvasRef.current!;
if (!width || !height) { if (!width || !height) {
@ -141,14 +156,13 @@ const RadialPatternBackground = ({
} }
const maxSide = Math.max(width, height); const maxSide = Math.max(width, height);
const dpr = window.devicePixelRatio;
requestMutation(() => { requestMutation(() => {
canvas.width = maxSide * dpr; canvas.width = maxSide * dpr;
canvas.height = maxSide * dpr; canvas.height = maxSide * dpr;
draw(); draw();
}); });
}, [getContainerSize]); }, [getContainerSize, dpr]);
return ( return (
<div <div

View File

@ -5,7 +5,7 @@ import React, {
import { getActions, getGlobal, withGlobal } from '../../global'; import { getActions, getGlobal, withGlobal } from '../../global';
import type { import type {
ApiChat, ApiMessage, ApiSticker, ApiTopic, ApiUser, ApiChat, ApiMessage, ApiMessageActionStarGift, ApiSticker, ApiTopic, ApiUser,
} from '../../api/types'; } from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import type { FocusDirection, MessageListType, ThreadId } from '../../types'; import type { FocusDirection, MessageListType, ThreadId } from '../../types';
@ -28,6 +28,7 @@ import {
} from '../../global/selectors'; } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { formatInteger, formatIntegerCompact } from '../../util/textFormat'; import { formatInteger, formatIntegerCompact } from '../../util/textFormat';
import { getGiftAttributes, getStickerFromGift } from '../common/helpers/gifts';
import { renderActionMessageText } from '../common/helpers/renderActionMessageText'; import { renderActionMessageText } from '../common/helpers/renderActionMessageText';
import renderText from '../common/helpers/renderText'; import renderText from '../common/helpers/renderText';
import { renderTextWithEntities } from '../common/helpers/renderTextWithEntities'; import { renderTextWithEntities } from '../common/helpers/renderTextWithEntities';
@ -45,6 +46,7 @@ import useFocusMessage from './message/hooks/useFocusMessage';
import AnimatedIconFromSticker from '../common/AnimatedIconFromSticker'; import AnimatedIconFromSticker from '../common/AnimatedIconFromSticker';
import Avatar from '../common/Avatar'; import Avatar from '../common/Avatar';
import GiftRibbon from '../common/gift/GiftRibbon'; import GiftRibbon from '../common/gift/GiftRibbon';
import RadialPatternBackground from '../common/profile/RadialPatternBackground';
import Sparkles from '../common/Sparkles'; import Sparkles from '../common/Sparkles';
import ActionMessageSuggestedAvatar from './ActionMessageSuggestedAvatar'; import ActionMessageSuggestedAvatar from './ActionMessageSuggestedAvatar';
import ActionMessageUpdatedAvatar from './ActionMessageUpdatedAvatar'; import ActionMessageUpdatedAvatar from './ActionMessageUpdatedAvatar';
@ -163,6 +165,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
const isJoinedMessage = isJoinedChannelMessage(message); const isJoinedMessage = isJoinedChannelMessage(message);
const isStarsGift = message.content.action?.type === 'giftStars'; const isStarsGift = message.content.action?.type === 'giftStars';
const isStarGift = message.content.action?.type === 'starGift'; const isStarGift = message.content.action?.type === 'starGift';
const isStarGiftUnique = message.content.action?.type === 'starGiftUnique';
const isPrizeStars = message.content.action?.type === 'prizeStars'; const isPrizeStars = message.content.action?.type === 'prizeStars';
const withServiceReactions = Boolean(message.areReactionsPossible && message?.reactions); const withServiceReactions = Boolean(message.areReactionsPossible && message?.reactions);
@ -318,7 +321,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
const giftMessage = message.content.action?.message; const giftMessage = message.content.action?.message;
return ( return (
<span <span
className="action-message-gift action-message-gift-code" className="action-message-gift action-message-centered"
tabIndex={0} tabIndex={0}
role="button" role="button"
onClick={handleGiftCodeClick} onClick={handleGiftCodeClick}
@ -366,7 +369,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
function renderStarsGift() { function renderStarsGift() {
return ( return (
<span <span
className="action-message-gift action-message-gift-code" className="action-message-gift action-message-centered"
tabIndex={0} tabIndex={0}
role="button" role="button"
onClick={handleStarGiftClick} onClick={handleStarGiftClick}
@ -421,9 +424,9 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
} }
function renderStarGiftUserDescription() { function renderStarGiftUserDescription() {
const starGift = message.content.action?.starGift; const starGift = message.content.action?.starGift as ApiMessageActionStarGift;
const targetUser = targetUsers && targetUsers[0]?.firstName; const targetUser = targetUsers && targetUsers[0]?.firstName;
const starGiftMessage = message.content.action?.starGift?.message; const starGiftMessage = starGift?.message;
if (!starGift) return undefined; if (!starGift) return undefined;
if (starGiftMessage) { if (starGiftMessage) {
@ -468,11 +471,11 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
function renderStarGift() { function renderStarGift() {
const starGift = message.content.action?.starGift; const starGift = message.content.action?.starGift;
if (!starGift || starGift.gift.type === 'starGiftUnique') return undefined; if (!starGift || starGift.gift.type !== 'starGift') return undefined;
return ( return (
<span <span
className="action-message-gift action-message-gift-code action-message-star-gift" className="action-message-gift action-message-centered"
tabIndex={0} tabIndex={0}
role="button" role="button"
onClick={handleStarGiftClick} onClick={handleStarGiftClick}
@ -507,12 +510,83 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
); );
} }
function renderStarGiftUnique() {
const starGift = message.content.action?.starGift;
if (!starGift || starGift.gift.type !== 'starGiftUnique') return undefined;
const sticker = getStickerFromGift(starGift.gift)!;
const attributes = getGiftAttributes(starGift.gift);
const { backdrop, pattern, model } = attributes || {};
if (!backdrop || !pattern || !model) return undefined;
const backgroundColors = [backdrop.centerColor, backdrop.edgeColor];
return (
<span
className="action-message-gift action-message-centered action-message-unique"
tabIndex={0}
role="button"
onClick={handleStarGiftClick}
>
<div className="action-message-unique-background-wrapper">
<RadialPatternBackground
className="action-message-unique-background"
backgroundColors={backgroundColors}
patternColor={backdrop.patternColor}
patternIcon={pattern.sticker}
/>
</div>
<AnimatedIconFromSticker
sticker={sticker}
play={canPlayAnimatedEmojis}
noLoop
nonInteractive
size={STAR_GIFT_STICKER_SIZE}
/>
{renderStarGiftUserCaption()}
<div className="action-message-unique-title" style={`color: ${backdrop.textColor}`}>
{starGift.gift.title} #{starGift.gift.number}
</div>
<div className="action-message-unique-properties" style={`color: ${backdrop.textColor}`}>
<div className="action-message-unique-property">
{oldLang('Gift2AttributeModel')}
</div>
<div className="action-message-unique-value">
{model.name}
</div>
<div className="action-message-unique-property">
{oldLang('Gift2AttributeBackdrop')}
</div>
<div className="action-message-unique-value">
{backdrop.name}
</div>
<div className="action-message-unique-property">
{oldLang('Gift2AttributeSymbol')}
</div>
<div className="action-message-unique-value">
{pattern.name}
</div>
</div>
<div className="action-message-button">
<Sparkles preset="button" />
{oldLang('Gift2UniqueView')}
</div>
<GiftRibbon
color={backdrop.patternColor || 'blue'}
text={oldLang('ActionStarGift')}
/>
</span>
);
}
function renderPrizeStars() { function renderPrizeStars() {
const isUnclaimed = message.content.action?.isUnclaimed; const isUnclaimed = message.content.action?.isUnclaimed;
return ( return (
<span <span
className="action-message-gift action-message-gift-code" className="action-message-gift action-message-centered"
tabIndex={0} tabIndex={0}
role="button" role="button"
onClick={handlePrizeStarsClick} onClick={handlePrizeStarsClick}
@ -577,6 +651,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
{isGiftCode && renderGiftCode()} {isGiftCode && renderGiftCode()}
{isStarsGift && renderStarsGift()} {isStarsGift && renderStarsGift()}
{isStarGift && renderStarGift()} {isStarGift && renderStarGift()}
{isStarGiftUnique && renderStarGiftUnique()}
{isPrizeStars && renderPrizeStars()} {isPrizeStars && renderPrizeStars()}
{isSuggestedAvatar && ( {isSuggestedAvatar && (
<ActionMessageSuggestedAvatar message={message} renderContent={renderContent} /> <ActionMessageSuggestedAvatar message={message} renderContent={renderContent} />

View File

@ -286,12 +286,37 @@
font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold);
} }
.action-message-gift-code { .action-message-centered {
margin-inline: auto; margin-inline: auto;
} }
.action-message-unique {
&::before {
content: "";
position: absolute;
inset: -0.25rem;
background: var(--pattern-color);
border-radius: calc(var(--border-radius-messages) + 0.25rem);
z-index: -1;
}
}
.action-message-unique-background-wrapper {
position: absolute;
inset: 0;
overflow: hidden;
border-radius: inherit;
}
.action-message-unique-background {
position: absolute;
inset: 0;
top: -6rem;
}
.action-message-user-caption, .action-message-user-caption,
.action-message-stars-balance { .action-message-stars-balance {
position: relative;
margin-top: 0.5rem; margin-top: 0.5rem;
display: flex; display: flex;
gap: 0.25rem; gap: 0.25rem;
@ -305,6 +330,31 @@
font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold);
} }
.action-message-unique-title {
position: relative;
font-size: 0.875rem;
}
.action-message-unique-properties {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.375rem;
font-size: 0.875rem;
margin-top: 0.5rem;
position: relative;
}
.action-message-unique-value {
color: white;
justify-self: flex-start;
}
.action-message-unique-property {
justify-self: flex-end;
font-weight: var(--font-weight-normal);
}
.action-message-user-avatar { .action-message-user-avatar {
margin-left: 0.25rem; margin-left: 0.25rem;
} }
@ -316,6 +366,7 @@
} }
.action-message-gift-subtitle { .action-message-gift-subtitle {
position: relative;
font-weight: normal; font-weight: normal;
text-wrap: balance; text-wrap: balance;
font-size: 0.8125rem; font-size: 0.8125rem;

View File

@ -3,7 +3,8 @@
.content { .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5rem; gap: 1rem;
padding-inline: 1rem !important;
max-height: min(92vh, 40rem) !important; max-height: min(92vh, 40rem) !important;
overflow-x: hidden; overflow-x: hidden;
} }

View File

@ -28,6 +28,7 @@ type OwnProps = {
footer?: TeactNode; footer?: TeactNode;
buttonText?: string; buttonText?: string;
className?: string; className?: string;
hasBackdrop?: boolean;
onClose: NoneToVoidFunction; onClose: NoneToVoidFunction;
onButtonClick?: NoneToVoidFunction; onButtonClick?: NoneToVoidFunction;
}; };
@ -41,6 +42,7 @@ const TableInfoModal = ({
footer, footer,
buttonText, buttonText,
className, className,
hasBackdrop,
onClose, onClose,
onButtonClick, onButtonClick,
}: OwnProps) => { }: OwnProps) => {
@ -55,6 +57,7 @@ const TableInfoModal = ({
isOpen={isOpen} isOpen={isOpen}
hasCloseButton={Boolean(title)} hasCloseButton={Boolean(title)}
hasAbsoluteCloseButton={!title} hasAbsoluteCloseButton={!title}
absoluteCloseButtonColor={hasBackdrop ? 'translucent-white' : undefined}
isSlim isSlim
title={title} title={title}
className={className} className={className}

View File

@ -36,7 +36,6 @@
font-size: 0.875rem; font-size: 0.875rem;
color: var(--color-text-secondary); color: var(--color-text-secondary);
text-align: center; text-align: center;
margin-top: 0.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
@ -53,8 +52,8 @@
.radialPattern { .radialPattern {
position: absolute; position: absolute;
top: -3rem; top: -3rem;
left: -1.5rem; left: -1rem;
right: -1.5rem; right: -1rem;
height: 16.5rem; height: 16.5rem;
z-index: -1; z-index: -1;
@ -77,4 +76,8 @@
font-size: 1.25rem; font-size: 1.25rem;
color: white; color: white;
} }
.description {
font-size: 0.875rem;
}
} }

View File

@ -138,14 +138,12 @@ const GiftInfoModal = ({
const isVisibleForMe = isNameHidden && targetUser; const isVisibleForMe = isNameHidden && targetUser;
const description = (() => { const description = (() => {
if (!userGift) {
return lang('GiftInfoSoldOutDescription');
}
if (gift.type === 'starGiftUnique') { if (gift.type === 'starGiftUnique') {
return lang('GiftInfoCollectible', { return lang('GiftInfoCollectible', {
number: gift.number, number: gift.number,
}); });
} }
if (!userGift) return lang('GiftInfoSoldOutDescription');
if (!canUpdate && !isSender) return undefined; if (!canUpdate && !isSender) return undefined;
if (!starsToConvert || canConvertDifference < 0) return undefined; if (!starsToConvert || canConvertDifference < 0) return undefined;
if (isConverted) { if (isConverted) {
@ -186,8 +184,8 @@ const GiftInfoModal = ({
})(); })();
function getTitle() { function getTitle() {
if (!userGift) return lang('GiftInfoSoldOutTitle');
if (gift?.type === 'starGiftUnique') return gift.title; if (gift?.type === 'starGiftUnique') return gift.title;
if (!userGift) return lang('GiftInfoSoldOutTitle');
return canUpdate ? lang('GiftInfoReceived') : lang('GiftInfoTitle'); return canUpdate ? lang('GiftInfoReceived') : lang('GiftInfoTitle');
} }
@ -219,7 +217,7 @@ const GiftInfoModal = ({
</p> </p>
)} )}
{description && ( {description && (
<p className={buildClassName(styles.description, !userGift && styles.soldOut)}> <p className={buildClassName(styles.description, !userGift && gift?.type === 'starGift' && styles.soldOut)}>
{description} {description}
</p> </p>
)} )}
@ -457,6 +455,7 @@ const GiftInfoModal = ({
<TableInfoModal <TableInfoModal
isOpen={isOpen} isOpen={isOpen}
header={modalData?.header} header={modalData?.header}
hasBackdrop={Boolean(radialPatternBackdrop)}
tableData={modalData?.tableData} tableData={modalData?.tableData}
footer={modalData?.footer} footer={modalData?.footer}
className={styles.modal} className={styles.modal}

View File

@ -16,7 +16,7 @@ import useOldLang from '../../hooks/useOldLang';
import useShowTransition from '../../hooks/useShowTransition'; import useShowTransition from '../../hooks/useShowTransition';
import Icon from '../common/icons/Icon'; import Icon from '../common/icons/Icon';
import Button from './Button'; import Button, { type OwnProps as ButtonProps } from './Button';
import Portal from './Portal'; import Portal from './Portal';
import './Modal.scss'; import './Modal.scss';
@ -33,6 +33,7 @@ export type OwnProps = {
isSlim?: boolean; isSlim?: boolean;
hasCloseButton?: boolean; hasCloseButton?: boolean;
hasAbsoluteCloseButton?: boolean; hasAbsoluteCloseButton?: boolean;
absoluteCloseButtonColor?: ButtonProps['color'];
noBackdrop?: boolean; noBackdrop?: boolean;
noBackdropClose?: boolean; noBackdropClose?: boolean;
children: React.ReactNode; children: React.ReactNode;
@ -57,6 +58,7 @@ const Modal: FC<OwnProps> = ({
header, header,
hasCloseButton, hasCloseButton,
hasAbsoluteCloseButton, hasAbsoluteCloseButton,
absoluteCloseButtonColor = 'translucent',
noBackdrop, noBackdrop,
noBackdropClose, noBackdropClose,
children, children,
@ -137,7 +139,7 @@ const Modal: FC<OwnProps> = ({
<Button <Button
className={buildClassName(hasAbsoluteCloseButton && 'modal-absolute-close-button')} className={buildClassName(hasAbsoluteCloseButton && 'modal-absolute-close-button')}
round round
color="translucent" color={absoluteCloseButtonColor}
size="smaller" size="smaller"
ariaLabel={lang('Close')} ariaLabel={lang('Close')}
onClick={onClose} onClick={onClose}

View File

@ -1,4 +1,4 @@
import type { ApiUserStarGift } from '../../../api/types'; import type { ApiMessageActionStarGift, ApiUserStarGift } from '../../../api/types';
import type { ActionReturnType } from '../../types'; import type { ActionReturnType } from '../../types';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
@ -242,8 +242,14 @@ addActionHandler('openGiftInfoModalFromMessage', (global, actions, payload): Act
if (!message || !message.content.action) return; if (!message || !message.content.action) return;
const action = message.content.action; const action = message.content.action;
if (action.type === 'starGiftUnique') {
actions.openGiftInfoModal({ gift: action.starGift?.gift!, tabId });
return;
}
if (action.type !== 'starGift') return; if (action.type !== 'starGift') return;
const starGift = action.starGift!;
const starGift = action.starGift! as ApiMessageActionStarGift;
const giftReceiverId = message.isOutgoing ? message.chatId : global.currentUserId!; const giftReceiverId = message.isOutgoing ? message.chatId : global.currentUserId!;