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, ApiPaidMedia,
ApiPhoto, ApiPhoto,
ApiPoll, ApiPoll,
ApiStarGiftUnique,
ApiSticker, ApiSticker,
ApiVideo, ApiVideo,
ApiVoice, ApiVoice,
@ -42,6 +43,7 @@ import {
buildApiThumbnailFromPath, buildApiThumbnailFromPath,
buildApiThumbnailFromStripped, buildApiThumbnailFromStripped,
} from './common'; } from './common';
import { buildApiStarGift } from './gifts';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
import { buildStickerFromDocument, processStickerResult } from './symbols'; import { buildStickerFromDocument, processStickerResult } from './symbols';
@ -753,9 +755,12 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef
audio = buildAudioFromDocument(document); audio = buildAudioFromDocument(document);
} }
let story: ApiWebPageStoryData | undefined; let story: ApiWebPageStoryData | undefined;
let gift: ApiStarGiftUnique | undefined;
let stickers: ApiWebPageStickerData | undefined; let stickers: ApiWebPageStickerData | undefined;
const attributeStory = attributes const attributeStory = attributes
?.find((a): a is GramJs.WebPageAttributeStory => a instanceof GramJs.WebPageAttributeStory); ?.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) { if (attributeStory) {
const peerId = getApiChatIdFromMtpPeer(attributeStory.peer); const peerId = getApiChatIdFromMtpPeer(attributeStory.peer);
story = { story = {
@ -767,6 +772,10 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef
addStoryToLocalDb(attributeStory.story, peerId); 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 => ( const attributeStickers = attributes?.find((a): a is GramJs.WebPageAttributeStickerSet => (
a instanceof GramJs.WebPageAttributeStickerSet a instanceof GramJs.WebPageAttributeStickerSet
)); ));
@ -798,6 +807,7 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef
video, video,
audio, audio,
story, story,
gift,
stickers, stickers,
mediaSize, mediaSize,
}; };

View File

@ -63,8 +63,8 @@ import {
buildApiFormattedText, buildApiFormattedText,
buildApiPhoto, buildApiPhoto,
} from './common'; } from './common';
import { buildApiStarGift } from './gifts';
import { buildMessageContent, buildMessageMediaContent, buildMessageTextContent } from './messageContent'; import { buildMessageContent, buildMessageMediaContent, buildMessageTextContent } from './messageContent';
import { buildApiStarGift } from './payments';
import { buildApiPeerColor, buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; import { buildApiPeerColor, buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
import { buildMessageReactions } from './reactions'; import { buildMessageReactions } from './reactions';

View File

@ -19,8 +19,6 @@ import type {
ApiPrepaidGiveaway, ApiPrepaidGiveaway,
ApiPrepaidStarsGiveaway, ApiPrepaidStarsGiveaway,
ApiReceipt, ApiReceipt,
ApiStarGift,
ApiStarGiftAttribute,
ApiStarGiveawayOption, ApiStarGiveawayOption,
ApiStarsAmount, ApiStarsAmount,
ApiStarsGiveawayWinnerOption, ApiStarsGiveawayWinnerOption,
@ -28,19 +26,17 @@ import type {
ApiStarsTransaction, ApiStarsTransaction,
ApiStarsTransactionPeer, ApiStarsTransactionPeer,
ApiStarTopupOption, ApiStarTopupOption,
ApiUserStarGift,
BoughtPaidMedia, BoughtPaidMedia,
} from '../../types'; } from '../../types';
import { numberToHexColor } from '../../../util/colors'; import { addWebDocumentToLocalDb } from '../helpers';
import { addDocumentToLocalDb, addWebDocumentToLocalDb } from '../helpers';
import { buildApiStarsSubscriptionPricing } from './chats'; import { buildApiStarsSubscriptionPricing } from './chats';
import { buildApiFormattedText, buildApiMessageEntity } from './common'; import { buildApiMessageEntity } from './common';
import { buildApiStarGift } from './gifts';
import { omitVirtualClassFields } from './helpers'; import { omitVirtualClassFields } from './helpers';
import { buildApiDocument, buildApiWebDocument, buildMessageMediaContent } from './messageContent'; import { buildApiDocument, buildApiWebDocument, buildMessageMediaContent } from './messageContent';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
import { buildStatisticsPercentage } from './statistics'; import { buildStatisticsPercentage } from './statistics';
import { buildStickerFromDocument } from './symbols';
export function buildShippingOptions(shippingOptions: GramJs.ShippingOption[] | undefined) { export function buildShippingOptions(shippingOptions: GramJs.ShippingOption[] | undefined) {
if (!shippingOptions) { if (!shippingOptions) {
@ -612,131 +608,3 @@ export function buildApiStarTopupOption(option: GramJs.TypeStarsTopupOption): Ap
isExtended: extended, 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'; } from '../../types';
import { DEBUG } from '../../../config'; import { DEBUG } from '../../../config';
import {
buildApiStarGift,
buildApiUserStarGift,
} from '../apiBuilders/gifts';
import { import {
buildApiBoost, buildApiBoost,
buildApiBoostsStatus, buildApiBoostsStatus,
@ -22,14 +26,12 @@ import {
buildApiPremiumGiftCodeOption, buildApiPremiumGiftCodeOption,
buildApiPremiumPromo, buildApiPremiumPromo,
buildApiReceipt, buildApiReceipt,
buildApiStarGift,
buildApiStarsAmount, buildApiStarsAmount,
buildApiStarsGiftOptions, buildApiStarsGiftOptions,
buildApiStarsGiveawayOptions, buildApiStarsGiveawayOptions,
buildApiStarsSubscription, buildApiStarsSubscription,
buildApiStarsTransaction, buildApiStarsTransaction,
buildApiStarTopupOption, buildApiStarTopupOption,
buildApiUserStarGift,
buildShippingOptions, buildShippingOptions,
} from '../apiBuilders/payments'; } from '../apiBuilders/payments';
import { buildApiPeerId } from '../apiBuilders/peers'; import { buildApiPeerId } from '../apiBuilders/peers';
@ -638,3 +640,15 @@ export async function fetchStarsTopupOptions() {
return result.map(buildApiStarTopupOption); 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, ApiLabeledPrice,
ApiPremiumGiftCodeOption, ApiPremiumGiftCodeOption,
ApiStarGift, ApiStarGift,
ApiStarGiftUnique,
} from './payments'; } from './payments';
import type { import type {
ApiMessageStoryData, ApiStory, ApiWebPageStickerData, ApiWebPageStoryData, ApiMessageStoryData, ApiStory, ApiWebPageStickerData, ApiWebPageStoryData,
@ -543,6 +544,7 @@ export interface ApiWebPage {
document?: ApiDocument; document?: ApiDocument;
video?: ApiVideo; video?: ApiVideo;
story?: ApiWebPageStoryData; story?: ApiWebPageStoryData;
gift?: ApiStarGiftUnique;
stickers?: ApiWebPageStickerData; stickers?: ApiWebPageStickerData;
mediaSize?: WebPageMediaSize; mediaSize?: WebPageMediaSize;
hasLargeMedia?: boolean; hasLargeMedia?: boolean;

View File

@ -1494,3 +1494,19 @@
"ActionUnsupportedTitle" = "Action not supported yet"; "ActionUnsupportedTitle" = "Action not supported yet";
"ActionUnsupportedDescription" = "Please, use one of our apps to complete this action."; "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}**."; "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; max-width: 17rem;
} }
.web-page-gift,
.action-message-gift { .action-message-gift {
display: flex !important; display: flex !important;
width: 13.75rem; width: 13.75rem;
@ -290,10 +291,21 @@
font-weight: var(--font-weight-semibold); 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 { .action-message-centered {
margin-inline: auto; margin-inline: auto;
} }
.web-page-unique,
.action-message-unique { .action-message-unique {
&::before { &::before {
content: ""; content: "";
@ -305,6 +317,7 @@
} }
} }
.web-page-unique-background-wrapper,
.action-message-unique-background-wrapper { .action-message-unique-background-wrapper {
position: absolute; position: absolute;
inset: 0; inset: 0;
@ -312,11 +325,15 @@
border-radius: inherit; border-radius: inherit;
} }
.web-page-unique-background,
.action-message-unique-background { .action-message-unique-background {
position: absolute; position: absolute;
inset: 0; inset: 0;
top: -6rem; top: -6rem;
} }
.web-page-unique-background {
top: -1rem;
}
.action-message-user-caption, .action-message-user-caption,
.action-message-stars-balance { .action-message-stars-balance {

View File

@ -34,6 +34,14 @@
border-radius: 0.25rem; border-radius: 0.25rem;
} }
&--unique-sticker {
position: relative;
width: 7.5rem;
height: 7.5rem;
overflow: hidden;
margin-block: 0.5rem;
}
&--stickers { &--stickers {
color: var(--accent-color); color: var(--accent-color);
border-radius: 0 !important; border-radius: 0 !important;
@ -60,6 +68,7 @@
.WebPage--content { .WebPage--content {
position: relative; position: relative;
&.is-gift,
&.is-story { &.is-story {
display: flex; display: flex;
flex-direction: column-reverse; flex-direction: column-reverse;
@ -85,6 +94,12 @@
} }
} }
.with-gift &--quick-button {
border-top: inherit;
margin-top: 0.0625rem;
margin-bottom: -0.125rem;
}
&-text { &-text {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -1,21 +1,24 @@
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import React, { memo, useRef } 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 { ApiMessage, ApiTypeStory } from '../../../api/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import { AudioOrigin, type ISettings } from '../../../types'; import { AudioOrigin, type ISettings } from '../../../types';
import { getMessageWebPage } from '../../../global/helpers'; import { getMessageWebPage } from '../../../global/helpers';
import { selectCanPlayAnimatedEmojis } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import trimText from '../../../util/trimText'; import trimText from '../../../util/trimText';
import { getGiftAttributes, getStickerFromGift } from '../../common/helpers/gifts';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
import { calculateMediaDimensions } from './helpers/mediaDimensions'; import { calculateMediaDimensions } from './helpers/mediaDimensions';
import { getWebpageButtonText } from './helpers/webpageType'; import { getWebpageButtonLangKey } from './helpers/webpageType';
import useDynamicColorListener from '../../../hooks/stickers/useDynamicColorListener'; import useDynamicColorListener from '../../../hooks/stickers/useDynamicColorListener';
import useAppLayout from '../../../hooks/useAppLayout'; import useAppLayout from '../../../hooks/useAppLayout';
import useEnsureStory from '../../../hooks/useEnsureStory'; import useEnsureStory from '../../../hooks/useEnsureStory';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
@ -23,6 +26,7 @@ import Audio from '../../common/Audio';
import Document from '../../common/Document'; import Document from '../../common/Document';
import EmojiIconBackground from '../../common/embedded/EmojiIconBackground'; import EmojiIconBackground from '../../common/embedded/EmojiIconBackground';
import PeerColorWrapper from '../../common/PeerColorWrapper'; import PeerColorWrapper from '../../common/PeerColorWrapper';
import RadialPatternBackground from '../../common/profile/RadialPatternBackground';
import SafeLink from '../../common/SafeLink'; import SafeLink from '../../common/SafeLink';
import StickerView from '../../common/StickerView'; import StickerView from '../../common/StickerView';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
@ -34,6 +38,7 @@ import './WebPage.scss';
const MAX_TEXT_LENGTH = 170; // symbols const MAX_TEXT_LENGTH = 170; // symbols
const WEBPAGE_STORY_TYPE = 'telegram_story'; const WEBPAGE_STORY_TYPE = 'telegram_story';
const WEBPAGE_GIFT_TYPE = 'telegram_nft';
const STICKER_SIZE = 80; const STICKER_SIZE = 80;
const EMOJI_SIZE = 38; const EMOJI_SIZE = 38;
@ -60,8 +65,12 @@ type OwnProps = {
onContainerClick?: ((e: React.MouseEvent) => void); onContainerClick?: ((e: React.MouseEvent) => void);
isEditing?: boolean; isEditing?: boolean;
}; };
type StateProps = {
canPlayAnimatedEmojis: boolean;
};
const STAR_GIFT_STICKER_SIZE = 120;
const WebPage: FC<OwnProps> = ({ const WebPage: FC<OwnProps & StateProps> = ({
message, message,
observeIntersectionForLoading, observeIntersectionForLoading,
observeIntersectionForPlaying, observeIntersectionForPlaying,
@ -89,8 +98,11 @@ const WebPage: FC<OwnProps> = ({
const { isMobile } = useAppLayout(); const { isMobile } = useAppLayout();
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const stickersRef = useRef<HTMLDivElement>(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(() => { const handleMediaClick = useLastCallback(() => {
onMediaClick!(); onMediaClick!();
@ -100,8 +112,9 @@ const WebPage: FC<OwnProps> = ({
onContainerClick?.(e); onContainerClick?.(e);
}); });
const handleQuickButtonClick = useLastCallback(() => { const handleOpenTelegramLink = useLastCallback(() => {
if (!webPage) return; if (!webPage) return;
openTelegramLink({ openTelegramLink({
url: webPage.url, url: webPage.url,
}); });
@ -132,8 +145,10 @@ const WebPage: FC<OwnProps> = ({
mediaSize, mediaSize,
} = webPage; } = webPage;
const isStory = type === WEBPAGE_STORY_TYPE; const isStory = type === WEBPAGE_STORY_TYPE;
const isGift = type === WEBPAGE_GIFT_TYPE;
const isExpiredStory = story && 'isDeleted' in story; 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 truncatedDescription = trimText(description, MAX_TEXT_LENGTH);
const isArticle = Boolean(truncatedDescription || title || siteName); const isArticle = Boolean(truncatedDescription || title || siteName);
let isSquarePhoto = Boolean(stickers); let isSquarePhoto = Boolean(stickers);
@ -159,31 +174,75 @@ const WebPage: FC<OwnProps> = ({
video && 'with-video', video && 'with-video',
!isArticle && 'no-article', !isArticle && 'no-article',
document && 'with-document', document && 'with-document',
quickButtonLangKey && 'with-quick-button', quickButtonTitle && 'with-quick-button',
isGift && 'with-gift',
); );
function renderQuickButton(langKey: string) { function renderQuickButton(caption: string) {
return ( return (
<Button <Button
className="WebPage--quick-button" className="WebPage--quick-button"
size="tiny" size="tiny"
color="translucent" color="translucent"
isRectangular isRectangular
onClick={handleQuickButtonClick} onClick={handleOpenTelegramLink}
> >
{lang(langKey)} {caption}
</Button> </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 ( return (
<PeerColorWrapper <PeerColorWrapper
className={className} className={className}
data-initial={(siteName || displayUrl)[0]} data-initial={(siteName || displayUrl)[0]}
dir={lang.isRtl ? 'rtl' : 'auto'} dir={oldLang.isRtl ? 'rtl' : 'auto'}
onClick={handleContainerClick} onClick={handleContainerClick}
> >
<div className={buildClassName('WebPage--content', isStory && 'is-story')}> <div className={buildClassName(
'WebPage--content',
isStory && 'is-story',
isGift && 'is-gift',
)}
>
{backgroundEmojiId && ( {backgroundEmojiId && (
<EmojiIconBackground <EmojiIconBackground
emojiDocumentId={backgroundEmojiId} emojiDocumentId={backgroundEmojiId}
@ -193,21 +252,24 @@ const WebPage: FC<OwnProps> = ({
{isStory && ( {isStory && (
<BaseStory story={story} isProtected={isProtected} isConnected={isConnected} isPreview /> <BaseStory story={story} isProtected={isProtected} isConnected={isConnected} isPreview />
)} )}
{isGift && !inPreview && (
renderStarGiftUnique()
)}
{isArticle && ( {isArticle && (
<div <div
className={buildClassName('WebPage-text', !inPreview && 'WebPage-text_interactive')} className={buildClassName('WebPage-text', !inPreview && 'WebPage-text_interactive')}
onClick={!inPreview ? () => openUrl({ url, shouldSkipModal: true }) : undefined} onClick={!inPreview ? () => openUrl({ url, shouldSkipModal: true }) : undefined}
> >
<SafeLink className="site-name" url={url} text={siteName || displayUrl} /> <SafeLink className="site-name" url={url} text={siteName || displayUrl} />
{!inPreview && title && ( {(!inPreview || isGift) && title && (
<p className="site-title">{renderText(title)}</p> <p className="site-title">{renderText(title)}</p>
)} )}
{truncatedDescription && ( {truncatedDescription && !isGift && (
<p className="site-description">{renderText(truncatedDescription, ['emoji', 'br'])}</p> <p className="site-description">{renderText(truncatedDescription, ['emoji', 'br'])}</p>
)} )}
</div> </div>
)} )}
{photo && !video && !document && ( {photo && !isGift && !video && !document && (
<Photo <Photo
photo={photo} photo={photo}
isOwn={message.isOutgoing} isOwn={message.isOutgoing}
@ -289,13 +351,19 @@ const WebPage: FC<OwnProps> = ({
{inPreview && displayUrl && !isArticle && ( {inPreview && displayUrl && !isArticle && (
<div className="WebPage-text"> <div className="WebPage-text">
<p className="site-name">{displayUrl}</p> <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>
)} )}
</div> </div>
{quickButtonLangKey && renderQuickButton(quickButtonLangKey)} {quickButtonTitle && renderQuickButton(quickButtonTitle)}
</PeerColorWrapper> </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 { .message-content {
&.gift {
--max-width: 18rem;
}
position: relative; position: relative;
max-width: var(--max-width); max-width: var(--max-width);

View File

@ -138,6 +138,10 @@ export function buildContentClassName(
if (webPage.document) { if (webPage.document) {
classNames.push('document'); classNames.push('document');
} }
if (webPage.gift) {
classNames.push('gift');
}
} }
if (invoice && !invoice.extendedMedia) { 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 // 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) { switch (type) {
case 'telegram_channel_request': case 'telegram_channel_request':
case 'telegram_megagroup_request': case 'telegram_megagroup_request':
case 'telegram_chat_request': case 'telegram_chat_request':
return 'lng_view_button_request_join'; return 'ViewButtonRequestJoin';
case 'telegram_message': case 'telegram_message':
return 'lng_view_button_message'; return 'ViewButtonMessage';
case 'telegram_bot': case 'telegram_bot':
return 'lng_view_button_bot'; return 'ViewButtonBot';
case 'telegram_voicechat': case 'telegram_voicechat':
return 'lng_view_button_voice_chat'; return 'ViewButtonVoiceChat';
case 'telegram_livestream': case 'telegram_livestream':
return 'lng_view_button_voice_chat_channel'; return 'ViewButtonVoiceChatChannel';
case 'telegram_megagroup': case 'telegram_megagroup':
case 'telegram_chat': case 'telegram_chat':
return 'lng_view_button_group'; return 'ViewButtonGroup';
case 'telegram_channel': case 'telegram_channel':
return 'lng_view_button_channel'; return 'ViewButtonChannel';
case 'telegram_user': case 'telegram_user':
return 'lng_view_button_user'; return 'ViewButtonUser';
case 'telegram_botapp': case 'telegram_botapp':
return 'lng_view_button_bot_app'; return 'ViewButtonBotApp';
case 'telegram_chatlist': case 'telegram_chatlist':
return 'ViewChatList'; return 'ViewChatList';
case 'telegram_story': case 'telegram_story':
return 'lng_view_button_story'; return 'ViewButtonStory';
case 'telegram_channel_boost': case 'telegram_channel_boost':
case 'telegram_group_boost': case 'telegram_group_boost':
return 'lng_view_button_boost'; return 'ViewButtonBoost';
case 'telegram_stickerset': case 'telegram_stickerset':
return 'lng_view_button_stickerset'; return 'ViewButtonStickerset';
case 'telegram_nft':
return 'ViewButtonGiftUnique';
default: default:
return undefined; return undefined;
} }

View File

@ -312,11 +312,10 @@ const GiftInfoModal = ({
const { const {
model, backdrop, pattern, originalDetails, model, backdrop, pattern, originalDetails,
} = giftAttributes || {}; } = giftAttributes || {};
const ownerId = gift.ownerId;
const ownerName = gift.ownerName; const ownerName = gift.ownerName;
tableData.push([ tableData.push([
lang('GiftInfoOwner'), lang('GiftInfoOwner'),
ownerId ? { chatId: ownerId } : ownerName || '', gift.ownerId ? { chatId: gift.ownerId } : ownerName || '',
]); ]);
if (model) { if (model) {

View File

@ -990,3 +990,23 @@ addActionHandler('launchPrepaidStarsGiveaway', async (global, actions, payload):
actions.openBoostStatistics({ chatId, tabId }); 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; storyId: number;
origin?: StoryViewerOrigin; origin?: StoryViewerOrigin;
} & WithTabId; } & WithTabId;
openUniqueGiftBySlug: {
slug: string;
} & WithTabId;
openPreviousStory: WithTabId | undefined; openPreviousStory: WithTabId | undefined;
openNextStory: WithTabId | undefined; openNextStory: WithTabId | undefined;
setStoryViewerMuted: { 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.getUserStarGifts#5e72c7e1 user_id:InputUser offset:string limit:int = payments.UserStarGifts;
payments.saveStarGift#92fd2aae flags:# unsave:flags.0?true msg_id:int = Bool; payments.saveStarGift#92fd2aae flags:# unsave:flags.0?true msg_id:int = Bool;
payments.convertStarGift#72770c83 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.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.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; phone.confirmCall#2efe1722 peer:InputPhoneCall g_a:bytes key_fingerprint:long protocol:PhoneCallProtocol = phone.PhoneCall;

View File

@ -384,5 +384,6 @@
"payments.getUserStarGifts", "payments.getUserStarGifts",
"payments.saveStarGift", "payments.saveStarGift",
"payments.convertStarGift", "payments.convertStarGift",
"payments.getUniqueStarGift",
"fragment.getCollectibleInfo" "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.getUserStarGifts#5e72c7e1 user_id:InputUser offset:string limit:int = payments.UserStarGifts;
payments.saveStarGift#92fd2aae flags:# unsave:flags.0?true msg_id:int = Bool; payments.saveStarGift#92fd2aae flags:# unsave:flags.0?true msg_id:int = Bool;
payments.convertStarGift#72770c83 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.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.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; payments.getConnectedStarRefBot#b7d998f0 peer:InputPeer bot:InputUser = payments.ConnectedStarRefBots;

View File

@ -1222,6 +1222,21 @@ export interface LangPair {
'ProfileTabSimilarChannels': undefined; 'ProfileTabSimilarChannels': undefined;
'ActionUnsupportedTitle': undefined; 'ActionUnsupportedTitle': undefined;
'ActionUnsupportedDescription': 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> { 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' | export type DeepLinkMethod = 'resolve' | 'login' | 'passport' | 'settings' | 'join' | 'addstickers' | 'addemoji' |
'setlanguage' | 'addtheme' | 'confirmphone' | 'socks' | 'proxy' | 'privatepost' | 'bg' | 'share' | 'msg' | 'msg_url' | '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 { interface PublicMessageLink {
type: 'publicMessageLink'; type: 'publicMessageLink';
@ -96,6 +97,11 @@ interface PremiumMultigiftLink {
referrer: string; referrer: string;
} }
interface GiftUniqueLink {
type: 'giftUniqueLink';
slug: string;
}
type DeepLink = type DeepLink =
TelegramPassportLink | TelegramPassportLink |
LoginCodeLink | LoginCodeLink |
@ -108,7 +114,8 @@ type DeepLink =
BusinessChatLink | BusinessChatLink |
PremiumReferrerLink | PremiumReferrerLink |
PremiumMultigiftLink | PremiumMultigiftLink |
ChatBoostLink; ChatBoostLink |
GiftUniqueLink;
type BuilderParams<T extends DeepLink> = Record<keyof Omit<T, 'type'>, string | undefined>; type BuilderParams<T extends DeepLink> = Record<keyof Omit<T, 'type'>, string | undefined>;
type BuilderReturnType<T extends DeepLink> = T | undefined; type BuilderReturnType<T extends DeepLink> = T | undefined;
@ -229,6 +236,8 @@ function parseTgLink(url: URL) {
return buildPremiumMultigiftLink({ referrer: queryParams.ref }); return buildPremiumMultigiftLink({ referrer: queryParams.ref });
case 'chatBoostLink': case 'chatBoostLink':
return buildChatBoostLink({ username: queryParams.domain, id: queryParams.channel }); return buildChatBoostLink({ username: queryParams.domain, id: queryParams.channel });
case 'giftUniqueLink':
return buildGiftUniqueLink({ slug: queryParams.slug });
default: default:
break; break;
} }
@ -331,6 +340,12 @@ function parseHttpLink(url: URL) {
id: isPrivateChannel ? pathParams[1] : undefined, id: isPrivateChannel ? pathParams[1] : undefined,
}); });
} }
case 'giftUniqueLink': {
const slug = pathParams.slice(1).join('/');
return buildGiftUniqueLink({
slug,
});
}
default: default:
break; break;
} }
@ -355,6 +370,7 @@ function getHttpDeepLinkType(
if (method === 'login') return 'loginCodeLink'; if (method === 'login') return 'loginCodeLink';
if (method === 'm') return 'businessChatLink'; if (method === 'm') return 'businessChatLink';
if (method === 'boost') return 'chatBoostLink'; if (method === 'boost') return 'chatBoostLink';
if (method === 'nft') return 'giftUniqueLink';
if (method === 'c') { if (method === 'c') {
if (queryParams.boost !== undefined) return 'chatBoostLink'; if (queryParams.boost !== undefined) return 'chatBoostLink';
return 'privateChannelLink'; return 'privateChannelLink';
@ -370,6 +386,7 @@ function getHttpDeepLinkType(
if (isUsernameValid(pathParams[0]) && pathParams.slice(1).every(isNumber)) { if (isUsernameValid(pathParams[0]) && pathParams.slice(1).every(isNumber)) {
return 'publicMessageLink'; return 'publicMessageLink';
} }
if (method === 'nft') return 'giftUniqueLink';
} else if (len === 4) { } else if (len === 4) {
if (method === 'c' && pathParams.slice(1).every(isNumber)) { if (method === 'c' && pathParams.slice(1).every(isNumber)) {
return 'privateMessageLink'; return 'privateMessageLink';
@ -424,6 +441,8 @@ function getTgDeepLinkType(
return 'premiumMultigiftLink'; return 'premiumMultigiftLink';
case 'boost': case 'boost':
return 'chatBoostLink'; return 'chatBoostLink';
case 'nft':
return 'giftUniqueLink';
default: default:
break; 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> { function buildPremiumReferrerLink(params: BuilderParams<PremiumReferrerLink>): BuilderReturnType<PremiumReferrerLink> {
const { const {
referrer, referrer,

View File

@ -66,6 +66,9 @@ export const processDeepLink = (url: string): boolean => {
isPrivate: Boolean(parsedLink.id), isPrivate: Boolean(parsedLink.id),
}); });
return true; return true;
case 'giftUniqueLink':
actions.openUniqueGiftBySlug({ slug: parsedLink.slug });
return true;
default: default:
break; break;
} }