Star gift: Support auction (#6541)

This commit is contained in:
Alexander Zinchuk 2025-12-23 12:50:30 +01:00
parent ef773026c0
commit 38d7cd0c5a
84 changed files with 4101 additions and 847 deletions

View File

@ -1,6 +1,7 @@
import { Api as GramJs } from '../../../lib/gramjs';
import type {
ApiAuctionBidLevel,
ApiDisallowedGiftsSettings,
ApiInputSavedStarGift,
ApiSavedStarGift,
@ -8,10 +9,14 @@ import type {
ApiStarGiftAttribute,
ApiStarGiftAttributeCounter,
ApiStarGiftAttributeId,
ApiStarGiftAuctionAcquiredGift,
ApiStarGiftAuctionState,
ApiStarGiftAuctionUserState,
ApiStarGiftCollection,
ApiStarGiftUpgradePreview,
ApiStarGiftUpgradePrice,
ApiTypeResaleStarGifts,
ApiTypeStarGiftAuctionState,
} from '../../types';
import { int2hex } from '../../../util/colors';
@ -20,6 +25,7 @@ import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { addDocumentToLocalDb } from '../helpers/localDb';
import { buildApiFormattedText } from './common';
import { buildApiCurrencyAmount } from './payments';
import { buildApiPeerId } from './peers';
import { getApiChatIdFromMtpPeer } from './peers';
import { buildStickerFromDocument } from './symbols';
import { buildApiUser } from './users';
@ -57,7 +63,7 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
const {
id, limited, stars, availabilityRemains, availabilityTotal, convertStars, firstSaleDate, lastSaleDate, soldOut,
birthday, upgradeStars, resellMinStars, title, availabilityResale, releasedBy,
requirePremium, limitedPerUser, perUserTotal, perUserRemains, lockedUntilDate,
requirePremium, limitedPerUser, perUserTotal, perUserRemains, lockedUntilDate, auction, giftsPerRound, background,
} = starGift;
addDocumentToLocalDb(starGift.sticker);
@ -87,6 +93,13 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
perUserTotal,
perUserRemains,
lockedUntilDate,
isAuction: auction,
giftsPerRound,
background: background ? {
centerColor: int2hex(background.centerColor),
edgeColor: int2hex(background.edgeColor),
textColor: int2hex(background.textColor),
} : undefined,
};
}
@ -159,8 +172,9 @@ export function buildApiStarGiftAttribute(attribute: GramJs.TypeStarGiftAttribut
export function buildApiSavedStarGift(userStarGift: GramJs.SavedStarGift, peerId: string): ApiSavedStarGift {
const {
gift, date, convertStars, fromId, message, msgId, nameHidden, unsaved, upgradeStars, transferStars, canUpgrade,
savedId, canExportAt, pinnedToTop, canResellAt, canTransferAt, prepaidUpgradeHash, dropOriginalDetailsStars,
gift, date, convertStars, fromId, message, msgId, nameHidden, unsaved, refunded, upgradeStars, transferStars,
canUpgrade, savedId, canExportAt, pinnedToTop, canResellAt, canTransferAt, prepaidUpgradeHash,
dropOriginalDetailsStars,
} = userStarGift;
const inputGift: ApiInputSavedStarGift | undefined = savedId && peerId
@ -176,6 +190,7 @@ export function buildApiSavedStarGift(userStarGift: GramJs.SavedStarGift, peerId
messageId: msgId,
isNameHidden: nameHidden,
isUnsaved: unsaved,
isRefunded: refunded,
canUpgrade,
alreadyPaidUpgradeStars: toJSNumber(upgradeStars),
transferStars: toJSNumber(transferStars),
@ -334,3 +349,104 @@ export function buildApiStarGiftUpgradePreview(
nextPrices: result.nextPrices?.map(buildApiStarGiftUpgradePrice) || [],
};
}
export function buildApiAuctionBidLevel(bidLevel: GramJs.AuctionBidLevel): ApiAuctionBidLevel {
return {
pos: bidLevel.pos,
amount: toJSNumber(bidLevel.amount) ?? 0,
date: bidLevel.date,
};
}
export function buildApiTypeStarGiftAuctionState(
state: GramJs.TypeStarGiftAuctionState,
): ApiTypeStarGiftAuctionState | undefined {
if (state instanceof GramJs.StarGiftAuctionStateNotModified) {
return undefined;
}
if (state instanceof GramJs.StarGiftAuctionStateFinished) {
const {
startDate, endDate, averagePrice, listedCount, fragmentListedCount, fragmentListedUrl,
} = state;
return {
type: 'finished',
startDate,
endDate,
averagePrice: toJSNumber(averagePrice),
listedCount,
fragmentListedCount,
fragmentListedUrl,
};
}
const {
version, startDate, endDate, minBidAmount, bidLevels, topBidders,
nextRoundAt, lastGiftNum, giftsLeft, currentRound, totalRounds,
} = state;
return {
type: 'active',
version,
startDate,
endDate,
minBidAmount: toJSNumber(minBidAmount),
bidLevels: bidLevels.map(buildApiAuctionBidLevel),
topBidders: topBidders.map((id) => buildApiPeerId(id, 'user')),
nextRoundAt,
lastGiftNum,
giftsLeft,
currentRound,
totalRounds,
};
}
export function buildApiStarGiftAuctionUserState(
userState: GramJs.StarGiftAuctionUserState,
): ApiStarGiftAuctionUserState {
const {
returned, bidAmount, bidDate, minBidAmount, bidPeer, acquiredCount,
} = userState;
return {
isReturned: returned || undefined,
bidAmount: bidAmount !== undefined ? toJSNumber(bidAmount) : undefined,
bidDate,
minBidAmount: minBidAmount !== undefined ? toJSNumber(minBidAmount) : undefined,
bidPeerId: bidPeer && getApiChatIdFromMtpPeer(bidPeer),
acquiredCount,
};
}
export function buildApiStarGiftAuctionState(
result: GramJs.payments.StarGiftAuctionState,
): ApiStarGiftAuctionState | undefined {
const gift = buildApiStarGift(result.gift);
if (gift.type !== 'starGift') return undefined;
const state = buildApiTypeStarGiftAuctionState(result.state);
if (!state) return undefined;
return {
gift,
state,
userState: buildApiStarGiftAuctionUserState(result.userState),
timeout: result.timeout,
};
}
export function buildApiStarGiftAuctionAcquiredGift(
result: GramJs.StarGiftAuctionAcquiredGift,
): ApiStarGiftAuctionAcquiredGift {
return {
peerId: getApiChatIdFromMtpPeer(result.peer),
date: result.date,
bidAmount: toJSNumber(result.bidAmount),
round: result.round,
position: result.pos,
message: result.message ? buildApiFormattedText(result.message) : undefined,
giftNumber: result.giftNum,
isNameHidden: result.nameHidden || undefined,
};
}

View File

@ -390,8 +390,9 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess
}
if (action instanceof GramJs.MessageActionStarGift) {
const {
nameHidden, saved, converted, upgraded, refunded, canUpgrade, prepaidUpgrade, gift, message, convertStars,
upgradeMsgId, giftMsgId, upgradeStars, fromId, peer, savedId, prepaidUpgradeHash,
nameHidden, saved, converted, upgraded, refunded, canUpgrade, prepaidUpgrade, auctionAcquired,
gift, message, convertStars, upgradeMsgId, giftMsgId, upgradeStars, fromId, peer, savedId,
prepaidUpgradeHash, toId, giftNum,
} = action;
const starGift = buildApiStarGift(gift);
@ -407,6 +408,7 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess
isRefunded: refunded,
canUpgrade,
isPrepaidUpgrade: prepaidUpgrade,
isAuctionAcquired: auctionAcquired,
gift: starGift,
message: message && buildApiFormattedText(message),
starsToConvert: toJSNumber(convertStars),
@ -417,6 +419,8 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess
peerId: peer && getApiChatIdFromMtpPeer(peer),
savedId: savedId !== undefined ? buildApiPeerId(savedId, 'user') : undefined,
prepaidUpgradeHash,
toId: toId && getApiChatIdFromMtpPeer(toId),
giftNumber: giftNum,
};
}
if (action instanceof GramJs.MessageActionStarGiftUnique) {

View File

@ -24,6 +24,7 @@ import type {
ApiVoice,
ApiWebDocument,
ApiWebPage,
ApiWebPageAuctionData,
ApiWebPageStickerData,
ApiWebPageStoryData,
BoughtPaidMedia,
@ -863,11 +864,16 @@ export function buildWebPage(webPage: GramJs.TypeWebPage): ApiWebPage | undefine
}
let story: ApiWebPageStoryData | undefined;
let gift: ApiStarGiftUnique | undefined;
let auction: ApiWebPageAuctionData | undefined;
let stickers: ApiWebPageStickerData | undefined;
const attributeStory = attributes
?.find((a): a is GramJs.WebPageAttributeStory => a instanceof GramJs.WebPageAttributeStory);
const attributeGift = attributes
?.find((a): a is GramJs.WebPageAttributeUniqueStarGift => a instanceof GramJs.WebPageAttributeUniqueStarGift);
const attributeAuction = attributes
?.find((a): a is GramJs.WebPageAttributeStarGiftAuction => (
a instanceof GramJs.WebPageAttributeStarGiftAuction
));
if (attributeStory) {
const peerId = getApiChatIdFromMtpPeer(attributeStory.peer);
story = {
@ -883,6 +889,15 @@ export function buildWebPage(webPage: GramJs.TypeWebPage): ApiWebPage | undefine
const starGift = buildApiStarGift(attributeGift.gift);
gift = starGift.type === 'starGiftUnique' ? starGift : undefined;
}
if (attributeAuction) {
const starGift = buildApiStarGift(attributeAuction.gift);
if (starGift.type === 'starGift') {
auction = {
gift: starGift,
endDate: attributeAuction.endDate,
};
}
}
const attributeStickers = attributes?.find((a): a is GramJs.WebPageAttributeStickerSet => (
a instanceof GramJs.WebPageAttributeStickerSet
));
@ -914,6 +929,7 @@ export function buildWebPage(webPage: GramJs.TypeWebPage): ApiWebPage | undefine
audio,
story,
gift,
auction,
stickers,
};
}

View File

@ -553,7 +553,7 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction):
const {
date, id, peer, amount, description, photo, title, refund, extendedMedia, failed, msgId, pending, gift, reaction,
subscriptionPeriod, stargift, giveawayPostId, starrefCommissionPermille, stargiftUpgrade, paidMessages,
stargiftResale, postsSearch, stargiftPrepaidUpgrade, stargiftDropOriginalDetails,
stargiftResale, postsSearch, stargiftPrepaidUpgrade, stargiftDropOriginalDetails, stargiftAuctionBid,
} = transaction;
if (photo) {
@ -595,6 +595,7 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction):
isPostsSearch: postsSearch,
isDropOriginalDetails: stargiftDropOriginalDetails,
isPrepaidUpgrade: stargiftPrepaidUpgrade,
isStarGiftAuctionBid: stargiftAuctionBid,
};
}

View File

