Gifts Modal: Support marketplace (#5936)

This commit is contained in:
Alexander Zinchuk 2025-06-04 20:41:18 +02:00
parent d7ea4a5748
commit 4330472674
40 changed files with 2132 additions and 312 deletions

View File

@ -1,3 +1,4 @@
import bigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs'; import { Api as GramJs } from '../../../lib/gramjs';
import type { import type {
@ -6,13 +7,18 @@ import type {
ApiSavedStarGift, ApiSavedStarGift,
ApiStarGift, ApiStarGift,
ApiStarGiftAttribute, ApiStarGiftAttribute,
ApiStarGiftAttributeCounter,
ApiStarGiftAttributeId,
ApiTypeResaleStarGifts,
} from '../../types'; } from '../../types';
import { numberToHexColor } from '../../../util/colors'; import { numberToHexColor } from '../../../util/colors';
import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { addDocumentToLocalDb } from '../helpers/localDb'; import { addDocumentToLocalDb } from '../helpers/localDb';
import { buildApiFormattedText } from './common'; import { buildApiFormattedText } from './common';
import { getApiChatIdFromMtpPeer } from './peers'; import { getApiChatIdFromMtpPeer } from './peers';
import { buildStickerFromDocument } from './symbols'; import { buildStickerFromDocument } from './symbols';
import { buildApiUser } from './users';
export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift { export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
if (starGift instanceof GramJs.StarGiftUnique) { if (starGift instanceof GramJs.StarGiftUnique) {
@ -40,7 +46,7 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
const { const {
id, limited, stars, availabilityRemains, availabilityTotal, convertStars, firstSaleDate, lastSaleDate, soldOut, id, limited, stars, availabilityRemains, availabilityTotal, convertStars, firstSaleDate, lastSaleDate, soldOut,
birthday, upgradeStars, resellMinStars, title, birthday, upgradeStars, resellMinStars, title, availabilityResale,
} = starGift; } = starGift;
addDocumentToLocalDb(starGift.sticker); addDocumentToLocalDb(starGift.sticker);
@ -63,6 +69,7 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
upgradeStars: upgradeStars?.toJSNumber(), upgradeStars: upgradeStars?.toJSNumber(),
title, title,
resellMinStars: resellMinStars?.toJSNumber(), resellMinStars: resellMinStars?.toJSNumber(),
availabilityResale: availabilityResale?.toJSNumber(),
}; };
} }
@ -101,11 +108,12 @@ export function buildApiStarGiftAttribute(attribute: GramJs.TypeStarGiftAttribut
if (attribute instanceof GramJs.StarGiftAttributeBackdrop) { if (attribute instanceof GramJs.StarGiftAttributeBackdrop) {
const { const {
name, rarityPermille, centerColor, edgeColor, patternColor, textColor, name, rarityPermille, centerColor, edgeColor, patternColor, textColor, backdropId,
} = attribute; } = attribute;
return { return {
type: 'backdrop', type: 'backdrop',
backdropId,
name, name,
rarityPercent: rarityPermille / 10, rarityPercent: rarityPermille / 10,
centerColor: numberToHexColor(centerColor), centerColor: numberToHexColor(centerColor),
@ -180,3 +188,92 @@ export function buildApiDisallowedGiftsSettings(
shouldDisallowPremiumGifts: disallowPremiumGifts, shouldDisallowPremiumGifts: disallowPremiumGifts,
}; };
} }
export function buildApiStarGiftAttributeId(
result: GramJs.TypeStarGiftAttributeId,
): ApiStarGiftAttributeId | undefined {
if (result instanceof GramJs.StarGiftAttributeIdModel) {
return {
type: 'model',
documentId: result.documentId.toString(),
};
}
if (result instanceof GramJs.StarGiftAttributeIdPattern) {
return {
type: 'pattern',
documentId: result.documentId.toString(),
};
}
if (result instanceof GramJs.StarGiftAttributeIdBackdrop) {
return {
type: 'backdrop',
backdropId: result.backdropId,
};
}
return undefined;
}
export function buildApiStarGiftAttributeCounter(
result: GramJs.TypeStarGiftAttributeCounter,
): ApiStarGiftAttributeCounter | undefined {
const {
count,
} = result;
const attribute = buildApiStarGiftAttributeId(result.attribute);
if (!attribute) return undefined;
return {
count,
attribute,
};
}
export function buildApiResaleGifts(
result: GramJs.payments.TypeResaleStarGifts,
): ApiTypeResaleStarGifts {
const {
count,
nextOffset,
attributesHash,
} = result;
const gifts = result.gifts.map((g) => buildApiStarGift(g));
const attributes = result.attributes?.map((a) => buildApiStarGiftAttribute(a)).filter(Boolean);
const users = result.users.map((u) => buildApiUser(u)).filter(Boolean);
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
const counters = result.counters?.map((c) => buildApiStarGiftAttributeCounter(c)).filter(Boolean);
return {
count,
gifts,
nextOffset,
attributes,
attributesHash: attributesHash?.toString(),
chats,
counters,
users,
};
}
export function buildInputResaleGiftsAttributes(attributes: ApiStarGiftAttributeId[]):
GramJs.TypeStarGiftAttributeId[] {
return attributes.map((attr) => {
switch (attr.type) {
case 'model':
return new GramJs.StarGiftAttributeIdModel({ documentId: bigInt(attr.documentId) });
case 'pattern':
return new GramJs.StarGiftAttributeIdPattern({ documentId: bigInt(attr.documentId) });
case 'backdrop':
return new GramJs.StarGiftAttributeIdBackdrop({ backdropId: attr.backdropId });
default:
throw new Error(`Unknown attribute type: ${(attr as any).type}`);
}
});
}

View File

@ -1,15 +1,17 @@
import bigInt from 'big-integer'; import bigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs'; import { Api as GramJs } from '../../../lib/gramjs';
import type { GiftProfileFilterOptions } from '../../../types'; import type { GiftProfileFilterOptions, ResaleGiftsFilterOptions } from '../../../types';
import type { import type {
ApiChat, ApiChat,
ApiPeer, ApiPeer,
ApiRequestInputSavedStarGift, ApiRequestInputSavedStarGift,
ApiStarGiftAttributeId,
ApiStarGiftRegular, ApiStarGiftRegular,
} from '../../types'; } from '../../types';
import { buildApiSavedStarGift, buildApiStarGift, buildApiStarGiftAttribute } from '../apiBuilders/gifts'; import { buildApiResaleGifts, buildApiSavedStarGift, buildApiStarGift,
buildApiStarGiftAttribute, buildInputResaleGiftsAttributes } from '../apiBuilders/gifts';
import { import {
buildApiStarsAmount, buildApiStarsAmount,
buildApiStarsGiftOptions, buildApiStarsGiftOptions,
@ -44,6 +46,48 @@ export async function fetchStarGifts() {
return result.gifts.map(buildApiStarGift).filter((gift): gift is ApiStarGiftRegular => gift.type === 'starGift'); return result.gifts.map(buildApiStarGift).filter((gift): gift is ApiStarGiftRegular => gift.type === 'starGift');
} }
export async function fetchResaleGifts({
giftId,
offset = '',
limit,
attributesHash = '0',
filter,
}: {
giftId: string;
offset?: string;
limit?: number;
attributesHash?: string;
filter?: ResaleGiftsFilterOptions;
}) {
type GetResaleStarGifts = ConstructorParameters<typeof GramJs.payments.GetResaleStarGifts>[0];
const attributes: ApiStarGiftAttributeId[] = [
...(filter?.backdropAttributes ?? []),
...(filter?.modelAttributes ?? []),
...(filter?.patternAttributes ?? []),
];
const params: GetResaleStarGifts = {
giftId: bigInt(giftId),
offset,
limit,
attributesHash: attributesHash ? bigInt(attributesHash) : undefined,
attributes: buildInputResaleGiftsAttributes(attributes),
...(filter && {
sortByPrice: filter.sortType === 'byPrice' || undefined,
sortByNum: filter.sortType === 'byNumber' || undefined,
} satisfies GetResaleStarGifts),
};
const result = await invokeRequest(new GramJs.payments.GetResaleStarGifts(params));
if (!result) {
return undefined;
}
return buildApiResaleGifts(result);
}
export async function fetchSavedStarGifts({ export async function fetchSavedStarGifts({
peer, peer,
offset = '', offset = '',

View File

@ -1,6 +1,7 @@
import type { ApiWebDocument } from './bots'; import type { ApiWebDocument } from './bots';
import type { ApiChat } from './chats'; import type { ApiChat } from './chats';
import type { ApiFormattedText, ApiSticker, BoughtPaidMedia } from './messages'; import type { ApiFormattedText, ApiSticker, BoughtPaidMedia } from './messages';
import type { ApiUser } from './users';
export interface ApiStarGiftRegular { export interface ApiStarGiftRegular {
type: 'starGift'; type: 'starGift';
@ -10,6 +11,7 @@ export interface ApiStarGiftRegular {
stars: number; stars: number;
availabilityRemains?: number; availabilityRemains?: number;
availabilityTotal?: number; availabilityTotal?: number;
availabilityResale?: number;
starsToConvert: number; starsToConvert: number;
isSoldOut?: true; isSoldOut?: true;
firstSaleDate?: number; firstSaleDate?: number;
@ -54,6 +56,7 @@ export interface ApiStarGiftAttributePattern {
export interface ApiStarGiftAttributeBackdrop { export interface ApiStarGiftAttributeBackdrop {
type: 'backdrop'; type: 'backdrop';
backdropId: number;
name: string; name: string;
centerColor: string; centerColor: string;
edgeColor: string; edgeColor: string;
@ -95,6 +98,37 @@ export interface ApiSavedStarGift {
upgradeMsgId?: number; // Local field, used for Action Message upgradeMsgId?: number; // Local field, used for Action Message
} }
export type StarGiftAttributeIdModel = {
type: 'model';
documentId: string;
};
export type ApiStarGiftAttributeIdPattern = {
type: 'pattern';
documentId: string;
};
export type ApiStarGiftAttributeIdBackdrop = {
type: 'backdrop';
backdropId: number;
};
export type ApiStarGiftAttributeId = StarGiftAttributeIdModel |
ApiStarGiftAttributeIdPattern | ApiStarGiftAttributeIdBackdrop;
export interface ApiStarGiftAttributeCounter<T extends ApiStarGiftAttributeId = ApiStarGiftAttributeId> {
attribute: T;
count: number;
}
export interface ApiTypeResaleStarGifts {
count: number;
gifts: ApiStarGift[];
nextOffset?: string;
attributes?: ApiStarGiftAttribute[];
attributesHash?: string;
chats: ApiChat[];
counters?: ApiStarGiftAttributeCounter[];
users: ApiUser[];
}
export interface ApiInputSavedStarGiftUser { export interface ApiInputSavedStarGiftUser {
type: 'user'; type: 'user';
messageId: number; messageId: number;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path fill="#000" d="m16 29-5.558-5.483a1.475 1.475 0 0 1 0-2.106 1.524 1.524 0 0 1 2.135 0L16 24.788l3.423-3.377a1.524 1.524 0 0 1 2.135 0c.59.581.59 1.524 0 2.106zm-3.423-18.41a1.524 1.524 0 0 1-2.135 0 1.475 1.475 0 0 1 0-2.107L16 3l5.558 5.483c.59.582.59 1.525 0 2.106a1.524 1.524 0 0 1-2.135 0L16 7.212z"/></svg>

After

Width:  |  Height:  |  Size: 392 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path fill="#000" d="M29.722 21.277c.375.34.37.918-.011 1.251a.904.904 0 0 1-1.193-.01l-2.615-2.368v7.977c0 .482-.404.873-.903.873s-.903-.391-.903-.873V20.15l-2.615 2.369a.904.904 0 0 1-1.193.01.832.832 0 0 1-.01-1.252L25 17z"/><path fill="#000" d="M20 4a1 1 0 0 1 1 1v1a5 5 0 0 1 5 5v2.5a1 1 0 1 1-2 0V11a3 3 0 0 0-3-3v1a1 1 0 1 1-2 0V8H9v1a1 1 0 0 1-2 0V8a3 3 0 0 0-3 3v11a3 3 0 0 0 3 3h12.5a1 1 0 1 1 0 2H7a5 5 0 0 1-5-5V11a5 5 0 0 1 5-5V5a1 1 0 0 1 2 0v1h10V5a1 1 0 0 1 1-1"/><path fill="#000" d="M21 12a1 1 0 1 1 0 2H7a1 1 0 1 1 0-2z"/></svg>

After

Width:  |  Height:  |  Size: 622 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path fill="#000" d="M29.722 21.277c.375.34.37.918-.012 1.252a.9.9 0 0 1-1.192-.01l-2.615-2.369v7.977c0 .482-.404.873-.903.873s-.903-.391-.903-.873V20.15l-2.615 2.368a.904.904 0 0 1-1.193.011.832.832 0 0 1-.01-1.252L25 17zM18.755 7.72a1 1 0 0 1 1.94.449l-.02.1-.86 3.73H25l.102.006a1.001 1.001 0 0 1 0 1.99L25 14h-5.648l-2.58 11.18a1 1 0 0 1-1.95-.449L15.684 21H9.736l-.965 4.18a1 1 0 0 1-1.949-.449L7.684 21H3a1 1 0 0 1 0-2h5.146L9.3 14H5a1 1 0 0 1 0-2h4.762l.965-4.18.028-.1a1 1 0 0 1 1.94.449l-.02.1-.86 3.73h5.947l.965-4.18zM10.198 19H16l.102.005q.02.002.04.006L17.3 14h-5.948z"/></svg>

After

Width:  |  Height:  |  Size: 665 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path fill="#000" d="M29.722 21.278c.375.34.37.917-.012 1.25a.9.9 0 0 1-1.192-.01l-2.615-2.368v7.977c0 .482-.404.873-.903.873s-.903-.39-.903-.873V20.15l-2.615 2.369a.9.9 0 0 1-1.192.01.83.83 0 0 1-.012-1.251L25 17zM14.924 24a.866.866 0 0 1-.876-.856V22.09a4.44 4.44 0 0 1-2.005-1.022q-.61-.546-.985-1.416c-.172-.4.054-.844.463-1.012.484-.2 1.024.065 1.28.511q.255.442.6.739.646.555 1.699.555.897 0 1.522-.41.625-.412.625-1.278 0-.778-.482-1.234-.483-.455-2.235-1.033-1.884-.6-2.586-1.433-.7-.834-.701-2.034 0-1.444.92-2.244t1.885-.911V8.856c0-.473.392-.856.876-.856s.877.383.877.856v1.01q1.095.18 1.807.812.412.364.694.823c.24.386.023.864-.399 1.049-.472.207-1.003-.047-1.342-.429a2 2 0 0 0-.278-.254q-.483-.356-1.315-.356-.964 0-1.468.433a1.37 1.37 0 0 0-.504 1.078q0 .734.657 1.156.658.422 2.28.889 1.51.444 2.29 1.41A3.46 3.46 0 0 1 19 18.712q0 1.578-.92 2.4t-2.28 1.022v1.011a.866.866 0 0 1-.876.856"/><path fill="#000" d="M2 16.5C2 9.596 7.596 4 14.5 4c5.456 0 10.095 3.495 11.8 8.367l.156.477.025.098a1 1 0 0 1-1.903.581l-.034-.095-.13-.4C22.98 8.934 19.08 6 14.5 6 8.701 6 4 10.701 4 16.5S8.701 27 14.5 27c2.01 0 3.884-.563 5.477-1.54a1 1 0 1 1 1.046 1.705A12.45 12.45 0 0 1 14.5 29C7.596 29 2 23.404 2 16.5"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1977,5 +1977,25 @@
"StarGiftSaleTransaction" = "Gift Purchase"; "StarGiftSaleTransaction" = "Gift Purchase";
"StarGiftPurchaseTransaction" = "Gift Sale"; "StarGiftPurchaseTransaction" = "Gift Sale";
"GiftBuyConfirmDescription" = "Do you want to buy **{gift}** for **{stars}**?"; "GiftBuyConfirmDescription" = "Do you want to buy **{gift}** for **{stars}**?";
"GiftBuyForPeerConfirmDescription" = "Do you want to buy **{gift}** for **{stars}** and gift it to **{peer}**?";
"ComposerTitleForwardFrom" = "From: **{users}**"; "ComposerTitleForwardFrom" = "From: **{users}**";
"ContextMenuItemMention" = "Mention"; "ContextMenuItemMention" = "Mention";
"GiftRibbonResale" = "resale";
"GiftCategoryResale" = "Resale";
"HeaderDescriptionResaleGifts_one" = "{count} for resale";
"HeaderDescriptionResaleGifts_other" = "{count} for resale";
"GiftSortByPrice" = "Sort by Price";
"GiftSortByNumber" = "Sort by Number";
"ContextMenuItemSelectAll" = "Select All";
"ButtonSort" = "Sort";
"GiftAttributeModelPlural_one" = "{count} Model";
"GiftAttributeModelPlural_other" = "{count} Models";
"GiftAttributeBackdropPlural_one" = "{count} Backdrop";
"GiftAttributeBackdropPlural_other" = "{count} Backdrops";
"GiftAttributeSymbolPlural_one" = "{count} Symbol";
"GiftAttributeSymbolPlural_other" = "{count} Symbols";
"ValueGiftSortByDate" = "Date";
"ValueGiftSortByPrice" = "Price";
"ValueGiftSortByNumber" = "Number";
"ResellGiftsNoFound" = "No gifts found";
"ResellGiftsClearFilters" = "Clear Filters";

View File

@ -19,9 +19,10 @@ const COLORS = {
type ColorKey = keyof typeof COLORS; type ColorKey = keyof typeof COLORS;
const COLOR_KEYS = new Set(Object.keys(COLORS) as ColorKey[]); const COLOR_KEYS = new Set(Object.keys(COLORS) as ColorKey[]);
type GradientColor = readonly [string, string];
type OwnProps = { type OwnProps = {
color: ColorKey | (string & {}); color: ColorKey | GradientColor | (string & {});
text: string; text: string;
className?: string; className?: string;
}; };
@ -40,7 +41,13 @@ const GiftRibbon = ({
const isDarkTheme = theme === 'dark'; const isDarkTheme = theme === 'dark';
const gradientColor = colorKey ? COLORS[colorKey][isDarkTheme ? 1 : 0] : undefined; const gradientColor: GradientColor | undefined
= Array.isArray(color)
? color as GradientColor
: colorKey
? COLORS[colorKey][isDarkTheme ? 1 : 0]
: undefined;
const startColor = gradientColor ? gradientColor[0] : color; const startColor = gradientColor ? gradientColor[0] : color;
const endColor = gradientColor ? gradientColor[1] : color; const endColor = gradientColor ? gradientColor[1] : color;

View File

@ -94,13 +94,14 @@ function GiftComposer({
} }
}, [shouldDisallowLimitedStarGifts, shouldPayForUpgrade]); }, [shouldDisallowLimitedStarGifts, shouldPayForUpgrade]);
const isStarGift = 'id' in gift; const isStarGift = 'id' in gift && gift.type === 'starGift';
const isPremiumGift = 'months' in gift;
const hasPremiumByStars = giftByStars && 'amount' in giftByStars; const hasPremiumByStars = giftByStars && 'amount' in giftByStars;
const isPeerUser = peer && isApiPeerUser(peer); const isPeerUser = peer && isApiPeerUser(peer);
const isSelf = peerId === currentUserId; const isSelf = peerId === currentUserId;
const localMessage = useMemo(() => { const localMessage = useMemo(() => {
if (!isStarGift) { if (isPremiumGift) {
const currentGift = shouldPayByStars && hasPremiumByStars ? giftByStars : gift; const currentGift = shouldPayByStars && hasPremiumByStars ? giftByStars : gift;
return { return {
id: -1, id: -1,
@ -121,32 +122,35 @@ function GiftComposer({
} satisfies ApiMessage; } satisfies ApiMessage;
} }
return { if (isStarGift) {
id: -1, return {
chatId: '0', id: -1,
isOutgoing: false, chatId: '0',
senderId: currentUserId, isOutgoing: false,
date: Math.floor(Date.now() / 1000), senderId: currentUserId,
content: { date: Math.floor(Date.now() / 1000),
action: { content: {
mediaType: 'action', action: {
type: 'starGift', mediaType: 'action',
message: giftMessage?.length ? { type: 'starGift',
text: giftMessage, message: giftMessage?.length ? {
} : undefined, text: giftMessage,
isNameHidden: shouldHideName || undefined, } : undefined,
starsToConvert: gift.starsToConvert, isNameHidden: shouldHideName || undefined,
canUpgrade: shouldPayForUpgrade || undefined, starsToConvert: gift.starsToConvert,
alreadyPaidUpgradeStars: shouldPayForUpgrade ? gift.upgradeStars : undefined, canUpgrade: shouldPayForUpgrade || undefined,
gift, alreadyPaidUpgradeStars: shouldPayForUpgrade ? gift.upgradeStars : undefined,
peerId, gift,
fromId: currentUserId, peerId,
fromId: currentUserId,
},
}, },
}, } satisfies ApiMessage;
} satisfies ApiMessage; }
return undefined;
}, [currentUserId, gift, giftMessage, isStarGift, }, [currentUserId, gift, giftMessage, isStarGift,
shouldHideName, shouldPayForUpgrade, peerId, shouldHideName, shouldPayForUpgrade, peerId,
shouldPayByStars, hasPremiumByStars, giftByStars]); shouldPayByStars, hasPremiumByStars, giftByStars, isPremiumGift]);
const handleGiftMessageChange = useLastCallback((e: ChangeEvent<HTMLTextAreaElement>) => { const handleGiftMessageChange = useLastCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
setGiftMessage(e.target.value); setGiftMessage(e.target.value);
@ -198,14 +202,16 @@ function GiftComposer({
return; return;
} }
openInvoice({ if (isPremiumGift) {
type: 'giftcode', openInvoice({
userIds: [peerId], type: 'giftcode',
currency: gift.currency, userIds: [peerId],
amount: gift.amount, currency: gift.currency,
option: gift, amount: gift.amount,
message: giftMessage ? { text: giftMessage } : undefined, option: gift,
}); message: giftMessage ? { text: giftMessage } : undefined,
});
}
}); });
const canUseStarsPayment = hasPremiumByStars && starBalance && (starBalance.amount > giftByStars.amount); const canUseStarsPayment = hasPremiumByStars && starBalance && (starBalance.amount > giftByStars.amount);
@ -320,7 +326,7 @@ function GiftComposer({
? formatStarsAsIcon(lang, giftByStars.amount, { asFont: true }) ? formatStarsAsIcon(lang, giftByStars.amount, { asFont: true })
: isStarGift : isStarGift
? formatStarsAsIcon(lang, gift.stars + (shouldPayForUpgrade ? gift.upgradeStars! : 0), { asFont: true }) ? formatStarsAsIcon(lang, gift.stars + (shouldPayForUpgrade ? gift.upgradeStars! : 0), { asFont: true })
: formatCurrency(lang, gift.amount, gift.currency); : isPremiumGift ? formatCurrency(lang, gift.amount, gift.currency) : undefined;
return ( return (
<div className={styles.footer}> <div className={styles.footer}>
@ -359,6 +365,8 @@ function GiftComposer({
customBackground && isBackgroundBlurred && styles.blurred, customBackground && isBackgroundBlurred && styles.blurred,
); );
if ((!isStarGift && !isPremiumGift) || !localMessage) return;
return ( return (
<div className={buildClassName(styles.root, 'custom-scroll')}> <div className={buildClassName(styles.root, 'custom-scroll')}>
<div <div
@ -375,7 +383,7 @@ function GiftComposer({
style={customBackgroundValue ? `--custom-background: ${customBackgroundValue}` : undefined} style={customBackgroundValue ? `--custom-background: ${customBackgroundValue}` : undefined}
/> />
<ActionMessage <ActionMessage
key={isStarGift ? gift.id : gift.months} key={isStarGift ? gift.id : isPremiumGift ? gift.months : undefined}
message={localMessage} message={localMessage}
threadId={MAIN_THREAD_ID} threadId={MAIN_THREAD_ID}
appearanceOrder={0} appearanceOrder={0}

View File

@ -31,7 +31,7 @@
} }
:global(html.theme-dark) & { :global(html.theme-dark) & {
background-color: var(--color-background); background-color: rgb(33, 33, 33);
} }
&:hover::before { &:hover::before {
@ -68,6 +68,7 @@
} }
.star { .star {
margin-inline-start: 0 !important;
margin-inline-end: 0.125rem; margin-inline-end: 0.125rem;
font-size: 0.75rem !important; font-size: 0.75rem !important;
} }
@ -86,3 +87,12 @@
margin-inline-start: 0 !important; margin-inline-start: 0 !important;
margin-inline-end: 0.125rem !important; margin-inline-end: 0.125rem !important;
} }
.radialPattern {
position: absolute;
inset: 0;
}
.stickerWrapper {
position: relative;
}

View File

@ -1,83 +1,160 @@
import React, { memo, useRef } from '../../../lib/teact/teact'; import React, { memo, useMemo, useRef, useState } from '../../../lib/teact/teact';
import { getActions } from '../../../global'; import { getActions } from '../../../global';
import type { import type {
ApiStarGiftRegular, ApiStarGift,
} from '../../../api/types'; } from '../../../api/types';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { formatStarsAsIcon } from '../../../util/localization/format';
import { getStickerFromGift } from '../../common/helpers/gifts';
import { getGiftAttributes } from '../../common/helpers/gifts';
import useFlag from '../../../hooks/useFlag';
import { type ObserveFn, useOnIntersect } from '../../../hooks/useIntersectionObserver'; import { type ObserveFn, useOnIntersect } from '../../../hooks/useIntersectionObserver';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker';
import GiftRibbon from '../../common/gift/GiftRibbon'; import GiftRibbon from '../../common/gift/GiftRibbon';
import Icon from '../../common/icons/Icon'; import RadialPatternBackground from '../../common/profile/RadialPatternBackground';
import StickerView from '../../common/StickerView';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import styles from './GiftItem.module.scss'; import styles from './GiftItem.module.scss';
export type OwnProps = { export type OwnProps = {
gift: ApiStarGiftRegular; gift: ApiStarGift;
observeIntersection?: ObserveFn; observeIntersection?: ObserveFn;
onClick: (gift: ApiStarGiftRegular) => void; onClick: (gift: ApiStarGift, target: 'original' | 'resell') => void;
isResale?: boolean;
}; };
const GIFT_STICKER_SIZE = 90; const GIFT_STICKER_SIZE = 90;
function GiftItemStar({ gift, observeIntersection, onClick }: OwnProps) { function GiftItemStar({
gift, observeIntersection, onClick, isResale,
}: OwnProps) {
const { openGiftInfoModal } = getActions(); const { openGiftInfoModal } = getActions();
const ref = useRef<HTMLDivElement>(); const ref = useRef<HTMLDivElement>();
const stickerRef = useRef<HTMLDivElement>();
const lang = useLang(); const lang = useLang();
const [shouldPlay, play] = useFlag(); const [isVisible, setIsVisible] = useState(false);
const { const sticker = getStickerFromGift(gift);
stars, const isGiftUnique = gift.type === 'starGiftUnique';
isLimited, const uniqueGift = isGiftUnique ? gift : undefined;
isSoldOut, const regularGift = !isGiftUnique ? gift : undefined;
sticker,
} = gift; const stars = !isGiftUnique ? regularGift?.stars : uniqueGift?.resellPriceInStars;
const resellMinStars = regularGift?.resellMinStars;
const priceInStarsAsString = !isGiftUnique && isResale && resellMinStars
? lang.number(resellMinStars) + '+' : stars;
const isLimited = !isGiftUnique && Boolean(regularGift?.isLimited);
const isSoldOut = !isGiftUnique && Boolean(regularGift?.isSoldOut);
const handleGiftClick = useLastCallback(() => { const handleGiftClick = useLastCallback(() => {
if (isSoldOut) { if (isSoldOut && !isResale) {
openGiftInfoModal({ gift }); openGiftInfoModal({ gift });
return; return;
} }
onClick(gift); onClick(gift, isResale ? 'resell' : 'original');
}); });
const radialPatternBackdrop = useMemo(() => {
const { backdrop, pattern } = getGiftAttributes(gift) || {};
if (!backdrop || !pattern) {
return undefined;
}
const backdropColors = [backdrop.centerColor, backdrop.edgeColor];
const patternColor = backdrop.patternColor;
return (
<RadialPatternBackground
className={styles.radialPattern}
backgroundColors={backdropColors}
patternColor={patternColor}
patternIcon={pattern.sticker}
/>
);
}, [gift]);
const giftNumber = isGiftUnique ? gift.number : 0;
const giftRibbon = useMemo(() => {
if (isGiftUnique) {
const { backdrop } = getGiftAttributes(gift) || {};
if (!backdrop) {
return undefined;
}
return (
<GiftRibbon
color={[backdrop.centerColor, backdrop.edgeColor]}
text={
lang('GiftSavedNumber', { number: giftNumber })
}
/>
);
}
if (isResale) {
return <GiftRibbon color="green" text={lang('GiftRibbonResale')} />;
}
if (isSoldOut) {
return <GiftRibbon color="red" text={lang('GiftSoldOut')} />;
}
if (isLimited) {
return <GiftRibbon color="blue" text={lang('GiftLimited')} />;
}
return undefined;
}, [isGiftUnique, isResale, gift, isSoldOut, isLimited, lang, giftNumber]);
useOnIntersect(ref, observeIntersection, (entry) => { useOnIntersect(ref, observeIntersection, (entry) => {
if (entry.isIntersecting) play(); const visible = entry.isIntersecting;
setIsVisible(visible);
}); });
return ( return (
<div <div
ref={ref} ref={ref}
className={buildClassName(styles.container, styles.starGift)} className={buildClassName(styles.container, styles.starGift, 'starGiftItem')}
tabIndex={0} tabIndex={0}
role="button" role="button"
onClick={handleGiftClick} onClick={handleGiftClick}
> >
<AnimatedIconFromSticker {radialPatternBackdrop}
sticker={sticker}
noLoop <div
play={shouldPlay} ref={stickerRef}
className={styles.stickerWrapper}
style={`width: ${GIFT_STICKER_SIZE}px; height: ${GIFT_STICKER_SIZE}px`}
>
{sticker && (
<StickerView
observeIntersectionForPlaying={observeIntersection}
observeIntersectionForLoading={observeIntersection}
containerRef={stickerRef}
sticker={sticker}
size={GIFT_STICKER_SIZE}
shouldPreloadPreview
/>
)}
</div>
<Button
className={styles.buy}
nonInteractive nonInteractive
size={GIFT_STICKER_SIZE} size="tiny"
/> color={isGiftUnique ? 'bluredStarsBadge' : 'stars'}
<Button className={styles.buy} nonInteractive size="tiny" color="stars" withSparkleEffect pill fluid> withSparkleEffect={isVisible}
<Icon name="star" className={styles.star} /> pill
<div className={styles.amount}> fluid
{stars} >
</div> {formatStarsAsIcon(lang, priceInStarsAsString || 0, { asFont: true, className: styles.star })}
</Button> </Button>
{isLimited && !isSoldOut && <GiftRibbon color="blue" text={lang('GiftLimited')} />} {giftRibbon}
{isSoldOut && <GiftRibbon color="red" text={lang('GiftSoldOut')} />}
</div> </div>
); );
} }

View File

@ -9,17 +9,37 @@
background-color: var(--color-background); background-color: var(--color-background);
:global(html.theme-dark) & { :global(html.theme-dark) & {
background-color: #181818; --color-background: #181818;
} }
} }
.root :global(.modal-dialog), .root :global(.modal-dialog),
.transition, .transition,
.content { .content {
height: min(92vh, 45rem); height: min(92vh, 49rem);
max-height: none !important; max-height: none !important;
} }
@media (max-width: 600px) {
.root :global(.modal-dialog),
.transition,
.content {
height: 100%;
}
}
@media (max-width: 600px) {
.root :global(.modal-dialog) {
width: 100%;
max-width: 100% !important;
height: 100%;
border: none !important;
border-radius: 0;
box-shadow: none !important;
}
}
.root :global(.modal-dialog), .root :global(.modal-dialog),
.root :global(.modal-content), .root :global(.modal-content),
.transition { .transition {
@ -31,13 +51,22 @@
flex-direction: column; flex-direction: column;
} }
.resaleScreenRoot,
.main { .main {
overflow-y: scroll; overflow-y: scroll;
height: 100%; padding-bottom: 0.5rem;
padding-bottom: 1rem; padding-inline: 0.5rem;
padding-inline: 1rem;
@include mixins.adapt-padding-to-scrollbar(1rem); @include mixins.adapt-padding-to-scrollbar(0.5rem);
}
.resaleScreenRoot {
height: calc(100% - 6.25rem);
margin-top: 6.25rem;
}
.main {
height: 100%;
} }
.giftSection { .giftSection {
@ -48,6 +77,7 @@
padding: 0.5rem; padding: 0.5rem;
} }
.resaleStarGiftsContainer,
.starGiftsContainer, .starGiftsContainer,
.premiumGiftsGallery { .premiumGiftsGallery {
display: flex; display: flex;
@ -58,6 +88,7 @@
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
.resaleStarGiftsContainer,
.starGiftsContainer { .starGiftsContainer {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
@ -67,6 +98,10 @@
padding-top: 0.75rem; padding-top: 0.75rem;
} }
.resaleStarGiftsContainer {
padding-bottom: 0.625rem;
}
.header { .header {
position: absolute; position: absolute;
z-index: 2; z-index: 2;
@ -83,8 +118,19 @@
border-bottom: 0.0625rem solid var(--color-borders); border-bottom: 0.0625rem solid var(--color-borders);
background: var(--color-background); background: var(--color-background);
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
transition: height 0.25s ease-out, transform 0.25s ease-out;
}
transition: 0.25s ease-out transform; .resaleHeader {
overflow: visible;
height: 6.25rem;
}
.resaleHeaderContentContainer {
align-items: center;
justify-items: center;
width: 100%;
} }
.headerSlide { .headerSlide {
@ -102,11 +148,27 @@
transform: translateY(-100%); transform: translateY(-100%);
} }
.resaleHeaderText,
.commonHeaderText { .commonHeaderText {
unicode-bidi: plaintext; unicode-bidi: plaintext;
margin: 0 0 0 4.5rem; margin: 0 0 0 4.5rem;
font-size: 1.25rem; font-size: 1.25rem;
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
line-height: 1.25rem;
}
.resaleHeaderText {
margin: 0;
margin-bottom: 0.25rem !important;
}
.resaleHeaderDescription {
unicode-bidi: plaintext;
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--color-text-secondary);
} }
.closeButton { .closeButton {
@ -186,3 +248,36 @@
height: auto; height: auto;
min-height: calc(100% - 3.5rem); min-height: calc(100% - 3.5rem);
} }
.notFoundGiftsRoot {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 5rem;
}
.notFoundGiftsDescription {
unicode-bidi: plaintext;
margin-block: 1rem;
font-size: 1rem;
font-weight: var(--font-weight-medium);
color: var(--color-text-secondary);
text-align: center;
}
.notFoundGiftsLink {
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

@ -8,6 +8,7 @@ import type {
ApiDisallowedGifts, ApiDisallowedGifts,
ApiPeer, ApiPeer,
ApiPremiumGiftCodeOption, ApiPremiumGiftCodeOption,
ApiStarGift,
ApiStarGiftRegular, ApiStarGiftRegular,
ApiStarsAmount, ApiStarsAmount,
} from '../../../api/types'; } from '../../../api/types';
@ -17,6 +18,7 @@ import type { StarGiftCategory } from '../../../types';
import { STARS_CURRENCY_CODE } from '../../../config'; import { STARS_CURRENCY_CODE } from '../../../config';
import { getUserFullName } from '../../../global/helpers'; import { getUserFullName } from '../../../global/helpers';
import { getPeerTitle, isApiPeerChat, isApiPeerUser } from '../../../global/helpers/peers'; import { getPeerTitle, isApiPeerChat, isApiPeerUser } from '../../../global/helpers/peers';
import { selectTabState } from '../../../global/selectors';
import { selectPeer, selectUserFullInfo } from '../../../global/selectors'; import { selectPeer, selectUserFullInfo } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { throttle } from '../../../util/schedulers'; import { throttle } from '../../../util/schedulers';
@ -36,6 +38,8 @@ import BalanceBlock from '../stars/BalanceBlock';
import GiftSendingOptions from './GiftComposer'; import GiftSendingOptions from './GiftComposer';
import GiftItemPremium from './GiftItemPremium'; import GiftItemPremium from './GiftItemPremium';
import GiftItemStar from './GiftItemStar'; import GiftItemStar from './GiftItemStar';
import GiftModalResaleScreen from './GiftModalResaleScreen';
import GiftResaleFilters from './GiftResaleFilters';
import StarGiftCategoryList from './StarGiftCategoryList'; import StarGiftCategoryList from './StarGiftCategoryList';
import styles from './GiftModal.module.scss'; import styles from './GiftModal.module.scss';
@ -46,7 +50,7 @@ export type OwnProps = {
modal: TabState['giftModal']; modal: TabState['giftModal'];
}; };
export type GiftOption = ApiPremiumGiftCodeOption | ApiStarGiftRegular; export type GiftOption = ApiPremiumGiftCodeOption | ApiStarGift;
type StateProps = { type StateProps = {
boostPerSentGift?: number; boostPerSentGift?: number;
@ -56,6 +60,8 @@ type StateProps = {
peer?: ApiPeer; peer?: ApiPeer;
isSelf?: boolean; isSelf?: boolean;
disallowedGifts?: ApiDisallowedGifts; disallowedGifts?: ApiDisallowedGifts;
resaleGiftsCount?: number;
areResaleGiftsLoading?: boolean;
}; };
const AVATAR_SIZE = 100; const AVATAR_SIZE = 100;
@ -72,9 +78,11 @@ const GiftModal: FC<OwnProps & StateProps> = ({
peer, peer,
isSelf, isSelf,
disallowedGifts, disallowedGifts,
resaleGiftsCount,
areResaleGiftsLoading,
}) => { }) => {
const { const {
closeGiftModal, closeGiftModal, openGiftInfoModal, resetResaleGifts, loadResaleGifts,
} = getActions(); } = getActions();
const dialogRef = useRef<HTMLDivElement>(); const dialogRef = useRef<HTMLDivElement>();
const transitionRef = useRef<HTMLDivElement>(); const transitionRef = useRef<HTMLDivElement>();
@ -89,6 +97,7 @@ const GiftModal: FC<OwnProps & StateProps> = ({
const chat = peer && isApiPeerChat(peer) ? peer : undefined; const chat = peer && isApiPeerChat(peer) ? peer : undefined;
const [selectedGift, setSelectedGift] = useState<GiftOption | undefined>(); const [selectedGift, setSelectedGift] = useState<GiftOption | undefined>();
const [selectedResellGift, setSelectedResellGift] = useState<ApiStarGift | undefined>();
const [shouldShowMainScreenHeader, setShouldShowMainScreenHeader] = useState(false); const [shouldShowMainScreenHeader, setShouldShowMainScreenHeader] = useState(false);
const [isMainScreenHeaderForStarGifts, setIsMainScreenHeaderForStarGifts] = useState(false); const [isMainScreenHeaderForStarGifts, setIsMainScreenHeaderForStarGifts] = useState(false);
const [isGiftScreenHeaderForStarGifts, setIsGiftScreenHeaderForStarGifts] = useState(false); const [isGiftScreenHeaderForStarGifts, setIsGiftScreenHeaderForStarGifts] = useState(false);
@ -143,14 +152,22 @@ const GiftModal: FC<OwnProps & StateProps> = ({
observe: observeIntersection, observe: observeIntersection,
} = useIntersectionObserver({ rootRef: scrollerRef, throttleMs: INTERSECTION_THROTTLE, isDisabled: !isOpen }); } = useIntersectionObserver({ rootRef: scrollerRef, throttleMs: INTERSECTION_THROTTLE, isDisabled: !isOpen });
const isResaleScreen = Boolean(selectedResellGift) && !selectedGift;
const isGiftScreen = Boolean(selectedGift); const isGiftScreen = Boolean(selectedGift);
const shouldShowHeader = isGiftScreen || shouldShowMainScreenHeader; const shouldShowHeader = isResaleScreen || isGiftScreen || shouldShowMainScreenHeader;
const isHeaderForStarGifts = isGiftScreen ? isGiftScreenHeaderForStarGifts : isMainScreenHeaderForStarGifts; const isHeaderForStarGifts = isGiftScreen ? isGiftScreenHeaderForStarGifts : isMainScreenHeaderForStarGifts;
useEffect(() => {
if (selectedResellGift) {
loadResaleGifts({ giftId: selectedResellGift.id });
}
}, [selectedResellGift]);
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
setShouldShowMainScreenHeader(false); setShouldShowMainScreenHeader(false);
setSelectedGift(undefined); setSelectedGift(undefined);
setSelectedResellGift(undefined);
setSelectedCategory('all'); setSelectedCategory('all');
} }
}, [isOpen]); }, [isOpen]);
@ -228,7 +245,18 @@ const GiftModal: FC<OwnProps & StateProps> = ({
); );
} }
const handleGiftClick = useLastCallback((gift: GiftOption) => { const handleGiftClick = useLastCallback((gift: GiftOption, target?: 'resell' | 'original') => {
if (target === 'resell') {
if (!('id' in gift)) {
return;
}
if (isResaleScreen) {
openGiftInfoModal({ gift, recipientId: renderingModal?.forPeerId });
return;
}
setSelectedResellGift(gift);
return;
}
setSelectedGift(gift); setSelectedGift(gift);
setIsGiftScreenHeaderForStarGifts('id' in gift); setIsGiftScreenHeaderForStarGifts('id' in gift);
}); });
@ -254,16 +282,34 @@ const GiftModal: FC<OwnProps & StateProps> = ({
return ( return (
<div className={styles.starGiftsContainer}> <div className={styles.starGiftsContainer}>
{starGiftsById && filteredGiftIds?.map((giftId) => { {starGiftsById && filteredGiftIds?.flatMap((giftId) => {
const gift = starGiftsById[giftId]; const gift = starGiftsById[giftId];
return ( const shouldShowResale = selectedCategory !== 'stock' && Boolean(gift.availabilityResale);
const shouldDuplicateAsResale = selectedCategory !== 'resale' && shouldShowResale && !gift.isSoldOut;
const elements = [
<GiftItemStar <GiftItemStar
key={giftId} key={giftId}
gift={gift} gift={gift}
observeIntersection={observeIntersection} observeIntersection={observeIntersection}
isResale={shouldShowResale && !shouldDuplicateAsResale}
onClick={handleGiftClick} onClick={handleGiftClick}
/> />,
); ];
if (shouldDuplicateAsResale) {
elements.push(
<GiftItemStar
key={`resale_${giftId}`}
isResale
gift={gift}
observeIntersection={observeIntersection}
onClick={handleGiftClick}
/>,
);
}
return elements;
})} })}
</div> </div>
); );
@ -290,12 +336,24 @@ const GiftModal: FC<OwnProps & StateProps> = ({
setSelectedCategory(category); setSelectedCategory(category);
}); });
const handleCloseModal = useLastCallback(() => {
setSelectedGift(undefined);
setSelectedResellGift(undefined);
resetResaleGifts();
closeGiftModal();
});
const handleCloseButtonClick = useLastCallback(() => { const handleCloseButtonClick = useLastCallback(() => {
if (isResaleScreen) {
setSelectedResellGift(undefined);
resetResaleGifts();
return;
}
if (isGiftScreen) { if (isGiftScreen) {
setSelectedGift(undefined); setSelectedGift(undefined);
return; return;
} }
closeGiftModal(); handleCloseModal();
}); });
function renderMainScreen() { function renderMainScreen() {
@ -337,17 +395,51 @@ const GiftModal: FC<OwnProps & StateProps> = ({
); );
} }
const isBackButton = isGiftScreen; const isBackButton = isGiftScreen || isResaleScreen;
const buttonClassName = buildClassName( const buttonClassName = buildClassName(
'animated-close-icon', 'animated-close-icon',
isBackButton && 'state-back', isBackButton && 'state-back',
); );
function renderHeader() {
if (!shouldShowHeader) return undefined;
if (isResaleScreen) {
const isFirstLoading = areResaleGiftsLoading && !resaleGiftsCount;
return (
<div className={styles.resaleHeaderContentContainer}>
<h2 className={styles.resaleHeaderText}>
{selectedResellGift.title}
</h2>
{isFirstLoading
&& (
<div className={styles.resaleHeaderDescription}>
{lang('Loading')}
</div>
)}
{!isFirstLoading && resaleGiftsCount !== undefined
&& (
<div className={styles.resaleHeaderDescription}>
{lang('HeaderDescriptionResaleGifts', {
count: resaleGiftsCount,
}, { withNodes: true, withMarkdown: true, pluralValue: resaleGiftsCount })}
</div>
)}
<GiftResaleFilters dialogRef={dialogRef} />
</div>
);
}
return (
<h2 className={styles.commonHeaderText}>
{lang(isHeaderForStarGifts ? (isSelf ? 'StarsGiftHeaderSelf' : 'StarsGiftHeader') : 'GiftPremiumHeader')}
</h2>
);
}
return ( return (
<Modal <Modal
dialogRef={dialogRef} dialogRef={dialogRef}
onClose={closeGiftModal} onClose={handleCloseModal}
isOpen={isOpen} isOpen={isOpen}
isSlim isSlim
contentClassName={styles.content} contentClassName={styles.content}
@ -365,24 +457,32 @@ const GiftModal: FC<OwnProps & StateProps> = ({
<div className={buttonClassName} /> <div className={buttonClassName} />
</Button> </Button>
<BalanceBlock className={styles.balance} balance={starBalance} withAddButton /> <BalanceBlock className={styles.balance} balance={starBalance} withAddButton />
<div className={buildClassName(styles.header, !shouldShowHeader && styles.hiddenHeader)}> <div className={buildClassName(
styles.header,
isResaleScreen && styles.resaleHeader,
!shouldShowHeader && styles.hiddenHeader)}
>
<Transition <Transition
name="slideVerticalFade" name="slideVerticalFade"
activeKey={Number(isHeaderForStarGifts)} activeKey={!shouldShowHeader ? 0 : isResaleScreen ? 1 : isHeaderForStarGifts ? 2 : 3}
slideClassName={styles.headerSlide} slideClassName={styles.headerSlide}
> >
<h2 className={styles.commonHeaderText}> {renderHeader()}
{lang(isHeaderForStarGifts ? (isSelf ? 'StarsGiftHeaderSelf' : 'StarsGiftHeader') : 'GiftPremiumHeader')}
</h2>
</Transition> </Transition>
</div> </div>
<Transition <Transition
ref={transitionRef} ref={transitionRef}
className={styles.transition} className={styles.transition}
name="pushSlide" name="pushSlide"
activeKey={isGiftScreen ? 1 : 0} activeKey={isGiftScreen ? 1 : isResaleScreen ? 2 : 0}
> >
{!isGiftScreen && renderMainScreen()} {!isGiftScreen && !isResaleScreen && renderMainScreen()}
{isResaleScreen && selectedResellGift
&& (
<GiftModalResaleScreen
onGiftClick={handleGiftClick}
/>
)}
{isGiftScreen && renderingModal?.forPeerId && ( {isGiftScreen && renderingModal?.forPeerId && (
<GiftSendingOptions <GiftSendingOptions
gift={selectedGift} gift={selectedGift}
@ -406,6 +506,10 @@ export default memo(withGlobal<OwnProps>((global, { modal }): StateProps => {
const isSelf = Boolean(currentUserId && modal?.forPeerId === currentUserId); const isSelf = Boolean(currentUserId && modal?.forPeerId === currentUserId);
const userFullInfo = peer ? selectUserFullInfo(global, peer?.id) : undefined; const userFullInfo = peer ? selectUserFullInfo(global, peer?.id) : undefined;
const { resaleGifts } = selectTabState(global);
const resaleGiftsCount = resaleGifts.count;
const areResaleGiftsLoading = resaleGifts.isLoading !== false;
return { return {
boostPerSentGift: global.appConfig?.boostsPerSentGift, boostPerSentGift: global.appConfig?.boostsPerSentGift,
starGiftsById: starGifts?.byId, starGiftsById: starGifts?.byId,
@ -414,12 +518,15 @@ export default memo(withGlobal<OwnProps>((global, { modal }): StateProps => {
peer, peer,
isSelf, isSelf,
disallowedGifts: userFullInfo?.disallowedGifts, disallowedGifts: userFullInfo?.disallowedGifts,
resaleGiftsCount,
areResaleGiftsLoading,
}; };
})(GiftModal)); })(GiftModal));
function getCategoryKey(category: StarGiftCategory) { function getCategoryKey(category: StarGiftCategory) {
if (category === 'all') return -2; if (category === 'all') return 0;
if (category === 'limited') return -1; if (category === 'limited') return 1;
if (category === 'stock') return 0; if (category === 'resale') return 2;
return category; if (category === 'stock') return 3;
return category + 3;
} }

View File

@ -0,0 +1,171 @@
import type { FC } from '../../../lib/teact/teact';
import React, {
memo,
useMemo,
useRef } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type {
ApiStarGift,
} from '../../../api/types';
import type { ResaleGiftsFilterOptions } from '../../../types';
import { RESALE_GIFTS_LIMIT } from '../../../config';
import { selectTabState,
} from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
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 styles from './GiftModal.module.scss';
export type OwnProps = {
onGiftClick: (gift: ApiStarGift) => void;
};
type StateProps = {
gift?: ApiStarGift;
resellGifts?: ApiStarGift[];
filter: ResaleGiftsFilterOptions;
areGiftsAllLoaded?: boolean;
areGiftsLoading?: boolean;
updateIteration: number;
};
const INTERSECTION_THROTTLE = 200;
const GiftModalResaleScreen: FC<OwnProps & StateProps> = ({
resellGifts,
gift,
filter,
areGiftsAllLoaded,
areGiftsLoading,
updateIteration,
onGiftClick,
}) => {
const {
loadResaleGifts,
updateResaleGiftsFilter,
} = getActions();
const scrollerRef = useRef<HTMLDivElement>();
const lang = useLang();
const resellGiftsIds = useMemo(() => {
return resellGifts?.map((g) => g.id);
}, [resellGifts]);
const hasFilter = Boolean(filter?.modelAttributes?.length
|| filter?.patternAttributes?.length || filter?.backdropAttributes?.length);
const handleLoadMoreResellGifts = useLastCallback(() => {
if (gift) {
loadResaleGifts({ giftId: gift.id });
}
});
const isGiftsEmpty = Boolean(!resellGifts || resellGifts.length === 0);
const [viewportIds, onLoadMore] = useInfiniteScroll(
handleLoadMoreResellGifts,
resellGiftsIds,
!gift,
RESALE_GIFTS_LIMIT,
);
const { observe } = useIntersectionObserver({ rootRef: scrollerRef, throttleMs: INTERSECTION_THROTTLE });
const handleResetGiftsFilter = useLastCallback(() => {
updateResaleGiftsFilter({ filter: {
...filter,
modelAttributes: [],
backdropAttributes: [],
patternAttributes: [],
} });
});
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()}
<InfiniteScroll
className={buildClassName(styles.resaleStarGiftsContainer)}
items={viewportIds}
onLoadMore={onLoadMore}
itemSelector=".starGiftItem"
noFastList
preloadBackwards={RESALE_GIFTS_LIMIT}
scrollContainerClosest={`.${styles.resaleScreenRoot}`}
>
{resellGifts?.map((gift) => (
<GiftItemStar
key={gift.id}
gift={gift}
observeIntersection={observe}
isResale
onClick={onGiftClick}
/>
))}
</InfiniteScroll>
</Transition>
</div>
);
};
export default memo(withGlobal<OwnProps>((global): StateProps => {
const {
starGifts,
} = global;
const { resaleGifts } = selectTabState(global);
const gift = resaleGifts?.giftId ? starGifts?.byId[resaleGifts.giftId] : undefined;
const filter = resaleGifts.filter;
const areGiftsAllLoaded = resaleGifts.isAllLoaded;
const areGiftsLoading = resaleGifts.isLoading;
const updateIteration = resaleGifts.updateIteration;
return {
resellGifts: resaleGifts.gifts,
gift,
filter,
areGiftsAllLoaded,
areGiftsLoading,
updateIteration,
};
})(GiftModalResaleScreen));

View File

@ -0,0 +1,160 @@
@use '../../../styles/mixins';
.root {
position: relative;
display: flex;
width: 100%;
margin-top: 0.5rem;
}
.buttonsContainer {
scrollbar-color: rgba(0, 0, 0, 0);
scrollbar-width: none;
overflow-x: auto;
overflow-y: hidden;
display: flex;
align-items: center;
height: 100%;
padding-right: 0.5rem;
padding-left: 0.5rem;
white-space: nowrap;
@include mixins.gradient-border-horizontal(0.5rem, 0.5rem);
&::-webkit-scrollbar {
height: 0;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0);
}
}
.itemIcon {
margin-inline-start: 0.25rem;
}
.sticker {
margin-inline-start: 0.375rem;
}
.backdropAttributeMenuItemText,
.menuItemStickerText,
.menuItemText {
flex-grow: 1;
}
.menuItemStickerText {
margin-inline-start: 1.125rem;
}
.backdropAttributeMenuItemText {
margin-inline-start: 3rem;
}
.backdrop {
position: absolute;
left: 0.625rem;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
}
.menuItemIcon {
margin-inline: 0 !important;
margin-inline-start: 0.5 !important;
}
.item {
display: flex;
align-items: center;
justify-content: center;
width: auto;
margin-inline: 0.25rem;
padding: 0.125rem 0.75rem;
border-radius: 1rem;
font-size: 0.875rem;
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
white-space: nowrap;
background-color: var(--color-background-secondary);
transition: 0.15s background-color;
&:hover {
cursor: pointer;
background-color: var(--color-background-secondary-accent);
}
}
.menuContentContainer {
overflow-y: scroll;
max-height: 20rem;
}
:global(.MenuItem) {
:global(.icon) {
color: var(--color-text);
}
}
.menu {
:global(.bubble) {
top: 2.25rem !important;
}
&.left {
:global(.bubble) {
right: auto !important;
left: 0.125rem !important;
}
}
&.right {
:global(.bubble) {
right: 0.125rem !important;
left: auto !important;
}
}
:global(.SearchInput) {
--color-placeholders: var(--color-text-secondary);
width: 15rem;
border: none;
border-bottom: 0.0625rem solid var(--color-borders);
border-radius: 0;
background-color: transparent;
:global(.form-control) {
caret-color: var(--color-icon-secondary);
}
:global(.icon) {
color: var(--color-icon-secondary);
}
input {
padding-left: 1.25rem;
font-size: 0.875rem;
font-weight: var(--font-weight-medium);
}
:global(.icon-container-left) {
width: 1.25rem;
margin-inline-start: 1rem;
}
}
:global(.icon) {
font-size: 1.25rem !important;
color: var(--color-text);
}
}

View File

@ -0,0 +1,655 @@
import { type MouseEvent as ReactMouseEvent } from 'react';
import type { ElementRef, FC } from '../../../lib/teact/teact';
import React, {
memo,
useCallback,
useMemo,
useRef,
useState } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type {
ApiStarGiftAttribute,
ApiStarGiftAttributeBackdrop,
ApiStarGiftAttributeCounter,
ApiStarGiftAttributeIdBackdrop,
ApiStarGiftAttributeIdPattern,
ApiStarGiftAttributeModel,
ApiStarGiftAttributePattern,
StarGiftAttributeIdModel,
} from '../../../api/types';
import type { ResaleGiftsFilterOptions, ResaleGiftsSortType } from '../../../types';
import { selectTabState,
} from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import Icon from '../../common/icons/Icon';
import RadialPatternBackground from '../../common/profile/RadialPatternBackground';
import Menu from '../../ui/Menu';
import MenuItem from '../../ui/MenuItem';
import SearchInput from '../../ui/SearchInput';
import ResaleGiftMenuAttributeSticker from './ResaleGiftMenuAttributeSticker';
import styles from './GiftResaleFilters.module.scss';
type OwnProps = {
dialogRef: ElementRef<HTMLDivElement>;
};
type StateProps = {
filter: ResaleGiftsFilterOptions;
attributes?: ApiStarGiftAttribute[];
counters?: ApiStarGiftAttributeCounter[];
};
const GiftResaleFilters: FC<StateProps & OwnProps> = ({
attributes,
counters,
filter,
dialogRef,
}) => {
const lang = useLang();
const {
updateResaleGiftsFilter,
} = getActions();
const [searchModelQuery, setSearchModelQuery] = useState('');
const [searchBackdropQuery, setSearchBackdropQuery] = useState('');
const [searchPatternQuery, setSearchPatternQuery] = useState('');
const filteredAttributes = useMemo(() => {
const map: {
model: ApiStarGiftAttributeModel[];
pattern: ApiStarGiftAttributePattern[];
backdrop: ApiStarGiftAttributeBackdrop[];
} = {
model: [],
pattern: [],
backdrop: [],
};
for (const counter of counters ?? []) {
const { attribute } = counter;
if (!counter.count) {
continue;
}
const found = attributes?.find((attr) => {
if (attr.type === 'backdrop' && attribute.type === 'backdrop') {
return attr.backdropId === attribute.backdropId;
}
if (attr.type === 'model' && attribute.type === 'model') {
return attr.sticker.id === attribute.documentId;
}
if (attr.type === 'pattern' && attribute.type === 'pattern') {
return attr.sticker.id === attribute.documentId;
}
return false;
});
if (found?.type === 'backdrop') {
map.backdrop.push(found);
}
if (found?.type === 'model') {
map.model.push(found);
}
if (found?.type === 'pattern') {
map.pattern.push(found);
}
}
return map;
}, [attributes, counters]);
const filteredAndSearchedAttributes = useMemo(() => {
const filterBySearch = <T extends { name?: string }>(items: T[], query: string): T[] => {
if (!query.trim()) return items;
return items.filter(
(item): item is T => Boolean(item.name?.toLowerCase().includes(query.toLowerCase())),
);
};
return {
model: filterBySearch(filteredAttributes.model, searchModelQuery),
pattern: filterBySearch(filteredAttributes.pattern, searchPatternQuery),
backdrop: filterBySearch(filteredAttributes.backdrop, searchBackdropQuery),
};
}, [filteredAttributes, searchModelQuery, searchBackdropQuery, searchPatternQuery]);
// Sort Menu
const sortMenuRef = useRef<HTMLDivElement>();
const {
isContextMenuOpen: isSortContextMenuOpen,
contextMenuAnchor: sortContextMenuAnchor,
handleContextMenu: handleSortContextMenu,
handleContextMenuClose: handleSortContextMenuClose,
handleContextMenuHide: handleSortContextMenuHide,
} = useContextMenuHandlers(dialogRef);
const getSortMenuElement = useLastCallback(() => sortMenuRef.current!);
// Model Menu
const modelMenuRef = useRef<HTMLDivElement>();
const {
isContextMenuOpen: isModelContextMenuOpen,
contextMenuAnchor: modelContextMenuAnchor,
handleContextMenu: handleModelContextMenu,
handleContextMenuClose: handleModelContextMenuClose,
handleContextMenuHide: handleModelContextMenuHide,
} = useContextMenuHandlers(dialogRef);
const getModelMenuElement = useLastCallback(
() => modelMenuRef.current!,
);
// Backdrop Menu
const backdropMenuRef = useRef<HTMLDivElement>();
const {
isContextMenuOpen: isBackdropContextMenuOpen,
contextMenuAnchor: backdropContextMenuAnchor,
handleContextMenu: handleBackdropContextMenu,
handleContextMenuClose: handleBackdropContextMenuClose,
handleContextMenuHide: handleBackdropContextMenuHide,
} = useContextMenuHandlers(dialogRef);
const getBackdropMenuElement = useLastCallback(() => backdropMenuRef.current!);
// Pattern Menu
const patternMenuRef = useRef<HTMLDivElement>();
const {
isContextMenuOpen: isPatternContextMenuOpen,
contextMenuAnchor: patternContextMenuAnchor,
handleContextMenu: handlePatternContextMenu,
handleContextMenuClose: handlePatternContextMenuClose,
handleContextMenuHide: handlePatternContextMenuHide,
} = useContextMenuHandlers(dialogRef);
const getPatternMenuElement = useLastCallback(() => patternMenuRef.current!);
const SortMenuButton: FC<{ onTrigger: (e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => void; isOpen?: boolean }>
= useMemo(() => {
const sortType = filter.sortType;
return ({ onTrigger, isOpen: isMenuOpen }) => (
<div
className={styles.item}
onClick={onTrigger}
>
{sortType === 'byDate' && lang('ValueGiftSortByDate')}
{sortType === 'byNumber' && lang('ValueGiftSortByNumber')}
{sortType === 'byPrice' && lang('ValueGiftSortByPrice')}
<Icon
name="dropdown-arrows"
className={styles.itemIcon}
/>
</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 })}
<Icon
name="dropdown-arrows"
className={styles.itemIcon}
/>
</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 })}
<Icon
name="dropdown-arrows"
className={styles.itemIcon}
/>
</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 })}
<Icon
name="dropdown-arrows"
className={styles.itemIcon}
/>
</div>
);
}, [lang, filter]);
const handleSortMenuItemClick = useLastCallback((type: ResaleGiftsSortType) => {
updateResaleGiftsFilter({ filter: {
...filter,
sortType: type,
} });
});
const handleSelectedAllModelsClick = useLastCallback(() => {
updateResaleGiftsFilter({ filter: {
...filter,
modelAttributes: [],
} });
});
const handleSelectedAllPatternsClick = useLastCallback(() => {
updateResaleGiftsFilter({ filter: {
...filter,
patternAttributes: [],
} });
});
const handleSelectedAllBackdropsClick = useLastCallback(() => {
updateResaleGiftsFilter({ filter: {
...filter,
backdropAttributes: [],
} });
});
const handleModelMenuItemClick = useLastCallback((attribute: ApiStarGiftAttributeModel) => {
if (!counters) return;
const attributes = filter.modelAttributes || [];
const modelAttribute
= counters.find((counter): counter is ApiStarGiftAttributeCounter<StarGiftAttributeIdModel> =>
counter.attribute.type === 'model' && counter.attribute.documentId === attribute.sticker.id,
)?.attribute;
if (!modelAttribute) return;
const isActive = attributes.some((item) => item.documentId === modelAttribute.documentId);
const updatedAttributes = isActive
? attributes.filter((item) => item.documentId !== modelAttribute.documentId)
: [...attributes, modelAttribute];
updateResaleGiftsFilter({ filter: {
...filter,
modelAttributes: updatedAttributes,
} });
});
const handlePatternMenuItemClick = useLastCallback((attribute: ApiStarGiftAttributePattern) => {
if (!counters) return;
const attributes = filter.patternAttributes || [];
const patternAttribute = counters.find(
(counter): counter is ApiStarGiftAttributeCounter<ApiStarGiftAttributeIdPattern> =>
counter.attribute.type === 'pattern' && counter.attribute.documentId === attribute.sticker.id,
)?.attribute;
if (!patternAttribute) return;
const isActive = attributes.some((item) => item.documentId === patternAttribute.documentId);
const updatedAttributes = isActive
? attributes.filter((item) => item.documentId !== patternAttribute.documentId)
: [...attributes, patternAttribute];
updateResaleGiftsFilter({ filter: {
...filter,
patternAttributes: updatedAttributes,
} });
});
const handleBackdropMenuItemClick = useLastCallback((attribute: ApiStarGiftAttributeBackdrop) => {
if (!counters) return;
const attributes = filter.backdropAttributes || [];
const backdropAttribute = counters.find(
(counter): counter is ApiStarGiftAttributeCounter<ApiStarGiftAttributeIdBackdrop> =>
counter.attribute.type === 'backdrop' && counter.attribute.backdropId === attribute.backdropId,
)?.attribute;
if (!backdropAttribute) return;
const isActive = attributes.some((item) => item.backdropId === backdropAttribute.backdropId);
const updatedAttributes = isActive
? attributes.filter((item) => item.backdropId !== backdropAttribute.backdropId)
: [...attributes, backdropAttribute];
updateResaleGiftsFilter({ filter: {
...filter,
backdropAttributes: updatedAttributes,
} });
});
function renderSortMenuItems() {
return (
<>
<MenuItem icon="sort-by-price" onClick={() => { handleSortMenuItemClick('byPrice'); }}>
<div className={styles.menuItemText}>
{lang('GiftSortByPrice')}
</div>
<Icon
className={styles.menuItemIcon}
name={filter?.sortType === 'byPrice' ? 'check' : 'placeholder'}
/>
</MenuItem>
<MenuItem icon="sort-by-date" onClick={() => { handleSortMenuItemClick('byDate'); }}>
<div className={styles.menuItemText}>
{lang('GiftSortByDate')}
</div>
<Icon
className={styles.menuItemIcon}
name={filter?.sortType === 'byDate' ? 'check' : 'placeholder'}
/>
</MenuItem>
<MenuItem icon="sort-by-number"onClick={() => { handleSortMenuItemClick('byNumber'); }}>
<div className={styles.menuItemText}>
{lang('GiftSortByNumber')}
</div>
<Icon
className={styles.menuItemIcon}
name={filter?.sortType === 'byNumber' ? 'check' : 'placeholder'}
/>
</MenuItem>
</>
);
}
function renderSortMenu() {
return (
<Menu
isOpen={isSortContextMenuOpen}
anchor={sortContextMenuAnchor}
ref={sortMenuRef}
className={buildClassName(
styles.menu,
styles.left,
'with-menu-transitions',
)}
getMenuElement={getSortMenuElement}
autoClose
onClose={handleSortContextMenuClose}
onCloseAnimationEnd={handleSortContextMenuHide}
positionX="left"
>
{renderSortMenuItems()}
</Menu>
);
}
const handleSearchModelInputReset = useCallback(() => {
setSearchModelQuery('');
}, []);
const handleSearchBackdropInputReset = useCallback(() => {
setSearchBackdropQuery('');
}, []);
const handleSearchPatternInputReset = useCallback(() => {
setSearchPatternQuery('');
}, []);
const handleSearchInputClick = useLastCallback((e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
});
const modelMenuItemsContainerRef = useRef<HTMLDivElement>();
const { observe } = useIntersectionObserver({
rootRef: modelMenuItemsContainerRef,
isDisabled: !modelContextMenuAnchor,
});
function renderModelMenuItems() {
const models = filteredAndSearchedAttributes.model;
const selectedAttributes = filter.modelAttributes ?? [];
const isSelectedAll = selectedAttributes.length === 0;
return (
<div className={styles.menuContentContainer} ref={modelMenuItemsContainerRef}>
<SearchInput
onClick={handleSearchInputClick}
className={styles.search}
value={searchModelQuery}
onChange={setSearchModelQuery}
onReset={handleSearchModelInputReset}
placeholder={lang('Search')}
/>
<MenuItem icon="select" onClick={handleSelectedAllModelsClick} disabled={isSelectedAll}>
{lang('ContextMenuItemSelectAll')}
</MenuItem>
{models.map((model) => {
const isSelected = isSelectedAll
|| selectedAttributes.some((attr) => attr.documentId === model.sticker.id);
return (
<MenuItem
key={model.name}
onClick={() => {
handleModelMenuItemClick(model);
}}
>
<ResaleGiftMenuAttributeSticker
className={styles.sticker}
sticker={model.sticker}
type="model"
observeIntersectionForLoading={observe}
observeIntersectionForPlaying={observe}
/>
<div className={styles.menuItemStickerText}>
{model.name}
</div>
<Icon
className={styles.menuItemIcon}
name={isSelected ? 'check' : 'placeholder'}
/>
</MenuItem>
);
})}
</div>
);
}
function renderModelMenu() {
return (
<Menu
isOpen={isModelContextMenuOpen}
anchor={modelContextMenuAnchor}
ref={modelMenuRef}
className={buildClassName(
styles.menu,
styles.left,
'with-menu-transitions',
)}
getMenuElement={getModelMenuElement}
autoClose
onClose={handleModelContextMenuClose}
onCloseAnimationEnd={handleModelContextMenuHide}
>
{renderModelMenuItems()}
</Menu>
);
}
function renderBackdropMenuItems() {
const backdrops = filteredAndSearchedAttributes.backdrop;
const selectedAttributes = filter.backdropAttributes ?? [];
const isSelectedAll = selectedAttributes.length === 0;
return (
<div className={styles.menuContentContainer}>
<SearchInput
onClick={handleSearchInputClick}
className={styles.search}
value={searchBackdropQuery}
onChange={setSearchBackdropQuery}
onReset={handleSearchBackdropInputReset}
placeholder={lang('Search')}
/>
<MenuItem icon="select" onClick={handleSelectedAllBackdropsClick} disabled={isSelectedAll}>
{lang('ContextMenuItemSelectAll')}
</MenuItem>
{backdrops.map((backdrop) => {
const isSelected = isSelectedAll
|| selectedAttributes.some((attr) => attr.backdropId === backdrop.backdropId);
return (
<MenuItem
key={backdrop.name}
onClick={() => {
handleBackdropMenuItemClick(backdrop);
}}
>
<RadialPatternBackground
className={styles.backdrop}
backgroundColors={[backdrop.centerColor, backdrop.edgeColor]}
patternColor={backdrop.patternColor}
/>
<div className={styles.backdropAttributeMenuItemText}>
{backdrop.name}
</div>
<Icon
className={styles.menuItemIcon}
name={isSelected ? 'check' : 'placeholder'}
/>
</MenuItem>
);
})}
</div>
);
}
function renderBackdropMenu() {
return (
<Menu
isOpen={isBackdropContextMenuOpen}
anchor={backdropContextMenuAnchor}
ref={backdropMenuRef}
className={buildClassName(styles.menu, styles.right, 'with-menu-transitions')}
getMenuElement={getBackdropMenuElement}
autoClose
onClose={handleBackdropContextMenuClose}
onCloseAnimationEnd={handleBackdropContextMenuHide}
positionX="right"
>
{renderBackdropMenuItems()}
</Menu>
);
}
function renderPatternMenuItems() {
const patterns = filteredAndSearchedAttributes.pattern;
const selectedAttributes = filter.patternAttributes ?? [];
const isSelectedAll = selectedAttributes.length === 0;
return (
<div className={styles.menuContentContainer}>
<SearchInput
onClick={handleSearchInputClick}
className={styles.search}
value={searchPatternQuery}
onChange={setSearchPatternQuery}
onReset={handleSearchPatternInputReset}
placeholder={lang('Search')}
/>
<MenuItem icon="select" onClick={handleSelectedAllPatternsClick} disabled={isSelectedAll}>
{lang('ContextMenuItemSelectAll')}
</MenuItem>
{patterns.map((pattern) => {
const isSelected = isSelectedAll
|| selectedAttributes.some((attr) => attr.documentId === pattern.sticker.id);
return (
<MenuItem
key={pattern.name}
onClick={() => {
handlePatternMenuItemClick(pattern);
}}
>
<ResaleGiftMenuAttributeSticker
className={styles.sticker}
sticker={pattern.sticker}
type="pattern"
/>
<div className={styles.menuItemStickerText}>
{pattern.name}
</div>
<Icon
className={styles.menuItemIcon}
name={isSelected ? 'check' : 'placeholder'}
/>
</MenuItem>
);
})}
</div>
);
}
function renderPatternMenu() {
return (
<Menu
isOpen={isPatternContextMenuOpen}
anchor={patternContextMenuAnchor}
ref={patternMenuRef}
className={buildClassName(styles.menu, styles.right, 'with-menu-transitions')}
getMenuElement={getPatternMenuElement}
autoClose
onClose={handlePatternContextMenuClose}
onCloseAnimationEnd={handlePatternContextMenuHide}
>
{renderPatternMenuItems()}
</Menu>
);
}
return (
<div className={styles.root}>
{Boolean(sortContextMenuAnchor) && renderSortMenu()}
{Boolean(modelContextMenuAnchor) && renderModelMenu()}
{Boolean(backdropContextMenuAnchor) && renderBackdropMenu()}
{Boolean(patternContextMenuAnchor) && renderPatternMenu()}
<div className={styles.buttonsContainer}>
<SortMenuButton
onTrigger={handleSortContextMenu}
isOpen={isSortContextMenuOpen}
/>
<ModelMenuButton
onTrigger={handleModelContextMenu}
isOpen={isModelContextMenuOpen}
/>
<BackdropMenuButton
onTrigger={handleBackdropContextMenu}
isOpen={isBackdropContextMenuOpen}
/>
<PatternMenuButton
onTrigger={handlePatternContextMenu}
isOpen={isPatternContextMenuOpen}
/>
</div>
</div>
);
};
export default memo(withGlobal((global): StateProps => {
const { resaleGifts } = selectTabState(global);
const attributes = resaleGifts.attributes;
const counters = resaleGifts.counters;
const filter = resaleGifts.filter;
return {
attributes,
counters,
filter,
};
})(GiftResaleFilters));

View File

@ -0,0 +1,7 @@
.root {
position: relative;
}
.thumb {
position: absolute;
}

View File

@ -0,0 +1,69 @@
import type { FC } from '../../../lib/teact/teact';
import React, { memo, useRef } from '../../../lib/teact/teact';
import { withGlobal } from '../../../global';
import type { ApiSticker } from '../../../api/types';
import type { ThemeKey } from '../../../types';
import { selectTheme } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { REM } from '../../common/helpers/mediaDimensions';
import useDynamicColorListener from '../../../hooks/stickers/useDynamicColorListener';
import { type ObserveFn } from '../../../hooks/useIntersectionObserver';
import StickerView from '../../common/StickerView';
import styles from './ResaleGiftMenuAttributeSticker.module.scss';
type OwnProps = {
className?: string;
type: 'model' | 'pattern';
sticker: ApiSticker;
observeIntersectionForLoading?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn;
};
type StateProps = {
theme: ThemeKey;
};
const ATTRIBUTE_STICKER_SIZE = 1.5 * REM;
const ResaleGiftMenuAttributeSticker: FC<StateProps & OwnProps> = ({
className,
type,
sticker,
observeIntersectionForLoading,
observeIntersectionForPlaying,
theme,
}) => {
const stickerRef = useRef<HTMLDivElement>();
const customColor = useDynamicColorListener(stickerRef, undefined, type !== 'pattern');
return (
<div
ref={stickerRef}
className={buildClassName(styles.root, className)}
style={`width: ${ATTRIBUTE_STICKER_SIZE}px; height: ${ATTRIBUTE_STICKER_SIZE}px`}
>
<StickerView
containerRef={stickerRef}
sticker={sticker}
size={ATTRIBUTE_STICKER_SIZE}
shouldPreloadPreview
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
thumbClassName={styles.thumb}
customColor={customColor}
/>
</div>
);
};
export default memo(withGlobal<OwnProps>((global): StateProps => {
return {
theme: selectTheme(global),
};
})(ResaleGiftMenuAttributeSticker));

View File

@ -37,6 +37,8 @@ const StarGiftCategoryList = ({
.sort((a, b) => a - b), .sort((a, b) => a - b),
[idsByCategory]); [idsByCategory]);
const hasResale = idsByCategory && idsByCategory['resale'].length > 0;
const [selectedCategory, setSelectedCategory] = useState<StarGiftCategory>('all'); const [selectedCategory, setSelectedCategory] = useState<StarGiftCategory>('all');
function handleItemClick(category: StarGiftCategory) { function handleItemClick(category: StarGiftCategory) {
@ -50,6 +52,7 @@ const StarGiftCategoryList = ({
if (category === 'all') return lang('AllGiftsCategory'); if (category === 'all') return lang('AllGiftsCategory');
if (category === 'stock') return lang('StockGiftsCategory'); if (category === 'stock') return lang('StockGiftsCategory');
if (category === 'limited') return lang('LimitedGiftsCategory'); if (category === 'limited') return lang('LimitedGiftsCategory');
if (category === 'resale') return lang('GiftCategoryResale');
return category; return category;
} }
@ -80,6 +83,7 @@ const StarGiftCategoryList = ({
<div ref={ref} className={buildClassName(styles.list, 'no-scrollbar')}> <div ref={ref} className={buildClassName(styles.list, 'no-scrollbar')}>
{renderCategoryItem('all')} {renderCategoryItem('all')}
{!areLimitedStarGiftsDisallowed && renderCategoryItem('limited')} {!areLimitedStarGiftsDisallowed && renderCategoryItem('limited')}
{!areLimitedStarGiftsDisallowed && hasResale && renderCategoryItem('resale')}
{renderCategoryItem('stock')} {renderCategoryItem('stock')}
{starCategories?.map(renderCategoryItem)} {starCategories?.map(renderCategoryItem)}
</div> </div>

View File

@ -53,6 +53,7 @@
font-size: 0.75rem; font-size: 0.75rem;
} }
.starAmountIcon,
.giftResalePriceStar { .giftResalePriceStar {
margin-inline-start: 0 !important; margin-inline-start: 0 !important;
} }
@ -121,10 +122,6 @@
margin-bottom: 0; margin-bottom: 0;
} }
.starAmountIcon {
margin-inline-start: 0 !important;
}
.modalHeader { .modalHeader {
position: absolute; position: absolute;
z-index: 2; z-index: 2;

View File

@ -58,6 +58,7 @@ type StateProps = {
collectibleEmojiStatuses?: ApiEmojiStatusType[]; collectibleEmojiStatuses?: ApiEmojiStatusType[];
tonExplorerUrl?: string; tonExplorerUrl?: string;
currentUser?: ApiUser; currentUser?: ApiUser;
recipientPeer?: ApiPeer;
}; };
const STICKER_SIZE = 120; const STICKER_SIZE = 120;
@ -73,6 +74,7 @@ const GiftInfoModal = ({
collectibleEmojiStatuses, collectibleEmojiStatuses,
tonExplorerUrl, tonExplorerUrl,
currentUser, currentUser,
recipientPeer,
}: OwnProps & StateProps) => { }: OwnProps & StateProps) => {
const { const {
closeGiftInfoModal, closeGiftInfoModal,
@ -83,6 +85,7 @@ const GiftInfoModal = ({
openGiftUpgradeModal, openGiftUpgradeModal,
showNotification, showNotification,
buyStarGift, buyStarGift,
closeGiftModal,
} = getActions(); } = getActions();
const [isConvertConfirmOpen, openConvertConfirm, closeConvertConfirm] = useFlag(); const [isConvertConfirmOpen, openConvertConfirm, closeConvertConfirm] = useFlag();
@ -161,7 +164,7 @@ const GiftInfoModal = ({
}); });
const handleBuyGift = useLastCallback(() => { const handleBuyGift = useLastCallback(() => {
if (!savedGift || gift?.type !== 'starGiftUnique' || !gift.resellPriceInStars) return; if (gift?.type !== 'starGiftUnique' || !gift.resellPriceInStars) return;
setIsConfirmModalOpen(true); setIsConfirmModalOpen(true);
}); });
@ -170,9 +173,11 @@ const GiftInfoModal = ({
}); });
const handleConfirmBuyGift = useLastCallback(() => { const handleConfirmBuyGift = useLastCallback(() => {
if (!savedGift || gift?.type !== 'starGiftUnique' || !gift.resellPriceInStars) return; const peer = recipientPeer || currentUser;
if (!peer || gift?.type !== 'starGiftUnique' || !gift.resellPriceInStars) return;
closeConfirmModal(); closeConfirmModal();
buyStarGift({ slug: gift.slug, stars: gift.resellPriceInStars }); closeGiftModal();
buyStarGift({ peerId: peer.id, slug: gift.slug, stars: gift.resellPriceInStars });
}); });
const giftAttributes = useMemo(() => { const giftAttributes = useMemo(() => {
@ -726,18 +731,34 @@ const GiftInfoModal = ({
> >
<GiftTransferPreview <GiftTransferPreview
peer={currentUser} peer={recipientPeer || currentUser}
gift={uniqueGift} gift={uniqueGift}
/> />
<p> {!recipientPeer
{lang('GiftBuyConfirmDescription', { && (
gift: lang('GiftUnique', { title: uniqueGift.title, number: uniqueGift.number }), <p>
stars: formatStarsAsText(lang, resellPriceInStars), {lang('GiftBuyConfirmDescription', {
}, { gift: lang('GiftUnique', { title: uniqueGift.title, number: uniqueGift.number }),
withNodes: true, stars: formatStarsAsText(lang, resellPriceInStars),
withMarkdown: true, }, {
})} withNodes: true,
</p> withMarkdown: true,
})}
</p>
)}
{recipientPeer
&& (
<p>
{lang('GiftBuyForPeerConfirmDescription', {
gift: lang('GiftUnique', { title: uniqueGift.title, number: uniqueGift.number }),
stars: formatStarsAsText(lang, resellPriceInStars),
peer: getPeerTitle(lang, recipientPeer),
}, {
withNodes: true,
withMarkdown: true,
})}
</p>
)}
</ConfirmDialog> </ConfirmDialog>
)} )}
{savedGift && ( {savedGift && (
@ -786,6 +807,8 @@ export default memo(withGlobal<OwnProps>(
const chat = targetPeer && isApiPeerChat(targetPeer) ? targetPeer : undefined; const chat = targetPeer && isApiPeerChat(targetPeer) ? targetPeer : undefined;
const hasAdminRights = chat && getHasAdminRight(chat, 'postMessages'); const hasAdminRights = chat && getHasAdminRight(chat, 'postMessages');
const currentUser = selectUser(global, currentUserId!); const currentUser = selectUser(global, currentUserId!);
const recipientPeer = modal?.recipientId && currentUserId !== modal.recipientId
? selectPeer(global, modal.recipientId) : undefined;
const currentUserEmojiStatus = currentUser?.emojiStatus; const currentUserEmojiStatus = currentUser?.emojiStatus;
const collectibleEmojiStatuses = global.collectibleEmojiStatuses?.statuses; const collectibleEmojiStatuses = global.collectibleEmojiStatuses?.statuses;
@ -799,6 +822,7 @@ export default memo(withGlobal<OwnProps>(
currentUserEmojiStatus, currentUserEmojiStatus,
collectibleEmojiStatuses, collectibleEmojiStatuses,
currentUser, currentUser,
recipientPeer,
}; };
}, },
)(GiftInfoModal)); )(GiftInfoModal));

View File

@ -317,6 +317,12 @@
} }
} }
&.bluredStarsBadge {
color: var(--color-white);
background: rgba(0, 0, 0, 0.2) !important;
backdrop-filter: blur(50px);
}
&.smaller { &.smaller {
height: 2.75rem; height: 2.75rem;
padding: 0.3125rem; padding: 0.3125rem;

View File

@ -22,7 +22,7 @@ export type OwnProps = {
size?: 'default' | 'smaller' | 'tiny'; size?: 'default' | 'smaller' | 'tiny';
color?: ( color?: (
'primary' | 'secondary' | 'gray' | 'danger' | 'translucent' | 'translucent-white' | 'translucent-black' 'primary' | 'secondary' | 'gray' | 'danger' | 'translucent' | 'translucent-white' | 'translucent-black'
| 'translucent-bordered' | 'dark' | 'green' | 'adaptive' | 'stars' | 'translucent-bordered' | 'dark' | 'green' | 'adaptive' | 'stars' | 'bluredStarsBadge'
); );
backgroundImage?: string; backgroundImage?: string;
id?: string; id?: string;

View File

@ -1,3 +1,4 @@
import type { MouseEvent as ReactMouseEvent } from 'react';
import type { ElementRef, FC } from '../../lib/teact/teact'; import type { ElementRef, FC } from '../../lib/teact/teact';
import React, { import React, {
memo, useEffect, useRef, memo, useEffect, useRef,
@ -43,7 +44,7 @@ type OwnProps = {
onReset?: NoneToVoidFunction; onReset?: NoneToVoidFunction;
onFocus?: NoneToVoidFunction; onFocus?: NoneToVoidFunction;
onBlur?: NoneToVoidFunction; onBlur?: NoneToVoidFunction;
onClick?: NoneToVoidFunction; onClick?: (e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => void;
onUpClick?: (event: React.MouseEvent<HTMLButtonElement>) => void; onUpClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onDownClick?: (event: React.MouseEvent<HTMLButtonElement>) => void; onDownClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onSpinnerClick?: NoneToVoidFunction; onSpinnerClick?: NoneToVoidFunction;

View File

@ -3,6 +3,7 @@ import type {
} from './api/types'; } from './api/types';
import type { import type {
GiftProfileFilterOptions, GiftProfileFilterOptions,
ResaleGiftsFilterOptions,
} from './types'; } from './types';
export const APP_CODE_NAME = 'A'; export const APP_CODE_NAME = 'A';
@ -104,6 +105,7 @@ export const GROUP_CALL_PARTICIPANTS_LIMIT = 100;
export const STORY_LIST_LIMIT = 100; export const STORY_LIST_LIMIT = 100;
export const API_GENERAL_ID_LIMIT = 100; export const API_GENERAL_ID_LIMIT = 100;
export const STATISTICS_PUBLIC_FORWARDS_LIMIT = 50; export const STATISTICS_PUBLIC_FORWARDS_LIMIT = 50;
export const RESALE_GIFTS_LIMIT = 50;
export const STORY_VIEWS_MIN_SEARCH = 15; export const STORY_VIEWS_MIN_SEARCH = 15;
export const STORY_MIN_REACTIONS_SORT = 10; export const STORY_MIN_REACTIONS_SORT = 10;
@ -465,3 +467,7 @@ export const DEFAULT_GIFT_PROFILE_FILTER_OPTIONS: GiftProfileFilterOptions = {
shouldIncludeDisplayed: true, shouldIncludeDisplayed: true,
shouldIncludeHidden: true, shouldIncludeHidden: true,
} as const; } as const;
export const DEFAULT_RESALE_GIFTS_FILTER_OPTIONS: ResaleGiftsFilterOptions = {
sortType: 'byDate',
};

View File

@ -146,13 +146,13 @@ addActionHandler('sendStarGift', (global, actions, payload): ActionReturnType =>
addActionHandler('buyStarGift', (global, actions, payload): ActionReturnType => { addActionHandler('buyStarGift', (global, actions, payload): ActionReturnType => {
const { const {
slug, stars, tabId = getCurrentTabId(), slug, peerId, stars, tabId = getCurrentTabId(),
} = payload; } = payload;
const inputInvoice: ApiInputInvoiceStarGiftResale = { const inputInvoice: ApiInputInvoiceStarGiftResale = {
type: 'stargiftResale', type: 'stargiftResale',
slug, slug,
peerId: global.currentUserId!, peerId,
}; };
payInputStarInvoice(global, inputInvoice, stars, tabId); payInputStarInvoice(global, inputInvoice, stars, tabId);

View File

@ -2,6 +2,7 @@ import type { ApiSavedStarGift, ApiStarGiftUnique } from '../../../api/types';
import type { StarGiftCategory } from '../../../types'; import type { StarGiftCategory } from '../../../types';
import type { ActionReturnType } from '../../types'; import type { ActionReturnType } from '../../types';
import { DEFAULT_RESALE_GIFTS_FILTER_OPTIONS, RESALE_GIFTS_LIMIT } from '../../../config';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { buildCollectionByKey } from '../../../util/iteratees'; import { buildCollectionByKey } from '../../../util/iteratees';
import { callApi } from '../../../api/gramjs'; import { callApi } from '../../../api/gramjs';
@ -11,8 +12,10 @@ import {
appendStarsSubscriptions, appendStarsSubscriptions,
appendStarsTransactions, appendStarsTransactions,
replacePeerSavedGifts, replacePeerSavedGifts,
updateChats,
updateStarsBalance, updateStarsBalance,
updateStarsSubscriptionLoading, updateStarsSubscriptionLoading,
updateUsers,
} from '../../reducers'; } from '../../reducers';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { import {
@ -102,6 +105,7 @@ addActionHandler('loadStarGifts', async (global): Promise<void> => {
all: [], all: [],
stock: [], stock: [],
limited: [], limited: [],
resale: [],
}; };
const allStarGiftIds = Object.keys(byId); const allStarGiftIds = Object.keys(byId);
@ -114,9 +118,13 @@ addActionHandler('loadStarGifts', async (global): Promise<void> => {
gift.availabilityRemains || !gift.availabilityTotal ? gift.id : undefined gift.availabilityRemains || !gift.availabilityTotal ? gift.id : undefined
)).filter(Boolean); )).filter(Boolean);
const resaleStarGiftIds = allStarGifts.map((gift) => (gift.availabilityResale ? gift.id : undefined))
.filter(Boolean);
idsByCategoryName.all = allStarGiftIds; idsByCategoryName.all = allStarGiftIds;
idsByCategoryName.limited = limitedStarGiftIds; idsByCategoryName.limited = limitedStarGiftIds;
idsByCategoryName.stock = stockedStarGiftIds; idsByCategoryName.stock = stockedStarGiftIds;
idsByCategoryName.resale = resaleStarGiftIds;
allStarGifts.forEach((gift) => { allStarGifts.forEach((gift) => {
const starsCategory = gift.stars; const starsCategory = gift.stars;
@ -137,6 +145,115 @@ addActionHandler('loadStarGifts', async (global): Promise<void> => {
setGlobal(global); setGlobal(global);
}); });
addActionHandler('updateResaleGiftsFilter', (global, actions, payload): ActionReturnType => {
const {
filter, tabId = getCurrentTabId(),
} = payload;
const tabState = selectTabState(global, tabId);
global = updateTabState(global, {
resaleGifts: {
...tabState.resaleGifts,
filter,
},
}, tabId);
if (tabState.resaleGifts.giftId) {
actions.loadResaleGifts({ giftId: tabState.resaleGifts.giftId, shouldRefresh: true, tabId });
}
setGlobal(global);
});
addActionHandler('loadResaleGifts', async (global, actions, payload): Promise<void> => {
const {
giftId, shouldRefresh, tabId = getCurrentTabId(),
} = payload;
let tabState = selectTabState(global, tabId);
if (tabState.resaleGifts.isLoading || (tabState.resaleGifts.isAllLoaded && !shouldRefresh)) return;
global = updateTabState(global, {
resaleGifts: {
...tabState.resaleGifts,
isLoading: true,
...(shouldRefresh && {
count: 0,
nextOffset: undefined,
isAllLoaded: false,
}),
},
}, tabId);
setGlobal(global);
global = getGlobal();
tabState = selectTabState(global, tabId);
const nextOffset = tabState.resaleGifts.nextOffset;
const attributesHash = tabState.resaleGifts.attributesHash;
const filter = tabState.resaleGifts.filter;
const result = await callApi('fetchResaleGifts', {
giftId,
offset: nextOffset,
limit: RESALE_GIFTS_LIMIT,
attributesHash,
filter,
});
if (!result) {
return;
};
const {
chats,
users,
} = result;
global = getGlobal();
tabState = selectTabState(global, tabId);
const currentGifts = tabState.resaleGifts.gifts;
const newGifts = !shouldRefresh ? currentGifts.concat(result.gifts) : result.gifts;
const currentUpdateIteration = tabState.resaleGifts.updateIteration;
const shouldUpdateIteration = tabState.resaleGifts.giftId !== giftId || shouldRefresh;
const updateIteration = shouldUpdateIteration ? currentUpdateIteration + 1 : currentUpdateIteration;
global = updateTabState(global, {
resaleGifts: {
...tabState.resaleGifts,
giftId,
count: result.count || tabState.resaleGifts.count,
gifts: newGifts,
attributes: result.attributes || tabState.resaleGifts.attributes,
counters: result.counters || tabState.resaleGifts.counters,
attributesHash: result.attributesHash,
nextOffset: result.nextOffset,
isLoading: false,
isAllLoaded: !result.nextOffset,
updateIteration,
},
}, tabId);
global = updateUsers(global, buildCollectionByKey(users, 'id'));
global = updateChats(global, buildCollectionByKey(chats, 'id'));
setGlobal(global);
});
addActionHandler('resetResaleGifts', (global, actions, payload): ActionReturnType => {
const {
tabId = getCurrentTabId(),
} = payload || {};
const tabState = selectTabState(global, tabId);
return updateTabState(global, {
resaleGifts: {
updateIteration: tabState.resaleGifts.updateIteration + 1,
filter: DEFAULT_RESALE_GIFTS_FILTER_OPTIONS,
count: 0,
gifts: [],
},
}, tabId);
});
addActionHandler('loadPeerSavedGifts', async (global, actions, payload): Promise<void> => { addActionHandler('loadPeerSavedGifts', async (global, actions, payload): Promise<void> => {
const { const {
peerId, shouldRefresh, tabId = getCurrentTabId(), peerId, shouldRefresh, tabId = getCurrentTabId(),

View File

@ -256,11 +256,13 @@ addActionHandler('openGiftInfoModal', (global, actions, payload): ActionReturnTy
} = payload; } = payload;
const peerId = 'peerId' in payload ? payload.peerId : undefined; const peerId = 'peerId' in payload ? payload.peerId : undefined;
const recipientId = 'recipientId' in payload ? payload.recipientId : undefined;
return updateTabState(global, { return updateTabState(global, {
giftInfoModal: { giftInfoModal: {
peerId, peerId,
gift, gift,
recipientId,
}, },
}, tabId); }, tabId);
}); });

View File

@ -9,6 +9,7 @@ import {
DEFAULT_MESSAGE_TEXT_SIZE_PX, DEFAULT_MESSAGE_TEXT_SIZE_PX,
DEFAULT_PATTERN_COLOR, DEFAULT_PATTERN_COLOR,
DEFAULT_PLAYBACK_RATE, DEFAULT_PLAYBACK_RATE,
DEFAULT_RESALE_GIFTS_FILTER_OPTIONS,
DEFAULT_VOLUME, DEFAULT_VOLUME,
IOS_DEFAULT_MESSAGE_TEXT_SIZE_PX, IOS_DEFAULT_MESSAGE_TEXT_SIZE_PX,
MACOS_DEFAULT_MESSAGE_TEXT_SIZE_PX, MACOS_DEFAULT_MESSAGE_TEXT_SIZE_PX,
@ -389,6 +390,15 @@ export const INITIAL_TAB_STATE: TabState = {
giftsByPeerId: {}, giftsByPeerId: {},
}, },
resaleGifts: {
gifts: [],
count: 0,
updateIteration: 0,
filter: {
...DEFAULT_RESALE_GIFTS_FILTER_OPTIONS,
},
},
storyViewer: { storyViewer: {
isMuted: true, isMuted: true,
isRibbonShown: false, isRibbonShown: false,

View File

@ -80,6 +80,7 @@ import type {
PerformanceType, PerformanceType,
Point, Point,
ProfileTabType, ProfileTabType,
ResaleGiftsFilterOptions,
ScrollTargetPosition, ScrollTargetPosition,
SendMessageParams, SendMessageParams,
SettingsScreens, SettingsScreens,
@ -2394,6 +2395,14 @@ export interface ActionPayloads {
loadPremiumGifts: undefined; loadPremiumGifts: undefined;
loadStarGifts: undefined; loadStarGifts: undefined;
updateResaleGiftsFilter: {
filter: ResaleGiftsFilterOptions;
} & WithTabId;
loadResaleGifts: {
giftId: string;
shouldRefresh?: boolean;
} & WithTabId;
resetResaleGifts: WithTabId | undefined;
loadDefaultTopicIcons: undefined; loadDefaultTopicIcons: undefined;
loadPremiumStickers: undefined; loadPremiumStickers: undefined;
@ -2403,6 +2412,7 @@ export interface ActionPayloads {
closeGiftModal: WithTabId | undefined; closeGiftModal: WithTabId | undefined;
sendStarGift: StarGiftInfo & WithTabId; sendStarGift: StarGiftInfo & WithTabId;
buyStarGift: { buyStarGift: {
peerId: string;
slug: string; slug: string;
stars: number; stars: number;
} & WithTabId; } & WithTabId;
@ -2419,6 +2429,7 @@ export interface ActionPayloads {
} & WithTabId; } & WithTabId;
openGiftInfoModal: ({ openGiftInfoModal: ({
peerId: string; peerId: string;
recipientId?: string;
gift: ApiSavedStarGift; gift: ApiSavedStarGift;
} | { } | {
gift: ApiStarGift; gift: ApiStarGift;

View File

@ -38,6 +38,7 @@ import type {
ApiSponsoredPeer, ApiSponsoredPeer,
ApiStarGift, ApiStarGift,
ApiStarGiftAttribute, ApiStarGiftAttribute,
ApiStarGiftAttributeCounter,
ApiStarGiveawayOption, ApiStarGiveawayOption,
ApiStarsSubscription, ApiStarsSubscription,
ApiStarsTransaction, ApiStarsTransaction,
@ -76,6 +77,7 @@ import type {
PaymentStep, PaymentStep,
ProfileEditProgress, ProfileEditProgress,
ProfileTabType, ProfileTabType,
ResaleGiftsFilterOptions,
ScrollTargetPosition, ScrollTargetPosition,
SettingsScreens, SettingsScreens,
SharedMediaType, SharedMediaType,
@ -217,6 +219,20 @@ export type TabState = {
filter: GiftProfileFilterOptions; filter: GiftProfileFilterOptions;
}; };
resaleGifts: {
giftId?: string;
gifts: ApiStarGift[];
count: number;
attributes?: ApiStarGiftAttribute[];
counters?: ApiStarGiftAttributeCounter[];
nextOffset?: string;
attributesHash?: string;
isLoading?: boolean;
isAllLoaded?: boolean;
filter: ResaleGiftsFilterOptions;
updateIteration: number;
};
leftColumn: { leftColumn: {
contentKey: LeftColumnContent; contentKey: LeftColumnContent;
settingsScreen: SettingsScreens; settingsScreen: SettingsScreens;
@ -761,6 +777,7 @@ export type TabState = {
giftInfoModal?: { giftInfoModal?: {
peerId?: string; peerId?: string;
recipientId?: string;
gift: ApiSavedStarGift | ApiStarGift; gift: ApiSavedStarGift | ApiStarGift;
}; };

View File

@ -1765,6 +1765,7 @@ payments.getUniqueStarGift#a1974d72 slug:string = payments.UniqueStarGift;
payments.getSavedStarGifts#23830de9 flags:# exclude_unsaved:flags.0?true exclude_saved:flags.1?true exclude_unlimited:flags.2?true exclude_limited:flags.3?true exclude_unique:flags.4?true sort_by_value:flags.5?true peer:InputPeer offset:string limit:int = payments.SavedStarGifts; payments.getSavedStarGifts#23830de9 flags:# exclude_unsaved:flags.0?true exclude_saved:flags.1?true exclude_unlimited:flags.2?true exclude_limited:flags.3?true exclude_unique:flags.4?true sort_by_value:flags.5?true peer:InputPeer offset:string limit:int = payments.SavedStarGifts;
payments.getStarGiftWithdrawalUrl#d06e93a8 stargift:InputSavedStarGift password:InputCheckPasswordSRP = payments.StarGiftWithdrawalUrl; payments.getStarGiftWithdrawalUrl#d06e93a8 stargift:InputSavedStarGift password:InputCheckPasswordSRP = payments.StarGiftWithdrawalUrl;
payments.toggleStarGiftsPinnedToTop#1513e7b0 peer:InputPeer stargift:Vector<InputSavedStarGift> = Bool; payments.toggleStarGiftsPinnedToTop#1513e7b0 peer:InputPeer stargift:Vector<InputSavedStarGift> = Bool;
payments.getResaleStarGifts#7a5fa236 flags:# sort_by_price:flags.1?true sort_by_num:flags.2?true attributes_hash:flags.0?long gift_id:long attributes:flags.3?Vector<StarGiftAttributeId> offset:string limit:int = payments.ResaleStarGifts;
payments.updateStarGiftPrice#3baea4e1 stargift:InputSavedStarGift resell_stars:long = Updates; payments.updateStarGiftPrice#3baea4e1 stargift:InputSavedStarGift resell_stars:long = Updates;
phone.requestCall#42ff96ed flags:# video:flags.0?true user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall; 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.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall;

View File

@ -319,6 +319,7 @@
"payments.getUniqueStarGift", "payments.getUniqueStarGift",
"payments.getStarGiftWithdrawalUrl", "payments.getStarGiftWithdrawalUrl",
"payments.toggleStarGiftsPinnedToTop", "payments.toggleStarGiftsPinnedToTop",
"payments.getResaleStarGifts",
"payments.updateStarGiftPrice", "payments.updateStarGiftPrice",
"langpack.getLangPack", "langpack.getLangPack",
"langpack.getStrings", "langpack.getStrings",

View File

@ -109,193 +109,197 @@ $icons-map: (
"double-badge": "\f148", "double-badge": "\f148",
"down": "\f149", "down": "\f149",
"download": "\f14a", "download": "\f14a",
"eats": "\f14b", "dropdown-arrows": "\f14b",
"edit": "\f14c", "eats": "\f14c",
"email": "\f14d", "edit": "\f14d",
"enter": "\f14e", "email": "\f14e",
"expand-modal": "\f14f", "enter": "\f14f",
"expand": "\f150", "expand-modal": "\f150",
"eye-crossed-outline": "\f151", "expand": "\f151",
"eye-crossed": "\f152", "eye-crossed-outline": "\f152",
"eye-outline": "\f153", "eye-crossed": "\f153",
"eye": "\f154", "eye-outline": "\f154",
"favorite-filled": "\f155", "eye": "\f155",
"favorite": "\f156", "favorite-filled": "\f156",
"file-badge": "\f157", "favorite": "\f157",
"flag": "\f158", "file-badge": "\f158",
"folder-badge": "\f159", "flag": "\f159",
"folder": "\f15a", "folder-badge": "\f15a",
"fontsize": "\f15b", "folder": "\f15b",
"forums": "\f15c", "fontsize": "\f15c",
"forward": "\f15d", "forums": "\f15d",
"fragment": "\f15e", "forward": "\f15e",
"frozen-time": "\f15f", "fragment": "\f15f",
"fullscreen": "\f160", "frozen-time": "\f160",
"gifs": "\f161", "fullscreen": "\f161",
"gift": "\f162", "gifs": "\f162",
"group-filled": "\f163", "gift": "\f163",
"group": "\f164", "group-filled": "\f164",
"grouped-disable": "\f165", "group": "\f165",
"grouped": "\f166", "grouped-disable": "\f166",
"hand-stop": "\f167", "grouped": "\f167",
"hashtag": "\f168", "hand-stop": "\f168",
"heart-outline": "\f169", "hashtag": "\f169",
"heart": "\f16a", "heart-outline": "\f16a",
"help": "\f16b", "heart": "\f16b",
"info-filled": "\f16c", "help": "\f16c",
"info": "\f16d", "info-filled": "\f16d",
"install": "\f16e", "info": "\f16e",
"italic": "\f16f", "install": "\f16f",
"key": "\f170", "italic": "\f170",
"keyboard": "\f171", "key": "\f171",
"lamp": "\f172", "keyboard": "\f172",
"language": "\f173", "lamp": "\f173",
"large-pause": "\f174", "language": "\f174",
"large-play": "\f175", "large-pause": "\f175",
"link-badge": "\f176", "large-play": "\f176",
"link-broken": "\f177", "link-badge": "\f177",
"link": "\f178", "link-broken": "\f178",
"location": "\f179", "link": "\f179",
"lock-badge": "\f17a", "location": "\f17a",
"lock": "\f17b", "lock-badge": "\f17b",
"logout": "\f17c", "lock": "\f17c",
"loop": "\f17d", "logout": "\f17d",
"mention": "\f17e", "loop": "\f17e",
"message-failed": "\f17f", "mention": "\f17f",
"message-pending": "\f180", "message-failed": "\f180",
"message-read": "\f181", "message-pending": "\f181",
"message-succeeded": "\f182", "message-read": "\f182",
"message": "\f183", "message-succeeded": "\f183",
"microphone-alt": "\f184", "message": "\f184",
"microphone": "\f185", "microphone-alt": "\f185",
"monospace": "\f186", "microphone": "\f186",
"more-circle": "\f187", "monospace": "\f187",
"more": "\f188", "more-circle": "\f188",
"move-caption-down": "\f189", "more": "\f189",
"move-caption-up": "\f18a", "move-caption-down": "\f18a",
"mute": "\f18b", "move-caption-up": "\f18b",
"muted": "\f18c", "mute": "\f18c",
"my-notes": "\f18d", "muted": "\f18d",
"new-chat-filled": "\f18e", "my-notes": "\f18e",
"next": "\f18f", "new-chat-filled": "\f18f",
"nochannel": "\f190", "next": "\f190",
"noise-suppression": "\f191", "nochannel": "\f191",
"non-contacts": "\f192", "noise-suppression": "\f192",
"one-filled": "\f193", "non-contacts": "\f193",
"open-in-new-tab": "\f194", "one-filled": "\f194",
"password-off": "\f195", "open-in-new-tab": "\f195",
"pause": "\f196", "password-off": "\f196",
"permissions": "\f197", "pause": "\f197",
"phone-discard-outline": "\f198", "permissions": "\f198",
"phone-discard": "\f199", "phone-discard-outline": "\f199",
"phone": "\f19a", "phone-discard": "\f19a",
"photo": "\f19b", "phone": "\f19b",
"pin-badge": "\f19c", "photo": "\f19c",
"pin-list": "\f19d", "pin-badge": "\f19d",
"pin": "\f19e", "pin-list": "\f19e",
"pinned-chat": "\f19f", "pin": "\f19f",
"pinned-message": "\f1a0", "pinned-chat": "\f1a0",
"pip": "\f1a1", "pinned-message": "\f1a1",
"play-story": "\f1a2", "pip": "\f1a2",
"play": "\f1a3", "play-story": "\f1a3",
"poll": "\f1a4", "play": "\f1a4",
"previous": "\f1a5", "poll": "\f1a5",
"privacy-policy": "\f1a6", "previous": "\f1a6",
"proof-of-ownership": "\f1a7", "privacy-policy": "\f1a7",
"quote-text": "\f1a8", "proof-of-ownership": "\f1a8",
"quote": "\f1a9", "quote-text": "\f1a9",
"radial-badge": "\f1aa", "quote": "\f1aa",
"readchats": "\f1ab", "radial-badge": "\f1ab",
"recent": "\f1ac", "readchats": "\f1ac",
"reload": "\f1ad", "recent": "\f1ad",
"remove-quote": "\f1ae", "reload": "\f1ae",
"remove": "\f1af", "remove-quote": "\f1af",
"reopen-topic": "\f1b0", "remove": "\f1b0",
"replace": "\f1b1", "reopen-topic": "\f1b1",
"replies": "\f1b2", "replace": "\f1b2",
"reply-filled": "\f1b3", "replies": "\f1b3",
"reply": "\f1b4", "reply-filled": "\f1b4",
"revenue-split": "\f1b5", "reply": "\f1b5",
"revote": "\f1b6", "revenue-split": "\f1b6",
"save-story": "\f1b7", "revote": "\f1b7",
"saved-messages": "\f1b8", "save-story": "\f1b8",
"schedule": "\f1b9", "saved-messages": "\f1b9",
"search": "\f1ba", "schedule": "\f1ba",
"select": "\f1bb", "search": "\f1bb",
"sell-outline": "\f1bc", "select": "\f1bc",
"sell": "\f1bd", "sell-outline": "\f1bd",
"send-outline": "\f1be", "sell": "\f1be",
"send": "\f1bf", "send-outline": "\f1bf",
"settings-filled": "\f1c0", "send": "\f1c0",
"settings": "\f1c1", "settings-filled": "\f1c1",
"share-filled": "\f1c2", "settings": "\f1c2",
"share-screen-outlined": "\f1c3", "share-filled": "\f1c3",
"share-screen-stop": "\f1c4", "share-screen-outlined": "\f1c4",
"share-screen": "\f1c5", "share-screen-stop": "\f1c5",
"show-message": "\f1c6", "share-screen": "\f1c6",
"sidebar": "\f1c7", "show-message": "\f1c7",
"skip-next": "\f1c8", "sidebar": "\f1c8",
"skip-previous": "\f1c9", "skip-next": "\f1c9",
"smallscreen": "\f1ca", "skip-previous": "\f1ca",
"smile": "\f1cb", "smallscreen": "\f1cb",
"sort": "\f1cc", "smile": "\f1cc",
"speaker-muted-story": "\f1cd", "sort-by-date": "\f1cd",
"speaker-outline": "\f1ce", "sort-by-number": "\f1ce",
"speaker-story": "\f1cf", "sort-by-price": "\f1cf",
"speaker": "\f1d0", "sort": "\f1d0",
"spoiler-disable": "\f1d1", "speaker-muted-story": "\f1d1",
"spoiler": "\f1d2", "speaker-outline": "\f1d2",
"sport": "\f1d3", "speaker-story": "\f1d3",
"star": "\f1d4", "speaker": "\f1d4",
"stars-lock": "\f1d5", "spoiler-disable": "\f1d5",
"stats": "\f1d6", "spoiler": "\f1d6",
"stealth-future": "\f1d7", "sport": "\f1d7",
"stealth-past": "\f1d8", "star": "\f1d8",
"stickers": "\f1d9", "stars-lock": "\f1d9",
"stop-raising-hand": "\f1da", "stats": "\f1da",
"stop": "\f1db", "stealth-future": "\f1db",
"story-caption": "\f1dc", "stealth-past": "\f1dc",
"story-expired": "\f1dd", "stickers": "\f1dd",
"story-priority": "\f1de", "stop-raising-hand": "\f1de",
"story-reply": "\f1df", "stop": "\f1df",
"strikethrough": "\f1e0", "story-caption": "\f1e0",
"tag-add": "\f1e1", "story-expired": "\f1e1",
"tag-crossed": "\f1e2", "story-priority": "\f1e2",
"tag-filter": "\f1e3", "story-reply": "\f1e3",
"tag-name": "\f1e4", "strikethrough": "\f1e4",
"tag": "\f1e5", "tag-add": "\f1e5",
"timer": "\f1e6", "tag-crossed": "\f1e6",
"toncoin": "\f1e7", "tag-filter": "\f1e7",
"trade": "\f1e8", "tag-name": "\f1e8",
"transcribe": "\f1e9", "tag": "\f1e9",
"truck": "\f1ea", "timer": "\f1ea",
"unarchive": "\f1eb", "toncoin": "\f1eb",
"underlined": "\f1ec", "trade": "\f1ec",
"unique-profile": "\f1ed", "transcribe": "\f1ed",
"unlist-outline": "\f1ee", "truck": "\f1ee",
"unlist": "\f1ef", "unarchive": "\f1ef",
"unlock-badge": "\f1f0", "underlined": "\f1f0",
"unlock": "\f1f1", "unique-profile": "\f1f1",
"unmute": "\f1f2", "unlist-outline": "\f1f2",
"unpin": "\f1f3", "unlist": "\f1f3",
"unread": "\f1f4", "unlock-badge": "\f1f4",
"up": "\f1f5", "unlock": "\f1f5",
"user-filled": "\f1f6", "unmute": "\f1f6",
"user-online": "\f1f7", "unpin": "\f1f7",
"user": "\f1f8", "unread": "\f1f8",
"video-outlined": "\f1f9", "up": "\f1f9",
"video-stop": "\f1fa", "user-filled": "\f1fa",
"video": "\f1fb", "user-online": "\f1fb",
"view-once": "\f1fc", "user": "\f1fc",
"voice-chat": "\f1fd", "video-outlined": "\f1fd",
"volume-1": "\f1fe", "video-stop": "\f1fe",
"volume-2": "\f1ff", "video": "\f1ff",
"volume-3": "\f200", "view-once": "\f200",
"web": "\f201", "voice-chat": "\f201",
"webapp": "\f202", "volume-1": "\f202",
"word-wrap": "\f203", "volume-2": "\f203",
"zoom-in": "\f204", "volume-3": "\f204",
"zoom-out": "\f205", "web": "\f205",
"webapp": "\f206",
"word-wrap": "\f207",
"zoom-in": "\f208",
"zoom-out": "\f209",
); );
.icon-active-sessions::before { .icon-active-sessions::before {
@ -520,6 +524,9 @@ $icons-map: (
.icon-download::before { .icon-download::before {
content: map.get($icons-map, "download"); content: map.get($icons-map, "download");
} }
.icon-dropdown-arrows::before {
content: map.get($icons-map, "dropdown-arrows");
}
.icon-eats::before { .icon-eats::before {
content: map.get($icons-map, "eats"); content: map.get($icons-map, "eats");
} }
@ -907,6 +914,15 @@ $icons-map: (
.icon-smile::before { .icon-smile::before {
content: map.get($icons-map, "smile"); content: map.get($icons-map, "smile");
} }
.icon-sort-by-date::before {
content: map.get($icons-map, "sort-by-date");
}
.icon-sort-by-number::before {
content: map.get($icons-map, "sort-by-number");
}
.icon-sort-by-price::before {
content: map.get($icons-map, "sort-by-price");
}
.icon-sort::before { .icon-sort::before {
content: map.get($icons-map, "sort"); content: map.get($icons-map, "sort");
} }

Binary file not shown.

Binary file not shown.

View File

@ -73,6 +73,7 @@ export type FontIconName =
| 'double-badge' | 'double-badge'
| 'down' | 'down'
| 'download' | 'download'
| 'dropdown-arrows'
| 'eats' | 'eats'
| 'edit' | 'edit'
| 'email' | 'email'
@ -202,6 +203,9 @@ export type FontIconName =
| 'skip-previous' | 'skip-previous'
| 'smallscreen' | 'smallscreen'
| 'smile' | 'smile'
| 'sort-by-date'
| 'sort-by-number'
| 'sort-by-price'
| 'sort' | 'sort'
| 'speaker-muted-story' | 'speaker-muted-story'
| 'speaker-outline' | 'speaker-outline'

View File

@ -26,6 +26,8 @@ import type {
ApiPhoto, ApiPhoto,
ApiReaction, ApiReaction,
ApiReactionWithPaid, ApiReactionWithPaid,
ApiStarGiftAttributeIdBackdrop,
ApiStarGiftAttributeIdPattern,
ApiStarGiftRegular, ApiStarGiftRegular,
ApiStarsSubscription, ApiStarsSubscription,
ApiStarsTransaction, ApiStarsTransaction,
@ -37,6 +39,7 @@ import type {
ApiTopic, ApiTopic,
ApiTypingStatus, ApiTypingStatus,
ApiVideo, ApiVideo,
StarGiftAttributeIdModel,
} from '../api/types'; } from '../api/types';
import type { DC_IDS } from '../config'; import type { DC_IDS } from '../config';
import type { SearchResultKey } from '../util/keys/searchResultKey'; import type { SearchResultKey } from '../util/keys/searchResultKey';
@ -687,7 +690,7 @@ export interface Point {
export type WebPageMediaSize = 'large' | 'small'; export type WebPageMediaSize = 'large' | 'small';
export type StarGiftCategory = number | 'all' | 'limited' | 'stock'; export type StarGiftCategory = number | 'all' | 'limited' | 'stock' | 'resale';
export type CallSound = ( export type CallSound = (
'join' | 'allowTalk' | 'leave' | 'connecting' | 'incoming' | 'end' | 'connect' | 'busy' | 'ringing' 'join' | 'allowTalk' | 'leave' | 'connecting' | 'incoming' | 'end' | 'connect' | 'busy' | 'ringing'
@ -705,6 +708,13 @@ export type GiftProfileFilterOptions = {
shouldIncludeDisplayed: boolean; shouldIncludeDisplayed: boolean;
shouldIncludeHidden: boolean; shouldIncludeHidden: boolean;
}; };
export type ResaleGiftsSortType = 'byDate' | 'byPrice' | 'byNumber';
export type ResaleGiftsFilterOptions = {
sortType: ResaleGiftsSortType;
modelAttributes?: StarGiftAttributeIdModel[];
patternAttributes?: ApiStarGiftAttributeIdPattern[];
backdropAttributes?: ApiStarGiftAttributeIdBackdrop[];
};
export type SendMessageParams = { export type SendMessageParams = {
chat?: ApiChat; chat?: ApiChat;

View File

@ -1514,6 +1514,17 @@ export interface LangPair {
'StarGiftSaleTransaction': undefined; 'StarGiftSaleTransaction': undefined;
'StarGiftPurchaseTransaction': undefined; 'StarGiftPurchaseTransaction': undefined;
'ContextMenuItemMention': undefined; 'ContextMenuItemMention': undefined;
'GiftRibbonResale': undefined;
'GiftCategoryResale': undefined;
'GiftSortByPrice': undefined;
'GiftSortByNumber': undefined;
'ContextMenuItemSelectAll': undefined;
'ButtonSort': undefined;
'ValueGiftSortByDate': undefined;
'ValueGiftSortByPrice': undefined;
'ValueGiftSortByNumber': undefined;
'ResellGiftsNoFound': undefined;
'ResellGiftsClearFilters': undefined;
} }
export interface LangPairWithVariables<V = LangVariable> { export interface LangPairWithVariables<V = LangVariable> {
@ -2464,6 +2475,11 @@ export interface LangPairWithVariables<V = LangVariable> {
'gift': V; 'gift': V;
'stars': V; 'stars': V;
}; };
'GiftBuyForPeerConfirmDescription': {
'gift': V;
'stars': V;
'peer': V;
};
'ComposerTitleForwardFrom': { 'ComposerTitleForwardFrom': {
'users': V; 'users': V;
}; };
@ -2758,6 +2774,18 @@ export interface LangPairPluralWithVariables<V = LangVariable> {
'PaidMessageTransaction': { 'PaidMessageTransaction': {
'count': V; 'count': V;
}; };
'HeaderDescriptionResaleGifts': {
'count': V;
};
'GiftAttributeModelPlural': {
'count': V;
};
'GiftAttributeBackdropPlural': {
'count': V;
};
'GiftAttributeSymbolPlural': {
'count': V;
};
} }
export type RegularLangKey = keyof LangPair; export type RegularLangKey = keyof LangPair;
export type RegularLangKeyWithVariables = keyof LangPairWithVariables; export type RegularLangKeyWithVariables = keyof LangPairWithVariables;