diff --git a/src/api/gramjs/apiBuilders/gifts.ts b/src/api/gramjs/apiBuilders/gifts.ts new file mode 100644 index 000000000..5ea94e483 --- /dev/null +++ b/src/api/gramjs/apiBuilders/gifts.ts @@ -0,0 +1,141 @@ +import { Api as GramJs } from '../../../lib/gramjs'; + +import type { + ApiStarGift, + ApiStarGiftAttribute, + ApiUserStarGift, +} from '../../types'; + +import { numberToHexColor } from '../../../util/colors'; +import { addDocumentToLocalDb } from '../helpers'; +import { buildApiFormattedText } from './common'; +import { buildApiPeerId } from './peers'; +import { buildStickerFromDocument } from './symbols'; + +export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift { + if (starGift instanceof GramJs.StarGiftUnique) { + const { + id, num, ownerId, ownerName, title, attributes, availabilityIssued, availabilityTotal, + } = starGift; + + return { + type: 'starGiftUnique', + id: id.toString(), + number: num, + ownerId: ownerId && buildApiPeerId(ownerId, 'user'), + ownerName, + attributes: attributes.map(buildApiStarGiftAttribute).filter(Boolean), + title, + totalCount: availabilityTotal, + issuedCount: availabilityIssued, + }; + } + + const { + id, limited, stars, availabilityRemains, availabilityTotal, convertStars, firstSaleDate, lastSaleDate, soldOut, + birthday, upgradeStars, + } = starGift; + + addDocumentToLocalDb(starGift.sticker); + + const sticker = buildStickerFromDocument(starGift.sticker)!; + + return { + type: 'starGift', + id: id.toString(), + isLimited: limited, + sticker, + stars: stars.toJSNumber(), + availabilityRemains, + availabilityTotal, + starsToConvert: convertStars.toJSNumber(), + firstSaleDate, + lastSaleDate, + isSoldOut: soldOut, + isBirthday: birthday, + upgradeStars: upgradeStars?.toJSNumber(), + }; +} + +export function buildApiStarGiftAttribute(attribute: GramJs.TypeStarGiftAttribute): ApiStarGiftAttribute | undefined { + if (attribute instanceof GramJs.StarGiftAttributeModel) { + const sticker = buildStickerFromDocument(attribute.document); + if (!sticker) { + return undefined; + } + + addDocumentToLocalDb(attribute.document); + + return { + type: 'model', + name: attribute.name, + rarityPercent: attribute.rarityPermille / 10, + sticker, + }; + } + + if (attribute instanceof GramJs.StarGiftAttributePattern) { + const sticker = buildStickerFromDocument(attribute.document); + if (!sticker) { + return undefined; + } + + addDocumentToLocalDb(attribute.document); + + return { + type: 'pattern', + name: attribute.name, + rarityPercent: attribute.rarityPermille / 10, + sticker, + }; + } + + if (attribute instanceof GramJs.StarGiftAttributeBackdrop) { + const { + name, rarityPermille, centerColor, edgeColor, patternColor, textColor, + } = attribute; + + return { + type: 'backdrop', + name, + rarityPercent: rarityPermille / 10, + centerColor: numberToHexColor(centerColor), + edgeColor: numberToHexColor(edgeColor), + patternColor: numberToHexColor(patternColor), + textColor: numberToHexColor(textColor), + }; + } + + if (attribute instanceof GramJs.StarGiftAttributeOriginalDetails) { + const { + date, recipientId, message, senderId, + } = attribute; + + return { + type: 'originalDetails', + date, + recipientId: recipientId && buildApiPeerId(recipientId, 'user'), + message: message && buildApiFormattedText(message), + senderId: senderId && buildApiPeerId(senderId, 'user'), + }; + } + + return undefined; +} + +export function buildApiUserStarGift(userStarGift: GramJs.UserStarGift): ApiUserStarGift { + const { + gift, date, convertStars, fromId, message, msgId, nameHidden, unsaved, + } = userStarGift; + + return { + gift: buildApiStarGift(gift), + date, + starsToConvert: convertStars?.toJSNumber(), + fromId: fromId && buildApiPeerId(fromId, 'user'), + message: message && buildApiFormattedText(message), + messageId: msgId, + isNameHidden: nameHidden, + isUnsaved: unsaved, + }; +} diff --git a/src/api/gramjs/apiBuilders/messageContent.ts b/src/api/gramjs/apiBuilders/messageContent.ts index 46e83bec1..84b7c4a48 100644 --- a/src/api/gramjs/apiBuilders/messageContent.ts +++ b/src/api/gramjs/apiBuilders/messageContent.ts @@ -15,6 +15,7 @@ import type { ApiPaidMedia, ApiPhoto, ApiPoll, + ApiStarGiftUnique, ApiSticker, ApiVideo, ApiVoice, @@ -42,6 +43,7 @@ import { buildApiThumbnailFromPath, buildApiThumbnailFromStripped, } from './common'; +import { buildApiStarGift } from './gifts'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; import { buildStickerFromDocument, processStickerResult } from './symbols'; @@ -753,9 +755,12 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef audio = buildAudioFromDocument(document); } let story: ApiWebPageStoryData | undefined; + let gift: ApiStarGiftUnique | 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); if (attributeStory) { const peerId = getApiChatIdFromMtpPeer(attributeStory.peer); story = { @@ -767,6 +772,10 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef addStoryToLocalDb(attributeStory.story, peerId); } } + if (attributeGift) { + const starGift = buildApiStarGift(attributeGift.gift); + gift = starGift.type === 'starGiftUnique' ? starGift : undefined; + } const attributeStickers = attributes?.find((a): a is GramJs.WebPageAttributeStickerSet => ( a instanceof GramJs.WebPageAttributeStickerSet )); @@ -798,6 +807,7 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef video, audio, story, + gift, stickers, mediaSize, }; diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 4dfea457e..290a93508 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -63,8 +63,8 @@ import { buildApiFormattedText, buildApiPhoto, } from './common'; +import { buildApiStarGift } from './gifts'; import { buildMessageContent, buildMessageMediaContent, buildMessageTextContent } from './messageContent'; -import { buildApiStarGift } from './payments'; import { buildApiPeerColor, buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; import { buildMessageReactions } from './reactions'; diff --git a/src/api/gramjs/apiBuilders/payments.ts b/src/api/gramjs/apiBuilders/payments.ts index dd3a9c352..5634e37b3 100644 --- a/src/api/gramjs/apiBuilders/payments.ts +++ b/src/api/gramjs/apiBuilders/payments.ts @@ -19,8 +19,6 @@ import type { ApiPrepaidGiveaway, ApiPrepaidStarsGiveaway, ApiReceipt, - ApiStarGift, - ApiStarGiftAttribute, ApiStarGiveawayOption, ApiStarsAmount, ApiStarsGiveawayWinnerOption, @@ -28,19 +26,17 @@ import type { ApiStarsTransaction, ApiStarsTransactionPeer, ApiStarTopupOption, - ApiUserStarGift, BoughtPaidMedia, } from '../../types'; -import { numberToHexColor } from '../../../util/colors'; -import { addDocumentToLocalDb, addWebDocumentToLocalDb } from '../helpers'; +import { addWebDocumentToLocalDb } from '../helpers'; import { buildApiStarsSubscriptionPricing } from './chats'; -import { buildApiFormattedText, buildApiMessageEntity } from './common'; +import { buildApiMessageEntity } from './common'; +import { buildApiStarGift } from './gifts'; import { omitVirtualClassFields } from './helpers'; import { buildApiDocument, buildApiWebDocument, buildMessageMediaContent } from './messageContent'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; import { buildStatisticsPercentage } from './statistics'; -import { buildStickerFromDocument } from './symbols'; export function buildShippingOptions(shippingOptions: GramJs.ShippingOption[] | undefined) { if (!shippingOptions) { @@ -612,131 +608,3 @@ export function buildApiStarTopupOption(option: GramJs.TypeStarsTopupOption): Ap isExtended: extended, }; } - -export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift { - if (starGift instanceof GramJs.StarGiftUnique) { - const { - id, num, ownerId, ownerName, title, attributes, availabilityIssued, availabilityTotal, - } = starGift; - - return { - type: 'starGiftUnique', - id: id.toString(), - number: num, - ownerId: ownerId && buildApiPeerId(ownerId, 'user'), - ownerName, - attributes: attributes.map(buildApiStarGiftAttribute).filter(Boolean), - title, - totalCount: availabilityTotal, - issuedCount: availabilityIssued, - }; - } - - const { - id, limited, stars, availabilityRemains, availabilityTotal, convertStars, firstSaleDate, lastSaleDate, soldOut, - birthday, upgradeStars, - } = starGift; - - addDocumentToLocalDb(starGift.sticker); - - const sticker = buildStickerFromDocument(starGift.sticker)!; - - return { - type: 'starGift', - id: id.toString(), - isLimited: limited, - sticker, - stars: stars.toJSNumber(), - availabilityRemains, - availabilityTotal, - starsToConvert: convertStars.toJSNumber(), - firstSaleDate, - lastSaleDate, - isSoldOut: soldOut, - isBirthday: birthday, - upgradeStars: upgradeStars?.toJSNumber(), - }; -} - -export function buildApiStarGiftAttribute(attribute: GramJs.TypeStarGiftAttribute): ApiStarGiftAttribute | undefined { - if (attribute instanceof GramJs.StarGiftAttributeModel) { - const sticker = buildStickerFromDocument(attribute.document); - if (!sticker) { - return undefined; - } - - addDocumentToLocalDb(attribute.document); - - return { - type: 'model', - name: attribute.name, - rarityPercent: attribute.rarityPermille / 10, - sticker, - }; - } - - if (attribute instanceof GramJs.StarGiftAttributePattern) { - const sticker = buildStickerFromDocument(attribute.document); - if (!sticker) { - return undefined; - } - - addDocumentToLocalDb(attribute.document); - - return { - type: 'pattern', - name: attribute.name, - rarityPercent: attribute.rarityPermille / 10, - sticker, - }; - } - - if (attribute instanceof GramJs.StarGiftAttributeBackdrop) { - const { - name, rarityPermille, centerColor, edgeColor, patternColor, textColor, - } = attribute; - - return { - type: 'backdrop', - name, - rarityPercent: rarityPermille / 10, - centerColor: numberToHexColor(centerColor), - edgeColor: numberToHexColor(edgeColor), - patternColor: numberToHexColor(patternColor), - textColor: numberToHexColor(textColor), - }; - } - - if (attribute instanceof GramJs.StarGiftAttributeOriginalDetails) { - const { - date, recipientId, message, senderId, - } = attribute; - - return { - type: 'originalDetails', - date, - recipientId: recipientId && buildApiPeerId(recipientId, 'user'), - message: message && buildApiFormattedText(message), - senderId: senderId && buildApiPeerId(senderId, 'user'), - }; - } - - return undefined; -} - -export function buildApiUserStarGift(userStarGift: GramJs.UserStarGift): ApiUserStarGift { - const { - gift, date, convertStars, fromId, message, msgId, nameHidden, unsaved, - } = userStarGift; - - return { - gift: buildApiStarGift(gift), - date, - starsToConvert: convertStars?.toJSNumber(), - fromId: fromId && buildApiPeerId(fromId, 'user'), - message: message && buildApiFormattedText(message), - messageId: msgId, - isNameHidden: nameHidden, - isUnsaved: unsaved, - }; -} diff --git a/src/api/gramjs/methods/payments.ts b/src/api/gramjs/methods/payments.ts index 947765d01..f0a92f5e5 100644 --- a/src/api/gramjs/methods/payments.ts +++ b/src/api/gramjs/methods/payments.ts @@ -12,6 +12,10 @@ import type { } from '../../types'; import { DEBUG } from '../../../config'; +import { + buildApiStarGift, + buildApiUserStarGift, +} from '../apiBuilders/gifts'; import { buildApiBoost, buildApiBoostsStatus, @@ -22,14 +26,12 @@ import { buildApiPremiumGiftCodeOption, buildApiPremiumPromo, buildApiReceipt, - buildApiStarGift, buildApiStarsAmount, buildApiStarsGiftOptions, buildApiStarsGiveawayOptions, buildApiStarsSubscription, buildApiStarsTransaction, buildApiStarTopupOption, - buildApiUserStarGift, buildShippingOptions, } from '../apiBuilders/payments'; import { buildApiPeerId } from '../apiBuilders/peers'; @@ -638,3 +640,15 @@ export async function fetchStarsTopupOptions() { return result.map(buildApiStarTopupOption); } + +export async function fetchUniqueStarGift({ slug }: { + slug: string; +}) { + const result = await invokeRequest(new GramJs.payments.GetUniqueStarGift({ slug })); + + if (!result) { + return undefined; + } + const gift = buildApiStarGift(result.gift); + return gift.type === 'starGiftUnique' ? gift : undefined; +} diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index ba266eeca..54b75d691 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -7,6 +7,7 @@ import type { ApiLabeledPrice, ApiPremiumGiftCodeOption, ApiStarGift, + ApiStarGiftUnique, } from './payments'; import type { ApiMessageStoryData, ApiStory, ApiWebPageStickerData, ApiWebPageStoryData, @@ -543,6 +544,7 @@ export interface ApiWebPage { document?: ApiDocument; video?: ApiVideo; story?: ApiWebPageStoryData; + gift?: ApiStarGiftUnique; stickers?: ApiWebPageStickerData; mediaSize?: WebPageMediaSize; hasLargeMedia?: boolean; diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index da4acf9ea..9e4a5b2a0 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1494,3 +1494,19 @@ "ActionUnsupportedTitle" = "Action not supported yet"; "ActionUnsupportedDescription" = "Please, use one of our apps to complete this action."; "LocationPermissionText" = "**{name}** requests access to set your **location**. You will be able to revoke this access in the profile page of **{name}**."; +"GiftWasNotFound" = "Gift was not found"; +"ViewButtonRequestJoin" = "REQUEST TO JOIN"; +"ViewButtonMessage" = "VIEW MESSAGE"; +"ViewButtonBot" = "VIEW BOT"; +"ViewButtonVoiceChat" = "VOICE CHAT"; +"ViewButtonVoiceChatChannel" = "LIVE STREAM"; +"ViewButtonGroup" = "VIEW GROUP"; +"ViewButtonChannel" = "VIEW CHANNEL"; +"ViewButtonUser" = "SEND MESSAGE"; +"ViewButtonBotApp" = "LAUNCH"; +"ViewChatList" = "VIEW CHAT LIST"; +"ViewButtonStory" = "VIEW STORY"; +"ViewButtonBoost" = "BOOST"; +"ViewButtonStickerset" = "VIEW STICKERS"; +"ViewButtonGiftUnique" = "VIEW COLLECTIBLE"; + diff --git a/src/components/middle/MessageList.scss b/src/components/middle/MessageList.scss index 8bf5cfc11..bbdc5d38e 100644 --- a/src/components/middle/MessageList.scss +++ b/src/components/middle/MessageList.scss @@ -277,6 +277,7 @@ max-width: 17rem; } + .web-page-gift, .action-message-gift { display: flex !important; width: 13.75rem; @@ -290,10 +291,21 @@ font-weight: var(--font-weight-semibold); } + .web-page-gift { + position: relative; + min-width: 12.5rem; + width: 100%; + margin-top: 0; + padding-block: 2rem !important; + border-radius: 0.25rem; + } + + .web-page-centered, .action-message-centered { margin-inline: auto; } + .web-page-unique, .action-message-unique { &::before { content: ""; @@ -305,6 +317,7 @@ } } + .web-page-unique-background-wrapper, .action-message-unique-background-wrapper { position: absolute; inset: 0; @@ -312,11 +325,15 @@ border-radius: inherit; } + .web-page-unique-background, .action-message-unique-background { position: absolute; inset: 0; top: -6rem; } + .web-page-unique-background { + top: -1rem; + } .action-message-user-caption, .action-message-stars-balance { diff --git a/src/components/middle/message/WebPage.scss b/src/components/middle/message/WebPage.scss index 42d6803fc..efc543407 100644 --- a/src/components/middle/message/WebPage.scss +++ b/src/components/middle/message/WebPage.scss @@ -34,6 +34,14 @@ border-radius: 0.25rem; } + &--unique-sticker { + position: relative; + width: 7.5rem; + height: 7.5rem; + overflow: hidden; + margin-block: 0.5rem; + } + &--stickers { color: var(--accent-color); border-radius: 0 !important; @@ -60,6 +68,7 @@ .WebPage--content { position: relative; + &.is-gift, &.is-story { display: flex; flex-direction: column-reverse; @@ -85,6 +94,12 @@ } } + .with-gift &--quick-button { + border-top: inherit; + margin-top: 0.0625rem; + margin-bottom: -0.125rem; + } + &-text { display: flex; flex-direction: column; diff --git a/src/components/middle/message/WebPage.tsx b/src/components/middle/message/WebPage.tsx index 0d8b9bef2..f5e3f6ef5 100644 --- a/src/components/middle/message/WebPage.tsx +++ b/src/components/middle/message/WebPage.tsx @@ -1,21 +1,24 @@ import type { FC } from '../../../lib/teact/teact'; import React, { memo, useRef } from '../../../lib/teact/teact'; -import { getActions } from '../../../global'; +import { getActions, withGlobal } from '../../../global'; import type { ApiMessage, ApiTypeStory } from '../../../api/types'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import { AudioOrigin, type ISettings } from '../../../types'; import { getMessageWebPage } from '../../../global/helpers'; +import { selectCanPlayAnimatedEmojis } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import trimText from '../../../util/trimText'; +import { getGiftAttributes, getStickerFromGift } from '../../common/helpers/gifts'; import renderText from '../../common/helpers/renderText'; import { calculateMediaDimensions } from './helpers/mediaDimensions'; -import { getWebpageButtonText } from './helpers/webpageType'; +import { getWebpageButtonLangKey } from './helpers/webpageType'; import useDynamicColorListener from '../../../hooks/stickers/useDynamicColorListener'; import useAppLayout from '../../../hooks/useAppLayout'; import useEnsureStory from '../../../hooks/useEnsureStory'; +import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import useOldLang from '../../../hooks/useOldLang'; @@ -23,6 +26,7 @@ import Audio from '../../common/Audio'; import Document from '../../common/Document'; import EmojiIconBackground from '../../common/embedded/EmojiIconBackground'; import PeerColorWrapper from '../../common/PeerColorWrapper'; +import RadialPatternBackground from '../../common/profile/RadialPatternBackground'; import SafeLink from '../../common/SafeLink'; import StickerView from '../../common/StickerView'; import Button from '../../ui/Button'; @@ -34,6 +38,7 @@ import './WebPage.scss'; const MAX_TEXT_LENGTH = 170; // symbols const WEBPAGE_STORY_TYPE = 'telegram_story'; +const WEBPAGE_GIFT_TYPE = 'telegram_nft'; const STICKER_SIZE = 80; const EMOJI_SIZE = 38; @@ -60,8 +65,12 @@ type OwnProps = { onContainerClick?: ((e: React.MouseEvent) => void); isEditing?: boolean; }; +type StateProps = { + canPlayAnimatedEmojis: boolean; +}; +const STAR_GIFT_STICKER_SIZE = 120; -const WebPage: FC = ({ +const WebPage: FC = ({ message, observeIntersectionForLoading, observeIntersectionForPlaying, @@ -89,8 +98,11 @@ const WebPage: FC = ({ const { isMobile } = useAppLayout(); // eslint-disable-next-line no-null/no-null const stickersRef = useRef(null); + // eslint-disable-next-line no-null/no-null + const giftStickersRef = useRef(null); - const lang = useOldLang(); + const oldLang = useOldLang(); + const lang = useLang(); const handleMediaClick = useLastCallback(() => { onMediaClick!(); @@ -100,8 +112,9 @@ const WebPage: FC = ({ onContainerClick?.(e); }); - const handleQuickButtonClick = useLastCallback(() => { + const handleOpenTelegramLink = useLastCallback(() => { if (!webPage) return; + openTelegramLink({ url: webPage.url, }); @@ -132,8 +145,10 @@ const WebPage: FC = ({ mediaSize, } = webPage; const isStory = type === WEBPAGE_STORY_TYPE; + const isGift = type === WEBPAGE_GIFT_TYPE; const isExpiredStory = story && 'isDeleted' in story; - const quickButtonLangKey = !inPreview && !isExpiredStory ? getWebpageButtonText(type) : undefined; + const quickButtonLangKey = !inPreview && !isExpiredStory ? getWebpageButtonLangKey(type) : undefined; + const quickButtonTitle = quickButtonLangKey && lang(quickButtonLangKey); const truncatedDescription = trimText(description, MAX_TEXT_LENGTH); const isArticle = Boolean(truncatedDescription || title || siteName); let isSquarePhoto = Boolean(stickers); @@ -159,31 +174,75 @@ const WebPage: FC = ({ video && 'with-video', !isArticle && 'no-article', document && 'with-document', - quickButtonLangKey && 'with-quick-button', + quickButtonTitle && 'with-quick-button', + isGift && 'with-gift', ); - function renderQuickButton(langKey: string) { + function renderQuickButton(caption: string) { return ( ); } + function renderStarGiftUnique() { + const gift = webPage?.gift; + if (!gift || gift.type !== 'starGiftUnique') return undefined; + + const sticker = getStickerFromGift(gift)!; + const attributes = getGiftAttributes(gift); + const { backdrop, pattern, model } = attributes || {}; + + if (!backdrop || !pattern || !model) return undefined; + + const backgroundColors = [backdrop.centerColor, backdrop.edgeColor]; + + return ( +
handleOpenTelegramLink()} + > +
+ +
+
+ +
+
+ ); + } + return ( -
+
{backgroundEmojiId && ( = ({ {isStory && ( )} + {isGift && !inPreview && ( + renderStarGiftUnique() + )} {isArticle && (
openUrl({ url, shouldSkipModal: true }) : undefined} > - {!inPreview && title && ( + {(!inPreview || isGift) && title && (

{renderText(title)}

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

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

)}
)} - {photo && !video && !document && ( + {photo && !isGift && !video && !document && ( = ({ {inPreview && displayUrl && !isArticle && (

{displayUrl}

-

{lang('Chat.Empty.LinkPreview')}

+

{oldLang('Chat.Empty.LinkPreview')}

)}
- {quickButtonLangKey && renderQuickButton(quickButtonLangKey)} + {quickButtonTitle && renderQuickButton(quickButtonTitle)} ); }; -export default memo(WebPage); +export default memo(withGlobal( + (global): StateProps => { + return { + canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global), + }; + }, +)(WebPage)); diff --git a/src/components/middle/message/_message-content.scss b/src/components/middle/message/_message-content.scss index 2100ee647..83e062fa5 100644 --- a/src/components/middle/message/_message-content.scss +++ b/src/components/middle/message/_message-content.scss @@ -21,6 +21,10 @@ } .message-content { + &.gift { + --max-width: 18rem; + } + position: relative; max-width: var(--max-width); diff --git a/src/components/middle/message/helpers/buildContentClassName.ts b/src/components/middle/message/helpers/buildContentClassName.ts index 90fdd678c..721ef26dc 100644 --- a/src/components/middle/message/helpers/buildContentClassName.ts +++ b/src/components/middle/message/helpers/buildContentClassName.ts @@ -138,6 +138,10 @@ export function buildContentClassName( if (webPage.document) { classNames.push('document'); } + + if (webPage.gift) { + classNames.push('gift'); + } } if (invoice && !invoice.extendedMedia) { diff --git a/src/components/middle/message/helpers/webpageType.ts b/src/components/middle/message/helpers/webpageType.ts index 8f3db567f..0ee1032bc 100644 --- a/src/components/middle/message/helpers/webpageType.ts +++ b/src/components/middle/message/helpers/webpageType.ts @@ -1,36 +1,38 @@ // https://github.com/telegramdesktop/tdesktop/blob/3da787791f6d227f69b32bf4003bc6071d05e2ac/Telegram/SourceFiles/history/view/history_view_view_button.cpp#L51 -export function getWebpageButtonText(type?: string) { +export function getWebpageButtonLangKey(type?: string) { switch (type) { case 'telegram_channel_request': case 'telegram_megagroup_request': case 'telegram_chat_request': - return 'lng_view_button_request_join'; + return 'ViewButtonRequestJoin'; case 'telegram_message': - return 'lng_view_button_message'; + return 'ViewButtonMessage'; case 'telegram_bot': - return 'lng_view_button_bot'; + return 'ViewButtonBot'; case 'telegram_voicechat': - return 'lng_view_button_voice_chat'; + return 'ViewButtonVoiceChat'; case 'telegram_livestream': - return 'lng_view_button_voice_chat_channel'; + return 'ViewButtonVoiceChatChannel'; case 'telegram_megagroup': case 'telegram_chat': - return 'lng_view_button_group'; + return 'ViewButtonGroup'; case 'telegram_channel': - return 'lng_view_button_channel'; + return 'ViewButtonChannel'; case 'telegram_user': - return 'lng_view_button_user'; + return 'ViewButtonUser'; case 'telegram_botapp': - return 'lng_view_button_bot_app'; + return 'ViewButtonBotApp'; case 'telegram_chatlist': return 'ViewChatList'; case 'telegram_story': - return 'lng_view_button_story'; + return 'ViewButtonStory'; case 'telegram_channel_boost': case 'telegram_group_boost': - return 'lng_view_button_boost'; + return 'ViewButtonBoost'; case 'telegram_stickerset': - return 'lng_view_button_stickerset'; + return 'ViewButtonStickerset'; + case 'telegram_nft': + return 'ViewButtonGiftUnique'; default: return undefined; } diff --git a/src/components/modals/gift/info/GiftInfoModal.tsx b/src/components/modals/gift/info/GiftInfoModal.tsx index 62b4dbde8..cc2408849 100644 --- a/src/components/modals/gift/info/GiftInfoModal.tsx +++ b/src/components/modals/gift/info/GiftInfoModal.tsx @@ -312,11 +312,10 @@ const GiftInfoModal = ({ const { model, backdrop, pattern, originalDetails, } = giftAttributes || {}; - const ownerId = gift.ownerId; const ownerName = gift.ownerName; tableData.push([ lang('GiftInfoOwner'), - ownerId ? { chatId: ownerId } : ownerName || '', + gift.ownerId ? { chatId: gift.ownerId } : ownerName || '', ]); if (model) { diff --git a/src/global/actions/api/payments.ts b/src/global/actions/api/payments.ts index 455ad4acd..06813b1a9 100644 --- a/src/global/actions/api/payments.ts +++ b/src/global/actions/api/payments.ts @@ -990,3 +990,23 @@ addActionHandler('launchPrepaidStarsGiveaway', async (global, actions, payload): actions.openBoostStatistics({ chatId, tabId }); }); + +addActionHandler('openUniqueGiftBySlug', async (global, actions, payload): Promise => { + const { + slug, tabId = getCurrentTabId(), + } = payload; + + const gift = await callApi('fetchUniqueStarGift', { slug }); + + if (!gift) { + actions.showNotification({ + message: { + key: 'GiftWasNotFound', + }, + tabId, + }); + return; + } + + actions.openGiftInfoModal({ gift, tabId }); +}); diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index cd593a273..19991ecf5 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -1468,6 +1468,9 @@ export interface ActionPayloads { storyId: number; origin?: StoryViewerOrigin; } & WithTabId; + openUniqueGiftBySlug: { + slug: string; + } & WithTabId; openPreviousStory: WithTabId | undefined; openNextStory: WithTabId | undefined; setStoryViewerMuted: { diff --git a/src/lib/gramjs/tl/apiTl.ts b/src/lib/gramjs/tl/apiTl.ts index 74d43deb3..2ff5e083f 100644 --- a/src/lib/gramjs/tl/apiTl.ts +++ b/src/lib/gramjs/tl/apiTl.ts @@ -1713,6 +1713,7 @@ payments.getStarGifts#c4563590 hash:int = payments.StarGifts; payments.getUserStarGifts#5e72c7e1 user_id:InputUser offset:string limit:int = payments.UserStarGifts; payments.saveStarGift#92fd2aae flags:# unsave:flags.0?true msg_id:int = Bool; payments.convertStarGift#72770c83 msg_id:int = Bool; +payments.getUniqueStarGift#a1974d72 slug:string = payments.UniqueStarGift; phone.requestCall#a6c4600c flags:# video:flags.0?true user_id:InputUser conference_call:flags.1?InputGroupCall 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 d0b080c0a..43de60066 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -384,5 +384,6 @@ "payments.getUserStarGifts", "payments.saveStarGift", "payments.convertStarGift", + "payments.getUniqueStarGift", "fragment.getCollectibleInfo" ] diff --git a/src/lib/gramjs/tl/static/api.tl b/src/lib/gramjs/tl/static/api.tl index 2c7a62c5a..821c1bb91 100644 --- a/src/lib/gramjs/tl/static/api.tl +++ b/src/lib/gramjs/tl/static/api.tl @@ -2508,6 +2508,7 @@ payments.getStarGifts#c4563590 hash:int = payments.StarGifts; payments.getUserStarGifts#5e72c7e1 user_id:InputUser offset:string limit:int = payments.UserStarGifts; payments.saveStarGift#92fd2aae flags:# unsave:flags.0?true msg_id:int = Bool; payments.convertStarGift#72770c83 msg_id:int = Bool; +payments.getUniqueStarGift#a1974d72 slug:string = payments.UniqueStarGift; payments.botCancelStarsSubscription#6dfa0622 flags:# restore:flags.0?true user_id:InputUser charge_id:string = Bool; payments.getConnectedStarRefBots#5869a553 flags:# peer:InputPeer offset_date:flags.2?int offset_link:flags.2?string limit:int = payments.ConnectedStarRefBots; payments.getConnectedStarRefBot#b7d998f0 peer:InputPeer bot:InputUser = payments.ConnectedStarRefBots; diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 2a54ad25b..c7a1c6f6c 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1222,6 +1222,21 @@ export interface LangPair { 'ProfileTabSimilarChannels': undefined; 'ActionUnsupportedTitle': undefined; 'ActionUnsupportedDescription': undefined; + 'GiftWasNotFound': undefined; + 'ViewButtonRequestJoin': undefined; + 'ViewButtonMessage': undefined; + 'ViewButtonBot': undefined; + 'ViewButtonVoiceChat': undefined; + 'ViewButtonVoiceChatChannel': undefined; + 'ViewButtonGroup': undefined; + 'ViewButtonChannel': undefined; + 'ViewButtonUser': undefined; + 'ViewButtonBotApp': undefined; + 'ViewChatList': undefined; + 'ViewButtonStory': undefined; + 'ViewButtonBoost': undefined; + 'ViewButtonStickerset': undefined; + 'ViewButtonGiftUnique': undefined; } export interface LangPairWithVariables { diff --git a/src/util/deepLinkParser.ts b/src/util/deepLinkParser.ts index af8c0f669..27423f89d 100644 --- a/src/util/deepLinkParser.ts +++ b/src/util/deepLinkParser.ts @@ -8,7 +8,8 @@ import { IS_BAD_URL_PARSER } from './windowEnvironment'; 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'; +'invoice' | 'addlist' | 'boost' | 'giftcode' | 'message' | 'premium_offer' | 'premium_multigift' | 'stars_topup' +| 'nft'; interface PublicMessageLink { type: 'publicMessageLink'; @@ -96,6 +97,11 @@ interface PremiumMultigiftLink { referrer: string; } +interface GiftUniqueLink { + type: 'giftUniqueLink'; + slug: string; +} + type DeepLink = TelegramPassportLink | LoginCodeLink | @@ -108,7 +114,8 @@ type DeepLink = BusinessChatLink | PremiumReferrerLink | PremiumMultigiftLink | - ChatBoostLink; + ChatBoostLink | + GiftUniqueLink; type BuilderParams = Record, string | undefined>; type BuilderReturnType = T | undefined; @@ -229,6 +236,8 @@ function parseTgLink(url: URL) { return buildPremiumMultigiftLink({ referrer: queryParams.ref }); case 'chatBoostLink': return buildChatBoostLink({ username: queryParams.domain, id: queryParams.channel }); + case 'giftUniqueLink': + return buildGiftUniqueLink({ slug: queryParams.slug }); default: break; } @@ -331,6 +340,12 @@ function parseHttpLink(url: URL) { id: isPrivateChannel ? pathParams[1] : undefined, }); } + case 'giftUniqueLink': { + const slug = pathParams.slice(1).join('/'); + return buildGiftUniqueLink({ + slug, + }); + } default: break; } @@ -355,6 +370,7 @@ function getHttpDeepLinkType( if (method === 'login') return 'loginCodeLink'; if (method === 'm') return 'businessChatLink'; if (method === 'boost') return 'chatBoostLink'; + if (method === 'nft') return 'giftUniqueLink'; if (method === 'c') { if (queryParams.boost !== undefined) return 'chatBoostLink'; return 'privateChannelLink'; @@ -370,6 +386,7 @@ function getHttpDeepLinkType( if (isUsernameValid(pathParams[0]) && pathParams.slice(1).every(isNumber)) { return 'publicMessageLink'; } + if (method === 'nft') return 'giftUniqueLink'; } else if (len === 4) { if (method === 'c' && pathParams.slice(1).every(isNumber)) { return 'privateMessageLink'; @@ -424,6 +441,8 @@ function getTgDeepLinkType( return 'premiumMultigiftLink'; case 'boost': return 'chatBoostLink'; + case 'nft': + return 'giftUniqueLink'; default: break; } @@ -629,6 +648,21 @@ function buildBusinessChatLink(params: BuilderParams): Builder }; } +function buildGiftUniqueLink(params: BuilderParams): BuilderReturnType { + const { + slug, + } = params; + + if (!slug) { + return undefined; + } + + return { + type: 'giftUniqueLink', + slug, + }; +} + function buildPremiumReferrerLink(params: BuilderParams): BuilderReturnType { const { referrer, diff --git a/src/util/deeplink.ts b/src/util/deeplink.ts index 1f6597895..4a1040f58 100644 --- a/src/util/deeplink.ts +++ b/src/util/deeplink.ts @@ -66,6 +66,9 @@ export const processDeepLink = (url: string): boolean => { isPrivate: Boolean(parsedLink.id), }); return true; + case 'giftUniqueLink': + actions.openUniqueGiftBySlug({ slug: parsedLink.slug }); + return true; default: break; }