From 683384074a23f2754f5e97fdaf1887564df181b8 Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Sun, 5 Jan 2025 20:18:47 +0100 Subject: [PATCH] Unique gifts: Follow up (#5397) --- src/api/gramjs/apiBuilders/messages.ts | 32 ++++++- src/api/types/messages.ts | 13 ++- .../helpers/renderActionMessageText.tsx | 4 + .../RadialPatternBackground.module.scss | 4 +- .../profile/RadialPatternBackground.tsx | 56 +++++++----- src/components/middle/ActionMessage.tsx | 91 +++++++++++++++++-- src/components/middle/MessageList.scss | 53 ++++++++++- .../modals/common/TableInfoModal.module.scss | 3 +- .../modals/common/TableInfoModal.tsx | 3 + .../gift/info/GiftInfoModal.module.scss | 9 +- .../modals/gift/info/GiftInfoModal.tsx | 9 +- src/components/ui/Modal.tsx | 6 +- src/global/actions/ui/stars.ts | 10 +- 13 files changed, 246 insertions(+), 47 deletions(-) diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index a3dbfa7cf..06a922ae3 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -14,6 +14,7 @@ import type { ApiKeyboardButton, ApiMessage, ApiMessageActionStarGift, + ApiMessageActionStarGiftUnique, ApiMessageEntity, ApiMessageForwardInfo, 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( action: GramJs.TypeMessageAction, senderId: string | undefined, @@ -395,7 +414,7 @@ function buildAction( let call: Partial | undefined; let amount: number | undefined; let stars: number | undefined; - let starGift: ApiMessageActionStarGift | undefined; + let starGift: ApiMessageActionStarGift | ApiMessageActionStarGiftUnique | undefined; let currency: string | undefined; let giftCryptoInfo: { currency: string; @@ -710,6 +729,7 @@ function buildAction( transactionId = action.transactionId; } else if (action instanceof GramJs.MessageActionStarGift && action.gift instanceof GramJs.StarGift) { type = 'starGift'; + starGift = buildApiMessageActionStarGift(action); if (isOutgoing) { text = 'ActionGiftOutbound'; translationValues.push('%gift_payment_amount%'); @@ -725,7 +745,15 @@ function buildAction( amount = action.gift.stars.toJSNumber(); 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 { text = 'ChatList.UnsupportedMessage'; } diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 0ac2f7c51..3126213d3 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -467,6 +467,16 @@ export interface ApiMessageActionStarGift { starsToConvert?: number; } +export interface ApiMessageActionStarGiftUnique { + isUpgrade?: true; + isTransferred?: true; + isSaved?: true; + isRefunded?: true; + gift: ApiStarGift; + canExportAt?: number; + transferStars?: number; +} + export interface ApiAction { mediaType: 'action'; text: string; @@ -487,6 +497,7 @@ export interface ApiAction { | 'giftCode' | 'prizeStars' | 'starGift' + | 'starGiftUnique' | 'other'; photo?: ApiPhoto; amount?: number; @@ -497,7 +508,7 @@ export interface ApiAction { currency: string; amount: number; }; - starGift?: ApiMessageActionStarGift; + starGift?: ApiMessageActionStarGift | ApiMessageActionStarGiftUnique; translationValues: string[]; call?: Partial; phoneCall?: PhoneCallAction; diff --git a/src/components/common/helpers/renderActionMessageText.tsx b/src/components/common/helpers/renderActionMessageText.tsx index dfa290bcd..36f8cdaa7 100644 --- a/src/components/common/helpers/renderActionMessageText.tsx +++ b/src/components/common/helpers/renderActionMessageText.tsx @@ -93,6 +93,10 @@ export function renderActionMessageText( .replace('un2', '%action_origin%') .replace(/\*\*/g, ''); } + if (translationKey.startsWith('Notification.StarsGift.Upgrade')) { + unprocessed = unprocessed + .replace('%@', '%action_origin%'); + } if (translationKey === 'BoostingReceivedPrizeFrom') { unprocessed = unprocessed .replace('**%s**', '%target_chat%') diff --git a/src/components/common/profile/RadialPatternBackground.module.scss b/src/components/common/profile/RadialPatternBackground.module.scss index d6e252be0..623b2a761 100644 --- a/src/components/common/profile/RadialPatternBackground.module.scss +++ b/src/components/common/profile/RadialPatternBackground.module.scss @@ -6,7 +6,9 @@ content: ''; position: absolute; 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)); } } diff --git a/src/components/common/profile/RadialPatternBackground.tsx b/src/components/common/profile/RadialPatternBackground.tsx index baa7a83f4..d692700c6 100644 --- a/src/components/common/profile/RadialPatternBackground.tsx +++ b/src/components/common/profile/RadialPatternBackground.tsx @@ -13,14 +13,14 @@ import { preloadImage } from '../../../util/files'; import useLastCallback from '../../../hooks/useLastCallback'; import useMedia from '../../../hooks/useMedia'; import useResizeObserver from '../../../hooks/useResizeObserver'; -import { useSignalEffect } from '../../../hooks/useSignalEffect'; +import useDevicePixelRatio from '../../../hooks/window/useDevicePixelRatio'; import styles from './RadialPatternBackground.module.scss'; type OwnProps = { backgroundColors: string[]; - patternColor: string; - patternIcon: ApiSticker; + patternColor?: string; + patternIcon?: ApiSticker; className?: string; }; @@ -28,32 +28,33 @@ const RINGS = 3; const BASE_RING_ITEM_COUNT = 8; const RING_INCREMENT = 0.5; const CENTER_EMPTINESS = 0.05; -const MAX_RADIUS = 0.5; +const MAX_RADIUS = 0.4; const BASE_ICON_SIZE = 20; -const MIN_SIZE = 200; +const MIN_SIZE = 250; 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++) { const ringItemCount = Math.floor(BASE_RING_ITEM_COUNT * (1 + (ring - 1) * RING_INCREMENT)); const ringProgress = ring / RINGS; 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++) { - const angle = (i / ringItemCount) * Math.PI * 2; + const angle = (i / ringItemCount) * Math.PI * 2 + angleShift; // Slightly oval const xOffset = ringRadius * 1.71 * Math.cos(angle); const yOffset = ringRadius * Math.sin(angle); const x = 0.5 + xOffset; 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(); coordinates.push({ - x, y, alpha, sizeFactor, + x, y, sizeFactor, }); } } @@ -73,9 +74,11 @@ const RadialPatternBackground = ({ const [getContainerSize, setContainerSize] = useSignal({ width: 0, height: 0 }); + const dpr = useDevicePixelRatio(); + const [emojiImage, setEmojiImage] = useState(); - const previewMediaHash = getStickerMediaHash(patternIcon, 'preview'); + const previewMediaHash = patternIcon && getStickerMediaHash(patternIcon, 'preview'); const previewUrl = useMedia(previewMediaHash); useEffect(() => { @@ -109,22 +112,34 @@ const RadialPatternBackground = ({ ctx.save(); 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 renderX = x * Math.max(width, MIN_SIZE) + centerShift; - const renderY = y * Math.max(height, MIN_SIZE) + centerShift; + 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 * dpr) + 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.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.fillStyle = patternColor; - ctx.globalCompositeOperation = 'source-atop'; + ctx.globalCompositeOperation = 'destination-out'; + ctx.fillStyle = radialGradient; ctx.fillRect(0, 0, width, height); ctx.restore(); }); @@ -133,7 +148,7 @@ const RadialPatternBackground = ({ draw(); }, [emojiImage]); - useSignalEffect(() => { + useEffect(() => { const { width, height } = getContainerSize(); const canvas = canvasRef.current!; if (!width || !height) { @@ -141,14 +156,13 @@ const RadialPatternBackground = ({ } const maxSide = Math.max(width, height); - const dpr = window.devicePixelRatio; requestMutation(() => { canvas.width = maxSide * dpr; canvas.height = maxSide * dpr; draw(); }); - }, [getContainerSize]); + }, [getContainerSize, dpr]); return (
= ({ const isJoinedMessage = isJoinedChannelMessage(message); const isStarsGift = message.content.action?.type === 'giftStars'; const isStarGift = message.content.action?.type === 'starGift'; + const isStarGiftUnique = message.content.action?.type === 'starGiftUnique'; const isPrizeStars = message.content.action?.type === 'prizeStars'; const withServiceReactions = Boolean(message.areReactionsPossible && message?.reactions); @@ -318,7 +321,7 @@ const ActionMessage: FC = ({ const giftMessage = message.content.action?.message; return ( = ({ function renderStarsGift() { return ( = ({ } function renderStarGiftUserDescription() { - const starGift = message.content.action?.starGift; + const starGift = message.content.action?.starGift as ApiMessageActionStarGift; const targetUser = targetUsers && targetUsers[0]?.firstName; - const starGiftMessage = message.content.action?.starGift?.message; + const starGiftMessage = starGift?.message; if (!starGift) return undefined; if (starGiftMessage) { @@ -468,11 +471,11 @@ const ActionMessage: FC = ({ function renderStarGift() { const starGift = message.content.action?.starGift; - if (!starGift || starGift.gift.type === 'starGiftUnique') return undefined; + if (!starGift || starGift.gift.type !== 'starGift') return undefined; return ( = ({ ); } + 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 ( + +
+ +
+ + {renderStarGiftUserCaption()} +
+ {starGift.gift.title} #{starGift.gift.number} +
+
+
+ {oldLang('Gift2AttributeModel')} +
+
+ {model.name} +
+
+ {oldLang('Gift2AttributeBackdrop')} +
+
+ {backdrop.name} +
+
+ {oldLang('Gift2AttributeSymbol')} +
+
+ {pattern.name} +
+
+ +
+ + {oldLang('Gift2UniqueView')} +
+ +
+ ); + } + function renderPrizeStars() { const isUnclaimed = message.content.action?.isUnclaimed; return ( = ({ {isGiftCode && renderGiftCode()} {isStarsGift && renderStarsGift()} {isStarGift && renderStarGift()} + {isStarGiftUnique && renderStarGiftUnique()} {isPrizeStars && renderPrizeStars()} {isSuggestedAvatar && ( diff --git a/src/components/middle/MessageList.scss b/src/components/middle/MessageList.scss index d5c7719b4..70e620401 100644 --- a/src/components/middle/MessageList.scss +++ b/src/components/middle/MessageList.scss @@ -286,12 +286,37 @@ font-weight: var(--font-weight-semibold); } - .action-message-gift-code { + .action-message-centered { 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-stars-balance { + position: relative; margin-top: 0.5rem; display: flex; gap: 0.25rem; @@ -305,6 +330,31 @@ 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 { margin-left: 0.25rem; } @@ -316,6 +366,7 @@ } .action-message-gift-subtitle { + position: relative; font-weight: normal; text-wrap: balance; font-size: 0.8125rem; diff --git a/src/components/modals/common/TableInfoModal.module.scss b/src/components/modals/common/TableInfoModal.module.scss index af49d6a47..91cd0715f 100644 --- a/src/components/modals/common/TableInfoModal.module.scss +++ b/src/components/modals/common/TableInfoModal.module.scss @@ -3,7 +3,8 @@ .content { display: flex; flex-direction: column; - gap: 0.5rem; + gap: 1rem; + padding-inline: 1rem !important; max-height: min(92vh, 40rem) !important; overflow-x: hidden; } diff --git a/src/components/modals/common/TableInfoModal.tsx b/src/components/modals/common/TableInfoModal.tsx index ce275efa7..343839b90 100644 --- a/src/components/modals/common/TableInfoModal.tsx +++ b/src/components/modals/common/TableInfoModal.tsx @@ -28,6 +28,7 @@ type OwnProps = { footer?: TeactNode; buttonText?: string; className?: string; + hasBackdrop?: boolean; onClose: NoneToVoidFunction; onButtonClick?: NoneToVoidFunction; }; @@ -41,6 +42,7 @@ const TableInfoModal = ({ footer, buttonText, className, + hasBackdrop, onClose, onButtonClick, }: OwnProps) => { @@ -55,6 +57,7 @@ const TableInfoModal = ({ isOpen={isOpen} hasCloseButton={Boolean(title)} hasAbsoluteCloseButton={!title} + absoluteCloseButtonColor={hasBackdrop ? 'translucent-white' : undefined} isSlim title={title} className={className} diff --git a/src/components/modals/gift/info/GiftInfoModal.module.scss b/src/components/modals/gift/info/GiftInfoModal.module.scss index e4714263a..e918e59c6 100644 --- a/src/components/modals/gift/info/GiftInfoModal.module.scss +++ b/src/components/modals/gift/info/GiftInfoModal.module.scss @@ -36,7 +36,6 @@ font-size: 0.875rem; color: var(--color-text-secondary); text-align: center; - margin-top: 0.5rem; margin-bottom: 1rem; } @@ -53,8 +52,8 @@ .radialPattern { position: absolute; top: -3rem; - left: -1.5rem; - right: -1.5rem; + left: -1rem; + right: -1rem; height: 16.5rem; z-index: -1; @@ -77,4 +76,8 @@ font-size: 1.25rem; color: white; } + + .description { + font-size: 0.875rem; + } } diff --git a/src/components/modals/gift/info/GiftInfoModal.tsx b/src/components/modals/gift/info/GiftInfoModal.tsx index f50597705..67d681564 100644 --- a/src/components/modals/gift/info/GiftInfoModal.tsx +++ b/src/components/modals/gift/info/GiftInfoModal.tsx @@ -138,14 +138,12 @@ const GiftInfoModal = ({ const isVisibleForMe = isNameHidden && targetUser; const description = (() => { - if (!userGift) { - return lang('GiftInfoSoldOutDescription'); - } if (gift.type === 'starGiftUnique') { return lang('GiftInfoCollectible', { number: gift.number, }); } + if (!userGift) return lang('GiftInfoSoldOutDescription'); if (!canUpdate && !isSender) return undefined; if (!starsToConvert || canConvertDifference < 0) return undefined; if (isConverted) { @@ -186,8 +184,8 @@ const GiftInfoModal = ({ })(); function getTitle() { - if (!userGift) return lang('GiftInfoSoldOutTitle'); if (gift?.type === 'starGiftUnique') return gift.title; + if (!userGift) return lang('GiftInfoSoldOutTitle'); return canUpdate ? lang('GiftInfoReceived') : lang('GiftInfoTitle'); } @@ -219,7 +217,7 @@ const GiftInfoModal = ({

)} {description && ( -

+

{description}

)} @@ -457,6 +455,7 @@ const GiftInfoModal = ({ = ({ header, hasCloseButton, hasAbsoluteCloseButton, + absoluteCloseButtonColor = 'translucent', noBackdrop, noBackdropClose, children, @@ -137,7 +139,7 @@ const Modal: FC = ({