Support unique gifts (#5394)
This commit is contained in:
parent
9e6aa47013
commit
180aef57bf
@ -374,8 +374,7 @@ function buildApiMessageActionStarGift(action: GramJs.MessageActionStarGift) : A
|
||||
isNameHidden: Boolean(nameHidden),
|
||||
isSaved: Boolean(saved),
|
||||
isConverted: Boolean(converted),
|
||||
// ToDo: Use `!` temporarily to support layer 196
|
||||
gift: buildApiStarGift(gift)!,
|
||||
gift: buildApiStarGift(gift),
|
||||
message: message && buildApiFormattedText(message),
|
||||
starsToConvert: convertStars?.toJSNumber(),
|
||||
};
|
||||
|
||||
@ -20,6 +20,7 @@ import type {
|
||||
ApiPrepaidStarsGiveaway,
|
||||
ApiReceipt,
|
||||
ApiStarGift,
|
||||
ApiStarGiftAttribute,
|
||||
ApiStarGiveawayOption,
|
||||
ApiStarsAmount,
|
||||
ApiStarsGiveawayWinnerOption,
|
||||
@ -31,13 +32,15 @@ import type {
|
||||
BoughtPaidMedia,
|
||||
} from '../../types';
|
||||
|
||||
import { addWebDocumentToLocalDb } from '../helpers';
|
||||
import { numberToHexColor } from '../../../util/colors';
|
||||
import { addDocumentToLocalDb, addWebDocumentToLocalDb } from '../helpers';
|
||||
import { buildApiStarsSubscriptionPricing } from './chats';
|
||||
import { buildApiFormattedText, buildApiMessageEntity } from './common';
|
||||
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) {
|
||||
@ -536,7 +539,7 @@ export function buildApiStarsTransactionPeer(peer: GramJs.TypeStarsTransactionPe
|
||||
export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): ApiStarsTransaction {
|
||||
const {
|
||||
date, id, peer, stars, description, photo, title, refund, extendedMedia, failed, msgId, pending, gift, reaction,
|
||||
subscriptionPeriod, stargift, giveawayPostId, starrefCommissionPermille,
|
||||
subscriptionPeriod, stargift, giveawayPostId, starrefCommissionPermille, stargiftUpgrade,
|
||||
} = transaction;
|
||||
|
||||
if (photo) {
|
||||
@ -547,7 +550,6 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction):
|
||||
.filter(Boolean) as BoughtPaidMedia[];
|
||||
|
||||
const starRefCommision = starrefCommissionPermille ? starrefCommissionPermille / 10 : undefined;
|
||||
const supportedStarGift = (stargift instanceof GramJs.StarGift) ? stargift : undefined;
|
||||
|
||||
return {
|
||||
id,
|
||||
@ -565,9 +567,10 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction):
|
||||
extendedMedia: boughtExtendedMedia,
|
||||
subscriptionPeriod,
|
||||
isReaction: reaction,
|
||||
starGift: supportedStarGift && buildApiStarGift(supportedStarGift),
|
||||
starGift: stargift && buildApiStarGift(stargift),
|
||||
giveawayPostId,
|
||||
starRefCommision,
|
||||
isGiftUpgrade: stargiftUpgrade,
|
||||
};
|
||||
}
|
||||
|
||||
@ -610,18 +613,38 @@ export function buildApiStarTopupOption(option: GramJs.TypeStarsTopupOption): Ap
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiStarGift(startGift: GramJs.TypeStarGift): ApiStarGift | undefined {
|
||||
const isTypeSupported = startGift instanceof GramJs.StarGift;
|
||||
if (!isTypeSupported) return undefined;
|
||||
export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
|
||||
if (starGift instanceof GramJs.StarGiftUnique) {
|
||||
const {
|
||||
id, num, ownerId, title, attributes, availabilityIssued, availabilityTotal,
|
||||
} = starGift;
|
||||
|
||||
return {
|
||||
type: 'starGiftUnique',
|
||||
id: id.toString(),
|
||||
number: num,
|
||||
ownerId: buildApiPeerId(ownerId, 'user'),
|
||||
attributes: attributes.map(buildApiStarGiftAttribute).filter(Boolean),
|
||||
title,
|
||||
totalCount: availabilityTotal,
|
||||
issuedCount: availabilityIssued,
|
||||
};
|
||||
}
|
||||
|
||||
const {
|
||||
id, limited, sticker, stars, availabilityRemains, availabilityTotal, convertStars, firstSaleDate, lastSaleDate,
|
||||
soldOut,
|
||||
} = startGift;
|
||||
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,
|
||||
stickerId: sticker.id.toString(),
|
||||
sticker,
|
||||
stars: stars.toJSNumber(),
|
||||
availabilityRemains,
|
||||
availabilityTotal,
|
||||
@ -629,17 +652,84 @@ export function buildApiStarGift(startGift: GramJs.TypeStarGift): ApiStarGift |
|
||||
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 {
|
||||
// ToDo: Use `!` temporarily to support layer 196
|
||||
gift: buildApiStarGift(gift)!,
|
||||
gift: buildApiStarGift(gift),
|
||||
date,
|
||||
starsToConvert: convertStars?.toJSNumber(),
|
||||
fromId: fromId && buildApiPeerId(fromId, 'user'),
|
||||
|
||||
@ -2,8 +2,12 @@ import BigInt from 'big-integer';
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiInputStorePaymentPurpose, ApiPeer, ApiRequestInputInvoice,
|
||||
ApiSticker, ApiThemeParameters,
|
||||
ApiChat,
|
||||
ApiInputStorePaymentPurpose,
|
||||
ApiPeer,
|
||||
ApiRequestInputInvoice,
|
||||
ApiStarGiftRegular,
|
||||
ApiThemeParameters,
|
||||
ApiUser,
|
||||
} from '../../types';
|
||||
|
||||
@ -29,7 +33,6 @@ import {
|
||||
buildShippingOptions,
|
||||
} from '../apiBuilders/payments';
|
||||
import { buildApiPeerId } from '../apiBuilders/peers';
|
||||
import { buildStickerFromDocument } from '../apiBuilders/symbols';
|
||||
import {
|
||||
buildInputInvoice, buildInputPeer, buildInputStorePaymentPurpose, buildInputThemeParams, buildShippingInfo,
|
||||
} from '../gramjsBuilders';
|
||||
@ -430,22 +433,8 @@ export async function fetchStarGifts() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const gifts = result.gifts.map(buildApiStarGift).filter(Boolean);
|
||||
const stickers : Record<string, ApiSticker> = {};
|
||||
|
||||
result.gifts.forEach((gift) => {
|
||||
if (!(gift instanceof GramJs.StarGift)) return;
|
||||
if (gift.sticker instanceof GramJs.Document) {
|
||||
localDb.documents[String(gift.sticker.id)] = gift.sticker;
|
||||
}
|
||||
|
||||
const sticker = buildStickerFromDocument(gift.sticker);
|
||||
if (sticker) {
|
||||
stickers[sticker.id] = sticker;
|
||||
}
|
||||
});
|
||||
|
||||
return { gifts, stickers };
|
||||
// Right now, only regular star gifts can be bought, but API are not specific
|
||||
return result.gifts.map(buildApiStarGift).filter((gift): gift is ApiStarGiftRegular => gift.type === 'starGift');
|
||||
}
|
||||
|
||||
export async function fetchUserStarGifts({
|
||||
@ -467,12 +456,10 @@ export async function fetchUserStarGifts({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const supportedGifts = result.gifts.filter(
|
||||
((gift) => gift instanceof GramJs.StarGift),
|
||||
).map(buildApiUserStarGift);
|
||||
const gifts = result.gifts.map(buildApiUserStarGift);
|
||||
|
||||
return {
|
||||
gifts: supportedGifts,
|
||||
gifts,
|
||||
nextOffset: result.nextOffset,
|
||||
};
|
||||
}
|
||||
@ -528,13 +515,10 @@ export async function fetchStarsStatus() {
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
const supportedHistory = result.history?.filter(
|
||||
(transaction) => !(transaction.stargift instanceof GramJs.StarGiftUnique),
|
||||
);
|
||||
|
||||
return {
|
||||
nextHistoryOffset: result.nextOffset,
|
||||
history: supportedHistory?.map(buildApiStarsTransaction),
|
||||
history: result.history?.map(buildApiStarsTransaction),
|
||||
nextSubscriptionOffset: result.subscriptionsNextOffset,
|
||||
subscriptions: result.subscriptions?.map(buildApiStarsSubscription),
|
||||
balance: buildApiStarsAmount(result.balance),
|
||||
@ -564,13 +548,9 @@ export async function fetchStarsTransactions({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const supportedHistory = result.history?.filter(
|
||||
(transaction) => !(transaction.stargift instanceof GramJs.StarGiftUnique),
|
||||
);
|
||||
|
||||
return {
|
||||
nextOffset: result.nextOffset,
|
||||
history: supportedHistory?.map(buildApiStarsTransaction),
|
||||
history: result.history?.map(buildApiStarsTransaction),
|
||||
balance: buildApiStarsAmount(result.balance),
|
||||
};
|
||||
}
|
||||
@ -589,15 +569,12 @@ export async function fetchStarsTransactionById({
|
||||
})],
|
||||
}));
|
||||
|
||||
const supportedHistory = result?.history?.filter(
|
||||
(transaction) => !(transaction.stargift instanceof GramJs.StarGiftUnique),
|
||||
);
|
||||
if (!supportedHistory?.[0]) {
|
||||
if (!result?.history?.[0]) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
transaction: buildApiStarsTransaction(supportedHistory[0]),
|
||||
transaction: buildApiStarsTransaction(result?.history[0]),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import type {
|
||||
ApiInvoice,
|
||||
ApiMessageEntity,
|
||||
ApiPaymentCredentials,
|
||||
ApiSticker,
|
||||
BoughtPaidMedia,
|
||||
} from './messages';
|
||||
import type { StatisticsOverviewPercentage } from './statistics';
|
||||
@ -189,10 +190,11 @@ export type ApiInputStorePaymentStarsGiveaway = {
|
||||
export type ApiInputStorePaymentPurpose = ApiInputStorePaymentGiveaway | ApiInputStorePaymentGiftcode |
|
||||
ApiInputStorePaymentStarsTopup | ApiInputStorePaymentStarsGift | ApiInputStorePaymentStarsGiveaway;
|
||||
|
||||
export type ApiStarGift = {
|
||||
export interface ApiStarGiftRegular {
|
||||
type: 'starGift';
|
||||
isLimited?: true;
|
||||
id: string;
|
||||
stickerId: string;
|
||||
sticker: ApiSticker;
|
||||
stars: number;
|
||||
availabilityRemains?: number;
|
||||
availabilityTotal?: number;
|
||||
@ -200,7 +202,57 @@ export type ApiStarGift = {
|
||||
isSoldOut?: true;
|
||||
firstSaleDate?: number;
|
||||
lastSaleDate?: number;
|
||||
};
|
||||
isBirthday?: true;
|
||||
upgradeStars?: number;
|
||||
}
|
||||
|
||||
export interface ApiStarGiftUnique {
|
||||
type: 'starGiftUnique';
|
||||
id: string;
|
||||
title: string;
|
||||
number: number;
|
||||
ownerId: string;
|
||||
issuedCount: number;
|
||||
totalCount: number;
|
||||
attributes: ApiStarGiftAttribute[];
|
||||
}
|
||||
|
||||
export type ApiStarGift = ApiStarGiftRegular | ApiStarGiftUnique;
|
||||
|
||||
export interface ApiStarGiftAttributeModel {
|
||||
type: 'model';
|
||||
name: string;
|
||||
rarityPercent: number;
|
||||
sticker: ApiSticker;
|
||||
}
|
||||
|
||||
export interface ApiStarGiftAttributePattern {
|
||||
type: 'pattern';
|
||||
name: string;
|
||||
rarityPercent: number;
|
||||
sticker: ApiSticker;
|
||||
}
|
||||
|
||||
export interface ApiStarGiftAttributeBackdrop {
|
||||
type: 'backdrop';
|
||||
name: string;
|
||||
centerColor: string;
|
||||
edgeColor: string;
|
||||
patternColor: string;
|
||||
textColor: string;
|
||||
rarityPercent: number;
|
||||
}
|
||||
|
||||
export interface ApiStarGiftAttributeOriginalDetails {
|
||||
type: 'originalDetails';
|
||||
senderId?: string;
|
||||
recipientId: string;
|
||||
date: number;
|
||||
message?: ApiFormattedText;
|
||||
}
|
||||
|
||||
export type ApiStarGiftAttribute = ApiStarGiftAttributeModel | ApiStarGiftAttributePattern
|
||||
| ApiStarGiftAttributeBackdrop | ApiStarGiftAttributeOriginalDetails;
|
||||
|
||||
export interface ApiUserStarGift {
|
||||
isNameHidden?: boolean;
|
||||
@ -371,6 +423,7 @@ export interface ApiStarsTransaction {
|
||||
extendedMedia?: BoughtPaidMedia[];
|
||||
subscriptionPeriod?: number;
|
||||
starRefCommision?: number;
|
||||
isGiftUpgrade?: true;
|
||||
}
|
||||
|
||||
export interface ApiStarsSubscription {
|
||||
|
||||
@ -1298,7 +1298,9 @@
|
||||
"GiftPremiumDescriptionLinkCaption" = "See Features >";
|
||||
"GiftPremiumDescriptionLink" = "https://telegram.org/faq_premium";
|
||||
"StarsGiftHeader" = "Send a Gift";
|
||||
"StarsGiftHeaderSelf" = "Buy a Gift";
|
||||
"StarGiftDescription" = "Give {user} gifts that can be kept on the profile or converted to Stars.";
|
||||
"StarGiftDescriptionSelf" = "Buy yourself a gift to display on your page or reserve for later.\n\nLimited-edition gifts upgraded to collectibles can be gifted to others later.";
|
||||
"GiftLimited" = "limited";
|
||||
"GiftDiscount" = "-{percent}%";
|
||||
"GiftSoldCount" = "{count} sold";
|
||||
@ -1342,11 +1344,25 @@
|
||||
"GiftInfoSoldOutTitle" = "Unavailable";
|
||||
"GiftInfoSoldOutDescription" = "This gift has been sold out";
|
||||
"GiftInfoSenderHidden" = "Only you can see the sender's name and message.";
|
||||
"GiftInfoOwner" = "Owner";
|
||||
"GiftInfoAvailability" = "Availability";
|
||||
"GiftInfoIssued" = "{issued}/{total} issued";
|
||||
"GiftInfoCollectible" = "Collectible #{number}";
|
||||
"GiftAttributeModel" = "Model";
|
||||
"GiftAttributeBackdrop" = "Backdrop";
|
||||
"GiftAttributeSymbol" = "Symbol";
|
||||
"GiftInfoOriginalInfo" = "Gifted to {user} on {date}."
|
||||
"GiftInfoOriginalInfoSender" = "Gifted by {sender} to {user} on {date}."
|
||||
"GiftInfoOriginalInfoText" = "Gifted to {user} on {date} with comment \"{text}\"."
|
||||
"GiftInfoOriginalInfoTextSender" = "Gifted by {sender} to {user} on {date} with comment \"{text}\"."
|
||||
"GiftInfoStatus" = "Status";
|
||||
"GiftInfoStatusNonUnique" = "Non-Unique";
|
||||
"StarsAmount" = "⭐️{amount}";
|
||||
"StarsAmountText_one" = "{amount} Star";
|
||||
"StarsAmountText_other" = "{amount} Stars";
|
||||
"AllGiftsCategory" = "All gifts";
|
||||
"LimitedGiftsCategory" = "Limited";
|
||||
"StockGiftsCategory" = "In Stock";
|
||||
"PremiumGiftDescription" = "Premium";
|
||||
"SendPaidReaction" = "Send ⭐️{amount}";
|
||||
"StarsPay" = "Confirm and Pay {amount}";
|
||||
|
||||
@ -6,9 +6,12 @@
|
||||
background-color: var(--accent-background-active-color);
|
||||
color: var(--accent-color);
|
||||
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
filter: brightness(1);
|
||||
transition: 150ms filter ease-in;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
&:hover {
|
||||
filter: brightness(1.1);
|
||||
|
||||
@ -16,7 +16,7 @@ const BadgeButton = ({
|
||||
onClick,
|
||||
}: OwnProps) => {
|
||||
return (
|
||||
<div className={buildClassName(styles.root, className)} onClick={onClick}>
|
||||
<div className={buildClassName(styles.root, onClick && styles.clickable, className)} onClick={onClick}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -35,6 +35,7 @@ type OwnProps<T = undefined> = {
|
||||
className?: string;
|
||||
fluid?: boolean;
|
||||
withPeerColors?: boolean;
|
||||
withEmojiStatus?: boolean;
|
||||
clickArg?: T;
|
||||
onClick?: (arg: T) => void;
|
||||
};
|
||||
@ -59,6 +60,7 @@ const PeerChip = <T,>({
|
||||
fluid,
|
||||
isSavedMessages,
|
||||
withPeerColors,
|
||||
withEmojiStatus,
|
||||
onClick,
|
||||
}: OwnProps<T> & StateProps) => {
|
||||
const lang = useOldLang();
|
||||
@ -91,7 +93,9 @@ const PeerChip = <T,>({
|
||||
);
|
||||
|
||||
titleText = getPeerTitle(lang, anyPeer) || title;
|
||||
titleElement = title || <FullNameTitle peer={anyPeer} isSavedMessages={isSavedMessages} withEmojiStatus />;
|
||||
titleElement = title || (
|
||||
<FullNameTitle peer={anyPeer} isSavedMessages={isSavedMessages} withEmojiStatus={withEmojiStatus} />
|
||||
);
|
||||
}
|
||||
|
||||
const fullClassName = buildClassName(
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) translate(6px, -6px) rotate(45deg);
|
||||
|
||||
font-size: 0.6875rem;
|
||||
font-size: 0.625rem;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-white);
|
||||
white-space: nowrap;
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
border-radius: 0.625rem;
|
||||
background-color: var(--color-hover-overlay);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
@ -34,15 +35,6 @@
|
||||
left: 0.25rem;
|
||||
}
|
||||
|
||||
.stars {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
|
||||
color: #E88011;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.hiddenGift {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
@ -60,3 +52,8 @@
|
||||
font-size: 1.25rem;
|
||||
backdrop-filter: blur(0.5rem);
|
||||
}
|
||||
|
||||
.radialPattern {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
@ -1,20 +1,22 @@
|
||||
import React, { memo } from '../../../lib/teact/teact';
|
||||
import React, { memo, useMemo, useRef } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiSticker, ApiUser, ApiUserStarGift } from '../../../api/types';
|
||||
import type { ApiUser, ApiUserStarGift } from '../../../api/types';
|
||||
|
||||
import { STARS_CURRENCY_CODE } from '../../../config';
|
||||
import { selectUser } from '../../../global/selectors';
|
||||
import { formatCurrency } from '../../../util/formatCurrency';
|
||||
import { CUSTOM_PEER_HIDDEN } from '../../../util/objects/customPeer';
|
||||
import { formatIntegerCompact } from '../../../util/textFormat';
|
||||
import { getGiftAttributes, getStickerFromGift, getTotalGiftAvailability } from '../helpers/gifts';
|
||||
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import { type ObserveFn, useOnIntersect } from '../../../hooks/useIntersectionObserver';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import AnimatedIconFromSticker from '../AnimatedIconFromSticker';
|
||||
import Avatar from '../Avatar';
|
||||
import Icon from '../icons/Icon';
|
||||
import RadialPatternBackground from '../profile/RadialPatternBackground';
|
||||
import GiftRibbon from './GiftRibbon';
|
||||
|
||||
import styles from './UserGift.module.scss';
|
||||
@ -22,20 +24,28 @@ import styles from './UserGift.module.scss';
|
||||
type OwnProps = {
|
||||
userId: string;
|
||||
gift: ApiUserStarGift;
|
||||
observeIntersection?: ObserveFn;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
fromPeer?: ApiUser;
|
||||
sticker?: ApiSticker;
|
||||
};
|
||||
|
||||
const GIFT_STICKER_SIZE = 90;
|
||||
|
||||
const UserGift = ({
|
||||
userId, gift, fromPeer, sticker,
|
||||
userId,
|
||||
gift,
|
||||
fromPeer,
|
||||
observeIntersection,
|
||||
}: OwnProps & StateProps) => {
|
||||
const { openGiftInfoModal } = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [shouldPlay, play] = useFlag();
|
||||
|
||||
const oldLang = useOldLang();
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
@ -45,16 +55,48 @@ const UserGift = ({
|
||||
});
|
||||
});
|
||||
|
||||
const handleOnIntersect = useLastCallback((entry: IntersectionObserverEntry) => {
|
||||
if (entry.isIntersecting) play();
|
||||
});
|
||||
|
||||
const avatarPeer = (gift.isNameHidden || !fromPeer) ? CUSTOM_PEER_HIDDEN : fromPeer;
|
||||
|
||||
const sticker = getStickerFromGift(gift.gift);
|
||||
|
||||
const radialPatternBackdrop = useMemo(() => {
|
||||
const { backdrop, pattern } = getGiftAttributes(gift.gift) || {};
|
||||
|
||||
if (!backdrop || !pattern) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const backdropColors = [backdrop.centerColor, backdrop.edgeColor];
|
||||
const patternColor = backdrop.patternColor;
|
||||
|
||||
return (
|
||||
<RadialPatternBackground
|
||||
className={styles.radialPattern}
|
||||
backgroundColors={backdropColors}
|
||||
patternColor={patternColor}
|
||||
patternIcon={pattern.sticker}
|
||||
/>
|
||||
);
|
||||
}, [gift.gift]);
|
||||
|
||||
useOnIntersect(ref, observeIntersection, sticker ? handleOnIntersect : undefined);
|
||||
|
||||
if (!sticker) return undefined;
|
||||
|
||||
const totalIssued = getTotalGiftAvailability(gift.gift);
|
||||
|
||||
return (
|
||||
<div className={styles.root} onClick={handleClick}>
|
||||
<div ref={ref} className={styles.root} onClick={handleClick}>
|
||||
{radialPatternBackdrop}
|
||||
<Avatar className={styles.avatar} peer={avatarPeer} size="micro" />
|
||||
<AnimatedIconFromSticker
|
||||
sticker={sticker}
|
||||
noLoop
|
||||
play={shouldPlay}
|
||||
nonInteractive
|
||||
size={GIFT_STICKER_SIZE}
|
||||
/>
|
||||
@ -63,13 +105,10 @@ const UserGift = ({
|
||||
<Icon name="eye-closed-outline" />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.stars}>
|
||||
{formatCurrency(gift.gift.stars, STARS_CURRENCY_CODE)}
|
||||
</div>
|
||||
{gift.gift.availabilityTotal && (
|
||||
{totalIssued && (
|
||||
<GiftRibbon
|
||||
color="blue"
|
||||
text={oldLang('Gift2Limited1OfRibbon', formatIntegerCompact(gift.gift.availabilityTotal))}
|
||||
text={oldLang('Gift2Limited1OfRibbon', formatIntegerCompact(totalIssued))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -78,11 +117,9 @@ const UserGift = ({
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { gift }): StateProps => {
|
||||
const sticker = global.stickers.starGifts.stickers[gift.gift.stickerId];
|
||||
const fromPeer = gift.fromId ? selectUser(global, gift.fromId) : undefined;
|
||||
|
||||
return {
|
||||
sticker,
|
||||
fromPeer,
|
||||
};
|
||||
},
|
||||
|
||||
56
src/components/common/helpers/gifts.ts
Normal file
56
src/components/common/helpers/gifts.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import type {
|
||||
ApiFormattedText,
|
||||
ApiStarGift,
|
||||
ApiStarGiftAttributeBackdrop,
|
||||
ApiStarGiftAttributeModel,
|
||||
ApiStarGiftAttributeOriginalDetails,
|
||||
ApiStarGiftAttributePattern,
|
||||
ApiSticker,
|
||||
} from '../../../api/types';
|
||||
|
||||
export type GiftAttributes = {
|
||||
model?: ApiStarGiftAttributeModel;
|
||||
originalDetails?: ApiStarGiftAttributeOriginalDetails;
|
||||
pattern?: ApiStarGiftAttributePattern;
|
||||
backdrop?: ApiStarGiftAttributeBackdrop;
|
||||
};
|
||||
|
||||
export function getStickerFromGift(gift: ApiStarGift): ApiSticker | undefined {
|
||||
if (gift.type === 'starGift') {
|
||||
return gift.sticker;
|
||||
}
|
||||
|
||||
return gift.attributes.find((attr): attr is ApiStarGiftAttributeModel => attr.type === 'model')?.sticker;
|
||||
}
|
||||
|
||||
export function getTotalGiftAvailability(gift: ApiStarGift): number | undefined {
|
||||
if (gift.type === 'starGift') {
|
||||
return gift.availabilityTotal;
|
||||
}
|
||||
|
||||
return gift.totalCount;
|
||||
}
|
||||
|
||||
export function getGiftMessage(gift: ApiStarGift): ApiFormattedText | undefined {
|
||||
if (gift.type !== 'starGiftUnique') return undefined;
|
||||
|
||||
return gift.attributes.find((attr): attr is ApiStarGiftAttributeOriginalDetails => attr.type === 'model')?.message;
|
||||
}
|
||||
|
||||
export function getGiftAttributes(gift: ApiStarGift): GiftAttributes | undefined {
|
||||
if (gift.type !== 'starGiftUnique') return undefined;
|
||||
|
||||
const model = gift.attributes.find((attr): attr is ApiStarGiftAttributeModel => attr.type === 'model');
|
||||
const backdrop = gift.attributes.find((attr): attr is ApiStarGiftAttributeBackdrop => attr.type === 'backdrop');
|
||||
const pattern = gift.attributes.find((attr): attr is ApiStarGiftAttributePattern => attr.type === 'pattern');
|
||||
const originalDetails = gift.attributes.find((attr): attr is ApiStarGiftAttributeOriginalDetails => (
|
||||
attr.type === 'originalDetails'
|
||||
));
|
||||
|
||||
return {
|
||||
model,
|
||||
originalDetails,
|
||||
pattern,
|
||||
backdrop,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
.root {
|
||||
border-radius: inherit;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(var(--_bg-1), var(--_bg-2)), radial-gradient(circle, #ffffff32, #ffffff00);
|
||||
}
|
||||
}
|
||||
|
||||
.canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
position: relative;
|
||||
}
|
||||
167
src/components/common/profile/RadialPatternBackground.tsx
Normal file
167
src/components/common/profile/RadialPatternBackground.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
import React, {
|
||||
memo, useEffect, useRef, useSignal, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiSticker } from '../../../api/types';
|
||||
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { getStickerMediaHash } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import buildStyle from '../../../util/buildStyle';
|
||||
import { preloadImage } from '../../../util/files';
|
||||
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useResizeObserver from '../../../hooks/useResizeObserver';
|
||||
import { useSignalEffect } from '../../../hooks/useSignalEffect';
|
||||
|
||||
import styles from './RadialPatternBackground.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
backgroundColors: string[];
|
||||
patternColor: string;
|
||||
patternIcon: ApiSticker;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const RINGS = 3;
|
||||
const BASE_RING_ITEM_COUNT = 8;
|
||||
const RING_INCREMENT = 0.5;
|
||||
const CENTER_EMPTINESS = 0.05;
|
||||
const MAX_RADIUS = 0.5;
|
||||
const BASE_ICON_SIZE = 20;
|
||||
|
||||
const MIN_SIZE = 200;
|
||||
|
||||
const PATTERN_POSITIONS = (() => {
|
||||
const coordinates: { x: number; y: number; alpha: number; sizeFactor: number }[] = [];
|
||||
for (let ring = 1; ring <= RINGS; ring++) {
|
||||
const ringItemCount = Math.floor(BASE_RING_ITEM_COUNT * (1 + (ring - 1) * RING_INCREMENT));
|
||||
const ringProgress = ring / RINGS;
|
||||
const ringRadius = CENTER_EMPTINESS + (MAX_RADIUS - CENTER_EMPTINESS) * ringProgress;
|
||||
|
||||
for (let i = 0; i < ringItemCount; i++) {
|
||||
const angle = (i / ringItemCount) * Math.PI * 2;
|
||||
// Slightly oval
|
||||
const xOffset = ringRadius * 1.71 * Math.cos(angle);
|
||||
const yOffset = ringRadius * Math.sin(angle);
|
||||
|
||||
const x = 0.5 + xOffset;
|
||||
const y = 0.5 + yOffset;
|
||||
const alpha = 0.2 + Math.min((1 - ringProgress + (Math.random() / 2 - 0.5)), 0) * 0.8;
|
||||
|
||||
const sizeFactor = 1.4 - ringProgress * Math.random();
|
||||
|
||||
coordinates.push({
|
||||
x, y, alpha, sizeFactor,
|
||||
});
|
||||
}
|
||||
}
|
||||
return coordinates;
|
||||
})();
|
||||
|
||||
const RadialPatternBackground = ({
|
||||
backgroundColors,
|
||||
patternColor,
|
||||
patternIcon,
|
||||
className,
|
||||
}: OwnProps) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const [getContainerSize, setContainerSize] = useSignal({ width: 0, height: 0 });
|
||||
|
||||
const [emojiImage, setEmojiImage] = useState<HTMLImageElement | undefined>();
|
||||
|
||||
const previewMediaHash = getStickerMediaHash(patternIcon, 'preview');
|
||||
const previewUrl = useMedia(previewMediaHash);
|
||||
|
||||
useEffect(() => {
|
||||
if (!previewUrl) return;
|
||||
preloadImage(previewUrl).then(setEmojiImage);
|
||||
}, [previewUrl]);
|
||||
|
||||
useResizeObserver(containerRef, (entry) => {
|
||||
setContainerSize({
|
||||
width: entry.contentRect.width,
|
||||
height: entry.contentRect.height,
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (container) {
|
||||
setContainerSize({
|
||||
width: container.clientWidth,
|
||||
height: container.clientHeight,
|
||||
});
|
||||
}
|
||||
}, [setContainerSize]);
|
||||
|
||||
const draw = useLastCallback(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas || !emojiImage) return;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
const { width, height } = canvas;
|
||||
if (!width || !height) return;
|
||||
|
||||
ctx.save();
|
||||
PATTERN_POSITIONS.forEach(({
|
||||
x, y, alpha, sizeFactor,
|
||||
}) => {
|
||||
const centerShift = (width - Math.max(width, MIN_SIZE)) / 2; // Shift coords if canvas is smaller than `MIN_SIZE`
|
||||
const renderX = x * Math.max(width, MIN_SIZE) + centerShift;
|
||||
const renderY = y * Math.max(height, MIN_SIZE) + centerShift;
|
||||
|
||||
const size = BASE_ICON_SIZE * sizeFactor * (centerShift ? 0.8 : 1);
|
||||
|
||||
ctx.globalAlpha = alpha;
|
||||
ctx.drawImage(emojiImage, renderX - size / 2, renderY - size / 2, size, size);
|
||||
});
|
||||
ctx.restore();
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = patternColor;
|
||||
ctx.globalCompositeOperation = 'source-atop';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
draw();
|
||||
}, [emojiImage]);
|
||||
|
||||
useSignalEffect(() => {
|
||||
const { width, height } = getContainerSize();
|
||||
const canvas = canvasRef.current!;
|
||||
if (!width || !height) {
|
||||
return;
|
||||
}
|
||||
|
||||
const maxSide = Math.max(width, height);
|
||||
const dpr = window.devicePixelRatio;
|
||||
requestMutation(() => {
|
||||
canvas.width = maxSide * dpr;
|
||||
canvas.height = maxSide * dpr;
|
||||
|
||||
draw();
|
||||
});
|
||||
}, [getContainerSize]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={buildClassName(styles.root, className)}
|
||||
style={buildStyle(
|
||||
`--_bg-1: ${backgroundColors[0]}`,
|
||||
`--_bg-2: ${backgroundColors[1] || backgroundColors[0]}`,
|
||||
)}
|
||||
>
|
||||
<canvas className={styles.canvas} ref={canvasRef} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(RadialPatternBackground);
|
||||
@ -21,7 +21,6 @@ import {
|
||||
selectGiftStickerForStars,
|
||||
selectIsCurrentUserPremium,
|
||||
selectIsMessageFocused,
|
||||
selectStarGiftSticker,
|
||||
selectTabState,
|
||||
selectTheme,
|
||||
selectTopicFromMessage,
|
||||
@ -81,7 +80,6 @@ type StateProps = {
|
||||
focusDirection?: FocusDirection;
|
||||
noFocusHighlight?: boolean;
|
||||
premiumGiftSticker?: ApiSticker;
|
||||
starGiftSticker?: ApiSticker;
|
||||
starsGiftSticker?: ApiSticker;
|
||||
canPlayAnimatedEmojis?: boolean;
|
||||
patternColor?: string;
|
||||
@ -108,7 +106,6 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
focusDirection,
|
||||
noFocusHighlight,
|
||||
premiumGiftSticker,
|
||||
starGiftSticker,
|
||||
starsGiftSticker,
|
||||
isInsideTopic,
|
||||
topic,
|
||||
@ -471,7 +468,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
|
||||
function renderStarGift() {
|
||||
const starGift = message.content.action?.starGift;
|
||||
if (!starGift) return undefined;
|
||||
if (!starGift || starGift.gift.type === 'starGiftUnique') return undefined;
|
||||
|
||||
return (
|
||||
<span
|
||||
@ -482,7 +479,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
|
||||
<AnimatedIconFromSticker
|
||||
sticker={starGiftSticker}
|
||||
sticker={starGift.gift.sticker}
|
||||
play={canPlayAnimatedEmojis}
|
||||
noLoop
|
||||
nonInteractive
|
||||
@ -522,7 +519,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
<AnimatedIconFromSticker
|
||||
key={message.id}
|
||||
sticker={starGiftSticker}
|
||||
sticker={starsGiftSticker}
|
||||
play={canPlayAnimatedEmojis}
|
||||
noLoop
|
||||
nonInteractive
|
||||
@ -642,9 +639,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const giftDuration = content.action?.months;
|
||||
const premiumGiftSticker = selectGiftStickerForDuration(global, giftDuration);
|
||||
|
||||
const starGift = content.action?.type === 'starGift' ? content.action.starGift?.gift : undefined;
|
||||
const starCount = content.action?.stars;
|
||||
const starGiftSticker = starGift?.stickerId ? selectStarGiftSticker(global, starGift.stickerId) : undefined;
|
||||
const starsGiftSticker = selectGiftStickerForStars(global, starCount);
|
||||
|
||||
const topic = selectTopicFromMessage(global, message);
|
||||
@ -658,7 +653,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
targetMessage,
|
||||
isFocused,
|
||||
premiumGiftSticker,
|
||||
starGiftSticker,
|
||||
starsGiftSticker,
|
||||
topic,
|
||||
patternColor,
|
||||
|
||||
@ -15,7 +15,7 @@ import Modal from '../../ui/Modal';
|
||||
|
||||
import styles from './TableInfoModal.module.scss';
|
||||
|
||||
type ChatItem = { chatId: string };
|
||||
type ChatItem = { chatId: string; withEmojiStatus?: boolean };
|
||||
|
||||
export type TableData = [TeactNode | undefined, TeactNode | ChatItem][];
|
||||
|
||||
@ -76,6 +76,7 @@ const TableInfoModal = ({
|
||||
className={styles.chatItem}
|
||||
forceShowSelf
|
||||
fluid
|
||||
withEmojiStatus={value.withEmojiStatus}
|
||||
clickArg={value.chatId}
|
||||
onClick={handleOpenChat}
|
||||
/>
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import React, { memo } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
import React, { memo, useRef } from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type {
|
||||
ApiStarGift,
|
||||
ApiSticker,
|
||||
ApiStarGiftRegular,
|
||||
} from '../../../api/types';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import { type ObserveFn, useOnIntersect } from '../../../hooks/useIntersectionObserver';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
@ -19,24 +20,27 @@ import Button from '../../ui/Button';
|
||||
import styles from './GiftItem.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
gift: ApiStarGift;
|
||||
onClick: (gift: ApiStarGift) => void;
|
||||
};
|
||||
|
||||
export type StateProps = {
|
||||
sticker?: ApiSticker;
|
||||
gift: ApiStarGiftRegular;
|
||||
observeIntersection?: ObserveFn;
|
||||
onClick: (gift: ApiStarGiftRegular) => void;
|
||||
};
|
||||
|
||||
const GIFT_STICKER_SIZE = 90;
|
||||
|
||||
function GiftItemStar({ sticker, gift, onClick }: OwnProps & StateProps) {
|
||||
function GiftItemStar({ gift, observeIntersection, onClick }: OwnProps) {
|
||||
const { openGiftInfoModal } = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const lang = useLang();
|
||||
const [shouldPlay, play] = useFlag();
|
||||
|
||||
const {
|
||||
stars,
|
||||
isLimited,
|
||||
isSoldOut,
|
||||
sticker,
|
||||
} = gift;
|
||||
|
||||
const handleGiftClick = useLastCallback(() => {
|
||||
@ -48,10 +52,13 @@ function GiftItemStar({ sticker, gift, onClick }: OwnProps & StateProps) {
|
||||
onClick(gift);
|
||||
});
|
||||
|
||||
if (!sticker) return undefined;
|
||||
useOnIntersect(ref, observeIntersection, (entry) => {
|
||||
if (entry.isIntersecting) play();
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={buildClassName(styles.container, styles.starGift)}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
@ -60,6 +67,7 @@ function GiftItemStar({ sticker, gift, onClick }: OwnProps & StateProps) {
|
||||
<AnimatedIconFromSticker
|
||||
sticker={sticker}
|
||||
noLoop
|
||||
play={shouldPlay}
|
||||
nonInteractive
|
||||
size={GIFT_STICKER_SIZE}
|
||||
/>
|
||||
@ -75,12 +83,4 @@ function GiftItemStar({ sticker, gift, onClick }: OwnProps & StateProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { gift }): StateProps => {
|
||||
const sticker = global.stickers.starGifts.stickers[gift.stickerId];
|
||||
|
||||
return {
|
||||
sticker,
|
||||
};
|
||||
},
|
||||
)(GiftItemStar));
|
||||
export default memo(GiftItemStar);
|
||||
|
||||
@ -6,7 +6,7 @@ import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type {
|
||||
ApiPremiumGiftCodeOption,
|
||||
ApiStarGift,
|
||||
ApiStarGiftRegular,
|
||||
ApiStarsAmount,
|
||||
ApiUser,
|
||||
} from '../../../api/types';
|
||||
@ -18,6 +18,7 @@ import { selectUser } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
@ -41,17 +42,19 @@ export type OwnProps = {
|
||||
modal: TabState['giftModal'];
|
||||
};
|
||||
|
||||
export type GiftOption = ApiPremiumGiftCodeOption | ApiStarGift;
|
||||
export type GiftOption = ApiPremiumGiftCodeOption | ApiStarGiftRegular;
|
||||
|
||||
type StateProps = {
|
||||
boostPerSentGift?: number;
|
||||
starGiftsById?: Record<string, ApiStarGift>;
|
||||
starGiftsById?: Record<string, ApiStarGiftRegular>;
|
||||
starGiftCategoriesByName: Record<StarGiftCategory, string[]>;
|
||||
starBalance?: ApiStarsAmount;
|
||||
user?: ApiUser;
|
||||
isSelf?: boolean;
|
||||
};
|
||||
|
||||
const AVATAR_SIZE = 100;
|
||||
const INTERSECTION_THROTTLE = 200;
|
||||
|
||||
const PremiumGiftModal: FC<OwnProps & StateProps> = ({
|
||||
modal,
|
||||
@ -59,6 +62,7 @@ const PremiumGiftModal: FC<OwnProps & StateProps> = ({
|
||||
starGiftCategoriesByName,
|
||||
starBalance,
|
||||
user,
|
||||
isSelf,
|
||||
}) => {
|
||||
const {
|
||||
closeGiftModal, requestConfetti,
|
||||
@ -70,6 +74,9 @@ const PremiumGiftModal: FC<OwnProps & StateProps> = ({
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const giftHeaderRef = useRef<HTMLHeadingElement>(null);
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const scrollerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isOpen = Boolean(modal);
|
||||
const renderingModal = useCurrentOrPrev(modal);
|
||||
|
||||
@ -91,6 +98,10 @@ const PremiumGiftModal: FC<OwnProps & StateProps> = ({
|
||||
return filteredGifts?.reduce((prev, gift) => (prev.amount < gift.amount ? prev : gift));
|
||||
}, [filteredGifts]);
|
||||
|
||||
const {
|
||||
observe: observeIntersection,
|
||||
} = useIntersectionObserver({ rootRef: scrollerRef, throttleMs: INTERSECTION_THROTTLE, isDisabled: !isOpen });
|
||||
|
||||
const showConfetti = useLastCallback(() => {
|
||||
const dialog = dialogRef.current;
|
||||
if (!dialog) return;
|
||||
@ -145,9 +156,14 @@ const PremiumGiftModal: FC<OwnProps & StateProps> = ({
|
||||
),
|
||||
}, { withNodes: true });
|
||||
|
||||
const starGiftDescription = lang('StarGiftDescription', {
|
||||
user: getUserFullName(user)!,
|
||||
}, { withNodes: true });
|
||||
const starGiftDescription = isSelf
|
||||
? lang('StarGiftDescriptionSelf', undefined, {
|
||||
withNodes: true,
|
||||
renderTextFilters: ['br'],
|
||||
})
|
||||
: lang('StarGiftDescription', {
|
||||
user: getUserFullName(user)!,
|
||||
}, { withNodes: true, withMarkdown: true });
|
||||
|
||||
function renderGiftPremiumHeader() {
|
||||
return (
|
||||
@ -168,7 +184,7 @@ const PremiumGiftModal: FC<OwnProps & StateProps> = ({
|
||||
function renderStarGiftsHeader() {
|
||||
return (
|
||||
<h2 ref={giftHeaderRef} className={buildClassName(styles.headerText, styles.center)}>
|
||||
{lang('StarsGiftHeader')}
|
||||
{lang(isSelf ? 'StarsGiftHeaderSelf' : 'StarsGiftHeader')}
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
@ -195,6 +211,7 @@ const PremiumGiftModal: FC<OwnProps & StateProps> = ({
|
||||
return (
|
||||
<GiftItemStar
|
||||
gift={gift}
|
||||
observeIntersection={observeIntersection}
|
||||
onClick={handleGiftClick}
|
||||
/>
|
||||
);
|
||||
@ -233,7 +250,7 @@ const PremiumGiftModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
function renderMainScreen() {
|
||||
return (
|
||||
<div className={buildClassName(styles.main, 'custom-scroll')} onScroll={handleScroll}>
|
||||
<div ref={scrollerRef} className={buildClassName(styles.main, 'custom-scroll')} onScroll={handleScroll}>
|
||||
<div className={styles.avatars}>
|
||||
<Avatar
|
||||
size={AVATAR_SIZE}
|
||||
@ -241,10 +258,9 @@ const PremiumGiftModal: FC<OwnProps & StateProps> = ({
|
||||
/>
|
||||
<img className={styles.logoBackground} src={StarsBackground} alt="" draggable={false} />
|
||||
</div>
|
||||
{renderGiftPremiumHeader()}
|
||||
{renderGiftPremiumDescription()}
|
||||
|
||||
{renderPremiumGifts()}
|
||||
{!isSelf && renderGiftPremiumHeader()}
|
||||
{!isSelf && renderGiftPremiumDescription()}
|
||||
{!isSelf && renderPremiumGifts()}
|
||||
|
||||
{renderStarGiftsHeader()}
|
||||
{renderStarGiftsDescription()}
|
||||
@ -294,7 +310,7 @@ const PremiumGiftModal: FC<OwnProps & StateProps> = ({
|
||||
slideClassName={styles.headerSlide}
|
||||
>
|
||||
<h2 className={styles.commonHeaderText}>
|
||||
{lang(isHeaderForStarGifts ? 'StarsGiftHeader' : 'GiftPremiumHeader')}
|
||||
{lang(isHeaderForStarGifts ? (isSelf ? 'StarsGiftHeaderSelf' : 'StarsGiftHeader') : 'GiftPremiumHeader')}
|
||||
</h2>
|
||||
</Transition>
|
||||
</div>
|
||||
@ -314,9 +330,15 @@ const PremiumGiftModal: FC<OwnProps & StateProps> = ({
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>((global, { modal }): StateProps => {
|
||||
const { starGiftsById, starGiftCategoriesByName, stars } = global;
|
||||
const {
|
||||
starGiftsById,
|
||||
starGiftCategoriesByName,
|
||||
stars,
|
||||
currentUserId,
|
||||
} = global;
|
||||
|
||||
const user = modal?.forUserId ? selectUser(global, modal.forUserId) : undefined;
|
||||
const isSelf = Boolean(currentUserId && modal?.forUserId === currentUserId);
|
||||
|
||||
return {
|
||||
boostPerSentGift: global.appConfig?.boostsPerSentGift,
|
||||
@ -324,15 +346,13 @@ export default memo(withGlobal<OwnProps>((global, { modal }): StateProps => {
|
||||
starGiftCategoriesByName,
|
||||
starBalance: stars?.balance,
|
||||
user,
|
||||
isSelf,
|
||||
};
|
||||
})(PremiumGiftModal));
|
||||
|
||||
function getCategoryKey(category: StarGiftCategory) {
|
||||
if (category === 'all') {
|
||||
return -1;
|
||||
}
|
||||
if (category === 'limited') {
|
||||
return 0;
|
||||
}
|
||||
if (category === 'all') return -2;
|
||||
if (category === 'stock') return -1;
|
||||
if (category === 'limited') return 0;
|
||||
return category;
|
||||
}
|
||||
|
||||
@ -46,12 +46,9 @@ const StarGiftCategoryList = ({
|
||||
}
|
||||
|
||||
function renderCategoryName(category: StarGiftCategory) {
|
||||
if (category === 'all') {
|
||||
return lang('AllGiftsCategory');
|
||||
}
|
||||
if (category === 'limited') {
|
||||
return lang('LimitedGiftsCategory');
|
||||
}
|
||||
if (category === 'all') return lang('AllGiftsCategory');
|
||||
if (category === 'stock') return lang('StockGiftsCategory');
|
||||
if (category === 'limited') return lang('LimitedGiftsCategory');
|
||||
return category;
|
||||
}
|
||||
|
||||
@ -64,7 +61,7 @@ const StarGiftCategoryList = ({
|
||||
)}
|
||||
onClick={() => handleItemClick(category)}
|
||||
>
|
||||
{category !== 'all' && category !== 'limited' && (
|
||||
{Number.isInteger(category) && (
|
||||
<StarIcon
|
||||
className={styles.star}
|
||||
type="gold"
|
||||
@ -81,6 +78,7 @@ const StarGiftCategoryList = ({
|
||||
return (
|
||||
<div ref={ref} className={buildClassName(styles.list, 'no-scrollbar')}>
|
||||
{renderCategoryItem('all')}
|
||||
{renderCategoryItem('stock')}
|
||||
{renderCategoryItem('limited')}
|
||||
{starCategories.map(renderCategoryItem)}
|
||||
</div>
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
.modal :global(.modal-dialog) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -25,6 +29,7 @@
|
||||
|
||||
.description {
|
||||
text-align: center;
|
||||
color: var(--_color-description, var(--color-text));
|
||||
}
|
||||
|
||||
.footerDescription {
|
||||
@ -44,3 +49,32 @@
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.radialPattern {
|
||||
position: absolute;
|
||||
top: -3rem;
|
||||
left: -1.5rem;
|
||||
right: -1.5rem;
|
||||
height: 16.5rem;
|
||||
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.uniqueAttribute {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.uniqueGift {
|
||||
gap: 0;
|
||||
|
||||
.giftSticker {
|
||||
margin-block: 1rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.25rem;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +1,22 @@
|
||||
import type { TeactNode } from '../../../../lib/teact/teact';
|
||||
import React, { memo, useMemo } from '../../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../../global';
|
||||
import { getActions, getGlobal, withGlobal } from '../../../../global';
|
||||
|
||||
import type { ApiSticker, ApiUser } from '../../../../api/types';
|
||||
import type {
|
||||
ApiUser,
|
||||
} from '../../../../api/types';
|
||||
import type { TabState } from '../../../../global/types';
|
||||
|
||||
import { getUserFullName } from '../../../../global/helpers';
|
||||
import { selectStarGiftSticker, selectUser } from '../../../../global/selectors';
|
||||
import { selectUser } from '../../../../global/selectors';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import buildStyle from '../../../../util/buildStyle';
|
||||
import { formatDateTimeToString } from '../../../../util/dates/dateFormat';
|
||||
import { formatStarsAsIcon, formatStarsAsText } from '../../../../util/localization/format';
|
||||
import { CUSTOM_PEER_HIDDEN } from '../../../../util/objects/customPeer';
|
||||
import { getServerTime } from '../../../../util/serverTime';
|
||||
import { formatInteger } from '../../../../util/textFormat';
|
||||
import { formatInteger, formatPercent } from '../../../../util/textFormat';
|
||||
import { getGiftAttributes, getStickerFromGift } from '../../../common/helpers/gifts';
|
||||
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
|
||||
|
||||
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
|
||||
@ -24,6 +29,7 @@ import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker';
|
||||
import Avatar from '../../../common/Avatar';
|
||||
import BadgeButton from '../../../common/BadgeButton';
|
||||
import StarIcon from '../../../common/icons/StarIcon';
|
||||
import RadialPatternBackground from '../../../common/profile/RadialPatternBackground';
|
||||
import Button from '../../../ui/Button';
|
||||
import ConfirmDialog from '../../../ui/ConfirmDialog';
|
||||
import Link from '../../../ui/Link';
|
||||
@ -36,7 +42,6 @@ export type OwnProps = {
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
sticker?: ApiSticker;
|
||||
userFrom?: ApiUser;
|
||||
targetUser?: ApiUser;
|
||||
currentUserId?: string;
|
||||
@ -46,7 +51,7 @@ type StateProps = {
|
||||
const STICKER_SIZE = 120;
|
||||
|
||||
const GiftInfoModal = ({
|
||||
modal, sticker, userFrom, targetUser, currentUserId, starGiftMaxConvertPeriod,
|
||||
modal, userFrom, targetUser, currentUserId, starGiftMaxConvertPeriod,
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
closeGiftInfoModal,
|
||||
@ -65,13 +70,16 @@ const GiftInfoModal = ({
|
||||
const { gift: typeGift } = renderingModal || {};
|
||||
const isUserGift = typeGift && 'gift' in typeGift;
|
||||
const userGift = isUserGift ? typeGift : undefined;
|
||||
const canUpdate = Boolean(userGift?.fromId && userGift.messageId);
|
||||
const canUpdate = Boolean(userGift?.messageId);
|
||||
const isSender = userGift?.fromId === currentUserId;
|
||||
const canConvertDifference = (userGift && starGiftMaxConvertPeriod && (
|
||||
userGift.date + starGiftMaxConvertPeriod - getServerTime()
|
||||
)) || 0;
|
||||
const conversionLeft = Math.ceil(canConvertDifference / 60 / 60 / 24);
|
||||
|
||||
const gift = isUserGift ? typeGift.gift : typeGift;
|
||||
const giftSticker = gift && getStickerFromGift(gift);
|
||||
|
||||
const handleClose = useLastCallback(() => {
|
||||
closeGiftInfoModal();
|
||||
});
|
||||
@ -94,15 +102,38 @@ const GiftInfoModal = ({
|
||||
handleClose();
|
||||
});
|
||||
|
||||
const giftAttributes = useMemo(() => {
|
||||
return gift && getGiftAttributes(gift);
|
||||
}, [gift]);
|
||||
|
||||
const radialPatternBackdrop = useMemo(() => {
|
||||
const { backdrop, pattern } = giftAttributes || {};
|
||||
|
||||
if (!backdrop || !pattern || !isOpen) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const backdropColors = [backdrop.centerColor, backdrop.edgeColor];
|
||||
const patternColor = backdrop.patternColor;
|
||||
|
||||
return (
|
||||
<RadialPatternBackground
|
||||
className={styles.radialPattern}
|
||||
backgroundColors={backdropColors}
|
||||
patternColor={patternColor}
|
||||
patternIcon={pattern.sticker}
|
||||
/>
|
||||
);
|
||||
}, [giftAttributes, isOpen]);
|
||||
|
||||
const modalData = useMemo(() => {
|
||||
if (!typeGift) {
|
||||
if (!typeGift || !gift) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const {
|
||||
fromId, isNameHidden, message, starsToConvert, isUnsaved, isConverted,
|
||||
fromId, isNameHidden, starsToConvert, isUnsaved, isConverted,
|
||||
} = userGift || {};
|
||||
const gift = isUserGift ? typeGift.gift : typeGift;
|
||||
|
||||
const isVisibleForMe = isNameHidden && targetUser;
|
||||
|
||||
@ -110,6 +141,11 @@ const GiftInfoModal = ({
|
||||
if (!userGift) {
|
||||
return lang('GiftInfoSoldOutDescription');
|
||||
}
|
||||
if (gift.type === 'starGiftUnique') {
|
||||
return lang('GiftInfoCollectible', {
|
||||
number: gift.number,
|
||||
});
|
||||
}
|
||||
if (!canUpdate && !isSender) return undefined;
|
||||
if (!starsToConvert || canConvertDifference < 0) return undefined;
|
||||
if (isConverted) {
|
||||
@ -149,14 +185,32 @@ const GiftInfoModal = ({
|
||||
});
|
||||
})();
|
||||
|
||||
function getTitle() {
|
||||
if (!userGift) return lang('GiftInfoSoldOutTitle');
|
||||
if (gift?.type === 'starGiftUnique') return gift.title;
|
||||
|
||||
return canUpdate ? lang('GiftInfoReceived') : lang('GiftInfoTitle');
|
||||
}
|
||||
|
||||
const descriptionColor = giftAttributes?.backdrop?.textColor;
|
||||
|
||||
const header = (
|
||||
<div className={styles.header}>
|
||||
<AnimatedIconFromSticker sticker={sticker} noLoop nonInteractive size={STICKER_SIZE} />
|
||||
<div
|
||||
className={buildClassName(styles.header, radialPatternBackdrop && styles.uniqueGift)}
|
||||
style={buildStyle(descriptionColor && `--_color-description: ${descriptionColor}`)}
|
||||
>
|
||||
{radialPatternBackdrop}
|
||||
<AnimatedIconFromSticker
|
||||
className={styles.giftSticker}
|
||||
sticker={giftSticker}
|
||||
noLoop
|
||||
nonInteractive
|
||||
size={STICKER_SIZE}
|
||||
/>
|
||||
<h1 className={styles.title}>
|
||||
{!userGift && lang('GiftInfoSoldOutTitle')}
|
||||
{userGift && lang(canUpdate ? 'GiftInfoReceived' : 'GiftInfoTitle')}
|
||||
{getTitle()}
|
||||
</h1>
|
||||
{userGift && (
|
||||
{gift.type === 'starGift' && (
|
||||
<p className={styles.amount}>
|
||||
<span className={styles.amount}>
|
||||
{formatInteger(gift.stars)}
|
||||
@ -173,68 +227,187 @@ const GiftInfoModal = ({
|
||||
);
|
||||
|
||||
const tableData: TableData = [];
|
||||
if (fromId || isNameHidden) {
|
||||
if (gift.type === 'starGift') {
|
||||
if ((fromId || isNameHidden)) {
|
||||
tableData.push([
|
||||
lang('GiftInfoFrom'),
|
||||
fromId ? { chatId: fromId } : (
|
||||
<>
|
||||
<Avatar size="small" peer={CUSTOM_PEER_HIDDEN} />
|
||||
<span className={styles.unknown}>{oldLang(CUSTOM_PEER_HIDDEN.titleKey!)}</span>
|
||||
</>
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
if (userGift?.date) {
|
||||
tableData.push([
|
||||
lang('GiftInfoDate'),
|
||||
formatDateTimeToString(userGift.date * 1000, lang.code, true),
|
||||
]);
|
||||
}
|
||||
|
||||
if (gift.firstSaleDate) {
|
||||
tableData.push([
|
||||
lang('GiftInfoFirstSale'),
|
||||
formatDateTimeToString(gift.firstSaleDate * 1000, lang.code, true),
|
||||
]);
|
||||
}
|
||||
|
||||
if (gift.lastSaleDate) {
|
||||
tableData.push([
|
||||
lang('GiftInfoLastSale'),
|
||||
formatDateTimeToString(gift.lastSaleDate * 1000, lang.code, true),
|
||||
]);
|
||||
}
|
||||
|
||||
tableData.push([
|
||||
lang('GiftInfoFrom'),
|
||||
fromId ? { chatId: fromId } : (
|
||||
<>
|
||||
<Avatar size="small" peer={CUSTOM_PEER_HIDDEN} />
|
||||
<span className={styles.unknown}>{oldLang(CUSTOM_PEER_HIDDEN.titleKey!)}</span>
|
||||
</>
|
||||
),
|
||||
lang('GiftInfoValue'),
|
||||
<div className={styles.giftValue}>
|
||||
{formatStarsAsIcon(lang, gift.stars)}
|
||||
{canUpdate && canConvertDifference > 0 && Boolean(starsToConvert) && (
|
||||
<BadgeButton onClick={openConvertConfirm}>
|
||||
{lang('GiftInfoConvert', { amount: starsToConvert }, { pluralValue: starsToConvert })}
|
||||
</BadgeButton>
|
||||
)}
|
||||
</div>,
|
||||
]);
|
||||
|
||||
if (gift.availabilityTotal) {
|
||||
tableData.push([
|
||||
lang('GiftInfoAvailability'),
|
||||
lang('GiftInfoAvailabilityValue', {
|
||||
count: gift.availabilityRemains || 0,
|
||||
total: gift.availabilityTotal,
|
||||
}, {
|
||||
pluralValue: gift.availabilityRemains || 0,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
if (gift.upgradeStars) {
|
||||
tableData.push([
|
||||
lang('GiftInfoStatus'),
|
||||
lang('GiftInfoStatusNonUnique'),
|
||||
]);
|
||||
}
|
||||
|
||||
if (userGift?.message) {
|
||||
tableData.push([
|
||||
undefined,
|
||||
renderTextWithEntities(userGift.message),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (userGift?.date) {
|
||||
if (gift.type === 'starGiftUnique') {
|
||||
const {
|
||||
model, backdrop, pattern, originalDetails,
|
||||
} = giftAttributes || {};
|
||||
tableData.push([
|
||||
lang('GiftInfoDate'),
|
||||
formatDateTimeToString(userGift.date * 1000, lang.code, true),
|
||||
lang('GiftInfoOwner'),
|
||||
{ chatId: gift.ownerId },
|
||||
]);
|
||||
}
|
||||
|
||||
if (gift.firstSaleDate) {
|
||||
tableData.push([
|
||||
lang('GiftInfoFirstSale'),
|
||||
formatDateTimeToString(gift.firstSaleDate * 1000, lang.code, true),
|
||||
]);
|
||||
}
|
||||
if (model) {
|
||||
tableData.push([
|
||||
lang('GiftAttributeModel'),
|
||||
<span className={styles.uniqueAttribute}>
|
||||
{model.name}<BadgeButton>{formatPercent(model.rarityPercent)}</BadgeButton>
|
||||
</span>,
|
||||
]);
|
||||
}
|
||||
|
||||
if (gift.lastSaleDate) {
|
||||
tableData.push([
|
||||
lang('GiftInfoLastSale'),
|
||||
formatDateTimeToString(gift.lastSaleDate * 1000, lang.code, true),
|
||||
]);
|
||||
}
|
||||
if (backdrop) {
|
||||
tableData.push([
|
||||
lang('GiftAttributeBackdrop'),
|
||||
<span className={styles.uniqueAttribute}>
|
||||
{backdrop.name}<BadgeButton>{formatPercent(backdrop.rarityPercent)}</BadgeButton>
|
||||
</span>,
|
||||
]);
|
||||
}
|
||||
|
||||
tableData.push([
|
||||
lang('GiftInfoValue'),
|
||||
<div className={styles.giftValue}>
|
||||
{formatStarsAsIcon(lang, gift.stars)}
|
||||
{canUpdate && canConvertDifference > 0 && Boolean(starsToConvert) && (
|
||||
<BadgeButton onClick={openConvertConfirm}>
|
||||
{lang('GiftInfoConvert', { amount: starsToConvert }, { pluralValue: starsToConvert })}
|
||||
</BadgeButton>
|
||||
)}
|
||||
</div>,
|
||||
]);
|
||||
if (pattern) {
|
||||
tableData.push([
|
||||
lang('GiftAttributeSymbol'),
|
||||
<span className={styles.uniqueAttribute}>
|
||||
{pattern.name}<BadgeButton>{formatPercent(pattern.rarityPercent)}</BadgeButton>
|
||||
</span>,
|
||||
]);
|
||||
}
|
||||
|
||||
if (gift.availabilityTotal) {
|
||||
tableData.push([
|
||||
lang('GiftInfoAvailability'),
|
||||
lang('GiftInfoAvailabilityValue', {
|
||||
count: gift.availabilityRemains || 0,
|
||||
total: gift.availabilityTotal,
|
||||
}, {
|
||||
pluralValue: gift.availabilityRemains || 0,
|
||||
lang('GiftInfoIssued', {
|
||||
issued: gift.issuedCount,
|
||||
total: gift.totalCount,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
if (message) {
|
||||
tableData.push([
|
||||
undefined,
|
||||
renderTextWithEntities(message),
|
||||
]);
|
||||
if (originalDetails) {
|
||||
const {
|
||||
date, recipientId, message, senderId,
|
||||
} = originalDetails;
|
||||
const global = getGlobal(); // User names does not need to be reactive
|
||||
|
||||
const openChat = (id: string) => {
|
||||
openChatWithInfo({ id });
|
||||
closeGiftInfoModal();
|
||||
};
|
||||
|
||||
const recipient = selectUser(global, recipientId)!;
|
||||
const sender = senderId ? selectUser(global, senderId) : undefined;
|
||||
|
||||
const formattedDate = formatDateTimeToString(date * 1000, lang.code, true);
|
||||
const recipientLink = (
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
<Link onClick={() => openChat(recipientId)} isPrimary>
|
||||
{getUserFullName(recipient)}
|
||||
</Link>
|
||||
);
|
||||
|
||||
let text: TeactNode | undefined;
|
||||
if (!sender || senderId === recipientId) {
|
||||
text = message ? lang('GiftInfoOriginalInfoText', {
|
||||
user: recipientLink,
|
||||
text: renderTextWithEntities(message),
|
||||
date: formattedDate,
|
||||
}, {
|
||||
withNodes: true,
|
||||
}) : lang('GiftInfoOriginalInfo', {
|
||||
user: recipientLink,
|
||||
date: formattedDate,
|
||||
}, {
|
||||
withNodes: true,
|
||||
});
|
||||
} else {
|
||||
const senderLink = (
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
<Link onClick={() => openChat(sender.id)} isPrimary>
|
||||
{getUserFullName(sender)}
|
||||
</Link>
|
||||
);
|
||||
text = message ? lang('GiftInfoOriginalInfoTextSender', {
|
||||
user: recipientLink,
|
||||
sender: senderLink,
|
||||
text: renderTextWithEntities(message),
|
||||
date: formattedDate,
|
||||
}, {
|
||||
withNodes: true,
|
||||
}) : lang('GiftInfoOriginalInfoSender', {
|
||||
user: recipientLink,
|
||||
date: formattedDate,
|
||||
sender: senderLink,
|
||||
}, {
|
||||
withNodes: true,
|
||||
});
|
||||
}
|
||||
|
||||
tableData.push([
|
||||
undefined,
|
||||
<span>{text}</span>,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const footer = (
|
||||
@ -274,7 +447,10 @@ const GiftInfoModal = ({
|
||||
tableData,
|
||||
footer,
|
||||
};
|
||||
}, [typeGift, userGift, isUserGift, targetUser, sticker, lang, canUpdate, canConvertDifference, isSender, oldLang]);
|
||||
}, [
|
||||
typeGift, userGift, targetUser, giftSticker, lang, canUpdate, canConvertDifference, isSender, oldLang, gift,
|
||||
radialPatternBackdrop, giftAttributes,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -283,6 +459,7 @@ const GiftInfoModal = ({
|
||||
header={modalData?.header}
|
||||
tableData={modalData?.tableData}
|
||||
footer={modalData?.footer}
|
||||
className={styles.modal}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
{userGift && (
|
||||
@ -323,16 +500,12 @@ export default memo(withGlobal<OwnProps>(
|
||||
(global, { modal }): StateProps => {
|
||||
const typeGift = modal?.gift;
|
||||
const isUserGift = typeGift && 'gift' in typeGift;
|
||||
const gift = isUserGift ? typeGift.gift : typeGift;
|
||||
const stickerId = gift?.stickerId;
|
||||
const sticker = stickerId ? selectStarGiftSticker(global, stickerId) : undefined;
|
||||
|
||||
const fromId = isUserGift && typeGift.fromId;
|
||||
const userFrom = fromId ? selectUser(global, fromId) : undefined;
|
||||
const targetUser = modal?.userId ? selectUser(global, modal.userId) : undefined;
|
||||
|
||||
return {
|
||||
sticker,
|
||||
userFrom,
|
||||
targetUser,
|
||||
currentUserId: global.currentUserId,
|
||||
|
||||
@ -42,7 +42,8 @@ const GiftRecipientPicker: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const displayedUserIds = useMemo(() => {
|
||||
const usersById = getGlobal().users.byId;
|
||||
const filteredContactIds = userIds ? filterUsersByName(userIds, usersById, searchQuery) : [];
|
||||
const idsWithSelf = userIds ? userIds.concat(currentUserId!) : undefined;
|
||||
const filteredContactIds = idsWithSelf ? filterUsersByName(idsWithSelf, usersById, searchQuery) : [];
|
||||
|
||||
return sortChatIds(unique(filteredContactIds).filter((userId) => {
|
||||
const user = usersById[userId];
|
||||
@ -50,8 +51,8 @@ const GiftRecipientPicker: FC<OwnProps & StateProps> = ({
|
||||
return true;
|
||||
}
|
||||
|
||||
return !isUserBot(user) && userId !== currentUserId;
|
||||
}));
|
||||
return !isUserBot(user);
|
||||
}), undefined, [currentUserId!]);
|
||||
}, [currentUserId, searchQuery, userIds]);
|
||||
|
||||
const handleSelectedUserIdsChange = useLastCallback((selectedId: string) => {
|
||||
@ -79,6 +80,7 @@ const GiftRecipientPicker: FC<OwnProps & StateProps> = ({
|
||||
isSearchable
|
||||
withDefaultPadding
|
||||
withStatus
|
||||
forceShowSelf
|
||||
/>
|
||||
</PickerModal>
|
||||
);
|
||||
|
||||
@ -8,6 +8,7 @@ export function getTransactionTitle(lang: OldLangFn, transaction: ApiStarsTransa
|
||||
if (transaction.starRefCommision) {
|
||||
return lang('StarTransactionCommission', formatPercent(transaction.starRefCommision));
|
||||
}
|
||||
if (transaction.isGiftUpgrade) return lang('Gift2TransactionUpgraded');
|
||||
if (transaction.extendedMedia) return lang('StarMediaPurchase');
|
||||
if (transaction.subscriptionPeriod) return transaction.title || lang('StarSubscriptionPurchase');
|
||||
if (transaction.isReaction) return lang('StarsReactionsSent');
|
||||
|
||||
@ -71,6 +71,10 @@ const StarsTransactionItem = ({ transaction, className }: OwnProps) => {
|
||||
avatarPeer = customPeer;
|
||||
}
|
||||
|
||||
if (transaction.isGiftUpgrade && transaction.starGift?.type === 'starGiftUnique') {
|
||||
description = transaction.starGift.title;
|
||||
}
|
||||
|
||||
if (transaction.photo) {
|
||||
avatarPeer = undefined;
|
||||
}
|
||||
|
||||
@ -14,11 +14,12 @@ import { buildStarsTransactionCustomPeer, formatStarsTransactionAmount } from '.
|
||||
import {
|
||||
selectCanPlayAnimatedEmojis,
|
||||
selectGiftStickerForStars,
|
||||
selectPeer, selectStarGiftSticker,
|
||||
selectPeer,
|
||||
} from '../../../../global/selectors';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { copyTextToClipboard } from '../../../../util/clipboard';
|
||||
import { formatDateTimeToString } from '../../../../util/dates/dateFormat';
|
||||
import { getStickerFromGift } from '../../../common/helpers/gifts';
|
||||
import { getTransactionTitle, isNegativeStarsAmount } from '../helpers/transaction';
|
||||
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
@ -57,6 +58,8 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
const oldLang = useOldLang();
|
||||
const { transaction } = modal || {};
|
||||
|
||||
const sticker = transaction?.starGift ? getStickerFromGift(transaction.starGift) : topSticker;
|
||||
|
||||
const handleOpenMedia = useLastCallback(() => {
|
||||
const media = transaction?.extendedMedia;
|
||||
if (!media) return;
|
||||
@ -73,7 +76,7 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
const {
|
||||
giveawayPostId, photo, stars,
|
||||
giveawayPostId, photo, stars, isGiftUpgrade, starGift,
|
||||
} = transaction;
|
||||
|
||||
const customPeer = (transaction.peer && transaction.peer.type !== 'peer'
|
||||
@ -84,7 +87,7 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const title = getTransactionTitle(oldLang, transaction);
|
||||
|
||||
const messageLink = peer && transaction.messageId
|
||||
const messageLink = peer && transaction.messageId && !isGiftUpgrade
|
||||
? getMessageLink(peer, undefined, transaction.messageId) : undefined;
|
||||
const giveawayMessageLink = peer && giveawayPostId && getMessageLink(peer, undefined, giveawayPostId);
|
||||
|
||||
@ -98,9 +101,11 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
: areAllVideos ? oldLang('Stars.Transfer.Videos', mediaAmount)
|
||||
: oldLang('Media', mediaAmount);
|
||||
|
||||
const description = transaction.description || (media ? mediaText : undefined);
|
||||
const description = transaction.description
|
||||
|| (isGiftUpgrade && starGift?.type === 'starGiftUnique' ? starGift.title : undefined)
|
||||
|| (media ? mediaText : undefined);
|
||||
|
||||
const shouldDisplayAvatar = !media && !topSticker;
|
||||
const shouldDisplayAvatar = !media && !sticker;
|
||||
const avatarPeer = !photo ? (peer || customPeer) : undefined;
|
||||
|
||||
const header = (
|
||||
@ -112,10 +117,10 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
onClick={handleOpenMedia}
|
||||
/>
|
||||
)}
|
||||
{!media && topSticker && (
|
||||
{!media && sticker && (
|
||||
<AnimatedIconFromSticker
|
||||
key={transaction.id}
|
||||
sticker={topSticker}
|
||||
sticker={sticker}
|
||||
play={canPlayAnimatedEmojis}
|
||||
noLoop
|
||||
nonInteractive
|
||||
@ -124,7 +129,7 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
{shouldDisplayAvatar && (
|
||||
<Avatar peer={avatarPeer} webPhoto={photo} size="giant" />
|
||||
)}
|
||||
{!topSticker && (
|
||||
{!sticker && (
|
||||
<img
|
||||
className={buildClassName(styles.starsBackground)}
|
||||
src={StarsBackground}
|
||||
@ -154,8 +159,17 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
]);
|
||||
}
|
||||
|
||||
if (isGiftUpgrade) {
|
||||
tableData.push([
|
||||
oldLang('StarGiftReason'),
|
||||
oldLang('StarGiftReasonUpgrade'),
|
||||
]);
|
||||
}
|
||||
|
||||
let peerLabel;
|
||||
if (isNegativeStarsAmount(stars) || transaction.isMyGift) {
|
||||
if (isGiftUpgrade) {
|
||||
peerLabel = oldLang('Stars.Transaction.GiftFrom');
|
||||
} else if (isNegativeStarsAmount(stars) || transaction.isMyGift) {
|
||||
peerLabel = oldLang('Stars.Transaction.To');
|
||||
} else if (transaction.starRefCommision) {
|
||||
peerLabel = oldLang('StarsTransaction.StarRefReason.Miniapp');
|
||||
@ -222,7 +236,7 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
tableData,
|
||||
footer,
|
||||
};
|
||||
}, [transaction, oldLang, lang, peer, topSticker, canPlayAnimatedEmojis]);
|
||||
}, [transaction, oldLang, lang, peer, sticker, canPlayAnimatedEmojis]);
|
||||
|
||||
const prevModalData = usePrevious(starModalData);
|
||||
const renderingModalData = prevModalData || starModalData;
|
||||
@ -248,13 +262,10 @@ export default memo(withGlobal<OwnProps>(
|
||||
const starCount = modal?.transaction.stars;
|
||||
const starsGiftSticker = modal?.transaction.isGift && selectGiftStickerForStars(global, starCount?.amount);
|
||||
|
||||
const starGiftStickerId = modal?.transaction.starGift?.stickerId;
|
||||
const starGiftSticker = starGiftStickerId && selectStarGiftSticker(global, starGiftStickerId);
|
||||
|
||||
return {
|
||||
peer,
|
||||
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
|
||||
topSticker: starGiftSticker || starsGiftSticker,
|
||||
topSticker: starsGiftSticker,
|
||||
};
|
||||
},
|
||||
)(StarsTransactionModal));
|
||||
|
||||
@ -123,6 +123,7 @@ const SuggestedStatusModal = ({ modal, currentUser, bot }: OwnProps & StateProps
|
||||
{mockPeerWithStatus && (
|
||||
<PeerChip
|
||||
mockPeer={mockPeerWithStatus}
|
||||
withEmojiStatus
|
||||
/>
|
||||
)}
|
||||
<Button size="smaller" onClick={handleSetStatus}>
|
||||
|
||||
@ -713,7 +713,12 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
</div>
|
||||
) : resultType === 'gifts' ? (
|
||||
(gifts?.map((gift) => (
|
||||
<UserGift userId={chatId} key={`${gift.date}-${gift.fromId}-${gift.gift.id}`} gift={gift} />
|
||||
<UserGift
|
||||
userId={chatId}
|
||||
key={`${gift.date}-${gift.fromId}-${gift.gift.id}`}
|
||||
gift={gift}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
/>
|
||||
)))
|
||||
) : undefined}
|
||||
</div>
|
||||
|
||||
@ -89,26 +89,27 @@ addActionHandler('loadStarGifts', async (global): Promise<void> => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { gifts, stickers } = result;
|
||||
|
||||
const starGiftsById = buildCollectionByKey(gifts, 'id');
|
||||
const starGiftsById = buildCollectionByKey(result, 'id');
|
||||
|
||||
const starGiftCategoriesByName: Record<StarGiftCategory, string[]> = {
|
||||
all: [],
|
||||
stock: [],
|
||||
limited: [],
|
||||
};
|
||||
|
||||
const allStarGiftIds = Object.keys(starGiftsById);
|
||||
const allStarGifts = Object.values(starGiftsById);
|
||||
|
||||
const limitedStarGiftIds = allStarGifts.map(
|
||||
(gift) => {
|
||||
return gift.isLimited ? gift.id : undefined;
|
||||
},
|
||||
).filter(Boolean) as string[];
|
||||
const limitedStarGiftIds = allStarGifts.map((gift) => (gift.isLimited ? gift.id : undefined))
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const stockedStarGiftIds = allStarGifts.map((gift) => (
|
||||
gift.availabilityRemains || !gift.availabilityTotal ? gift.id : undefined
|
||||
)).filter(Boolean) as string[];
|
||||
|
||||
starGiftCategoriesByName.all = allStarGiftIds;
|
||||
starGiftCategoriesByName.limited = limitedStarGiftIds;
|
||||
starGiftCategoriesByName.stock = stockedStarGiftIds;
|
||||
|
||||
allStarGifts.forEach((gift) => {
|
||||
const starsCategory = gift.stars;
|
||||
@ -123,12 +124,6 @@ addActionHandler('loadStarGifts', async (global): Promise<void> => {
|
||||
...global,
|
||||
starGiftsById,
|
||||
starGiftCategoriesByName,
|
||||
stickers: {
|
||||
...global.stickers,
|
||||
starGifts: {
|
||||
stickers,
|
||||
},
|
||||
},
|
||||
};
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
@ -260,11 +260,6 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
|
||||
if (!cached.messages.pollById) {
|
||||
cached.messages.pollById = initialState.messages.pollById;
|
||||
}
|
||||
|
||||
if (!cached.stickers.starGifts) {
|
||||
cached.stickers.starGifts = initialState.stickers.starGifts;
|
||||
cached.users.giftsById = initialState.users.giftsById;
|
||||
}
|
||||
}
|
||||
|
||||
function updateCache(force?: boolean) {
|
||||
|
||||
@ -183,6 +183,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
starGiftCategoriesByName: {
|
||||
all: [],
|
||||
limited: [],
|
||||
stock: [],
|
||||
},
|
||||
|
||||
stickers: {
|
||||
@ -207,9 +208,6 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
stickers: [],
|
||||
emojis: [],
|
||||
},
|
||||
starGifts: {
|
||||
stickers: {},
|
||||
},
|
||||
forEmoji: {},
|
||||
},
|
||||
|
||||
|
||||
@ -171,10 +171,6 @@ export function selectIsAlwaysHighPriorityEmoji<T extends GlobalState>(
|
||||
|| stickerSet.id === RESTRICTED_EMOJI_SET_ID;
|
||||
}
|
||||
|
||||
export function selectStarGiftSticker<T extends GlobalState>(global: T, id: string) {
|
||||
return global.stickers.starGifts.stickers[id];
|
||||
}
|
||||
|
||||
export function selectGiftStickerForDuration<T extends GlobalState>(global: T, duration = 1) {
|
||||
const stickers = global.premiumGifts?.stickers;
|
||||
if (!stickers) return undefined;
|
||||
|
||||
@ -27,7 +27,7 @@ import type {
|
||||
ApiSavedReactionTag,
|
||||
ApiSession,
|
||||
ApiSponsoredMessage,
|
||||
ApiStarGift,
|
||||
ApiStarGiftRegular,
|
||||
ApiStarsAmount,
|
||||
ApiStarTopupOption,
|
||||
ApiStealthMode,
|
||||
@ -290,7 +290,7 @@ export type GlobalState = {
|
||||
};
|
||||
};
|
||||
availableEffectById: Record<string, ApiAvailableEffect>;
|
||||
starGiftsById: Record<string, ApiStarGift>;
|
||||
starGiftsById: Record<string, ApiStarGiftRegular>;
|
||||
starGiftCategoriesByName: Record<StarGiftCategory, string[]>;
|
||||
|
||||
stickers: {
|
||||
@ -328,9 +328,6 @@ export type GlobalState = {
|
||||
stickers: ApiSticker[];
|
||||
emojis: ApiSticker[];
|
||||
};
|
||||
starGifts: {
|
||||
stickers: Record<string, ApiSticker>;
|
||||
};
|
||||
};
|
||||
|
||||
customEmojis: {
|
||||
|
||||
@ -17,7 +17,7 @@ import type {
|
||||
ApiPhoto,
|
||||
ApiReaction,
|
||||
ApiReactionWithPaid,
|
||||
ApiStarGift,
|
||||
ApiStarGiftRegular,
|
||||
ApiStarsSubscription,
|
||||
ApiStarsTransaction,
|
||||
ApiStickerSet,
|
||||
@ -561,7 +561,7 @@ export type ConfettiStyle = 'poppers' | 'top-down';
|
||||
|
||||
export type StarGiftInfo = {
|
||||
userId: string;
|
||||
gift: ApiStarGift;
|
||||
gift: ApiStarGiftRegular;
|
||||
shouldHideName?: boolean;
|
||||
message?: ApiFormattedText;
|
||||
};
|
||||
@ -631,7 +631,7 @@ export type ConfettiParams = OptionalCombine<{
|
||||
|
||||
export type WebPageMediaSize = 'large' | 'small';
|
||||
|
||||
export type StarGiftCategory = number | 'all' | 'limited';
|
||||
export type StarGiftCategory = number | 'all' | 'limited' | 'stock';
|
||||
|
||||
export type CallSound = (
|
||||
'join' | 'allowTalk' | 'leave' | 'connecting' | 'incoming' | 'end' | 'connect' | 'busy' | 'ringing'
|
||||
|
||||
36
src/types/language.d.ts
vendored
36
src/types/language.d.ts
vendored
@ -1108,6 +1108,8 @@ export interface LangPair {
|
||||
'GiftPremiumDescriptionLinkCaption': undefined;
|
||||
'GiftPremiumDescriptionLink': undefined;
|
||||
'StarsGiftHeader': undefined;
|
||||
'StarsGiftHeaderSelf': undefined;
|
||||
'StarGiftDescriptionSelf': undefined;
|
||||
'GiftLimited': undefined;
|
||||
'GiftSoldOut': undefined;
|
||||
'GiftMessagePlaceholder': undefined;
|
||||
@ -1130,8 +1132,15 @@ export interface LangPair {
|
||||
'GiftInfoSoldOutTitle': undefined;
|
||||
'GiftInfoSoldOutDescription': undefined;
|
||||
'GiftInfoSenderHidden': undefined;
|
||||
'GiftInfoOwner': undefined;
|
||||
'GiftAttributeModel': undefined;
|
||||
'GiftAttributeBackdrop': undefined;
|
||||
'GiftAttributeSymbol': undefined;
|
||||
'GiftInfoStatus': undefined;
|
||||
'GiftInfoStatusNonUnique': undefined;
|
||||
'AllGiftsCategory': undefined;
|
||||
'LimitedGiftsCategory': undefined;
|
||||
'StockGiftsCategory': undefined;
|
||||
'PremiumGiftDescription': undefined;
|
||||
'StarsReactionLinkText': undefined;
|
||||
'StarsReactionLink': undefined;
|
||||
@ -1547,6 +1556,33 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
|
||||
'GiftInfoSaved': {
|
||||
'link': V;
|
||||
};
|
||||
'GiftInfoIssued': {
|
||||
'issued': V;
|
||||
'total': V;
|
||||
};
|
||||
'GiftInfoCollectible': {
|
||||
'number': V;
|
||||
};
|
||||
'GiftInfoOriginalInfo': {
|
||||
'user': V;
|
||||
'date': V;
|
||||
};
|
||||
'GiftInfoOriginalInfoSender': {
|
||||
'sender': V;
|
||||
'user': V;
|
||||
'date': V;
|
||||
};
|
||||
'GiftInfoOriginalInfoText': {
|
||||
'user': V;
|
||||
'date': V;
|
||||
'text': V;
|
||||
};
|
||||
'GiftInfoOriginalInfoTextSender': {
|
||||
'sender': V;
|
||||
'user': V;
|
||||
'date': V;
|
||||
'text': V;
|
||||
};
|
||||
'StarsAmount': {
|
||||
'amount': V;
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user