diff --git a/CLAUDE.md b/CLAUDE.md index 4fa21bf29..74da294c8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -190,7 +190,7 @@ const NewComp = (props: OwnProps & StateProps) => { … } ``` ### 5. Memoization -* Wrap most components with `memo()` to avoid unnecessary updates. +* Wrap most components with `memo()` to avoid unnecessary updates. Consider skipping memo for simple wrapper components whose children change on almost every render. * Don't pass freshly created objects or arrays as props to memoized components. * **Exceptions** (no memo): `ListItem`, `Button`, `MenuItem`, etc. diff --git a/src/api/gramjs/methods/stars.ts b/src/api/gramjs/methods/stars.ts index d1e91d475..2db788ac1 100644 --- a/src/api/gramjs/methods/stars.ts +++ b/src/api/gramjs/methods/stars.ts @@ -19,6 +19,7 @@ import { buildApiResaleGifts, buildApiSavedStarGift, buildApiStarGift, + buildApiStarGiftAttribute, buildApiStarGiftAuctionAcquiredGift, buildApiStarGiftAuctionState, buildApiStarGiftCollection, @@ -629,3 +630,21 @@ export function resolveStarGiftOffer({ shouldReturnTrue: true, }); } + +export async function fetchStarGiftUpgradeAttributes({ + giftId, +}: { + giftId: string; +}) { + const result = await invokeRequest(new GramJs.payments.GetStarGiftUpgradeAttributes({ + giftId: BigInt(giftId), + })); + + if (!result) { + return undefined; + } + + return { + attributes: result.attributes.map(buildApiStarGiftAttribute).filter(Boolean), + }; +} diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index a29354803..e13579c60 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -2665,3 +2665,17 @@ "ChatListAuctionView" = "View"; "BotAuthSuccessTitle" = "Login Successful"; "BotAuthSuccessText" = "You've successfully logged into **{url}**"; +"GiftPreviewSelectedTraits" = "Selected traits"; +"GiftPreviewCountModels_one" = "This collection features **{count}** unique model"; +"GiftPreviewCountModels_other" = "This collection features **{count}** unique models"; +"GiftPreviewCountCraftableModels_one" = "This collection features **{count}** craftable model"; +"GiftPreviewCountCraftableModels_other" = "This collection features **{count}** craftable models"; +"GiftPreviewCountPatterns_one" = "This collection features **{count}** unique pattern"; +"GiftPreviewCountPatterns_other" = "This collection features **{count}** unique patterns"; +"GiftPreviewCountBackdrops_one" = "This collection features **{count}** unique backdrop"; +"GiftPreviewCountBackdrops_other" = "This collection features **{count}** unique backdrops"; +"GiftUpgradeViewAll" = "View all variants >"; +"GiftPreviewToggleCraftableModels" = "View Craftable Models >"; +"GiftPreviewToggleRegularModels" = "View Primary Models >"; +"AriaGiftPreviewPlay" = "Play random previews"; +"AriaGiftPreviewStop" = "Pause random previews"; diff --git a/src/bundles/stars.ts b/src/bundles/stars.ts index 84771752e..9d1903e48 100644 --- a/src/bundles/stars.ts +++ b/src/bundles/stars.ts @@ -13,6 +13,7 @@ export { default as GiftInfoValueModal } from '../components/modals/gift/value/G export { default as GiftLockedModal } from '../components/modals/gift/locked/GiftLockedModal'; export { default as GiftResalePriceComposerModal } from '../components/modals/gift/resale/GiftResalePriceComposerModal'; export { default as GiftUpgradeModal } from '../components/modals/gift/upgrade/GiftUpgradeModal'; +export { default as GiftPreviewModal } from '../components/modals/gift/preview/GiftPreviewModal'; export { default as GiftAuctionModal } from '../components/modals/gift/auction/GiftAuctionModal'; export { default as GiftAuctionBidModal } from '../components/modals/gift/auction/GiftAuctionBidModal'; export { default as GiftAuctionInfoModal } from '../components/modals/gift/auction/GiftAuctionInfoModal'; diff --git a/src/components/calls/phone/PhoneCallButton.tsx b/src/components/calls/phone/PhoneCallButton.tsx index 85de8aa11..e241171da 100644 --- a/src/components/calls/phone/PhoneCallButton.tsx +++ b/src/components/calls/phone/PhoneCallButton.tsx @@ -1,5 +1,4 @@ -import type { FC } from '../../../lib/teact/teact'; -import type React from '../../../lib/teact/teact'; +import type { TeactNode } from '../../../lib/teact/teact'; import { memo } from '../../../lib/teact/teact'; import type { IconName } from '../../../types/icons'; @@ -12,18 +11,17 @@ import Button from '../../ui/Button'; import styles from './PhoneCallButton.module.scss'; type OwnProps = { - onClick: VoidFunction; label: string; icon?: IconName; iconClassName?: string; - customIcon?: React.ReactNode; + customIcon?: TeactNode; className?: string; isDisabled?: boolean; isActive?: boolean; + onClick: VoidFunction; }; -const PhoneCallButton: FC = ({ - onClick, +const PhoneCallButton = ({ label, customIcon, icon, @@ -31,7 +29,8 @@ const PhoneCallButton: FC = ({ className, isDisabled, isActive, -}) => { + onClick, +}: OwnProps) => { return (
+ ); } diff --git a/src/components/modals/gift/UniqueGiftHeader.module.scss b/src/components/modals/gift/UniqueGiftHeader.module.scss index 6c6c51dde..bbcb5f397 100644 --- a/src/components/modals/gift/UniqueGiftHeader.module.scss +++ b/src/components/modals/gift/UniqueGiftHeader.module.scss @@ -1,8 +1,10 @@ .root { --_height: 15rem; + overflow: hidden; display: flex; flex-direction: column; + flex-shrink: 0; gap: 0.25rem; align-items: center; @@ -10,6 +12,7 @@ height: var(--_height); margin-bottom: 0.5rem; padding-bottom: 1rem; + border-radius: 0 0 0.625rem 0.625rem; &.withManageButtons { --_height: 19.25rem; @@ -114,6 +117,7 @@ .subtitle { font-size: 1rem; line-height: 1.375rem; + color: var(--tinted-text-color); text-align: center; text-wrap: balance; diff --git a/src/components/modals/gift/UniqueGiftHeader.tsx b/src/components/modals/gift/UniqueGiftHeader.tsx index 04b6d46d0..20cbb5e75 100644 --- a/src/components/modals/gift/UniqueGiftHeader.tsx +++ b/src/components/modals/gift/UniqueGiftHeader.tsx @@ -1,3 +1,4 @@ +import type React from '@teact'; import type { TeactNode } from '@teact'; import { memo, useMemo } from '@teact'; import { getActions } from '../../../global'; @@ -6,7 +7,8 @@ import type { ApiPeer, ApiSavedStarGift, ApiStarGiftAttributeBackdrop, ApiStarGiftAttributeModel, ApiStarGiftAttributePattern, - ApiTypeCurrencyAmount } from '../../../api/types'; + ApiTypeCurrencyAmount, +} from '../../../api/types'; import { formatStarsTransactionAmount, @@ -42,6 +44,7 @@ type OwnProps = { resellPrice?: ApiTypeCurrencyAmount; showManageButtons?: boolean; savedGift?: ApiSavedStarGift; + children?: React.ReactNode; }; const STICKER_SIZE = 120; @@ -58,6 +61,7 @@ const UniqueGiftHeader = ({ resellPrice, showManageButtons, savedGift, + children, }: OwnProps) => { const { openChat, @@ -88,11 +92,13 @@ const UniqueGiftHeader = ({ }, [backdropAttribute, patternAttribute, isMobile]); return ( -
{ if (subtitlePeer) { openChat({ id: subtitlePeer.id }); @@ -142,6 +147,7 @@ const UniqueGiftHeader = ({ {resellPrice.currency === 'TON' && }

)} + {children}
); }; diff --git a/src/components/modals/gift/auction/GiftAuctionBidModal.module.scss b/src/components/modals/gift/auction/GiftAuctionBidModal.module.scss index cf40a082f..5645ff2f9 100644 --- a/src/components/modals/gift/auction/GiftAuctionBidModal.module.scss +++ b/src/components/modals/gift/auction/GiftAuctionBidModal.module.scss @@ -1,3 +1,7 @@ +.root { + z-index: calc(var(--z-modal-low-priority) + 1); +} + .content { display: flex; flex-direction: column; diff --git a/src/components/modals/gift/auction/GiftAuctionBidModal.tsx b/src/components/modals/gift/auction/GiftAuctionBidModal.tsx index f1f495de3..32351142f 100644 --- a/src/components/modals/gift/auction/GiftAuctionBidModal.tsx +++ b/src/components/modals/gift/auction/GiftAuctionBidModal.tsx @@ -333,6 +333,7 @@ const GiftAuctionBidModal = ({ isOpen={isOpen} hasAbsoluteCloseButton isSlim + className={styles.root} contentClassName={styles.content} onClose={closeGiftAuctionBidModal} isLowStackPriority diff --git a/src/components/modals/gift/auction/GiftAuctionModal.module.scss b/src/components/modals/gift/auction/GiftAuctionModal.module.scss index ab9bad8ae..f06ed7d23 100644 --- a/src/components/modals/gift/auction/GiftAuctionModal.module.scss +++ b/src/components/modals/gift/auction/GiftAuctionModal.module.scss @@ -4,7 +4,7 @@ .modalContent { position: relative; - max-height: min(97vh, 48rem) !important; + max-height: min(97vh, 50rem) !important; } .header { diff --git a/src/components/modals/gift/info/GiftInfoModal.module.scss b/src/components/modals/gift/info/GiftInfoModal.module.scss index c74e71adf..201f7cc46 100644 --- a/src/components/modals/gift/info/GiftInfoModal.module.scss +++ b/src/components/modals/gift/info/GiftInfoModal.module.scss @@ -56,11 +56,6 @@ color: var(--color-error) !important; } -.modalContent { - position: relative; - max-height: min(97vh, 48rem) !important; -} - .moreMenuButton { position: absolute; z-index: 1; diff --git a/src/components/modals/gift/info/GiftInfoModal.tsx b/src/components/modals/gift/info/GiftInfoModal.tsx index 11335e46a..ea7c95548 100644 --- a/src/components/modals/gift/info/GiftInfoModal.tsx +++ b/src/components/modals/gift/info/GiftInfoModal.tsx @@ -24,7 +24,7 @@ import { import { CUSTOM_PEER_HIDDEN } from '../../../../util/objects/customPeer'; import { getServerTime } from '../../../../util/serverTime'; import { renderGiftOriginalInfo } from '../../../common/helpers/giftOriginalInfo'; -import { getGiftAttributes, getGiftRarityTitle, getStickerFromGift } from '../../../common/helpers/gifts'; +import { getGiftAttributes, getStickerFromGift } from '../../../common/helpers/gifts'; import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities'; import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev'; @@ -95,9 +95,8 @@ const GiftInfoModal = ({ buyStarGift, closeGiftModal, openGiftInfoValueModal, - updateResaleGiftsFilter, - openGiftInMarket, openGiftDescriptionRemoveModal, + openGiftPreviewModal, } = getActions(); const [isConvertConfirmOpen, openConvertConfirm, closeConvertConfirm] = useFlag(); @@ -109,57 +108,6 @@ const GiftInfoModal = ({ const uniqueGiftHeaderRef = useRef(); - 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); @@ -303,6 +251,13 @@ const GiftInfoModal = ({ return gift && getGiftAttributes(gift); }, [gift]); + const handleOpenPreviewModal = useLastCallback(() => { + if (!gift) return; + openGiftPreviewModal({ + originGift: gift, + }); + }); + const uniqueGiftTitle = useMemo(() => { if (!gift || gift.type !== 'starGiftUnique' || !giftAttributes?.backdrop) return undefined; @@ -709,13 +664,8 @@ const GiftInfoModal = ({ if (model) { tableData.push([ lang('GiftAttributeModel'), - - - {model.name} - + + {model.name} , ]); @@ -724,14 +674,9 @@ const GiftInfoModal = ({ if (backdrop) { tableData.push([ lang('GiftAttributeBackdrop'), - - - {backdrop.name} - - {getGiftRarityTitle(lang, backdrop.rarity)} + + {backdrop.name} + , ]); } @@ -739,14 +684,9 @@ const GiftInfoModal = ({ if (pattern) { tableData.push([ lang('GiftAttributeSymbol'), - - - {pattern.name} - - {getGiftRarityTitle(lang, pattern.rarity)} + + {pattern.name} + , ]); } @@ -874,7 +814,6 @@ const GiftInfoModal = ({ gift, giftAttributes, renderFooterButton, isTargetChat, isGiftUnique, saleDateInfo, canBuyGift, giftOwnerTitle, resellPrice, uniqueGiftTitle, uniqueGiftSubtitle, releasedByPeer, - handleSymbolClick, handleBackdropClick, handleModelClick, ]); const moreMenuItems = typeGift && ( @@ -896,8 +835,7 @@ const GiftInfoModal = ({ hasBackdrop={isGiftUnique} tableData={modalData?.tableData} footer={modalData?.footer} - className={styles.modal} - contentClassName={styles.modalContent} + className={buildClassName(styles.modal, 'tall')} closeButtonColor={isGiftUnique ? 'translucent-white' : undefined} moreMenuItems={moreMenuItems} onClose={handleClose} diff --git a/src/components/modals/gift/offer/GiftOfferAcceptModal.tsx b/src/components/modals/gift/offer/GiftOfferAcceptModal.tsx index bd3b7be01..a7048946e 100644 --- a/src/components/modals/gift/offer/GiftOfferAcceptModal.tsx +++ b/src/components/modals/gift/offer/GiftOfferAcceptModal.tsx @@ -17,14 +17,14 @@ import { } from '../../../../util/localization/format'; import { round } from '../../../../util/math'; import { formatPercent } from '../../../../util/textFormat'; -import { getGiftAttributes, getGiftRarityTitle } from '../../../common/helpers/gifts'; +import { getGiftAttributes } from '../../../common/helpers/gifts'; import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev'; import useLang from '../../../../hooks/useLang'; import useLastCallback from '../../../../hooks/useLastCallback'; -import BadgeButton from '../../../common/BadgeButton'; import GiftTransferPreview from '../../../common/gift/GiftTransferPreview'; +import GiftRarityBadge from '../../../common/GiftRarityBadge'; import ConfirmDialog from '../../../ui/ConfirmDialog'; import TableInfo, { type TableData } from '../../common/TableInfo'; @@ -85,7 +85,7 @@ const GiftOfferAcceptModal = ({ lang('GiftAttributeModel'), {model.name} - {getGiftRarityTitle(lang, model.rarity)} + , ]); } @@ -95,7 +95,7 @@ const GiftOfferAcceptModal = ({ lang('GiftAttributeBackdrop'), {backdrop.name} - {getGiftRarityTitle(lang, backdrop.rarity)} + , ]); } @@ -105,7 +105,7 @@ const GiftOfferAcceptModal = ({ lang('GiftAttributeSymbol'), {pattern.name} - {getGiftRarityTitle(lang, pattern.rarity)} + , ]); } diff --git a/src/components/modals/gift/preview/GiftPreviewModal.async.tsx b/src/components/modals/gift/preview/GiftPreviewModal.async.tsx new file mode 100644 index 000000000..5fa93c936 --- /dev/null +++ b/src/components/modals/gift/preview/GiftPreviewModal.async.tsx @@ -0,0 +1,14 @@ +import type { OwnProps } from './GiftPreviewModal'; + +import { Bundles } from '../../../../util/moduleLoader'; + +import useModuleLoader from '../../../../hooks/useModuleLoader'; + +const GiftPreviewModalAsync = (props: OwnProps) => { + const { modal } = props; + const GiftPreviewModal = useModuleLoader(Bundles.Stars, 'GiftPreviewModal', !modal); + + return GiftPreviewModal ? : undefined; +}; + +export default GiftPreviewModalAsync; diff --git a/src/components/modals/gift/preview/GiftPreviewModal.module.scss b/src/components/modals/gift/preview/GiftPreviewModal.module.scss new file mode 100644 index 000000000..00deedcd8 --- /dev/null +++ b/src/components/modals/gift/preview/GiftPreviewModal.module.scss @@ -0,0 +1,110 @@ +@use "../../../../styles/mixins"; + +.content { + overflow: hidden; + display: flex; + flex-direction: column; + + height: min(92vh, 46rem); // Required to keep Transition height + padding: 0 !important; +} + +.tabs { + position: sticky; + z-index: 1; + top: 0; + + padding-top: 0.5rem; + + background-color: var(--color-background); +} + +.dialog, +.transition { + overflow: hidden; +} + +.grid { + overflow-y: scroll; + display: grid; + grid-auto-rows: max-content; + grid-template-columns: repeat(3, 1fr); + gap: 0.5rem; + + padding: 0.5rem; + + @include mixins.adapt-padding-to-scrollbar(0.5rem, 0.125rem); // Prevent outline cutouts +} + +.gridHeader { + display: flex; + grid-column: 1 / 4; + grid-row: 1; + flex-direction: column; + place-self: center; + align-items: center; + justify-content: center; + + color: var(--color-text-secondary); +} + +.item { + aspect-ratio: 1 / 1; +} + +.header { + --_height: 20rem; + + justify-content: flex-end; + margin-bottom: 0; +} + +.traitButtons { + z-index: 1; + + display: flex; + gap: 0.375rem; + justify-content: center; + + width: 100%; + margin-top: 0.25rem; + padding: 0 1rem; +} + +.traitButton { + overflow: visible; + display: flex; + flex-direction: column; + flex-shrink: 1; + gap: 0.125rem; +} + +.traitName { + font-size: 0.8125rem; + line-height: 1; +} + +.traitType { + font-size: 0.625rem; + color: var(--tinted-text-color); +} + +.traitRarity { + --accent-background-active-color: var(--_badge-bg); + + position: absolute; + top: 0; + right: 0; + transform: translate(0.25rem, -50%); + + color: white; + + transition: background-color 150ms linear; +} + +.playButton { + position: absolute; + z-index: 1; + top: 0.875rem; + right: 0.875rem; +} diff --git a/src/components/modals/gift/preview/GiftPreviewModal.tsx b/src/components/modals/gift/preview/GiftPreviewModal.tsx new file mode 100644 index 000000000..3ad2209d8 --- /dev/null +++ b/src/components/modals/gift/preview/GiftPreviewModal.tsx @@ -0,0 +1,490 @@ +import { memo, useEffect, useMemo, useRef, useState } from '../../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../../global'; + +import type { + ApiStarGiftAttributeBackdrop, + ApiStarGiftAttributeModel, + ApiStarGiftAttributePattern, +} from '../../../../api/types'; +import type { TabState } from '../../../../global/types'; +import type { AnimationLevel } from '../../../../types'; + +import { selectAnimationLevel } from '../../../../global/selectors/sharedState'; +import buildClassName from '../../../../util/buildClassName'; +import { getNextArrowReplacement } from '../../../../util/localization/format'; +import { resolveTransitionName } from '../../../../util/resolveTransitionName'; +import { getGiftAttributes, getRandomGiftPreviewAttributes } from '../../../common/helpers/gifts'; + +import useInterval from '../../../../hooks/schedulers/useInterval'; +import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev'; +import useFlag from '../../../../hooks/useFlag'; +import { useIntersectionObserver } from '../../../../hooks/useIntersectionObserver'; +import useLang from '../../../../hooks/useLang'; +import useLastCallback from '../../../../hooks/useLastCallback'; + +import GiftRarityBadge from '../../../common/GiftRarityBadge'; +import Button from '../../../ui/Button'; +import InfiniteScroll from '../../../ui/InfiniteScroll'; +import Link from '../../../ui/Link'; +import Modal from '../../../ui/Modal'; +import TabList, { type TabWithProperties } from '../../../ui/TabList'; +import Transition from '../../../ui/Transition'; +import GiftAttributeItem from '../GiftAttributeItem'; +import UniqueGiftHeader from '../UniqueGiftHeader'; + +import styles from './GiftPreviewModal.module.scss'; + +export type OwnProps = { + modal: TabState['giftPreviewModal']; +}; + +type StateProps = { + animationLevel: AnimationLevel; +}; + +const MODEL_STICKER_SIZE = 80; +const PATTERN_STICKER_SIZE = 60; +const INTERSECTION_THROTTLE = 200; +const PLAYBACK_INTERVAL = 5000; + +enum AttributeTab { + Model, + Backdrop, + Pattern, +} + +const GiftPreviewModal = ({ modal, animationLevel }: OwnProps & StateProps) => { + const { + closeGiftPreviewModal, + openGiftInMarket, + updateResaleGiftsFilter, + } = getActions(); + const [isCraftableModelsMode, showCraftableModels, showRegularModels] = useFlag(); + const [isPlayingRandomPreviews, playRandomPreviews, stopRandomPreviews] = useFlag(true); + + const modelsContainerRef = useRef(); + const patternsContainerRef = useRef(); + const backdropsContainerRef = useRef(); + + const isOpen = Boolean(modal); + const renderingModal = useCurrentOrPrev(modal); + + const originGift = renderingModal?.originGift; + const initialAttributes = useMemo(() => originGift && getGiftAttributes(originGift), [originGift]); + + const lang = useLang(); + + const [selectedTabIndex, setSelectedTabIndex] = useState(AttributeTab.Model); + + const { regularModels, craftableModels, patterns, backdrops } = useMemo(() => { + if (!renderingModal?.attributes) { + return { regularModels: [], craftableModels: [], patterns: [], backdrops: [] }; + } + + const result: { + regularModels: ApiStarGiftAttributeModel[]; + craftableModels: ApiStarGiftAttributeModel[]; + patterns: ApiStarGiftAttributePattern[]; + backdrops: ApiStarGiftAttributeBackdrop[]; + } = { regularModels: [], craftableModels: [], patterns: [], backdrops: [] }; + + for (const attr of renderingModal.attributes) { + if (attr.type === 'model') { + if (attr.rarity.type === 'regular') { + result.regularModels.push(attr); + } else { + result.craftableModels.push(attr); + } + } + if (attr.type === 'pattern') result.patterns.push(attr); + if (attr.type === 'backdrop') result.backdrops.push(attr); + } + + return result; + }, [renderingModal?.attributes]); + + const firstModel = regularModels[0]; + const firstPattern = patterns[0]; + const firstBackdrop = backdrops[0]; + + const [selectedModel, setSelectedModel] = useState(firstModel); + const [selectedPattern, setSelectedPattern] = useState(firstPattern); + const [selectedBackdrop, setSelectedBackdrop] = useState(firstBackdrop); + + useEffect(() => { + if (isOpen) return; + setSelectedTabIndex(AttributeTab.Model); + showRegularModels(); + }, [isOpen]); + + useEffect(() => { + const newModel = initialAttributes?.model || firstModel; + setSelectedModel(newModel); + setSelectedPattern(initialAttributes?.pattern || firstPattern); + setSelectedBackdrop(initialAttributes?.backdrop || firstBackdrop); + if (newModel && newModel.rarity.type !== 'regular') showCraftableModels(); + }, [initialAttributes, firstModel, firstPattern, firstBackdrop]); + + useInterval(() => { + if (!originGift || !selectedModel || !selectedPattern || !selectedBackdrop) return; + const newAttributes = getRandomGiftPreviewAttributes(renderingModal?.attributes, { + model: selectedModel, + pattern: selectedPattern, + backdrop: selectedBackdrop, + }); + setSelectedModel(newAttributes.model); + setSelectedPattern(newAttributes.pattern); + setSelectedBackdrop(newAttributes.backdrop); + }, isPlayingRandomPreviews ? PLAYBACK_INTERVAL : undefined, true); + + const { + observe: observeModelsIntersection, + } = useIntersectionObserver({ + rootRef: modelsContainerRef, + throttleMs: INTERSECTION_THROTTLE, + isDisabled: selectedTabIndex !== AttributeTab.Model, + }); + + const { + observe: observePatternsIntersection, + } = useIntersectionObserver({ + rootRef: patternsContainerRef, + throttleMs: INTERSECTION_THROTTLE, + isDisabled: selectedTabIndex !== AttributeTab.Pattern, + }); + + const { + observe: observeBackdropsIntersection, + } = useIntersectionObserver({ + rootRef: backdropsContainerRef, + throttleMs: INTERSECTION_THROTTLE, + isDisabled: selectedTabIndex !== AttributeTab.Backdrop, + }); + + const tabs = useMemo(() => [ + { title: lang('GiftAttributeModel') }, + { title: lang('GiftAttributeBackdrop') }, + { title: lang('GiftAttributeSymbol') }, + ], [lang]); + + const handleClose = useLastCallback(() => closeGiftPreviewModal()); + + const handleSelectModel = useLastCallback((model: ApiStarGiftAttributeModel) => { + setSelectedModel(model); + stopRandomPreviews(); + }); + + const handleSelectPattern = useLastCallback((pattern: ApiStarGiftAttributePattern) => { + setSelectedPattern(pattern); + stopRandomPreviews(); + }); + + const handleSelectBackdrop = useLastCallback((backdrop: ApiStarGiftAttributeBackdrop) => { + setSelectedBackdrop(backdrop); + stopRandomPreviews(); + }); + + const handleSymbolClick = useLastCallback(() => { + if (!originGift || !selectedPattern) return; + + openGiftInMarket({ gift: originGift }); + updateResaleGiftsFilter({ + filter: { + sortType: 'byDate', + modelAttributes: [], + backdropAttributes: [], + patternAttributes: [{ + type: 'pattern', + documentId: selectedPattern.sticker.id, + }], + }, + }); + handleClose(); + }); + + const handleBackdropClick = useLastCallback(() => { + if (!originGift || !selectedBackdrop) return; + + openGiftInMarket({ gift: originGift }); + updateResaleGiftsFilter({ + filter: { + sortType: 'byDate', + modelAttributes: [], + backdropAttributes: [{ + type: 'backdrop', + backdropId: selectedBackdrop.backdropId, + }], + patternAttributes: [], + }, + }); + handleClose(); + }); + + const handleModelClick = useLastCallback(() => { + if (!originGift) return; + + openGiftInMarket({ gift: originGift }); + updateResaleGiftsFilter({ + filter: { + sortType: 'byDate', + modelAttributes: [{ + type: 'model', + documentId: selectedModel!.sticker.id, + }], + backdropAttributes: [], + patternAttributes: [], + }, + }); + handleClose(); + }); + + const modalHeader = useMemo(() => { + return ( + <> + + + + + + ); + } + + function renderTabContent() { + switch (selectedTabIndex) { + case AttributeTab.Model: + return ( + + + {lang( + isCraftableModelsMode ? 'GiftPreviewCountCraftableModels' : 'GiftPreviewCountModels', + { count: isCraftableModelsMode ? craftableModels.length : regularModels.length }, { + pluralValue: isCraftableModelsMode ? craftableModels.length : regularModels.length, + withNodes: true, + withMarkdown: true, + })} + + {Boolean(craftableModels?.length) && ( + isCraftableModelsMode ? showRegularModels() : showCraftableModels()} + > + {lang( + isCraftableModelsMode ? 'GiftPreviewToggleRegularModels' : 'GiftPreviewToggleCraftableModels', + undefined, + { withNodes: true, specialReplacement: getNextArrowReplacement() }, + )} + + )} + + )} + items={isCraftableModelsMode ? craftableModels : regularModels} + noFastList + > + {(isCraftableModelsMode ? craftableModels : regularModels).map((model) => ( + + ))} + + ); + case AttributeTab.Pattern: + return ( + + + {lang('GiftPreviewCountPatterns', { count: patterns.length }, { + pluralValue: patterns.length, + withNodes: true, + withMarkdown: true, + })} + + + )} + items={patterns} + noFastList + > + {patterns.map((pattern) => ( + + ))} + + ); + case AttributeTab.Backdrop: + return ( + + + {lang('GiftPreviewCountBackdrops', { count: backdrops.length }, { + pluralValue: backdrops.length, + withNodes: true, + withMarkdown: true, + })} + + + )} + items={backdrops} + noFastList + > + {backdrops.map((backdrop) => ( + + ))} + + ); + default: + return undefined; + } + } + + return ( + + {renderHeader()} + + + {renderTabContent()} + + + ); +}; + +export default memo(withGlobal( + (global): Complete => { + return { + animationLevel: selectAnimationLevel(global), + }; + }, +)(GiftPreviewModal)); diff --git a/src/components/modals/gift/upgrade/GiftUpgradeModal.module.scss b/src/components/modals/gift/upgrade/GiftUpgradeModal.module.scss index d05bb5b8e..e40cf57a0 100644 --- a/src/components/modals/gift/upgrade/GiftUpgradeModal.module.scss +++ b/src/components/modals/gift/upgrade/GiftUpgradeModal.module.scss @@ -1,3 +1,7 @@ +.header { + --_height: 17.5rem; +} + .footer { display: flex; flex-direction: column; @@ -19,3 +23,14 @@ text-transform: none; opacity: 0.6; } + +.subtitle { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.viewAllText { + margin-inline-start: 0.125rem; +} diff --git a/src/components/modals/gift/upgrade/GiftUpgradeModal.tsx b/src/components/modals/gift/upgrade/GiftUpgradeModal.tsx index 0a94abc0b..ffea5dadf 100644 --- a/src/components/modals/gift/upgrade/GiftUpgradeModal.tsx +++ b/src/components/modals/gift/upgrade/GiftUpgradeModal.tsx @@ -3,7 +3,7 @@ import { } from '../../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../../global'; -import type { ApiPeer } from '../../../../api/types'; +import type { ApiPeer, ApiStarGiftAttributeModel } from '../../../../api/types'; import type { TabState } from '../../../../global/types'; import { getPeerTitle } from '../../../../global/helpers/peers'; @@ -19,6 +19,7 @@ import useLang from '../../../../hooks/useLang'; import useLastCallback from '../../../../hooks/useLastCallback'; import AnimatedCounter from '../../../common/AnimatedCounter'; +import CustomEmoji from '../../../common/CustomEmoji'; import Icon from '../../../common/icons/Icon'; import Button from '../../../ui/Button'; import Checkbox from '../../../ui/Checkbox'; @@ -38,6 +39,7 @@ type StateProps = { }; const PREVIEW_UPDATE_INTERVAL = 3000; +const BUTTON_MODELS_COUNT = 3; const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => { const { @@ -47,6 +49,7 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => { upgradePrepaidGift, openStarGiftPriceDecreaseInfoModal, shiftGiftUpgradeNextPrice, + openGiftPreviewModal, } = getActions(); const isOpen = Boolean(modal); @@ -70,6 +73,12 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => { const nextPriceDate = nextPrice?.date; const upgradeStars = renderingModal?.currentUpgradeStars; + const previewModels = useMemo(() => { + return renderingModal?.sampleAttributes + ?.filter((attr): attr is ApiStarGiftAttributeModel => attr.type === 'model') + .slice(0, BUTTON_MODELS_COUNT); + }, [renderingModal?.sampleAttributes]); + const handleUpgrade = useLastCallback(() => { const gift = renderingModal?.gift; @@ -103,6 +112,11 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => { setPreviewAttributes(getRandomGiftPreviewAttributes(renderingModal.sampleAttributes, previewAttributes)); }); + const handleViewAllVariants = useLastCallback(() => { + if (!renderingModal?.gift) return; + openGiftPreviewModal({ originGift: renderingModal.gift.gift }); + }); + const handleOpenPriceInfo = useLastCallback(() => { if (!renderingModal?.prices) return; @@ -155,16 +169,43 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => { ['auction', lang('GiftUpgradeTradeableTitle'), lang('GiftUpgradeTradeableDescription')], ]) satisfies TableAboutData; - const subtitle = renderingRecipient + const subtitleText = renderingRecipient ? lang('GiftPeerUpgradeText', { peer: getPeerTitle(lang, renderingRecipient) }) : lang('GiftUpgradeTextOwn'); + const subtitle = ( +
+ + {subtitleText} + + +
+ ); + const hasPriceDecreaseInfo = Boolean(nextPriceDate) && Boolean(renderingModal?.prices?.length) && !gift?.alreadyPaidUpgradeStars; const header = ( { header, footer, }; - }, [previewAttributes, isOpen, lang, - renderingRecipient, renderingModal?.gift, - shouldKeepOriginalDetails, isPrepaid, - renderingModal?.prices?.length, - nextPriceDate, formattedPriceElement]); + }, [ + previewAttributes, isOpen, lang, renderingRecipient, renderingModal?.gift, shouldKeepOriginalDetails, isPrepaid, + renderingModal?.prices?.length, nextPriceDate, formattedPriceElement, previewModels, + ]); return ( ; type?: 'button' | 'submit' | 'reset'; - children?: React.ReactNode; + children?: TeactNode; size?: 'default' | 'smaller' | 'tiny'; color?: ( 'primary' | 'secondary' | 'gray' | 'danger' | 'translucent' | 'translucent-white' | 'translucent-black' @@ -76,7 +75,7 @@ export type OwnProps = { // Longest animation duration; const CLICKED_TIMEOUT = 400; -const Button: FC = ({ +const Button = ({ ref, type = 'button', id, @@ -125,7 +124,7 @@ const Button: FC = ({ onMouseLeave, onFocus, onTransitionEnd, -}) => { +}: OwnProps) => { let elementRef = useRef(); if (ref) { elementRef = ref; diff --git a/src/components/ui/Modal.scss b/src/components/ui/Modal.scss index 7859480e5..16c2de449 100644 --- a/src/components/ui/Modal.scss +++ b/src/components/ui/Modal.scss @@ -1,258 +1,266 @@ -.Modal { - position: relative; - z-index: var(--z-modal); - color: var(--color-text); - - &.confirm, - &.pin { - z-index: var(--z-modal-confirm); - } - - &.low-priority { - z-index: var(--z-modal-low-priority); - } - - &.delete, - &.error, - &.confirm, - &.pin, - &.unpin-all { - .modal-dialog { - max-width: 24rem; - } - } - - &.error { - .modal-content .dialog-buttons { - margin-top: 1rem; - } - } - - &.narrow { - .modal-dialog { - max-width: 20rem; - } - } - - &.slim { - .modal-dialog { - max-width: 26.25rem; - } - - .modal-content { - max-height: min(92vh, 36rem); - } - } - - .modal-container { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - - display: flex; - align-items: center; - justify-content: center; - } - - &.with-balance-bar { - .modal-container { - flex-direction: column; - } - - .modal-dialog { - margin-top: 0; - } - } - - .modal-backdrop { - position: fixed; - z-index: -1; - top: 0; - right: 0; - bottom: 0; - left: 0; - - background-color: rgba(0, 0, 0, 0.25); - } - - &.transparent-backdrop .modal-backdrop { - background-color: transparent; - } - - .modal-dialog { +@layer ui.modal { + .Modal { position: relative; - transform: translate3d(0, -1rem, 0); + z-index: var(--z-modal); + color: var(--color-text); - display: inline-flex; - flex-direction: column; - - width: 100%; - min-width: 17.5rem; - max-width: 35rem; - margin: 2rem auto; - border-radius: var(--border-radius-modal); - - background-color: var(--color-background); - box-shadow: 0 0.25rem 0.5rem 0.125rem var(--color-default-shadow); - - transition: transform 0.2s ease, opacity 0.2s ease; - - body.no-page-transitions & { - transform: none !important; - transition: none; + &.confirm, + &.pin { + z-index: var(--z-modal-confirm); } - @media (max-width: 450px) { - max-width: calc(100vw - 3rem) !important; + &.low-priority { + z-index: var(--z-modal-low-priority); } - } - &.open .modal-dialog { - transform: translate3d(0, 0, 0); - } - - &.closing .modal-dialog { - transform: translate3d(0, 1rem, 0); - } - - .modal-header, - %modal-header { - display: flex; - flex-shrink: 0; - align-items: center; - padding: 1.3125rem 1.375rem 0; - } - - .modal-title { - overflow: hidden; - flex: 1 1 auto; - - font-size: 1.25rem; - font-weight: var(--font-weight-medium); - text-overflow: ellipsis; - - &:not(:only-child) { - margin: 0 1rem; + &.delete, + &.error, + &.confirm, + &.pin, + &.unpin-all { + .modal-dialog { + max-width: 24rem; + } } - } - .modal-header-condensed { - @extend %modal-header; + &.error { + .modal-content .dialog-buttons { + margin-top: 1rem; + } + } - min-height: 3.5rem; - padding: 0.375rem 0.75rem !important; + &.narrow { + .modal-dialog { + max-width: 20rem; + } + } - .modal-action-button { - width: auto; - min-width: 5rem; - height: 2.25rem; - margin-left: auto; - padding-right: 1.25rem; - padding-left: 1.25rem; + &.slim { + .modal-dialog { + max-width: 26.25rem; + } - font-size: 0.875rem; - line-height: 1.625rem; + .modal-content { + max-height: min(92vh, 36rem); + } - &.danger { - color: var(--color-white); - background-color: var(--color-error); + &.tall .modal-content { + max-height: min(92vh, 50rem); + } + } - &:hover, - &:focus, - &:active { - background-color: var(--color-error-shade); + .modal-container { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + + display: flex; + align-items: center; + justify-content: center; + } + + &.with-balance-bar { + .modal-container { + flex-direction: column; + } + + .modal-dialog { + margin-top: 0; + } + } + + .modal-backdrop { + position: fixed; + z-index: -1; + top: 0; + right: 0; + bottom: 0; + left: 0; + + background-color: rgba(0, 0, 0, 0.25); + } + + &.transparent-backdrop .modal-backdrop { + background-color: transparent; + } + + .modal-dialog { + position: relative; + transform: translate3d(0, -1rem, 0); + + display: inline-flex; + flex-direction: column; + + width: 100%; + min-width: 17.5rem; + max-width: 35rem; + margin: 2rem auto; + border-radius: var(--border-radius-modal); + + background-color: var(--color-background); + box-shadow: 0 0.25rem 0.5rem 0.125rem var(--color-default-shadow); + + transition: + transform 0.2s ease, + opacity 0.2s ease; + + body.no-page-transitions & { + transform: none !important; + transition: none; + } + + @media (max-width: 450px) { + max-width: calc(100vw - 3rem) !important; + } + } + + &.open .modal-dialog { + transform: translate3d(0, 0, 0); + } + + &.closing .modal-dialog { + transform: translate3d(0, 1rem, 0); + } + + .modal-header, + %modal-header { + display: flex; + flex-shrink: 0; + align-items: center; + padding: 1.3125rem 1.375rem 0; + } + + .modal-title { + overflow: hidden; + flex: 1 1 auto; + + font-size: 1.25rem; + font-weight: var(--font-weight-medium); + text-overflow: ellipsis; + + &:not(:only-child) { + margin: 0 1rem; + } + } + + .modal-header-condensed { + @extend %modal-header; + + min-height: 3.5rem; + padding: 0.375rem 0.75rem !important; + + .modal-action-button { + width: auto; + min-width: 5rem; + height: 2.25rem; + margin-left: auto; + padding-right: 1.25rem; + padding-left: 1.25rem; + + font-size: 0.875rem; + line-height: 1.625rem; + + &.danger { + color: var(--color-white); + background-color: var(--color-error); + + &:hover, + &:focus, + &:active { + background-color: var(--color-error-shade); + } } } } - } - .modal-content { - overflow-y: auto; - flex-grow: 1; + .modal-content { + overflow-y: auto; + flex-grow: 1; - width: 100%; - max-height: 92vh; - padding: 1rem 1.5rem 1.1875rem; + width: 100%; + max-height: min(92vh, 50rem); + padding: 1rem 1.5rem 1.1875rem; - b, - strong { - overflow-wrap: anywhere; + b, + strong { + overflow-wrap: anywhere; + } + } + + .modal-title, + .modal-content, + .modal-content > p { + unicode-bidi: plaintext; + } + + .modal-about { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 5; + + text-overflow: ellipsis; + } + + .modal-help { + font-size: 0.9375rem; + line-height: 1.3; + color: var(--color-text-secondary); + } + + .dialog-buttons { + display: flex; + flex-direction: row-reverse; + flex-wrap: wrap; + gap: 0.5rem 1rem; + justify-content: flex-start; + } + + .dialog-buttons-column { + display: flex; + flex-direction: column; + align-items: flex-end; + } + + .dialog-buttons-centered { + justify-content: center; + } + + .dialog-checkbox { + margin: 1rem -1.125rem; + } + + .dialog-checkbox-group { + margin: 0 -1.125rem 1rem; + } + + .confirm-dialog-button { + width: auto; + height: auto; + + font-weight: var(--font-weight-semibold); + text-align: right; + white-space: pre-wrap; + } + + .dialog-button-spacer { + flex-grow: 1; + } + + .modal-absolute-close-button { + position: absolute; + z-index: 3; + top: 0.875rem; + left: 0.875rem; + } + + .modal-more-button { + position: absolute; + z-index: 3; + top: 0.875rem; + right: 0.875rem; } } - - .modal-title, - .modal-content, - .modal-content > p { - unicode-bidi: plaintext; - } - - .modal-about { - overflow: hidden; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 5; - - text-overflow: ellipsis; - } - - .modal-help { - font-size: 0.9375rem; - line-height: 1.3; - color: var(--color-text-secondary); - } - - .dialog-buttons { - display: flex; - flex-direction: row-reverse; - flex-wrap: wrap; - gap: 0.5rem 1rem; - justify-content: flex-start; - } - - .dialog-buttons-column { - display: flex; - flex-direction: column; - align-items: flex-end; - } - - .dialog-buttons-centered { - justify-content: center; - } - - .dialog-checkbox { - margin: 1rem -1.125rem; - } - - .dialog-checkbox-group { - margin: 0 -1.125rem 1rem; - } - - .confirm-dialog-button { - width: auto; - height: auto; - - font-weight: var(--font-weight-semibold); - text-align: right; - white-space: pre-wrap; - } - - .dialog-button-spacer { - flex-grow: 1; - } - - .modal-absolute-close-button { - position: absolute; - z-index: 3; - top: 0.875rem; - left: 0.875rem; - } - - .modal-more-button { - position: absolute; - z-index: 3; - top: 0.875rem; - right: 0.875rem; - } } diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index 01fe588a1..a748aa553 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -47,13 +47,13 @@ export type OwnProps = { isLowStackPriority?: boolean; dialogContent?: React.ReactNode; moreMenuItems?: TeactNode; - onClose: () => void; - onCloseAnimationEnd?: () => void; - onEnter?: () => void; withBalanceBar?: boolean; currencyInBalanceBar?: 'TON' | 'XTR'; isCondensedHeader?: boolean; noFreezeOnClose?: boolean; + onClose: NoneToVoidFunction; + onCloseAnimationEnd?: NoneToVoidFunction; + onEnter?: NoneToVoidFunction; }; const Modal = (props: OwnProps) => { @@ -61,10 +61,10 @@ const Modal = (props: OwnProps) => { dialogRef, isOpen, noBackdropClose, + noFreezeOnClose, onClose, onCloseAnimationEnd, onEnter, - noFreezeOnClose, } = props; const { @@ -72,8 +72,8 @@ const Modal = (props: OwnProps) => { shouldRender, } = useShowTransition({ isOpen, - onCloseAnimationEnd, withShouldRender: true, + onCloseAnimationEnd, }); const shouldFreeze = !noFreezeOnClose && !isOpen; diff --git a/src/global/actions/api/stars.ts b/src/global/actions/api/stars.ts index c6bc90e9a..c6c81b0aa 100644 --- a/src/global/actions/api/stars.ts +++ b/src/global/actions/api/stars.ts @@ -752,3 +752,20 @@ addActionHandler('loadActiveGiftAuctions', async (global, actions, payload): Pro }; setGlobal(global); }); + +addActionHandler('openGiftPreviewModal', async (global, _actions, payload): Promise => { + const { originGift, tabId = getCurrentTabId() } = payload; + + const giftId = originGift.type === 'starGiftUnique' ? originGift.regularGiftId : originGift.id; + const result = await callApi('fetchStarGiftUpgradeAttributes', { giftId }); + if (!result) return; + + global = getGlobal(); + global = updateTabState(global, { + giftPreviewModal: { + originGift, + attributes: result.attributes, + }, + }, tabId); + setGlobal(global); +}); diff --git a/src/global/actions/ui/stars.ts b/src/global/actions/ui/stars.ts index 5091095dd..0dbbad899 100644 --- a/src/global/actions/ui/stars.ts +++ b/src/global/actions/ui/stars.ts @@ -422,6 +422,8 @@ addTabStateResetterAction('closeGiftResalePriceComposerModal', 'giftResalePriceC addTabStateResetterAction('closeGiftUpgradeModal', 'giftUpgradeModal'); +addTabStateResetterAction('closeGiftPreviewModal', 'giftPreviewModal'); + addActionHandler('closeGiftAuctionModal', (global, _actions, payload): ActionReturnType => { const { shouldKeepAuction, tabId = getCurrentTabId() } = payload || {}; const tabState = selectTabState(global, tabId); diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index cadd74ff6..b4d08f271 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -2740,6 +2740,10 @@ export interface ActionPayloads { gift: ApiStarGiftUnique; } & WithTabId; closeGiftInfoValueModal: WithTabId | undefined; + openGiftPreviewModal: { + originGift: ApiStarGift; + } & WithTabId; + closeGiftPreviewModal: WithTabId | undefined; loadActiveGiftAuctions: undefined; openActiveGiftAuctionsModal: WithTabId | undefined; closeActiveGiftAuctionsModal: WithTabId | undefined; diff --git a/src/global/types/tabState.ts b/src/global/types/tabState.ts index bd599745f..fef732248 100644 --- a/src/global/types/tabState.ts +++ b/src/global/types/tabState.ts @@ -901,6 +901,11 @@ export type TabState = { emojiStatus: ApiEmojiStatusCollectible; }; + giftPreviewModal?: { + attributes: ApiStarGiftAttribute[]; + originGift: ApiStarGift; + }; + giftAuctionModal?: { auctionGiftId: string; sampleAttributes?: ApiStarGiftAttribute[]; diff --git a/src/lib/gramjs/tl/apiTl.ts b/src/lib/gramjs/tl/apiTl.ts index bbd945306..d85e69d31 100644 --- a/src/lib/gramjs/tl/apiTl.ts +++ b/src/lib/gramjs/tl/apiTl.ts @@ -1904,6 +1904,7 @@ payments.getStarGiftAuctionState#5c9ff4d6 auction:InputStarGiftAuction version:i payments.getStarGiftAuctionAcquiredGifts#6ba2cbec gift_id:long = payments.StarGiftAuctionAcquiredGifts; payments.getStarGiftActiveAuctions#a5d0514d hash:long = payments.StarGiftActiveAuctions; payments.resolveStarGiftOffer#e9ce781c flags:# decline:flags.0?true offer_msg_id:int = Updates; +payments.getStarGiftUpgradeAttributes#6d038b58 gift_id:long = payments.StarGiftUpgradeAttributes; phone.requestCall#42ff96ed flags:# video:flags.0?true user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall; phone.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall; phone.confirmCall#2efe1722 peer:InputPhoneCall g_a:bytes key_fingerprint:long protocol:PhoneCallProtocol = phone.PhoneCall; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 66b8701ff..36ce8cf42 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -359,6 +359,7 @@ "payments.getStarGiftAuctionAcquiredGifts", "payments.getStarGiftActiveAuctions", "payments.resolveStarGiftOffer", + "payments.getStarGiftUpgradeAttributes", "langpack.getLangPack", "langpack.getStrings", "langpack.getLanguages", diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss index 0f6d9f6b6..f7e7cb824 100644 --- a/src/styles/_mixins.scss +++ b/src/styles/_mixins.scss @@ -5,12 +5,12 @@ } } -@mixin adapt-padding-to-scrollbar($padding) { - padding-inline-end: calc($padding - var(--scrollbar-width)); +@mixin adapt-padding-to-scrollbar($padding, $forceSpace: 0px) { + padding-inline-end: calc(max($padding - var(--scrollbar-width), $forceSpace)); } -@mixin adapt-margin-to-scrollbar($margin) { - margin-inline-end: calc($margin - var(--scrollbar-width)); +@mixin adapt-margin-to-scrollbar($margin, $forceSpace: 0px) { + margin-inline-end: calc(max($margin - var(--scrollbar-width), $forceSpace)); } @mixin filter-outline($width: 0.125rem, $color) { diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 0de8ccd2d..24dde6e63 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1955,6 +1955,12 @@ export interface LangPair { 'ChatListAuctionOutbid': undefined; 'ChatListAuctionView': undefined; 'BotAuthSuccessTitle': undefined; + 'GiftPreviewSelectedTraits': undefined; + 'GiftUpgradeViewAll': undefined; + 'GiftPreviewToggleCraftableModels': undefined; + 'GiftPreviewToggleRegularModels': undefined; + 'AriaGiftPreviewPlay': undefined; + 'AriaGiftPreviewStop': undefined; } export interface LangPairWithVariables { @@ -3892,6 +3898,18 @@ export interface LangPairPluralWithVariables { 'ChatListAuctionTitle': { 'count': V; }; + 'GiftPreviewCountModels': { + 'count': V; + }; + 'GiftPreviewCountCraftableModels': { + 'count': V; + }; + 'GiftPreviewCountPatterns': { + 'count': V; + }; + 'GiftPreviewCountBackdrops': { + 'count': V; + }; } export type RegularLangKey = keyof LangPair; export type RegularLangKeyWithVariables = keyof LangPairWithVariables;