Layer 181: Telegram Stars, Collapsible quotes, Fact Check (#4637)

This commit is contained in:
zubiden 2024-06-14 14:22:19 +02:00 committed by Alexander Zinchuk
parent 9034a66c9c
commit 46c85ebb88
159 changed files with 3368 additions and 662 deletions

View File

@ -260,6 +260,15 @@ export function buildApiMessageEntity(entity: GramJs.TypeMessageEntity): ApiMess
};
}
if (entity instanceof GramJs.MessageEntityBlockquote) {
return {
type: ApiMessageEntityTypes.Blockquote,
canCollapse: entity.collapsed,
offset,
length,
};
}
return {
type: type as `${ApiMessageEntityDefault['type']}`,
offset,

View File

@ -6,6 +6,7 @@ import type {
ApiAttachment,
ApiChat,
ApiContact,
ApiFactCheck,
ApiGroupCall,
ApiInputMessageReplyInfo,
ApiInputReplyInfo,
@ -52,6 +53,7 @@ import {
} from '../helpers';
import { buildApiCallDiscardReason } from './calls';
import {
buildApiFormattedText,
buildApiPhoto,
} from './common';
import { buildMessageContent, buildMessageMediaContent, buildMessageTextContent } from './messageContent';
@ -149,12 +151,7 @@ export function buildApiMessageFromNotification(
export type UniversalMessage = (
Pick<GramJs.Message & GramJs.MessageService, ('id' | 'date')>
& Pick<Partial<GramJs.Message & GramJs.MessageService>, (
'out' | 'message' | 'entities' | 'fromId' | 'peerId' | 'fwdFrom' | 'replyTo' | 'replyMarkup' | 'post' |
'media' | 'action' | 'views' | 'editDate' | 'editHide' | 'mediaUnread' | 'groupedId' | 'mentioned' | 'viaBotId' |
'replies' | 'fromScheduled' | 'postAuthor' | 'noforwards' | 'reactions' | 'forwards' | 'silent' | 'pinned' |
'savedPeerId' | 'fromBoostsApplied' | 'quickReplyShortcutId' | 'viaBusinessBotId'
)>
& Partial<GramJs.Message & GramJs.MessageService>
);
export function buildApiMessageWithChatId(
@ -192,6 +189,7 @@ export function buildApiMessageWithChatId(
const emojiOnlyCount = getEmojiOnlyCountForMessage(content, groupedId);
const hasComments = mtpMessage.replies?.comments;
const senderBoosts = mtpMessage.fromBoostsApplied;
const factCheck = mtpMessage.factcheck && buildApiFactCheck(mtpMessage.factcheck);
const savedPeerId = mtpMessage.savedPeerId && getApiChatIdFromMtpPeer(mtpMessage.savedPeerId);
@ -234,6 +232,7 @@ export function buildApiMessageWithChatId(
savedPeerId,
senderBoosts,
viaBusinessBotId: mtpMessage.viaBusinessBotId?.toString(),
factCheck,
});
}
@ -319,6 +318,15 @@ function buildApiReplyInfo(replyHeader: GramJs.TypeMessageReplyHeader): ApiReply
return undefined;
}
export function buildApiFactCheck(factCheck: GramJs.FactCheck): ApiFactCheck {
return {
shouldFetch: factCheck.needCheck,
hash: factCheck.hash.toString(),
text: factCheck.text && buildApiFormattedText(factCheck.text),
countryCode: factCheck.country,
};
}
function buildAction(
action: GramJs.TypeMessageAction,
senderId: string | undefined,
@ -450,6 +458,7 @@ function buildAction(
amount = Number(action.totalAmount);
currency = action.currency;
text = 'PaymentSuccessfullyPaid';
type = 'receipt';
if (targetPeerId) {
targetUserIds.push(targetPeerId);
}

View File

@ -9,8 +9,12 @@ import type {
ApiInvoice, ApiLabeledPrice, ApiMyBoost, ApiPaymentCredentials,
ApiPaymentForm, ApiPaymentSavedInfo, ApiPremiumGiftCodeOption, ApiPremiumPromo, ApiPremiumSubscriptionOption,
ApiReceipt,
ApiStarsTransaction,
ApiStarsTransactionPeer,
ApiStarTopupOption,
} from '../../types';
import { addWebDocumentToLocalDb } from '../helpers';
import { buildApiMessageEntity } from './common';
import { omitVirtualClassFields } from './helpers';
import { buildApiDocument, buildApiWebDocument } from './messageContent';
@ -37,7 +41,35 @@ export function buildShippingOptions(shippingOptions: GramJs.ShippingOption[] |
});
}
export function buildApiReceipt(receipt: GramJs.payments.PaymentReceipt): ApiReceipt {
export function buildApiReceipt(receipt: GramJs.payments.TypePaymentReceipt): ApiReceipt {
const { photo } = receipt;
if (photo) {
addWebDocumentToLocalDb(photo);
}
if (receipt instanceof GramJs.payments.PaymentReceiptStars) {
const {
botId, currency, date, description: text, title, totalAmount, transactionId,
} = receipt;
if (photo) {
addWebDocumentToLocalDb(photo);
}
return {
type: 'stars',
currency,
botId: buildApiPeerId(botId, 'user'),
date,
text,
title,
totalAmount: -totalAmount.toJSNumber(),
transactionId,
photo: photo && buildApiWebDocument(photo),
};
}
const {
invoice,
info,
@ -46,6 +78,8 @@ export function buildApiReceipt(receipt: GramJs.payments.PaymentReceipt): ApiRec
totalAmount,
credentialsTitle,
tipAmount,
title,
description: text,
} = receipt;
const { shippingAddress, phone, name } = (info || {});
@ -70,6 +104,7 @@ export function buildApiReceipt(receipt: GramJs.payments.PaymentReceipt): ApiRec
}
return {
type: 'regular',
currency,
prices: mappedPrices,
info: { shippingAddress, phone, name },
@ -78,10 +113,23 @@ export function buildApiReceipt(receipt: GramJs.payments.PaymentReceipt): ApiRec
shippingPrices,
shippingMethod,
tipAmount: tipAmount ? tipAmount.toJSNumber() : 0,
title,
text,
photo: photo && buildApiWebDocument(photo),
};
}
export function buildApiPaymentForm(form: GramJs.payments.PaymentForm): ApiPaymentForm {
export function buildApiPaymentForm(form: GramJs.payments.TypePaymentForm): ApiPaymentForm {
if (form instanceof GramJs.payments.PaymentFormStars) {
const { botId, formId } = form;
return {
type: 'stars',
botId: buildApiPeerId(botId, 'user'),
formId: String(formId),
};
}
const {
formId,
canSaveCredentials,
@ -93,6 +141,7 @@ export function buildApiPaymentForm(form: GramJs.payments.PaymentForm): ApiPayme
invoice,
savedCredentials,
url,
botId,
} = form;
const {
@ -121,7 +170,9 @@ export function buildApiPaymentForm(form: GramJs.payments.PaymentForm): ApiPayme
const nativeData = nativeParams ? JSON.parse(nativeParams.data) : {};
return {
type: 'regular',
url,
botId: buildApiPeerId(botId, 'user'),
canSaveCredentials,
isPasswordMissing,
formId: String(formId),
@ -146,12 +197,13 @@ export function buildApiPaymentForm(form: GramJs.payments.PaymentForm): ApiPayme
needZip: Boolean(nativeData?.need_zip),
publishableKey: nativeData?.publishable_key,
publicToken: nativeData?.public_token,
tokenizeUrl: nativeData?.tokenize_url,
},
...(savedCredentials && { savedCredentials: buildApiPaymentCredentials(savedCredentials) }),
savedCredentials: savedCredentials && buildApiPaymentCredentials(savedCredentials),
};
}
export function buildApiInvoiceFromForm(form: GramJs.payments.PaymentForm): ApiInvoice {
export function buildApiInvoiceFromForm(form: GramJs.payments.TypePaymentForm): ApiInvoice {
const {
invoice, description: text, title, photo,
} = form;
@ -328,3 +380,61 @@ export function buildApiPremiumGiftCodeOption(option: GramJs.PremiumGiftCodeOpti
users,
};
}
export function buildApiStarsTransactionPeer(peer: GramJs.TypeStarsTransactionPeer): ApiStarsTransactionPeer {
if (peer instanceof GramJs.StarsTransactionPeerAppStore) {
return { type: 'appStore' };
}
if (peer instanceof GramJs.StarsTransactionPeerPlayMarket) {
return { type: 'playMarket' };
}
if (peer instanceof GramJs.StarsTransactionPeerPremiumBot) {
return { type: 'premiumBot' };
}
if (peer instanceof GramJs.StarsTransactionPeerFragment) {
return { type: 'fragment' };
}
if (peer instanceof GramJs.StarsTransactionPeer) {
return { type: 'peer', id: getApiChatIdFromMtpPeer(peer.peer) };
}
return { type: 'unsupported' };
}
export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): ApiStarsTransaction {
const {
date, id, peer, stars, description, photo, title, refund,
} = transaction;
if (photo) {
addWebDocumentToLocalDb(photo);
}
return {
id,
date,
peer: buildApiStarsTransactionPeer(peer),
stars: stars.toJSNumber(),
title,
description,
photo: photo && buildApiWebDocument(photo),
isRefund: refund,
};
}
export function buildApiStarTopupOption(option: GramJs.TypeStarsTopupOption): ApiStarTopupOption {
const {
amount, currency, stars, extended,
} = option;
return {
amount: amount.toJSNumber(),
currency,
stars: stars.toJSNumber(),
isExtended: extended,
};
}

View File

@ -22,6 +22,7 @@ import type {
ApiReportReason,
ApiRequestInputInvoice,
ApiSendMessageAction,
ApiStarTopupOption,
ApiSticker,
ApiStory,
ApiStorySkipped,
@ -604,6 +605,15 @@ function buildPremiumGiftCodeOption(optionData: ApiPremiumGiftCodeOption) {
});
}
function buildInputStarsTopupOption(option: ApiStarTopupOption) {
return new GramJs.StarsTopupOption({
stars: BigInt(option.stars),
amount: BigInt(option.amount),
currency: option.currency,
extended: option.isExtended,
});
}
export function buildInputInvoice(invoice: ApiRequestInputInvoice) {
switch (invoice.type) {
case 'message': {
@ -619,6 +629,12 @@ export function buildInputInvoice(invoice: ApiRequestInputInvoice) {
});
}
case 'stars': {
return new GramJs.InputInvoiceStars({
option: buildInputStarsTopupOption(invoice.option),
});
}
case 'giveaway':
default: {
const purpose = buildInputStorePaymentPurpose(invoice.purpose);

View File

@ -83,9 +83,8 @@ function addMediaToLocalDb(media: GramJs.TypeMessageMedia) {
addPhotoToLocalDb(media.game.photo);
}
if (media instanceof GramJs.MessageMediaInvoice
&& media.photo) {
localDb.webDocuments[String(media.photo.url)] = media.photo;
if (media instanceof GramJs.MessageMediaInvoice && media.photo) {
addWebDocumentToLocalDb(media.photo);
}
}
@ -154,6 +153,10 @@ export function addEntitiesToLocalDb(entities: (GramJs.TypeUser | GramJs.TypeCha
});
}
export function addWebDocumentToLocalDb(webDocument: GramJs.TypeWebDocument) {
localDb.webDocuments[webDocument.url] = webDocument;
}
export function swapLocalInvoiceMedia(
chatId: string, messageId: number, extendedMedia: GramJs.TypeMessageExtendedMedia,
) {

View File

@ -33,7 +33,9 @@ import {
buildInputThemeParams,
generateRandomBigInt,
} from '../gramjsBuilders';
import { addEntitiesToLocalDb, addUserToLocalDb, deserializeBytes } from '../helpers';
import {
addEntitiesToLocalDb, addUserToLocalDb, addWebDocumentToLocalDb, deserializeBytes,
} from '../helpers';
import localDb from '../localDb';
import { invokeRequest } from './client';
@ -561,10 +563,6 @@ function addPhotoToLocalDb(photo: GramJs.Photo) {
localDb.photos[String(photo.id)] = photo;
}
function addWebDocumentToLocalDb(webDocument: GramJs.TypeWebDocument) {
localDb.webDocuments[webDocument.url] = webDocument;
}
export function setBotInfo({
bot,
langCode,

View File

@ -33,7 +33,7 @@ export {
reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs,
saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, transcribeAudio,
closePoll, fetchExtendedMedia, translateText, fetchMessageViews, fetchDiscussionMessage, clickSponsoredMessage,
fetchOutboxReadDate, exportMessageLink, fetchQuickReplies, sendQuickReply,
fetchOutboxReadDate, exportMessageLink, fetchQuickReplies, sendQuickReply, fetchFactChecks,
deleteSavedHistory,
} from './messages';
@ -100,7 +100,8 @@ export * from './stories';
export {
validateRequestedInfo, sendPaymentForm, getPaymentForm, getReceipt, fetchPremiumPromo, fetchTemporaryPaymentPassword,
applyBoost, fetchBoostList, fetchBoostStatus, fetchGiveawayInfo, fetchMyBoosts, applyGiftCode, checkGiftCode,
getPremiumGiftCodeOptions, launchPrepaidGiveaway,
getPremiumGiftCodeOptions, launchPrepaidGiveaway, fetchStarsStatus, fetchStarsTopupOptions, fetchStarsTransactions,
sendStarPaymentForm,
} from './payments';
export * from './fragment';

View File

@ -49,6 +49,7 @@ import { buildApiChatFromPreview, buildApiSendAsPeerId } from '../apiBuilders/ch
import { buildApiFormattedText } from '../apiBuilders/common';
import { buildMessageMediaContent, buildMessageTextContent, buildWebPage } from '../apiBuilders/messageContent';
import {
buildApiFactCheck,
buildApiMessage,
buildApiQuickReply,
buildApiSponsoredMessage,
@ -1047,6 +1048,25 @@ export async function fetchMessageViews({
};
}
export async function fetchFactChecks({
chat, ids,
}: {
chat: ApiChat;
ids: number[];
}) {
const chunks = split(ids, API_GENERAL_ID_LIMIT);
const results = await Promise.all(chunks.map((chunkIds) => (
invokeRequest(new GramJs.messages.GetFactCheck({
peer: buildInputPeer(chat.id, chat.accessHash),
msgId: chunkIds,
}))
)));
if (!results || results.some((result) => !result)) return undefined;
return results.flatMap((result) => result!).map(buildApiFactCheck);
}
export async function fetchDiscussionMessage({
chat, messageId,
}: {

View File

@ -6,6 +6,7 @@ import type {
OnApiUpdate,
} from '../../types';
import { DEBUG } from '../../../config';
import { buildApiChatFromPreview } from '../apiBuilders/chats';
import {
buildApiBoost,
@ -18,6 +19,8 @@ import {
buildApiPremiumGiftCodeOption,
buildApiPremiumPromo,
buildApiReceipt,
buildApiStarsTransaction,
buildApiStarTopupOption,
buildShippingOptions,
} from '../apiBuilders/payments';
import { buildApiUser } from '../apiBuilders/users';
@ -26,6 +29,7 @@ import {
} from '../gramjsBuilders';
import {
addEntitiesToLocalDb,
addWebDocumentToLocalDb,
deserializeBytes,
serializeBytes,
} from '../helpers';
@ -124,6 +128,34 @@ export async function sendPaymentForm({
return Boolean(result);
}
export async function sendStarPaymentForm({
formId,
inputInvoice,
}: {
formId: string;
inputInvoice: ApiRequestInputInvoice;
}) {
const result = await invokeRequest(new GramJs.payments.SendStarsForm({
formId: BigInt(formId),
invoice: buildInputInvoice(inputInvoice),
}));
if (!result) return false;
if (result instanceof GramJs.payments.PaymentVerificationNeeded) {
if (DEBUG) {
// eslint-disable-next-line no-console
console.warn('Unexpected PaymentVerificationNeeded in sendStarsForm');
}
return undefined;
} else {
handleGramJsUpdate(result.updates);
}
return Boolean(result);
}
export async function getPaymentForm(inputInvoice: ApiRequestInputInvoice) {
const result = await invokeRequest(new GramJs.payments.GetPaymentForm({
invoice: buildInputInvoice(inputInvoice),
@ -134,7 +166,7 @@ export async function getPaymentForm(inputInvoice: ApiRequestInputInvoice) {
}
if (result.photo) {
localDb.webDocuments[result.photo.url] = result.photo;
addWebDocumentToLocalDb(result.photo);
}
addEntitiesToLocalDb(result.users);
@ -143,7 +175,6 @@ export async function getPaymentForm(inputInvoice: ApiRequestInputInvoice) {
form: buildApiPaymentForm(result),
invoice: buildApiInvoiceFromForm(result),
users: result.users.map(buildApiUser).filter(Boolean),
botId: result.botId.toString(),
};
}
@ -387,3 +418,66 @@ export function launchPrepaidGiveaway({
shouldReturnTrue: true,
});
}
export async function fetchStarsStatus() {
const result = await invokeRequest(new GramJs.payments.GetStarsStatus({
peer: new GramJs.InputPeerSelf(),
}));
if (!result) {
return undefined;
}
const users = result.users.map(buildApiUser).filter(Boolean);
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
return {
users,
chats,
nextOffset: result.nextOffset,
history: result.history.map(buildApiStarsTransaction),
balance: result.balance.toJSNumber(),
};
}
export async function fetchStarsTransactions({
offset,
isInbound,
isOutbound,
}: {
offset?: string;
isInbound?: true;
isOutbound?: true;
}) {
const result = await invokeRequest(new GramJs.payments.GetStarsTransactions({
peer: new GramJs.InputPeerSelf(),
offset,
inbound: isInbound,
outbound: isOutbound,
}));
if (!result) {
return undefined;
}
const users = result.users.map(buildApiUser).filter(Boolean);
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
return {
users,
chats,
nextOffset: result.nextOffset,
history: result.history.map(buildApiStarsTransaction),
balance: result.balance.toJSNumber(),
};
}
export async function fetchStarsTopupOptions() {
const result = await invokeRequest(new GramJs.payments.GetStarsTopupOptions());
if (!result) {
return undefined;
}
return result.map(buildApiStarTopupOption);
}

View File

@ -244,12 +244,7 @@ export function updater(update: Update) {
if (update.message instanceof GramJs.MessageService) {
const { action } = update.message;
if (action instanceof GramJs.MessageActionPaymentSent) {
onUpdate({
'@type': 'updatePaymentStateCompleted',
slug: action.invoiceSlug,
});
} else if (action instanceof GramJs.MessageActionChatEditTitle) {
if (action instanceof GramJs.MessageActionChatEditTitle) {
onUpdate({
'@type': 'updateChat',
id: message.chatId,
@ -1194,6 +1189,11 @@ export function updater(update: Update) {
chatId: buildApiPeerId(update.channelId, 'channel'),
isEnabled: update.enabled ? true : undefined,
});
} else if (update instanceof GramJs.UpdateStarsBalance) {
onUpdate({
'@type': 'updateStarsBalance',
balance: update.balance.toJSNumber(),
});
} else if (update instanceof LocalUpdatePremiumFloodWait) {
onUpdate({
'@type': 'updatePremiumFloodWait',

View File

@ -2,7 +2,7 @@ import type { ThreadId } from '../../types';
import type { ApiWebDocument } from './bots';
import type { ApiGroupCall, PhoneCallAction } from './calls';
import type { ApiChat, ApiPeerColor } from './chats';
import type { ApiInputStorePaymentPurpose, ApiPremiumGiftCodeOption } from './payments';
import type { ApiInputStorePaymentPurpose, ApiPremiumGiftCodeOption, ApiStarTopupOption } from './payments';
import type { ApiMessageStoryData, ApiWebPageStickerData, ApiWebPageStoryData } from './stories';
export interface ApiDimensions {
@ -208,8 +208,13 @@ export type ApiInputInvoiceGiftCode = {
option: ApiPremiumGiftCodeOption;
};
export type ApiInputInvoiceStars = {
type: 'stars';
option: ApiStarTopupOption;
};
export type ApiInputInvoice = ApiInputInvoiceMessage | ApiInputInvoiceSlug | ApiInputInvoiceGiveaway
| ApiInputInvoiceGiftCode;
| ApiInputInvoiceGiftCode | ApiInputInvoiceStars;
/* Used for Invoice request */
export type ApiRequestInputInvoiceMessage = {
@ -229,8 +234,13 @@ export type ApiRequestInputInvoiceGiveaway = {
option: ApiPremiumGiftCodeOption;
};
export type ApiRequestInputInvoiceStars = {
type: 'stars';
option: ApiStarTopupOption;
};
export type ApiRequestInputInvoice = ApiRequestInputInvoiceMessage | ApiRequestInputInvoiceSlug
| ApiRequestInputInvoiceGiveaway;
| ApiRequestInputInvoiceGiveaway | ApiRequestInputInvoiceStars;
export interface ApiInvoice {
text: string;
@ -345,6 +355,7 @@ export interface ApiAction {
| 'suggestProfilePhoto'
| 'joinedChannel'
| 'chatBoost'
| 'receipt'
| 'other';
photo?: ApiPhoto;
amount?: number;
@ -445,7 +456,7 @@ export type ApiMessageEntityDefault = {
type: Exclude<
`${ApiMessageEntityTypes}`,
`${ApiMessageEntityTypes.Pre}` | `${ApiMessageEntityTypes.TextUrl}` | `${ApiMessageEntityTypes.MentionName}` |
`${ApiMessageEntityTypes.CustomEmoji}`
`${ApiMessageEntityTypes.CustomEmoji}` | `${ApiMessageEntityTypes.Blockquote}`
>;
offset: number;
length: number;
@ -472,6 +483,13 @@ export type ApiMessageEntityMentionName = {
userId: string;
};
export type ApiMessageEntityBlockquote = {
type: ApiMessageEntityTypes.Blockquote;
offset: number;
length: number;
canCollapse?: boolean;
};
export type ApiMessageEntityCustomEmoji = {
type: ApiMessageEntityTypes.CustomEmoji;
offset: number;
@ -480,7 +498,7 @@ export type ApiMessageEntityCustomEmoji = {
};
export type ApiMessageEntity = ApiMessageEntityDefault | ApiMessageEntityPre | ApiMessageEntityTextUrl |
ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji;
ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji | ApiMessageEntityBlockquote;
export enum ApiMessageEntityTypes {
Bold = 'MessageEntityBold',
@ -587,6 +605,7 @@ export interface ApiMessage {
readDate?: number;
savedPeerId?: string;
senderBoosts?: number;
factCheck?: ApiFactCheck;
}
export interface ApiReactions {
@ -835,6 +854,13 @@ export type ApiQuickReply = {
topMessageId: number;
};
export type ApiFactCheck = {
shouldFetch?: true;
hash: string;
countryCode?: string;
text?: ApiFormattedText;
};
export type ApiSponsoredMessageReportResult = {
type: 'reported' | 'hidden' | 'premiumRequired';
} | {

View File

@ -22,8 +22,10 @@ export interface ApiPaymentSavedInfo {
shippingAddress?: ApiShippingAddress;
}
export interface ApiPaymentForm {
export interface ApiPaymentFormRegular {
type: 'regular';
url: string;
botId: string;
canSaveCredentials?: boolean;
isPasswordMissing?: boolean;
formId: string;
@ -35,12 +37,21 @@ export interface ApiPaymentForm {
nativeParams: ApiPaymentFormNativeParams;
}
export interface ApiPaymentFormStars {
type: 'stars';
formId: string;
botId: string;
}
export type ApiPaymentForm = ApiPaymentFormRegular | ApiPaymentFormStars;
export interface ApiPaymentFormNativeParams {
needCardholderName?: boolean;
needCountry?: boolean;
needZip?: boolean;
publishableKey?: string;
publicToken?: string;
tokenizeUrl?: string;
}
export interface ApiLabeledPrice {
@ -48,7 +59,21 @@ export interface ApiLabeledPrice {
amount: number;
}
export interface ApiReceipt {
export interface ApiReceiptStars {
type: 'stars';
botId?: string;
peer?: ApiStarsTransactionPeer;
date: number;
title?: string;
text?: string;
photo?: ApiWebDocument;
currency: string;
totalAmount: number;
transactionId: string;
}
export interface ApiReceiptRegular {
type: 'regular';
photo?: ApiWebDocument;
text?: string;
title?: string;
@ -66,6 +91,8 @@ export interface ApiReceipt {
shippingMethod?: string;
}
export type ApiReceipt = ApiReceiptRegular | ApiReceiptStars;
export interface ApiPremiumPromo {
videoSections: ApiPremiumSection[];
videos: ApiDocument[];
@ -179,3 +206,54 @@ export interface ApiPrepaidGiveaway {
quantity: number;
date: number;
}
export interface ApiStarsTransactionPeerUnsupported {
type: 'unsupported';
}
export interface ApiStarsTransactionPeerAppStore {
type: 'appStore';
}
export interface ApiStarsTransactionPeerPlayMarket {
type: 'playMarket';
}
export interface ApiStarsTransactionPeerPremiumBot {
type: 'premiumBot';
}
export interface ApiStarsTransactionPeerFragment {
type: 'fragment';
}
export interface ApiStarsTransactionPeerPeer {
type: 'peer';
id: string;
}
export type ApiStarsTransactionPeer =
| ApiStarsTransactionPeerUnsupported
| ApiStarsTransactionPeerAppStore
| ApiStarsTransactionPeerPlayMarket
| ApiStarsTransactionPeerPremiumBot
| ApiStarsTransactionPeerFragment
| ApiStarsTransactionPeerPeer;
export interface ApiStarsTransaction {
id: string;
peer: ApiStarsTransactionPeer;
stars: number;
isRefund?: true;
date: number;
title?: string;
description?: string;
photo?: ApiWebDocument;
}
export interface ApiStarTopupOption {
isExtended?: true;
stars: number;
currency: string;
amount: number;
}

View File

@ -20,6 +20,7 @@ import type {
} from './chats';
import type {
ApiFormattedText,
ApiInputInvoice,
ApiMessage,
ApiMessageExtendedMediaPreview,
ApiPhoto,
@ -518,7 +519,7 @@ export type ApiUpdatePaymentVerificationNeeded = {
export type ApiUpdatePaymentStateCompleted = {
'@type': 'updatePaymentStateCompleted';
slug?: string;
inputInvoice: ApiInputInvoice;
};
export type ApiUpdatePrivacy = {
@ -738,6 +739,11 @@ export type ApiUpdatePremiumFloodWait = {
isUpload?: boolean;
};
export type ApiUpdateStarsBalance = {
'@type': 'updateStarsBalance';
balance: number;
};
export type ApiUpdate = (
ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate |
ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser |
@ -768,7 +774,7 @@ export type ApiUpdate = (
ApiRequestReconnectApi | ApiRequestSync | ApiUpdateFetchingDifference | ApiUpdateChannelMessages |
ApiUpdateStealthMode | ApiUpdateAttachMenuBots | ApiUpdateNewAuthorization | ApiUpdateGroupInvitePrivacyForbidden |
ApiUpdateViewForumAsMessages | ApiUpdateSavedDialogPinned | ApiUpdatePinnedSavedDialogIds | ApiUpdateChatLastMessage |
ApiUpdateDeleteSavedHistory | ApiUpdatePremiumFloodWait |
ApiUpdateDeleteSavedHistory | ApiUpdatePremiumFloodWait | ApiUpdateStarsBalance |
ApiUpdateQuickReplyMessage | ApiUpdateQuickReplies | ApiDeleteQuickReply | ApiUpdateDeleteQuickReplyMessages
);

View File

Before

Width:  |  Height:  |  Size: 567 B

After

Width:  |  Height:  |  Size: 567 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="88" height="84" fill="none"><path fill="#fff" d="M41.307 70.48 21.729 82.472a3.49 3.49 0 0 1-5.205-3.835l3.03-11.929a14.17 14.17 0 0 1 7.6-9.282l21.358-10.255a2.834 2.834 0 0 0-1.71-5.346l-23.774 4.116c-4.592.795-9.3-.473-12.871-3.466l-7.511-6.294a3.49 3.49 0 0 1 1.969-6.153l22.947-1.796a5.16 5.16 0 0 0 4.362-3.167l8.852-21.37a3.49 3.49 0 0 1 6.448 0l8.852 21.37a5.16 5.16 0 0 0 4.362 3.167l23.073 1.806a3.49 3.49 0 0 1 1.992 6.134L67.906 51.175a5.16 5.16 0 0 0-1.668 5.13l5.41 22.474a3.49 3.49 0 0 1-5.216 3.792L46.693 70.48a5.16 5.16 0 0 0-5.386 0"/><path fill="url(#a)" d="M41.307 70.48 21.729 82.472a3.49 3.49 0 0 1-5.205-3.835l3.03-11.929a14.17 14.17 0 0 1 7.6-9.282l21.358-10.255a2.834 2.834 0 0 0-1.71-5.346l-23.774 4.116c-4.592.795-9.3-.473-12.871-3.466l-7.511-6.294a3.49 3.49 0 0 1 1.969-6.153l22.947-1.796a5.16 5.16 0 0 0 4.362-3.167l8.852-21.37a3.49 3.49 0 0 1 6.448 0l8.852 21.37a5.16 5.16 0 0 0 4.362 3.167l23.073 1.806a3.49 3.49 0 0 1 1.992 6.134L67.906 51.175a5.16 5.16 0 0 0-1.668 5.13l5.41 22.474a3.49 3.49 0 0 1-5.216 3.792L46.693 70.48a5.16 5.16 0 0 0-5.386 0"/><path fill="url(#b)" d="M41.307 70.48 21.729 82.472a3.49 3.49 0 0 1-5.205-3.835l3.03-11.929a14.17 14.17 0 0 1 7.6-9.282l21.358-10.255a2.834 2.834 0 0 0-1.71-5.346l-23.774 4.116c-4.592.795-9.3-.473-12.871-3.466l-7.511-6.294a3.49 3.49 0 0 1 1.969-6.153l22.947-1.796a5.16 5.16 0 0 0 4.362-3.167l8.852-21.37a3.49 3.49 0 0 1 6.448 0l8.852 21.37a5.16 5.16 0 0 0 4.362 3.167l23.073 1.806a3.49 3.49 0 0 1 1.992 6.134L67.906 51.175a5.16 5.16 0 0 0-1.668 5.13l5.41 22.474a3.49 3.49 0 0 1-5.216 3.792L46.693 70.48a5.16 5.16 0 0 0-5.386 0"/><path stroke="url(#c)" stroke-width="1.667" d="M41.307 70.48 21.729 82.472a3.49 3.49 0 0 1-5.205-3.835l3.03-11.929a14.17 14.17 0 0 1 7.6-9.282l21.358-10.255a2.834 2.834 0 0 0-1.71-5.346l-23.774 4.116c-4.592.795-9.3-.473-12.871-3.466l-7.511-6.294a3.49 3.49 0 0 1 1.969-6.153l22.947-1.796a5.16 5.16 0 0 0 4.362-3.167l8.852-21.37a3.49 3.49 0 0 1 6.448 0l8.852 21.37a5.16 5.16 0 0 0 4.362 3.167l23.073 1.806a3.49 3.49 0 0 1 1.992 6.134L67.906 51.175a5.16 5.16 0 0 0-1.668 5.13l5.41 22.474a3.49 3.49 0 0 1-5.216 3.792L46.693 70.48a5.16 5.16 0 0 0-5.386 0Z" style="mix-blend-mode:soft-light"/><defs><linearGradient id="a" x1="3" x2="84.148" y1="63.5" y2="-1.323" gradientUnits="userSpaceOnUse"><stop stop-color="#6B93FF"/><stop offset=".439" stop-color="#976FFF"/><stop offset="1" stop-color="#E46ACE"/></linearGradient><linearGradient id="b" x1="-7" x2="80.569" y1="102" y2="-4.497" gradientUnits="userSpaceOnUse"><stop stop-color="#FDEB32"/><stop offset=".439" stop-color="#FEBD04"/><stop offset="1" stop-color="#D75902"/></linearGradient><linearGradient id="c" x1="94.417" x2="44.063" y1="26.667" y2="42.313" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" stop-opacity="0"/><stop offset=".396" stop-color="#fff" stop-opacity=".85"/><stop offset=".521" stop-color="#fff"/><stop offset=".646" stop-color="#fff" stop-opacity=".85"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
src/assets/stars-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -25,6 +25,8 @@ export { default as StatusPickerMenu } from '../components/left/main/StatusPicke
export { default as BoostModal } from '../components/modals/boost/BoostModal';
export { default as GiftCodeModal } from '../components/modals/giftcode/GiftCodeModal';
export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal';
export { default as StarsBalanceModal } from '../components/modals/stars/StarsBalanceModal';
export { default as StarPaymentModal } from '../components/modals/stars/StarsPaymentModal';
export { default as AboutAdsModal } from '../components/common/AboutAdsModal';
export { default as ReportAdModal } from '../components/modals/reportAd/ReportAdModal';

View File

@ -12,7 +12,7 @@ import Button from '../ui/Button';
import ListItem from '../ui/ListItem';
import Modal from '../ui/Modal';
import Separator from '../ui/Separator';
import Icon from './Icon';
import Icon from './icons/Icon';
import SafeLink from './SafeLink';
import styles from './AboutAdsModal.module.scss';

View File

@ -44,7 +44,7 @@ import Button from '../ui/Button';
import Link from '../ui/Link';
import ProgressSpinner from '../ui/ProgressSpinner';
import AnimatedIcon from './AnimatedIcon';
import Icon from './Icon';
import Icon from './icons/Icon';
import './Audio.scss';

View File

@ -36,6 +36,10 @@
height: 100%;
}
&.force-fit &__media {
object-fit: cover;
}
.emoji {
width: 1rem;
height: 1rem;

View File

@ -5,6 +5,7 @@ import { getActions } from '../../global';
import type {
ApiChat, ApiPeer, ApiPhoto, ApiUser,
ApiWebDocument,
} from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import type { CustomPeer, StoryViewerOrigin } from '../../types';
@ -16,6 +17,7 @@ import {
getChatTitle,
getPeerStoryHtmlId,
getUserFullName,
getWebDocumentHash,
isAnonymousForwardsChat,
isChatWithRepliesBot,
isDeletedUser,
@ -34,7 +36,7 @@ import useMediaTransition from '../../hooks/useMediaTransition';
import OptimizedVideo from '../ui/OptimizedVideo';
import AvatarStoryCircle from './AvatarStoryCircle';
import Icon from './Icon';
import Icon from './icons/Icon';
import './Avatar.scss';
@ -51,6 +53,7 @@ type OwnProps = {
size?: AvatarSize;
peer?: ApiPeer | CustomPeer;
photo?: ApiPhoto;
webPhoto?: ApiWebDocument;
text?: string;
isSavedMessages?: boolean;
isSavedDialog?: boolean;
@ -74,6 +77,7 @@ const Avatar: FC<OwnProps> = ({
size = 'large',
peer,
photo,
webPhoto,
text,
isSavedMessages,
isSavedDialog,
@ -118,6 +122,8 @@ const Avatar: FC<OwnProps> = ({
if (photo.isVideo && withVideo) {
videoHash = `videoAvatar${photo.id}?size=u`;
}
} else if (webPhoto) {
imageHash = getWebDocumentHash(webPhoto);
}
}
@ -230,6 +236,7 @@ const Avatar: FC<OwnProps> = ({
isReplies && 'replies-bot-account',
isPremiumGradient && 'premium-gradient-bg',
isRoundedRect && 'forum',
(photo || webPhoto) && 'force-fit',
((withStory && realPeer?.hasStories) || forPremiumPromo) && 'with-story-circle',
withStorySolid && realPeer?.hasStories && 'with-story-solid',
withStorySolid && forceFriendStorySolid && 'close-friend',

View File

@ -0,0 +1,31 @@
@use '../../styles/mixins';
.root {
display: inline-block;
width: 100%;
}
.collapsed {
@include mixins.gradient-border-bottom(1rem);
}
.gradientContainer {
max-height: inherit;
}
.collapseIcon {
position: absolute;
display: grid;
place-items: center;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
bottom: 0;
right: 0;
}
.clickable {
cursor: var(--custom-cursor, pointer);
}

View File

@ -0,0 +1,65 @@
import React, {
type TeactNode,
useRef,
} from '../../lib/teact/teact';
import { ApiMessageEntityTypes } from '../../api/types';
import buildClassName from '../../util/buildClassName';
import useCollapsibleLines from '../../hooks/element/useCollapsibleLines';
import useLastCallback from '../../hooks/useLastCallback';
import Icon from './icons/Icon';
import styles from './Blockquote.module.scss';
type OwnProps = {
canBeCollapsible?: boolean;
isToggleDisabled?: boolean;
children: TeactNode;
};
const MAX_LINES = 4;
const Blockquote = ({ canBeCollapsible, isToggleDisabled, children }: OwnProps) => {
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLQuoteElement>(null);
const {
isCollapsed, isCollapsible, setIsCollapsed,
} = useCollapsibleLines(ref, MAX_LINES, undefined, !canBeCollapsible);
const canExpand = !isToggleDisabled && isCollapsed;
const handleExpand = useLastCallback(() => {
setIsCollapsed(false);
});
const handleToggle = useLastCallback(() => {
setIsCollapsed((prev) => !prev);
});
return (
<span className={styles.root} onClick={canExpand ? handleExpand : undefined}>
<blockquote
ref={ref}
data-entity-type={ApiMessageEntityTypes.Blockquote}
>
<div className={buildClassName(styles.gradientContainer, isCollapsed && styles.collapsed)}>
{children}
</div>
{isCollapsible && (
<div
className={buildClassName(styles.collapseIcon, !isToggleDisabled && styles.clickable)}
onClick={!isToggleDisabled ? handleToggle : undefined}
aria-hidden
>
<Icon name={isCollapsed ? 'down' : 'up'} />
</div>
)}
</blockquote>
</span>
);
};
export default Blockquote;

View File

@ -158,7 +158,7 @@ import ResponsiveHoverButton from '../ui/ResponsiveHoverButton';
import Spinner from '../ui/Spinner';
import Avatar from './Avatar';
import DeleteMessageModal from './DeleteMessageModal.async';
import Icon from './Icon';
import Icon from './icons/Icon';
import ReactionAnimatedEmoji from './reactions/ReactionAnimatedEmoji';
import './Composer.scss';

View File

@ -14,7 +14,7 @@ import usePrevious from '../../hooks/usePrevious';
import Button from '../ui/Button';
import Modal from '../ui/Modal';
import Icon from './Icon';
import Icon from './icons/Icon';
import Picker from './Picker';
import styles from './CountryPickerModal.module.scss';

View File

@ -43,7 +43,7 @@ import { useStickerPickerObservers } from './hooks/useStickerPickerObservers';
import StickerSetCover from '../middle/composer/StickerSetCover';
import Button from '../ui/Button';
import Loading from '../ui/Loading';
import Icon from './Icon';
import Icon from './icons/Icon';
import StickerButton from './StickerButton';
import StickerSet from './StickerSet';

View File

@ -22,7 +22,7 @@ import useLastCallback from '../../hooks/useLastCallback';
import CustomEmoji from './CustomEmoji';
import FakeIcon from './FakeIcon';
import PremiumIcon from './PremiumIcon';
import StarIcon from './icons/StarIcon';
import VerifiedIcon from './VerifiedIcon';
import styles from './FullNameTitle.module.scss';
@ -127,7 +127,7 @@ const FullNameTitle: FC<OwnProps> = ({
onClick={onEmojiStatusClick}
/>
)}
{withEmojiStatus && !realPeer?.emojiStatus && isPremium && <PremiumIcon />}
{withEmojiStatus && !realPeer?.emojiStatus && isPremium && <StarIcon />}
</>
)}
{iconElement}

View File

@ -33,7 +33,7 @@ import Transition from '../ui/Transition';
import Avatar from './Avatar';
import DotAnimation from './DotAnimation';
import FullNameTitle from './FullNameTitle';
import Icon from './Icon';
import Icon from './icons/Icon';
import TopicIcon from './TopicIcon';
import TypingStatus from './TypingStatus';

View File

@ -12,7 +12,7 @@ import useLastCallback from '../../hooks/useLastCallback';
import Button from '../ui/Button';
import DropdownMenu from '../ui/DropdownMenu';
import MenuItem from '../ui/MenuItem';
import Icon from './Icon';
import Icon from './icons/Icon';
import styles from './LinkField.module.scss';

View File

@ -4,7 +4,7 @@ import React, { memo } from '../../lib/teact/teact';
import type { ApiMessageOutgoingStatus } from '../../api/types';
import Transition from '../ui/Transition';
import Icon from './Icon';
import Icon from './icons/Icon';
import './MessageOutgoingStatus.scss';

View File

@ -29,6 +29,7 @@ interface OwnProps {
inChatList?: boolean;
forcePlayback?: boolean;
focusedQuote?: string;
isInSelectMode?: boolean;
}
const MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS = 3;
@ -49,6 +50,7 @@ function MessageText({
inChatList,
forcePlayback,
focusedQuote,
isInSelectMode,
}: OwnProps) {
// eslint-disable-next-line no-null/no-null
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
@ -104,6 +106,7 @@ function MessageText({
cacheBuster: textCacheBusterRef.current.toString(),
forcePlayback,
focusedQuote,
isInSelectMode,
}),
].flat().filter(Boolean)}
</>

View File

@ -1,3 +1,22 @@
.root {
position: relative;
overflow: hidden;
padding-inline-start: 0.5rem;
border-radius: 0.25rem;
background-color: var(--accent-background-color);
color: var(--accent-color);
&::before {
content: "";
display: block;
position: absolute;
top: 0;
bottom: 0;
inset-inline-start: 0;
width: 0.1875rem;
background: var(--bar-gradient, var(--accent-color));
}
}

View File

@ -4,7 +4,7 @@ import React, {
} from '../../lib/teact/teact';
import type { ApiCountry } from '../../api/types';
import type { CustomPeer, CustomPeerType } from '../../types';
import type { CustomPeer, CustomPeerType, UniqueCustomPeer } from '../../types';
import { requestMeasure } from '../../lib/fasterdom/fasterdom';
import { isUserId } from '../../global/helpers';
@ -28,7 +28,7 @@ import './Picker.scss';
type OwnProps = {
className?: string;
categories?: CustomPeer[];
categories?: UniqueCustomPeer[];
itemIds: string[];
selectedCategories?: CustomPeerType[];
selectedIds: string[];

View File

@ -15,7 +15,7 @@ import renderText from './helpers/renderText';
import useLang from '../../hooks/useLang';
import Avatar from './Avatar';
import Icon from './Icon';
import Icon from './icons/Icon';
import './PickerSelectedItem.scss';

View File

@ -1,23 +0,0 @@
.PremiumIcon {
flex-shrink: 0;
display: flex;
width: 1rem;
height: 1rem;
&.big {
width: 1.5rem;
height: 1.5rem;
}
--color-fill: var(--color-primary);
& > svg {
width: 100%;
height: 100%;
}
&.clickable {
cursor: var(--custom-cursor, pointer);
pointer-events: auto;
}
}

View File

@ -1,56 +0,0 @@
import type { FC } from '../../lib/teact/teact';
import React, { memo } from '../../lib/teact/teact';
import buildClassName from '../../util/buildClassName';
import useUniqueId from '../../hooks/useUniqueId';
import './PremiumIcon.scss';
type OwnProps = {
withGradient?: boolean;
big?: boolean;
className?: string;
onClick?: VoidFunction;
};
// eslint-disable-next-line max-len
const STAR_PATH = 'M6.63869 12.1902L3.50621 14.1092C3.18049 14.3087 2.75468 14.2064 2.55515 13.8807C2.45769 13.7216 2.42864 13.5299 2.47457 13.3491L2.95948 11.4405C3.13452 10.7515 3.60599 10.1756 4.24682 9.86791L7.6642 8.22716C7.82352 8.15067 7.89067 7.95951 7.81418 7.80019C7.75223 7.67116 7.61214 7.59896 7.47111 7.62338L3.66713 8.28194C2.89387 8.41581 2.1009 8.20228 1.49941 7.69823L0.297703 6.69116C0.00493565 6.44581 -0.0335059 6.00958 0.211842 5.71682C0.33117 5.57442 0.502766 5.48602 0.687982 5.47153L4.35956 5.18419C4.61895 5.16389 4.845 4.99974 4.94458 4.75937L6.36101 1.3402C6.5072 0.987302 6.91179 0.819734 7.26469 0.965925C7.43413 1.03612 7.56876 1.17075 7.63896 1.3402L9.05539 4.75937C9.15496 4.99974 9.38101 5.16389 9.6404 5.18419L13.3322 5.47311C13.713 5.50291 13.9975 5.83578 13.9677 6.2166C13.9534 6.39979 13.8667 6.56975 13.7269 6.68896L10.9114 9.08928C10.7131 9.25826 10.6267 9.52425 10.6876 9.77748L11.5532 13.3733C11.6426 13.7447 11.414 14.1182 11.0427 14.2076C10.8642 14.2506 10.676 14.2208 10.5195 14.1249L7.36128 12.1902C7.13956 12.0544 6.8604 12.0544 6.63869 12.1902Z';
const PremiumIcon: FC<OwnProps> = ({
withGradient,
big,
className,
onClick,
}) => {
const randomId = useUniqueId();
return (
<i
onClick={onClick}
className={buildClassName(
'PremiumIcon', className, withGradient && 'gradient', onClick && 'clickable', big && 'big',
)}
title="Premium"
>
{withGradient ? (
<svg width="14" height="15" viewBox="0 0 14 15" fill="none">
<defs>
<linearGradient id={randomId} x1="3" y1="63.5001" x2="84.1475" y2="-1.32262" gradientUnits="userSpaceOnUse">
<stop stop-color="#6B93FF" />
<stop offset="0.439058" stop-color="#976FFF" />
<stop offset="1" stop-color="#E46ACE" />
</linearGradient>
</defs>
<path fill-rule="evenodd" clip-rule="evenodd" d={STAR_PATH} fill={`url(#${randomId})`} />
</svg>
) : (
<svg width="14" height="15" viewBox="0 0 14 15" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d={STAR_PATH} fill="var(--color-fill)" />
</svg>
)}
</i>
);
};
export default memo(PremiumIcon);

View File

@ -11,7 +11,7 @@ import buildStyle from '../../util/buildStyle';
import useLang from '../../hooks/useLang';
import useResizeObserver from '../../hooks/useResizeObserver';
import Icon from './Icon';
import Icon from './icons/Icon';
import styles from './PremiumProgress.module.scss';

View File

@ -16,7 +16,7 @@ import Button from '../ui/Button';
import Modal, { ANIMATION_DURATION } from '../ui/Modal';
import Separator from '../ui/Separator';
import AnimatedIconWithPreview from './AnimatedIconWithPreview';
import Icon from './Icon';
import Icon from './icons/Icon';
import styles from './PrivacySettingsNoticeModal.module.scss';

View File

@ -23,7 +23,7 @@ import RippleEffect from '../ui/RippleEffect';
import Avatar from './Avatar';
import DotAnimation from './DotAnimation';
import FullNameTitle from './FullNameTitle';
import Icon from './Icon';
import Icon from './icons/Icon';
import TypingStatus from './TypingStatus';
type OwnProps = {
@ -227,7 +227,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
/>
)}
<Avatar
key={customPeer?.type || user?.id}
key={user?.id}
size={avatarSize}
peer={customPeer || user}
className={buildClassName(isSavedDialog && 'overlay-avatar')}

View File

@ -27,7 +27,7 @@
}
.VerifiedIcon,
.PremiumIcon {
.StarIcon {
--color-fill: var(--color-white);
--color-checkmark: var(--color-primary);

View File

@ -30,7 +30,7 @@ import useMediaTransition from '../../hooks/useMediaTransition';
import OptimizedVideo from '../ui/OptimizedVideo';
import Spinner from '../ui/Spinner';
import Icon from './Icon';
import Icon from './icons/Icon';
import './ProfilePhoto.scss';

View File

@ -1,5 +1,5 @@
import type { FC } from '../../lib/teact/teact';
import React, { memo } from '../../lib/teact/teact';
import type { TeactNode } from '../../lib/teact/teact';
import React from '../../lib/teact/teact';
import { getActions } from '../../global';
import { ApiMessageEntityTypes } from '../../api/types';
@ -17,17 +17,17 @@ type OwnProps = {
url?: string;
text: string;
className?: string;
children?: React.ReactNode;
children?: TeactNode;
isRtl?: boolean;
};
const SafeLink: FC<OwnProps> = ({
const SafeLink = ({
url,
text,
className,
children,
isRtl,
}) => {
}: OwnProps) => {
const { openUrl } = getActions();
const content = children || text;
@ -96,4 +96,4 @@ function getUnicodeUrl(url?: string) {
return undefined;
}
export default memo(SafeLink);
export default SafeLink;

View File

@ -37,7 +37,7 @@ import useThumbnail from '../../../hooks/useThumbnail';
import useMessageTranslation from '../../middle/message/hooks/useMessageTranslation';
import ActionMessage from '../../middle/ActionMessage';
import Icon from '../Icon';
import Icon from '../icons/Icon';
import MediaSpoiler from '../MediaSpoiler';
import MessageSummary from '../MessageSummary';
import EmojiIconBackground from './EmojiIconBackground';

View File

@ -20,7 +20,7 @@ import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useMedia from '../../../hooks/useMedia';
import Icon from '../Icon';
import Icon from '../icons/Icon';
import './EmbeddedMessage.scss';

View File

@ -23,7 +23,7 @@ import { useFastClick } from '../../../hooks/useFastClick';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import Icon from '../Icon';
import Icon from '../icons/Icon';
import EmojiIconBackground from './EmojiIconBackground';
import './EmbeddedMessage.scss';

View File

@ -14,6 +14,7 @@ import { buildCustomEmojiHtmlFromEntity } from '../../middle/composer/helpers/cu
import renderText from './renderText';
import MentionLink from '../../middle/message/MentionLink';
import Blockquote from '../Blockquote';
import CodeBlock from '../code/CodeBlock';
import CustomEmoji from '../CustomEmoji';
import SafeLink from '../SafeLink';
@ -46,6 +47,7 @@ export function renderTextWithEntities({
cacheBuster,
forcePlayback,
focusedQuote,
isInSelectMode,
}: {
text: string;
entities?: ApiMessageEntity[];
@ -64,6 +66,7 @@ export function renderTextWithEntities({
cacheBuster?: string;
forcePlayback?: boolean;
focusedQuote?: string;
isInSelectMode?: boolean;
}) {
if (!entities?.length) {
return renderMessagePart({
@ -170,6 +173,7 @@ export function renderTextWithEntities({
sharedCanvasHqRef,
cacheBuster,
forcePlayback,
isInSelectMode,
});
if (Array.isArray(newEntity)) {
@ -384,6 +388,7 @@ function processEntity({
sharedCanvasHqRef,
cacheBuster,
forcePlayback,
isInSelectMode,
} : {
entity: ApiMessageEntity;
entityContent: TextPart;
@ -402,6 +407,7 @@ function processEntity({
sharedCanvasHqRef?: React.RefObject<HTMLCanvasElement>;
cacheBuster?: string;
forcePlayback?: boolean;
isInSelectMode?: boolean;
}) {
const entityText = typeof entityContent === 'string' && entityContent;
const renderedContent = nestedEntityContent.length ? nestedEntityContent : entityContent;
@ -451,11 +457,9 @@ function processEntity({
return <strong data-entity-type={entity.type}>{renderNestedMessagePart()}</strong>;
case ApiMessageEntityTypes.Blockquote:
return (
<span className="text-entity-blockquote-wrapper">
<blockquote data-entity-type={entity.type}>
{renderNestedMessagePart()}
</blockquote>
</span>
<Blockquote canBeCollapsible={entity.canCollapse} isToggleDisabled={isInSelectMode}>
{renderNestedMessagePart()}
</Blockquote>
);
case ApiMessageEntityTypes.BotCommand:
return (

View File

@ -1,9 +1,9 @@
import type { AriaRole } from 'react';
import React from '../../lib/teact/teact';
import React from '../../../lib/teact/teact';
import type { IconName } from '../../types/icons';
import type { IconName } from '../../../types/icons';
import buildClassName from '../../util/buildClassName';
import buildClassName from '../../../util/buildClassName';
type OwnProps = {
name: IconName;

View File

@ -0,0 +1,28 @@
.root {
--color-fill: var(--color-primary);
flex-shrink: 0;
display: flex;
width: 1rem;
height: 1rem;
}
.middle {
width: 1.25rem;
height: 1.25rem;
}
.big {
width: 1.5rem;
height: 1.5rem;
}
.svg {
width: 100%;
height: 100%;
}
.clickable {
cursor: var(--custom-cursor, pointer);
pointer-events: auto;
}

View File

@ -0,0 +1,139 @@
import type { FC } from '../../../lib/teact/teact';
import React, { memo } from '../../../lib/teact/teact';
import buildClassName from '../../../util/buildClassName';
import useUniqueId from '../../../hooks/useUniqueId';
import styles from './StarIcon.module.scss';
type OwnProps = {
type?: 'gold' | 'premium' | 'regular';
size?: 'small' | 'middle' | 'big';
className?: string;
onClick?: VoidFunction;
};
/* eslint-disable max-len */
const STAR_PATH = 'M6.63869 12.1902L3.50621 14.1092C3.18049 14.3087 2.75468 14.2064 2.55515 13.8807C2.45769 13.7216 2.42864 13.5299 2.47457 13.3491L2.95948 11.4405C3.13452 10.7515 3.60599 10.1756 4.24682 9.86791L7.6642 8.22716C7.82352 8.15067 7.89067 7.95951 7.81418 7.80019C7.75223 7.67116 7.61214 7.59896 7.47111 7.62338L3.66713 8.28194C2.89387 8.41581 2.1009 8.20228 1.49941 7.69823L0.297703 6.69116C0.00493565 6.44581 -0.0335059 6.00958 0.211842 5.71682C0.33117 5.57442 0.502766 5.48602 0.687982 5.47153L4.35956 5.18419C4.61895 5.16389 4.845 4.99974 4.94458 4.75937L6.36101 1.3402C6.5072 0.987302 6.91179 0.819734 7.26469 0.965925C7.43413 1.03612 7.56876 1.17075 7.63896 1.3402L9.05539 4.75937C9.15496 4.99974 9.38101 5.16389 9.6404 5.18419L13.3322 5.47311C13.713 5.50291 13.9975 5.83578 13.9677 6.2166C13.9534 6.39979 13.8667 6.56975 13.7269 6.68896L10.9114 9.08928C10.7131 9.25826 10.6267 9.52425 10.6876 9.77748L11.5532 13.3733C11.6426 13.7447 11.414 14.1182 11.0427 14.2076C10.8642 14.2506 10.676 14.2208 10.5195 14.1249L7.36128 12.1902C7.13956 12.0544 6.8604 12.0544 6.63869 12.1902Z';
const GOLD_STAR_PATH = 'M10.5197 16.2049L6.46899 18.6864C6.04779 18.9444 5.49716 18.8121 5.23913 18.3909C5.11311 18.1852 5.07554 17.9373 5.13494 17.7035L5.762 15.2354C5.98835 14.3444 6.59803 13.5997 7.42671 13.2018L11.8459 11.0801C12.0519 10.9812 12.1387 10.734 12.0398 10.528C11.9597 10.3611 11.7786 10.2677 11.5962 10.2993L6.67709 11.1509C5.67715 11.324 4.65172 11.0479 3.87392 10.3961L2.31994 9.09382C1.94135 8.77655 1.89164 8.21245 2.20891 7.83386C2.36321 7.64972 2.58511 7.53541 2.82462 7.51667L7.5725 7.1451C7.90793 7.11885 8.20025 6.90658 8.32901 6.59574L10.1607 2.17427C10.3497 1.71792 10.8729 1.50123 11.3292 1.69028C11.5484 1.78105 11.7225 1.95514 11.8132 2.17427L13.6449 6.59574C13.7736 6.90658 14.066 7.11885 14.4014 7.1451L19.1754 7.51871C19.6678 7.55725 20.0358 7.9877 19.9972 8.48015C19.9787 8.71704 19.8666 8.93682 19.6858 9.09098L16.0449 12.1949C15.7886 12.4134 15.6768 12.7574 15.7556 13.0849L16.8749 17.7348C16.9905 18.215 16.6949 18.698 16.2147 18.8137C15.9839 18.8692 15.7406 18.8307 15.5382 18.7068L11.4541 16.2049C11.1674 16.0292 10.8064 16.0292 10.5197 16.2049Z';
/* eslint-enable max-len */
const StarIcon: FC<OwnProps> = ({
type = 'regular',
size = 'small',
className,
onClick,
}) => {
const randomId = useUniqueId();
const validSvgRandomId = `svg-${randomId}`; // ID must start with a letter
return (
<i
onClick={onClick}
className={buildClassName(
'StarIcon',
styles.root,
className,
onClick && styles.clickable,
styles[size],
)}
>
{type === 'gold'
? <GoldStarIcon randomId={validSvgRandomId} />
: type === 'premium'
? <PremiumStarIcon randomId={validSvgRandomId} />
: <RegularStarIcon />}
</i>
);
};
function GoldStarIcon({ randomId }: { randomId: string }) {
const fillId = `${randomId}-fill`;
const stroke1Id = `${randomId}-stroke1`;
const stroke2Id = `${randomId}-stroke2`;
return (
<svg className={styles.svg} width="21" height="20" viewBox="0 0 21 20" fill="none">
<defs>
<linearGradient
id={fillId}
x1="0.434893"
y1="22.5796"
x2="34.2364"
y2="-15.5089"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#FDEB32" />
<stop offset="0.439058" stop-color="#FEBD04" />
<stop offset="1" stop-color="#D75902" />
</linearGradient>
<linearGradient
id={stroke1Id}
x1="22.5"
y1="2.5"
x2="8"
y2="12.5"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#DB5A00" />
<stop offset="1" stop-color="#FF9145" />
</linearGradient>
<linearGradient
id={stroke2Id}
x1="24.5"
y1="2"
x2="11"
y2="10.2302"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="white" stop-opacity="0" />
<stop offset="0.395833" stop-color="white" stop-opacity="0.85" />
<stop offset="0.520833" stop-color="white" />
<stop offset="0.645833" stop-color="white" stop-opacity="0.85" />
<stop offset="1" stop-color="white" stop-opacity="0" />
</linearGradient>
</defs>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d={GOLD_STAR_PATH}
fill={`url(#${fillId})`}
stroke={`url(#${stroke1Id})`}
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d={GOLD_STAR_PATH}
stroke={`url(#${stroke2Id})`}
stroke-width="2"
style="mix-blend-mode:soft-light"
/>
</svg>
);
}
function PremiumStarIcon({ randomId }: { randomId: string }) {
return (
<svg className={styles.svg} width="14" height="15" viewBox="0 0 14 15" fill="none">
<defs>
<linearGradient id={randomId} x1="3" y1="63.5001" x2="84.1475" y2="-1.32262" gradientUnits="userSpaceOnUse">
<stop stop-color="#6B93FF" />
<stop offset="0.439058" stop-color="#976FFF" />
<stop offset="1" stop-color="#E46ACE" />
</linearGradient>
</defs>
<path fill-rule="evenodd" clip-rule="evenodd" d={STAR_PATH} fill={`url(#${randomId})`} />
</svg>
);
}
function RegularStarIcon() {
return (
<svg className={styles.svg} width="14" height="15" viewBox="0 0 14 15" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d={STAR_PATH} fill="var(--color-fill)" />
</svg>
);
}
export default memo(StarIcon);

View File

@ -22,7 +22,7 @@ import useSelectorSignal from '../../../hooks/useSelectorSignal';
import ListItem from '../../ui/ListItem';
import Transition, { ACTIVE_SLIDE_CLASS_NAME, TO_SLIDE_CLASS_NAME } from '../../ui/Transition';
import Icon from '../Icon';
import Icon from '../icons/Icon';
import styles from './BusinessHours.module.scss';

View File

@ -1,4 +1,4 @@
import type { FC } from '../../../lib/teact/teact';
import type { TeactNode } from '../../../lib/teact/teact';
import React, { memo, useEffect, useRef } from '../../../lib/teact/teact';
import { ApiMessageEntityTypes } from '../../../api/types';
@ -11,7 +11,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import './Spoiler.scss';
type OwnProps = {
children?: React.ReactNode;
children?: TeactNode;
containerId?: string;
};
@ -19,10 +19,10 @@ const revealByContainerId: Map<string, VoidFunction[]> = new Map();
const buildClassName = createClassNameBuilder('Spoiler');
const Spoiler: FC<OwnProps> = ({
const Spoiler = ({
children,
containerId,
}) => {
}: OwnProps) => {
// eslint-disable-next-line no-null/no-null
const contentRef = useRef<HTMLDivElement>(null);

View File

@ -89,7 +89,7 @@
color: var(--color-white);
}
.VerifiedIcon, .PremiumIcon {
.VerifiedIcon, .StarIcon {
--color-fill: #fff;
--color-checkmark: var(--color-primary);
}

View File

@ -121,7 +121,7 @@
color: var(--color-primary);
}
.PremiumIcon {
.StarIcon {
width: 1.5rem;
height: 1.5rem;
}

View File

@ -14,7 +14,7 @@ import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
import useFlag from '../../../hooks/useFlag';
import CustomEmoji from '../../common/CustomEmoji';
import PremiumIcon from '../../common/PremiumIcon';
import StarIcon from '../../common/icons/StarIcon';
import CustomEmojiEffect from '../../common/reactions/CustomEmojiEffect';
import Button from '../../ui/Button';
import StatusPickerMenu from './StatusPickerMenu.async';
@ -82,7 +82,7 @@ const StatusButton: FC<StateProps> = ({ emojiStatus }) => {
size={EMOJI_STATUS_SIZE}
loopLimit={EMOJI_STATUS_LOOP_LIMIT}
/>
) : <PremiumIcon />}
) : <StarIcon />}
</Button>
<StatusPickerMenu
statusButtonRef={buttonRef}

View File

@ -6,7 +6,7 @@ import type { ApiPremiumSection } from '../../../global/types';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import PremiumIcon from '../../common/PremiumIcon';
import StarIcon from '../../common/icons/StarIcon';
import ListItem from '../../ui/ListItem';
type OwnProps = {
@ -21,7 +21,7 @@ function PremiumStatusItem({ premiumSection }: OwnProps) {
return (
<div className="settings-item">
<ListItem
leftElement={<PremiumIcon className="icon" withGradient big />}
leftElement={<StarIcon className="icon" type="premium" size="big" />}
onClick={handleOpenPremiumModal}
>
{lang('PrivacyLastSeenPremium')}

View File

@ -3,7 +3,7 @@ import { getActions } from '../../../global';
import useLang from '../../../hooks/useLang';
import Icon from '../../common/Icon';
import Icon from '../../common/icons/Icon';
import styles from './PrivacyLockedOption.module.scss';

View File

@ -103,7 +103,7 @@
}
}
.settings-main-menu-premium .PremiumIcon {
.settings-main-menu-star .StarIcon {
margin-right: 1.25rem;
}

View File

@ -78,7 +78,7 @@ const SettingsExperimental: FC<OwnProps & StateProps> = ({
<div className="settings-item">
<ListItem
// eslint-disable-next-line react/jsx-no-bind
onClick={() => requestConfetti({})}
onClick={() => requestConfetti({ withStars: true })}
icon="animations"
>
<div className="title">Launch some confetti!</div>

View File

@ -9,13 +9,14 @@ import {
selectIsGiveawayGiftsPurchaseAvailable,
selectIsPremiumPurchaseBlocked,
} from '../../../global/selectors';
import { formatInteger } from '../../../util/textFormat';
import useFlag from '../../../hooks/useFlag';
import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import PremiumIcon from '../../common/PremiumIcon';
import StarIcon from '../../common/icons/StarIcon';
import ChatExtra from '../../common/profile/ChatExtra';
import ProfileInfo from '../../common/ProfileInfo';
import ConfirmDialog from '../../ui/ConfirmDialog';
@ -32,16 +33,20 @@ type StateProps = {
currentUserId?: string;
canBuyPremium?: boolean;
isGiveawayAvailable?: boolean;
starsBalance?: number;
shouldDisplayStars?: boolean;
};
const SettingsMain: FC<OwnProps & StateProps> = ({
isActive,
onScreenSelect,
onReset,
currentUserId,
sessionCount,
canBuyPremium,
isGiveawayAvailable,
starsBalance,
shouldDisplayStars,
onScreenSelect,
onReset,
}) => {
const {
loadProfilePhotos,
@ -49,6 +54,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
openSupportChat,
openUrl,
openPremiumGiftingModal,
openStarsBalanceModal,
} = getActions();
const [isSupportDialogOpen, openSupportDialog, closeSupportDialog] = useFlag(false);
@ -156,18 +162,31 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
<div className="settings-main-menu">
{canBuyPremium && (
<ListItem
leftElement={<PremiumIcon className="icon" withGradient big />}
className="settings-main-menu-premium"
leftElement={<StarIcon className="icon" type="premium" size="big" />}
className="settings-main-menu-star"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => openPremiumModal()}
>
{lang('TelegramPremium')}
</ListItem>
)}
{shouldDisplayStars && (
<ListItem
leftElement={<StarIcon className="icon" type="gold" size="big" />}
className="settings-main-menu-star"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => openStarsBalanceModal({})}
>
{lang('MenuTelegramStars')}
{Boolean(starsBalance) && (
<span className="settings-item__current-value">{formatInteger(starsBalance)}</span>
)}
</ListItem>
)}
{isGiveawayAvailable && (
<ListItem
icon="gift"
className="settings-main-menu-premium"
className="settings-main-menu-star"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => openPremiumGiftingModal()}
>
@ -213,12 +232,16 @@ export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const { currentUserId } = global;
const isGiveawayAvailable = selectIsGiveawayGiftsPurchaseAvailable(global);
const starsBalance = global.stars?.balance;
const shouldDisplayStars = Boolean(global.stars?.history?.all?.transactions.length);
return {
sessionCount: global.activeSessions.orderedHashes.length,
currentUserId,
canBuyPremium: !selectIsPremiumPurchaseBlocked(global),
isGiveawayAvailable,
starsBalance,
shouldDisplayStars,
};
},
)(SettingsMain));

View File

@ -11,7 +11,7 @@ import { selectCanSetPasscode, selectIsCurrentUserPremium } from '../../../globa
import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import PremiumIcon from '../../common/PremiumIcon';
import StarIcon from '../../common/icons/StarIcon';
import Checkbox from '../../ui/Checkbox';
import ListItem from '../../ui/ListItem';
@ -282,7 +282,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
<ListItem
narrow
allowDisabledClick
rightElement={isCurrentUserPremium && <PremiumIcon big withGradient />}
rightElement={isCurrentUserPremium && <StarIcon size="big" type="premium" />}
className="no-icon"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => onScreenSelect(SettingsScreens.PrivacyVoiceMessages)}
@ -296,7 +296,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
</ListItem>
<ListItem
narrow
rightElement={isCurrentUserPremium && <PremiumIcon big withGradient />}
rightElement={isCurrentUserPremium && <StarIcon size="big" type="premium" />}
className="no-icon"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => onScreenSelect(SettingsScreens.PrivacyMessages)}

View File

@ -9,7 +9,7 @@ import renderText from '../../common/helpers/renderText';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import PremiumIcon from '../../common/PremiumIcon';
import StarIcon from '../../common/icons/StarIcon';
import Checkbox from '../../ui/Checkbox';
import ListItem from '../../ui/ListItem';
@ -55,7 +55,7 @@ const SettingsPrivacyLastSeen = ({
)}
<div className="settings-item">
<ListItem
leftElement={<PremiumIcon className="icon" withGradient big />}
leftElement={<StarIcon className="icon" type="premium" size="big" />}
onClick={handleOpenPremiumModal}
>
{isCurrentUserPremium ? lang('PrivacyLastSeenPremiumForPremium') : lang('PrivacyLastSeenPremium')}

View File

@ -21,7 +21,7 @@ import useFlag from '../../hooks/useFlag';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import Icon from '../common/Icon';
import Icon from '../common/icons/Icon';
import Picker from '../common/Picker';
import Button from '../ui/Button';
import ConfirmDialog from '../ui/ConfirmDialog';

View File

@ -1,4 +1,3 @@
import type { FC } from '../../lib/teact/teact';
import React, { memo, useRef } from '../../lib/teact/teact';
import { withGlobal } from '../../global';
@ -32,6 +31,7 @@ interface Confetti {
};
size: number;
color: string;
isStar?: boolean;
flicker: number;
flickerFrequency: number;
rotation: number;
@ -42,8 +42,11 @@ interface Confetti {
const CONFETTI_FADEOUT_TIMEOUT = 10000;
const DEFAULT_CONFETTI_SIZE = 10;
const CONFETTI_COLORS = ['#E8BC2C', '#D0049E', '#02CBFE', '#5723FD', '#FE8C27', '#6CB859'];
// eslint-disable-next-line max-len
const STAR_PATH = new Path2D('M6.63869 12.1902L3.50621 14.1092C3.18049 14.3087 2.75468 14.2064 2.55515 13.8807C2.45769 13.7216 2.42864 13.5299 2.47457 13.3491L2.95948 11.4405C3.13452 10.7515 3.60599 10.1756 4.24682 9.86791L7.6642 8.22716C7.82352 8.15067 7.89067 7.95951 7.81418 7.80019C7.75223 7.67116 7.61214 7.59896 7.47111 7.62338L3.66713 8.28194C2.89387 8.41581 2.1009 8.20228 1.49941 7.69823L0.297703 6.69116C0.00493565 6.44581 -0.0335059 6.00958 0.211842 5.71682C0.33117 5.57442 0.502766 5.48602 0.687982 5.47153L4.35956 5.18419C4.61895 5.16389 4.845 4.99974 4.94458 4.75937L6.36101 1.3402C6.5072 0.987302 6.91179 0.819734 7.26469 0.965925C7.43413 1.03612 7.56876 1.17075 7.63896 1.3402L9.05539 4.75937C9.15496 4.99974 9.38101 5.16389 9.6404 5.18419L13.3322 5.47311C13.713 5.50291 13.9975 5.83578 13.9677 6.2166C13.9534 6.39979 13.8667 6.56975 13.7269 6.68896L10.9114 9.08928C10.7131 9.25826 10.6267 9.52425 10.6876 9.77748L11.5532 13.3733C11.6426 13.7447 11.414 14.1182 11.0427 14.2076C10.8642 14.2506 10.676 14.2208 10.5195 14.1249L7.36128 12.1902C7.13956 12.0544 6.8604 12.0544 6.63869 12.1902Z');
const STAR_SIZE_MULTIPLIER = 1.5;
const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
const ConfettiContainer = ({ confetti }: StateProps) => {
// eslint-disable-next-line no-null/no-null
const canvasRef = useRef<HTMLCanvasElement>(null);
const confettiRef = useRef<Confetti[]>([]);
@ -76,6 +79,7 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
rotation: 0,
lastDrawnAt: Date.now(),
frameCount: 0,
isStar: confetti?.withStars && Math.random() > 0.8,
});
}
});
@ -143,17 +147,29 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
confettiRef.current[i] = newConfetti;
ctx.fillStyle = color;
ctx.beginPath();
ctx.ellipse(
pos.x,
pos.y,
size,
flicker,
rotation,
0,
2 * Math.PI,
);
ctx.fill();
if (c.isStar) {
ctx.save();
ctx.translate(pos.x, pos.y);
ctx.scale(
(size / DEFAULT_CONFETTI_SIZE) * STAR_SIZE_MULTIPLIER,
(size / DEFAULT_CONFETTI_SIZE) * STAR_SIZE_MULTIPLIER,
);
ctx.rotate(rotation);
ctx.fill(STAR_PATH);
ctx.restore();
} else {
ctx.beginPath();
ctx.ellipse(
pos.x,
pos.y,
size,
flicker,
rotation,
0,
2 * Math.PI,
);
ctx.fill();
}
});
confettiRef.current = confettiRef.current.filter((c) => !confettiToRemove.includes(c));
if (confettiRef.current.length) {

View File

@ -248,6 +248,7 @@ const Main: FC<OwnProps & StateProps> = ({
loadSavedReactionTags,
loadTimezones,
loadQuickReplies,
loadStarStatus,
} = getActions();
if (DEBUG && !DEBUG_isLogged) {
@ -328,6 +329,7 @@ const Main: FC<OwnProps & StateProps> = ({
loadSavedReactionTags();
loadTimezones();
loadQuickReplies();
loadStarStatus();
}
}, [isMasterTab, isSynced]);

View File

@ -22,7 +22,7 @@ import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import AvatarList from '../../common/AvatarList';
import Icon from '../../common/Icon';
import Icon from '../../common/icons/Icon';
import Button from '../../ui/Button';
import Link from '../../ui/Link';
import Modal from '../../ui/Modal';
@ -122,6 +122,7 @@ const GiftPremiumModal: FC<OwnProps & StateProps> = ({
left,
width,
height,
withStars: true,
});
}
});

View File

@ -31,7 +31,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import CalendarModal from '../../common/CalendarModal';
import CountryPickerModal from '../../common/CountryPickerModal';
import GroupChatInfo from '../../common/GroupChatInfo';
import Icon from '../../common/Icon';
import Icon from '../../common/icons/Icon';
import Button from '../../ui/Button';
import ConfirmDialog from '../../ui/ConfirmDialog';
import InputText from '../../ui/InputText';

View File

@ -7,7 +7,7 @@ import buildClassName from '../../../util/buildClassName';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import Icon from '../../common/Icon';
import Icon from '../../common/icons/Icon';
import styles from './GiveawayTypeOption.module.scss';

View File

@ -15,7 +15,7 @@ import sortChatIds from '../../common/helpers/sortChatIds';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import Icon from '../../common/Icon';
import Icon from '../../common/icons/Icon';
import Picker from '../../common/Picker';
import Button from '../../ui/Button';
import Modal from '../../ui/Modal';

View File

@ -202,6 +202,7 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
left,
width,
height,
withStars: true,
});
}
});

View File

@ -15,7 +15,7 @@ import renderText from '../../../common/helpers/renderText';
import useFlag from '../../../../hooks/useFlag';
import useLang from '../../../../hooks/useLang';
import Icon from '../../../common/Icon';
import Icon from '../../../common/icons/Icon';
import Button from '../../../ui/Button';
import Modal from '../../../ui/Modal';
import PremiumLimitsCompare from './PremiumLimitsCompare';

View File

@ -102,7 +102,9 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
observeIntersectionForPlaying,
onPinnedIntersectionChange,
}) => {
const { openPremiumModal, requestConfetti, checkGiftCode } = getActions();
const {
openPremiumModal, requestConfetti, checkGiftCode, getReceipt,
} = getActions();
const lang = useLang();
@ -150,7 +152,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
useEffect(() => {
if (isVisible && shouldShowConfettiRef.current) {
shouldShowConfettiRef.current = false;
requestConfetti({});
requestConfetti({ withStars: true });
}
}, [isVisible, requestConfetti]);
@ -210,6 +212,15 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
checkGiftCode({ slug, message: { chatId: message.chatId, messageId: message.id } });
};
const handleClick = () => {
if (message.content.action?.type === 'receipt') {
getReceipt({
chatId: message.chatId,
messageId: message.id,
});
}
};
// TODO Refactoring for action rendering
const shouldSkipRender = isInsideTopic && message.content.action?.text === 'TopicWasCreatedAction';
if (shouldSkipRender) {
@ -294,7 +305,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
onContextMenu={handleContextMenu}
>
{!isSuggestedAvatar && !isGiftCode && !isJoinedMessage && (
<span className="action-message-content">{renderContent()}</span>
<span className="action-message-content" onClick={handleClick}>{renderContent()}</span>
)}
{isGift && renderGift()}
{isGiftCode && renderGiftCode()}

View File

@ -132,6 +132,7 @@ type StateProps = {
const MESSAGE_REACTIONS_POLLING_INTERVAL = 20 * 1000;
const MESSAGE_COMMENTS_POLLING_INTERVAL = 20 * 1000;
const MESSAGE_FACT_CHECK_UPDATE_INTERVAL = 5 * 1000;
const MESSAGE_STORY_POLLING_INTERVAL = 5 * 60 * 1000;
const BOTTOM_THRESHOLD = 50;
const UNREAD_DIVIDER_TOP = 10;
@ -188,7 +189,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
}) => {
const {
loadViewportMessages, setScrollOffset, loadSponsoredMessages, loadMessageReactions, copyMessagesByIds,
loadMessageViews, loadPeerStoriesByIds,
loadMessageViews, loadPeerStoriesByIds, loadFactChecks,
} = getActions();
// eslint-disable-next-line no-null/no-null
@ -320,6 +321,17 @@ const MessageList: FC<OwnProps & StateProps> = ({
loadMessageViews({ chatId, ids });
}, MESSAGE_COMMENTS_POLLING_INTERVAL, true);
useInterval(() => {
if (!messageIds || !messagesById || threadId !== MAIN_THREAD_ID || type === 'scheduled') {
return;
}
const ids = messageIds.filter((id) => messagesById[id].factCheck?.shouldFetch);
if (!ids.length) return;
loadFactChecks({ chatId, ids });
}, MESSAGE_FACT_CHECK_UPDATE_INTERVAL);
const loadMoreAround = useMemo(() => {
if (type !== 'thread') {
return undefined;

View File

@ -10,7 +10,7 @@ import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import AnimatedIconWithPreview from '../common/AnimatedIconWithPreview';
import Icon from '../common/Icon';
import Icon from '../common/icons/Icon';
import Button from '../ui/Button';
import styles from './PremiumRequiredMessage.module.scss';

View File

@ -30,7 +30,7 @@ import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useMouseInside from '../../../hooks/useMouseInside';
import Icon from '../../common/Icon';
import Icon from '../../common/icons/Icon';
import Menu from '../../ui/Menu';
import MenuItem from '../../ui/MenuItem';
import ResponsiveHoverButton from '../../ui/ResponsiveHoverButton';

View File

@ -38,7 +38,7 @@ import useMenuPosition from '../../../hooks/useMenuPosition';
import useShowTransition from '../../../hooks/useShowTransition';
import { ClosableEmbeddedMessage } from '../../common/embedded/EmbeddedMessage';
import Icon from '../../common/Icon';
import Icon from '../../common/icons/Icon';
import Button from '../../ui/Button';
import Menu from '../../ui/Menu';
import MenuItem from '../../ui/MenuItem';

View File

@ -1,21 +1,6 @@
.root {
position: relative;
margin: 0.25rem 0.25rem 0.875rem 0.25rem;
border-radius: 0.25rem;
overflow: hidden;
background-color: var(--accent-background-color);
color: var(--accent-color);
&::before {
content: "";
display: block;
position: absolute;
top: 0;
bottom: 0;
inset-inline-start: 0;
width: 0.1875rem;
background: var(--bar-gradient, var(--accent-color));
}
}
.info-container {

View File

@ -0,0 +1,52 @@
@use '../../../styles/mixins';
.root {
margin-top: 0.5rem;
font-size: 0.875rem;
padding-block: 0.25rem;
padding-inline-end: 0.25rem;
}
.title {
font-weight: 500;
}
.content {
color: var(--color-text);
}
.separator {
--color-dividers: var(--accent-color);
margin-block: 0.25rem;
opacity: 0.25;
}
.footnote {
font-size: 0.75rem;
}
.collapsed {
@include mixins.gradient-border-bottom(1rem);
}
.cutoutWrapper {
max-height: inherit;
}
.collapseIcon {
position: absolute;
display: grid;
place-items: center;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
bottom: 0;
right: 0;
}
.clickable {
cursor: var(--custom-cursor, pointer);
}

View File

@ -0,0 +1,89 @@
import React, { memo, useMemo, useRef } from '../../../lib/teact/teact';
import type { ApiFactCheck } from '../../../api/types';
import buildClassName from '../../../util/buildClassName';
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
import useCollapsibleLines from '../../../hooks/element/useCollapsibleLines';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import Icon from '../../common/icons/Icon';
import PeerColorWrapper from '../../common/PeerColorWrapper';
import Separator from '../../ui/Separator';
import styles from './FactCheck.module.scss';
type OwnProps = {
factCheck: ApiFactCheck;
isToggleDisabled?: boolean;
};
const COLOR = {
color: 0,
};
const MAX_LINES = 4;
const FactCheck = ({ factCheck, isToggleDisabled }: OwnProps) => {
const lang = useLang();
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const cutoutRef = useRef<HTMLDivElement>(null);
const {
isCollapsed, isCollapsible, setIsCollapsed,
} = useCollapsibleLines(ref, MAX_LINES, cutoutRef);
const countryLocalized = useMemo(() => {
if (!factCheck.countryCode || !lang.code) return undefined;
const displayNames = new Intl.DisplayNames([lang.code], { type: 'region' });
return displayNames.of(factCheck.countryCode);
}, [factCheck.countryCode, lang.code]);
const canExpand = !isToggleDisabled && isCollapsed;
const handleExpand = useLastCallback(() => {
setIsCollapsed(false);
});
const handleToggle = useLastCallback(() => {
setIsCollapsed((prev) => !prev);
});
if (!factCheck.text) {
return undefined;
}
return (
<PeerColorWrapper peerColor={COLOR} className={styles.root} onClick={canExpand ? handleExpand : undefined}>
<div
ref={cutoutRef}
className={buildClassName(styles.cutoutWrapper, isCollapsed && styles.collapsed)}
>
<div className={styles.title}>{lang('FactCheck')}</div>
<div ref={ref} className={styles.content}>
{renderTextWithEntities({
text: factCheck.text.text,
entities: factCheck.text.entities,
})}
</div>
<Separator className={styles.separator} />
<div className={styles.footnote}>{lang('FactCheckFooter', countryLocalized)}</div>
</div>
{isCollapsible && (
<div
className={buildClassName(styles.collapseIcon, !isToggleDisabled && styles.clickable)}
onClick={!isToggleDisabled ? handleToggle : undefined}
aria-hidden
>
<Icon name={isCollapsed ? 'down' : 'up'} />
</div>
)}
</PeerColorWrapper>
);
};
export default memo(FactCheck);

View File

@ -38,16 +38,16 @@
top: 0;
padding: 0.25rem 0.5rem;
margin: 0.25rem;
background-color: rgba(0, 0, 0, 0.2);
background-color: rgba(0, 0, 0, 0.4);
border-radius: var(--border-radius-messages-small);
color: var(--color-white);
font-weight: 500;
span {
margin-left: 0.5rem;
}
}
}
.test-invoice {
margin-left: 0.5rem;
}
}
.invoice-image-container {

View File

@ -5,6 +5,7 @@ import type { ApiMessage } from '../../../api/types';
import type { ISettings } from '../../../types';
import { CUSTOM_APPENDIX_ATTRIBUTE, MESSAGE_CONTENT_SELECTOR } from '../../../config';
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
import { getMessageInvoice, getWebDocumentHash } from '../../../global/helpers';
import buildStyle from '../../../util/buildStyle';
import { formatCurrency } from '../../../util/formatCurrency';
@ -67,8 +68,10 @@ const Invoice: FC<OwnProps> = ({
if (photoUrl) {
const contentEl = ref.current!.closest<HTMLDivElement>(MESSAGE_CONTENT_SELECTOR)!;
getCustomAppendixBg(photoUrl, false, isSelected, theme).then((appendixBg) => {
contentEl.style.setProperty('--appendix-bg', appendixBg);
contentEl.setAttribute(CUSTOM_APPENDIX_ATTRIBUTE, '');
requestMutation(() => {
contentEl.style.setProperty('--appendix-bg', appendixBg);
contentEl.setAttribute(CUSTOM_APPENDIX_ATTRIBUTE, '');
});
});
}
}, [shouldAffectAppendix, photoUrl, isInSelectMode, isSelected, theme]);
@ -117,7 +120,7 @@ const Invoice: FC<OwnProps> = ({
)}
<p className="description-text">
{formatCurrency(amount, currency, lang.code)}
{isTest && <span>{lang('PaymentTestInvoice')}</span>}
{isTest && <span className="test-invoice">{lang('PaymentTestInvoice')}</span>}
</p>
</div>
</div>

View File

@ -1,4 +1,4 @@
import type { FC } from '../../../lib/teact/teact';
import type { TeactNode } from '../../../lib/teact/teact';
import React from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
@ -12,19 +12,19 @@ import useAppLayout from '../../../hooks/useAppLayout';
type OwnProps = {
userId?: string;
username?: string;
children: React.ReactNode;
children: TeactNode;
};
type StateProps = {
userOrChat?: ApiPeer;
};
const MentionLink: FC<OwnProps & StateProps> = ({
const MentionLink = ({
userId,
username,
userOrChat,
children,
}) => {
}: OwnProps & StateProps) => {
const {
openChat,
openChatByUsername,

View File

@ -145,9 +145,9 @@ import DotAnimation from '../../common/DotAnimation';
import EmbeddedMessage from '../../common/embedded/EmbeddedMessage';
import EmbeddedStory from '../../common/embedded/EmbeddedStory';
import FakeIcon from '../../common/FakeIcon';
import Icon from '../../common/Icon';
import Icon from '../../common/icons/Icon';
import StarIcon from '../../common/icons/StarIcon';
import MessageText from '../../common/MessageText';
import PremiumIcon from '../../common/PremiumIcon';
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
import TopicChip from '../../common/TopicChip';
import Button from '../../ui/Button';
@ -157,6 +157,7 @@ import AnimatedEmoji from './AnimatedEmoji';
import CommentButton from './CommentButton';
import Contact from './Contact';
import ContextMenuContainer from './ContextMenuContainer.async';
import FactCheck from './FactCheck';
import Game from './Game';
import Giveaway from './Giveaway';
import InlineButtons from './InlineButtons';
@ -470,7 +471,7 @@ const Message: FC<OwnProps & StateProps> = ({
);
const {
id: messageId, chatId, forwardInfo, viaBotId, isTranscriptionError,
id: messageId, chatId, forwardInfo, viaBotId, isTranscriptionError, factCheck,
} = message;
useEffect(() => {
@ -524,6 +525,8 @@ const Message: FC<OwnProps & StateProps> = ({
const noUserColors = isOwn && !isCustomShape;
const hasFactCheck = Boolean(factCheck?.text);
const hasSubheader = hasTopicChip || hasMessageReply || hasStoryReply;
const selectMessage = useLastCallback((e?: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => {
@ -627,12 +630,13 @@ const Message: FC<OwnProps & StateProps> = ({
}, [focusLastMessage, isLastInList, transcribedText, withVoiceTranscription]);
const textMessage = album?.hasMultipleCaptions ? undefined : (album?.captionMessage || message);
const hasText = textMessage && hasMessageText(textMessage);
const hasTextContent = textMessage && hasMessageText(textMessage);
const hasText = hasTextContent || hasFactCheck;
const containerClassName = buildClassName(
'Message message-list-item',
isFirstInGroup && 'first-in-group',
isProtected && !hasText ? 'is-protected' : 'allow-selection',
isProtected && !hasTextContent ? 'is-protected' : 'allow-selection',
isLastInGroup && 'last-in-group',
isFirstInDocumentGroup && 'first-in-document-group',
isLastInDocumentGroup && 'last-in-document-group',
@ -915,6 +919,7 @@ const Message: FC<OwnProps & StateProps> = ({
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
withTranslucentThumbs={isCustomShape}
isInSelectMode={isInSelectMode}
/>
);
}
@ -1227,6 +1232,9 @@ const Message: FC<OwnProps & StateProps> = ({
</div>
</div>
)}
{hasFactCheck && (
<FactCheck factCheck={factCheck} isToggleDisabled={isInSelectMode} />
)}
{metaPosition === 'in-text' && renderReactionsAndMeta()}
</div>
)}
@ -1331,7 +1339,7 @@ const Message: FC<OwnProps & StateProps> = ({
observeIntersectionForPlaying={observeIntersectionForPlaying}
/>
)}
{!asForwarded && !senderEmojiStatus && senderIsPremium && <PremiumIcon />}
{!asForwarded && !senderEmojiStatus && senderIsPremium && <StarIcon />}
{senderPeer?.fakeType && <FakeIcon fakeType={senderPeer.fakeType} />}
</span>
) : !botSender ? (

View File

@ -31,7 +31,7 @@ import useShowTransition from '../../../hooks/useShowTransition';
import useSignal from '../../../hooks/useSignal';
import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef';
import Icon from '../../common/Icon';
import Icon from '../../common/icons/Icon';
import MediaSpoiler from '../../common/MediaSpoiler';
import Button from '../../ui/Button';
import OptimizedVideo from '../../ui/OptimizedVideo';

View File

@ -21,7 +21,7 @@ import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import Avatar from '../../common/Avatar';
import Icon from '../../common/Icon';
import Icon from '../../common/icons/Icon';
import Button from '../../ui/Button';
import Skeleton from '../../ui/placeholder/Skeleton';

View File

@ -19,7 +19,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import AboutAdsModal from '../../common/AboutAdsModal.async';
import Avatar from '../../common/Avatar';
import Icon from '../../common/Icon';
import Icon from '../../common/icons/Icon';
import PeerColorWrapper from '../../common/PeerColorWrapper';
import Button from '../../ui/Button';
import MessageAppendix from './MessageAppendix';

View File

@ -322,7 +322,7 @@
color: var(--accent-color);
}
.PremiumIcon {
.StarIcon {
--color-fill: var(--accent-color);
vertical-align: middle;
opacity: 0.5;
@ -963,12 +963,6 @@
font-size: 0.875rem;
}
// Remove extra bottom padding from `blockquote`
.text-entity-blockquote-wrapper {
display: inline-block;
width: 100%;
}
blockquote, .blockquote {
display: inline-block;
position: relative;

View File

@ -40,10 +40,11 @@ export function buildContentClassName(
giveaway, giveawayResults,
} = getMessageContent(message);
const text = album?.hasMultipleCaptions ? undefined : getMessageContent(album?.captionMessage || message).text;
const hasFactCheck = Boolean(message.factCheck?.text);
const classNames = [MESSAGE_CONTENT_CLASS_NAME];
const isMedia = storyData || photo || video || location || invoice?.extendedMedia;
const hasText = text || location?.type === 'venue' || isGeoLiveActive;
const hasText = text || location?.type === 'venue' || isGeoLiveActive || hasFactCheck;
const isMediaWithNoText = isMedia && !hasText;
const isViaBot = Boolean(message.viaBotId);
@ -144,7 +145,7 @@ export function buildContentClassName(
classNames.push('has-background');
}
if (hasSubheader || asForwarded || isViaBot || !isMediaWithNoText || forceSenderName) {
if (hasSubheader || asForwarded || isViaBot || !isMediaWithNoText || forceSenderName || hasFactCheck) {
classNames.push('has-solid-background');
}

View File

@ -7,7 +7,7 @@ import buildClassName from '../../../../util/buildClassName';
import { REM } from '../../../common/helpers/mediaDimensions';
import CustomEmoji from '../../../common/CustomEmoji';
import Icon from '../../../common/Icon';
import Icon from '../../../common/icons/Icon';
import styles from './ReactionSelectorReaction.module.scss';

View File

@ -10,7 +10,7 @@ import useFlag from '../../../../hooks/useFlag';
import useMedia from '../../../../hooks/useMedia';
import AnimatedSticker from '../../../common/AnimatedSticker';
import Icon from '../../../common/Icon';
import Icon from '../../../common/icons/Icon';
import styles from './ReactionSelectorReaction.module.scss';

View File

@ -15,6 +15,8 @@ import InviteViaLinkModal from './inviteViaLink/InviteViaLinkModal.async';
import MapModal from './map/MapModal.async';
import OneTimeMediaModal from './oneTimeMedia/OneTimeMediaModal.async';
import ReportAdModal from './reportAd/ReportAdModal.async';
import StarsBalanceModal from './stars/StarsBalanceModal.async';
import StarsPaymentModal from './stars/StarsPaymentModal.async';
import UrlAuthModal from './urlAuth/UrlAuthModal.async';
import WebAppModal from './webApp/WebAppModal.async';
@ -30,6 +32,8 @@ type ModalKey = keyof Pick<TabState,
'requestedAttachBotInstall' |
'collectibleInfoModal' |
'reportAdModal' |
'starsBalanceModal' |
'isStarPaymentModalOpen' |
'webApp'
>;
@ -57,6 +61,8 @@ const MODALS: ModalRegistry = {
webApp: WebAppModal,
collectibleInfoModal: CollectibleInfoModal,
mapModal: MapModal,
isStarPaymentModalOpen: StarsPaymentModal,
starsBalanceModal: StarsBalanceModal,
};
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;

View File

@ -17,7 +17,7 @@ import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import Avatar from '../../common/Avatar';
import Icon from '../../common/Icon';
import Icon from '../../common/icons/Icon';
import PremiumProgress from '../../common/PremiumProgress';
import Button from '../../ui/Button';
import ConfirmDialog from '../../ui/ConfirmDialog';

View File

@ -20,7 +20,7 @@ import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
import Icon from '../../common/Icon';
import Icon from '../../common/icons/Icon';
import PickerSelectedItem from '../../common/PickerSelectedItem';
import Button from '../../ui/Button';
import Modal from '../../ui/Modal';

View File

@ -0,0 +1,37 @@
.content {
display: flex;
flex-direction: column;
gap: 0.5rem;
max-height: min(92vh, 40rem) !important;
overflow-x: hidden;
}
.title {
background-color: var(--color-background-secondary);
}
.value {
word-break: break-word;
}
.table .cell {
border: 1px solid var(--color-borders);
padding: 0.25rem 0.5rem;
}
.logo {
width: 6.25rem;
height: 6.25rem;
align-self: center;
}
.avatar {
align-self: center;
}
.chatItem {
margin: 0;
width: fit-content;
background-color: var(--color-background);
color: var(--color-primary);
}

View File

@ -0,0 +1,100 @@
import React, { memo, type TeactNode } from '../../../lib/teact/teact';
import { getActions } from '../../../global';
import type { ApiPeer, ApiWebDocument } from '../../../api/types';
import type { CustomPeer } from '../../../types';
import buildClassName from '../../../util/buildClassName';
import useLastCallback from '../../../hooks/useLastCallback';
import Avatar from '../../common/Avatar';
import PickerSelectedItem from '../../common/PickerSelectedItem';
import Button from '../../ui/Button';
import Modal from '../../ui/Modal';
import styles from './TableInfoModal.module.scss';
type ChatItem = { chatId: string };
export type TableData = [TeactNode, TeactNode | ChatItem][];
type OwnProps = {
isOpen?: boolean;
title?: string;
tableData?: TableData;
headerImageUrl?: string;
headerAvatarPeer?: ApiPeer | CustomPeer;
headerAvatarWebPhoto?: ApiWebDocument;
header?: TeactNode;
footer?: TeactNode;
buttonText?: string;
onClose: NoneToVoidFunction;
onButtonClick?: NoneToVoidFunction;
};
const TableInfoModal = ({
isOpen,
title,
tableData,
headerImageUrl,
headerAvatarPeer,
headerAvatarWebPhoto,
header,
footer,
buttonText,
onClose,
onButtonClick,
}: OwnProps) => {
const { openChat } = getActions();
const handleOpenChat = useLastCallback((peerId: string) => {
openChat({ id: peerId });
onClose();
});
const withAvatar = Boolean(headerAvatarPeer || headerAvatarWebPhoto);
return (
<Modal
isOpen={isOpen}
hasCloseButton={Boolean(title)}
hasAbsoluteCloseButton={!title}
isSlim
title={title}
contentClassName={styles.content}
onClose={onClose}
>
{withAvatar ? (
<Avatar peer={headerAvatarPeer} webPhoto={headerAvatarWebPhoto} size="jumbo" className={styles.avatar} />
) : (
<img className={styles.logo} src={headerImageUrl} alt="" draggable={false} />
)}
{header}
<table className={styles.table}>
{tableData?.map(([label, value]) => (
<tr className={styles.row}>
<td className={buildClassName(styles.cell, styles.title)}>{label}</td>
<td className={buildClassName(styles.cell, styles.value)}>
{typeof value === 'object' && 'chatId' in value ? (
<PickerSelectedItem
peerId={value.chatId}
className={styles.chatItem}
forceShowSelf
fluid
clickArg={value.chatId}
onClick={handleOpenChat}
/>
) : value}
</td>
</tr>
))}
</table>
{footer}
{buttonText && (
<Button onClick={onButtonClick || onClose}>{buttonText}</Button>
)}
</Modal>
);
};
export default memo(TableInfoModal);

View File

@ -1,38 +1,8 @@
.content {
display: flex;
flex-direction: column;
gap: 0.5rem;
max-height: min(92vh, 40rem) !important;
overflow-x: hidden;
}
.clickable {
color: var(--color-primary);
cursor: pointer;
}
.title {
background-color: var(--color-background-secondary);
}
.table td {
border: 1px solid var(--color-borders);
padding: 0.25rem 0.5rem;
}
.chat-item {
margin: 0;
width: fit-content;
background-color: var(--color-background);
color: var(--color-primary);
}
.logo {
width: 6.25rem;
height: 6.25rem;
align-self: center;
}
.centered {
text-align: center !important;
}

View File

@ -1,4 +1,4 @@
import React, { memo } from '../../../lib/teact/teact';
import React, { memo, useMemo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiPeer } from '../../../api/types';
@ -14,9 +14,7 @@ import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import LinkField from '../../common/LinkField';
import PickerSelectedItem from '../../common/PickerSelectedItem';
import Button from '../../ui/Button';
import Modal from '../../ui/Modal';
import TableInfoModal, { type TableData } from '../common/TableInfoModal';
import styles from './GiftCodeModal.module.scss';
@ -39,18 +37,13 @@ const GiftCodeModal = ({
messageSender,
}: OwnProps & StateProps) => {
const {
closeGiftCodeModal, openChat, applyGiftCode, focusMessage,
closeGiftCodeModal, applyGiftCode, focusMessage,
} = getActions();
const lang = useLang();
const isOpen = Boolean(modal);
const canUse = (!modal?.info.toId || modal?.info.toId === currentUserId) && !modal?.info.usedAt;
const handleOpenChat = useLastCallback((peerId: string) => {
openChat({ id: peerId });
closeGiftCodeModal();
});
const handleOpenGiveaway = useLastCallback(() => {
if (!modal || !modal.info.giveawayMessageId) return;
focusMessage({
@ -68,101 +61,63 @@ const GiftCodeModal = ({
closeGiftCodeModal();
});
function renderContent() {
const modalData = useMemo(() => {
if (!modal) return undefined;
const { slug, info } = modal;
const fromId = info.fromId || messageSender?.id;
return (
const header = (
<>
<img className={styles.logo} src={PremiumLogo} alt="" draggable={false} />
<p className={styles.centered}>{renderText(lang('lng_gift_link_about'), ['simple_markdown'])}</p>
<LinkField title="BoostingGiftLink" link={`${TME_LINK_PREFIX}/${GIFTCODE_PATH}/${slug}`} />
<table className={styles.table}>
<tr>
<td className={styles.title}>{lang('BoostingFrom')}</td>
<td>
{fromId ? (
<PickerSelectedItem
peerId={fromId}
className={styles.chatItem}
forceShowSelf
fluid
clickArg={fromId}
onClick={handleOpenChat}
/>
) : lang('BoostingNoRecipient')}
</td>
</tr>
<tr>
<td className={styles.title}>
{lang('BoostingTo')}
</td>
<td>
{info.toId ? (
<PickerSelectedItem
peerId={info.toId}
className={styles.chatItem}
forceShowSelf
fluid
clickArg={info.toId}
onClick={handleOpenChat}
/>
) : lang('BoostingNoRecipient')}
</td>
</tr>
<tr>
<td className={styles.title}>
{lang('BoostingGift')}
</td>
<td>
{lang('BoostingTelegramPremiumFor', lang('Months', info.months, 'i'))}
</td>
</tr>
<tr>
<td className={styles.title}>
{lang('BoostingReason')}
</td>
<td className={buildClassName(info.giveawayMessageId && styles.clickable)} onClick={handleOpenGiveaway}>
{info.isFromGiveaway && !info.toId ? lang('BoostingIncompleteGiveaway')
: lang(info.isFromGiveaway ? 'BoostingGiveaway' : 'BoostingYouWereSelected')}
</td>
</tr>
<tr>
<td className={styles.title}>
{lang('BoostingDate')}
</td>
<td>
{formatDateTimeToString(info.date * 1000, lang.code, true)}
</td>
</tr>
</table>
<span className={styles.centered}>
{renderText(
info.usedAt ? lang('BoostingUsedLinkDate', formatDateTimeToString(info.usedAt * 1000, lang.code, true))
: lang('BoostingSendLinkToAnyone'),
['simple_markdown'],
)}
</span>
<Button onClick={handleButtonClick}>
{canUse ? lang('BoostingUseLink') : lang('Close')}
</Button>
</>
);
}
const tableData = [
[lang('BoostingFrom'), fromId ? { chatId: fromId } : lang('BoostingNoRecipient')],
[lang('BoostingTo'), info.toId ? { chatId: info.toId } : lang('BoostingNoRecipient')],
[lang('BoostingGift'), lang('BoostingTelegramPremiumFor', lang('Months', info.months, 'i'))],
[lang('BoostingReason'), (
<span className={buildClassName(info.giveawayMessageId && styles.clickable)} onClick={handleOpenGiveaway}>
{info.isFromGiveaway && !info.toId ? lang('BoostingIncompleteGiveaway')
: lang(info.isFromGiveaway ? 'BoostingGiveaway' : 'BoostingYouWereSelected')}
</span>
)],
[lang('BoostingDate'), formatDateTimeToString(info.date * 1000, lang.code, true)],
] satisfies TableData;
const footer = (
<span className={styles.centered}>
{renderText(
info.usedAt ? lang('BoostingUsedLinkDate', formatDateTimeToString(info.usedAt * 1000, lang.code, true))
: lang('BoostingSendLinkToAnyone'),
['simple_markdown'],
)}
</span>
);
return {
header,
tableData,
footer,
};
}, [lang, messageSender?.id, modal]);
if (!modalData) return undefined;
return (
<Modal
<TableInfoModal
isOpen={isOpen}
hasCloseButton
isSlim
title={lang('lng_gift_link_title')}
contentClassName={styles.content}
headerImageUrl={PremiumLogo}
tableData={modalData.tableData}
header={modalData.header}
footer={modalData.footer}
buttonText={canUse ? lang('BoostingUseLink') : lang('Close')}
onButtonClick={handleButtonClick}
onClose={closeGiftCodeModal}
>
{renderContent()}
</Modal>
/>
);
};

View File

@ -11,7 +11,7 @@ import buildClassName from '../../../util/buildClassName';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import Icon from '../../common/Icon';
import Icon from '../../common/icons/Icon';
import SafeLink from '../../common/SafeLink';
import Button from '../../ui/Button';
import ListItem from '../../ui/ListItem';

View File

@ -0,0 +1,31 @@
import React, { memo } from '../../../lib/teact/teact';
import buildClassName from '../../../util/buildClassName';
import { formatInteger } from '../../../util/textFormat';
import useLang from '../../../hooks/useLang';
import StarIcon from '../../common/icons/StarIcon';
import styles from './StarsBalanceModal.module.scss';
type OwnProps = {
balance: number;
className?: string;
};
const BalanceBlock = ({ balance, className }: OwnProps) => {
const lang = useLang();
return (
<div className={buildClassName(styles.balance, className)}>
<span className={styles.smallerText}>{lang('StarsBalance')}</span>
<div className={styles.balanceBottom}>
<StarIcon type="gold" size="middle" />
{formatInteger(balance)}
</div>
</div>
);
};
export default memo(BalanceBlock);

View File

@ -0,0 +1,18 @@
import type { FC } from '../../../lib/teact/teact';
import React from '../../../lib/teact/teact';
import type { OwnProps } from './StarsBalanceModal';
import { Bundles } from '../../../util/moduleLoader';
import useModuleLoader from '../../../hooks/useModuleLoader';
const StarsBalanceModalAsync: FC<OwnProps> = (props) => {
const { modal } = props;
const StarsBalanceModal = useModuleLoader(Bundles.Extra, 'StarsBalanceModal', !modal);
// eslint-disable-next-line react/jsx-props-no-spreading
return StarsBalanceModal ? <StarsBalanceModal {...props} /> : undefined;
};
export default StarsBalanceModalAsync;

View File

@ -0,0 +1,239 @@
@use '../../../styles/mixins';
.root :global(.modal-content) {
padding: 0;
}
.root :global(.modal-dialog) {
height: min(calc(55vh + 41px + 193px), 90vh);
max-width: 26.25rem;
}
.root :global(.modal-dialog),
.root :global(.modal-content),
.transition {
overflow: hidden;
}
.main {
height: 100%;
overflow-y: scroll;
}
.section {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.5rem;
position: relative;
@include mixins.adapt-padding-to-scrollbar(0.5rem);
@include mixins.side-panel-section;
}
.secondaryInfo {
font-size: 0.875rem;
color: var(--color-text-secondary);
background-color: var(--color-background-secondary);
padding: 0.5rem 1rem;
}
.logo {
margin: 1rem;
width: 6.25rem;
height: 6.25rem;
min-height: 6.25rem;
}
.logoBackground {
position: absolute;
top: 0.75rem;
left: 50%;
transform: translateX(-50%);
height: 8rem;
}
.headerHext {
font-size: 1.5rem;
font-weight: 500;
text-align: center;
margin-inline: 0.5rem;
}
.description {
text-align: center;
margin-inline: 0.5rem;
margin-bottom: 1rem;
text-wrap: balance;
}
.header {
z-index: 2;
display: flex;
align-items: center;
border-bottom: 0.0625rem solid var(--color-borders);
position: absolute;
width: 100%;
left: 0;
top: 0;
height: 3.5rem;
padding: 0.5rem;
background: var(--color-background);
transition: 0.25s ease-out transform;
}
.starHeaderText {
font-size: 1.25rem;
font-weight: 500;
margin: 0 0 0 3rem;
unicode-bidi: plaintext;
}
.hiddenHeader {
transform: translateY(-100%);
}
.closeButton {
position: absolute;
top: 0.5rem;
left: 0.5rem;
z-index: 3;
}
.balance {
display: flex;
flex-direction: column;
align-items: flex-end;
line-height: 1;
}
.smallerText {
font-size: 0.875rem;
}
.balanceBottom {
font-weight: 500;
display: flex;
gap: 0.25rem;
line-height: 1.5;
}
.modalBalance {
position: absolute;
top: 0.75rem;
right: 1.25rem;
z-index: 3;
}
.options {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
width: 100%;
}
.option {
--_background-color: var(--color-background-secondary);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.125rem;
padding: 1rem;
border-radius: 0.625rem;
background-color: var(--_background-color);
transition: background-color 0.25s ease-out;
cursor: var(--custom-cursor, pointer);
&:hover {
--_background-color: var(--color-background-secondary-accent);
}
}
.optionTop {
display: flex;
align-items: center;
gap: 0.25rem;
font-weight: 500;
font-size: 1.5rem;
line-height: 1;
}
.stackedStars {
display: grid;
grid-auto-columns: 0.4375rem;
grid-auto-flow: column;
justify-items: end;
}
.stackedStar {
@include mixins.filter-outline(0.0625rem, var(--_background-color));
}
.optionBottom {
font-size: 0.875rem;
color: var(--color-text-secondary);
}
.moreOptions {
grid-column: 1/-1;
}
.iconDown {
margin-inline-start: 0.25rem;
font-size: 1.5rem;
}
.paymentContent {
display: flex;
flex-direction: column;
align-items: center;
}
.paymentImages {
display: grid;
grid-auto-columns: 3.5rem;
grid-auto-flow: column;
place-items: center;
margin-bottom: 1rem;
position: relative;
}
.paymentPhoto {
outline: 0.25rem solid var(--color-background);
z-index: 1;
}
.paymentImageBackground {
height: 7rem;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.paymentAmount {
display: flex;
line-height: 1.125;
gap: 0.125rem;
}
.paymentButton {
display: flex;
gap: 0.125rem;
}
.paymentButtonStar {
--color-fill: white !important;
}
.transactions {
display: flex;
flex-direction: column;
}

Some files were not shown because too many files have changed in this diff Show More