Gifts: Add attribute preview modal (#6710)
This commit is contained in:
parent
52da4643c1
commit
64c5b740d3
@ -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.
|
||||
|
||||
|
||||
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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<OwnProps> = ({
|
||||
onClick,
|
||||
const PhoneCallButton = ({
|
||||
label,
|
||||
customIcon,
|
||||
icon,
|
||||
@ -31,7 +29,8 @@ const PhoneCallButton: FC<OwnProps> = ({
|
||||
className,
|
||||
isDisabled,
|
||||
isActive,
|
||||
}) => {
|
||||
onClick,
|
||||
}: OwnProps) => {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<Button
|
||||
|
||||
@ -1,21 +1,32 @@
|
||||
.root {
|
||||
&.uncommon {
|
||||
color: var(--color-gift-uncommon);
|
||||
background-color: var(--color-gift-uncommon-bg);
|
||||
}
|
||||
.crafted {
|
||||
--_color: revert;
|
||||
--_background-color: revert;
|
||||
|
||||
&.rare {
|
||||
color: var(--color-gift-rare);
|
||||
background-color: var(--color-gift-rare-bg);
|
||||
}
|
||||
|
||||
&.epic {
|
||||
color: var(--color-gift-epic);
|
||||
background-color: var(--color-gift-epic-bg);
|
||||
}
|
||||
|
||||
&.legendary {
|
||||
color: var(--color-gift-legendary);
|
||||
background-color: var(--color-gift-legendary-bg);
|
||||
}
|
||||
color: var(--_color);
|
||||
background-color: var(--_background-color);
|
||||
}
|
||||
|
||||
.inverted {
|
||||
color: var(--color-white);
|
||||
background-color: var(--_color);
|
||||
}
|
||||
|
||||
.uncommon {
|
||||
--_color: var(--color-gift-uncommon);
|
||||
--_background-color: var(--color-gift-uncommon-bg);
|
||||
}
|
||||
|
||||
.rare {
|
||||
--_color: var(--color-gift-rare);
|
||||
--_background-color: var(--color-gift-rare-bg);
|
||||
}
|
||||
|
||||
.epic {
|
||||
--_color: var(--color-gift-epic);
|
||||
--_background-color: var(--color-gift-epic-bg);
|
||||
}
|
||||
|
||||
.legendary {
|
||||
--_color: var(--color-gift-legendary);
|
||||
--_background-color: var(--color-gift-legendary-bg);
|
||||
}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import { memo } from '../../lib/teact/teact';
|
||||
|
||||
import type { ApiStarGiftAttributeRarity } from '../../api/types';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
@ -13,16 +11,28 @@ import styles from './GiftRarityBadge.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
rarity: ApiStarGiftAttributeRarity;
|
||||
shouldInvertRare?: boolean;
|
||||
className?: string;
|
||||
onClick?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
const GiftRarityBadge = ({ rarity }: OwnProps) => {
|
||||
const GiftRarityBadge = ({ rarity, shouldInvertRare, className, onClick }: OwnProps) => {
|
||||
const lang = useLang();
|
||||
|
||||
return (
|
||||
<BadgeButton className={buildClassName(styles.root, rarity.type !== 'regular' && styles[rarity.type])}>
|
||||
<BadgeButton
|
||||
className={buildClassName(
|
||||
styles.root,
|
||||
rarity.type !== 'regular' && styles[rarity.type],
|
||||
rarity.type !== 'regular' && styles.crafted,
|
||||
shouldInvertRare && rarity.type !== 'regular' && styles.inverted,
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{getGiftRarityTitle(lang, rarity)}
|
||||
</BadgeButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(GiftRarityBadge);
|
||||
export default GiftRarityBadge;
|
||||
|
||||
@ -32,6 +32,7 @@ import GiftInfoModal from './gift/info/GiftInfoModal.async';
|
||||
import GiftLockedModal from './gift/locked/GiftLockedModal.async';
|
||||
import GiftDescriptionRemoveModal from './gift/message/GiftDescriptionRemoveModal.async';
|
||||
import GiftOfferAcceptModal from './gift/offer/GiftOfferAcceptModal.async';
|
||||
import GiftPreviewModal from './gift/preview/GiftPreviewModal.async';
|
||||
import GiftRecipientPicker from './gift/recipient/GiftRecipientPicker.async';
|
||||
import GiftResalePriceComposerModal from './gift/resale/GiftResalePriceComposerModal.async';
|
||||
import StarGiftPriceDecreaseInfoModal from './gift/StarGiftPriceDecreaseInfoModal.async';
|
||||
@ -105,6 +106,7 @@ type ModalKey = keyof Pick<TabState,
|
||||
'emojiStatusAccessModal' |
|
||||
'locationAccessModal' |
|
||||
'aboutAdsModal' |
|
||||
'giftPreviewModal' |
|
||||
'giftUpgradeModal' |
|
||||
'giftAuctionModal' |
|
||||
'giftAuctionBidModal' |
|
||||
@ -184,6 +186,7 @@ const MODALS: ModalRegistry = {
|
||||
emojiStatusAccessModal: EmojiStatusAccessModal,
|
||||
locationAccessModal: LocationAccessModal,
|
||||
aboutAdsModal: AboutAdsModal,
|
||||
giftPreviewModal: GiftPreviewModal,
|
||||
giftUpgradeModal: GiftUpgradeModal,
|
||||
giftAuctionModal: GiftAuctionModal,
|
||||
giftAuctionBidModal: GiftAuctionBidModal,
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
max-height: min(92vh, 45rem) !important;
|
||||
padding-inline: 1rem !important;
|
||||
}
|
||||
|
||||
|
||||
91
src/components/modals/gift/GiftAttributeItem.module.scss
Normal file
91
src/components/modals/gift/GiftAttributeItem.module.scss
Normal file
@ -0,0 +1,91 @@
|
||||
.root {
|
||||
--_selected-color: var(--color-primary);
|
||||
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding: 0.875rem;
|
||||
border-radius: 1rem;
|
||||
|
||||
color: #fff;
|
||||
|
||||
background-color: var(--color-background-secondary);
|
||||
outline: 0.125rem solid transparent;
|
||||
|
||||
transition: outline-color 0.15s ease-out;
|
||||
|
||||
&::before {
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
border-radius: inherit;
|
||||
|
||||
opacity: 0;
|
||||
background-color: var(--color-hover-overlay);
|
||||
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
&::after {
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
inset: 0.125rem;
|
||||
|
||||
border: 0.125rem solid transparent;
|
||||
border-radius: 0.9375rem;
|
||||
|
||||
transition: border-color 0.15s linear;
|
||||
}
|
||||
|
||||
&.hasBackground {
|
||||
--_selected-color: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
:global(html.theme-dark) & {
|
||||
background-color: rgb(33, 33, 33);
|
||||
|
||||
&.hasBackground {
|
||||
--_selected-color: rgb(33, 33, 33);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.selected::after {
|
||||
border-color: var(--_selected-color);
|
||||
}
|
||||
|
||||
.radialPattern {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.stickerWrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rarity {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.4375rem;
|
||||
backdrop-filter: blur(0.5rem);
|
||||
}
|
||||
|
||||
.regular {
|
||||
color: white;
|
||||
backdrop-filter: blur(0.5rem) brightness(0.8);
|
||||
}
|
||||
123
src/components/modals/gift/GiftAttributeItem.tsx
Normal file
123
src/components/modals/gift/GiftAttributeItem.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import type { ElementRef, TeactNode } from '@teact';
|
||||
import { memo, useMemo, useRef } from '@teact';
|
||||
|
||||
import type {
|
||||
ApiStarGiftAttributeBackdrop,
|
||||
ApiStarGiftAttributeRarity,
|
||||
ApiSticker,
|
||||
} from '../../../api/types';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import useDynamicColorListener from '../../../hooks/stickers/useDynamicColorListener';
|
||||
import { type ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import GiftRarityBadge from '../../common/GiftRarityBadge';
|
||||
import RadialPatternBackground from '../../common/profile/RadialPatternBackground';
|
||||
import StickerView from '../../common/StickerView';
|
||||
|
||||
import styles from './GiftAttributeItem.module.scss';
|
||||
|
||||
type OwnProps<T> = {
|
||||
ref?: ElementRef<HTMLDivElement>;
|
||||
children?: TeactNode;
|
||||
backdrop?: ApiStarGiftAttributeBackdrop;
|
||||
patternSticker?: ApiSticker;
|
||||
sticker?: ApiSticker;
|
||||
stickerSize?: number;
|
||||
stickerNoPlay?: boolean;
|
||||
rarity?: ApiStarGiftAttributeRarity;
|
||||
isSelected?: boolean;
|
||||
className?: string;
|
||||
clickArg?: T;
|
||||
observeIntersection?: ObserveFn;
|
||||
onClick?: (arg: T) => void;
|
||||
};
|
||||
|
||||
const DEFAULT_STICKER_SIZE = 90;
|
||||
|
||||
const GiftAttributeItem = <T,>({
|
||||
ref,
|
||||
backdrop,
|
||||
patternSticker,
|
||||
sticker,
|
||||
stickerSize = DEFAULT_STICKER_SIZE,
|
||||
stickerNoPlay,
|
||||
rarity,
|
||||
isSelected,
|
||||
className,
|
||||
clickArg,
|
||||
children,
|
||||
observeIntersection,
|
||||
onClick,
|
||||
}: OwnProps<T>) => {
|
||||
const stickerRef = useRef<HTMLDivElement>();
|
||||
|
||||
const customColor = useDynamicColorListener(stickerRef, undefined, !sticker?.shouldUseTextColor);
|
||||
|
||||
const radialPatternBackdrop = useMemo(() => {
|
||||
if (!backdrop) return undefined;
|
||||
|
||||
const backdropColors: [string, string] = [backdrop.centerColor, backdrop.edgeColor];
|
||||
|
||||
return (
|
||||
<RadialPatternBackground
|
||||
className={styles.radialPattern}
|
||||
backgroundColors={backdropColors}
|
||||
patternIcon={patternSticker}
|
||||
ringsCount={1}
|
||||
ovalFactor={1}
|
||||
/>
|
||||
);
|
||||
}, [backdrop, patternSticker]);
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
onClick?.(clickArg!);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={buildClassName(
|
||||
styles.root,
|
||||
isSelected && styles.selected,
|
||||
radialPatternBackdrop && styles.hasBackground,
|
||||
className,
|
||||
)}
|
||||
role={onClick ? 'button' : undefined}
|
||||
tabIndex={onClick ? 0 : undefined}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{radialPatternBackdrop}
|
||||
|
||||
<div
|
||||
ref={stickerRef}
|
||||
className={styles.stickerWrapper}
|
||||
style={`width: ${stickerSize}px; height: ${stickerSize}px`}
|
||||
>
|
||||
{sticker && (
|
||||
<StickerView
|
||||
containerRef={stickerRef}
|
||||
sticker={sticker}
|
||||
size={stickerSize}
|
||||
observeIntersectionForPlaying={observeIntersection}
|
||||
observeIntersectionForLoading={observeIntersection}
|
||||
shouldPreloadPreview
|
||||
noPlay={stickerNoPlay}
|
||||
customColor={customColor}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{rarity && (
|
||||
<GiftRarityBadge
|
||||
rarity={rarity}
|
||||
className={buildClassName(styles.rarity, rarity.type === 'regular' && styles.regular)}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(GiftAttributeItem);
|
||||
@ -51,6 +51,8 @@
|
||||
}
|
||||
|
||||
.starGift {
|
||||
overflow: visible;
|
||||
flex-basis: auto;
|
||||
padding: 0.875rem;
|
||||
padding-bottom: 0.625rem;
|
||||
}
|
||||
@ -90,11 +92,11 @@
|
||||
|
||||
.auction,
|
||||
.premiumRequired {
|
||||
outline: 0.125rem solid #D18D21;
|
||||
outline: 0.125rem solid #D18D21 !important;
|
||||
outline-offset: -0.125rem;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 0.125rem solid #D18D21;
|
||||
outline: 0.125rem solid #D18D21 !important;
|
||||
outline-offset: -0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,22 +5,18 @@ import type { ApiStarGift, ApiTypeCurrencyAmount } from '../../../api/types';
|
||||
|
||||
import { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../../config';
|
||||
import { selectIsCurrentUserPremium } from '../../../global/selectors';
|
||||
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment.ts';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatStarsAsIcon, formatTonAsIcon } from '../../../util/localization/format';
|
||||
|
||||
import Icon from '../../common/icons/Icon'; ;
|
||||
import { getGiftAttributes, getStickerFromGift } from '../../common/helpers/gifts';
|
||||
|
||||
import useFlag from '../../../hooks/useFlag.ts';
|
||||
import { type ObserveFn, useOnIntersect } from '../../../hooks/useIntersectionObserver';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import GiftRibbon from '../../common/gift/GiftRibbon';
|
||||
import RadialPatternBackground from '../../common/profile/RadialPatternBackground';
|
||||
import StickerView from '../../common/StickerView';
|
||||
import Icon from '../../common/icons/Icon';
|
||||
import Button from '../../ui/Button';
|
||||
import GiftAttributeItem from './GiftAttributeItem';
|
||||
|
||||
import styles from './GiftItem.module.scss';
|
||||
|
||||
@ -48,7 +44,6 @@ function GiftItemStar({
|
||||
} = getActions();
|
||||
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
const stickerRef = useRef<HTMLDivElement>();
|
||||
|
||||
function getPriceAmount(amounts?: ApiTypeCurrencyAmount[]) {
|
||||
if (!amounts) return { amount: 0, currency: STARS_CURRENCY_CODE };
|
||||
@ -65,7 +60,6 @@ function GiftItemStar({
|
||||
const lang = useLang();
|
||||
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isHover, markHover, unmarkHover] = useFlag();
|
||||
|
||||
const sticker = getStickerFromGift(gift);
|
||||
const isGiftUnique = gift.type === 'starGiftUnique';
|
||||
@ -132,38 +126,19 @@ function GiftItemStar({
|
||||
onClick?.(gift, isResale ? 'resell' : 'original');
|
||||
});
|
||||
|
||||
const radialPatternBackdrop = useMemo(() => {
|
||||
const { backdrop, pattern } = getGiftAttributes(gift) || {};
|
||||
|
||||
if (!backdrop || !pattern) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const backdropColors = [backdrop.centerColor, backdrop.edgeColor];
|
||||
|
||||
return (
|
||||
<RadialPatternBackground
|
||||
className={styles.radialPattern}
|
||||
backgroundColors={backdropColors}
|
||||
patternIcon={pattern.sticker}
|
||||
ringsCount={1}
|
||||
ovalFactor={1}
|
||||
/>
|
||||
);
|
||||
}, [gift]);
|
||||
const giftAttrs = useMemo(() => getGiftAttributes(gift), [gift]);
|
||||
|
||||
const giftNumber = isGiftUnique ? gift.number : 0;
|
||||
const isLocked = Boolean(gift.type === 'starGift' && gift.lockedUntilDate);
|
||||
|
||||
const giftRibbon = useMemo(() => {
|
||||
if (isGiftUnique) {
|
||||
const { backdrop } = getGiftAttributes(gift) || {};
|
||||
if (!backdrop) {
|
||||
if (!giftAttrs?.backdrop) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
<GiftRibbon
|
||||
color={[backdrop.centerColor, backdrop.edgeColor]}
|
||||
color={[giftAttrs.backdrop.centerColor, giftAttrs.backdrop.edgeColor]}
|
||||
text={
|
||||
lang('GiftSavedNumber', { number: giftNumber })
|
||||
}
|
||||
@ -186,7 +161,7 @@ function GiftItemStar({
|
||||
return <GiftRibbon color="blue" text={lang('GiftLimited')} />;
|
||||
}
|
||||
return undefined;
|
||||
}, [isGiftUnique, isResale, gift, isSoldOut,
|
||||
}, [isGiftUnique, isResale, giftAttrs, isSoldOut,
|
||||
isLimited, lang, giftNumber, isPremiumRequired, isAuction]);
|
||||
|
||||
useOnIntersect(ref, observeIntersection, (entry) => {
|
||||
@ -217,8 +192,13 @@ function GiftItemStar({
|
||||
}, [withTransferBadge, priceCurrency, formattedPrice, isAuction, lang]);
|
||||
|
||||
return (
|
||||
<div
|
||||
<GiftAttributeItem
|
||||
ref={ref}
|
||||
backdrop={giftAttrs?.backdrop}
|
||||
patternSticker={giftAttrs?.pattern?.sticker}
|
||||
sticker={sticker}
|
||||
stickerSize={GIFT_STICKER_SIZE}
|
||||
observeIntersection={observeIntersection}
|
||||
className={buildClassName(
|
||||
'interactive-gift',
|
||||
styles.container,
|
||||
@ -228,32 +208,8 @@ function GiftItemStar({
|
||||
isAuction && styles.auction,
|
||||
noClickable && styles.noClickable,
|
||||
)}
|
||||
tabIndex={noClickable ? undefined : 0}
|
||||
role={noClickable ? undefined : 'button'}
|
||||
onClick={noClickable ? undefined : handleGiftClick}
|
||||
onMouseEnter={!IS_TOUCH_ENV && !noClickable ? markHover : undefined}
|
||||
onMouseLeave={!IS_TOUCH_ENV && !noClickable ? unmarkHover : undefined}
|
||||
>
|
||||
{radialPatternBackdrop}
|
||||
|
||||
<div
|
||||
ref={stickerRef}
|
||||
className={styles.stickerWrapper}
|
||||
style={`width: ${GIFT_STICKER_SIZE}px; height: ${GIFT_STICKER_SIZE}px`}
|
||||
>
|
||||
{sticker && (
|
||||
<StickerView
|
||||
observeIntersectionForPlaying={observeIntersection}
|
||||
observeIntersectionForLoading={observeIntersection}
|
||||
containerRef={stickerRef}
|
||||
sticker={sticker}
|
||||
size={GIFT_STICKER_SIZE}
|
||||
shouldLoop={isHover}
|
||||
shouldPreloadPreview
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
{!hideBadge && (
|
||||
<Button
|
||||
className={buildClassName(
|
||||
@ -272,7 +228,7 @@ function GiftItemStar({
|
||||
)}
|
||||
{giftRibbon}
|
||||
{isLocked && <Icon name="lock-badge" className={styles.lockIcon} />}
|
||||
</div>
|
||||
</GiftAttributeItem>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 (
|
||||
<div className={buildClassName(styles.root,
|
||||
'interactive-gift',
|
||||
showManageButtons && styles.withManageButtons,
|
||||
badge && styles.withBadge,
|
||||
className)}
|
||||
<div
|
||||
className={buildClassName(styles.root,
|
||||
'interactive-gift',
|
||||
showManageButtons && styles.withManageButtons,
|
||||
badge && styles.withBadge,
|
||||
className)}
|
||||
style={buildStyle(subtitleColor && `--tinted-text-color: ${subtitleColor}`)}
|
||||
>
|
||||
<Transition
|
||||
className={styles.transition}
|
||||
@ -118,7 +124,6 @@ const UniqueGiftHeader = ({
|
||||
{Boolean(subtitle) && (
|
||||
<div
|
||||
className={buildClassName(styles.subtitle, subtitlePeer && styles.subtitleBadge)}
|
||||
style={buildStyle(subtitleColor && `color: ${subtitleColor}`)}
|
||||
onClick={() => {
|
||||
if (subtitlePeer) {
|
||||
openChat({ id: subtitlePeer.id });
|
||||
@ -142,6 +147,7 @@ const UniqueGiftHeader = ({
|
||||
{resellPrice.currency === 'TON' && <Icon name="toncoin" />}
|
||||
</p>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
.root {
|
||||
z-index: calc(var(--z-modal-low-priority) + 1);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@ -333,6 +333,7 @@ const GiftAuctionBidModal = ({
|
||||
isOpen={isOpen}
|
||||
hasAbsoluteCloseButton
|
||||
isSlim
|
||||
className={styles.root}
|
||||
contentClassName={styles.content}
|
||||
onClose={closeGiftAuctionBidModal}
|
||||
isLowStackPriority
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
.modalContent {
|
||||
position: relative;
|
||||
max-height: min(97vh, 48rem) !important;
|
||||
max-height: min(97vh, 50rem) !important;
|
||||
}
|
||||
|
||||
.header {
|
||||
|
||||
@ -56,11 +56,6 @@
|
||||
color: var(--color-error) !important;
|
||||
}
|
||||
|
||||
.modalContent {
|
||||
position: relative;
|
||||
max-height: min(97vh, 48rem) !important;
|
||||
}
|
||||
|
||||
.moreMenuButton {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
@ -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<HTMLDivElement>();
|
||||
|
||||
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'),
|
||||
<span className={styles.uniqueAttribute}>
|
||||
<span
|
||||
className={styles.attributeName}
|
||||
onClick={handleModelClick}
|
||||
>
|
||||
{model.name}
|
||||
</span>
|
||||
<span className={styles.uniqueAttribute} onClick={handleOpenPreviewModal}>
|
||||
<span className={styles.attributeName}>{model.name}</span>
|
||||
<GiftRarityBadge rarity={model.rarity} />
|
||||
</span>,
|
||||
]);
|
||||
@ -724,14 +674,9 @@ const GiftInfoModal = ({
|
||||
if (backdrop) {
|
||||
tableData.push([
|
||||
lang('GiftAttributeBackdrop'),
|
||||
<span className={styles.uniqueAttribute}>
|
||||
<span
|
||||
className={styles.attributeName}
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
{backdrop.name}
|
||||
</span>
|
||||
<BadgeButton>{getGiftRarityTitle(lang, backdrop.rarity)}</BadgeButton>
|
||||
<span className={styles.uniqueAttribute} onClick={handleOpenPreviewModal}>
|
||||
<span className={styles.attributeName}>{backdrop.name}</span>
|
||||
<GiftRarityBadge rarity={backdrop.rarity} />
|
||||
</span>,
|
||||
]);
|
||||
}
|
||||
@ -739,14 +684,9 @@ const GiftInfoModal = ({
|
||||
if (pattern) {
|
||||
tableData.push([
|
||||
lang('GiftAttributeSymbol'),
|
||||
<span className={styles.uniqueAttribute}>
|
||||
<span
|
||||
className={styles.attributeName}
|
||||
onClick={handleSymbolClick}
|
||||
>
|
||||
{pattern.name}
|
||||
</span>
|
||||
<BadgeButton>{getGiftRarityTitle(lang, pattern.rarity)}</BadgeButton>
|
||||
<span className={styles.uniqueAttribute} onClick={handleOpenPreviewModal}>
|
||||
<span className={styles.attributeName}>{pattern.name}</span>
|
||||
<GiftRarityBadge rarity={pattern.rarity} />
|
||||
</span>,
|
||||
]);
|
||||
}
|
||||
@ -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}
|
||||
|
||||
@ -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'),
|
||||
<span className={styles.attributeValue}>
|
||||
<span>{model.name}</span>
|
||||
<BadgeButton>{getGiftRarityTitle(lang, model.rarity)}</BadgeButton>
|
||||
<GiftRarityBadge rarity={model.rarity} />
|
||||
</span>,
|
||||
]);
|
||||
}
|
||||
@ -95,7 +95,7 @@ const GiftOfferAcceptModal = ({
|
||||
lang('GiftAttributeBackdrop'),
|
||||
<span className={styles.attributeValue}>
|
||||
<span>{backdrop.name}</span>
|
||||
<BadgeButton>{getGiftRarityTitle(lang, backdrop.rarity)}</BadgeButton>
|
||||
<GiftRarityBadge rarity={backdrop.rarity} />
|
||||
</span>,
|
||||
]);
|
||||
}
|
||||
@ -105,7 +105,7 @@ const GiftOfferAcceptModal = ({
|
||||
lang('GiftAttributeSymbol'),
|
||||
<span className={styles.attributeValue}>
|
||||
<span>{pattern.name}</span>
|
||||
<BadgeButton>{getGiftRarityTitle(lang, pattern.rarity)}</BadgeButton>
|
||||
<GiftRarityBadge rarity={pattern.rarity} />
|
||||
</span>,
|
||||
]);
|
||||
}
|
||||
|
||||
@ -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 ? <GiftPreviewModal {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default GiftPreviewModalAsync;
|
||||
110
src/components/modals/gift/preview/GiftPreviewModal.module.scss
Normal file
110
src/components/modals/gift/preview/GiftPreviewModal.module.scss
Normal file
@ -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;
|
||||
}
|
||||
490
src/components/modals/gift/preview/GiftPreviewModal.tsx
Normal file
490
src/components/modals/gift/preview/GiftPreviewModal.tsx
Normal file
@ -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<HTMLDivElement>();
|
||||
const patternsContainerRef = useRef<HTMLDivElement>();
|
||||
const backdropsContainerRef = useRef<HTMLDivElement>();
|
||||
|
||||
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<ApiStarGiftAttributeModel | undefined>(firstModel);
|
||||
const [selectedPattern, setSelectedPattern] = useState<ApiStarGiftAttributePattern | undefined>(firstPattern);
|
||||
const [selectedBackdrop, setSelectedBackdrop] = useState<ApiStarGiftAttributeBackdrop | undefined>(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<TabWithProperties[]>(() => [
|
||||
{ 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 (
|
||||
<>
|
||||
<Button
|
||||
className="modal-absolute-close-button"
|
||||
round
|
||||
color="translucent-white"
|
||||
size="tiny"
|
||||
iconName="close"
|
||||
ariaLabel={lang('Close')}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
<Button
|
||||
className={styles.playButton}
|
||||
round
|
||||
color="translucent-white"
|
||||
size="tiny"
|
||||
iconName={isPlayingRandomPreviews ? 'pause' : 'play'}
|
||||
ariaLabel={isPlayingRandomPreviews ? lang('AriaGiftPreviewStop') : lang('AriaGiftPreviewPlay')}
|
||||
onClick={isPlayingRandomPreviews ? stopRandomPreviews : playRandomPreviews}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}, [lang, isPlayingRandomPreviews]);
|
||||
|
||||
function renderHeader() {
|
||||
if (!selectedModel || !selectedPattern || !selectedBackdrop) return undefined;
|
||||
return (
|
||||
<UniqueGiftHeader
|
||||
className={styles.header}
|
||||
modelAttribute={selectedModel}
|
||||
backdropAttribute={selectedBackdrop}
|
||||
patternAttribute={selectedPattern}
|
||||
title={originGift?.title}
|
||||
subtitle={lang('GiftPreviewSelectedTraits')}
|
||||
>
|
||||
<div
|
||||
className={styles.traitButtons}
|
||||
style={`--_badge-bg: ${selectedBackdrop.centerColor}`}
|
||||
>
|
||||
<Button
|
||||
className={styles.traitButton}
|
||||
color="transparentBlured"
|
||||
onClick={handleModelClick}
|
||||
>
|
||||
<span className={styles.traitName}>{selectedModel.name}</span>
|
||||
<span className={styles.traitType}>{lang('GiftAttributeModel')}</span>
|
||||
<GiftRarityBadge
|
||||
shouldInvertRare
|
||||
rarity={selectedModel.rarity}
|
||||
className={styles.traitRarity}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.traitButton}
|
||||
color="transparentBlured"
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
<span className={styles.traitName}>{selectedBackdrop.name}</span>
|
||||
<span className={styles.traitType}>{lang('GiftAttributeBackdrop')}</span>
|
||||
<GiftRarityBadge
|
||||
shouldInvertRare
|
||||
rarity={selectedBackdrop.rarity}
|
||||
className={styles.traitRarity}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.traitButton}
|
||||
color="transparentBlured"
|
||||
onClick={handleSymbolClick}
|
||||
>
|
||||
<span className={styles.traitName}>{selectedPattern.name}</span>
|
||||
<span className={styles.traitType}>{lang('GiftAttributeSymbol')}</span>
|
||||
<GiftRarityBadge
|
||||
shouldInvertRare
|
||||
rarity={selectedPattern.rarity}
|
||||
className={styles.traitRarity}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</UniqueGiftHeader>
|
||||
);
|
||||
}
|
||||
|
||||
function renderTabContent() {
|
||||
switch (selectedTabIndex) {
|
||||
case AttributeTab.Model:
|
||||
return (
|
||||
<InfiniteScroll
|
||||
ref={modelsContainerRef}
|
||||
className={buildClassName(styles.grid, 'custom-scroll')}
|
||||
beforeChildren={(
|
||||
<div className={styles.gridHeader}>
|
||||
<span className={styles.count}>
|
||||
{lang(
|
||||
isCraftableModelsMode ? 'GiftPreviewCountCraftableModels' : 'GiftPreviewCountModels',
|
||||
{ count: isCraftableModelsMode ? craftableModels.length : regularModels.length }, {
|
||||
pluralValue: isCraftableModelsMode ? craftableModels.length : regularModels.length,
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
})}
|
||||
</span>
|
||||
{Boolean(craftableModels?.length) && (
|
||||
<Link
|
||||
isPrimary
|
||||
onClick={() => isCraftableModelsMode ? showRegularModels() : showCraftableModels()}
|
||||
>
|
||||
{lang(
|
||||
isCraftableModelsMode ? 'GiftPreviewToggleRegularModels' : 'GiftPreviewToggleCraftableModels',
|
||||
undefined,
|
||||
{ withNodes: true, specialReplacement: getNextArrowReplacement() },
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
items={isCraftableModelsMode ? craftableModels : regularModels}
|
||||
noFastList
|
||||
>
|
||||
{(isCraftableModelsMode ? craftableModels : regularModels).map((model) => (
|
||||
<GiftAttributeItem
|
||||
className={styles.item}
|
||||
key={model.name}
|
||||
sticker={model.sticker}
|
||||
stickerSize={MODEL_STICKER_SIZE}
|
||||
rarity={model.rarity}
|
||||
isSelected={selectedModel?.name === model.name}
|
||||
clickArg={model}
|
||||
onClick={handleSelectModel}
|
||||
observeIntersection={observeModelsIntersection}
|
||||
/>
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
);
|
||||
case AttributeTab.Pattern:
|
||||
return (
|
||||
<InfiniteScroll
|
||||
ref={patternsContainerRef}
|
||||
className={buildClassName(styles.grid, 'custom-scroll')}
|
||||
beforeChildren={(
|
||||
<div className={styles.gridHeader}>
|
||||
<span className={styles.count}>
|
||||
{lang('GiftPreviewCountPatterns', { count: patterns.length }, {
|
||||
pluralValue: patterns.length,
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
items={patterns}
|
||||
noFastList
|
||||
>
|
||||
{patterns.map((pattern) => (
|
||||
<GiftAttributeItem
|
||||
key={pattern.name}
|
||||
className={styles.item}
|
||||
backdrop={selectedBackdrop}
|
||||
sticker={pattern.sticker}
|
||||
patternSticker={pattern.sticker}
|
||||
stickerNoPlay
|
||||
stickerSize={PATTERN_STICKER_SIZE}
|
||||
rarity={pattern.rarity}
|
||||
isSelected={selectedPattern?.name === pattern.name}
|
||||
clickArg={pattern}
|
||||
onClick={handleSelectPattern}
|
||||
observeIntersection={observePatternsIntersection}
|
||||
/>
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
);
|
||||
case AttributeTab.Backdrop:
|
||||
return (
|
||||
<InfiniteScroll
|
||||
ref={backdropsContainerRef}
|
||||
className={buildClassName(styles.grid, 'custom-scroll')}
|
||||
beforeChildren={(
|
||||
<div className={styles.gridHeader}>
|
||||
<span className={styles.count}>
|
||||
{lang('GiftPreviewCountBackdrops', { count: backdrops.length }, {
|
||||
pluralValue: backdrops.length,
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
items={backdrops}
|
||||
noFastList
|
||||
>
|
||||
{backdrops.map((backdrop) => (
|
||||
<GiftAttributeItem
|
||||
key={backdrop.backdropId}
|
||||
className={styles.item}
|
||||
backdrop={backdrop}
|
||||
sticker={selectedModel?.sticker}
|
||||
stickerNoPlay
|
||||
patternSticker={selectedPattern?.sticker}
|
||||
rarity={backdrop.rarity}
|
||||
isSelected={selectedBackdrop?.backdropId === backdrop.backdropId}
|
||||
clickArg={backdrop}
|
||||
onClick={handleSelectBackdrop}
|
||||
observeIntersection={observeBackdropsIntersection}
|
||||
/>
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
className={buildClassName(styles.root, 'tall')}
|
||||
hasAbsoluteCloseButton
|
||||
absoluteCloseButtonColor="translucent-white"
|
||||
dialogClassName={styles.dialog}
|
||||
contentClassName={styles.content}
|
||||
header={modalHeader}
|
||||
isSlim
|
||||
onClose={handleClose}
|
||||
>
|
||||
{renderHeader()}
|
||||
<TabList
|
||||
className={styles.tabs}
|
||||
activeTab={selectedTabIndex}
|
||||
tabs={tabs}
|
||||
onSwitchTab={setSelectedTabIndex}
|
||||
/>
|
||||
<Transition
|
||||
className={styles.transition}
|
||||
name={resolveTransitionName('slideOptimized', animationLevel, undefined, lang.isRtl)}
|
||||
activeKey={selectedTabIndex}
|
||||
renderCount={tabs.length}
|
||||
>
|
||||
{renderTabContent()}
|
||||
</Transition>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): Complete<StateProps> => {
|
||||
return {
|
||||
animationLevel: selectAnimationLevel(global),
|
||||
};
|
||||
},
|
||||
)(GiftPreviewModal));
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 = (
|
||||
<div className={styles.subtitle}>
|
||||
<span className={styles.subtitleText}>
|
||||
{subtitleText}
|
||||
</span>
|
||||
<Button
|
||||
size="tiny"
|
||||
color="transparentBlured"
|
||||
pill
|
||||
fluid
|
||||
className={styles.viewAllButton}
|
||||
onClick={handleViewAllVariants}
|
||||
>
|
||||
{previewModels?.map((model) => (
|
||||
<CustomEmoji
|
||||
sticker={model.sticker}
|
||||
noPlay
|
||||
/>
|
||||
))}
|
||||
<span className={styles.viewAllText}>
|
||||
{lang('GiftUpgradeViewAll', undefined, { withNodes: true, specialReplacement: getNextArrowReplacement() })}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const hasPriceDecreaseInfo = Boolean(nextPriceDate)
|
||||
&& Boolean(renderingModal?.prices?.length)
|
||||
&& !gift?.alreadyPaidUpgradeStars;
|
||||
|
||||
const header = (
|
||||
<UniqueGiftHeader
|
||||
className={styles.header}
|
||||
modelAttribute={previewAttributes.model}
|
||||
backdropAttribute={previewAttributes.backdrop}
|
||||
patternAttribute={previewAttributes.pattern}
|
||||
@ -227,11 +268,10 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
|
||||
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 (
|
||||
<TableAboutModal
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import type { MouseEvent as ReactMouseEvent } from 'react';
|
||||
import type { ElementRef, FC } from '../../lib/teact/teact';
|
||||
import type React from '../../lib/teact/teact';
|
||||
import type { ElementRef, TeactNode } from '../../lib/teact/teact';
|
||||
import { useRef, useState } from '../../lib/teact/teact';
|
||||
|
||||
import type { IconName } from '../../types/icons';
|
||||
@ -22,7 +21,7 @@ import './Button.scss';
|
||||
export type OwnProps = {
|
||||
ref?: ElementRef<HTMLButtonElement | HTMLAnchorElement>;
|
||||
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<OwnProps> = ({
|
||||
const Button = ({
|
||||
ref,
|
||||
type = 'button',
|
||||
id,
|
||||
@ -125,7 +124,7 @@ const Button: FC<OwnProps> = ({
|
||||
onMouseLeave,
|
||||
onFocus,
|
||||
onTransitionEnd,
|
||||
}) => {
|
||||
}: OwnProps) => {
|
||||
let elementRef = useRef<HTMLButtonElement | HTMLAnchorElement>();
|
||||
if (ref) {
|
||||
elementRef = ref;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -752,3 +752,20 @@ addActionHandler('loadActiveGiftAuctions', async (global, actions, payload): Pro
|
||||
};
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('openGiftPreviewModal', async (global, _actions, payload): Promise<void> => {
|
||||
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);
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -901,6 +901,11 @@ export type TabState = {
|
||||
emojiStatus: ApiEmojiStatusCollectible;
|
||||
};
|
||||
|
||||
giftPreviewModal?: {
|
||||
attributes: ApiStarGiftAttribute[];
|
||||
originGift: ApiStarGift;
|
||||
};
|
||||
|
||||
giftAuctionModal?: {
|
||||
auctionGiftId: string;
|
||||
sampleAttributes?: ApiStarGiftAttribute[];
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -359,6 +359,7 @@
|
||||
"payments.getStarGiftAuctionAcquiredGifts",
|
||||
"payments.getStarGiftActiveAuctions",
|
||||
"payments.resolveStarGiftOffer",
|
||||
"payments.getStarGiftUpgradeAttributes",
|
||||
"langpack.getLangPack",
|
||||
"langpack.getStrings",
|
||||
"langpack.getLanguages",
|
||||
|
||||
@ -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) {
|
||||
|
||||
18
src/types/language.d.ts
vendored
18
src/types/language.d.ts
vendored
@ -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<V = LangVariable> {
|
||||
@ -3892,6 +3898,18 @@ export interface LangPairPluralWithVariables<V = LangVariable> {
|
||||
'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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user