diff --git a/src/api/gramjs/apiBuilders/gifts.ts b/src/api/gramjs/apiBuilders/gifts.ts index c95815107..08601c746 100644 --- a/src/api/gramjs/apiBuilders/gifts.ts +++ b/src/api/gramjs/apiBuilders/gifts.ts @@ -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, + }; +} diff --git a/src/api/gramjs/apiBuilders/messageActions.ts b/src/api/gramjs/apiBuilders/messageActions.ts index a66aa7b31..caf6ab360 100644 --- a/src/api/gramjs/apiBuilders/messageActions.ts +++ b/src/api/gramjs/apiBuilders/messageActions.ts @@ -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) { diff --git a/src/api/gramjs/apiBuilders/messageContent.ts b/src/api/gramjs/apiBuilders/messageContent.ts index 266d48f7e..1754e040c 100644 --- a/src/api/gramjs/apiBuilders/messageContent.ts +++ b/src/api/gramjs/apiBuilders/messageContent.ts @@ -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, }; } diff --git a/src/api/gramjs/apiBuilders/payments.ts b/src/api/gramjs/apiBuilders/payments.ts index ea74ed375..e7cf2604f 100644 --- a/src/api/gramjs/apiBuilders/payments.ts +++ b/src/api/gramjs/apiBuilders/payments.ts @@ -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, }; } diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index 94edb0ffe..e32b2af5e 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -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); diff --git a/src/api/gramjs/methods/stars.ts b/src/api/gramjs/methods/stars.ts index 92e086d7e..1b504f76e 100644 --- a/src/api/gramjs/methods/stars.ts +++ b/src/api/gramjs/methods/stars.ts @@ -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, diff --git a/src/api/gramjs/updates/mtpUpdateHandler.ts b/src/api/gramjs/updates/mtpUpdateHandler.ts index b8d8aeb47..cb72bd3a9 100644 --- a/src/api/gramjs/updates/mtpUpdateHandler.ts +++ b/src/api/gramjs/updates/mtpUpdateHandler.ts @@ -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', diff --git a/src/api/types/messageActions.ts b/src/api/types/messageActions.ts index 9ff21fa3c..adf0d2ab9 100644 --- a/src/api/types/messageActions.ts +++ b/src/api/types/messageActions.ts @@ -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 { diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 2602c3123..be7c1bb8a 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -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; /** diff --git a/src/api/types/payments.ts b/src/api/types/payments.ts index f3460a76f..07490398d 100644 --- a/src/api/types/payments.ts +++ b/src/api/types/payments.ts @@ -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; diff --git a/src/api/types/stars.ts b/src/api/types/stars.ts index ca1ab9f6d..f89f1e0f5 100644 --- a/src/api/types/stars.ts +++ b/src/api/types/stars.ts @@ -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; +} diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index add51f3d7..04b81e5f7 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -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; diff --git a/src/assets/font-icons/auction-drop.svg b/src/assets/font-icons/auction-drop.svg new file mode 100644 index 000000000..41c485da5 --- /dev/null +++ b/src/assets/font-icons/auction-drop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/auction-filled.svg b/src/assets/font-icons/auction-filled.svg new file mode 100644 index 000000000..1859bac65 --- /dev/null +++ b/src/assets/font-icons/auction-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/auction-next-round.svg b/src/assets/font-icons/auction-next-round.svg new file mode 100644 index 000000000..bb881d5b7 --- /dev/null +++ b/src/assets/font-icons/auction-next-round.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/user-stars.svg b/src/assets/font-icons/user-stars.svg index 08943452a..cd4c26bdb 100644 --- a/src/assets/font-icons/user-stars.svg +++ b/src/assets/font-icons/user-stars.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index ef119ff6d..52df59eec 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -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"; diff --git a/src/bundles/stars.ts b/src/bundles/stars.ts index 755488a0b..5a29a7098 100644 --- a/src/bundles/stars.ts +++ b/src/bundles/stars.ts @@ -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'; diff --git a/src/components/common/helpers/gifts.ts b/src/components/common/helpers/gifts.ts index f8009c52d..e360ae265 100644 --- a/src/components/common/helpers/gifts.ts +++ b/src/components/common/helpers/gifts.ts @@ -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, + }; +} diff --git a/src/components/common/pickers/PickerModal.tsx b/src/components/common/pickers/PickerModal.tsx index 9d6fb8934..748bd16e5 100644 --- a/src/components/common/pickers/PickerModal.tsx +++ b/src/components/common/pickers/PickerModal.tsx @@ -42,6 +42,7 @@ const PickerModal = ({ containerRef: dialogRef, selector: `.modal-content ${itemsContainerSelector}`, isBottomNotch: true, + shouldHideTopNotch: true, }, [modalProps.isOpen]); return ( diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 8a127aee5..8948d4481 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -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( payment, limitReachedModal, deleteFolderDialogModal, + activeGiftAuction, } = selectTabState(global); const { wasTimeFormatSetManually, foldersPosition } = selectSharedSettings(global); @@ -693,6 +706,7 @@ export default memo(withGlobal( isAccountFrozen, isAppConfigLoaded: global.isAppConfigLoaded, isFoldersSidebarShown: foldersPosition === FOLDERS_POSITION_LEFT && !isMobile && selectAreFoldersPresent(global), + activeGiftAuction, }; }, )(Main)); diff --git a/src/components/middle/message/ActionMessageText.tsx b/src/components/middle/message/ActionMessageText.tsx index df6e86d32..e0359d973 100644 --- a/src/components/middle/message/ActionMessageText.tsx +++ b/src/components/middle/message/ActionMessageText.tsx @@ -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)); diff --git a/src/components/middle/message/WebPage.scss b/src/components/middle/message/WebPage.scss index 7c6f4df67..0a165ce29 100644 --- a/src/components/middle/message/WebPage.scss +++ b/src/components/middle/message/WebPage.scss @@ -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 { diff --git a/src/components/middle/message/WebPage.tsx b/src/components/middle/message/WebPage.tsx index ccbf38bac..c6eb9b746 100644 --- a/src/components/middle/message/WebPage.tsx +++ b/src/components/middle/message/WebPage.tsx @@ -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 = ({ 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 = ({ !isArticle && 'no-article', document && 'with-document', quickButtonTitle && 'with-quick-button', - isGift && 'with-gift', + (isGift || isAuction) && 'with-gift', ); - function renderQuickButton(caption: string) { + function renderQuickButton() { return ( ); } @@ -195,7 +202,7 @@ const WebPage: FC = ({
{backgroundEmojiId && ( @@ -215,6 +222,14 @@ const WebPage: FC = ({ onClick={handleOpenTelegramLink} /> )} + {isAuction && webPage.auction && ( + + )} {isArticle && (
= ({ {title && (

{renderText(title)}

)} - {truncatedDescription && !isGift && ( + {truncatedDescription && !isGift && !isAuction && (

{renderText(truncatedDescription, ['emoji', 'br'])}

)}
)} - {photo && !isGift && !video && !document && ( + {photo && !isGift && !isAuction && !video && !document && ( = ({
)} - {quickButtonTitle && renderQuickButton(quickButtonTitle)} + {quickButtonTitle && renderQuickButton()} ); }; diff --git a/src/components/middle/message/WebPageStarGiftAuction.module.scss b/src/components/middle/message/WebPageStarGiftAuction.module.scss new file mode 100644 index 000000000..11c69ceed --- /dev/null +++ b/src/components/middle/message/WebPageStarGiftAuction.module.scss @@ -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); +} diff --git a/src/components/middle/message/WebPageStarGiftAuction.tsx b/src/components/middle/message/WebPageStarGiftAuction.tsx new file mode 100644 index 000000000..aa3d21ecb --- /dev/null +++ b/src/components/middle/message/WebPageStarGiftAuction.tsx @@ -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(); + 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 ( +
+ +
+ {isFinished ? lang('GiftAuctionFinished') : } +
+ + + +
{title}
+
{subtitleText}
+
+ ); +}; + +export default memo(WebPageStarGiftAuction); diff --git a/src/components/middle/message/_message-content.scss b/src/components/middle/message/_message-content.scss index 0420b4d1e..cc289c437 100644 --- a/src/components/middle/message/_message-content.scss +++ b/src/components/middle/message/_message-content.scss @@ -27,7 +27,8 @@ .message-content { position: relative; max-width: var(--max-width); - &.gift { + &.gift, + &.auction { --max-width: 18rem; } diff --git a/src/components/middle/message/actions/StarGift.tsx b/src/components/middle/message/actions/StarGift.tsx index 31d04941f..0218e8e1b 100644 --- a/src/components/middle/message/actions/StarGift.tsx +++ b/src/components/middle/message/actions/StarGift.tsx @@ -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 = ({ )}

- {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( 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, diff --git a/src/components/middle/message/helpers/buildContentClassName.ts b/src/components/middle/message/helpers/buildContentClassName.ts index 42b6d5bb5..4dc1cb746 100644 --- a/src/components/middle/message/helpers/buildContentClassName.ts +++ b/src/components/middle/message/helpers/buildContentClassName.ts @@ -144,6 +144,10 @@ export function buildContentClassName( if (webPage.gift) { classNames.push('gift'); } + + if (webPage.auction) { + classNames.push('auction'); + } } if (invoice && !invoice.extendedMedia) { diff --git a/src/components/middle/message/helpers/webpageType.ts b/src/components/middle/message/helpers/webpageType.ts index 31158219f..5e9da7cd0 100644 --- a/src/components/middle/message/helpers/webpageType.ts +++ b/src/components/middle/message/helpers/webpageType.ts @@ -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; +} diff --git a/src/components/modals/ModalContainer.tsx b/src/components/modals/ModalContainer.tsx index 227333fa6..87a079346 100644 --- a/src/components/modals/ModalContainer.tsx +++ b/src/components/modals/ModalContainer.tsx @@ -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 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 ( +
+ {tableData.map(([label, value]) => ( + <> + {Boolean(label) &&
{label}
} +
+ {typeof value === 'object' && 'chatId' in value ? ( + + ) : value} +
+ + ))} +
+ ); +}; + +export default memo(TableInfo); diff --git a/src/components/modals/common/TableInfoModal.module.scss b/src/components/modals/common/TableInfoModal.module.scss index c539a85fc..399a6518e 100644 --- a/src/components/modals/common/TableInfoModal.module.scss +++ b/src/components/modals/common/TableInfoModal.module.scss @@ -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); -} diff --git a/src/components/modals/common/TableInfoModal.tsx b/src/components/modals/common/TableInfoModal.tsx index 999d342c5..d5cd41f2f 100644 --- a/src/components/modals/common/TableInfoModal.tsx +++ b/src/components/modals/common/TableInfoModal.tsx @@ -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 = ({ )} {header} -
- {tableData?.map(([label, value]) => ( - <> - {Boolean(label) &&
{label}
} -
- {typeof value === 'object' && 'chatId' in value ? ( - - ) : value} -
- - ))} -
+ {footer} {buttonText && ( + {!hideBadge && ( + + )} {giftRibbon} {isLocked && }

diff --git a/src/components/modals/gift/GiftModal.module.scss b/src/components/modals/gift/GiftModal.module.scss index 04e19d692..121b7af9e 100644 --- a/src/components/modals/gift/GiftModal.module.scss +++ b/src/components/modals/gift/GiftModal.module.scss @@ -16,7 +16,7 @@ .root :global(.modal-dialog), .transition, .content { - height: min(92vh, 49rem); + height: min(98vh, 49rem); max-height: none !important; } diff --git a/src/components/modals/gift/GiftModal.tsx b/src/components/modals/gift/GiftModal.tsx index 53c83af3b..4db26b53c 100644 --- a/src/components/modals/gift/GiftModal.tsx +++ b/src/components/modals/gift/GiftModal.tsx @@ -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 = ({ closeResaleGiftsMarket, loadMyUniqueGifts, openGiftTransferConfirmModal, + setGiftModalSelectedGift, + clearActiveGiftAuction, } = getActions(); const dialogRef = useRef(); const transitionRef = useRef(); @@ -118,7 +120,7 @@ const GiftModal: FC = ({ const user = peer && isApiPeerUser(peer) ? peer : undefined; const chat = peer && isApiPeerChat(peer) ? peer : undefined; - const [selectedGift, setSelectedGift] = useState(); + 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 = ({ 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) => { if (isGiftScreen) return; @@ -303,8 +309,7 @@ const GiftModal: FC = ({ openGiftInMarket({ gift, tabId }); return; } - setSelectedGift(gift); - setIsGiftScreenHeaderForStarGifts('id' in gift); + setGiftModalSelectedGift({ gift }); }); const handleMyGiftClick = useLastCallback((gift: ApiStarGift) => { @@ -423,7 +428,7 @@ const GiftModal: FC = ({ }); const handleCloseModal = useLastCallback(() => { - setSelectedGift(undefined); + setGiftModalSelectedGift({ gift: undefined }); resetResaleGifts(); closeGiftModal(); }); @@ -434,7 +439,8 @@ const GiftModal: FC = ({ return; } if (isGiftScreen) { - setSelectedGift(undefined); + setGiftModalSelectedGift({ gift: undefined }); + clearActiveGiftAuction(); return; } handleCloseModal(); @@ -589,7 +595,7 @@ const GiftModal: FC = ({ /> )} {isGiftScreen && renderingModal?.forPeerId && ( - { + const { modal } = props; + const GiftAuctionAcquiredModal = useModuleLoader(Bundles.Stars, 'GiftAuctionAcquiredModal', !modal); + + return GiftAuctionAcquiredModal ? : undefined; +}; + +export default GiftAuctionAcquiredModalAsync; diff --git a/src/components/modals/gift/auction/GiftAuctionAcquiredModal.module.scss b/src/components/modals/gift/auction/GiftAuctionAcquiredModal.module.scss new file mode 100644 index 000000000..02fe92d8b --- /dev/null +++ b/src/components/modals/gift/auction/GiftAuctionAcquiredModal.module.scss @@ -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; +} diff --git a/src/components/modals/gift/auction/GiftAuctionAcquiredModal.tsx b/src/components/modals/gift/auction/GiftAuctionAcquiredModal.tsx new file mode 100644 index 000000000..bacbfdead --- /dev/null +++ b/src/components/modals/gift/auction/GiftAuctionAcquiredModal.tsx @@ -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(); + + 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, ( + + {renderingGiftSticker && ( + + )} + {header} + + )], + [lang('GiftAuctionRecipient'), { chatId: gift.peerId }], + [lang('GiftAuctionDate'), formatDateTimeToString(gift.date * 1000, lang.code, true)], + [lang('GiftAuctionAcceptedBid'), ( + + {formatStarsAsIcon(lang, gift.bidAmount, { className: styles.starIcon })} + + {lang('GiftAuctionTopPosition', { position: gift.position })} + + + )], + ]; + + 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 ( + +
+
+ {giftItems?.map((item) => ( + + ))} +
+
+ +
+ ); +}; + +export default memo(withGlobal( + (global): Complete => { + const { giftAuctionAcquiredModal } = selectTabState(global); + + return { + acquiredGifts: giftAuctionAcquiredModal?.acquiredGifts, + giftTitle: giftAuctionAcquiredModal?.giftTitle, + giftSticker: giftAuctionAcquiredModal?.giftSticker, + }; + }, +)(GiftAuctionAcquiredModal)); diff --git a/src/components/modals/gift/auction/GiftAuctionBidModal.async.tsx b/src/components/modals/gift/auction/GiftAuctionBidModal.async.tsx new file mode 100644 index 000000000..935e73577 --- /dev/null +++ b/src/components/modals/gift/auction/GiftAuctionBidModal.async.tsx @@ -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 ? : undefined; +}; + +export default GiftAuctionBidModalAsync; diff --git a/src/components/modals/gift/auction/GiftAuctionBidModal.module.scss b/src/components/modals/gift/auction/GiftAuctionBidModal.module.scss new file mode 100644 index 000000000..7c6f53a12 --- /dev/null +++ b/src/components/modals/gift/auction/GiftAuctionBidModal.module.scss @@ -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; +} diff --git a/src/components/modals/gift/auction/GiftAuctionBidModal.tsx b/src/components/modals/gift/auction/GiftAuctionBidModal.tsx new file mode 100644 index 000000000..6dbb6f859 --- /dev/null +++ b/src/components/modals/gift/auction/GiftAuctionBidModal.tsx @@ -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 ( + <> + + + + + ); + }, [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) => { + 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 ( +
+
+
+ + {lang.number(currentMinBid)} +
+
{lang('GiftAuctionMinimumBid')}
+
+
+
+ +
+
{lang('GiftAuctionUntilNextRound')}
+
+
+
+ + {lang.number(activeState?.giftsLeft || 0)} +
+
{lang('GiftAuctionLeft')}
+
+
+ ); + } + + 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 ( + + {isWinning ? ( +
+ {lang('GiftAuctionYoureWinning')} + + {lang('GiftUnique', { title: giftTitle, number: nextGiftNum ? lang.number(nextGiftNum) : undefined })} + +
+ ) : ( +
{lang('GiftAuctionYourBidWillBe')}
+ )} +
+ ); + } + + function renderUserBid() { + return ( +
+ {renderCurrentBidSectionTitle()} +
+
+ {userPosition && userPosition > 100 ? `${userPosition}+` : (userPosition || 1)} +
+
+ {currentUserPeer && } + {currentUserPeer && } +
+
+ + {lang.number(selectedBidAmount)} +
+
+
+ ); + } + + function renderTopBidderRow( + index: number, + emoji: string, + peer: ApiPeer | undefined, + amount: number | undefined, + activeKey: number, + ) { + return ( +
+
+ {renderText(emoji, ['emoji'])} +
+ + {peer && ( + <> + + + + )} + + {amount !== undefined && ( +
+ + {lang.number(amount)} +
+ )} +
+ ); + } + + function renderTopWinners() { + const topCount = DEFAULT_TOP_BIDDERS_COUNT; + const bidLevels = activeState?.bidLevels; + + return ( +
+
+ {lang('GiftAuctionTopWinners', { count: topCount }, { pluralValue: topCount })} +
+ {renderTopBidderRow(0, '🥇', topBidder1, bidLevels?.[0]?.amount, topBidder1Key)} + {renderTopBidderRow(1, '🥈', topBidder2, bidLevels?.[1]?.amount, topBidder2Key)} + {renderTopBidderRow(2, '🥉', topBidder3, bidLevels?.[2]?.amount, topBidder3Key)} +
+ ); + } + + return ( + +
+ +
+ + + +

{lang('GiftAuctionPlaceBid')}

+ {renderInfoCards()} + + {renderUserBid()} + {renderTopWinners()} + + + +

{lang('GiftAuctionCustomBidDescription', { count: renderingAuctionState?.gift.giftsPerRound })}

+
+ + +
+
+
+ ); +}; + +export default memo(withGlobal( + (global): Complete => { + 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)); diff --git a/src/components/modals/gift/auction/GiftAuctionChangeRecipientModal.async.tsx b/src/components/modals/gift/auction/GiftAuctionChangeRecipientModal.async.tsx new file mode 100644 index 000000000..c9105a742 --- /dev/null +++ b/src/components/modals/gift/auction/GiftAuctionChangeRecipientModal.async.tsx @@ -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 ? : undefined; +}; + +export default GiftAuctionChangeRecipientModalAsync; diff --git a/src/components/modals/gift/auction/GiftAuctionChangeRecipientModal.module.scss b/src/components/modals/gift/auction/GiftAuctionChangeRecipientModal.module.scss new file mode 100644 index 000000000..63ae1f0c4 --- /dev/null +++ b/src/components/modals/gift/auction/GiftAuctionChangeRecipientModal.module.scss @@ -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); +} diff --git a/src/components/modals/gift/auction/GiftAuctionChangeRecipientModal.tsx b/src/components/modals/gift/auction/GiftAuctionChangeRecipientModal.tsx new file mode 100644 index 000000000..b88572921 --- /dev/null +++ b/src/components/modals/gift/auction/GiftAuctionChangeRecipientModal.tsx @@ -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 ( + +
+ + + +
+

+ {lang('GiftAuctionChangeRecipientDescription', { + oldPeer: getPeerTitle(lang, renderingOldPeer), + newPeer: getPeerTitle(lang, renderingNewPeer), + }, { + withNodes: true, + withMarkdown: true, + })} +

+
+ ); +}; + +export default memo( + withGlobal((global, { modal }): Complete => { + const oldPeer = modal?.oldPeerId ? selectPeer(global, modal.oldPeerId) : undefined; + const newPeer = modal?.newPeerId ? selectPeer(global, modal.newPeerId) : undefined; + + return { + oldPeer, + newPeer, + }; + })(GiftAuctionChangeRecipientModal), +); diff --git a/src/components/modals/gift/auction/GiftAuctionInfoModal.async.tsx b/src/components/modals/gift/auction/GiftAuctionInfoModal.async.tsx new file mode 100644 index 000000000..0ea0d7697 --- /dev/null +++ b/src/components/modals/gift/auction/GiftAuctionInfoModal.async.tsx @@ -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 ? : undefined; +}; + +export default GiftAuctionInfoModalAsync; diff --git a/src/components/modals/gift/auction/GiftAuctionInfoModal.module.scss b/src/components/modals/gift/auction/GiftAuctionInfoModal.module.scss new file mode 100644 index 000000000..427d5276b --- /dev/null +++ b/src/components/modals/gift/auction/GiftAuctionInfoModal.module.scss @@ -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; +} diff --git a/src/components/modals/gift/auction/GiftAuctionInfoModal.tsx b/src/components/modals/gift/auction/GiftAuctionInfoModal.tsx new file mode 100644 index 000000000..12aaefca9 --- /dev/null +++ b/src/components/modals/gift/auction/GiftAuctionInfoModal.tsx @@ -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 ( +
+
+ +
+
+ {lang('GiftAuctionInfoTitle')} +
+
+ {lang('GiftAuctionInfoSubtitle')} +
+
+ ); + }, [lang]); + + const footer = useMemo(() => { + if (!isOpen) return undefined; + return ( +
+ +
+ ); + }, [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 ( + + ); +}; + +export default memo(withGlobal( + (global): Complete => { + const { activeGiftAuction } = selectTabState(global); + + return { + activeGiftAuction, + }; + }, +)(GiftAuctionInfoModal)); diff --git a/src/components/modals/gift/auction/GiftAuctionModal.async.tsx b/src/components/modals/gift/auction/GiftAuctionModal.async.tsx new file mode 100644 index 000000000..3bfd37a10 --- /dev/null +++ b/src/components/modals/gift/auction/GiftAuctionModal.async.tsx @@ -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 ? : undefined; +}; + +export default GiftAuctionModalAsync; diff --git a/src/components/modals/gift/auction/GiftAuctionModal.module.scss b/src/components/modals/gift/auction/GiftAuctionModal.module.scss new file mode 100644 index 000000000..aa2d0a91d --- /dev/null +++ b/src/components/modals/gift/auction/GiftAuctionModal.module.scss @@ -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; +} diff --git a/src/components/modals/gift/auction/GiftAuctionModal.tsx b/src/components/modals/gift/auction/GiftAuctionModal.tsx new file mode 100644 index 000000000..77ee32105 --- /dev/null +++ b/src/components/modals/gift/auction/GiftAuctionModal.tsx @@ -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 ( +
+ +

+ {giftTitle} +

+ {isFinished ? ( + {lang('GiftAuctionEnded')} + ) : ( +

+ {lang('GiftAuctionTopBidders', { + count: giftsPerRound, + gift: {giftTitle}, + link: {lang('GiftAuctionLearnMore')}, + }, { pluralValue: giftsPerRound, withNodes: true, withMarkdown: true })} +

+ )} +
+ ); + }, [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 = ( +
+ {acquiredCount > 0 && ( + + {lang('GiftAuctionItemsBought', { + count: acquiredCount, + gift: giftSticker && ( + + ), + }, { pluralValue: acquiredCount, withNodes: true })} + + )} + +
+ ); + + return { + tableData, + footer, + }; + }, [gift, state, userState, isFinished, lang, handleJoinClick, handleItemsBoughtClick, handleClose]); + + return ( + + ); +}; + +export default memo(withGlobal( + (global): Complete => { + const { activeGiftAuction } = selectTabState(global); + + return { + auctionState: activeGiftAuction, + }; + }, +)(GiftAuctionModal)); diff --git a/src/components/modals/gift/info/GiftInfoModal.module.scss b/src/components/modals/gift/info/GiftInfoModal.module.scss index 86ed1a108..28efeb65a 100644 --- a/src/components/modals/gift/info/GiftInfoModal.module.scss +++ b/src/components/modals/gift/info/GiftInfoModal.module.scss @@ -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; diff --git a/src/components/modals/gift/info/GiftInfoModal.tsx b/src/components/modals/gift/info/GiftInfoModal.tsx index 019cca15a..96d1512de 100644 --- a/src/components/modals/gift/info/GiftInfoModal.tsx +++ b/src/components/modals/gift/info/GiftInfoModal.tsx @@ -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()} {Boolean(description) && ( -

+

{description}

)} diff --git a/src/components/modals/gift/upgrade/GiftUpgradeModal.tsx b/src/components/modals/gift/upgrade/GiftUpgradeModal.tsx index fbf886b74..20438cfe3 100644 --- a/src/components/modals/gift/upgrade/GiftUpgradeModal.tsx +++ b/src/components/modals/gift/upgrade/GiftUpgradeModal.tsx @@ -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(); + const [previewAttributes, setPreviewAttributes] = useState(); 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( }; }, )(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, - }; -} diff --git a/src/components/modals/paidReaction/StarSlider.module.scss b/src/components/modals/paidReaction/StarSlider.module.scss index c875171f1..def7b6b60 100644 --- a/src/components/modals/paidReaction/StarSlider.module.scss +++ b/src/components/modals/paidReaction/StarSlider.module.scss @@ -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); } diff --git a/src/components/modals/paidReaction/StarSlider.tsx b/src/components/modals/paidReaction/StarSlider.tsx index 08e618acc..2a9034ea5 100644 --- a/src/components/modals/paidReaction/StarSlider.tsx +++ b/src/components/modals/paidReaction/StarSlider.tsx @@ -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(); + const containerRef = useRef(); + const floatingBadgeContentRef = useRef(); + 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(); + 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) => { - 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) => { + startXRef.current = e.clientX; + setIsDragging(false); + }); + + const handlePointerMove = useLastCallback((e: React.PointerEvent) => { + 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 ( -
+
-
-
- - +
+
+ +
+
+ + +
+
+ {floatingBadgeDescription} +
+
- - - - - - - - +
-
+
+ {shouldAllowCustomValue && ( + + )}
); }; -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); diff --git a/src/components/modals/stars/helpers/transaction.ts b/src/components/modals/stars/helpers/transaction.ts index 7cb84b050..c00bbfb8d 100644 --- a/src/components/modals/stars/helpers/transaction.ts +++ b/src/components/modals/stars/helpers/transaction.ts @@ -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)); } diff --git a/src/components/modals/stars/transaction/StarsTransactionModal.tsx b/src/components/modals/stars/transaction/StarsTransactionModal.tsx index a74b1b556..ce9aac09b 100644 --- a/src/components/modals/stars/transaction/StarsTransactionModal.tsx +++ b/src/components/modals/stars/transaction/StarsTransactionModal.tsx @@ -260,7 +260,7 @@ const StarsTransactionModal: FC = ({ 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 = ({ 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}'); diff --git a/src/components/ui/TextTimer.tsx b/src/components/ui/TextTimer.tsx index 6aee9ed87..2941d7080 100644 --- a/src/components/ui/TextTimer.tsx +++ b/src/components/ui/TextTimer.tsx @@ -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(':'); diff --git a/src/global/actions/api/payments.ts b/src/global/actions/api/payments.ts index 6e874e398..dca1b3984 100644 --- a/src/global/actions/api/payments.ts +++ b/src/global/actions/api/payments.ts @@ -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 => { 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 { + 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( global: T, inputInvoice: ApiInputInvoice, price: number, ...[tabId = getCurrentTabId()]: TabArgs @@ -1229,6 +1249,26 @@ addActionHandler('openUniqueGiftBySlug', async (global, actions, payload): Promi actions.openGiftInfoModal({ gift, tabId }); }); +addActionHandler('openGiftAuctionBySlug', async (global, actions, payload): Promise => { + 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 => { const { gift, password, tabId = getCurrentTabId(), diff --git a/src/global/actions/api/stars.ts b/src/global/actions/api/stars.ts index ab4bfd283..9e9d08d31 100644 --- a/src/global/actions/api/stars.ts +++ b/src/global/actions/api/stars.ts @@ -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 => { + 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 => { + 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 => { 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 => { + 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); +}); diff --git a/src/global/actions/apiUpdaters/misc.ts b/src/global/actions/apiUpdaters/misc.ts index 399c8f476..8bdc25bb3 100644 --- a/src/global/actions/apiUpdaters/misc.ts +++ b/src/global/actions/apiUpdaters/misc.ts @@ -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; diff --git a/src/global/actions/apiUpdaters/payments.ts b/src/global/actions/apiUpdaters/payments.ts index 3a2565581..3fd068987 100644 --- a/src/global/actions/apiUpdaters/payments.ts +++ b/src/global/actions/apiUpdaters/payments.ts @@ -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; + } } }); diff --git a/src/global/actions/ui/stars.ts b/src/global/actions/ui/stars.ts index b176d3a29..57839f902 100644 --- a/src/global/actions/ui/stars.ts +++ b/src/global/actions/ui/stars.ts @@ -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(), diff --git a/src/global/helpers/payments.ts b/src/global/helpers/payments.ts index 13b2f2289..dac43c3f7 100644 --- a/src/global/helpers/payments.ts +++ b/src/global/helpers/payments.ts @@ -261,6 +261,23 @@ export function getRequestInputInvoice( }; } + 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; } diff --git a/src/global/reducers/gifts.ts b/src/global/reducers/gifts.ts index 928160efe..59251e80e 100644 --- a/src/global/reducers/gifts.ts +++ b/src/global/reducers/gifts.ts @@ -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( @@ -46,3 +52,73 @@ export function removeGiftInfoOriginalDetails( }, }, tabId); } + +function getAuctionStateVersion(state: ApiTypeStarGiftAuctionState): number { + return state.type === 'active' ? state.version : 0; +} + +export function updateActiveGiftAuction( + 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( + 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( + 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); +} diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index ce1b6543f..550e4ee1d 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -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; diff --git a/src/global/types/tabState.ts b/src/global/types/tabState.ts index e6ccf51e2..007ee303c 100644 --- a/src/global/types/tabState.ts +++ b/src/global/types/tabState.ts @@ -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; diff --git a/src/hooks/useScrollNotch.ts b/src/hooks/useScrollNotch.ts index bd71db1de..183782d56 100644 --- a/src/hooks/useScrollNotch.ts +++ b/src/hooks/useScrollNotch.ts @@ -12,10 +12,12 @@ const useScrollNotch = ({ containerRef, selector, isBottomNotch, + shouldHideTopNotch, }: { containerRef: ElementRef; selector: string; isBottomNotch?: boolean; + shouldHideTopNotch?: boolean; }, deps: unknown[]) => { useLayoutEffect(() => { const elements = containerRef.current?.querySelectorAll(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(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; diff --git a/src/lib/gramjs/tl/apiTl.ts b/src/lib/gramjs/tl/apiTl.ts index 0cbbfdb8c..8e8fadab7 100644 --- a/src/lib/gramjs/tl/apiTl.ts +++ b/src/lib/gramjs/tl/apiTl.ts @@ -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; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 04593281e..e56105753 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -352,6 +352,8 @@ "payments.getResaleStarGifts", "payments.updateStarGiftPrice", "payments.getStarGiftCollections", + "payments.getStarGiftAuctionState", + "payments.getStarGiftAuctionAcquiredGifts", "langpack.getLangPack", "langpack.getStrings", "langpack.getLanguages", diff --git a/src/styles/icons.css b/src/styles/icons.css index 5c280a21d..ba26a5b5b 100644 --- a/src/styles/icons.css +++ b/src/styles/icons.css @@ -3,8 +3,8 @@ font-weight: normal; font-style: normal; font-display: block; - src: url("./icons.woff2?a07bab7d97a4c6540cca18b1d787228b") format("woff2"), -url("./icons.woff?a07bab7d97a4c6540cca18b1d787228b") format("woff"); + src: url("./icons.woff2?78af88c0f83feccd1a41bd4d851b003d") format("woff2"), +url("./icons.woff?78af88c0f83feccd1a41bd4d851b003d") format("woff"); } .icon-char::before { @@ -78,867 +78,876 @@ url("./icons.woff?a07bab7d97a4c6540cca18b1d787228b") format("woff"); .icon-attach::before { content: "\f113"; } -.icon-auction::before { +.icon-auction-drop::before { content: "\f114"; } -.icon-author-hidden::before { +.icon-auction-filled::before { content: "\f115"; } -.icon-avatar-archived-chats::before { +.icon-auction-next-round::before { content: "\f116"; } -.icon-avatar-deleted-account::before { +.icon-auction::before { content: "\f117"; } -.icon-avatar-saved-messages::before { +.icon-author-hidden::before { content: "\f118"; } -.icon-bold::before { +.icon-avatar-archived-chats::before { content: "\f119"; } -.icon-boost-outline::before { +.icon-avatar-deleted-account::before { content: "\f11a"; } -.icon-boost::before { +.icon-avatar-saved-messages::before { content: "\f11b"; } -.icon-boostcircle::before { +.icon-bold::before { content: "\f11c"; } -.icon-boosts::before { +.icon-boost-outline::before { content: "\f11d"; } -.icon-bot-command::before { +.icon-boost::before { content: "\f11e"; } -.icon-bot-commands-filled::before { +.icon-boostcircle::before { content: "\f11f"; } -.icon-bots::before { +.icon-boosts::before { content: "\f120"; } -.icon-bug::before { +.icon-bot-command::before { content: "\f121"; } -.icon-calendar-filter::before { +.icon-bot-commands-filled::before { content: "\f122"; } -.icon-calendar::before { +.icon-bots::before { content: "\f123"; } -.icon-camera-add::before { +.icon-bug::before { content: "\f124"; } -.icon-camera::before { +.icon-calendar-filter::before { content: "\f125"; } -.icon-car::before { +.icon-calendar::before { content: "\f126"; } -.icon-card::before { +.icon-camera-add::before { content: "\f127"; } -.icon-cash-circle::before { +.icon-camera::before { content: "\f128"; } -.icon-channel-filled::before { +.icon-car::before { content: "\f129"; } -.icon-channel::before { +.icon-card::before { content: "\f12a"; } -.icon-channelviews::before { +.icon-cash-circle::before { content: "\f12b"; } -.icon-chat-badge::before { +.icon-channel-filled::before { content: "\f12c"; } -.icon-chats-badge::before { +.icon-channel::before { content: "\f12d"; } -.icon-check::before { +.icon-channelviews::before { content: "\f12e"; } -.icon-clock-edit::before { +.icon-chat-badge::before { content: "\f12f"; } -.icon-clock::before { +.icon-chats-badge::before { content: "\f130"; } -.icon-close-circle::before { +.icon-check::before { content: "\f131"; } -.icon-close-topic::before { +.icon-clock-edit::before { content: "\f132"; } -.icon-close::before { +.icon-clock::before { content: "\f133"; } -.icon-closed-gift::before { +.icon-close-circle::before { content: "\f134"; } -.icon-cloud-download::before { +.icon-close-topic::before { content: "\f135"; } -.icon-collapse-modal::before { +.icon-close::before { content: "\f136"; } -.icon-collapse::before { +.icon-closed-gift::before { content: "\f137"; } -.icon-colorize::before { +.icon-cloud-download::before { content: "\f138"; } -.icon-comments-sticker::before { +.icon-collapse-modal::before { content: "\f139"; } -.icon-comments::before { +.icon-collapse::before { content: "\f13a"; } -.icon-copy-media::before { +.icon-colorize::before { content: "\f13b"; } -.icon-copy::before { +.icon-comments-sticker::before { content: "\f13c"; } -.icon-crown-take-off-outline::before { +.icon-comments::before { content: "\f13d"; } -.icon-crown-take-off::before { +.icon-copy-media::before { content: "\f13e"; } -.icon-crown-wear-outline::before { +.icon-copy::before { content: "\f13f"; } -.icon-crown-wear::before { +.icon-crown-take-off-outline::before { content: "\f140"; } -.icon-darkmode::before { +.icon-crown-take-off::before { content: "\f141"; } -.icon-data::before { +.icon-crown-wear-outline::before { content: "\f142"; } -.icon-delete-filled::before { +.icon-crown-wear::before { content: "\f143"; } -.icon-delete-left::before { +.icon-darkmode::before { content: "\f144"; } -.icon-delete-user::before { +.icon-data::before { content: "\f145"; } -.icon-delete::before { +.icon-delete-filled::before { content: "\f146"; } -.icon-diamond::before { +.icon-delete-left::before { content: "\f147"; } -.icon-document::before { +.icon-delete-user::before { content: "\f148"; } -.icon-double-badge::before { +.icon-delete::before { content: "\f149"; } -.icon-down::before { +.icon-diamond::before { content: "\f14a"; } -.icon-download::before { +.icon-document::before { content: "\f14b"; } -.icon-dropdown-arrows::before { +.icon-double-badge::before { content: "\f14c"; } -.icon-eats::before { +.icon-down::before { content: "\f14d"; } -.icon-edit::before { +.icon-download::before { content: "\f14e"; } -.icon-email::before { +.icon-dropdown-arrows::before { content: "\f14f"; } -.icon-enter::before { +.icon-eats::before { content: "\f150"; } -.icon-expand-modal::before { +.icon-edit::before { content: "\f151"; } -.icon-expand::before { +.icon-email::before { content: "\f152"; } -.icon-eye-crossed-outline::before { +.icon-enter::before { content: "\f153"; } -.icon-eye-crossed::before { +.icon-expand-modal::before { content: "\f154"; } -.icon-eye-outline::before { +.icon-expand::before { content: "\f155"; } -.icon-eye::before { +.icon-eye-crossed-outline::before { content: "\f156"; } -.icon-favorite-filled::before { +.icon-eye-crossed::before { content: "\f157"; } -.icon-favorite::before { +.icon-eye-outline::before { content: "\f158"; } -.icon-file-badge::before { +.icon-eye::before { content: "\f159"; } -.icon-flag::before { +.icon-favorite-filled::before { content: "\f15a"; } -.icon-folder-badge::before { +.icon-favorite::before { content: "\f15b"; } -.icon-folder-tabs-bot::before { +.icon-file-badge::before { content: "\f15c"; } -.icon-folder-tabs-channel::before { +.icon-flag::before { content: "\f15d"; } -.icon-folder-tabs-chat::before { +.icon-folder-badge::before { content: "\f15e"; } -.icon-folder-tabs-chats::before { +.icon-folder-tabs-bot::before { content: "\f15f"; } -.icon-folder-tabs-folder::before { +.icon-folder-tabs-channel::before { content: "\f160"; } -.icon-folder-tabs-group::before { +.icon-folder-tabs-chat::before { content: "\f161"; } -.icon-folder-tabs-star::before { +.icon-folder-tabs-chats::before { content: "\f162"; } -.icon-folder-tabs-user::before { +.icon-folder-tabs-folder::before { content: "\f163"; } -.icon-folder::before { +.icon-folder-tabs-group::before { content: "\f164"; } -.icon-fontsize::before { +.icon-folder-tabs-star::before { content: "\f165"; } -.icon-forums::before { +.icon-folder-tabs-user::before { content: "\f166"; } -.icon-forward::before { +.icon-folder::before { content: "\f167"; } -.icon-fragment::before { +.icon-fontsize::before { content: "\f168"; } -.icon-frozen-time::before { +.icon-forums::before { content: "\f169"; } -.icon-fullscreen::before { +.icon-forward::before { content: "\f16a"; } -.icon-gifs::before { +.icon-fragment::before { content: "\f16b"; } -.icon-gift-transfer-inline::before { +.icon-frozen-time::before { content: "\f16c"; } -.icon-gift::before { +.icon-fullscreen::before { content: "\f16d"; } -.icon-group-filled::before { +.icon-gifs::before { content: "\f16e"; } -.icon-group::before { +.icon-gift-transfer-inline::before { content: "\f16f"; } -.icon-grouped-disable::before { +.icon-gift::before { content: "\f170"; } -.icon-grouped::before { +.icon-group-filled::before { content: "\f171"; } -.icon-hand-stop::before { +.icon-group::before { content: "\f172"; } -.icon-hashtag::before { +.icon-grouped-disable::before { content: "\f173"; } -.icon-hd-photo::before { +.icon-grouped::before { content: "\f174"; } -.icon-heart-outline::before { +.icon-hand-stop::before { content: "\f175"; } -.icon-heart::before { +.icon-hashtag::before { content: "\f176"; } -.icon-help::before { +.icon-hd-photo::before { content: "\f177"; } -.icon-info-filled::before { +.icon-heart-outline::before { content: "\f178"; } -.icon-info::before { +.icon-heart::before { content: "\f179"; } -.icon-install::before { +.icon-help::before { content: "\f17a"; } -.icon-italic::before { +.icon-info-filled::before { content: "\f17b"; } -.icon-key::before { +.icon-info::before { content: "\f17c"; } -.icon-keyboard::before { +.icon-install::before { content: "\f17d"; } -.icon-lamp::before { +.icon-italic::before { content: "\f17e"; } -.icon-language::before { +.icon-key::before { content: "\f17f"; } -.icon-large-pause::before { +.icon-keyboard::before { content: "\f180"; } -.icon-large-play::before { +.icon-lamp::before { content: "\f181"; } -.icon-link-badge::before { +.icon-language::before { content: "\f182"; } -.icon-link-broken::before { +.icon-large-pause::before { content: "\f183"; } -.icon-link::before { +.icon-large-play::before { content: "\f184"; } -.icon-location::before { +.icon-link-badge::before { content: "\f185"; } -.icon-lock-badge::before { +.icon-link-broken::before { content: "\f186"; } -.icon-lock::before { +.icon-link::before { content: "\f187"; } -.icon-logout::before { +.icon-location::before { content: "\f188"; } -.icon-loop::before { +.icon-lock-badge::before { content: "\f189"; } -.icon-mention::before { +.icon-lock::before { content: "\f18a"; } -.icon-menu::before { +.icon-logout::before { content: "\f18b"; } -.icon-message-failed::before { +.icon-loop::before { content: "\f18c"; } -.icon-message-pending::before { +.icon-mention::before { content: "\f18d"; } -.icon-message-read::before { +.icon-menu::before { content: "\f18e"; } -.icon-message-succeeded::before { +.icon-message-failed::before { content: "\f18f"; } -.icon-message::before { +.icon-message-pending::before { content: "\f190"; } -.icon-microphone-alt::before { +.icon-message-read::before { content: "\f191"; } -.icon-microphone::before { +.icon-message-succeeded::before { content: "\f192"; } -.icon-monospace::before { +.icon-message::before { content: "\f193"; } -.icon-more-circle::before { +.icon-microphone-alt::before { content: "\f194"; } -.icon-more::before { +.icon-microphone::before { content: "\f195"; } -.icon-move-caption-down::before { +.icon-monospace::before { content: "\f196"; } -.icon-move-caption-up::before { +.icon-more-circle::before { content: "\f197"; } -.icon-mute::before { +.icon-more::before { content: "\f198"; } -.icon-muted::before { +.icon-move-caption-down::before { content: "\f199"; } -.icon-my-notes::before { +.icon-move-caption-up::before { content: "\f19a"; } -.icon-new-chat-filled::before { +.icon-mute::before { content: "\f19b"; } -.icon-next::before { +.icon-muted::before { content: "\f19c"; } -.icon-nochannel::before { +.icon-my-notes::before { content: "\f19d"; } -.icon-noise-suppression::before { +.icon-new-chat-filled::before { content: "\f19e"; } -.icon-non-contacts::before { +.icon-next::before { content: "\f19f"; } -.icon-note::before { +.icon-nochannel::before { content: "\f1a0"; } -.icon-one-filled::before { +.icon-noise-suppression::before { content: "\f1a1"; } -.icon-open-in-new-tab::before { +.icon-non-contacts::before { content: "\f1a2"; } -.icon-password-off::before { +.icon-note::before { content: "\f1a3"; } -.icon-pause::before { +.icon-one-filled::before { content: "\f1a4"; } -.icon-permissions::before { +.icon-open-in-new-tab::before { content: "\f1a5"; } -.icon-phone-discard-outline::before { +.icon-password-off::before { content: "\f1a6"; } -.icon-phone-discard::before { +.icon-pause::before { content: "\f1a7"; } -.icon-phone::before { +.icon-permissions::before { content: "\f1a8"; } -.icon-photo::before { +.icon-phone-discard-outline::before { content: "\f1a9"; } -.icon-pin-badge::before { +.icon-phone-discard::before { content: "\f1aa"; } -.icon-pin-list::before { +.icon-phone::before { content: "\f1ab"; } -.icon-pin::before { +.icon-photo::before { content: "\f1ac"; } -.icon-pinned-chat::before { +.icon-pin-badge::before { content: "\f1ad"; } -.icon-pinned-message::before { +.icon-pin-list::before { content: "\f1ae"; } -.icon-pip::before { +.icon-pin::before { content: "\f1af"; } -.icon-play-story::before { +.icon-pinned-chat::before { content: "\f1b0"; } -.icon-play::before { +.icon-pinned-message::before { content: "\f1b1"; } -.icon-poll::before { +.icon-pip::before { content: "\f1b2"; } -.icon-previous::before { +.icon-play-story::before { content: "\f1b3"; } -.icon-privacy-policy::before { +.icon-play::before { content: "\f1b4"; } -.icon-proof-of-ownership::before { +.icon-poll::before { content: "\f1b5"; } -.icon-quote-text::before { +.icon-previous::before { content: "\f1b6"; } -.icon-quote::before { +.icon-privacy-policy::before { content: "\f1b7"; } -.icon-radial-badge::before { +.icon-proof-of-ownership::before { content: "\f1b8"; } -.icon-rating-icons-level1::before { +.icon-quote-text::before { content: "\f1b9"; } -.icon-rating-icons-level10::before { +.icon-quote::before { content: "\f1ba"; } -.icon-rating-icons-level2::before { +.icon-radial-badge::before { content: "\f1bb"; } -.icon-rating-icons-level20::before { +.icon-rating-icons-level1::before { content: "\f1bc"; } -.icon-rating-icons-level3::before { +.icon-rating-icons-level10::before { content: "\f1bd"; } -.icon-rating-icons-level30::before { +.icon-rating-icons-level2::before { content: "\f1be"; } -.icon-rating-icons-level4::before { +.icon-rating-icons-level20::before { content: "\f1bf"; } -.icon-rating-icons-level40::before { +.icon-rating-icons-level3::before { content: "\f1c0"; } -.icon-rating-icons-level5::before { +.icon-rating-icons-level30::before { content: "\f1c1"; } -.icon-rating-icons-level50::before { +.icon-rating-icons-level4::before { content: "\f1c2"; } -.icon-rating-icons-level6::before { +.icon-rating-icons-level40::before { content: "\f1c3"; } -.icon-rating-icons-level60::before { +.icon-rating-icons-level5::before { content: "\f1c4"; } -.icon-rating-icons-level7::before { +.icon-rating-icons-level50::before { content: "\f1c5"; } -.icon-rating-icons-level70::before { +.icon-rating-icons-level6::before { content: "\f1c6"; } -.icon-rating-icons-level8::before { +.icon-rating-icons-level60::before { content: "\f1c7"; } -.icon-rating-icons-level80::before { +.icon-rating-icons-level7::before { content: "\f1c8"; } -.icon-rating-icons-level9::before { +.icon-rating-icons-level70::before { content: "\f1c9"; } -.icon-rating-icons-level90::before { +.icon-rating-icons-level8::before { content: "\f1ca"; } -.icon-rating-icons-negative::before { +.icon-rating-icons-level80::before { content: "\f1cb"; } -.icon-readchats::before { +.icon-rating-icons-level9::before { content: "\f1cc"; } -.icon-recent::before { +.icon-rating-icons-level90::before { content: "\f1cd"; } -.icon-refund::before { +.icon-rating-icons-negative::before { content: "\f1ce"; } -.icon-reload::before { +.icon-readchats::before { content: "\f1cf"; } -.icon-remove-quote::before { +.icon-recent::before { content: "\f1d0"; } -.icon-remove::before { +.icon-refund::before { content: "\f1d1"; } -.icon-reopen-topic::before { +.icon-reload::before { content: "\f1d2"; } -.icon-reorder-tabs::before { +.icon-remove-quote::before { content: "\f1d3"; } -.icon-replace::before { +.icon-remove::before { content: "\f1d4"; } -.icon-replies::before { +.icon-reopen-topic::before { content: "\f1d5"; } -.icon-reply-filled::before { +.icon-reorder-tabs::before { content: "\f1d6"; } -.icon-reply::before { +.icon-replace::before { content: "\f1d7"; } -.icon-revenue-split::before { +.icon-replies::before { content: "\f1d8"; } -.icon-revote::before { +.icon-reply-filled::before { content: "\f1d9"; } -.icon-save-story::before { +.icon-reply::before { content: "\f1da"; } -.icon-saved-messages::before { +.icon-revenue-split::before { content: "\f1db"; } -.icon-schedule::before { +.icon-revote::before { content: "\f1dc"; } -.icon-scheduled::before { +.icon-save-story::before { content: "\f1dd"; } -.icon-sd-photo::before { +.icon-saved-messages::before { content: "\f1de"; } -.icon-search::before { +.icon-schedule::before { content: "\f1df"; } -.icon-select::before { +.icon-scheduled::before { content: "\f1e0"; } -.icon-sell-outline::before { +.icon-sd-photo::before { content: "\f1e1"; } -.icon-sell::before { +.icon-search::before { content: "\f1e2"; } -.icon-send-outline::before { +.icon-select::before { content: "\f1e3"; } -.icon-send::before { +.icon-sell-outline::before { content: "\f1e4"; } -.icon-settings-filled::before { +.icon-sell::before { content: "\f1e5"; } -.icon-settings::before { +.icon-send-outline::before { content: "\f1e6"; } -.icon-share-filled::before { +.icon-send::before { content: "\f1e7"; } -.icon-share-screen-outlined::before { +.icon-settings-filled::before { content: "\f1e8"; } -.icon-share-screen-stop::before { +.icon-settings::before { content: "\f1e9"; } -.icon-share-screen::before { +.icon-share-filled::before { content: "\f1ea"; } -.icon-show-message::before { +.icon-share-screen-outlined::before { content: "\f1eb"; } -.icon-sidebar::before { +.icon-share-screen-stop::before { content: "\f1ec"; } -.icon-skip-next::before { +.icon-share-screen::before { content: "\f1ed"; } -.icon-skip-previous::before { +.icon-show-message::before { content: "\f1ee"; } -.icon-smallscreen::before { +.icon-sidebar::before { content: "\f1ef"; } -.icon-smile::before { +.icon-skip-next::before { content: "\f1f0"; } -.icon-sort-by-date::before { +.icon-skip-previous::before { content: "\f1f1"; } -.icon-sort-by-number::before { +.icon-smallscreen::before { content: "\f1f2"; } -.icon-sort-by-price::before { +.icon-smile::before { content: "\f1f3"; } -.icon-sort::before { +.icon-sort-by-date::before { content: "\f1f4"; } -.icon-speaker-muted-story::before { +.icon-sort-by-number::before { content: "\f1f5"; } -.icon-speaker-outline::before { +.icon-sort-by-price::before { content: "\f1f6"; } -.icon-speaker-story::before { +.icon-sort::before { content: "\f1f7"; } -.icon-speaker::before { +.icon-speaker-muted-story::before { content: "\f1f8"; } -.icon-spoiler-disable::before { +.icon-speaker-outline::before { content: "\f1f9"; } -.icon-spoiler::before { +.icon-speaker-story::before { content: "\f1fa"; } -.icon-sport::before { +.icon-speaker::before { content: "\f1fb"; } -.icon-star::before { +.icon-spoiler-disable::before { content: "\f1fc"; } -.icon-stars-lock::before { +.icon-spoiler::before { content: "\f1fd"; } -.icon-stars-refund::before { +.icon-sport::before { content: "\f1fe"; } -.icon-stats::before { +.icon-star::before { content: "\f1ff"; } -.icon-stealth-future::before { +.icon-stars-lock::before { content: "\f200"; } -.icon-stealth-past::before { +.icon-stars-refund::before { content: "\f201"; } -.icon-stickers::before { +.icon-stats::before { content: "\f202"; } -.icon-stop-raising-hand::before { +.icon-stealth-future::before { content: "\f203"; } -.icon-stop::before { +.icon-stealth-past::before { content: "\f204"; } -.icon-story-caption::before { +.icon-stickers::before { content: "\f205"; } -.icon-story-expired::before { +.icon-stop-raising-hand::before { content: "\f206"; } -.icon-story-priority::before { +.icon-stop::before { content: "\f207"; } -.icon-story-reply::before { +.icon-story-caption::before { content: "\f208"; } -.icon-strikethrough::before { +.icon-story-expired::before { content: "\f209"; } -.icon-tag-add::before { +.icon-story-priority::before { content: "\f20a"; } -.icon-tag-crossed::before { +.icon-story-reply::before { content: "\f20b"; } -.icon-tag-filter::before { +.icon-strikethrough::before { content: "\f20c"; } -.icon-tag-name::before { +.icon-tag-add::before { content: "\f20d"; } -.icon-tag::before { +.icon-tag-crossed::before { content: "\f20e"; } -.icon-timer::before { +.icon-tag-filter::before { content: "\f20f"; } -.icon-toncoin::before { +.icon-tag-name::before { content: "\f210"; } -.icon-tools::before { +.icon-tag::before { content: "\f211"; } -.icon-topic-new::before { +.icon-timer::before { content: "\f212"; } -.icon-trade::before { +.icon-toncoin::before { content: "\f213"; } -.icon-transcribe::before { +.icon-tools::before { content: "\f214"; } -.icon-truck::before { +.icon-topic-new::before { content: "\f215"; } -.icon-unarchive::before { +.icon-trade::before { content: "\f216"; } -.icon-underlined::before { +.icon-transcribe::before { content: "\f217"; } -.icon-understood::before { +.icon-truck::before { content: "\f218"; } -.icon-unique-profile::before { +.icon-unarchive::before { content: "\f219"; } -.icon-unlist-outline::before { +.icon-underlined::before { content: "\f21a"; } -.icon-unlist::before { +.icon-understood::before { content: "\f21b"; } -.icon-unlock-badge::before { +.icon-unique-profile::before { content: "\f21c"; } -.icon-unlock::before { +.icon-unlist-outline::before { content: "\f21d"; } -.icon-unmute::before { +.icon-unlist::before { content: "\f21e"; } -.icon-unpin::before { +.icon-unlock-badge::before { content: "\f21f"; } -.icon-unread::before { +.icon-unlock::before { content: "\f220"; } -.icon-up::before { +.icon-unmute::before { content: "\f221"; } -.icon-user-filled::before { +.icon-unpin::before { content: "\f222"; } -.icon-user-online::before { +.icon-unread::before { content: "\f223"; } -.icon-user-stars::before { +.icon-up::before { content: "\f224"; } -.icon-user::before { +.icon-user-filled::before { content: "\f225"; } -.icon-video-outlined::before { +.icon-user-online::before { content: "\f226"; } -.icon-video-stop::before { +.icon-user-stars::before { content: "\f227"; } -.icon-video::before { +.icon-user::before { content: "\f228"; } -.icon-view-once::before { +.icon-video-outlined::before { content: "\f229"; } -.icon-voice-chat::before { +.icon-video-stop::before { content: "\f22a"; } -.icon-volume-1::before { +.icon-video::before { content: "\f22b"; } -.icon-volume-2::before { +.icon-view-once::before { content: "\f22c"; } -.icon-volume-3::before { +.icon-voice-chat::before { content: "\f22d"; } -.icon-warning::before { +.icon-volume-1::before { content: "\f22e"; } -.icon-web::before { +.icon-volume-2::before { content: "\f22f"; } -.icon-webapp::before { +.icon-volume-3::before { content: "\f230"; } -.icon-word-wrap::before { +.icon-warning::before { content: "\f231"; } -.icon-zoom-in::before { +.icon-web::before { content: "\f232"; } -.icon-zoom-out::before { +.icon-webapp::before { content: "\f233"; } +.icon-word-wrap::before { + content: "\f234"; +} +.icon-zoom-in::before { + content: "\f235"; +} +.icon-zoom-out::before { + content: "\f236"; +} diff --git a/src/styles/icons.scss b/src/styles/icons.scss index f779cff39..9b0906817 100644 --- a/src/styles/icons.scss +++ b/src/styles/icons.scss @@ -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", ); diff --git a/src/styles/icons.woff b/src/styles/icons.woff index cdbd4c2e2..6ed95509a 100644 Binary files a/src/styles/icons.woff and b/src/styles/icons.woff differ diff --git a/src/styles/icons.woff2 b/src/styles/icons.woff2 index e993561d9..39db27fcc 100644 Binary files a/src/styles/icons.woff2 and b/src/styles/icons.woff2 differ diff --git a/src/types/icons/font.ts b/src/types/icons/font.ts index f16e15bcd..fc0cf13bd 100644 --- a/src/types/icons/font.ts +++ b/src/types/icons/font.ts @@ -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' diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 20a2e7321..6b1bb309f 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -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 { 'ActionStarGiftLimitedRibbon': { 'total': V; }; + 'ActionStarGiftAuctionWon': { + 'cost': V; + }; + 'ActionStarGiftAuctionFor': { + 'peer': V; + }; + 'ActionStarGiftAuctionBought': { + 'cost': V; + }; 'ActionSuggestedPhotoYou': { 'user': V; }; @@ -3120,6 +3165,43 @@ export interface LangPairWithVariables { '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 { '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; diff --git a/src/util/deepLinkParser.ts b/src/util/deepLinkParser.ts index 21fe2efb9..e4c033143 100644 --- a/src/util/deepLinkParser.ts +++ b/src/util/deepLinkParser.ts @@ -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): BuilderRetu }; } +function buildGiftAuctionLink(params: BuilderParams): BuilderReturnType { + const { + slug, + } = params; + + if (!slug) { + return undefined; + } + + return { + type: 'giftAuctionLink', + slug, + }; +} + function buildSettingsScreen(screenParam: string) { switch (screenParam) { case 'devices': diff --git a/src/util/deeplink.ts b/src/util/deeplink.ts index f53304340..b0db574f5 100644 --- a/src/util/deeplink.ts +++ b/src/util/deeplink.ts @@ -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 });