Gifts: Support unique links for gifts (#5450)

This commit is contained in:
Alexander Zinchuk 2025-01-21 18:21:26 +01:00
parent 93cb31c980
commit 86cb0f5ee6
22 changed files with 411 additions and 173 deletions

View File

@ -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,
};
}

View File

@ -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,
};

View File

@ -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';

View File

@ -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,
};
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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";

View File

@ -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 {

View File

@ -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;

View File

@ -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<OwnProps> = ({
const WebPage: FC<OwnProps & StateProps> = ({
message,
observeIntersectionForLoading,
observeIntersectionForPlaying,
@ -89,8 +98,11 @@ const WebPage: FC<OwnProps> = ({
const { isMobile } = useAppLayout();
// eslint-disable-next-line no-null/no-null
const stickersRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const giftStickersRef = useRef<HTMLDivElement>(null);
const lang = useOldLang();
const oldLang = useOldLang();
const lang = useLang();
const handleMediaClick = useLastCallback(() => {
onMediaClick!();
@ -100,8 +112,9 @@ const WebPage: FC<OwnProps> = ({
onContainerClick?.(e);
});
const handleQuickButtonClick = useLastCallback(() => {
const handleOpenTelegramLink = useLastCallback(() => {
if (!webPage) return;
openTelegramLink({
url: webPage.url,
});
@ -132,8 +145,10 @@ const WebPage: FC<OwnProps> = ({
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<OwnProps> = ({
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 (
<Button
className="WebPage--quick-button"
size="tiny"
color="translucent"
isRectangular
onClick={handleQuickButtonClick}
onClick={handleOpenTelegramLink}
>
{lang(langKey)}
{caption}
</Button>
);
}
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 (
<div
className="web-page-gift web-page-centered web-page-unique"
onClick={() => handleOpenTelegramLink()}
>
<div className="web-page-unique-background-wrapper">
<RadialPatternBackground
className="web-page-unique-background"
backgroundColors={backgroundColors}
patternColor={backdrop.patternColor}
patternIcon={pattern.sticker}
/>
</div>
<div ref={giftStickersRef} key={sticker.id} className="WebPage--unique-sticker">
<StickerView
containerRef={giftStickersRef}
sticker={sticker}
size={STAR_GIFT_STICKER_SIZE}
observeIntersectionForPlaying={observeIntersectionForPlaying}
observeIntersectionForLoading={observeIntersectionForLoading}
/>
</div>
</div>
);
}
return (
<PeerColorWrapper
className={className}
data-initial={(siteName || displayUrl)[0]}
dir={lang.isRtl ? 'rtl' : 'auto'}
dir={oldLang.isRtl ? 'rtl' : 'auto'}
onClick={handleContainerClick}
>
<div className={buildClassName('WebPage--content', isStory && 'is-story')}>
<div className={buildClassName(
'WebPage--content',
isStory && 'is-story',
isGift && 'is-gift',
)}
>
{backgroundEmojiId && (
<EmojiIconBackground
emojiDocumentId={backgroundEmojiId}
@ -193,21 +252,24 @@ const WebPage: FC<OwnProps> = ({
{isStory && (
<BaseStory story={story} isProtected={isProtected} isConnected={isConnected} isPreview />
)}
{isGift && !inPreview && (
renderStarGiftUnique()
)}
{isArticle && (
<div
className={buildClassName('WebPage-text', !inPreview && 'WebPage-text_interactive')}
onClick={!inPreview ? () => openUrl({ url, shouldSkipModal: true }) : undefined}
>
<SafeLink className="site-name" url={url} text={siteName || displayUrl} />
{!inPreview && title && (
{(!inPreview || isGift) && title && (
<p className="site-title">{renderText(title)}</p>
)}
{truncatedDescription && (
{truncatedDescription && !isGift && (
<p className="site-description">{renderText(truncatedDescription, ['emoji', 'br'])}</p>
)}
</div>
)}
{photo && !video && !document && (
{photo && !isGift && !video && !document && (
<Photo
photo={photo}
isOwn={message.isOutgoing}
@ -289,13 +351,19 @@ const WebPage: FC<OwnProps> = ({
{inPreview && displayUrl && !isArticle && (
<div className="WebPage-text">
<p className="site-name">{displayUrl}</p>
<p className="site-description">{lang('Chat.Empty.LinkPreview')}</p>
<p className="site-description">{oldLang('Chat.Empty.LinkPreview')}</p>
</div>
)}
</div>
{quickButtonLangKey && renderQuickButton(quickButtonLangKey)}
{quickButtonTitle && renderQuickButton(quickButtonTitle)}
</PeerColorWrapper>
);
};
export default memo(WebPage);
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
return {
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
};
},
)(WebPage));

View File

@ -21,6 +21,10 @@
}
.message-content {
&.gift {
--max-width: 18rem;
}
position: relative;
max-width: var(--max-width);

View File

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

View File

@ -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;
}

View File

@ -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) {

View File

@ -990,3 +990,23 @@ addActionHandler('launchPrepaidStarsGiveaway', async (global, actions, payload):
actions.openBoostStatistics({ chatId, tabId });
});
addActionHandler('openUniqueGiftBySlug', async (global, actions, payload): Promise<void> => {
const {
slug, tabId = getCurrentTabId(),
} = payload;
const gift = await callApi('fetchUniqueStarGift', { slug });
if (!gift) {
actions.showNotification({
message: {
key: 'GiftWasNotFound',
},
tabId,
});
return;
}
actions.openGiftInfoModal({ gift, tabId });
});

View File

@ -1468,6 +1468,9 @@ export interface ActionPayloads {
storyId: number;
origin?: StoryViewerOrigin;
} & WithTabId;
openUniqueGiftBySlug: {
slug: string;
} & WithTabId;
openPreviousStory: WithTabId | undefined;
openNextStory: WithTabId | undefined;
setStoryViewerMuted: {

View File

@ -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;

View File

@ -384,5 +384,6 @@
"payments.getUserStarGifts",
"payments.saveStarGift",
"payments.convertStarGift",
"payments.getUniqueStarGift",
"fragment.getCollectibleInfo"
]

View File

@ -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;

View File

@ -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<V extends unknown = LangVariable> {

View File

@ -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<T extends DeepLink> = Record<keyof Omit<T, 'type'>, string | undefined>;
type BuilderReturnType<T extends DeepLink> = 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<BusinessChatLink>): Builder
};
}
function buildGiftUniqueLink(params: BuilderParams<GiftUniqueLink>): BuilderReturnType<GiftUniqueLink> {
const {
slug,
} = params;
if (!slug) {
return undefined;
}
return {
type: 'giftUniqueLink',
slug,
};
}
function buildPremiumReferrerLink(params: BuilderParams<PremiumReferrerLink>): BuilderReturnType<PremiumReferrerLink> {
const {
referrer,

View File

@ -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;
}