Gifts Modal: Support marketplace (#5936)
This commit is contained in:
parent
d7ea4a5748
commit
4330472674
@ -1,3 +1,4 @@
|
||||
import bigInt from 'big-integer';
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type {
|
||||
@ -6,13 +7,18 @@ import type {
|
||||
ApiSavedStarGift,
|
||||
ApiStarGift,
|
||||
ApiStarGiftAttribute,
|
||||
ApiStarGiftAttributeCounter,
|
||||
ApiStarGiftAttributeId,
|
||||
ApiTypeResaleStarGifts,
|
||||
} from '../../types';
|
||||
|
||||
import { numberToHexColor } from '../../../util/colors';
|
||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||
import { addDocumentToLocalDb } from '../helpers/localDb';
|
||||
import { buildApiFormattedText } from './common';
|
||||
import { getApiChatIdFromMtpPeer } from './peers';
|
||||
import { buildStickerFromDocument } from './symbols';
|
||||
import { buildApiUser } from './users';
|
||||
|
||||
export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
|
||||
if (starGift instanceof GramJs.StarGiftUnique) {
|
||||
@ -40,7 +46,7 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
|
||||
|
||||
const {
|
||||
id, limited, stars, availabilityRemains, availabilityTotal, convertStars, firstSaleDate, lastSaleDate, soldOut,
|
||||
birthday, upgradeStars, resellMinStars, title,
|
||||
birthday, upgradeStars, resellMinStars, title, availabilityResale,
|
||||
} = starGift;
|
||||
|
||||
addDocumentToLocalDb(starGift.sticker);
|
||||
@ -63,6 +69,7 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
|
||||
upgradeStars: upgradeStars?.toJSNumber(),
|
||||
title,
|
||||
resellMinStars: resellMinStars?.toJSNumber(),
|
||||
availabilityResale: availabilityResale?.toJSNumber(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -101,11 +108,12 @@ export function buildApiStarGiftAttribute(attribute: GramJs.TypeStarGiftAttribut
|
||||
|
||||
if (attribute instanceof GramJs.StarGiftAttributeBackdrop) {
|
||||
const {
|
||||
name, rarityPermille, centerColor, edgeColor, patternColor, textColor,
|
||||
name, rarityPermille, centerColor, edgeColor, patternColor, textColor, backdropId,
|
||||
} = attribute;
|
||||
|
||||
return {
|
||||
type: 'backdrop',
|
||||
backdropId,
|
||||
name,
|
||||
rarityPercent: rarityPermille / 10,
|
||||
centerColor: numberToHexColor(centerColor),
|
||||
@ -180,3 +188,92 @@ export function buildApiDisallowedGiftsSettings(
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
import bigInt from 'big-integer';
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type { GiftProfileFilterOptions } from '../../../types';
|
||||
import type { GiftProfileFilterOptions, ResaleGiftsFilterOptions } from '../../../types';
|
||||
import type {
|
||||
ApiChat,
|
||||
ApiPeer,
|
||||
ApiRequestInputSavedStarGift,
|
||||
ApiStarGiftAttributeId,
|
||||
ApiStarGiftRegular,
|
||||
} from '../../types';
|
||||
|
||||
import { buildApiSavedStarGift, buildApiStarGift, buildApiStarGiftAttribute } from '../apiBuilders/gifts';
|
||||
import { buildApiResaleGifts, buildApiSavedStarGift, buildApiStarGift,
|
||||
buildApiStarGiftAttribute, buildInputResaleGiftsAttributes } from '../apiBuilders/gifts';
|
||||
import {
|
||||
buildApiStarsAmount,
|
||||
buildApiStarsGiftOptions,
|
||||
@ -44,6 +46,48 @@ export async function fetchStarGifts() {
|
||||
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({
|
||||
peer,
|
||||
offset = '',
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { ApiWebDocument } from './bots';
|
||||
import type { ApiChat } from './chats';
|
||||
import type { ApiFormattedText, ApiSticker, BoughtPaidMedia } from './messages';
|
||||
import type { ApiUser } from './users';
|
||||
|
||||
export interface ApiStarGiftRegular {
|
||||
type: 'starGift';
|
||||
@ -10,6 +11,7 @@ export interface ApiStarGiftRegular {
|
||||
stars: number;
|
||||
availabilityRemains?: number;
|
||||
availabilityTotal?: number;
|
||||
availabilityResale?: number;
|
||||
starsToConvert: number;
|
||||
isSoldOut?: true;
|
||||
firstSaleDate?: number;
|
||||
@ -54,6 +56,7 @@ export interface ApiStarGiftAttributePattern {
|
||||
|
||||
export interface ApiStarGiftAttributeBackdrop {
|
||||
type: 'backdrop';
|
||||
backdropId: number;
|
||||
name: string;
|
||||
centerColor: string;
|
||||
edgeColor: string;
|
||||
@ -95,6 +98,37 @@ export interface ApiSavedStarGift {
|
||||
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 {
|
||||
type: 'user';
|
||||
messageId: number;
|
||||
|
||||
1
src/assets/font-icons/dropdown-arrows.svg
Normal file
1
src/assets/font-icons/dropdown-arrows.svg
Normal 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 |
1
src/assets/font-icons/sort-by-date.svg
Normal file
1
src/assets/font-icons/sort-by-date.svg
Normal 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 |
1
src/assets/font-icons/sort-by-number.svg
Normal file
1
src/assets/font-icons/sort-by-number.svg
Normal 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 |
1
src/assets/font-icons/sort-by-price.svg
Normal file
1
src/assets/font-icons/sort-by-price.svg
Normal 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 |
@ -1977,5 +1977,25 @@
|
||||
"StarGiftSaleTransaction" = "Gift Purchase";
|
||||
"StarGiftPurchaseTransaction" = "Gift Sale";
|
||||
"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}**";
|
||||
"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";
|
||||
@ -19,9 +19,10 @@ const COLORS = {
|
||||
type ColorKey = keyof typeof COLORS;
|
||||
|
||||
const COLOR_KEYS = new Set(Object.keys(COLORS) as ColorKey[]);
|
||||
type GradientColor = readonly [string, string];
|
||||
|
||||
type OwnProps = {
|
||||
color: ColorKey | (string & {});
|
||||
color: ColorKey | GradientColor | (string & {});
|
||||
text: string;
|
||||
className?: string;
|
||||
};
|
||||
@ -40,7 +41,13 @@ const GiftRibbon = ({
|
||||
|
||||
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 endColor = gradientColor ? gradientColor[1] : color;
|
||||
|
||||
|
||||
@ -94,13 +94,14 @@ function GiftComposer({
|
||||
}
|
||||
}, [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 isPeerUser = peer && isApiPeerUser(peer);
|
||||
const isSelf = peerId === currentUserId;
|
||||
|
||||
const localMessage = useMemo(() => {
|
||||
if (!isStarGift) {
|
||||
if (isPremiumGift) {
|
||||
const currentGift = shouldPayByStars && hasPremiumByStars ? giftByStars : gift;
|
||||
return {
|
||||
id: -1,
|
||||
@ -121,32 +122,35 @@ function GiftComposer({
|
||||
} satisfies ApiMessage;
|
||||
}
|
||||
|
||||
return {
|
||||
id: -1,
|
||||
chatId: '0',
|
||||
isOutgoing: false,
|
||||
senderId: currentUserId,
|
||||
date: Math.floor(Date.now() / 1000),
|
||||
content: {
|
||||
action: {
|
||||
mediaType: 'action',
|
||||
type: 'starGift',
|
||||
message: giftMessage?.length ? {
|
||||
text: giftMessage,
|
||||
} : undefined,
|
||||
isNameHidden: shouldHideName || undefined,
|
||||
starsToConvert: gift.starsToConvert,
|
||||
canUpgrade: shouldPayForUpgrade || undefined,
|
||||
alreadyPaidUpgradeStars: shouldPayForUpgrade ? gift.upgradeStars : undefined,
|
||||
gift,
|
||||
peerId,
|
||||
fromId: currentUserId,
|
||||
if (isStarGift) {
|
||||
return {
|
||||
id: -1,
|
||||
chatId: '0',
|
||||
isOutgoing: false,
|
||||
senderId: currentUserId,
|
||||
date: Math.floor(Date.now() / 1000),
|
||||
content: {
|
||||
action: {
|
||||
mediaType: 'action',
|
||||
type: 'starGift',
|
||||
message: giftMessage?.length ? {
|
||||
text: giftMessage,
|
||||
} : undefined,
|
||||
isNameHidden: shouldHideName || undefined,
|
||||
starsToConvert: gift.starsToConvert,
|
||||
canUpgrade: shouldPayForUpgrade || undefined,
|
||||
alreadyPaidUpgradeStars: shouldPayForUpgrade ? gift.upgradeStars : undefined,
|
||||
gift,
|
||||
peerId,
|
||||
fromId: currentUserId,
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies ApiMessage;
|
||||
} satisfies ApiMessage;
|
||||
}
|
||||
return undefined;
|
||||
}, [currentUserId, gift, giftMessage, isStarGift,
|
||||
shouldHideName, shouldPayForUpgrade, peerId,
|
||||
shouldPayByStars, hasPremiumByStars, giftByStars]);
|
||||
shouldPayByStars, hasPremiumByStars, giftByStars, isPremiumGift]);
|
||||
|
||||
const handleGiftMessageChange = useLastCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setGiftMessage(e.target.value);
|
||||
@ -198,14 +202,16 @@ function GiftComposer({
|
||||
return;
|
||||
}
|
||||
|
||||
openInvoice({
|
||||
type: 'giftcode',
|
||||
userIds: [peerId],
|
||||
currency: gift.currency,
|
||||
amount: gift.amount,
|
||||
option: gift,
|
||||
message: giftMessage ? { text: giftMessage } : undefined,
|
||||
});
|
||||
if (isPremiumGift) {
|
||||
openInvoice({
|
||||
type: 'giftcode',
|
||||
userIds: [peerId],
|
||||
currency: gift.currency,
|
||||
amount: gift.amount,
|
||||
option: gift,
|
||||
message: giftMessage ? { text: giftMessage } : undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const canUseStarsPayment = hasPremiumByStars && starBalance && (starBalance.amount > giftByStars.amount);
|
||||
@ -320,7 +326,7 @@ function GiftComposer({
|
||||
? formatStarsAsIcon(lang, giftByStars.amount, { asFont: true })
|
||||
: isStarGift
|
||||
? 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 (
|
||||
<div className={styles.footer}>
|
||||
@ -359,6 +365,8 @@ function GiftComposer({
|
||||
customBackground && isBackgroundBlurred && styles.blurred,
|
||||
);
|
||||
|
||||
if ((!isStarGift && !isPremiumGift) || !localMessage) return;
|
||||
|
||||
return (
|
||||
<div className={buildClassName(styles.root, 'custom-scroll')}>
|
||||
<div
|
||||
@ -375,7 +383,7 @@ function GiftComposer({
|
||||
style={customBackgroundValue ? `--custom-background: ${customBackgroundValue}` : undefined}
|
||||
/>
|
||||
<ActionMessage
|
||||
key={isStarGift ? gift.id : gift.months}
|
||||
key={isStarGift ? gift.id : isPremiumGift ? gift.months : undefined}
|
||||
message={localMessage}
|
||||
threadId={MAIN_THREAD_ID}
|
||||
appearanceOrder={0}
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
}
|
||||
|
||||
:global(html.theme-dark) & {
|
||||
background-color: var(--color-background);
|
||||
background-color: rgb(33, 33, 33);
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
@ -68,6 +68,7 @@
|
||||
}
|
||||
|
||||
.star {
|
||||
margin-inline-start: 0 !important;
|
||||
margin-inline-end: 0.125rem;
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
@ -86,3 +87,12 @@
|
||||
margin-inline-start: 0 !important;
|
||||
margin-inline-end: 0.125rem !important;
|
||||
}
|
||||
|
||||
.radialPattern {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.stickerWrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@ -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 type {
|
||||
ApiStarGiftRegular,
|
||||
ApiStarGift,
|
||||
} from '../../../api/types';
|
||||
|
||||
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 useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker';
|
||||
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 styles from './GiftItem.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
gift: ApiStarGiftRegular;
|
||||
gift: ApiStarGift;
|
||||
observeIntersection?: ObserveFn;
|
||||
onClick: (gift: ApiStarGiftRegular) => void;
|
||||
onClick: (gift: ApiStarGift, target: 'original' | 'resell') => void;
|
||||
isResale?: boolean;
|
||||
};
|
||||
|
||||
const GIFT_STICKER_SIZE = 90;
|
||||
|
||||
function GiftItemStar({ gift, observeIntersection, onClick }: OwnProps) {
|
||||
function GiftItemStar({
|
||||
gift, observeIntersection, onClick, isResale,
|
||||
}: OwnProps) {
|
||||
const { openGiftInfoModal } = getActions();
|
||||
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
const stickerRef = useRef<HTMLDivElement>();
|
||||
|
||||
const lang = useLang();
|
||||
const [shouldPlay, play] = useFlag();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const {
|
||||
stars,
|
||||
isLimited,
|
||||
isSoldOut,
|
||||
sticker,
|
||||
} = gift;
|
||||
const sticker = getStickerFromGift(gift);
|
||||
const isGiftUnique = gift.type === 'starGiftUnique';
|
||||
const uniqueGift = isGiftUnique ? gift : undefined;
|
||||
const regularGift = !isGiftUnique ? gift : undefined;
|
||||
|
||||
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(() => {
|
||||
if (isSoldOut) {
|
||||
if (isSoldOut && !isResale) {
|
||||
openGiftInfoModal({ gift });
|
||||
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) => {
|
||||
if (entry.isIntersecting) play();
|
||||
const visible = entry.isIntersecting;
|
||||
setIsVisible(visible);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={buildClassName(styles.container, styles.starGift)}
|
||||
className={buildClassName(styles.container, styles.starGift, 'starGiftItem')}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={handleGiftClick}
|
||||
>
|
||||
<AnimatedIconFromSticker
|
||||
sticker={sticker}
|
||||
noLoop
|
||||
play={shouldPlay}
|
||||
{radialPatternBackdrop}
|
||||
|
||||
<div
|
||||
ref={stickerRef}
|
||||
className={styles.stickerWrapper}
|
||||
style={`width: ${GIFT_STICKER_SIZE}px; height: ${GIFT_STICKER_SIZE}px`}
|
||||
>
|
||||
{sticker && (
|
||||
<StickerView
|
||||
observeIntersectionForPlaying={observeIntersection}
|
||||
observeIntersectionForLoading={observeIntersection}
|
||||
containerRef={stickerRef}
|
||||
sticker={sticker}
|
||||
size={GIFT_STICKER_SIZE}
|
||||
shouldPreloadPreview
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<Button
|
||||
className={styles.buy}
|
||||
nonInteractive
|
||||
size={GIFT_STICKER_SIZE}
|
||||
/>
|
||||
<Button className={styles.buy} nonInteractive size="tiny" color="stars" withSparkleEffect pill fluid>
|
||||
<Icon name="star" className={styles.star} />
|
||||
<div className={styles.amount}>
|
||||
{stars}
|
||||
</div>
|
||||
size="tiny"
|
||||
color={isGiftUnique ? 'bluredStarsBadge' : 'stars'}
|
||||
withSparkleEffect={isVisible}
|
||||
pill
|
||||
fluid
|
||||
>
|
||||
{formatStarsAsIcon(lang, priceInStarsAsString || 0, { asFont: true, className: styles.star })}
|
||||
</Button>
|
||||
{isLimited && !isSoldOut && <GiftRibbon color="blue" text={lang('GiftLimited')} />}
|
||||
{isSoldOut && <GiftRibbon color="red" text={lang('GiftSoldOut')} />}
|
||||
{giftRibbon}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -9,17 +9,37 @@
|
||||
background-color: var(--color-background);
|
||||
|
||||
:global(html.theme-dark) & {
|
||||
background-color: #181818;
|
||||
--color-background: #181818;
|
||||
}
|
||||
}
|
||||
|
||||
.root :global(.modal-dialog),
|
||||
.transition,
|
||||
.content {
|
||||
height: min(92vh, 45rem);
|
||||
height: min(92vh, 49rem);
|
||||
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-content),
|
||||
.transition {
|
||||
@ -31,13 +51,22 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.resaleScreenRoot,
|
||||
.main {
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
padding-bottom: 1rem;
|
||||
padding-inline: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-inline: 0.5rem;
|
||||
|
||||
@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 {
|
||||
@ -48,6 +77,7 @@
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.resaleStarGiftsContainer,
|
||||
.starGiftsContainer,
|
||||
.premiumGiftsGallery {
|
||||
display: flex;
|
||||
@ -58,6 +88,7 @@
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.resaleStarGiftsContainer,
|
||||
.starGiftsContainer {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
@ -67,6 +98,10 @@
|
||||
padding-top: 0.75rem;
|
||||
}
|
||||
|
||||
.resaleStarGiftsContainer {
|
||||
padding-bottom: 0.625rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
@ -83,8 +118,19 @@
|
||||
border-bottom: 0.0625rem solid var(--color-borders);
|
||||
|
||||
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 {
|
||||
@ -102,11 +148,27 @@
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
.resaleHeaderText,
|
||||
.commonHeaderText {
|
||||
unicode-bidi: plaintext;
|
||||
|
||||
margin: 0 0 0 4.5rem;
|
||||
|
||||
font-size: 1.25rem;
|
||||
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 {
|
||||
@ -186,3 +248,36 @@
|
||||
height: auto;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import type {
|
||||
ApiDisallowedGifts,
|
||||
ApiPeer,
|
||||
ApiPremiumGiftCodeOption,
|
||||
ApiStarGift,
|
||||
ApiStarGiftRegular,
|
||||
ApiStarsAmount,
|
||||
} from '../../../api/types';
|
||||
@ -17,6 +18,7 @@ import type { StarGiftCategory } from '../../../types';
|
||||
import { STARS_CURRENCY_CODE } from '../../../config';
|
||||
import { getUserFullName } from '../../../global/helpers';
|
||||
import { getPeerTitle, isApiPeerChat, isApiPeerUser } from '../../../global/helpers/peers';
|
||||
import { selectTabState } from '../../../global/selectors';
|
||||
import { selectPeer, selectUserFullInfo } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { throttle } from '../../../util/schedulers';
|
||||
@ -36,6 +38,8 @@ import BalanceBlock from '../stars/BalanceBlock';
|
||||
import GiftSendingOptions from './GiftComposer';
|
||||
import GiftItemPremium from './GiftItemPremium';
|
||||
import GiftItemStar from './GiftItemStar';
|
||||
import GiftModalResaleScreen from './GiftModalResaleScreen';
|
||||
import GiftResaleFilters from './GiftResaleFilters';
|
||||
import StarGiftCategoryList from './StarGiftCategoryList';
|
||||
|
||||
import styles from './GiftModal.module.scss';
|
||||
@ -46,7 +50,7 @@ export type OwnProps = {
|
||||
modal: TabState['giftModal'];
|
||||
};
|
||||
|
||||
export type GiftOption = ApiPremiumGiftCodeOption | ApiStarGiftRegular;
|
||||
export type GiftOption = ApiPremiumGiftCodeOption | ApiStarGift;
|
||||
|
||||
type StateProps = {
|
||||
boostPerSentGift?: number;
|
||||
@ -56,6 +60,8 @@ type StateProps = {
|
||||
peer?: ApiPeer;
|
||||
isSelf?: boolean;
|
||||
disallowedGifts?: ApiDisallowedGifts;
|
||||
resaleGiftsCount?: number;
|
||||
areResaleGiftsLoading?: boolean;
|
||||
};
|
||||
|
||||
const AVATAR_SIZE = 100;
|
||||
@ -72,9 +78,11 @@ const GiftModal: FC<OwnProps & StateProps> = ({
|
||||
peer,
|
||||
isSelf,
|
||||
disallowedGifts,
|
||||
resaleGiftsCount,
|
||||
areResaleGiftsLoading,
|
||||
}) => {
|
||||
const {
|
||||
closeGiftModal,
|
||||
closeGiftModal, openGiftInfoModal, resetResaleGifts, loadResaleGifts,
|
||||
} = getActions();
|
||||
const dialogRef = useRef<HTMLDivElement>();
|
||||
const transitionRef = useRef<HTMLDivElement>();
|
||||
@ -89,6 +97,7 @@ const GiftModal: FC<OwnProps & StateProps> = ({
|
||||
const chat = peer && isApiPeerChat(peer) ? peer : undefined;
|
||||
|
||||
const [selectedGift, setSelectedGift] = useState<GiftOption | undefined>();
|
||||
const [selectedResellGift, setSelectedResellGift] = useState<ApiStarGift | undefined>();
|
||||
const [shouldShowMainScreenHeader, setShouldShowMainScreenHeader] = useState(false);
|
||||
const [isMainScreenHeaderForStarGifts, setIsMainScreenHeaderForStarGifts] = useState(false);
|
||||
const [isGiftScreenHeaderForStarGifts, setIsGiftScreenHeaderForStarGifts] = useState(false);
|
||||
@ -143,14 +152,22 @@ const GiftModal: FC<OwnProps & StateProps> = ({
|
||||
observe: observeIntersection,
|
||||
} = useIntersectionObserver({ rootRef: scrollerRef, throttleMs: INTERSECTION_THROTTLE, isDisabled: !isOpen });
|
||||
|
||||
const isResaleScreen = Boolean(selectedResellGift) && !selectedGift;
|
||||
const isGiftScreen = Boolean(selectedGift);
|
||||
const shouldShowHeader = isGiftScreen || shouldShowMainScreenHeader;
|
||||
const shouldShowHeader = isResaleScreen || isGiftScreen || shouldShowMainScreenHeader;
|
||||
const isHeaderForStarGifts = isGiftScreen ? isGiftScreenHeaderForStarGifts : isMainScreenHeaderForStarGifts;
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedResellGift) {
|
||||
loadResaleGifts({ giftId: selectedResellGift.id });
|
||||
}
|
||||
}, [selectedResellGift]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setShouldShowMainScreenHeader(false);
|
||||
setSelectedGift(undefined);
|
||||
setSelectedResellGift(undefined);
|
||||
setSelectedCategory('all');
|
||||
}
|
||||
}, [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);
|
||||
setIsGiftScreenHeaderForStarGifts('id' in gift);
|
||||
});
|
||||
@ -254,16 +282,34 @@ const GiftModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
return (
|
||||
<div className={styles.starGiftsContainer}>
|
||||
{starGiftsById && filteredGiftIds?.map((giftId) => {
|
||||
{starGiftsById && filteredGiftIds?.flatMap((giftId) => {
|
||||
const gift = starGiftsById[giftId];
|
||||
return (
|
||||
const shouldShowResale = selectedCategory !== 'stock' && Boolean(gift.availabilityResale);
|
||||
const shouldDuplicateAsResale = selectedCategory !== 'resale' && shouldShowResale && !gift.isSoldOut;
|
||||
|
||||
const elements = [
|
||||
<GiftItemStar
|
||||
key={giftId}
|
||||
gift={gift}
|
||||
observeIntersection={observeIntersection}
|
||||
isResale={shouldShowResale && !shouldDuplicateAsResale}
|
||||
onClick={handleGiftClick}
|
||||
/>
|
||||
);
|
||||
/>,
|
||||
];
|
||||
|
||||
if (shouldDuplicateAsResale) {
|
||||
elements.push(
|
||||
<GiftItemStar
|
||||
key={`resale_${giftId}`}
|
||||
isResale
|
||||
gift={gift}
|
||||
observeIntersection={observeIntersection}
|
||||
onClick={handleGiftClick}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
return elements;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
@ -290,12 +336,24 @@ const GiftModal: FC<OwnProps & StateProps> = ({
|
||||
setSelectedCategory(category);
|
||||
});
|
||||
|
||||
const handleCloseModal = useLastCallback(() => {
|
||||
setSelectedGift(undefined);
|
||||
setSelectedResellGift(undefined);
|
||||
resetResaleGifts();
|
||||
closeGiftModal();
|
||||
});
|
||||
|
||||
const handleCloseButtonClick = useLastCallback(() => {
|
||||
if (isResaleScreen) {
|
||||
setSelectedResellGift(undefined);
|
||||
resetResaleGifts();
|
||||
return;
|
||||
}
|
||||
if (isGiftScreen) {
|
||||
setSelectedGift(undefined);
|
||||
return;
|
||||
}
|
||||
closeGiftModal();
|
||||
handleCloseModal();
|
||||
});
|
||||
|
||||
function renderMainScreen() {
|
||||
@ -337,17 +395,51 @@ const GiftModal: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const isBackButton = isGiftScreen;
|
||||
const isBackButton = isGiftScreen || isResaleScreen;
|
||||
|
||||
const buttonClassName = buildClassName(
|
||||
'animated-close-icon',
|
||||
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 (
|
||||
<Modal
|
||||
dialogRef={dialogRef}
|
||||
onClose={closeGiftModal}
|
||||
onClose={handleCloseModal}
|
||||
isOpen={isOpen}
|
||||
isSlim
|
||||
contentClassName={styles.content}
|
||||
@ -365,24 +457,32 @@ const GiftModal: FC<OwnProps & StateProps> = ({
|
||||
<div className={buttonClassName} />
|
||||
</Button>
|
||||
<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
|
||||
name="slideVerticalFade"
|
||||
activeKey={Number(isHeaderForStarGifts)}
|
||||
activeKey={!shouldShowHeader ? 0 : isResaleScreen ? 1 : isHeaderForStarGifts ? 2 : 3}
|
||||
slideClassName={styles.headerSlide}
|
||||
>
|
||||
<h2 className={styles.commonHeaderText}>
|
||||
{lang(isHeaderForStarGifts ? (isSelf ? 'StarsGiftHeaderSelf' : 'StarsGiftHeader') : 'GiftPremiumHeader')}
|
||||
</h2>
|
||||
{renderHeader()}
|
||||
</Transition>
|
||||
</div>
|
||||
<Transition
|
||||
ref={transitionRef}
|
||||
className={styles.transition}
|
||||
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 && (
|
||||
<GiftSendingOptions
|
||||
gift={selectedGift}
|
||||
@ -406,6 +506,10 @@ export default memo(withGlobal<OwnProps>((global, { modal }): StateProps => {
|
||||
const isSelf = Boolean(currentUserId && modal?.forPeerId === currentUserId);
|
||||
const userFullInfo = peer ? selectUserFullInfo(global, peer?.id) : undefined;
|
||||
|
||||
const { resaleGifts } = selectTabState(global);
|
||||
const resaleGiftsCount = resaleGifts.count;
|
||||
const areResaleGiftsLoading = resaleGifts.isLoading !== false;
|
||||
|
||||
return {
|
||||
boostPerSentGift: global.appConfig?.boostsPerSentGift,
|
||||
starGiftsById: starGifts?.byId,
|
||||
@ -414,12 +518,15 @@ export default memo(withGlobal<OwnProps>((global, { modal }): StateProps => {
|
||||
peer,
|
||||
isSelf,
|
||||
disallowedGifts: userFullInfo?.disallowedGifts,
|
||||
resaleGiftsCount,
|
||||
areResaleGiftsLoading,
|
||||
};
|
||||
})(GiftModal));
|
||||
|
||||
function getCategoryKey(category: StarGiftCategory) {
|
||||
if (category === 'all') return -2;
|
||||
if (category === 'limited') return -1;
|
||||
if (category === 'stock') return 0;
|
||||
return category;
|
||||
if (category === 'all') return 0;
|
||||
if (category === 'limited') return 1;
|
||||
if (category === 'resale') return 2;
|
||||
if (category === 'stock') return 3;
|
||||
return category + 3;
|
||||
}
|
||||
|
||||
171
src/components/modals/gift/GiftModalResaleScreen.tsx
Normal file
171
src/components/modals/gift/GiftModalResaleScreen.tsx
Normal 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));
|
||||
160
src/components/modals/gift/GiftResaleFilters.module.scss
Normal file
160
src/components/modals/gift/GiftResaleFilters.module.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
655
src/components/modals/gift/GiftResaleFilters.tsx
Normal file
655
src/components/modals/gift/GiftResaleFilters.tsx
Normal 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));
|
||||
@ -0,0 +1,7 @@
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
position: absolute;
|
||||
}
|
||||
@ -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));
|
||||
@ -37,6 +37,8 @@ const StarGiftCategoryList = ({
|
||||
.sort((a, b) => a - b),
|
||||
[idsByCategory]);
|
||||
|
||||
const hasResale = idsByCategory && idsByCategory['resale'].length > 0;
|
||||
|
||||
const [selectedCategory, setSelectedCategory] = useState<StarGiftCategory>('all');
|
||||
|
||||
function handleItemClick(category: StarGiftCategory) {
|
||||
@ -50,6 +52,7 @@ const StarGiftCategoryList = ({
|
||||
if (category === 'all') return lang('AllGiftsCategory');
|
||||
if (category === 'stock') return lang('StockGiftsCategory');
|
||||
if (category === 'limited') return lang('LimitedGiftsCategory');
|
||||
if (category === 'resale') return lang('GiftCategoryResale');
|
||||
return category;
|
||||
}
|
||||
|
||||
@ -80,6 +83,7 @@ const StarGiftCategoryList = ({
|
||||
<div ref={ref} className={buildClassName(styles.list, 'no-scrollbar')}>
|
||||
{renderCategoryItem('all')}
|
||||
{!areLimitedStarGiftsDisallowed && renderCategoryItem('limited')}
|
||||
{!areLimitedStarGiftsDisallowed && hasResale && renderCategoryItem('resale')}
|
||||
{renderCategoryItem('stock')}
|
||||
{starCategories?.map(renderCategoryItem)}
|
||||
</div>
|
||||
|
||||
@ -53,6 +53,7 @@
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.starAmountIcon,
|
||||
.giftResalePriceStar {
|
||||
margin-inline-start: 0 !important;
|
||||
}
|
||||
@ -121,10 +122,6 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.starAmountIcon {
|
||||
margin-inline-start: 0 !important;
|
||||
}
|
||||
|
||||
.modalHeader {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
|
||||
@ -58,6 +58,7 @@ type StateProps = {
|
||||
collectibleEmojiStatuses?: ApiEmojiStatusType[];
|
||||
tonExplorerUrl?: string;
|
||||
currentUser?: ApiUser;
|
||||
recipientPeer?: ApiPeer;
|
||||
};
|
||||
|
||||
const STICKER_SIZE = 120;
|
||||
@ -73,6 +74,7 @@ const GiftInfoModal = ({
|
||||
collectibleEmojiStatuses,
|
||||
tonExplorerUrl,
|
||||
currentUser,
|
||||
recipientPeer,
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
closeGiftInfoModal,
|
||||
@ -83,6 +85,7 @@ const GiftInfoModal = ({
|
||||
openGiftUpgradeModal,
|
||||
showNotification,
|
||||
buyStarGift,
|
||||
closeGiftModal,
|
||||
} = getActions();
|
||||
|
||||
const [isConvertConfirmOpen, openConvertConfirm, closeConvertConfirm] = useFlag();
|
||||
@ -161,7 +164,7 @@ const GiftInfoModal = ({
|
||||
});
|
||||
|
||||
const handleBuyGift = useLastCallback(() => {
|
||||
if (!savedGift || gift?.type !== 'starGiftUnique' || !gift.resellPriceInStars) return;
|
||||
if (gift?.type !== 'starGiftUnique' || !gift.resellPriceInStars) return;
|
||||
setIsConfirmModalOpen(true);
|
||||
});
|
||||
|
||||
@ -170,9 +173,11 @@ const GiftInfoModal = ({
|
||||
});
|
||||
|
||||
const handleConfirmBuyGift = useLastCallback(() => {
|
||||
if (!savedGift || gift?.type !== 'starGiftUnique' || !gift.resellPriceInStars) return;
|
||||
const peer = recipientPeer || currentUser;
|
||||
if (!peer || gift?.type !== 'starGiftUnique' || !gift.resellPriceInStars) return;
|
||||
closeConfirmModal();
|
||||
buyStarGift({ slug: gift.slug, stars: gift.resellPriceInStars });
|
||||
closeGiftModal();
|
||||
buyStarGift({ peerId: peer.id, slug: gift.slug, stars: gift.resellPriceInStars });
|
||||
});
|
||||
|
||||
const giftAttributes = useMemo(() => {
|
||||
@ -726,18 +731,34 @@ const GiftInfoModal = ({
|
||||
>
|
||||
|
||||
<GiftTransferPreview
|
||||
peer={currentUser}
|
||||
peer={recipientPeer || currentUser}
|
||||
gift={uniqueGift}
|
||||
/>
|
||||
<p>
|
||||
{lang('GiftBuyConfirmDescription', {
|
||||
gift: lang('GiftUnique', { title: uniqueGift.title, number: uniqueGift.number }),
|
||||
stars: formatStarsAsText(lang, resellPriceInStars),
|
||||
}, {
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
})}
|
||||
</p>
|
||||
{!recipientPeer
|
||||
&& (
|
||||
<p>
|
||||
{lang('GiftBuyConfirmDescription', {
|
||||
gift: lang('GiftUnique', { title: uniqueGift.title, number: uniqueGift.number }),
|
||||
stars: formatStarsAsText(lang, resellPriceInStars),
|
||||
}, {
|
||||
withNodes: true,
|
||||
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>
|
||||
)}
|
||||
{savedGift && (
|
||||
@ -786,6 +807,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
const chat = targetPeer && isApiPeerChat(targetPeer) ? targetPeer : undefined;
|
||||
const hasAdminRights = chat && getHasAdminRight(chat, 'postMessages');
|
||||
const currentUser = selectUser(global, currentUserId!);
|
||||
const recipientPeer = modal?.recipientId && currentUserId !== modal.recipientId
|
||||
? selectPeer(global, modal.recipientId) : undefined;
|
||||
const currentUserEmojiStatus = currentUser?.emojiStatus;
|
||||
const collectibleEmojiStatuses = global.collectibleEmojiStatuses?.statuses;
|
||||
|
||||
@ -799,6 +822,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
currentUserEmojiStatus,
|
||||
collectibleEmojiStatuses,
|
||||
currentUser,
|
||||
recipientPeer,
|
||||
};
|
||||
},
|
||||
)(GiftInfoModal));
|
||||
|
||||
@ -317,6 +317,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.bluredStarsBadge {
|
||||
color: var(--color-white);
|
||||
background: rgba(0, 0, 0, 0.2) !important;
|
||||
backdrop-filter: blur(50px);
|
||||
}
|
||||
|
||||
&.smaller {
|
||||
height: 2.75rem;
|
||||
padding: 0.3125rem;
|
||||
|
||||
@ -22,7 +22,7 @@ export type OwnProps = {
|
||||
size?: 'default' | 'smaller' | 'tiny';
|
||||
color?: (
|
||||
'primary' | 'secondary' | 'gray' | 'danger' | 'translucent' | 'translucent-white' | 'translucent-black'
|
||||
| 'translucent-bordered' | 'dark' | 'green' | 'adaptive' | 'stars'
|
||||
| 'translucent-bordered' | 'dark' | 'green' | 'adaptive' | 'stars' | 'bluredStarsBadge'
|
||||
);
|
||||
backgroundImage?: string;
|
||||
id?: string;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { MouseEvent as ReactMouseEvent } from 'react';
|
||||
import type { ElementRef, FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useRef,
|
||||
@ -43,7 +44,7 @@ type OwnProps = {
|
||||
onReset?: NoneToVoidFunction;
|
||||
onFocus?: NoneToVoidFunction;
|
||||
onBlur?: NoneToVoidFunction;
|
||||
onClick?: NoneToVoidFunction;
|
||||
onClick?: (e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
onUpClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onDownClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onSpinnerClick?: NoneToVoidFunction;
|
||||
|
||||
@ -3,6 +3,7 @@ import type {
|
||||
} from './api/types';
|
||||
import type {
|
||||
GiftProfileFilterOptions,
|
||||
ResaleGiftsFilterOptions,
|
||||
} from './types';
|
||||
|
||||
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 API_GENERAL_ID_LIMIT = 100;
|
||||
export const STATISTICS_PUBLIC_FORWARDS_LIMIT = 50;
|
||||
export const RESALE_GIFTS_LIMIT = 50;
|
||||
|
||||
export const STORY_VIEWS_MIN_SEARCH = 15;
|
||||
export const STORY_MIN_REACTIONS_SORT = 10;
|
||||
@ -465,3 +467,7 @@ export const DEFAULT_GIFT_PROFILE_FILTER_OPTIONS: GiftProfileFilterOptions = {
|
||||
shouldIncludeDisplayed: true,
|
||||
shouldIncludeHidden: true,
|
||||
} as const;
|
||||
|
||||
export const DEFAULT_RESALE_GIFTS_FILTER_OPTIONS: ResaleGiftsFilterOptions = {
|
||||
sortType: 'byDate',
|
||||
};
|
||||
|
||||
@ -146,13 +146,13 @@ addActionHandler('sendStarGift', (global, actions, payload): ActionReturnType =>
|
||||
|
||||
addActionHandler('buyStarGift', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
slug, stars, tabId = getCurrentTabId(),
|
||||
slug, peerId, stars, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
const inputInvoice: ApiInputInvoiceStarGiftResale = {
|
||||
type: 'stargiftResale',
|
||||
slug,
|
||||
peerId: global.currentUserId!,
|
||||
peerId,
|
||||
};
|
||||
|
||||
payInputStarInvoice(global, inputInvoice, stars, tabId);
|
||||
|
||||
@ -2,6 +2,7 @@ import type { ApiSavedStarGift, ApiStarGiftUnique } from '../../../api/types';
|
||||
import type { StarGiftCategory } from '../../../types';
|
||||
import type { ActionReturnType } from '../../types';
|
||||
|
||||
import { DEFAULT_RESALE_GIFTS_FILTER_OPTIONS, RESALE_GIFTS_LIMIT } from '../../../config';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
@ -11,8 +12,10 @@ import {
|
||||
appendStarsSubscriptions,
|
||||
appendStarsTransactions,
|
||||
replacePeerSavedGifts,
|
||||
updateChats,
|
||||
updateStarsBalance,
|
||||
updateStarsSubscriptionLoading,
|
||||
updateUsers,
|
||||
} from '../../reducers';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import {
|
||||
@ -102,6 +105,7 @@ addActionHandler('loadStarGifts', async (global): Promise<void> => {
|
||||
all: [],
|
||||
stock: [],
|
||||
limited: [],
|
||||
resale: [],
|
||||
};
|
||||
|
||||
const allStarGiftIds = Object.keys(byId);
|
||||
@ -114,9 +118,13 @@ addActionHandler('loadStarGifts', async (global): Promise<void> => {
|
||||
gift.availabilityRemains || !gift.availabilityTotal ? gift.id : undefined
|
||||
)).filter(Boolean);
|
||||
|
||||
const resaleStarGiftIds = allStarGifts.map((gift) => (gift.availabilityResale ? gift.id : undefined))
|
||||
.filter(Boolean);
|
||||
|
||||
idsByCategoryName.all = allStarGiftIds;
|
||||
idsByCategoryName.limited = limitedStarGiftIds;
|
||||
idsByCategoryName.stock = stockedStarGiftIds;
|
||||
idsByCategoryName.resale = resaleStarGiftIds;
|
||||
|
||||
allStarGifts.forEach((gift) => {
|
||||
const starsCategory = gift.stars;
|
||||
@ -137,6 +145,115 @@ addActionHandler('loadStarGifts', async (global): Promise<void> => {
|
||||
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> => {
|
||||
const {
|
||||
peerId, shouldRefresh, tabId = getCurrentTabId(),
|
||||
|
||||
@ -256,11 +256,13 @@ addActionHandler('openGiftInfoModal', (global, actions, payload): ActionReturnTy
|
||||
} = payload;
|
||||
|
||||
const peerId = 'peerId' in payload ? payload.peerId : undefined;
|
||||
const recipientId = 'recipientId' in payload ? payload.recipientId : undefined;
|
||||
|
||||
return updateTabState(global, {
|
||||
giftInfoModal: {
|
||||
peerId,
|
||||
gift,
|
||||
recipientId,
|
||||
},
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
DEFAULT_MESSAGE_TEXT_SIZE_PX,
|
||||
DEFAULT_PATTERN_COLOR,
|
||||
DEFAULT_PLAYBACK_RATE,
|
||||
DEFAULT_RESALE_GIFTS_FILTER_OPTIONS,
|
||||
DEFAULT_VOLUME,
|
||||
IOS_DEFAULT_MESSAGE_TEXT_SIZE_PX,
|
||||
MACOS_DEFAULT_MESSAGE_TEXT_SIZE_PX,
|
||||
@ -389,6 +390,15 @@ export const INITIAL_TAB_STATE: TabState = {
|
||||
giftsByPeerId: {},
|
||||
},
|
||||
|
||||
resaleGifts: {
|
||||
gifts: [],
|
||||
count: 0,
|
||||
updateIteration: 0,
|
||||
filter: {
|
||||
...DEFAULT_RESALE_GIFTS_FILTER_OPTIONS,
|
||||
},
|
||||
},
|
||||
|
||||
storyViewer: {
|
||||
isMuted: true,
|
||||
isRibbonShown: false,
|
||||
|
||||
@ -80,6 +80,7 @@ import type {
|
||||
PerformanceType,
|
||||
Point,
|
||||
ProfileTabType,
|
||||
ResaleGiftsFilterOptions,
|
||||
ScrollTargetPosition,
|
||||
SendMessageParams,
|
||||
SettingsScreens,
|
||||
@ -2394,6 +2395,14 @@ export interface ActionPayloads {
|
||||
|
||||
loadPremiumGifts: undefined;
|
||||
loadStarGifts: undefined;
|
||||
updateResaleGiftsFilter: {
|
||||
filter: ResaleGiftsFilterOptions;
|
||||
} & WithTabId;
|
||||
loadResaleGifts: {
|
||||
giftId: string;
|
||||
shouldRefresh?: boolean;
|
||||
} & WithTabId;
|
||||
resetResaleGifts: WithTabId | undefined;
|
||||
loadDefaultTopicIcons: undefined;
|
||||
loadPremiumStickers: undefined;
|
||||
|
||||
@ -2403,6 +2412,7 @@ export interface ActionPayloads {
|
||||
closeGiftModal: WithTabId | undefined;
|
||||
sendStarGift: StarGiftInfo & WithTabId;
|
||||
buyStarGift: {
|
||||
peerId: string;
|
||||
slug: string;
|
||||
stars: number;
|
||||
} & WithTabId;
|
||||
@ -2419,6 +2429,7 @@ export interface ActionPayloads {
|
||||
} & WithTabId;
|
||||
openGiftInfoModal: ({
|
||||
peerId: string;
|
||||
recipientId?: string;
|
||||
gift: ApiSavedStarGift;
|
||||
} | {
|
||||
gift: ApiStarGift;
|
||||
|
||||
@ -38,6 +38,7 @@ import type {
|
||||
ApiSponsoredPeer,
|
||||
ApiStarGift,
|
||||
ApiStarGiftAttribute,
|
||||
ApiStarGiftAttributeCounter,
|
||||
ApiStarGiveawayOption,
|
||||
ApiStarsSubscription,
|
||||
ApiStarsTransaction,
|
||||
@ -76,6 +77,7 @@ import type {
|
||||
PaymentStep,
|
||||
ProfileEditProgress,
|
||||
ProfileTabType,
|
||||
ResaleGiftsFilterOptions,
|
||||
ScrollTargetPosition,
|
||||
SettingsScreens,
|
||||
SharedMediaType,
|
||||
@ -217,6 +219,20 @@ export type TabState = {
|
||||
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: {
|
||||
contentKey: LeftColumnContent;
|
||||
settingsScreen: SettingsScreens;
|
||||
@ -761,6 +777,7 @@ export type TabState = {
|
||||
|
||||
giftInfoModal?: {
|
||||
peerId?: string;
|
||||
recipientId?: string;
|
||||
gift: ApiSavedStarGift | ApiStarGift;
|
||||
};
|
||||
|
||||
|
||||
@ -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.getStarGiftWithdrawalUrl#d06e93a8 stargift:InputSavedStarGift password:InputCheckPasswordSRP = payments.StarGiftWithdrawalUrl;
|
||||
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;
|
||||
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;
|
||||
|
||||
@ -319,6 +319,7 @@
|
||||
"payments.getUniqueStarGift",
|
||||
"payments.getStarGiftWithdrawalUrl",
|
||||
"payments.toggleStarGiftsPinnedToTop",
|
||||
"payments.getResaleStarGifts",
|
||||
"payments.updateStarGiftPrice",
|
||||
"langpack.getLangPack",
|
||||
"langpack.getStrings",
|
||||
|
||||
@ -109,193 +109,197 @@ $icons-map: (
|
||||
"double-badge": "\f148",
|
||||
"down": "\f149",
|
||||
"download": "\f14a",
|
||||
"eats": "\f14b",
|
||||
"edit": "\f14c",
|
||||
"email": "\f14d",
|
||||
"enter": "\f14e",
|
||||
"expand-modal": "\f14f",
|
||||
"expand": "\f150",
|
||||
"eye-crossed-outline": "\f151",
|
||||
"eye-crossed": "\f152",
|
||||
"eye-outline": "\f153",
|
||||
"eye": "\f154",
|
||||
"favorite-filled": "\f155",
|
||||
"favorite": "\f156",
|
||||
"file-badge": "\f157",
|
||||
"flag": "\f158",
|
||||
"folder-badge": "\f159",
|
||||
"folder": "\f15a",
|
||||
"fontsize": "\f15b",
|
||||
"forums": "\f15c",
|
||||
"forward": "\f15d",
|
||||
"fragment": "\f15e",
|
||||
"frozen-time": "\f15f",
|
||||
"fullscreen": "\f160",
|
||||
"gifs": "\f161",
|
||||
"gift": "\f162",
|
||||
"group-filled": "\f163",
|
||||
"group": "\f164",
|
||||
"grouped-disable": "\f165",
|
||||
"grouped": "\f166",
|
||||
"hand-stop": "\f167",
|
||||
"hashtag": "\f168",
|
||||
"heart-outline": "\f169",
|
||||
"heart": "\f16a",
|
||||
"help": "\f16b",
|
||||
"info-filled": "\f16c",
|
||||
"info": "\f16d",
|
||||
"install": "\f16e",
|
||||
"italic": "\f16f",
|
||||
"key": "\f170",
|
||||
"keyboard": "\f171",
|
||||
"lamp": "\f172",
|
||||
"language": "\f173",
|
||||
"large-pause": "\f174",
|
||||
"large-play": "\f175",
|
||||
"link-badge": "\f176",
|
||||
"link-broken": "\f177",
|
||||
"link": "\f178",
|
||||
"location": "\f179",
|
||||
"lock-badge": "\f17a",
|
||||
"lock": "\f17b",
|
||||
"logout": "\f17c",
|
||||
"loop": "\f17d",
|
||||
"mention": "\f17e",
|
||||
"message-failed": "\f17f",
|
||||
"message-pending": "\f180",
|
||||
"message-read": "\f181",
|
||||
"message-succeeded": "\f182",
|
||||
"message": "\f183",
|
||||
"microphone-alt": "\f184",
|
||||
"microphone": "\f185",
|
||||
"monospace": "\f186",
|
||||
"more-circle": "\f187",
|
||||
"more": "\f188",
|
||||
"move-caption-down": "\f189",
|
||||
"move-caption-up": "\f18a",
|
||||
"mute": "\f18b",
|
||||
"muted": "\f18c",
|
||||
"my-notes": "\f18d",
|
||||
"new-chat-filled": "\f18e",
|
||||
"next": "\f18f",
|
||||
"nochannel": "\f190",
|
||||
"noise-suppression": "\f191",
|
||||
"non-contacts": "\f192",
|
||||
"one-filled": "\f193",
|
||||
"open-in-new-tab": "\f194",
|
||||
"password-off": "\f195",
|
||||
"pause": "\f196",
|
||||
"permissions": "\f197",
|
||||
"phone-discard-outline": "\f198",
|
||||
"phone-discard": "\f199",
|
||||
"phone": "\f19a",
|
||||
"photo": "\f19b",
|
||||
"pin-badge": "\f19c",
|
||||
"pin-list": "\f19d",
|
||||
"pin": "\f19e",
|
||||
"pinned-chat": "\f19f",
|
||||
"pinned-message": "\f1a0",
|
||||
"pip": "\f1a1",
|
||||
"play-story": "\f1a2",
|
||||
"play": "\f1a3",
|
||||
"poll": "\f1a4",
|
||||
"previous": "\f1a5",
|
||||
"privacy-policy": "\f1a6",
|
||||
"proof-of-ownership": "\f1a7",
|
||||
"quote-text": "\f1a8",
|
||||
"quote": "\f1a9",
|
||||
"radial-badge": "\f1aa",
|
||||
"readchats": "\f1ab",
|
||||
"recent": "\f1ac",
|
||||
"reload": "\f1ad",
|
||||
"remove-quote": "\f1ae",
|
||||
"remove": "\f1af",
|
||||
"reopen-topic": "\f1b0",
|
||||
"replace": "\f1b1",
|
||||
"replies": "\f1b2",
|
||||
"reply-filled": "\f1b3",
|
||||
"reply": "\f1b4",
|
||||
"revenue-split": "\f1b5",
|
||||
"revote": "\f1b6",
|
||||
"save-story": "\f1b7",
|
||||
"saved-messages": "\f1b8",
|
||||
"schedule": "\f1b9",
|
||||
"search": "\f1ba",
|
||||
"select": "\f1bb",
|
||||
"sell-outline": "\f1bc",
|
||||
"sell": "\f1bd",
|
||||
"send-outline": "\f1be",
|
||||
"send": "\f1bf",
|
||||
"settings-filled": "\f1c0",
|
||||
"settings": "\f1c1",
|
||||
"share-filled": "\f1c2",
|
||||
"share-screen-outlined": "\f1c3",
|
||||
"share-screen-stop": "\f1c4",
|
||||
"share-screen": "\f1c5",
|
||||
"show-message": "\f1c6",
|
||||
"sidebar": "\f1c7",
|
||||
"skip-next": "\f1c8",
|
||||
"skip-previous": "\f1c9",
|
||||
"smallscreen": "\f1ca",
|
||||
"smile": "\f1cb",
|
||||
"sort": "\f1cc",
|
||||
"speaker-muted-story": "\f1cd",
|
||||
"speaker-outline": "\f1ce",
|
||||
"speaker-story": "\f1cf",
|
||||
"speaker": "\f1d0",
|
||||
"spoiler-disable": "\f1d1",
|
||||
"spoiler": "\f1d2",
|
||||
"sport": "\f1d3",
|
||||
"star": "\f1d4",
|
||||
"stars-lock": "\f1d5",
|
||||
"stats": "\f1d6",
|
||||
"stealth-future": "\f1d7",
|
||||
"stealth-past": "\f1d8",
|
||||
"stickers": "\f1d9",
|
||||
"stop-raising-hand": "\f1da",
|
||||
"stop": "\f1db",
|
||||
"story-caption": "\f1dc",
|
||||
"story-expired": "\f1dd",
|
||||
"story-priority": "\f1de",
|
||||
"story-reply": "\f1df",
|
||||
"strikethrough": "\f1e0",
|
||||
"tag-add": "\f1e1",
|
||||
"tag-crossed": "\f1e2",
|
||||
"tag-filter": "\f1e3",
|
||||
"tag-name": "\f1e4",
|
||||
"tag": "\f1e5",
|
||||
"timer": "\f1e6",
|
||||
"toncoin": "\f1e7",
|
||||
"trade": "\f1e8",
|
||||
"transcribe": "\f1e9",
|
||||
"truck": "\f1ea",
|
||||
"unarchive": "\f1eb",
|
||||
"underlined": "\f1ec",
|
||||
"unique-profile": "\f1ed",
|
||||
"unlist-outline": "\f1ee",
|
||||
"unlist": "\f1ef",
|
||||
"unlock-badge": "\f1f0",
|
||||
"unlock": "\f1f1",
|
||||
"unmute": "\f1f2",
|
||||
"unpin": "\f1f3",
|
||||
"unread": "\f1f4",
|
||||
"up": "\f1f5",
|
||||
"user-filled": "\f1f6",
|
||||
"user-online": "\f1f7",
|
||||
"user": "\f1f8",
|
||||
"video-outlined": "\f1f9",
|
||||
"video-stop": "\f1fa",
|
||||
"video": "\f1fb",
|
||||
"view-once": "\f1fc",
|
||||
"voice-chat": "\f1fd",
|
||||
"volume-1": "\f1fe",
|
||||
"volume-2": "\f1ff",
|
||||
"volume-3": "\f200",
|
||||
"web": "\f201",
|
||||
"webapp": "\f202",
|
||||
"word-wrap": "\f203",
|
||||
"zoom-in": "\f204",
|
||||
"zoom-out": "\f205",
|
||||
"dropdown-arrows": "\f14b",
|
||||
"eats": "\f14c",
|
||||
"edit": "\f14d",
|
||||
"email": "\f14e",
|
||||
"enter": "\f14f",
|
||||
"expand-modal": "\f150",
|
||||
"expand": "\f151",
|
||||
"eye-crossed-outline": "\f152",
|
||||
"eye-crossed": "\f153",
|
||||
"eye-outline": "\f154",
|
||||
"eye": "\f155",
|
||||
"favorite-filled": "\f156",
|
||||
"favorite": "\f157",
|
||||
"file-badge": "\f158",
|
||||
"flag": "\f159",
|
||||
"folder-badge": "\f15a",
|
||||
"folder": "\f15b",
|
||||
"fontsize": "\f15c",
|
||||
"forums": "\f15d",
|
||||
"forward": "\f15e",
|
||||
"fragment": "\f15f",
|
||||
"frozen-time": "\f160",
|
||||
"fullscreen": "\f161",
|
||||
"gifs": "\f162",
|
||||
"gift": "\f163",
|
||||
"group-filled": "\f164",
|
||||
"group": "\f165",
|
||||
"grouped-disable": "\f166",
|
||||
"grouped": "\f167",
|
||||
"hand-stop": "\f168",
|
||||
"hashtag": "\f169",
|
||||
"heart-outline": "\f16a",
|
||||
"heart": "\f16b",
|
||||
"help": "\f16c",
|
||||
"info-filled": "\f16d",
|
||||
"info": "\f16e",
|
||||
"install": "\f16f",
|
||||
"italic": "\f170",
|
||||
"key": "\f171",
|
||||
"keyboard": "\f172",
|
||||
"lamp": "\f173",
|
||||
"language": "\f174",
|
||||
"large-pause": "\f175",
|
||||
"large-play": "\f176",
|
||||
"link-badge": "\f177",
|
||||
"link-broken": "\f178",
|
||||
"link": "\f179",
|
||||
"location": "\f17a",
|
||||
"lock-badge": "\f17b",
|
||||
"lock": "\f17c",
|
||||
"logout": "\f17d",
|
||||
"loop": "\f17e",
|
||||
"mention": "\f17f",
|
||||
"message-failed": "\f180",
|
||||
"message-pending": "\f181",
|
||||
"message-read": "\f182",
|
||||
"message-succeeded": "\f183",
|
||||
"message": "\f184",
|
||||
"microphone-alt": "\f185",
|
||||
"microphone": "\f186",
|
||||
"monospace": "\f187",
|
||||
"more-circle": "\f188",
|
||||
"more": "\f189",
|
||||
"move-caption-down": "\f18a",
|
||||
"move-caption-up": "\f18b",
|
||||
"mute": "\f18c",
|
||||
"muted": "\f18d",
|
||||
"my-notes": "\f18e",
|
||||
"new-chat-filled": "\f18f",
|
||||
"next": "\f190",
|
||||
"nochannel": "\f191",
|
||||
"noise-suppression": "\f192",
|
||||
"non-contacts": "\f193",
|
||||
"one-filled": "\f194",
|
||||
"open-in-new-tab": "\f195",
|
||||
"password-off": "\f196",
|
||||
"pause": "\f197",
|
||||
"permissions": "\f198",
|
||||
"phone-discard-outline": "\f199",
|
||||
"phone-discard": "\f19a",
|
||||
"phone": "\f19b",
|
||||
"photo": "\f19c",
|
||||
"pin-badge": "\f19d",
|
||||
"pin-list": "\f19e",
|
||||
"pin": "\f19f",
|
||||
"pinned-chat": "\f1a0",
|
||||
"pinned-message": "\f1a1",
|
||||
"pip": "\f1a2",
|
||||
"play-story": "\f1a3",
|
||||
"play": "\f1a4",
|
||||
"poll": "\f1a5",
|
||||
"previous": "\f1a6",
|
||||
"privacy-policy": "\f1a7",
|
||||
"proof-of-ownership": "\f1a8",
|
||||
"quote-text": "\f1a9",
|
||||
"quote": "\f1aa",
|
||||
"radial-badge": "\f1ab",
|
||||
"readchats": "\f1ac",
|
||||
"recent": "\f1ad",
|
||||
"reload": "\f1ae",
|
||||
"remove-quote": "\f1af",
|
||||
"remove": "\f1b0",
|
||||
"reopen-topic": "\f1b1",
|
||||
"replace": "\f1b2",
|
||||
"replies": "\f1b3",
|
||||
"reply-filled": "\f1b4",
|
||||
"reply": "\f1b5",
|
||||
"revenue-split": "\f1b6",
|
||||
"revote": "\f1b7",
|
||||
"save-story": "\f1b8",
|
||||
"saved-messages": "\f1b9",
|
||||
"schedule": "\f1ba",
|
||||
"search": "\f1bb",
|
||||
"select": "\f1bc",
|
||||
"sell-outline": "\f1bd",
|
||||
"sell": "\f1be",
|
||||
"send-outline": "\f1bf",
|
||||
"send": "\f1c0",
|
||||
"settings-filled": "\f1c1",
|
||||
"settings": "\f1c2",
|
||||
"share-filled": "\f1c3",
|
||||
"share-screen-outlined": "\f1c4",
|
||||
"share-screen-stop": "\f1c5",
|
||||
"share-screen": "\f1c6",
|
||||
"show-message": "\f1c7",
|
||||
"sidebar": "\f1c8",
|
||||
"skip-next": "\f1c9",
|
||||
"skip-previous": "\f1ca",
|
||||
"smallscreen": "\f1cb",
|
||||
"smile": "\f1cc",
|
||||
"sort-by-date": "\f1cd",
|
||||
"sort-by-number": "\f1ce",
|
||||
"sort-by-price": "\f1cf",
|
||||
"sort": "\f1d0",
|
||||
"speaker-muted-story": "\f1d1",
|
||||
"speaker-outline": "\f1d2",
|
||||
"speaker-story": "\f1d3",
|
||||
"speaker": "\f1d4",
|
||||
"spoiler-disable": "\f1d5",
|
||||
"spoiler": "\f1d6",
|
||||
"sport": "\f1d7",
|
||||
"star": "\f1d8",
|
||||
"stars-lock": "\f1d9",
|
||||
"stats": "\f1da",
|
||||
"stealth-future": "\f1db",
|
||||
"stealth-past": "\f1dc",
|
||||
"stickers": "\f1dd",
|
||||
"stop-raising-hand": "\f1de",
|
||||
"stop": "\f1df",
|
||||
"story-caption": "\f1e0",
|
||||
"story-expired": "\f1e1",
|
||||
"story-priority": "\f1e2",
|
||||
"story-reply": "\f1e3",
|
||||
"strikethrough": "\f1e4",
|
||||
"tag-add": "\f1e5",
|
||||
"tag-crossed": "\f1e6",
|
||||
"tag-filter": "\f1e7",
|
||||
"tag-name": "\f1e8",
|
||||
"tag": "\f1e9",
|
||||
"timer": "\f1ea",
|
||||
"toncoin": "\f1eb",
|
||||
"trade": "\f1ec",
|
||||
"transcribe": "\f1ed",
|
||||
"truck": "\f1ee",
|
||||
"unarchive": "\f1ef",
|
||||
"underlined": "\f1f0",
|
||||
"unique-profile": "\f1f1",
|
||||
"unlist-outline": "\f1f2",
|
||||
"unlist": "\f1f3",
|
||||
"unlock-badge": "\f1f4",
|
||||
"unlock": "\f1f5",
|
||||
"unmute": "\f1f6",
|
||||
"unpin": "\f1f7",
|
||||
"unread": "\f1f8",
|
||||
"up": "\f1f9",
|
||||
"user-filled": "\f1fa",
|
||||
"user-online": "\f1fb",
|
||||
"user": "\f1fc",
|
||||
"video-outlined": "\f1fd",
|
||||
"video-stop": "\f1fe",
|
||||
"video": "\f1ff",
|
||||
"view-once": "\f200",
|
||||
"voice-chat": "\f201",
|
||||
"volume-1": "\f202",
|
||||
"volume-2": "\f203",
|
||||
"volume-3": "\f204",
|
||||
"web": "\f205",
|
||||
"webapp": "\f206",
|
||||
"word-wrap": "\f207",
|
||||
"zoom-in": "\f208",
|
||||
"zoom-out": "\f209",
|
||||
);
|
||||
|
||||
.icon-active-sessions::before {
|
||||
@ -520,6 +524,9 @@ $icons-map: (
|
||||
.icon-download::before {
|
||||
content: map.get($icons-map, "download");
|
||||
}
|
||||
.icon-dropdown-arrows::before {
|
||||
content: map.get($icons-map, "dropdown-arrows");
|
||||
}
|
||||
.icon-eats::before {
|
||||
content: map.get($icons-map, "eats");
|
||||
}
|
||||
@ -907,6 +914,15 @@ $icons-map: (
|
||||
.icon-smile::before {
|
||||
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 {
|
||||
content: map.get($icons-map, "sort");
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -73,6 +73,7 @@ export type FontIconName =
|
||||
| 'double-badge'
|
||||
| 'down'
|
||||
| 'download'
|
||||
| 'dropdown-arrows'
|
||||
| 'eats'
|
||||
| 'edit'
|
||||
| 'email'
|
||||
@ -202,6 +203,9 @@ export type FontIconName =
|
||||
| 'skip-previous'
|
||||
| 'smallscreen'
|
||||
| 'smile'
|
||||
| 'sort-by-date'
|
||||
| 'sort-by-number'
|
||||
| 'sort-by-price'
|
||||
| 'sort'
|
||||
| 'speaker-muted-story'
|
||||
| 'speaker-outline'
|
||||
|
||||
@ -26,6 +26,8 @@ import type {
|
||||
ApiPhoto,
|
||||
ApiReaction,
|
||||
ApiReactionWithPaid,
|
||||
ApiStarGiftAttributeIdBackdrop,
|
||||
ApiStarGiftAttributeIdPattern,
|
||||
ApiStarGiftRegular,
|
||||
ApiStarsSubscription,
|
||||
ApiStarsTransaction,
|
||||
@ -37,6 +39,7 @@ import type {
|
||||
ApiTopic,
|
||||
ApiTypingStatus,
|
||||
ApiVideo,
|
||||
StarGiftAttributeIdModel,
|
||||
} from '../api/types';
|
||||
import type { DC_IDS } from '../config';
|
||||
import type { SearchResultKey } from '../util/keys/searchResultKey';
|
||||
@ -687,7 +690,7 @@ export interface Point {
|
||||
|
||||
export type WebPageMediaSize = 'large' | 'small';
|
||||
|
||||
export type StarGiftCategory = number | 'all' | 'limited' | 'stock';
|
||||
export type StarGiftCategory = number | 'all' | 'limited' | 'stock' | 'resale';
|
||||
|
||||
export type CallSound = (
|
||||
'join' | 'allowTalk' | 'leave' | 'connecting' | 'incoming' | 'end' | 'connect' | 'busy' | 'ringing'
|
||||
@ -705,6 +708,13 @@ export type GiftProfileFilterOptions = {
|
||||
shouldIncludeDisplayed: boolean;
|
||||
shouldIncludeHidden: boolean;
|
||||
};
|
||||
export type ResaleGiftsSortType = 'byDate' | 'byPrice' | 'byNumber';
|
||||
export type ResaleGiftsFilterOptions = {
|
||||
sortType: ResaleGiftsSortType;
|
||||
modelAttributes?: StarGiftAttributeIdModel[];
|
||||
patternAttributes?: ApiStarGiftAttributeIdPattern[];
|
||||
backdropAttributes?: ApiStarGiftAttributeIdBackdrop[];
|
||||
};
|
||||
|
||||
export type SendMessageParams = {
|
||||
chat?: ApiChat;
|
||||
|
||||
28
src/types/language.d.ts
vendored
28
src/types/language.d.ts
vendored
@ -1514,6 +1514,17 @@ export interface LangPair {
|
||||
'StarGiftSaleTransaction': undefined;
|
||||
'StarGiftPurchaseTransaction': 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> {
|
||||
@ -2464,6 +2475,11 @@ export interface LangPairWithVariables<V = LangVariable> {
|
||||
'gift': V;
|
||||
'stars': V;
|
||||
};
|
||||
'GiftBuyForPeerConfirmDescription': {
|
||||
'gift': V;
|
||||
'stars': V;
|
||||
'peer': V;
|
||||
};
|
||||
'ComposerTitleForwardFrom': {
|
||||
'users': V;
|
||||
};
|
||||
@ -2758,6 +2774,18 @@ export interface LangPairPluralWithVariables<V = LangVariable> {
|
||||
'PaidMessageTransaction': {
|
||||
'count': V;
|
||||
};
|
||||
'HeaderDescriptionResaleGifts': {
|
||||
'count': V;
|
||||
};
|
||||
'GiftAttributeModelPlural': {
|
||||
'count': V;
|
||||
};
|
||||
'GiftAttributeBackdropPlural': {
|
||||
'count': V;
|
||||
};
|
||||
'GiftAttributeSymbolPlural': {
|
||||
'count': V;
|
||||
};
|
||||
}
|
||||
export type RegularLangKey = keyof LangPair;
|
||||
export type RegularLangKeyWithVariables = keyof LangPairWithVariables;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user