@ -798,6 +798,20 @@ export function buildInputInvoice(invoice: ApiRequestInputInvoice) {
});
}
case 'stargiftAuctionBid': {
const {
giftId, bidAmount, peer, message, shouldHideName, isUpdateBid,
} = invoice;
return new GramJs.InputInvoiceStarGiftAuctionBid({
giftId: BigInt(giftId),
bidAmount: BigInt(bidAmount),
peer: peer && buildInputPeer(peer.id, peer.accessHash || ''),
message: message && buildInputTextWithEntities(message),
hideName: shouldHideName || undefined,
updateBid: isUpdateBid || undefined,
});
}
case 'giveaway':
default: {
const purpose = buildInputStorePaymentPurpose(invoice.purpose);

View File

@ -18,6 +18,8 @@ import {
buildApiResaleGifts,
buildApiSavedStarGift,
buildApiStarGift,
buildApiStarGiftAuctionAcquiredGift,
buildApiStarGiftAuctionState,
buildApiStarGiftCollection,
buildApiStarGiftUpgradePreview,
buildInputResaleGiftsAttributes,
@ -412,6 +414,51 @@ export async function fetchStarGiftUpgradePreview({
return buildApiStarGiftUpgradePreview(result);
}
export async function fetchStarGiftAuctionState({
giftId,
slug,
version = 0,
}: {
giftId?: string;
slug?: string;
version?: number;
}) {
if (!giftId && !slug) return undefined;
const auction = slug
? new GramJs.InputStarGiftAuctionSlug({ slug })
: new GramJs.InputStarGiftAuction({ giftId: BigInt(giftId!) });
const result = await invokeRequest(new GramJs.payments.GetStarGiftAuctionState({
auction,
version,
}));
if (!result) {
return undefined;
}
return buildApiStarGiftAuctionState(result);
}
export async function fetchStarGiftAuctionAcquiredGifts({
giftId,
}: {
giftId: string;
}) {
const result = await invokeRequest(new GramJs.payments.GetStarGiftAuctionAcquiredGifts({
giftId: BigInt(giftId),
}));
if (!result) {
return undefined;
}
return {
gifts: result.gifts.map(buildApiStarGiftAuctionAcquiredGift),
};
}
export function upgradeStarGift({
inputSavedGift,
shouldKeepOriginalDetails,

View File

@ -31,6 +31,7 @@ import {
buildApiFormattedText,
buildApiPhoto, buildApiUsernames, buildPrivacyRules,
} from '../apiBuilders/common';
import { buildApiStarGiftAuctionUserState, buildApiTypeStarGiftAuctionState } from '../apiBuilders/gifts';
import { omitVirtualClassFields } from '../apiBuilders/helpers';
import {
buildApiMessageExtendedMediaPreview,
@ -1095,6 +1096,22 @@ export function updater(update: Update) {
'@type': 'updateStarsBalance',
balance,
});
} else if (update instanceof GramJs.UpdateStarGiftAuctionState) {
const state = buildApiTypeStarGiftAuctionState(update.state);
if (!state) {
return;
}
sendApiUpdate({
'@type': 'updateStarGiftAuctionState',
giftId: update.giftId.toString(),
state,
});
} else if (update instanceof GramJs.UpdateStarGiftAuctionUserState) {
sendApiUpdate({
'@type': 'updateStarGiftAuctionUserState',
giftId: update.giftId.toString(),
userState: buildApiStarGiftAuctionUserState(update.userState),
});
} else if (update instanceof GramJs.UpdatePaidReactionPrivacy) {
sendApiUpdate({
'@type': 'updatePaidReactionPrivacy',

View File

@ -243,6 +243,7 @@ export interface ApiMessageActionStarGift extends ActionMediaType {
isRefunded?: true;
canUpgrade?: true;
isPrepaidUpgrade?: true;
isAuctionAcquired?: true;
gift: ApiStarGiftRegular;
message?: ApiFormattedText;
starsToConvert?: number;
@ -253,6 +254,8 @@ export interface ApiMessageActionStarGift extends ActionMediaType {
peerId?: string;
savedId?: string;
prepaidUpgradeHash?: string;
toId?: string;
giftNumber?: number;
}
export interface ApiMessageActionStarGiftUnique extends ActionMediaType {

View File

@ -10,7 +10,7 @@ import type {
ApiLabeledPrice,
} from './payments';
import type { ApiTypePeerColor } from './peers';
import type { ApiStarGiftUnique, ApiTypeCurrencyAmount } from './stars';
import type { ApiStarGiftRegular, ApiStarGiftUnique, ApiTypeCurrencyAmount } from './stars';
import type {
ApiMessageStoryData, ApiStory, ApiWebPageStickerData, ApiWebPageStoryData,
} from './stories';
@ -394,10 +394,16 @@ export interface ApiWebPageFull {
video?: ApiVideo;
story?: ApiWebPageStoryData;
gift?: ApiStarGiftUnique;
auction?: ApiWebPageAuctionData;
stickers?: ApiWebPageStickerData;
hasLargeMedia?: boolean;
}
export type ApiWebPageAuctionData = {
gift: ApiStarGiftRegular;
endDate: number;
};
export type ApiWebPage = ApiWebPagePending | ApiWebPageEmpty | ApiWebPageFull;
/**

View File

@ -412,11 +412,22 @@ export type ApiInputInvoiceStarGiftPrepaidUpgrade = {
hash: string;
};
export type ApiInputInvoiceStarGiftAuctionBid = {
type: 'stargiftAuctionBid';
giftId: string;
bidAmount: number;
peerId?: string;
message?: ApiFormattedText;
shouldHideName?: boolean;
isUpdateBid?: boolean;
};
export type ApiInputInvoice = ApiInputInvoiceMessage | ApiInputInvoiceSlug | ApiInputInvoiceGiveaway
| ApiInputInvoiceGiftCode | ApiInputInvoicePremiumGiftStars | ApiInputInvoiceStars | ApiInputInvoiceStarsGift
| ApiInputInvoiceStarsGiveaway | ApiInputInvoiceStarGift | ApiInputInvoiceChatInviteSubscription
| ApiInputInvoiceStarGiftUpgrade | ApiInputInvoiceStarGiftTransfer | ApiInputInvoiceStarGiftResale
| ApiInputInvoiceStarGiftDropOriginalDetails | ApiInputInvoiceStarGiftPrepaidUpgrade;
| ApiInputInvoiceStarGiftDropOriginalDetails | ApiInputInvoiceStarGiftPrepaidUpgrade
| ApiInputInvoiceStarGiftAuctionBid;
/* Used for Invoice request */
export type ApiRequestInputInvoiceMessage = {
@ -497,12 +508,23 @@ export type ApiRequestInputInvoiceStarGiftPrepaidUpgrade = {
hash: string;
};
export type ApiRequestInputInvoiceStarGiftAuctionBid = {
type: 'stargiftAuctionBid';
giftId: string;
bidAmount: number;
peer?: ApiPeer;
message?: ApiFormattedText;
shouldHideName?: boolean;
isUpdateBid?: boolean;
};
export type ApiRequestInputInvoice = ApiRequestInputInvoiceMessage | ApiRequestInputInvoiceSlug
| ApiRequestInputInvoiceGiveaway | ApiRequestInputInvoiceStars | ApiRequestInputInvoiceStarsGiveaway
| ApiRequestInputInvoiceChatInviteSubscription | ApiRequestInputInvoiceStarGift
| ApiRequestInputInvoiceStarGiftUpgrade | ApiRequestInputInvoiceStarGiftTransfer
| ApiRequestInputInvoicePremiumGiftStars | ApiRequestInputInvoiceStarGiftResale
| ApiRequestInputInvoiceStarGiftDropOriginalDetails | ApiRequestInputInvoiceStarGiftPrepaidUpgrade;
| ApiRequestInputInvoiceStarGiftDropOriginalDetails | ApiRequestInputInvoiceStarGiftPrepaidUpgrade
| ApiRequestInputInvoiceStarGiftAuctionBid;
export interface ApiUniqueStarGiftValueInfo {
isLastSaleOnFragment?: true;

View File

@ -27,6 +27,15 @@ export interface ApiStarGiftRegular {
perUserTotal?: number;
perUserRemains?: number;
lockedUntilDate?: number;
isAuction?: true;
giftsPerRound?: number;
background?: ApiStarGiftBackground;
}
export interface ApiStarGiftBackground {
centerColor: string;
edgeColor: string;
textColor: string;
}
export interface ApiStarGiftUnique {
@ -103,6 +112,7 @@ export interface ApiStarGiftUpgradePreview {
export interface ApiSavedStarGift {
isNameHidden?: boolean;
isUnsaved?: boolean;
isRefunded?: boolean;
fromId?: string;
date: number;
gift: ApiStarGift;
@ -259,6 +269,7 @@ export interface ApiStarsTransaction {
isPostsSearch?: true;
isDropOriginalDetails?: true;
isPrepaidUpgrade?: true;
isStarGiftAuctionBid?: true;
}
export interface ApiStarsSubscription {
@ -315,3 +326,63 @@ export interface ApiStarsRating {
stars: number;
nextLevelStars?: number;
}
export interface ApiAuctionBidLevel {
pos: number;
amount: number;
date: number;
}
export interface ApiStarGiftAuctionStateActive {
type: 'active';
version: number;
startDate: number;
endDate: number;
minBidAmount: number;
bidLevels: ApiAuctionBidLevel[];
topBidders: string[];
nextRoundAt: number;
lastGiftNum: number;
giftsLeft: number;
currentRound: number;
totalRounds: number;
}
export interface ApiStarGiftAuctionStateFinished {
type: 'finished';
startDate: number;
endDate: number;
averagePrice: number;
listedCount?: number;
fragmentListedCount?: number;
fragmentListedUrl?: string;
}
export interface ApiStarGiftAuctionUserState {
isReturned?: true;
bidAmount?: number;
bidDate?: number;
minBidAmount?: number;
bidPeerId?: string;
acquiredCount: number;
}
export type ApiTypeStarGiftAuctionState = ApiStarGiftAuctionStateActive | ApiStarGiftAuctionStateFinished;
export interface ApiStarGiftAuctionState {
gift: ApiStarGiftRegular;
state: ApiTypeStarGiftAuctionState;
userState: ApiStarGiftAuctionUserState;
timeout: number;
}
export interface ApiStarGiftAuctionAcquiredGift {
peerId: string;
date: number;
bidAmount: number;
round: number;
position: number;
message?: ApiFormattedText;
giftNumber?: number;
isNameHidden?: true;
}

View File

@ -45,7 +45,7 @@ import type {
} from './misc';
import type { ApiEmojiStatusType, ApiPeerSettings } from './peers';
import type { ApiPrivacyKey, LangPackStringValue, PrivacyVisibility } from './settings';
import type { ApiTypeCurrencyAmount } from './stars';
import type { ApiStarGiftAuctionUserState, ApiTypeCurrencyAmount, ApiTypeStarGiftAuctionState } from './stars';
import type { ApiStealthMode, ApiStory, ApiStorySkipped } from './stories';
import type {
ApiUser, ApiUserFullInfo, ApiUserStatus,
@ -834,6 +834,18 @@ export type ApiUpdateStarsBalance = {
balance: ApiTypeCurrencyAmount;
};
export type ApiUpdateStarGiftAuctionState = {
'@type': 'updateStarGiftAuctionState';
giftId: string;
state: ApiTypeStarGiftAuctionState;
};
export type ApiUpdateStarGiftAuctionUserState = {
'@type': 'updateStarGiftAuctionUserState';
giftId: string;
userState: ApiStarGiftAuctionUserState;
};
export type ApiUpdateDeleteProfilePhoto = {
'@type': 'updateDeleteProfilePhoto';
peerId: string;
@ -915,10 +927,11 @@ export type ApiUpdate = (
ApiRequestReconnectApi | ApiRequestSync | ApiUpdateFetchingDifference | ApiUpdateChannelMessages |
ApiUpdateStealthMode | ApiUpdateAttachMenuBots | ApiUpdateNewAuthorization | ApiUpdateGroupInvitePrivacyForbidden |
ApiUpdateViewForumAsMessages | ApiUpdateSavedDialogPinned | ApiUpdatePinnedSavedDialogIds | ApiUpdateChatLastMessage |
ApiUpdateDeleteSavedHistory | ApiUpdatePremiumFloodWait | ApiUpdateStarsBalance | ApiUpdateBotCommands |
ApiUpdateQuickReplyMessage | ApiUpdateQuickReplies | ApiDeleteQuickReply | ApiUpdateDeleteQuickReplyMessages |
ApiUpdateDeleteProfilePhoto | ApiUpdateNewProfilePhoto | ApiUpdateEntities | ApiUpdatePaidReactionPrivacy |
ApiUpdateLangPackTooLong | ApiUpdateLangPack | ApiUpdateNotSupportedInFrozenAccountError
ApiUpdateDeleteSavedHistory | ApiUpdatePremiumFloodWait | ApiUpdateStarsBalance | ApiUpdateStarGiftAuctionState
| ApiUpdateStarGiftAuctionUserState | ApiUpdateBotCommands | ApiUpdateQuickReplyMessage | ApiUpdateQuickReplies
| ApiDeleteQuickReply | ApiUpdateDeleteQuickReplyMessages | ApiUpdateDeleteProfilePhoto | ApiUpdateNewProfilePhoto
| ApiUpdateEntities | ApiUpdatePaidReactionPrivacy | ApiUpdateLangPackTooLong | ApiUpdateLangPack
| ApiUpdateNotSupportedInFrozenAccountError
);
export type OnApiUpdate = (update: ApiUpdate) => void;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M29.556 13.557C29.556 6.082 23.474 0 16 0S2.444 6.082 2.444 13.557v.543l9.13 6.917a3.67 3.67 0 0 0-1.453 2.911v4.387A3.69 3.69 0 0 0 13.806 32h4.387a3.69 3.69 0 0 0 3.685-3.685v-4.387a3.67 3.67 0 0 0-1.453-2.91l9.131-6.918zm-18.558.848 1.859 4.842-7.25-5.493c.06-.035.127-.071.183-.105.492-.3.818-.498 1.591-.498s1.099.198 1.59.498c.486.294 1.086.634 2.027.756m4.583 5.839-2.314-6.03a5.7 5.7 0 0 0 1.154-.567c.492-.298.816-.496 1.587-.496s1.095.198 1.587.496a5.7 5.7 0 0 0 1.157.57l-2.313 6.027zm5.44-5.84c.938-.122 1.536-.462 2.018-.757.492-.298.816-.496 1.587-.496s1.094.198 1.585.496c.056.035.123.071.182.106l-7.221 5.472zM16 2.187c5.637 0 10.32 4.126 11.21 9.514-.574-.347-1.299-.735-2.583-.735-1.384 0-2.127.453-2.724.816-.49.299-.814.496-1.585.496s-1.094-.197-1.585-.496c-.597-.363-1.34-.816-2.724-.816s-2.126.453-2.723.816c-.492.299-.816.496-1.587.496-.773 0-1.098-.197-1.59-.497-.598-.363-1.341-.815-2.727-.815-1.288 0-2.014.39-2.591.737.89-5.39 5.573-9.516 11.21-9.516m3.693 26.13a1.5 1.5 0 0 1-1.5 1.498h-4.386a1.5 1.5 0 0 1-1.499-1.499v-4.387a1.5 1.5 0 0 1 1.499-1.498h4.387a1.5 1.5 0 0 1 1.499 1.498z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 32 32"><path d="m23.632 12.635-6.73 6.73a.697.697 0 0 1-.986 0l-6.149-6.15a.697.697 0 0 1 0-.986l6.73-6.73a.697.697 0 0 1 .986 0l6.149 6.15a.697.697 0 0 1 0 .986M28.447 11.533a2.34 2.34 0 0 1-3.307 0L17.6 3.992A2.338 2.338 0 1 1 20.906.685l7.54 7.541a2.34 2.34 0 0 1 0 3.307M31.159 26.892a2.87 2.87 0 0 1-4.06 0l-7.528-7.527a.697.697 0 0 1 0-.987l3.075-3.074a.697.697 0 0 1 .986 0l7.527 7.527a2.87 2.87 0 0 1 0 4.06M15.771 24.209a2.34 2.34 0 0 1-3.306 0l-7.542-7.542A2.338 2.338 0 1 1 8.23 13.36l7.541 7.542a2.34 2.34 0 0 1 0 3.307M18.896 29.805v1.498A.697.697 0 0 1 18.2 32H.697A.697.697 0 0 1 0 31.303v-1.498c0-1.37 1.11-2.481 2.481-2.481h13.934c1.37 0 2.481 1.11 2.481 2.48"/></svg>

After

Width:  |  Height:  |  Size: 750 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 32 32"><path d="M20.989 22.53q-.163 0-.322-.02a2.53 2.53 0 0 1-1.641-.914l-.954-1.171-1.468.258a2.52 2.52 0 0 1-2.919-1.952 2.5 2.5 0 0 1 .4-1.963l.73-1.033.028-.04a4 4 0 0 1-.194-.291l-.526-.885a2.52 2.52 0 0 1-.264-1.946 2.47 2.47 0 0 1 1.217-1.529l.182-.087a2.56 2.56 0 0 1 1.727-.115l1.43.413.972-1.013c.933-.975 2.511-1.029 3.52-.117a2.52 2.52 0 0 1 .829 1.735l.072 1.403 1.366.629c.61.28 1.09.803 1.322 1.433a2.47 2.47 0 0 1-.085 1.909 2.5 2.5 0 0 1-1.457 1.304l-1.32.45-.145 1.323a2.47 2.47 0 0 1-.934 1.681 2.55 2.55 0 0 1-1.566.539m-2.083-4.347 1.72 2.11a.48.48 0 0 0 .306.172c.13.012.252-.02.35-.097a.4.4 0 0 0 .156-.285l.291-2.625 2.56-.872a.44.44 0 0 0 .253-.225.4.4 0 0 0 .017-.327.48.48 0 0 0-.247-.269l-2.5-1.15-.137-2.649a.45.45 0 0 0-.148-.31c-.168-.15-.46-.187-.649.012l-1.836 1.916-2.631-.76a.47.47 0 0 0-.311.015l-.089.044c-.088.049-.142.159-.164.238a.44.44 0 0 0 .048.343l.526.882c.12.204.284.38.482.526l1.175.862-1.203.822q-.221.151-.375.37l-.73 1.032a.43.43 0 0 0-.068.337c.058.268.327.39.544.357z" class="st3"/><path d="M20.139 27.861c-6.54 0-11.862-5.321-11.862-11.861S13.6 4.139 20.14 4.139 32 9.46 32 16s-5.321 11.861-11.861 11.861m0-21.66c-5.403 0-9.799 4.396-9.799 9.799s4.396 9.798 9.799 9.798 9.798-4.395 9.798-9.798-4.395-9.798-9.798-9.798M8.169 9.296H2.797a.604.604 0 0 1-.604-.605v-.854c0-.333.27-.604.604-.604H8.17c.333 0 .604.27.604.604v.854c0 .334-.27.605-.604.605M8.169 24.767H2.797a.604.604 0 0 1-.604-.604v-.854c0-.334.27-.605.604-.605H8.17c.333 0 .604.27.604.605v.854c0 .333-.27.604-.604.604M5.975 17.031H.605A.604.604 0 0 1 0 16.427v-.854c0-.334.27-.604.604-.604h5.37c.335 0 .605.27.605.604v.854c0 .334-.27.604-.604.604"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" x="0" y="0" version="1.1" viewBox="0 0 32 32"><style>.st3{fill:#4e8ee5}</style><path d="M11.1 18c-7.4 0-10 4.3-10.9 6.8-.4 1-.2 2.1.4 2.9.7 1 1.9 1.6 3.1 1.6h14.8c1.3 0 2.4-.6 3.1-1.6.6-.9.7-1.9.4-2.9-.9-2.5-3.5-6.8-10.9-6.8m8.6 8.4c-.2.3-.7.5-1.2.5H3.7c-.5 0-1-.2-1.2-.5-.2-.2-.2-.5-.1-.8.9-2.4 3.1-5.2 8.7-5.2s7.8 2.8 8.7 5.2c.1.3.1.5-.1.8M31.8 23.9c-.8-2.3-3-6.1-9.6-6.1h-.8c-.7 0-1.2.6-1.1 1.2 0 .7.6 1.2 1.2 1.1h.7c4.8 0 6.6 2.4 7.3 4.5.1.2 0 .3-.1.4-.1.2-.4.3-.8.3h-4.3c-.7 0-1.2.5-1.2 1.2s.5 1.2 1.2 1.2h4.3c1.1 0 2.1-.5 2.7-1.3.7-.7.8-1.6.5-2.5M4.6 11.2l.9.7c.1.1.2.2.3.2v.1l-.4 1.4c-.2.8-.1 1.6.4 2.3.8 1.3 2.6 1.7 3.9.9l1.5-.9 1.5.9c.5.3 1 .4 1.5.4.2 0 .4 0 .7-.1.8-.2 1.4-.7 1.8-1.3.4-.7.5-1.4.3-2.2l-.4-1.5.8-.6c.2.4.4.8.8 1l.4.3-.1.5c-.2.7-.1 1.5.3 2.1.8 1.2 2.4 1.6 3.7.9l.6-.3.6.4c.4.2.9.4 1.4.4.2 0 .4 0 .6-.1.7-.2 1.3-.6 1.7-1.2s.5-1.4.3-2.1l-.1-.5.4-.4c.6-.5.9-1.1 1-1.9.1-.7-.2-1.4-.6-2-.5-.6-1.2-.9-1.9-1l-.7-.1-.4-.8c-.3-.5-.8-1-1.4-1.2-1.3-.5-2.7 0-3.4 1.2l-.4.8h-.7c-.3 0-.6.1-.9.2-.1-.3-.3-.6-.5-.9-.5-.6-1.2-1-2-1l-1.6.2-.6-1.5c-.3-.7-.9-1.3-1.6-1.5-1.5-.7-3.2 0-3.8 1.5L7.9 6l-1.7.1c-.7 0-1.3.3-1.8.8l-.2.2c-.5.6-.8 1.3-.7 2.1s.5 1.5 1.1 2m14.9-.9c.1-.1.2-.2.3-.2l2.1-.2.8-1.9c.1-.2.3-.2.4-.1.2.1.2.1.3.1l.8 1.9 2.1.2c.1 0 .2.1.3.1.1.1.1.1.1.2s-.1.2-.1.2L25 11.9l.5 2v.2l-.2.2H25l-1.8-1.1-1.8 1c-.1.1-.4.1-.5-.1 0-.1-.1-.2 0-.3l.3-1.3.1-.1 2.1-1.2c.1-.1 0-.2-.1-.2l-2.5.1h-.1c-.1 0-.2-.1-.2-.2l-.6-.5c-.1-.1-.1-.2-.1-.2-.3.3-.3.2-.3.1M5.9 8.7c.2-.2.3-.2.4-.2l3.1-.2 1.2-2.8c.1-.3.5-.4.7-.3l.3.3 1.2 2.8 3.2.2c.1 0 .3.1.4.2s.1.2.1.4c0 .1-.1.3-.2.3l-2.4 2 .7 2.9c0 .1 0 .3-.1.4s-.2.2-.3.2-.3 0-.4-.1l-2.7-1.6-2.7 1.6c-.2.1-.6.1-.7-.2-.1-.1-.1-.3-.1-.4l.4-1.4c.1-.2.1-.4.3-.6l3.9-2c.1-.1.1-.3-.1-.2l-4.5.4c-.3-.1-.5-.2-.7-.4L6 9.3c-.1-.1-.1-.2-.1-.3z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" x="0" y="0" version="1.1" viewBox="0 0 32 32"><path d="M11.1 18c-7.4 0-10 4.3-10.9 6.8-.4 1-.2 2.1.4 2.9.7 1 1.9 1.6 3.1 1.6h14.8c1.3 0 2.4-.6 3.1-1.6.6-.9.7-1.9.4-2.9-.9-2.5-3.5-6.8-10.9-6.8m8.6 8.4c-.2.3-.7.5-1.2.5H3.7c-.5 0-1-.2-1.2-.5-.2-.2-.2-.5-.1-.8.9-2.4 3.1-5.2 8.7-5.2s7.8 2.8 8.7 5.2c.1.3.1.5-.1.8M31.8 23.9c-.8-2.3-3-6.1-9.6-6.1h-.8c-.7 0-1.2.6-1.1 1.2 0 .7.6 1.2 1.2 1.1h.7c4.8 0 6.6 2.4 7.3 4.5.1.2 0 .3-.1.4-.1.2-.4.3-.8.3h-4.3c-.7 0-1.2.5-1.2 1.2s.5 1.2 1.2 1.2h4.3c1.1 0 2.1-.5 2.7-1.3.7-.7.8-1.6.5-2.5M4.6 11.2l.9.7c.1.1.2.2.3.2v.1l-.4 1.4c-.2.8-.1 1.6.4 2.3.8 1.3 2.6 1.7 3.9.9l1.5-.9 1.5.9c.5.3 1 .4 1.5.4.2 0 .4 0 .7-.1.8-.2 1.4-.7 1.8-1.3.4-.7.5-1.4.3-2.2l-.4-1.5.8-.6c.2.4.4.8.8 1l.4.3-.1.5c-.2.7-.1 1.5.3 2.1.8 1.2 2.4 1.6 3.7.9l.6-.3.6.4c.4.2.9.4 1.4.4.2 0 .4 0 .6-.1.7-.2 1.3-.6 1.7-1.2s.5-1.4.3-2.1l-.1-.5.4-.4c.6-.5.9-1.1 1-1.9.1-.7-.2-1.4-.6-2-.5-.6-1.2-.9-1.9-1l-.7-.1-.4-.8c-.3-.5-.8-1-1.4-1.2-1.3-.5-2.7 0-3.4 1.2l-.4.8h-.7c-.3 0-.6.1-.9.2-.1-.3-.3-.6-.5-.9-.5-.6-1.2-1-2-1l-1.6.2-.6-1.5c-.3-.7-.9-1.3-1.6-1.5-1.5-.7-3.2 0-3.8 1.5L7.9 6l-1.7.1c-.7 0-1.3.3-1.8.8l-.2.2c-.5.6-.8 1.3-.7 2.1s.5 1.5 1.1 2m14.9-.9c.1-.1.2-.2.3-.2l2.1-.2.8-1.9c.1-.2.3-.2.4-.1.2.1.2.1.3.1l.8 1.9 2.1.2c.1 0 .2.1.3.1.1.1.1.1.1.2s-.1.2-.1.2L25 11.9l.5 2v.2l-.2.2H25l-1.8-1.1-1.8 1c-.1.1-.4.1-.5-.1 0-.1-.1-.2 0-.3l.3-1.3.1-.1 2.1-1.2c.1-.1 0-.2-.1-.2l-2.5.1h-.1c-.1 0-.2-.1-.2-.2l-.6-.5c-.1-.1-.1-.2-.1-.2-.3.3-.3.2-.3.1M5.9 8.7c.2-.2.3-.2.4-.2l3.1-.2 1.2-2.8c.1-.3.5-.4.7-.3l.3.3 1.2 2.8 3.2.2c.1 0 .3.1.4.2s.1.2.1.4c0 .1-.1.3-.2.3l-2.4 2 .7 2.9c0 .1 0 .3-.1.4s-.2.2-.3.2-.3 0-.4-.1l-2.7-1.6-2.7 1.6c-.2.1-.6.1-.7-.2-.1-.1-.1-.3-.1-.4l.4-1.4c.1-.2.1-.4.3-.6l3.9-2c.1-.1.1-.3-.1-.2l-4.5.4c-.3-.1-.5-.2-.7-.4L6 9.3c-.1-.1-.1-.2-.1-.3z"/></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1513,6 +1513,7 @@
"GiftInfoDescriptionUpgrade2" = "Upgrade this gift to turn it to a unique collectible.";
"GiftInfoPeerDescriptionFreeUpgradeOut" = "{peer} can turn this gift into a unique collectible";
"GiftInfoDescriptionUpgraded" = "This gift was turned into a unique collectible.";
"GiftInfoDescriptionRefunded" = "This gift was downgraded because a request to refund the payment related to this gift was made, and the money was returned.";
"GiftInfoFrom" = "From";
"GiftInfoDate" = "Date";
"GiftInfoValue" = "Value";
@ -1929,6 +1930,9 @@
"ActionStarGiftUniqueBackdrop" = "Backdrop";
"ActionStarGiftUniqueSymbol" = "Symbol";
"ActionStarGiftSelf" = "Saved Gift";
"ActionStarGiftAuctionWon" = "You won the auction with a bid of {cost}";
"ActionStarGiftAuctionFor" = "Gift for {peer}";
"ActionStarGiftAuctionBought" = "You bought this gift at auction for {cost}.";
"ActionSuggestedPhotoYou" = "You suggested this photo for {user}'s Telegram profile.";
"ActionSuggestedPhoto" = "{user} suggests this photo for your Telegram profile.";
"ActionSuggestedPhotoButton" = "View Photo";
@ -2343,6 +2347,8 @@
"StarGiftReasonDropOriginalDetails" = "Removed Description";
"GiftAnUpgradeButton" = "Gift an Upgrade";
"GiftPrepaidUpgradeTransactionTitle" = "Gift Upgrade";
"StarGiftAuctionBidTransaction" = "Auction Bid";
"StarGiftAuctionBidRefundedTransaction" = "Refunded Auction Bid";
"ActionStarGiftPrepaidUpgraded" = "{user} turned the gift into a unique collectible";
"ActionStarGiftPrepaidUpgradedYou" = "You turned the gift into a unique collectible";
"UserNoteTitle" = "Notes";
@ -2405,6 +2411,66 @@
"StarGiftUpgradeCostModalTitle" = "Upgrade Cost";
"StarGiftUpgradeCostHint" = "Users who upgrade their gifts first get collectibles with lower numbers.";
"StarGiftPriceDecreaseTimer" = "Price decreases in {timer}";
"GiftRibbonAuction" = "auction";
"GiftAuctionJoin" = "Join";
"GiftAuctionLearnMore" = "Learn More >";
"GiftAuctionTopBidders_one" = "Top **{count}** bidder will get **{gift}** gift this round. {link}";
"GiftAuctionTopBidders_other" = "Top **{count}** bidders will get **{gift}** gifts this round. {link}";
"GiftAuctionStarted" = "Started";
"GiftAuctionEnds" = "Ends";
"GiftAuctionCurrentRound" = "Current Round";
"GiftAuctionRoundValue" = "{current} of {total}";
"GiftAuctionDescription_one" = "{count} gift is dropped at varying intervals to the top {count} bidder by bid amount. {link}";
"GiftAuctionDescription_other" = "{count} gifts are dropped at varying intervals to the top {count} bidders by bid amount. {link}";
"GiftAuctionPlaceBid" = "Place a Bid";
"GiftAuctionPlaceBidButton" = "Place a {amount} Bid";
"GiftAuctionMinimumBid" = "minimum bid";
"GiftAuctionUntilNextRound" = "until next round";
"GiftAuctionLeft" = "left";
"GiftAuctionTimeLeft" = "{time} left";
"GiftAuctionYourBidWillBe" = "Your bid will be";
"GiftAuctionYoureWinning" = "You're winning";
"GiftAuctionTopWinners_one" = "Top {count} Winner";
"GiftAuctionTopWinners_other" = "Top {count} Winners";
"GiftAuctionAddToBid" = "Add {amount} to Your Bid";
"GiftAuctionBalance" = "Balance";
"GiftAuctionInfoTitle" = "Auction";
"GiftAuctionInfoSubtitle" = "Join the battle for exclusive gifts.";
"GiftAuctionInfoTopBiddersTitle_one" = "Top {count} Bidder";
"GiftAuctionInfoTopBiddersTitle_other" = "Top {count} Bidders";
"GiftAuctionInfoTopBiddersSubtitle_one" = "{count} gift is dropped at varying intervals to the top {count} bidder by bid amount.";
"GiftAuctionInfoTopBiddersSubtitle_other" = "{count} gifts are dropped at varying intervals to the top {count} bidders by bid amount.";
"GiftAuctionInfoBidCarryoverTitle" = "Bid Carryover";
"GiftAuctionInfoBidCarryoverSubtitle" = "If your bid leaves the top {count}, it will automatically join the next round.";
"GiftAuctionInfoMissedBiddersTitle" = "Missed Bidders";
"GiftAuctionInfoMissedBiddersSubtitle" = "If your bid doesn't win after the final round, your Stars will be fully refunded.";
"GiftAuctionItemsBought_one" = "{count} {gift} item bought >";
"GiftAuctionItemsBought_other" = "{count} {gift} items bought >";
"GiftAuctionBoughtGiftsTitle_one" = "{count} Bought Gift";
"GiftAuctionBoughtGiftsTitle_other" = "{count} Bought Gifts";
"GiftAuctionBoughtGiftHeader" = "{gift} #{giftNumber} in round {round}";
"GiftAuctionRecipient" = "Recipient";
"GiftAuctionDate" = "Date";
"GiftAuctionAcceptedBid" = "Accepted Bid";
"GiftAuctionTopPosition" = "TOP {position}";
"GiftAuctionCustomBidTitle" = "Place a Custom Bid";
"GiftAuctionCustomBidDescription" = "If you fall below the top {count}, your bid will roll over to the next round.";
"GiftAuctionCustomBidPlaceholder" = "Custom Bid...";
"GiftAuctionCustomBidButton" = "Place a Bid";
"GiftAuctionBidPlacedTitle" = "Your bid has been placed";
"GiftAuctionBidIncreasedTitle" = "Your bid has been increased";
"GiftAuctionBidPlacedMessage" = "If you fall below the top {count}, your bid will roll over to the next round.";
"GiftAuctionFinished" = "Finished";
"GiftAuctionEnded" = "Auction ended";
"GiftAuctionSoldOut" = "Sold Out!";
"GiftAuctionGifts_one" = "{count} Gift";
"GiftAuctionGifts_other" = "{count} Gifts";
"GiftAuctionChangeRecipientTitle" = "Change Recipient";
"GiftAuctionChangeRecipientDescription" = "You've already placed a bid on this gift for **{oldPeer}**. Do you want to raise your bid and change the recipient to **{newPeer}**?";
"GiftAuctionAveragePrice" = "Average Price";
"GiftAuctionTapToBidMore" = "click to bid more";
"GiftAuctionWonNotification" = "You won {gift} at the auction!";
"StarGift" = "Star Gift";
"SettingsItemPrivacyPasskeys" = "Passkeys";
"SettingsItemPrivacyOn" = "Enabled";
"SettingsItemPrivacyOff" = "Disabled";

View File

@ -1,3 +1,5 @@
/* eslint-disable @stylistic/max-len */
export { default as StarsGiftModal } from '../components/modals/stars/gift/StarsGiftModal';
export { default as StarsGiftingPickerModal } from '../components/main/premium/StarsGiftingPickerModal';
export { default as StarsBalanceModal } from '../components/modals/stars/StarsBalanceModal';
@ -12,6 +14,11 @@ export { default as GiftInfoValueModal } from '../components/modals/gift/value/G
export { default as GiftLockedModal } from '../components/modals/gift/locked/GiftLockedModal';
export { default as GiftResalePriceComposerModal } from '../components/modals/gift/resale/GiftResalePriceComposerModal';
export { default as GiftUpgradeModal } from '../components/modals/gift/upgrade/GiftUpgradeModal';
export { default as GiftAuctionModal } from '../components/modals/gift/auction/GiftAuctionModal';
export { default as GiftAuctionBidModal } from '../components/modals/gift/auction/GiftAuctionBidModal';
export { default as GiftAuctionInfoModal } from '../components/modals/gift/auction/GiftAuctionInfoModal';
export { default as GiftAuctionAcquiredModal } from '../components/modals/gift/auction/GiftAuctionAcquiredModal';
export { default as GiftAuctionChangeRecipientModal } from '../components/modals/gift/auction/GiftAuctionChangeRecipientModal';
export { default as StarGiftPriceDecreaseInfoModal } from '../components/modals/gift/StarGiftPriceDecreaseInfoModal';
export { default as GiftStatusInfoModal } from '../components/modals/gift/status/GiftStatusInfoModal';
export { default as GiftWithdrawModal } from '../components/modals/gift/withdraw/GiftWithdrawModal';

View File

@ -15,6 +15,12 @@ export type GiftAttributes = {
backdrop?: ApiStarGiftAttributeBackdrop;
};
export type GiftPreviewAttributes = {
model: ApiStarGiftAttributeModel;
pattern: ApiStarGiftAttributePattern;
backdrop: ApiStarGiftAttributeBackdrop;
};
export function getStickerFromGift(gift: ApiStarGift): ApiSticker | undefined {
if (gift.type === 'starGift') {
return gift.sticker;
@ -52,3 +58,46 @@ function getGiftAttributesFromList(attributes: ApiStarGiftAttribute[]) {
backdrop,
};
}
export function getRandomGiftPreviewAttributes(
list: ApiStarGiftAttribute[],
previousSelection?: GiftPreviewAttributes,
): GiftPreviewAttributes {
const models = list.filter((attr): attr is ApiStarGiftAttributeModel => (
attr.type === 'model' && attr.name !== previousSelection?.model.name
));
const patterns = list.filter((attr): attr is ApiStarGiftAttributePattern => (
attr.type === 'pattern' && attr.name !== previousSelection?.pattern.name
));
const backdrops = list.filter((attr): attr is ApiStarGiftAttributeBackdrop => (
attr.type === 'backdrop' && attr.name !== previousSelection?.backdrop.name
));
if (!models.length || !patterns.length || !backdrops.length) {
// Fallback: re-filter without exclusions if any category is empty
const fallbackModels = models.length ? models
: list.filter((attr): attr is ApiStarGiftAttributeModel => attr.type === 'model');
const fallbackPatterns = patterns.length ? patterns
: list.filter((attr): attr is ApiStarGiftAttributePattern => attr.type === 'pattern');
const fallbackBackdrops = backdrops.length ? backdrops
: list.filter((attr): attr is ApiStarGiftAttributeBackdrop => attr.type === 'backdrop');
return {
model: fallbackModels[Math.floor(Math.random() * fallbackModels.length)],
pattern: fallbackPatterns[Math.floor(Math.random() * fallbackPatterns.length)],
backdrop: fallbackBackdrops[Math.floor(Math.random() * fallbackBackdrops.length)],
};
}
const randomModel = models[Math.floor(Math.random() * models.length)];
const randomPattern = patterns[Math.floor(Math.random() * patterns.length)];
const randomBackdrop = backdrops[Math.floor(Math.random() * backdrops.length)];
return {
model: randomModel,
pattern: randomPattern,
backdrop: randomBackdrop,
};
}

View File

@ -42,6 +42,7 @@ const PickerModal = ({
containerRef: dialogRef,
selector: `.modal-content ${itemsContainerSelector}`,
isBottomNotch: true,
shouldHideTopNotch: true,
}, [modalProps.isOpen]);
return (

View File

@ -8,7 +8,7 @@ import {
import { addExtraClass } from '../../lib/teact/teact-dom';
import { getActions, getGlobal, withGlobal } from '../../global';
import type { ApiChatFolder, ApiLimitTypeWithModal, ApiUser } from '../../api/types';
import type { ApiChatFolder, ApiLimitTypeWithModal, ApiStarGiftAuctionState, ApiUser } from '../../api/types';
import type { TabState } from '../../global/types';
import { BASE_EMOJI_KEYWORD_LANG, DEBUG, FOLDERS_POSITION_LEFT, INACTIVE_MARKER } from '../../config';
@ -145,6 +145,7 @@ type StateProps = {
isAccountFrozen?: boolean;
isAppConfigLoaded?: boolean;
isFoldersSidebarShown: boolean;
activeGiftAuction?: ApiStarGiftAuctionState;
};
const APP_OUTDATED_TIMEOUT_MS = 5 * 60 * 1000; // 5 min
@ -198,6 +199,7 @@ const Main = ({
isAccountFrozen,
isAppConfigLoaded,
isFoldersSidebarShown,
activeGiftAuction,
}: OwnProps & StateProps) => {
const {
initMain,
@ -258,6 +260,7 @@ const Main = ({
loadAllStories,
loadAllHiddenStories,
loadContentSettings,
loadActiveGiftAuction,
loadPromoData,
} = getActions();
@ -439,6 +442,15 @@ const Main = ({
});
}, [currentUserId]);
// Refresh active gift auction subscription
const auctionTimeout = activeGiftAuction?.timeout;
const auctionGiftId = activeGiftAuction?.gift.id;
useInterval(() => {
if (auctionGiftId) {
loadActiveGiftAuction({ giftId: auctionGiftId });
}
}, auctionTimeout ? auctionTimeout * 1000 : undefined);
// Restore Transition slide class after async rendering
useLayoutEffect(() => {
const container = containerRef.current!;
@ -635,6 +647,7 @@ export default memo(withGlobal<OwnProps>(
payment,
limitReachedModal,
deleteFolderDialogModal,
activeGiftAuction,
} = selectTabState(global);
const { wasTimeFormatSetManually, foldersPosition } = selectSharedSettings(global);
@ -693,6 +706,7 @@ export default memo(withGlobal<OwnProps>(
isAccountFrozen,
isAppConfigLoaded: global.isAppConfigLoaded,
isFoldersSidebarShown: foldersPosition === FOLDERS_POSITION_LEFT && !isMobile && selectAreFoldersPresent(global),
activeGiftAuction,
};
},
)(Main));

View File

@ -556,7 +556,7 @@ const ActionMessageText = ({
case 'starGift': {
const {
gift, alreadyPaidUpgradeStars, peerId, savedId, fromId, isPrepaidUpgrade,
gift, alreadyPaidUpgradeStars, peerId, savedId, fromId, isPrepaidUpgrade, isAuctionAcquired,
} = action;
const isToChannel = Boolean(peerId && savedId);
@ -572,6 +572,10 @@ const ActionMessageText = ({
const starsAmount = gift.stars + (alreadyPaidUpgradeStars || 0);
const cost = renderStrong(formatStarsAsText(lang, starsAmount));
if (isAuctionAcquired) {
return lang('ActionStarGiftAuctionWon', { cost }, { withNodes: true });
}
if (isPrepaidUpgrade && gift.upgradeStars) {
const upgradeCost = renderStrong(formatStarsAsText(lang, gift.upgradeStars));

View File

@ -72,6 +72,11 @@
opacity: 0.85;
background-color: transparent !important;
}
.icon {
margin-inline-end: 0.25rem;
font-size: 1rem !important;
}
}
.with-gift &--quick-button {

View File

@ -13,7 +13,7 @@ import buildClassName from '../../../util/buildClassName';
import { tryParseDeepLink } from '../../../util/deepLinkParser';
import trimText from '../../../util/trimText';
import renderText from '../../common/helpers/renderText';
import { getWebpageButtonLangKey } from './helpers/webpageType';
import { getWebpageButtonIcon, getWebpageButtonLangKey } from './helpers/webpageType';
import useDynamicColorListener from '../../../hooks/stickers/useDynamicColorListener';
import useEnsureStory from '../../../hooks/useEnsureStory';
@ -23,6 +23,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import Audio from '../../common/Audio';
import Document from '../../common/Document';
import EmojiIconBackground from '../../common/embedded/EmojiIconBackground';
import Icon from '../../common/icons/Icon';
import PeerColorWrapper from '../../common/PeerColorWrapper';
import SafeLink from '../../common/SafeLink';
import StickerView from '../../common/StickerView';
@ -30,6 +31,7 @@ import Button from '../../ui/Button';
import BaseStory from './BaseStory';
import Photo from './Photo';
import Video from './Video';
import WebPageStarGiftAuction from './WebPageStarGiftAuction';
import WebPageUniqueGift from './WebPageUniqueGift';
import './WebPage.scss';
@ -37,6 +39,7 @@ import './WebPage.scss';
const MAX_TEXT_LENGTH = 170; // symbols
const WEBPAGE_STORY_TYPE = 'telegram_story';
const WEBPAGE_GIFT_TYPE = 'telegram_nft';
const WEBPAGE_AUCTION_TYPE = 'telegram_auction';
const STICKER_SIZE = 80;
const EMOJI_SIZE = 38;
@ -145,11 +148,14 @@ const WebPage: FC<OwnProps & StateProps> = ({
const { mediaSize } = messageWebPage;
const isStory = type === WEBPAGE_STORY_TYPE;
const isGift = type === WEBPAGE_GIFT_TYPE;
const isAuction = type === WEBPAGE_AUCTION_TYPE;
const isExpiredStory = story && 'isDeleted' in story;
const resultType = stickers?.isEmoji ? 'telegram_emojiset' : type;
const quickButtonLangKey = !isExpiredStory ? getWebpageButtonLangKey(resultType) : undefined;
const auctionEndDate = isAuction && webPage.auction ? webPage.auction.endDate : undefined;
const quickButtonLangKey = !isExpiredStory ? getWebpageButtonLangKey(resultType, auctionEndDate) : undefined;
const quickButtonTitle = quickButtonLangKey && lang(quickButtonLangKey);
const quickButtonIcon = getWebpageButtonIcon(resultType);
const truncatedDescription = trimText(description, MAX_TEXT_LENGTH);
const isArticle = Boolean(truncatedDescription || title || siteName);
@ -167,20 +173,21 @@ const WebPage: FC<OwnProps & StateProps> = ({
!isArticle && 'no-article',
document && 'with-document',
quickButtonTitle && 'with-quick-button',
isGift && 'with-gift',
(isGift || isAuction) && 'with-gift',
);
function renderQuickButton(caption: string) {
function renderQuickButton() {
return (
<Button
className="WebPage--quick-button"
size="tiny"
color="translucent"
isRectangular
noForcedUpperCase
noForcedUpperCase={!isAuction}
onClick={handleOpenTelegramLink}
>
{caption}
{quickButtonIcon && <Icon name={quickButtonIcon} />}
{quickButtonTitle}
</Button>
);
}
@ -195,7 +202,7 @@ const WebPage: FC<OwnProps & StateProps> = ({
<div className={buildClassName(
'WebPage--content',
isStory && 'is-story',
isGift && 'is-gift',
(isGift || isAuction) && 'is-gift',
)}
>
{backgroundEmojiId && (
@ -215,6 +222,14 @@ const WebPage: FC<OwnProps & StateProps> = ({
onClick={handleOpenTelegramLink}
/>
)}
{isAuction && webPage.auction && (
<WebPageStarGiftAuction
auction={webPage.auction}
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
onClick={handleOpenTelegramLink}
/>
)}
{isArticle && (
<div
className={buildClassName('WebPage-text', 'WebPage-text_interactive')}
@ -224,12 +239,12 @@ const WebPage: FC<OwnProps & StateProps> = ({
{title && (
<p className="site-title">{renderText(title)}</p>
)}
{truncatedDescription && !isGift && (
{truncatedDescription && !isGift && !isAuction && (
<p className="site-description">{renderText(truncatedDescription, ['emoji', 'br'])}</p>
)}
</div>
)}
{photo && !isGift && !video && !document && (
{photo && !isGift && !isAuction && !video && !document && (
<Photo
photo={photo}
isOwn={message?.isOutgoing}
@ -310,7 +325,7 @@ const WebPage: FC<OwnProps & StateProps> = ({
</div>
)}
</div>
{quickButtonTitle && renderQuickButton(quickButtonTitle)}
{quickButtonTitle && renderQuickButton()}
</PeerColorWrapper>
);
};

View File

@ -0,0 +1,69 @@
.root {
cursor: var(--custom-cursor, pointer);
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
min-width: 12.5rem;
margin-top: 0;
padding-top: 2rem;
padding-bottom: 0.75rem;
border-radius: 0.25rem;
font-weight: var(--font-weight-semibold);
}
.background {
position: absolute;
inset: 0;
border-radius: inherit;
}
.stickerWrapper {
position: relative;
z-index: 1;
width: 7.5rem;
height: 7.5rem;
margin-block: 0.5rem;
}
.title {
position: relative;
z-index: 1;
font-size: 0.9375rem;
font-weight: var(--font-weight-bold);
line-height: 1.25rem;
}
.subtitle {
position: relative;
z-index: 1;
font-size: 0.8125rem;
font-weight: var(--font-weight-medium);
line-height: 1rem;
opacity: 0.75;
}
.badge {
position: absolute;
z-index: 2;
top: 0.5rem;
left: 0.5rem;
padding: 0.125rem 0.375rem;
border-radius: 1rem;
font-size: 0.6875rem;
font-weight: var(--font-weight-medium);
line-height: 0.75rem;
backdrop-filter: blur(0.5rem);
}

View File

@ -0,0 +1,103 @@
import { memo, useMemo, useRef, useState } from '../../../lib/teact/teact';
import type { ApiWebPageAuctionData } from '../../../api/types';
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
import buildClassName from '../../../util/buildClassName';
import { getServerTime } from '../../../util/serverTime';
import useFlag from '../../../hooks/useFlag';
import { type ObserveFn } from '../../../hooks/useIntersectionObserver';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import GiftEffectWrapper from '../../common/gift/GiftEffectWrapper';
import RadialPatternBackground from '../../common/profile/RadialPatternBackground';
import StickerView from '../../common/StickerView';
import TextTimer from '../../ui/TextTimer';
import styles from './WebPageStarGiftAuction.module.scss';
type OwnProps = {
auction: ApiWebPageAuctionData;
observeIntersectionForLoading?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn;
onClick?: NoneToVoidFunction;
};
const GIFT_STICKER_SIZE = 120;
const DEFAULT_CENTER_COLOR = '#254e7a';
const DEFAULT_EDGE_COLOR = '#0f2a49';
const WebPageStarGiftAuction = ({
auction,
observeIntersectionForLoading,
observeIntersectionForPlaying,
onClick,
}: OwnProps) => {
const lang = useLang();
const stickerRef = useRef<HTMLDivElement>();
const [isHover, markHover, unmarkHover] = useFlag();
const { gift, endDate } = auction;
const { background, title, availabilityTotal, isSoldOut } = gift;
const textColor = background?.textColor || '#ffffff';
const [isFinished, setIsFinished] = useState(() => endDate < getServerTime());
const handleTimerEnd = useLastCallback(() => {
setIsFinished(true);
});
const backgroundColors = useMemo(() => {
const centerColor = background?.centerColor || DEFAULT_CENTER_COLOR;
const edgeColor = background?.edgeColor || DEFAULT_EDGE_COLOR;
return [centerColor, edgeColor];
}, [background]);
const subtitleText = useMemo(() => {
if (isFinished || isSoldOut) {
return lang('GiftAuctionSoldOut');
}
return lang('GiftAuctionGifts', { count: availabilityTotal || 0 }, { pluralValue: availabilityTotal || 0 });
}, [availabilityTotal, isFinished, isSoldOut, lang]);
return (
<div
className={buildClassName('interactive-gift', styles.root)}
style={`color: ${textColor}`}
onClick={onClick}
onMouseEnter={!IS_TOUCH_ENV ? markHover : undefined}
onMouseLeave={!IS_TOUCH_ENV ? unmarkHover : undefined}
>
<RadialPatternBackground
className={styles.background}
backgroundColors={backgroundColors}
withAdaptiveHeight
/>
<div className={styles.badge} style={`background-color: ${backgroundColors[0]}`}>
{isFinished ? lang('GiftAuctionFinished') : <TextTimer endsAt={endDate} onEnd={handleTimerEnd} />}
</div>
<GiftEffectWrapper
ref={stickerRef}
className={styles.stickerWrapper}
withSparkles
sparklesColor={textColor}
>
<StickerView
containerRef={stickerRef}
sticker={gift.sticker}
size={GIFT_STICKER_SIZE}
shouldLoop={isHover}
observeIntersectionForPlaying={observeIntersectionForPlaying}
observeIntersectionForLoading={observeIntersectionForLoading}
/>
</GiftEffectWrapper>
<div className={styles.title}>{title}</div>
<div className={styles.subtitle}>{subtitleText}</div>
</div>
);
};
export default memo(WebPageStarGiftAuction);

View File

@ -27,7 +27,8 @@
.message-content {
position: relative;
max-width: var(--max-width);
&.gift {
&.gift,
&.auction {
--max-width: 18rem;
}

View File

@ -72,23 +72,31 @@ const StarGiftAction = ({
const peer = isOutgoing ? recipient : sender;
const isChannel = peer && isApiPeerChat(peer) && isChatChannel(peer);
const isAuction = action.isAuctionAcquired;
const backgroundColor = useDynamicColorListener(ref, 'background-color', !action.gift.availabilityTotal);
const fallbackPeerTitle = lang('ActionFallbackSomeone');
const peerTitle = peer && getPeerTitle(lang, peer);
const auctionToTitle = recipient && getPeerTitle(lang, recipient);
const isSelf = sender?.id === recipient?.id;
const auctionBid = isAuction ? action.gift.stars : undefined;
const giftDescription = useMemo(() => {
const peerLink = renderPeerLink(peer?.id, peerTitle || fallbackPeerTitle);
const starsAmount = action.starsToConvert !== undefined
? formatStarsAsText(lang, action.starsToConvert) : undefined;
if (isAuction && auctionBid !== undefined) {
return lang('ActionStarGiftAuctionBought', { cost: formatStarsAsText(lang, auctionBid) });
}
if (action.isUpgraded) {
return lang('ActionStarGiftUpgraded');
}
if (action.alreadyPaidUpgradeStars) {
if (action.alreadyPaidUpgradeStars && !isAuction) {
return translateWithYou(
lang, 'ActionStarGiftUpgradeText', !isOutgoing || isSelf, { peer: peerLink },
);
@ -100,7 +108,7 @@ const StarGiftAction = ({
);
}
if (starGiftMaxConvertPeriod && getServerTime() < message.date + starGiftMaxConvertPeriod) {
if (starGiftMaxConvertPeriod && getServerTime() < message.date + starGiftMaxConvertPeriod && starsAmount) {
return translateWithYou(
lang, 'ActionStarGiftConvertText', !isOutgoing || isSelf, { peer: peerLink, amount: starsAmount },
);
@ -116,8 +124,8 @@ const StarGiftAction = ({
lang, 'ActionStarGiftNoConvertText', !isOutgoing || isSelf, { peer: peerLink },
);
}, [
action, fallbackPeerTitle, isChannel, isOutgoing, lang, message.date, peer?.id, peerTitle, starGiftMaxConvertPeriod,
isSelf,
action, auctionBid, fallbackPeerTitle, isAuction, isChannel, isOutgoing, lang, message.date, peer?.id, peerTitle,
starGiftMaxConvertPeriod, isSelf,
]);
return (
@ -157,7 +165,11 @@ const StarGiftAction = ({
)}
<div className={styles.info}>
<h3 className={styles.title}>
{isSelf ? lang('ActionStarGiftSelf') : lang(
{isAuction && recipient ? lang(
'ActionStarGiftAuctionFor',
{ peer: renderPeerLink(recipient.id, auctionToTitle || fallbackPeerTitle) },
{ withNodes: true },
) : isSelf ? lang('ActionStarGiftSelf') : lang(
isOutgoing ? 'ActionStarGiftTo' : 'ActionStarGiftFrom',
{
peer: renderPeerLink(peer?.id, peerTitle || fallbackPeerTitle),
@ -188,7 +200,8 @@ export default memo(withGlobal<OwnProps>(
const messageSender = selectSender(global, message);
const giftSender = action.fromId ? selectPeer(global, action.fromId) : undefined;
const messageRecipient = message.isOutgoing ? selectPeer(global, message.chatId) : currentUser;
const giftRecipient = action.peerId ? selectPeer(global, action.peerId) : undefined;
const giftRecipientId = action.toId || action.peerId;
const giftRecipient = giftRecipientId ? selectPeer(global, giftRecipientId) : undefined;
return {
canPlayAnimatedEmojis,

View File

@ -144,6 +144,10 @@ export function buildContentClassName(
if (webPage.gift) {
classNames.push('gift');
}
if (webPage.auction) {
classNames.push('auction');
}
}
if (invoice && !invoice.extendedMedia) {

View File

@ -1,7 +1,10 @@
import type { IconName } from '../../../../types/icons';
import type { RegularLangKey } from '../../../../types/language';
import { getServerTime } from '../../../../util/serverTime';
// https://github.com/telegramdesktop/tdesktop/blob/3da787791f6d227f69b32bf4003bc6071d05e2ac/Telegram/SourceFiles/history/view/history_view_view_button.cpp#L51
export function getWebpageButtonLangKey(type?: string): RegularLangKey | undefined {
export function getWebpageButtonLangKey(type?: string, auctionEndDate?: number): RegularLangKey | undefined {
switch (type) {
case 'telegram_channel_request':
case 'telegram_megagroup_request':
@ -37,7 +40,18 @@ export function getWebpageButtonLangKey(type?: string): RegularLangKey | undefin
return 'ViewButtonEmojiset';
case 'telegram_nft':
return 'ViewButtonGiftUnique';
case 'telegram_auction': {
const isFinished = auctionEndDate !== undefined && auctionEndDate * 1000 < getServerTime();
return isFinished ? 'PollViewResults' : 'GiftAuctionJoin';
}
default:
return undefined;
}
}
export function getWebpageButtonIcon(type?: string): IconName | undefined {
if (type === 'telegram_auction') {
return 'auction-filled';
}
return undefined;
}

View File

@ -19,6 +19,11 @@ import CollectibleInfoModal from './collectible/CollectibleInfoModal.async';
import DeleteAccountModal from './deleteAccount/DeleteAccountModal.async';
import EmojiStatusAccessModal from './emojiStatusAccess/EmojiStatusAccessModal.async';
import FrozenAccountModal from './frozenAccount/FrozenAccountModal.async';
import GiftAuctionAcquiredModal from './gift/auction/GiftAuctionAcquiredModal.async';
import GiftAuctionBidModal from './gift/auction/GiftAuctionBidModal.async';
import GiftAuctionChangeRecipientModal from './gift/auction/GiftAuctionChangeRecipientModal.async';
import GiftAuctionInfoModal from './gift/auction/GiftAuctionInfoModal.async';
import GiftAuctionModal from './gift/auction/GiftAuctionModal.async';
import PremiumGiftModal from './gift/GiftModal.async';
import GiftInfoModal from './gift/info/GiftInfoModal.async';
import GiftLockedModal from './gift/locked/GiftLockedModal.async';
@ -94,6 +99,11 @@ type ModalKey = keyof Pick<TabState,
'locationAccessModal' |
'aboutAdsModal' |
'giftUpgradeModal' |
'giftAuctionModal' |
'giftAuctionBidModal' |
'giftAuctionInfoModal' |
'giftAuctionChangeRecipientModal' |
'giftAuctionAcquiredModal' |
'starGiftPriceDecreaseInfoModal' |
'monetizationVerificationModal' |
'giftWithdrawModal' |
@ -161,6 +171,11 @@ const MODALS: ModalRegistry = {
locationAccessModal: LocationAccessModal,
aboutAdsModal: AboutAdsModal,
giftUpgradeModal: GiftUpgradeModal,
giftAuctionModal: GiftAuctionModal,
giftAuctionBidModal: GiftAuctionBidModal,
giftAuctionInfoModal: GiftAuctionInfoModal,
giftAuctionChangeRecipientModal: GiftAuctionChangeRecipientModal,
giftAuctionAcquiredModal: GiftAuctionAcquiredModal,
starGiftPriceDecreaseInfoModal: StarGiftPriceDecreaseInfoModal,
monetizationVerificationModal: VerificationMonetizationModal,
giftWithdrawModal: GiftWithdrawModal,

View File

@ -0,0 +1,44 @@
.table {
overflow: hidden;
display: grid;
grid-template-columns: max-content 1fr;
flex-shrink: 0;
gap: 1px;
border: 1px solid var(--color-borders);
border-radius: 1rem;
background-color: var(--color-borders);
}
.cell {
position: relative;
display: flex;
align-items: center;
min-height: 2.5rem;
padding: 0.25rem 0.5rem;
font-size: 0.9375rem;
}
.title {
background-color: var(--color-background-secondary);
}
.value {
min-width: 2rem;
overflow-wrap: anywhere;
background-color: var(--color-background);
}
.fullWidth {
grid-column: 1 / -1;
}
.chatItem {
width: fit-content;
color: var(--color-primary);
background-color: var(--color-background);
}

View File

@ -0,0 +1,64 @@
import { memo, type TeactNode } from '../../../lib/teact/teact';
import { getActions } from '../../../global';
import buildClassName from '../../../util/buildClassName';
import useLastCallback from '../../../hooks/useLastCallback';
import PeerChip from '../../common/PeerChip';
import styles from './TableInfo.module.scss';
type ChatItem = { chatId: string; withEmojiStatus?: boolean };
export type TableData = [TeactNode | undefined, TeactNode | ChatItem][];
type OwnProps = {
tableData?: TableData;
className?: string;
onChatClick?: (peerId: string) => void;
};
const TableInfo = ({
tableData,
className,
onChatClick,
}: OwnProps) => {
const { openChat } = getActions();
const handleOpenChat = useLastCallback((peerId: string) => {
if (onChatClick) {
onChatClick(peerId);
} else {
openChat({ id: peerId });
}
});
if (!tableData?.length) {
return undefined;
}
return (
<div className={buildClassName(styles.table, className)}>
{tableData.map(([label, value]) => (
<>
{Boolean(label) && <div className={buildClassName(styles.cell, styles.title)}>{label}</div>}
<div className={buildClassName(styles.cell, styles.value, !label && styles.fullWidth)}>
{typeof value === 'object' && 'chatId' in value ? (
<PeerChip
peerId={value.chatId}
className={styles.chatItem}
forceShowSelf
withEmojiStatus={value.withEmojiStatus}
clickArg={value.chatId}
onClick={handleOpenChat}
/>
) : value}
</div>
</>
))}
</div>
);
};
export default memo(TableInfo);

View File

@ -1,5 +1,3 @@
@use '../../../styles/mixins';
.content {
overflow-x: hidden;
display: flex;
@ -10,55 +8,10 @@
padding-inline: 1rem !important;
}
.title {
background-color: var(--color-background-secondary);
}
.value {
min-width: 2rem;
overflow-wrap: anywhere;
background-color: var(--color-background);
}
.table {
overflow: hidden;
display: grid;
grid-template-columns: max-content 1fr;
flex-shrink: 0;
gap: 1px;
border: 1px solid var(--color-borders);
border-radius: 1rem;
background-color: var(--color-borders);
}
.noFooter {
margin-top: 1.5rem;
}
.cell {
position: relative;
display: flex;
align-items: center;
min-height: 2.5rem;
padding: 0.25rem 0.5rem;
font-size: 0.9375rem;
}
.fullWidth {
grid-column: 1 / -1;
}
.avatar {
align-self: center;
}
.chatItem {
width: fit-content;
color: var(--color-primary);
background-color: var(--color-background);
}

View File

@ -9,15 +9,13 @@ import buildClassName from '../../../util/buildClassName';
import useLastCallback from '../../../hooks/useLastCallback';
import Avatar from '../../common/Avatar';
import PeerChip from '../../common/PeerChip';
import Button from '../../ui/Button';
import Modal from '../../ui/Modal';
import TableInfo, { type TableData } from './TableInfo';
import styles from './TableInfoModal.module.scss';
type ChatItem = { chatId: string; withEmojiStatus?: boolean };
export type TableData = [TeactNode | undefined, TeactNode | ChatItem][];
export type { TableData };
type OwnProps = {
isOpen?: boolean;
@ -59,7 +57,8 @@ const TableInfoModal = ({
currencyInBalanceBar,
}: OwnProps) => {
const { openChat } = getActions();
const handleOpenChat = useLastCallback((peerId: string) => {
const handleChatClick = useLastCallback((peerId: string) => {
openChat({ id: peerId });
onClose();
});
@ -84,25 +83,7 @@ const TableInfoModal = ({
<Avatar peer={headerAvatarPeer} size="jumbo" className={styles.avatar} />
)}
{header}
<div className={buildClassName(styles.table, tableClassName)}>
{tableData?.map(([label, value]) => (
<>
{Boolean(label) && <div className={buildClassName(styles.cell, styles.title)}>{label}</div>}
<div className={buildClassName(styles.cell, styles.value, !label && styles.fullWidth)}>
{typeof value === 'object' && 'chatId' in value ? (
<PeerChip
peerId={value.chatId}
className={styles.chatItem}
forceShowSelf
withEmojiStatus={value.withEmojiStatus}
clickArg={value.chatId}
onClick={handleOpenChat}
/>
) : value}
</div>
</>
))}
</div>
<TableInfo tableData={tableData} className={tableClassName} onChatClick={handleChatClick} />
{footer}
{buttonText && (
<Button

View File

@ -144,6 +144,7 @@
margin-inline-end: 0.125rem !important;
}
.bottomDescription,
.description {
margin-bottom: 0.5rem;
margin-left: 1rem;
@ -151,12 +152,23 @@
color: var(--color-text-secondary);
}
.bottomDescription {
margin-bottom: 1rem;
}
.main-button {
display: flex;
font-size: 1rem;
font-weight: var(--font-weight-semibold);
}
.buttonSubtitle {
display: block;
font-size: 0.8125rem;
font-weight: var(--font-weight-normal);
opacity: 0.7;
}
.star {
--color-fill: var(--color-white);

View File

@ -7,7 +7,7 @@ import { getActions, withGlobal } from '../../../global';
import type { ThemeKey } from '../../../types';
import type { GiftOption } from './GiftModal';
import {
type ApiMessage, type ApiPeer, type ApiStarsAmount, MAIN_THREAD_ID,
type ApiMessage, type ApiPeer, type ApiStarGiftAuctionState, type ApiStarsAmount, MAIN_THREAD_ID,
} from '../../../api/types';
import { getPeerTitle, isApiPeerUser } from '../../../global/helpers/peers';
@ -16,8 +16,11 @@ import {
} from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import buildStyle from '../../../util/buildStyle';
import { formatCountdown } from '../../../util/dates/dateFormat';
import { HOUR } from '../../../util/dates/units';
import { formatCurrency } from '../../../util/formatCurrency';
import { formatStarsAsIcon } from '../../../util/localization/format';
import { getServerTime } from '../../../util/serverTime';
import useCustomBackground from '../../../hooks/useCustomBackground';
import useLang from '../../../hooks/useLang';
@ -30,6 +33,7 @@ import Link from '../../ui/Link';
import ListItem from '../../ui/ListItem';
import Switcher from '../../ui/Switcher';
import TextArea from '../../ui/TextArea';
import TextTimer from '../../ui/TextTimer';
import styles from './GiftComposer.module.scss';
@ -53,9 +57,11 @@ export type StateProps = {
paidMessagesStars?: number;
areUniqueStarGiftsDisallowed?: boolean;
shouldDisallowLimitedStarGifts?: boolean;
activeGiftAuction?: ApiStarGiftAuctionState;
};
const LIMIT_DISPLAY_THRESHOLD = 50;
const TEXT_TIMER_THRESHOLD = 48 * HOUR;
function GiftComposer({
gift,
@ -74,9 +80,11 @@ function GiftComposer({
paidMessagesStars,
areUniqueStarGiftsDisallowed,
shouldDisallowLimitedStarGifts,
activeGiftAuction,
}: OwnProps & StateProps) {
const {
sendStarGift, sendPremiumGiftByStars, openInvoice, openGiftUpgradeModal, openStarsBalanceModal,
openGiftAuctionBidModal, openGiftAuctionInfoModal, openGiftAuctionChangeRecipientModal,
} = getActions();
const lang = useLang();
@ -180,7 +188,31 @@ function GiftComposer({
openStarsBalanceModal({});
});
const handleLearnMoreClick = useLastCallback(() => {
openGiftAuctionInfoModal({});
});
const handleMainButtonClick = useLastCallback(() => {
if (activeGiftAuction) {
const existingBidPeerId = activeGiftAuction.userState.bidPeerId;
if (existingBidPeerId && existingBidPeerId !== peerId) {
openGiftAuctionChangeRecipientModal({
oldPeerId: existingBidPeerId,
newPeerId: peerId,
message: giftMessage || undefined,
shouldHideName: shouldHideName || undefined,
});
return;
}
openGiftAuctionBidModal({
peerId,
message: giftMessage || undefined,
shouldHideName: shouldHideName || undefined,
});
return;
}
if (isStarGift) {
sendStarGift({
peerId,
@ -328,6 +360,12 @@ function GiftComposer({
? formatStarsAsIcon(lang, gift.stars + (shouldPayForUpgrade ? gift.upgradeStars! : 0), { asFont: true })
: isPremiumGift ? formatCurrency(lang, gift.amount, gift.currency) : undefined;
const giftsPerRound = activeGiftAuction?.gift.giftsPerRound;
const auctionEndDate = activeGiftAuction?.state.endDate;
const auctionTimeLeft = auctionEndDate ? auctionEndDate - getServerTime() : undefined;
const shouldUseTextTimer = auctionTimeLeft !== undefined && auctionTimeLeft > 0
&& auctionTimeLeft < TEXT_TIMER_THRESHOLD;
return (
<div className={styles.footer}>
{isStarGift && Boolean(gift.availabilityRemains) && (
@ -341,13 +379,37 @@ function GiftComposer({
className={styles.limited}
/>
)}
{activeGiftAuction && Boolean(giftsPerRound) && (
<div className={styles.bottomDescription}>
{lang('GiftAuctionDescription', {
count: giftsPerRound,
link: <Link isPrimary onClick={handleLearnMoreClick}>{lang('GiftAuctionLearnMore')}</Link>,
}, { pluralValue: giftsPerRound, withNodes: true })}
</div>
)}
<Button
className={styles.mainButton}
size="smaller"
size={auctionTimeLeft ? undefined : 'smaller'}
onClick={handleMainButtonClick}
isLoading={isPaymentFormLoading}
noForcedUpperCase
>
{lang('GiftSend', {
{activeGiftAuction ? (
<div>
<div>
{lang('GiftAuctionPlaceBid')}
</div>
{auctionTimeLeft !== undefined && auctionTimeLeft > 0 && (
<div className={styles.buttonSubtitle}>
{lang('GiftAuctionTimeLeft', {
time: shouldUseTextTimer
? <TextTimer endsAt={auctionEndDate!} />
: formatCountdown(lang, auctionTimeLeft),
}, { withNodes: true })}
</div>
)}
</div>
) : lang('GiftSend', {
amount,
}, {
withNodes: true,
@ -434,6 +496,7 @@ export default memo(withGlobal<OwnProps>(
paidMessagesStars,
areUniqueStarGiftsDisallowed,
shouldDisallowLimitedStarGifts,
activeGiftAuction: tabState.activeGiftAuction,
};
},
)(GiftComposer));

View File

@ -40,6 +40,14 @@
&:hover::before {
opacity: 1;
}
&.noClickable {
cursor: default;
&:hover::before {
opacity: 0;
}
}
}
.starGift {
@ -80,6 +88,7 @@
font-size: 0.75rem !important;
}
.auction,
.premiumRequired {
outline: 0.125rem solid #D18D21;
outline-offset: -0.125rem;

View File

@ -28,8 +28,10 @@ export type OwnProps = {
gift: ApiStarGift;
isResale?: boolean;
withTransferBadge?: boolean;
hideBadge?: boolean;
noClickable?: boolean;
observeIntersection?: ObserveFn;
onClick: (gift: ApiStarGift, target: 'original' | 'resell') => void;
onClick?: (gift: ApiStarGift, target: 'original' | 'resell') => void;
};
type StateProps = {
@ -39,9 +41,11 @@ type StateProps = {
const GIFT_STICKER_SIZE = 90;
function GiftItemStar({
gift, isResale, isCurrentUserPremium, withTransferBadge, observeIntersection, onClick,
gift, isResale, isCurrentUserPremium, withTransferBadge, hideBadge, noClickable, observeIntersection, onClick,
}: OwnProps & StateProps) {
const { openGiftInfoModal, openPremiumModal, showNotification, checkCanSendGift } = getActions();
const {
openGiftInfoModal, openPremiumModal, showNotification, checkCanSendGift, openGiftAuctionModal,
} = getActions();
const ref = useRef<HTMLDivElement>();
const stickerRef = useRef<HTMLDivElement>();
@ -77,11 +81,17 @@ function GiftItemStar({
? lang.number(resellMinStars) + '+' : priceInfo?.amount || 0;
const isLimited = !isGiftUnique && Boolean(regularGift?.isLimited);
const isSoldOut = !isGiftUnique && Boolean(regularGift?.isSoldOut);
const isAuction = !isGiftUnique && !isResale && Boolean(regularGift?.isAuction);
const isPremiumRequired = Boolean(gift?.requirePremium);
const isUserLimitReached = Boolean(regularGift?.limitedPerUser && !regularGift?.perUserRemains);
const perUserTotal = regularGift?.perUserTotal;
const handleGiftClick = useLastCallback(() => {
if (isAuction) {
openGiftAuctionModal({ gift });
return;
}
if (isSoldOut && !isResale) {
openGiftInfoModal({ gift });
return;
@ -110,12 +120,12 @@ function GiftItemStar({
if (isLocked) {
checkCanSendGift({
gift,
onSuccess: () => onClick(gift, isResale ? 'resell' : 'original'),
onSuccess: () => onClick?.(gift, isResale ? 'resell' : 'original'),
});
return;
}
onClick(gift, isResale ? 'resell' : 'original');
onClick?.(gift, isResale ? 'resell' : 'original');
});
const radialPatternBackdrop = useMemo(() => {
@ -162,14 +172,18 @@ function GiftItemStar({
if (isSoldOut) {
return <GiftRibbon color="red" text={lang('GiftSoldOut')} />;
}
if (isAuction) {
return <GiftRibbon color="orange" text={lang('GiftRibbonAuction')} />;
}
if (isPremiumRequired) {
return <GiftRibbon color="orange" text={lang('LimitPremium')} />;
return <GiftRibbon color="orange" text={lang('GiftRibbonPremium')} />;
}
if (isLimited) {
return <GiftRibbon color="blue" text={lang('GiftLimited')} />;
}
return undefined;
}, [isGiftUnique, isResale, gift, isSoldOut, isLimited, lang, giftNumber, isPremiumRequired]);
}, [isGiftUnique, isResale, gift, isSoldOut,
isLimited, lang, giftNumber, isPremiumRequired, isAuction]);
useOnIntersect(ref, observeIntersection, (entry) => {
const visible = entry.isIntersecting;
@ -181,6 +195,10 @@ function GiftItemStar({
return lang('GiftTransferTitle');
}
if (isAuction) {
return lang('GiftAuctionJoin');
}
if (priceCurrency === TON_CURRENCY_CODE) {
return formatTonAsIcon(lang, formattedPrice || 0, {
shouldConvertFromNanos: true,
@ -192,7 +210,7 @@ function GiftItemStar({
asFont: true,
className: styles.star,
});
}, [withTransferBadge, priceCurrency, formattedPrice, lang]);
}, [withTransferBadge, priceCurrency, formattedPrice, isAuction, lang]);
return (
<div
@ -203,12 +221,14 @@ function GiftItemStar({
styles.starGift,
'starGiftItem',
isPremiumRequired && styles.premiumRequired,
isAuction && styles.auction,
noClickable && styles.noClickable,
)}
tabIndex={0}
role="button"
onClick={handleGiftClick}
onMouseEnter={!IS_TOUCH_ENV ? markHover : undefined}
onMouseLeave={!IS_TOUCH_ENV ? unmarkHover : undefined}
tabIndex={noClickable ? undefined : 0}
role={noClickable ? undefined : 'button'}
onClick={noClickable ? undefined : handleGiftClick}
onMouseEnter={!IS_TOUCH_ENV && !noClickable ? markHover : undefined}
onMouseLeave={!IS_TOUCH_ENV && !noClickable ? unmarkHover : undefined}
>
{radialPatternBackdrop}
@ -230,20 +250,22 @@ function GiftItemStar({
)}
</div>
<Button
className={buildClassName(
styles.buy,
withTransferBadge && styles.transferBadge,
)}
nonInteractive
size="tiny"
color={isGiftUnique ? 'bluredStarsBadge' : 'stars'}
withSparkleEffect={isVisible && !withTransferBadge}
pill
fluid
>
{badgeContent}
</Button>
{!hideBadge && (
<Button
className={buildClassName(
styles.buy,
withTransferBadge && styles.transferBadge,
)}
nonInteractive
size="tiny"
color={isGiftUnique ? 'bluredStarsBadge' : 'stars'}
withSparkleEffect={isVisible && !withTransferBadge}
pill
fluid
>
{badgeContent}
</Button>
)}
{giftRibbon}
{isLocked && <Icon name="lock-badge" className={styles.lockIcon} />}
</div>

View File

@ -16,7 +16,7 @@
.root :global(.modal-dialog),
.transition,
.content {
height: min(92vh, 49rem);
height: min(98vh, 49rem);
max-height: none !important;
}

View File

@ -41,7 +41,7 @@ import InfiniteScroll from '../../ui/InfiniteScroll';
import Modal from '../../ui/Modal';
import Transition from '../../ui/Transition';
import BalanceBlock from '../stars/BalanceBlock';
import GiftSendingOptions from './GiftComposer';
import GiftComposer from './GiftComposer';
import GiftItemPremium from './GiftItemPremium';
import GiftItemStar from './GiftItemStar';
import GiftModalResaleScreen from './GiftModalResaleScreen';
@ -104,6 +104,8 @@ const GiftModal: FC<OwnProps & StateProps> = ({
closeResaleGiftsMarket,
loadMyUniqueGifts,
openGiftTransferConfirmModal,
setGiftModalSelectedGift,
clearActiveGiftAuction,
} = getActions();
const dialogRef = useRef<HTMLDivElement>();
const transitionRef = useRef<HTMLDivElement>();
@ -118,7 +120,7 @@ const GiftModal: FC<OwnProps & StateProps> = ({
const user = peer && isApiPeerUser(peer) ? peer : undefined;
const chat = peer && isApiPeerChat(peer) ? peer : undefined;
const [selectedGift, setSelectedGift] = useState<GiftOption | undefined>();
const selectedGift = renderingModal?.selectedGift;
const [shouldShowMainScreenHeader, setShouldShowMainScreenHeader] = useState(false);
const [isMainScreenHeaderForStarGifts, setIsMainScreenHeaderForStarGifts] = useState(false);
const [isGiftScreenHeaderForStarGifts, setIsGiftScreenHeaderForStarGifts] = useState(false);
@ -192,10 +194,14 @@ const GiftModal: FC<OwnProps & StateProps> = ({
useEffect(() => {
if (!isOpen) {
setShouldShowMainScreenHeader(false);
setSelectedGift(undefined);
setGiftModalSelectedGift({ gift: undefined });
setSelectedCategory('all');
}
}, [isOpen, tabId, closeResaleGiftsMarket]);
}, [isOpen]);
useEffect(() => {
setIsGiftScreenHeaderForStarGifts(Boolean(selectedGift && 'id' in selectedGift));
}, [selectedGift]);
const handleScroll = useLastCallback((e: React.UIEvent<HTMLDivElement>) => {
if (isGiftScreen) return;
@ -303,8 +309,7 @@ const GiftModal: FC<OwnProps & StateProps> = ({
openGiftInMarket({ gift, tabId });
return;
}
setSelectedGift(gift);
setIsGiftScreenHeaderForStarGifts('id' in gift);
setGiftModalSelectedGift({ gift });
});
const handleMyGiftClick = useLastCallback((gift: ApiStarGift) => {
@ -423,7 +428,7 @@ const GiftModal: FC<OwnProps & StateProps> = ({
});
const handleCloseModal = useLastCallback(() => {
setSelectedGift(undefined);
setGiftModalSelectedGift({ gift: undefined });
resetResaleGifts();
closeGiftModal();
});
@ -434,7 +439,8 @@ const GiftModal: FC<OwnProps & StateProps> = ({
return;
}
if (isGiftScreen) {
setSelectedGift(undefined);
setGiftModalSelectedGift({ gift: undefined });
clearActiveGiftAuction();
return;
}
handleCloseModal();
@ -589,7 +595,7 @@ const GiftModal: FC<OwnProps & StateProps> = ({
/>
)}
{isGiftScreen && renderingModal?.forPeerId && (
<GiftSendingOptions
<GiftComposer
gift={selectedGift}
giftByStars={giftsByStars.get(selectedGift)}
peerId={renderingModal.forPeerId}

View File

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

View File

@ -0,0 +1,50 @@
.modal {
max-width: 26rem;
}
.content {
display: flex;
flex-direction: column;
padding-top: 0 !important;
padding-inline: 1.25rem !important;
}
.giftsListContainer {
position: relative;
}
.giftsList {
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 1rem;
max-height: 29.5rem;
}
.giftHeader {
display: flex;
gap: 0.5rem;
align-items: center;
font-size: 0.9375rem;
font-weight: var(--font-weight-medium);
}
.bidValue {
display: flex;
align-items: center;
line-height: 1rem;
}
.badge {
margin-inline-start: 0.5rem;
}
.starIcon {
font-size: 1rem;
}
.okButton {
margin-top: 0.5rem;
}

View File

@ -0,0 +1,133 @@
import { memo, useMemo, useRef } from '../../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../../global';
import type { ApiStarGiftAuctionAcquiredGift, ApiSticker } from '../../../../api/types';
import type { TabState } from '../../../../global/types';
import { selectTabState } from '../../../../global/selectors';
import { formatDateTimeToString } from '../../../../util/dates/dateFormat';
import { formatStarsAsIcon } from '../../../../util/localization/format';
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback';
import useScrollNotch from '../../../../hooks/useScrollNotch';
import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker';
import BadgeButton from '../../../common/BadgeButton';
import Button from '../../../ui/Button';
import Modal from '../../../ui/Modal';
import TableInfo, { type TableData } from '../../common/TableInfo';
import styles from './GiftAuctionAcquiredModal.module.scss';
export type OwnProps = {
modal: TabState['giftAuctionAcquiredModal'];
};
type StateProps = {
acquiredGifts?: ApiStarGiftAuctionAcquiredGift[];
giftTitle?: string;
giftSticker?: ApiSticker;
};
const GiftAuctionAcquiredModal = ({ modal, acquiredGifts, giftTitle, giftSticker }: OwnProps & StateProps) => {
const { closeGiftAuctionAcquiredModal } = getActions();
const containerRef = useRef<HTMLDivElement>();
const lang = useLang();
const isOpen = Boolean(modal?.giftId);
const renderingGifts = useCurrentOrPrev(acquiredGifts);
const renderingGiftTitle = useCurrentOrPrev(giftTitle);
const renderingGiftSticker = useCurrentOrPrev(giftSticker);
const handleClose = useLastCallback(() => {
closeGiftAuctionAcquiredModal();
});
const giftItems = useMemo(() => {
if (!renderingGifts?.length) return undefined;
return renderingGifts.map((gift) => {
const header = lang('GiftAuctionBoughtGiftHeader', {
gift: renderingGiftTitle || lang('StarGift'),
giftNumber: gift.giftNumber ? lang.number(gift.giftNumber) : '',
round: lang.number(gift.round),
});
const tableData: TableData = [
[undefined, (
<span className={styles.giftHeader}>
{renderingGiftSticker && (
<AnimatedIconFromSticker
className={styles.giftSticker}
sticker={renderingGiftSticker}
size={20}
play={false}
/>
)}
<span>{header}</span>
</span>
)],
[lang('GiftAuctionRecipient'), { chatId: gift.peerId }],
[lang('GiftAuctionDate'), formatDateTimeToString(gift.date * 1000, lang.code, true)],
[lang('GiftAuctionAcceptedBid'), (
<span className={styles.bidValue}>
{formatStarsAsIcon(lang, gift.bidAmount, { className: styles.starIcon })}
<BadgeButton className={styles.badge}>
{lang('GiftAuctionTopPosition', { position: gift.position })}
</BadgeButton>
</span>
)],
];
return { tableData, key: `${gift.round}-${gift.giftNumber}` };
});
}, [renderingGifts, renderingGiftTitle, renderingGiftSticker, lang]);
const giftsCount = renderingGifts?.length || 0;
useScrollNotch({
containerRef,
selector: `.${styles.giftsList}`,
isBottomNotch: true,
}, [giftItems]);
return (
<Modal
isOpen={isOpen}
hasCloseButton
title={lang('GiftAuctionBoughtGiftsTitle', { count: giftsCount }, { pluralValue: giftsCount })}
className={styles.modal}
contentClassName={styles.content}
onClose={handleClose}
isCondensedHeader
isSlim
>
<div className={styles.giftsListContainer} ref={containerRef}>
<div className={styles.giftsList}>
{giftItems?.map((item) => (
<TableInfo key={item.key} tableData={item.tableData} />
))}
</div>
</div>
<Button className={styles.okButton} onClick={handleClose}>
{lang('OK')}
</Button>
</Modal>
);
};
export default memo(withGlobal<OwnProps>(
(global): Complete<StateProps> => {
const { giftAuctionAcquiredModal } = selectTabState(global);
return {
acquiredGifts: giftAuctionAcquiredModal?.acquiredGifts,
giftTitle: giftAuctionAcquiredModal?.giftTitle,
giftSticker: giftAuctionAcquiredModal?.giftSticker,
};
},
)(GiftAuctionAcquiredModal));

View File

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

View File

@ -0,0 +1,232 @@
.content {
display: flex;
flex-direction: column;
gap: 0.25rem;
max-height: min(92vh, 38rem) !important;
}
.headerControlPanel {
position: absolute;
z-index: 3;
top: 0.75rem;
right: 1.25rem;
display: flex;
align-items: center;
}
.slider {
flex-shrink: 0;
margin-top: 2rem;
}
.title {
margin-top: 1rem;
margin-bottom: 0.5rem;
font-size: 1.5rem;
font-weight: var(--font-weight-medium);
line-height: 1.5rem;
text-align: center;
}
.subtitle {
margin-bottom: 1rem;
font-size: 0.875rem;
color: var(--color-text-secondary);
text-align: center;
}
.infoCards {
display: flex;
gap: 0.5rem;
padding-bottom: 0.5rem;
}
.infoCard {
flex: 1;
padding: 0.75rem;
border-radius: 0.75rem;
text-align: center;
background: var(--color-background-secondary);
}
.infoCardValue {
display: flex;
gap: 0.25rem;
align-items: center;
justify-content: center;
font-size: 1.125rem;
font-weight: var(--font-weight-bold);
}
.infoCardLabel {
font-size: 0.75rem;
line-height: 1rem;
color: var(--color-text-secondary);
}
.separator {
margin-top: 1rem;
}
.winningStatus,
.bidderInfoSlide {
display: flex;
gap: 0.5rem;
align-items: center;
}
.winningText {
font-size: 0.9375rem;
font-weight: var(--font-weight-medium);
color: var(--color-green);
}
.winningBadge {
padding: 0.0625rem 0.5rem;
border-radius: 0.875rem;
font-size: 0.75rem;
line-height: 0.875rem;
color: var(--color-text-green);
background-color: rgba(var(--color-text-green-rgb), 0.1);
}
.section {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.sectionTitle {
font-size: 0.9375rem;
font-weight: var(--font-weight-medium);
color: var(--color-primary);
}
.sectionTitleTransition {
height: 1.25rem;
}
.bidderRow {
display: flex;
gap: 0.25rem;
align-items: center;
padding: 0.25rem 0;
}
.bidderPosition {
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
min-width: 1.5rem;
font-size: 0.875rem;
font-weight: var(--font-weight-medium);
}
.bidderInfo {
display: flex;
flex: 1;
gap: 0.5rem;
align-items: center;
height: 2.125rem;
}
.bidderName {
overflow: hidden;
flex: 1;
font-size: 0.9375rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.topBidderRow {
position: relative;
display: flex;
gap: 0.25rem;
align-items: center;
padding: 0.25rem 0;
&::after {
content: "";
position: absolute;
right: 0;
bottom: 0;
left: 4.4375rem;
height: 1px;
opacity: 0.75;
background-color: var(--color-borders);
}
&:last-child::after {
display: none;
}
}
.topBidderPosition {
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
min-width: 1.5rem;
font-size: 1.125rem;
}
.bidderAmount {
display: flex;
gap: 0.25rem;
align-items: center;
font-size: 0.9375rem;
font-weight: var(--font-weight-medium);
color: var(--color-text-secondary);
}
.buttonStar {
margin-inline-start: 0 !important;
}
.giftSticker {
margin-right: 0.25rem;
}
.customBidInput {
position: relative;
:global(.input-group) {
margin-bottom: 0;
}
:global(.form-control) {
padding-left: 2rem;
}
}
.customBidInputIcon {
position: absolute;
z-index: 1;
top: 50%;
left: 0.75rem;
transform: translateY(-50%);
font-size: 1rem;
}

View File

@ -0,0 +1,422 @@
import { memo, useEffect, useMemo, useState } from '../../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../../global';
import type {
ApiPeer,
ApiStarGiftAuctionState,
ApiStarsAmount,
} from '../../../../api/types';
import type { TabState } from '../../../../global/types';
import { selectPeer, selectTabState } from '../../../../global/selectors';
import { formatStarsAsIcon } from '../../../../util/localization/format';
import renderText from '../../../common/helpers/renderText';
import { useTransitionActiveKey } from '../../../../hooks/animations/useTransitionActiveKey';
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
import useFlag from '../../../../hooks/useFlag';
import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback';
import AnimatedCounter from '../../../common/AnimatedCounter';
import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker';
import Avatar from '../../../common/Avatar';
import FullNameTitle from '../../../common/FullNameTitle';
import StarIcon from '../../../common/icons/StarIcon';
import Button from '../../../ui/Button';
import ConfirmDialog from '../../../ui/ConfirmDialog';
import InputText from '../../../ui/InputText';
import Modal from '../../../ui/Modal';
import TextTimer from '../../../ui/TextTimer';
import Transition from '../../../ui/Transition';
import StarSlider from '../../paidReaction/StarSlider';
import BalanceBlock from '../../stars/BalanceBlock';
import styles from './GiftAuctionBidModal.module.scss';
export type OwnProps = {
modal: TabState['giftAuctionBidModal'];
};
type StateProps = {
auctionState?: ApiStarGiftAuctionState;
starBalance?: ApiStarsAmount;
currentUserPeer?: ApiPeer;
topBidderIds?: string[];
};
const DEFAULT_BID_AMOUNT = 50;
const MAX_BID_AMOUNT_STEP = 50000;
const MAX_CUSTOM_BID_AMOUNT = 1000000000;
const BID_ROUNDING_STEP = 10000;
const MIN_SLIDER_PROGRESS = 0.25;
const GIFT_STICKER_SIZE = 24;
const DEFAULT_TOP_BIDDERS_COUNT = 3;
const GiftAuctionBidModal = ({
modal,
auctionState,
starBalance,
currentUserPeer,
topBidderIds,
}: OwnProps & StateProps) => {
const { closeGiftAuctionBidModal, sendStarGiftAuctionBid, loadActiveGiftAuction } = getActions();
const isOpen = Boolean(modal?.isOpen);
const renderingAuctionState = useCurrentOrPrev(auctionState);
const renderingTopBidderIds = useCurrentOrPrev(topBidderIds);
const renderingTopBidderPeers = useMemo(() => {
if (!renderingTopBidderIds) return undefined;
const global = getGlobal();
return renderingTopBidderIds
.map((id) => selectPeer(global, id))
.filter(Boolean);
}, [renderingTopBidderIds]);
const [topBidder1, topBidder2, topBidder3] = renderingTopBidderPeers || [];
const topBidder1Key = useTransitionActiveKey([topBidder1?.id || '0']);
const topBidder2Key = useTransitionActiveKey([topBidder2?.id || '0']);
const topBidder3Key = useTransitionActiveKey([topBidder3?.id || '0']);
const giftsPerRound = renderingAuctionState?.gift.giftsPerRound || 0;
const lang = useLang();
const activeState = renderingAuctionState?.state.type === 'active'
? renderingAuctionState.state
: undefined;
const userState = renderingAuctionState?.userState;
const [selectedBidAmount, setSelectedBidAmount] = useState(DEFAULT_BID_AMOUNT);
const [isCustomBidModalOpen, openCustomBidModal, closeCustomBidModal] = useFlag();
const [customBidValue, setCustomBidValue] = useState('');
const baseMinBid = activeState?.minBidAmount || DEFAULT_BID_AMOUNT;
const currentMinBid = userState?.minBidAmount || baseMinBid;
const sliderMaxValue = Math.ceil(currentMinBid / BID_ROUNDING_STEP) * BID_ROUNDING_STEP + MAX_BID_AMOUNT_STEP;
const currentProgress = (currentMinBid - baseMinBid) / (sliderMaxValue - baseMinBid);
const adjustedMinBid = Math.floor(
(currentMinBid - MIN_SLIDER_PROGRESS * sliderMaxValue) / (1 - MIN_SLIDER_PROGRESS),
);
const giftMinBid = currentProgress > MIN_SLIDER_PROGRESS
? Math.max(1, adjustedMinBid)
: baseMinBid;
useEffect(() => {
setSelectedBidAmount(currentMinBid);
}, [currentMinBid]);
const nextRoundAt = activeState?.nextRoundAt;
const bidDifference = userState?.bidAmount ? selectedBidAmount - userState.bidAmount : 0;
const isAtMaxValue = selectedBidAmount >= sliderMaxValue;
const sliderSecondaryText = useMemo(() => {
if (isAtMaxValue) return lang('GiftAuctionTapToBidMore');
if (bidDifference <= 0) return undefined;
return (
<>
+
<AnimatedCounter text={lang.number(bidDifference)} />
</>
);
}, [bidDifference, isAtMaxValue, lang]);
const handleAmountChange = useLastCallback((value: number) => {
setSelectedBidAmount(value);
});
const handleTimerEnd = useLastCallback(() => {
if (!renderingAuctionState?.gift.id) return;
loadActiveGiftAuction({ giftId: renderingAuctionState.gift.id });
});
const handleBadgeClick = useLastCallback(() => {
if (isAtMaxValue) {
openCustomBidModal();
}
});
const handleCustomBidChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.replace(/\D/g, '');
const numValue = Number(value);
if (numValue > MAX_CUSTOM_BID_AMOUNT) return;
setCustomBidValue(value);
});
const handleCustomBidSubmit = useLastCallback(() => {
if (!renderingAuctionState?.gift.id || !modal) return;
const resultValue = Number(customBidValue);
if (resultValue < currentMinBid) return;
setSelectedBidAmount(resultValue);
closeCustomBidModal();
setCustomBidValue('');
const { peerId, message, shouldHideName } = modal;
const isUpdateBid = Boolean(userState?.bidAmount);
sendStarGiftAuctionBid({
giftId: renderingAuctionState.gift.id,
bidAmount: resultValue,
peerId,
message: message ? { text: message } : undefined,
shouldHideName,
isUpdateBid,
});
});
const handleSubmit = useLastCallback(() => {
if (!renderingAuctionState?.gift.id || !modal) return;
const { peerId, message, shouldHideName } = modal;
const isUpdateBid = Boolean(userState?.bidAmount);
sendStarGiftAuctionBid({
giftId: renderingAuctionState.gift.id,
bidAmount: selectedBidAmount,
peerId,
message: message ? { text: message } : undefined,
shouldHideName,
isUpdateBid,
});
});
const userPosition = useMemo(() => {
if (!selectedBidAmount || !activeState?.bidLevels?.length) return undefined;
const { bidLevels } = activeState;
const userBidDate = userState?.bidDate || Number.MAX_SAFE_INTEGER;
for (const level of bidLevels) {
if (level.amount < selectedBidAmount
|| (level.amount === selectedBidAmount && level.date >= userBidDate)) {
return level.pos;
}
}
return bidLevels[bidLevels.length - 1].pos + 1;
}, [selectedBidAmount, activeState, userState?.bidDate]);
function renderInfoCards() {
return (
<div className={styles.infoCards}>
<div className={styles.infoCard}>
<div className={styles.infoCardValue}>
<StarIcon type="gold" size="adaptive" />
{lang.number(currentMinBid)}
</div>
<div className={styles.infoCardLabel}>{lang('GiftAuctionMinimumBid')}</div>
</div>
<div className={styles.infoCard}>
<div className={styles.infoCardValue}>
<TextTimer endsAt={nextRoundAt || 0} shouldShowZeroOnEnd onEnd={handleTimerEnd} />
</div>
<div className={styles.infoCardLabel}>{lang('GiftAuctionUntilNextRound')}</div>
</div>
<div className={styles.infoCard}>
<div className={styles.infoCardValue}>
<AnimatedIconFromSticker
noLoop={false}
className={styles.giftSticker}
sticker={renderingAuctionState?.gift.sticker}
size={GIFT_STICKER_SIZE}
/>
{lang.number(activeState?.giftsLeft || 0)}
</div>
<div className={styles.infoCardLabel}>{lang('GiftAuctionLeft')}</div>
</div>
</div>
);
}
const isWinning = Boolean(userState?.bidAmount && userPosition && userPosition <= giftsPerRound);
function renderCurrentBidSectionTitle() {
const giftTitle = renderingAuctionState?.gift.title || lang('StarGift');
const nextGiftNum = userPosition && userPosition <= 100
? (activeState?.lastGiftNum || 0) + userPosition
: undefined;
return (
<Transition
name="fade"
activeKey={isWinning ? 0 : 1}
className={styles.sectionTitleTransition}
slideClassName={styles.bidderInfoSlide}
>
{isWinning ? (
<div className={styles.winningStatus}>
<span className={styles.winningText}>{lang('GiftAuctionYoureWinning')}</span>
<span className={styles.winningBadge}>
{lang('GiftUnique', { title: giftTitle, number: nextGiftNum ? lang.number(nextGiftNum) : undefined })}
</span>
</div>
) : (
<div className={styles.sectionTitle}>{lang('GiftAuctionYourBidWillBe')}</div>
)}
</Transition>
);
}
function renderUserBid() {
return (
<div className={styles.section}>
{renderCurrentBidSectionTitle()}
<div className={styles.bidderRow}>
<div className={styles.bidderPosition}>
{userPosition && userPosition > 100 ? `${userPosition}+` : (userPosition || 1)}
</div>
<div className={styles.bidderInfo}>
{currentUserPeer && <Avatar peer={currentUserPeer} size="small" />}
{currentUserPeer && <FullNameTitle peer={currentUserPeer} className={styles.bidderName} />}
</div>
<div className={styles.bidderAmount}>
<StarIcon type="gold" size="adaptive" />
{lang.number(selectedBidAmount)}
</div>
</div>
</div>
);
}
function renderTopBidderRow(
index: number,
emoji: string,
peer: ApiPeer | undefined,
amount: number | undefined,
activeKey: number,
) {
return (
<div className={styles.topBidderRow} key={index}>
<div className={styles.topBidderPosition}>
{renderText(emoji, ['emoji'])}
</div>
<Transition
name="fade"
activeKey={activeKey}
className={styles.bidderInfo}
slideClassName={styles.bidderInfoSlide}
>
{peer && (
<>
<Avatar peer={peer} size="small" />
<FullNameTitle peer={peer} className={styles.bidderName} />
</>
)}
</Transition>
{amount !== undefined && (
<div className={styles.bidderAmount}>
<StarIcon type="gold" size="adaptive" />
{lang.number(amount)}
</div>
)}
</div>
);
}
function renderTopWinners() {
const topCount = DEFAULT_TOP_BIDDERS_COUNT;
const bidLevels = activeState?.bidLevels;
return (
<div className={styles.section}>
<div className={styles.sectionTitle}>
{lang('GiftAuctionTopWinners', { count: topCount }, { pluralValue: topCount })}
</div>
{renderTopBidderRow(0, '🥇', topBidder1, bidLevels?.[0]?.amount, topBidder1Key)}
{renderTopBidderRow(1, '🥈', topBidder2, bidLevels?.[1]?.amount, topBidder2Key)}
{renderTopBidderRow(2, '🥉', topBidder3, bidLevels?.[2]?.amount, topBidder3Key)}
</div>
);
}
return (
<Modal
isOpen={isOpen}
hasAbsoluteCloseButton
isSlim
contentClassName={styles.content}
onClose={closeGiftAuctionBidModal}
isLowStackPriority
>
<div className={styles.headerControlPanel}>
<BalanceBlock balance={starBalance} className={styles.modalBalance} withAddButton />
</div>
<StarSlider
className={styles.slider}
defaultValue={currentMinBid}
minValue={giftMinBid}
minAllowedValue={currentMinBid}
maxValue={sliderMaxValue}
floatingBadgeDescription={sliderSecondaryText}
onChange={handleAmountChange}
onBadgeClick={handleBadgeClick}
shouldUseDynamicColor
shouldAllowCustomValue
/>
<h3 className={styles.title}>{lang('GiftAuctionPlaceBid')}</h3>
{renderInfoCards()}
{renderUserBid()}
{renderTopWinners()}
<Button noForcedUpperCase onClick={handleSubmit}>
{lang(userState?.bidAmount ? 'GiftAuctionAddToBid' : 'GiftAuctionPlaceBidButton', {
amount: formatStarsAsIcon(lang,
userState?.bidAmount ? selectedBidAmount - userState.bidAmount : selectedBidAmount,
{ asFont: true, className: styles.buttonStar }),
}, { withNodes: true })}
</Button>
<ConfirmDialog
isOpen={isCustomBidModalOpen}
title={lang('GiftAuctionCustomBidTitle')}
confirmLabel={lang('GiftAuctionCustomBidButton')}
isConfirmDisabled={!customBidValue || Number(customBidValue) < currentMinBid}
confirmHandler={handleCustomBidSubmit}
onClose={closeCustomBidModal}
>
<p>{lang('GiftAuctionCustomBidDescription', { count: renderingAuctionState?.gift.giftsPerRound })}</p>
<div className={styles.customBidInput}>
<StarIcon type="gold" size="adaptive" className={styles.customBidInputIcon} />
<InputText
value={customBidValue}
onChange={handleCustomBidChange}
placeholder={lang('GiftAuctionCustomBidPlaceholder')}
inputMode="numeric"
teactExperimentControlled
/>
</div>
</ConfirmDialog>
</Modal>
);
};
export default memo(withGlobal<OwnProps>(
(global): Complete<StateProps> => {
const { activeGiftAuction } = selectTabState(global);
const { stars, currentUserId } = global;
const currentUserPeer = currentUserId ? selectPeer(global, currentUserId) : undefined;
const topBidderIds = activeGiftAuction?.state.type === 'active'
? activeGiftAuction.state.topBidders
: undefined;
return {
auctionState: activeGiftAuction,
starBalance: stars?.balance,
currentUserPeer,
topBidderIds,
};
},
)(GiftAuctionBidModal));

View File

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

View File

@ -0,0 +1,14 @@
.preview {
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: center;
margin-top: 0.5rem;
margin-bottom: 1rem;
}
.arrow {
font-size: 2rem;
color: var(--color-text-secondary);
}

View File

@ -0,0 +1,90 @@
import { memo } from '../../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../../global';
import type { ApiPeer } from '../../../../api/types';
import type { TabState } from '../../../../global/types';
import { getPeerTitle } from '../../../../global/helpers/peers';
import { selectPeer } from '../../../../global/selectors';
import { REM } from '../../../common/helpers/mediaDimensions';
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback';
import Avatar from '../../../common/Avatar';
import Icon from '../../../common/icons/Icon';
import ConfirmDialog from '../../../ui/ConfirmDialog';
import styles from './GiftAuctionChangeRecipientModal.module.scss';
export type OwnProps = {
modal: TabState['giftAuctionChangeRecipientModal'];
};
type StateProps = {
oldPeer?: ApiPeer;
newPeer?: ApiPeer;
};
const AVATAR_SIZE = 4 * REM;
const GiftAuctionChangeRecipientModal = ({ modal, oldPeer, newPeer }: OwnProps & StateProps) => {
const { closeGiftAuctionChangeRecipientModal, openGiftAuctionBidModal } = getActions();
const lang = useLang();
const isOpen = Boolean(modal?.isOpen);
const renderingOldPeer = useCurrentOrPrev(oldPeer);
const renderingNewPeer = useCurrentOrPrev(newPeer);
const renderingModal = useCurrentOrPrev(modal);
const handleConfirm = useLastCallback(() => {
if (!renderingModal) return;
closeGiftAuctionChangeRecipientModal();
openGiftAuctionBidModal({
peerId: renderingModal.newPeerId,
message: renderingModal.message,
shouldHideName: renderingModal.shouldHideName,
});
});
if (!renderingOldPeer || !renderingNewPeer) return undefined;
return (
<ConfirmDialog
isOpen={isOpen}
title={lang('GiftAuctionChangeRecipientTitle')}
onClose={closeGiftAuctionChangeRecipientModal}
confirmLabel={lang('Continue')}
confirmHandler={handleConfirm}
>
<div className={styles.preview}>
<Avatar peer={renderingOldPeer} size={AVATAR_SIZE} />
<Icon name="next" className={styles.arrow} />
<Avatar peer={renderingNewPeer} size={AVATAR_SIZE} />
</div>
<p>
{lang('GiftAuctionChangeRecipientDescription', {
oldPeer: getPeerTitle(lang, renderingOldPeer),
newPeer: getPeerTitle(lang, renderingNewPeer),
}, {
withNodes: true,
withMarkdown: true,
})}
</p>
</ConfirmDialog>
);
};
export default memo(
withGlobal<OwnProps>((global, { modal }): Complete<StateProps> => {
const oldPeer = modal?.oldPeerId ? selectPeer(global, modal.oldPeerId) : undefined;
const newPeer = modal?.newPeerId ? selectPeer(global, modal.newPeerId) : undefined;
return {
oldPeer,
newPeer,
};
})(GiftAuctionChangeRecipientModal),
);

View File

@ -0,0 +1,14 @@
import type { OwnProps } from './GiftAuctionInfoModal';
import { Bundles } from '../../../../util/moduleLoader';
import useModuleLoader from '../../../../hooks/useModuleLoader';
const GiftAuctionInfoModalAsync = (props: OwnProps) => {
const { modal } = props;
const GiftAuctionInfoModal = useModuleLoader(Bundles.Stars, 'GiftAuctionInfoModal', !modal?.isOpen);
return GiftAuctionInfoModal ? <GiftAuctionInfoModal {...props} /> : undefined;
};
export default GiftAuctionInfoModalAsync;

View File

@ -0,0 +1,53 @@
.header {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-top: 0.125rem;
margin-bottom: 0.25rem;
}
.iconWrapper {
display: flex;
align-items: center;
justify-content: center;
width: 5rem;
height: 5rem;
margin-top: 0.25rem;
margin-bottom: 0.5rem;
border-radius: 50%;
font-size: 1rem;
background-color: var(--accent-color);
}
.icon {
font-size: 2.5rem;
color: var(--color-white);
}
.title {
padding-top: 0.25rem;
font-size: 1.5rem;
font-weight: var(--font-weight-medium);
text-align: center;
}
.subtitle {
padding-top: 0.25rem;
text-align: center;
}
.footer {
display: flex;
flex-direction: column;
align-self: stretch;
margin-top: 0.5rem;
}
.understoodIcon {
font-size: 1.1875rem;
}

View File

@ -0,0 +1,101 @@
import { memo, useMemo } from '../../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../../global';
import type { ApiStarGiftAuctionState } from '../../../../api/types';
import type { TabState } from '../../../../global/types';
import { selectTabState } from '../../../../global/selectors';
import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback';
import Icon from '../../../common/icons/Icon';
import Button from '../../../ui/Button';
import TableAboutModal, { type TableAboutData } from '../../common/TableAboutModal';
import styles from './GiftAuctionInfoModal.module.scss';
export type OwnProps = {
modal: TabState['giftAuctionInfoModal'];
};
type StateProps = {
activeGiftAuction?: ApiStarGiftAuctionState;
};
const GiftAuctionInfoModal = ({
modal,
activeGiftAuction,
}: OwnProps & StateProps) => {
const { closeGiftAuctionInfoModal } = getActions();
const lang = useLang();
const isOpen = Boolean(modal?.isOpen && activeGiftAuction);
const handleClose = useLastCallback(() => {
closeGiftAuctionInfoModal();
});
const header = useMemo(() => {
return (
<div className={styles.header}>
<div className={styles.iconWrapper}>
<Icon name="auction-filled" className={styles.icon} />
</div>
<div className={styles.title}>
{lang('GiftAuctionInfoTitle')}
</div>
<div className={styles.subtitle}>
{lang('GiftAuctionInfoSubtitle')}
</div>
</div>
);
}, [lang]);
const footer = useMemo(() => {
if (!isOpen) return undefined;
return (
<div className={styles.footer}>
<Button
iconName="understood"
iconClassName={styles.understoodIcon}
onClick={handleClose}
>
{lang('ButtonUnderstood')}
</Button>
</div>
);
}, [lang, isOpen, handleClose]);
const listItemData = useMemo(() => {
const count = activeGiftAuction?.gift.giftsPerRound || 0;
return [
['auction-drop', lang('GiftAuctionInfoTopBiddersTitle', { count }, { pluralValue: count }),
lang('GiftAuctionInfoTopBiddersSubtitle', { count }, { pluralValue: count })],
['auction-next-round', lang('GiftAuctionInfoBidCarryoverTitle'),
lang('GiftAuctionInfoBidCarryoverSubtitle', { count })],
['stars-refund', lang('GiftAuctionInfoMissedBiddersTitle'),
lang('GiftAuctionInfoMissedBiddersSubtitle')],
] satisfies TableAboutData;
}, [lang, activeGiftAuction]);
return (
<TableAboutModal
isOpen={isOpen}
header={header}
listItemData={listItemData}
footer={footer}
onClose={handleClose}
/>
);
};
export default memo(withGlobal<OwnProps>(
(global): Complete<StateProps> => {
const { activeGiftAuction } = selectTabState(global);
return {
activeGiftAuction,
};
},
)(GiftAuctionInfoModal));

View File

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

View File

@ -0,0 +1,82 @@
.modal :global(.modal-dialog) {
overflow: hidden;
}
.modalContent {
position: relative;
max-height: min(97vh, 48rem) !important;
}
.header {
display: flex;
flex-direction: column;
gap: 0.5rem;
align-items: center;
text-wrap: balance;
}
.title {
margin: 0;
margin-top: 0.25rem;
font-size: 1.5rem;
font-weight: var(--font-weight-medium);
line-height: 1.75rem;
}
.description {
margin: 0;
font-size: 0.875rem;
text-align: center;
}
.finishedBadge {
padding: 0.25rem 0.75rem;
border-radius: 1rem;
font-size: 0.8125rem;
font-weight: var(--font-weight-medium);
color: var(--color-text-secondary);
background-color: var(--color-background-secondary);
}
.giftName {
font-weight: var(--font-weight-bold);
white-space: nowrap;
}
.footer {
display: flex;
flex-direction: column;
gap: 0.75rem;
align-items: center;
}
.itemsBoughtLink {
display: flex;
gap: 0.25rem;
align-items: center;
}
.itemsBoughtSticker {
display: inline-flex;
vertical-align: middle;
}
.footerButton {
width: 100%;
margin-top: 0.5rem;
}
.buttonSubtitle {
display: block;
font-size: 0.8125rem;
font-weight: var(--font-weight-normal);
opacity: 0.7;
}
.starIcon {
line-height: 1rem !important;
}

View File

@ -0,0 +1,220 @@
import { memo, useMemo } from '../../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../../global';
import type { ApiStarGiftAuctionState } from '../../../../api/types';
import type { TabState } from '../../../../global/types';
import { selectTabState } from '../../../../global/selectors';
import { formatCountdown, formatDateTimeToString } from '../../../../util/dates/dateFormat';
import { HOUR } from '../../../../util/dates/units';
import { formatStarsAsIcon } from '../../../../util/localization/format';
import { getServerTime } from '../../../../util/serverTime';
import { getStickerFromGift } from '../../../common/helpers/gifts';
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback';
import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker';
import Button from '../../../ui/Button';
import Link from '../../../ui/Link';
import TextTimer from '../../../ui/TextTimer';
import TableInfoModal, { type TableData } from '../../common/TableInfoModal';
import GiftItemStar from '../GiftItemStar';
import styles from './GiftAuctionModal.module.scss';
const TEXT_TIMER_THRESHOLD = 48 * HOUR;
export type OwnProps = {
modal: TabState['giftAuctionModal'];
};
type StateProps = {
auctionState?: ApiStarGiftAuctionState;
};
const GiftAuctionModal = ({ modal, auctionState }: OwnProps & StateProps) => {
const {
closeGiftAuctionModal,
setGiftModalSelectedGift,
openGiftAuctionInfoModal,
openGiftAuctionAcquiredModal,
} = getActions();
const isOpen = Boolean(modal?.isOpen);
const renderingAuctionState = useCurrentOrPrev(auctionState);
const gift = renderingAuctionState?.gift;
const state = renderingAuctionState?.state;
const userState = renderingAuctionState?.userState;
const isFinished = state?.type === 'finished';
const lang = useLang();
const handleClose = useLastCallback(() => closeGiftAuctionModal());
const handleLearnMoreClick = useLastCallback(() => {
openGiftAuctionInfoModal({});
});
const handleItemsBoughtClick = useLastCallback(() => {
if (!gift) return;
const giftSticker = getStickerFromGift(gift);
openGiftAuctionAcquiredModal({ giftId: gift.id, giftTitle: gift.title, giftSticker });
});
const handleJoinClick = useLastCallback(() => {
if (!gift) return;
closeGiftAuctionModal({ shouldKeepActiveAuction: true });
setGiftModalSelectedGift({ gift });
});
const header = useMemo(() => {
if (!gift || !state) {
return undefined;
}
const giftTitle = gift.title || lang('StarGift');
const giftsPerRound = gift.giftsPerRound || 0;
return (
<div className={styles.header}>
<GiftItemStar gift={gift} hideBadge noClickable />
<h1 className={styles.title}>
{giftTitle}
</h1>
{isFinished ? (
<span className={styles.finishedBadge}>{lang('GiftAuctionEnded')}</span>
) : (
<p className={styles.description}>
{lang('GiftAuctionTopBidders', {
count: giftsPerRound,
gift: <span className={styles.giftName}>{giftTitle}</span>,
link: <Link isPrimary onClick={handleLearnMoreClick}>{lang('GiftAuctionLearnMore')}</Link>,
}, { pluralValue: giftsPerRound, withNodes: true, withMarkdown: true })}
</p>
)}
</div>
);
}, [gift, state, isFinished, lang, handleLearnMoreClick]);
const modalData = useMemo(() => {
if (!gift || !state || !userState) {
return undefined;
}
const tableData: TableData = [];
tableData.push([
lang('GiftAuctionStarted'),
formatDateTimeToString(state.startDate * 1000, lang.code, true),
]);
tableData.push([
lang('GiftAuctionEnds'),
formatDateTimeToString(state.endDate * 1000, lang.code, true),
]);
if (gift.availabilityTotal) {
tableData.push([
lang('GiftInfoAvailability'),
lang('GiftInfoAvailabilityValue', {
count: gift.availabilityRemains || 0,
total: lang.number(gift.availabilityTotal),
}, { pluralValue: gift.availabilityRemains || 0 }),
]);
}
if (state.type === 'active') {
tableData.push([
lang('GiftAuctionCurrentRound'),
lang('GiftAuctionRoundValue', {
current: lang.number(state.currentRound),
total: lang.number(state.totalRounds),
}),
]);
}
if (isFinished) {
tableData.push([
lang('GiftAuctionAveragePrice'),
formatStarsAsIcon(lang, state.averagePrice, { className: styles.starIcon }),
]);
}
const acquiredCount = userState.acquiredCount;
const giftSticker = getStickerFromGift(gift);
const auctionTimeLeft = state.endDate - getServerTime();
const shouldUseTextTimer = auctionTimeLeft > 0 && auctionTimeLeft < TEXT_TIMER_THRESHOLD;
const footer = (
<div className={styles.footer}>
{acquiredCount > 0 && (
<Link className={styles.itemsBoughtLink} isPrimary onClick={handleItemsBoughtClick}>
{lang('GiftAuctionItemsBought', {
count: acquiredCount,
gift: giftSticker && (
<AnimatedIconFromSticker
className={styles.itemsBoughtSticker}
sticker={giftSticker}
size={20}
play={false}
/>
),
}, { pluralValue: acquiredCount, withNodes: true })}
</Link>
)}
<Button
noForcedUpperCase
className={styles.footerButton}
onClick={isFinished ? handleClose : handleJoinClick}
>
{isFinished ? lang('OK') : (
<div>
<div>
{lang('GiftAuctionJoin')}
</div>
{auctionTimeLeft > 0 && (
<div className={styles.buttonSubtitle}>
{lang('GiftAuctionTimeLeft', {
time: shouldUseTextTimer
? <TextTimer endsAt={state.endDate} />
: formatCountdown(lang, auctionTimeLeft),
}, { withNodes: true })}
</div>
)}
</div>
)}
</Button>
</div>
);
return {
tableData,
footer,
};
}, [gift, state, userState, isFinished, lang, handleJoinClick, handleItemsBoughtClick, handleClose]);
return (
<TableInfoModal
isOpen={isOpen}
header={header}
footer={modalData?.footer}
tableData={modalData?.tableData}
className={styles.modal}
contentClassName={styles.modalContent}
onClose={handleClose}
/>
);
};
export default memo(withGlobal<OwnProps>(
(global): Complete<StateProps> => {
const { activeGiftAuction } = selectTabState(global);
return {
auctionState: activeGiftAuction,
};
},
)(GiftAuctionModal));

View File

@ -42,8 +42,8 @@
margin-bottom: 0;
}
.soldOut {
color: var(--color-error);
.warningDescription {
color: var(--color-error) !important;
}
.modalContent {
@ -51,18 +51,6 @@
max-height: min(97vh, 48rem) !important;
}
.headerSplitButton {
position: absolute;
right: 0.375rem;
display: flex;
flex-direction: row;
border-radius: 1rem;
backdrop-filter: blur(0.5rem);
}
.moreMenuButton {
position: absolute;
z-index: 1;

View File

@ -440,9 +440,12 @@ const GiftInfoModal = ({
const isVisibleForMe = isNameHidden && renderingTargetPeer;
const isWarningDescription = savedGift?.isRefunded || (!savedGift && gift?.type === 'starGift');
const description = (() => {
if (!savedGift) return lang('GiftInfoSoldOutDescription');
if (isTargetChat) return undefined;
if (savedGift.isRefunded) return lang('GiftInfoDescriptionRefunded');
if (savedGift.upgradeMsgId) return lang('GiftInfoDescriptionUpgraded');
if (canManage && savedGift.canUpgrade && savedGift.alreadyPaidUpgradeStars && !savedGift.upgradeMsgId) {
@ -592,7 +595,7 @@ const GiftInfoModal = ({
{getTitle()}
</h1>
{Boolean(description) && (
<p className={buildClassName(styles.description, !savedGift && gift?.type === 'starGift' && styles.soldOut)}>
<p className={buildClassName(styles.description, isWarningDescription && styles.warningDescription)}>
{description}
</p>
)}

View File

@ -5,10 +5,7 @@ import { getActions, withGlobal } from '../../../../global';
import type {
ApiPeer,
ApiStarGiftAttribute,
ApiStarGiftAttributeBackdrop,
ApiStarGiftAttributeModel,
ApiStarGiftAttributePattern,
} from '../../../../api/types';
import type { TabState } from '../../../../global/types';
import { ApiMediaFormat } from '../../../../api/types';
@ -17,6 +14,7 @@ import { getStickerMediaHash } from '../../../../global/helpers';
import { getPeerTitle } from '../../../../global/helpers/peers';
import { selectPeer } from '../../../../global/selectors';
import { fetch } from '../../../../util/mediaLoader';
import { getRandomGiftPreviewAttributes, type GiftPreviewAttributes } from '../../../common/helpers/gifts';
import useInterval from '../../../../hooks/schedulers/useInterval';
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
@ -42,12 +40,6 @@ type StateProps = {
recipient?: ApiPeer;
};
type Attributes = {
model: ApiStarGiftAttributeModel;
pattern: ApiStarGiftAttributePattern;
backdrop: ApiStarGiftAttributeBackdrop;
};
const PREVIEW_UPDATE_INTERVAL = 3000;
const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
@ -67,7 +59,7 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
const isPrepaid = Boolean(renderingModal?.gift?.prepaidUpgradeHash);
const [previewAttributes, setPreviewAttributes] = useState<Attributes | undefined>();
const [previewAttributes, setPreviewAttributes] = useState<GiftPreviewAttributes | undefined>();
const lang = useLang();
@ -111,7 +103,7 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
const updatePreviewAttributes = useLastCallback(() => {
if (!renderingModal?.sampleAttributes) return;
setPreviewAttributes(getRandomAttributes(renderingModal.sampleAttributes, previewAttributes));
setPreviewAttributes(getRandomGiftPreviewAttributes(renderingModal.sampleAttributes, previewAttributes));
});
const handleOpenPriceInfo = useLastCallback(() => {
@ -274,25 +266,3 @@ export default memo(withGlobal<OwnProps>(
};
},
)(GiftUpgradeModal));
function getRandomAttributes(list: ApiStarGiftAttribute[], previousSelection?: Attributes): Attributes {
const models = list.filter((attr): attr is ApiStarGiftAttributeModel => (
attr.type === 'model' && attr.name !== previousSelection?.model.name
));
const patterns = list.filter((attr): attr is ApiStarGiftAttributePattern => (
attr.type === 'pattern' && attr.name !== previousSelection?.pattern.name
));
const backdrops = list.filter((attr): attr is ApiStarGiftAttributeBackdrop => (
attr.type === 'backdrop' && attr.name !== previousSelection?.backdrop.name
));
const randomModel = models[Math.floor(Math.random() * models.length)];
const randomPattern = patterns[Math.floor(Math.random() * patterns.length)];
const randomBackdrop = backdrops[Math.floor(Math.random() * backdrops.length)];
return {
model: randomModel,
pattern: randomPattern,
backdrop: randomBackdrop,
};
}

View File

@ -1,7 +1,10 @@
@use "../../../styles/mixins";
/* stylelint-disable plugin/no-low-performance-animation-properties */
.root {
--_size: 1.875rem;
--_transition: 0.15s;
--progress: 0;
position: relative;
@ -9,6 +12,24 @@
padding-top: 4rem;
@include mixins.reset-range();
&.dragging {
.progress {
transition: background-color var(--_transition);
}
.floatingBadgeWrapper {
transition: none;
}
.floatingBadgeText {
transition: background-color var(--_transition);
}
.floatingBadgeTriangle {
transition: none;
}
}
}
.slider {
@ -79,6 +100,8 @@
background-image: var(--stars-gradient);
transition: width var(--_transition), background-color var(--_transition);
&::after {
content: "";
@ -93,12 +116,14 @@
background-color: white;
}
&.dynamicColor {
background-color: var(--dynamic-color);
background-image: none;
}
}
.floatingBadgeWrapper {
--_min-x: 0;
--_max-x: 100%;
pointer-events: none;
position: absolute;
@ -107,11 +132,13 @@
transform:
translateX(
clamp(
var(--_min-x),
var(--min-badge-x, 0px),
calc(var(--_size) / 2 + var(--progress) * (100% - var(--_size))),
var(--_max-x),
var(--max-badge-x, 100%)
)
);
transition: transform var(--_transition);
}
.floatingBadge {
@ -121,12 +148,15 @@
top: -1rem;
left: 0;
transform: translate(-50%, -100%);
&.clickable {
pointer-events: auto;
cursor: pointer;
}
}
.floatingBadgeText {
display: flex;
gap: 0.125rem;
align-items: center;
position: relative;
padding: 0.5rem 1rem;
border-radius: 2rem;
@ -138,6 +168,65 @@
white-space: nowrap;
background-image: var(--stars-gradient);
transition:
border-radius var(--_transition),
width var(--_transition),
background-color var(--_transition);
&.dynamicColor {
background-color: var(--dynamic-color);
background-image: none;
}
&.noTransition {
transition: none;
}
}
.floatingBadgeSparkles {
position: absolute;
inset: 0;
color: white;
}
.floatingBadgeContent {
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 1.5rem;
&.withDescription {
.floatingBadgeTitle {
transform: translateY(-0.5rem);
font-size: 1rem;
}
.floatingBadgeDescription {
opacity: 0.8;
}
}
}
.floatingBadgeTitle {
display: flex;
gap: 0.125rem;
align-items: center;
transition: transform var(--_transition), font-size var(--_transition);
}
.floatingBadgeDescription {
position: absolute;
bottom: -0.125rem;
font-size: 0.75rem;
opacity: 0;
transition: opacity var(--_transition);
}
.floatingBadgeTriangle {
@ -145,5 +234,22 @@
z-index: -1;
bottom: 0;
left: 50%;
transform: translate(-50%, 33%);
transition: transform var(--_transition);
}
.floatingBadgeTrianglePath {
transition: fill var(--_transition);
}
.customValueIcon {
pointer-events: none;
position: absolute;
z-index: 2;
right: 0.375rem;
bottom: 0.375rem;
font-size: 1.125rem;
color: var(--color-text-secondary);
}

View File

@ -1,14 +1,15 @@
import type React from '../../../lib/teact/teact';
import type { TeactNode } from '../../../lib/teact/teact';
import {
memo, useMemo, useRef, useState,
} from '../../../lib/teact/teact';
memo, useEffect,
useMemo, useRef, useState } from '../../../lib/teact/teact';
import { requestMeasure, requestMutation } from '../../../lib/fasterdom/fasterdom';
import buildClassName from '../../../util/buildClassName';
import { formatInteger } from '../../../util/textFormat';
import buildStyle from '../../../util/buildStyle';
import { REM } from '../../common/helpers/mediaDimensions';
import useEffectOnce from '../../../hooks/useEffectOnce';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import usePrevious from '../../../hooks/usePrevious';
import useResizeObserver from '../../../hooks/useResizeObserver';
import AnimatedCounter from '../../common/AnimatedCounter';
@ -20,87 +21,264 @@ import styles from './StarSlider.module.scss';
type OwnProps = {
maxValue: number;
defaultValue: number;
minValue?: number;
minAllowedValue?: number;
className?: string;
floatingBadgeDescription?: TeactNode;
shouldUseDynamicColor?: boolean;
shouldAllowCustomValue?: boolean;
onChange: (value: number) => void;
onBadgeClick?: NoneToVoidFunction;
};
const DEFAULT_POINTS = [50, 100, 500, 1000, 2000, 5000, 10000];
const LARGE_STEP = 10000;
const THUMB_SIZE_IN_PIXELS = 1.875 * REM;
const BEAK_WIDTH_IN_PIXELS = 28;
const DEFAULT_RADIUS_IN_REM = 2;
const MIN_RADIUS_IN_REM = 0.375;
const BADGE_HORIZONTAL_PADDING = 2 * REM;
const BADGE_ICON_SIZE = 1.5 * REM;
const BADGE_TITLE_GAP = 0.125 * REM;
const DRAG_DISTANCE_THRESHOLD = 5;
const BADGE_WIDTH_DELTA = 6;
let textMeasureCanvas: HTMLCanvasElement | undefined;
function getTextWidth(text: string, font: string): number {
if (!textMeasureCanvas) {
textMeasureCanvas = document.createElement('canvas');
}
const ctx = textMeasureCanvas.getContext('2d')!;
ctx.font = font;
return ctx.measureText(text).width;
}
const SLIDER_COLORS = [
'#955CDB', // Purple
'#955CDB', // Purple
'#46A3EB', // Blue
'#40A920', // Green
'#E29A09', // Yellow
'#ED771E', // Orange
'#E14542', // Red
'#596473', // Silver (100% only)
];
function getColorForProgress(progress: number): string {
if (progress >= 1) return SLIDER_COLORS[SLIDER_COLORS.length - 1];
const regularColorsCount = SLIDER_COLORS.length - 1;
const index = Math.floor(progress * regularColorsCount);
return SLIDER_COLORS[Math.min(index, regularColorsCount - 1)];
}
const StarSlider = ({
maxValue,
defaultValue,
minValue,
minAllowedValue,
className,
floatingBadgeDescription,
shouldUseDynamicColor,
shouldAllowCustomValue,
onChange,
onBadgeClick,
}: OwnProps) => {
const floatingBadgeRef = useRef<HTMLDivElement>();
const containerRef = useRef<HTMLDivElement>();
const floatingBadgeContentRef = useRef<HTMLDivElement>();
const lang = useLang();
const min = minValue ?? 1;
const points = useMemo(() => {
const result = [];
for (let i = 0; i < DEFAULT_POINTS.length; i++) {
if (DEFAULT_POINTS[i] <= min) continue;
if (DEFAULT_POINTS[i] < maxValue) {
result.push(DEFAULT_POINTS[i]);
}
if (DEFAULT_POINTS[i] >= maxValue) {
result.push(maxValue);
break;
return result;
}
}
const lastPoint = DEFAULT_POINTS[DEFAULT_POINTS.length - 1];
let nextPoint = lastPoint + LARGE_STEP;
while (nextPoint < maxValue) {
result.push(nextPoint);
nextPoint += LARGE_STEP;
}
result.push(maxValue);
return result;
}, [maxValue]);
}, [maxValue, min]);
const [value, setValue] = useState(0);
const [containerWidth, setContainerWidth] = useState(0);
const [badgeWidth, setBadgeWidth] = useState(0);
const [isDragging, setIsDragging] = useState(false);
const startXRef = useRef<number | undefined>();
const prevBadgeWidth = usePrevious(badgeWidth);
useEffectOnce(() => {
setValue(getProgress(points, defaultValue));
});
const badgeText = lang.number(getValue(points, value, min));
const updateSafeBadgePosition = useLastCallback(() => {
const badge = floatingBadgeRef.current;
if (!badge) return;
const parent = badge.parentElement!;
const minAllowedProgress = minAllowedValue !== undefined
? getProgress(points, minAllowedValue, min) : 0;
requestMeasure(() => {
const safeMinX = parent.offsetLeft + badge.offsetWidth / 2;
const safeMaxX = parent.offsetLeft + parent.offsetWidth - badge.offsetWidth / 2;
useEffect(() => {
setValue(getProgress(points, defaultValue, min));
}, [defaultValue, points, min]);
requestMutation(() => {
parent.style.setProperty('--_min-x', `${safeMinX}px`);
parent.style.setProperty('--_max-x', `${safeMaxX}px`);
});
useEffect(() => {
if (!floatingBadgeContentRef.current) return;
const titleEl = floatingBadgeContentRef.current.querySelector(`.${styles.floatingBadgeTitle}`);
if (!titleEl) return;
const computedStyle = getComputedStyle(titleEl);
const font = `${computedStyle.fontWeight} ${computedStyle.fontSize} ${computedStyle.fontFamily}`;
const textWidth = getTextWidth(badgeText, font);
const titleWidth = BADGE_ICON_SIZE + BADGE_TITLE_GAP + textWidth;
const descriptionEl = floatingBadgeContentRef.current.querySelector(`.${styles.floatingBadgeDescription}`);
const descriptionWidth = descriptionEl?.scrollWidth || 0;
const contentWidth = Math.max(titleWidth, descriptionWidth);
const newBadgeWidth = contentWidth + BADGE_HORIZONTAL_PADDING;
setBadgeWidth((currentWidth) => {
if (Math.abs(newBadgeWidth - currentWidth) < BADGE_WIDTH_DELTA) {
return currentWidth;
}
return newBadgeWidth;
});
}, [badgeText, floatingBadgeDescription]);
const handleContainerResize = useLastCallback((entry: ResizeObserverEntry) => {
setContainerWidth(entry.contentRect.width);
});
useResizeObserver(floatingBadgeRef, updateSafeBadgePosition);
useResizeObserver(containerRef, handleContainerResize);
const progress = value / points.length;
const {
minBadgeX, maxBadgeX, beakOffset, cornerRadius,
} = useMemo(() => {
return calcBadgePosition(containerWidth, badgeWidth, progress);
}, [containerWidth, badgeWidth, progress]);
const handleChange = useLastCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = Number(event.currentTarget.value);
setValue(newValue);
const rawValue = Number(event.currentTarget.value);
const clampedValue = Math.max(rawValue, minAllowedProgress);
setValue(clampedValue);
onChange(getValue(points, newValue));
const resultValue = getValue(points, clampedValue, min);
onChange(resultValue);
});
const handlePointerDown = useLastCallback((e: React.PointerEvent<HTMLInputElement>) => {
startXRef.current = e.clientX;
setIsDragging(false);
});
const handlePointerMove = useLastCallback((e: React.PointerEvent<HTMLInputElement>) => {
if (startXRef.current === undefined) return;
const distance = Math.abs(e.clientX - startXRef.current);
if (distance >= DRAG_DISTANCE_THRESHOLD) {
setIsDragging(true);
}
});
const handlePointerUp = useLastCallback(() => {
startXRef.current = undefined;
setIsDragging(false);
});
const { left: radiusLeft, right: radiusRight } = cornerRadius;
const dynamicColor = shouldUseDynamicColor ? getColorForProgress(progress) : undefined;
const badgeStyle = buildStyle(
`border-radius: 2rem 2rem ${radiusRight}rem ${radiusLeft}rem`,
Boolean(badgeWidth) && `width: ${badgeWidth}px`,
);
const rootStyle = buildStyle(
`--progress: ${progress}`,
`--min-badge-x: ${minBadgeX}px`,
`--max-badge-x: ${maxBadgeX}px`,
dynamicColor && `--dynamic-color: ${dynamicColor}`,
);
return (
<div className={buildClassName(styles.root, className)} style={`--progress: ${value / points.length}`}>
<div
ref={containerRef}
className={buildClassName(styles.root, isDragging && styles.dragging, className)}
style={rootStyle}
>
<div className={styles.floatingBadgeWrapper}>
<div className={styles.floatingBadge} ref={floatingBadgeRef}>
<div className={styles.floatingBadgeText}>
<Icon name="star" className={styles.floatingBadgeIcon} />
<AnimatedCounter text={formatInteger(getValue(points, value))} />
<div
className={buildClassName(styles.floatingBadge, onBadgeClick && styles.clickable)}
onClick={onBadgeClick}
>
<div
className={buildClassName(
styles.floatingBadgeText,
shouldUseDynamicColor && styles.dynamicColor,
(!prevBadgeWidth || prevBadgeWidth === 0) && styles.noTransition,
)}
style={badgeStyle}
>
<Sparkles preset="button" className={styles.floatingBadgeSparkles} />
<div
ref={floatingBadgeContentRef}
className={buildClassName(
styles.floatingBadgeContent,
floatingBadgeDescription && styles.withDescription,
)}
>
<div className={styles.floatingBadgeTitle}>
<Icon name="star" className={styles.floatingBadgeIcon} />
<AnimatedCounter text={badgeText} />
</div>
<div className={styles.floatingBadgeDescription}>
{floatingBadgeDescription}
</div>
</div>
</div>
<svg className={styles.floatingBadgeTriangle} width="28" height="28" viewBox="0 0 28 28" fill="none">
<defs>
<linearGradient id="StarBadgeTriangle" x1="0" x2="1" y1="0" y2="0">
<stop offset="-50%" stop-color="#FFAA00" />
<stop offset="150%" stop-color="#FFCD3A" />
</linearGradient>
</defs>
<path d="m 28,4 v 9 c 0.0089,7.283278 -3.302215,5.319646 -6.750951,8.589815 l -5.8284,5.82843 c -0.781,0.78105 -2.0474,0.78104 -2.8284,0 L 6.7638083,21.589815 C 2.8288652,17.959047 0.04527024,20.332086 0,13 V 4 C 0,4 0.00150581,0.97697493 3,1 5.3786658,1.018266 22.594519,0.9142007 25,1 c 2.992326,0.1067311 3,3 3,3 z" fill="url(#StarBadgeTriangle)" />
<svg
className={styles.floatingBadgeTriangle}
width="28"
height="28"
viewBox="0 0 28 28"
fill="none"
aria-hidden="true"
role="presentation"
style={`transform: translate(calc(-50% + ${beakOffset}px), 33%)`}
>
{!shouldUseDynamicColor && (
<defs>
<linearGradient id="StarBadgeTriangle" x1="0" x2="1" y1="0" y2="0">
<stop offset="-50%" stop-color="#FFAA00" />
<stop offset="150%" stop-color="#FFCD3A" />
</linearGradient>
</defs>
)}
<path
className={styles.floatingBadgeTrianglePath}
d="m 28,4 v 9 c 0.0089,7.283278 -3.302215,5.319646 -6.750951,8.589815 l -5.8284,5.82843 c -0.781,0.78105 -2.0474,0.78104 -2.8284,0 L 6.7638083,21.589815 C 2.8288652,17.959047 0.04527024,20.332086 0,13 V 4 C 0,4 0.00150581,0.97697493 3,1 5.3786658,1.018266 22.594519,0.9142007 25,1 c 2.992326,0.1067311 3,3 3,3 z"
fill={dynamicColor || 'url(#StarBadgeTriangle)'}
/>
</svg>
</div>
</div>
<div className={styles.progress}>
<div className={buildClassName(styles.progress, shouldUseDynamicColor && styles.dynamicColor)}>
<Sparkles preset="progress" className={styles.sparkles} />
</div>
<input
@ -108,28 +286,82 @@ const StarSlider = ({
type="range"
min={0}
max={points.length}
defaultValue={getProgress(points, defaultValue)}
defaultValue={getProgress(points, defaultValue, min)}
step="any"
onChange={handleChange}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
/>
{shouldAllowCustomValue && (
<Icon name="add" className={styles.customValueIcon} />
)}
</div>
);
};
function getProgress(points: number[], value: number) {
function getProgress(points: number[], value: number, minValue: number) {
const pointIndex = points.findIndex((point) => value <= point);
const prevPoint = points[pointIndex - 1] || 1;
const prevPoint = points[pointIndex - 1] || minValue;
const nextPoint = points[pointIndex] || points[points.length - 1];
if (nextPoint === prevPoint) return pointIndex;
const progress = (value - prevPoint) / (nextPoint - prevPoint);
return pointIndex + progress;
}
function getValue(points: number[], progress: number) {
function getValue(points: number[], progress: number, minValue: number) {
const pointIndex = Math.floor(progress);
const prevPoint = points[pointIndex - 1] || 1;
const prevPoint = points[pointIndex - 1] || minValue;
const nextPoint = points[pointIndex] || points[points.length - 1];
const value = prevPoint + (nextPoint - prevPoint) * (progress - pointIndex);
return Math.round(value);
}
function calcBadgePosition(
containerWidth: number,
badgeWidth: number,
progress: number,
) {
const halfBadgeWidth = badgeWidth / 2;
const halfThumbSize = THUMB_SIZE_IN_PIXELS / 2;
const baseTargetX = halfThumbSize + progress * (containerWidth - THUMB_SIZE_IN_PIXELS);
const cornerTargetX = progress * containerWidth;
const edgeZone = THUMB_SIZE_IN_PIXELS / 2;
const distanceToLeftEdge = cornerTargetX;
const distanceToRightEdge = containerWidth - cornerTargetX;
const minEdgeDistance = Math.min(distanceToLeftEdge, distanceToRightEdge);
const t = Math.min(1, minEdgeDistance / edgeZone);
const targetX = cornerTargetX + t * (baseTargetX - cornerTargetX);
const minBadgeX = halfBadgeWidth;
const maxBadgeX = containerWidth - halfBadgeWidth;
const clampedBadgeX = Math.max(minBadgeX, Math.min(targetX, maxBadgeX));
const beakOffset = targetX - clampedBadgeX;
const thresholdPx = DEFAULT_RADIUS_IN_REM / 2 * REM;
const beakHalfWidth = BEAK_WIDTH_IN_PIXELS / 2;
const distanceToEdge = halfBadgeWidth - Math.abs(beakOffset);
const normalizedDistance = Math.max(0, distanceToEdge - beakHalfWidth);
let edgeRadius = DEFAULT_RADIUS_IN_REM;
if (normalizedDistance < thresholdPx) {
const radiusT = 1 - (normalizedDistance / thresholdPx);
edgeRadius = DEFAULT_RADIUS_IN_REM - radiusT * (DEFAULT_RADIUS_IN_REM - MIN_RADIUS_IN_REM);
}
const leftRadius = beakOffset < 0 ? edgeRadius : DEFAULT_RADIUS_IN_REM;
const rightRadius = beakOffset > 0 ? edgeRadius : DEFAULT_RADIUS_IN_REM;
return {
minBadgeX,
maxBadgeX,
beakOffset,
cornerRadius: { left: leftRadius, right: rightRadius },
};
}
export default memo(StarSlider);

View File

@ -37,6 +37,12 @@ export function getTransactionTitle(oldLang: OldLangFn, lang: LangFn, transactio
return lang('GiftPrepaidUpgradeTransactionTitle');
}
if (transaction.isStarGiftAuctionBid) {
return isNegativeAmount(transaction.amount)
? lang('StarGiftAuctionBidTransaction')
: lang('StarGiftAuctionBidRefundedTransaction');
}
if (transaction.starRefCommision) {
return oldLang('StarTransactionCommission', formatPercent(transaction.starRefCommision));
}

View File

@ -260,7 +260,7 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
peerLabel = oldLang('Stars.Transaction.Via');
}
if (!transaction.isPostsSearch && !isDropOriginalDetails) {
if (!transaction.isPostsSearch && !isDropOriginalDetails && !transaction.isStarGiftAuctionBid) {
tableData.push([
peerLabel,
peerId ? { chatId: peerId } : toName || '',
@ -312,6 +312,18 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
formatDateTimeToString(transaction.date * 1000, oldLang.code, true),
]);
if (transaction.isStarGiftAuctionBid && gift?.type === 'starGift' && gift.availabilityTotal) {
tableData.push([
lang('GiftInfoAvailability'),
lang('GiftInfoAvailabilityValue', {
count: gift.availabilityRemains || 0,
total: gift.availabilityTotal,
}, {
pluralValue: gift.availabilityRemains || 0,
}),
]);
}
const footerText = oldLang('lng_credits_box_out_about');
const footerTextParts = footerText.split('{link}');

View File

@ -10,12 +10,13 @@ import AnimatedCounter from '../common/AnimatedCounter';
type OwnProps = {
endsAt: number;
shouldShowZeroOnEnd?: boolean;
onEnd?: NoneToVoidFunction;
};
const UPDATE_FREQUENCY = 500; // Sometimes second gets skipped if using 1000
const TextTimer = ({ endsAt, onEnd }: OwnProps) => {
const TextTimer = ({ endsAt, shouldShowZeroOnEnd, onEnd }: OwnProps) => {
const forceUpdate = useForceUpdate();
const serverTime = getServerTime();
@ -28,9 +29,9 @@ const TextTimer = ({ endsAt, onEnd }: OwnProps) => {
}
}, [isActive, onEnd]);
if (!isActive) return undefined;
if (!isActive && !shouldShowZeroOnEnd) return undefined;
const timeLeft = endsAt - serverTime;
const timeLeft = Math.max(0, endsAt - serverTime);
const time = formatMediaDuration(timeLeft);
const timeParts = time.split(':');

View File

@ -1,5 +1,6 @@
import type {
ApiInputInvoice, ApiInputInvoicePremiumGiftStars, ApiInputInvoiceStarGift, ApiInputInvoiceStarGiftResale,
ApiInputInvoice, ApiInputInvoicePremiumGiftStars, ApiInputInvoiceStarGift,
ApiInputInvoiceStarGiftAuctionBid, ApiInputInvoiceStarGiftResale,
ApiRequestInputInvoice,
} from '../../../api/types';
import type { ApiCredentials } from '../../../components/payment/PaymentModal';
@ -575,7 +576,7 @@ addActionHandler('checkCanSendGift', async (global, actions, payload): Promise<v
addActionHandler('openGiftModal', async (global, actions, payload): Promise<void> => {
const {
forUserId, selectedResaleGift, tabId = getCurrentTabId(),
forUserId, selectedGift, selectedResaleGift, tabId = getCurrentTabId(),
} = payload;
if (selectIsCurrentUserFrozen(global)) {
@ -592,6 +593,7 @@ addActionHandler('openGiftModal', async (global, actions, payload): Promise<void
forPeerId: forUserId,
gifts,
selectedResaleGift,
selectedGift,
},
}, tabId);
setGlobal(global);
@ -1140,6 +1142,24 @@ addActionHandler('upgradePrepaidGift', (global, actions, payload): ActionReturnT
payInputStarInvoice(global, invoice, stars, tabId);
});
addActionHandler('sendStarGiftAuctionBid', (global, actions, payload): ActionReturnType => {
const {
giftId, bidAmount, peerId, message, shouldHideName, isUpdateBid, tabId = getCurrentTabId(),
} = payload;
const invoice: ApiInputInvoiceStarGiftAuctionBid = {
type: 'stargiftAuctionBid',
giftId,
bidAmount,
peerId,
message,
shouldHideName,
isUpdateBid,
};
payInputStarInvoice(global, invoice, bidAmount, tabId);
});
async function payInputStarInvoice<T extends GlobalState>(
global: T, inputInvoice: ApiInputInvoice, price: number,
...[tabId = getCurrentTabId()]: TabArgs<T>
@ -1229,6 +1249,26 @@ addActionHandler('openUniqueGiftBySlug', async (global, actions, payload): Promi
actions.openGiftInfoModal({ gift, tabId });
});
addActionHandler('openGiftAuctionBySlug', async (global, actions, payload): Promise<void> => {
const {
slug, tabId = getCurrentTabId(),
} = payload;
const auctionState = await callApi('fetchStarGiftAuctionState', { slug });
if (!auctionState) {
actions.showNotification({
message: {
key: 'GiftWasNotFound',
},
tabId,
});
return;
}
actions.openGiftAuctionModal({ gift: auctionState.gift, tabId });
});
addActionHandler('processStarGiftWithdrawal', async (global, actions, payload): Promise<void> => {
const {
gift, password, tabId = getCurrentTabId(),

View File

@ -12,11 +12,12 @@ import { getServerTime } from '../../../util/serverTime';
import { callApi } from '../../../api/gramjs';
import { RESALE_GIFTS_LIMIT } from '../../../limits';
import { areInputSavedGiftsEqual, getRequestInputSavedStarGift } from '../../helpers/payments';
import { addActionHandler, getGlobal, setGlobal } from '../../index';
import { addActionHandler, getGlobal, getPromiseActions, setGlobal } from '../../index';
import {
appendStarsSubscriptions,
appendStarsTransactions,
replacePeerSavedGifts,
updateActiveGiftAuction,
updateChats,
updatePeerStarGiftCollections,
updateStarsBalance,
@ -579,6 +580,43 @@ addActionHandler('shiftGiftUpgradeNextPrice', async (global, _actions, payload):
setGlobal(global);
});
addActionHandler('openGiftAuctionModal', async (global, _actions, payload): Promise<void> => {
const { gift, tabId = getCurrentTabId() } = payload;
await getPromiseActions().loadActiveGiftAuction({ giftId: gift.id, tabId });
global = getGlobal();
global = updateTabState(global, {
giftAuctionModal: { isOpen: true },
}, tabId);
setGlobal(global);
});
addActionHandler('loadActiveGiftAuction', async (global, _actions, payload): Promise<void> => {
const { giftId, tabId = getCurrentTabId() } = payload;
const currentAuction = selectTabState(global, tabId).activeGiftAuction;
const currentVersion = currentAuction?.state.type === 'active' ? currentAuction.state.version : 0;
const auctionState = await callApi('fetchStarGiftAuctionState', {
giftId,
version: currentVersion,
});
if (!auctionState) return;
global = getGlobal();
global = updateActiveGiftAuction(global, auctionState, tabId);
setGlobal(global);
});
addActionHandler('clearActiveGiftAuction', (global, _actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
activeGiftAuction: undefined,
}, tabId);
});
addActionHandler('toggleSavedGiftPinned', async (global, actions, payload): Promise<void> => {
const { gift, peerId, tabId = getCurrentTabId() } = payload;
@ -650,3 +688,26 @@ addActionHandler('loadStarGiftCollections', async (global, actions, payload): Pr
global = updatePeerStarGiftCollections(global, peerId, result.collections);
setGlobal(global);
});
addActionHandler('openGiftAuctionAcquiredModal', async (global, actions, payload): Promise<void> => {
const {
giftId, giftTitle, giftSticker, tabId = getCurrentTabId(),
} = payload;
const result = await callApi('fetchStarGiftAuctionAcquiredGifts', { giftId });
if (!result) return;
global = getGlobal();
global = updateTabState(global, {
giftAuctionAcquiredModal: {
giftId,
giftTitle,
giftSticker,
acquiredGifts: result.gifts,
},
}, tabId);
setGlobal(global);
});

View File

@ -240,6 +240,40 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
case 'newMessage': {
const action = update.message.content?.action;
if (action?.type === 'starGift' && update.message.isOutgoing) {
const { gift } = action;
if (!gift.isAuction || update.message.chatId === SERVICE_NOTIFICATIONS_USER_ID) return undefined;
const { chatId, id } = update.message;
if (!chatId || !id) return;
Object.values(global.byTabId).forEach(({ id: tabId }) => {
actions.focusMessage({
chatId,
messageId: id,
tabId,
});
actions.closeGiftAuctionBidModal({ tabId });
actions.closeGiftModal({ tabId });
actions.showNotification({
icon: 'auction-filled',
message: {
key: 'GiftAuctionWonNotification',
variables: {
gift: gift.title,
},
},
tabId,
});
actions.requestConfetti({ withStars: true, tabId });
});
return undefined;
}
if (!update.message.isOutgoing && update.message.chatId !== SERVICE_NOTIFICATIONS_USER_ID) return undefined;
if (action?.type !== 'starGiftUnique') return undefined;
const actionStarGift = action.gift;

View File

@ -4,7 +4,12 @@ import { formatCurrencyAsString } from '../../../util/formatCurrency';
import * as langProvider from '../../../util/oldLangProvider';
import { getPeerTitle } from '../../helpers/peers';
import { addActionHandler, getGlobal, setGlobal } from '../../index';
import { removeGiftInfoOriginalDetails, updateStarsBalance } from '../../reducers';
import {
removeGiftInfoOriginalDetails,
updateActiveGiftAuctionState,
updateActiveGiftAuctionUserState,
updateStarsBalance,
} from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
import { selectPeer, selectTabState } from '../../selectors';
@ -210,6 +215,27 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
});
}
if (inputInvoice?.type === 'stargiftAuctionBid') {
const { activeGiftAuction } = selectTabState(global, tabId);
const giftsPerRound = activeGiftAuction?.gift.giftsPerRound;
actions.showNotification({
icon: 'auction-filled',
title: {
key: inputInvoice.isUpdateBid ? 'GiftAuctionBidIncreasedTitle' : 'GiftAuctionBidPlacedTitle',
},
message: {
key: 'GiftAuctionBidPlacedMessage',
variables: { count: giftsPerRound },
},
tabId,
});
if (activeGiftAuction?.gift.id === inputInvoice.giftId) {
actions.loadActiveGiftAuction({ giftId: inputInvoice.giftId, tabId });
}
}
break;
}
@ -221,5 +247,29 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
actions.loadStarStatus();
break;
}
case 'updateStarGiftAuctionState': {
const { giftId, state } = update;
Object.keys(global.byTabId).forEach((tabIdStr) => {
const tabId = Number(tabIdStr);
global = updateActiveGiftAuctionState(global, giftId, state, tabId);
});
setGlobal(global);
break;
}
case 'updateStarGiftAuctionUserState': {
const { giftId, userState } = update;
Object.keys(global.byTabId).forEach((tabIdStr) => {
const tabId = Number(tabIdStr);
global = updateActiveGiftAuctionUserState(global, giftId, userState, tabId);
});
setGlobal(global);
break;
}
}
});

View File

@ -11,8 +11,7 @@ import { callApi } from '../../../api/gramjs';
import { addTabStateResetterAction } from '../../helpers/meta';
import { getPrizeStarsTransactionFromGiveaway, getStarsTransactionFromGift } from '../../helpers/payments';
import { addActionHandler, getGlobal, setGlobal } from '../../index';
import {
clearStarPayment, openStarsTransactionModal,
import { clearStarPayment, openStarsTransactionModal,
} from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
import {
@ -206,6 +205,31 @@ addTabStateResetterAction('closeStarsSubscriptionModal', 'starsSubscriptionModal
addTabStateResetterAction('closeGiftModal', 'giftModal');
addActionHandler('setGiftModalSelectedGift', (global, actions, payload): ActionReturnType => {
const { gift, tabId = getCurrentTabId() } = payload;
const tabState = selectTabState(global, tabId);
const giftModal = tabState?.giftModal;
if (giftModal) {
return updateTabState(global, {
giftModal: {
...giftModal,
selectedGift: gift,
},
}, tabId);
}
if (gift && 'id' in gift) {
actions.openGiftModal({
forUserId: global.currentUserId!,
selectedGift: gift,
tabId,
});
}
return undefined;
});
addActionHandler('closeStarsGiftModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
@ -387,6 +411,52 @@ addTabStateResetterAction('closeGiftResalePriceComposerModal', 'giftResalePriceC
addTabStateResetterAction('closeGiftUpgradeModal', 'giftUpgradeModal');
addActionHandler('closeGiftAuctionModal', (global, _actions, payload): ActionReturnType => {
const { shouldKeepActiveAuction, tabId = getCurrentTabId() } = payload || {};
const tabState = selectTabState(global, tabId);
return updateTabState(global, {
giftAuctionModal: undefined,
activeGiftAuction: shouldKeepActiveAuction ? tabState?.activeGiftAuction : undefined,
}, tabId);
});
addActionHandler('openGiftAuctionBidModal', (global, _actions, payload): ActionReturnType => {
const { peerId, message, shouldHideName, tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
giftAuctionBidModal: { isOpen: true, peerId, message, shouldHideName },
}, tabId);
});
addTabStateResetterAction('closeGiftAuctionBidModal', 'giftAuctionBidModal');
addActionHandler('openGiftAuctionInfoModal', (global, _actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
giftAuctionInfoModal: { isOpen: true },
}, tabId);
});
addTabStateResetterAction('closeGiftAuctionInfoModal', 'giftAuctionInfoModal');
addActionHandler('openGiftAuctionChangeRecipientModal', (global, _actions, payload): ActionReturnType => {
const {
oldPeerId, newPeerId, message, shouldHideName, tabId = getCurrentTabId(),
} = payload;
return updateTabState(global, {
giftAuctionChangeRecipientModal: {
isOpen: true, oldPeerId, newPeerId, message, shouldHideName,
},
}, tabId);
});
addTabStateResetterAction('closeGiftAuctionChangeRecipientModal', 'giftAuctionChangeRecipientModal');
addTabStateResetterAction('closeGiftAuctionAcquiredModal', 'giftAuctionAcquiredModal');
addActionHandler('openStarGiftPriceDecreaseInfoModal', (global, actions, payload): ActionReturnType => {
const {
prices, currentPrice, minPrice, maxPrice, tabId = getCurrentTabId(),

View File

@ -261,6 +261,23 @@ export function getRequestInputInvoice<T extends GlobalState>(
};
}
if (inputInvoice.type === 'stargiftAuctionBid') {
const {
giftId, bidAmount, peerId, message, shouldHideName, isUpdateBid,
} = inputInvoice;
const peer = peerId ? selectPeer(global, peerId) : undefined;
return {
type: 'stargiftAuctionBid',
giftId,
bidAmount,
peer,
message,
shouldHideName,
isUpdateBid,
};
}
return undefined;
}

View File

@ -1,7 +1,13 @@
import type { ApiSavedStarGift } from '../../api/types';
import type {
ApiSavedStarGift,
ApiStarGiftAuctionState,
ApiStarGiftAuctionUserState,
ApiTypeStarGiftAuctionState,
} from '../../api/types';
import type { GlobalState } from '../types';
import { getCurrentTabId } from '../../util/establishMultitabRole';
import { selectTabState } from '../selectors';
import { updateTabState } from './tabs';
export function removeGiftInfoOriginalDetails<T extends GlobalState>(
@ -46,3 +52,73 @@ export function removeGiftInfoOriginalDetails<T extends GlobalState>(
},
}, tabId);
}
function getAuctionStateVersion(state: ApiTypeStarGiftAuctionState): number {
return state.type === 'active' ? state.version : 0;
}
export function updateActiveGiftAuction<T extends GlobalState>(
global: T,
auctionState: ApiStarGiftAuctionState,
tabId: number = getCurrentTabId(),
): T {
const currentAuction = selectTabState(global, tabId).activeGiftAuction;
const serverVersion = getAuctionStateVersion(auctionState.state);
const clientVersion = currentAuction ? getAuctionStateVersion(currentAuction.state) : -1;
if (serverVersion >= clientVersion) {
return updateTabState(global, {
activeGiftAuction: auctionState,
}, tabId);
}
return global;
}
export function updateActiveGiftAuctionState<T extends GlobalState>(
global: T,
giftId: string,
state: ApiTypeStarGiftAuctionState,
tabId: number,
): T {
const activeAuction = selectTabState(global, tabId).activeGiftAuction;
if (!activeAuction || activeAuction.gift.id !== giftId) {
return global;
}
const serverVersion = getAuctionStateVersion(state);
const clientVersion = getAuctionStateVersion(activeAuction.state);
if (serverVersion > clientVersion) {
return updateTabState(global, {
activeGiftAuction: {
...activeAuction,
state,
},
}, tabId);
}
return global;
}
export function updateActiveGiftAuctionUserState<T extends GlobalState>(
global: T,
giftId: string,
userState: ApiStarGiftAuctionUserState,
tabId: number,
): T {
const activeAuction = selectTabState(global, tabId).activeGiftAuction;
if (!activeAuction || activeAuction.gift.id !== giftId) {
return global;
}
return updateTabState(global, {
activeGiftAuction: {
...activeAuction,
userState,
},
}, tabId);
}

View File

@ -33,6 +33,7 @@ import type {
ApiPaymentStatus,
ApiPeer,
ApiPhoto,
ApiPremiumGiftCodeOption,
ApiPremiumSection,
ApiPreparedInlineMessage,
ApiPrivacyKey,
@ -46,6 +47,7 @@ import type {
ApiSessionData,
ApiStarGift,
ApiStarGiftAttributeOriginalDetails,
ApiStarGiftRegular,
ApiStarGiftUnique,
ApiStarGiftUpgradePrice,
ApiStarsSubscription,
@ -1653,6 +1655,9 @@ export interface ActionPayloads {
openUniqueGiftBySlug: {
slug: string;
} & WithTabId;
openGiftAuctionBySlug: {
slug: string;
} & WithTabId;
openPreviousStory: WithTabId | undefined;
openNextStory: WithTabId | undefined;
setStoryViewerMuted: {
@ -2600,9 +2605,13 @@ export interface ActionPayloads {
openGiftModal: {
forUserId: string;
selectedGift?: ApiStarGift;
selectedResaleGift?: ApiStarGift;
} & WithTabId;
closeGiftModal: WithTabId | undefined;
setGiftModalSelectedGift: {
gift: ApiPremiumGiftCodeOption | ApiStarGift | undefined;
} & WithTabId;
sendStarGift: StarGiftInfo & WithTabId;
buyStarGift: {
peerId: string;
@ -2686,6 +2695,45 @@ export interface ActionPayloads {
gift: ApiStarGiftUnique;
} & WithTabId;
closeGiftInfoValueModal: WithTabId | undefined;
openGiftAuctionModal: {
gift: ApiStarGiftRegular;
} & WithTabId;
closeGiftAuctionModal: {
shouldKeepActiveAuction?: boolean;
} & WithTabId | undefined;
openGiftAuctionBidModal: {
peerId?: string;
message?: string;
shouldHideName?: boolean;
} & WithTabId | undefined;
closeGiftAuctionBidModal: WithTabId | undefined;
openGiftAuctionInfoModal: WithTabId | undefined;
closeGiftAuctionInfoModal: WithTabId | undefined;
openGiftAuctionChangeRecipientModal: {
oldPeerId: string;
newPeerId: string;
message?: string;
shouldHideName?: boolean;
} & WithTabId;
closeGiftAuctionChangeRecipientModal: WithTabId | undefined;
openGiftAuctionAcquiredModal: {
giftId: string;
giftTitle?: string;
giftSticker?: ApiSticker;
} & WithTabId;
closeGiftAuctionAcquiredModal: WithTabId | undefined;
sendStarGiftAuctionBid: {
giftId: string;
bidAmount: number;
peerId?: string;
message?: ApiFormattedText;
shouldHideName?: boolean;
isUpdateBid?: boolean;
} & WithTabId;
loadActiveGiftAuction: {
giftId: string;
} & WithTabId;
clearActiveGiftAuction: WithTabId | undefined;
processStarGiftWithdrawal: {
gift: ApiInputSavedStarGift;
password: string;

View File

@ -44,6 +44,8 @@ import type {
ApiStarGiftAttribute,
ApiStarGiftAttributeCounter,
ApiStarGiftAttributeOriginalDetails,
ApiStarGiftAuctionAcquiredGift,
ApiStarGiftAuctionState,
ApiStarGiftUnique,
ApiStarGiftUpgradePrice,
ApiStarGiveawayOption,
@ -703,7 +705,9 @@ export type TabState = {
forPeerId: string;
gifts?: ApiPremiumGiftCodeOption[];
selectedResaleGift?: ApiStarGift;
selectedGift?: ApiPremiumGiftCodeOption | ApiStarGift;
};
activeGiftAuction?: ApiStarGiftAuctionState;
chatRefundModal?: {
userId: string;
starsToRefund: number;
@ -889,6 +893,36 @@ export type TabState = {
emojiStatus: ApiEmojiStatusCollectible;
};
giftAuctionModal?: {
isOpen?: boolean;
};
giftAuctionBidModal?: {
isOpen?: boolean;
peerId?: string;
message?: string;
shouldHideName?: boolean;
};
giftAuctionInfoModal?: {
isOpen?: boolean;
};
giftAuctionChangeRecipientModal?: {
isOpen?: boolean;
oldPeerId?: string;
newPeerId?: string;
message?: string;
shouldHideName?: boolean;
};
giftAuctionAcquiredModal?: {
giftId?: string;
giftTitle?: string;
giftSticker?: ApiSticker;
acquiredGifts?: ApiStarGiftAuctionAcquiredGift[];
};
starGiftPriceDecreaseInfoModal?: {
prices: ApiStarGiftUpgradePrice[];
currentPrice: number;

View File

@ -12,10 +12,12 @@ const useScrollNotch = ({
containerRef,
selector,
isBottomNotch,
shouldHideTopNotch,
}: {
containerRef: ElementRef<HTMLDivElement>;
selector: string;
isBottomNotch?: boolean;
shouldHideTopNotch?: boolean;
}, deps: unknown[]) => {
useLayoutEffect(() => {
const elements = containerRef.current?.querySelectorAll<HTMLElement>(selector);
@ -28,19 +30,21 @@ const useScrollNotch = ({
const isAtEnd = scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD;
requestMutation(() => {
if (!shouldHideTopNotch) {
toggleExtraClass(target, 'scrolled', isScrolled);
}
if (isBottomNotch) {
toggleExtraClass(target, 'scrolled-to-end', isAtEnd);
} else {
toggleExtraClass(target, 'scrolled', isScrolled);
}
});
}, THROTTLE_DELAY);
elements.forEach((el) => {
if (!shouldHideTopNotch) {
addExtraClass(el, 'with-notch');
}
if (isBottomNotch) {
addExtraClass(el, 'with-bottom-notch');
} else {
addExtraClass(el, 'with-notch');
}
el.addEventListener('scroll', handleScroll, { passive: true });
});
@ -55,7 +59,7 @@ const useScrollNotch = ({
});
};
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, [containerRef, selector, isBottomNotch, ...deps]);
}, [containerRef, selector, isBottomNotch, shouldHideTopNotch, ...deps]);
useEffect(() => {
const elements = containerRef.current?.querySelectorAll<HTMLElement>(selector);
@ -67,15 +71,16 @@ const useScrollNotch = ({
const isAtEnd = scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD;
requestMutation(() => {
if (!shouldHideTopNotch) {
toggleExtraClass(el, 'scrolled', isScrolled);
}
if (isBottomNotch) {
toggleExtraClass(el, 'scrolled-to-end', isAtEnd);
} else {
toggleExtraClass(el, 'scrolled', isScrolled);
}
});
});
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, [containerRef, selector, isBottomNotch, ...deps]);
}, [containerRef, selector, isBottomNotch, shouldHideTopNotch, ...deps]);
};
export default useScrollNotch;

View File

@ -1881,6 +1881,8 @@ payments.updateStarGiftPrice#edbe6ccb stargift:InputSavedStarGift resell_amount:
payments.getStarGiftCollections#981b91dd peer:InputPeer hash:long = payments.StarGiftCollections;
payments.getUniqueStarGiftValueInfo#4365af6b slug:string = payments.UniqueStarGiftValueInfo;
payments.checkCanSendGift#c0c4edc9 gift_id:long = payments.CheckCanSendGiftResult;
payments.getStarGiftAuctionState#5c9ff4d6 auction:InputStarGiftAuction version:int = payments.StarGiftAuctionState;
payments.getStarGiftAuctionAcquiredGifts#6ba2cbec gift_id:long = payments.StarGiftAuctionAcquiredGifts;
phone.requestCall#42ff96ed flags:# video:flags.0?true user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
phone.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
phone.confirmCall#2efe1722 peer:InputPhoneCall g_a:bytes key_fingerprint:long protocol:PhoneCallProtocol = phone.PhoneCall;

View File

@ -352,6 +352,8 @@
"payments.getResaleStarGifts",
"payments.updateStarGiftPrice",
"payments.getStarGiftCollections",
"payments.getStarGiftAuctionState",
"payments.getStarGiftAuctionAcquiredGifts",
"langpack.getLangPack",
"langpack.getStrings",
"langpack.getLanguages",

File diff suppressed because it is too large Load Diff

View File

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

Binary file not shown.

Binary file not shown.

View File

@ -18,6 +18,9 @@ export type FontIconName =
| 'arrow-right'
| 'ask-support'
| 'attach'
| 'auction-drop'
| 'auction-filled'
| 'auction-next-round'
| 'auction'
| 'author-hidden'
| 'avatar-archived-chats'

View File

@ -1268,6 +1268,7 @@ export interface LangPair {
'GiftInfoDescriptionFreeUpgrade': undefined;
'GiftInfoDescriptionUpgrade2': undefined;
'GiftInfoDescriptionUpgraded': undefined;
'GiftInfoDescriptionRefunded': undefined;
'GiftInfoFrom': undefined;
'GiftInfoDate': undefined;
'GiftInfoValue': undefined;
@ -1745,6 +1746,8 @@ export interface LangPair {
'StarGiftReasonDropOriginalDetails': undefined;
'GiftAnUpgradeButton': undefined;
'GiftPrepaidUpgradeTransactionTitle': undefined;
'StarGiftAuctionBidTransaction': undefined;
'StarGiftAuctionBidRefundedTransaction': undefined;
'ActionStarGiftPrepaidUpgradedYou': undefined;
'UserNoteTitle': undefined;
'UserNoteHint': undefined;
@ -1787,6 +1790,39 @@ export interface LangPair {
'StarGiftPriceDecreaseInfoLink': undefined;
'StarGiftUpgradeCostModalTitle': undefined;
'StarGiftUpgradeCostHint': undefined;
'GiftRibbonAuction': undefined;
'GiftAuctionJoin': undefined;
'GiftAuctionLearnMore': undefined;
'GiftAuctionStarted': undefined;
'GiftAuctionEnds': undefined;
'GiftAuctionCurrentRound': undefined;
'GiftAuctionPlaceBid': undefined;
'GiftAuctionMinimumBid': undefined;
'GiftAuctionUntilNextRound': undefined;
'GiftAuctionLeft': undefined;
'GiftAuctionYourBidWillBe': undefined;
'GiftAuctionYoureWinning': undefined;
'GiftAuctionBalance': undefined;
'GiftAuctionInfoTitle': undefined;
'GiftAuctionInfoSubtitle': undefined;
'GiftAuctionInfoBidCarryoverTitle': undefined;
'GiftAuctionInfoMissedBiddersTitle': undefined;
'GiftAuctionInfoMissedBiddersSubtitle': undefined;
'GiftAuctionRecipient': undefined;
'GiftAuctionDate': undefined;
'GiftAuctionAcceptedBid': undefined;
'GiftAuctionCustomBidTitle': undefined;
'GiftAuctionCustomBidPlaceholder': undefined;
'GiftAuctionCustomBidButton': undefined;
'GiftAuctionBidPlacedTitle': undefined;
'GiftAuctionBidIncreasedTitle': undefined;
'GiftAuctionFinished': undefined;
'GiftAuctionEnded': undefined;
'GiftAuctionSoldOut': undefined;
'GiftAuctionChangeRecipientTitle': undefined;
'GiftAuctionAveragePrice': undefined;
'GiftAuctionTapToBidMore': undefined;
'StarGift': undefined;
'SettingsItemPrivacyPasskeys': undefined;
'SettingsItemPrivacyOn': undefined;
'SettingsItemPrivacyOff': undefined;
@ -2645,6 +2681,15 @@ export interface LangPairWithVariables<V = LangVariable> {
'ActionStarGiftLimitedRibbon': {
'total': V;
};
'ActionStarGiftAuctionWon': {
'cost': V;
};
'ActionStarGiftAuctionFor': {
'peer': V;
};
'ActionStarGiftAuctionBought': {
'cost': V;
};
'ActionSuggestedPhotoYou': {
'user': V;
};
@ -3120,6 +3165,43 @@ export interface LangPairWithVariables<V = LangVariable> {
'StarGiftPriceDecreaseTimer': {
'timer': V;
};
'GiftAuctionRoundValue': {
'current': V;
'total': V;
};
'GiftAuctionPlaceBidButton': {
'amount': V;
};
'GiftAuctionTimeLeft': {
'time': V;
};
'GiftAuctionAddToBid': {
'amount': V;
};
'GiftAuctionInfoBidCarryoverSubtitle': {
'count': V;
};
'GiftAuctionBoughtGiftHeader': {
'gift': V;
'giftNumber': V;
'round': V;
};
'GiftAuctionTopPosition': {
'position': V;
};
'GiftAuctionCustomBidDescription': {
'count': V;
};
'GiftAuctionBidPlacedMessage': {
'count': V;
};
'GiftAuctionChangeRecipientDescription': {
'oldPeer': V;
'newPeer': V;
};
'GiftAuctionWonNotification': {
'gift': V;
};
'SettingsPasskeyUsedAt': {
'date': V;
};
@ -3527,6 +3609,34 @@ export interface LangPairPluralWithVariables<V = LangVariable> {
'list': V;
'count': V;
};
'GiftAuctionTopBidders': {
'count': V;
'gift': V;
'link': V;
};
'GiftAuctionDescription': {
'count': V;
'link': V;
};
'GiftAuctionTopWinners': {
'count': V;
};
'GiftAuctionInfoTopBiddersTitle': {
'count': V;
};
'GiftAuctionInfoTopBiddersSubtitle': {
'count': V;
};
'GiftAuctionItemsBought': {
'count': V;
'gift': V;
};
'GiftAuctionBoughtGiftsTitle': {
'count': V;
};
'GiftAuctionGifts': {
'count': V;
};
}
export type RegularLangKey = keyof LangPair;
export type RegularLangKeyWithVariables = keyof LangPairWithVariables;

View File

@ -10,7 +10,7 @@ import { isUsernameValid } from './entities/username';
export type DeepLinkMethod = 'resolve' | 'login' | 'passport' | 'settings' | 'join' | 'addstickers' | 'addemoji' |
'setlanguage' | 'addtheme' | 'confirmphone' | 'socks' | 'proxy' | 'privatepost' | 'bg' | 'share' | 'msg' | 'msg_url' |
'invoice' | 'addlist' | 'boost' | 'giftcode' | 'message' | 'premium_offer' | 'premium_multigift' | 'stars_topup'
| 'nft' | 'stars' | 'ton';
| 'nft' | 'stars' | 'ton' | 'stargift_auction';
interface PublicMessageLink {
type: 'publicMessageLink';
@ -104,6 +104,11 @@ interface GiftUniqueLink {
slug: string;
}
interface GiftAuctionLink {
type: 'giftAuctionLink';
slug: string;
}
interface StarsModalLink {
type: 'stars';
}
@ -131,6 +136,7 @@ type DeepLink =
PremiumMultigiftLink |
ChatBoostLink |
GiftUniqueLink |
GiftAuctionLink |
StarsModalLink |
TonModalLink |
SettingsScreenLink;
@ -269,6 +275,8 @@ function parseTgLink(url: URL) {
return buildChatBoostLink({ username: queryParams.domain, id: queryParams.channel });
case 'giftUniqueLink':
return buildGiftUniqueLink({ slug: queryParams.slug });
case 'giftAuctionLink':
return buildGiftAuctionLink({ slug: queryParams.slug });
case 'stars':
return { type: 'stars' } satisfies StarsModalLink;
case 'ton':
@ -384,6 +392,11 @@ function parseHttpLink(url: URL) {
slug,
});
}
case 'giftAuctionLink': {
return buildGiftAuctionLink({
slug: pathParams[1],
});
}
default:
break;
}
@ -409,6 +422,7 @@ function getHttpDeepLinkType(
if (method === 'm') return 'businessChatLink';
if (method === 'boost') return 'chatBoostLink';
if (method === 'nft') return 'giftUniqueLink';
if (method === 'auction') return 'giftAuctionLink';
if (method === 'c') {
if (queryParams.boost !== undefined) return 'chatBoostLink';
return 'privateChannelLink';
@ -481,6 +495,8 @@ function getTgDeepLinkType(
return 'chatBoostLink';
case 'nft':
return 'giftUniqueLink';
case 'stargift_auction':
return 'giftAuctionLink';
case 'stars':
return 'stars';
case 'ton':
@ -710,6 +726,21 @@ function buildGiftUniqueLink(params: BuilderParams<GiftUniqueLink>): BuilderRetu
};
}
function buildGiftAuctionLink(params: BuilderParams<GiftAuctionLink>): BuilderReturnType<GiftAuctionLink> {
const {
slug,
} = params;
if (!slug) {
return undefined;
}
return {
type: 'giftAuctionLink',
slug,
};
}
function buildSettingsScreen(screenParam: string) {
switch (screenParam) {
case 'devices':

View File

@ -79,6 +79,9 @@ export const processDeepLink = (url: string, linkContext?: LinkContext): boolean
case 'giftUniqueLink':
actions.openUniqueGiftBySlug({ slug: parsedLink.slug });
return true;
case 'giftAuctionLink':
actions.openGiftAuctionBySlug({ slug: parsedLink.slug });
return true;
case 'settings':
if (!parsedLink.screen) {
actions.openLeftColumnContent({ contentKey: LeftColumnContent.Settings });