Star Gift: Support craft (#6713)

This commit is contained in:
Alexander Zinchuk 2026-02-27 19:51:28 +01:00
parent a29f92b1b2
commit d7a4621ad7
64 changed files with 5339 additions and 1220 deletions

107
CLAUDE.md
View File

@ -420,3 +420,110 @@ lang('MarkdownKey', undefined, { withNodes: true, withMarkdown: true });
**7. Beyond React**
Use `getTranslationFn()` to grab the same `lang` function in non-component code. Discouraged, use object syntax.
# ⚠️ IMPORTANT: Fasterdom & Rendering Phases
## Rendering Cycle
```
--- frame start ---
1. effects
2. requested measures (DOM reads)
3. render JSX → DOM
4. layout effects
5. requested mutations (DOM writes)
6. forced reflow measure (avoid!)
7. forced reflow mutate (avoid!)
--- frame end ---
```
## Phase Rules
| Hook/Context | Can READ (measure) | Can WRITE (mutate) |
|--------------|-------------------|-------------------|
| `useLayoutEffect` | ❌ NO | ✅ YES |
| `useLayout` (deprecated) | ✅ YES | ❌ NO |
| Event handlers (default) | ✅ YES | ❌ NO (use `requestMutation`) |
| `requestMeasure` callback | ✅ YES | ❌ NO |
| `requestMutation` callback | ❌ NO | ✅ YES |
## Usage Patterns
```typescript
// ✅ CORRECT: Read in measure phase, write in mutation phase
requestMeasure(() => {
const width = element.offsetWidth; // READ
requestMutation(() => {
element.style.width = `${width * 2}px`; // WRITE
});
});
// ❌ WRONG: Alternating reads/writes causes layout thrashing
const width = element.offsetWidth; // READ → reflow
element.style.width = `${width * 2}px`; // WRITE → reflow
const height = element.offsetHeight; // READ → reflow again!
```
## Signals: State Without Re-renders
Signals deliver updates **without causing component renders**. Use for frequently-updated values.
```typescript
// Create signal
const [getValue, setValue] = createSignal(initialValue);
// Get value
getValue();
// Set value (notifies subscribers, NO re-render)
setValue(newValue);
// Subscribe to changes
getValue.subscribe(() => { /* react to change */ });
```
**Signal Hooks:**
- `useSignal()` Create signal tied to component
- `useDerivedSignal()` Derive new signal from other signals/variables
- `useDerivedState()` Convert signal to render variable (triggers re-render)
- `useStateRef()` Access current value without it being a dependency
**When to use signals:**
- Typing text, caret position
- Animation state tracking
- Values that change frequently but don't need re-render
- Cross-component communication without prop drilling
## Key Optimization Hooks
| Hook | Purpose |
|------|---------|
| `useLastCallback` | Stable callback reference, always latest scope |
| `useStateRef` | Access state without triggering effects |
| `useLayoutEffectWithPrevDeps` | Synchronous effect with previous values |
| `useSyncEffect` | Effect that runs during render (not RAF) |
| `useResizeObserver` | Efficient element size observation |
| `useIntersectionObserver` | Viewport visibility tracking |
## Heavy Animation Handling
```typescript
// Mark animation start (pauses non-critical updates)
const endAnimation = beginHeavyAnimation(duration);
// Run code only when fully idle (no animations + browser idle)
onFullyIdle(() => {
// Safe for heavy computations
});
```
## Performance Checklist
1. **Animations first** Evaluate if code negatively impacts animations
2. **Simplify algorithms** Move complex ones to `onFullyIdle`
3. **No loops in selectors** Avoid iterations in `withGlobal` selectors
4. **Minimize re-renders** Especially in `Message`, `Chat`, `Sticker`, etc.
5. **Understand effect timing** `useEffect` vs `useLayoutEffect`
6. **Prefer signals** When you need effects only, not renders
7. **Use `requestForcedReflow`** Only as last resort for sync measure+mutate

View File

@ -100,6 +100,7 @@ export interface GramJsAppConfig extends LimitsConfig {
stars_stargift_resale_amount_max?: number;
stars_stargift_resale_amount_min?: number;
stars_stargift_resale_commission_permille?: number;
stargifts_craft_attribute_permilles?: number[];
ton_stargift_resale_amount_min?: number;
ton_stargift_resale_amount_max?: number;
ton_stargift_resale_commission_permille?: number;
@ -239,6 +240,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
starsStargiftResaleAmountMin: appConfig.stars_stargift_resale_amount_min,
starsStargiftResaleAmountMax: appConfig.stars_stargift_resale_amount_max,
starsStargiftResaleCommissionPermille: appConfig.stars_stargift_resale_commission_permille,
stargiftsCraftAttributePermilles: appConfig.stargifts_craft_attribute_permilles,
tonStargiftResaleAmountMin: appConfig.ton_stargift_resale_amount_min,
tonStargiftResaleAmountMax: appConfig.ton_stargift_resale_amount_max,
tonStargiftResaleCommissionPermille: appConfig.ton_stargift_resale_commission_permille,

View File

@ -36,7 +36,7 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
const {
id, num, ownerId, ownerName, title, attributes, availabilityIssued, availabilityTotal, slug, ownerAddress,
giftAddress, resellAmount, releasedBy, resaleTonOnly, requirePremium, valueCurrency, valueAmount, giftId,
valueUsdAmount, burned, crafted,
valueUsdAmount, burned, crafted, craftChancePermille,
} = starGift;
return {
@ -63,6 +63,7 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
offerMinStars: starGift.offerMinStars,
isBurned: burned,
isCrafted: crafted,
craftChancePermille,
};
}
@ -202,7 +203,7 @@ export function buildApiSavedStarGift(userStarGift: GramJs.SavedStarGift, peerId
const {
gift, date, convertStars, fromId, message, msgId, nameHidden, unsaved, refunded, upgradeStars, transferStars,
canUpgrade, savedId, canExportAt, pinnedToTop, canResellAt, canTransferAt, prepaidUpgradeHash,
dropOriginalDetailsStars,
dropOriginalDetailsStars, canCraftAt,
} = userStarGift;
const inputGift: ApiInputSavedStarGift | undefined = savedId && peerId
@ -230,6 +231,7 @@ export function buildApiSavedStarGift(userStarGift: GramJs.SavedStarGift, peerId
isPinned: pinnedToTop,
dropOriginalDetailsStars: dropOriginalDetailsStars !== undefined ? toJSNumber(dropOriginalDetailsStars) : undefined,
prepaidUpgradeHash,
canCraftAt,
};
}

View File

@ -426,7 +426,7 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess
if (action instanceof GramJs.MessageActionStarGiftUnique) {
const {
upgrade, transferred, saved, refunded, gift, canExportAt, transferStars, fromId, peer, savedId,
resaleAmount, prepaidUpgrade, dropOriginalDetailsStars, fromOffer,
resaleAmount, prepaidUpgrade, dropOriginalDetailsStars, fromOffer, canCraftAt,
} = action;
const starGift = buildApiStarGift(gift);
@ -451,6 +451,7 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess
dropOriginalDetailsStars: dropOriginalDetailsStars !== undefined
? toJSNumber(dropOriginalDetailsStars)
: undefined,
canCraftAt,
};
}
if (action instanceof GramJs.MessageActionPaidMessagesPrice) {

View File

@ -105,12 +105,14 @@ export async function fetchResaleGifts({
limit = DEFAULT_PRIMITIVES.INT,
attributesHash,
filter,
forCraft,
}: {
giftId: string;
offset?: string;
limit?: number;
attributesHash?: string;
filter?: ResaleGiftsFilterOptions;
forCraft?: boolean;
}) {
type GetResaleStarGifts = ConstructorParameters<typeof GramJs.payments.GetResaleStarGifts>[0];
@ -126,6 +128,7 @@ export async function fetchResaleGifts({
limit,
attributesHash: attributesHash ? BigInt(attributesHash) : DEFAULT_PRIMITIVES.BIGINT,
attributes: buildInputResaleGiftsAttributes(attributes),
forCraft: forCraft || undefined,
...(filter && {
sortByPrice: filter.sortType === 'byPrice' || undefined,
sortByNum: filter.sortType === 'byNumber' || undefined,
@ -631,6 +634,54 @@ export function resolveStarGiftOffer({
});
}
export async function fetchCraftStarGifts({
giftId,
peerId,
offset = DEFAULT_PRIMITIVES.STRING,
limit = DEFAULT_PRIMITIVES.INT,
}: {
giftId: string;
peerId: string;
offset?: string;
limit?: number;
}) {
const result = await invokeRequest(new GramJs.payments.GetCraftStarGifts({
giftId: BigInt(giftId),
offset,
limit,
}));
if (!result) {
return undefined;
}
return {
gifts: result.gifts.map((g) => buildApiSavedStarGift(g, peerId)),
nextOffset: result.nextOffset,
count: result.count,
};
}
export async function craftStarGift({
inputSavedGifts,
}: {
inputSavedGifts: ApiRequestInputSavedStarGift[];
}) {
try {
await invokeRequest(new GramJs.payments.CraftStarGift({
stargift: inputSavedGifts.map(buildInputSavedStarGift),
}), {
shouldThrow: true,
});
return undefined;
} catch (err) {
if (err instanceof RPCError) {
return { error: err.errorMessage };
}
throw err;
}
}
export async function fetchStarGiftUpgradeAttributes({
giftId,
}: {

View File

@ -1136,6 +1136,10 @@ export function updater(update: Update) {
giftId: update.giftId.toString(),
userState: buildApiStarGiftAuctionUserState(update.userState),
});
} else if (update instanceof GramJs.UpdateStarGiftCraftFail) {
sendApiUpdate({
'@type': 'updateStarGiftCraftFail',
});
} else if (update instanceof GramJs.UpdatePaidReactionPrivacy) {
sendApiUpdate({
'@type': 'updatePaidReactionPrivacy',

View File

@ -274,6 +274,7 @@ export interface ApiMessageActionStarGiftUnique extends ActionMediaType {
savedId?: string;
resaleAmount?: ApiTypeCurrencyAmount;
dropOriginalDetailsStars?: number;
canCraftAt?: number;
}
export interface ApiMessageActionChannelJoined extends ActionMediaType {

View File

@ -265,6 +265,7 @@ export interface ApiAppConfig {
starsStargiftResaleAmountMin?: number;
starsStargiftResaleAmountMax?: number;
starsStargiftResaleCommissionPermille?: number;
stargiftsCraftAttributePermilles?: number[];
starsSuggestedPostAmountMax: number;
starsSuggestedPostAmountMin: number;
starsSuggestedPostCommissionPermille: number;

View File

@ -63,6 +63,7 @@ export interface ApiStarGiftUnique {
offerMinStars?: number;
isBurned?: true;
isCrafted?: true;
craftChancePermille?: number;
}
export type ApiStarGift = ApiStarGiftRegular | ApiStarGiftUnique;
@ -149,6 +150,7 @@ export interface ApiSavedStarGift {
upgradeMsgId?: number; // Local field, used for Action Message
localTag?: number; // Local field, used for key in list
dropOriginalDetailsStars?: number;
canCraftAt?: number;
}
export type StarGiftAttributeIdModel = {

View File

@ -861,6 +861,10 @@ export type ApiUpdateStarGiftAuctionUserState = {
userState: ApiStarGiftAuctionUserState;
};
export type ApiUpdateStarGiftCraftFail = {
'@type': 'updateStarGiftCraftFail';
};
export type ApiUpdateDeleteProfilePhoto = {
'@type': 'updateDeleteProfilePhoto';
peerId: string;
@ -943,7 +947,8 @@ export type ApiUpdate = (
ApiUpdateStealthMode | ApiUpdateAttachMenuBots | ApiUpdateNewAuthorization | ApiUpdateGroupInvitePrivacyForbidden |
ApiUpdateViewForumAsMessages | ApiUpdateSavedDialogPinned | ApiUpdatePinnedSavedDialogIds | ApiUpdateChatLastMessage |
ApiUpdateDeleteSavedHistory | ApiUpdatePremiumFloodWait | ApiUpdateStarsBalance | ApiUpdateStarGiftAuctionState
| ApiUpdateStarGiftAuctionUserState | ApiUpdateBotCommands | ApiUpdateQuickReplyMessage | ApiUpdateQuickReplies
| ApiUpdateStarGiftAuctionUserState | ApiUpdateStarGiftCraftFail | ApiUpdateBotCommands
| ApiUpdateQuickReplyMessage | ApiUpdateQuickReplies
| ApiDeleteQuickReply | ApiUpdateDeleteQuickReplyMessages | ApiUpdateDeleteProfilePhoto | ApiUpdateNewProfilePhoto
| ApiUpdateEntities | ApiUpdatePaidReactionPrivacy | ApiUpdateLangPackTooLong | ApiUpdateLangPack
| ApiUpdateNotSupportedInFrozenAccountError

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="29" fill="none"><defs><filter id="a" width="120%" height="120%" x="-10%" y="-10%"><feGaussianBlur in="SourceGraphic" stdDeviation=".3"/></filter></defs><path fill="#fff" d="M32 16c0 4.302-1.698 8.208-4.46 11.083-1.299 1.352-3.249 1.733-5.105 1.48-1.97-.27-4.55-.563-6.435-.563-1.886 0-4.466.293-6.434.562-1.857.254-3.807-.127-5.106-1.479A15.95 15.95 0 0 1 0 16C0 7.163 7.163 0 16 0s16 7.163 16 16" filter="url(#a)"/></svg>

After

Width:  |  Height:  |  Size: 481 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 32 32"><path d="M16 0C7.163 0 0 7.163 0 16s7.163 16 16 16 16-7.163 16-16S24.837 0 16 0m7.5 17.5h-6v6a1.5 1.5 0 0 1-3 0v-6h-6a1.5 1.5 0 0 1 0-3h6v-6a1.5 1.5 0 0 1 3 0v6h6a1.5 1.5 0 0 1 0 3"/></svg>

After

Width:  |  Height:  |  Size: 270 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M12.36 25.174c-1.506 0-2.732-1.226-2.732-2.732s1.226-2.731 2.732-2.731 2.731 1.226 2.731 2.731-1.225 2.732-2.731 2.732m0-3.463a.732.732 0 1 0 .002 1.464.732.732 0 0 0-.002-1.464M19.76 31.458c-1.507 0-2.732-1.226-2.732-2.731s1.225-2.732 2.731-2.732 2.732 1.226 2.732 2.732-1.226 2.731-2.732 2.731m0-3.463a.732.732 0 1 0 0 1.465.732.732 0 0 0 0-1.465M11.334 31.31a1 1 0 0 1-.707-1.707l9.452-9.452a1 1 0 1 1 1.414 1.414l-9.452 9.453a1 1 0 0 1-.707.293M22.315 17.215a1 1 0 0 1-.495-.132l-5.607-3.205-5.606 3.205a1 1 0 0 1-.992-1.736l6.598-3.772 6.599 3.772a1 1 0 0 1-.497 1.868"/><path d="M22.315 11.88a1 1 0 0 1-.495-.132l-5.607-3.204-5.606 3.204a1 1 0 1 1-.992-1.736l6.598-3.77 6.599 3.77a1 1 0 0 1-.497 1.868"/><path d="M24.819 27.818a1 1 0 0 1-.623-1.783c3.17-2.52 4.99-6.278 4.99-10.308 0-7.27-5.916-13.185-13.185-13.185-7.271 0-13.186 5.915-13.186 13.185 0 4.03 1.818 7.788 4.99 10.308a1 1 0 0 1-1.245 1.566C2.91 24.698.815 20.37.815 15.727.815 7.354 7.627.542 16 .542s15.185 6.812 15.185 15.185c0 4.643-2.094 8.971-5.745 11.874a1 1 0 0 1-.621.217"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M22.155.843H9.845C4.882.843.844 4.881.844 9.844v12.312c0 4.962 4.038 9 9 9h12.311c4.964 0 9.002-4.038 9.002-9V9.844c0-4.963-4.038-9.001-9.002-9.001m7.002 9.001V15h-2.404c-.437-1.552-1.864-2.692-3.554-2.692s-3.118 1.14-3.555 2.692H17v-4.507h-1a1.694 1.694 0 0 1-1.692-1.692c0-.933.76-1.691 1.692-1.691h1V2.843h5.155c3.861 0 7.002 3.14 7.002 7.001M9.845 2.843H15v2.404c-1.55.437-2.692 1.865-2.692 3.554S13.45 11.918 15 12.356V15h-4.507v1c0 .933-.759 1.692-1.692 1.692A1.693 1.693 0 0 1 7.109 16v-1H2.844V9.844c0-3.86 3.14-7.001 7-7.001M2.844 22.156V17h2.403c.436 1.551 1.864 2.692 3.554 2.692S11.918 18.55 12.355 17H15v4.507h1c.933 0 1.692.759 1.692 1.691 0 .934-.759 1.693-1.692 1.693h-1v4.265H9.845a7.01 7.01 0 0 1-7.001-7m19.311 7H17v-2.403c1.552-.436 2.692-1.865 2.692-3.555s-1.14-3.117-2.692-3.553V17h4.507v-1c0-.933.759-1.692 1.692-1.692s1.692.759 1.692 1.692v1h4.266v5.155c0 3.86-3.14 7.001-7.002 7.001" /></svg>

After

Width:  |  Height:  |  Size: 986 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M11.945 19.473V15.37a.49.49 0 0 0-.484-.5H3.598a.494.494 0 0 0-.489.5c0 .113.04.223.106.309q1.553 2.016 3.41 3.02c1.285.695 2.504 1.167 4.45 1.417l.144.016c.36.047.68-.215.722-.582l.004-.04zM13.723 14.871h12.902a.49.49 0 0 1 .484.5v1.2a.497.497 0 0 1-.437.5q-1.745.17-2.969 1.109c-1.375 1.058-1.82 1.949-1.848 3.53-.015.962.426 1.892 1.325 2.782l.062.063a.66.66 0 0 1 .203.484v1.336c0 .367-.289.668-.648.668h-8.91c-.36 0-.649-.3-.649-.668v-1.21c0-.243.13-.466.336-.583q1.113-.634 1.114-1.953 0-1.354-1.176-1.942a.5.5 0 0 1-.274-.449v-4.867c0-.277.219-.5.485-.5M12.996 9.402l-.262-1.117a.664.664 0 0 1 .48-.805l.024-.007L25.2 5.07a1.224 1.224 0 0 1 1.426.934c.145.621-.227 1.246-.832 1.398l-.02.004-12.02 2.492a.643.643 0 0 1-.75-.464zM9.355 13.441l2.13-.55a1.17 1.17 0 0 0 .828-1.395l-.688-3.066a2.7 2.7 0 0 0-.375-.89l-.102-.157q-1.85-2.78-2.683-2.547-.846.239-1.211 3.707l-.008.094a2.7 2.7 0 0 0 .055.879l.687 3.054c.14.625.754 1.02 1.367.871m0 0"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1576,6 +1576,7 @@
"GiftInfoCollectible" = "Collectible #{number}";
"GiftInfoUniqueTitle" = "{name} {number}";
"GiftSavedNumber" = "#{number}";
"GiftInfoModelCrafted" = "{model} (crafted)";
"GiftAttributeModel" = "Model";
"GiftAttributeBackdrop" = "Backdrop";
"GiftAttributeSymbol" = "Symbol";
@ -1593,6 +1594,32 @@
"GiftInfoUpgradeBadge" = "upgrade";
"GiftInfoUpgradeForFree" = "Upgrade For Free";
"GiftInfoUpgrade" = "Upgrade";
"GiftInfoCraft" = "Craft";
"GiftCraftTitle" = "Craft Gift";
"GiftCraftButton" = "CRAFT {giftName}";
"GiftCraftSuccessChance" = "{percent} Success Chance";
"GiftCraftEmptyHint" = "Tap {button} to add your first gift";
"GiftCraftNewGift" = "Craft New Gift";
"GiftCraftSelectTitle" = "Select Gifts";
"GiftCraftSelectYourGifts" = "Your Gifts";
"GiftCraftSelectMarketGifts_one" = "{count} Suitable Gift on Sale";
"GiftCraftSelectMarketGifts_other" = "{count} Suitable Gifts on Sale";
"GiftCraftDescription" = "Add up to **4 gifts** to craft new {giftLine}";
"GiftCraftWarning" = "If crafting fails, all selected gifts will be consumed.";
"GiftCraftingTitle" = "Crafting";
"GiftCraftFailedTitle" = "Crafting Failed";
"GiftCraftFailedDescription_one" = "This crafting attempt was unsuccessful.\n**{count} gift** was lost.";
"GiftCraftFailedDescription_other" = "This crafting attempt was unsuccessful.\n**{count} gifts** were lost.";
"GiftCraftInfoTitle" = "Gift Crafting";
"GiftCraftInfoSubtitle" = "Turn your gifts into rare, epic, uncommon and legendary versions.";
"GiftCraftInfoCraftTitle" = "Get Rare Models";
"GiftCraftInfoCraftDescription" = "Select up to 4 gifts to craft a new exclusive model.";
"GiftCraftInfoChanceTitle" = "Maximize Chances";
"GiftCraftInfoChanceDescription" = "Combine more gifts to increase your odds of success.";
"GiftCraftInfoRiskTitle" = "Affect the Result";
"GiftCraftInfoRiskDescription" = "Use gifts with the same attribute to boost its chance.";
"GiftCraftHelp" = "Help";
"GiftCraftViewAll" = "View all craftable models >";
"GiftInfoWithdraw" = "Withdraw";
"GiftInfoWear" = "Wear";
"GiftInfoTakeOff" = "Take Off";

Binary file not shown.

Binary file not shown.

View File

@ -13,6 +13,9 @@ 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 GiftCraftModal } from '../components/modals/gift/craft/GiftCraftModal';
export { default as GiftCraftSelectModal } from '../components/modals/gift/craft/GiftCraftSelectModal';
export { default as GiftCraftInfoModal } from '../components/modals/gift/craft/GiftCraftInfoModal';
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';

View File

@ -1,10 +1,21 @@
import { memo, useRef } from '../../lib/teact/teact';
import { memo } from '../../lib/teact/teact';
import type { GlobalState } from '../../global/types';
import { selectTabState } from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle';
import useSelector from '../../hooks/data/useSelector';
import useShowTransition from '../../hooks/useShowTransition';
import styles from './Sparkles.module.scss';
function selectIsHeavyModalOpen(global: GlobalState) {
const tabState = selectTabState(global);
return Boolean(tabState.giftCraftModal || tabState.giftCraftSelectModal);
}
type ButtonParameters = {
preset: 'button';
};
@ -89,7 +100,15 @@ const Sparkles = ({
noAnimation,
...presetSettings
}: OwnProps) => {
const ref = useRef<HTMLDivElement>();
const isHeavyModalOpen = useSelector(selectIsHeavyModalOpen);
const { ref, shouldRender } = useShowTransition<HTMLDivElement>({
isOpen: !isHeavyModalOpen,
withShouldRender: true,
noMountTransition: true,
});
if (!shouldRender) return undefined;
if (presetSettings.preset === 'button') {
return (
@ -124,7 +143,11 @@ const Sparkles = ({
if (presetSettings.preset === 'progress') {
return (
<div ref={ref} className={buildClassName(styles.root, styles.progress, className)} style={style}>
<div
ref={ref}
className={buildClassName(styles.root, styles.progress, className)}
style={style}
>
{PROGRESS_POSITIONS.map((position) => {
return (
<div

View File

@ -2,16 +2,12 @@
position: absolute;
top: -0.125rem;
right: -0.125rem;
width: 3.5rem;
height: 3.5rem;
}
.text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) translate(6px, -6px) rotate(45deg);
font-size: 0.625rem;
font-weight: var(--font-weight-semibold);

View File

@ -22,10 +22,15 @@ type ColorKey = keyof typeof COLORS;
const COLOR_KEYS = new Set(Object.keys(COLORS) as ColorKey[]);
type GradientColor = readonly [string, string];
const DEFAULT_SIZE = 56;
const TEXT_OFFSET_RATIO = 6 / DEFAULT_SIZE;
type OwnProps = {
color: ColorKey | GradientColor | (string & {});
text: string;
size?: number;
className?: string;
textClassName?: string;
};
type StateProps = {
@ -33,7 +38,7 @@ type StateProps = {
};
const GiftRibbon = ({
text, color, className, theme,
text, color, size = DEFAULT_SIZE, className, textClassName, theme,
}: OwnProps & StateProps) => {
const randomId = useUniqueId();
const validSvgRandomId = `svg-${randomId}`; // ID must start with a letter
@ -52,9 +57,11 @@ const GiftRibbon = ({
const startColor = gradientColor ? gradientColor[0] : color;
const endColor = gradientColor ? gradientColor[1] : color;
const textOffset = Math.round(size * TEXT_OFFSET_RATIO);
return (
<div className={buildClassName(styles.root, className)}>
<svg className={styles.ribbon} width="56" height="56" viewBox="0 0 56 56" fill="none">
<div className={buildClassName(styles.root, className)} style={`width: ${size}px; height: ${size}px`}>
<svg className={styles.ribbon} width={size} height={size} viewBox="0 0 56 56" fill="none" aria-hidden="true">
<path d="M52.4851 26.4853L29.5145 3.51472C27.2641 1.26428 24.2119 0 21.0293 0H2.82824C1.04643 0 0.154103 2.15429 1.41403 3.41422L52.5856 54.5858C53.8455 55.8457 55.9998 54.9534 55.9998 53.1716V34.9706C55.9998 31.788 54.7355 28.7357 52.4851 26.4853Z" fill={`url(#${validSvgRandomId})`} />
<defs>
<linearGradient id={validSvgRandomId} x1="27.9998" y1="1" x2="27.9998" y2="55" gradientUnits="userSpaceOnUse">
@ -63,7 +70,12 @@ const GiftRibbon = ({
</linearGradient>
</defs>
</svg>
<div className={styles.text}>{text}</div>
<div
className={buildClassName(styles.text, textClassName)}
style={`transform: translate(-50%, -50%) translate(${textOffset}px, ${-textOffset}px) rotate(45deg)`}
>
{text}
</div>
</div>
);
};

View File

@ -1,5 +1,7 @@
import BrokenGiftPreview from '../../../assets/broken-gift.svg';
import QrPlane from '../../../assets/tgs/auth/QrPlane.tgs';
import BannedDuck from '../../../assets/tgs/BannedDuck.tgs';
import BrokenGift from '../../../assets/tgs/BrokenGift.tgs';
import CameraFlip from '../../../assets/tgs/calls/CameraFlip.tgs';
import HandFilled from '../../../assets/tgs/calls/HandFilled.tgs';
import HandOutline from '../../../assets/tgs/calls/HandOutline.tgs';
@ -8,6 +10,7 @@ import VoiceAllowTalk from '../../../assets/tgs/calls/VoiceAllowTalk.tgs';
import VoiceMini from '../../../assets/tgs/calls/VoiceMini.tgs';
import VoiceMuted from '../../../assets/tgs/calls/VoiceMuted.tgs';
import VoiceOutlined from '../../../assets/tgs/calls/VoiceOutlined.tgs';
import CraftProgress from '../../../assets/tgs/CraftProgress.tgs';
import Diamond from '../../../assets/tgs/Diamond.tgs';
import DuckNothingFound from '../../../assets/tgs/DuckNothingFound.tgs';
import Flame from '../../../assets/tgs/general/Flame.tgs';
@ -42,6 +45,7 @@ import SearchPreview from '../../../assets/tgs-previews/Search.svg';
import PasskeysPreview from '../../../assets/tgs-previews/settings/Passkeys.svg';
export const LOCAL_TGS_PREVIEW_URLS = {
BrokenGift: BrokenGiftPreview,
DuckNothingFound: DuckNothingFoundPreview,
Search: SearchPreview,
Passkeys: PasskeysPreview,
@ -82,6 +86,8 @@ export const LOCAL_TGS_URLS = {
Report,
SearchingDuck,
BannedDuck,
BrokenGift,
CraftProgress,
Diamond,
Search,
DuckNothingFound,

View File

@ -1,4 +1,4 @@
import { memo, useEffect, useLayoutEffect, useMemo, useRef, useSignal, useState } from '../../../lib/teact/teact';
import { memo, useEffect, useLayoutEffect, useMemo, useRef, useSignal } from '../../../lib/teact/teact';
import type { ApiSticker } from '../../../api/types';
@ -10,6 +10,7 @@ import { adjustHsv, getColorLuma, hex2rgb } from '../../../util/colors.ts';
import { preloadImage } from '../../../util/files';
import { REM } from '../helpers/mediaDimensions';
import useAsync from '../../../hooks/useAsync';
import useLastCallback from '../../../hooks/useLastCallback';
import useMedia from '../../../hooks/useMedia';
import useResizeObserver from '../../../hooks/useResizeObserver';
@ -20,7 +21,8 @@ import styles from './RadialPatternBackground.module.scss';
type OwnProps = {
backgroundColors?: string[];
patternIcon?: ApiSticker;
patternColor?: number;
patternUrl?: string;
patternColor?: string;
patternSize?: number;
maxRadius?: number;
centerEmptiness?: number;
@ -48,6 +50,8 @@ const DEFAULT_OVAL_FACTOR = 1.4;
const RadialPatternBackground = ({
backgroundColors,
patternIcon,
patternUrl,
patternColor,
patternSize = DEFAULT_PATTERN_SIZE,
centerEmptiness = DEFAULT_CENTER_EMPTINESS,
ringsCount = DEFAULT_RINGS_COUNT,
@ -67,15 +71,15 @@ const RadialPatternBackground = ({
const dpr = useDevicePixelRatio();
const [emojiImage, setEmojiImage] = useState<HTMLImageElement | undefined>();
const previewMediaHash = patternIcon && getStickerMediaHash(patternIcon, 'preview');
const previewUrl = useMedia(previewMediaHash);
useEffect(() => {
if (!previewUrl) return;
preloadImage(previewUrl).then(setEmojiImage);
}, [previewUrl]);
const imageUrl = previewUrl || patternUrl;
const { result: emojiImage } = useAsync(
() => (imageUrl ? preloadImage(imageUrl) : Promise.resolve(undefined)),
[imageUrl],
);
const patternPositions = useMemo(() => {
const coordinates: { x: number; y: number; sizeFactor: number }[] = [];
@ -146,9 +150,13 @@ const RadialPatternBackground = ({
ctx.drawImage(emojiImage, renderX - size / 2, renderY - size / 2, size, size);
});
const patternColor = backgroundColors?.[1] ?? backgroundColors?.[0] ?? '#000000';
const isDark = getColorLuma(hex2rgb(patternColor)) < DARK_LUMA_THRESHOLD;
ctx.fillStyle = adjustHsv(patternColor, 0.5, isDark ? 0.28 : -0.28);
if (patternColor) {
ctx.fillStyle = patternColor;
} else {
const baseColor = backgroundColors?.[1] ?? backgroundColors?.[0] ?? '#000000';
const isDark = getColorLuma(hex2rgb(baseColor)) < DARK_LUMA_THRESHOLD;
ctx.fillStyle = adjustHsv(baseColor, 0.5, isDark ? 0.28 : -0.28);
}
ctx.globalCompositeOperation = 'source-in';
ctx.fillRect(0, 0, width, height);
@ -171,7 +179,7 @@ const RadialPatternBackground = ({
useEffect(() => {
draw();
}, [emojiImage, patternPositions, yPosition, ovalFactor]);
}, [emojiImage, patternPositions, yPosition, ovalFactor, patternColor]);
useLayoutEffect(() => {
const { width, height } = getContainerSize();

View File

@ -702,6 +702,17 @@ const ActionMessageText = ({
if (isSavedMessages) {
if (isUpgrade) return lang('ActionStarGiftUpgradedSelf');
if (isTransferred) return lang('ActionStarGiftTransferredSelf');
if (resaleAmount) {
const amountText = formatCurrencyAmountAsText(lang, resaleAmount);
return lang(
'ApiMessageMessageActionResaleStarGiftUniqueOutgoing',
{
gift: lang('GiftUnique', { title: gift.title, number: gift.number }),
stars: asPreview ? amountText : renderStrong(amountText),
},
{ withNodes: true },
);
}
if (gift.isCrafted) return lang('ActionStarGiftCraftedSelf');
}

View File

@ -27,6 +27,9 @@ import GiftAuctionBidModal from './gift/auction/GiftAuctionBidModal.async';
import GiftAuctionChangeRecipientModal from './gift/auction/GiftAuctionChangeRecipientModal.async';
import GiftAuctionInfoModal from './gift/auction/GiftAuctionInfoModal.async';
import GiftAuctionModal from './gift/auction/GiftAuctionModal.async';
import GiftCraftInfoModal from './gift/craft/GiftCraftInfoModal.async';
import GiftCraftModal from './gift/craft/GiftCraftModal.async';
import GiftCraftSelectModal from './gift/craft/GiftCraftSelectModal.async';
import PremiumGiftModal from './gift/GiftModal.async';
import GiftInfoModal from './gift/info/GiftInfoModal.async';
import GiftLockedModal from './gift/locked/GiftLockedModal.async';
@ -108,6 +111,9 @@ type ModalKey = keyof Pick<TabState,
'aboutAdsModal' |
'giftPreviewModal' |
'giftUpgradeModal' |
'giftCraftModal' |
'giftCraftSelectModal' |
'giftCraftInfoModal' |
'giftAuctionModal' |
'giftAuctionBidModal' |
'giftAuctionInfoModal' |
@ -188,6 +194,9 @@ const MODALS: ModalRegistry = {
aboutAdsModal: AboutAdsModal,
giftPreviewModal: GiftPreviewModal,
giftUpgradeModal: GiftUpgradeModal,
giftCraftModal: GiftCraftModal,
giftCraftSelectModal: GiftCraftSelectModal,
giftCraftInfoModal: GiftCraftInfoModal,
giftAuctionModal: GiftAuctionModal,
giftAuctionBidModal: GiftAuctionBidModal,
giftAuctionInfoModal: GiftAuctionInfoModal,

View File

@ -32,6 +32,7 @@ type OwnProps = {
hasBackdrop?: boolean;
closeButtonColor?: 'translucent' | 'translucent-white';
moreMenuItems?: TeactNode;
headerRightToolBar?: TeactNode;
onClose: NoneToVoidFunction;
onButtonClick?: NoneToVoidFunction;
withBalanceBar?: boolean;
@ -54,6 +55,7 @@ const TableInfoModal = ({
hasBackdrop,
closeButtonColor,
moreMenuItems,
headerRightToolBar,
onClose,
onButtonClick,
withBalanceBar,
@ -79,6 +81,7 @@ const TableInfoModal = ({
className={className}
contentClassName={buildClassName(styles.content, contentClassName)}
moreMenuItems={moreMenuItems}
headerRightToolBar={headerRightToolBar}
onClose={onClose}
withBalanceBar={withBalanceBar}
currencyInBalanceBar={currencyInBalanceBar}

View File

@ -0,0 +1,41 @@
import type { TeactNode } from '../../../lib/teact/teact';
import { memo } from '../../../lib/teact/teact';
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
import Link from '../../ui/Link';
import styles from './GiftEmptyState.module.scss';
type OwnProps = {
description: TeactNode;
linkText?: TeactNode;
onLinkClick?: NoneToVoidFunction;
};
const GiftEmptyState = ({ description, linkText, onLinkClick }: OwnProps) => {
return (
<div className={styles.root}>
<AnimatedIconWithPreview
size={160}
tgsUrl={LOCAL_TGS_URLS.SearchingDuck}
nonInteractive
noLoop
/>
<div className={styles.description}>
{description}
</div>
{Boolean(linkText && onLinkClick) && (
<Link
className={styles.link}
onClick={onLinkClick}
>
{linkText}
</Link>
)}
</div>
);
};
export default memo(GiftEmptyState);

View File

@ -14,18 +14,16 @@ import { selectTabState,
} from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { RESALE_GIFTS_LIMIT } from '../../../limits';
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
import InfiniteScroll from '../../ui/InfiniteScroll';
import Link from '../../ui/Link';
import Transition from '../../ui/Transition';
import GiftItemStar from './GiftItemStar';
import ResaleGiftsNotFound from './ResaleGiftsNotFound';
import styles from './GiftModal.module.scss';
@ -96,37 +94,19 @@ const GiftModalResaleScreen: FC<OwnProps & StateProps> = ({
} });
});
function renderNothingFoundGiftsWithFilter() {
return (
<div className={styles.notFoundGiftsRoot}>
<AnimatedIconWithPreview
size={160}
tgsUrl={LOCAL_TGS_URLS.SearchingDuck}
nonInteractive
noLoop
/>
<div className={styles.notFoundGiftsDescription}>
{lang('ResellGiftsNoFound')}
</div>
{hasFilter && (
<Link
className={styles.notFoundGiftsLink}
onClick={handleResetGiftsFilter}
>
{lang('ResellGiftsClearFilters')}
</Link>
)}
</div>
);
}
return (
<div ref={scrollerRef} className={buildClassName(styles.resaleScreenRoot, 'custom-scroll')}>
<Transition
name="zoomFade"
activeKey={updateIteration}
>
{isGiftsEmpty && areGiftsAllLoaded && renderNothingFoundGiftsWithFilter()}
{isGiftsEmpty && areGiftsAllLoaded && (
<ResaleGiftsNotFound
description={lang('ResellGiftsNoFound')}
linkText={hasFilter ? lang('ResellGiftsClearFilters') : undefined}
onLinkClick={hasFilter ? handleResetGiftsFilter : undefined}
/>
)}
<InfiniteScroll
className={buildClassName(styles.resaleStarGiftsContainer)}
items={viewportIds}

View File

@ -1,5 +1,5 @@
import { type MouseEvent as ReactMouseEvent } from 'react';
import type { ElementRef, FC } from '../../../lib/teact/teact';
import type { ElementRef } from '../../../lib/teact/teact';
import type React from '../../../lib/teact/teact';
import {
memo,
@ -39,8 +39,12 @@ import ResaleGiftMenuAttributeSticker from './ResaleGiftMenuAttributeSticker';
import styles from './GiftResaleFilters.module.scss';
type FilterType = 'resale' | 'craft';
type OwnProps = {
dialogRef: ElementRef<HTMLDivElement>;
className?: string;
filterType?: FilterType;
};
type StateProps = {
filter: ResaleGiftsFilterOptions;
@ -48,15 +52,20 @@ type StateProps = {
counters?: ApiStarGiftAttributeCounter[];
};
const GiftResaleFilters: FC<StateProps & OwnProps> = ({
const DEFAULT_CRAFT_FILTER: ResaleGiftsFilterOptions = { sortType: 'byPrice' };
const GiftResaleFilters = ({
attributes,
counters,
filter,
dialogRef,
}) => {
className,
filterType = 'resale',
}: OwnProps & StateProps) => {
const lang = useLang();
const {
updateResaleGiftsFilter,
updateCraftGiftsFilter,
} = getActions();
const [searchModelQuery, setSearchModelQuery] = useState('');
@ -193,100 +202,114 @@ const GiftResaleFilters: FC<StateProps & OwnProps> = ({
} = useContextMenuHandlers(dialogRef);
const getPatternMenuElement = useLastCallback(() => patternMenuRef.current!);
const SortMenuButton: FC<{ onTrigger: (e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => void; isOpen?: boolean }>
= useMemo(() => {
const sortType = filter.sortType;
const iconName = sortType === 'byDate' ? 'sort-by-date'
: sortType === 'byNumber' ? 'sort-by-number'
: 'sort-by-price';
return ({ onTrigger, isOpen: isMenuOpen }) => (
<div
className={styles.item}
onClick={onTrigger}
>
<Icon
name={iconName}
className={styles.itemIcon}
/>
{sortType === 'byDate' && lang('ValueGiftSortByDate')}
{sortType === 'byNumber' && lang('ValueGiftSortByNumber')}
{sortType === 'byPrice' && lang('ValueGiftSortByPrice')}
</div>
);
}, [lang, filter]);
const SortMenuButton = useMemo(() => {
const sortType = filter.sortType;
const iconName = sortType === 'byDate' ? 'sort-by-date'
: sortType === 'byNumber' ? 'sort-by-number'
: 'sort-by-price';
return ({ onTrigger, isOpen: isMenuOpen }: {
onTrigger: (e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => void;
isOpen?: boolean;
}) => (
<div
className={styles.item}
onClick={onTrigger}
>
<Icon
name={iconName}
className={styles.itemIcon}
/>
{sortType === 'byDate' && lang('ValueGiftSortByDate')}
{sortType === 'byNumber' && lang('ValueGiftSortByNumber')}
{sortType === 'byPrice' && lang('ValueGiftSortByPrice')}
</div>
);
}, [lang, filter]);
const ModelMenuButton:
FC<{ onTrigger: (e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => void; isOpen?: boolean }>
= useMemo(() => {
const attributesCount = filter?.modelAttributes?.length || 0;
return ({ onTrigger, isOpen: isMenuOpen }) => (
<div
className={styles.item}
onClick={onTrigger}
>
{attributesCount === 0 && lang('GiftAttributeModel')}
{attributesCount > 0
&& lang('GiftAttributeModelPlural', { count: attributesCount }, { pluralValue: attributesCount })}
{renderDropdownArrows(isMenuOpen)}
</div>
);
}, [lang, filter]);
const BackdropMenuButton:
FC<{ onTrigger: (e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => void; isOpen?: boolean }>
= useMemo(() => {
const attributesCount = filter?.backdropAttributes?.length || 0;
return ({ onTrigger, isOpen: isMenuOpen }) => (
<div
className={styles.item}
onClick={onTrigger}
>
{attributesCount === 0 && lang('GiftAttributeBackdrop')}
{attributesCount > 0
&& lang('GiftAttributeBackdropPlural', { count: attributesCount }, { pluralValue: attributesCount })}
{renderDropdownArrows(isMenuOpen)}
</div>
);
}, [lang, filter]);
const PatternMenuButton: FC<{ onTrigger: (e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => void; isOpen?: boolean }>
= useMemo(() => {
const attributesCount = filter?.patternAttributes?.length || 0;
return ({ onTrigger, isOpen: isMenuOpen }) => (
<div
className={styles.item}
onClick={onTrigger}
>
{attributesCount === 0 && lang('GiftAttributeSymbol')}
{attributesCount > 0
&& lang('GiftAttributeSymbolPlural', { count: attributesCount }, { pluralValue: attributesCount })}
{renderDropdownArrows(isMenuOpen)}
</div>
);
}, [lang, filter]);
const ModelMenuButton = useMemo(() => {
const attributesCount = filter?.modelAttributes?.length || 0;
return ({ onTrigger, isOpen: isMenuOpen }: {
onTrigger: (e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => void;
isOpen?: boolean;
}) => (
<div
className={styles.item}
onClick={onTrigger}
>
{attributesCount === 0 && lang('GiftAttributeModel')}
{attributesCount > 0
&& lang('GiftAttributeModelPlural', { count: attributesCount }, { pluralValue: attributesCount })}
{renderDropdownArrows(isMenuOpen)}
</div>
);
}, [lang, filter]);
const BackdropMenuButton = useMemo(() => {
const attributesCount = filter?.backdropAttributes?.length || 0;
return ({ onTrigger, isOpen: isMenuOpen }: {
onTrigger: (e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => void;
isOpen?: boolean;
}) => (
<div
className={styles.item}
onClick={onTrigger}
>
{attributesCount === 0 && lang('GiftAttributeBackdrop')}
{attributesCount > 0
&& lang('GiftAttributeBackdropPlural', { count: attributesCount }, { pluralValue: attributesCount })}
{renderDropdownArrows(isMenuOpen)}
</div>
);
}, [lang, filter]);
const PatternMenuButton = useMemo(() => {
const attributesCount = filter?.patternAttributes?.length || 0;
return ({ onTrigger, isOpen: isMenuOpen }: {
onTrigger: (e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => void;
isOpen?: boolean;
}) => (
<div
className={styles.item}
onClick={onTrigger}
>
{attributesCount === 0 && lang('GiftAttributeSymbol')}
{attributesCount > 0
&& lang('GiftAttributeSymbolPlural', { count: attributesCount }, { pluralValue: attributesCount })}
{renderDropdownArrows(isMenuOpen)}
</div>
);
}, [lang, filter]);
const handleFilterUpdate = useLastCallback((newFilter: ResaleGiftsFilterOptions) => {
if (filterType === 'craft') {
updateCraftGiftsFilter({ filter: newFilter });
} else {
updateResaleGiftsFilter({ filter: newFilter });
}
});
const handleSortMenuItemClick = useLastCallback((type: ResaleGiftsSortType) => {
updateResaleGiftsFilter({ filter: {
handleFilterUpdate({
...filter,
sortType: type,
} });
});
});
const handleSelectedAllModelsClick = useLastCallback(() => {
updateResaleGiftsFilter({ filter: {
handleFilterUpdate({
...filter,
modelAttributes: [],
} });
});
});
const handleSelectedAllPatternsClick = useLastCallback(() => {
updateResaleGiftsFilter({ filter: {
handleFilterUpdate({
...filter,
patternAttributes: [],
} });
});
});
const handleSelectedAllBackdropsClick = useLastCallback(() => {
updateResaleGiftsFilter({ filter: {
handleFilterUpdate({
...filter,
backdropAttributes: [],
} });
});
});
const handleModelMenuItemClick = useLastCallback((attribute: ApiStarGiftAttributeModel) => {
@ -303,10 +326,10 @@ const GiftResaleFilters: FC<StateProps & OwnProps> = ({
const updatedAttributes = isActive
? modelAttributes.filter((item) => item.documentId !== modelAttribute.documentId)
: [...modelAttributes, modelAttribute];
updateResaleGiftsFilter({ filter: {
handleFilterUpdate({
...filter,
modelAttributes: updatedAttributes,
} });
});
});
const handlePatternMenuItemClick = useLastCallback((attribute: ApiStarGiftAttributePattern) => {
@ -323,10 +346,10 @@ const GiftResaleFilters: FC<StateProps & OwnProps> = ({
const updatedAttributes = isActive
? patternAttributes.filter((item) => item.documentId !== patternAttribute.documentId)
: [...patternAttributes, patternAttribute];
updateResaleGiftsFilter({ filter: {
handleFilterUpdate({
...filter,
patternAttributes: updatedAttributes,
} });
});
});
const handleBackdropMenuItemClick = useLastCallback((attribute: ApiStarGiftAttributeBackdrop) => {
@ -343,10 +366,10 @@ const GiftResaleFilters: FC<StateProps & OwnProps> = ({
const updatedAttributes = isActive
? backdropAttributes.filter((item) => item.backdropId !== backdropAttribute.backdropId)
: [...backdropAttributes, backdropAttribute];
updateResaleGiftsFilter({ filter: {
handleFilterUpdate({
...filter,
backdropAttributes: updatedAttributes,
} });
});
});
function renderDropdownArrows(isOpen?: boolean) {
@ -648,7 +671,7 @@ const GiftResaleFilters: FC<StateProps & OwnProps> = ({
}
return (
<div className={styles.root}>
<div className={buildClassName(styles.root, className)}>
{Boolean(sortContextMenuAnchor) && renderSortMenu()}
{Boolean(modelContextMenuAnchor) && renderModelMenu()}
{Boolean(backdropContextMenuAnchor) && renderBackdropMenu()}
@ -675,16 +698,22 @@ const GiftResaleFilters: FC<StateProps & OwnProps> = ({
);
};
export default memo(withGlobal((global): Complete<StateProps> => {
const { resaleGifts } = selectTabState(global);
export default memo(withGlobal<OwnProps>((global, { filterType }): Complete<StateProps> => {
const tabState = selectTabState(global);
const attributes = resaleGifts.attributes;
const counters = resaleGifts.counters;
const filter = resaleGifts.filter;
if (filterType === 'craft') {
const craftModal = tabState.giftCraftModal;
return {
filter: craftModal?.marketFilter || DEFAULT_CRAFT_FILTER,
attributes: craftModal?.marketAttributes,
counters: craftModal?.marketCounters,
};
}
const { resaleGifts } = tabState;
return {
attributes,
counters,
filter,
filter: resaleGifts.filter,
attributes: resaleGifts.attributes,
counters: resaleGifts.counters,
};
})(GiftResaleFilters));

View File

@ -0,0 +1,30 @@
.root {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 5rem;
}
.description {
margin-block: 1rem;
font-size: 1rem;
font-weight: var(--font-weight-medium);
color: var(--color-text-secondary);
text-align: center;
}
.link {
font-weight: var(--font-weight-medium);
color: var(--color-links);
transition: opacity 0.15s ease-in;
&:active,
&:hover {
color: var(--color-links);
text-decoration: none;
opacity: 0.85;
}
}

View File

@ -0,0 +1,43 @@
import type { TeactNode } from '../../../lib/teact/teact';
import { memo } from '../../../lib/teact/teact';
import buildClassName from '../../../util/buildClassName';
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
import Link from '../../ui/Link';
import styles from './ResaleGiftsNotFound.module.scss';
type OwnProps = {
className?: string;
description: TeactNode;
linkText?: TeactNode;
onLinkClick?: NoneToVoidFunction;
};
const ResaleGiftsNotFound = ({ className, description, linkText, onLinkClick }: OwnProps) => {
return (
<div className={buildClassName(styles.root, className)}>
<AnimatedIconWithPreview
size={160}
tgsUrl={LOCAL_TGS_URLS.SearchingDuck}
nonInteractive
noLoop
/>
<div className={styles.description}>
{description}
</div>
{Boolean(linkText && onLinkClick) && (
<Link
className={styles.link}
onClick={onLinkClick}
>
{linkText}
</Link>
)}
</div>
);
};
export default memo(ResaleGiftsNotFound);

View File

@ -0,0 +1,16 @@
import { memo } from '../../../../lib/teact/teact';
import type { OwnProps } from './GiftCraftInfoModal';
import { Bundles } from '../../../../util/moduleLoader';
import useModuleLoader from '../../../../hooks/useModuleLoader';
const GiftCraftInfoModalAsync = (props: OwnProps) => {
const { modal } = props;
const GiftCraftInfoModal = useModuleLoader(Bundles.Stars, 'GiftCraftInfoModal', !modal);
return GiftCraftInfoModal ? <GiftCraftInfoModal {...props} /> : undefined;
};
export default memo(GiftCraftInfoModalAsync);

View File

@ -0,0 +1,18 @@
.header {
--_height: 16rem;
}
.footer {
display: flex;
flex-direction: column;
align-self: stretch;
margin-top: 0.5rem;
}
.understoodIcon {
font-size: 1.1875rem;
}
.content {
gap: 1.5rem;
}

View File

@ -0,0 +1,91 @@
import { memo, useMemo } from '../../../../lib/teact/teact';
import { getActions } from '../../../../global';
import type { TabState } from '../../../../global/types';
import { getGiftAttributes } from '../../../common/helpers/gifts';
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback';
import Button from '../../../ui/Button';
import TableAboutModal, { type TableAboutData } from '../../common/TableAboutModal';
import UniqueGiftHeader from '../UniqueGiftHeader';
import styles from './GiftCraftInfoModal.module.scss';
export type OwnProps = {
modal: TabState['giftCraftInfoModal'];
};
const GiftCraftInfoModal = ({
modal,
}: OwnProps) => {
const { closeGiftCraftInfoModal } = getActions();
const lang = useLang();
const isOpen = Boolean(modal);
const renderingModal = useCurrentOrPrev(modal);
const gift = renderingModal?.gift;
const handleClose = useLastCallback(() => {
closeGiftCraftInfoModal();
});
const giftAttributes = useMemo(() => {
return gift ? getGiftAttributes(gift) : undefined;
}, [gift]);
const header = useMemo(() => {
if (!giftAttributes) return undefined;
return (
<UniqueGiftHeader
className={styles.header}
modelAttribute={giftAttributes.model!}
backdropAttribute={giftAttributes.backdrop!}
patternAttribute={giftAttributes.pattern!}
title={lang('GiftCraftInfoTitle')}
subtitle={lang('GiftCraftInfoSubtitle')}
/>
);
}, [giftAttributes, lang]);
const footer = useMemo(() => {
if (!isOpen) return undefined;
return (
<div className={styles.footer}>
<Button
iconName="understood"
iconClassName={styles.understoodIcon}
onClick={handleClose}
>
{lang('ButtonUnderstood')}
</Button>
</div>
);
}, [lang, isOpen, handleClose]);
const listItemData = useMemo(() => {
return [
['radial-badge', lang('GiftCraftInfoCraftTitle'), lang('GiftCraftInfoCraftDescription')],
['combine-craft', lang('GiftCraftInfoChanceTitle'), lang('GiftCraftInfoChanceDescription')],
['boost-craft-chance', lang('GiftCraftInfoRiskTitle'), lang('GiftCraftInfoRiskDescription')],
] satisfies TableAboutData;
}, [lang]);
return (
<TableAboutModal
isOpen={isOpen}
header={header}
listItemData={listItemData}
footer={footer}
hasBackdrop
contentClassName={styles.content}
onClose={handleClose}
/>
);
};
export default memo(GiftCraftInfoModal);

View File

@ -0,0 +1,16 @@
import { memo } from '../../../../lib/teact/teact';
import type { OwnProps } from './GiftCraftModal';
import { Bundles } from '../../../../util/moduleLoader';
import useModuleLoader from '../../../../hooks/useModuleLoader';
const GiftCraftModalAsync = (props: OwnProps) => {
const { modal } = props;
const GiftCraftModal = useModuleLoader(Bundles.Stars, 'GiftCraftModal', !modal);
return GiftCraftModal ? <GiftCraftModal {...props} /> : undefined;
};
export default memo(GiftCraftModalAsync);

View File

@ -0,0 +1,722 @@
@use "../../../../styles/mixins";
.modal {
:global(.modal-dialog) {
overflow: clip;
background: #232E3F;
}
:global(.modal-content) {
max-height: 92vh !important;
}
}
.patternOverlay {
position: absolute;
z-index: 0;
inset: 0;
border-radius: var(--border-radius-modal);
}
.patternSlide,
.patternBackground {
position: absolute !important;
inset: 0 !important;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 !important;
}
.title {
z-index: 1;
align-self: flex-start;
margin: 0;
margin-left: 4.3125rem;
padding: 0.75rem;
font-size: 1.25rem;
font-weight: var(--font-weight-medium);
color: white;
}
.helpButton {
position: absolute;
z-index: 3;
top: 0.875rem;
right: 0.875rem;
}
.header {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
/* 3D Cube Animation */
.cube {
pointer-events: none;
position: relative;
transform-style: preserve-3d;
width: 100%;
height: 100%;
}
.corners {
display: none;
}
.face {
position: absolute;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: var(--cube-size);
height: var(--cube-size);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 1.875rem;
backface-visibility: hidden;
background: #4F5B71;
transition: opacity 1s;
&.faceHidden {
opacity: 0;
}
&[data-face="front"] {
transform: translateZ(var(--cube-half));
}
&[data-face="back"] {
transform: translateZ(calc(-1 * var(--cube-half))) rotateY(180deg);
}
&[data-face="left"] {
transform: translateX(calc(-1 * var(--cube-half))) rotateY(-90deg);
}
&[data-face="right"] {
transform: translateX(var(--cube-half)) rotateY(90deg);
}
&[data-face="top"] {
transform: translateY(calc(-1 * var(--cube-half))) rotateX(90deg);
}
&[data-face="bottom"] {
transform: translateY(var(--cube-half)) rotateX(-90deg);
}
}
.faceHighChance {
background: #36595F;
}
.faceFailed {
border-color: #653B31;
}
.faceIcon {
font-size: 2.5rem;
color: rgba(255, 255, 255, 0.5);
}
.craftedGiftFace {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
opacity: 0;
animation: fadeInContent 0.25s ease-out 0.25s both;
}
.slotBackdrop,
.craftedGiftBackdrop,
.attributeRing,
.burnedGiftBackdrop {
position: absolute;
inset: 0;
}
.slotSticker,
.craftedGiftSticker,
.patternAttribute,
.burnedGift,
.burnedGiftSticker {
position: relative;
}
.failedGiftFace {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
opacity: 0;
background: linear-gradient(180deg, #683E34 0%, #51291F 100%);
animation: fadeInContent 0.2s ease-out 0.2s both;
}
@keyframes fadeInContent {
to {
opacity: 1;
}
}
.burnedCount {
position: absolute;
bottom: 0.5rem;
margin-top: 0.25rem;
font-size: 0.875rem;
font-weight: var(--font-weight-semibold);
color: white;
}
.slotsGrid {
--perspective: 600px;
--cube-size: 7.5rem;
--cube-half: 3.75rem;
--slot-size: 4.5rem;
position: relative;
display: grid;
grid-template-columns: var(--slot-size) var(--cube-size) var(--slot-size);
grid-template-rows: var(--slot-size) var(--slot-size);
gap: 1.5rem;
row-gap: 17px;
align-items: center;
justify-content: center;
justify-items: center;
padding: 1.125rem;
perspective: var(--perspective);
&.activated {
.craftSlot:not(.craftSlotFilled) {
opacity: 0;
}
.slotChance,
.slotClear {
opacity: 0;
}
}
}
.slotWrapper {
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
width: var(--slot-size);
height: var(--slot-size);
&.bottomRow {
align-self: start;
}
&.used {
pointer-events: none;
}
}
.cubeWrapper {
position: relative;
transform-style: preserve-3d;
transform: scale(0.913);
display: flex;
grid-column: 2;
grid-row: 1 / 3;
align-items: center;
justify-content: center;
width: var(--cube-size);
height: var(--cube-size);
}
.craftSlot {
pointer-events: auto;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: var(--slot-size);
height: var(--slot-size);
border-radius: 1.125rem;
background: rgba(255, 255, 255, 0.08);
transition: transform 0.15s, background-color 0.15s, opacity 0.5s;
&:hover {
background: rgba(255, 255, 255, 0.12);
}
&.animating {
will-change: transform;
z-index: 100;
.slotChance,
.slotClear {
opacity: 0;
}
}
}
.slotIcon {
font-size: 1.875rem;
color: white;
}
.craftSlotFilled {
position: relative;
background: transparent;
&:hover {
background: transparent;
}
}
.craftSlotHidden {
opacity: 0;
transition: opacity 0.5s !important;
}
.removing {
transform: scale(0.8);
opacity: 0;
transition: opacity 0.15s, transform 0.15s ease-out;
}
.slotInner {
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
border-radius: 1.125rem;
}
.slotChance {
position: absolute;
top: -0.3125rem;
left: -0.3125rem;
padding: 0.0625rem 0.25rem;
border-radius: 0.625rem;
font-size: 0.75rem;
font-weight: var(--font-weight-semibold);
color: white;
opacity: 0.9;
background-color: #374354B2;
backdrop-filter: blur(25px);
transition: opacity 0.15s;
}
.slotClear {
cursor: pointer;
position: absolute;
top: -0.3125rem;
right: -0.3125rem;
display: flex;
align-items: center;
justify-content: center;
width: 1.25rem;
height: 1.25rem;
padding: 0;
border: none;
border-radius: 50%;
font-size: 0.75rem;
color: white;
opacity: 0.9;
background-color: #374354B2;
backdrop-filter: blur(25px);
outline: none !important;
transition: opacity 0.15s;
&:hover {
opacity: 1;
}
}
.centerAnvil {
position: relative;
display: flex;
grid-column: 2;
grid-row: 1 / 3;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: var(--cube-size);
height: var(--cube-size);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 1.875rem;
background: #4F5B71;
&.centerAnvilHighChance {
background: #36595F;
}
}
.progressRing {
position: absolute;
inset: 0;
margin: auto;
}
.anvilIcon {
font-size: 3.125rem;
color: white;
}
.percentage {
position: absolute;
bottom: 0.75rem;
font-size: 1.1875rem;
font-weight: var(--font-weight-semibold);
color: white;
}
.infoTransition {
z-index: 1;
min-height: 20.5rem;
}
.infoSection,
.craftingSection,
.failedSection {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.75rem 1.5rem 1.5rem;
text-align: center;
}
.craftingTitle {
margin: 0 0 0.25rem;
font-size: 1.25rem;
font-weight: var(--font-weight-semibold);
color: white;
}
.craftingGiftName {
margin: 0 0 1rem;
font-size: 1rem;
font-weight: var(--font-weight-medium);
color: #8D979B;
}
.craftingWarning,
.failedDescription {
margin: 0.25rem 0 1.5rem;
font-size: 1rem;
color: white;
}
.craftingWarning {
max-width: 15rem;
margin: 3.5rem 0 1.5rem;
color: #8D979B;
}
.failedDescription {
text-wrap: balance;
}
.infoDescription {
max-width: 18rem;
min-height: 2.625rem;
margin: 0 0 0.5rem;
font-size: 1rem;
line-height: 1.25;
color: white;
}
.giftLine {
display: flex;
gap: 0.25rem;
align-items: center;
justify-content: center;
}
.giftIcon {
display: inline-block;
vertical-align: middle;
}
.infoWarning {
max-width: 18rem;
margin: 0.5rem 0 0.5rem;
font-size: 1rem;
line-height: 1.25;
color: white;
}
.attributeCircles {
--circle-attribute-size: 4rem;
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
align-items: center;
align-items: flex-start;
align-self: center;
justify-content: center;
max-width: 17rem;
padding-top: 0.4375rem;
}
.viewAllButton {
gap: 0.1875rem;
margin-top: 0.625rem;
margin-bottom: 0.5rem;
filter: none;
}
.viewAllText {
margin-inline-start: 0.125rem;
}
.attributeCircle {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: var(--circle-attribute-size);
@include mixins.with-vt-type('craftAttributes');
}
.attributeContent {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: var(--circle-attribute-size);
height: var(--circle-attribute-size);
}
.colorAttribute {
width: 2rem;
height: 1.8125rem;
margin-top: -0.1875rem;
/* stylelint-disable-next-line plugin/use-baseline */
mask: url('../../../../assets/attribute-mask.svg') center / 100% 100% no-repeat;
}
.patternAttributeThumb,
.burnedGiftBadge {
position: absolute;
top: 0;
right: 0;
}
.burnedGiftBadgeText {
font-size: 0.5rem;
}
.attributePercent {
position: absolute;
bottom: 0.25rem;
font-size: 0.6875rem;
font-weight: var(--font-weight-semibold);
color: rgba(255, 255, 255, 0.85);
}
.footer {
z-index: 1;
width: 100%;
padding: 1rem;
padding-top: 0;
}
.emptyHintIcon {
display: inline-block;
font-size: 0.75rem;
vertical-align: middle;
}
.craftButton {
width: 100%;
min-height: 3.5rem;
padding: 0.75rem 1rem;
&:global(.danger) {
color: var(--color-white);
background-color: var(--color-error);
&:not(.disabled):not(:disabled) {
&:active,
&:global(.active),
&:focus {
opacity: 0.85 !important;
}
@media (hover: hover) {
&:hover {
opacity: 0.85 !important;
}
}
}
}
}
.craftButtonCrafting {
opacity: 1 !important;
background-color: rgba(255, 255, 255, 0.08) !important;
}
.craftButtonHighChance {
background: linear-gradient(90deg, #0A8A90, #34C351) !important;
}
.craftButtonContent {
display: flex;
flex-direction: column;
gap: 0.125rem;
justify-content: center;
height: 100%;
}
.craftButtonTitle {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 1rem;
font-weight: var(--font-weight-semibold);
}
.craftButtonSubtitle {
font-size: 0.8125rem;
font-weight: var(--font-weight-medium);
}
.failedTitle {
margin: 0 0 0.5rem;
font-size: 1.25rem;
font-weight: var(--font-weight-semibold);
color: white;
}
.burnedGifts {
display: flex;
gap: 0.75rem;
justify-content: center;
}
.burnedGiftInner {
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
width: 4.25rem;
height: 4.25rem;
border-radius: 0.625rem;
}
@include mixins.on-active-vt('craftAttributes') {
&::view-transition-group(.craftAttribute) {
animation-duration: 250ms;
animation-timing-function: ease-out;
}
&::view-transition-old(.craftAttribute) {
:local {
animation-name: craftAttributeFadeOut;
}
@keyframes craftAttributeFadeOut {
to {
transform: scale(0.8);
opacity: 0;
}
}
}
&::view-transition-new(.craftAttribute) {
:local {
animation-name: craftAttributeFadeIn;
}
@keyframes craftAttributeFadeIn {
from {
transform: scale(0.8);
opacity: 0;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
import { memo } from '../../../../lib/teact/teact';
import type { OwnProps } from './GiftCraftSelectModal';
import { Bundles } from '../../../../util/moduleLoader';
import useModuleLoader from '../../../../hooks/useModuleLoader';
const GiftCraftSelectModalAsync = (props: OwnProps) => {
const { modal } = props;
const GiftCraftSelectModal = useModuleLoader(Bundles.Stars, 'GiftCraftSelectModal', !modal);
return GiftCraftSelectModal ? <GiftCraftSelectModal {...props} /> : undefined;
};
export default memo(GiftCraftSelectModalAsync);

View File

@ -0,0 +1,144 @@
.modal {
:global(.modal-dialog) {
overflow: clip;
}
:global(.modal-content) {
height: min(92vh, 39rem);
}
}
.content {
display: flex;
flex-direction: column;
padding: 0 !important;
}
.title {
margin: 0;
padding: 1rem 1rem 0.5rem;
border-bottom: 0.0625rem solid transparent;
font-size: 1.25rem;
font-weight: var(--font-weight-semibold);
text-align: center;
transition: border-bottom-color 0.15s ease;
&.titleWithBorder {
border-bottom-color: var(--color-borders);
}
}
.wrapper {
position: relative;
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
}
.scrollContainer {
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0.5rem;
height: 100%;
padding: 0 1rem 1rem;
opacity: 1;
transition: opacity 0.2s;
}
.section {
width: 100%;
}
.sectionTitle {
margin: 0;
padding: 0.25rem 0;
font-size: 0.875rem;
font-weight: var(--font-weight-medium);
color: var(--color-primary);
text-transform: uppercase;
}
.filters {
position: sticky;
z-index: 2;
top: 0;
width: 100%;
margin-top: 0;
margin-bottom: 0.5rem;
padding-block: 0.5rem;
border-bottom: 0.0625rem solid transparent;
background-color: var(--color-background);
transition: border-bottom-color 0.15s ease;
&.stuck {
border-bottom-color: var(--color-borders);
}
}
.transitionWrapper {
z-index: 0;
min-height: 28rem;
}
.giftsGrid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
align-content: start;
min-height: 28rem;
}
.loading {
pointer-events: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
opacity: 1;
transition: opacity 0.2s;
}
.hidden {
opacity: 0;
}
.giftWrapper {
position: relative;
}
.giftChance {
position: absolute;
top: 0.25rem;
left: 0.25rem;
padding: 0.0625rem 0.25rem;
border-radius: 0.625rem;
font-size: 0.75rem;
font-weight: var(--font-weight-semibold);
color: white;
background-color: rgba(0, 0, 0, 0.3);
}
.notFound {
padding-top: 5rem;
padding-bottom: 12rem;
}

View File

@ -0,0 +1,367 @@
import {
memo, useMemo, useRef, useState,
} from '../../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../../global';
import type { ApiSavedStarGift, ApiStarGift, ApiStarGiftUnique } from '../../../../api/types';
import type { TabState } from '../../../../global/types';
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
import { getSavedGiftKey } from '../../../../global/helpers/stars';
import { selectTabState } from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName';
import { throttle } from '../../../../util/schedulers';
import { formatPercent } from '../../../../util/textFormat';
import { getGiftAttributes } from '../../../common/helpers/gifts';
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
import useInfiniteScroll from '../../../../hooks/useInfiniteScroll';
import { useIntersectionObserver } from '../../../../hooks/useIntersectionObserver';
import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback';
import usePrevious from '../../../../hooks/usePrevious';
import InfiniteScroll from '../../../ui/InfiniteScroll';
import Loading from '../../../ui/Loading';
import Modal from '../../../ui/Modal';
import Transition from '../../../ui/Transition';
import GiftItemStar from '../GiftItemStar';
import GiftResaleFilters from '../GiftResaleFilters';
import ResaleGiftsNotFound from '../ResaleGiftsNotFound';
import styles from './GiftCraftSelectModal.module.scss';
export type OwnProps = {
modal: TabState['giftCraftSelectModal'];
};
type StateProps = {
craftModal?: TabState['giftCraftModal'];
};
const CRAFT_GIFTS_LIMIT = 50;
const INTERSECTION_THROTTLE = 200;
const SCROLL_THROTTLE = 200;
const runThrottledForScroll = throttle((cb: NoneToVoidFunction) => cb(), SCROLL_THROTTLE, true);
type CraftGiftItemProps = {
gift: ApiStarGift;
chancePercent?: number;
chanceColor?: string;
showPrice?: boolean;
observe?: ObserveFn;
onClick: (gift: ApiStarGift) => void;
};
const CraftGiftItem = memo(({ gift, chancePercent, chanceColor, showPrice, observe, onClick }: CraftGiftItemProps) => {
return (
<div className={styles.giftWrapper}>
<GiftItemStar
gift={gift}
observeIntersection={observe}
hideBadge={!showPrice}
onClick={onClick}
/>
{Boolean(chancePercent && chancePercent > 0) && (
<span
className={styles.giftChance}
style={chanceColor ? `background-color: ${chanceColor}` : undefined}
>
+
{formatPercent(chancePercent!, 0)}
</span>
)}
</div>
);
});
const GiftCraftSelectModal = ({ modal, craftModal }: OwnProps & StateProps) => {
const {
closeGiftCraftSelectModal, selectGiftForCraft,
loadMoreCraftableGifts, loadMoreMarketCraftableGifts,
updateCraftGiftsFilter, openGiftInfoModal,
} = getActions();
const scrollerRef = useRef<HTMLDivElement>();
const dialogRef = useRef<HTMLDivElement>();
const filtersSeparatorRef = useRef<HTMLDivElement>();
const [isScrolled, setIsScrolled] = useState(false);
const [isFiltersSeparatorAbove, setIsFiltersSeparatorAbove] = useState(false);
const lang = useLang();
const isOpen = Boolean(modal);
const renderingModal = useCurrentOrPrev(modal);
const renderingCraftModal = useCurrentOrPrev(craftModal);
const {
gift1,
gift2,
gift3,
gift4,
myCraftableGifts,
marketCraftableGifts,
marketFilter,
marketUpdateIteration = 0,
marketCraftableGiftsCount,
myCraftableGiftsNextOffset,
marketCraftableGiftsNextOffset,
isMarketLoading,
} = renderingCraftModal || {};
const { isLoading } = renderingModal || {};
const hasMoreMyGifts = Boolean(myCraftableGiftsNextOffset);
const hasMoreMarketGifts = Boolean(marketCraftableGiftsNextOffset);
const { observe } = useIntersectionObserver({
rootRef: scrollerRef,
throttleMs: INTERSECTION_THROTTLE,
isDisabled: !isOpen,
});
const hasMarketGiftsData = Boolean(marketCraftableGifts?.length);
const isMyDataLoaded = myCraftableGifts !== undefined;
const isMarketDataLoaded = marketCraftableGifts !== undefined;
const isDataLoaded = isMyDataLoaded && isMarketDataLoaded && !isLoading;
const prevMarketIteration = usePrevious(marketUpdateIteration);
const isMarketJustUpdated = prevMarketIteration !== undefined && prevMarketIteration !== marketUpdateIteration;
const isFiltersStuck = isScrolled && isFiltersSeparatorAbove;
const shouldShowTitleBorder = isScrolled && !isFiltersStuck;
const handleScroll = useLastCallback((e: { currentTarget: HTMLDivElement }) => {
const scroller = e.currentTarget;
runThrottledForScroll(() => {
setIsScrolled(scroller.scrollTop > 0);
const separator = filtersSeparatorRef.current;
if (separator && hasMarketGiftsData) {
const scrollerRect = scroller.getBoundingClientRect();
const separatorRect = separator.getBoundingClientRect();
setIsFiltersSeparatorAbove(separatorRect.top <= scrollerRect.top);
}
});
});
const selectedIds = useMemo(() => {
return new Set(
[gift1, gift2, gift3, gift4]
.filter((g): g is ApiSavedStarGift => Boolean(g))
.map((g) => getSavedGiftKey(g)),
);
}, [gift1, gift2, gift3, gift4]);
const selectedUniqueIds = useMemo(() => {
return new Set(
[gift1, gift2, gift3, gift4]
.filter((g): g is ApiSavedStarGift => Boolean(g) && g.gift.type === 'starGiftUnique')
.map((g) => (g.gift as ApiStarGiftUnique).id),
);
}, [gift1, gift2, gift3, gift4]);
const availableMyGifts = useMemo(() => {
if (!myCraftableGifts) return [];
return myCraftableGifts.filter((g) => {
if (g.gift.type === 'starGiftUnique' && selectedUniqueIds.has(g.gift.id)) {
return false;
}
return !selectedIds.has(getSavedGiftKey(g));
});
}, [myCraftableGifts, selectedIds, selectedUniqueIds]);
const availableMarketGifts = useMemo(() => {
if (!marketCraftableGifts) return [];
return marketCraftableGifts.filter((g) => !selectedUniqueIds.has(g.id));
}, [marketCraftableGifts, selectedUniqueIds]);
const myGiftByIdMap = useMemo(() => {
const map = new Map<string, ApiSavedStarGift>();
availableMyGifts.forEach((g) => {
if (g.gift.type === 'starGiftUnique') {
map.set(g.gift.id, g);
}
});
return map;
}, [availableMyGifts]);
const handleLoadMore = useLastCallback(() => {
if (hasMoreMyGifts) {
loadMoreCraftableGifts();
} else if (hasMoreMarketGifts) {
loadMoreMarketCraftableGifts();
}
});
const allItemIds = useMemo(() => {
const myIds = availableMyGifts.map((g) => `my-${getSavedGiftKey(g)}`);
const marketIds = availableMarketGifts.map((g) => `market-${g.id}`);
return [...myIds, ...marketIds];
}, [availableMyGifts, availableMarketGifts]);
const [viewportIds, getMore] = useInfiniteScroll(
handleLoadMore,
allItemIds,
!isOpen,
CRAFT_GIFTS_LIMIT,
);
const handleClose = useLastCallback(() => {
closeGiftCraftSelectModal();
});
const handleMyGiftClick = useLastCallback((gift: ApiStarGift) => {
if (gift.type !== 'starGiftUnique') return;
const savedGift = myGiftByIdMap.get(gift.id);
const slotIndex = renderingModal?.slotIndex;
if (savedGift && slotIndex !== undefined) {
selectGiftForCraft({ gift: savedGift, slotIndex });
}
});
const handleMarketGiftClick = useLastCallback((gift: ApiStarGift) => {
const slotIndex = renderingModal?.slotIndex;
if (slotIndex === undefined) return;
openGiftInfoModal({ gift, craftSlotIndex: slotIndex });
});
const handleResetMarketFilter = useLastCallback(() => {
updateCraftGiftsFilter({
filter: {
sortType: marketFilter?.sortType || 'byPrice',
modelAttributes: [],
backdropAttributes: [],
patternAttributes: [],
},
});
});
const hasMyGifts = availableMyGifts.length > 0;
const hasMarketGifts = availableMarketGifts.length > 0;
const isMarketGiftsEmpty = !hasMarketGifts && Boolean(marketCraftableGifts);
const hasMarketFilter = Boolean(
marketFilter?.modelAttributes?.length
|| marketFilter?.patternAttributes?.length
|| marketFilter?.backdropAttributes?.length,
);
const shouldShowMarketSection = isMarketDataLoaded && (hasMarketGifts || hasMarketFilter || isMarketLoading);
return (
<Modal
dialogRef={dialogRef}
isOpen={isOpen}
onClose={handleClose}
className={styles.modal}
contentClassName={styles.content}
hasAbsoluteCloseButton
isSlim
isLowStackPriority
>
<h3 className={buildClassName(styles.title, shouldShowTitleBorder && styles.titleWithBorder)}>
{lang('GiftCraftSelectTitle')}
</h3>
<div className={styles.wrapper}>
<Loading className={buildClassName(styles.loading, !isLoading && styles.hidden)} />
<InfiniteScroll
ref={scrollerRef}
className={buildClassName(styles.scrollContainer, isLoading && styles.hidden, 'custom-scroll')}
items={viewportIds}
onLoadMore={getMore}
onScroll={handleScroll}
itemSelector=".starGiftItem"
noFastList
noScrollRestore={isMarketJustUpdated}
preloadBackwards={CRAFT_GIFTS_LIMIT}
>
{isOpen && !hasMyGifts && !shouldShowMarketSection && isDataLoaded && (
<ResaleGiftsNotFound
className={styles.notFound}
description={lang('ResellGiftsNoFound')}
/>
)}
{isOpen && hasMyGifts && (
<div className={styles.section}>
<p className={styles.sectionTitle}>{lang('GiftCraftSelectYourGifts')}</p>
<div className={styles.giftsGrid}>
{availableMyGifts.map((savedGift) => {
const chancePercent = savedGift.gift.type === 'starGiftUnique'
? (savedGift.gift.craftChancePermille || 0) / 10
: 0;
const { backdrop } = getGiftAttributes(savedGift.gift) || {};
return (
<CraftGiftItem
key={`my-${getSavedGiftKey(savedGift)}`}
gift={savedGift.gift}
chancePercent={chancePercent}
chanceColor={backdrop?.centerColor}
observe={observe}
onClick={handleMyGiftClick}
/>
);
})}
</div>
</div>
)}
{isOpen && shouldShowMarketSection && (
<div className={styles.section}>
<p className={styles.sectionTitle}>
{lang('GiftCraftSelectMarketGifts', {
count: marketCraftableGiftsCount || 0 },
{ pluralValue: marketCraftableGiftsCount || 0 })}
</p>
<div ref={filtersSeparatorRef} />
<GiftResaleFilters
dialogRef={dialogRef}
className={buildClassName(styles.filters, isFiltersStuck && styles.stuck)}
filterType="craft"
/>
<Transition
className={styles.transitionWrapper}
name="semiFade"
activeKey={marketUpdateIteration}
>
{isMarketGiftsEmpty && (
<ResaleGiftsNotFound
className={styles.notFound}
description={lang('ResellGiftsNoFound')}
linkText={hasMarketFilter ? lang('ResellGiftsClearFilters') : undefined}
onLinkClick={hasMarketFilter ? handleResetMarketFilter : undefined}
/>
)}
{hasMarketGifts && (
<div className={styles.giftsGrid}>
{availableMarketGifts.map((marketGift) => {
const chancePercent = (marketGift.craftChancePermille || 0) / 10;
const { backdrop } = getGiftAttributes(marketGift) || {};
return (
<CraftGiftItem
key={`market-${marketGift.id}`}
gift={marketGift}
chancePercent={chancePercent}
chanceColor={backdrop?.centerColor}
showPrice
observe={observe}
onClick={handleMarketGiftClick}
/>
);
})}
</div>
)}
</Transition>
</div>
)}
</InfiniteScroll>
</div>
</Modal>
);
};
export default memo(withGlobal<OwnProps>((global): Complete<StateProps> => {
const tabState = selectTabState(global);
return {
craftModal: tabState.giftCraftModal,
};
})(GiftCraftSelectModal));

View File

@ -0,0 +1,16 @@
.root {
transform: rotate(135deg);
}
.background {
stroke: rgba(255, 255, 255, 0.15);
stroke-linecap: round;
stroke-width: 0.375rem;
}
.fill {
stroke: white;
stroke-linecap: round;
stroke-width: 0.375rem;
transition: stroke-dashoffset 0.3s ease;
}

View File

@ -0,0 +1,61 @@
import { memo } from '../../../../lib/teact/teact';
import buildClassName from '../../../../util/buildClassName';
import { clamp } from '../../../../util/math';
import { REM } from '../../../common/helpers/mediaDimensions';
import styles from './RadialProgress.module.scss';
type OwnProps = {
progress: number;
size?: number;
className?: string;
};
const VIEWBOX_SIZE = 100;
const RADIUS_RATIO = 0.35; // 35% of viewbox size
const STROKE_START = 0.125;
const STROKE_END = 0.875;
const ARC_RANGE = STROKE_END - STROKE_START; // 0.75 = 270 degrees
export const DEFAULT_RING_SIZE = 7.5 * REM;
const RadialProgress = ({ progress, size = DEFAULT_RING_SIZE, className }: OwnProps) => {
const center = VIEWBOX_SIZE / 2;
const radius = VIEWBOX_SIZE * RADIUS_RATIO;
const clampedProgress = clamp(progress, 0, 100);
const progressStrokeEnd = STROKE_START + ARC_RANGE * (clampedProgress / 100);
const bgDashoffset = 1 - ARC_RANGE;
const fillDashoffset = 1 - (progressStrokeEnd - STROKE_START);
return (
<svg
className={buildClassName(styles.root, className)}
viewBox={`0 0 ${VIEWBOX_SIZE} ${VIEWBOX_SIZE}`}
style={`width: ${size}px; height: ${size}px`}
>
<circle
className={styles.background}
cx={center}
cy={center}
r={radius}
fill="none"
pathLength="1"
style={`stroke-dasharray: 1; stroke-dashoffset: ${bgDashoffset}`}
/>
<circle
className={styles.fill}
cx={center}
cy={center}
r={radius}
fill="none"
pathLength="1"
style={`stroke-dasharray: 1; stroke-dashoffset: ${fillDashoffset}`}
/>
</svg>
);
};
export default memo(RadialProgress);

View File

@ -2,6 +2,21 @@
overflow: hidden;
}
.headerRightButtons {
position: absolute;
z-index: 3;
top: 0.875rem;
right: 3rem;
display: flex;
gap: 0.5rem;
align-items: center;
}
.craftButton {
position: relative;
}
.uniqueTitleNumber {
&.small {
font-size: 0.75em;
@ -73,9 +88,7 @@
.giftResalePriceContainer {
pointer-events: auto;
position: absolute;
top: 0.875rem;
right: 3.25rem;
position: relative;
display: flex;
align-items: center;

View File

@ -16,7 +16,7 @@ import { selectPeer, selectUser } from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName';
import { copyTextToClipboard } from '../../../../util/clipboard';
import { formatDateTimeToString } from '../../../../util/dates/dateFormat';
import { formatCurrencyAsString } from '../../../../util/formatCurrency';
import { formatCurrency, formatCurrencyAsString } from '../../../../util/formatCurrency';
import {
formatStarsAsIcon, formatStarsAsText, formatTonAsIcon, formatTonAsText,
getNextArrowReplacement,
@ -91,6 +91,7 @@ const GiftInfoModal = ({
openChatWithInfo,
focusMessage,
openGiftUpgradeModal,
openGiftCraftModal,
showNotification,
buyStarGift,
closeGiftModal,
@ -247,6 +248,12 @@ const GiftInfoModal = ({
});
});
const handleOpenCraftModal = useLastCallback(() => {
if (!savedGift || savedGift.gift.type !== 'starGiftUnique') return;
handleClose();
openGiftCraftModal({ gift: savedGift });
});
const giftAttributes = useMemo(() => {
return gift && getGiftAttributes(gift);
}, [gift]);
@ -262,6 +269,7 @@ const GiftInfoModal = ({
if (!gift || gift.type !== 'starGiftUnique' || !giftAttributes?.backdrop) return undefined;
const numberColor = giftAttributes.backdrop.textColor;
const digitCount = String(gift.number).length;
const numberSizeClass = digitCount >= 6 ? styles.small : styles.regular;
const styledNumber = (
@ -300,9 +308,7 @@ const GiftInfoModal = ({
<Button className={styles.buyButton} onClick={handleBuyGift}>
<div>
{lang('ButtonBuyGift', {
stars: resellPrice?.currency === TON_CURRENCY_CODE
? formatTonAsIcon(lang, resellPrice.amount, { shouldConvertFromNanos: true })
: formatStarsAsIcon(lang, resellPrice?.amount, { asFont: true }),
stars: formatCurrency(lang, resellPrice.amount, resellPrice.currency, { asFontIcon: true }),
}, { withNodes: true })}
</div>
{resellPrice?.currency === TON_CURRENCY_CODE && Boolean(resellPriceInStars) && (
@ -389,6 +395,14 @@ const GiftInfoModal = ({
return text;
}, [gift, lang]);
// ToDo
// const canCraft = Boolean(
// canManage && savedGift?.canCraftAt && getServerTime() >= savedGift.canCraftAt,
// );
// Mock for Tests
const canCraft = Boolean(canManage && savedGift?.canCraftAt);
const modalData = useMemo(() => {
if (!typeGift || !gift) {
return undefined;
@ -487,10 +501,7 @@ const GiftInfoModal = ({
}
const uniqueGiftModalHeader = (
<div
className={styles.modalHeader}
>
<div className={styles.modalHeader}>
<Button
className={styles.closeButton}
round
@ -500,23 +511,34 @@ const GiftInfoModal = ({
ariaLabel={lang('Close')}
onClick={handleClose}
/>
{Boolean(resellPrice?.amount) && (
<div className={styles.giftResalePriceContainer}>
{resellPrice.currency === TON_CURRENCY_CODE
? formatTonAsIcon(lang, resellPrice.amount, {
className: styles.giftResalePriceStar,
shouldConvertFromNanos: true,
})
: formatStarsAsIcon(lang, resellPrice.amount, {
asFont: true,
className: styles.giftResalePriceStar,
})}
</div>
)}
</div>
);
const headerRightToolBar = (Boolean(resellPrice?.amount) || canCraft) ? (
<div className={styles.headerRightButtons}>
{Boolean(resellPrice?.amount) && (
<div className={styles.giftResalePriceContainer}>
{formatCurrency(lang, resellPrice.amount, resellPrice.currency, {
asFontIcon: true,
iconClassName: styles.giftResalePriceStar,
})}
</div>
)}
{canCraft && (
<Button
className={styles.craftButton}
round
color="translucent-white"
size="tiny"
ariaLabel={lang('GiftInfoCraft')}
onClick={handleOpenCraftModal}
>
<Icon name="craft" />
</Button>
)}
</div>
) : undefined;
const uniqueGiftHeader = isGiftUnique && (
<div ref={uniqueGiftHeaderRef} className={buildClassName(styles.header, styles.uniqueGift)}>
<UniqueGiftHeader
@ -804,6 +826,7 @@ const GiftInfoModal = ({
return {
modalHeader: isGiftUnique ? uniqueGiftModalHeader : undefined,
headerRightToolBar: isGiftUnique ? headerRightToolBar : undefined,
header: isGiftUnique ? uniqueGiftHeader : regularHeader,
tableData,
footer,
@ -812,7 +835,7 @@ const GiftInfoModal = ({
typeGift, savedGift, renderingTargetPeer, giftSticker, lang,
canManage, hasConvertOption, isSender, oldLang, tonExplorerUrl,
gift, giftAttributes, renderFooterButton, isTargetChat,
isGiftUnique, saleDateInfo,
isGiftUnique, saleDateInfo, canCraft, handleOpenCraftModal,
canBuyGift, giftOwnerTitle, resellPrice, uniqueGiftTitle, uniqueGiftSubtitle, releasedByPeer,
]);
@ -831,6 +854,7 @@ const GiftInfoModal = ({
<TableInfoModal
isOpen={isOpen}
modalHeader={modalData?.modalHeader}
headerRightToolBar={modalData?.headerRightToolBar}
header={modalData?.header}
hasBackdrop={isGiftUnique}
tableData={modalData?.tableData}
@ -842,7 +866,7 @@ const GiftInfoModal = ({
onClose={handleClose}
withBalanceBar={Boolean(canBuyGift)}
currencyInBalanceBar={confirmPrice?.currency}
isLowStackPriority
isLowStackPriority={renderingModal?.craftSlotIndex !== undefined ? true : undefined}
/>
{uniqueGift && currentUser && Boolean(confirmPrice) && (
<ConfirmDialog

View File

@ -123,11 +123,20 @@ const GiftPreviewModal = ({ modal, animationLevel }: OwnProps & StateProps) => {
if (newModel && newModel.rarity.type !== 'regular') showCraftableModels();
}, [initialAttributes, firstModel, firstPattern, firstBackdrop]);
useEffect(() => {
if (renderingModal?.shouldShowCraftableOnStart) {
showCraftableModels();
}
}, [renderingModal?.shouldShowCraftableOnStart]);
const handleStickerAnimationEnded = useLastCallback((modelName: string) => {
if (modelName !== selectedModel?.name || !isPlayingRandomPreviews) return;
if (!originGift || !selectedModel || !selectedPattern || !selectedBackdrop) return;
const newAttributes = getRandomGiftPreviewAttributes(renderingModal?.attributes, {
const attributesToUse = renderingModal?.shouldShowCraftableOnStart && isCraftableModelsMode
? renderingModal.attributes.filter((attr) => attr.type !== 'model' || attr.rarity.type !== 'regular')
: renderingModal?.attributes;
const newAttributes = getRandomGiftPreviewAttributes(attributesToUse, {
model: selectedModel,
pattern: selectedPattern,
backdrop: selectedBackdrop,

View File

@ -47,6 +47,7 @@ export type OwnProps = {
isLowStackPriority?: boolean;
dialogContent?: React.ReactNode;
moreMenuItems?: TeactNode;
headerRightToolBar?: TeactNode;
withBalanceBar?: boolean;
currencyInBalanceBar?: 'TON' | 'XTR';
isCondensedHeader?: boolean;
@ -95,6 +96,7 @@ const Modal = (props: OwnProps) => {
dialogStyle,
dialogContent,
moreMenuItems,
headerRightToolBar: headerToolBar,
withBalanceBar,
isCondensedHeader,
currencyInBalanceBar = 'XTR',
@ -226,6 +228,7 @@ const Modal = (props: OwnProps) => {
)}
<div className={modalDialogClassName} ref={actualDialogRef} style={dialogStyle}>
{renderHeader()}
{headerToolBar}
{Boolean(moreMenuItems) && (
<>
<Button

View File

@ -1,4 +1,10 @@
import type { ApiSavedStarGift, ApiStarGiftUnique } from '../../../api/types';
import type {
ApiInputSavedStarGift,
ApiRequestInputSavedStarGift,
ApiSavedStarGift,
ApiStarGiftAttribute,
ApiStarGiftUnique,
} from '../../../api/types';
import type { ActionReturnType } from '../../types';
import {
@ -10,6 +16,7 @@ import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { buildCollectionByCallback, buildCollectionByKey } from '../../../util/iteratees';
import { getServerTime } from '../../../util/serverTime';
import { callApi } from '../../../api/gramjs';
import { preloadGiftAttributeStickers } from '../../../components/common/helpers/gifts';
import { RESALE_GIFTS_LIMIT } from '../../../limits';
import { areInputSavedGiftsEqual, getRequestInputSavedStarGift } from '../../helpers/payments';
import { addActionHandler, getGlobal, getPromiseActions, setGlobal } from '../../index';
@ -27,6 +34,8 @@ import {
import { updateTabState } from '../../reducers/tabs';
import {
selectActiveGiftsCollectionId,
selectChat,
selectChatMessage,
selectGiftProfileFilter,
selectPeer,
selectPeerCollectionSavedGifts,
@ -753,8 +762,389 @@ addActionHandler('loadActiveGiftAuctions', async (global, actions, payload): Pro
setGlobal(global);
});
addActionHandler('openGiftInfoModalFromMessage', async (global, actions, payload): Promise<void> => {
const {
chatId, messageId, tabId = getCurrentTabId(),
} = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
await getPromiseActions().loadMessage({ chatId, messageId });
global = getGlobal();
const message = selectChatMessage(global, chatId, messageId);
if (!message || !message.content.action) return;
const action = message.content.action;
if (action.type !== 'starGift' && action.type !== 'starGiftUnique') return;
const starGift = action.type === 'starGift' ? action : undefined;
const uniqueGift = action.type === 'starGiftUnique' ? action : undefined;
const giftMsgId = starGift?.giftMsgId;
const giftReceiverId = action.peerId || (message.isOutgoing ? message.chatId : global.currentUserId!);
const inputGift: ApiInputSavedStarGift = (() => {
if (giftMsgId) {
return { type: 'user', messageId: giftMsgId };
}
if (action.savedId) {
return { type: 'chat', chatId, savedId: action.savedId };
}
return { type: 'user', messageId };
})();
const fromId = action.fromId || (message.isOutgoing ? global.currentUserId! : message.chatId);
const gift: ApiSavedStarGift = {
date: message.date,
gift: action.gift,
message: starGift?.message,
starsToConvert: starGift?.starsToConvert,
isNameHidden: starGift?.isNameHidden,
isUnsaved: !action.isSaved,
fromId,
messageId: message.id,
isConverted: starGift?.isConverted,
upgradeMsgId: starGift?.upgradeMsgId,
canUpgrade: starGift?.canUpgrade,
alreadyPaidUpgradeStars: starGift?.alreadyPaidUpgradeStars,
inputGift,
canExportAt: uniqueGift?.canExportAt,
savedId: action.savedId,
transferStars: uniqueGift?.transferStars,
dropOriginalDetailsStars: uniqueGift?.dropOriginalDetailsStars,
prepaidUpgradeHash: starGift?.prepaidUpgradeHash,
canCraftAt: uniqueGift?.canCraftAt,
};
actions.openGiftInfoModal({ peerId: giftReceiverId, gift, tabId });
});
addActionHandler('openGiftInfoValueModal', async (global, actions, payload): Promise<void> => {
const { gift, tabId = getCurrentTabId() } = payload;
const result = await callApi('fetchUniqueStarGiftValueInfo', { slug: gift.slug });
if (!result) return;
global = getGlobal();
global = updateTabState(global, {
giftInfoValueModal: {
valueInfo: result,
gift,
},
}, tabId);
setGlobal(global);
});
addActionHandler('openGiftCraftModal', async (global, _actions, payload): Promise<void> => {
const { gift, tabId = getCurrentTabId() } = payload;
const uniqueGift = gift?.gift.type === 'starGiftUnique' ? gift.gift : undefined;
const regularGiftId = uniqueGift?.regularGiftId;
let previewAttributes: ApiStarGiftAttribute[] | undefined;
if (regularGiftId) {
const result = await callApi('fetchStarGiftUpgradeAttributes', { giftId: regularGiftId });
if (result) {
const craftableModels = result.attributes.filter(
(attr) => attr.type === 'model' && attr.rarity.type !== 'regular',
);
preloadGiftAttributeStickers(craftableModels);
previewAttributes = craftableModels;
}
}
global = getGlobal();
global = updateTabState(global, {
giftCraftModal: {
regularGiftId,
regularGiftTitle: uniqueGift?.title,
gift1: gift,
marketFilter: { sortType: 'byPrice' },
marketUpdateIteration: 0,
previewAttributes,
},
}, tabId);
setGlobal(global);
});
addActionHandler('openGiftCraftSelectModal', async (global, actions, payload): Promise<void> => {
const { slotIndex, tabId = getCurrentTabId() } = payload;
const tabState = selectTabState(global, tabId);
const craftModal = tabState.giftCraftModal;
if (!craftModal?.regularGiftId) return;
const shouldLoadMyGifts = !craftModal.myCraftableGifts || craftModal.shouldRefreshMyCraftableGifts;
const shouldLoadMarketGifts = !craftModal.marketCraftableGifts;
if (!shouldLoadMyGifts && !shouldLoadMarketGifts) {
global = updateTabState(global, {
giftCraftSelectModal: { slotIndex },
}, tabId);
setGlobal(global);
return;
}
global = updateTabState(global, {
giftCraftSelectModal: { slotIndex, isLoading: true },
}, tabId);
setGlobal(global);
const [myGiftsResult, marketGiftsResult] = await Promise.all([
shouldLoadMyGifts
? callApi('fetchCraftStarGifts', { giftId: craftModal.regularGiftId, peerId: global.currentUserId! })
: undefined,
shouldLoadMarketGifts
? callApi('fetchResaleGifts', {
giftId: craftModal.regularGiftId,
filter: craftModal.marketFilter,
forCraft: true,
})
: undefined,
]);
global = getGlobal();
const currentCraftModal = selectTabState(global, tabId).giftCraftModal;
const currentSelectModal = selectTabState(global, tabId).giftCraftSelectModal;
if (!currentCraftModal || !currentSelectModal) return;
// Filter to only unique gifts
const savedGifts = myGiftsResult?.gifts.filter((g) => g.gift.type === 'starGiftUnique');
const marketGifts = marketGiftsResult?.gifts.filter(
(g): g is ApiStarGiftUnique => g.type === 'starGiftUnique',
);
const didLoadMyGifts = shouldLoadMyGifts && myGiftsResult;
const didLoadMarketGifts = shouldLoadMarketGifts && marketGiftsResult;
global = updateTabState(global, {
giftCraftModal: {
...currentCraftModal,
myCraftableGifts: didLoadMyGifts ? savedGifts : currentCraftModal.myCraftableGifts,
myCraftableGiftsNextOffset: didLoadMyGifts
? myGiftsResult.nextOffset : currentCraftModal.myCraftableGiftsNextOffset,
shouldRefreshMyCraftableGifts: shouldLoadMyGifts ? !myGiftsResult :
currentCraftModal.shouldRefreshMyCraftableGifts,
marketCraftableGifts: didLoadMarketGifts ? marketGifts : currentCraftModal.marketCraftableGifts,
marketCraftableGiftsNextOffset: didLoadMarketGifts
? marketGiftsResult.nextOffset : currentCraftModal.marketCraftableGiftsNextOffset,
marketCraftableGiftsCount: didLoadMarketGifts
? marketGiftsResult.count : currentCraftModal.marketCraftableGiftsCount,
marketAttributes: didLoadMarketGifts ? marketGiftsResult.attributes : currentCraftModal.marketAttributes,
marketCounters: didLoadMarketGifts ? marketGiftsResult.counters : currentCraftModal.marketCounters,
marketAttributesHash: didLoadMarketGifts
? marketGiftsResult.attributesHash : currentCraftModal.marketAttributesHash,
},
giftCraftSelectModal: {
...currentSelectModal,
isLoading: false,
},
}, tabId);
setGlobal(global);
});
addActionHandler('loadMoreCraftableGifts', async (global, actions, payload): Promise<void> => {
const { tabId = getCurrentTabId() } = payload || {};
const tabState = selectTabState(global, tabId);
const craftModal = tabState.giftCraftModal;
if (!craftModal?.myCraftableGiftsNextOffset) return;
const gift1Unique = craftModal.gift1?.gift.type === 'starGiftUnique' ? craftModal.gift1.gift : undefined;
if (!gift1Unique?.regularGiftId) return;
const result = await callApi('fetchCraftStarGifts', {
giftId: gift1Unique.regularGiftId,
peerId: global.currentUserId!,
offset: craftModal.myCraftableGiftsNextOffset,
});
if (!result) return;
global = getGlobal();
const currentCraftModal = selectTabState(global, tabId).giftCraftModal;
if (!currentCraftModal) return;
// Filter to only unique gifts
const newSavedGifts = result.gifts.filter((g) => g.gift.type === 'starGiftUnique');
global = updateTabState(global, {
giftCraftModal: {
...currentCraftModal,
myCraftableGifts: [...(currentCraftModal.myCraftableGifts || []), ...newSavedGifts],
myCraftableGiftsNextOffset: result.nextOffset,
},
}, tabId);
setGlobal(global);
});
addActionHandler('loadMoreMarketCraftableGifts', async (global, actions, payload): Promise<void> => {
const { tabId = getCurrentTabId() } = payload || {};
const tabState = selectTabState(global, tabId);
const craftModal = tabState.giftCraftModal;
if (!craftModal?.regularGiftId) return;
if (craftModal.isMarketLoading) return;
if (craftModal.marketCraftableGifts && !craftModal.marketCraftableGiftsNextOffset) return;
global = updateTabState(global, {
giftCraftModal: {
...craftModal,
isMarketLoading: true,
},
}, tabId);
setGlobal(global);
const result = await callApi('fetchResaleGifts', {
giftId: craftModal.regularGiftId,
offset: craftModal.marketCraftableGiftsNextOffset,
filter: craftModal.marketFilter,
forCraft: true,
});
global = getGlobal();
const currentCraftModal = selectTabState(global, tabId).giftCraftModal;
if (!currentCraftModal) return;
if (!result) {
global = updateTabState(global, {
giftCraftModal: { ...currentCraftModal, isMarketLoading: false },
}, tabId);
setGlobal(global);
return;
}
const newGifts = result.gifts.filter((g): g is ApiStarGiftUnique => g.type === 'starGiftUnique');
global = updateTabState(global, {
giftCraftModal: {
...currentCraftModal,
marketCraftableGifts: [...(currentCraftModal.marketCraftableGifts || []), ...newGifts],
marketCraftableGiftsNextOffset: result.nextOffset,
isMarketLoading: false,
},
}, tabId);
setGlobal(global);
});
addActionHandler('updateCraftGiftsFilter', async (global, actions, payload): Promise<void> => {
const { filter, tabId = getCurrentTabId() } = payload;
const tabState = selectTabState(global, tabId);
const modal = tabState.giftCraftModal;
if (!modal?.regularGiftId) return;
global = updateTabState(global, {
giftCraftModal: {
...modal,
marketFilter: filter,
isMarketLoading: true,
},
}, tabId);
setGlobal(global);
const result = await callApi('fetchResaleGifts', {
giftId: modal.regularGiftId,
filter,
forCraft: true,
});
global = getGlobal();
const currentModal = selectTabState(global, tabId).giftCraftModal;
if (!currentModal) return;
if (!result) {
global = updateTabState(global, {
giftCraftModal: { ...currentModal, isMarketLoading: false },
}, tabId);
setGlobal(global);
return;
}
const newGifts = result.gifts.filter((g): g is ApiStarGiftUnique => g.type === 'starGiftUnique');
global = updateTabState(global, {
giftCraftModal: {
...currentModal,
marketCraftableGifts: newGifts,
marketCraftableGiftsNextOffset: result.nextOffset,
marketCraftableGiftsCount: result.count,
marketCounters: result.counters,
marketUpdateIteration: currentModal.marketUpdateIteration + 1,
isMarketLoading: false,
},
}, tabId);
setGlobal(global);
});
addActionHandler('craftStarGift', async (global, _actions, payload): Promise<void> => {
const { tabId = getCurrentTabId() } = payload || {};
const tabState = selectTabState(global, tabId);
const modal = tabState.giftCraftModal;
if (!modal?.regularGiftId) return;
const savedGifts = [modal.gift1, modal.gift2, modal.gift3, modal.gift4].filter(
(g): g is ApiSavedStarGift => Boolean(g),
);
if (savedGifts.length === 0) return;
const inputSavedGifts = savedGifts
.map((g) => g.inputGift && getRequestInputSavedStarGift(global, g.inputGift))
.filter((g): g is ApiRequestInputSavedStarGift => Boolean(g));
if (inputSavedGifts.length === 0) return;
const result = await callApi('craftStarGift', { inputSavedGifts });
if (result?.error) {
global = getGlobal();
const currentModal = selectTabState(global, tabId).giftCraftModal;
if (!currentModal) return;
global = updateTabState(global, {
giftCraftModal: {
...currentModal,
craftResult: { success: false, isError: true },
},
}, tabId);
setGlobal(global);
}
});
addActionHandler('openAboutStarGiftModal', async (global, actions, payload): Promise<void> => {
const { tabId = getCurrentTabId() } = payload || {};
const result = await callApi('fetchPremiumPromo');
let videoId: string | undefined;
let videoThumbnail;
if (result?.promo) {
const giftsIndex = result.promo.videoSections.indexOf('gifts');
if (giftsIndex !== -1 && giftsIndex < result.promo.videos.length) {
const video = result.promo.videos[giftsIndex];
videoId = video.id;
videoThumbnail = video.thumbnail;
}
}
global = getGlobal();
global = updateTabState(global, {
aboutStarGiftModal: { videoId, videoThumbnail },
}, tabId);
setGlobal(global);
});
addActionHandler('openGiftPreviewModal', async (global, _actions, payload): Promise<void> => {
const { originGift, tabId = getCurrentTabId() } = payload;
const { originGift, shouldShowCraftableOnStart, tabId = getCurrentTabId() } = payload;
const giftId = originGift.type === 'starGiftUnique' ? originGift.regularGiftId : originGift.id;
const result = await callApi('fetchStarGiftUpgradeAttributes', { giftId });
@ -765,6 +1155,7 @@ addActionHandler('openGiftPreviewModal', async (global, _actions, payload): Prom
giftPreviewModal: {
originGift,
attributes: result.attributes,
shouldShowCraftableOnStart,
},
}, tabId);
setGlobal(global);

View File

@ -339,6 +339,15 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
actions.reloadPeerSavedGifts({ peerId: global.currentUserId! });
}
if (tabState.giftCraftModal && actionStarGift.isCrafted) {
global = updateTabState(global, {
giftCraftModal: {
...tabState.giftCraftModal,
craftResult: { success: true, gift: actionStarGift },
},
}, tabId);
}
});
setGlobal(global);

View File

@ -162,6 +162,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
const starGiftModalState = selectTabState(global, tabId).giftInfoModal;
if (starGiftModalState) {
const { craftSlotIndex, gift } = starGiftModalState;
const actualGift = 'gift' in gift ? gift.gift : gift;
const giftId = actualGift.type === 'starGiftUnique' ? actualGift.id : undefined;
actions.showNotification({
message: {
key: 'StarsGiftBought',
@ -173,6 +177,11 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
}
actions.reloadPeerSavedGifts({ peerId: inputInvoice.peerId });
actions.requestConfetti({ withStars: true, tabId });
if (craftSlotIndex !== undefined && giftId) {
actions.selectPurchasedGiftForCraft({ giftId, slotIndex: craftSlotIndex, tabId });
}
actions.closeGiftInfoModal({ tabId });
}
}
@ -270,5 +279,21 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
setGlobal(global);
break;
}
case 'updateStarGiftCraftFail': {
Object.values(global.byTabId).forEach(({ id: tabId }) => {
const modal = selectTabState(global, tabId).giftCraftModal;
if (modal) {
global = updateTabState(global, {
giftCraftModal: {
...modal,
craftResult: { success: false },
},
}, tabId);
}
});
setGlobal(global);
break;
}
}
});

View File

@ -1,16 +1,12 @@
import { getPromiseActions } from '../../../global';
import type { ApiInputSavedStarGift, ApiSavedStarGift } from '../../../api/types';
import type { ApiSavedStarGift, ApiStarGiftUnique } from '../../../api/types';
import type { ActionReturnType } from '../../types';
import { STARS_CURRENCY_CODE } from '../../../config';
import { selectChat } from '../../../global/selectors';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
import * as langProvider from '../../../util/oldLangProvider';
import { callApi } from '../../../api/gramjs';
import { addTabStateResetterAction } from '../../helpers/meta';
import { getPrizeStarsTransactionFromGiveaway, getStarsTransactionFromGift } from '../../helpers/payments';
import { addActionHandler, getGlobal, setGlobal } from '../../index';
import { addActionHandler, setGlobal } from '../../index';
import { clearStarPayment, openStarsTransactionModal } from '../../reducers';
import { removeGiftAuction } from '../../reducers/gifts';
import { updateTabState } from '../../reducers/tabs';
@ -18,6 +14,14 @@ import {
selectChatMessage, selectIsCurrentUserFrozen, selectShouldRemoveGiftAuction, selectStarsPayment, selectTabState,
} from '../../selectors';
function buildShortSavedGift(gift: ApiStarGiftUnique, fromId?: string): ApiSavedStarGift {
return {
gift,
date: Math.floor(Date.now() / 1000),
fromId,
};
}
addActionHandler('processOriginStarsPayment', (global, actions, payload): ActionReturnType => {
const { originData, status, tabId = getCurrentTabId() } = payload;
const {
@ -248,69 +252,9 @@ addActionHandler('closeStarsGiftModal', (global, actions, payload): ActionReturn
}, tabId);
});
addActionHandler('openGiftInfoModalFromMessage', async (global, actions, payload): Promise<void> => {
const {
chatId, messageId, tabId = getCurrentTabId(),
} = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
await getPromiseActions().loadMessage({ chatId, messageId });
global = getGlobal();
const message = selectChatMessage(global, chatId, messageId);
if (!message || !message.content.action) return;
const action = message.content.action;
if (action.type !== 'starGift' && action.type !== 'starGiftUnique') return;
const starGift = action.type === 'starGift' ? action : undefined;
const uniqueGift = action.type === 'starGiftUnique' ? action : undefined;
const giftMsgId = starGift?.giftMsgId;
const giftReceiverId = action.peerId || (message.isOutgoing ? message.chatId : global.currentUserId!);
const inputGift: ApiInputSavedStarGift = (() => {
if (giftMsgId) {
return { type: 'user', messageId: giftMsgId };
}
if (action.savedId) {
return { type: 'chat', chatId, savedId: action.savedId };
}
return { type: 'user', messageId };
})();
const fromId = action.fromId || (message.isOutgoing ? global.currentUserId! : message.chatId);
const gift: ApiSavedStarGift = {
date: message.date,
gift: action.gift,
message: starGift?.message,
starsToConvert: starGift?.starsToConvert,
isNameHidden: starGift?.isNameHidden,
isUnsaved: !action.isSaved,
fromId,
messageId: message.id,
isConverted: starGift?.isConverted,
upgradeMsgId: starGift?.upgradeMsgId,
canUpgrade: starGift?.canUpgrade,
alreadyPaidUpgradeStars: starGift?.alreadyPaidUpgradeStars,
inputGift,
canExportAt: uniqueGift?.canExportAt,
savedId: action.savedId,
transferStars: uniqueGift?.transferStars,
dropOriginalDetailsStars: uniqueGift?.dropOriginalDetailsStars,
prepaidUpgradeHash: starGift?.prepaidUpgradeHash,
};
actions.openGiftInfoModal({ peerId: giftReceiverId, gift, tabId });
});
addActionHandler('openGiftInfoModal', (global, actions, payload): ActionReturnType => {
const {
gift, tabId = getCurrentTabId(),
gift, craftSlotIndex, tabId = getCurrentTabId(),
} = payload;
const peerId = 'peerId' in payload ? payload.peerId : undefined;
@ -321,6 +265,7 @@ addActionHandler('openGiftInfoModal', (global, actions, payload): ActionReturnTy
peerId,
gift,
recipientId,
craftSlotIndex,
},
}, tabId);
});
@ -398,22 +343,6 @@ addActionHandler('closeResaleGiftsMarket', (global, actions, payload): ActionRet
return global;
});
addActionHandler('openGiftInfoValueModal', async (global, actions, payload): Promise<void> => {
const { gift, tabId = getCurrentTabId() } = payload;
const result = await callApi('fetchUniqueStarGiftValueInfo', { slug: gift.slug });
if (!result) return;
global = getGlobal();
global = updateTabState(global, {
giftInfoValueModal: {
valueInfo: result,
gift,
},
}, tabId);
setGlobal(global);
});
addTabStateResetterAction('closeGiftInfoModal', 'giftInfoModal');
addTabStateResetterAction('closeGiftInfoValueModal', 'giftInfoValueModal');
@ -422,6 +351,95 @@ addTabStateResetterAction('closeGiftResalePriceComposerModal', 'giftResalePriceC
addTabStateResetterAction('closeGiftUpgradeModal', 'giftUpgradeModal');
addTabStateResetterAction('closeGiftCraftModal', 'giftCraftModal');
addTabStateResetterAction('closeGiftCraftSelectModal', 'giftCraftSelectModal');
addActionHandler('openGiftCraftInfoModal', (global, _actions, payload): ActionReturnType => {
const { gift, tabId = getCurrentTabId() } = payload;
return updateTabState(global, {
giftCraftInfoModal: { gift },
}, tabId);
});
addTabStateResetterAction('closeGiftCraftInfoModal', 'giftCraftInfoModal');
addActionHandler('selectGiftForCraft', (global, _actions, payload): ActionReturnType => {
const { gift, slotIndex, tabId = getCurrentTabId() } = payload;
const tabState = selectTabState(global, tabId);
const modal = tabState.giftCraftModal;
if (!modal) return undefined;
const slots = [modal.gift1, modal.gift2, modal.gift3, modal.gift4];
slots[slotIndex] = gift;
return updateTabState(global, {
giftCraftModal: {
...modal,
gift1: slots[0],
gift2: slots[1],
gift3: slots[2],
gift4: slots[3],
},
giftCraftSelectModal: gift ? undefined : tabState.giftCraftSelectModal,
}, tabId);
});
addActionHandler('selectPurchasedGiftForCraft', (global, actions, payload): ActionReturnType => {
const { giftId, slotIndex, tabId = getCurrentTabId() } = payload;
const tabState = selectTabState(global, tabId);
const craftModal = tabState.giftCraftModal;
const giftInfoModal = tabState.giftInfoModal;
if (!craftModal) return undefined;
const giftFromModal = giftInfoModal?.gift;
const actualGift = giftFromModal && 'gift' in giftFromModal ? giftFromModal.gift : giftFromModal;
if (!actualGift || actualGift.type !== 'starGiftUnique' || actualGift.id !== giftId) {
return undefined;
}
const shortSavedGift = buildShortSavedGift(actualGift, global.currentUserId);
const slots = [craftModal.gift1, craftModal.gift2, craftModal.gift3, craftModal.gift4];
slots[slotIndex] = shortSavedGift;
global = updateTabState(global, {
giftCraftModal: {
...craftModal,
gift1: slots[0],
gift2: slots[1],
gift3: slots[2],
gift4: slots[3],
shouldRefreshMyCraftableGifts: true,
},
}, tabId);
actions.closeGiftCraftSelectModal({ tabId });
return global;
});
addActionHandler('resetGiftCraftResult', (global, _actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
const modal = selectTabState(global, tabId).giftCraftModal;
if (!modal) return undefined;
return updateTabState(global, {
giftCraftModal: {
...modal,
craftResult: undefined,
gift1: undefined,
gift2: undefined,
gift3: undefined,
gift4: undefined,
},
}, tabId);
});
addTabStateResetterAction('closeGiftPreviewModal', 'giftPreviewModal');
addActionHandler('closeGiftAuctionModal', (global, _actions, payload): ActionReturnType => {
@ -476,30 +494,6 @@ addActionHandler('closeGiftAuctionInfoModal', (global, _actions, payload): Actio
return global;
});
addActionHandler('openAboutStarGiftModal', async (global, _actions, payload): Promise<void> => {
const { tabId = getCurrentTabId() } = payload || {};
const result = await callApi('fetchPremiumPromo');
let videoId: string | undefined;
let videoThumbnail;
if (result?.promo) {
const giftsIndex = result.promo.videoSections.indexOf('gifts');
if (giftsIndex !== -1 && giftsIndex < result.promo.videos.length) {
const video = result.promo.videos[giftsIndex];
videoId = video.id;
videoThumbnail = video.thumbnail;
}
}
global = getGlobal();
global = updateTabState(global, {
aboutStarGiftModal: { videoId, videoThumbnail },
}, tabId);
setGlobal(global);
});
addTabStateResetterAction('closeAboutStarGiftModal', 'aboutStarGiftModal');
addActionHandler('openGiftAuctionChangeRecipientModal', (global, _actions, payload): ActionReturnType => {

View File

@ -2640,6 +2640,9 @@ export interface ActionPayloads {
updateResaleGiftsFilter: {
filter: ResaleGiftsFilterOptions;
} & WithTabId;
updateCraftGiftsFilter: {
filter: ResaleGiftsFilterOptions;
} & WithTabId;
loadResaleGifts: {
giftId: string;
shouldRefresh?: boolean;
@ -2678,8 +2681,10 @@ export interface ActionPayloads {
peerId: string;
recipientId?: string;
gift: ApiSavedStarGift;
craftSlotIndex?: number;
} | {
gift: ApiStarGift;
craftSlotIndex?: number;
}) & WithTabId;
openLockedGiftModalInfo: {
untilDate?: number;
@ -2709,6 +2714,31 @@ export interface ActionPayloads {
closeGiftUpgradeModal: WithTabId | undefined;
shiftGiftUpgradeNextPrice: WithTabId | undefined;
openGiftCraftModal: {
gift: ApiSavedStarGift;
} & WithTabId;
closeGiftCraftModal: WithTabId | undefined;
resetGiftCraftResult: WithTabId | undefined;
openGiftCraftSelectModal: {
slotIndex: number;
} & WithTabId;
closeGiftCraftSelectModal: WithTabId | undefined;
openGiftCraftInfoModal: {
gift: ApiStarGiftUnique;
} & WithTabId;
closeGiftCraftInfoModal: WithTabId | undefined;
selectGiftForCraft: {
gift?: ApiSavedStarGift;
slotIndex: number;
} & WithTabId;
selectPurchasedGiftForCraft: {
giftId: string;
slotIndex: number;
} & WithTabId;
loadMoreCraftableGifts: WithTabId | undefined;
loadMoreMarketCraftableGifts: WithTabId | undefined;
craftStarGift: WithTabId | undefined;
openStarGiftPriceDecreaseInfoModal: {
prices: ApiStarGiftUpgradePrice[];
currentPrice: number;
@ -2742,6 +2772,7 @@ export interface ActionPayloads {
closeGiftInfoValueModal: WithTabId | undefined;
openGiftPreviewModal: {
originGift: ApiStarGift;
shouldShowCraftableOnStart?: boolean;
} & WithTabId;
closeGiftPreviewModal: WithTabId | undefined;
loadActiveGiftAuctions: undefined;

View File

@ -841,6 +841,7 @@ export type TabState = {
peerId?: string;
recipientId?: string;
gift: ApiSavedStarGift | ApiStarGift;
craftSlotIndex?: number;
};
giftInfoValueModal?: {
@ -891,6 +892,44 @@ export type TabState = {
maxPrice?: number;
};
giftCraftModal?: {
regularGiftId?: string;
regularGiftTitle?: string;
gift1?: ApiSavedStarGift;
gift2?: ApiSavedStarGift;
gift3?: ApiSavedStarGift;
gift4?: ApiSavedStarGift;
previewAttributes?: ApiStarGiftAttribute[];
myCraftableGifts?: ApiSavedStarGift[];
myCraftableGiftsNextOffset?: string;
shouldRefreshMyCraftableGifts?: boolean;
marketCraftableGifts?: ApiStarGiftUnique[];
marketCraftableGiftsNextOffset?: string;
marketCraftableGiftsCount?: number;
isMarketLoading?: boolean;
marketFilter: ResaleGiftsFilterOptions;
marketAttributes?: ApiStarGiftAttribute[];
marketCounters?: ApiStarGiftAttributeCounter[];
marketAttributesHash?: string;
marketUpdateIteration: number;
craftResult?: {
success: true;
gift: ApiStarGiftUnique;
} | {
success: false;
isError?: true;
};
};
giftCraftSelectModal?: {
slotIndex: number;
isLoading?: boolean;
};
giftCraftInfoModal?: {
gift: ApiStarGiftUnique;
};
giftWithdrawModal?: {
gift: ApiSavedStarGift;
isLoading?: boolean;
@ -904,6 +943,7 @@ export type TabState = {
giftPreviewModal?: {
attributes: ApiStarGiftAttribute[];
originGift: ApiStarGift;
shouldShowCraftableOnStart?: boolean;
};
giftAuctionModal?: {

View File

@ -1905,6 +1905,8 @@ payments.getStarGiftAuctionAcquiredGifts#6ba2cbec gift_id:long = payments.StarGi
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;
payments.getCraftStarGifts#fd05dd00 gift_id:long offset:string limit:int = payments.SavedStarGifts;
payments.craftStarGift#b0f9684f stargift:Vector<InputSavedStarGift> = Updates;
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;

View File

@ -359,6 +359,8 @@
"payments.getStarGiftAuctionAcquiredGifts",
"payments.getStarGiftActiveAuctions",
"payments.resolveStarGiftOffer",
"payments.getCraftStarGifts",
"payments.craftStarGift",
"payments.getStarGiftUpgradeAttributes",
"langpack.getLangPack",
"langpack.getStrings",

File diff suppressed because it is too large Load Diff

View File

@ -16,315 +16,319 @@
}
$icons-map: (
"active-sessions": "\f101",
"add-one-badge": "\f102",
"add-user-filled": "\f103",
"add-user": "\f104",
"add": "\f105",
"admin": "\f106",
"allow-speak": "\f107",
"animals": "\f108",
"animations": "\f109",
"archive-filled": "\f10a",
"archive-from-main": "\f10b",
"archive-to-main": "\f10c",
"archive": "\f10d",
"arrow-down-circle": "\f10e",
"arrow-down": "\f10f",
"arrow-left": "\f110",
"arrow-right": "\f111",
"ask-support": "\f112",
"attach": "\f113",
"auction-drop": "\f114",
"auction-filled": "\f115",
"auction-next-round": "\f116",
"auction": "\f117",
"author-hidden": "\f118",
"avatar-archived-chats": "\f119",
"avatar-deleted-account": "\f11a",
"avatar-saved-messages": "\f11b",
"bold": "\f11c",
"boost-outline": "\f11d",
"boost": "\f11e",
"boostcircle": "\f11f",
"boosts": "\f120",
"bot-command": "\f121",
"bot-commands-filled": "\f122",
"bots": "\f123",
"bug": "\f124",
"calendar-filter": "\f125",
"calendar": "\f126",
"camera-add": "\f127",
"camera": "\f128",
"car": "\f129",
"card": "\f12a",
"cash-circle": "\f12b",
"channel-filled": "\f12c",
"channel": "\f12d",
"channelviews": "\f12e",
"chat-badge": "\f12f",
"chats-badge": "\f130",
"check": "\f131",
"clock-edit": "\f132",
"clock": "\f133",
"close-circle": "\f134",
"close-topic": "\f135",
"close": "\f136",
"closed-gift": "\f137",
"cloud-download": "\f138",
"collapse-modal": "\f139",
"collapse": "\f13a",
"colorize": "\f13b",
"comments-sticker": "\f13c",
"comments": "\f13d",
"copy-media": "\f13e",
"copy": "\f13f",
"crown-take-off-outline": "\f140",
"crown-take-off": "\f141",
"crown-wear-outline": "\f142",
"crown-wear": "\f143",
"darkmode": "\f144",
"data": "\f145",
"delete-filled": "\f146",
"delete-left": "\f147",
"delete-user": "\f148",
"delete": "\f149",
"diamond": "\f14a",
"document": "\f14b",
"double-badge": "\f14c",
"down": "\f14d",
"download": "\f14e",
"dropdown-arrows": "\f14f",
"eats": "\f150",
"edit": "\f151",
"email": "\f152",
"enter": "\f153",
"expand-modal": "\f154",
"expand": "\f155",
"eye-crossed-outline": "\f156",
"eye-crossed": "\f157",
"eye-outline": "\f158",
"eye": "\f159",
"favorite-filled": "\f15a",
"favorite": "\f15b",
"file-badge": "\f15c",
"flag": "\f15d",
"folder-badge": "\f15e",
"folder-tabs-bot": "\f15f",
"folder-tabs-channel": "\f160",
"folder-tabs-chat": "\f161",
"folder-tabs-chats": "\f162",
"folder-tabs-folder": "\f163",
"folder-tabs-group": "\f164",
"folder-tabs-star": "\f165",
"folder-tabs-user": "\f166",
"folder": "\f167",
"fontsize": "\f168",
"forums": "\f169",
"forward": "\f16a",
"fragment": "\f16b",
"frozen-time": "\f16c",
"fullscreen": "\f16d",
"gifs": "\f16e",
"gift-transfer-inline": "\f16f",
"gift": "\f170",
"group-filled": "\f171",
"group": "\f172",
"grouped-disable": "\f173",
"grouped": "\f174",
"hand-stop": "\f175",
"hashtag": "\f176",
"hd-photo": "\f177",
"heart-outline": "\f178",
"heart": "\f179",
"help": "\f17a",
"info-filled": "\f17b",
"info": "\f17c",
"install": "\f17d",
"italic": "\f17e",
"key": "\f17f",
"keyboard": "\f180",
"lamp": "\f181",
"language": "\f182",
"large-pause": "\f183",
"large-play": "\f184",
"link-badge": "\f185",
"link-broken": "\f186",
"link": "\f187",
"location": "\f188",
"lock-badge": "\f189",
"lock": "\f18a",
"logout": "\f18b",
"loop": "\f18c",
"mention": "\f18d",
"menu": "\f18e",
"message-failed": "\f18f",
"message-pending": "\f190",
"message-read": "\f191",
"message-succeeded": "\f192",
"message": "\f193",
"microphone-alt": "\f194",
"microphone": "\f195",
"monospace": "\f196",
"more-circle": "\f197",
"more": "\f198",
"move-caption-down": "\f199",
"move-caption-up": "\f19a",
"mute": "\f19b",
"muted": "\f19c",
"my-notes": "\f19d",
"new-chat-filled": "\f19e",
"next-link": "\f19f",
"next": "\f1a0",
"nochannel": "\f1a1",
"noise-suppression": "\f1a2",
"non-contacts": "\f1a3",
"note": "\f1a4",
"one-filled": "\f1a5",
"open-in-new-tab": "\f1a6",
"password-off": "\f1a7",
"pause": "\f1a8",
"permissions": "\f1a9",
"phone-discard-outline": "\f1aa",
"phone-discard": "\f1ab",
"phone": "\f1ac",
"photo": "\f1ad",
"pin-badge": "\f1ae",
"pin-list": "\f1af",
"pin": "\f1b0",
"pinned-chat": "\f1b1",
"pinned-message": "\f1b2",
"pip": "\f1b3",
"play-story": "\f1b4",
"play": "\f1b5",
"poll": "\f1b6",
"previous": "\f1b7",
"privacy-policy": "\f1b8",
"proof-of-ownership": "\f1b9",
"quote-text": "\f1ba",
"quote": "\f1bb",
"radial-badge": "\f1bc",
"rating-icons-level1": "\f1bd",
"rating-icons-level10": "\f1be",
"rating-icons-level2": "\f1bf",
"rating-icons-level20": "\f1c0",
"rating-icons-level3": "\f1c1",
"rating-icons-level30": "\f1c2",
"rating-icons-level4": "\f1c3",
"rating-icons-level40": "\f1c4",
"rating-icons-level5": "\f1c5",
"rating-icons-level50": "\f1c6",
"rating-icons-level6": "\f1c7",
"rating-icons-level60": "\f1c8",
"rating-icons-level7": "\f1c9",
"rating-icons-level70": "\f1ca",
"rating-icons-level8": "\f1cb",
"rating-icons-level80": "\f1cc",
"rating-icons-level9": "\f1cd",
"rating-icons-level90": "\f1ce",
"rating-icons-negative": "\f1cf",
"readchats": "\f1d0",
"recent": "\f1d1",
"refund": "\f1d2",
"reload": "\f1d3",
"remove-quote": "\f1d4",
"remove": "\f1d5",
"reopen-topic": "\f1d6",
"reorder-tabs": "\f1d7",
"replace": "\f1d8",
"replies": "\f1d9",
"reply-filled": "\f1da",
"reply": "\f1db",
"revenue-split": "\f1dc",
"revote": "\f1dd",
"save-story": "\f1de",
"saved-messages": "\f1df",
"schedule": "\f1e0",
"scheduled": "\f1e1",
"sd-photo": "\f1e2",
"search": "\f1e3",
"select": "\f1e4",
"sell-outline": "\f1e5",
"sell": "\f1e6",
"send-outline": "\f1e7",
"send": "\f1e8",
"settings-filled": "\f1e9",
"settings": "\f1ea",
"share-filled": "\f1eb",
"share-screen-outlined": "\f1ec",
"share-screen-stop": "\f1ed",
"share-screen": "\f1ee",
"show-message": "\f1ef",
"sidebar": "\f1f0",
"skip-next": "\f1f1",
"skip-previous": "\f1f2",
"smallscreen": "\f1f3",
"smile": "\f1f4",
"sort-by-date": "\f1f5",
"sort-by-number": "\f1f6",
"sort-by-price": "\f1f7",
"sort": "\f1f8",
"speaker-muted-story": "\f1f9",
"speaker-outline": "\f1fa",
"speaker-story": "\f1fb",
"speaker": "\f1fc",
"spoiler-disable": "\f1fd",
"spoiler": "\f1fe",
"sport": "\f1ff",
"star": "\f200",
"stars-lock": "\f201",
"stars-refund": "\f202",
"stats": "\f203",
"stealth-future": "\f204",
"stealth-past": "\f205",
"stickers": "\f206",
"stop-raising-hand": "\f207",
"stop": "\f208",
"story-caption": "\f209",
"story-expired": "\f20a",
"story-priority": "\f20b",
"story-reply": "\f20c",
"strikethrough": "\f20d",
"tag-add": "\f20e",
"tag-crossed": "\f20f",
"tag-filter": "\f210",
"tag-name": "\f211",
"tag": "\f212",
"timer": "\f213",
"toncoin": "\f214",
"tools": "\f215",
"topic-new": "\f216",
"trade": "\f217",
"transcribe": "\f218",
"truck": "\f219",
"unarchive": "\f21a",
"underlined": "\f21b",
"understood": "\f21c",
"unique-profile": "\f21d",
"unlist-outline": "\f21e",
"unlist": "\f21f",
"unlock-badge": "\f220",
"unlock": "\f221",
"unmute": "\f222",
"unpin": "\f223",
"unread": "\f224",
"up": "\f225",
"user-filled": "\f226",
"user-online": "\f227",
"user-stars": "\f228",
"user": "\f229",
"video-outlined": "\f22a",
"video-stop": "\f22b",
"video": "\f22c",
"view-once": "\f22d",
"voice-chat": "\f22e",
"volume-1": "\f22f",
"volume-2": "\f230",
"volume-3": "\f231",
"warning": "\f232",
"web": "\f233",
"webapp": "\f234",
"word-wrap": "\f235",
"zoom-in": "\f236",
"zoom-out": "\f237",
"zoom-out": "\f101",
"zoom-in": "\f102",
"word-wrap": "\f103",
"webapp": "\f104",
"web": "\f105",
"warning": "\f106",
"volume-3": "\f107",
"volume-2": "\f108",
"volume-1": "\f109",
"voice-chat": "\f10a",
"view-once": "\f10b",
"video": "\f10c",
"video-stop": "\f10d",
"video-outlined": "\f10e",
"user": "\f10f",
"user-stars": "\f110",
"user-online": "\f111",
"user-filled": "\f112",
"up": "\f113",
"unread": "\f114",
"unpin": "\f115",
"unmute": "\f116",
"unlock": "\f117",
"unlock-badge": "\f118",
"unlist": "\f119",
"unlist-outline": "\f11a",
"unique-profile": "\f11b",
"understood": "\f11c",
"underlined": "\f11d",
"unarchive": "\f11e",
"truck": "\f11f",
"transcribe": "\f120",
"trade": "\f121",
"topic-new": "\f122",
"tools": "\f123",
"toncoin": "\f124",
"timer": "\f125",
"tag": "\f126",
"tag-name": "\f127",
"tag-filter": "\f128",
"tag-crossed": "\f129",
"tag-add": "\f12a",
"strikethrough": "\f12b",
"story-reply": "\f12c",
"story-priority": "\f12d",
"story-expired": "\f12e",
"story-caption": "\f12f",
"stop": "\f130",
"stop-raising-hand": "\f131",
"stickers": "\f132",
"stealth-past": "\f133",
"stealth-future": "\f134",
"stats": "\f135",
"stars-refund": "\f136",
"stars-lock": "\f137",
"star": "\f138",
"sport": "\f139",
"spoiler": "\f13a",
"spoiler-disable": "\f13b",
"speaker": "\f13c",
"speaker-story": "\f13d",
"speaker-outline": "\f13e",
"speaker-muted-story": "\f13f",
"sort": "\f140",
"sort-by-price": "\f141",
"sort-by-number": "\f142",
"sort-by-date": "\f143",
"smile": "\f144",
"smallscreen": "\f145",
"skip-previous": "\f146",
"skip-next": "\f147",
"sidebar": "\f148",
"show-message": "\f149",
"share-screen": "\f14a",
"share-screen-stop": "\f14b",
"share-screen-outlined": "\f14c",
"share-filled": "\f14d",
"settings": "\f14e",
"settings-filled": "\f14f",
"send": "\f150",
"send-outline": "\f151",
"sell": "\f152",
"sell-outline": "\f153",
"select": "\f154",
"search": "\f155",
"sd-photo": "\f156",
"scheduled": "\f157",
"schedule": "\f158",
"saved-messages": "\f159",
"save-story": "\f15a",
"revote": "\f15b",
"revenue-split": "\f15c",
"reply": "\f15d",
"reply-filled": "\f15e",
"replies": "\f15f",
"replace": "\f160",
"reorder-tabs": "\f161",
"reopen-topic": "\f162",
"remove": "\f163",
"remove-quote": "\f164",
"reload": "\f165",
"refund": "\f166",
"recent": "\f167",
"readchats": "\f168",
"radial-badge": "\f169",
"quote": "\f16a",
"quote-text": "\f16b",
"proof-of-ownership": "\f16c",
"privacy-policy": "\f16d",
"previous": "\f16e",
"poll": "\f16f",
"play": "\f170",
"play-story": "\f171",
"pip": "\f172",
"pinned-message": "\f173",
"pinned-chat": "\f174",
"pin": "\f175",
"pin-list": "\f176",
"pin-badge": "\f177",
"photo": "\f178",
"phone": "\f179",
"phone-discard": "\f17a",
"phone-discard-outline": "\f17b",
"permissions": "\f17c",
"pause": "\f17d",
"password-off": "\f17e",
"open-in-new-tab": "\f17f",
"one-filled": "\f180",
"note": "\f181",
"non-contacts": "\f182",
"noise-suppression": "\f183",
"nochannel": "\f184",
"next": "\f185",
"next-link": "\f186",
"new-chat-filled": "\f187",
"my-notes": "\f188",
"muted": "\f189",
"mute": "\f18a",
"move-caption-up": "\f18b",
"move-caption-down": "\f18c",
"more": "\f18d",
"more-circle": "\f18e",
"monospace": "\f18f",
"microphone": "\f190",
"microphone-alt": "\f191",
"message": "\f192",
"message-succeeded": "\f193",
"message-read": "\f194",
"message-pending": "\f195",
"message-failed": "\f196",
"menu": "\f197",
"mention": "\f198",
"loop": "\f199",
"logout": "\f19a",
"lock": "\f19b",
"lock-badge": "\f19c",
"location": "\f19d",
"link": "\f19e",
"link-broken": "\f19f",
"link-badge": "\f1a0",
"large-play": "\f1a1",
"large-pause": "\f1a2",
"language": "\f1a3",
"lamp": "\f1a4",
"keyboard": "\f1a5",
"key": "\f1a6",
"italic": "\f1a7",
"install": "\f1a8",
"info": "\f1a9",
"info-filled": "\f1aa",
"help": "\f1ab",
"heart": "\f1ac",
"heart-outline": "\f1ad",
"hd-photo": "\f1ae",
"hashtag": "\f1af",
"hand-stop": "\f1b0",
"grouped": "\f1b1",
"grouped-disable": "\f1b2",
"group": "\f1b3",
"group-filled": "\f1b4",
"gift": "\f1b5",
"gift-transfer-inline": "\f1b6",
"gifs": "\f1b7",
"fullscreen": "\f1b8",
"frozen-time": "\f1b9",
"fragment": "\f1ba",
"forward": "\f1bb",
"forums": "\f1bc",
"fontsize": "\f1bd",
"folder": "\f1be",
"folder-badge": "\f1bf",
"flag": "\f1c0",
"file-badge": "\f1c1",
"favorite": "\f1c2",
"favorite-filled": "\f1c3",
"eye": "\f1c4",
"eye-outline": "\f1c5",
"eye-crossed": "\f1c6",
"eye-crossed-outline": "\f1c7",
"expand": "\f1c8",
"expand-modal": "\f1c9",
"enter": "\f1ca",
"email": "\f1cb",
"edit": "\f1cc",
"eats": "\f1cd",
"dropdown-arrows": "\f1ce",
"download": "\f1cf",
"down": "\f1d0",
"double-badge": "\f1d1",
"document": "\f1d2",
"diamond": "\f1d3",
"delete": "\f1d4",
"delete-user": "\f1d5",
"delete-left": "\f1d6",
"delete-filled": "\f1d7",
"data": "\f1d8",
"darkmode": "\f1d9",
"crown-wear": "\f1da",
"crown-wear-outline": "\f1db",
"crown-take-off": "\f1dc",
"crown-take-off-outline": "\f1dd",
"craft": "\f1de",
"copy": "\f1df",
"copy-media": "\f1e0",
"comments": "\f1e1",
"comments-sticker": "\f1e2",
"combine-craft": "\f1e3",
"colorize": "\f1e4",
"collapse": "\f1e5",
"collapse-modal": "\f1e6",
"cloud-download": "\f1e7",
"closed-gift": "\f1e8",
"close": "\f1e9",
"close-topic": "\f1ea",
"close-circle": "\f1eb",
"clock": "\f1ec",
"clock-edit": "\f1ed",
"check": "\f1ee",
"chats-badge": "\f1ef",
"chat-badge": "\f1f0",
"channelviews": "\f1f1",
"channel": "\f1f2",
"channel-filled": "\f1f3",
"cash-circle": "\f1f4",
"card": "\f1f5",
"car": "\f1f6",
"camera": "\f1f7",
"camera-add": "\f1f8",
"calendar": "\f1f9",
"calendar-filter": "\f1fa",
"bug": "\f1fb",
"bots": "\f1fc",
"bot-commands-filled": "\f1fd",
"bot-command": "\f1fe",
"boosts": "\f1ff",
"boostcircle": "\f200",
"boost": "\f201",
"boost-outline": "\f202",
"boost-craft-chance": "\f203",
"bold": "\f204",
"avatar-saved-messages": "\f205",
"avatar-deleted-account": "\f206",
"avatar-archived-chats": "\f207",
"author-hidden": "\f208",
"auction": "\f209",
"auction-next-round": "\f20a",
"auction-filled": "\f20b",
"auction-drop": "\f20c",
"attach": "\f20d",
"ask-support": "\f20e",
"arrow-right": "\f20f",
"arrow-left": "\f210",
"arrow-down": "\f211",
"arrow-down-circle": "\f212",
"archive": "\f213",
"archive-to-main": "\f214",
"archive-from-main": "\f215",
"archive-filled": "\f216",
"animations": "\f217",
"animals": "\f218",
"allow-speak": "\f219",
"admin": "\f21a",
"add": "\f21b",
"add-user": "\f21c",
"add-user-filled": "\f21d",
"add-one-badge": "\f21e",
"add-filled": "\f21f",
"active-sessions": "\f220",
"rating-icons-negative": "\f221",
"rating-icons-level90": "\f222",
"rating-icons-level9": "\f223",
"rating-icons-level80": "\f224",
"rating-icons-level8": "\f225",
"rating-icons-level70": "\f226",
"rating-icons-level7": "\f227",
"rating-icons-level60": "\f228",
"rating-icons-level6": "\f229",
"rating-icons-level50": "\f22a",
"rating-icons-level5": "\f22b",
"rating-icons-level40": "\f22c",
"rating-icons-level4": "\f22d",
"rating-icons-level30": "\f22e",
"rating-icons-level3": "\f22f",
"rating-icons-level20": "\f230",
"rating-icons-level2": "\f231",
"rating-icons-level10": "\f232",
"rating-icons-level1": "\f233",
"folder-tabs-user": "\f234",
"folder-tabs-star": "\f235",
"folder-tabs-group": "\f236",
"folder-tabs-folder": "\f237",
"folder-tabs-chats": "\f238",
"folder-tabs-chat": "\f239",
"folder-tabs-channel": "\f23a",
"folder-tabs-bot": "\f23b",
);

Binary file not shown.

Binary file not shown.

View File

@ -1,312 +1,316 @@
export type FontIconName =
| 'active-sessions'
| 'add-one-badge'
| 'add-user-filled'
| 'add-user'
| 'add'
| 'admin'
| 'allow-speak'
| 'animals'
| 'animations'
| 'archive-filled'
| 'archive-from-main'
| 'archive-to-main'
| 'archive'
| 'arrow-down-circle'
| 'arrow-down'
| 'arrow-left'
| 'arrow-right'
| 'ask-support'
| 'attach'
| 'auction-drop'
| 'auction-filled'
| 'auction-next-round'
| 'auction'
| 'author-hidden'
| 'avatar-archived-chats'
| 'avatar-deleted-account'
| 'avatar-saved-messages'
| 'bold'
| 'boost-outline'
| 'boost'
| 'boostcircle'
| 'boosts'
| 'bot-command'
| 'bot-commands-filled'
| 'bots'
| 'bug'
| 'calendar-filter'
| 'calendar'
| 'camera-add'
| 'camera'
| 'car'
| 'card'
| 'cash-circle'
| 'channel-filled'
| 'channel'
| 'channelviews'
| 'chat-badge'
| 'chats-badge'
| 'check'
| 'clock-edit'
| 'clock'
| 'close-circle'
| 'close-topic'
| 'close'
| 'closed-gift'
| 'cloud-download'
| 'collapse-modal'
| 'collapse'
| 'colorize'
| 'comments-sticker'
| 'comments'
| 'copy-media'
| 'copy'
| 'crown-take-off-outline'
| 'crown-take-off'
| 'crown-wear-outline'
| 'crown-wear'
| 'darkmode'
| 'data'
| 'delete-filled'
| 'delete-left'
| 'delete-user'
| 'delete'
| 'diamond'
| 'document'
| 'double-badge'
| 'down'
| 'download'
| 'dropdown-arrows'
| 'eats'
| 'edit'
| 'email'
| 'enter'
| 'expand-modal'
| 'expand'
| 'eye-crossed-outline'
| 'eye-crossed'
| 'eye-outline'
| 'eye'
| 'favorite-filled'
| 'favorite'
| 'file-badge'
| 'flag'
| 'folder-badge'
| 'folder-tabs-bot'
| 'folder-tabs-channel'
| 'folder-tabs-chat'
| 'folder-tabs-chats'
| 'folder-tabs-folder'
| 'folder-tabs-group'
| 'folder-tabs-star'
| 'folder-tabs-user'
| 'folder'
| 'fontsize'
| 'forums'
| 'forward'
| 'fragment'
| 'frozen-time'
| 'fullscreen'
| 'gifs'
| 'gift-transfer-inline'
| 'gift'
| 'group-filled'
| 'group'
| 'grouped-disable'
| 'grouped'
| 'hand-stop'
| 'hashtag'
| 'hd-photo'
| 'heart-outline'
| 'heart'
| 'help'
| 'info-filled'
| 'info'
| 'install'
| 'italic'
| 'key'
| 'keyboard'
| 'lamp'
| 'language'
| 'large-pause'
| 'large-play'
| 'link-badge'
| 'link-broken'
| 'link'
| 'location'
| 'lock-badge'
| 'lock'
| 'logout'
| 'loop'
| 'mention'
| 'menu'
| 'message-failed'
| 'message-pending'
| 'message-read'
| 'message-succeeded'
| 'message'
| 'microphone-alt'
| 'microphone'
| 'monospace'
| 'more-circle'
| 'more'
| 'move-caption-down'
| 'move-caption-up'
| 'mute'
| 'muted'
| 'my-notes'
| 'new-chat-filled'
| 'next-link'
| 'next'
| 'nochannel'
| 'noise-suppression'
| 'non-contacts'
| 'note'
| 'one-filled'
| 'open-in-new-tab'
| 'password-off'
| 'pause'
| 'permissions'
| 'phone-discard-outline'
| 'phone-discard'
| 'phone'
| 'photo'
| 'pin-badge'
| 'pin-list'
| 'pin'
| 'pinned-chat'
| 'pinned-message'
| 'pip'
| 'play-story'
| 'play'
| 'poll'
| 'previous'
| 'privacy-policy'
| 'proof-of-ownership'
| 'quote-text'
| 'quote'
| 'radial-badge'
| 'rating-icons-level1'
| 'rating-icons-level10'
| 'rating-icons-level2'
| 'rating-icons-level20'
| 'rating-icons-level3'
| 'rating-icons-level30'
| 'rating-icons-level4'
| 'rating-icons-level40'
| 'rating-icons-level5'
| 'rating-icons-level50'
| 'rating-icons-level6'
| 'rating-icons-level60'
| 'rating-icons-level7'
| 'rating-icons-level70'
| 'rating-icons-level8'
| 'rating-icons-level80'
| 'rating-icons-level9'
| 'rating-icons-level90'
| 'rating-icons-negative'
| 'readchats'
| 'recent'
| 'refund'
| 'reload'
| 'remove-quote'
| 'remove'
| 'reopen-topic'
| 'reorder-tabs'
| 'replace'
| 'replies'
| 'reply-filled'
| 'reply'
| 'revenue-split'
| 'revote'
| 'save-story'
| 'saved-messages'
| 'schedule'
| 'scheduled'
| 'sd-photo'
| 'search'
| 'select'
| 'sell-outline'
| 'sell'
| 'send-outline'
| 'send'
| 'settings-filled'
| 'settings'
| 'share-filled'
| 'share-screen-outlined'
| 'share-screen-stop'
| 'share-screen'
| 'show-message'
| 'sidebar'
| 'skip-next'
| 'skip-previous'
| 'smallscreen'
| 'smile'
| 'sort-by-date'
| 'sort-by-number'
| 'sort-by-price'
| 'sort'
| 'speaker-muted-story'
| 'speaker-outline'
| 'speaker-story'
| 'speaker'
| 'spoiler-disable'
| 'spoiler'
| 'sport'
| 'star'
| 'stars-lock'
| 'stars-refund'
| 'stats'
| 'stealth-future'
| 'stealth-past'
| 'stickers'
| 'stop-raising-hand'
| 'stop'
| 'story-caption'
| 'story-expired'
| 'story-priority'
| 'story-reply'
| 'strikethrough'
| 'tag-add'
| 'tag-crossed'
| 'tag-filter'
| 'tag-name'
| 'tag'
| 'timer'
| 'toncoin'
| 'tools'
| 'topic-new'
| 'trade'
| 'transcribe'
| 'truck'
| 'unarchive'
| 'underlined'
| 'understood'
| 'unique-profile'
| 'unlist-outline'
| 'unlist'
| 'unlock-badge'
| 'unlock'
| 'unmute'
| 'unpin'
| 'unread'
| 'up'
| 'user-filled'
| 'user-online'
| 'user-stars'
| 'user'
| 'video-outlined'
| 'video-stop'
| 'video'
| 'view-once'
| 'voice-chat'
| 'volume-1'
| 'volume-2'
| 'volume-3'
| 'warning'
| 'web'
| 'webapp'
| 'word-wrap'
| 'zoom-out'
| 'zoom-in'
| 'zoom-out';
| 'word-wrap'
| 'webapp'
| 'web'
| 'warning'
| 'volume-3'
| 'volume-2'
| 'volume-1'
| 'voice-chat'
| 'view-once'
| 'video'
| 'video-stop'
| 'video-outlined'
| 'user'
| 'user-stars'
| 'user-online'
| 'user-filled'
| 'up'
| 'unread'
| 'unpin'
| 'unmute'
| 'unlock'
| 'unlock-badge'
| 'unlist'
| 'unlist-outline'
| 'unique-profile'
| 'understood'
| 'underlined'
| 'unarchive'
| 'truck'
| 'transcribe'
| 'trade'
| 'topic-new'
| 'tools'
| 'toncoin'
| 'timer'
| 'tag'
| 'tag-name'
| 'tag-filter'
| 'tag-crossed'
| 'tag-add'
| 'strikethrough'
| 'story-reply'
| 'story-priority'
| 'story-expired'
| 'story-caption'
| 'stop'
| 'stop-raising-hand'
| 'stickers'
| 'stealth-past'
| 'stealth-future'
| 'stats'
| 'stars-refund'
| 'stars-lock'
| 'star'
| 'sport'
| 'spoiler'
| 'spoiler-disable'
| 'speaker'
| 'speaker-story'
| 'speaker-outline'
| 'speaker-muted-story'
| 'sort'
| 'sort-by-price'
| 'sort-by-number'
| 'sort-by-date'
| 'smile'
| 'smallscreen'
| 'skip-previous'
| 'skip-next'
| 'sidebar'
| 'show-message'
| 'share-screen'
| 'share-screen-stop'
| 'share-screen-outlined'
| 'share-filled'
| 'settings'
| 'settings-filled'
| 'send'
| 'send-outline'
| 'sell'
| 'sell-outline'
| 'select'
| 'search'
| 'sd-photo'
| 'scheduled'
| 'schedule'
| 'saved-messages'
| 'save-story'
| 'revote'
| 'revenue-split'
| 'reply'
| 'reply-filled'
| 'replies'
| 'replace'
| 'reorder-tabs'
| 'reopen-topic'
| 'remove'
| 'remove-quote'
| 'reload'
| 'refund'
| 'recent'
| 'readchats'
| 'radial-badge'
| 'quote'
| 'quote-text'
| 'proof-of-ownership'
| 'privacy-policy'
| 'previous'
| 'poll'
| 'play'
| 'play-story'
| 'pip'
| 'pinned-message'
| 'pinned-chat'
| 'pin'
| 'pin-list'
| 'pin-badge'
| 'photo'
| 'phone'
| 'phone-discard'
| 'phone-discard-outline'
| 'permissions'
| 'pause'
| 'password-off'
| 'open-in-new-tab'
| 'one-filled'
| 'note'
| 'non-contacts'
| 'noise-suppression'
| 'nochannel'
| 'next'
| 'next-link'
| 'new-chat-filled'
| 'my-notes'
| 'muted'
| 'mute'
| 'move-caption-up'
| 'move-caption-down'
| 'more'
| 'more-circle'
| 'monospace'
| 'microphone'
| 'microphone-alt'
| 'message'
| 'message-succeeded'
| 'message-read'
| 'message-pending'
| 'message-failed'
| 'menu'
| 'mention'
| 'loop'
| 'logout'
| 'lock'
| 'lock-badge'
| 'location'
| 'link'
| 'link-broken'
| 'link-badge'
| 'large-play'
| 'large-pause'
| 'language'
| 'lamp'
| 'keyboard'
| 'key'
| 'italic'
| 'install'
| 'info'
| 'info-filled'
| 'help'
| 'heart'
| 'heart-outline'
| 'hd-photo'
| 'hashtag'
| 'hand-stop'
| 'grouped'
| 'grouped-disable'
| 'group'
| 'group-filled'
| 'gift'
| 'gift-transfer-inline'
| 'gifs'
| 'fullscreen'
| 'frozen-time'
| 'fragment'
| 'forward'
| 'forums'
| 'fontsize'
| 'folder'
| 'folder-badge'
| 'flag'
| 'file-badge'
| 'favorite'
| 'favorite-filled'
| 'eye'
| 'eye-outline'
| 'eye-crossed'
| 'eye-crossed-outline'
| 'expand'
| 'expand-modal'
| 'enter'
| 'email'
| 'edit'
| 'eats'
| 'dropdown-arrows'
| 'download'
| 'down'
| 'double-badge'
| 'document'
| 'diamond'
| 'delete'
| 'delete-user'
| 'delete-left'
| 'delete-filled'
| 'data'
| 'darkmode'
| 'crown-wear'
| 'crown-wear-outline'
| 'crown-take-off'
| 'crown-take-off-outline'
| 'craft'
| 'copy'
| 'copy-media'
| 'comments'
| 'comments-sticker'
| 'combine-craft'
| 'colorize'
| 'collapse'
| 'collapse-modal'
| 'cloud-download'
| 'closed-gift'
| 'close'
| 'close-topic'
| 'close-circle'
| 'clock'
| 'clock-edit'
| 'check'
| 'chats-badge'
| 'chat-badge'
| 'channelviews'
| 'channel'
| 'channel-filled'
| 'cash-circle'
| 'card'
| 'car'
| 'camera'
| 'camera-add'
| 'calendar'
| 'calendar-filter'
| 'bug'
| 'bots'
| 'bot-commands-filled'
| 'bot-command'
| 'boosts'
| 'boostcircle'
| 'boost'
| 'boost-outline'
| 'boost-craft-chance'
| 'bold'
| 'avatar-saved-messages'
| 'avatar-deleted-account'
| 'avatar-archived-chats'
| 'author-hidden'
| 'auction'
| 'auction-next-round'
| 'auction-filled'
| 'auction-drop'
| 'attach'
| 'ask-support'
| 'arrow-right'
| 'arrow-left'
| 'arrow-down'
| 'arrow-down-circle'
| 'archive'
| 'archive-to-main'
| 'archive-from-main'
| 'archive-filled'
| 'animations'
| 'animals'
| 'allow-speak'
| 'admin'
| 'add'
| 'add-user'
| 'add-user-filled'
| 'add-one-badge'
| 'add-filled'
| 'active-sessions'
| 'rating-icons-negative'
| 'rating-icons-level90'
| 'rating-icons-level9'
| 'rating-icons-level80'
| 'rating-icons-level8'
| 'rating-icons-level70'
| 'rating-icons-level7'
| 'rating-icons-level60'
| 'rating-icons-level6'
| 'rating-icons-level50'
| 'rating-icons-level5'
| 'rating-icons-level40'
| 'rating-icons-level4'
| 'rating-icons-level30'
| 'rating-icons-level3'
| 'rating-icons-level20'
| 'rating-icons-level2'
| 'rating-icons-level10'
| 'rating-icons-level1'
| 'folder-tabs-user'
| 'folder-tabs-star'
| 'folder-tabs-group'
| 'folder-tabs-folder'
| 'folder-tabs-chats'
| 'folder-tabs-chat'
| 'folder-tabs-channel'
| 'folder-tabs-bot';

View File

@ -1320,6 +1320,24 @@ export interface LangPair {
'GiftInfoUpgradeBadge': undefined;
'GiftInfoUpgradeForFree': undefined;
'GiftInfoUpgrade': undefined;
'GiftInfoCraft': undefined;
'GiftCraftTitle': undefined;
'GiftCraftNewGift': undefined;
'GiftCraftSelectTitle': undefined;
'GiftCraftSelectYourGifts': undefined;
'GiftCraftWarning': undefined;
'GiftCraftingTitle': undefined;
'GiftCraftFailedTitle': undefined;
'GiftCraftInfoTitle': undefined;
'GiftCraftInfoSubtitle': undefined;
'GiftCraftInfoCraftTitle': undefined;
'GiftCraftInfoCraftDescription': undefined;
'GiftCraftInfoChanceTitle': undefined;
'GiftCraftInfoChanceDescription': undefined;
'GiftCraftInfoRiskTitle': undefined;
'GiftCraftInfoRiskDescription': undefined;
'GiftCraftHelp': undefined;
'GiftCraftViewAll': undefined;
'GiftInfoWithdraw': undefined;
'GiftInfoWear': undefined;
'GiftInfoTakeOff': undefined;
@ -2396,6 +2414,9 @@ export interface LangPairWithVariables<V = LangVariable> {
'GiftSavedNumber': {
'number': V;
};
'GiftInfoModelCrafted': {
'model': V;
};
'GiftInfoPeerOriginalInfo': {
'peer': V;
'date': V;
@ -2416,6 +2437,18 @@ export interface LangPairWithVariables<V = LangVariable> {
'date': V;
'text': V;
};
'GiftCraftButton': {
'giftName': V;
};
'GiftCraftSuccessChance': {
'percent': V;
};
'GiftCraftEmptyHint': {
'button': V;
};
'GiftCraftDescription': {
'giftLine': V;
};
'GiftTransferTONBlocked': {
'time': V;
};
@ -3665,6 +3698,12 @@ export interface LangPairPluralWithVariables<V = LangVariable> {
'count': V;
'total': V;
};
'GiftCraftSelectMarketGifts': {
'count': V;
};
'GiftCraftFailedDescription': {
'count': V;
};
'GiftWithdrawWait': {
'days': V;
};

View File

@ -31,3 +31,5 @@ export const VTT_PROFILE_BUSINESS_HOURS_COLLAPSE = VTT_PROFILE_BUSINESS_HOURS.wi
export const VTT_PROFILE_NOTE = VTT_CHAT_EXTRA.with('profileNote');
export const VTT_PROFILE_NOTE_EXPAND = VTT_PROFILE_NOTE.with('profileNoteExpand');
export const VTT_PROFILE_NOTE_COLLAPSE = VTT_PROFILE_NOTE.with('profileNoteCollapse');
export const VTT_CRAFT_ATTRIBUTES = new VTTypes(['craftAttributes']);